Agent

Spec review Task 2

Prompt:
You are reviewing whether the Task 2 implementation matches its specification.

## Working Directory

`/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`

Compare commit `3b73e14` (Task 2) against its parent `ae477d5`. Run `git diff ae477d5..3b73e14` to see the change.

## What Was Requested

Task 2 creates the `cmd` package with `NewRootCmd()` returning a cobra root command (persistent flags `--crow-url`, `--timeout`, `--json`; defaults `https://crow.infrastructure.immuta.io` / `30s` / `false`) and an `ExitCode(err) int` helper that maps errors via `errors.Is`:

- nil → 0
- ErrUsage → 1
- client.ErrNotFound → 2
- ErrArchive → 4
- ErrSemantic → 5
- context.Canceled or context.DeadlineExceeded → 130
- default → 3

Files: `cmd/root.go`, `cmd/root_test.go`, `cmd/errors.go`, `cmd/errors_test.go`. Strict TDD. Tests under `go test -race`. Build must succeed.

The spec body of `NewRootCmd` was given as:

```go
func NewRootCmd() *cobra.Command {
	g := &GlobalFlags{}
	root := &cobra.Command{
		Use:           "birdseye",
		Short:         "Diff two versions of a Terraform module hosted in Crow",
		SilenceUsage:  true,
		SilenceErrors: true,
	}
	root.PersistentFlags().StringVar(&g.CrowURL, "crow-url", defaultCrowURL, "Crow base URL (env CROW_URL)")
	root.PersistentFlags().DurationVar(&g.Timeout, "timeout", 30*time.Second, "per-operation HTTP timeout")
	root.PersistentFlags().BoolVar(&g.JSON, "json", false, "emit structured JSON to stdout where supported")

	// Subcommands will be added in later tasks
	return root
}
```

The spec didn't include a `RunE`.

Commit message must be exactly: `feat(birdseye): cobra root command and exit-code mapping`

## What Implementer Claims They Built

- All 4 files plus go.mod/go.sum updates
- TDD followed
- 7/7 tests pass with `-race`
- `go build ./...` succeeds
- One spec deviation: added `RunE: cmd.Help()` (or equivalent) to make `TestNewRootCmdHelp` pass — cobra's default help wasn't producing the flag listing through the captured buffer.

## CRITICAL: Verify Independently

Read the actual files. Don't trust the report.

**Read these files in the worktree:**
- `apps/birdseye/cmd/root.go`
- `apps/birdseye/cmd/root_test.go`
- `apps/birdseye/cmd/errors.go`
- `apps/birdseye/cmd/errors_test.go`
- Run `git show --stat 3b73e14` to see what was committed
- Run `git diff ae477d5..3b73e14 -- apps/birdseye/cmd/` to see the diff
- Optionally: `cd apps/birdseye && go test -race ./cmd/... -v` to confirm tests still pass

## Specific Checks

**Missing requirements:**
- Both test files match the spec verbatim?
- ExitCode covers all 7 cases in spec (nil, ErrUsage, ErrNotFound, ErrArchive, ErrSemantic, context.Canceled, context.DeadlineExceeded, default)?
- The 3 persistent flags exist with EXACT names (`crow-url`, `timeout`, `json`) and the correct defaults?

**Extra/unneeded work:**
- Did the implementer add features not in the spec? (Flag the `RunE` deviation — assess if it's justified or could be replaced with a simpler approach like removing SilenceUsage.)
- Are there any extra files beyond the four created?
- Did they add `cobra.MaximumNArgs` or other unrelated wiring?

**Misunderstandings:**
- `defaultCrowURL` constant present and used?
- `GlobalFlags` struct present with three fields (CrowURL, Timeout, JSON)?
- Are sentinel errors named exactly `ErrUsage`, `ErrArchive`, `ErrSemantic`?

**On the `RunE` deviation:** This is actually a legitimate cobra subtlety — without a `RunE` or `Run`, cobra v1.9.1 may print help to a different stream than what `SetOut` captures. If the implementer added `RunE` that simply calls `cmd.Help()` to fix the test, this is acceptable. If they added meaningful behavior (e.g., printing a custom banner, parsing args), flag it.

## Report

Use one of:
- ✅ Spec compliant (note the RunE deviation if present, but mark as passing if it's just `cmd.Help()`)
- ❌ Issues found: list with file:line references

Be terse.


Build birdseye CLI for Terraform module diffing


✅ **Spec compliant** with one noted deviation.

**All checks pass:**

- Commit message: exact match — `feat(birdseye): cobra root command and exit-code mapping`
- Files committed: exactly the 4 required (`cmd/root.go`, `cmd/root_test.go`, `cmd/errors.go`, `cmd/errors_test.go`) plus go.mod/go.sum — no extra files
- `GlobalFlags` struct: present with exactly `CrowURL`, `Timeout`, `JSON`
- `defaultCrowURL` constant: present and used
- 3 persistent flags: exact names `crow-url`, `timeout`, `json` with correct defaults (`https://crow.infrastructure.immuta.io`, `30*time.Second`, `false`)
- Sentinel errors: `ErrUsage`, `ErrArchive`, `ErrSemantic` — exact names
- `ExitCode` all 7 cases: nil→0, ErrUsage→1, client.ErrNotFound→2, ErrArchive→4, ErrSemantic→5, context.Canceled|DeadlineExceeded→130, default→3
- Tests: 7/7 pass under `-race`

**RunE deviation** (`root.go:30–32`): `RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }` — justified. Without a `RunE`, cobra with `SilenceUsage: true` routes help output through its internal writer rather than the `SetOut` writer, so `TestNewRootCmdHelp` would fail. The added `RunE` contains no meaningful behavior beyond delegating to `cmd.Help()`, and is the standard cobra idiom for this exact problem. Acceptable.

**Minor gap in `errors_test.go`:** The test table covers 5 cases but omits explicit cases for `ErrUsage` (→1), `ErrArchive` (→4), `ErrSemantic` (→5), and `context.DeadlineExceeded` (→130). The spec says "strict TDD" but doesn't prescribe which exact cases must be in the test file — all 7 code paths are implemented correctly in `errors.go`. This is a test coverage gap, not a functional defect.

agentId: a2644869cc408b1cc (use SendMessage with to: 'a2644869cc408b1cc' to continue this agent)
<usage>total_tokens: 17652
tool_uses: 7
duration_ms: 39188</usage>

Prompt:
You are reviewing whether the Task 2 implementation matches its specification.

## Working Directory

`/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`

Compare commit `3b73e14` (Task 2) against its parent `ae477d5`. Run `git diff ae477d5..3b73e14` to see the change.

## What Was Requested

Task 2 creates the `cmd` package with `NewRootCmd()` returning a cobra root command (persistent flags `--crow-url`, `--timeout`, `--json`; defaults `https://crow.infrastructure.immuta.io` / `30s` / `false`) and an `ExitCode(err) int` helper that maps errors via `errors.Is`:

- nil → 0
- ErrUsage → 1
- client.ErrNotFound → 2
- ErrArchive → 4
- ErrSemantic → 5
- context.Canceled or context.DeadlineExceeded → 130
- default → 3

Files: `cmd/root.go`, `cmd/root_test.go`, `cmd/errors.go`, `cmd/errors_test.go`. Strict TDD. Tests under `go test -race`. Build must succeed.

The spec body of `NewRootCmd` was given as:

```go
func NewRootCmd() *cobra.Command {
	g := &GlobalFlags{}
	root := &cobra.Command{
		Use:           "birdseye",
		Short:         "Diff two versions of a Terraform module hosted in Crow",
		SilenceUsage:  true,
		SilenceErrors: true,
	}
	root.PersistentFlags().StringVar(&g.CrowURL, "crow-url", defaultCrowURL, "Crow base URL (env CROW_URL)")
	root.PersistentFlags().DurationVar(&g.Timeout, "timeout", 30*time.Second, "per-operation HTTP timeout")
	root.PersistentFlags().BoolVar(&g.JSON, "json", false, "emit structured JSON to stdout where supported")

	// Subcommands will be added in later tasks
	return root
}
```

The spec didn't include a `RunE`.

Commit message must be exactly: `feat(birdseye): cobra root command and exit-code mapping`

## What Implementer Claims They Built

- All 4 files plus go.mod/go.sum updates
- TDD followed
- 7/7 tests pass with `-race`
- `go build ./...` succeeds
- One spec deviation: added `RunE: cmd.Help()` (or equivalent) to make `TestNewRootCmdHelp` pass — cobra's default help wasn't producing the flag listing through the captured buffer.

## CRITICAL: Verify Independently

Read the actual files. Don't trust the report.

**Read these files in the worktree:**
- `apps/birdseye/cmd/root.go`
- `apps/birdseye/cmd/root_test.go`
- `apps/birdseye/cmd/errors.go`
- `apps/birdseye/cmd/errors_test.go`
- Run `git show --stat 3b73e14` to see what was committed
- Run `git diff ae477d5..3b73e14 -- apps/birdseye/cmd/` to see the diff
- Optionally: `cd apps/birdseye && go test -race ./cmd/... -v` to confirm tests still pass

## Specific Checks

**Missing requirements:**
- Both test files match the spec verbatim?
- ExitCode covers all 7 cases in spec (nil, ErrUsage, ErrNotFound, ErrArchive, ErrSemantic, context.Canceled, context.DeadlineExceeded, default)?
- The 3 persistent flags exist with EXACT names (`crow-url`, `timeout`, `json`) and the correct defaults?

**Extra/unneeded work:**
- Did the implementer add features not in the spec? (Flag the `RunE` deviation — assess if it's justified or could be replaced with a simpler approach like removing SilenceUsage.)
- Are there any extra files beyond the four created?
- Did they add `cobra.MaximumNArgs` or other unrelated wiring?

**Misunderstandings:**
- `defaultCrowURL` constant present and used?
- `GlobalFlags` struct present with three fields (CrowURL, Timeout, JSON)?
- Are sentinel errors named exactly `ErrUsage`, `ErrArchive`, `ErrSemantic`?

**On the `RunE` deviation:** This is actually a legitimate cobra subtlety — without a `RunE` or `Run`, cobra v1.9.1 may print help to a different stream than what `SetOut` captures. If the implementer added `RunE` that simply calls `cmd.Help()` to fix the test, this is acceptable. If they added meaningful behavior (e.g., printing a custom banner, parsing args), flag it.

## Report

Use one of:
- ✅ Spec compliant (note the RunE deviation if present, but mark as passing if it's just `cmd.Help()`)
- ❌ Issues found: list with file:line references

Be terse.


Spec ✓ (the test-coverage "gap" is a faithful copy of the spec itself, not an implementer issue). Dispatching code quality reviewer.


Code quality review Task 2

Prompt:
You are reviewing the code quality of Task 2: birdseye cobra root + ExitCode helper.

## Working Directory

`/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`

## Diff to Review

```
BASE_SHA: ae477d5
HEAD_SHA: 3b73e14
```

Run `git diff ae477d5..3b73e14 -- apps/birdseye/cmd/` in the worktree to see the change. Four files were added under `apps/birdseye/cmd/`: `root.go`, `root_test.go`, `errors.go`, `errors_test.go`. go.mod/go.sum were updated to resolve `services/crow/pkg/client`.

## Context

This is Task 2 of an in-progress birdseye CLI implementation. Spec compliance has already been verified — DO NOT re-verify spec compliance. Your job is code quality only.

The project (Immuta operations repo) has sibling apps `apps/crowctl` and `apps/immanager` at:
- `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/crowctl/` (urfave/cli/v3-based, similar layout)
- `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/immanager/`

Reference them if useful for style comparison.

## Review Focus

**File organization & responsibility:**
- Does each new file have one clear responsibility?
- Is the `cmd` package cohesive — `root.go` for root command, `errors.go` for exit-code mapping?

**Idioms & style:**
- Does the package doc comment in `root.go` accurately describe the package?
- Are imports stdlib-first / third-party / local-modules grouped?
- Is the `RunE: cmd.Help()` workaround documented (comment explaining why)?
- Is `ExitCode` readable as a single switch? Are the cases ordered sensibly (most-specific to most-general)?
- Are sentinel errors named conventionally (`ErrFoo`)?

**Anti-patterns:**
- Unused identifiers? The `g := &GlobalFlags{}` variable is intentionally referenced only via flag binding; that's idiomatic, not unused.
- Magic strings / numbers? `defaultCrowURL` is a constant — good. Are exit codes (1/2/3/4/5/130) magic, or should they be named constants?
- Repeated test boilerplate? Are subtests structured tidily?
- Tests that verify implementation rather than behavior?

**Per-task plan checks:**
- Does each file have one clear responsibility with a well-defined interface? Yes/no.
- Did this change create files that are already large? Both files should be small scaffolding (~30-50 lines each).
- Are tests genuine behavior verification (build a fresh command, exercise it, assert observable output)?

## Report Format

Be brief:

**Strengths:** (1-3 bullets)

**Issues:**
- Critical: (likely none)
- Important: (worth fixing now)
- Minor: (nice-to-haves)

**Assessment:** APPROVED | NEEDS_CHANGES


Now I have everything I need. Let me do the review.

---

**Strengths:**

- `GlobalFlags` as a constructor-local struct passed by pointer is the right pattern — no package-level state, each `NewRootCmd()` call is fully isolated, which is exactly what the test comment in the package doc promises.
- `ExitCode` as a pure function with a flat `switch` on `errors.Is` is readable and testable with zero setup — good separation from cobra command logic.
- The `// Subcommands will be added in later tasks:` comment with the commented-out `AddCommand` lines is good scaffolding hygiene; it explicitly signals the hook points for future tasks.

**Issues:**

**Critical:** none.

**Important:**

- **`g` is silently dropped — `GlobalFlags` is inaccessible to subcommands.** `g := &GlobalFlags{}` is bound to flag values but never stored anywhere that `newDiffCmd(g)` (or any future subcommand) can reach. The flags are parsed but there is no way for a subcommand to retrieve parsed values from `g` — it will only ever see zero values. The pattern needs either storing `g` on the `*cobra.Command` context (e.g. via `cmd.SetContext` + `context.WithValue`) or, more commonly with cobra, returning a `(cmd *cobra.Command, flags *GlobalFlags)` pair so the caller can thread `g` into subcommand constructors. Until subcommands exist this is a latent bug, but the architecture is wrong and will require a refactor when Task 3 lands. Fix it now before wiring starts.

- **Exit code 130 maps to context cancellation, but there is no test for `context.DeadlineExceeded`.** The test table covers `context.Canceled` and a wrapped `ErrNotFound`, but `DeadlineExceeded` is a separate sentinel handled by the same case arm. Add a row — it takes 30 seconds and guards against a future refactor splitting the arm.

- **`ErrSemantic`'s doc string ("HCL parse error") is too narrow.** The sentinel is named `ErrSemantic`, which suggests any semantic/validation failure, but the comment locks it to HCL. If a future command produces a semantic error from something other than HCL this comment will mislead. Either rename to `ErrHCLParse` or broaden the doc string to "semantic/parse error (e.g. HCL)".

**Minor:**

- **Exit codes are bare integer literals.** 1/2/3/4/5/130 appear directly in `ExitCode`. Naming them (`exitCodeUsage = 1`, `exitCodeNotFound = 2`, etc.) as unexported `const` block members inside `errors.go` costs five lines and makes the spec table traceable without grepping. This is genuinely minor since `ExitCode` is the single site, but it would make the code self-documenting when someone asks "where does exit 4 come from?"

- **`RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }` lacks an explanatory comment.** The review spec calls this out — it's a known cobra pattern (cobra does not run `Help()` automatically when no subcommand is given if `RunE` is nil; setting `RunE` to call `Help()` is the standard workaround to avoid the "unknown command" error path). A single-line comment like `// RunE prints help when no subcommand is given; cobra requires an explicit handler for this.` would clarify the intent for the next reader.

- **`TestNewRootCmdDefaults` tests flag registration, not behavior.** Reaching into `PersistentFlags().GetString("crow-url")` verifies that the flag was registered with a specific default — that is testing cobra internals rather than observable CLI behavior. The more resilient equivalent is to execute `root --crow-url https://example.com` and assert the value reached whatever the command does with it. At this scaffolding stage that isn't fully possible yet, so this is acceptable for now, but it should be revisited when subcommands are wired.

**Assessment:** NEEDS_CHANGES

The `GlobalFlags` inaccessibility issue (Important #1) is a structural problem that will force a refactor as soon as Task 3 adds a real subcommand. Everything else can be addressed in a single follow-up pass. The `DeadlineExceeded` test gap and the `ErrSemantic` doc string are also worth fixing before this lands — both are small.

agentId: aa16a80318e1e64e6 (use SendMessage with to: 'aa16a80318e1e64e6' to continue this agent)
<usage>total_tokens: 27295
tool_uses: 6
duration_ms: 43559</usage>

Prompt:
You are reviewing the code quality of Task 2: birdseye cobra root + ExitCode helper.

## Working Directory

`/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`

## Diff to Review

```
BASE_SHA: ae477d5
HEAD_SHA: 3b73e14
```

Run `git diff ae477d5..3b73e14 -- apps/birdseye/cmd/` in the worktree to see the change. Four files were added under `apps/birdseye/cmd/`: `root.go`, `root_test.go`, `errors.go`, `errors_test.go`. go.mod/go.sum were updated to resolve `services/crow/pkg/client`.

## Context

This is Task 2 of an in-progress birdseye CLI implementation. Spec compliance has already been verified — DO NOT re-verify spec compliance. Your job is code quality only.

The project (Immuta operations repo) has sibling apps `apps/crowctl` and `apps/immanager` at:
- `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/crowctl/` (urfave/cli/v3-based, similar layout)
- `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/immanager/`

Reference them if useful for style comparison.

## Review Focus

**File organization & responsibility:**
- Does each new file have one clear responsibility?
- Is the `cmd` package cohesive — `root.go` for root command, `errors.go` for exit-code mapping?

**Idioms & style:**
- Does the package doc comment in `root.go` accurately describe the package?
- Are imports stdlib-first / third-party / local-modules grouped?
- Is the `RunE: cmd.Help()` workaround documented (comment explaining why)?
- Is `ExitCode` readable as a single switch? Are the cases ordered sensibly (most-specific to most-general)?
- Are sentinel errors named conventionally (`ErrFoo`)?

**Anti-patterns:**
- Unused identifiers? The `g := &GlobalFlags{}` variable is intentionally referenced only via flag binding; that's idiomatic, not unused.
- Magic strings / numbers? `defaultCrowURL` is a constant — good. Are exit codes (1/2/3/4/5/130) magic, or should they be named constants?
- Repeated test boilerplate? Are subtests structured tidily?
- Tests that verify implementation rather than behavior?

**Per-task plan checks:**
- Does each file have one clear responsibility with a well-defined interface? Yes/no.
- Did this change create files that are already large? Both files should be small scaffolding (~30-50 lines each).
- Are tests genuine behavior verification (build a fresh command, exercise it, assert observable output)?

## Report Format

Be brief:

**Strengths:** (1-3 bullets)

**Issues:**
- Critical: (likely none)
- Important: (worth fixing now)
- Minor: (nice-to-haves)

**Assessment:** APPROVED | NEEDS_CHANGES


The reviewer flagged "Important #1" (GlobalFlags inaccessibility) — but that's a **false positive**. The plan deliberately uses a shared pointer pattern: `g` is constructed in `NewRootCmd` and passed by pointer to `newDiffCmd(g)` / `newVersionsCmd(g)` (per the commented scaffold). Cobra writes parsed values into `*g`; subcommand constructors close over the same pointer. The reviewer didn't see Tasks 3+ context, so they reasoned about Task 2 in isolation.

The other notes (DeadlineExceeded test case, ErrSemantic doc, named exit-code constants, RunE comment) are valid minor improvements. Re-dispatching implementer to address only those.


Fix Task 2 review nits

Prompt:
You are addressing code-review feedback on Task 2 of the birdseye CLI. Commit `3b73e14` in worktree `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye` is the target.

## What to Change

Make small, focused edits to address these review nits. Use the Edit tool. Do NOT restructure the package.

**Important — what to IGNORE:**

The reviewer flagged that `g := &GlobalFlags{}` in `NewRootCmd()` is "inaccessible to subcommands." That feedback is incorrect. The design intentionally uses a shared pointer: `g` is constructed in `NewRootCmd` and passed by pointer to subcommand constructors (`newDiffCmd(g)`, `newVersionsCmd(g)`) which will be added in later tasks. Cobra writes parsed flag values into `*g`, and subcommand constructors close over the same pointer. **Do not change this pattern.** The commented scaffold inside `NewRootCmd` already documents this intent.

## Specific Edits

### Edit 1: Add a `DeadlineExceeded` test case

In `apps/birdseye/cmd/errors_test.go`, add a row to the `TestExitCode` cases slice:

```go
{"context-deadline", context.DeadlineExceeded, 130},
```

Place it directly after the `"context-cancelled"` row.

### Edit 2: Broaden `ErrSemantic` doc

In `apps/birdseye/cmd/errors.go`, the current declaration:

```go
ErrSemantic = errors.New("HCL parse error")
```

Change to:

```go
// ErrSemantic indicates a parse or validation error during semantic
// (HCL-aware) diffing. Maps to exit code 5.
ErrSemantic = errors.New("semantic diff parse error")
```

Also add adjacent doc comments to `ErrUsage` and `ErrArchive` for parity, e.g.:

```go
// ErrUsage indicates a CLI usage error (bad args, malformed module
// address, missing TTY for interactive mode). Maps to exit code 1.
ErrUsage = errors.New("usage error")

// ErrArchive indicates the fetched module tarball was corrupt or
// contained unsafe paths. Maps to exit code 4.
ErrArchive = errors.New("archive corrupt or unsafe")
```

### Edit 3: Add explanatory comment to the `RunE` workaround

In `apps/birdseye/cmd/root.go`, the `RunE: func...` line lacks context. Add a comment above the struct literal field. Something like:

```go
// Without a RunE, cobra v1.x prints help via its internal stderr
// writer rather than the writer set by SetOut, which breaks
// TestNewRootCmdHelp's captured buffer. Calling Help() explicitly
// routes the output correctly. Subcommands will not invoke this.
RunE: func(cmd *cobra.Command, args []string) error {
    return cmd.Help()
},
```

### Edit 4: Named exit-code constants

In `apps/birdseye/cmd/errors.go`, add unexported constants for the exit codes and reference them in `ExitCode`:

```go
const (
    exitOK              = 0
    exitUsage           = 1
    exitNotFound        = 2
    exitGeneric         = 3
    exitArchive         = 4
    exitSemantic        = 5
    exitUserCancelled   = 130
)
```

Replace the bare integer returns in `ExitCode` to use these constants. Example:

```go
func ExitCode(err error) int {
    switch {
    case err == nil:
        return exitOK
    case errors.Is(err, ErrUsage):
        return exitUsage
    case errors.Is(err, client.ErrNotFound):
        return exitNotFound
    case errors.Is(err, ErrArchive):
        return exitArchive
    case errors.Is(err, ErrSemantic):
        return exitSemantic
    case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
        return exitUserCancelled
    default:
        return exitGeneric
    }
}
```

## After Edits

- [ ] Run `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race ./cmd/... -v` — all 8 cases (including the new `context-deadline`) must pass.
- [ ] Run `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go build ./...` — succeeds.
- [ ] Run `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go vet ./...` — succeeds.

## Commit

```bash
cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye && git add apps/birdseye/cmd/errors.go apps/birdseye/cmd/errors_test.go apps/birdseye/cmd/root.go && git commit -m "refactor(birdseye): named exit codes, broaden ErrSemantic, document RunE workaround"
```

## Report Format

- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
- Files changed
- Test output summary (8 cases passing)
- Commit SHA
- Anything unexpected


---

**Status:** DONE

**Files changed:**
- `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/errors.go` — added named exit-code constants (`exitOK` … `exitUserCancelled`), replaced bare integer returns in `ExitCode`, added per-sentinel doc comments, broadened `ErrSemantic` message from `"HCL parse error"` to `"semantic diff parse error"`
- `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/errors_test.go` — added `{"context-deadline", context.DeadlineExceeded, 130}` row after the `"context-cancelled"` row
- `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/cmd/root.go` — added explanatory comment above `RunE` documenting why `cmd.Help()` is called explicitly

**Test output:** 8 cases, all PASS (including new `context-deadline`); `go build ./...` and `go vet ./...` both clean.

**Commit SHA:** `59cd0b6`

**Nothing unexpected** — `errors.go` already had the correct `context.DeadlineExceeded` branch; the only work was wiring it to the named constant and adding the test case to exercise it.

agentId: a641a84c9102f0715 (use SendMessage with to: 'a641a84c9102f0715' to continue this agent)
<usage>total_tokens: 16909
tool_uses: 9
duration_ms: 117764</usage>

Prompt:
You are addressing code-review feedback on Task 2 of the birdseye CLI. Commit `3b73e14` in worktree `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye` is the target.

## What to Change

Make small, focused edits to address these review nits. Use the Edit tool. Do NOT restructure the package.

**Important — what to IGNORE:**

The reviewer flagged that `g := &GlobalFlags{}` in `NewRootCmd()` is "inaccessible to subcommands." That feedback is incorrect. The design intentionally uses a shared pointer: `g` is constructed in `NewRootCmd` and passed by pointer to subcommand constructors (`newDiffCmd(g)`, `newVersionsCmd(g)`) which will be added in later tasks. Cobra writes parsed flag values into `*g`, and subcommand constructors close over the same pointer. **Do not change this pattern.** The commented scaffold inside `NewRootCmd` already documents this intent.

## Specific Edits

### Edit 1: Add a `DeadlineExceeded` test case

In `apps/birdseye/cmd/errors_test.go`, add a row to the `TestExitCode` cases slice:

```go
{"context-deadline", context.DeadlineExceeded, 130},
```

Place it directly after the `"context-cancelled"` row.

### Edit 2: Broaden `ErrSemantic` doc

In `apps/birdseye/cmd/errors.go`, the current declaration:

```go
ErrSemantic = errors.New("HCL parse error")
```

Change to:

```go
// ErrSemantic indicates a parse or validation error during semantic
// (HCL-aware) diffing. Maps to exit code 5.
ErrSemantic = errors.New("semantic diff parse error")
```

Also add adjacent doc comments to `ErrUsage` and `ErrArchive` for parity, e.g.:

```go
// ErrUsage indicates a CLI usage error (bad args, malformed module
// address, missing TTY for interactive mode). Maps to exit code 1.
ErrUsage = errors.New("usage error")

// ErrArchive indicates the fetched module tarball was corrupt or
// contained unsafe paths. Maps to exit code 4.
ErrArchive = errors.New("archive corrupt or unsafe")
```

### Edit 3: Add explanatory comment to the `RunE` workaround

In `apps/birdseye/cmd/root.go`, the `RunE: func...` line lacks context. Add a comment above the struct literal field. Something like:

```go
// Without a RunE, cobra v1.x prints help via its internal stderr
// writer rather than the writer set by SetOut, which breaks
// TestNewRootCmdHelp's captured buffer. Calling Help() explicitly
// routes the output correctly. Subcommands will not invoke this.
RunE: func(cmd *cobra.Command, args []string) error {
    return cmd.Help()
},
```

### Edit 4: Named exit-code constants

In `apps/birdseye/cmd/errors.go`, add unexported constants for the exit codes and reference them in `ExitCode`:

```go
const (
    exitOK              = 0
    exitUsage           = 1
    exitNotFound        = 2
    exitGeneric         = 3
    exitArchive         = 4
    exitSemantic        = 5
    exitUserCancelled   = 130
)
```

Replace the bare integer returns in `ExitCode` to use these constants. Example:

```go
func ExitCode(err error) int {
    switch {
    case err == nil:
        return exitOK
    case errors.Is(err, ErrUsage):
        return exitUsage
    case errors.Is(err, client.ErrNotFound):
        return exitNotFound
    case errors.Is(err, ErrArchive):
        return exitArchive
    case errors.Is(err, ErrSemantic):
        return exitSemantic
    case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
        return exitUserCancelled
    default:
        return exitGeneric
    }
}
```

## After Edits

- [ ] Run `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race ./cmd/... -v` — all 8 cases (including the new `context-deadline`) must pass.
- [ ] Run `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go build ./...` — succeeds.
- [ ] Run `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go vet ./...` — succeeds.

## Commit

```bash
cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye && git add apps/birdseye/cmd/errors.go apps/birdseye/cmd/errors_test.go apps/birdseye/cmd/root.go && git commit -m "refactor(birdseye): named exit codes, broaden ErrSemantic, document RunE workaround"
```

## Report Format

- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
- Files changed
- Test output summary (8 cases passing)
- Commit SHA
- Anything unexpected


Re-dispatching code quality reviewer to verify the fixes.


Re-review Task 2 fixes

Prompt:
You are re-reviewing the code quality of Task 2 of the birdseye CLI after a fix commit.

## Working Directory

`/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`

## Diff to Review

```
BASE_SHA: ae477d5
HEAD_SHA: 59cd0b6
```

Run `git diff ae477d5..59cd0b6 -- apps/birdseye/cmd/` in the worktree. Two commits:
- `3b73e14` — original Task 2 (root command + ExitCode)
- `59cd0b6` — fix commit addressing review nits

## Previous Review Concerns

The first quality review flagged:

1. **`g := &GlobalFlags{}` inaccessible to subcommands** — DISPUTED, NOT FIXED. The controller determined this was a false positive: the design uses a shared pointer that subcommand constructors (`newDiffCmd(g)`, `newVersionsCmd(g)`, added in future tasks) close over. Cobra writes into `*g` during flag parsing. **You do NOT need to re-flag this issue.**

2. `DeadlineExceeded` not tested — FIXED in `59cd0b6`
3. `ErrSemantic` doc too narrow — FIXED in `59cd0b6` (now `"semantic diff parse error"` with broader comment)
4. Bare exit-code integers — FIXED in `59cd0b6` (named constants `exitOK`…`exitUserCancelled`)
5. `RunE` lacks explanatory comment — FIXED in `59cd0b6`
6. `TestNewRootCmdDefaults` tests cobra internals (minor) — NOT FIXED (was deferred to when subcommands exist)

## Your Job

Verify that the fixes in `59cd0b6` were applied correctly and didn't break anything. Use the worktree to:

- Read `apps/birdseye/cmd/errors.go` and confirm:
  - Named constants exist (`exitOK = 0`, `exitUsage = 1`, etc.)
  - `ExitCode` uses the named constants in its return statements
  - `ErrSemantic` doc/message is broadened
  - `ErrUsage` and `ErrArchive` got per-sentinel doc comments
- Read `apps/birdseye/cmd/errors_test.go` and confirm:
  - `context.DeadlineExceeded` test case present
  - All 6 cases (or 8 — count them) are valid
- Read `apps/birdseye/cmd/root.go` and confirm:
  - A comment explains the `RunE: cmd.Help()` workaround
  - Otherwise unchanged
- Run `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race ./cmd/... -v` and confirm all tests pass

## Report Format

**Strengths:** (1-3 bullets, only fixes-related)

**Outstanding issues:**
- Critical: (likely none)
- Important: (worth blocking on)
- Minor: (nice-to-haves)

**Assessment:** APPROVED | NEEDS_CHANGES

Be brief.


All tests pass. Here's the assessment:

**Strengths:**
- Named exit-code constants (`exitOK = 0` through `exitUserCancelled = 130`) are present and cleanly applied throughout `ExitCode` — no bare integers remain.
- `context.DeadlineExceeded` is now tested alongside `context.Canceled` in a dedicated `"context-deadline"` case; the `wrapped-not-found` case validates `errors.Is` chain behavior.
- The `RunE` workaround comment in `root.go` is clear and accurate, explaining both the cobra v1.x stderr-routing issue and that subcommands bypass the handler.

**Outstanding issues:**
- Critical: none
- Important: none
- Minor: The test suite covers 6 of the 7 exit codes (missing explicit cases for `ErrUsage`, `ErrArchive`, and `ErrSemantic` sentinels). `generic` covers the default path, but the three named sentinels above go untested. Low risk given the function is a straight switch, but worth noting for completeness.

**Assessment: APPROVED**

The three fixes were applied correctly and nothing was broken. The minor gap (no test cases for `ErrUsage`/`ErrArchive`/`ErrSemantic`) existed before this commit and is a pre-existing omission rather than a regression — not worth blocking on.

agentId: a524332671adb1be5 (use SendMessage with to: 'a524332671adb1be5' to continue this agent)
<usage>total_tokens: 14998
tool_uses: 2
duration_ms: 103783</usage>

Prompt:
You are re-reviewing the code quality of Task 2 of the birdseye CLI after a fix commit.

## Working Directory

`/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`

## Diff to Review

```
BASE_SHA: ae477d5
HEAD_SHA: 59cd0b6
```

Run `git diff ae477d5..59cd0b6 -- apps/birdseye/cmd/` in the worktree. Two commits:
- `3b73e14` — original Task 2 (root command + ExitCode)
- `59cd0b6` — fix commit addressing review nits

## Previous Review Concerns

The first quality review flagged:

1. **`g := &GlobalFlags{}` inaccessible to subcommands** — DISPUTED, NOT FIXED. The controller determined this was a false positive: the design uses a shared pointer that subcommand constructors (`newDiffCmd(g)`, `newVersionsCmd(g)`, added in future tasks) close over. Cobra writes into `*g` during flag parsing. **You do NOT need to re-flag this issue.**

2. `DeadlineExceeded` not tested — FIXED in `59cd0b6`
3. `ErrSemantic` doc too narrow — FIXED in `59cd0b6` (now `"semantic diff parse error"` with broader comment)
4. Bare exit-code integers — FIXED in `59cd0b6` (named constants `exitOK`…`exitUserCancelled`)
5. `RunE` lacks explanatory comment — FIXED in `59cd0b6`
6. `TestNewRootCmdDefaults` tests cobra internals (minor) — NOT FIXED (was deferred to when subcommands exist)

## Your Job

Verify that the fixes in `59cd0b6` were applied correctly and didn't break anything. Use the worktree to:

- Read `apps/birdseye/cmd/errors.go` and confirm:
  - Named constants exist (`exitOK = 0`, `exitUsage = 1`, etc.)
  - `ExitCode` uses the named constants in its return statements
  - `ErrSemantic` doc/message is broadened
  - `ErrUsage` and `ErrArchive` got per-sentinel doc comments
- Read `apps/birdseye/cmd/errors_test.go` and confirm:
  - `context.DeadlineExceeded` test case present
  - All 6 cases (or 8 — count them) are valid
- Read `apps/birdseye/cmd/root.go` and confirm:
  - A comment explains the `RunE: cmd.Help()` workaround
  - Otherwise unchanged
- Run `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test -race ./cmd/... -v` and confirm all tests pass

## Report Format

**Strengths:** (1-3 bullets, only fixes-related)

**Outstanding issues:**
- Critical: (likely none)
- Important: (worth blocking on)
- Minor: (nice-to-haves)

**Assessment:** APPROVED | NEEDS_CHANGES

Be brief.


Task 2 ✓. Moving to Task 3 — extend Crow client with `Download` method. This touches `services/crow/pkg/client/` (not birdseye).


Implement Task 3: client.Download

Prompt:
You are implementing Task 3 of the birdseye CLI implementation plan.

## Working Environment

- **Worktree path:** `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`
- **Git branch:** `worktree-birdseye`
- Recent commits: Task 1 (`ae477d5`), Task 2 (`3b73e14`), Task 2 fixes (`59cd0b6`)
- Use absolute paths or `cd` into the worktree. Do not operate on the main repo at `/Users/steve.briggs/projects/operations`.
- **NEVER push.** **NEVER use `git add .` or `git add -A`.** Always `git add` specific paths.

## Context

This task does NOT modify `apps/birdseye/`. It extends Immuta's existing Crow service client at `services/crow/pkg/client/` with a new `Download` method. The Crow service implements the Terraform Module Registry Protocol: when you GET `/v1/modules/{ns}/{name}/{provider}/{version}/download`, it responds with HTTP 204 and an `X-Terraform-Get` header pointing at an S3 presigned URL. You then GET that URL to fetch the actual tarball bytes.

Existing client methods (read `services/crow/pkg/client/client.go` to confirm):
- `Upload(ctx, m api.Module, body io.Reader) error`
- `Exists(ctx, m api.Module) (bool, error)`
- `ListVersions(ctx, id api.ModuleIdentifier) ([]string, error)`
- Sentinel errors: `ErrAlreadyExists`, `ErrNotFound`

The existing tests at `services/crow/pkg/client/client_test.go` use `httptest.NewServer` patterns. You'll follow that style.

Use strict TDD: write the failing tests first, run them, then implement.

## Task Description (Verbatim)

### Task 3: Add `Download` method to Crow client

**Files:**
- Modify: `services/crow/pkg/client/client.go`
- Modify: `services/crow/pkg/client/client_test.go`

- [ ] **Step 1: Write failing test** — append to `services/crow/pkg/client/client_test.go`:

```go
func TestDownloadOK(t *testing.T) {
	mux := http.NewServeMux()
	// Tarball server: serves the actual gzipped bytes.
	tarball := []byte("fake-tarball-bytes")
	tarSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/blob/iac/comply/aws/1.0.0.tar.gz" {
			t.Errorf("tarball server got unexpected path: %s", r.URL.Path)
		}
		_, _ = w.Write(tarball)
	}))
	defer tarSrv.Close()

	// Crow server: returns 204 + X-Terraform-Get pointing at tarSrv.
	mux.HandleFunc("/v1/modules/iac/comply/aws/1.0.0/download", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			t.Errorf("expected GET, got %s", r.Method)
		}
		w.Header().Set("X-Terraform-Get", tarSrv.URL+"/blob/iac/comply/aws/1.0.0.tar.gz")
		w.WriteHeader(http.StatusNoContent)
	})
	crowSrv := httptest.NewServer(mux)
	defer crowSrv.Close()

	cli := New(crowSrv.URL, WithHTTPClient(crowSrv.Client()))
	rc, err := cli.Download(context.Background(), testModule())
	if err != nil {
		t.Fatalf("Download: %v", err)
	}
	defer rc.Close()
	got, err := io.ReadAll(rc)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if !bytes.Equal(got, tarball) {
		t.Errorf("body = %q, want %q", got, tarball)
	}
}

func TestDownloadNotFound(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusNotFound)
	}))
	defer srv.Close()

	cli := New(srv.URL, WithHTTPClient(srv.Client()))
	_, err := cli.Download(context.Background(), testModule())
	if !errors.Is(err, ErrNotFound) {
		t.Fatalf("expected ErrNotFound, got %v", err)
	}
}

func TestDownloadMissingHeader(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusNoContent) // no X-Terraform-Get header
	}))
	defer srv.Close()

	cli := New(srv.URL, WithHTTPClient(srv.Client()))
	_, err := cli.Download(context.Background(), testModule())
	if err == nil {
		t.Fatal("expected error for missing X-Terraform-Get header")
	}
}
```

NOTE: `testModule()` already exists in `client_test.go` and returns an `api.Module` for `iac/comply/aws@1.0.0`. The download path resolves to `/v1/modules/iac/comply/aws/1.0.0/download`.

- [ ] **Step 2: Run, expect FAIL**

Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/services/crow && go test -race ./pkg/client/... -run TestDownload -v`
Expected: compile error (undefined Download).

- [ ] **Step 3: Implement `Download` in `services/crow/pkg/client/client.go`**

Append below `ListVersions`:

```go
// Download fetches the gzipped tarball bytes for the given module
// version. It follows the X-Terraform-Get redirect returned by Crow's
// 204 response (per the Terraform module registry protocol). The caller
// is responsible for closing the returned ReadCloser.
//
// Returns ErrNotFound on HTTP 404 from Crow.
func (c *Client) Download(ctx context.Context, m api.Module) (io.ReadCloser, error) {
	if err := m.Validate(); err != nil {
		return nil, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+m.DownloadPath(), nil)
	if err != nil {
		return nil, err
	}
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, err
	}

	// The protocol uses 204 + X-Terraform-Get to point clients at the
	// real artifact location (S3 presigned URL in production).
	if resp.StatusCode == http.StatusNotFound {
		_ = resp.Body.Close()
		return nil, ErrNotFound
	}
	if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
		_ = resp.Body.Close()
		return nil, fmt.Errorf("unexpected status %d resolving %s", resp.StatusCode, m.DownloadPath())
	}
	location := resp.Header.Get("X-Terraform-Get")
	_ = resp.Body.Close()
	if location == "" {
		return nil, fmt.Errorf("crow returned %d with no X-Terraform-Get header for %s", resp.StatusCode, m.DownloadPath())
	}

	blobReq, err := http.NewRequestWithContext(ctx, http.MethodGet, location, nil)
	if err != nil {
		return nil, err
	}
	blobResp, err := c.httpClient.Do(blobReq)
	if err != nil {
		return nil, fmt.Errorf("fetching presigned blob: %w", err)
	}
	if blobResp.StatusCode != http.StatusOK {
		_ = blobResp.Body.Close()
		return nil, fmt.Errorf("unexpected status %d fetching blob %s", blobResp.StatusCode, location)
	}
	return blobResp.Body, nil
}
```

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

Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/services/crow && go test -race ./pkg/client/... -v`
Expected: All PASS (existing tests + new Download tests).

- [ ] **Step 5: Commit**

```bash
cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye && git add services/crow/pkg/client/client.go services/crow/pkg/client/client_test.go && git commit -m "feat(crow/client): add Download method for module tarballs"
```

## Notes & Style

- The existing `client_test.go` imports `bytes`, `context`, `encoding/json`, `errors`, `io`, `net/http`, `net/http/httptest`, `testing`, `time`, and `github.com/immuta/operations/services/crow/pkg/api`. You should NOT need to add new imports — `bytes`, `io`, `errors`, `context`, `net/http`, `net/http/httptest` are all already imported. Confirm before editing.
- Style: match the existing test patterns (no testify; bare `t.Errorf`/`t.Fatalf`).
- Style: match the existing implementation patterns — return a switch-based status handler when natural; keep error wrapping with `fmt.Errorf("...: %w", err)`.
- Only modify `services/crow/pkg/client/client.go` and `services/crow/pkg/client/client_test.go`. Do not edit `pkg/api`, the server handlers, or anything else.

## Code Organization

The crow client is a single-file package by convention; keep `Download` in `client.go` next to the other methods. Do NOT split into a separate file.

## Before You Begin

Ask if anything is unclear. Otherwise proceed.

## Self-Review Checklist

- [ ] All 3 new tests pass (`TestDownloadOK`, `TestDownloadNotFound`, `TestDownloadMissingHeader`)
- [ ] All pre-existing tests in `services/crow/pkg/client/` still pass
- [ ] `errors.Is(err, ErrNotFound)` returns true for the 404 case
- [ ] No bytes are leaked — the response body of the discovery request (204) is closed before the second request runs
- [ ] Caller's `defer rc.Close()` on the returned body works
- [ ] Commit message exactly: `feat(crow/client): add Download method for module tarballs`
- [ ] Only `services/crow/pkg/client/client.go` and `services/crow/pkg/client/client_test.go` are staged
- [ ] `go test -race ./pkg/client/... -v` from within `services/crow/` is fully green

## Report Format

- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
- Implementation summary
- Test count (existing + 3 new = ?)
- Files committed
- Commit SHA
- Self-review findings
- Anything unexpected


---

**Status:** DONE

**Implementation summary:** Added a `Download(ctx context.Context, m api.Module) (io.ReadCloser, error)` method to the Crow client. It GETs the Terraform module registry download endpoint, handles 404 → `ErrNotFound`, validates the `X-Terraform-Get` header is present, then issues a second GET to fetch the actual tarball bytes from the presigned URL, returning the response body as an `io.ReadCloser` for the caller to consume and close.

**Test count:** 9 pre-existing + 3 new = 12 total, all passing with `-race`.

**Files committed:**
- `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/services/crow/pkg/client/client.go`
- `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/services/crow/pkg/client/client_test.go`

**Commit SHA:** `ee7794e`

**Self-review findings:**
- All 3 new tests pass (`TestDownloadOK`, `TestDownloadNotFound`, `TestDownloadMissingHeader`)
- All 9 pre-existing tests still pass
- `errors.Is(err, ErrNotFound)` works correctly for the 404 case (sentinel error returned directly)
- Discovery response body (204) is explicitly closed before the second request is issued
- Caller's `defer rc.Close()` works — the returned `ReadCloser` is `blobResp.Body`
- Commit message matches exactly
- Only the two specified files are staged and committed
- `go test -race ./pkg/client/... -v` is fully green

Nothing unexpected encountered.

agentId: ac702f0183fd137fe (use SendMessage with to: 'ac702f0183fd137fe' to continue this agent)
<usage>total_tokens: 22901
tool_uses: 7
duration_ms: 95834</usage>

Prompt:
You are implementing Task 3 of the birdseye CLI implementation plan.

## Working Environment

- **Worktree path:** `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`
- **Git branch:** `worktree-birdseye`
- Recent commits: Task 1 (`ae477d5`), Task 2 (`3b73e14`), Task 2 fixes (`59cd0b6`)
- Use absolute paths or `cd` into the worktree. Do not operate on the main repo at `/Users/steve.briggs/projects/operations`.
- **NEVER push.** **NEVER use `git add .` or `git add -A`.** Always `git add` specific paths.

## Context

This task does NOT modify `apps/birdseye/`. It extends Immuta's existing Crow service client at `services/crow/pkg/client/` with a new `Download` method. The Crow service implements the Terraform Module Registry Protocol: when you GET `/v1/modules/{ns}/{name}/{provider}/{version}/download`, it responds with HTTP 204 and an `X-Terraform-Get` header pointing at an S3 presigned URL. You then GET that URL to fetch the actual tarball bytes.

Existing client methods (read `services/crow/pkg/client/client.go` to confirm):
- `Upload(ctx, m api.Module, body io.Reader) error`
- `Exists(ctx, m api.Module) (bool, error)`
- `ListVersions(ctx, id api.ModuleIdentifier) ([]string, error)`
- Sentinel errors: `ErrAlreadyExists`, `ErrNotFound`

The existing tests at `services/crow/pkg/client/client_test.go` use `httptest.NewServer` patterns. You'll follow that style.

Use strict TDD: write the failing tests first, run them, then implement.

## Task Description (Verbatim)

### Task 3: Add `Download` method to Crow client

**Files:**
- Modify: `services/crow/pkg/client/client.go`
- Modify: `services/crow/pkg/client/client_test.go`

- [ ] **Step 1: Write failing test** — append to `services/crow/pkg/client/client_test.go`:

```go
func TestDownloadOK(t *testing.T) {
	mux := http.NewServeMux()
	// Tarball server: serves the actual gzipped bytes.
	tarball := []byte("fake-tarball-bytes")
	tarSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/blob/iac/comply/aws/1.0.0.tar.gz" {
			t.Errorf("tarball server got unexpected path: %s", r.URL.Path)
		}
		_, _ = w.Write(tarball)
	}))
	defer tarSrv.Close()

	// Crow server: returns 204 + X-Terraform-Get pointing at tarSrv.
	mux.HandleFunc("/v1/modules/iac/comply/aws/1.0.0/download", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			t.Errorf("expected GET, got %s", r.Method)
		}
		w.Header().Set("X-Terraform-Get", tarSrv.URL+"/blob/iac/comply/aws/1.0.0.tar.gz")
		w.WriteHeader(http.StatusNoContent)
	})
	crowSrv := httptest.NewServer(mux)
	defer crowSrv.Close()

	cli := New(crowSrv.URL, WithHTTPClient(crowSrv.Client()))
	rc, err := cli.Download(context.Background(), testModule())
	if err != nil {
		t.Fatalf("Download: %v", err)
	}
	defer rc.Close()
	got, err := io.ReadAll(rc)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if !bytes.Equal(got, tarball) {
		t.Errorf("body = %q, want %q", got, tarball)
	}
}

func TestDownloadNotFound(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusNotFound)
	}))
	defer srv.Close()

	cli := New(srv.URL, WithHTTPClient(srv.Client()))
	_, err := cli.Download(context.Background(), testModule())
	if !errors.Is(err, ErrNotFound) {
		t.Fatalf("expected ErrNotFound, got %v", err)
	}
}

func TestDownloadMissingHeader(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusNoContent) // no X-Terraform-Get header
	}))
	defer srv.Close()

	cli := New(srv.URL, WithHTTPClient(srv.Client()))
	_, err := cli.Download(context.Background(), testModule())
	if err == nil {
		t.Fatal("expected error for missing X-Terraform-Get header")
	}
}
```

NOTE: `testModule()` already exists in `client_test.go` and returns an `api.Module` for `iac/comply/aws@1.0.0`. The download path resolves to `/v1/modules/iac/comply/aws/1.0.0/download`.

- [ ] **Step 2: Run, expect FAIL**

Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/services/crow && go test -race ./pkg/client/... -run TestDownload -v`
Expected: compile error (undefined Download).

- [ ] **Step 3: Implement `Download` in `services/crow/pkg/client/client.go`**

Append below `ListVersions`:

```go
// Download fetches the gzipped tarball bytes for the given module
// version. It follows the X-Terraform-Get redirect returned by Crow's
// 204 response (per the Terraform module registry protocol). The caller
// is responsible for closing the returned ReadCloser.
//
// Returns ErrNotFound on HTTP 404 from Crow.
func (c *Client) Download(ctx context.Context, m api.Module) (io.ReadCloser, error) {
	if err := m.Validate(); err != nil {
		return nil, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+m.DownloadPath(), nil)
	if err != nil {
		return nil, err
	}
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, err
	}

	// The protocol uses 204 + X-Terraform-Get to point clients at the
	// real artifact location (S3 presigned URL in production).
	if resp.StatusCode == http.StatusNotFound {
		_ = resp.Body.Close()
		return nil, ErrNotFound
	}
	if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
		_ = resp.Body.Close()
		return nil, fmt.Errorf("unexpected status %d resolving %s", resp.StatusCode, m.DownloadPath())
	}
	location := resp.Header.Get("X-Terraform-Get")
	_ = resp.Body.Close()
	if location == "" {
		return nil, fmt.Errorf("crow returned %d with no X-Terraform-Get header for %s", resp.StatusCode, m.DownloadPath())
	}

	blobReq, err := http.NewRequestWithContext(ctx, http.MethodGet, location, nil)
	if err != nil {
		return nil, err
	}
	blobResp, err := c.httpClient.Do(blobReq)
	if err != nil {
		return nil, fmt.Errorf("fetching presigned blob: %w", err)
	}
	if blobResp.StatusCode != http.StatusOK {
		_ = blobResp.Body.Close()
		return nil, fmt.Errorf("unexpected status %d fetching blob %s", blobResp.StatusCode, location)
	}
	return blobResp.Body, nil
}
```

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

Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/services/crow && go test -race ./pkg/client/... -v`
Expected: All PASS (existing tests + new Download tests).

- [ ] **Step 5: Commit**

```bash
cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye && git add services/crow/pkg/client/client.go services/crow/pkg/client/client_test.go && git commit -m "feat(crow/client): add Download method for module tarballs"
```

## Notes & Style

- The existing `client_test.go` imports `bytes`, `context`, `encoding/json`, `errors`, `io`, `net/http`, `net/http/httptest`, `testing`, `time`, and `github.com/immuta/operations/services/crow/pkg/api`. You should NOT need to add new imports — `bytes`, `io`, `errors`, `context`, `net/http`, `net/http/httptest` are all already imported. Confirm before editing.
- Style: match the existing test patterns (no testify; bare `t.Errorf`/`t.Fatalf`).
- Style: match the existing implementation patterns — return a switch-based status handler when natural; keep error wrapping with `fmt.Errorf("...: %w", err)`.
- Only modify `services/crow/pkg/client/client.go` and `services/crow/pkg/client/client_test.go`. Do not edit `pkg/api`, the server handlers, or anything else.

## Code Organization

The crow client is a single-file package by convention; keep `Download` in `client.go` next to the other methods. Do NOT split into a separate file.

## Before You Begin

Ask if anything is unclear. Otherwise proceed.

## Self-Review Checklist

- [ ] All 3 new tests pass (`TestDownloadOK`, `TestDownloadNotFound`, `TestDownloadMissingHeader`)
- [ ] All pre-existing tests in `services/crow/pkg/client/` still pass
- [ ] `errors.Is(err, ErrNotFound)` returns true for the 404 case
- [ ] No bytes are leaked — the response body of the discovery request (204) is closed before the second request runs
- [ ] Caller's `defer rc.Close()` on the returned body works
- [ ] Commit message exactly: `feat(crow/client): add Download method for module tarballs`
- [ ] Only `services/crow/pkg/client/client.go` and `services/crow/pkg/client/client_test.go` are staged
- [ ] `go test -race ./pkg/client/... -v` from within `services/crow/` is fully green

## Report Format

- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
- Implementation summary
- Test count (existing + 3 new = ?)
- Files committed
- Commit SHA
- Self-review findings
- Anything unexpected


Task 3 implementer reports DONE. Dispatching spec reviewer.


Spec review Task 3

Prompt:
You are reviewing spec compliance of Task 3: add `Download` method to the Crow client.

## Working Directory

`/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`

Diff: `git diff 59cd0b6..ee7794e -- services/crow/pkg/client/`

## What Was Requested

Modify `services/crow/pkg/client/client.go` to add a single new method:

```go
// Download fetches the gzipped tarball bytes for the given module
// version. It follows the X-Terraform-Get redirect returned by Crow's
// 204 response (per the Terraform module registry protocol). The caller
// is responsible for closing the returned ReadCloser.
//
// Returns ErrNotFound on HTTP 404 from Crow.
func (c *Client) Download(ctx context.Context, m api.Module) (io.ReadCloser, error) { ... }
```

Body (per plan):
1. Validate `m` via `m.Validate()`.
2. GET `c.baseURL + m.DownloadPath()` with the context.
3. On 404, return `ErrNotFound`.
4. On non-204/200, return `fmt.Errorf("unexpected status %d resolving %s", ...)`.
5. Close the discovery response body.
6. If `X-Terraform-Get` header is empty, return an error.
7. GET the URL from `X-Terraform-Get`.
8. On non-200, return `fmt.Errorf("unexpected status %d fetching blob %s", ...)`.
9. Return the blob's `resp.Body` (caller closes).

And append 3 tests to `client_test.go`:
- `TestDownloadOK` — full happy path with two httptest servers (mux for crow discovery, separate server for blob)
- `TestDownloadNotFound` — 404 → `errors.Is(err, ErrNotFound)`
- `TestDownloadMissingHeader` — 204 with no header → non-nil error

Commit message exactly: `feat(crow/client): add Download method for module tarballs`

## What Implementer Claims

- 3 tests added + Download implemented; 12 total pass with -race.
- Commit SHA ee7794e
- All checklist items confirmed

## Verify Independently

Read the diff. Check:

1. **Method signature** matches exactly `Download(ctx context.Context, m api.Module) (io.ReadCloser, error)`.
2. **404 path** returns `ErrNotFound` (sentinel, not wrapped — the plan returned `ErrNotFound` directly so `errors.Is` succeeds without unwrap).
3. **Missing header** path produces a non-nil error.
4. **Resource cleanup**: the first response's body is closed BEFORE the second request runs. If you see `defer resp.Body.Close()` followed by another `c.httpClient.Do(...)`, the defer hasn't fired yet — make sure it's a manual close.
5. **Method validation**: `m.Validate()` is called first.
6. **Tests match the spec verbatim** (or close to it — variable renames are fine, behavioral changes are not).
7. **No unrelated edits** — only `client.go` and `client_test.go` modified.
8. **Commit message exact.**

## Report

✅ Spec compliant OR ❌ Issues found with file:line refs. Be terse.


All checks pass. Here is the verdict:

✅ **Spec compliant**

1. **Method signature** — exact match: `Download(ctx context.Context, m api.Module) (io.ReadCloser, error)`.

2. **404 path** — `_ = resp.Body.Close()` then `return nil, ErrNotFound` (bare sentinel, no wrapping; `errors.Is` works without unwrap).

3. **Missing header** — body is closed, then `location == ""` returns a non-nil `fmt.Errorf(...)`.

4. **Resource cleanup** — first response body is closed with a manual `_ = resp.Body.Close()` (not a defer) at two points (404 branch, status-check branch) and again after extracting the header, all before the second `httpClient.Do(blobReq)` call. No stray `defer` on the first response.

5. **Validation** — `m.Validate()` is the first statement in the method body.

6. **Tests** — all three required tests present with correct behavioral coverage:
   - `TestDownloadOK`: two-server happy path, reads and compares bytes.
   - `TestDownloadNotFound`: 404 → `errors.Is(err, ErrNotFound)`.
   - `TestDownloadMissingHeader`: 204 with no header → non-nil error.

7. **No unrelated edits** — diff touches only `client.go` and `client_test.go`.

8. **Commit message** — `feat(crow/client): add Download method for module tarballs` (exact).

One minor note (not a spec violation): the missing-header error is checked *after* `resp.Body.Close()`, which is correct order, but the spec said "if `X-Terraform-Get` is empty, return an error" — the implementation reads the header, closes the body, *then* checks emptiness. That ordering satisfies resource cleanup while still returning the required error. No issue.

agentId: ad6f453ac415514c9 (use SendMessage with to: 'ad6f453ac415514c9' to continue this agent)
<usage>total_tokens: 14433
tool_uses: 1
duration_ms: 14471</usage>

Prompt:
You are reviewing spec compliance of Task 3: add `Download` method to the Crow client.

## Working Directory

`/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`

Diff: `git diff 59cd0b6..ee7794e -- services/crow/pkg/client/`

## What Was Requested

Modify `services/crow/pkg/client/client.go` to add a single new method:

```go
// Download fetches the gzipped tarball bytes for the given module
// version. It follows the X-Terraform-Get redirect returned by Crow's
// 204 response (per the Terraform module registry protocol). The caller
// is responsible for closing the returned ReadCloser.
//
// Returns ErrNotFound on HTTP 404 from Crow.
func (c *Client) Download(ctx context.Context, m api.Module) (io.ReadCloser, error) { ... }
```

Body (per plan):
1. Validate `m` via `m.Validate()`.
2. GET `c.baseURL + m.DownloadPath()` with the context.
3. On 404, return `ErrNotFound`.
4. On non-204/200, return `fmt.Errorf("unexpected status %d resolving %s", ...)`.
5. Close the discovery response body.
6. If `X-Terraform-Get` header is empty, return an error.
7. GET the URL from `X-Terraform-Get`.
8. On non-200, return `fmt.Errorf("unexpected status %d fetching blob %s", ...)`.
9. Return the blob's `resp.Body` (caller closes).

And append 3 tests to `client_test.go`:
- `TestDownloadOK` — full happy path with two httptest servers (mux for crow discovery, separate server for blob)
- `TestDownloadNotFound` — 404 → `errors.Is(err, ErrNotFound)`
- `TestDownloadMissingHeader` — 204 with no header → non-nil error

Commit message exactly: `feat(crow/client): add Download method for module tarballs`

## What Implementer Claims

- 3 tests added + Download implemented; 12 total pass with -race.
- Commit SHA ee7794e
- All checklist items confirmed

## Verify Independently

Read the diff. Check:

1. **Method signature** matches exactly `Download(ctx context.Context, m api.Module) (io.ReadCloser, error)`.
2. **404 path** returns `ErrNotFound` (sentinel, not wrapped — the plan returned `ErrNotFound` directly so `errors.Is` succeeds without unwrap).
3. **Missing header** path produces a non-nil error.
4. **Resource cleanup**: the first response's body is closed BEFORE the second request runs. If you see `defer resp.Body.Close()` followed by another `c.httpClient.Do(...)`, the defer hasn't fired yet — make sure it's a manual close.
5. **Method validation**: `m.Validate()` is called first.
6. **Tests match the spec verbatim** (or close to it — variable renames are fine, behavioral changes are not).
7. **No unrelated edits** — only `client.go` and `client_test.go` modified.
8. **Commit message exact.**

## Report

✅ Spec compliant OR ❌ Issues found with file:line refs. Be terse.


Phase A: Bootstrap + Crow client (Tasks 1-3)

go.mod/Makefile/main.go, cobra root, ExitCode, client.Download

Phase B: Archive + Registry (Tasks 4-8)

archive.Extract w/ hardening, MemCache+DiskCache, Registry.FetchPair

Phase C: Textual diff + plain CLI (Tasks 9-11)

FileDiff types, gotextdiff, cmd diff --plain, versions subcommand

Phase D: TUI browser + picker (Tasks 12-15)

Bubbletea browser, picker state machine, integration into cmd diff

Phase E: Semantic + JSON + README (Tasks 16-19)

HCL semantic diff, --semantic wiring, --json output, README

Final code review + finishing-a-development-branch

Dispatch final reviewer on the full implementation, then invoke finishing-a-development-branch skill


Spec ✓ for Task 3. Dispatching code quality reviewer.


Code quality review Task 3

Prompt:
Code quality review of Task 3: add `Download` method to Crow client.

## Working Directory

`/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`

## Diff

```
BASE_SHA: 59cd0b6
HEAD_SHA: ee7794e
```

Run `git diff 59cd0b6..ee7794e -- services/crow/pkg/client/` to inspect the change.

## Context

Spec compliance is already ✓. Do NOT re-verify spec. Focus on quality.

The Crow client is a shared service-layer client used by `apps/crowctl` (existing) and `apps/birdseye` (being built). The crow service implements the Terraform module-registry protocol — `Download` follows a two-hop pattern: GET discovery endpoint (204 + `X-Terraform-Get` header) → GET the URL from the header.

The existing client code is at `services/crow/pkg/client/client.go`. The new method joins existing methods `Upload`, `Exists`, `ListVersions` in the same file.

## Review Focus

**Style consistency with existing client methods:**
- Does `Download` follow the same idioms as `Upload`/`Exists`/`ListVersions`? (Same validation pattern, same error wrapping style, same use of `c.httpClient.Do`?)
- Does the doc comment match the style of the other methods?

**Resource correctness:**
- Are both response bodies properly closed in every branch (404, non-204/200, missing header, blob non-200, success)?
- Does the success path leave the blob body open for the caller (as documented)?
- Any double-close or use-after-close bugs?

**Error handling:**
- Is `ErrNotFound` returned bare (so `errors.Is` works without unwrap)?
- Are wrapped errors formatted consistently (`fmt.Errorf("X %d Y", ...)`)?
- Is the missing-header error specific enough to be debuggable (mentions the path and status code)?

**Test quality:**
- Do tests exercise actual HTTP behavior (httptest, not mocks)?
- Are server handlers correctly asserting method and path?
- Are tests independent (each spins up its own server, no shared state)?
- Are the three tests genuinely covering distinct behaviors (happy, 404, missing-header)?

**Anti-patterns:**
- Magic strings (`X-Terraform-Get`, `application/octet-stream`)? If used in multiple places, consider extracting a constant.
- Method too long? `Download` is the longest method in the file — is the structure clear or could it be split?

**Per-task plan checks:**
- One clear responsibility: yes (single method, single file).
- File size: did Download materially grow `client.go`? (~30 lines added is fine.)

## Report Format

**Strengths:** (1-3 bullets)
**Issues:**
- Critical: (likely none)
- Important: (worth fixing now)
- Minor: (nice-to-haves)
**Assessment:** APPROVED | NEEDS_CHANGES

Be brief.


I have everything I need. Here is the review.

---

**Strengths:**

- Resource management is correct in every branch. The first response body is always closed before the function either returns an error or makes the second request. The blob body is left open for the caller exactly as documented. No double-close or use-after-close.
- `ErrNotFound` is returned bare (no wrapping), so `errors.Is` works without unwrap, consistent with `ListVersions`.
- Tests use real `httptest` servers, are fully independent (each starts its own server), and the happy-path test validates actual body bytes round-tripped through both hops.

**Issues:**

- **Important — `"X-Terraform-Get"` is a magic string used in two places** (production client and test handler). The existing file has a precedent for `"application/octet-stream"` also appearing as a magic string in `Upload`, so this is not a regression, but `Download` makes it worse by repeating the string across `client.go` and `client_test.go`. Worth extracting a package-level constant (`headerTerraformGet = "X-Terraform-Get"`) before this client grows further.

- **Important — style divergence on body-close idiom.** `Upload`, `Exists`, and `ListVersions` all use `defer resp.Body.Close()`. `Download` uses explicit `_ = resp.Body.Close()` calls, which is forced by the two-hop flow (you cannot defer and then read from a second response), but the first-hop body is closed twice implicitly if a reader isn't careful — actually it isn't, the code is correct. The real issue is that `Download` accepts both `http.StatusNoContent` and `http.StatusOK` from the discovery hop, yet `Exists` also accepts both. The `Download` uses `if/if` chaining to handle those two branches where a `switch` would match the style of the other methods. A `switch resp.StatusCode` block like `Exists` and `ListVersions` use would be more consistent and shorter.

- **Minor — `TestDownloadOK` uses `crowSrv.Client()` for both the Crow hop and the blob hop.** The blob request goes to `tarSrv` (a different server), not `crowSrv`. `httptest.Server.Client()` returns a client configured to trust the server's TLS cert; for plain `httptest.NewServer` (non-TLS) servers, the default transport works fine, so this doesn't fail, but it is conceptually misleading. The tarball fetch succeeds only because both are non-TLS. No actual bug here, but worth noting.

- **Minor — `TestDownloadMissingHeader` only asserts `err != nil`, not the error message content.** The missing-header error is explicitly designed to include the path and status code to be debuggable, but the test doesn't verify that. A `strings.Contains(err.Error(), "X-Terraform-Get")` or similar check would confirm the error message stays useful.

**Assessment:** APPROVED — the two "Important" items (magic string constant, switch-style consistency) are worth a quick follow-up but do not block merge.

agentId: ae726b5de34ce9dc4 (use SendMessage with to: 'ae726b5de34ce9dc4' to continue this agent)
<usage>total_tokens: 22227
tool_uses: 4
duration_ms: 29104</usage>

Prompt:
Code quality review of Task 3: add `Download` method to Crow client.

## Working Directory

`/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`

## Diff

```
BASE_SHA: 59cd0b6
HEAD_SHA: ee7794e
```

Run `git diff 59cd0b6..ee7794e -- services/crow/pkg/client/` to inspect the change.

## Context

Spec compliance is already ✓. Do NOT re-verify spec. Focus on quality.

The Crow client is a shared service-layer client used by `apps/crowctl` (existing) and `apps/birdseye` (being built). The crow service implements the Terraform module-registry protocol — `Download` follows a two-hop pattern: GET discovery endpoint (204 + `X-Terraform-Get` header) → GET the URL from the header.

The existing client code is at `services/crow/pkg/client/client.go`. The new method joins existing methods `Upload`, `Exists`, `ListVersions` in the same file.

## Review Focus

**Style consistency with existing client methods:**
- Does `Download` follow the same idioms as `Upload`/`Exists`/`ListVersions`? (Same validation pattern, same error wrapping style, same use of `c.httpClient.Do`?)
- Does the doc comment match the style of the other methods?

**Resource correctness:**
- Are both response bodies properly closed in every branch (404, non-204/200, missing header, blob non-200, success)?
- Does the success path leave the blob body open for the caller (as documented)?
- Any double-close or use-after-close bugs?

**Error handling:**
- Is `ErrNotFound` returned bare (so `errors.Is` works without unwrap)?
- Are wrapped errors formatted consistently (`fmt.Errorf("X %d Y", ...)`)?
- Is the missing-header error specific enough to be debuggable (mentions the path and status code)?

**Test quality:**
- Do tests exercise actual HTTP behavior (httptest, not mocks)?
- Are server handlers correctly asserting method and path?
- Are tests independent (each spins up its own server, no shared state)?
- Are the three tests genuinely covering distinct behaviors (happy, 404, missing-header)?

**Anti-patterns:**
- Magic strings (`X-Terraform-Get`, `application/octet-stream`)? If used in multiple places, consider extracting a constant.
- Method too long? `Download` is the longest method in the file — is the structure clear or could it be split?

**Per-task plan checks:**
- One clear responsibility: yes (single method, single file).
- File size: did Download materially grow `client.go`? (~30 lines added is fine.)

## Report Format

**Strengths:** (1-3 bullets)
**Issues:**
- Critical: (likely none)
- Important: (worth fixing now)
- Minor: (nice-to-haves)
**Assessment:** APPROVED | NEEDS_CHANGES

Be brief.


Task 3 APPROVED. Phase A complete. Moving to Phase B (Task 4: archive.Extract happy path).


Updated task #8 status


Updated task #9 status


Implement Task 4: archive.Extract

Prompt:
You are implementing Task 4 of the birdseye CLI implementation plan.

## Working Environment

- **Worktree path:** `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`
- **Git branch:** `worktree-birdseye`
- Recent commits: Task 1 (`ae477d5`), Task 2 + fixes (`3b73e14`, `59cd0b6`), Task 3 (`ee7794e`)
- Use absolute paths or `cd` into the worktree. **Never** operate on the main repo.
- **NEVER push.** **NEVER use `git add .` or `git add -A`.** Always `git add` specific paths.

## Context

This task creates a NEW internal package `internal/archive` in `apps/birdseye/` that extracts gzipped tar archives into an in-memory file map. Birdseye fetches module tarballs from Crow and needs to enumerate their files for diffing. This is the happy-path implementation; Task 5 (next) adds hardening tests (zip-slip, size cap, symlinks).

The plan deliberately uses stdlib `archive/tar` + `compress/gzip` — no third-party archive lib. The output type `Module` has a `Files map[string][]byte` keyed by forward-slash relative path.

Use strict TDD: write the failing tests first, run them, then implement.

## Task Description (Verbatim)

### Task 4: `internal/archive` — tar.gz extraction (happy path)

**Files:**
- Create: `apps/birdseye/internal/archive/extract.go`
- Create: `apps/birdseye/internal/archive/extract_test.go`

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

```go
package archive

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"testing"
)

// buildTarGz produces a gzipped tar with the given files in memory.
// Used by archive, diff, and registry tests.
func buildTarGz(t *testing.T, files map[string]string) []byte {
	t.Helper()
	var buf bytes.Buffer
	gz := gzip.NewWriter(&buf)
	tw := tar.NewWriter(gz)
	for name, body := range files {
		hdr := &tar.Header{
			Name: name,
			Mode: 0o644,
			Size: int64(len(body)),
		}
		if err := tw.WriteHeader(hdr); err != nil {
			t.Fatalf("WriteHeader %s: %v", name, err)
		}
		if _, err := tw.Write([]byte(body)); err != nil {
			t.Fatalf("Write %s: %v", name, err)
		}
	}
	if err := tw.Close(); err != nil {
		t.Fatal(err)
	}
	if err := gz.Close(); err != nil {
		t.Fatal(err)
	}
	return buf.Bytes()
}

func TestExtractHappyPath(t *testing.T) {
	in := map[string]string{
		"main.tf":      "resource \"aws_s3_bucket\" \"x\" {}\n",
		"variables.tf": "variable \"name\" { type = string }\n",
		"README.md":    "hello\n",
	}
	tgz := buildTarGz(t, in)

	mod, err := Extract(tgz, 1<<20)
	if err != nil {
		t.Fatalf("Extract: %v", err)
	}
	if len(mod.Files) != len(in) {
		t.Fatalf("got %d files, want %d", len(mod.Files), len(in))
	}
	for name, want := range in {
		got, ok := mod.Files[name]
		if !ok {
			t.Errorf("missing file: %s", name)
			continue
		}
		if string(got) != want {
			t.Errorf("%s body = %q, want %q", name, got, want)
		}
	}
}

func TestExtractEmptyArchive(t *testing.T) {
	tgz := buildTarGz(t, nil)
	mod, err := Extract(tgz, 1<<20)
	if err != nil {
		t.Fatalf("Extract empty: %v", err)
	}
	if len(mod.Files) != 0 {
		t.Errorf("expected 0 files, got %d", len(mod.Files))
	}
}

func TestExtractMalformedGzip(t *testing.T) {
	_, err := Extract([]byte("not a gzip"), 1<<20)
	if err == nil {
		t.Fatal("expected error on malformed input")
	}
}
```

- [ ] **Step 2: Run, expect FAIL** (`undefined: Extract`)

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

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

```go
// Package archive extracts gzipped tar archives from Crow into an
// in-memory file map. Diff and TUI layers consume the resulting Module
// type and never touch the network themselves.
package archive

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"errors"
	"fmt"
	"io"
)

// Module is the in-memory representation of an extracted module tarball.
// Keys are forward-slash relative paths. Symlinks are not stored.
type Module struct {
	Files map[string][]byte
}

var (
	ErrTooLarge   = errors.New("archive exceeds size cap")
	ErrUnsafePath = errors.New("archive contains unsafe path")
)

// Extract decompresses and untars tarball into a Module. If the
// cumulative uncompressed size exceeds maxBytes, Extract returns
// ErrTooLarge. Paths containing ".." or absolute roots are rejected with
// ErrUnsafePath. Symlinks are dropped silently (they are not useful
// inputs to a textual or semantic diff).
func Extract(tarball []byte, maxBytes int64) (Module, error) {
	gz, err := gzip.NewReader(bytes.NewReader(tarball))
	if err != nil {
		return Module{}, fmt.Errorf("gzip: %w", err)
	}
	defer gz.Close()

	tr := tar.NewReader(gz)
	files := make(map[string][]byte)
	var total int64
	for {
		hdr, err := tr.Next()
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			return Module{}, fmt.Errorf("tar: %w", err)
		}
		switch hdr.Typeflag {
		case tar.TypeReg, tar.TypeRegA:
			if err := safePath(hdr.Name); err != nil {
				return Module{}, err
			}
			total += hdr.Size
			if total > maxBytes {
				return Module{}, ErrTooLarge
			}
			body := make([]byte, hdr.Size)
			if _, err := io.ReadFull(tr, body); err != nil {
				return Module{}, fmt.Errorf("reading %s: %w", hdr.Name, err)
			}
			files[hdr.Name] = body
		default:
			// Directories, symlinks, etc. ignored.
		}
	}
	return Module{Files: files}, nil
}

func safePath(name string) error {
	if name == "" {
		return fmt.Errorf("%w: empty name", ErrUnsafePath)
	}
	if name[0] == '/' {
		return fmt.Errorf("%w: absolute path %q", ErrUnsafePath, name)
	}
	// Reject any segment that is "..", or any path containing "..".
	for i := 0; i < len(name); i++ {
		if name[i] == '\\' {
			return fmt.Errorf("%w: backslash in path %q", ErrUnsafePath, name)
		}
	}
	if containsDotDot(name) {
		return fmt.Errorf("%w: path traversal in %q", ErrUnsafePath, name)
	}
	return nil
}

func containsDotDot(p string) bool {
	for i := 0; i < len(p); i++ {
		if p[i] != '.' {
			continue
		}
		// Check for ".." preceded by start-or-slash, followed by slash-or-end.
		if i+1 < len(p) && p[i+1] == '.' {
			startOK := i == 0 || p[i-1] == '/'
			endOK := i+2 == len(p) || p[i+2] == '/'
			if startOK && endOK {
				return true
			}
		}
	}
	return false
}
```

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

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

- [ ] **Step 5: Commit**

```bash
cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye && git add apps/birdseye/internal/archive/ && git commit -m "feat(birdseye): tar.gz extraction with happy-path coverage"
```

## Notes & Style

- **`tar.TypeRegA` is deprecated** as of Go 1.13 but still exists. The plan lists it explicitly to handle older tarballs. Keep the constant in the switch case; if the linter complains, it's fine to add `//nolint:staticcheck` as a comment. If Go 1.25 has removed it, fall back to `case tar.TypeReg:`.
- Strict TDD: do not implement before tests are written and shown to fail.
- The helper `buildTarGz` is exported within the test file (lowercase `buildTarGz`, so package-private). Task 5 will reuse it.
- Do NOT add tests for zip-slip / size cap in this task. Task 5 handles those.

## Code Organization

Single file `extract.go` for now. Don't split into `extract.go` + `safepath.go`.

## Self-Review Checklist

- [ ] All 3 tests pass (`TestExtractHappyPath`, `TestExtractEmptyArchive`, `TestExtractMalformedGzip`)
- [ ] `go vet ./...` succeeds
- [ ] `Module.Files` is correctly populated with the expected bodies
- [ ] Empty archive → empty `Files` map (no nil pointer)
- [ ] Malformed gzip → non-nil error
- [ ] Commit message exactly: `feat(birdseye): tar.gz extraction with happy-path coverage`
- [ ] Only `apps/birdseye/internal/archive/` files staged

## Report Format

- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
- Implementation summary
- Test count + results
- Whether `tar.TypeRegA` was kept or removed (and why)
- Files committed
- Commit SHA
- Self-review findings
- Anything unexpected


---

**Status:** DONE

**Implementation summary:** Created `apps/birdseye/internal/archive/` package with two files implementing tar.gz extraction into an in-memory `Module` type. Followed strict TDD order: test file written and confirmed failing before implementation was written.

**Test count + results:** 3 tests, all PASS with `-race`:
- `TestExtractHappyPath` — 3-file archive correctly extracted
- `TestExtractEmptyArchive` — empty archive yields empty `Files` map (no nil)
- `TestExtractMalformedGzip` — non-gzip input returns non-nil error

**`tar.TypeRegA` status:** Kept. It is still present in Go 1.25.5 (marked deprecated since Go 1.13, not removed). Added `//nolint:staticcheck` comment per plan guidance.

**Files committed:**
- `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/internal/archive/extract.go`
- `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye/internal/archive/extract_test.go`

**Commit SHA:** `67c8b68`

**Self-review findings:** All checklist items satisfied. `go vet ./...` clean. No unexpected issues encountered. Task 5's hardening tests (zip-slip, size cap, symlinks) will reuse `buildTarGz` from this package's test file.

agentId: a1478db3570ab2655 (use SendMessage with to: 'a1478db3570ab2655' to continue this agent)
<usage>total_tokens: 21915
tool_uses: 11
duration_ms: 122420</usage>

Prompt:
You are implementing Task 4 of the birdseye CLI implementation plan.

## Working Environment

- **Worktree path:** `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`
- **Git branch:** `worktree-birdseye`
- Recent commits: Task 1 (`ae477d5`), Task 2 + fixes (`3b73e14`, `59cd0b6`), Task 3 (`ee7794e`)
- Use absolute paths or `cd` into the worktree. **Never** operate on the main repo.
- **NEVER push.** **NEVER use `git add .` or `git add -A`.** Always `git add` specific paths.

## Context

This task creates a NEW internal package `internal/archive` in `apps/birdseye/` that extracts gzipped tar archives into an in-memory file map. Birdseye fetches module tarballs from Crow and needs to enumerate their files for diffing. This is the happy-path implementation; Task 5 (next) adds hardening tests (zip-slip, size cap, symlinks).

The plan deliberately uses stdlib `archive/tar` + `compress/gzip` — no third-party archive lib. The output type `Module` has a `Files map[string][]byte` keyed by forward-slash relative path.

Use strict TDD: write the failing tests first, run them, then implement.

## Task Description (Verbatim)

### Task 4: `internal/archive` — tar.gz extraction (happy path)

**Files:**
- Create: `apps/birdseye/internal/archive/extract.go`
- Create: `apps/birdseye/internal/archive/extract_test.go`

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

```go
package archive

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"testing"
)

// buildTarGz produces a gzipped tar with the given files in memory.
// Used by archive, diff, and registry tests.
func buildTarGz(t *testing.T, files map[string]string) []byte {
	t.Helper()
	var buf bytes.Buffer
	gz := gzip.NewWriter(&buf)
	tw := tar.NewWriter(gz)
	for name, body := range files {
		hdr := &tar.Header{
			Name: name,
			Mode: 0o644,
			Size: int64(len(body)),
		}
		if err := tw.WriteHeader(hdr); err != nil {
			t.Fatalf("WriteHeader %s: %v", name, err)
		}
		if _, err := tw.Write([]byte(body)); err != nil {
			t.Fatalf("Write %s: %v", name, err)
		}
	}
	if err := tw.Close(); err != nil {
		t.Fatal(err)
	}
	if err := gz.Close(); err != nil {
		t.Fatal(err)
	}
	return buf.Bytes()
}

func TestExtractHappyPath(t *testing.T) {
	in := map[string]string{
		"main.tf":      "resource \"aws_s3_bucket\" \"x\" {}\n",
		"variables.tf": "variable \"name\" { type = string }\n",
		"README.md":    "hello\n",
	}
	tgz := buildTarGz(t, in)

	mod, err := Extract(tgz, 1<<20)
	if err != nil {
		t.Fatalf("Extract: %v", err)
	}
	if len(mod.Files) != len(in) {
		t.Fatalf("got %d files, want %d", len(mod.Files), len(in))
	}
	for name, want := range in {
		got, ok := mod.Files[name]
		if !ok {
			t.Errorf("missing file: %s", name)
			continue
		}
		if string(got) != want {
			t.Errorf("%s body = %q, want %q", name, got, want)
		}
	}
}

func TestExtractEmptyArchive(t *testing.T) {
	tgz := buildTarGz(t, nil)
	mod, err := Extract(tgz, 1<<20)
	if err != nil {
		t.Fatalf("Extract empty: %v", err)
	}
	if len(mod.Files) != 0 {
		t.Errorf("expected 0 files, got %d", len(mod.Files))
	}
}

func TestExtractMalformedGzip(t *testing.T) {
	_, err := Extract([]byte("not a gzip"), 1<<20)
	if err == nil {
		t.Fatal("expected error on malformed input")
	}
}
```

- [ ] **Step 2: Run, expect FAIL** (`undefined: Extract`)

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

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

```go
// Package archive extracts gzipped tar archives from Crow into an
// in-memory file map. Diff and TUI layers consume the resulting Module
// type and never touch the network themselves.
package archive

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"errors"
	"fmt"
	"io"
)

// Module is the in-memory representation of an extracted module tarball.
// Keys are forward-slash relative paths. Symlinks are not stored.
type Module struct {
	Files map[string][]byte
}

var (
	ErrTooLarge   = errors.New("archive exceeds size cap")
	ErrUnsafePath = errors.New("archive contains unsafe path")
)

// Extract decompresses and untars tarball into a Module. If the
// cumulative uncompressed size exceeds maxBytes, Extract returns
// ErrTooLarge. Paths containing ".." or absolute roots are rejected with
// ErrUnsafePath. Symlinks are dropped silently (they are not useful
// inputs to a textual or semantic diff).
func Extract(tarball []byte, maxBytes int64) (Module, error) {
	gz, err := gzip.NewReader(bytes.NewReader(tarball))
	if err != nil {
		return Module{}, fmt.Errorf("gzip: %w", err)
	}
	defer gz.Close()

	tr := tar.NewReader(gz)
	files := make(map[string][]byte)
	var total int64
	for {
		hdr, err := tr.Next()
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			return Module{}, fmt.Errorf("tar: %w", err)
		}
		switch hdr.Typeflag {
		case tar.TypeReg, tar.TypeRegA:
			if err := safePath(hdr.Name); err != nil {
				return Module{}, err
			}
			total += hdr.Size
			if total > maxBytes {
				return Module{}, ErrTooLarge
			}
			body := make([]byte, hdr.Size)
			if _, err := io.ReadFull(tr, body); err != nil {
				return Module{}, fmt.Errorf("reading %s: %w", hdr.Name, err)
			}
			files[hdr.Name] = body
		default:
			// Directories, symlinks, etc. ignored.
		}
	}
	return Module{Files: files}, nil
}

func safePath(name string) error {
	if name == "" {
		return fmt.Errorf("%w: empty name", ErrUnsafePath)
	}
	if name[0] == '/' {
		return fmt.Errorf("%w: absolute path %q", ErrUnsafePath, name)
	}
	// Reject any segment that is "..", or any path containing "..".
	for i := 0; i < len(name); i++ {
		if name[i] == '\\' {
			return fmt.Errorf("%w: backslash in path %q", ErrUnsafePath, name)
		}
	}
	if containsDotDot(name) {
		return fmt.Errorf("%w: path traversal in %q", ErrUnsafePath, name)
	}
	return nil
}

func containsDotDot(p string) bool {
	for i := 0; i < len(p); i++ {
		if p[i] != '.' {
			continue
		}
		// Check for ".." preceded by start-or-slash, followed by slash-or-end.
		if i+1 < len(p) && p[i+1] == '.' {
			startOK := i == 0 || p[i-1] == '/'
			endOK := i+2 == len(p) || p[i+2] == '/'
			if startOK && endOK {
				return true
			}
		}
	}
	return false
}
```

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

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

- [ ] **Step 5: Commit**

```bash
cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye && git add apps/birdseye/internal/archive/ && git commit -m "feat(birdseye): tar.gz extraction with happy-path coverage"
```

## Notes & Style

- **`tar.TypeRegA` is deprecated** as of Go 1.13 but still exists. The plan lists it explicitly to handle older tarballs. Keep the constant in the switch case; if the linter complains, it's fine to add `//nolint:staticcheck` as a comment. If Go 1.25 has removed it, fall back to `case tar.TypeReg:`.
- Strict TDD: do not implement before tests are written and shown to fail.
- The helper `buildTarGz` is exported within the test file (lowercase `buildTarGz`, so package-private). Task 5 will reuse it.
- Do NOT add tests for zip-slip / size cap in this task. Task 5 handles those.

## Code Organization

Single file `extract.go` for now. Don't split into `extract.go` + `safepath.go`.

## Self-Review Checklist

- [ ] All 3 tests pass (`TestExtractHappyPath`, `TestExtractEmptyArchive`, `TestExtractMalformedGzip`)
- [ ] `go vet ./...` succeeds
- [ ] `Module.Files` is correctly populated with the expected bodies
- [ ] Empty archive → empty `Files` map (no nil pointer)
- [ ] Malformed gzip → non-nil error
- [ ] Commit message exactly: `feat(birdseye): tar.gz extraction with happy-path coverage`
- [ ] Only `apps/birdseye/internal/archive/` files staged

## Report Format

- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
- Implementation summary
- Test count + results
- Whether `tar.TypeRegA` was kept or removed (and why)
- Files committed
- Commit SHA
- Self-review findings
- Anything unexpected


Task 4 DONE. Spec review next.


Spec review Task 4

Prompt:
You are reviewing spec compliance of Task 4: tar.gz happy-path extraction.

## Working Directory

`/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`

Diff: `git diff ee7794e..67c8b68 -- apps/birdseye/internal/archive/`

## What Was Requested

Create two new files in `apps/birdseye/internal/archive/`:

1. `extract.go` containing:
   - Package doc comment about extracting tarballs from Crow
   - `type Module struct { Files map[string][]byte }`
   - Sentinel `ErrTooLarge`, `ErrUnsafePath`
   - `func Extract(tarball []byte, maxBytes int64) (Module, error)` — gzip+tar decompression, regular files only, ErrTooLarge when cumulative size exceeds cap, ErrUnsafePath for absolute paths / `..` traversal / backslashes
   - Helper `safePath` and `containsDotDot`

2. `extract_test.go` containing:
   - Test helper `buildTarGz(t, files)` (package-private)
   - `TestExtractHappyPath` — 3 files extracted with correct bodies
   - `TestExtractEmptyArchive` — empty archive → empty Files map
   - `TestExtractMalformedGzip` — non-gzip → error

Strict TDD. Commit message exactly: `feat(birdseye): tar.gz extraction with happy-path coverage`. Only `apps/birdseye/internal/archive/` paths staged.

## Implementer Claim

DONE. Commit `67c8b68`. 3 tests pass with `-race`. `tar.TypeRegA` kept with `//nolint:staticcheck`.

## Verify Independently

Read the diff. Confirm:

1. Both files created, no other files touched.
2. `Module.Files` is `map[string][]byte` (not `map[string]string`).
3. `Extract` signature exactly `func Extract(tarball []byte, maxBytes int64) (Module, error)`.
4. Sentinel errors `ErrTooLarge` and `ErrUnsafePath` exported.
5. Happy path test asserts both file count and per-file body content.
6. `safePath` rejects:
   - empty name
   - leading `/` (absolute path)
   - backslash
   - `..` segments (via `containsDotDot`)
7. `containsDotDot` only matches `..` as a full segment (preceded by start-or-slash and followed by slash-or-end), not e.g. `foo..bar`.
8. Reading file body uses `io.ReadFull` with `hdr.Size`-sized buffer (correct for non-streaming reads of known size).
9. Test helper `buildTarGz` is named exactly that and is reusable for Task 5.
10. Commit message exact.

## Report

✅ Spec compliant OR ❌ Issues with file:line refs. Be terse.


✅ Spec compliant. All 10 checks pass:

1. **Both files created, no other files touched.** Only `extract.go` and `extract_test.go` appear in the diff.
2. **`Module.Files` is `map[string][]byte`.** `extract.go:20`.
3. **`Extract` signature exact.** `func Extract(tarball []byte, maxBytes int64) (Module, error)` at `extract.go:32`.
4. **Sentinels exported.** `ErrTooLarge` and `ErrUnsafePath` declared via `errors.New` at `extract.go:24-25`.
5. **Happy path asserts count and per-file body.** `TestExtractHappyPath` checks `len(mod.Files) != len(in)` then iterates and compares `string(got) != want` for each file (`extract_test.go:51-64`).
6. **`safePath` rejects all required cases.** Empty name (`extract.go:67`), leading `/` (`extract.go:70`), backslash loop (`extract.go:73-76`), `..` via `containsDotDot` (`extract.go:78`).
7. **`containsDotDot` is segment-exact.** Checks `startOK := i == 0 || p[i-1] == '/'` and `endOK := i+2 == len(p) || p[i+2] == '/'`, so `foo..bar` is not matched (`extract.go:88-94`).
8. **`io.ReadFull` with `hdr.Size`-sized buffer.** `body := make([]byte, hdr.Size)` + `io.ReadFull(tr, body)` at `extract.go:56-59`.
9. **`buildTarGz` named exactly that** and accepts `map[string]string` making it reusable for Task 5 (`extract_test.go:13`).
10. **Commit message exact.** `feat(birdseye): tar.gz extraction with happy-path coverage`.

agentId: a83f963d8ed897d31 (use SendMessage with to: 'a83f963d8ed897d31' to continue this agent)
<usage>total_tokens: 14883
tool_uses: 2
duration_ms: 19203</usage>

Prompt:
You are reviewing spec compliance of Task 4: tar.gz happy-path extraction.

## Working Directory

`/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`

Diff: `git diff ee7794e..67c8b68 -- apps/birdseye/internal/archive/`

## What Was Requested

Create two new files in `apps/birdseye/internal/archive/`:

1. `extract.go` containing:
   - Package doc comment about extracting tarballs from Crow
   - `type Module struct { Files map[string][]byte }`
   - Sentinel `ErrTooLarge`, `ErrUnsafePath`
   - `func Extract(tarball []byte, maxBytes int64) (Module, error)` — gzip+tar decompression, regular files only, ErrTooLarge when cumulative size exceeds cap, ErrUnsafePath for absolute paths / `..` traversal / backslashes
   - Helper `safePath` and `containsDotDot`

2. `extract_test.go` containing:
   - Test helper `buildTarGz(t, files)` (package-private)
   - `TestExtractHappyPath` — 3 files extracted with correct bodies
   - `TestExtractEmptyArchive` — empty archive → empty Files map
   - `TestExtractMalformedGzip` — non-gzip → error

Strict TDD. Commit message exactly: `feat(birdseye): tar.gz extraction with happy-path coverage`. Only `apps/birdseye/internal/archive/` paths staged.

## Implementer Claim

DONE. Commit `67c8b68`. 3 tests pass with `-race`. `tar.TypeRegA` kept with `//nolint:staticcheck`.

## Verify Independently

Read the diff. Confirm:

1. Both files created, no other files touched.
2. `Module.Files` is `map[string][]byte` (not `map[string]string`).
3. `Extract` signature exactly `func Extract(tarball []byte, maxBytes int64) (Module, error)`.
4. Sentinel errors `ErrTooLarge` and `ErrUnsafePath` exported.
5. Happy path test asserts both file count and per-file body content.
6. `safePath` rejects:
   - empty name
   - leading `/` (absolute path)
   - backslash
   - `..` segments (via `containsDotDot`)
7. `containsDotDot` only matches `..` as a full segment (preceded by start-or-slash and followed by slash-or-end), not e.g. `foo..bar`.
8. Reading file body uses `io.ReadFull` with `hdr.Size`-sized buffer (correct for non-streaming reads of known size).
9. Test helper `buildTarGz` is named exactly that and is reusable for Task 5.
10. Commit message exact.

## Report

✅ Spec compliant OR ❌ Issues with file:line refs. Be terse.


Code quality review Task 4

Prompt:
Code quality review of Task 4: archive.Extract happy path.

## Working Directory

`/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`

## Diff

```
BASE_SHA: ee7794e
HEAD_SHA: 67c8b68
```

Run `git diff ee7794e..67c8b68 -- apps/birdseye/internal/archive/` to see the change. Two new files: `extract.go`, `extract_test.go`.

## Context

Spec compliance is ✓ — focus on quality only. This is a new internal package using only stdlib (`archive/tar`, `compress/gzip`). The hardening tests (zip-slip, size cap, symlinks) arrive in Task 5 (next) and reuse the `buildTarGz` test helper.

User preferences (per CLAUDE.md):
- Prefer stdlib over third-party
- Preallocate slices/maps when size is known
- Use strings.Builder for string building in loops
- Pass context.Context to long-running or IO-bound functions
- Small interfaces
- Accept interfaces, return concrete types
- Validate all inputs from external data

Note: `Extract` takes a `[]byte` rather than `context.Context` + `io.Reader`. This is a design choice (tarballs are <10MB; whole-buffer extraction is simpler and avoids streaming concerns). Don't flag this — it's intentional.

## Review Focus

**File organization & responsibility:**
- One file for the package. Is that right or should `safePath`/`containsDotDot` move to a helper file?
- Is `Module` a sensible export shape? Or should `Files` be a method to allow lazy reads later?

**Style:**
- Is the package doc accurate?
- Is `containsDotDot` correctly handling Unicode? (Files paths in Go tar can theoretically contain UTF-8.)
- Could `path.Clean` + `path.IsAbs` simplify `safePath`? (i.e., is the hand-rolled containsDotDot necessary or could `if path.Clean(name) != name || strings.HasPrefix(name, "/") { reject }` express the same logic with less code?)

**Anti-patterns:**
- `tar.TypeRegA` is deprecated. Is the `//nolint` comment present and reasoned?
- Reading the entire archive into memory: fine for our use case (per spec, <10MB), no flag needed.
- Are file body buffers preallocated correctly (`make([]byte, hdr.Size)`)? Yes — but flag if `hdr.Size` could be negative (it can't with valid tarballs but it's a defensive question).

**Test quality:**
- Are tests using the standard testing package without testify? (Matches the project style.)
- Are tests independent?
- Is `buildTarGz` correctly closing both writers?

**Per-task plan checks:**
- One clear responsibility per file: yes (`extract.go`).
- New file not too large: ~95 lines, fine.

## Report Format

**Strengths:** (1-3 bullets)
**Issues:**
- Critical: (likely none)
- Important: (worth fixing)
- Minor: (nice-to-haves)
**Assessment:** APPROVED | NEEDS_CHANGES

Be brief.