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

## Working Environment

- **Worktree path:** `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`
- **Git branch:** `worktree-birdseye` (already checked out)
- **HEAD:** `474d56d` (most recent: nested HCL block hashing fix)
- All paths in this prompt resolve inside the worktree. Use absolute paths or `cd` into the worktree.
- **NEVER push.** **NEVER use `git add .` or `git add -A`.** Always `git add` specific paths.
- Local Go: 1.26.2; module directive is 1.25.5.

## Context

`birdseye` already exists as a Go CLI in `apps/birdseye/`. Sibling internal packages (`internal/archive`, `internal/registry`, `internal/diff`, `internal/tui`) follow standard Go layout: one production file + one test file per concept, table-driven tests without testify, package doc on the first file.

This task creates a NEW package `apps/birdseye/internal/known/` for persisting module addresses the user has successfully diff'd, so future invocations can offer them as autocomplete suggestions. Spec: `docs/superpowers/specs/2026-05-13-birdseye-known-modules-design.md` (in the worktree).

The package depends on `github.com/immuta/operations/services/crow/pkg/api` for `ValidateSegments`. That import already resolves — it's used by `apps/birdseye/cmd/diff.go` and `apps/birdseye/internal/registry/registry.go`.

Tasks 2-5 add hardening tests, CLI wiring, and TUI wiring. Your job is just the new package with happy-path tests.

## Task Description (Verbatim)

### Task 1: `internal/known` — Path, Load, Add (happy paths)

**Files:**
- Create: `apps/birdseye/internal/known/known.go`
- Create: `apps/birdseye/internal/known/known_test.go`

- [ ] **Step 1: Write failing test** at `apps/birdseye/internal/known/known_test.go`:

```go
package known

import (
	"os"
	"path/filepath"
	"testing"
)

func TestLoadMissingFileReturnsEmpty(t *testing.T) {
	dir := t.TempDir()
	got, err := Load(filepath.Join(dir, "missing"))
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if len(got) != 0 {
		t.Errorf("expected empty slice, got %v", got)
	}
}

func TestAddCreatesDirectoryAndFile(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "subdir", "known-modules")
	if err := Add(path, "iac/comply/aws"); err != nil {
		t.Fatalf("Add: %v", err)
	}
	body, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("ReadFile: %v", err)
	}
	if string(body) != "iac/comply/aws\n" {
		t.Errorf("body = %q", body)
	}
}

func TestAddThenLoadRoundTrip(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "known-modules")
	if err := Add(path, "iac/comply/aws"); err != nil {
		t.Fatalf("Add: %v", err)
	}
	got, err := Load(path)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if len(got) != 1 || got[0] != "iac/comply/aws" {
		t.Errorf("Load = %v, want [iac/comply/aws]", got)
	}
}

func TestPathHonorsUserConfigDir(t *testing.T) {
	got, err := Path()
	if err != nil {
		t.Fatalf("Path: %v", err)
	}
	if !filepath.IsAbs(got) {
		t.Errorf("expected absolute path, got %q", got)
	}
	base := filepath.Base(got)
	if base != "known-modules" {
		t.Errorf("expected basename %q, got %q", "known-modules", base)
	}
	parent := filepath.Base(filepath.Dir(got))
	if parent != "birdseye" {
		t.Errorf("expected parent dir %q, got %q", "birdseye", parent)
	}
}
```

- [ ] **Step 2: Run, expect FAIL** (`undefined: Load`, etc.)

Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test ./internal/known/...`
Expected: compile error.

- [ ] **Step 3: Implement `apps/birdseye/internal/known/known.go`**

```go
// Package known persists a sorted, deduplicated list of Crow module
// addresses the user has successfully diff'd or browsed, so future
// invocations can offer shell-completion and TUI suggestions.
//
// The on-disk format is plain text, one ns/name/provider per line,
// alphabetically sorted, with a trailing newline. Empty or missing
// file means "no suggestions yet" — not an error.
//
// Concurrent writers: last writer wins. Two birdseye processes running
// simultaneously may each see the file before the other's Rename
// lands; whichever Rename completes last sets the final contents.
// Acceptable for a low-frequency, append-only-ish list.
package known

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/immuta/operations/services/crow/pkg/api"
)

// Path returns the default known-modules file path, honoring
// os.UserConfigDir() (XDG_CONFIG_HOME on Linux, Application Support on
// macOS). The directory is NOT created here — Add creates it lazily on
// first write.
func Path() (string, error) {
	cfg, err := os.UserConfigDir()
	if err != nil {
		return "", fmt.Errorf("resolve user config dir: %w", err)
	}
	return filepath.Join(cfg, "birdseye", "known-modules"), nil
}

// Load reads the file at path. Returns an empty slice if the file
// does not exist (first run is normal). Lines failing the
// ns/name/provider validation are silently skipped. The result is
// sorted ascending.
func Load(path string) ([]string, error) {
	f, err := os.Open(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, fmt.Errorf("open known-modules: %w", err)
	}
	defer f.Close()

	var out []string
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		if !validAddress(line) {
			continue
		}
		out = append(out, line)
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("scan known-modules: %w", err)
	}
	sort.Strings(out)
	return out, nil
}

// Add inserts address into the file at path. Existing entries are
// preserved; duplicates deduplicated; the final list is sorted.
// Writes are atomic via temp file + rename in the same directory.
// The directory is created with 0o755 if missing. If address is not a
// well-formed ns/name/provider triple, Add returns an error without
// touching the file.
func Add(path, address string) error {
	if !validAddress(address) {
		return fmt.Errorf("invalid module address %q", address)
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return fmt.Errorf("mkdir known-modules dir: %w", err)
	}

	existing, err := Load(path)
	if err != nil {
		return err
	}
	merged := mergeAndSort(existing, address)

	tmp, err := os.CreateTemp(filepath.Dir(path), ".known-modules-*.tmp")
	if err != nil {
		return fmt.Errorf("create temp: %w", err)
	}
	tmpName := tmp.Name()
	w := bufio.NewWriter(tmp)
	for _, addr := range merged {
		if _, err := w.WriteString(addr); err != nil {
			_ = tmp.Close()
			_ = os.Remove(tmpName)
			return fmt.Errorf("write temp: %w", err)
		}
		if err := w.WriteByte('\n'); err != nil {
			_ = tmp.Close()
			_ = os.Remove(tmpName)
			return fmt.Errorf("write temp: %w", err)
		}
	}
	if err := w.Flush(); err != nil {
		_ = tmp.Close()
		_ = os.Remove(tmpName)
		return fmt.Errorf("flush temp: %w", err)
	}
	if err := tmp.Close(); err != nil {
		_ = os.Remove(tmpName)
		return fmt.Errorf("close temp: %w", err)
	}
	if err := os.Rename(tmpName, path); err != nil {
		_ = os.Remove(tmpName)
		return fmt.Errorf("rename temp: %w", err)
	}
	return nil
}

// mergeAndSort returns a deduplicated, sorted slice combining existing
// with the new address.
func mergeAndSort(existing []string, address string) []string {
	seen := make(map[string]struct{}, len(existing)+1)
	out := make([]string, 0, len(existing)+1)
	for _, a := range existing {
		if _, dup := seen[a]; dup {
			continue
		}
		seen[a] = struct{}{}
		out = append(out, a)
	}
	if _, dup := seen[address]; !dup {
		out = append(out, address)
	}
	sort.Strings(out)
	return out
}

// validAddress checks that s is exactly three slash-separated segments
// each passing api.ValidateSegments.
func validAddress(s string) bool {
	parts := strings.Split(s, "/")
	if len(parts) != 3 {
		return false
	}
	return api.ValidateSegments(parts) == nil
}
```

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

Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race ./internal/known/... -v`
Expected: All 4 tests PASS.

- [ ] **Step 5: Commit**

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

## Notes & Style

- Strict TDD: write the test file first, run it (compile error expected), THEN write `known.go`.
- The `api.ValidateSegments` import resolves via the existing `replace` directive in `apps/birdseye/go.mod`.
- Project uses stdlib `testing` only — no testify. Use `t.Errorf`/`t.Fatalf` patterns matching the existing test files.
- `bufio.Scanner` is the idiomatic line reader; default token size of 64K is more than sufficient.
- Atomic write pattern (temp + rename) mirrors what `apps/birdseye/internal/registry/cache.go` `DiskCache.Put` already does — read it if you want a reference.

## Code Organization

One file per concept. `known.go` is fine as a single file containing Path/Load/Add/mergeAndSort/validAddress — don't split into helper files.

## Before You Begin

If anything is unclear, ask. Otherwise proceed.

## Self-Review Checklist

- [ ] All 4 tests pass under `-race`
- [ ] `go vet ./internal/known/...` clean
- [ ] Package doc on `known.go` describes the package purpose
- [ ] No new third-party dependencies introduced (go.mod unchanged except possibly go.sum)
- [ ] Commit message exactly: `feat(birdseye): known-modules autocomplete cache with Path/Load/Add`
- [ ] Only `apps/birdseye/internal/known/` files staged (plus go.sum if it changed)

## Report Format

- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
- Files created
- Test count + race-clean status
- go.mod/go.sum changes
- 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 1 following strict TDD. Let me start by reading the spec and reference files.


Bash

List existing internal packages


drwxr-xr-x steve.briggs staff 128 B Tue May 12 23:28:06 2026 archive
drwxr-xr-x steve.briggs staff 192 B Tue May 12 23:37:34 2026 registry
drwxr-xr-x steve.briggs staff 192 B Tue May 12 23:55:20 2026 .
drwxr-xr-x steve.briggs staff 352 B Wed May 13 00:31:04 2026 ..
drwxr-xr-x steve.briggs staff 256 B Wed May 13 11:02:26 2026 tui
drwxr-xr-x steve.briggs staff 224 B Wed May 13 11:07:04 2026 diff


1	// Package registry talks to a Crow server through its typed client and
2	// caches module tarballs on disk so repeated diffs against the same
3	// versions stay fast.
4	package registry
5
6	import (
7		"errors"
8		"fmt"
9		"os"
10		"path/filepath"
11		"sync"
12
13		"github.com/immuta/operations/services/crow/pkg/api"
14	)
15
16	// Cache is the storage seam used by Registry. Production uses DiskCache;
17	// tests inject MemCache.
18	type Cache interface {
19		Get(api.Module) ([]byte, bool, error)
20		Put(api.Module, []byte) error
21	}
22
23	// MemCache is a thread-safe in-memory Cache implementation.
24	type MemCache struct {
25		mu    sync.RWMutex
26		items map[string][]byte
27	}
28
29	// NewMemCache returns an empty MemCache.
30	func NewMemCache() *MemCache {
31		return &MemCache{items: make(map[string][]byte)}
32	}
33
34	func memKey(m api.Module) string {
35		return m.Namespace + "/" + m.Name + "/" + m.Provider + "/" + m.Version
36	}
37
38	func (c *MemCache) Get(m api.Module) ([]byte, bool, error) {
39		c.mu.RLock()
40		defer c.mu.RUnlock()
41		b, ok := c.items[memKey(m)]
42		if !ok {
43			return nil, false, nil
44		}
45		return b, true, nil
46	}
47
48	func (c *MemCache) Put(m api.Module, body []byte) error {
49		c.mu.Lock()
50		defer c.mu.Unlock()
51		c.items[memKey(m)] = body
52		return nil
53	}
54
55	// DiskCache stores tarballs under
56	//
57	//	<root>/<namespace>/<name>/<provider>/<version>.tar.gz
58	//
59	// Writes are atomic via a temp file + rename.
60	type DiskCache struct {
61		root string
62	}
63
64	// NewDiskCache creates the root directory if it does not exist.
65	func NewDiskCache(root string) (*DiskCache, error) {
66		if err := os.MkdirAll(root, 0o755); err != nil {
67			return nil, fmt.Errorf("mkdir cache root: %w", err)
68		}
69		return &DiskCache{root: root}, nil
70	}
71
72	func (c *DiskCache) path(m api.Module) string {
73		return filepath.Join(c.root, m.Namespace, m.Name, m.Provider, m.Version+".tar.gz")
74	}
75
76	func (c *DiskCache) Get(m api.Module) ([]byte, bool, error) {
77		b, err := os.ReadFile(c.path(m))
78		if errors.Is(err, os.ErrNotExist) {
79			return nil, false, nil
80		}
81		if err != nil {
82			return nil, false, fmt.Errorf("read cache: %w", err)
83		}
84		return b, true, nil
85	}
86
87	func (c *DiskCache) Put(m api.Module, body []byte) error {
88		final := c.path(m)
89		if err := os.MkdirAll(filepath.Dir(final), 0o755); err != nil {
90			return fmt.Errorf("mkdir cache dir: %w", err)
91		}
92		tmp, err := os.CreateTemp(filepath.Dir(final), ".birdseye-*.tmp")
93		if err != nil {
94			return fmt.Errorf("create temp: %w", err)
95		}
96		tmpName := tmp.Name()
97		if _, err := tmp.Write(body); err != nil {
98			_ = tmp.Close()
99			_ = os.Remove(tmpName)
100			return fmt.Errorf("write temp: %w", err)
101		}
102		if err := tmp.Close(); err != nil {
103			_ = os.Remove(tmpName)
104			return fmt.Errorf("close temp: %w", err)
105		}
106		if err := os.Rename(tmpName, final); err != nil {
107			_ = os.Remove(tmpName)
108			return fmt.Errorf("rename: %w", err)
109		}
110		return nil
111	}
112


Good. Now let me check the `api.ValidateSegments` function to understand what it expects:


find /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye -path "*/crow/pkg/api*" -name "*.go" 2>/dev/null | head -20

Find crow api package files


List birdseye app directory


drwxr-xr-x steve.briggs staff 256 B  Tue May 12 18:19:50 2026 ..
.rw-r--r-- steve.briggs staff 226 B  Tue May 12 18:19:59 2026 Makefile
.rw-r--r-- steve.briggs staff   6 B  Tue May 12 18:20:01 2026 .gitignore
.rw-r--r-- steve.briggs staff 602 B  Tue May 12 18:20:06 2026 main.go
drwxr-xr-x steve.briggs staff 192 B  Tue May 12 23:55:20 2026 internal
.rw-r--r-- steve.briggs staff 8.7 KB Wed May 13 00:20:22 2026 go.sum
.rw-r--r-- steve.briggs staff 2.1 KB Wed May 13 00:20:22 2026 go.mod
.rw-r--r-- steve.briggs staff 1.3 KB Wed May 13 00:29:13 2026 README.md
drwxr-xr-x steve.briggs staff 352 B  Wed May 13 00:31:04 2026 .
drwxr-xr-x steve.briggs staff  96 B  Wed May 13 00:35:19 2026 bin
drwxr-xr-x steve.briggs staff 320 B  Wed May 13 11:02:01 2026 cmd


1	// Package api declares the Crow HTTP wire protocol: request/response types
2	// and segment/path helpers. Both the Crow server and its clients import this
3	// package so they cannot drift on what a valid module path or response shape
4	// looks like.
5	package api
6
7	type ModuleIdentifier struct {
8		Namespace string
9		Name      string
10		Provider  string
11	}
12
13	type Module struct {
14		ModuleIdentifier
15		Version string
16	}
17
18	type VersionEntry struct {
19		Version string `json:"version"`
20	}
21
22	type ModuleVersions struct {
23		Versions []VersionEntry `json:"versions"`
24	}
25
26	type ListVersionsResponse struct {
27		Modules []ModuleVersions `json:"modules"`
28	}
29
30	type ErrorResponse struct {
31		Error string `json:"error"`
32	}
33
34	// ServiceDiscoveryResponse is the body of /.well-known/terraform.json. The
35	// "modules.v1" key is dictated by HashiCorp's Terraform service discovery
36	// protocol and cannot be renamed to camelCase.
37	// See https://developer.hashicorp.com/terraform/internals/remote-service-discovery
38	type ServiceDiscoveryResponse struct {
39		ModulesV1 string `json:"modules.v1"` //nolint:tagliatelle // external wire format
40	}
41


1	package api
2
3	import (
4		"fmt"
5		"regexp"
6		"strings"
7	)
8
9	// segmentRegex matches a single path segment: starts with alphanumeric,
10	// followed by alphanumeric, dots, underscores, or hyphens. It is the single
11	// source of truth for segment validation — the Crow server and its clients
12	// both import this package so they cannot drift.
13	var segmentRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`)
14
15	// versionSemverRegex is the SemVer 2.0.0 grammar from semver.org, minus the
16	// build metadata suffix. Two intentional deviations from the spec:
17	//   - No leading 'v' allowed. Terraform's native module loader strips the
18	//     prefix when building download URLs (per hashicorp/go-version
19	//     normalization), while terragrunt's tfr:// passes the version through
20	//     verbatim. A v-prefixed version uploaded today would resolve via
21	//     terragrunt and 404 via terraform — failing fast on upload prevents
22	//     the divergence.
23	//   - No '+build.metadata' suffix allowed. segmentRegex above doesn't
24	//     permit '+', so a build-tagged version couldn't be served from the
25	//     URL even if we accepted it on upload.
26	var versionSemverRegex = regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?$`)
27
28	type InvalidSegmentError struct {
29		Segment string
30	}
31
32	func (e InvalidSegmentError) Error() string {
33		return fmt.Sprintf("invalid segment %q: must match %s", e.Segment, segmentRegex)
34	}
35
36	// InvalidVersionError is returned when a version string is not valid SemVer
37	// 2.0.0 (no leading 'v', no '+build' metadata).
38	type InvalidVersionError struct {
39		Version string
40	}
41
42	func (e InvalidVersionError) Error() string {
43		return fmt.Sprintf("invalid version %q: expected bare SemVer 2.0.0 (e.g. \"1.2.3\", \"1.2.3-rc.1\") per https://semver.org", e.Version)
44	}
45
46	// ValidateSegments returns an InvalidSegmentError for the first segment that
47	// fails the regex. An empty slice passes.
48	func ValidateSegments(segments []string) error {
49		for _, s := range segments {
50			if !segmentRegex.MatchString(s) {
51				return InvalidSegmentError{Segment: s}
52			}
53		}
54		return nil
55	}
56
57	// ValidateVersion enforces SemVer 2.0.0 on a module version string. It is
58	// applied at upload boundaries (server handler + client) so non-semver
59	// versions can't enter storage. Download/list paths intentionally don't
60	// call this so that any v-prefixed versions still in S3 from before the
61	// guard remain reachable.
62	func ValidateVersion(version string) error {
63		if !versionSemverRegex.MatchString(version) {
64			return InvalidVersionError{Version: version}
65		}
66		return nil
67	}
68
69	func (id ModuleIdentifier) Validate() error {
70		return ValidateSegments([]string{id.Namespace, id.Name, id.Provider})
71	}
72
73	func (id ModuleIdentifier) VersionsPath() string {
74		return "/v1/modules/" + strings.Join([]string{id.Namespace, id.Name, id.Provider}, "/") + "/versions"
75	}
76
77	func (m Module) Validate() error {
78		return ValidateSegments([]string{m.Namespace, m.Name, m.Provider, m.Version})
79	}
80
81	func (m Module) UploadPath() string {
82		return "/v1/modules/" + strings.Join([]string{m.Namespace, m.Name, m.Provider, m.Version}, "/")
83	}
84
85	func (m Module) DownloadPath() string {
86		return m.UploadPath() + "/download"
87	}
88


Good. Now I have everything I need. Let me proceed with the TDD steps.

**Step 1: Write the failing test file first.**


Create the known package directory


(Bash completed with no output)


Write

package known

import (
	"os"
	"path/filepath"
	"testing"
)

func TestLoadMissingFileReturnsEmpty(t *testing.T) {
	dir := t.TempDir()
	got, err := Load(filepath.Join(dir, "missing"))
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if len(got) != 0 {
		t.Errorf("expected empty slice, got %v", got)
	}
}

func TestAddCreatesDirectoryAndFile(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "subdir", "known-modules")
	if err := Add(path, "iac/comply/aws"); err != nil {
		t.Fatalf("Add: %v", err)
	}
	body, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("ReadFile: %v", err)
	}
	if string(body) != "iac/comply/aws\n" {
		t.Errorf("body = %q", body)
	}
}

func TestAddThenLoadRoundTrip(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "known-modules")
	if err := Add(path, "iac/comply/aws"); err != nil {
		t.Fatalf("Add: %v", err)
	}
	got, err := Load(path)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if len(got) != 1 || got[0] != "iac/comply/aws" {
		t.Errorf("Load = %v, want [iac/comply/aws]", got)
	}
}

func TestPathHonorsUserConfigDir(t *testing.T) {
	got, err := Path()
	if err != nil {
		t.Fatalf("Path: %v", err)
	}
	if !filepath.IsAbs(got) {
		t.Errorf("expected absolute path, got %q", got)
	}
	base := filepath.Base(got)
	if base != "known-modules" {
		t.Errorf("expected basename %q, got %q", "known-modules", base)
	}
	parent := filepath.Base(filepath.Dir(got))
	if parent != "birdseye" {
		t.Errorf("expected parent dir %q, got %q", "birdseye", parent)
	}
}


File created successfully at: /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/internal/known/known_test.go (file state is current in your context — no need to Read it back)


**Step 2: Run — expect compile error.**


Bash

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

Run tests expecting compile failure


Error: Exit code 1
# github.com/immuta/operations/apps/birdseye/internal/known [github.com/immuta/operations/apps/birdseye/internal/known.test]
internal/known/known_test.go:11:14: undefined: Load
internal/known/known_test.go:23:12: undefined: Add
internal/known/known_test.go:38:12: undefined: Add
internal/known/known_test.go:41:14: undefined: Load
internal/known/known_test.go:51:14: undefined: Path
FAIL	github.com/immuta/operations/apps/birdseye/internal/known [build failed]
FAIL


Compile error confirmed. **Step 3: Write the implementation.**


Write

// Package known persists a sorted, deduplicated list of Crow module
// addresses the user has successfully diff'd or browsed, so future
// invocations can offer shell-completion and TUI suggestions.
//
// The on-disk format is plain text, one ns/name/provider per line,
// alphabetically sorted, with a trailing newline. Empty or missing
// file means "no suggestions yet" — not an error.
//
// Concurrent writers: last writer wins. Two birdseye processes running
// simultaneously may each see the file before the other's Rename
// lands; whichever Rename completes last sets the final contents.
// Acceptable for a low-frequency, append-only-ish list.
package known

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/immuta/operations/services/crow/pkg/api"
)

// Path returns the default known-modules file path, honoring
// os.UserConfigDir() (XDG_CONFIG_HOME on Linux, Application Support on
// macOS). The directory is NOT created here — Add creates it lazily on
// first write.
func Path() (string, error) {
	cfg, err := os.UserConfigDir()
	if err != nil {
		return "", fmt.Errorf("resolve user config dir: %w", err)
	}
	return filepath.Join(cfg, "birdseye", "known-modules"), nil
}

// Load reads the file at path. Returns an empty slice if the file
// does not exist (first run is normal). Lines failing the
// ns/name/provider validation are silently skipped. The result is
// sorted ascending.
func Load(path string) ([]string, error) {
	f, err := os.Open(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, fmt.Errorf("open known-modules: %w", err)
	}
	defer f.Close()

	var out []string
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		if !validAddress(line) {
			continue
		}
		out = append(out, line)
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("scan known-modules: %w", err)
	}
	sort.Strings(out)
	return out, nil
}

// Add inserts address into the file at path. Existing entries are
// preserved; duplicates deduplicated; the final list is sorted.
// Writes are atomic via temp file + rename in the same directory.
// The directory is created with 0o755 if missing. If address is not a
// well-formed ns/name/provider triple, Add returns an error without
// touching the file.
func Add(path, address string) error {
	if !validAddress(address) {
		return fmt.Errorf("invalid module address %q", address)
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return fmt.Errorf("mkdir known-modules dir: %w", err)
	}

	existing, err := Load(path)
	if err != nil {
		return err
	}
	merged := mergeAndSort(existing, address)

	tmp, err := os.CreateTemp(filepath.Dir(path), ".known-modules-*.tmp")
	if err != nil {
		return fmt.Errorf("create temp: %w", err)
	}
	tmpName := tmp.Name()
	w := bufio.NewWriter(tmp)
	for _, addr := range merged {
		if _, err := w.WriteString(addr); err != nil {
			_ = tmp.Close()
			_ = os.Remove(tmpName)
			return fmt.Errorf("write temp: %w", err)
		}
		if err := w.WriteByte('\n'); err != nil {
			_ = tmp.Close()
			_ = os.Remove(tmpName)
			return fmt.Errorf("write temp: %w", err)
		}
	}
	if err := w.Flush(); err != nil {
		_ = tmp.Close()
		_ = os.Remove(tmpName)
		return fmt.Errorf("flush temp: %w", err)
	}
	if err := tmp.Close(); err != nil {
		_ = os.Remove(tmpName)
		return fmt.Errorf("close temp: %w", err)
	}
	if err := os.Rename(tmpName, path); err != nil {
		_ = os.Remove(tmpName)
		return fmt.Errorf("rename temp: %w", err)
	}
	return nil
}

// mergeAndSort returns a deduplicated, sorted slice combining existing
// with the new address.
func mergeAndSort(existing []string, address string) []string {
	seen := make(map[string]struct{}, len(existing)+1)
	out := make([]string, 0, len(existing)+1)
	for _, a := range existing {
		if _, dup := seen[a]; dup {
			continue
		}
		seen[a] = struct{}{}
		out = append(out, a)
	}
	if _, dup := seen[address]; !dup {
		out = append(out, address)
	}
	sort.Strings(out)
	return out
}

// validAddress checks that s is exactly three slash-separated segments
// each passing api.ValidateSegments.
func validAddress(s string) bool {
	parts := strings.Split(s, "/")
	if len(parts) != 3 {
		return false
	}
	return api.ValidateSegments(parts) == nil
}


File created successfully at: /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/internal/known/known.go (file state is current in your context — no need to Read it back)


**Step 4: Run with race detector — expect PASS.**


Bash

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

Run tests with race detector expecting all pass


=== RUN   TestLoadMissingFileReturnsEmpty
--- PASS: TestLoadMissingFileReturnsEmpty (0.00s)
=== RUN   TestAddCreatesDirectoryAndFile
--- PASS: TestAddCreatesDirectoryAndFile (0.00s)
=== RUN   TestAddThenLoadRoundTrip
--- PASS: TestAddThenLoadRoundTrip (0.00s)
=== RUN   TestPathHonorsUserConfigDir
--- PASS: TestPathHonorsUserConfigDir (0.00s)
PASS
ok  	github.com/immuta/operations/apps/birdseye/internal/known	1.680s


All 4 tests pass. Now run `go vet`:


cd /Users/steve.briggs/projects/operations/.claire/worktrees/birdseye/apps/birdseye 2>/dev/null; cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go vet ./internal/known/... 2>&1

Run go vet on the new package


Clean. Check go.mod/go.sum changes:


git diff apps/birdseye/go.mod apps/birdseye/go.sum 2>&1

Check if go.mod or go.sum changed


No changes to go.mod or go.sum. **Step 5: Commit.**


git add apps/birdseye/internal/known/ && git commit -m "$(cat <<'EOF'
feat(birdseye): known-modules autocomplete cache with Path/Load/Add

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

Commit the new known package


[worktree-birdseye 32e305b] feat(birdseye): known-modules autocomplete cache with Path/Load/Add
 2 files changed, 218 insertions(+)
 create mode 100644 apps/birdseye/internal/known/known.go
 create mode 100644 apps/birdseye/internal/known/known_test.go


**Status: DONE**

**Files created:**
- `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/internal/known/known.go`
- `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/internal/known/known_test.go`

**Test count:** 4 tests, all passing, race-clean (1.680s)

**go.mod/go.sum changes:** None — `api.ValidateSegments` was already a resolved dependency via the existing `replace` directive.

**Commit SHA:** `32e305b`

**Anything unexpected:** Nothing. The implementation is a clean single-file package. `validAddress` calls `api.ValidateSegments` with the three split parts, matching exactly what `ModuleIdentifier.Validate()` does in the crow api package. Atomic write pattern mirrors `DiskCache.Put` in `internal/registry/cache.go` as intended.