Combined spec + code-quality review of Task 8 (Registry Fetch + FetchPair) in worktree `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`. Do spec first, then quality.

Diff: `git diff a278a7a..743c194 -- apps/birdseye/`

## Spec — What Was Requested

Create `apps/birdseye/internal/registry/registry.go`:

```go
type Registry struct { client *client.Client; cache Cache }
func New(c *client.Client, cache Cache) *Registry
func (r *Registry) Fetch(ctx context.Context, m api.Module) ([]byte, error)
func (r *Registry) FetchPair(ctx context.Context, a, b api.Module) ([]byte, []byte, error)
```

`Fetch` must be cache-first: consult `r.cache.Get(m)` BEFORE calling `r.client.Download`. On miss, download, ReadAll, cache.Put (with cache write failure swallowed), return body. `FetchPair` uses `errgroup.WithContext` so the first failure cancels the sibling.

Create `apps/birdseye/internal/registry/registry_test.go` with 5 tests:
- `TestRegistryFetchCacheMiss` — happy path, body matches, cache populated after
- `TestRegistryFetchCacheHitSkipsNetwork` — pre-seed cache, assert handler is never hit
- `TestRegistryFetchNotFound` — `errors.Is(err, client.ErrNotFound)`
- `TestRegistryFetchPair` — both bodies returned, ordered correctly
- `TestRegistryFetchPairOneMissing` — `errors.Is(err, client.ErrNotFound)`

Plus a `fakeCrow` test helper that spins up a Crow-protocol-aware httptest server.

Commit message exact: `feat(birdseye): registry with cache-first Fetch and concurrent FetchPair`.

## Quality Focus

- Concurrency: `errgroup.WithContext` used, returned `gctx` passed to both `Fetch` calls?
- Both `Fetch` goroutines write to local `aBytes`/`bBytes` — verify no races (assignment after error check is fine because `Wait` synchronizes; race detector should catch any issue).
- Resource cleanup: `defer rc.Close()` on the download body?
- `_ = err` swallow on `cache.Put` failure — intentional per spec; should NOT be flagged unless missing entirely.
- Test fixture `fakeCrow` — is it correctly using `t.Cleanup(blob.Close)` (so blob server survives until test ends)?

## Report Format

**Spec:** ✅ compliant OR ❌ issues with file:line
**Quality:**
- Strengths: 1-2 bullets
- Issues: Critical / Important / Minor
- Assessment: APPROVED | NEEDS_CHANGES

Be terse.


- 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


Launching skill: superpowers:using-superpowers


Base directory for this skill: /Users/steve.briggs/.claude/plugins/cache/claude-plugins-official/superpowers/5.1.0/skills/using-superpowers

<SUBAGENT-STOP>
If you were dispatched as a subagent to execute a specific task, skip this skill.
</SUBAGENT-STOP>

<EXTREMELY-IMPORTANT>
If you think there is even a 1% chance a skill might apply to what you are doing, you ABSOLUTELY MUST invoke the skill.

IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT.

This is not negotiable. This is not optional. You cannot rationalize your way out of this.
</EXTREMELY-IMPORTANT>

## Instruction Priority

Superpowers skills override default system prompt behavior, but **user instructions always take precedence**:

1. **User's explicit instructions** (CLAUDE.md, GEMINI.md, AGENTS.md, direct requests) — highest priority
2. **Superpowers skills** — override default system behavior where they conflict
3. **Default system prompt** — lowest priority

If CLAUDE.md, GEMINI.md, or AGENTS.md says "don't use TDD" and a skill says "always use TDD," follow the user's instructions. The user is in control.

## How to Access Skills

**In Claude Code:** Use the `Skill` tool. When you invoke a skill, its content is loaded and presented to you—follow it directly. Never use the Read tool on skill files.

**In Copilot CLI:** Use the `skill` tool. Skills are auto-discovered from installed plugins. The `skill` tool works the same as Claude Code's `Skill` tool.

**In Gemini CLI:** Skills activate via the `activate_skill` tool. Gemini loads skill metadata at session start and activates the full content on demand.

**In other environments:** Check your platform's documentation for how skills are loaded.

## Platform Adaptation

Skills use Claude Code tool names. Non-CC platforms: see `references/copilot-tools.md` (Copilot CLI), `references/codex-tools.md` (Codex) for tool equivalents. Gemini CLI users get the tool mapping loaded automatically via GEMINI.md.

# Using Skills

## The Rule

**Invoke relevant or requested skills BEFORE any response or action.** Even a 1% chance a skill might apply means that you should invoke the skill to check. If an invoked skill turns out to be wrong for the situation, you don't need to use it.

```dot
digraph skill_flow {
    "User message received" [shape=doublecircle];
    "About to EnterPlanMode?" [shape=doublecircle];
    "Already brainstormed?" [shape=diamond];
    "Invoke brainstorming skill" [shape=box];
    "Might any skill apply?" [shape=diamond];
    "Invoke Skill tool" [shape=box];
    "Announce: 'Using [skill] to [purpose]'" [shape=box];
    "Has checklist?" [shape=diamond];
    "Create TodoWrite todo per item" [shape=box];
    "Follow skill exactly" [shape=box];
    "Respond (including clarifications)" [shape=doublecircle];

    "About to EnterPlanMode?" -> "Already brainstormed?";
    "Already brainstormed?" -> "Invoke brainstorming skill" [label="no"];
    "Already brainstormed?" -> "Might any skill apply?" [label="yes"];
    "Invoke brainstorming skill" -> "Might any skill apply?";

    "User message received" -> "Might any skill apply?";
    "Might any skill apply?" -> "Invoke Skill tool" [label="yes, even 1%"];
    "Might any skill apply?" -> "Respond (including clarifications)" [label="definitely not"];
    "Invoke Skill tool" -> "Announce: 'Using [skill] to [purpose]'";
    "Announce: 'Using [skill] to [purpose]'" -> "Has checklist?";
    "Has checklist?" -> "Create TodoWrite todo per item" [label="yes"];
    "Has checklist?" -> "Follow skill exactly" [label="no"];
    "Create TodoWrite todo per item" -> "Follow skill exactly";
}
```

## Red Flags

These thoughts mean STOP—you're rationalizing:

| Thought | Reality |
|---------|---------|
| "This is just a simple question" | Questions are tasks. Check for skills. |
| "I need more context first" | Skill check comes BEFORE clarifying questions. |
| "Let me explore the codebase first" | Skills tell you HOW to explore. Check first. |
| "I can check git/files quickly" | Files lack conversation context. Check for skills. |
| "Let me gather information first" | Skills tell you HOW to gather information. |
| "This doesn't need a formal skill" | If a skill exists, use it. |
| "I remember this skill" | Skills evolve. Read current version. |
| "This doesn't count as a task" | Action = task. Check for skills. |
| "The skill is overkill" | Simple things become complex. Use it. |
| "I'll just do this one thing first" | Check BEFORE doing anything. |
| "This feels productive" | Undisciplined action wastes time. Skills prevent this. |
| "I know what that means" | Knowing the concept ≠ using the skill. Invoke it. |

## Skill Priority

When multiple skills could apply, use this order:

1. **Process skills first** (brainstorming, debugging) - these determine HOW to approach the task
2. **Implementation skills second** (frontend-design, mcp-builder) - these guide execution

"Let's build X" → brainstorming first, then implementation skills.
"Fix this bug" → debugging first, then domain-specific skills.

## Skill Types

**Rigid** (TDD, debugging): Follow exactly. Don't adapt away discipline.

**Flexible** (patterns): Adapt principles to context.

The skill itself tells you which.

## User Instructions

Instructions say WHAT, not HOW. "Add X" or "Fix Y" doesn't mean skip workflows.


Using the receiving-code-review skill since this is a code review task.


Launching skill: superpowers:receiving-code-review


Base directory for this skill: /Users/steve.briggs/.claude/plugins/cache/claude-plugins-official/superpowers/5.1.0/skills/receiving-code-review

# Code Review Reception

## Overview

Code review requires technical evaluation, not emotional performance.

**Core principle:** Verify before implementing. Ask before assuming. Technical correctness over social comfort.

## The Response Pattern

```
WHEN receiving code review feedback:

1. READ: Complete feedback without reacting
2. UNDERSTAND: Restate requirement in own words (or ask)
3. VERIFY: Check against codebase reality
4. EVALUATE: Technically sound for THIS codebase?
5. RESPOND: Technical acknowledgment or reasoned pushback
6. IMPLEMENT: One item at a time, test each
```

## Forbidden Responses

**NEVER:**
- "You're absolutely right!" (explicit CLAUDE.md violation)
- "Great point!" / "Excellent feedback!" (performative)
- "Let me implement that now" (before verification)

**INSTEAD:**
- Restate the technical requirement
- Ask clarifying questions
- Push back with technical reasoning if wrong
- Just start working (actions > words)

## Handling Unclear Feedback

```
IF any item is unclear:
  STOP - do not implement anything yet
  ASK for clarification on unclear items

WHY: Items may be related. Partial understanding = wrong implementation.
```

**Example:**
```
your human partner: "Fix 1-6"
You understand 1,2,3,6. Unclear on 4,5.

❌ WRONG: Implement 1,2,3,6 now, ask about 4,5 later
✅ RIGHT: "I understand items 1,2,3,6. Need clarification on 4 and 5 before proceeding."
```

## Source-Specific Handling

### From your human partner
- **Trusted** - implement after understanding
- **Still ask** if scope unclear
- **No performative agreement**
- **Skip to action** or technical acknowledgment

### From External Reviewers
```
BEFORE implementing:
  1. Check: Technically correct for THIS codebase?
  2. Check: Breaks existing functionality?
  3. Check: Reason for current implementation?
  4. Check: Works on all platforms/versions?
  5. Check: Does reviewer understand full context?

IF suggestion seems wrong:
  Push back with technical reasoning

IF can't easily verify:
  Say so: "I can't verify this without [X]. Should I [investigate/ask/proceed]?"

IF conflicts with your human partner's prior decisions:
  Stop and discuss with your human partner first
```

**your human partner's rule:** "External feedback - be skeptical, but check carefully"

## YAGNI Check for "Professional" Features

```
IF reviewer suggests "implementing properly":
  grep codebase for actual usage

  IF unused: "This endpoint isn't called. Remove it (YAGNI)?"
  IF used: Then implement properly
```

**your human partner's rule:** "You and reviewer both report to me. If we don't need this feature, don't add it."

## Implementation Order

```
FOR multi-item feedback:
  1. Clarify anything unclear FIRST
  2. Then implement in this order:
     - Blocking issues (breaks, security)
     - Simple fixes (typos, imports)
     - Complex fixes (refactoring, logic)
  3. Test each fix individually
  4. Verify no regressions
```

## When To Push Back

Push back when:
- Suggestion breaks existing functionality
- Reviewer lacks full context
- Violates YAGNI (unused feature)
- Technically incorrect for this stack
- Legacy/compatibility reasons exist
- Conflicts with your human partner's architectural decisions

**How to push back:**
- Use technical reasoning, not defensiveness
- Ask specific questions
- Reference working tests/code
- Involve your human partner if architectural

**Signal if uncomfortable pushing back out loud:** "Strange things are afoot at the Circle K"

## Acknowledging Correct Feedback

When feedback IS correct:
```
✅ "Fixed. [Brief description of what changed]"
✅ "Good catch - [specific issue]. Fixed in [location]."
✅ [Just fix it and show in the code]

❌ "You're absolutely right!"
❌ "Great point!"
❌ "Thanks for catching that!"
❌ "Thanks for [anything]"
❌ ANY gratitude expression
```

**Why no thanks:** Actions speak. Just fix it. The code itself shows you heard the feedback.

**If you catch yourself about to write "Thanks":** DELETE IT. State the fix instead.

## Gracefully Correcting Your Pushback

If you pushed back and were wrong:
```
✅ "You were right - I checked [X] and it does [Y]. Implementing now."
✅ "Verified this and you're correct. My initial understanding was wrong because [reason]. Fixing."

❌ Long apology
❌ Defending why you pushed back
❌ Over-explaining
```

State the correction factually and move on.

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Performative agreement | State requirement or just act |
| Blind implementation | Verify against codebase first |
| Batch without testing | One at a time, test each |
| Assuming reviewer is right | Check if breaks things |
| Avoiding pushback | Technical correctness > comfort |
| Partial implementation | Clarify all items first |
| Can't verify, proceed anyway | State limitation, ask for direction |

## Real Examples

**Performative Agreement (Bad):**
```
Reviewer: "Remove legacy code"
❌ "You're absolutely right! Let me remove that..."
```

**Technical Verification (Good):**
```
Reviewer: "Remove legacy code"
✅ "Checking... build target is 10.15+, this API needs 13+. Need legacy for backward compat. Current impl has wrong bundle ID - fix it or drop pre-13 support?"
```

**YAGNI (Good):**
```
Reviewer: "Implement proper metrics tracking with database, date filters, CSV export"
✅ "Grepped codebase - nothing calls this endpoint. Remove it (YAGNI)? Or is there usage I'm missing?"
```

**Unclear Item (Good):**
```
your human partner: "Fix items 1-6"
You understand 1,2,3,6. Unclear on 4,5.
✅ "Understand 1,2,3,6. Need clarification on 4 and 5 before implementing."
```

## GitHub Thread Replies

When replying to inline review comments on GitHub, reply in the comment thread (`gh api repos/{owner}/{repo}/pulls/{pr}/comments/{id}/replies`), not as a top-level PR comment.

## The Bottom Line

**External feedback = suggestions to evaluate, not orders to follow.**

Verify. Question. Then implement.

No performative agreement. Technical rigor always.


This is a review task, not receiving review feedback — I'll read the diff and evaluate directly.


Bash

git diff a278a7a..743c194 -- apps/birdseye/

Get the diff for Task 8 registry files


diff --git a/apps/birdseye/go.mod b/apps/birdseye/go.mod
index 6c1ad32..0351025 100644
--- a/apps/birdseye/go.mod
+++ b/apps/birdseye/go.mod
@@ -39,6 +39,7 @@ require (
 	github.com/valyala/fasttemplate v1.2.2 // indirect
 	golang.org/x/crypto v0.48.0 // indirect
 	golang.org/x/net v0.50.0 // indirect
+	golang.org/x/sync v0.20.0 // indirect
 	golang.org/x/sys v0.41.0 // indirect
 	golang.org/x/text v0.34.0 // indirect
 	golang.org/x/time v0.14.0 // indirect
diff --git a/apps/birdseye/go.sum b/apps/birdseye/go.sum
index b2b5315..2bd9588 100644
--- a/apps/birdseye/go.sum
+++ b/apps/birdseye/go.sum
@@ -68,6 +68,8 @@ golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
 golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
 golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
 golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
 golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
diff --git a/apps/birdseye/internal/registry/registry.go b/apps/birdseye/internal/registry/registry.go
new file mode 100644
index 0000000..7c1f1cb
--- /dev/null
+++ b/apps/birdseye/internal/registry/registry.go
@@ -0,0 +1,78 @@
+package registry
+
+import (
+	"context"
+	"fmt"
+	"io"
+
+	"github.com/immuta/operations/services/crow/pkg/api"
+	"github.com/immuta/operations/services/crow/pkg/client"
+	"golang.org/x/sync/errgroup"
+)
+
+// Registry composes a typed Crow client with a Cache. Fetch is cache-first;
+// FetchPair fans Fetch out concurrently with shared cancellation.
+type Registry struct {
+	client *client.Client
+	cache  Cache
+}
+
+// New constructs a Registry. Both arguments must be non-nil.
+func New(c *client.Client, cache Cache) *Registry {
+	return &Registry{client: c, cache: cache}
+}
+
+// Fetch returns the gzipped tarball bytes for the given module version,
+// consulting the cache first. Network errors and 404s from the
+// underlying client surface unchanged (callers can errors.Is against
+// client.ErrNotFound).
+func (r *Registry) Fetch(ctx context.Context, m api.Module) ([]byte, error) {
+	if body, hit, err := r.cache.Get(m); err != nil {
+		return nil, fmt.Errorf("cache get: %w", err)
+	} else if hit {
+		return body, nil
+	}
+	rc, err := r.client.Download(ctx, m)
+	if err != nil {
+		return nil, err
+	}
+	defer rc.Close()
+	body, err := io.ReadAll(rc)
+	if err != nil {
+		return nil, fmt.Errorf("read tarball: %w", err)
+	}
+	if err := r.cache.Put(m, body); err != nil {
+		// Cache write failure is not fatal — we still have the body.
+		// Surface as a warning via context-derived logger if the caller
+		// configured one; for now we just swallow it.
+		_ = err
+	}
+	return body, nil
+}
+
+// FetchPair fetches a and b concurrently. The first error cancels its
+// sibling via errgroup.WithContext.
+func (r *Registry) FetchPair(ctx context.Context, a, b api.Module) ([]byte, []byte, error) {
+	g, gctx := errgroup.WithContext(ctx)
+	var aBytes, bBytes []byte
+	g.Go(func() error {
+		body, err := r.Fetch(gctx, a)
+		if err != nil {
+			return err
+		}
+		aBytes = body
+		return nil
+	})
+	g.Go(func() error {
+		body, err := r.Fetch(gctx, b)
+		if err != nil {
+			return err
+		}
+		bBytes = body
+		return nil
+	})
+	if err := g.Wait(); err != nil {
+		return nil, nil, err
+	}
+	return aBytes, bBytes, nil
+}
diff --git a/apps/birdseye/internal/registry/registry_test.go b/apps/birdseye/internal/registry/registry_test.go
new file mode 100644
index 0000000..af68ab4
--- /dev/null
+++ b/apps/birdseye/internal/registry/registry_test.go
@@ -0,0 +1,126 @@
+package registry
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/immuta/operations/services/crow/pkg/client"
+)
+
+// fakeCrow returns a httptest.Server impersonating Crow's download
+// protocol. The map maps version -> tarball bytes (or "" to 404).
+func fakeCrow(t *testing.T, versions map[string][]byte) *httptest.Server {
+	t.Helper()
+	mux := http.NewServeMux()
+	blob := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		v := strings.TrimPrefix(r.URL.Path, "/blob/")
+		body, ok := versions[v]
+		if !ok {
+			w.WriteHeader(http.StatusNotFound)
+			return
+		}
+		_, _ = w.Write(body)
+	}))
+	t.Cleanup(blob.Close)
+
+	mux.HandleFunc("/v1/modules/iac/comply/aws/", func(w http.ResponseWriter, r *http.Request) {
+		// /v1/modules/iac/comply/aws/<version>/download
+		parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/v1/modules/iac/comply/aws/"), "/")
+		if len(parts) != 2 || parts[1] != "download" {
+			http.NotFound(w, r)
+			return
+		}
+		version := parts[0]
+		if _, ok := versions[version]; !ok {
+			w.WriteHeader(http.StatusNotFound)
+			return
+		}
+		w.Header().Set("X-Terraform-Get", blob.URL+"/blob/"+version)
+		w.WriteHeader(http.StatusNoContent)
+	})
+	srv := httptest.NewServer(mux)
+	t.Cleanup(srv.Close)
+	return srv
+}
+
+func TestRegistryFetchCacheMiss(t *testing.T) {
+	srv := fakeCrow(t, map[string][]byte{"1.0.0": []byte("body-100")})
+	cli := client.New(srv.URL, client.WithHTTPClient(srv.Client()))
+	cache := NewMemCache()
+	reg := New(cli, cache)
+
+	got, err := reg.Fetch(context.Background(), mod("1.0.0"))
+	if err != nil {
+		t.Fatalf("Fetch: %v", err)
+	}
+	if string(got) != "body-100" {
+		t.Errorf("body = %q", got)
+	}
+	// And it should now be cached.
+	if cached, ok, _ := cache.Get(mod("1.0.0")); !ok || string(cached) != "body-100" {
+		t.Errorf("expected cache hit after fetch")
+	}
+}
+
+func TestRegistryFetchCacheHitSkipsNetwork(t *testing.T) {
+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+		t.Error("network should not have been called on cache hit")
+		w.WriteHeader(http.StatusInternalServerError)
+	}))
+	defer srv.Close()
+	cli := client.New(srv.URL, client.WithHTTPClient(srv.Client()))
+	cache := NewMemCache()
+	_ = cache.Put(mod("1.0.0"), []byte("cached"))
+	reg := New(cli, cache)
+
+	got, err := reg.Fetch(context.Background(), mod("1.0.0"))
+	if err != nil {
+		t.Fatalf("Fetch: %v", err)
+	}
+	if string(got) != "cached" {
+		t.Errorf("body = %q", got)
+	}
+}
+
+func TestRegistryFetchNotFound(t *testing.T) {
+	srv := fakeCrow(t, map[string][]byte{}) // no versions
+	cli := client.New(srv.URL, client.WithHTTPClient(srv.Client()))
+	reg := New(cli, NewMemCache())
+
+	_, err := reg.Fetch(context.Background(), mod("9.9.9"))
+	if !errors.Is(err, client.ErrNotFound) {
+		t.Fatalf("expected ErrNotFound, got %v", err)
+	}
+}
+
+func TestRegistryFetchPair(t *testing.T) {
+	srv := fakeCrow(t, map[string][]byte{
+		"1.0.0": []byte("body-100"),
+		"1.1.0": []byte("body-110"),
+	})
+	cli := client.New(srv.URL, client.WithHTTPClient(srv.Client()))
+	reg := New(cli, NewMemCache())
+
+	a, b, err := reg.FetchPair(context.Background(), mod("1.0.0"), mod("1.1.0"))
+	if err != nil {
+		t.Fatalf("FetchPair: %v", err)
+	}
+	if string(a) != "body-100" || string(b) != "body-110" {
+		t.Errorf("got (%q, %q)", a, b)
+	}
+}
+
+func TestRegistryFetchPairOneMissing(t *testing.T) {
+	srv := fakeCrow(t, map[string][]byte{"1.0.0": []byte("body-100")})
+	cli := client.New(srv.URL, client.WithHTTPClient(srv.Client()))
+	reg := New(cli, NewMemCache())
+
+	_, _, err := reg.FetchPair(context.Background(), mod("1.0.0"), mod("9.9.9"))
+	if !errors.Is(err, client.ErrNotFound) {
+		t.Fatalf("expected ErrNotFound, got %v", err)
+	}
+}


find /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/internal/registry -type f 2>/dev/null && find /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye -name "*.go" | head -30

List registry files in worktree


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


1	package registry
2
3	import (
4		"testing"
5
6		"github.com/immuta/operations/services/crow/pkg/api"
7	)
8
9	func mod(version string) api.Module {
10		return api.Module{
11			ModuleIdentifier: api.ModuleIdentifier{Namespace: "iac", Name: "comply", Provider: "aws"},
12			Version:          version,
13		}
14	}
15
16	func TestMemCacheRoundTrip(t *testing.T) {
17		c := NewMemCache()
18		if _, ok, err := c.Get(mod("1.0.0")); err != nil || ok {
19			t.Fatalf("expected miss; got ok=%v err=%v", ok, err)
20		}
21		if err := c.Put(mod("1.0.0"), []byte("payload")); err != nil {
22			t.Fatalf("Put: %v", err)
23		}
24		got, ok, err := c.Get(mod("1.0.0"))
25		if err != nil || !ok {
26			t.Fatalf("expected hit; got ok=%v err=%v", ok, err)
27		}
28		if string(got) != "payload" {
29			t.Errorf("body = %q", got)
30		}
31	}
32
33	func TestDiskCacheMissThenHit(t *testing.T) {
34		dir := t.TempDir()
35		c, err := NewDiskCache(dir)
36		if err != nil {
37			t.Fatalf("NewDiskCache: %v", err)
38		}
39		if _, ok, err := c.Get(mod("1.0.0")); err != nil || ok {
40			t.Fatalf("expected miss; got ok=%v err=%v", ok, err)
41		}
42		if err := c.Put(mod("1.0.0"), []byte("disk-payload")); err != nil {
43			t.Fatalf("Put: %v", err)
44		}
45		got, ok, err := c.Get(mod("1.0.0"))
46		if err != nil || !ok {
47			t.Fatalf("expected hit; got ok=%v err=%v", ok, err)
48		}
49		if string(got) != "disk-payload" {
50			t.Errorf("body = %q", got)
51		}
52	}
53
54	func TestDiskCacheSurvivesNewInstance(t *testing.T) {
55		dir := t.TempDir()
56		c1, _ := NewDiskCache(dir)
57		if err := c1.Put(mod("2.0.0"), []byte("persisted")); err != nil {
58			t.Fatal(err)
59		}
60		c2, _ := NewDiskCache(dir)
61		got, ok, err := c2.Get(mod("2.0.0"))
62		if err != nil || !ok {
63			t.Fatalf("expected hit; got ok=%v err=%v", ok, err)
64		}
65		if string(got) != "persisted" {
66			t.Errorf("body = %q", got)
67		}
68	}
69


find /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye -name "*.go" | xargs grep -l "Download\|ErrNotFound\|WithHTTPClient" 2>/dev/null | head -10

Find client package for Download and ErrNotFound signatures


find /Users/steve.briggs/projects/operations -path "*/crow/pkg/client/*.go" 2>/dev/null | head -10

Find crow client package


1	// Package client is a reusable Crow HTTP client. It is used by apps/crowctl
2	// and is safe to use from tests (point it at an httptest.Server via
3	// WithHTTPClient or a localhost baseURL).
4	package client
5
6	import (
7		"context"
8		"encoding/json"
9		"errors"
10		"fmt"
11		"io"
12		"net/http"
13
14		"github.com/immuta/operations/services/crow/pkg/api"
15	)
16
17	type Client struct {
18		httpClient *http.Client
19		baseURL    string
20	}
21
22	type Option func(*Client)
23
24	// WithHTTPClient overrides the default *http.Client (useful for tests and
25	// for callers that want custom timeouts or transport).
26	func WithHTTPClient(h *http.Client) Option {
27		return func(c *Client) { c.httpClient = h }
28	}
29
30	// New constructs a Client pointed at baseURL (e.g. "https://crow.infrastructure.immuta.io").
31	// baseURL must not end in "/".
32	func New(baseURL string, opts ...Option) *Client {
33		c := &Client{baseURL: baseURL, httpClient: http.DefaultClient}
34		for _, opt := range opts {
35			opt(c)
36		}
37		return c
38	}
39
40	// Typed errors consumers can match with errors.Is.
41	var (
42		ErrAlreadyExists = errors.New("module version already exists")
43		ErrNotFound      = errors.New("module version not found")
44	)
45
46	// Upload PUTs the given gzipped-tar body for the module version. It returns
47	// ErrAlreadyExists on HTTP 409, nil on 201, and a generic error otherwise.
48	func (c *Client) Upload(ctx context.Context, m api.Module, body io.Reader) error {
49		if err := m.Validate(); err != nil {
50			return err
51		}
52		if err := api.ValidateVersion(m.Version); err != nil {
53			return err
54		}
55		req, err := http.NewRequestWithContext(ctx, http.MethodPut, c.baseURL+m.UploadPath(), body)
56		if err != nil {
57			return err
58		}
59		req.Header.Set("Content-Type", "application/octet-stream")
60		resp, err := c.httpClient.Do(req)
61		if err != nil {
62			return err
63		}
64		defer resp.Body.Close()
65		switch resp.StatusCode {
66		case http.StatusCreated:
67			return nil
68		case http.StatusConflict:
69			return ErrAlreadyExists
70		default:
71			return fmt.Errorf("unexpected status %d uploading %s", resp.StatusCode, m.UploadPath())
72		}
73	}
74
75	// Exists returns (true, nil) on 200 or 204, (false, nil) on 404, and an
76	// error for any other status.
77	func (c *Client) Exists(ctx context.Context, m api.Module) (bool, error) {
78		if err := m.Validate(); err != nil {
79			return false, err
80		}
81		req, err := http.NewRequestWithContext(ctx, http.MethodHead, c.baseURL+m.DownloadPath(), nil)
82		if err != nil {
83			return false, err
84		}
85		resp, err := c.httpClient.Do(req)
86		if err != nil {
87			return false, err
88		}
89		defer resp.Body.Close()
90		switch resp.StatusCode {
91		case http.StatusOK, http.StatusNoContent:
92			return true, nil
93		case http.StatusNotFound:
94			return false, nil
95		default:
96			return false, fmt.Errorf("unexpected status %d checking %s", resp.StatusCode, m.DownloadPath())
97		}
98	}
99
100	// ListVersions returns the module's version list in server order.
101	// Returns ErrNotFound when the module has no published versions.
102	func (c *Client) ListVersions(ctx context.Context, id api.ModuleIdentifier) ([]string, error) {
103		if err := id.Validate(); err != nil {
104			return nil, err
105		}
106		req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+id.VersionsPath(), nil)
107		if err != nil {
108			return nil, err
109		}
110		resp, err := c.httpClient.Do(req)
111		if err != nil {
112			return nil, err
113		}
114		defer resp.Body.Close()
115		switch resp.StatusCode {
116		case http.StatusOK:
117			var body api.ListVersionsResponse
118			if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
119				return nil, fmt.Errorf("decoding response: %w", err)
120			}
121			if len(body.Modules) == 0 {
122				return nil, ErrNotFound
123			}
124			out := make([]string, len(body.Modules[0].Versions))
125			for i, v := range body.Modules[0].Versions {
126				out[i] = v.Version
127			}
128			return out, nil
129		case http.StatusNotFound:
130			return nil, ErrNotFound
131		default:
132			return nil, fmt.Errorf("unexpected status %d listing %s", resp.StatusCode, id.VersionsPath())
133		}
134	}
135
136	// Download fetches the gzipped tarball bytes for the given module
137	// version. It follows the X-Terraform-Get redirect returned by Crow's
138	// 204 response (per the Terraform module registry protocol). The caller
139	// is responsible for closing the returned ReadCloser.
140	//
141	// Returns ErrNotFound on HTTP 404 from Crow.
142	func (c *Client) Download(ctx context.Context, m api.Module) (io.ReadCloser, error) {
143		if err := m.Validate(); err != nil {
144			return nil, err
145		}
146		req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+m.DownloadPath(), nil)
147		if err != nil {
148			return nil, err
149		}
150		resp, err := c.httpClient.Do(req)
151		if err != nil {
152			return nil, err
153		}
154
155		// The protocol uses 204 + X-Terraform-Get to point clients at the
156		// real artifact location (S3 presigned URL in production).
157		if resp.StatusCode == http.StatusNotFound {
158			_ = resp.Body.Close()
159			return nil, ErrNotFound
160		}
161		if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
162			_ = resp.Body.Close()
163			return nil, fmt.Errorf("unexpected status %d resolving %s", resp.StatusCode, m.DownloadPath())
164		}
165		location := resp.Header.Get("X-Terraform-Get")
166		_ = resp.Body.Close()
167		if location == "" {
168			return nil, fmt.Errorf("crow returned %d with no X-Terraform-Get header for %s", resp.StatusCode, m.DownloadPath())
169		}
170
171		blobReq, err := http.NewRequestWithContext(ctx, http.MethodGet, location, nil)
172		if err != nil {
173			return nil, err
174		}
175		blobResp, err := c.httpClient.Do(blobReq)
176		if err != nil {
177			return nil, fmt.Errorf("fetching presigned blob: %w", err)
178		}
179		if blobResp.StatusCode != http.StatusOK {
180			_ = blobResp.Body.Close()
181			return nil, fmt.Errorf("unexpected status %d fetching blob %s", blobResp.StatusCode, location)
182		}
183		return blobResp.Body, nil
184	}
185


git log --oneline a278a7a..743c194 -- apps/birdseye/

Check commit messages in the range


743c194 feat(birdseye): registry with cache-first Fetch and concurrent FetchPair


I now have everything I need. Here is the review, Sir Charles of St Cheese.

---

**Spec:** COMPLIANT

All required signatures present and correct. `New`, `Fetch`, `FetchPair` match the spec exactly. Cache-first ordering in `Fetch` is correct (Get before Download). `FetchPair` uses `errgroup.WithContext` and passes the returned `gctx` to both goroutines. All 5 required tests exist. `fakeCrow` is present. Commit message matches exactly.

---

**Quality:**

**Strengths:**
- `cache.Get` error path in `Fetch` correctly wraps and surfaces the error rather than treating it as a miss — avoids silently ignoring a corrupted cache.
- `DiskCache.Put` uses atomic write (temp + rename) which is solid defensive engineering for the concurrent `FetchPair` case where the same module could theoretically be cached twice simultaneously.

**Issues:**

**Critical:** None.

**Important:**

- `registry_test.go:97` — `defer srv.Close()` is used in `TestRegistryFetchCacheHitSkipsNetwork` but `t.Cleanup(srv.Close)` is used in `fakeCrow`. Inconsistency is harmless but the `defer` form in a test body is fine since it's the only server there. Not a correctness issue.

- `registry_test.go:29` — the `fakeCrow` blob handler route (`/blob/<version>`) is hardcoded into the test's download protocol simulation. The blob server is shared across all module versions served by the same `fakeCrow` instance. This is structurally fine for the tests as written, but `fakeCrow` will silently serve any version from any test-local server if the key exists — the mux path for `HandleFunc` is `/v1/modules/iac/comply/aws/` which catches all subpaths via the default `http.ServeMux` prefix matching. That's intentional and correct here, but worth noting it's prefix-matched, not exact.

**Minor:**

- `registry.go:45-49` — the comment on the swallowed cache write error mentions "surface as a warning via context-derived logger if the caller configured one." This is aspirational text that describes a capability that does not exist. It should either be removed or replaced with a plain "// intentionally swallowed" comment. Aspirational comments describing unimplemented behavior are noise and can mislead future readers.

- `registry_test.go` does not use `t.Parallel()` anywhere. Given `FetchPair` tests exercise concurrency, running with `-race` plus `-parallel` would give more confidence. Not blocking, just an opportunity.

- `golang.org/x/sync` is marked `// indirect` in `go.mod` (line added in diff). It is directly imported by `registry.go`. Should be a direct dependency. Run `go mod tidy` to correct.

**Assessment: NEEDS_CHANGES**

One actionable fix before merge: run `go mod tidy` — `golang.org/x/sync` must be a direct dependency, not indirect. The aspirational logger comment is a minor cleanup. Everything else is compliant.