diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 55dfd9f..3e8fa09 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -29,7 +29,13 @@ Decimal phases appear between their surrounding integers in numeric order.
3. User running without root/CAP_NET_RAW sees a clear error message with a `sudo` hint — not a panic or silent failure
4. On exit, user sees a per-protocol packet count summary printed to stderr
5. User can pass `--verbose` and see per-window protocol activity lines on stderr
-**Plans**: TBD
+**Plans:** 4 plans
+
+Plans:
+- [ ] 01-01-PLAN.md — Go 1.24 setup, module init, shared types, config-driven classifier with tests
+- [ ] 01-02-PLAN.md — Capture package: OpenCapture, ListInterfaces, privilege error handling
+- [ ] 01-03-PLAN.md — Aggregation: time-windowed accumulator, exit summary, verbose output
+- [ ] 01-04-PLAN.md — CLI wiring: Cobra commands, signal handling, pipeline assembly, smoke test
### Phase 2: Audio Synthesis Engine
**Goal**: The synthesis and encoding stack produces a valid MP3 from synthetic WindowSnapshot inputs — audio pipeline fully validated before any real traffic flows through it
@@ -71,7 +77,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
-| 1. Capture and Classification | 0/? | Not started | - |
+| 1. Capture and Classification | 0/4 | Planning complete | - |
| 2. Audio Synthesis Engine | 0/? | Not started | - |
| 3. Pipeline Integration and MVP | 0/? | Not started | - |
| 4. Power User Features | 0/? | Not started | - |
diff --git a/.planning/config.json b/.planning/config.json
index 67cbcac..b4645a1 100644
--- a/.planning/config.json
+++ b/.planning/config.json
@@ -25,7 +25,8 @@
"text_mode": false,
"research_before_questions": false,
"discuss_mode": "discuss",
- "skip_discuss": false
+ "skip_discuss": false,
+ "_auto_chain_active": false
},
"hooks": {
"context_warnings": true
diff --git a/.planning/phases/01-capture-and-classification/01-01-PLAN.md b/.planning/phases/01-capture-and-classification/01-01-PLAN.md
new file mode 100644
index 0000000..728e96c
--- /dev/null
+++ b/.planning/phases/01-capture-and-classification/01-01-PLAN.md
@@ -0,0 +1,337 @@
+---
+phase: 01-capture-and-classification
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - go.mod
+ - go.sum
+ - classify/rules.go
+ - classify/classifier.go
+ - classify/classifier_test.go
+ - classify/types.go
+autonomous: true
+requirements: [CLAS-01]
+
+must_haves:
+ truths:
+ - "Each of 10+ protocol classes (ICMP, DNS, HTTPS, HTTP, SSH, SMTP, NTP, DHCP, other-TCP, other-UDP, unknown) is correctly identified from packet headers"
+ - "Classification rules are stored in a config-driven slice/struct, not hardcoded switch statements"
+ - "Unrecognized traffic is grouped as 'unknown' class"
+ artifacts:
+ - path: "classify/types.go"
+ provides: "TrafficClass type, ClassifiedPacket struct, WindowSnapshot struct"
+ exports: ["TrafficClass", "ClassifiedPacket", "WindowSnapshot"]
+ - path: "classify/rules.go"
+ provides: "Rule struct and DefaultRules slice with 10+ protocol rules"
+ contains: "DefaultRules"
+ - path: "classify/classifier.go"
+ provides: "Classify function that matches packets against rule table"
+ exports: ["Classify", "NewClassifier"]
+ - path: "classify/classifier_test.go"
+ provides: "Unit tests for all 10+ protocol classes"
+ min_lines: 80
+ key_links:
+ - from: "classify/classifier.go"
+ to: "classify/rules.go"
+ via: "Classifier uses DefaultRules slice"
+ pattern: "DefaultRules"
+ - from: "classify/classifier.go"
+ to: "classify/types.go"
+ via: "Returns TrafficClass from Classify function"
+ pattern: "TrafficClass"
+---
+
+
+Bootstrap Go project and implement the protocol classification engine with full test coverage.
+
+Purpose: Establish the Go module, core types shared across all packages, and the config-driven classifier that maps packets to one of 10+ traffic classes. This is the foundation every other plan depends on.
+
+Output: Compiling Go module with classify package, passing unit tests for all protocol classes.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/01-capture-and-classification/01-CONTEXT.md
+@.planning/phases/01-capture-and-classification/01-RESEARCH.md
+
+
+
+
+
+ Task 1: Install Go 1.24, initialize module, and create shared types
+ go.mod, classify/types.go
+
+ .planning/phases/01-capture-and-classification/01-RESEARCH.md
+
+
+1. Install Go 1.24+ from https://go.dev/dl/ (NOT apt which gives 1.22). Download the linux-arm64 tarball:
+ - wget https://go.dev/dl/go1.24.1.linux-arm64.tar.gz
+ - sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.24.1.linux-arm64.tar.gz
+ - Ensure /usr/local/go/bin is in PATH (export PATH=$PATH:/usr/local/go/bin)
+ - Verify: `go version` must show go1.24.x
+
+2. Initialize the Go module:
+ - cd /home/dev/workspace/yoloyolo
+ - go mod init github.com/netsynth/netsynth
+ - mkdir -p classify capture aggregate cmd/netsynth
+
+3. Create classify/types.go with the following types (these are the contracts for all packages):
+
+```go
+package classify
+
+// TrafficClass represents a classified network traffic category.
+type TrafficClass string
+
+const (
+ ClassICMP TrafficClass = "ICMP"
+ ClassDNS TrafficClass = "DNS"
+ ClassHTTPS TrafficClass = "HTTPS"
+ ClassHTTP TrafficClass = "HTTP"
+ ClassSSH TrafficClass = "SSH"
+ ClassSMTP TrafficClass = "SMTP"
+ ClassNTP TrafficClass = "NTP"
+ ClassDHCP TrafficClass = "DHCP"
+ ClassOtherTCP TrafficClass = "other-TCP"
+ ClassOtherUDP TrafficClass = "other-UDP"
+ ClassUnknown TrafficClass = "unknown"
+)
+
+// AllClasses returns all known traffic classes in display order.
+func AllClasses() []TrafficClass {
+ return []TrafficClass{
+ ClassICMP, ClassDNS, ClassHTTPS, ClassHTTP, ClassSSH,
+ ClassSMTP, ClassNTP, ClassDHCP, ClassOtherTCP, ClassOtherUDP, ClassUnknown,
+ }
+}
+
+// ClassifiedPacket pairs a raw packet's classification result with metadata.
+type ClassifiedPacket struct {
+ Class TrafficClass
+ SrcPort uint16
+ DstPort uint16
+ Protocol string // "tcp", "udp", "icmp"
+ Length int
+}
+
+// WindowSnapshot holds aggregated packet counts for a time window.
+// This is the contract between the aggregation stage and future audio synthesis (Phase 2).
+type WindowSnapshot struct {
+ Counts map[TrafficClass]int64
+ TotalPackets int64
+ WindowIndex int
+}
+```
+
+4. Fetch Phase 1 dependencies:
+ - go get github.com/gopacket/gopacket@v1.5.0
+ - go get github.com/packetcap/go-pcap@latest
+ - go get github.com/spf13/cobra@v1.10.2
+ - go mod tidy
+
+
+ cd /home/dev/workspace/yoloyolo && go version | grep -q "go1.24" && go build ./classify/...
+
+
+ - `go version` output contains "go1.24"
+ - go.mod contains `module github.com/netsynth/netsynth`
+ - go.mod contains `github.com/gopacket/gopacket`
+ - go.mod contains `github.com/packetcap/go-pcap`
+ - go.mod contains `github.com/spf13/cobra`
+ - classify/types.go contains `type TrafficClass string`
+ - classify/types.go contains `ClassICMP`, `ClassDNS`, `ClassHTTPS`, `ClassHTTP`, `ClassSSH`, `ClassSMTP`, `ClassNTP`, `ClassDHCP`, `ClassOtherTCP`, `ClassOtherUDP`, `ClassUnknown`
+ - classify/types.go contains `type ClassifiedPacket struct`
+ - classify/types.go contains `type WindowSnapshot struct`
+ - `go build ./classify/...` exits 0
+
+ Go 1.24 installed, module initialized with all Phase 1 deps, classify/types.go compiles with all 11 traffic classes and shared types.
+
+
+
+ Task 2: Implement config-driven classifier with tests for all protocol classes
+ classify/rules.go, classify/classifier.go, classify/classifier_test.go
+
+ classify/types.go
+ .planning/phases/01-capture-and-classification/01-RESEARCH.md
+
+
+ - TestClassifyICMP: ICMP packet (no port) -> ClassICMP
+ - TestClassifyDNS_UDP: UDP dst port 53 -> ClassDNS
+ - TestClassifyDNS_TCP: TCP dst port 53 -> ClassDNS
+ - TestClassifyHTTPS: TCP dst port 443 -> ClassHTTPS
+ - TestClassifyHTTP: TCP dst port 80 -> ClassHTTP
+ - TestClassifySSH: TCP dst port 22 -> ClassSSH
+ - TestClassifySMTP: TCP dst port 25 -> ClassSMTP
+ - TestClassifyNTP: UDP dst port 123 -> ClassNTP
+ - TestClassifyDHCP: UDP dst port 67 or 68 -> ClassDHCP
+ - TestClassifyOtherTCP: TCP dst port 8080 (unmatched) -> ClassOtherTCP
+ - TestClassifyOtherUDP: UDP dst port 9999 (unmatched) -> ClassOtherUDP
+ - TestClassifyUnknown: Packet with no TCP/UDP/ICMP layer -> ClassUnknown
+ - TestRulesAreOrderDependent: First matching rule wins
+
+
+1. Create classify/rules.go with Rule struct and DefaultRules per D-01 and D-02:
+
+```go
+package classify
+
+// Rule defines a classification rule. Protocol is "tcp", "udp", or "icmp".
+// DstPort 0 means match any port for this protocol.
+// Rules are evaluated in order; first match wins.
+type Rule struct {
+ Protocol string
+ DstPort uint16
+ Class TrafficClass
+}
+
+// DefaultRules is the D-01 protocol map — ordered, first-match-wins.
+// Per D-02: config-driven slice, not a switch statement.
+var DefaultRules = []Rule{
+ {Protocol: "icmp", DstPort: 0, Class: ClassICMP},
+ {Protocol: "udp", DstPort: 53, Class: ClassDNS},
+ {Protocol: "tcp", DstPort: 53, Class: ClassDNS},
+ {Protocol: "tcp", DstPort: 443, Class: ClassHTTPS},
+ {Protocol: "tcp", DstPort: 80, Class: ClassHTTP},
+ {Protocol: "tcp", DstPort: 22, Class: ClassSSH},
+ {Protocol: "tcp", DstPort: 25, Class: ClassSMTP},
+ {Protocol: "udp", DstPort: 123, Class: ClassNTP},
+ {Protocol: "udp", DstPort: 67, Class: ClassDHCP},
+ {Protocol: "udp", DstPort: 68, Class: ClassDHCP},
+ // Catch-alls (must be last):
+ {Protocol: "tcp", DstPort: 0, Class: ClassOtherTCP},
+ {Protocol: "udp", DstPort: 0, Class: ClassOtherUDP},
+}
+```
+
+2. Create classify/classifier.go:
+
+```go
+package classify
+
+import (
+ "github.com/gopacket/gopacket"
+ "github.com/gopacket/gopacket/layers"
+)
+
+// Classifier classifies packets against a rule table.
+type Classifier struct {
+ rules []Rule
+}
+
+// NewClassifier creates a classifier with the given rules.
+func NewClassifier(rules []Rule) *Classifier {
+ return &Classifier{rules: rules}
+}
+
+// Classify inspects a gopacket.Packet and returns a ClassifiedPacket.
+// It checks ICMP first, then extracts TCP/UDP port info and matches against rules.
+// Per D-03: returns ClassUnknown if no rule matches.
+func (c *Classifier) Classify(pkt gopacket.Packet) ClassifiedPacket {
+ result := ClassifiedPacket{
+ Class: ClassUnknown,
+ Length: len(pkt.Data()),
+ }
+
+ // Check ICMP
+ if pkt.Layer(layers.LayerTypeICMPv4) != nil || pkt.Layer(layers.LayerTypeICMPv6) != nil {
+ result.Protocol = "icmp"
+ // Match against rules
+ for _, rule := range c.rules {
+ if rule.Protocol == "icmp" {
+ result.Class = rule.Class
+ return result
+ }
+ }
+ return result
+ }
+
+ // Check TCP
+ if tcpLayer := pkt.Layer(layers.LayerTypeTCP); tcpLayer != nil {
+ tcp := tcpLayer.(*layers.TCP)
+ result.Protocol = "tcp"
+ result.SrcPort = uint16(tcp.SrcPort)
+ result.DstPort = uint16(tcp.DstPort)
+ for _, rule := range c.rules {
+ if rule.Protocol == "tcp" && (rule.DstPort == 0 || rule.DstPort == result.DstPort) {
+ result.Class = rule.Class
+ return result
+ }
+ }
+ return result
+ }
+
+ // Check UDP
+ if udpLayer := pkt.Layer(layers.LayerTypeUDP); udpLayer != nil {
+ udp := udpLayer.(*layers.UDP)
+ result.Protocol = "udp"
+ result.SrcPort = uint16(udp.SrcPort)
+ result.DstPort = uint16(udp.DstPort)
+ for _, rule := range c.rules {
+ if rule.Protocol == "udp" && (rule.DstPort == 0 || rule.DstPort == result.DstPort) {
+ result.Class = rule.Class
+ return result
+ }
+ }
+ return result
+ }
+
+ // No recognized transport layer -> ClassUnknown per D-03
+ return result
+}
+```
+
+3. Write tests in classify/classifier_test.go using gopacket test packet construction:
+ - Build synthetic packets using gopacket.SerializeBuffer with layers.Ethernet + layers.IPv4 + layers.TCP/UDP/ICMP headers
+ - Test each of the 10+ protocol classes plus ClassUnknown
+ - Test that rule order matters (first match wins)
+
+Run RED phase first: write all tests, verify they fail (classifier not yet imported or logic wrong).
+Then GREEN: implement until all pass.
+
+
+ cd /home/dev/workspace/yoloyolo && go test -v -count=1 ./classify/...
+
+
+ - classify/rules.go contains `type Rule struct`
+ - classify/rules.go contains `var DefaultRules = []Rule{`
+ - classify/rules.go contains at least 12 Rule entries (10 specific + 2 catch-all)
+ - classify/classifier.go contains `func (c *Classifier) Classify(pkt gopacket.Packet) ClassifiedPacket`
+ - classify/classifier.go contains `func NewClassifier(rules []Rule) *Classifier`
+ - classify/classifier.go does NOT contain `switch` keyword (per D-02)
+ - classify/classifier_test.go contains `TestClassify` with subtests for ICMP, DNS, HTTPS, HTTP, SSH, SMTP, NTP, DHCP, OtherTCP, OtherUDP, Unknown
+ - `go test -v -count=1 ./classify/...` exits 0 with all subtests passing
+
+ Classifier passes tests for all 11 traffic classes. Rules stored as config-driven slice (D-02). First-match-wins ordering verified. No switch statements in classification logic.
+
+
+
+
+
+- `go version` shows 1.24+
+- `go build ./classify/...` compiles without errors
+- `go test -v -count=1 ./classify/...` all tests pass
+- `go vet ./classify/...` no issues
+- No import of `google/gopacket` anywhere (must be `gopacket/gopacket`)
+
+
+
+- Go 1.24 installed and working
+- Module initialized with gopacket, go-pcap, cobra dependencies
+- 11 TrafficClass constants defined
+- ClassifiedPacket and WindowSnapshot types defined (shared contracts)
+- Config-driven classifier with 12+ rules in a slice (not switch)
+- All protocol class unit tests passing
+
+
+
diff --git a/.planning/phases/01-capture-and-classification/01-02-PLAN.md b/.planning/phases/01-capture-and-classification/01-02-PLAN.md
new file mode 100644
index 0000000..83d98c4
--- /dev/null
+++ b/.planning/phases/01-capture-and-classification/01-02-PLAN.md
@@ -0,0 +1,263 @@
+---
+phase: 01-capture-and-classification
+plan: 02
+type: execute
+wave: 2
+depends_on: ["01-01"]
+files_modified:
+ - capture/capture.go
+ - capture/capture_test.go
+autonomous: true
+requirements: [CAPT-01, CAPT-02, CAPT-04]
+
+must_haves:
+ truths:
+ - "User can specify a network interface via -i flag and packets are captured from it"
+ - "User can list available network interfaces via --list-interfaces"
+ - "User without root/CAP_NET_RAW sees platform-specific error with sudo hint"
+ artifacts:
+ - path: "capture/capture.go"
+ provides: "OpenCapture, ListInterfaces, permission error handling"
+ exports: ["OpenCapture", "ListInterfaces", "StartCapture"]
+ - path: "capture/capture_test.go"
+ provides: "Tests for ListInterfaces and permission error messages"
+ min_lines: 60
+ key_links:
+ - from: "capture/capture.go"
+ to: "github.com/packetcap/go-pcap"
+ via: "pcap.OpenLive for live capture"
+ pattern: "pcap\\.OpenLive"
+ - from: "capture/capture.go"
+ to: "net.Interfaces"
+ via: "stdlib for interface enumeration (NOT pcap.FindAllDevs)"
+ pattern: "net\\.Interfaces"
+ - from: "capture/capture.go"
+ to: "runtime.GOOS"
+ via: "OS detection for platform-specific error messages"
+ pattern: "runtime\\.GOOS"
+---
+
+
+Implement the capture package: live packet capture via go-pcap, interface listing via net.Interfaces(), and platform-specific privilege error handling.
+
+Purpose: This package is the data source for the entire pipeline. It wraps go-pcap with proper error handling and provides the channel-based packet source that the classifier consumes.
+
+Output: capture/ package with OpenCapture, ListInterfaces, StartCapture functions and unit tests.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/phases/01-capture-and-classification/01-CONTEXT.md
+@.planning/phases/01-capture-and-classification/01-RESEARCH.md
+@.planning/phases/01-capture-and-classification/01-01-SUMMARY.md
+
+
+
+```go
+package classify
+
+type TrafficClass string
+// 11 constants: ClassICMP, ClassDNS, ClassHTTPS, ClassHTTP, ClassSSH, ClassSMTP, ClassNTP, ClassDHCP, ClassOtherTCP, ClassOtherUDP, ClassUnknown
+
+type ClassifiedPacket struct {
+ Class TrafficClass
+ SrcPort uint16
+ DstPort uint16
+ Protocol string
+ Length int
+}
+
+type WindowSnapshot struct {
+ Counts map[TrafficClass]int64
+ TotalPackets int64
+ WindowIndex int
+}
+```
+
+
+
+
+
+
+ Task 1: Implement capture package with OpenCapture, ListInterfaces, and StartCapture
+ capture/capture.go, capture/capture_test.go
+
+ classify/types.go
+ go.mod
+ .planning/phases/01-capture-and-classification/01-RESEARCH.md
+
+
+ - TestListInterfaces: net.Interfaces() returns at least one interface (lo always exists)
+ - TestListInterfacesFormat: Each interface has Name and Flags fields populated
+ - TestPermissionErrorLinux: When runtime.GOOS=="linux" and OpenLive fails with "operation not permitted", error message contains "sudo setcap cap_net_raw+ep"
+ - TestPermissionErrorDarwin: When runtime.GOOS=="darwin" and OpenLive fails with "permission denied", error message contains "sudo netsynth"
+ - TestIsPermissionError: Recognizes "permission denied", "operation not permitted", and "pcap_create" error strings
+ - TestIsPermissionErrorNegative: Does not flag "interface not found" as permission error
+
+
+1. Create capture/capture.go with these functions:
+
+```go
+package capture
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "os"
+ "runtime"
+ "strings"
+ "sync/atomic"
+
+ "github.com/gopacket/gopacket"
+ "github.com/gopacket/gopacket/layers"
+ pcap "github.com/packetcap/go-pcap"
+)
+
+// ListInterfaces returns all network interfaces using Go stdlib.
+// Does NOT use pcap.FindAllDevs (go-pcap has no such function).
+// Requires no privileges (CAPT-02).
+func ListInterfaces() ([]net.Interface, error) {
+ return net.Interfaces()
+}
+
+// OpenCapture opens a live capture handle on the given interface.
+// snaplen=65535, promiscuous=false, timeout=0 (block until packet).
+// Returns a platform-specific error message if permission is denied (CAPT-04, D-05).
+func OpenCapture(ctx context.Context, iface string) (*pcap.Handle, error) {
+ handle, err := pcap.OpenLive(ctx, iface, 65535, false, 0, false)
+ if err != nil {
+ if isPermissionError(err) {
+ return nil, permissionErrorMsg(iface)
+ }
+ return nil, fmt.Errorf("failed to open interface %q: %w", iface, err)
+ }
+ return handle, nil
+}
+
+// StartCapture opens a capture handle and returns a channel of gopacket.Packet.
+// The channel is closed when ctx is cancelled (Ctrl+C) or capture ends.
+// Uses non-blocking send with drop counter per Pitfall 4 from RESEARCH.md.
+// Uses dynamic link type detection per Pitfall 6 from RESEARCH.md.
+func StartCapture(ctx context.Context, iface string) (<-chan gopacket.Packet, *int64, error) {
+ handle, err := OpenCapture(ctx, iface)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ lt := layers.LinkType(handle.LinkType())
+ packetSource := gopacket.NewPacketSource(handle, lt)
+ packetSource.NoCopy = true // safe: go-pcap copies internally
+
+ packets := make(chan gopacket.Packet, 512)
+ var droppedPackets int64
+
+ go func() {
+ defer close(packets)
+ defer handle.Close()
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+ pkt, err := packetSource.NextPacket()
+ if err != nil {
+ // Context cancelled or handle closed
+ return
+ }
+ select {
+ case packets <- pkt:
+ default:
+ atomic.AddInt64(&droppedPackets, 1)
+ }
+ }
+ }()
+
+ return packets, &droppedPackets, nil
+}
+
+// isPermissionError checks if the error indicates insufficient privileges.
+func isPermissionError(err error) bool {
+ s := strings.ToLower(err.Error())
+ return strings.Contains(s, "permission denied") ||
+ strings.Contains(s, "operation not permitted") ||
+ strings.Contains(s, "pcap_create")
+}
+
+// permissionErrorMsg returns a platform-specific privilege hint (D-05).
+func permissionErrorMsg(iface string) error {
+ bin, _ := os.Executable()
+ if bin == "" {
+ bin = "netsynth"
+ }
+ switch runtime.GOOS {
+ case "linux":
+ return fmt.Errorf(
+ "packet capture requires root or CAP_NET_RAW.\n"+
+ "Run as root: sudo %s -i %s\n"+
+ "Or grant capability: sudo setcap cap_net_raw+ep %s",
+ bin, iface, bin)
+ default: // darwin and others
+ return fmt.Errorf(
+ "packet capture requires root privileges.\n"+
+ "Run as root: sudo %s -i %s", bin, iface)
+ }
+}
+```
+
+2. Create capture/capture_test.go:
+ - TestListInterfaces: Call ListInterfaces(), assert len > 0 (lo always exists), assert first interface has non-empty Name
+ - TestIsPermissionError: Table-driven test with positive cases ("permission denied", "operation not permitted", "pcap_create: foo") and negative cases ("interface not found", "no such device")
+ - TestPermissionErrorMsg: Call permissionErrorMsg("eth0") on current OS, verify the error string contains "sudo" and the interface name "eth0". On linux verify it contains "setcap cap_net_raw+ep"
+ - NOTE: Do NOT test OpenCapture or StartCapture directly in unit tests -- they require root/CAP_NET_RAW. Those are integration-tested manually via `sudo ./netsynth -i eth0`.
+
+Write tests first (RED), then implement (GREEN).
+
+
+ cd /home/dev/workspace/yoloyolo && go test -v -count=1 ./capture/...
+
+
+ - capture/capture.go contains `func ListInterfaces() ([]net.Interface, error)`
+ - capture/capture.go contains `func OpenCapture(ctx context.Context, iface string) (*pcap.Handle, error)`
+ - capture/capture.go contains `func StartCapture(ctx context.Context, iface string) (<-chan gopacket.Packet, *int64, error)`
+ - capture/capture.go contains `net.Interfaces()` (NOT pcap.FindAllDevs)
+ - capture/capture.go contains `runtime.GOOS` for platform detection
+ - capture/capture.go contains `"setcap cap_net_raw+ep"` in linux error branch
+ - capture/capture.go contains `atomic.AddInt64(&droppedPackets, 1)` for non-blocking send
+ - capture/capture.go contains `layers.LinkType(handle.LinkType())` for dynamic link type
+ - capture/capture_test.go contains `TestListInterfaces`
+ - capture/capture_test.go contains `TestIsPermissionError`
+ - capture/capture_test.go contains `TestPermissionErrorMsg`
+ - `go test -v -count=1 ./capture/...` exits 0
+
+ capture package compiles and tests pass. ListInterfaces uses net.Interfaces (no pcap dependency for listing). Permission errors show platform-specific sudo/setcap hints. StartCapture uses non-blocking send with drop counter and dynamic link type.
+
+
+
+
+
+- `go test -v -count=1 ./capture/...` all tests pass
+- `go vet ./capture/...` no issues
+- No import of `gopacket/pcap` (must use `packetcap/go-pcap`)
+- No import of `google/gopacket` (must use `gopacket/gopacket`)
+
+
+
+- ListInterfaces returns system interfaces without privileges
+- Permission error detection covers "permission denied", "operation not permitted", "pcap_create"
+- Error messages are platform-specific (Linux: setcap hint, macOS: sudo hint)
+- StartCapture uses buffered channel (512) with atomic drop counter
+- Dynamic link type detection (not hardcoded LinkTypeEthernet)
+- All unit tests passing
+
+
+
diff --git a/.planning/phases/01-capture-and-classification/01-03-PLAN.md b/.planning/phases/01-capture-and-classification/01-03-PLAN.md
new file mode 100644
index 0000000..28daf37
--- /dev/null
+++ b/.planning/phases/01-capture-and-classification/01-03-PLAN.md
@@ -0,0 +1,344 @@
+---
+phase: 01-capture-and-classification
+plan: 03
+type: execute
+wave: 2
+depends_on: ["01-01"]
+files_modified:
+ - aggregate/window.go
+ - aggregate/summary.go
+ - aggregate/window_test.go
+ - aggregate/summary_test.go
+autonomous: true
+requirements: [CLAS-03, CLAS-04]
+
+must_haves:
+ truths:
+ - "On exit, user sees a per-protocol packet count summary printed to stderr"
+ - "User can pass --verbose and see per-window protocol activity lines on stderr"
+ - "Aggregator accumulates packet counts into time windows and emits WindowSnapshots"
+ artifacts:
+ - path: "aggregate/window.go"
+ provides: "Ticker-driven window aggregator that emits WindowSnapshots"
+ exports: ["Aggregate"]
+ - path: "aggregate/summary.go"
+ provides: "PrintSummary and PrintWindowLine functions for stderr output"
+ exports: ["PrintSummary", "PrintWindowLine"]
+ - path: "aggregate/window_test.go"
+ provides: "Tests for window aggregation logic"
+ min_lines: 50
+ - path: "aggregate/summary_test.go"
+ provides: "Tests for summary formatting and verbose output"
+ min_lines: 60
+ key_links:
+ - from: "aggregate/window.go"
+ to: "classify/types.go"
+ via: "Consumes ClassifiedPacket, produces WindowSnapshot"
+ pattern: "classify\\.ClassifiedPacket"
+ - from: "aggregate/summary.go"
+ to: "classify/types.go"
+ via: "Uses TrafficClass and WindowSnapshot for formatting"
+ pattern: "classify\\.TrafficClass"
+---
+
+
+Implement the aggregation and stats reporting package: time-windowed accumulation, exit summary, and verbose per-window output.
+
+Purpose: This package bridges classified packets and the eventual audio engine (Phase 2). It accumulates per-class counts into time windows, prints verbose activity lines during capture, and prints a final protocol summary on exit.
+
+Output: aggregate/ package with Aggregate, PrintSummary, PrintWindowLine functions and full unit tests.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/phases/01-capture-and-classification/01-CONTEXT.md
+@.planning/phases/01-capture-and-classification/01-RESEARCH.md
+@.planning/phases/01-capture-and-classification/01-01-SUMMARY.md
+
+
+
+```go
+package classify
+
+type TrafficClass string
+
+const (
+ ClassICMP TrafficClass = "ICMP"
+ ClassDNS TrafficClass = "DNS"
+ ClassHTTPS TrafficClass = "HTTPS"
+ ClassHTTP TrafficClass = "HTTP"
+ ClassSSH TrafficClass = "SSH"
+ ClassSMTP TrafficClass = "SMTP"
+ ClassNTP TrafficClass = "NTP"
+ ClassDHCP TrafficClass = "DHCP"
+ ClassOtherTCP TrafficClass = "other-TCP"
+ ClassOtherUDP TrafficClass = "other-UDP"
+ ClassUnknown TrafficClass = "unknown"
+)
+
+func AllClasses() []TrafficClass { ... }
+
+type ClassifiedPacket struct {
+ Class TrafficClass
+ SrcPort uint16
+ DstPort uint16
+ Protocol string
+ Length int
+}
+
+type WindowSnapshot struct {
+ Counts map[TrafficClass]int64
+ TotalPackets int64
+ WindowIndex int
+}
+```
+
+
+
+
+
+
+ Task 1: Implement ticker-driven window aggregator
+ aggregate/window.go, aggregate/window_test.go
+
+ classify/types.go
+ .planning/phases/01-capture-and-classification/01-RESEARCH.md
+
+
+ - TestAggregateEmitsSnapshot: Send 5 ClassifiedPackets, trigger window tick, receive WindowSnapshot with correct counts
+ - TestAggregateMultipleClasses: Send packets of 3 different classes, verify snapshot has all 3 with correct counts
+ - TestAggregateDoneFlushesPartial: Close done channel mid-window, verify final partial snapshot is emitted
+ - TestAggregateEmptyWindow: Trigger tick with no packets, verify snapshot has zero counts and TotalPackets==0
+ - TestAggregateWindowIndex: Multiple ticks increment WindowIndex (0, 1, 2...)
+
+
+1. Create aggregate/window.go implementing the ticker-driven aggregation pattern from RESEARCH.md Pattern 2:
+
+```go
+package aggregate
+
+import (
+ "time"
+
+ "github.com/netsynth/netsynth/classify"
+)
+
+// DefaultWindowMs is the default time window duration in milliseconds.
+// Claude's discretion per CONTEXT.md.
+const DefaultWindowMs = 500
+
+// Aggregate reads ClassifiedPackets from events, accumulates counts per TrafficClass
+// in time windows of windowMs duration, and emits a WindowSnapshot per window.
+// When done is closed, flushes the current partial window and closes the output channel.
+// If verbose is true, calls onSnapshot for each window (used for --verbose stderr output).
+func Aggregate(done <-chan struct{}, events <-chan classify.ClassifiedPacket,
+ windowMs int, onSnapshot func(classify.WindowSnapshot)) <-chan classify.WindowSnapshot {
+
+ out := make(chan classify.WindowSnapshot, 8)
+ go func() {
+ defer close(out)
+ ticker := time.NewTicker(time.Duration(windowMs) * time.Millisecond)
+ defer ticker.Stop()
+
+ counts := make(map[classify.TrafficClass]int64)
+ var totalPackets int64
+ windowIndex := 0
+
+ flush := func() {
+ snap := classify.WindowSnapshot{
+ Counts: counts,
+ TotalPackets: totalPackets,
+ WindowIndex: windowIndex,
+ }
+ if onSnapshot != nil {
+ onSnapshot(snap)
+ }
+ out <- snap
+ counts = make(map[classify.TrafficClass]int64)
+ totalPackets = 0
+ windowIndex++
+ }
+
+ for {
+ select {
+ case <-done:
+ flush() // flush final partial window
+ return
+ case <-ticker.C:
+ flush()
+ case ev, ok := <-events:
+ if !ok {
+ flush()
+ return
+ }
+ counts[ev.Class]++
+ totalPackets++
+ }
+ }
+ }()
+ return out
+}
+```
+
+2. Create aggregate/window_test.go:
+ - Use a done channel and a buffered events channel to control timing without real tickers
+ - For ticker tests, use a short windowMs (10ms) and sleep briefly to trigger ticks
+ - For done-flush tests, close done channel immediately and verify output
+ - Verify WindowSnapshot.Counts map contains expected class keys and values
+ - Verify WindowSnapshot.TotalPackets matches sum of counts
+ - Verify WindowIndex increments across windows
+
+Write tests first (RED), then implement (GREEN).
+
+
+ cd /home/dev/workspace/yoloyolo && go test -v -count=1 -race ./aggregate/...
+
+
+ - aggregate/window.go contains `func Aggregate(done <-chan struct{}, events <-chan classify.ClassifiedPacket`
+ - aggregate/window.go contains `const DefaultWindowMs = 500`
+ - aggregate/window.go contains `time.NewTicker`
+ - aggregate/window.go imports `github.com/netsynth/netsynth/classify`
+ - aggregate/window_test.go contains `TestAggregate` with subtests for snapshot emission, multiple classes, done flush, empty window, and window index
+ - `go test -v -count=1 -race ./aggregate/...` exits 0
+
+ Aggregate function emits WindowSnapshots on ticker ticks and flushes partial window on done. All timing-based tests pass including race detector.
+
+
+
+ Task 2: Implement exit summary and verbose window output formatting
+ aggregate/summary.go, aggregate/summary_test.go
+
+ classify/types.go
+ aggregate/window.go
+ .planning/phases/01-capture-and-classification/01-RESEARCH.md
+
+
+ - TestPrintSummary: Given totals map {ICMP: 10, DNS: 50, HTTPS: 100}, output contains "ICMP", "DNS", "HTTPS" with correct counts and percentages, plus "TOTAL" line
+ - TestPrintSummaryEmpty: Given empty totals, output contains "TOTAL" with 0 packets
+ - TestPrintSummarySorted: Output classes appear in stable alphabetical or canonical order (not random map order)
+ - TestPrintWindowLine: Given WindowSnapshot with {DNS: 5, HTTPS: 10}, output contains window index, class names, and counts on a single line
+ - TestPrintWindowLineEmpty: Given empty snapshot, output contains window index with no class entries
+ - TestAccumulateTotals: Given multiple WindowSnapshots, AccumulateTotals produces correct cumulative counts
+
+
+1. Create aggregate/summary.go:
+
+```go
+package aggregate
+
+import (
+ "fmt"
+ "io"
+ "sort"
+
+ "github.com/netsynth/netsynth/classify"
+)
+
+// PrintSummary writes a per-protocol packet count summary to w (typically os.Stderr).
+// Output format matches tcpdump conventions (CLAS-03).
+func PrintSummary(w io.Writer, totals map[classify.TrafficClass]int64) {
+ var totalPackets int64
+ for _, count := range totals {
+ totalPackets += count
+ }
+
+ fmt.Fprintln(w, "\n--- Protocol Summary ---")
+
+ // Sort classes for stable output
+ classes := make([]string, 0, len(totals))
+ for c := range totals {
+ classes = append(classes, string(c))
+ }
+ sort.Strings(classes)
+
+ for _, className := range classes {
+ c := classify.TrafficClass(className)
+ count := totals[c]
+ pct := 0.0
+ if totalPackets > 0 {
+ pct = float64(count) / float64(totalPackets) * 100.0
+ }
+ fmt.Fprintf(w, " %-15s %8d packets (%5.1f%%)\n", c, count, pct)
+ }
+ fmt.Fprintf(w, " %-15s %8d packets\n", "TOTAL", totalPackets)
+}
+
+// PrintWindowLine writes a single verbose per-window activity line to w (CLAS-04).
+// Format: [window N] CLASS:count CLASS:count ... (total: N)
+func PrintWindowLine(w io.Writer, snap classify.WindowSnapshot) {
+ fmt.Fprintf(w, "[window %d]", snap.WindowIndex)
+ // Sort for stable output
+ classes := make([]string, 0, len(snap.Counts))
+ for c := range snap.Counts {
+ classes = append(classes, string(c))
+ }
+ sort.Strings(classes)
+ for _, className := range classes {
+ c := classify.TrafficClass(className)
+ if snap.Counts[c] > 0 {
+ fmt.Fprintf(w, " %s:%d", c, snap.Counts[c])
+ }
+ }
+ fmt.Fprintf(w, " (total: %d)\n", snap.TotalPackets)
+}
+
+// AccumulateTotals merges a WindowSnapshot's counts into cumulative totals.
+func AccumulateTotals(totals map[classify.TrafficClass]int64, snap classify.WindowSnapshot) {
+ for class, count := range snap.Counts {
+ totals[class] += count
+ }
+}
+```
+
+2. Create aggregate/summary_test.go:
+ - Use bytes.Buffer as io.Writer to capture output
+ - TestPrintSummary: Verify output contains expected class names, counts, percentages, and TOTAL line
+ - TestPrintSummaryEmpty: Verify output contains "TOTAL" with 0
+ - TestPrintSummarySorted: Verify output lines are in alphabetical order (capture output, split lines, check ordering)
+ - TestPrintWindowLine: Verify format "[window 0] DNS:5 HTTPS:10 (total: 15)"
+ - TestAccumulateTotals: Two snapshots accumulated, verify merged counts
+
+Write tests first (RED), then implement (GREEN).
+
+
+ cd /home/dev/workspace/yoloyolo && go test -v -count=1 ./aggregate/...
+
+
+ - aggregate/summary.go contains `func PrintSummary(w io.Writer, totals map[classify.TrafficClass]int64)`
+ - aggregate/summary.go contains `func PrintWindowLine(w io.Writer, snap classify.WindowSnapshot)`
+ - aggregate/summary.go contains `func AccumulateTotals(totals map[classify.TrafficClass]int64, snap classify.WindowSnapshot)`
+ - aggregate/summary.go contains `"--- Protocol Summary ---"`
+ - aggregate/summary.go contains `sort.Strings` for stable output ordering
+ - aggregate/summary_test.go contains `TestPrintSummary`, `TestPrintSummaryEmpty`, `TestPrintSummarySorted`
+ - aggregate/summary_test.go contains `TestPrintWindowLine`, `TestAccumulateTotals`
+ - `go test -v -count=1 ./aggregate/...` exits 0
+
+ PrintSummary outputs per-protocol counts with percentages and TOTAL to stderr (CLAS-03). PrintWindowLine outputs per-window activity for --verbose mode (CLAS-04). AccumulateTotals correctly merges snapshots. All formatting tests pass with stable sorted output.
+
+
+
+
+
+- `go test -v -count=1 -race ./aggregate/...` all tests pass
+- `go vet ./aggregate/...` no issues
+- Output format matches tcpdump convention (stderr, per-protocol counts with percentages)
+
+
+
+- Aggregate emits WindowSnapshots on time ticks and flushes on done
+- PrintSummary prints per-protocol packet counts with percentages and TOTAL to an io.Writer
+- PrintWindowLine prints per-window activity for verbose mode
+- AccumulateTotals correctly accumulates across multiple snapshots
+- All output is sorted for deterministic display
+- All unit tests passing (including race detector)
+
+
+
diff --git a/.planning/phases/01-capture-and-classification/01-04-PLAN.md b/.planning/phases/01-capture-and-classification/01-04-PLAN.md
new file mode 100644
index 0000000..8fe57c1
--- /dev/null
+++ b/.planning/phases/01-capture-and-classification/01-04-PLAN.md
@@ -0,0 +1,323 @@
+---
+phase: 01-capture-and-classification
+plan: 04
+type: execute
+wave: 3
+depends_on: ["01-01", "01-02", "01-03"]
+files_modified:
+ - cmd/netsynth/main.go
+ - cmd/netsynth/main_test.go
+autonomous: false
+requirements: [CAPT-01, CAPT-02, CAPT-04, CLAS-01, CLAS-03, CLAS-04]
+
+must_haves:
+ truths:
+ - "User can run `netsynth -i eth0` and see packets being classified live to stderr"
+ - "User can run `netsynth --list-interfaces` and see available interfaces"
+ - "User without root sees platform-specific error with sudo hint"
+ - "On exit (Ctrl+C), user sees per-protocol packet count summary on stderr"
+ - "User can pass `--verbose` and see per-window protocol activity on stderr"
+ artifacts:
+ - path: "cmd/netsynth/main.go"
+ provides: "Cobra CLI wiring, signal handling, pipeline assembly"
+ exports: ["main"]
+ - path: "cmd/netsynth/main_test.go"
+ provides: "Tests for flag parsing and list-interfaces output"
+ min_lines: 30
+ key_links:
+ - from: "cmd/netsynth/main.go"
+ to: "capture/capture.go"
+ via: "StartCapture for packet channel, ListInterfaces for --list-interfaces"
+ pattern: "capture\\.StartCapture|capture\\.ListInterfaces"
+ - from: "cmd/netsynth/main.go"
+ to: "classify/classifier.go"
+ via: "NewClassifier + Classify in pipeline goroutine"
+ pattern: "classify\\.NewClassifier|classifier\\.Classify"
+ - from: "cmd/netsynth/main.go"
+ to: "aggregate/window.go"
+ via: "Aggregate consumes classified packets channel"
+ pattern: "aggregate\\.Aggregate"
+ - from: "cmd/netsynth/main.go"
+ to: "aggregate/summary.go"
+ via: "PrintSummary on exit, PrintWindowLine for verbose callback"
+ pattern: "aggregate\\.PrintSummary|aggregate\\.PrintWindowLine"
+---
+
+
+Wire all packages into the Cobra CLI: flag parsing, signal handling, capture-classify-aggregate pipeline, verbose output, and exit summary.
+
+Purpose: This is the final integration that makes `netsynth` a runnable tool. It connects capture -> classify -> aggregate stages via channels, handles Ctrl+C gracefully, and prints the exit summary. All Phase 1 success criteria become observable here.
+
+Output: Working `netsynth` binary with -i, --list-interfaces, --verbose flags. Checkpoint for manual smoke test.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/phases/01-capture-and-classification/01-CONTEXT.md
+@.planning/phases/01-capture-and-classification/01-RESEARCH.md
+@.planning/phases/01-capture-and-classification/01-01-SUMMARY.md
+@.planning/phases/01-capture-and-classification/01-02-SUMMARY.md
+@.planning/phases/01-capture-and-classification/01-03-SUMMARY.md
+
+
+
+```go
+func ListInterfaces() ([]net.Interface, error)
+func OpenCapture(ctx context.Context, iface string) (*pcap.Handle, error)
+func StartCapture(ctx context.Context, iface string) (<-chan gopacket.Packet, *int64, error)
+```
+
+
+```go
+type TrafficClass string // 11 constants
+type ClassifiedPacket struct { Class TrafficClass; SrcPort, DstPort uint16; Protocol string; Length int }
+type WindowSnapshot struct { Counts map[TrafficClass]int64; TotalPackets int64; WindowIndex int }
+type Classifier struct { ... }
+func NewClassifier(rules []Rule) *Classifier
+func (c *Classifier) Classify(pkt gopacket.Packet) ClassifiedPacket
+var DefaultRules []Rule
+```
+
+
+```go
+const DefaultWindowMs = 500
+func Aggregate(done <-chan struct{}, events <-chan classify.ClassifiedPacket, windowMs int, onSnapshot func(classify.WindowSnapshot)) <-chan classify.WindowSnapshot
+func PrintSummary(w io.Writer, totals map[classify.TrafficClass]int64)
+func PrintWindowLine(w io.Writer, snap classify.WindowSnapshot)
+func AccumulateTotals(totals map[classify.TrafficClass]int64, snap classify.WindowSnapshot)
+```
+
+
+
+
+
+
+ Task 1: Wire Cobra CLI with capture-classify-aggregate pipeline and signal handling
+ cmd/netsynth/main.go, cmd/netsynth/main_test.go
+
+ capture/capture.go
+ classify/classifier.go
+ classify/rules.go
+ classify/types.go
+ aggregate/window.go
+ aggregate/summary.go
+ go.mod
+ .planning/phases/01-capture-and-classification/01-RESEARCH.md
+
+
+1. Create cmd/netsynth/main.go with Cobra root command:
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/signal"
+ "strings"
+ "sync/atomic"
+ "syscall"
+
+ "github.com/spf13/cobra"
+
+ "github.com/netsynth/netsynth/aggregate"
+ "github.com/netsynth/netsynth/capture"
+ "github.com/netsynth/netsynth/classify"
+)
+
+var (
+ ifaceName string
+ listIfaces bool
+ verbose bool
+)
+
+func main() {
+ rootCmd := &cobra.Command{
+ Use: "netsynth",
+ Short: "Sonify live network traffic into ambient audio",
+ Long: "NetSynth captures network traffic, classifies it by protocol, and (in future phases) synthesizes an ambient MP3 soundscape.",
+ RunE: run,
+ }
+
+ rootCmd.Flags().StringVarP(&ifaceName, "interface", "i", "", "Network interface to capture on (required unless --list-interfaces)")
+ rootCmd.Flags().BoolVar(&listIfaces, "list-interfaces", false, "List available network interfaces and exit")
+ rootCmd.Flags().BoolVar(&verbose, "verbose", false, "Print per-window protocol activity to stderr")
+
+ if err := rootCmd.Execute(); err != nil {
+ os.Exit(1)
+ }
+}
+
+func run(cmd *cobra.Command, args []string) error {
+ // --list-interfaces mode (CAPT-02)
+ if listIfaces {
+ return runListInterfaces()
+ }
+
+ // Require -i flag
+ if ifaceName == "" {
+ return fmt.Errorf("interface required: use -i or --list-interfaces to see available interfaces")
+ }
+
+ // Set up signal handling (Ctrl+C)
+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
+ defer stop()
+
+ // Stage 1: Capture (CAPT-01)
+ fmt.Fprintf(os.Stderr, "Starting capture on %s... (press Ctrl+C to stop)\n", ifaceName)
+ packets, droppedPtr, err := capture.StartCapture(ctx, ifaceName)
+ if err != nil {
+ return err // CAPT-04: permission error already has platform-specific message
+ }
+
+ // Stage 2: Classify (CLAS-01)
+ classifier := classify.NewClassifier(classify.DefaultRules)
+ classified := make(chan classify.ClassifiedPacket, 1024)
+ go func() {
+ defer close(classified)
+ for pkt := range packets {
+ classified <- classifier.Classify(pkt)
+ }
+ }()
+
+ // Stage 3: Aggregate with optional verbose callback (CLAS-04)
+ var onSnapshot func(classify.WindowSnapshot)
+ if verbose {
+ onSnapshot = func(snap classify.WindowSnapshot) {
+ aggregate.PrintWindowLine(os.Stderr, snap) // CLAS-04: --verbose output
+ }
+ }
+ snapshots := aggregate.Aggregate(ctx.Done(), classified, aggregate.DefaultWindowMs, onSnapshot)
+
+ // Consume snapshots and accumulate totals
+ totals := make(map[classify.TrafficClass]int64)
+ for snap := range snapshots {
+ aggregate.AccumulateTotals(totals, snap)
+ }
+
+ // Print exit summary (CLAS-03)
+ dropped := atomic.LoadInt64(droppedPtr)
+ if dropped > 0 {
+ fmt.Fprintf(os.Stderr, "\nWarning: %d packets dropped (channel buffer full)\n", dropped)
+ }
+ aggregate.PrintSummary(os.Stderr, totals)
+
+ return nil
+}
+
+func runListInterfaces() error {
+ ifaces, err := capture.ListInterfaces()
+ if err != nil {
+ return fmt.Errorf("error listing interfaces: %w", err)
+ }
+ if len(ifaces) == 0 {
+ fmt.Fprintln(os.Stderr, "No interfaces found. If interfaces are missing, run with sudo.")
+ return nil
+ }
+ fmt.Fprintln(os.Stderr, "Available interfaces:")
+ for _, iface := range ifaces {
+ addrs, _ := iface.Addrs()
+ addrStrs := make([]string, len(addrs))
+ for i, a := range addrs {
+ addrStrs[i] = a.String()
+ }
+ fmt.Fprintf(os.Stderr, " %-15s flags=%s addrs=%s\n",
+ iface.Name, iface.Flags, strings.Join(addrStrs, ", "))
+ }
+ return nil
+}
+```
+
+2. Create cmd/netsynth/main_test.go with:
+ - TestListInterfacesFlag: Execute rootCmd with `--list-interfaces`, verify it exits 0 (integration-lite test)
+ - TestMissingInterfaceFlag: Execute rootCmd with no flags, verify error message contains "interface required"
+ - TestHelpOutput: Execute rootCmd with `--help`, verify output contains "-i", "--list-interfaces", "--verbose"
+
+3. Build the binary:
+ - `CGO_ENABLED=0 go build -o netsynth ./cmd/netsynth`
+ - Verify: `./netsynth --help` shows usage
+ - Verify: `./netsynth --list-interfaces` shows interfaces
+ - Verify: `./netsynth -i eth0` without root shows permission error with sudo hint
+
+4. Run full test suite: `go test -v -race ./...`
+
+
+ cd /home/dev/workspace/yoloyolo && CGO_ENABLED=0 go build -o netsynth ./cmd/netsynth && ./netsynth --list-interfaces 2>&1 | grep -q "flags=" && go test -v -count=1 ./...
+
+
+ - cmd/netsynth/main.go contains `rootCmd.Flags().StringVarP(&ifaceName, "interface", "i"`
+ - cmd/netsynth/main.go contains `rootCmd.Flags().BoolVar(&listIfaces, "list-interfaces"`
+ - cmd/netsynth/main.go contains `rootCmd.Flags().BoolVar(&verbose, "verbose"`
+ - cmd/netsynth/main.go contains `signal.NotifyContext`
+ - cmd/netsynth/main.go contains `capture.StartCapture`
+ - cmd/netsynth/main.go contains `classify.NewClassifier(classify.DefaultRules)`
+ - cmd/netsynth/main.go contains `aggregate.Aggregate(`
+ - cmd/netsynth/main.go contains `aggregate.PrintSummary(os.Stderr`
+ - cmd/netsynth/main.go contains `aggregate.PrintWindowLine(os.Stderr`
+ - `CGO_ENABLED=0 go build -o netsynth ./cmd/netsynth` exits 0
+ - `./netsynth --list-interfaces` output contains interface names and "flags="
+ - `./netsynth 2>&1` (no flags) output contains "interface required"
+ - `go test -v -count=1 ./...` exits 0 (all unit tests pass)
+
+ netsynth binary builds and runs. --list-interfaces shows interfaces. Missing -i shows clear error. Signal handling wires Ctrl+C to clean pipeline shutdown. Exit summary prints per-protocol counts. All tests pass.
+
+
+
+ Task 2: Smoke test live capture on real interface
+ cmd/netsynth/main.go
+
+ This is a human verification checkpoint. The executor should present the smoke test instructions below to the user and wait for approval. No code changes are needed -- this verifies the work done in Task 1.
+
+
+ Complete Phase 1 pipeline: netsynth CLI with live packet capture, protocol classification, and stderr output.
+
+
+ 1. Build: `cd /home/dev/workspace/yoloyolo && CGO_ENABLED=0 go build -o netsynth ./cmd/netsynth`
+ 2. List interfaces: `./netsynth --list-interfaces` -- should show eth0 and lo at minimum
+ 3. Test permission error (non-root): `./netsynth -i eth0` -- should show sudo/setcap hint
+ 4. Test live capture (needs root): `sudo ./netsynth -i eth0 --verbose`
+ - Generate some traffic in another terminal: `curl https://example.com`, `ping -c 3 8.8.8.8`
+ - You should see per-window verbose lines on stderr showing HTTPS, ICMP, DNS counts
+ - Press Ctrl+C
+ - You should see "--- Protocol Summary ---" with per-protocol packet counts and percentages
+ 5. Verify no-flag error: `./netsynth` -- should say "interface required"
+ 6. Verify help: `./netsynth --help` -- should show -i, --list-interfaces, --verbose flags
+
+
+ cd /home/dev/workspace/yoloyolo && ./netsynth --list-interfaces 2>&1 | grep -q "flags="
+
+ User confirmed live capture works: packets classified, verbose output shows per-window activity, Ctrl+C produces protocol summary.
+ Type "approved" if live capture works correctly, or describe issues
+
+
+
+
+
+- `CGO_ENABLED=0 go build -o netsynth ./cmd/netsynth` compiles successfully
+- `./netsynth --list-interfaces` shows system interfaces
+- `./netsynth` (no flags) shows clear error about missing -i flag
+- `./netsynth --help` shows all three flags
+- `go test -v -race ./...` all tests pass across all packages
+- No import of `google/gopacket` anywhere
+- No switch statements in classify/classifier.go
+
+
+
+- netsynth binary builds with CGO_ENABLED=0 (no CGo needed for Phase 1)
+- -i flag accepted, --list-interfaces works, --verbose works
+- Permission error shows platform-specific sudo hint
+- Live capture classifies packets and prints verbose lines (when --verbose)
+- Ctrl+C produces clean exit with per-protocol summary
+- All unit tests pass across classify/, capture/, aggregate/, and cmd/netsynth/
+
+
+