1	# Birdseye Known-Modules Autocomplete Cache Implementation Plan
2
3	> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
5	**Goal:** Persist user-confirmed module addresses (`namespace/name/provider`) in a plain-text file at `os.UserConfigDir()/birdseye/known-modules`, and offer them as suggestions through cobra shell completion (CLI) and `bubbles/textinput` ghost-text (TUI picker).
6
7	**Architecture:** A new `internal/known` package handles file I/O (Path/Load/Add) with atomic temp+rename writes. The `cmd` layer wires shell completion via cobra's `ValidArgsFunction` and calls `Add` after successful diffs. The `tui/picker` accepts pre-loaded suggestions and dispatches a fire-and-forget `Add` after `ListVersions` succeeds.
8
9	**Tech Stack:** Go 1.25.5, stdlib (`os`, `path/filepath`, `bufio`, `sort`, `strings`), cobra `ValidArgsFunction`, `bubbles/textinput.SetSuggestions`. Reuses `api.ValidateSegments` from `services/crow/pkg/api`. No new third-party deps.
10
11	**Spec:** `docs/superpowers/specs/2026-05-13-birdseye-known-modules-design.md`
12
13	**Conventions:**
14	- TDD: red → green → commit. Run `go test -race ./...` before every commit.
15	- Absolute paths in shell commands; never `git add .` or `git add -A`.
16	- Conventional commits: `feat(birdseye): ...`, `test(birdseye): ...`.
17
18	---
19
20	## Task 1: `internal/known` — Path, Load, Add (happy paths)
21
22	**Files:**
23	- Create: `apps/birdseye/internal/known/known.go`
24	- Create: `apps/birdseye/internal/known/known_test.go`
25
26	- [ ] **Step 1: Write failing test** at `apps/birdseye/internal/known/known_test.go`:
27
28	```go
29	package known
30
31	import (
32		"os"
33		"path/filepath"
34		"testing"
35	)
36
37	func TestLoadMissingFileReturnsEmpty(t *testing.T) {
38		dir := t.TempDir()
39		got, err := Load(filepath.Join(dir, "missing"))
40		if err != nil {
41			t.Fatalf("Load: %v", err)
42		}
43		if len(got) != 0 {
44			t.Errorf("expected empty slice, got %v", got)
45		}
46	}
47
48	func TestAddCreatesDirectoryAndFile(t *testing.T) {
49		dir := t.TempDir()
50		path := filepath.Join(dir, "subdir", "known-modules")
51		if err := Add(path, "iac/comply/aws"); err != nil {
52			t.Fatalf("Add: %v", err)
53		}
54		body, err := os.ReadFile(path)
55		if err != nil {
56			t.Fatalf("ReadFile: %v", err)
57		}
58		if string(body) != "iac/comply/aws\n" {
59			t.Errorf("body = %q", body)
60		}
61	}
62
63	func TestAddThenLoadRoundTrip(t *testing.T) {
64		dir := t.TempDir()
65		path := filepath.Join(dir, "known-modules")
66		if err := Add(path, "iac/comply/aws"); err != nil {
67			t.Fatalf("Add: %v", err)
68		}
69		got, err := Load(path)
70		if err != nil {
71			t.Fatalf("Load: %v", err)
72		}
73		if len(got) != 1 || got[0] != "iac/comply/aws" {
74			t.Errorf("Load = %v, want [iac/comply/aws]", got)
75		}
76	}
77
78	func TestPathHonorsUserConfigDir(t *testing.T) {
79		got, err := Path()
80		if err != nil {
81			t.Fatalf("Path: %v", err)
82		}
83		if !filepath.IsAbs(got) {
84			t.Errorf("expected absolute path, got %q", got)
85		}
86		base := filepath.Base(got)
87		if base != "known-modules" {
88			t.Errorf("expected basename %q, got %q", "known-modules", base)
89		}
90		parent := filepath.Base(filepath.Dir(got))
91		if parent != "birdseye" {
92			t.Errorf("expected parent dir %q, got %q", "birdseye", parent)
93		}
94	}
95	```
96
97	- [ ] **Step 2: Run, expect FAIL** (`undefined: Load`, etc.)
98
99	Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test ./internal/known/...`
100	Expected: compile error.
101
102	- [ ] **Step 3: Implement `apps/birdseye/internal/known/known.go`**
103
104	```go
105	// Package known persists a sorted, deduplicated list of Crow module
106	// addresses the user has successfully diff'd or browsed, so future
107	// invocations can offer shell-completion and TUI suggestions.
108	//
109	// The on-disk format is plain text, one ns/name/provider per line,
110	// alphabetically sorted, with a trailing newline. Empty or missing
111	// file means "no suggestions yet" — not an error.
112	//
113	// Concurrent writers: last writer wins. Two birdseye processes running
114	// simultaneously may each see the file before the other's Rename
115	// lands; whichever Rename completes last sets the final contents.
116	// Acceptable for a low-frequency, append-only-ish list.
117	package known
118
119	import (
120		"bufio"
121		"fmt"
122		"os"
123		"path/filepath"
124		"sort"
125		"strings"
126
127		"github.com/immuta/operations/services/crow/pkg/api"
128	)
129
130	// Path returns the default known-modules file path, honoring
131	// os.UserConfigDir() (XDG_CONFIG_HOME on Linux, Application Support on
132	// macOS). The directory is NOT created here — Add creates it lazily on
133	// first write.
134	func Path() (string, error) {
135		cfg, err := os.UserConfigDir()
136		if err != nil {
137			return "", fmt.Errorf("resolve user config dir: %w", err)
138		}
139		return filepath.Join(cfg, "birdseye", "known-modules"), nil
140	}
141
142	// Load reads the file at path. Returns an empty slice if the file
143	// does not exist (first run is normal). Lines failing the
144	// ns/name/provider validation are silently skipped. The result is
145	// sorted ascending.
146	func Load(path string) ([]string, error) {
147		f, err := os.Open(path)
148		if err != nil {
149			if os.IsNotExist(err) {
150				return nil, nil
151			}
152			return nil, fmt.Errorf("open known-modules: %w", err)
153		}
154		defer f.Close()
155
156		var out []string
157		scanner := bufio.NewScanner(f)
158		for scanner.Scan() {
159			line := strings.TrimSpace(scanner.Text())
160			if line == "" {
161				continue
162			}
163			if !validAddress(line) {
164				continue
165			}
166			out = append(out, line)
167		}
168		if err := scanner.Err(); err != nil {
169			return nil, fmt.Errorf("scan known-modules: %w", err)
170		}
171		sort.Strings(out)
172		return out, nil
173	}
174
175	// Add inserts address into the file at path. Existing entries are
176	// preserved; duplicates deduplicated; the final list is sorted.
177	// Writes are atomic via temp file + rename in the same directory.
178	// The directory is created with 0o755 if missing. If address is not a
179	// well-formed ns/name/provider triple, Add returns an error without
180	// touching the file.
181	func Add(path, address string) error {
182		if !validAddress(address) {
183			return fmt.Errorf("invalid module address %q", address)
184		}
185		if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
186			return fmt.Errorf("mkdir known-modules dir: %w", err)
187		}
188
189		existing, err := Load(path)
190		if err != nil {
191			return err
192		}
193		merged := mergeAndSort(existing, address)
194
195		tmp, err := os.CreateTemp(filepath.Dir(path), ".known-modules-*.tmp")
196		if err != nil {
197			return fmt.Errorf("create temp: %w", err)
198		}
199		tmpName := tmp.Name()
200		w := bufio.NewWriter(tmp)
201		for _, addr := range merged {
202			if _, err := w.WriteString(addr); err != nil {
203				_ = tmp.Close()
204				_ = os.Remove(tmpName)
205				return fmt.Errorf("write temp: %w", err)
206			}
207			if err := w.WriteByte('\n'); err != nil {
208				_ = tmp.Close()
209				_ = os.Remove(tmpName)
210				return fmt.Errorf("write temp: %w", err)
211			}
212		}
213		if err := w.Flush(); err != nil {
214			_ = tmp.Close()
215			_ = os.Remove(tmpName)
216			return fmt.Errorf("flush temp: %w", err)
217		}
218		if err := tmp.Close(); err != nil {
219			_ = os.Remove(tmpName)
220			return fmt.Errorf("close temp: %w", err)
221		}
222		if err := os.Rename(tmpName, path); err != nil {
223			_ = os.Remove(tmpName)
224			return fmt.Errorf("rename temp: %w", err)
225		}
226		return nil
227	}
228
229	// mergeAndSort returns a deduplicated, sorted slice combining existing
230	// with the new address.
231	func mergeAndSort(existing []string, address string) []string {
232		seen := make(map[string]struct{}, len(existing)+1)
233		out := make([]string, 0, len(existing)+1)
234		for _, a := range existing {
235			if _, dup := seen[a]; dup {
236				continue
237			}
238			seen[a] = struct{}{}
239			out = append(out, a)
240		}
241		if _, dup := seen[address]; !dup {
242			out = append(out, address)
243		}
244		sort.Strings(out)
245		return out
246	}
247
248	// validAddress checks that s is exactly three slash-separated segments
249	// each passing api.ValidateSegments.
250	func validAddress(s string) bool {
251		parts := strings.Split(s, "/")
252		if len(parts) != 3 {
253			return false
254		}
255		return api.ValidateSegments(parts) == nil
256	}
257	```
258
259	- [ ] **Step 4: Run, expect PASS**
260
261	Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race ./internal/known/... -v`
262	Expected: All 4 tests PASS.
263
264	- [ ] **Step 5: Commit**
265
266	```bash
267	cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye && git add apps/birdseye/internal/known/ && git commit -m "feat(birdseye): known-modules autocomplete cache with Path/Load/Add"
268	```
269
270	---
271
272	## Task 2: `internal/known` — hardening tests
273
274	**Files:**
275	- Modify: `apps/birdseye/internal/known/known_test.go`
276
277	- [ ] **Step 1: Append failing tests** to `apps/birdseye/internal/known/known_test.go`:
278
279	```go
280	import (
281		"strings"
282		// existing imports preserved
283	)
284
285	func TestAddDeduplicates(t *testing.T) {
286		dir := t.TempDir()
287		path := filepath.Join(dir, "known-modules")
288		for i := 0; i < 3; i++ {
289			if err := Add(path, "iac/comply/aws"); err != nil {
290				t.Fatalf("Add iteration %d: %v", i, err)
291			}
292		}
293		body, err := os.ReadFile(path)
294		if err != nil {
295			t.Fatal(err)
296		}
297		lines := strings.Split(strings.TrimRight(string(body), "\n"), "\n")
298		if len(lines) != 1 {
299			t.Errorf("expected 1 unique line, got %d: %v", len(lines), lines)
300		}
301	}
302
303	func TestAddSortsAlphabetically(t *testing.T) {
304		dir := t.TempDir()
305		path := filepath.Join(dir, "known-modules")
306		for _, addr := range []string{"z/y/x", "a/b/c", "m/n/o"} {
307			if err := Add(path, addr); err != nil {
308				t.Fatalf("Add %s: %v", addr, err)
309			}
310		}
311		got, err := Load(path)
312		if err != nil {
313			t.Fatal(err)
314		}
315		want := []string{"a/b/c", "m/n/o", "z/y/x"}
316		if len(got) != len(want) {
317			t.Fatalf("got %d lines, want %d", len(got), len(want))
318		}
319		for i := range got {
320			if got[i] != want[i] {
321				t.Errorf("got[%d] = %s, want %s", i, got[i], want[i])
322			}
323		}
324	}
325
326	func TestAddAtomicNoLingeringTemp(t *testing.T) {
327		dir := t.TempDir()
328		path := filepath.Join(dir, "known-modules")
329		if err := Add(path, "iac/comply/aws"); err != nil {
330			t.Fatalf("Add: %v", err)
331		}
332		entries, err := os.ReadDir(dir)
333		if err != nil {
334			t.Fatal(err)
335		}
336		for _, e := range entries {
337			if strings.HasPrefix(e.Name(), ".known-modules-") && strings.HasSuffix(e.Name(), ".tmp") {
338				t.Errorf("found lingering temp file: %s", e.Name())
339			}
340		}
341	}
342
343	func TestAddRejectsBadAddress(t *testing.T) {
344		dir := t.TempDir()
345		path := filepath.Join(dir, "known-modules")
346		cases := []string{
347			"not-three-parts",
348			"two/parts",
349			"four/parts/here/extra",
350			"has spaces/in/it",
351			"",
352		}
353		for _, addr := range cases {
354			t.Run(addr, func(t *testing.T) {
355				if err := Add(path, addr); err == nil {
356					t.Errorf("expected error for %q, got nil", addr)
357				}
358			})
359		}
360		// File must not have been created.
361		if _, err := os.Stat(path); !os.IsNotExist(err) {
362			t.Errorf("expected file to NOT exist, got err=%v", err)
363		}
364	}
365
366	func TestLoadSkipsMalformedLines(t *testing.T) {
367		dir := t.TempDir()
368		path := filepath.Join(dir, "known-modules")
369		content := "iac/comply/aws\nnot-valid\ntwo/parts\nplatform/networking/aws\n"
370		if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
371			t.Fatal(err)
372		}
373		got, err := Load(path)
374		if err != nil {
375			t.Fatalf("Load: %v", err)
376		}
377		want := []string{"iac/comply/aws", "platform/networking/aws"}
378		if len(got) != len(want) {
379			t.Fatalf("got %v, want %v", got, want)
380		}
381		for i := range got {
382			if got[i] != want[i] {
383				t.Errorf("got[%d] = %s, want %s", i, got[i], want[i])
384			}
385		}
386	}
387
388	func TestLoadSkipsBlankLines(t *testing.T) {
389		dir := t.TempDir()
390		path := filepath.Join(dir, "known-modules")
391		content := "\niac/comply/aws\n\n\nplatform/networking/aws\n"
392		if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
393			t.Fatal(err)
394		}
395		got, err := Load(path)
396		if err != nil {
397			t.Fatalf("Load: %v", err)
398		}
399		if len(got) != 2 {
400			t.Errorf("got %d lines, want 2: %v", len(got), got)
401		}
402	}
403	```
404
405	- [ ] **Step 2: Run, expect PASS**
406
407	Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race ./internal/known/... -v`
408	Expected: All 10 tests PASS (4 from Task 1 + 6 new).
409
410	- [ ] **Step 3: Commit**
411
412	```bash
413	cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye && git add apps/birdseye/internal/known/known_test.go && git commit -m "test(birdseye): harden known-modules cache (dedup, sort, atomic, validation)"
414	```
415
416	---
417
418	## Task 3: `cmd/diff` — `--known-modules-file` flag + post-success Add + tests
419
420	**Files:**
421	- Modify: `apps/birdseye/cmd/diff.go`
422	- Modify: `apps/birdseye/cmd/diff_test.go`
423
424	- [ ] **Step 1: Write failing tests** — append to `apps/birdseye/cmd/diff_test.go`:
425
426	```go
427	import (
428		// existing imports preserved
429		"os"
430		"path/filepath"
431	)
432
433	func TestDiffAddsToKnownAfterSuccess(t *testing.T) {
434		srv := fakeCrowTwoVersions(t)
435		knownFile := filepath.Join(t.TempDir(), "known-modules")
436		root := NewRootCmd()
437		var stdout bytes.Buffer
438		root.SetOut(&stdout)
439		root.SetArgs([]string{
440			"diff",
441			"iac/comply/aws", "1.0.0", "1.1.0",
442			"--crow-url", srv.URL,
443			"--plain",
444			"--cache-dir", t.TempDir(),
445			"--known-modules-file", knownFile,
446		})
447		if err := root.ExecuteContext(context.Background()); err != nil {
448			t.Fatalf("execute: %v", err)
449		}
450		body, err := os.ReadFile(knownFile)
451		if err != nil {
452			t.Fatalf("known-modules file not created: %v", err)
453		}
454		if !strings.Contains(string(body), "iac/comply/aws") {
455			t.Errorf("known-modules missing address:\n%s", body)
456		}
457	}
458
459	func TestDiffDoesNotAddOnFailure(t *testing.T) {
460		// fakeCrow with only one of the two requested versions
461		srv := fakeCrow(t, map[string][]byte{"1.0.0": []byte("body-100")})
462		knownFile := filepath.Join(t.TempDir(), "known-modules")
463		root := NewRootCmd()
464		root.SetErr(&bytes.Buffer{})
465		root.SetArgs([]string{
466			"diff",
467			"iac/comply/aws", "1.0.0", "9.9.9",
468			"--crow-url", srv.URL,
469			"--plain",
470			"--cache-dir", t.TempDir(),
471			"--known-modules-file", knownFile,
472		})
473		err := root.ExecuteContext(context.Background())
474		if err == nil {
475			t.Fatal("expected error for missing version")
476		}
477		if _, statErr := os.Stat(knownFile); !os.IsNotExist(statErr) {
478			t.Errorf("known-modules file should NOT exist after failure, stat err=%v", statErr)
479		}
480	}
481	```
482
483	NOTE: `fakeCrow` exists at `apps/birdseye/internal/registry/registry_test.go` — different package. Since `cmd` does NOT import `registry`'s tests, we need a `fakeCrow` helper in cmd's test file. The existing `fakeCrowTwoVersions` in `diff_test.go` already covers the success path. For the failure test, write a minimal failing-version helper inline:
484
485	Replace the body of `TestDiffDoesNotAddOnFailure` with the inline server below (do not reuse the registry-package helper):
486
487	```go
488	func TestDiffDoesNotAddOnFailure(t *testing.T) {
489		mux := http.NewServeMux()
490		// All download attempts return 404
491		mux.HandleFunc("/v1/modules/", func(w http.ResponseWriter, _ *http.Request) {
492			w.WriteHeader(http.StatusNotFound)
493		})
494		srv := httptest.NewServer(mux)
495		t.Cleanup(srv.Close)
496
497		knownFile := filepath.Join(t.TempDir(), "known-modules")
498		root := NewRootCmd()
499		root.SetErr(&bytes.Buffer{})
500		root.SetArgs([]string{
501			"diff",
502			"iac/comply/aws", "1.0.0", "1.1.0",
503			"--crow-url", srv.URL,
504			"--plain",
505			"--cache-dir", t.TempDir(),
506			"--known-modules-file", knownFile,
507		})
508		if err := root.ExecuteContext(context.Background()); err == nil {
509			t.Fatal("expected error for 404s")
510		}
511		if _, statErr := os.Stat(knownFile); !os.IsNotExist(statErr) {
512			t.Errorf("known-modules file should NOT exist after failure, stat err=%v", statErr)
513		}
514	}
515	```
516
517	- [ ] **Step 2: Run, expect FAIL** (`unknown flag --known-modules-file`)
518
519	Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test ./cmd/... -run TestDiffAddsToKnown -v`
520
521	- [ ] **Step 3: Update `apps/birdseye/cmd/diff.go`**
522
523	Add the field to `diffOpts`:
524
525	```go
526	type diffOpts struct {
527		Semantic         bool
528		Plain            bool
529		CacheDir         string
530		KnownModulesFile string // NEW
531	}
532	```
533
534	Add the flag in `newDiffCmd`:
535
536	```go
537	c.Flags().StringVar(&o.KnownModulesFile, "known-modules-file", "", "override known-modules autocomplete file (mostly for tests)")
538	_ = c.Flags().MarkHidden("known-modules-file")
539	```
540
541	Add a helper at file scope (below `defaultCacheDir`):
542
543	```go
544	// resolveKnownPath returns override if non-empty, otherwise the default
545	// known.Path(). Errors fall back to "" so Load and Add become no-ops.
546	func resolveKnownPath(override string) string {
547		if override != "" {
548			return override
549		}
550		p, err := known.Path()
551		if err != nil {
552			return ""
553		}
554		return p
555	}
556	```
557
558	Add `"github.com/immuta/operations/apps/birdseye/internal/known"` to the import block.
559
560	At the END of `runDiff`, BEFORE the final TUI/plain/JSON render dispatch, add the best-effort Add:
561
562	```go
563		if err := known.Add(resolveKnownPath(o.KnownModulesFile),
564			id.Namespace+"/"+id.Name+"/"+id.Provider); err != nil {
565			_ = err // best-effort: failure does not block the diff
566		}
567	```
568
569	(Place this immediately AFTER `result := diff.Result{Files: ...}` and any `if o.Semantic` block, but BEFORE the `if g.JSON { ... }` / `if !o.Plain && isTTY(...) { ... }` / `return renderPlain(...)` block. Adding before the render means a failed render does NOT prevent the cache update, but a failed extract/fetch DOES — exactly what the spec wants.)
570
571	- [ ] **Step 4: Run, expect PASS**
572
573	Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race ./cmd/... -v`
574	Expected: All previous + 2 new tests PASS.
575
576	- [ ] **Step 5: Commit**
577
578	```bash
579	cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye && git add apps/birdseye/cmd/ && git commit -m "feat(birdseye): cache module address after successful CLI diff"
580	```
581
582	---
583
584	## Task 4: `cmd/diff` + `cmd/versions` — ValidArgsFunction for shell completion
585
586	**Files:**
587	- Modify: `apps/birdseye/cmd/diff.go`
588	- Modify: `apps/birdseye/cmd/versions.go`
589	- Modify: `apps/birdseye/cmd/versions_test.go`
590	- Modify: `apps/birdseye/cmd/diff_test.go`
591
592	- [ ] **Step 1: Write failing tests** — append to `apps/birdseye/cmd/diff_test.go`:
593
594	```go
595	func TestDiffValidArgsFunctionLoadsCache(t *testing.T) {
596		knownFile := filepath.Join(t.TempDir(), "known-modules")
597		if err := os.WriteFile(knownFile, []byte("alpha/aaa/aws\nbravo/bbb/gcp\n"), 0o600); err != nil {
598			t.Fatal(err)
599		}
600
601		root := NewRootCmd()
602		// Drive cobra completion via the __complete pseudo-command.
603		root.SetArgs([]string{"__complete", "diff", "--known-modules-file", knownFile, ""})
604		var stdout bytes.Buffer
605		root.SetOut(&stdout)
606		if err := root.ExecuteContext(context.Background()); err != nil {
607			t.Fatalf("execute completion: %v", err)
608		}
609		out := stdout.String()
610		for _, want := range []string{"alpha/aaa/aws", "bravo/bbb/gcp"} {
611			if !strings.Contains(out, want) {
612				t.Errorf("completion output missing %q\n%s", want, out)
613			}
614		}
615	}
616	```
617
618	And to `apps/birdseye/cmd/versions_test.go`:
619
620	```go
621	import (
622		// existing imports preserved
623		"os"
624		"path/filepath"
625	)
626
627	func TestVersionsValidArgsFunctionLoadsCache(t *testing.T) {
628		knownFile := filepath.Join(t.TempDir(), "known-modules")
629		if err := os.WriteFile(knownFile, []byte("alpha/aaa/aws\n"), 0o600); err != nil {
630			t.Fatal(err)
631		}
632
633		root := NewRootCmd()
634		root.SetArgs([]string{"__complete", "versions", "--known-modules-file", knownFile, ""})
635		var stdout bytes.Buffer
636		root.SetOut(&stdout)
637		if err := root.ExecuteContext(context.Background()); err != nil {
638			t.Fatalf("execute completion: %v", err)
639		}
640		if !strings.Contains(stdout.String(), "alpha/aaa/aws") {
641			t.Errorf("completion missing entry:\n%s", stdout.String())
642		}
643	}
644
645	func TestVersionsDoesNotAddToKnown(t *testing.T) {
646		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
647			_ = json.NewEncoder(w).Encode(api.ListVersionsResponse{
648				Modules: []api.ModuleVersions{
649					{Versions: []api.VersionEntry{{Version: "1.0.0"}}},
650				},
651			})
652		}))
653		defer srv.Close()
654
655		knownFile := filepath.Join(t.TempDir(), "known-modules")
656		root := NewRootCmd()
657		var stdout bytes.Buffer
658		root.SetOut(&stdout)
659		root.SetArgs([]string{"versions", "iac/comply/aws",
660			"--crow-url", srv.URL,
661			"--known-modules-file", knownFile,
662		})
663		if err := root.ExecuteContext(context.Background()); err != nil {
664			t.Fatalf("execute: %v", err)
665		}
666		if _, statErr := os.Stat(knownFile); !os.IsNotExist(statErr) {
667			t.Errorf("known-modules file should NOT exist after versions, stat err=%v", statErr)
668		}
669	}
670	```
671
672	- [ ] **Step 2: Run, expect FAIL** (versions has no `--known-modules-file` flag yet; diff has no `ValidArgsFunction`)
673
674	Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test ./cmd/... -run TestDiffValidArgs -v`
675
676	- [ ] **Step 3: Add `ValidArgsFunction` to `newDiffCmd` in `apps/birdseye/cmd/diff.go`**
677
678	Just before the closing `return c` of `newDiffCmd`, add:
679
680	```go
681		c.ValidArgsFunction = func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
682			if len(args) > 0 {
683				return nil, cobra.ShellCompDirectiveNoFileComp
684			}
685			suggestions, _ := known.Load(resolveKnownPath(o.KnownModulesFile))
686			return suggestions, cobra.ShellCompDirectiveNoFileComp
687		}
688	```
689
690	- [ ] **Step 4: Update `apps/birdseye/cmd/versions.go`**
691
692	Add the matching flag + completion to `newVersionsCmd`. Replace the existing function body:
693
694	```go
695	func newVersionsCmd(g *GlobalFlags) *cobra.Command {
696		var knownModulesFile string
697		c := &cobra.Command{
698			Use:           "versions <module>",
699			Short:         "List published versions of a Crow module",
700			Args:          cobra.ExactArgs(1),
701			SilenceUsage:  true,
702			SilenceErrors: true,
703			RunE: func(cmd *cobra.Command, args []string) error {
704				id, err := parseModuleAddress(args[0])
705				if err != nil {
706					return err
707				}
708				cli := client.New(g.CrowURL, client.WithHTTPClient(newHTTPClient(g.Timeout)))
709				versions, err := cli.ListVersions(cmd.Context(), id)
710				if err != nil {
711					return err
712				}
713				for _, v := range versions {
714					fmt.Fprintln(cmd.OutOrStdout(), v)
715				}
716				return nil
717			},
718		}
719		c.Flags().StringVar(&knownModulesFile, "known-modules-file", "", "override known-modules autocomplete file (mostly for tests)")
720		_ = c.Flags().MarkHidden("known-modules-file")
721		c.ValidArgsFunction = func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
722			if len(args) > 0 {
723				return nil, cobra.ShellCompDirectiveNoFileComp
724			}
725			suggestions, _ := known.Load(resolveKnownPath(knownModulesFile))
726			return suggestions, cobra.ShellCompDirectiveNoFileComp
727		}
728		return c
729	}
730	```
731
732	Add `"github.com/immuta/operations/apps/birdseye/internal/known"` to the imports.
733
734	- [ ] **Step 5: Run, expect PASS**
735
736	Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race ./cmd/... -v`
737	Expected: All previous + 3 new tests PASS.
738
739	- [ ] **Step 6: Commit**
740
741	```bash
742	cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye && git add apps/birdseye/cmd/ && git commit -m "feat(birdseye): shell completion for diff and versions from known-modules cache"
743	```
744
745	---
746
747	## Task 5: `internal/tui/picker` — suggestions + fire-and-forget Add + wire `runInteractive`
748
749	**Files:**
750	- Modify: `apps/birdseye/internal/tui/picker.go`
751	- Modify: `apps/birdseye/internal/tui/picker_test.go`
752	- Modify: `apps/birdseye/cmd/diff.go`
753
754	- [ ] **Step 1: Append failing tests** to `apps/birdseye/internal/tui/picker_test.go`:
755
756	```go
757	import (
758		// existing: testing, tea
759		"context"
760	)
761
762	func TestPickerAcceptsSuggestionsArg(t *testing.T) {
763		p := NewPicker(context.Background(), nil, []string{"iac/comply/aws", "platform/networking/aws"}, "")
764		if !p.input.ShowSuggestions {
765			t.Error("expected ShowSuggestions == true when suggestions provided")
766		}
767		// bubbles/textinput stores suggestions internally; we can't easily
768		// inspect them directly, but ShowSuggestions=true confirms the
769		// setter ran (we set it conditionally on len > 0).
770	}
771
772	func TestPickerNoSuggestionsLeavesShowSuggestionsFalse(t *testing.T) {
773		p := NewPicker(context.Background(), nil, nil, "")
774		if p.input.ShowSuggestions {
775			t.Error("expected ShowSuggestions == false when suggestions are nil/empty")
776		}
777	}
778
779	func TestPickerEmptySuggestionsLeavesShowSuggestionsFalse(t *testing.T) {
780		p := NewPicker(context.Background(), nil, []string{}, "")
781		if p.input.ShowSuggestions {
782			t.Error("expected ShowSuggestions == false when suggestions slice is empty")
783		}
784	}
785	```
786
787	- [ ] **Step 2: Update existing picker tests** in `apps/birdseye/internal/tui/picker_test.go` to use the new `NewPicker` signature
788
789	Find every existing `NewPicker(context.Background(), nil)` call and replace with `NewPicker(context.Background(), nil, nil, "")`. There are three such calls in the existing tests:
790
791	- `TestPickerStartsAtAskModule`
792	- `TestPickerAdvancesOnEnter`
793	- `TestPickerRejectsBadAddressInline`
794
795	Each becomes `p := NewPicker(context.Background(), nil, nil, "")`.
796
797	- [ ] **Step 3: Run, expect FAIL** (`NewPicker` signature mismatch)
798
799	Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test ./internal/tui/... -run TestPicker -v`
800
801	- [ ] **Step 4: Update `apps/birdseye/internal/tui/picker.go`**
802
803	Change the `Picker` struct to add a `knownPath` field. Locate the `type Picker struct` and add `knownPath string` to the field list.
804
805	Change `NewPicker` signature and body:
806
807	```go
808	func NewPicker(ctx context.Context, deps *Deps, suggestions []string, knownPath string) *Picker {
809		ti := textinput.New()
810		ti.Placeholder = "namespace/name/provider"
811		ti.Focus()
812		if len(suggestions) > 0 {
813			ti.SetSuggestions(suggestions)
814			ti.ShowSuggestions = true
815		}
816		sp := spinner.New()
817		sp.Spinner = spinner.Dot
818		return &Picker{
819			ctx:       ctx,
820			deps:      deps,
821			state:     StateAskModule,
822			input:     ti,
823			spinner:   sp,
824			knownPath: knownPath,
825		}
826	}
827	```
828
829	Add the import for `known`:
830
831	```go
832	"github.com/immuta/operations/apps/birdseye/internal/known"
833	```
834
835	Modify `updateLoading` — in the `versionsLoadedMsg` branch, BEFORE returning, dispatch the fire-and-forget `Add`. Replace the existing branch:
836
837	```go
838		case versionsLoadedMsg:
839			if m.err != nil {
840				p.errMessage = m.err.Error()
841				p.state = StateError
842				return p, nil
843			}
844			items := make([]list.Item, 0, len(m.versions))
845			for _, v := range m.versions {
846				items = append(items, versionItem(v))
847			}
848			p.versionsA = list.New(items, list.NewDefaultDelegate(), p.width, p.height-4)
849			p.versionsA.Title = "Select version A"
850			p.state = StateAskVersionA
851			return p, addKnownCmd(p.knownPath, p.moduleID)
852	```
853
854	Add `addKnownCmd` at file scope (below `loadPairCmd`):
855
856	```go
857	// addKnownCmd dispatches a fire-and-forget known.Add. The returned
858	// tea.Msg is nil — bubbletea ignores nil messages cleanly. Errors are
859	// swallowed (best-effort cache update).
860	func addKnownCmd(path string, id api.ModuleIdentifier) tea.Cmd {
861		if path == "" {
862			return nil
863		}
864		addr := id.Namespace + "/" + id.Name + "/" + id.Provider
865		return func() tea.Msg {
866			_ = known.Add(path, addr)
867			return nil
868		}
869	}
870	```
871
872	- [ ] **Step 5: Update `runInteractive` in `apps/birdseye/cmd/diff.go`**
873
874	Replace the `tui.NewPicker(...)` call:
875
876	Before:
877	```go
878		p := tui.NewPicker(ctx, &tui.Deps{Client: cli, Reg: reg})
879	```
880
881	After:
882	```go
883		knownPath := resolveKnownPath(o.KnownModulesFile)
884		suggestions, _ := known.Load(knownPath)
885		p := tui.NewPicker(ctx, &tui.Deps{Client: cli, Reg: reg}, suggestions, knownPath)
886	```
887
888	- [ ] **Step 6: Run all tests, expect PASS**
889
890	Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race ./... -v`
891	Expected: all PASS — existing + 3 new picker tests + 3 existing picker tests updated to new signature.
892
893	- [ ] **Step 7: Verify build**
894
895	Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go vet ./... && go build ./...`
896	Expected: clean.
897
898	- [ ] **Step 8: Commit**
899
900	```bash
901	cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye && git add apps/birdseye/internal/tui/ apps/birdseye/cmd/diff.go && git commit -m "feat(birdseye): TUI textinput suggestions and best-effort cache from interactive picker"
902	```
903
904	---
905
906	## Plan self-review
907
908	**Spec coverage:**
909	- `internal/known` Path/Load/Add ✓ Tasks 1, 2
910	- Atomic temp+rename writes ✓ Task 1 implementation, Task 2 test
911	- Dedup + sort ✓ Task 1 implementation, Task 2 tests
912	- Malformed line skipping ✓ Task 1 `validAddress` filter, Task 2 test
913	- Bad-address rejection ✓ Task 1 implementation, Task 2 test
914	- `--known-modules-file` hidden flag on `diff` ✓ Task 3
915	- `--known-modules-file` hidden flag on `versions` ✓ Task 4
916	- Post-success `Add` from CLI diff ✓ Task 3
917	- `Add` NOT called from `versions` ✓ Task 4 `TestVersionsDoesNotAddToKnown`
918	- `Add` NOT called on failure ✓ Task 3 `TestDiffDoesNotAddOnFailure`
919	- Cobra `ValidArgsFunction` on diff and versions ✓ Task 4
920	- TUI suggestions via `SetSuggestions` / `ShowSuggestions` ✓ Task 5
921	- `addKnownCmd` fire-and-forget on `versionsLoadedMsg` ✓ Task 5
922	- `runInteractive` loads suggestions before picker ✓ Task 5 Step 5
923
924	**Placeholder scan:** No TBDs, no "implement later", every step has concrete code.
925
926	**Type consistency:**
927	- `NewPicker(ctx, deps, suggestions, knownPath)` — same 4-arg signature in Task 5 step 4 and step 5 ✓
928	- `known.Path() (string, error)`, `Load(path string) ([]string, error)`, `Add(path, address string) error` — same signatures across Tasks 1-5 ✓
929	- `resolveKnownPath(override string) string` — declared in Task 3, reused in Tasks 4 and 5 ✓
930	- `diffOpts.KnownModulesFile` — declared in Task 3, referenced in Task 5 ✓
931
932	**Open caveats:**
933	- The picker has an existing `ctx` field added in the Task 13 fix (from the prior session). Verify it exists by reading `picker.go` before Step 4; if absent, add it in this task. The Task 5 implementation here ASSUMES it exists.
934	- `bubbles/textinput.SetSuggestions` is available in `v0.21.0` per go.mod (Task 1 of prior plan). If the API differs in this exact version, fall back to setting `ti.SetSuggestions(...)` directly with whatever the actual method name is.
935


Now let me run the build and tests, and run smoke tests.


Bash

cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go build ./... 2>&1

Build birdseye


cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go vet ./... 2>&1

Go vet birdseye


(Bash completed with no output)


cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race ./... 2>&1 | tail -60

Run all birdseye tests with race detector


cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/services/crow && go test -race ./pkg/client/... 2>&1 | tail -20

Run crow client tests with race detector


?   	github.com/immuta/operations/apps/birdseye	[no test files]
ok  	github.com/immuta/operations/apps/birdseye/cmd	1.932s
ok  	github.com/immuta/operations/apps/birdseye/internal/archive	(cached)
ok  	github.com/immuta/operations/apps/birdseye/internal/diff	(cached)
ok  	github.com/immuta/operations/apps/birdseye/internal/known	1.360s
ok  	github.com/immuta/operations/apps/birdseye/internal/registry	(cached)
ok  	github.com/immuta/operations/apps/birdseye/internal/tui	1.620s


ok  	github.com/immuta/operations/services/crow/pkg/client	(cached)


All tests pass cleanly. Now smoke-test the binary.


cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && ls Makefile 2>&1 && cat Makefile 2>&1 | head -40

Check Makefile


.rw-r--r-- steve.briggs staff 226 B Tue May 12 18:19:59 2026 Makefile
.PHONY: build test test-race lint clean

BIN := bin/birdseye

build:
	@mkdir -p bin
	go build -o $(BIN) .

test:
	go test ./...

test-race:
	go test -race ./...

lint:
	go vet ./...

clean:
	rm -rf bin

.DEFAULT_GOAL := build


cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && make build 2>&1 | tail -5

Build via make


go build -o bin/birdseye .


cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && ./bin/birdseye diff --help 2>&1

Check diff help for hidden flag


cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && ./bin/birdseye versions --help 2>&1

Check versions help for hidden flag


cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && ./bin/birdseye __complete diff "" 2>&1

Smoke test diff completion


cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && ./bin/birdseye __complete versions "" 2>&1

Smoke test versions completion


Diff two versions of a module hosted in Crow.

If module/versions are omitted, the interactive TUI launches
(see picker task). With --plain, prints unified diff to stdout.

Usage:
  birdseye diff [module] [versionA] [versionB] [flags]

Flags:
      --cache-dir string   module tarball cache directory (default "/Users/steve.briggs/Library/Caches/birdseye")
  -h, --help               help for diff
      --no-tui             alias for --plain
      --plain              force plain (non-TUI) output
      --semantic           include HCL-aware semantic diff alongside textual

Global Flags:
      --crow-url string    Crow base URL (env CROW_URL) (default "https://crow.infrastructure.immuta.io")
      --json               emit structured JSON to stdout where supported
      --timeout duration   per-operation HTTP timeout (default 30s)


List published versions of a Crow module

Usage:
  birdseye versions <module> [flags]

Flags:
  -h, --help   help for versions

Global Flags:
      --crow-url string    Crow base URL (env CROW_URL) (default "https://crow.infrastructure.immuta.io")
      --json               emit structured JSON to stdout where supported
      --timeout duration   per-operation HTTP timeout (default 30s)


iac/comply/aws
:4
Completion ended with directive: ShellCompDirectiveNoFileComp


Note: The completion smoke test shows `iac/comply/aws` is already in the user's real cache from prior test runs. Let me verify if any tests are leaking into the real config dir.


ls -la "/Users/steve.briggs/Library/Application Support/birdseye/" 2>&1; echo "---"; cat "/Users/steve.briggs/Library/Application Support/birdseye/known-modules" 2>&1

Inspect real config dir for leakage


drwx------ steve.briggs staff 2.8 KB Wed May 13 11:45:09 2026 ..
.rw------- steve.briggs staff  15 B  Wed May 13 11:53:37 2026 known-modules
drwxr-xr-x steve.briggs staff  96 B  Wed May 13 11:53:37 2026 .
---
iac/comply/aws


Real config dir `~/Library/Application Support/birdseye/known-modules` exists and contains `iac/comply/aws`. This was modified at 11:53:37, which matches the time the test suite ran. Let me confirm whether tests are leaking or whether this is from a prior manual smoke run.


stat -f "%Sm" -t "%H:%M:%S" "/Users/steve.briggs/Library/Application Support/birdseye/known-modules" 2>&1; date "+%H:%M:%S"

Get modification time of real cache file


11:53:37
11:54:54


rm -f "/Users/steve.briggs/Library/Application Support/birdseye/known-modules" && rmdir "/Users/steve.briggs/Library/Application Support/birdseye" 2>&1; ls "/Users/steve.briggs/Library/Application Support/" | grep birdseye 2>&1; echo "--- cache cleared ---"

Clear the real config dir to retest


--- cache cleared ---


cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race -count=1 ./... 2>&1 | tail -15

Re-run full test suite with count=1


?   	github.com/immuta/operations/apps/birdseye	[no test files]
ok  	github.com/immuta/operations/apps/birdseye/cmd	1.443s
ok  	github.com/immuta/operations/apps/birdseye/internal/archive	2.038s
ok  	github.com/immuta/operations/apps/birdseye/internal/diff	2.247s
ok  	github.com/immuta/operations/apps/birdseye/internal/known	1.807s
ok  	github.com/immuta/operations/apps/birdseye/internal/registry	1.678s
ok  	github.com/immuta/operations/apps/birdseye/internal/tui	2.493s


ls -la "/Users/steve.briggs/Library/Application Support/birdseye/" 2>&1; echo "---"; ls "/Users/steve.briggs/Library/Application Support/" | grep birdseye 2>&1

Check if test run created the cache file


drwx------ steve.briggs staff 2.8 KB Wed May 13 11:55:05 2026 ..
.rw------- steve.briggs staff  15 B  Wed May 13 11:55:05 2026 known-modules
drwxr-xr-x steve.briggs staff  96 B  Wed May 13 11:55:05 2026 .
---
drwxr-xr-x steve.briggs staff  96 B  Wed May 13 11:55:05 2026 birdseye


**FOUND A BUG.** Tests ARE leaking to the real config dir! Right after running `go test -race ./...`, the file `~/Library/Application Support/birdseye/known-modules` was recreated containing `iac/comply/aws`. This violates the spec's hermeticity requirement and pollutes the user's host.

Let me track down which test is the culprit. Looking at `TestPathHonorsUserConfigDir` — it only calls `Path()`, which doesn't write. Let me grep for any test path that doesn't pass `--known-modules-file`.


grep -rn "NewPicker\|known.Add\|known.Load" /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye 2>&1 | grep -v "_test.go" | head -30

Find all production callers of known package


grep -rn "NewRootCmd\|runDiff\|runInteractive" /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/*_test.go 2>&1 | head -30

Locate all test callers of run paths


/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/versions.go:42:		suggestions, _ := known.Load(resolveKnownPath(knownModulesFile))
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff.go:62:		suggestions, _ := known.Load(resolveKnownPath(o.KnownModulesFile))
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff.go:154:	if err := known.Add(resolveKnownPath(o.KnownModulesFile),
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff.go:190:	suggestions, _ := known.Load(knownPath)
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff.go:191:	p := tui.NewPicker(ctx, &tui.Deps{Client: cli, Reg: reg}, suggestions, knownPath)
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/internal/tui/picker.go:77:// NewPicker constructs a picker. deps may be nil for input-only tests
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/internal/tui/picker.go:81:func NewPicker(ctx context.Context, deps *Deps, suggestions []string, knownPath string) *Picker {
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/internal/tui/picker.go:248:// addKnownCmd dispatches a fire-and-forget known.Add. The returned
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/internal/tui/picker.go:257:		_ = known.Add(path, addr)


/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff_test.go:67:	root := NewRootCmd()
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff_test.go:91:	root := NewRootCmd()
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff_test.go:110:	root := NewRootCmd()
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff_test.go:125:	root := NewRootCmd()
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff_test.go:155:	root := NewRootCmd()
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff_test.go:187:	root := NewRootCmd()
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff_test.go:220:	root := NewRootCmd()
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff_test.go:244:	root := NewRootCmd()
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/root_test.go:9:func TestNewRootCmdHelp(t *testing.T) {
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/root_test.go:10:	root := NewRootCmd()
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/root_test.go:26:func TestNewRootCmdDefaults(t *testing.T) {
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/root_test.go:27:	root := NewRootCmd()
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/root_test.go:33:func TestNewRootCmdRespectsCrowURLEnv(t *testing.T) {
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/root_test.go:35:	root := NewRootCmd()
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/versions_test.go:31:	root := NewRootCmd()
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/versions_test.go:50:	root := NewRootCmd()
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/versions_test.go:73:	root := NewRootCmd()


Looking at `diff_test.go`: `TestDiffPlainOutput` (line 65), `TestDiffIdenticalVersionsNoChanges` (line 89), `TestDiffJSONOutput` (line 123), `TestDiffSemanticIncluded` (line 153) — these all execute `diff iac/comply/aws 1.0.0 1.1.0` against the fake server, but **do NOT pass `--known-modules-file`**. So `resolveKnownPath("")` falls through to `known.Path()`, which returns the real `~/Library/Application Support/birdseye/known-modules` path. Those tests are writing to the user's real config dir.

Let me confirm by running just one such test in isolation:


rm -rf "/Users/steve.briggs/Library/Application Support/birdseye/" 2>&1; cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race -count=1 -run TestDiffPlainOutput ./cmd/... 2>&1 | tail -3 && echo "---" && ls -la "/Users/steve.briggs/Library/Application Support/birdseye/" 2>&1

Run single test to confirm leak


ok  	github.com/immuta/operations/apps/birdseye/cmd	1.423s
---
drwx------ steve.briggs staff 2.8 KB Wed May 13 11:55:43 2026 ..
.rw------- steve.briggs staff  15 B  Wed May 13 11:55:43 2026 known-modules
drwxr-xr-x steve.briggs staff  96 B  Wed May 13 11:55:43 2026 .


**Confirmed bug.** `TestDiffPlainOutput` (which does NOT pass `--known-modules-file`) writes to the user's real `~/Library/Application Support/birdseye/known-modules`. The spec explicitly states: "Hermetic in tests: every test path injects a tempdir, so the host's real `~/.config/birdseye/known-modules` is never touched." This invariant is violated.

The other diff success tests with the same omission (`TestDiffIdenticalVersionsNoChanges`, `TestDiffJSONOutput`, `TestDiffSemanticIncluded`) have the same defect.

Clean up:


rm -rf "/Users/steve.briggs/Library/Application Support/birdseye/" 2>&1; echo "cleaned"

Clean up test pollution from real config dir


cleaned


Now let me check the remaining review items.


grep -rn "testify" /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/ 2>&1 | head -5

Check for testify usage


(Bash completed with no output)


grep -rn "ValidateSegments" /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/services/crow/pkg/api/ 2>&1 | head -10

Confirm ValidateSegments exists in api


/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/services/crow/pkg/api/paths.go:46:// ValidateSegments returns an InvalidSegmentError for the first segment that
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/services/crow/pkg/api/paths.go:48:func ValidateSegments(segments []string) error {
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/services/crow/pkg/api/paths.go:70:	return ValidateSegments([]string{id.Namespace, id.Name, id.Provider})
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/services/crow/pkg/api/paths.go:78:	return ValidateSegments([]string{m.Namespace, m.Name, m.Provider, m.Version})
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/services/crow/pkg/api/paths_test.go:8:func TestValidateSegments(t *testing.T) {
/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/services/crow/pkg/api/paths_test.go:26:			err := ValidateSegments(c.segments)


git diff 474d56d..HEAD -- /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff.go | grep -E "parseModuleAddress|ValidateVersion" | head -10

Verify parseModuleAddress not modified


No testify, parseModuleAddress unchanged, ValidateSegments exists. Now let me also check whether `TestVersionsCmd` leaks (it does NOT call diff, so it shouldn't trigger Add).


cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race -count=1 -run TestVersionsCmd ./cmd/... 2>&1 | tail -3 && echo "---" && ls "/Users/steve.briggs/Library/Application Support/" 2>&1 | grep birdseye || echo "no leak"

Check versions cmd test for leak


ok  	github.com/immuta/operations/apps/birdseye/cmd	1.345s
---
no leak


Good — `TestVersionsCmd` does not leak (versions never writes). Confirms the leak source is specifically the diff tests that omit `--known-modules-file`.

Let me also double-check the `TestPathHonorsUserConfigDir` test for safety — it calls `Path()` which doesn't write, so it's fine. And let me peek at what other test scenarios I should worry about.


cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race -count=1 -run "TestDiffBadModuleAddress|TestDiffSemantic|TestDiffJSON|TestDiffIdentical" ./cmd/... 2>&1 | tail -3 && echo "---" && ls "/Users/steve.briggs/Library/Application Support/" 2>&1 | grep birdseye || echo "no leak"

Test other diff cases for leak


ok  	github.com/immuta/operations/apps/birdseye/cmd	1.505s
---
drwxr-xr-x steve.briggs staff  96 B  Wed May 13 11:57:20 2026 birdseye


Confirmed: the `TestDiffIdentical/JSON/Semantic` tests also leak. `TestDiffBadModuleAddress` is the only one that does NOT (it errors before reaching Add).


rm -rf "/Users/steve.briggs/Library/Application Support/birdseye/" 2>&1; echo cleaned

Clean up real config dir leak


Let me also verify the failure-render scenario: spec says "Failed render → cache already written (correct behavior — diff conceptually succeeded)". Looking at runDiff in diff.go: `known.Add` is at line 154-157, BEFORE the JSON/plain/TUI render dispatch starting at line 159. Correct per plan.

One more thing worth verifying — the `_ = err` pattern in diff.go line 154-157 uses the variable name `err` from the if-init clause, which is unused after the assignment. Let me look more carefully:


154		if err := known.Add(resolveKnownPath(o.KnownModulesFile),
155			id.Namespace+"/"+id.Name+"/"+id.Provider); err != nil {
156			_ = err // best-effort: cache failure does not block diff
157		}
158
159		if g.JSON {
160			return renderJSON(cmd.OutOrStdout(), result)
161		}
162		if !o.Plain && isTTY(cmd.OutOrStdout()) {
163			return tui.RunBrowser(ctx, result)


`_ = err` inside the body is a no-op since `err` was already known to be non-nil — this is technically the documented swallow pattern. The block could be `_ = known.Add(...)` more cleanly, but this matches the established convention. Acceptable.

Now I have enough information. Let me compose the report.

---

**Overall:** NEEDS_FIXES_BEFORE_MERGE

There is one real and reproducible defect: several `cmd/diff` success-path tests omit `--known-modules-file` and therefore write to the host's real `~/Library/Application Support/birdseye/known-modules`. This breaks the spec's stated hermeticity invariant ("the host's real `~/.config/birdseye/known-modules` is never touched") and creates host-side state visible across test runs. The rest of the feature is solid.

**Integration coherence:**
- Dependency graph `cmd → internal/known` and `internal/tui → internal/known` is clean. No cycles.
- `internal/known` stays pure: it imports only stdlib + `services/crow/pkg/api` (for `ValidateSegments`). No logging, no cobra, no tea — good.
- Path resolution is centralized in `cmd.resolveKnownPath` (single source for "where do we read/write"). TUI receives the resolved path from cmd, which is the correct seam.
- The picker's helper `parseModuleAddressForPicker` is a small intentional duplication of `cmd.parseModuleAddress` to avoid a cycle — already commented as such; acceptable.

**End-to-end correctness:**
- Positional CLI diff success path writes the file — verified by `TestDiffAddsToKnownAfterSuccess`.
- `diff <TAB>` completion reads file — verified by `TestDiffValidArgsFunctionLoadsCache` (and confirmed by smoke `./bin/birdseye __complete diff ""`).
- `versions <TAB>` reads but never writes — verified by `TestVersionsValidArgsFunctionLoadsCache` + `TestVersionsDoesNotAddToKnown`.
- First-run / missing-file path returns nil suggestions — covered by `TestLoadMissingFileReturnsEmpty`.
- The "Add before render" placement means a failed render still keeps the cache write (matches spec).

**Failure modes:**
- 404 / fetch failure does not write — `TestDiffDoesNotAddOnFailure` verified.
- `os.UserConfigDir()` failure → `Path()` errors → `resolveKnownPath` returns `""` → `os.Open("")` fails with non-IsNotExist (ENOENT path "" varies by OS, but `os.IsNotExist` returns true on darwin/linux for empty path), so Load returns nil/nil and Add returns "create temp" error which the caller swallows. No crash. Verified by code inspection — `os.CreateTemp` is called with `filepath.Dir("") == "."`, so on the worst case it would create a temp in CWD; however, `os.MkdirAll(filepath.Dir(""), 0o755)` is `os.MkdirAll(".", 0o755)` which is a no-op success, then `existing, err := Load("")` returns nil/nil (os.Open("") → ENOENT → IsNotExist), then `os.CreateTemp(".", ".known-modules-*.tmp")` would actually succeed in CWD and `os.Rename(tmpName, "")` would fail. So a stray temp file could land in CWD if `Path()` fails. Minor edge case; not realistic on darwin/linux where UserConfigDir essentially never fails.
- Manual malformed file entries skipped by `validAddress` — covered by `TestLoadSkipsMalformedLines`.

**Resource hygiene:**
- `Add` cleans up the temp file on every error branch (write/flush/close/rename failures all call `os.Remove(tmpName)`).
- `Load` uses `defer f.Close()` and checks `scanner.Err()`.
- `addKnownCmd` returns nil tea.Cmd when path is empty — no goroutine spawned. Good.
- The picker `addKnownCmd` returns `nil` tea.Msg which bubbletea handles cleanly.

**Test independence:**
- All tests inside `internal/known` use `t.TempDir()` — clean.
- `TestDiffAddsToKnownAfterSuccess`, `TestDiffDoesNotAddOnFailure`, `TestDiffValidArgsFunctionLoadsCache`, `TestVersionsDoesNotAddToKnown`, `TestVersionsValidArgsFunctionLoadsCache` correctly inject `--known-modules-file`.
- **DEFECT:** `TestDiffPlainOutput`, `TestDiffIdenticalVersionsNoChanges`, `TestDiffJSONOutput`, `TestDiffSemanticIncluded` in `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff_test.go` do NOT pass `--known-modules-file`. They invoke a successful `birdseye diff iac/comply/aws 1.0.0 1.1.0 ...`, so `runDiff` reaches `known.Add(resolveKnownPath(""), "iac/comply/aws")` → resolves to `known.Path()` → real `~/Library/Application Support/birdseye/known-modules` (macOS) or `~/.config/birdseye/known-modules` (linux). Reproduced by hand: starting with the dir deleted, running just `go test -run TestDiffPlainOutput ./cmd/...` recreates `~/Library/Application Support/birdseye/known-modules` containing `iac/comply/aws`. This violates the spec's "Hermetic in tests" invariant explicitly stated in the spec's Goals section and again in the Testing strategy section ("the real `~/.config/birdseye/` is never touched").

**Style consistency:**
- Error wrapping uses `fmt.Errorf("…: %w", err)` — matches the rest of birdseye.
- Tests use stdlib `testing` only — no testify (consistent).
- Hidden flag pattern (`MarkHidden("known-modules-file")`) is duplicated literally between diff.go and versions.go. A small `addKnownModulesFlag(c, &v)` helper would DRY this up, but it's only two call sites; acceptable.
- `_ = err // best-effort` inside the if-body in diff.go (line 156) is technically redundant (err is already known non-nil and unused); `_ = known.Add(...)` at the call site is the more idiomatic swallow. Minor style nit; not blocking.
- `parseModuleAddressForPicker` mirrors `cmd.parseModuleAddress` minus the `ErrUsage` wrapping — duplication is intentional and commented.

**Build sanity:**
- `go build ./...` — clean.
- `go vet ./...` — clean.
- `go test -race ./...` — all pass (in `apps/birdseye`: cmd, archive, diff, known, registry, tui — all OK).
- `services/crow/pkg/client` tests — pass (cached, no touched code).

**Smoke output:**
- `./bin/birdseye diff --help` — `--known-modules-file` correctly hidden.
- `./bin/birdseye versions --help` — `--known-modules-file` correctly hidden.
- `./bin/birdseye __complete diff ""` — runs cleanly, returns the suggestion list plus directive `ShellCompDirectiveNoFileComp`.
- `./bin/birdseye __complete versions ""` — same.

**Spec coverage:**
Every requirement in the spec maps to an implemented task:
- `internal/known` Path/Load/Add atomic writes, dedup, sort, validation — Tasks 1+2.
- Hidden `--known-modules-file` flag on diff + versions — Tasks 3+4.
- Post-success Add from CLI diff (positional path only) — Task 3.
- Versions never writes — Task 4.
- Failed-diff never writes — Task 3 test.
- ValidArgsFunction on diff + versions — Task 4.
- TUI SetSuggestions / ShowSuggestions — Task 5.
- Fire-and-forget addKnownCmd from picker `versionsLoadedMsg` — Task 5.
- `runInteractive` preloads suggestions — Task 5.
The only spec promise NOT met is the testing-strategy invariant about hermetic tests (see Critical issue below).

**Things that could break unrelated tests:**
- `parseModuleAddress` unchanged.
- No exported symbol renames or shadowing.
- New `internal/known` package adds one new import edge into the crow `api` package, which was already imported by cmd/tui; no risk.

---

## Critical issues

1. **Test hermeticity violation** — `apps/birdseye/cmd/diff_test.go` at lines 65, 89, 123, 153: `TestDiffPlainOutput`, `TestDiffIdenticalVersionsNoChanges`, `TestDiffJSONOutput`, `TestDiffSemanticIncluded` all execute a successful `birdseye diff` without `--known-modules-file`, causing `runDiff` to write to the user's real `~/Library/Application Support/birdseye/known-modules` (or `~/.config/birdseye/...` on linux). The spec explicitly requires "the host's real `~/.config/birdseye/` is never touched." Confirmed by reproducing: deleting the real dir, running `go test -run TestDiffPlainOutput ./cmd/...`, then observing the file was recreated. Fix: add `"--known-modules-file", filepath.Join(t.TempDir(), "known-modules")` to each of the four test's `SetArgs` slices (they already use `t.TempDir()` for `--cache-dir`, so the pattern is established). Same `path/filepath` import already present.

## Minor issues

1. **Redundant `_ = err` swallow** — `apps/birdseye/cmd/diff.go:154-157`: the body of the `if err != nil` is just `_ = err // ...`. The idiomatic swallow would be `_ = known.Add(resolveKnownPath(o.KnownModulesFile), id.Namespace+"/"+id.Name+"/"+id.Provider)`. Functionally identical; style-only.

2. **Hidden-flag duplication** — `--known-modules-file` registration appears verbatim in both `cmd/diff.go` and `cmd/versions.go`. A `cmd.addKnownModulesFlag(c, &v)` helper would DRY it. Not blocking.

3. **`Path()` failure with stray temp** — extremely unlikely on darwin/linux, but if `os.UserConfigDir()` ever fails, `resolveKnownPath` returns `""` and `Add("")` will create a temp in CWD before failing the Rename. The temp is removed on the Rename failure path. Not a real-world concern but worth noting.