You are implementing Task 3 of the birdseye known-modules cache implementation plan.

## Working Environment

- **Worktree path:** `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`
- **Git branch:** `worktree-birdseye`
- **HEAD:** `171c32a` (Task 2: known hardening tests)
- Use absolute paths or `cd` into the worktree.
- **NEVER push.** **NEVER `git add .` / `git add -A`.**

## Context

Task 1 added `internal/known` (package with `Path()`, `Load()`, `Add()`). Task 2 hardened its tests. Your job: wire the cache update into the CLI `diff` subcommand.

You need to:
1. Add a hidden `--known-modules-file` flag (test override)
2. Add a `resolveKnownPath(override string) string` helper
3. After successful `runDiff` (positional path), call `known.Add(...)` as a best-effort step
4. Add two integration tests: one for the success path adding the address, one for the failure path NOT adding

The `runDiff` function in `apps/birdseye/cmd/diff.go` is the existing positional-args handler. The new `Add` call must happen AFTER the diff has been successfully computed (i.e., after `archive.Extract` and `diff.Textual` succeed) but BEFORE the final render dispatch (TUI/plain/JSON). That way: a failed fetch/extract does NOT add (correct), but a failed render does not block the add (also correct — the diff already succeeded conceptually).

## Task Description (Verbatim)

### Task 3: `cmd/diff` — `--known-modules-file` flag + post-success Add + tests

**Files:**
- Modify: `apps/birdseye/cmd/diff.go`
- Modify: `apps/birdseye/cmd/diff_test.go`

- [ ] **Step 1: Write failing tests** — append to `apps/birdseye/cmd/diff_test.go`:

```go
import (
	// existing imports preserved
	"os"
	"path/filepath"
)

func TestDiffAddsToKnownAfterSuccess(t *testing.T) {
	srv := fakeCrowTwoVersions(t)
	knownFile := filepath.Join(t.TempDir(), "known-modules")
	root := NewRootCmd()
	var stdout bytes.Buffer
	root.SetOut(&stdout)
	root.SetArgs([]string{
		"diff",
		"iac/comply/aws", "1.0.0", "1.1.0",
		"--crow-url", srv.URL,
		"--plain",
		"--cache-dir", t.TempDir(),
		"--known-modules-file", knownFile,
	})
	if err := root.ExecuteContext(context.Background()); err != nil {
		t.Fatalf("execute: %v", err)
	}
	body, err := os.ReadFile(knownFile)
	if err != nil {
		t.Fatalf("known-modules file not created: %v", err)
	}
	if !strings.Contains(string(body), "iac/comply/aws") {
		t.Errorf("known-modules missing address:\n%s", body)
	}
}

func TestDiffDoesNotAddOnFailure(t *testing.T) {
	mux := http.NewServeMux()
	// All download attempts return 404
	mux.HandleFunc("/v1/modules/", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusNotFound)
	})
	srv := httptest.NewServer(mux)
	t.Cleanup(srv.Close)

	knownFile := filepath.Join(t.TempDir(), "known-modules")
	root := NewRootCmd()
	root.SetErr(&bytes.Buffer{})
	root.SetArgs([]string{
		"diff",
		"iac/comply/aws", "1.0.0", "1.1.0",
		"--crow-url", srv.URL,
		"--plain",
		"--cache-dir", t.TempDir(),
		"--known-modules-file", knownFile,
	})
	if err := root.ExecuteContext(context.Background()); err == nil {
		t.Fatal("expected error for 404s")
	}
	if _, statErr := os.Stat(knownFile); !os.IsNotExist(statErr) {
		t.Errorf("known-modules file should NOT exist after failure, stat err=%v", statErr)
	}
}
```

NOTE: Merge new imports (`os`, `path/filepath`, `http`, `httptest`) into the existing block at the top of `diff_test.go`. Some are likely already there.

- [ ] **Step 2: Run, expect FAIL** (`unknown flag --known-modules-file`)

Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test ./cmd/... -run TestDiffAddsToKnown -v`

- [ ] **Step 3: Update `apps/birdseye/cmd/diff.go`**

Add the field to `diffOpts`:

```go
type diffOpts struct {
	Semantic         bool
	Plain            bool
	CacheDir         string
	KnownModulesFile string // NEW
}
```

Add the flag in `newDiffCmd` (find the existing flag registrations and add this group):

```go
c.Flags().StringVar(&o.KnownModulesFile, "known-modules-file", "", "override known-modules autocomplete file (mostly for tests)")
_ = c.Flags().MarkHidden("known-modules-file")
```

Add a helper at file scope (below `defaultCacheDir`):

```go
// resolveKnownPath returns override if non-empty, otherwise the default
// known.Path(). Errors fall back to "" so Load and Add become no-ops.
func resolveKnownPath(override string) string {
	if override != "" {
		return override
	}
	p, err := known.Path()
	if err != nil {
		return ""
	}
	return p
}
```

Add `"github.com/immuta/operations/apps/birdseye/internal/known"` to the import block.

In `runDiff`, insert the best-effort Add immediately AFTER `result := diff.Result{Files: diff.Textual(modAExtracted, modBExtracted)}` and the optional `if o.Semantic { ... }` block, but BEFORE the JSON / TUI / plain render dispatch:

```go
	if err := known.Add(resolveKnownPath(o.KnownModulesFile),
		id.Namespace+"/"+id.Name+"/"+id.Provider); err != nil {
		_ = err // best-effort: cache failure does not block diff
	}
```

To clarify: the current end of `runDiff` looks roughly like:

```go
result := diff.Result{Files: diff.Textual(modAExtracted, modBExtracted)}
if o.Semantic {
    sem, err := diff.Semantic(modAExtracted, modBExtracted)
    ...
    result.Semantic = sem
}

if g.JSON {
    return renderJSON(cmd.OutOrStdout(), result)
}
if !o.Plain && isTTY(cmd.OutOrStdout()) {
    return tui.RunBrowser(ctx, result)
}
return renderPlain(cmd.OutOrStdout(), result)
```

The new Add call goes BETWEEN the optional semantic block and the JSON/TUI/plain dispatch — i.e., right after the `if o.Semantic { ... }` closing brace, before `if g.JSON { ... }`.

- [ ] **Step 4: Run, expect PASS**

Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race ./cmd/... -v`
Expected: All previous + 2 new tests PASS.

- [ ] **Step 5: Verify build**

Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go vet ./... && go build ./...`
Expected: clean.

- [ ] **Step 6: Commit**

```bash
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"
```

If `go.sum` was modified (unlikely — known is a local internal package), include it.

## Notes & Style

- The `id` variable is already in scope in `runDiff` — it's the `api.ModuleIdentifier` parsed earlier from `args[0]`.
- Use `_ = err` to mark the swallowed error — matches the pattern in `internal/registry/registry.go` for cache-write failures.
- `id.Namespace+"/"+id.Name+"/"+id.Provider` produces the canonical `ns/name/provider` address string. (`api.ModuleIdentifier` doesn't have a `String()` method — that's the convention.)
- The failure test uses an httptest server returning 404 for all `/v1/modules/...` requests. Crow client `Download` returns `ErrNotFound`, which `Fetch`/`FetchPair` propagates, and `runDiff` returns it to cobra — non-zero exit, no cache write.

## Code Organization

`diff.go` already does heavy lifting; adding ~15 lines for flag + Add call + helper is fine. Don't split into a new file.

## Self-Review Checklist

- [ ] Both new tests pass under `-race`
- [ ] All previous `cmd` tests still pass
- [ ] `go vet ./...` clean
- [ ] `go build ./...` succeeds
- [ ] `--known-modules-file` is marked hidden (won't appear in `--help`)
- [ ] Add call is positioned BETWEEN semantic block and render dispatch (so a successful diff with failed render still caches, but a failed fetch does NOT)
- [ ] `--known-modules-file` flag registered ONLY on diff (not root, not versions — Task 4 adds it to versions separately)
- [ ] Commit message exactly: `feat(birdseye): cache module address after successful CLI diff`

## Report Format

- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
- Files modified
- Test count + race-clean
- Commit SHA
- Anything unexpected


- migrate-flux-app: Use when migrating a Flux CD application from the infrastructure/kubernetes/application-deployments repo to the application-deployment repo. Covers copying app folders, updating kustomizations, committing, pushing, and creating merge requests.
- app-infra-generate: Generate an application-infrastructure Terraform module from a YAML spec produced by app-infra-spec. Reads pattern files and a versions manifest to produce main.tf, variables.tf, outputs.tf, overlay guidance, and a triage list.
- app-infra-spec: Structured questionnaire to gather requirements for an application-infrastructure Terraform module. Produces a YAML spec for the app-infra-generate skill.
- obsidian-load: Load context from Obsidian notes in the `Claude/` directory into the current conversation.
- obsidian-save: Save current conversation context to an Obsidian note under the `Claude/` directory.
- mind:search: Search memories for specific content or patterns
- mind:stats: Show memory statistics and storage information
- mind:recent: Show recent memories and activity timeline
- mind:ask: Ask questions about memories and get context-aware answers
- superpowers:using-git-worktrees: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
- superpowers:receiving-code-review: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
- superpowers:finishing-a-development-branch: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
- superpowers:dispatching-parallel-agents: Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies
- superpowers:writing-plans: Use when you have a spec or requirements for a multi-step task, before touching code
- superpowers:using-superpowers: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
- superpowers:writing-skills: Use when creating new skills, editing existing skills, or verifying skills work before deployment
- superpowers:requesting-code-review: Use when completing tasks, implementing major features, or before merging to verify work meets requirements
- superpowers:executing-plans: Use when you have a written implementation plan to execute in a separate session with review checkpoints
- superpowers:verification-before-completion: Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
- superpowers:brainstorming: You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation.
- superpowers:systematic-debugging: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
- superpowers:test-driven-development: Use when implementing any feature or bugfix, before writing implementation code
- superpowers:subagent-driven-development: Use when executing implementation plans with independent tasks in the current session
- mind:memory: Claude Mind - Search and manage Claude's persistent memory stored in a single portable .mv2 file
- mind:mind: Claude Mind - Search and manage Claude's persistent memory stored in a single portable .mv2 file
- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled remote agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching. Also handles migrating existing Claude API code between Claude model versions (4.5 → 4.6, 4.6 → 4.7, retired-model replacements).
TRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.
SKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a pull request
- security-review: Complete a security review of the pending changes on the current branch


I'll implement Task 3 by first reading the existing files to understand the current state.


1	package cmd
2
3	import (
4		"context"
5		"encoding/json"
6		"fmt"
7		"io"
8		"net/http"
9		"os"
10		"path/filepath"
11		"strings"
12		"time"
13
14		tea "github.com/charmbracelet/bubbletea"
15		"github.com/spf13/cobra"
16		"golang.org/x/term"
17
18		"github.com/immuta/operations/apps/birdseye/internal/archive"
19		"github.com/immuta/operations/apps/birdseye/internal/diff"
20		"github.com/immuta/operations/apps/birdseye/internal/registry"
21		"github.com/immuta/operations/apps/birdseye/internal/tui"
22		"github.com/immuta/operations/services/crow/pkg/api"
23		"github.com/immuta/operations/services/crow/pkg/client"
24	)
25
26	// diffOpts holds the resolved options for `birdseye diff`.
27	type diffOpts struct {
28		Semantic bool
29		Plain    bool
30		CacheDir string
31	}
32
33	const maxArchiveBytes = 50 << 20 // 50 MiB safety cap on uncompressed size
34
35	func newDiffCmd(g *GlobalFlags) *cobra.Command {
36		o := &diffOpts{}
37		c := &cobra.Command{
38			Use:   "diff [module] [versionA] [versionB]",
39			Short: "Diff two versions of a Crow module",
40			Long: "Diff two versions of a module hosted in Crow.\n\n" +
41				"If module/versions are omitted, the interactive TUI launches\n" +
42				"(see picker task). With --plain, prints unified diff to stdout.",
43			Args:          cobra.MaximumNArgs(3),
44			SilenceUsage:  true,
45			SilenceErrors: true,
46			RunE: func(cmd *cobra.Command, args []string) error {
47				return runDiff(cmd.Context(), cmd, args, g, o)
48			},
49		}
50		c.Flags().BoolVar(&o.Semantic, "semantic", false, "include HCL-aware semantic diff alongside textual")
51		c.Flags().BoolVar(&o.Plain, "plain", false, "force plain (non-TUI) output")
52		c.Flags().BoolVar(&o.Plain, "no-tui", false, "alias for --plain")
53		c.Flags().StringVar(&o.CacheDir, "cache-dir", defaultCacheDir(), "module tarball cache directory")
54		return c
55	}
56
57	func defaultCacheDir() string {
58		if home, err := os.UserCacheDir(); err == nil {
59			return filepath.Join(home, "birdseye")
60		}
61		return ".birdseye-cache"
62	}
63
64	func newHTTPClient(d time.Duration) *http.Client {
65		return &http.Client{Timeout: d}
66	}
67
68	func parseModuleAddress(s string) (api.ModuleIdentifier, error) {
69		parts := strings.Split(s, "/")
70		if len(parts) != 3 {
71			return api.ModuleIdentifier{}, fmt.Errorf("%w: module address must be namespace/name/provider, got %q", ErrUsage, s)
72		}
73		id := api.ModuleIdentifier{Namespace: parts[0], Name: parts[1], Provider: parts[2]}
74		if err := id.Validate(); err != nil {
75			return api.ModuleIdentifier{}, fmt.Errorf("%w: %v", ErrUsage, err)
76		}
77		return id, nil
78	}
79
80	func runDiff(ctx context.Context, cmd *cobra.Command, args []string, g *GlobalFlags, o *diffOpts) error {
81		if len(args) == 0 {
82			return runInteractive(ctx, cmd, g, o)
83		}
84		if len(args) != 3 {
85			return fmt.Errorf("%w: diff requires <module> <versionA> <versionB>", ErrUsage)
86		}
87		id, err := parseModuleAddress(args[0])
88		if err != nil {
89			return err
90		}
91		if err := api.ValidateVersion(args[1]); err != nil {
92			return fmt.Errorf("%w: %v", ErrUsage, err)
93		}
94		if err := api.ValidateVersion(args[2]); err != nil {
95			return fmt.Errorf("%w: %v", ErrUsage, err)
96		}
97		modA := api.Module{ModuleIdentifier: id, Version: args[1]}
98		modB := api.Module{ModuleIdentifier: id, Version: args[2]}
99
100		cli := client.New(g.CrowURL, client.WithHTTPClient(newHTTPClient(g.Timeout)))
101		cache, err := registry.NewDiskCache(o.CacheDir)
102		if err != nil {
103			return fmt.Errorf("cache init: %w", err)
104		}
105		reg := registry.New(cli, cache)
106
107		aBytes, bBytes, err := reg.FetchPair(ctx, modA, modB)
108		if err != nil {
109			return err
110		}
111
112		modAExtracted, err := archive.Extract(aBytes, maxArchiveBytes)
113		if err != nil {
114			return fmt.Errorf("%w: %v", ErrArchive, err)
115		}
116		modBExtracted, err := archive.Extract(bBytes, maxArchiveBytes)
117		if err != nil {
118			return fmt.Errorf("%w: %v", ErrArchive, err)
119		}
120
121		result := diff.Result{Files: diff.Textual(modAExtracted, modBExtracted)}
122		if o.Semantic {
123			sem, err := diff.Semantic(modAExtracted, modBExtracted)
124			if err != nil {
125				return fmt.Errorf("%w: %v", ErrSemantic, err)
126			}
127			result.Semantic = sem
128		}
129
130		if g.JSON {
131			return renderJSON(cmd.OutOrStdout(), result)
132		}
133		if !o.Plain && isTTY(cmd.OutOrStdout()) {
134			return tui.RunBrowser(ctx, result)
135		}
136		return renderPlain(cmd.OutOrStdout(), result)
137	}
138
139	// isTTY reports whether w is a *os.File backed by a terminal.
140	func isTTY(w io.Writer) bool {
141		f, ok := w.(*os.File)
142		if !ok {
143			return false
144		}
145		return term.IsTerminal(int(f.Fd()))
146	}
147
148	func runInteractive(ctx context.Context, cmd *cobra.Command, g *GlobalFlags, o *diffOpts) error {
149		if !isTTY(cmd.OutOrStdout()) {
150			return fmt.Errorf("%w: interactive mode requires a TTY", ErrUsage)
151		}
152		cli := client.New(g.CrowURL, client.WithHTTPClient(newHTTPClient(g.Timeout)))
153		cache, err := registry.NewDiskCache(o.CacheDir)
154		if err != nil {
155			return fmt.Errorf("cache init: %w", err)
156		}
157		reg := registry.New(cli, cache)
158
159		// Stage 1: picker → fetched bytes
160		p := tui.NewPicker(ctx, &tui.Deps{Client: cli, Reg: reg})
161		prog := tea.NewProgram(p, tea.WithAltScreen(), tea.WithContext(ctx))
162		if _, err := prog.Run(); err != nil {
163			return err
164		}
165		aBytes, bBytes := p.Bytes()
166		if aBytes == nil || bBytes == nil {
167			// User quit before completing picker; not an error.
168			return nil
169		}
170
171		// Stage 2: extract + diff + browser
172		modA, err := archive.Extract(aBytes, maxArchiveBytes)
173		if err != nil {
174			return fmt.Errorf("%w: %v", ErrArchive, err)
175		}
176		modB, err := archive.Extract(bBytes, maxArchiveBytes)
177		if err != nil {
178			return fmt.Errorf("%w: %v", ErrArchive, err)
179		}
180		result := diff.Result{Files: diff.Textual(modA, modB)}
181		if o.Semantic {
182			sem, err := diff.Semantic(modA, modB)
183			if err != nil {
184				return fmt.Errorf("%w: %v", ErrSemantic, err)
185			}
186			result.Semantic = sem
187		}
188		return tui.RunBrowser(ctx, result)
189	}
190
191	// renderPlain writes the diff result to w in unified-diff format.
192	// "no differences" is emitted when there are zero files in the result.
193	func renderPlain(w io.Writer, r diff.Result) error {
194		if len(r.Files) == 0 && len(r.Semantic) == 0 {
195			_, err := fmt.Fprintln(w, "no differences")
196			return err
197		}
198		for i, fd := range r.Files {
199			if i > 0 {
200				_, _ = fmt.Fprintln(w)
201			}
202			_, _ = fmt.Fprintf(w, "=== %s (%s) +%d -%d\n", fd.Path, fd.Status, fd.Adds, fd.Dels)
203			_, _ = fmt.Fprint(w, fd.Unified)
204		}
205		if len(r.Semantic) > 0 {
206			_, _ = fmt.Fprintln(w)
207			_, _ = fmt.Fprintln(w, "=== semantic changes ===")
208			for _, c := range r.Semantic {
209				_, _ = fmt.Fprintf(w, "%s %s %s\n", c.Status, c.Kind, c.Name)
210			}
211		}
212		return nil
213	}
214
215	func renderJSON(w io.Writer, r diff.Result) error {
216		type fileOut struct {
217			Path    string `json:"path"`
218			Status  string `json:"status"`
219			Adds    int    `json:"adds"`
220			Dels    int    `json:"dels"`
221			Unified string `json:"unified"`
222		}
223		type semOut struct {
224			Kind   string `json:"kind"`
225			Name   string `json:"name"`
226			Status string `json:"status"`
227			Detail string `json:"detail,omitempty"`
228		}
229		type payload struct {
230			Files    []fileOut `json:"files"`
231			Semantic []semOut  `json:"semantic,omitempty"`
232		}
233		out := payload{Files: make([]fileOut, 0, len(r.Files))}
234		for _, fd := range r.Files {
235			out.Files = append(out.Files, fileOut{Path: fd.Path, Status: fd.Status.String(), Adds: fd.Adds, Dels: fd.Dels, Unified: fd.Unified})
236		}
237		for _, c := range r.Semantic {
238			out.Semantic = append(out.Semantic, semOut{Kind: c.Kind, Name: c.Name, Status: c.Status.String(), Detail: c.Detail})
239		}
240		return json.NewEncoder(w).Encode(out)
241	}
242


1	package cmd
2
3	import (
4		"archive/tar"
5		"bytes"
6		"compress/gzip"
7		"context"
8		"encoding/json"
9		"net/http"
10		"net/http/httptest"
11		"strings"
12		"testing"
13	)
14
15	// fakeCrow with two versions of moduleA. v1 has main.tf; v2 has main.tf
16	// (modified) and a new variables.tf.
17	func fakeCrowTwoVersions(t *testing.T) *httptest.Server {
18		t.Helper()
19		mkTgz := func(files map[string]string) []byte {
20			var buf bytes.Buffer
21			gz := gzip.NewWriter(&buf)
22			tw := tar.NewWriter(gz)
23			for n, b := range files {
24				_ = tw.WriteHeader(&tar.Header{Name: n, Mode: 0o644, Size: int64(len(b)), Typeflag: tar.TypeReg})
25				_, _ = tw.Write([]byte(b))
26			}
27			_ = tw.Close()
28			_ = gz.Close()
29			return buf.Bytes()
30		}
31		v1 := mkTgz(map[string]string{"main.tf": "resource \"aws_s3_bucket\" \"alpha\" {}\n"})
32		v2 := mkTgz(map[string]string{
33			"main.tf":      "resource \"aws_s3_bucket\" \"beta\" {}\n",
34			"variables.tf": "variable \"name\" { type = string }\n",
35		})
36
37		mux := http.NewServeMux()
38		blob := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
39			switch r.URL.Path {
40			case "/blob/1.0.0":
41				_, _ = w.Write(v1)
42			case "/blob/1.1.0":
43				_, _ = w.Write(v2)
44			default:
45				http.NotFound(w, r)
46			}
47		}))
48		t.Cleanup(blob.Close)
49		mux.HandleFunc("/v1/modules/iac/comply/aws/", func(w http.ResponseWriter, r *http.Request) {
50			parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/v1/modules/iac/comply/aws/"), "/")
51			if len(parts) != 2 || parts[1] != "download" {
52				http.NotFound(w, r)
53				return
54			}
55			w.Header().Set("X-Terraform-Get", blob.URL+"/blob/"+parts[0])
56			w.WriteHeader(http.StatusNoContent)
57		})
58		srv := httptest.NewServer(mux)
59		t.Cleanup(srv.Close)
60		return srv
61	}
62
63	func TestDiffPlainOutput(t *testing.T) {
64		srv := fakeCrowTwoVersions(t)
65		root := NewRootCmd()
66		var stdout, stderr bytes.Buffer
67		root.SetOut(&stdout)
68		root.SetErr(&stderr)
69		root.SetArgs([]string{
70			"diff",
71			"iac/comply/aws", "1.0.0", "1.1.0",
72			"--crow-url", srv.URL,
73			"--plain",
74			"--cache-dir", t.TempDir(),
75		})
76		if err := root.ExecuteContext(context.Background()); err != nil {
77			t.Fatalf("execute: %v\nstderr: %s", err, stderr.String())
78		}
79		out := stdout.String()
80		for _, want := range []string{"main.tf", "variables.tf", "alpha", "beta"} {
81			if !strings.Contains(out, want) {
82				t.Errorf("plain output missing %q\n--- output ---\n%s", want, out)
83			}
84		}
85	}
86
87	func TestDiffIdenticalVersionsNoChanges(t *testing.T) {
88		srv := fakeCrowTwoVersions(t)
89		root := NewRootCmd()
90		var stdout bytes.Buffer
91		root.SetOut(&stdout)
92		root.SetArgs([]string{
93			"diff",
94			"iac/comply/aws", "1.0.0", "1.0.0",
95			"--crow-url", srv.URL,
96			"--plain",
97			"--cache-dir", t.TempDir(),
98		})
99		if err := root.ExecuteContext(context.Background()); err != nil {
100			t.Fatalf("execute: %v", err)
101		}
102		if got := strings.TrimSpace(stdout.String()); got != "no differences" {
103			t.Errorf("output = %q, want \"no differences\"", got)
104		}
105	}
106
107	func TestDiffBadModuleAddress(t *testing.T) {
108		root := NewRootCmd()
109		var stderr bytes.Buffer
110		root.SetErr(&stderr)
111		root.SetArgs([]string{"diff", "bad-address", "1.0.0", "1.1.0"})
112		err := root.ExecuteContext(context.Background())
113		if err == nil {
114			t.Fatal("expected error for bad address")
115		}
116		if ExitCode(err) != 1 {
117			t.Errorf("exit code = %d, want 1", ExitCode(err))
118		}
119	}
120
121	func TestDiffJSONOutput(t *testing.T) {
122		srv := fakeCrowTwoVersions(t)
123		root := NewRootCmd()
124		var stdout bytes.Buffer
125		root.SetOut(&stdout)
126		root.SetArgs([]string{
127			"diff",
128			"iac/comply/aws", "1.0.0", "1.1.0",
129			"--crow-url", srv.URL,
130			"--plain",
131			"--json",
132			"--cache-dir", t.TempDir(),
133		})
134		if err := root.ExecuteContext(context.Background()); err != nil {
135			t.Fatalf("execute: %v", err)
136		}
137		var payload struct {
138			Files []struct {
139				Path   string `json:"path"`
140				Status string `json:"status"`
141			} `json:"files"`
142		}
143		if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
144			t.Fatalf("invalid json: %v\nraw:\n%s", err, stdout.String())
145		}
146		if len(payload.Files) == 0 {
147			t.Errorf("expected at least one file entry")
148		}
149	}
150
151	func TestDiffSemanticIncluded(t *testing.T) {
152		srv := fakeCrowTwoVersions(t)
153		root := NewRootCmd()
154		var stdout bytes.Buffer
155		root.SetOut(&stdout)
156		root.SetArgs([]string{
157			"diff",
158			"iac/comply/aws", "1.0.0", "1.1.0",
159			"--crow-url", srv.URL,
160			"--plain",
161			"--semantic",
162			"--cache-dir", t.TempDir(),
163		})
164		if err := root.ExecuteContext(context.Background()); err != nil {
165			t.Fatalf("execute: %v", err)
166		}
167		out := stdout.String()
168		for _, want := range []string{
169			"=== semantic changes ===",
170			"resource",
171			"aws_s3_bucket.alpha",
172			"aws_s3_bucket.beta",
173			"variable",
174			"name",
175		} {
176			if !strings.Contains(out, want) {
177				t.Errorf("semantic output missing %q\n--- output ---\n%s", want, out)
178			}
179		}
180	}
181


Now let me check the `known` package to understand its API:


Error: EISDIR: illegal operation on a directory, read '/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/internal/known'


Bash

List known package files


drwxr-xr-x steve.briggs staff 224 B  Wed May 13 11:34:20 2026 ..
.rw-r--r-- steve.briggs staff 4.2 KB Wed May 13 11:34:56 2026 known.go
.rw-r--r-- steve.briggs staff 4.5 KB Wed May 13 11:41:20 2026 known_test.go
drwxr-xr-x steve.briggs staff 128 B  Wed May 13 11:41:20 2026 .


1	// Package known persists a sorted, deduplicated list of Crow module
2	// addresses the user has successfully diff'd or browsed, so future
3	// invocations can offer shell-completion and TUI suggestions.
4	//
5	// The on-disk format is plain text, one ns/name/provider per line,
6	// alphabetically sorted, with a trailing newline. Empty or missing
7	// file means "no suggestions yet" — not an error.
8	//
9	// Concurrent writers: last writer wins. Two birdseye processes running
10	// simultaneously may each see the file before the other's Rename
11	// lands; whichever Rename completes last sets the final contents.
12	// Acceptable for a low-frequency, append-only-ish list.
13	package known
14
15	import (
16		"bufio"
17		"fmt"
18		"os"
19		"path/filepath"
20		"sort"
21		"strings"
22
23		"github.com/immuta/operations/services/crow/pkg/api"
24	)
25
26	// Path returns the default known-modules file path, honoring
27	// os.UserConfigDir() (XDG_CONFIG_HOME on Linux, Application Support on
28	// macOS). The directory is NOT created here — Add creates it lazily on
29	// first write.
30	func Path() (string, error) {
31		cfg, err := os.UserConfigDir()
32		if err != nil {
33			return "", fmt.Errorf("resolve user config dir: %w", err)
34		}
35		return filepath.Join(cfg, "birdseye", "known-modules"), nil
36	}
37
38	// Load reads the file at path. Returns an empty slice if the file
39	// does not exist (first run is normal). Lines failing the
40	// ns/name/provider validation are silently skipped. The result is
41	// sorted ascending.
42	func Load(path string) ([]string, error) {
43		f, err := os.Open(path)
44		if err != nil {
45			if os.IsNotExist(err) {
46				return nil, nil
47			}
48			return nil, fmt.Errorf("open known-modules: %w", err)
49		}
50		defer f.Close()
51
52		var out []string
53		scanner := bufio.NewScanner(f)
54		for scanner.Scan() {
55			line := strings.TrimSpace(scanner.Text())
56			if line == "" {
57				continue
58			}
59			if !validAddress(line) {
60				continue
61			}
62			out = append(out, line)
63		}
64		if err := scanner.Err(); err != nil {
65			return nil, fmt.Errorf("scan known-modules: %w", err)
66		}
67		sort.Strings(out)
68		return out, nil
69	}
70
71	// Add inserts address into the file at path. Existing entries are
72	// preserved; duplicates deduplicated; the final list is sorted.
73	// Writes are atomic via temp file + rename in the same directory.
74	// The directory is created with 0o755 if missing. If address is not a
75	// well-formed ns/name/provider triple, Add returns an error without
76	// touching the file.
77	func Add(path, address string) error {
78		if !validAddress(address) {
79			return fmt.Errorf("invalid module address %q", address)
80		}
81		if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
82			return fmt.Errorf("mkdir known-modules dir: %w", err)
83		}
84
85		existing, err := Load(path)
86		if err != nil {
87			return err
88		}
89		merged := mergeAndSort(existing, address)
90
91		tmp, err := os.CreateTemp(filepath.Dir(path), ".known-modules-*.tmp")
92		if err != nil {
93			return fmt.Errorf("create temp: %w", err)
94		}
95		tmpName := tmp.Name()
96		w := bufio.NewWriter(tmp)
97		for _, addr := range merged {
98			if _, err := w.WriteString(addr); err != nil {
99				_ = tmp.Close()
100				_ = os.Remove(tmpName)
101				return fmt.Errorf("write temp: %w", err)
102			}
103			if err := w.WriteByte('\n'); err != nil {
104				_ = tmp.Close()
105				_ = os.Remove(tmpName)
106				return fmt.Errorf("write temp: %w", err)
107			}
108		}
109		if err := w.Flush(); err != nil {
110			_ = tmp.Close()
111			_ = os.Remove(tmpName)
112			return fmt.Errorf("flush temp: %w", err)
113		}
114		if err := tmp.Close(); err != nil {
115			_ = os.Remove(tmpName)
116			return fmt.Errorf("close temp: %w", err)
117		}
118		if err := os.Rename(tmpName, path); err != nil {
119			_ = os.Remove(tmpName)
120			return fmt.Errorf("rename temp: %w", err)
121		}
122		return nil
123	}
124
125	// mergeAndSort returns a deduplicated, sorted slice combining existing
126	// with the new address.
127	func mergeAndSort(existing []string, address string) []string {
128		seen := make(map[string]struct{}, len(existing)+1)
129		out := make([]string, 0, len(existing)+1)
130		for _, a := range existing {
131			if _, dup := seen[a]; dup {
132				continue
133			}
134			seen[a] = struct{}{}
135			out = append(out, a)
136		}
137		if _, dup := seen[address]; !dup {
138			out = append(out, address)
139		}
140		sort.Strings(out)
141		return out
142	}
143
144	// validAddress checks that s is exactly three slash-separated segments
145	// each passing api.ValidateSegments.
146	func validAddress(s string) bool {
147		parts := strings.Split(s, "/")
148		if len(parts) != 3 {
149			return false
150		}
151		return api.ValidateSegments(parts) == nil
152	}
153


Now I have everything I need. Let me implement the changes in the correct order:

**Step 1: Add the new tests to `diff_test.go`**


Edit

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
)


The file /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff_test.go has been updated successfully. (file state is current in your context — no need to Read it back)


Now append the two new test functions to the end of the test file:


func TestDiffSemanticIncluded(t *testing.T) {
	srv := fakeCrowTwoVersions(t)
	root := NewRootCmd()
	var stdout bytes.Buffer
	root.SetOut(&stdout)
	root.SetArgs([]string{
		"diff",
		"iac/comply/aws", "1.0.0", "1.1.0",
		"--crow-url", srv.URL,
		"--plain",
		"--semantic",
		"--cache-dir", t.TempDir(),
	})
	if err := root.ExecuteContext(context.Background()); err != nil {
		t.Fatalf("execute: %v", err)
	}
	out := stdout.String()
	for _, want := range []string{
		"=== semantic changes ===",
		"resource",
		"aws_s3_bucket.alpha",
		"aws_s3_bucket.beta",
		"variable",
		"name",
	} {
		if !strings.Contains(out, want) {
			t.Errorf("semantic output missing %q\n--- output ---\n%s", want, out)
		}
	}
}

func TestDiffSemanticIncluded(t *testing.T) {
	srv := fakeCrowTwoVersions(t)
	root := NewRootCmd()
	var stdout bytes.Buffer
	root.SetOut(&stdout)
	root.SetArgs([]string{
		"diff",
		"iac/comply/aws", "1.0.0", "1.1.0",
		"--crow-url", srv.URL,
		"--plain",
		"--semantic",
		"--cache-dir", t.TempDir(),
	})
	if err := root.ExecuteContext(context.Background()); err != nil {
		t.Fatalf("execute: %v", err)
	}
	out := stdout.String()
	for _, want := range []string{
		"=== semantic changes ===",
		"resource",
		"aws_s3_bucket.alpha",
		"aws_s3_bucket.beta",
		"variable",
		"name",
	} {
		if !strings.Contains(out, want) {
			t.Errorf("semantic output missing %q\n--- output ---\n%s", want, out)
		}
	}
}

func TestDiffAddsToKnownAfterSuccess(t *testing.T) {
	srv := fakeCrowTwoVersions(t)
	knownFile := filepath.Join(t.TempDir(), "known-modules")
	root := NewRootCmd()
	var stdout bytes.Buffer
	root.SetOut(&stdout)
	root.SetArgs([]string{
		"diff",
		"iac/comply/aws", "1.0.0", "1.1.0",
		"--crow-url", srv.URL,
		"--plain",
		"--cache-dir", t.TempDir(),
		"--known-modules-file", knownFile,
	})
	if err := root.ExecuteContext(context.Background()); err != nil {
		t.Fatalf("execute: %v", err)
	}
	body, err := os.ReadFile(knownFile)
	if err != nil {
		t.Fatalf("known-modules file not created: %v", err)
	}
	if !strings.Contains(string(body), "iac/comply/aws") {
		t.Errorf("known-modules missing address:\n%s", body)
	}
}

func TestDiffDoesNotAddOnFailure(t *testing.T) {
	mux := http.NewServeMux()
	// All download attempts return 404
	mux.HandleFunc("/v1/modules/", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusNotFound)
	})
	srv := httptest.NewServer(mux)
	t.Cleanup(srv.Close)

	knownFile := filepath.Join(t.TempDir(), "known-modules")
	root := NewRootCmd()
	root.SetErr(&bytes.Buffer{})
	root.SetArgs([]string{
		"diff",
		"iac/comply/aws", "1.0.0", "1.1.0",
		"--crow-url", srv.URL,
		"--plain",
		"--cache-dir", t.TempDir(),
		"--known-modules-file", knownFile,
	})
	if err := root.ExecuteContext(context.Background()); err == nil {
		t.Fatal("expected error for 404s")
	}
	if _, statErr := os.Stat(knownFile); !os.IsNotExist(statErr) {
		t.Errorf("known-modules file should NOT exist after failure, stat err=%v", statErr)
	}
}


**Step 2: Run to confirm failure (unknown flag)**


Bash

cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test ./cmd/... -run TestDiffAddsToKnown -v 2>&1

Run new tests expecting failure on unknown flag


Error: Exit code 1
=== RUN   TestDiffAddsToKnownAfterSuccess
    diff_test.go:199: execute: unknown flag: --known-modules-file
--- FAIL: TestDiffAddsToKnownAfterSuccess (0.00s)
FAIL
FAIL	github.com/immuta/operations/apps/birdseye/cmd	4.464s
FAIL


Good, fails as expected. Now implement the changes in `diff.go`:

**Step 3: Update `diff.go`**


Edit

	"github.com/immuta/operations/apps/birdseye/internal/archive"
	"github.com/immuta/operations/apps/birdseye/internal/diff"
	"github.com/immuta/operations/apps/birdseye/internal/registry"
	"github.com/immuta/operations/apps/birdseye/internal/tui"
	"github.com/immuta/operations/services/crow/pkg/api"
	"github.com/immuta/operations/services/crow/pkg/client"
)

	"github.com/immuta/operations/apps/birdseye/internal/archive"
	"github.com/immuta/operations/apps/birdseye/internal/diff"
	"github.com/immuta/operations/apps/birdseye/internal/known"
	"github.com/immuta/operations/apps/birdseye/internal/registry"
	"github.com/immuta/operations/apps/birdseye/internal/tui"
	"github.com/immuta/operations/services/crow/pkg/api"
	"github.com/immuta/operations/services/crow/pkg/client"
)


The file /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff.go has been updated successfully. (file state is current in your context — no need to Read it back)


// diffOpts holds the resolved options for `birdseye diff`.
type diffOpts struct {
	Semantic bool
	Plain    bool
	CacheDir string
}

// diffOpts holds the resolved options for `birdseye diff`.
type diffOpts struct {
	Semantic         bool
	Plain            bool
	CacheDir         string
	KnownModulesFile string // test override for known-modules cache path
}


	c.Flags().BoolVar(&o.Semantic, "semantic", false, "include HCL-aware semantic diff alongside textual")
	c.Flags().BoolVar(&o.Plain, "plain", false, "force plain (non-TUI) output")
	c.Flags().BoolVar(&o.Plain, "no-tui", false, "alias for --plain")
	c.Flags().StringVar(&o.CacheDir, "cache-dir", defaultCacheDir(), "module tarball cache directory")
	return c
}

	c.Flags().BoolVar(&o.Semantic, "semantic", false, "include HCL-aware semantic diff alongside textual")
	c.Flags().BoolVar(&o.Plain, "plain", false, "force plain (non-TUI) output")
	c.Flags().BoolVar(&o.Plain, "no-tui", false, "alias for --plain")
	c.Flags().StringVar(&o.CacheDir, "cache-dir", defaultCacheDir(), "module tarball cache directory")
	c.Flags().StringVar(&o.KnownModulesFile, "known-modules-file", "", "override known-modules autocomplete file (mostly for tests)")
	_ = c.Flags().MarkHidden("known-modules-file")
	return c
}


Now add the `resolveKnownPath` helper after `defaultCacheDir`:


func defaultCacheDir() string {
	if home, err := os.UserCacheDir(); err == nil {
		return filepath.Join(home, "birdseye")
	}
	return ".birdseye-cache"
}

func defaultCacheDir() string {
	if home, err := os.UserCacheDir(); err == nil {
		return filepath.Join(home, "birdseye")
	}
	return ".birdseye-cache"
}

// resolveKnownPath returns override if non-empty, otherwise the default
// known.Path(). Errors fall back to "" so Load and Add become no-ops.
func resolveKnownPath(override string) string {
	if override != "" {
		return override
	}
	p, err := known.Path()
	if err != nil {
		return ""
	}
	return p
}


Now insert the `known.Add` call in `runDiff` between the semantic block and the render dispatch:


	result := diff.Result{Files: diff.Textual(modAExtracted, modBExtracted)}
	if o.Semantic {
		sem, err := diff.Semantic(modAExtracted, modBExtracted)
		if err != nil {
			return fmt.Errorf("%w: %v", ErrSemantic, err)
		}
		result.Semantic = sem
	}

	if g.JSON {
		return renderJSON(cmd.OutOrStdout(), result)
	}
	if !o.Plain && isTTY(cmd.OutOrStdout()) {
		return tui.RunBrowser(ctx, result)
	}
	return renderPlain(cmd.OutOrStdout(), result)
}

	result := diff.Result{Files: diff.Textual(modAExtracted, modBExtracted)}
	if o.Semantic {
		sem, err := diff.Semantic(modAExtracted, modBExtracted)
		if err != nil {
			return fmt.Errorf("%w: %v", ErrSemantic, err)
		}
		result.Semantic = sem
	}

	if err := known.Add(resolveKnownPath(o.KnownModulesFile),
		id.Namespace+"/"+id.Name+"/"+id.Provider); err != nil {
		_ = err // best-effort: cache failure does not block diff
	}

	if g.JSON {
		return renderJSON(cmd.OutOrStdout(), result)
	}
	if !o.Plain && isTTY(cmd.OutOrStdout()) {
		return tui.RunBrowser(ctx, result)
	}
	return renderPlain(cmd.OutOrStdout(), result)
}


**Step 4: Run the full test suite with -race**


Bash

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

Run all cmd tests with race detector


=== RUN   TestDiffPlainOutput
--- PASS: TestDiffPlainOutput (0.01s)
=== RUN   TestDiffIdenticalVersionsNoChanges
--- PASS: TestDiffIdenticalVersionsNoChanges (0.01s)
=== RUN   TestDiffBadModuleAddress
--- PASS: TestDiffBadModuleAddress (0.00s)
=== RUN   TestDiffJSONOutput
--- PASS: TestDiffJSONOutput (0.01s)
=== RUN   TestDiffSemanticIncluded
--- PASS: TestDiffSemanticIncluded (0.01s)
=== RUN   TestDiffAddsToKnownAfterSuccess
--- PASS: TestDiffAddsToKnownAfterSuccess (0.01s)
=== RUN   TestDiffDoesNotAddOnFailure
--- PASS: TestDiffDoesNotAddOnFailure (0.00s)
=== RUN   TestExitCode
=== RUN   TestExitCode/nil
=== RUN   TestExitCode/not-found
=== RUN   TestExitCode/wrapped-not-found
=== RUN   TestExitCode/context-cancelled
=== RUN   TestExitCode/context-deadline
=== RUN   TestExitCode/generic
--- PASS: TestExitCode (0.00s)
    --- PASS: TestExitCode/nil (0.00s)
    --- PASS: TestExitCode/not-found (0.00s)
    --- PASS: TestExitCode/wrapped-not-found (0.00s)
    --- PASS: TestExitCode/context-cancelled (0.00s)
    --- PASS: TestExitCode/context-deadline (0.00s)
    --- PASS: TestExitCode/generic (0.00s)
=== RUN   TestNewRootCmdHelp
--- PASS: TestNewRootCmdHelp (0.00s)
=== RUN   TestNewRootCmdDefaults
--- PASS: TestNewRootCmdDefaults (0.00s)
=== RUN   TestNewRootCmdRespectsCrowURLEnv
--- PASS: TestNewRootCmdRespectsCrowURLEnv (0.00s)
=== RUN   TestVersionsCmd
--- PASS: TestVersionsCmd (0.00s)
PASS
ok  	github.com/immuta/operations/apps/birdseye/cmd	5.469s


All 13 tests pass with the race detector. Now verify build and vet:

**Step 5: Verify build**


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

Run go vet and go build


(Bash completed with no output)


Clean. **Step 6: Commit**


git add apps/birdseye/cmd/ && git commit -m "$(cat <<'EOF'
feat(birdseye): cache module address after successful CLI diff

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EOF
)"

Commit Task 3 changes


[worktree-birdseye 51a2c0e] feat(birdseye): cache module address after successful CLI diff
 2 files changed, 81 insertions(+), 3 deletions(-)


All done, Sir Charles of St Cheese. Here's the summary:

---

**Status:** DONE

**Files modified:**
- `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff.go` — added `KnownModulesFile` field to `diffOpts`, registered hidden `--known-modules-file` flag, added `resolveKnownPath` helper, inserted best-effort `known.Add(...)` call between the semantic block and the render dispatch
- `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/diff_test.go` — added `os` and `path/filepath` imports, appended `TestDiffAddsToKnownAfterSuccess` and `TestDiffDoesNotAddOnFailure`

**Test count:** 13 tests in `./cmd/...`, all PASS, race-clean (`go test -race ./cmd/... -v`)

**Build:** `go vet ./...` and `go build ./...` both clean

**Commit SHA:** `51a2c0e`

**Nothing unexpected** — the `known` package's `Add` function accepts an empty path string and returns an error (which is swallowed via `_ = err`), so the no-op path when `resolveKnownPath` returns `""` is safe. The 404 failure test confirms no file is created when `FetchPair` fails before the Add call is ever reached.