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


- 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 start by invoking the required skill before doing anything else.


Launching skill: superpowers:using-superpowers


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

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

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

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

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

## Instruction Priority

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

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

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

## How to Access Skills

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

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

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

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

## Platform Adaptation

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

# Using Skills

## The Rule

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

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

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

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

## Red Flags

These thoughts mean STOP—you're rationalizing:

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

## Skill Priority

When multiple skills could apply, use this order:

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

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

## Skill Types

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

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

The skill itself tells you which.

## User Instructions

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


Now I'll invoke the code review skill since this is a code review task.


Launching skill: superpowers:receiving-code-review


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

# Code Review Reception

## Overview

Code review requires technical evaluation, not emotional performance.

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

## The Response Pattern

```
WHEN receiving code review feedback:

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

## Forbidden Responses

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

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

## Handling Unclear Feedback

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

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

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

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

## Source-Specific Handling

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

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

IF suggestion seems wrong:
  Push back with technical reasoning

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

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

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

## YAGNI Check for "Professional" Features

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

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

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

## Implementation Order

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

## When To Push Back

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

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

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

## Acknowledging Correct Feedback

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

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

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

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

## Gracefully Correcting Your Pushback

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

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

State the correction factually and move on.

## Common Mistakes

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

## Real Examples

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

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

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

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

## GitHub Thread Replies

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

## The Bottom Line

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

Verify. Question. Then implement.

No performative agreement. Technical rigor always.


I'll now gather the diff and the four new files to review.


Bash

git diff ae477d5..3b73e14 -- apps/birdseye/cmd/

Get the diff for the birdseye cmd package


diff --git a/apps/birdseye/cmd/errors.go b/apps/birdseye/cmd/errors.go
new file mode 100644
index 0000000..79b8f68
--- /dev/null
+++ b/apps/birdseye/cmd/errors.go
@@ -0,0 +1,37 @@
+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
+	}
+}
diff --git a/apps/birdseye/cmd/errors_test.go b/apps/birdseye/cmd/errors_test.go
new file mode 100644
index 0000000..0de6802
--- /dev/null
+++ b/apps/birdseye/cmd/errors_test.go
@@ -0,0 +1,31 @@
+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)
+			}
+		})
+	}
+}
diff --git a/apps/birdseye/cmd/root.go b/apps/birdseye/cmd/root.go
new file mode 100644
index 0000000..af9c90b
--- /dev/null
+++ b/apps/birdseye/cmd/root.go
@@ -0,0 +1,42 @@
+// 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,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return cmd.Help()
+		},
+	}
+	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
+}
diff --git a/apps/birdseye/cmd/root_test.go b/apps/birdseye/cmd/root_test.go
new file mode 100644
index 0000000..bbfff68
--- /dev/null
+++ b/apps/birdseye/cmd/root_test.go
@@ -0,0 +1,31 @@
+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)
+	}
+}


List files in birdseye cmd package


ls /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/immanager/cmd/ 2>/dev/null || ls /Users/steve.briggs/projects/operations/.claude/worktrees/birdseye/apps/immanager/

List immanager cmd structure for style comparison


drwxr-xr-x steve.briggs staff 256 B  Tue May 12 21:18:52 2026 ..
.rw-r--r-- steve.briggs staff 704 B  Tue May 12 21:18:58 2026 root_test.go
.rw-r--r-- steve.briggs staff 1.3 KB Tue May 12 22:49:47 2026 root.go
.rw-r--r-- steve.briggs staff 628 B  Tue May 12 22:56:36 2026 errors_test.go
.rw-r--r-- steve.briggs staff 871 B  Tue May 12 22:57:08 2026 errors.go
drwxr-xr-x steve.briggs staff 192 B  Tue May 12 22:57:08 2026 .


.rw-r--r-- steve.briggs staff 348 B  Tue May 12 18:16:41 2026 cluster.go
.rw-r--r-- steve.briggs staff 2.0 KB Tue May 12 18:16:41 2026 cluster_list.go
.rw-r--r-- steve.briggs staff 6.0 KB Tue May 12 18:16:41 2026 cluster_maintenance.go
.rw-r--r-- steve.briggs staff 283 B  Tue May 12 18:16:41 2026 datadog.go
.rw-r--r-- steve.briggs staff 2.6 KB Tue May 12 18:16:41 2026 datadog_event.go
.rw-r--r-- steve.briggs staff 561 B  Tue May 12 18:16:41 2026 instance.go
.rw-r--r-- steve.briggs staff 2.5 KB Tue May 12 18:16:41 2026 instance_backup.go
.rw-r--r-- steve.briggs staff 1.8 KB Tue May 12 18:16:41 2026 instance_get.go
.rw-r--r-- steve.briggs staff 8.8 KB Tue May 12 18:16:41 2026 instance_health.go
.rw-r--r-- steve.briggs staff 4.3 KB Tue May 12 18:16:41 2026 instance_list.go
.rw-r--r-- steve.briggs staff  11 KB Tue May 12 18:16:41 2026 instance_migrate.go
.rw-r--r-- steve.briggs staff 4.4 KB Tue May 12 18:16:41 2026 instance_recover.go
.rw-r--r-- steve.briggs staff 5.2 KB Tue May 12 18:16:41 2026 instance_set.go
.rw-r--r-- steve.briggs staff 598 B  Tue May 12 18:16:41 2026 org.go
.rw-r--r-- steve.briggs staff 5.3 KB Tue May 12 18:16:41 2026 org_list.go
.rw-r--r-- steve.briggs staff 971 B  Tue May 12 18:16:41 2026 release.go
.rw-r--r-- steve.briggs staff 4.5 KB Tue May 12 18:16:41 2026 release_list.go
.rw-r--r-- steve.briggs staff 1.7 KB Tue May 12 18:16:41 2026 release_set.go
.rw-r--r-- steve.briggs staff 4.9 KB Tue May 12 18:16:41 2026 release_upgrade.go
.rw-r--r-- steve.briggs staff 1.6 KB Tue May 12 18:16:41 2026 root.go
.rw-r--r-- steve.briggs staff 346 B  Tue May 12 18:16:41 2026 service.go
.rw-r--r-- steve.briggs staff  12 B  Tue May 12 18:16:41 2026 service_get.go
drwxr-xr-x steve.briggs staff 800 B  Tue May 12 18:16:41 2026 .
.rw-r--r-- steve.briggs staff 849 B  Tue May 12 18:16:41 2026 service_upgrade.go
drwxr-xr-x steve.briggs staff 352 B  Tue May 12 18:16:41 2026 ..


1	package cmd
2
3	import (
4		"fmt"
5		"os"
6		"strings"
7
8		"github.com/spf13/cobra"
9		"gitlab.infrastructure.immuta.io/infrastructure/immanager/internal/portal"
10	)
11
12	type InstanceCLI struct {
13		client portal.Client
14	}
15
16	func RootCommand() *cobra.Command {
17		cmd := &cobra.Command{
18			Use:               "immanager",
19			Version:           "0.1.0",
20			Short:             "A CLI used to manage the fleet of Immuta SaaS instances",
21			SilenceUsage:      false,
22		}
23
24		c := &InstanceCLI{}
25
26		cmd.AddCommand(c.NewInstanceCommand())
27		cmd.AddCommand(c.NewClusterCommand())
28		cmd.AddCommand(c.NewReleaseCommand())
29		cmd.AddCommand(c.NewDatadogCmd())
30		cmd.AddCommand(c.NewReleaseListCommand())
31		cmd.AddCommand(c.NewOrgCommand())
32
33		cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
34			// Skip check for completion and help commands
35			fullPath := cmd.CommandPath()
36			if strings.Contains(fullPath, " completion") || strings.Contains(fullPath, " help") || cmd.Name() == "help" {
37				return nil
38			}
39
40			missing := []string{}
41			for _, envVar := range []string{"ENV", "PORTAL_API_KEY", "PORTAL_API_TOKEN"} {
42				if os.Getenv(envVar) == "" {
43					missing = append(missing, envVar)
44				}
45			}
46
47			if len(missing) > 0 {
48				fmt.Printf("Warning: The following required environment variables are not set: %s\n\n", strings.Join(missing, ", "))
49				cmd.Usage()
50				os.Exit(1)
51			}
52
53			internalClient, err := portal.NewClient(portal.GetClientConfig())
54			if err != nil {
55				return err
56			}
57			c.client = internalClient
58			return nil
59		}
60
61		return cmd
62	}
63
64	func Execute() {
65		if err := RootCommand().Execute(); err != nil {
66			os.Exit(1)
67		}
68	}
69


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.