You are implementing Task 2 of the birdseye CLI implementation plan.

## Working Environment

- **Worktree path:** `/Users/steve.briggs/projects/operations/.claude/worktrees/birdseye`
- **Git branch:** `worktree-birdseye`
- Most recent commit: `ae477d5 feat(birdseye): bootstrap module skeleton` (Task 1 — provides go.mod, Makefile, main.go)
- All paths in this prompt are relative to the worktree. Use absolute paths or `cd` into the worktree path above.
- **NEVER push.** **NEVER use `git add .` or `git add -A`.** Always `git add` specific paths.
- Local Go: 1.26.2 (go.mod directive is 1.25.5 — fine).

## Context

Task 1 created the module skeleton with `main.go` that imports a `cmd` package and calls `cmd.NewRootCmd()` and `cmd.ExitCode()`. Right now the build is broken because the `cmd` package doesn't exist. Your job is to create it.

This is the cobra root command for the `birdseye` CLI. Subcommands (`diff`, `versions`) are added in later tasks — your job is JUST the root command plus the exit-code helper used by `main.go`. Do NOT add subcommands yourself.

You'll create 4 files: `cmd/root.go`, `cmd/root_test.go`, `cmd/errors.go`, `cmd/errors_test.go`. Use strict TDD: write the failing test, run it, implement, run again.

## Task Description (Verbatim)

### Task 2: Cobra root command + exit-code helper

**Files:**
- Create: `apps/birdseye/cmd/root.go`
- Create: `apps/birdseye/cmd/root_test.go`
- Create: `apps/birdseye/cmd/errors.go`
- Create: `apps/birdseye/cmd/errors_test.go`

- [ ] **Step 1: Write failing test for `NewRootCmd`** at `apps/birdseye/cmd/root_test.go`:

```go
package cmd

import (
	"bytes"
	"strings"
	"testing"
)

func TestNewRootCmdHelp(t *testing.T) {
	root := NewRootCmd()
	var buf bytes.Buffer
	root.SetOut(&buf)
	root.SetErr(&buf)
	root.SetArgs([]string{"--help"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute: %v", err)
	}
	out := buf.String()
	for _, want := range []string{"birdseye", "--crow-url", "--timeout"} {
		if !strings.Contains(out, want) {
			t.Errorf("help output missing %q\n---\n%s", want, out)
		}
	}
}

func TestNewRootCmdDefaults(t *testing.T) {
	root := NewRootCmd()
	if got, _ := root.PersistentFlags().GetString("crow-url"); got != "https://crow.infrastructure.immuta.io" {
		t.Errorf("crow-url default = %q", got)
	}
}
```

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

Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test ./cmd/... -run TestNewRootCmd -v`
Expected: compile error or undefined symbol.

- [ ] **Step 3: Implement `apps/birdseye/cmd/root.go`**

```go
// Package cmd wires up the cobra command tree for birdseye. Functions
// return *cobra.Command (rather than using package-level vars) so each
// test builds a fresh tree and flag state does not leak between cases.
package cmd

import (
	"time"

	"github.com/spf13/cobra"
)

// GlobalFlags are the cross-subcommand options exposed on the root command.
type GlobalFlags struct {
	CrowURL string
	Timeout time.Duration
	JSON    bool
}

const defaultCrowURL = "https://crow.infrastructure.immuta.io"

// NewRootCmd returns the configured root command. Subcommands are wired
// in here.
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:
	//   root.AddCommand(newDiffCmd(g))
	//   root.AddCommand(newVersionsCmd(g))
	return root
}
```

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

Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go test ./cmd/... -run TestNewRootCmd -v`
Expected: PASS (both tests).

- [ ] **Step 5: Write failing test for `ExitCode`** at `apps/birdseye/cmd/errors_test.go`:

```go
package cmd

import (
	"context"
	"errors"
	"fmt"
	"testing"

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

func TestExitCode(t *testing.T) {
	cases := []struct {
		name string
		err  error
		want int
	}{
		{"nil", nil, 0},
		{"not-found", client.ErrNotFound, 2},
		{"wrapped-not-found", fmt.Errorf("fetch: %w", client.ErrNotFound), 2},
		{"context-cancelled", context.Canceled, 130},
		{"generic", errors.New("boom"), 3},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			if got := ExitCode(c.err); got != c.want {
				t.Errorf("ExitCode(%v) = %d, want %d", c.err, got, c.want)
			}
		})
	}
}
```

- [ ] **Step 6: Run test, expect FAIL** (`undefined: ExitCode`)

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

- [ ] **Step 7: Implement `apps/birdseye/cmd/errors.go`**

```go
package cmd

import (
	"context"
	"errors"

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

// Sentinel errors mapped to exit codes. Packages that want a specific exit
// behaviour wrap these (e.g. `fmt.Errorf("extract: %w", ErrArchive)`).
var (
	ErrUsage    = errors.New("usage error")
	ErrArchive  = errors.New("archive corrupt or unsafe")
	ErrSemantic = errors.New("HCL parse error")
)

// ExitCode maps an error to a process exit code per the spec table.
// Returns 0 for nil.
func ExitCode(err error) int {
	switch {
	case err == nil:
		return 0
	case errors.Is(err, ErrUsage):
		return 1
	case errors.Is(err, client.ErrNotFound):
		return 2
	case errors.Is(err, ErrArchive):
		return 4
	case errors.Is(err, ErrSemantic):
		return 5
	case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
		return 130
	default:
		return 3
	}
}
```

- [ ] **Step 8: Run all cmd tests, expect PASS**

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

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

Run: `cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go build ./...`
Expected: no output, exit 0.

- [ ] **Step 10: Commit**

```bash
cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye && git add apps/birdseye/cmd/ && git commit -m "feat(birdseye): cobra root command and exit-code mapping"
```

## Notes & Style

- Strict TDD: write each test, run it (expect FAIL), then write the minimal code, run it (expect PASS). Do not write `errors.go` before `errors_test.go`.
- `client.ErrNotFound` is defined in `services/crow/pkg/client/client.go`. The replace directive in go.mod points the module path to `../../services/crow`, so this import will resolve.
- `go mod tidy` may also need to be run after first import resolution. If it modifies go.sum, include the change in the commit.
- The `g` (GlobalFlags) variable in `NewRootCmd` is intentionally unused in this task's scope — it will be passed to subcommands in later tasks. Do NOT add `_ = g` or otherwise hide the future-use intent. (If linters complain, that's expected and acceptable for this scaffold stage.)
- **Important:** After Step 7, `go mod tidy` should resolve the `services/crow/pkg/client` import. If go.sum changes, add it to the commit. Use `git add apps/birdseye/cmd/ apps/birdseye/go.sum apps/birdseye/go.mod` if both go.sum and go.mod were modified.

## Code Organization

You reason best about code you can hold in context at once. Keep:
- `root.go` — only the root command and global flags struct
- `errors.go` — only ExitCode and sentinel errors
- One responsibility per file. Do not merge them.

## Before You Begin

Ask if anything is unclear. Otherwise proceed.

## When You're in Over Your Head

It is always OK to stop and report BLOCKED or NEEDS_CONTEXT.

## Self-Review Checklist

- [ ] Both test files compile and pass (`go test -race ./cmd/... -v`)
- [ ] All five TestExitCode subtests pass
- [ ] Both TestNewRootCmd tests pass
- [ ] `go build ./...` succeeds from within `apps/birdseye/`
- [ ] No `_ = g` or other obvious hack to silence unused-var warnings
- [ ] Commit message exactly: `feat(birdseye): cobra root command and exit-code mapping`
- [ ] Only `apps/birdseye/cmd/` (and optionally go.mod/go.sum if updated) staged in the commit
- [ ] No extra files (no example_test.go, no doc.go, no main_test.go)

## Report Format

When done, report:
- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
- Implementation summary
- Test results (e.g., "go test -race ./cmd/... -v" output summary, number of tests passing)
- `go build ./...` output
- Files committed (filenames)
- Commit SHA
- Self-review findings
- Any concerns


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


I'll implement Task 2 following strict TDD. Let me start by examining the existing skeleton.


Bash

List birdseye app directory


Read go.mod


drwxr-xr-x steve.briggs staff 256 B  Tue May 12 18:19:50 2026 ..
.rw-r--r-- steve.briggs staff 226 B  Tue May 12 18:19:59 2026 Makefile
.rw-r--r-- steve.briggs staff   6 B  Tue May 12 18:20:01 2026 .gitignore
.rw-r--r-- steve.briggs staff 602 B  Tue May 12 18:20:06 2026 main.go
.rw-r--r-- steve.briggs staff 573 B  Tue May 12 21:10:14 2026 go.mod
drwxr-xr-x steve.briggs staff 224 B  Tue May 12 21:10:14 2026 .
.rw-r--r-- steve.briggs staff 1.2 KB Tue May 12 21:10:14 2026 go.sum


module github.com/immuta/operations/apps/birdseye

go 1.25.5

require (
	github.com/charmbracelet/bubbles v0.21.0
	github.com/charmbracelet/bubbletea v1.3.4
	github.com/charmbracelet/lipgloss v1.1.0
	github.com/hashicorp/hcl/v2 v2.23.0
	github.com/hexops/gotextdiff v1.0.3
	github.com/immuta/operations/libs/clog v0.0.0
	github.com/immuta/operations/services/crow v0.0.0
	github.com/spf13/cobra v1.9.1
	golang.org/x/sync v0.11.0
)

replace github.com/immuta/operations/services/crow => ../../services/crow

replace github.com/immuta/operations/libs/clog => ../../libs/clog


1	// Command birdseye diffs two versions of a Terraform module hosted in
2	// Crow, Immuta's private module registry. See cmd/diff.go for the diff
3	// subcommand and cmd/versions.go for the versions subcommand.
4	package main
5
6	import (
7		"context"
8		"fmt"
9		"os"
10		"os/signal"
11		"syscall"
12
13		"github.com/immuta/operations/apps/birdseye/cmd"
14	)
15
16	func main() {
17		ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
18		defer stop()
19
20		root := cmd.NewRootCmd()
21		if err := root.ExecuteContext(ctx); err != nil {
22			fmt.Fprintln(os.Stderr, "birdseye:", err)
23			os.Exit(cmd.ExitCode(err))
24		}
25	}
26


ls /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/services/crow/pkg/client/ 2>/dev/null || echo "NOT FOUND"

Check crow client package exists


drwxr-xr-x steve.briggs staff 128 B  Tue May 12 18:16:41 2026 ..
.rw-r--r-- steve.briggs staff 3.7 KB Tue May 12 18:16:41 2026 client.go
drwxr-xr-x steve.briggs staff 128 B  Tue May 12 18:16:41 2026 .
.rw-r--r-- steve.briggs staff 5.4 KB Tue May 12 18:16:41 2026 client_test.go


Check ErrNotFound definition in crow client


43:	ErrNotFound      = errors.New("module version not found")
101:// Returns ErrNotFound when the module has no published versions.
122:			return nil, ErrNotFound
130:		return nil, ErrNotFound


Good. `ErrNotFound` is defined. Now let me follow the TDD steps.

**Step 1: Write the failing test for `NewRootCmd`**


Create cmd directory


(Bash completed with no output)


Write

package cmd

import (
	"bytes"
	"strings"
	"testing"
)

func TestNewRootCmdHelp(t *testing.T) {
	root := NewRootCmd()
	var buf bytes.Buffer
	root.SetOut(&buf)
	root.SetErr(&buf)
	root.SetArgs([]string{"--help"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute: %v", err)
	}
	out := buf.String()
	for _, want := range []string{"birdseye", "--crow-url", "--timeout"} {
		if !strings.Contains(out, want) {
			t.Errorf("help output missing %q\n---\n%s", want, out)
		}
	}
}

func TestNewRootCmdDefaults(t *testing.T) {
	root := NewRootCmd()
	if got, _ := root.PersistentFlags().GetString("crow-url"); got != "https://crow.infrastructure.immuta.io" {
		t.Errorf("crow-url default = %q", got)
	}
}


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


**Step 2: Run test, expect FAIL**


Bash

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

Run test expecting compile failure


Error: Exit code 1
# github.com/immuta/operations/apps/birdseye/cmd [github.com/immuta/operations/apps/birdseye/cmd.test]
cmd/root_test.go:10:10: undefined: NewRootCmd
cmd/root_test.go:27:10: undefined: NewRootCmd
FAIL	github.com/immuta/operations/apps/birdseye/cmd [build failed]
FAIL


Good, compile error as expected. **Step 3: Implement `root.go`**


Write

// Package cmd wires up the cobra command tree for birdseye. Functions
// return *cobra.Command (rather than using package-level vars) so each
// test builds a fresh tree and flag state does not leak between cases.
package cmd

import (
	"time"

	"github.com/spf13/cobra"
)

// GlobalFlags are the cross-subcommand options exposed on the root command.
type GlobalFlags struct {
	CrowURL string
	Timeout time.Duration
	JSON    bool
}

const defaultCrowURL = "https://crow.infrastructure.immuta.io"

// NewRootCmd returns the configured root command. Subcommands are wired
// in here.
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:
	//   root.AddCommand(newDiffCmd(g))
	//   root.AddCommand(newVersionsCmd(g))
	return root
}


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


**Step 4: Run test, expect PASS**


Bash

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

Run root command tests expecting PASS


Error: Exit code 1
# github.com/immuta/operations/apps/birdseye/cmd
cmd/root.go:9:2: missing go.sum entry for module providing package github.com/spf13/cobra (imported by github.com/immuta/operations/apps/birdseye/cmd); to add:
	go get github.com/immuta/operations/apps/birdseye/cmd
FAIL	github.com/immuta/operations/apps/birdseye/cmd [setup failed]
FAIL


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

Run go mod tidy to update go.sum


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

Run root command tests after go mod tidy


Error: Exit code 1
=== RUN   TestNewRootCmdHelp
    root_test.go:21: help output missing "birdseye"
        ---
        Diff two versions of a Terraform module hosted in Crow

    root_test.go:21: help output missing "--crow-url"
        ---
        Diff two versions of a Terraform module hosted in Crow

    root_test.go:21: help output missing "--timeout"
        ---
        Diff two versions of a Terraform module hosted in Crow

--- FAIL: TestNewRootCmdHelp (0.00s)
=== RUN   TestNewRootCmdDefaults
--- PASS: TestNewRootCmdDefaults (0.00s)
FAIL
FAIL	github.com/immuta/operations/apps/birdseye/cmd	0.451s
FAIL


The help output isn't fully captured — the `--help` flag causes cobra to write to both out and err separately. The buf is only getting the short description. Let me investigate the cobra behavior with `--help`.


cd /Users/steve.briggs/projects/operations/.claire/worktrees/birdseye/apps/birdseye 2>/dev/null || true
# Test what cobra does with --help
cat << 'EOF' > /tmp/cobra_help_test.go
package main

import (
	"bytes"
	"fmt"
	"time"
	"github.com/spf13/cobra"
)

func main() {
	g := struct {
		CrowURL string
		Timeout time.Duration
		JSON    bool
	}{}
	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", "https://crow.infrastructure.immuta.io", "Crow base URL")
	root.PersistentFlags().DurationVar(&g.Timeout, "timeout", 30*time.Second, "timeout")
	root.PersistentFlags().BoolVar(&g.JSON, "json", false, "json")

	var buf bytes.Buffer
	root.SetOut(&buf)
	root.SetErr(&buf)
	root.SetArgs([]string{"--help"})
	err := root.Execute()
	fmt.Printf("err=%v\n", err)
	fmt.Printf("buf=%q\n", buf.String())
}
EOF
cd /tmp && go mod init testcobra 2>/dev/null; go get github.com/spf13/cobra@v1.9.1 2>/dev/null; go run cobra_help_test.go 2>&1 | head -30

Test cobra --help output behavior


go: cannot run *_test.go files (cobra_help_test.go)


mv /tmp/cobra_help_test.go /tmp/cobra_help_main.go
cd /tmp && go mod init testcobra 2>/dev/null || true
go get github.com/spf13/cobra@v1.9.1 2>&1 | tail -3
go run /tmp/cobra_help_main.go 2>&1

Run cobra help test with renamed file


err=<nil>
buf="Diff two versions of a Terraform module hosted in Crow\n\n"


The cobra `--help` flag on a root command with `SilenceUsage: true` only writes the short description to the writer, not the full usage. The full usage goes to os.Stdout directly. Let me check the cobra docs more carefully — it seems `SetOut` should redirect help output.


# Investigate more: the help command vs --help flag
cat << 'EOF' > /tmp/cobra_help2.go
package main

import (
	"bytes"
	"fmt"
	"time"
	"github.com/spf13/cobra"
)

func main() {
	g := struct {
		CrowURL string
		Timeout time.Duration
		JSON    bool
	}{}
	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", "https://crow.infrastructure.immuta.io", "Crow base URL")
	root.PersistentFlags().DurationVar(&g.Timeout, "timeout", 30*time.Second, "timeout")
	root.PersistentFlags().BoolVar(&g.JSON, "json", false, "json")

	var buf bytes.Buffer
	root.SetOut(&buf)
	root.SetErr(&buf)

	// Try "help" subcommand instead of --help flag
	root.SetArgs([]string{"help"})
	err := root.Execute()
	fmt.Printf("err=%v\n", err)
	fmt.Printf("buf=%q\n", buf.String())

	// Also try calling Help() directly
	buf.Reset()
	root.Help()
	fmt.Printf("help() buf=%q\n", buf.String())
}
EOF
cd /tmp && go run cobra_help2.go 2>&1

Test cobra help subcommand and Help() method


err=<nil>
buf="Diff two versions of a Terraform module hosted in Crow\n\n"
help() buf="Diff two versions of a Terraform module hosted in Crow\n\n"


# Check what cobra version does - maybe it writes to stdout vs writer
cat << 'EOF' > /tmp/cobra_help3.go
package main

import (
	"bytes"
	"fmt"
	"os"
	"time"
	"github.com/spf13/cobra"
)

func main() {
	g := struct {
		CrowURL string
		Timeout time.Duration
		JSON    bool
	}{}
	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", "https://crow.infrastructure.immuta.io", "Crow base URL")
	root.PersistentFlags().DurationVar(&g.Timeout, "timeout", 30*time.Second, "timeout")
	root.PersistentFlags().BoolVar(&g.JSON, "json", false, "json")

	var buf bytes.Buffer
	root.SetOut(&buf)
	root.SetErr(&buf)

	// Capture stdout
	old := os.Stdout
	r, w, _ := os.Pipe()
	os.Stdout = w

	root.SetArgs([]string{"--help"})
	err := root.Execute()

	w.Close()
	os.Stdout = old
	var stdoutBuf bytes.Buffer
	stdoutBuf.ReadFrom(r)

	fmt.Printf("err=%v\n", err)
	fmt.Printf("buf=%q\n", buf.String())
	fmt.Printf("stdout=%q\n", stdoutBuf.String())
}
EOF
cd /tmp && go run cobra_help3.go 2>&1

Check if cobra writes full help to stdout not SetOut writer


err=<nil>
buf="Diff two versions of a Terraform module hosted in Crow\n\n"
stdout=""


# Check cobra source code behavior - maybe it needs a RunE to actually show full help
cat << 'EOF' > /tmp/cobra_help4.go
package main

import (
	"bytes"
	"fmt"
	"time"
	"github.com/spf13/cobra"
)

func main() {
	g := struct {
		CrowURL string
		Timeout time.Duration
		JSON    bool
	}{}
	root := &cobra.Command{
		Use:           "birdseye",
		Short:         "Diff two versions of a Terraform module hosted in Crow",
		SilenceUsage:  true,
		SilenceErrors: true,
		RunE: func(cmd *cobra.Command, args []string) error {
			return cmd.Help()
		},
	}
	root.PersistentFlags().StringVar(&g.CrowURL, "crow-url", "https://crow.infrastructure.immuta.io", "Crow base URL")
	root.PersistentFlags().DurationVar(&g.Timeout, "timeout", 30*time.Second, "timeout")
	root.PersistentFlags().BoolVar(&g.JSON, "json", false, "json")

	var buf bytes.Buffer
	root.SetOut(&buf)
	root.SetErr(&buf)

	root.SetArgs([]string{"--help"})
	err := root.Execute()
	fmt.Printf("err=%v\n", err)
	fmt.Printf("buf len=%d\n", buf.Len())
	fmt.Printf("buf=%s\n", buf.String())
}
EOF
cd /tmp && go run cobra_help4.go 2>&1

Test cobra with RunE that calls Help


err=<nil>
buf len=307
buf=Diff two versions of a Terraform module hosted in Crow

Usage:
  birdseye [flags]

Flags:
      --crow-url string    Crow base URL (default "https://crow.infrastructure.immuta.io")
  -h, --help               help for birdseye
      --json               json
      --timeout duration   timeout (default 30s)


The root command needs a `RunE` that calls `cmd.Help()` to make the full help output flow through `SetOut`. This is a known cobra behavior — without a `Run`/`RunE`, `--help` on a command with no subcommands writes abbreviated output. The task spec doesn't include `RunE` in root.go, but I need to add it to make the tests pass. Let me check if there's another approach — perhaps calling `UsageFunc` or `HelpFunc`.

Actually, looking more carefully: when a command has no `Run`/`RunE` and `--help` is passed, cobra prints just the short + long descriptions via `SetOut`. The full usage (flags etc.) is printed when there IS a `Run`. Since the test explicitly checks for `--crow-url` and `--timeout` in help output, I need to add a `RunE` that shows help. This is correct behavior for a root command that delegates to subcommands.


Edit

// NewRootCmd returns the configured root command. Subcommands are wired
// in here.
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,
	}

// NewRootCmd returns the configured root command. Subcommands are wired
// in here.
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,
		RunE: func(cmd *cobra.Command, args []string) error {
			return cmd.Help()
		},
	}


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


**Step 4 (retry): Run test, expect PASS**


Bash

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

Run root command tests expecting PASS


=== RUN   TestNewRootCmdHelp
--- PASS: TestNewRootCmdHelp (0.00s)
=== RUN   TestNewRootCmdDefaults
--- PASS: TestNewRootCmdDefaults (0.00s)
PASS
ok  	github.com/immuta/operations/apps/birdseye/cmd	0.603s


Both root command tests pass. **Step 5: Write failing test for `ExitCode`**


Write

package cmd

import (
	"context"
	"errors"
	"fmt"
	"testing"

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

func TestExitCode(t *testing.T) {
	cases := []struct {
		name string
		err  error
		want int
	}{
		{"nil", nil, 0},
		{"not-found", client.ErrNotFound, 2},
		{"wrapped-not-found", fmt.Errorf("fetch: %w", client.ErrNotFound), 2},
		{"context-cancelled", context.Canceled, 130},
		{"generic", errors.New("boom"), 3},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			if got := ExitCode(c.err); got != c.want {
				t.Errorf("ExitCode(%v) = %d, want %d", c.err, got, c.want)
			}
		})
	}
}


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


**Step 6: Run test, expect FAIL**


Bash

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

Run ExitCode test expecting compile failure


Error: Exit code 1
# github.com/immuta/operations/apps/birdseye/cmd
cmd/errors_test.go:9:2: module github.com/immuta/operations/services/crow provides package github.com/immuta/operations/services/crow/pkg/client and is replaced but not required; to add it:
	go get github.com/immuta/operations/services/crow
FAIL	github.com/immuta/operations/apps/birdseye/cmd [setup failed]
FAIL


cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go get github.com/immuta/operations/services/crow 2>&1

Add crow dependency


go: added github.com/aws/aws-sdk-go-v2 v1.41.1
go: added github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4
go: added github.com/aws/aws-sdk-go-v2/config v1.32.9
go: added github.com/aws/aws-sdk-go-v2/credentials v1.19.9
go: added github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17
go: added github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17
go: added github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17
go: added github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4
go: added github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17
go: added github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4
go: added github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8
go: added github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17
go: added github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17
go: added github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
go: added github.com/aws/aws-sdk-go-v2/service/signin v1.0.5
go: added github.com/aws/aws-sdk-go-v2/service/sso v1.30.10
go: added github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14
go: added github.com/aws/aws-sdk-go-v2/service/sts v1.41.6
go: added github.com/aws/smithy-go v1.24.1
go: added github.com/davecgh/go-spew v1.1.1
go: added github.com/immuta/operations/services/crow v0.0.0-20260512210412-d8eb33139c1a
go: added github.com/labstack/echo/v4 v4.15.0
go: added github.com/labstack/gommon v0.4.2
go: added github.com/mattn/go-colorable v0.1.14
go: added github.com/mattn/go-isatty v0.0.20
go: added github.com/pmezard/go-difflib v1.0.0
go: added github.com/stretchr/objx v0.5.3
go: added github.com/stretchr/testify v1.11.1
go: added github.com/valyala/bytebufferpool v1.0.0
go: added github.com/valyala/fasttemplate v1.2.2
go: added golang.org/x/crypto v0.48.0
go: added golang.org/x/net v0.50.0
go: added golang.org/x/sys v0.41.0
go: added golang.org/x/text v0.34.0
go: added golang.org/x/time v0.14.0


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

Run ExitCode test after dependency added, still expecting undefined: ExitCode


Error: Exit code 1
# github.com/immuta/operations/apps/birdseye/cmd [github.com/immuta/operations/apps/birdseye/cmd.test]
cmd/errors_test.go:26:14: undefined: ExitCode
FAIL	github.com/immuta/operations/apps/birdseye/cmd [build failed]
FAIL


Good — compile error as expected. **Step 7: Implement `errors.go`**


Write

package cmd

import (
	"context"
	"errors"

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

// Sentinel errors mapped to exit codes. Packages that want a specific exit
// behaviour wrap these (e.g. `fmt.Errorf("extract: %w", ErrArchive)`).
var (
	ErrUsage    = errors.New("usage error")
	ErrArchive  = errors.New("archive corrupt or unsafe")
	ErrSemantic = errors.New("HCL parse error")
)

// ExitCode maps an error to a process exit code per the spec table.
// Returns 0 for nil.
func ExitCode(err error) int {
	switch {
	case err == nil:
		return 0
	case errors.Is(err, ErrUsage):
		return 1
	case errors.Is(err, client.ErrNotFound):
		return 2
	case errors.Is(err, ErrArchive):
		return 4
	case errors.Is(err, ErrSemantic):
		return 5
	case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
		return 130
	default:
		return 3
	}
}


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


**Step 8: Run all cmd tests with race detector, expect PASS**


Bash

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

Run all cmd tests with race detector


=== RUN   TestExitCode
=== RUN   TestExitCode/nil
=== RUN   TestExitCode/not-found
=== RUN   TestExitCode/wrapped-not-found
=== RUN   TestExitCode/context-cancelled
=== RUN   TestExitCode/generic
--- PASS: TestExitCode (0.00s)
    --- PASS: TestExitCode/nil (0.00s)
    --- PASS: TestExitCode/not-found (0.00s)
    --- PASS: TestExitCode/wrapped-not-found (0.00s)
    --- PASS: TestExitCode/context-cancelled (0.00s)
    --- PASS: TestExitCode/generic (0.00s)
=== RUN   TestNewRootCmdHelp
--- PASS: TestNewRootCmdHelp (0.00s)
=== RUN   TestNewRootCmdDefaults
--- PASS: TestNewRootCmdDefaults (0.00s)
PASS
ok  	github.com/immuta/operations/apps/birdseye/cmd	1.844s


All 7 tests pass. **Step 9: Verify build**


cd /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/birdseye && go build ./... 2>&1; echo "exit: $?"

Verify go build succeeds


exit: 0


**Step 10: Commit**


Check git status before staging


On branch worktree-birdseye
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   apps/birdseye/go.mod
	modified:   apps/birdseye/go.sum

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	.claude/mind.mv2.lock
	apps/birdseye/cmd/

no changes added to commit (use "git add" and/or "git commit -a")


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

Stage cmd directory and updated go files


(Bash completed with no output)


Show staged files before commit


 apps/birdseye/cmd/errors.go      | 37 +++++++++++++++++++
 apps/birdseye/cmd/errors_test.go | 31 ++++++++++++++++
 apps/birdseye/cmd/root.go        | 42 +++++++++++++++++++++
 apps/birdseye/cmd/root_test.go   | 31 ++++++++++++++++
 apps/birdseye/go.mod             | 49 ++++++++++++++++++++-----
 apps/birdseye/go.sum             | 79 +++++++++++++++++++++++++++++++++++++---
 6 files changed, 254 insertions(+), 15 deletions(-)


git commit -m "$(cat <<'EOF'
feat(birdseye): cobra root command and exit-code mapping
EOF
)"

Create commit with exact message specified in task


[worktree-birdseye 3b73e14] feat(birdseye): cobra root command and exit-code mapping
 6 files changed, 254 insertions(+), 15 deletions(-)
 create mode 100644 apps/birdseye/cmd/errors.go
 create mode 100644 apps/birdseye/cmd/errors_test.go
 create mode 100644 apps/birdseye/cmd/root.go
 create mode 100644 apps/birdseye/cmd/root_test.go


---

**Status:** DONE

**Implementation summary:**

Created 4 files in `apps/birdseye/cmd/` following strict TDD (write failing test → verify compile error → implement → verify pass):

- `root.go` — `GlobalFlags` struct, `defaultCrowURL` const, `NewRootCmd()` returning a cobra command with `--crow-url`, `--timeout`, `--json` persistent flags. Added a `RunE` that calls `cmd.Help()` so the full flag listing appears in captured output (needed because cobra only prints the full usage when a command has a `Run`/`RunE` handler).
- `root_test.go` — `TestNewRootCmdHelp` and `TestNewRootCmdDefaults` as specified.
- `errors.go` — `ErrUsage`, `ErrArchive`, `ErrSemantic` sentinel errors; `ExitCode()` mapping nil→0, ErrUsage→1, `client.ErrNotFound`→2, ErrArchive→4, ErrSemantic→5, context.Canceled/DeadlineExceeded→130, default→3.
- `errors_test.go` — `TestExitCode` with 5 subtests as specified.

`go mod tidy` + `go get github.com/immuta/operations/services/crow` were run to resolve imports; go.mod and go.sum are included in the commit.

**Test results (`go test -race ./cmd/... -v`):**
- `TestExitCode/nil` PASS
- `TestExitCode/not-found` PASS
- `TestExitCode/wrapped-not-found` PASS
- `TestExitCode/context-cancelled` PASS
- `TestExitCode/generic` PASS
- `TestNewRootCmdHelp` PASS
- `TestNewRootCmdDefaults` PASS
- 7/7 tests passing, no race conditions

**`go build ./...` output:** no output, exit 0

**Files committed:**
- `apps/birdseye/cmd/errors.go`
- `apps/birdseye/cmd/errors_test.go`
- `apps/birdseye/cmd/root.go`
- `apps/birdseye/cmd/root_test.go`
- `apps/birdseye/go.mod` (updated with new deps)
- `apps/birdseye/go.sum` (updated)

**Commit SHA:** `3b73e14`

**Self-review findings:**
- One deviation from the task spec: `RunE` was added to `NewRootCmd` to make `TestNewRootCmdHelp` pass. Without it, cobra writes only the short description through `SetOut` when `--help` is invoked on a command without a `Run`/`RunE`, so `--crow-url` and `--timeout` would not appear in the captured buffer. The `RunE` calling `cmd.Help()` is correct idiomatic cobra for a root command that delegates to subcommands, and the future `AddCommand` calls will shadow it naturally.
- No `_ = g` or other suppression hacks. The `g` variable is used (passed to `PersistentFlags().StringVar` etc.) so there is no unused-variable issue.