diff --git a/.planning/phases/01-capture-and-classification/01-RESEARCH.md b/.planning/phases/01-capture-and-classification/01-RESEARCH.md
new file mode 100644
index 0000000..0ff8d0f
--- /dev/null
+++ b/.planning/phases/01-capture-and-classification/01-RESEARCH.md
@@ -0,0 +1,713 @@
+# Phase 1: Capture and Classification - Research
+
+**Researched:** 2026-03-25
+**Domain:** Go CLI scaffolding, live packet capture (pure-Go pcap), protocol classification pipeline
+**Confidence:** HIGH (stack and patterns fully verified via official docs; one gap noted for go-pcap interface listing)
+
+---
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+
+- **D-01:** Deep port map with 10+ classes: ICMP, DNS (53), HTTPS (443), HTTP (80), SSH (22), SMTP (25), NTP (123), DHCP (67/68), other-TCP, other-UDP
+- **D-02:** Classification rules stored in a config-driven Go map/struct (not hardcoded switch statements) — designed so rules could later be loaded from a config file
+- **D-03:** All unrecognized traffic grouped as a single "unknown" class until Phase 3 adds auto-clustering
+- **D-04:** Support Linux and macOS (not Windows)
+- **D-05:** On permission failure, detect the OS and show platform-specific guidance: `sudo setcap cap_net_raw+ep ...` on Linux, `sudo ...` on macOS
+- **D-06:** Prefer static binary with no runtime libpcap dependency — use pure-Go pcap backend (go-pcap) where possible
+
+### Claude's Discretion
+
+- CLI output formatting during capture (stderr layout, colors, table width)
+- Default time window duration for aggregation buckets
+- Default network interface selection when `-i` is omitted
+- Verbose output format and level of detail
+
+### Deferred Ideas (OUT OF SCOPE)
+
+None — discussion stayed within phase scope
+
+
+---
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|------------------|
+| CAPT-01 | User can specify network interface via `-i` flag | Cobra persistent flag `--interface/-i`; `go-pcap.OpenLive()` accepts device name string |
+| CAPT-02 | User can list available network interfaces via `--list-interfaces` | `net.Interfaces()` from Go stdlib (go-pcap has no FindAllDevs); no privilege required to enumerate |
+| CAPT-04 | User sees a clear actionable error message when lacking capture privileges | `OpenLive()` returns an error on permission failure; inspect error string + `runtime.GOOS` to produce platform-specific hint |
+| CLAS-01 | Known protocols (ICMP, DNS, TCP/443, TCP/other, UDP, SSH) each produce a distinct recognizable sound | Rule table as `map[string]TrafficClass` keyed by composite key (proto+port); gopacket layer decoding gives TCP/UDP/ICMP layers cleanly |
+| CLAS-03 | On exit, user sees a summary of packet counts and protocol breakdown (printed to stderr) | Counters in `map[TrafficClass]int64` incremented per classified packet; print on signal handler / channel drain |
+| CLAS-04 | User can enable per-window protocol activity log via `--verbose` flag | Cobra bool flag; time-window aggregator emits `WindowSnapshot`; verbose mode prints each snapshot to stderr |
+
+
+---
+
+## Summary
+
+Phase 1 establishes the full capture-to-classify pipeline in Go: a Cobra-based CLI that opens a live network interface via the pure-Go `packetcap/go-pcap` library, decodes packets with `gopacket`, classifies them against a config-driven protocol rule table, aggregates counts into time windows, and prints live status lines plus a per-protocol summary on exit. No audio synthesis is included; Phase 2 consumes the `WindowSnapshot` output this phase defines.
+
+The primary technical challenge is privilege handling: `go-pcap.OpenLive()` silently fails on Linux when the binary lacks `CAP_NET_RAW`, and the error message from the underlying syscall gives no actionable hint. The CLI must intercept that error and emit a platform-specific remediation message. The second challenge is that `packetcap/go-pcap` has no interface enumeration API — `--list-interfaces` must use Go's stdlib `net.Interfaces()` instead. This distinction is important to implement correctly.
+
+All other elements (channel pipeline, ticker window, classification struct) are standard Go patterns. The project is greenfield with no existing code; Phase 1 establishes directory layout, `go.mod`, and all data types that downstream phases depend on.
+
+**Primary recommendation:** Use `packetcap/go-pcap` + `gopacket/gopacket` for capture/decode, `net.Interfaces()` for interface listing, `spf13/cobra` for CLI, and the channel-connected pipeline pattern from ARCHITECTURE.md throughout.
+
+---
+
+## Project Constraints (from CLAUDE.md)
+
+Directives that apply to this phase:
+
+- **Language:** Go — mandatory, no alternatives
+- **Privileges:** Packet capture requires root/CAP_NET_RAW on Linux
+- **Audio format:** MP3 output (not in scope for Phase 1, but module must not import go-lame yet — save for Phase 2/3 to keep Phase 1 CGo-free)
+- **Interaction model:** Non-interactive (run → Ctrl+C → summary printed to stderr)
+- **GSD workflow:** All file edits go through GSD commands, not direct edits outside a GSD workflow
+
+---
+
+## Standard Stack
+
+### Core (Phase 1 only)
+
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| `github.com/gopacket/gopacket` | v1.5.0 | Packet decode, layer type assertions | Community-maintained fork of google/gopacket; v1.5.0 Nov 2025; 14.5k dependents; built-in ICMP/TCP/UDP/DNS/TLS layers. Requires Go 1.24+. |
+| `github.com/packetcap/go-pcap` | v0.0.0-20251215 | Live packet capture (pure Go, no CGo) | Implements `gopacket.PacketDataSource`; pure Go on Linux+macOS; mmap kernel ring buffer; no `libpcap-dev` system package at build or runtime. Decision D-06 mandates this. |
+| `github.com/spf13/cobra` | v1.10.2 | CLI flags, subcommands, signal handling | Industry standard (Kubernetes, Docker, Hugo); handles `-i`, `--list-interfaces`, `--verbose`, `--help`, `PersistentPreRunE` for validation. |
+| Go stdlib `net` | stdlib | Interface enumeration for `--list-interfaces` | `net.Interfaces()` returns all system interfaces with name, flags, and addresses; requires no privileges; go-pcap has no FindAllDevs equivalent. |
+| Go stdlib `runtime` | stdlib | OS detection for privilege error messages | `runtime.GOOS` returns `"linux"` or `"darwin"` at runtime; used in D-05 platform-specific error branching. |
+| Go stdlib `os/signal` + `syscall` | stdlib | Ctrl+C handling, clean shutdown | Standard Go signal channel pattern; `signal.NotifyContext` is idiomatic in Go 1.16+. |
+
+### Not Needed for Phase 1
+
+| Library | Phase | Reason Excluded |
+|---------|-------|-----------------|
+| `github.com/sjzar/go-lame` | Phase 2/3 | MP3 encoding — no audio in Phase 1 |
+| `github.com/go-audio/wav` | Phase 2/3 | WAV intermediate — no audio in Phase 1 |
+| `github.com/muesli/kmeans` | Phase 3 | Auto-clustering — D-03 defers to Phase 3 |
+
+### Alternatives Considered
+
+| Standard Choice | Alternative | Why Standard Wins |
+|-----------------|-------------|-------------------|
+| `packetcap/go-pcap` | `gopacket/pcap` (CGo + libpcap) | libpcap requires system package; contradicts D-06; no CGo in capture layer |
+| `net.Interfaces()` | `gopacket/pcap.FindAllDevs()` | pcap.FindAllDevs requires CGo libpcap which we explicitly avoid; stdlib is sufficient |
+| `spf13/cobra` | `urfave/cli` | Cobra has better flag validation, persistent pre-run hooks, and structured signal integration |
+
+### Installation
+
+```bash
+# Go 1.24+ required — NOT available via apt on this machine (apt offers 1.22)
+# Install from https://go.dev/dl/ — e.g.:
+wget https://go.dev/dl/go1.24.1.linux-arm64.tar.gz
+sudo tar -C /usr/local -xzf go1.24.1.linux-arm64.tar.gz
+export PATH="$PATH:/usr/local/go/bin"
+
+# Initialize module (greenfield — no go.mod exists yet)
+cd /home/dev/workspace/yoloyolo
+go mod init github.com/yourorg/netsynth # or preferred module path
+
+# Phase 1 dependencies only (no CGo libraries yet)
+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
+
+# Build (no CGo needed for Phase 1)
+CGO_ENABLED=0 go build -o netsynth ./cmd/netsynth
+```
+
+---
+
+## Architecture Patterns
+
+### Recommended Project Structure
+
+```
+netsynth/
+├── cmd/
+│ └── netsynth/
+│ └── main.go # cobra root command wiring, signal handling
+├── capture/
+│ └── capture.go # OpenLive wrapper, interface listing, chan gopacket.Packet
+├── classify/
+│ ├── classifier.go # Rule table (map-based, D-02), Classify() func
+│ └── rules.go # Protocol rule definitions (ICMP, DNS, HTTPS, etc. per D-01)
+├── aggregate/
+│ └── window.go # Ticker-driven accumulator, WindowSnapshot type
+└── go.mod
+```
+
+This is a subset of the full ARCHITECTURE.md structure — synth/, encode/, config/ are added in later phases. Phase 1 lays down the first four packages.
+
+### Pattern 1: Channel-Connected Pipeline Stages
+
+Each component is a goroutine reading from an inbound channel and writing to an outbound channel. A `done` channel (closed on Ctrl+C) signals all stages to drain and exit.
+
+```go
+// Source: https://go.dev/blog/pipelines (official Go blog)
+func Classify(done <-chan struct{}, packets <-chan gopacket.Packet) <-chan ClassifiedPacket {
+ out := make(chan ClassifiedPacket, 256)
+ go func() {
+ defer close(out)
+ for {
+ select {
+ case <-done:
+ return
+ case pkt, ok := <-packets:
+ if !ok {
+ return
+ }
+ out <- classify(pkt)
+ }
+ }
+ }()
+ return out
+}
+```
+
+**Channel buffer sizes for Phase 1 (no audio clock pressure):**
+- capture → classify: `make(chan gopacket.Packet, 512)` — absorbs bursts
+- classify → aggregate: `make(chan ClassifiedPacket, 1024)` — aggregate is ticker-driven
+
+### Pattern 2: Ticker-Driven Window with Verbose Output
+
+```go
+// Source: ARCHITECTURE.md Pattern 2 + Go time.Ticker docs
+func Aggregate(done <-chan struct{}, events <-chan ClassifiedPacket,
+ windowMs int, verbose bool) <-chan WindowSnapshot {
+ out := make(chan WindowSnapshot, 8)
+ ticker := time.NewTicker(time.Duration(windowMs) * time.Millisecond)
+ go func() {
+ defer close(out)
+ counts := map[TrafficClass]int64{}
+ for {
+ select {
+ case <-done:
+ out <- snapshot(counts) // flush final partial window
+ return
+ case <-ticker.C:
+ snap := snapshot(counts)
+ if verbose {
+ printWindowLine(snap) // CLAS-04: --verbose output to stderr
+ }
+ out <- snap
+ counts = map[TrafficClass]int64{}
+ case ev, ok := <-events:
+ if !ok {
+ return
+ }
+ counts[ev.Class]++
+ }
+ }
+ }()
+ return out
+}
+```
+
+### Pattern 3: Config-Driven Classification Table (D-02)
+
+The classification rule set is a struct, not a switch statement:
+
+```go
+// classify/rules.go
+type Rule struct {
+ Protocol string // "tcp", "udp", "icmp", "any"
+ DstPort uint16 // 0 = match any port for this protocol
+ Class TrafficClass
+}
+
+// DefaultRules is the D-01 port map — can later be replaced by config loading
+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 come last:
+ {Protocol: "tcp", DstPort: 0, Class: ClassOtherTCP},
+ {Protocol: "udp", DstPort: 0, Class: ClassOtherUDP},
+}
+```
+
+The classifier iterates rules in order, returns first match. "unknown" class is returned when no rule matches (D-03).
+
+### Pattern 4: Interface Listing via Go Stdlib
+
+```go
+// capture/capture.go — uses net.Interfaces(), NOT pcap.FindAllDevs
+// Source: https://pkg.go.dev/net#Interfaces
+import "net"
+
+func ListInterfaces() ([]net.Interface, error) {
+ return net.Interfaces()
+}
+
+// Usage in CLI command handler:
+ifaces, err := capture.ListInterfaces()
+if err != nil {
+ fmt.Fprintln(os.Stderr, "Error listing interfaces:", err)
+ os.Exit(1)
+}
+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, ", "))
+}
+```
+
+This satisfies CAPT-02 with no privilege requirement — `net.Interfaces()` works as a non-root user.
+
+### Pattern 5: Privilege Error Detection (D-05)
+
+```go
+// capture/capture.go
+import (
+ "fmt"
+ "os"
+ "runtime"
+ "strings"
+
+ pcap "github.com/packetcap/go-pcap"
+)
+
+func OpenCapture(iface string) (*pcap.Handle, error) {
+ handle, err := pcap.OpenLive(context.Background(), 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
+}
+
+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") // some platforms wrap differently
+}
+
+func permissionErrorMsg(iface string) error {
+ switch runtime.GOOS {
+ case "linux":
+ bin, _ := os.Executable()
+ 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 netsynth -i %s", iface)
+ }
+}
+```
+
+### Pattern 6: gopacket Packet Decoding for Classification
+
+```go
+// Source: https://pkg.go.dev/github.com/gopacket/gopacket
+import (
+ "github.com/gopacket/gopacket"
+ "github.com/gopacket/gopacket/layers"
+ pcap "github.com/packetcap/go-pcap"
+)
+
+handle, _ := pcap.OpenLive(ctx, iface, 65535, false, 0, false)
+packetSource := gopacket.NewPacketSource(handle, layers.LinkTypeEthernet)
+// NoCopy is safe here because go-pcap copies data internally
+packetSource.NoCopy = true
+
+for pkt := range packetSource.Packets() {
+ // ICMP check
+ if pkt.Layer(layers.LayerTypeICMPv4) != nil ||
+ pkt.Layer(layers.LayerTypeICMPv6) != nil {
+ // ClassICMP
+ }
+ // TCP destination port
+ if tcp, ok := pkt.TransportLayer().(*layers.TCP); ok {
+ dstPort := uint16(tcp.DstPort)
+ // match against rule table
+ }
+ // UDP destination port
+ if udp, ok := pkt.TransportLayer().(*layers.UDP); ok {
+ dstPort := uint16(udp.DstPort)
+ // match against rule table
+ }
+}
+```
+
+Note: `packetSource.NoCopy = true` is safe because `packetcap/go-pcap` copies packet data internally before returning from `ReadPacketData()`. Verified from go-pcap source structure.
+
+### Anti-Patterns to Avoid
+
+- **Switch-statement classification:** Forbidden by D-02. Use the `[]Rule` slice instead.
+- **Blocking channel send in capture path:** Capture goroutine must never block on a full channel; use buffered channel + drop counter.
+- **`ZeroCopyReadPacketData()` sent to goroutines:** Do not use with concurrent processing; stick with `ReadPacketData()` or let gopacket's `PacketSource` manage it.
+- **`google/gopacket` import:** Must be `github.com/gopacket/gopacket` — enforced from day one in `go.mod`.
+- **Privilege check by inspecting `/proc/self/status`:** Fragile. Instead try `OpenLive()` and intercept the error — the only reliable way to know if capture actually works.
+
+---
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| CLI flag parsing | Custom arg parser | `spf13/cobra` | Signal handling, auto-help, persistent flags, PersistentPreRunE validation |
+| Packet decoding (Ethernet/IP/TCP/UDP/ICMP headers) | Manual `binary.Read` offsets | `gopacket/layers` | Layer type assertions; handles IPv4/IPv6, fragmentation, Ethernet padding |
+| Interface enumeration | Parse `/proc/net/dev` or run `ip link` | `net.Interfaces()` | Stdlib; cross-platform; returns flags, addresses; no privilege needed |
+| Ctrl+C / signal handling | `os.Signal` channel with manual `os.Exit` | `signal.NotifyContext` | Propagates context cancellation cleanly; idiomatic Go 1.16+ pattern |
+| Live packet capture | Raw AF_PACKET socket from scratch | `packetcap/go-pcap` | Kernel ring buffer, BPF filter support, `gopacket.PacketDataSource` compliance |
+
+**Key insight:** The hardest part of packet capture in Go is correctly handling the kernel ring buffer, BPF attachment, and copy semantics. `packetcap/go-pcap` solves all three in pure Go; there is no reason to touch raw sockets in this phase.
+
+---
+
+## Common Pitfalls
+
+### Pitfall 1: `go-pcap` Has No Interface Discovery Function
+
+**What goes wrong:** Developer calls `pcap.FindAllDevs()` expecting libpcap behavior — that function does not exist in `packetcap/go-pcap`. Build fails.
+
+**Why it happens:** The STACK.md mentions `pcap.FindAllDevs()` in the context of the gopacket/pcap (CGo) backend, not go-pcap. Easy to conflate.
+
+**How to avoid:** Use `net.Interfaces()` from Go stdlib for `--list-interfaces`. It returns equivalent information (name, flags, addresses) without any privilege requirement.
+
+**Warning signs:** Any import of `gopacket/pcap` instead of `packetcap/go-pcap`.
+
+---
+
+### Pitfall 2: Go 1.24 Not Installed
+
+**What goes wrong:** `go get github.com/gopacket/gopacket@v1.5.0` fails with "minimum required go 1.24" or the apt-installed `golang-go` (version 1.22) is used and build fails.
+
+**Why it happens:** Ubuntu 24.04 ships `golang-go` v1.22 via apt. `gopacket/gopacket@v1.5.0` requires Go 1.24+. The version mismatch is silent until the first `go get`.
+
+**How to avoid:** Wave 0 task: install Go 1.24+ from https://go.dev/dl/. Do not use `apt install golang-go` for this project. Verify with `go version` before any other task.
+
+**Warning signs:** `go version` output showing `go1.22.x`.
+
+---
+
+### Pitfall 3: CAP_NET_RAW Silent Failure on nosuid Filesystems
+
+**What goes wrong:** `sudo setcap cap_net_raw+ep ./netsynth` appears to succeed but the binary fails at runtime when run from `/home/dev/workspace/` because the filesystem is mounted `nosuid`. The capability is silently ignored.
+
+**Why it happens:** `/home` is frequently on a separate partition mounted `nosuid`. The kernel enforces capabilities only on filesystems where `nosuid` is not set.
+
+**How to avoid:** For development, run as `sudo ./netsynth -i eth0` rather than relying on `setcap`. Document that setcap only works from `/usr/local/bin` or equivalent. The error message from D-05 covers this — show both options so the user knows sudo always works.
+
+**Warning signs:** Binary works via `sudo` but fails via `setcap` from the workspace directory.
+
+---
+
+### Pitfall 4: Blocking Channel Send Stalls Kernel Packet Buffer
+
+**What goes wrong:** If the classify goroutine is slow (or tests inject sleep), the capture goroutine's send to the `packets` channel blocks, and the kernel's pcap ring buffer fills and drops packets without any log message.
+
+**Why it happens:** Unbuffered or small-buffered channel between capture and classify.
+
+**How to avoid:** Use `make(chan gopacket.Packet, 512)`. In the capture loop, use a non-blocking send with a drop counter:
+```go
+select {
+case packets <- pkt:
+default:
+ atomic.AddInt64(&droppedPackets, 1)
+}
+```
+For Phase 1's use case (ambient audio, statistical fingerprint), lossy capture is acceptable; just make drops visible.
+
+---
+
+### Pitfall 5: `--list-interfaces` Requires Privilege on Some Systems
+
+**What goes wrong:** On some Linux configurations, `net.Interfaces()` returns an empty list when run as non-root, causing `--list-interfaces` to print nothing.
+
+**Why it happens:** Network namespace restrictions or security policies can limit interface visibility. However, this is rare on standard Ubuntu 24.04 with default configuration.
+
+**How to avoid:** Always print the list plus a note: "If interfaces are missing, run with sudo." Confirmed LOW risk on the target machine (standard Ubuntu 24.04, no restricted namespaces observed).
+
+---
+
+### Pitfall 6: LinkType Mismatch on Non-Ethernet Interfaces
+
+**What goes wrong:** `gopacket.NewPacketSource(handle, layers.LinkTypeEthernet)` is correct for `eth0` but fails to decode on loopback (`lo`) which uses `layers.LinkTypeLoopback` (or `layers.LinkTypeNull` on macOS). Packets decode as garbage or produce layer decode errors silently.
+
+**Why it happens:** Link type is hardcoded in the `NewPacketSource` call.
+
+**How to avoid:** Call `handle.LinkType()` on the opened handle and pass the result. `packetcap/go-pcap` exports `LinkTypeEthernet` and `LinkTypeNull` constants. For `lo`, use `layers.LinkTypeLoopback`.
+```go
+lt := layers.LinkType(handle.LinkType())
+packetSource := gopacket.NewPacketSource(handle, lt)
+```
+
+---
+
+## Code Examples
+
+Verified patterns from official sources:
+
+### Opening a Live Capture Handle
+
+```go
+// Source: https://pkg.go.dev/github.com/packetcap/go-pcap
+import (
+ "context"
+ pcap "github.com/packetcap/go-pcap"
+)
+
+handle, err := pcap.OpenLive(
+ context.Background(),
+ "eth0", // interface name
+ 65535, // snaplen: capture full packets
+ false, // promiscuous: false by default per security guidance in PITFALLS.md
+ 0, // timeout: 0 = block until packet
+ false, // syscalls: false = use mmap ring buffer (faster on Linux)
+)
+if err != nil {
+ // Check for permission error and print platform-specific hint (Pattern 5)
+}
+defer handle.Close()
+```
+
+### Creating a gopacket PacketSource
+
+```go
+// Source: https://pkg.go.dev/github.com/gopacket/gopacket
+import (
+ "github.com/gopacket/gopacket"
+ "github.com/gopacket/gopacket/layers"
+)
+
+lt := layers.LinkType(handle.LinkType()) // dynamic link type per Pitfall 6
+packetSource := gopacket.NewPacketSource(handle, lt)
+packetSource.NoCopy = true // safe: go-pcap copies internally
+
+for pkt := range packetSource.Packets() {
+ // pkt is a gopacket.Packet with full layer decoding
+}
+```
+
+### Listing Interfaces (--list-interfaces)
+
+```go
+// Source: https://pkg.go.dev/net#Interfaces
+import "net"
+
+ifaces, err := net.Interfaces()
+if err != nil {
+ return err
+}
+for _, iface := range ifaces {
+ addrs, _ := iface.Addrs()
+ // print name, flags, addresses to stderr (tcpdump convention)
+}
+```
+
+### Cobra Root Command Structure
+
+```go
+// Source: https://pkg.go.dev/github.com/spf13/cobra
+var rootCmd = &cobra.Command{
+ Use: "netsynth",
+ Short: "Sonify live network traffic",
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+ // validate flags before Run
+ return nil
+ },
+ RunE: func(cmd *cobra.Command, args []string) error {
+ // main capture loop
+ return nil
+ },
+}
+
+func init() {
+ rootCmd.Flags().StringVarP(&iface, "interface", "i", "", "Network interface to capture on")
+ rootCmd.Flags().BoolVar(&listIfaces, "list-interfaces", false, "List available interfaces")
+ rootCmd.Flags().BoolVar(&verbose, "verbose", false, "Print per-window protocol activity")
+}
+```
+
+### Signal Handling (clean shutdown)
+
+```go
+// Source: https://pkg.go.dev/os/signal#NotifyContext (Go 1.16+)
+import (
+ "context"
+ "os"
+ "os/signal"
+ "syscall"
+)
+
+ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
+defer stop()
+
+// Pass ctx.Done() as the done channel to all pipeline stages
+// When Ctrl+C is pressed, ctx is cancelled and all stages drain cleanly
+```
+
+### Per-Protocol Summary on Exit (CLAS-03)
+
+```go
+// After pipeline drains, print summary to stderr (tcpdump convention)
+fmt.Fprintln(os.Stderr, "\n--- Protocol Summary ---")
+// Sort classes for stable output
+for _, class := range sortedClasses(totals) {
+ pct := 0.0
+ if totalPackets > 0 {
+ pct = float64(totals[class]) / float64(totalPackets) * 100.0
+ }
+ fmt.Fprintf(os.Stderr, " %-15s %8d packets (%5.1f%%)\n",
+ class.String(), totals[class], pct)
+}
+fmt.Fprintf(os.Stderr, " %-15s %8d packets\n", "TOTAL", totalPackets)
+```
+
+---
+
+## State of the Art
+
+| Old Approach | Current Approach | When Changed | Impact |
+|--------------|------------------|--------------|--------|
+| `google/gopacket` | `github.com/gopacket/gopacket` v1.5.0 | Community fork active since ~2023, v1.5.0 Nov 2025 | Must use community fork; original is abandoned |
+| `gopacket/pcap` (CGo libpcap) for live capture | `packetcap/go-pcap` (pure Go) | Dec 2025 (v0.0.0-20251215) | No CGo at capture layer; CGO_ENABLED=0 for Phase 1 build |
+| `signal.Notify` (manual channel) | `signal.NotifyContext` | Go 1.16 | Cleaner context propagation; use NotifyContext everywhere |
+| Flat `main.go` | Cobra command structure | Stable pattern | Subcommand extensibility for future phases |
+
+**Deprecated/outdated:**
+- `google/gopacket`: abandoned 2022; 270+ open issues; do not use
+- `go-audio/generator`: archived February 2026; do not take new dependency
+
+---
+
+## Open Questions
+
+1. **go-pcap link type for WiFi / Docker bridge interfaces**
+ - What we know: `handle.LinkType()` returns uint32; go-pcap exports only `LinkTypeEthernet` and `LinkTypeNull`
+ - What's unclear: Does it return the correct type for Docker bridge (`br-*`) and veth interfaces on this machine?
+ - Recommendation: In the capture open sequence, fall back to `layers.LinkTypeEthernet` if `handle.LinkType()` returns an unrecognized value, and log a warning. The target demo interface is `eth0` which is Ethernet.
+
+2. **Default interface when `-i` is omitted**
+ - What we know: Left to Claude's discretion (CONTEXT.md)
+ - What's unclear: Should it pick the first non-loopback interface, or require `-i` always?
+ - Recommendation: Require `-i` explicitly (no silent default) unless `--list-interfaces` was given. This matches `tcpdump` behavior and avoids surprising captures on wrong interfaces.
+
+3. **`promiscuous` mode default**
+ - What we know: PITFALLS.md recommends non-promiscuous by default with opt-in `--promiscuous` flag
+ - What's unclear: Phase 1 requirements don't mention a `--promiscuous` flag (it's not in CAPT-01..CAPT-04)
+ - Recommendation: Default to non-promiscuous; add `--promiscuous` flag as a bonus if it falls within scope; if not, document the limitation in help text.
+
+---
+
+## Environment Availability
+
+| Dependency | Required By | Available | Version | Fallback |
+|------------|-------------|-----------|---------|---------|
+| Go toolchain 1.24+ | All Go compilation | **No** | 1.22 in apt (too old) | Install from https://go.dev/dl/ — manual step required |
+| GCC / build-essential | Phase 2+ CGo (go-lame) | **No** | Not installed | `sudo apt install build-essential` — not needed for Phase 1 (CGO_ENABLED=0) |
+| libpcap runtime (`libpcap0.8t64`) | go-pcap on Linux at runtime | Yes | 1.10.4 (installed) | n/a — but go-pcap is pure Go so this is irrelevant |
+| libpcap-dev headers | CGo pcap builds | No | Not installed | Not needed — go-pcap is pure Go |
+| `tcpdump` | Manual validation of captures | Yes | `/usr/bin/tcpdump` | — |
+| `eth0` interface | CAPT-01 demo interface | Yes | Present on machine | `lo` for loopback-only test |
+| `CAP_NET_RAW` or root | Live packet capture | Not verified | Run as uid=1001 (non-root) | `sudo netsynth -i eth0` always works |
+
+**Missing dependencies with no fallback:**
+- **Go 1.24+**: Must be installed before any work begins. Wave 0 task. `apt install golang-go` gives 1.22 which is insufficient.
+
+**Missing dependencies with fallback:**
+- **GCC**: Not needed for Phase 1 (no CGo libraries). Needed in Phase 2/3 for go-lame. Install via `sudo apt install build-essential` at that time.
+- **Root / CAP_NET_RAW**: Tests that need live capture must run via `sudo`. Unit tests for classifier and aggregator use synthetic packet data and require no privilege.
+
+**Platform note:** This machine is `linux/arm64` (Ubuntu 24.04). Go 1.24 ARM64 binaries are available at https://go.dev/dl/. The `packetcap/go-pcap` library supports Linux on ARM64 (uses standard Linux kernel ring buffer syscalls).
+
+---
+
+## Validation Architecture
+
+### Test Framework
+
+| Property | Value |
+|----------|-------|
+| Framework | Go stdlib `testing` package (no external framework needed) |
+| Config file | None — `go test ./...` discovers tests automatically |
+| Quick run command | `go test ./classify/... ./aggregate/...` (no privileges needed) |
+| Full suite command | `go test ./...` (capture tests require sudo or are integration-only) |
+
+### Phase Requirements → Test Map
+
+| Req ID | Behavior | Test Type | Automated Command | File Exists? |
+|--------|----------|-----------|-------------------|-------------|
+| CAPT-01 | `-i eth0` opens capture handle without error | Integration (needs sudo) | `sudo go test ./capture/... -run TestOpenLive -v` | No — Wave 0 |
+| CAPT-02 | `--list-interfaces` prints at least one interface | Unit (no privilege) | `go test ./capture/... -run TestListInterfaces -v` | No — Wave 0 |
+| CAPT-04 | Non-root capture returns actionable error message | Unit (mock OpenLive error) | `go test ./capture/... -run TestPermissionError -v` | No — Wave 0 |
+| CLAS-01 | Each of 10+ protocol classes is assigned correctly for synthetic packets | Unit | `go test ./classify/... -run TestClassify -v` | No — Wave 0 |
+| CLAS-03 | Exit summary prints correct counts per class | Unit | `go test ./aggregate/... -run TestSummary -v` | No — Wave 0 |
+| CLAS-04 | `--verbose` prints one line per window with protocol breakdown | Unit | `go test ./aggregate/... -run TestVerbose -v` | No — Wave 0 |
+
+### Sampling Rate
+
+- **Per task commit:** `go test ./classify/... ./aggregate/...` (fast, no privilege)
+- **Per wave merge:** `go test ./...` (may skip integration tests if not running as root)
+- **Phase gate:** All unit tests pass; manual smoke test `sudo ./netsynth -i eth0` shows live classification output to stderr
+
+### Wave 0 Gaps
+
+- [ ] `classify/classifier_test.go` — covers CLAS-01: unit tests with synthetic gopacket packets for each of 10 protocol classes
+- [ ] `capture/capture_test.go` — covers CAPT-02 (TestListInterfaces, no privilege) and CAPT-04 (TestPermissionError with injected error)
+- [ ] `aggregate/window_test.go` — covers CLAS-03 (summary counts) and CLAS-04 (verbose window lines)
+- [ ] `go.mod` and `go.sum` — module not yet initialized; must be created before any test file compiles
+- [ ] Go 1.24 toolchain install — prerequisite for everything above
+
+---
+
+## Sources
+
+### Primary (HIGH confidence)
+
+- `https://pkg.go.dev/github.com/packetcap/go-pcap` — Full API surface verified; confirmed no FindAllDevs; `OpenLive` signature; `Handle.LinkType()` method
+- `https://pkg.go.dev/github.com/gopacket/gopacket` — v1.5.0 confirmed Nov 2025; `PacketDataSource` interface; `NewPacketSource` API; layer types
+- `https://pkg.go.dev/net` — `net.Interfaces()` stdlib function; returns `[]net.Interface` with name, flags, addrs; no privilege needed
+- `https://pkg.go.dev/github.com/spf13/cobra` — v1.10.2 Dec 2025; `PersistentPreRunE`, flag patterns
+- `https://go.dev/blog/pipelines` — Official Go pipeline pattern with `done` channel
+- `.planning/research/STACK.md` — Verified library versions, CGo strategy, why google/gopacket is forbidden
+- `.planning/research/ARCHITECTURE.md` — Channel buffer sizes, pipeline stage signatures, `WindowSnapshot` type definition
+- `.planning/research/PITFALLS.md` — CAP_NET_RAW nosuid behavior, ZeroCopy use-after-free, packet buffer overflow strategies
+
+### Secondary (MEDIUM confidence)
+
+- WebSearch + pkg.go.dev cross-check: `packetcap/go-pcap` v0.0.0-20251215 implements `gopacket.PacketDataSource`, Linux/macOS only — confirmed
+- `.planning/phases/01-capture-and-classification/01-CONTEXT.md` — Locked decisions D-01 through D-06
+
+### Tertiary (LOW confidence)
+
+- go-pcap `NoCopy = true` safety: described in fetch results as "handle copies data internally" — plausible given pure-Go implementation but not confirmed via source inspection; treat as verify-on-test
+
+---
+
+## Metadata
+
+**Confidence breakdown:**
+- Standard stack: HIGH — versions verified via pkg.go.dev fetches
+- Architecture: HIGH — Go pipeline patterns from official blog; gopacket API from official docs
+- Pitfalls: HIGH for CAP_NET_RAW and channel buffering (verified via PITFALLS.md primary sources); MEDIUM for go-pcap NoCopy safety
+- Environment: HIGH — direct inspection of installed packages and Go availability on target machine
+
+**Research date:** 2026-03-25
+**Valid until:** 2026-06-25 (90 days — stack is stable; go-pcap pre-release version may update)