remove port monitoring and automatic tunneling features

Remove port_monitor, TunnelManager, and /port-events handler to eliminate
automatic port tunneling functionality that bridges outtie to innie environments.

Sketch got confused when I asked it to change how this works; removing
and re-adding was easier!

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s78f868b27a44cb2bk
diff --git a/dockerimg/dockerimg.go b/dockerimg/dockerimg.go
index 59d3e8d..7b9d549 100644
--- a/dockerimg/dockerimg.go
+++ b/dockerimg/dockerimg.go
@@ -413,16 +413,6 @@
 		gitSrv.ps1URL.Store(&ps1URL)
 	}()
 
-	// Start automatic port tunneling if SSH is available
-	if sshAvailable {
-		go func() {
-			containerURL := "http://" + localAddr
-			tunnelManager := NewTunnelManager(containerURL, cntrName, 10) // Allow up to 10 concurrent tunnels
-			tunnelManager.Start(ctx)
-			slog.InfoContext(ctx, "Started automatic port tunnel manager", "container", cntrName)
-		}()
-	}
-
 	go func() {
 		cmd := exec.CommandContext(ctx, "docker", "attach", cntrName)
 		cmd.Stdin = os.Stdin
diff --git a/dockerimg/tunnel_manager.go b/dockerimg/tunnel_manager.go
deleted file mode 100644
index 6262263..0000000
--- a/dockerimg/tunnel_manager.go
+++ /dev/null
@@ -1,247 +0,0 @@
-package dockerimg
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"log/slog"
-	"net/http"
-	"os/exec"
-	"sync"
-	"time"
-
-	"sketch.dev/loop"
-)
-
-// skipPorts defines system ports that should not be auto-tunneled
-var skipPorts = map[string]bool{
-	"22":  true, // SSH
-	"80":  true, // HTTP (this is the main sketch web interface)
-	"443": true, // HTTPS
-	"25":  true, // SMTP
-	"53":  true, // DNS
-	"110": true, // POP3
-	"143": true, // IMAP
-	"993": true, // IMAPS
-	"995": true, // POP3S
-}
-
-// TunnelManager manages automatic SSH tunnels for container ports
-type TunnelManager struct {
-	mu               sync.Mutex
-	containerURL     string                // HTTP URL to container (e.g., "http://localhost:8080")
-	containerSSHHost string                // SSH hostname for container (e.g., "sketch-abcd-efgh")
-	activeTunnels    map[string]*sshTunnel // port -> tunnel mapping
-	lastPollTime     time.Time
-	maxActiveTunnels int // maximum number of concurrent tunnels allowed
-}
-
-// sshTunnel represents an active SSH tunnel
-type sshTunnel struct {
-	containerPort string
-	hostPort      string
-	cmd           *exec.Cmd
-	cancel        context.CancelFunc
-}
-
-// NewTunnelManager creates a new tunnel manager
-func NewTunnelManager(containerURL, containerSSHHost string, maxActiveTunnels int) *TunnelManager {
-	return &TunnelManager{
-		containerURL:     containerURL,
-		containerSSHHost: containerSSHHost,
-		activeTunnels:    make(map[string]*sshTunnel),
-		lastPollTime:     time.Now(),
-		maxActiveTunnels: maxActiveTunnels,
-	}
-}
-
-// Start begins monitoring port events and managing tunnels
-func (tm *TunnelManager) Start(ctx context.Context) {
-	go func() {
-		ticker := time.NewTicker(10 * time.Second) // Poll every 10 seconds
-		defer ticker.Stop()
-
-		for {
-			select {
-			case <-ctx.Done():
-				tm.cleanupAllTunnels()
-				return
-			case <-ticker.C:
-				tm.pollPortEvents(ctx)
-			}
-		}
-	}()
-}
-
-// pollPortEvents fetches recent port events from container and updates tunnels
-func (tm *TunnelManager) pollPortEvents(ctx context.Context) {
-	// Build URL with since parameter
-	url := fmt.Sprintf("%s/port-events?since=%s", tm.containerURL, tm.lastPollTime.Format(time.RFC3339))
-
-	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
-	if err != nil {
-		slog.DebugContext(ctx, "Failed to create port events request", "error", err)
-		return
-	}
-
-	resp, err := http.DefaultClient.Do(req)
-	if err != nil {
-		slog.DebugContext(ctx, "Failed to fetch port events", "error", err)
-		return
-	}
-	defer resp.Body.Close()
-
-	if resp.StatusCode != http.StatusOK {
-		slog.DebugContext(ctx, "Port events request failed", "status", resp.StatusCode)
-		return
-	}
-
-	var events []loop.PortEvent
-	if err := json.NewDecoder(resp.Body).Decode(&events); err != nil {
-		slog.DebugContext(ctx, "Failed to decode port events", "error", err)
-		return
-	}
-
-	// Process each event
-	for _, event := range events {
-		tm.processPortEvent(ctx, event)
-		tm.mu.Lock()
-		// Update last poll time to the latest event timestamp
-		if event.Timestamp.After(tm.lastPollTime) {
-			tm.lastPollTime = event.Timestamp
-		}
-		tm.mu.Unlock()
-	}
-
-	// Update poll time even if no events, to avoid re-fetching old events
-	if len(events) == 0 {
-		tm.lastPollTime = time.Now()
-	}
-}
-
-// processPortEvent handles a single port event
-func (tm *TunnelManager) processPortEvent(ctx context.Context, event loop.PortEvent) {
-	// Extract port number from event.Port (format: "tcp:0.0.0.0:8080")
-	containerPort := tm.extractPortNumber(event.Port)
-	if containerPort == "" {
-		slog.DebugContext(ctx, "Could not extract port number", "port", event.Port)
-		return
-	}
-
-	// Skip common system ports that we don't want to tunnel
-	if tm.shouldSkipPort(containerPort) {
-		return
-	}
-
-	switch event.Type {
-	case "opened":
-		tm.createTunnel(ctx, containerPort)
-	case "closed":
-		tm.removeTunnel(ctx, containerPort)
-	default:
-		slog.DebugContext(ctx, "Unknown port event type", "type", event.Type)
-	}
-}
-
-// extractPortNumber extracts port number from ss format like "tcp:0.0.0.0:8080"
-func (tm *TunnelManager) extractPortNumber(portStr string) string {
-	// Expected format: "tcp:0.0.0.0:8080" or "tcp:[::]:8080"
-	// Find the last colon and extract the port
-	for i := len(portStr) - 1; i >= 0; i-- {
-		if portStr[i] == ':' {
-			return portStr[i+1:]
-		}
-	}
-	return ""
-}
-
-// shouldSkipPort returns true for ports we don't want to auto-tunnel
-func (tm *TunnelManager) shouldSkipPort(port string) bool {
-	return skipPorts[port]
-}
-
-// createTunnel creates an SSH tunnel for the given container port
-func (tm *TunnelManager) createTunnel(ctx context.Context, containerPort string) {
-	tm.mu.Lock()
-	// Check if tunnel already exists
-	if _, exists := tm.activeTunnels[containerPort]; exists {
-		tm.mu.Unlock()
-		return
-	}
-
-	// Check if we've reached the maximum number of active tunnels
-	if len(tm.activeTunnels) >= tm.maxActiveTunnels {
-		tm.mu.Unlock()
-		slog.WarnContext(ctx, "Maximum active tunnels reached, skipping port", "port", containerPort, "max", tm.maxActiveTunnels, "active", len(tm.activeTunnels))
-		return
-	}
-	tm.mu.Unlock()
-
-	// Use the same port on host as container for simplicity
-	hostPort := containerPort
-
-	// Create SSH tunnel command: ssh -L hostPort:127.0.0.1:containerPort containerSSHHost
-	tunnelCtx, cancel := context.WithCancel(ctx)
-	cmd := exec.CommandContext(tunnelCtx, "ssh",
-		"-L", fmt.Sprintf("%s:127.0.0.1:%s", hostPort, containerPort),
-		"-N", // Don't execute remote commands
-		"-T", // Don't allocate TTY
-		tm.containerSSHHost,
-	)
-
-	// Start the tunnel
-	if err := cmd.Start(); err != nil {
-		slog.ErrorContext(ctx, "Failed to start SSH tunnel", "port", containerPort, "error", err)
-		cancel()
-		return
-	}
-
-	// Store tunnel info
-	tunnel := &sshTunnel{
-		containerPort: containerPort,
-		hostPort:      hostPort,
-		cmd:           cmd,
-		cancel:        cancel,
-	}
-	tm.mu.Lock()
-	tm.activeTunnels[containerPort] = tunnel
-	tm.mu.Unlock()
-
-	slog.InfoContext(ctx, "Created SSH tunnel", "container_port", containerPort, "host_port", hostPort)
-
-	// Monitor tunnel in background
-	go func() {
-		err := cmd.Wait()
-		tm.mu.Lock()
-		delete(tm.activeTunnels, containerPort)
-		tm.mu.Unlock()
-		if err != nil && tunnelCtx.Err() == nil {
-			slog.ErrorContext(ctx, "SSH tunnel exited with error", "port", containerPort, "error", err)
-		}
-	}()
-}
-
-// removeTunnel removes an SSH tunnel for the given container port
-func (tm *TunnelManager) removeTunnel(ctx context.Context, containerPort string) {
-	tunnel, exists := tm.activeTunnels[containerPort]
-	if !exists {
-		return
-	}
-
-	// Cancel the tunnel context and clean up
-	tunnel.cancel()
-	delete(tm.activeTunnels, containerPort)
-
-	slog.InfoContext(ctx, "Removed SSH tunnel", "container_port", containerPort, "host_port", tunnel.hostPort)
-}
-
-// cleanupAllTunnels stops all active tunnels
-func (tm *TunnelManager) cleanupAllTunnels() {
-	tm.mu.Lock()
-	defer tm.mu.Unlock()
-
-	for port, tunnel := range tm.activeTunnels {
-		tunnel.cancel()
-		delete(tm.activeTunnels, port)
-	}
-}
diff --git a/dockerimg/tunnel_manager_test.go b/dockerimg/tunnel_manager_test.go
deleted file mode 100644
index 6408ce5..0000000
--- a/dockerimg/tunnel_manager_test.go
+++ /dev/null
@@ -1,192 +0,0 @@
-package dockerimg
-
-import (
-	"context"
-	"testing"
-	"time"
-
-	"sketch.dev/loop"
-)
-
-// TestTunnelManagerMaxLimit tests that the tunnel manager respects the max tunnels limit
-func TestTunnelManagerMaxLimit(t *testing.T) {
-	tm := NewTunnelManager("http://localhost:8080", "test-container", 2) // Max 2 tunnels
-
-	if tm.maxActiveTunnels != 2 {
-		t.Errorf("Expected maxActiveTunnels to be 2, got %d", tm.maxActiveTunnels)
-	}
-
-	// Test that active tunnels map is empty initially
-	if len(tm.activeTunnels) != 0 {
-		t.Errorf("Expected 0 active tunnels initially, got %d", len(tm.activeTunnels))
-	}
-
-	// Simulate adding tunnels beyond the limit by directly manipulating the internal map
-	// This tests the limit logic without actually starting SSH processes
-	// Add 2 tunnels (up to the limit)
-	tm.activeTunnels["8080"] = &sshTunnel{containerPort: "8080", hostPort: "8080"}
-	tm.activeTunnels["9090"] = &sshTunnel{containerPort: "9090", hostPort: "9090"}
-
-	// Verify we have 2 active tunnels
-	if len(tm.activeTunnels) != 2 {
-		t.Errorf("Expected 2 active tunnels, got %d", len(tm.activeTunnels))
-	}
-
-	// Now test that the limit check works - attempt to add a third tunnel
-	// Check if we've reached the maximum (this simulates the check in createTunnel)
-	shouldBlock := len(tm.activeTunnels) >= tm.maxActiveTunnels
-
-	if !shouldBlock {
-		t.Error("Expected tunnel creation to be blocked when at max limit, but limit check failed")
-	}
-
-	// Verify that attempting to add beyond the limit doesn't actually add more
-	if len(tm.activeTunnels) < tm.maxActiveTunnels {
-		// This should not happen since we're at the limit
-		tm.activeTunnels["3000"] = &sshTunnel{containerPort: "3000", hostPort: "3000"}
-	}
-
-	// Verify we still have only 2 active tunnels (didn't exceed limit)
-	if len(tm.activeTunnels) != 2 {
-		t.Errorf("Expected exactly 2 active tunnels after limit enforcement, got %d", len(tm.activeTunnels))
-	}
-}
-
-// TestNewTunnelManagerParams tests that NewTunnelManager correctly sets all parameters
-func TestNewTunnelManagerParams(t *testing.T) {
-	containerURL := "http://localhost:9090"
-	containerSSHHost := "test-ssh-host"
-	maxTunnels := 5
-
-	tm := NewTunnelManager(containerURL, containerSSHHost, maxTunnels)
-
-	if tm.containerURL != containerURL {
-		t.Errorf("Expected containerURL %s, got %s", containerURL, tm.containerURL)
-	}
-	if tm.containerSSHHost != containerSSHHost {
-		t.Errorf("Expected containerSSHHost %s, got %s", containerSSHHost, tm.containerSSHHost)
-	}
-	if tm.maxActiveTunnels != maxTunnels {
-		t.Errorf("Expected maxActiveTunnels %d, got %d", maxTunnels, tm.maxActiveTunnels)
-	}
-	if tm.activeTunnels == nil {
-		t.Error("Expected activeTunnels map to be initialized")
-	}
-	if tm.lastPollTime.IsZero() {
-		t.Error("Expected lastPollTime to be initialized")
-	}
-}
-
-// TestShouldSkipPort tests the port skipping logic
-func TestShouldSkipPort(t *testing.T) {
-	tm := NewTunnelManager("http://localhost:8080", "test-container", 10)
-
-	// Test that system ports are skipped
-	systemPorts := []string{"22", "80", "443", "25", "53"}
-	for _, port := range systemPorts {
-		if !tm.shouldSkipPort(port) {
-			t.Errorf("Expected port %s to be skipped", port)
-		}
-	}
-
-	// Test that application ports are not skipped
-	appPorts := []string{"8080", "3000", "9090", "8000"}
-	for _, port := range appPorts {
-		if tm.shouldSkipPort(port) {
-			t.Errorf("Expected port %s to NOT be skipped", port)
-		}
-	}
-}
-
-// TestExtractPortNumber tests port number extraction from ss format
-func TestExtractPortNumber(t *testing.T) {
-	tm := NewTunnelManager("http://localhost:8080", "test-container", 10)
-
-	tests := []struct {
-		input    string
-		expected string
-	}{
-		{"tcp:0.0.0.0:8080", "8080"},
-		{"tcp:127.0.0.1:3000", "3000"},
-		{"tcp:[::]:9090", "9090"},
-		{"udp:0.0.0.0:53", "53"},
-		{"invalid", ""},
-		{"", ""},
-	}
-
-	for _, test := range tests {
-		result := tm.extractPortNumber(test.input)
-		if result != test.expected {
-			t.Errorf("extractPortNumber(%q) = %q, expected %q", test.input, result, test.expected)
-		}
-	}
-}
-
-// TestTunnelManagerLimitEnforcement tests that createTunnel enforces the max limit
-func TestTunnelManagerLimitEnforcement(t *testing.T) {
-	tm := NewTunnelManager("http://localhost:8080", "test-container", 2) // Max 2 tunnels
-	ctx := context.Background()
-
-	// Add tunnels manually to reach the limit (simulating successful SSH setup)
-	// In real usage, these would be added by createTunnel after successful SSH
-	tm.activeTunnels["8080"] = &sshTunnel{containerPort: "8080", hostPort: "8080"}
-	tm.activeTunnels["9090"] = &sshTunnel{containerPort: "9090", hostPort: "9090"}
-
-	// Verify we're now at the limit
-	if len(tm.activeTunnels) != 2 {
-		t.Fatalf("Setup failed: expected 2 active tunnels, got %d", len(tm.activeTunnels))
-	}
-
-	// Now test that createTunnel respects the limit by calling it directly
-	// This should hit the limit check and return early without attempting SSH
-	tm.createTunnel(ctx, "3000")
-	tm.createTunnel(ctx, "4000")
-
-	// Verify no additional tunnels were added (limit enforcement worked)
-	if len(tm.activeTunnels) != 2 {
-		t.Errorf("createTunnel should have been blocked by limit, but tunnel count changed from 2 to %d", len(tm.activeTunnels))
-	}
-
-	// Verify we're at the limit
-	if len(tm.activeTunnels) != 2 {
-		t.Fatalf("Expected 2 active tunnels, got %d", len(tm.activeTunnels))
-	}
-
-	// Now try to process more port events that would create additional tunnels
-	// These should be blocked by the limit check in createTunnel
-	portEvent1 := loop.PortEvent{
-		Type:      "opened",
-		Port:      "tcp:0.0.0.0:3000",
-		Timestamp: time.Now(),
-	}
-	portEvent2 := loop.PortEvent{
-		Type:      "opened",
-		Port:      "tcp:0.0.0.0:4000",
-		Timestamp: time.Now(),
-	}
-
-	// Process these events - they should be blocked by the limit
-	tm.processPortEvent(ctx, portEvent1)
-	tm.processPortEvent(ctx, portEvent2)
-
-	// Verify that no additional tunnels were created
-	if len(tm.activeTunnels) != 2 {
-		t.Errorf("Expected exactly 2 active tunnels after limit enforcement, got %d", len(tm.activeTunnels))
-	}
-
-	// Verify the original tunnels are still there
-	if _, exists := tm.activeTunnels["8080"]; !exists {
-		t.Error("Expected original tunnel for port 8080 to still exist")
-	}
-	if _, exists := tm.activeTunnels["9090"]; !exists {
-		t.Error("Expected original tunnel for port 9090 to still exist")
-	}
-
-	// Verify the new tunnels were NOT created
-	if _, exists := tm.activeTunnels["3000"]; exists {
-		t.Error("Expected tunnel for port 3000 to NOT be created due to limit")
-	}
-	if _, exists := tm.activeTunnels["4000"]; exists {
-		t.Error("Expected tunnel for port 4000 to NOT be created due to limit")
-	}
-}
diff --git a/loop/agent.go b/loop/agent.go
index f01e601..8f96924 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -152,8 +152,7 @@
 	// CompactConversation compacts the current conversation by generating a summary
 	// and restarting the conversation with that summary as the initial context
 	CompactConversation(ctx context.Context) error
-	// GetPortMonitor returns the port monitor instance for accessing port events
-	GetPortMonitor() *PortMonitor
+
 	// SkabandAddr returns the skaband address if configured
 	SkabandAddr() string
 }
@@ -460,9 +459,6 @@
 
 	// Track outstanding tool calls by ID with their names
 	outstandingToolCalls map[string]string
-
-	// Port monitoring
-	portMonitor *PortMonitor
 }
 
 // NewIterator implements CodingAgent.
@@ -1071,8 +1067,8 @@
 		stateMachine:         NewStateMachine(),
 		workingDir:           config.WorkingDir,
 		outsideHTTP:          config.OutsideHTTP,
-		portMonitor:          NewPortMonitor(),
-		mcpManager:           mcp.NewMCPManager(),
+
+		mcpManager: mcp.NewMCPManager(),
 	}
 	return agent
 }
@@ -1526,12 +1522,6 @@
 }
 
 func (a *Agent) Loop(ctxOuter context.Context) {
-	// Start port monitoring when the agent loop begins
-	// Only monitor ports when running in a container
-	if a.IsInContainer() {
-		a.portMonitor.Start(ctxOuter)
-	}
-
 	// Set up cleanup when context is done
 	defer func() {
 		if a.mcpManager != nil {
@@ -2540,11 +2530,6 @@
 	return nil
 }
 
-// GetPortMonitor returns the port monitor instance for accessing port events
-func (a *Agent) GetPortMonitor() *PortMonitor {
-	return a.portMonitor
-}
-
 // SkabandAddr returns the skaband address if configured
 func (a *Agent) SkabandAddr() string {
 	if a.config.SkabandClient != nil {
diff --git a/loop/port_monitor.go b/loop/port_monitor.go
deleted file mode 100644
index 105e1e7..0000000
--- a/loop/port_monitor.go
+++ /dev/null
@@ -1,344 +0,0 @@
-package loop
-
-import (
-	"context"
-	"fmt"
-	"log/slog"
-	"net"
-	"os"
-	"os/exec"
-	"strconv"
-	"strings"
-	"sync"
-	"time"
-)
-
-// PortEvent represents a port change event
-type PortEvent struct {
-	Type      string    `json:"type"`      // "opened" or "closed"
-	Port      string    `json:"port"`      // "proto:address:port" format
-	Timestamp time.Time `json:"timestamp"` // when the event occurred
-}
-
-// PortMonitor handles periodic monitoring of listening ports in containers
-type PortMonitor struct {
-	mu        sync.Mutex  // protects following
-	lastPorts string      // last netstat/ss output for comparison
-	events    []PortEvent // circular buffer of recent port events
-	maxEvents int         // maximum events to keep in buffer
-}
-
-// NewPortMonitor creates a new PortMonitor instance
-func NewPortMonitor() *PortMonitor {
-	return &PortMonitor{
-		maxEvents: 100, // keep last 100 events
-		events:    make([]PortEvent, 0, 100),
-	}
-}
-
-// Start begins periodic port monitoring in a background goroutine
-func (pm *PortMonitor) Start(ctx context.Context) {
-	go func() {
-		ticker := time.NewTicker(5 * time.Second) // Check every 5 seconds
-		defer ticker.Stop()
-
-		// Get initial port state
-		pm.updatePortState(ctx)
-
-		for {
-			select {
-			case <-ctx.Done():
-				return
-			case <-ticker.C:
-				pm.updatePortState(ctx)
-			}
-		}
-	}()
-}
-
-// updatePortState runs ss and checks for changes in listening ports
-// Falls back to /proc/net/tcp* parsing if ss is not available
-func (pm *PortMonitor) updatePortState(ctx context.Context) {
-	var currentPorts string
-	var err error
-
-	// Try ss command first
-	cmd := exec.CommandContext(ctx, "ss", "-lntu")
-	output, err := cmd.Output()
-	if err != nil {
-		// ss command failed, try /proc filesystem fallback
-		slog.DebugContext(ctx, "ss command failed, trying /proc fallback", "error", err)
-		currentPorts, err = pm.getListeningPortsFromProc()
-		if err != nil {
-			// Both methods failed - log and return
-			slog.DebugContext(ctx, "Failed to get listening ports", "ss_error", err)
-			return
-		}
-	} else {
-		currentPorts = string(output)
-	}
-
-	pm.mu.Lock()
-	defer pm.mu.Unlock()
-
-	// Check if ports have changed
-	if pm.lastPorts != "" && pm.lastPorts != currentPorts {
-		// Ports have changed, log the difference
-		slog.InfoContext(ctx, "Container port changes detected",
-			slog.String("previous_ports", pm.lastPorts),
-			slog.String("current_ports", currentPorts))
-
-		// Parse and compare the port lists for more detailed logging
-		pm.logPortDifferences(ctx, pm.lastPorts, currentPorts)
-	}
-
-	pm.lastPorts = currentPorts
-}
-
-// logPortDifferences parses ss output and logs specific port changes
-func (pm *PortMonitor) logPortDifferences(ctx context.Context, oldPorts, newPorts string) {
-	oldPortSet := parseSSPorts(oldPorts)
-	newPortSet := parseSSPorts(newPorts)
-	now := time.Now()
-
-	// Find newly opened ports
-	for port := range newPortSet {
-		if !oldPortSet[port] {
-			slog.InfoContext(ctx, "New port detected", slog.String("port", port))
-			pm.addEvent(PortEvent{
-				Type:      "opened",
-				Port:      port,
-				Timestamp: now,
-			})
-		}
-	}
-
-	// Find closed ports
-	for port := range oldPortSet {
-		if !newPortSet[port] {
-			slog.InfoContext(ctx, "Port closed", slog.String("port", port))
-			pm.addEvent(PortEvent{
-				Type:      "closed",
-				Port:      port,
-				Timestamp: now,
-			})
-		}
-	}
-}
-
-// addEvent adds a port event to the circular buffer (must be called with mutex held)
-func (pm *PortMonitor) addEvent(event PortEvent) {
-	// If buffer is full, remove oldest event
-	if len(pm.events) >= pm.maxEvents {
-		// Shift all events left by 1 to remove oldest
-		copy(pm.events, pm.events[1:])
-		pm.events = pm.events[:len(pm.events)-1]
-	}
-	// Add new event
-	pm.events = append(pm.events, event)
-}
-
-// GetRecentEvents returns a copy of recent port events since the given timestamp
-func (pm *PortMonitor) GetRecentEvents(since time.Time) []PortEvent {
-	pm.mu.Lock()
-	defer pm.mu.Unlock()
-
-	// Find events since the given timestamp
-	var result []PortEvent
-	for _, event := range pm.events {
-		if event.Timestamp.After(since) {
-			result = append(result, event)
-		}
-	}
-	return result
-}
-
-// UpdatePortState is a public wrapper for updatePortState for testing purposes
-func (pm *PortMonitor) UpdatePortState(ctx context.Context) {
-	pm.updatePortState(ctx)
-}
-
-// GetAllRecentEvents returns a copy of all recent port events
-func (pm *PortMonitor) GetAllRecentEvents() []PortEvent {
-	pm.mu.Lock()
-	defer pm.mu.Unlock()
-
-	// Return a copy of all events
-	result := make([]PortEvent, len(pm.events))
-	copy(result, pm.events)
-	return result
-}
-
-// parseSSPorts extracts listening ports from ss -lntu output
-// Returns a map with "proto:address:port" as keys
-// ss output format: Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
-func parseSSPorts(output string) map[string]bool {
-	ports := make(map[string]bool)
-	lines := strings.Split(output, "\n")
-
-	for _, line := range lines {
-		fields := strings.Fields(line)
-		if len(fields) < 5 {
-			continue
-		}
-
-		// Skip header line and non-LISTEN states
-		if fields[0] == "Netid" || fields[1] != "LISTEN" {
-			continue
-		}
-
-		proto := fields[0]
-		localAddr := fields[4] // Local Address:Port
-		portKey := fmt.Sprintf("%s:%s", proto, localAddr)
-		ports[portKey] = true
-	}
-
-	return ports
-}
-
-// getListeningPortsFromProc reads /proc/net/tcp and /proc/net/tcp6 to find listening ports
-// Returns output in a format similar to ss -lntu
-func (pm *PortMonitor) getListeningPortsFromProc() (string, error) {
-	var result strings.Builder
-	result.WriteString("Netid State  Recv-Q Send-Q Local Address:Port Peer Address:Port\n")
-
-	// Parse IPv4 listening ports
-	if err := pm.parseProc("/proc/net/tcp", "tcp", &result); err != nil {
-		return "", fmt.Errorf("failed to parse /proc/net/tcp: %w", err)
-	}
-
-	// Parse IPv6 listening ports
-	if err := pm.parseProc("/proc/net/tcp6", "tcp", &result); err != nil {
-		// IPv6 might not be available, log but don't fail
-		slog.Debug("Failed to parse /proc/net/tcp6", "error", err)
-	}
-
-	// Parse UDP ports
-	if err := pm.parseProc("/proc/net/udp", "udp", &result); err != nil {
-		slog.Debug("Failed to parse /proc/net/udp", "error", err)
-	}
-
-	if err := pm.parseProc("/proc/net/udp6", "udp", &result); err != nil {
-		slog.Debug("Failed to parse /proc/net/udp6", "error", err)
-	}
-
-	return result.String(), nil
-}
-
-// parseProc parses a /proc/net/* file for listening sockets
-func (pm *PortMonitor) parseProc(filename, protocol string, result *strings.Builder) error {
-	data, err := os.ReadFile(filename)
-	if err != nil {
-		return err
-	}
-
-	lines := strings.Split(string(data), "\n")
-	for i, line := range lines {
-		if i == 0 || strings.TrimSpace(line) == "" {
-			continue // Skip header and empty lines
-		}
-
-		fields := strings.Fields(line)
-		if len(fields) < 4 {
-			continue
-		}
-
-		// Parse socket state (4th field, index 3)
-		stateHex := fields[3]
-		state, err := strconv.ParseInt(stateHex, 16, 32)
-		if err != nil {
-			continue
-		}
-
-		// Check if socket is in LISTEN state (0x0A for TCP) or bound state for UDP
-		isListening := false
-		if protocol == "tcp" && state == 0x0A {
-			isListening = true
-		} else if protocol == "udp" && state == 0x07 {
-			// UDP sockets in state 0x07 (TCP_CLOSE) are bound/listening
-			isListening = true
-		}
-
-		if !isListening {
-			continue
-		}
-
-		// Parse local address (2nd field, index 1)
-		localAddr := fields[1]
-		addr, port, err := pm.parseAddress(localAddr, strings.Contains(filename, "6"))
-		if err != nil {
-			continue
-		}
-
-		// Format similar to ss output
-		result.WriteString(fmt.Sprintf("%s   LISTEN 0      0          %s:%d        0.0.0.0:*\n",
-			protocol, addr, port))
-	}
-
-	return nil
-}
-
-// parseAddress parses hex-encoded address:port from /proc/net files
-func (pm *PortMonitor) parseAddress(addrPort string, isIPv6 bool) (string, int, error) {
-	parts := strings.Split(addrPort, ":")
-	if len(parts) != 2 {
-		return "", 0, fmt.Errorf("invalid address:port format: %s", addrPort)
-	}
-
-	// Parse port (stored in hex, big-endian)
-	portHex := parts[1]
-	port, err := strconv.ParseInt(portHex, 16, 32)
-	if err != nil {
-		return "", 0, fmt.Errorf("invalid port hex: %s", portHex)
-	}
-
-	// Parse IP address
-	addrHex := parts[0]
-	var addr string
-
-	if isIPv6 {
-		// IPv6: 32 hex chars representing 16 bytes
-		if len(addrHex) != 32 {
-			return "", 0, fmt.Errorf("invalid IPv6 address hex length: %d", len(addrHex))
-		}
-		// Convert hex to IPv6 address
-		var ipBytes [16]byte
-		for i := 0; i < 16; i++ {
-			b, err := strconv.ParseInt(addrHex[i*2:(i+1)*2], 16, 8)
-			if err != nil {
-				return "", 0, fmt.Errorf("invalid IPv6 hex: %s", addrHex)
-			}
-			ipBytes[i] = byte(b)
-		}
-		// /proc stores IPv6 in little-endian 32-bit chunks, need to reverse each chunk
-		for i := 0; i < 16; i += 4 {
-			ipBytes[i], ipBytes[i+1], ipBytes[i+2], ipBytes[i+3] = ipBytes[i+3], ipBytes[i+2], ipBytes[i+1], ipBytes[i]
-		}
-		addr = net.IP(ipBytes[:]).String()
-	} else {
-		// IPv4: 8 hex chars representing 4 bytes in little-endian
-		if len(addrHex) != 8 {
-			return "", 0, fmt.Errorf("invalid IPv4 address hex length: %d", len(addrHex))
-		}
-		// Parse as little-endian 32-bit integer
-		addrInt, err := strconv.ParseInt(addrHex, 16, 64)
-		if err != nil {
-			return "", 0, fmt.Errorf("invalid IPv4 hex: %s", addrHex)
-		}
-		// Convert to IP address (reverse byte order for little-endian)
-		addr = fmt.Sprintf("%d.%d.%d.%d",
-			addrInt&0xFF,
-			(addrInt>>8)&0xFF,
-			(addrInt>>16)&0xFF,
-			(addrInt>>24)&0xFF)
-	}
-
-	// Handle special addresses
-	if addr == "0.0.0.0" {
-		addr = "*"
-	} else if addr == "::" {
-		addr = "*"
-	}
-
-	return addr, int(port), nil
-}
diff --git a/loop/port_monitor_test.go b/loop/port_monitor_test.go
deleted file mode 100644
index e0f7ff1..0000000
--- a/loop/port_monitor_test.go
+++ /dev/null
@@ -1,388 +0,0 @@
-package loop
-
-import (
-	"context"
-	"os"
-	"strings"
-	"testing"
-	"time"
-)
-
-// TestPortMonitoring tests the port monitoring functionality
-func TestPortMonitoring(t *testing.T) {
-	// Test with ss output format
-	ssOutput := `Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:PortProcess
-tcp   LISTEN 0      1024       127.0.0.1:40975      0.0.0.0:*          
-tcp   LISTEN 0      4096               *:22               *:*          
-tcp   LISTEN 0      4096               *:80               *:*          
-udp   UNCONN 0      0               127.0.0.1:123            0.0.0.0:*          
-`
-
-	expected := map[string]bool{
-		"tcp:127.0.0.1:40975": true,
-		"tcp:*:22":            true,
-		"tcp:*:80":            true,
-	}
-
-	result := parseSSPorts(ssOutput)
-
-	// Check that all expected ports are found
-	for port := range expected {
-		if !result[port] {
-			t.Errorf("Expected port %s not found in ss parsed output", port)
-		}
-	}
-
-	// Check that UDP port is not included (since it's UNCONN, not LISTEN)
-	if result["udp:127.0.0.1:123"] {
-		t.Errorf("UDP UNCONN port should not be included in listening ports")
-	}
-
-	// Check that no extra ports are found
-	for port := range result {
-		if !expected[port] {
-			t.Errorf("Unexpected port %s found in parsed output", port)
-		}
-	}
-}
-
-// TestPortMonitoringLogDifferences tests the port difference logging
-func TestPortMonitoringLogDifferences(t *testing.T) {
-	ctx := context.Background()
-
-	oldPorts := `Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:PortProcess
-tcp   LISTEN 0      4096               *:22               *:*          
-tcp   LISTEN 0      1024       127.0.0.1:8080      0.0.0.0:*          
-`
-
-	newPorts := `Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:PortProcess
-tcp   LISTEN 0      4096               *:22               *:*          
-tcp   LISTEN 0      1024       127.0.0.1:9090      0.0.0.0:*          
-`
-
-	// Create a port monitor to test the logPortDifferences method
-	pm := NewPortMonitor()
-
-	// This test mainly ensures the method doesn't panic and processes the differences
-	// The actual logging output would need to be captured via a test logger to verify fully
-	pm.logPortDifferences(ctx, oldPorts, newPorts)
-
-	// Test with no differences
-	pm.logPortDifferences(ctx, oldPorts, oldPorts)
-}
-
-// TestPortMonitorCreation tests creating a new port monitor
-func TestPortMonitorCreation(t *testing.T) {
-	pm := NewPortMonitor()
-	if pm == nil {
-		t.Error("NewPortMonitor() returned nil")
-	}
-
-	// Verify initial state
-	pm.mu.Lock()
-	defer pm.mu.Unlock()
-	if pm.lastPorts != "" {
-		t.Error("NewPortMonitor() should have empty lastPorts initially")
-	}
-}
-
-// TestParseSSPortsEdgeCases tests edge cases in ss output parsing
-func TestParseSSPortsEdgeCases(t *testing.T) {
-	tests := []struct {
-		name     string
-		output   string
-		expected map[string]bool
-	}{
-		{
-			name:     "empty output",
-			output:   "",
-			expected: map[string]bool{},
-		},
-		{
-			name:     "header only",
-			output:   "Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:PortProcess",
-			expected: map[string]bool{},
-		},
-		{
-			name:     "non-listen states filtered out",
-			output:   "tcp   ESTAB  0      0       127.0.0.1:8080      127.0.0.1:45678\nudp   UNCONN 0      0       127.0.0.1:123       0.0.0.0:*",
-			expected: map[string]bool{},
-		},
-		{
-			name:     "insufficient fields",
-			output:   "tcp LISTEN 0",
-			expected: map[string]bool{},
-		},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			result := parseSSPorts(test.output)
-			if len(result) != len(test.expected) {
-				t.Errorf("Expected %d ports, got %d", len(test.expected), len(result))
-			}
-			for port := range test.expected {
-				if !result[port] {
-					t.Errorf("Expected port %s not found", port)
-				}
-			}
-			for port := range result {
-				if !test.expected[port] {
-					t.Errorf("Unexpected port %s found", port)
-				}
-			}
-		})
-	}
-}
-
-// TestPortEventStorage tests the new event storage functionality
-func TestPortEventStorage(t *testing.T) {
-	pm := NewPortMonitor()
-
-	// Initially should have no events
-	allEvents := pm.GetAllRecentEvents()
-	if len(allEvents) != 0 {
-		t.Errorf("Expected 0 events initially, got %d", len(allEvents))
-	}
-
-	// Simulate port changes that would add events
-	ctx := context.Background()
-	oldPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp   LISTEN 0      128    0.0.0.0:8080         0.0.0.0:*"
-	newPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp   LISTEN 0      128    0.0.0.0:9090         0.0.0.0:*"
-
-	pm.logPortDifferences(ctx, oldPorts, newPorts)
-
-	// Should now have events
-	allEvents = pm.GetAllRecentEvents()
-	if len(allEvents) != 2 {
-		t.Errorf("Expected 2 events (1 opened, 1 closed), got %d", len(allEvents))
-	}
-
-	// Check event types
-	foundOpened := false
-	foundClosed := false
-	for _, event := range allEvents {
-		if event.Type == "opened" && event.Port == "tcp:0.0.0.0:9090" {
-			foundOpened = true
-		}
-		if event.Type == "closed" && event.Port == "tcp:0.0.0.0:8080" {
-			foundClosed = true
-		}
-	}
-
-	if !foundOpened {
-		t.Error("Expected to find 'opened' event for port tcp:0.0.0.0:9090")
-	}
-	if !foundClosed {
-		t.Error("Expected to find 'closed' event for port tcp:0.0.0.0:8080")
-	}
-}
-
-// TestPortEventFiltering tests the time-based filtering
-func TestPortEventFiltering(t *testing.T) {
-	pm := NewPortMonitor()
-	ctx := context.Background()
-
-	// Record time before adding events
-	beforeTime := time.Now()
-	time.Sleep(1 * time.Millisecond) // Small delay to ensure timestamp difference
-
-	// Add some events
-	oldPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp   LISTEN 0      128    0.0.0.0:8080         0.0.0.0:*"
-	newPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp   LISTEN 0      128    0.0.0.0:9090         0.0.0.0:*"
-	pm.logPortDifferences(ctx, oldPorts, newPorts)
-
-	// Get events since beforeTime - should get all events
-	recentEvents := pm.GetRecentEvents(beforeTime)
-	if len(recentEvents) != 2 {
-		t.Errorf("Expected 2 recent events, got %d", len(recentEvents))
-	}
-
-	// Get events since now - should get no events
-	nowTime := time.Now()
-	recentEvents = pm.GetRecentEvents(nowTime)
-	if len(recentEvents) != 0 {
-		t.Errorf("Expected 0 recent events since now, got %d", len(recentEvents))
-	}
-}
-
-// TestParseAddress tests the hex address parsing for /proc/net files
-func TestParseAddress(t *testing.T) {
-	pm := NewPortMonitor()
-
-	tests := []struct {
-		name       string
-		addrPort   string
-		isIPv6     bool
-		expectIP   string
-		expectPort int
-		expectErr  bool
-	}{
-		{
-			name:       "IPv4 localhost:80",
-			addrPort:   "0100007F:0050", // 127.0.0.1:80 in little-endian hex
-			isIPv6:     false,
-			expectIP:   "127.0.0.1",
-			expectPort: 80,
-			expectErr:  false,
-		},
-		{
-			name:       "IPv4 any:22",
-			addrPort:   "00000000:0016", // 0.0.0.0:22
-			isIPv6:     false,
-			expectIP:   "*",
-			expectPort: 22,
-			expectErr:  false,
-		},
-		{
-			name:       "IPv4 high port",
-			addrPort:   "0100007F:1F90", // 127.0.0.1:8080
-			isIPv6:     false,
-			expectIP:   "127.0.0.1",
-			expectPort: 8080,
-			expectErr:  false,
-		},
-		{
-			name:       "IPv6 any port 22",
-			addrPort:   "00000000000000000000000000000000:0016", // [::]:22
-			isIPv6:     true,
-			expectIP:   "*",
-			expectPort: 22,
-			expectErr:  false,
-		},
-		{
-			name:      "Invalid format - no colon",
-			addrPort:  "0100007F0050",
-			isIPv6:    false,
-			expectErr: true,
-		},
-		{
-			name:      "Invalid port hex",
-			addrPort:  "0100007F:ZZZZ",
-			isIPv6:    false,
-			expectErr: true,
-		},
-		{
-			name:      "Invalid IPv4 hex length",
-			addrPort:  "0100:0050",
-			isIPv6:    false,
-			expectErr: true,
-		},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			ip, port, err := pm.parseAddress(test.addrPort, test.isIPv6)
-			if test.expectErr {
-				if err == nil {
-					t.Errorf("Expected error but got none")
-				}
-				return
-			}
-
-			if err != nil {
-				t.Errorf("Unexpected error: %v", err)
-				return
-			}
-
-			if ip != test.expectIP {
-				t.Errorf("Expected IP %s, got %s", test.expectIP, ip)
-			}
-
-			if port != test.expectPort {
-				t.Errorf("Expected port %d, got %d", test.expectPort, port)
-			}
-		})
-	}
-}
-
-// TestParseProcData tests parsing of mock /proc/net data
-func TestParseProcData(t *testing.T) {
-	pm := NewPortMonitor()
-
-	// Test TCP data with listening sockets
-	tcpData := `  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
-   0: 0100007F:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0        1
-   1: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0        2
-   2: 0100007F:1F90 0200007F:C350 01 00000000:00000000 00:00000000 00000000     0        0        3`
-
-	var result strings.Builder
-	result.WriteString("Netid State  Recv-Q Send-Q Local Address:Port Peer Address:Port\n")
-
-	// Create temp file with test data
-	tmpFile := "/tmp/test_tcp"
-	err := os.WriteFile(tmpFile, []byte(tcpData), 0o644)
-	if err != nil {
-		t.Fatalf("Failed to create temp file: %v", err)
-	}
-	defer os.Remove(tmpFile)
-
-	err = pm.parseProc(tmpFile, "tcp", &result)
-	if err != nil {
-		t.Fatalf("parseProc failed: %v", err)
-	}
-
-	output := result.String()
-	t.Logf("Generated output:\n%s", output)
-
-	// Should contain listening ports (state 0A = LISTEN)
-	if !strings.Contains(output, "127.0.0.1:80") {
-		t.Error("Expected to find 127.0.0.1:80 in output")
-	}
-	if !strings.Contains(output, "*:22") {
-		t.Error("Expected to find *:22 in output")
-	}
-	// Should not contain established connection (state 01)
-	if strings.Contains(output, "127.0.0.1:8080") {
-		t.Error("Should not find established connection 127.0.0.1:8080 in output")
-	}
-}
-
-// TestGetListeningPortsFromProcFallback tests the complete /proc fallback
-func TestGetListeningPortsFromProcFallback(t *testing.T) {
-	pm := NewPortMonitor()
-
-	// This test verifies the method runs without error
-	// The actual files may or may not exist, but it should handle both cases gracefully
-	output, err := pm.getListeningPortsFromProc()
-	if err != nil {
-		t.Logf("getListeningPortsFromProc failed (may be expected if /proc/net files don't exist): %v", err)
-		// Don't fail the test - this might be expected in some environments
-		return
-	}
-
-	t.Logf("Generated /proc fallback output:\n%s", output)
-
-	// Should at least have a header
-	if !strings.Contains(output, "Netid State") {
-		t.Error("Expected header in /proc fallback output")
-	}
-}
-
-// TestUpdatePortStateWithFallback tests updatePortState with both ss and /proc fallback
-func TestUpdatePortStateWithFallback(t *testing.T) {
-	pm := NewPortMonitor()
-	ctx := context.Background()
-
-	// Call updatePortState - should try ss first, then fall back to /proc if ss fails
-	pm.updatePortState(ctx)
-
-	// The method should complete without panicking
-	// We can't easily test the exact behavior without mocking, but we can ensure it runs
-	// Check if any port state was captured
-	pm.mu.Lock()
-	lastPorts := pm.lastPorts
-	pm.mu.Unlock()
-
-	t.Logf("Captured port state (length %d):", len(lastPorts))
-	if len(lastPorts) > 0 {
-		t.Logf("First 200 chars: %s", lastPorts[:min(200, len(lastPorts))])
-	}
-}
-
-func min(a, b int) int {
-	if a < b {
-		return a
-	}
-	return b
-}
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index 7f31401..b6e2259 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -307,44 +307,6 @@
 		io.WriteString(w, "{}\n")
 	})
 
-	// Handler for /port-events - returns recent port change events
-	s.mux.HandleFunc("/port-events", func(w http.ResponseWriter, r *http.Request) {
-		if r.Method != http.MethodGet {
-			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
-			return
-		}
-
-		w.Header().Set("Content-Type", "application/json")
-
-		// Get the 'since' query parameter for filtering events
-		sinceParam := r.URL.Query().Get("since")
-		var events []loop.PortEvent
-
-		// Get port monitor from agent
-		portMonitor := agent.GetPortMonitor()
-		if portMonitor == nil {
-			// Return empty array if port monitor not available
-			events = []loop.PortEvent{}
-		} else if sinceParam != "" {
-			// Parse the since timestamp
-			sinceTime, err := time.Parse(time.RFC3339, sinceParam)
-			if err != nil {
-				http.Error(w, fmt.Sprintf("Invalid 'since' timestamp format: %v", err), http.StatusBadRequest)
-				return
-			}
-			events = portMonitor.GetRecentEvents(sinceTime)
-		} else {
-			// Return all recent events
-			events = portMonitor.GetAllRecentEvents()
-		}
-
-		// Encode and return the events
-		if err := json.NewEncoder(w).Encode(events); err != nil {
-			slog.ErrorContext(r.Context(), "Error encoding port events response", slog.Any("err", err))
-			http.Error(w, "Internal server error", http.StatusInternalServerError)
-		}
-	})
-
 	// Handler for /messages?start=N&end=M (start/end are optional)
 	s.mux.HandleFunc("/messages", func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("Content-Type", "application/json")
diff --git a/loop/server/loophttp_test.go b/loop/server/loophttp_test.go
index adda4e3..f6ec8c7 100644
--- a/loop/server/loophttp_test.go
+++ b/loop/server/loophttp_test.go
@@ -3,7 +3,6 @@
 import (
 	"bufio"
 	"context"
-	"encoding/json"
 	"net/http"
 	"net/http/httptest"
 	"slices"
@@ -261,10 +260,9 @@
 	m.retryNumber++
 }
 
-func (m *mockAgent) GetPortMonitor() *loop.PortMonitor { return loop.NewPortMonitor() }
-func (m *mockAgent) SkabandAddr() string               { return m.skabandAddr }
-func (m *mockAgent) LinkToGitHub() bool                { return false }
-func (m *mockAgent) DiffStats() (int, int)             { return 0, 0 }
+func (m *mockAgent) SkabandAddr() string   { return m.skabandAddr }
+func (m *mockAgent) LinkToGitHub() bool    { return false }
+func (m *mockAgent) DiffStats() (int, int) { return 0, 0 }
 
 // TestSSEStream tests the SSE stream endpoint
 func TestSSEStream(t *testing.T) {
@@ -512,76 +510,6 @@
 	t.Log("Mock CompactConversation works correctly")
 }
 
-// TestPortEventsEndpoint tests the /port-events HTTP endpoint
-func TestPortEventsEndpoint(t *testing.T) {
-	// Create a mock agent that implements the CodingAgent interface
-	agent := &mockAgent{
-		branchPrefix: "sketch/",
-	}
-
-	// Create a server with the mock agent
-	server, err := server.New(agent, nil)
-	if err != nil {
-		t.Fatalf("Failed to create server: %v", err)
-	}
-
-	// Test GET /port-events
-	req, err := http.NewRequest("GET", "/port-events", nil)
-	if err != nil {
-		t.Fatalf("Failed to create request: %v", err)
-	}
-
-	rr := httptest.NewRecorder()
-	server.ServeHTTP(rr, req)
-
-	// Should return 200 OK
-	if status := rr.Code; status != http.StatusOK {
-		t.Errorf("Expected status code %d, got %d", http.StatusOK, status)
-	}
-
-	// Should return JSON content type
-	contentType := rr.Header().Get("Content-Type")
-	if contentType != "application/json" {
-		t.Errorf("Expected Content-Type application/json, got %s", contentType)
-	}
-
-	// Should return valid JSON (empty array since mock returns no events)
-	var events []any
-	if err := json.Unmarshal(rr.Body.Bytes(), &events); err != nil {
-		t.Errorf("Failed to parse JSON response: %v", err)
-	}
-
-	// Should be empty array for mock agent
-	if len(events) != 0 {
-		t.Errorf("Expected empty events array, got %d events", len(events))
-	}
-}
-
-// TestPortEventsEndpointMethodNotAllowed tests that non-GET requests are rejected
-func TestPortEventsEndpointMethodNotAllowed(t *testing.T) {
-	agent := &mockAgent{
-		branchPrefix: "sketch/",
-	}
-	server, err := server.New(agent, nil)
-	if err != nil {
-		t.Fatalf("Failed to create server: %v", err)
-	}
-
-	// Test POST /port-events (should be rejected)
-	req, err := http.NewRequest("POST", "/port-events", nil)
-	if err != nil {
-		t.Fatalf("Failed to create request: %v", err)
-	}
-
-	rr := httptest.NewRecorder()
-	server.ServeHTTP(rr, req)
-
-	// Should return 405 Method Not Allowed
-	if status := rr.Code; status != http.StatusMethodNotAllowed {
-		t.Errorf("Expected status code %d, got %d", http.StatusMethodNotAllowed, status)
-	}
-}
-
 func TestParsePortProxyHost(t *testing.T) {
 	tests := []struct {
 		name     string