--- 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/ After completion, create `.planning/phases/01-capture-and-classification/01-04-SUMMARY.md`