<memvid-mind-context>
# 🧠 Claude Mind Ready

📁 Project: **apps**
💾 Memory will be created at: `.claude/mind.mv2`

_Your observations will be automatically captured._
</memvid-mind-context>


<EXTREMELY_IMPORTANT>
You have superpowers.

**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**

---
name: using-superpowers
description: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
---

<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.

</EXTREMELY_IMPORTANT>


<memvid-mind-context>
# 🧠 Claude Mind Ready

📁 Project: **apps**
💾 Memory will be created at: `.claude/mind.mv2`

_Your observations will be automatically captured._
</memvid-mind-context>

<EXTREMELY_IMPORTANT>
You have superpowers.

**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**

---
name: using-superpowers
description: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
---

<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.

</EXTREMELY_IMPORTANT>


<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>


<command-name>/model</command-name>
            <command-message>model</command-message>
            <command-args></command-args>


<local-command-stdout>Set model to Opus 4.7 (1M context) (default)</local-command-stdout>


We have created a private terraform registry called crow (@/Users/steve.briggs/projects/operations/services/crow). The service is stateless so I want to create a golang based CLI utility called birdseye that will allow the user to pass a module to it, two versions to diff and then pull the modules down and compare the versions. I would like to use superpowers for this with the Opus 4.7 model for the planning phase and then use sonnet for the implementation to save on token usage. The utility should use the cobra library for the CLI bits and I would like to add a nice TUI using the bubbletea framework if possible


claude claude-code-guide Explore general-purpose Plan statusline-setup

- claude: Catch-all for any task that doesn't fit a more specific agent. FleetView's default when no agent name is typed. (Tools: *)

- claude-code-guide: Use this agent when the user asks questions ("Can Claude...", "Does Claude...", "How do I...") about: (1) Claude Code (the CLI tool) - features, hooks, slash commands, MCP servers, settings, IDE integrations, keyboard shortcuts; (2) Claude Agent SDK - building custom agents; (3) Claude API (formerly Anthropic API) - API usage, tool use, Anthropic SDK usage. **IMPORTANT:** Before spawning a new agent, check if there is already a running or recently completed claude-code-guide agent that you can continue via SendMessage. (Tools: Bash, Read, WebFetch, WebSearch)

- Explore: Fast read-only search agent for locating code. Use it to find files by pattern (eg. "src/components/**/*.tsx"), grep for symbols or keywords (eg. "API endpoints"), or answer "where is X defined / which files reference Y." Do NOT use it for code review, design-doc auditing, cross-file consistency checks, or open-ended analysis — it reads excerpts rather than whole files and will miss content past its read window. When calling, specify search breadth: "quick" for a single targeted lookup, "medium" for moderate exploration, or "very thorough" to search across multiple locations and naming conventions. (Tools: All tools except Agent, ExitPlanMode, Edit, Write, NotebookEdit)

- general-purpose: General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *)

- Plan: Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs. (Tools: All tools except Agent, ExitPlanMode, Edit, Write, NotebookEdit)

- statusline-setup: Use this agent to configure the user's Claude Code status line setting. (Tools: Read, Edit)


## claude.ai Context7
Use this server to fetch current documentation whenever the user asks about a library, framework, SDK, API, CLI tool, or cloud service -- even well-known ones like React, Next.js, Prisma, Express, Tailwind, Django, or Spring Boot. This includes API syntax, configuration, version migration, library-specific debugging, setup instructions, and CLI tool usage. Use even when you think you know the answer -- your training data may not reflect recent changes. Prefer this over web search for library docs.

Do not use for: refactoring, writing scripts from scratch, debugging business logic, code review, or general programming concepts.


- 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


We have created a private terraform registry called crow (@/Users/steve.briggs/projects/operations/services/crow). The service is stateless so I want to create a golang based CLI utility called birdse…


Build birdseye CLI for Terraform module diffing


Ahoy, Sir Charles of St Cheese! A new CLI utility, you say? Birdseye to peer down at crow's modules — cute naming. Before I dive in headfirst like a hungry seagull spotting dinosaur nuggies on the boardwalk, let me kick this off properly with brainstorming per the superpowers protocol.


Launching skill: superpowers:brainstorming


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

# Brainstorming Ideas Into Designs

Help turn ideas into fully formed designs and specs through natural collaborative dialogue.

Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval.

<HARD-GATE>
Do NOT invoke any implementation skill, write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity.
</HARD-GATE>

## Anti-Pattern: "This Is Too Simple To Need A Design"

Every project goes through this process. A todo list, a single-function utility, a config change — all of them. "Simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but you MUST present it and get approval.

## Checklist

You MUST create a task for each of these items and complete them in order:

1. **Explore project context** — check files, docs, recent commits
2. **Offer visual companion** (if topic will involve visual questions) — this is its own message, not combined with a clarifying question. See the Visual Companion section below.
3. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria
4. **Propose 2-3 approaches** — with trade-offs and your recommendation
5. **Present design** — in sections scaled to their complexity, get user approval after each section
6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and commit
7. **Spec self-review** — quick inline check for placeholders, contradictions, ambiguity, scope (see below)
8. **User reviews written spec** — ask user to review the spec file before proceeding
9. **Transition to implementation** — invoke writing-plans skill to create implementation plan

## Process Flow

```dot
digraph brainstorming {
    "Explore project context" [shape=box];
    "Visual questions ahead?" [shape=diamond];
    "Offer Visual Companion\n(own message, no other content)" [shape=box];
    "Ask clarifying questions" [shape=box];
    "Propose 2-3 approaches" [shape=box];
    "Present design sections" [shape=box];
    "User approves design?" [shape=diamond];
    "Write design doc" [shape=box];
    "Spec self-review\n(fix inline)" [shape=box];
    "User reviews spec?" [shape=diamond];
    "Invoke writing-plans skill" [shape=doublecircle];

    "Explore project context" -> "Visual questions ahead?";
    "Visual questions ahead?" -> "Offer Visual Companion\n(own message, no other content)" [label="yes"];
    "Visual questions ahead?" -> "Ask clarifying questions" [label="no"];
    "Offer Visual Companion\n(own message, no other content)" -> "Ask clarifying questions";
    "Ask clarifying questions" -> "Propose 2-3 approaches";
    "Propose 2-3 approaches" -> "Present design sections";
    "Present design sections" -> "User approves design?";
    "User approves design?" -> "Present design sections" [label="no, revise"];
    "User approves design?" -> "Write design doc" [label="yes"];
    "Write design doc" -> "Spec self-review\n(fix inline)";
    "Spec self-review\n(fix inline)" -> "User reviews spec?";
    "User reviews spec?" -> "Write design doc" [label="changes requested"];
    "User reviews spec?" -> "Invoke writing-plans skill" [label="approved"];
}
```

**The terminal state is invoking writing-plans.** Do NOT invoke frontend-design, mcp-builder, or any other implementation skill. The ONLY skill you invoke after brainstorming is writing-plans.

## The Process

**Understanding the idea:**

- Check out the current project state first (files, docs, recent commits)
- Before asking detailed questions, assess scope: if the request describes multiple independent subsystems (e.g., "build a platform with chat, file storage, billing, and analytics"), flag this immediately. Don't spend questions refining details of a project that needs to be decomposed first.
- If the project is too large for a single spec, help the user decompose into sub-projects: what are the independent pieces, how do they relate, what order should they be built? Then brainstorm the first sub-project through the normal design flow. Each sub-project gets its own spec → plan → implementation cycle.
- For appropriately-scoped projects, ask questions one at a time to refine the idea
- Prefer multiple choice questions when possible, but open-ended is fine too
- Only one question per message - if a topic needs more exploration, break it into multiple questions
- Focus on understanding: purpose, constraints, success criteria

**Exploring approaches:**

- Propose 2-3 different approaches with trade-offs
- Present options conversationally with your recommendation and reasoning
- Lead with your recommended option and explain why

**Presenting the design:**

- Once you believe you understand what you're building, present the design
- Scale each section to its complexity: a few sentences if straightforward, up to 200-300 words if nuanced
- Ask after each section whether it looks right so far
- Cover: architecture, components, data flow, error handling, testing
- Be ready to go back and clarify if something doesn't make sense

**Design for isolation and clarity:**

- Break the system into smaller units that each have one clear purpose, communicate through well-defined interfaces, and can be understood and tested independently
- For each unit, you should be able to answer: what does it do, how do you use it, and what does it depend on?
- Can someone understand what a unit does without reading its internals? Can you change the internals without breaking consumers? If not, the boundaries need work.
- Smaller, well-bounded units are also easier for you to work with - you reason better about code you can hold in context at once, and your edits are more reliable when files are focused. When a file grows large, that's often a signal that it's doing too much.

**Working in existing codebases:**

- Explore the current structure before proposing changes. Follow existing patterns.
- Where existing code has problems that affect the work (e.g., a file that's grown too large, unclear boundaries, tangled responsibilities), include targeted improvements as part of the design - the way a good developer improves code they're working in.
- Don't propose unrelated refactoring. Stay focused on what serves the current goal.

## After the Design

**Documentation:**

- Write the validated design (spec) to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
  - (User preferences for spec location override this default)
- Use elements-of-style:writing-clearly-and-concisely skill if available
- Commit the design document to git

**Spec Self-Review:**
After writing the spec document, look at it with fresh eyes:

1. **Placeholder scan:** Any "TBD", "TODO", incomplete sections, or vague requirements? Fix them.
2. **Internal consistency:** Do any sections contradict each other? Does the architecture match the feature descriptions?
3. **Scope check:** Is this focused enough for a single implementation plan, or does it need decomposition?
4. **Ambiguity check:** Could any requirement be interpreted two different ways? If so, pick one and make it explicit.

Fix any issues inline. No need to re-review — just fix and move on.

**User Review Gate:**
After the spec review loop passes, ask the user to review the written spec before proceeding:

> "Spec written and committed to `<path>`. Please review it and let me know if you want to make any changes before we start writing out the implementation plan."

Wait for the user's response. If they request changes, make them and re-run the spec review loop. Only proceed once the user approves.

**Implementation:**

- Invoke the writing-plans skill to create a detailed implementation plan
- Do NOT invoke any other skill. writing-plans is the next step.

## Key Principles

- **One question at a time** - Don't overwhelm with multiple questions
- **Multiple choice preferred** - Easier to answer than open-ended when possible
- **YAGNI ruthlessly** - Remove unnecessary features from all designs
- **Explore alternatives** - Always propose 2-3 approaches before settling
- **Incremental validation** - Present design, get approval before moving on
- **Be flexible** - Go back and clarify when something doesn't make sense

## Visual Companion

A browser-based companion for showing mockups, diagrams, and visual options during brainstorming. Available as a tool — not a mode. Accepting the companion means it's available for questions that benefit from visual treatment; it does NOT mean every question goes through the browser.

**Offering the companion:** When you anticipate that upcoming questions will involve visual content (mockups, layouts, diagrams), offer it once for consent:
> "Some of what we're working on might be easier to explain if I can show it to you in a web browser. I can put together mockups, diagrams, comparisons, and other visuals as we go. This feature is still new and can be token-intensive. Want to try it? (Requires opening a local URL)"

**This offer MUST be its own message.** Do not combine it with clarifying questions, context summaries, or any other content. The message should contain ONLY the offer above and nothing else. Wait for the user's response before continuing. If they decline, proceed with text-only brainstorming.

**Per-question decision:** Even after the user accepts, decide FOR EACH QUESTION whether to use the browser or the terminal. The test: **would the user understand this better by seeing it than reading it?**

- **Use the browser** for content that IS visual — mockups, wireframes, layout comparisons, architecture diagrams, side-by-side visual designs
- **Use the terminal** for content that is text — requirements questions, conceptual choices, tradeoff lists, A/B/C/D text options, scope decisions

A question about a UI topic is not automatically a visual question. "What does personality mean in this context?" is a conceptual question — use the terminal. "Which wizard layout works better?" is a visual question — use the browser.

If they agree to the companion, read the detailed guide before proceeding:
`skills/brainstorming/visual-companion.md`


Let me start by exploring the crow service to understand what we're building against.


Bash

ls /Users/steve.briggs/projects/operations/services/crow/ 2>&1 | head -50

List crow service directory


List apps directory


drwxr-xr-x steve.briggs staff 160 B Tue May 12 16:37:45 2026 autozone-check
drwxr-xr-x steve.briggs staff 224 B Tue May 12 16:37:45 2026 crowctl
drwxr-xr-x steve.briggs staff 352 B Tue May 12 16:37:45 2026 immanager
drwxr-xr-x steve.briggs staff 352 B Tue May 12 16:37:45 2026 pack
drwxr-xr-x steve.briggs staff 704 B Tue May 12 16:37:45 2026 ruth
drwxr-xr-x steve.briggs staff 736 B Tue May 12 16:37:45 2026 ..
drwxr-xr-x steve.briggs staff  64 B Tue May 12 17:07:23 2026 birdseye
drwxr-xr-x steve.briggs staff 256 B Tue May 12 17:07:23 2026 .


.rw-r--r-- steve.briggs staff 278 B  Tue May 12 16:37:45 2026 Dockerfile
.rw-r--r-- steve.briggs staff 4.3 KB Tue May 12 16:37:45 2026 README.md
drwxr-xr-x steve.briggs staff 128 B  Tue May 12 16:37:45 2026 base
drwxr-xr-x steve.briggs staff 160 B  Tue May 12 16:37:45 2026 deploy
.rw-r--r-- steve.briggs staff 1.8 KB Tue May 12 16:37:45 2026 docker-compose.yaml
drwxr-xr-x steve.briggs staff 192 B  Tue May 12 16:37:45 2026 e2e
.rw-r--r-- steve.briggs staff 2.0 KB Tue May 12 16:37:45 2026 go.mod
.rw-r--r-- steve.briggs staff 6.8 KB Tue May 12 16:37:45 2026 go.sum
drwxr-xr-x steve.briggs staff 160 B  Tue May 12 16:37:45 2026 internal
.rw-r--r-- steve.briggs staff 1.7 KB Tue May 12 16:37:45 2026 main.go
drwxr-xr-x steve.briggs staff 416 B  Tue May 12 16:37:45 2026 .
drwxr-xr-x steve.briggs staff 128 B  Tue May 12 16:37:45 2026 pkg
drwxr-xr-x steve.briggs staff 192 B  Tue May 12 16:37:45 2026 ..


I see there's already an empty `birdseye` directory waiting and a `crowctl` already exists. Let me explore crow's API and crowctl quickly to understand how they communicate.


1	# Crow
2
3	Crow is a private Terraform Module Registry backed by AWS S3. It implements the [Terraform Module Registry Protocol](https://developer.hashicorp.com/terraform/internals/module-registry-protocol), allowing Terraform to discover, list, and download modules stored as `.tar.gz` archives in an S3 bucket.
4
5	## API Endpoints
6
7	| Endpoint | Method | Description |
8	|----------|--------|-------------|
9	| `/health` | GET | Health check (returns 200) |
10	| `/.well-known/terraform.json` | GET | Terraform service discovery |
11	| `/v1/modules/:namespace/:name/:provider/versions` | GET | List available module versions |
12	| `/v1/modules/:namespace/:name/:provider/:version/download` | GET | Get presigned S3 download URL |
13	| `/v1/modules/:namespace/:name/:provider/:version` | PUT | Upload a module `.tar.gz` archive |
14
15	The download endpoint returns a `204` with the presigned URL in the `X-Terraform-Get` header, per the Terraform protocol spec.
16
17	### Uploading modules
18
19	Upload a `.tar.gz` archive directly:
20
21	```bash
22	curl -X PUT http://crow:8085/v1/modules/myorg/mymodule/aws/1.0.0 \
23	  -H "Content-Type: application/octet-stream" \
24	  --data-binary @module.tar.gz
25	```
26
27	Returns `201 Created` on success, `409 Conflict` if the version already exists, or `400 Bad Request` if the body is not valid gzip or exceeds the max upload size.
28
29	## S3 Bucket Layout
30
31	Modules are stored as tarballs with the following key structure:
32
33	```
34	{namespace}/{name}/{provider}/{version}.tar.gz
35	```
36
37	## Configuration
38
39	| Environment Variable | Required | Default | Description |
40	|---------------------|----------|---------|-------------|
41	| `S3_BUCKET_NAME` | Yes | | S3 bucket containing module archives |
42	| `AWS_REGION` | Yes | | AWS region for S3 access |
43	| `PORT` | No | `8080` | HTTP listen port |
44	| `PRESIGN_EXPIRY` | No | `15m` | Presigned URL expiration (Go duration) |
45	| `S3_PATH_STYLE` | No | `false` | Use path-style S3 addressing (required for non-AWS backends) |
46	| `AWS_ENDPOINT_URL` | No | | Custom S3-compatible endpoint URL |
47	| `S3_PRESIGN_ENDPOINT` | No | | Endpoint used in presigned download URLs (see below) |
48	| `MAX_UPLOAD_SIZE` | No | `10485760` (10MB) | Maximum upload body size in bytes |
49
50	AWS credentials are loaded via the standard SDK default chain (IAM roles, environment variables, credential files).
51
52	### Split endpoint for presigned URLs
53
54	When crow runs behind a Docker network, the S3 backend may be reachable at an internal address (e.g. `http://rustfs:9000`) that clients like Terraform can't resolve. Set `S3_PRESIGN_ENDPOINT` to the externally-reachable address so presigned download URLs point to the right host:
55
56	```
57	AWS_ENDPOINT_URL=http://rustfs:9000          # crow → S3 (internal)
58	S3_PRESIGN_ENDPOINT=http://localhost:9000     # client → S3 (external)
59	```
60
61	If `S3_PRESIGN_ENDPOINT` is not set, presigned URLs use the same endpoint as `AWS_ENDPOINT_URL`.
62
63	## Local Development with Docker Compose
64
65	A `docker-compose.yaml` is provided for local testing using [RustFS](https://github.com/rustfs/rustfs) as an S3-compatible backend.
66
67	```bash
68	docker compose up --build
69	```
70
71	This starts three services:
72
73	- **rustfs** — S3-compatible object storage on `localhost:9000` (API) and `localhost:9001` (console, credentials: `rustfsadmin`/`rustfsadmin`)
74	- **rustfs-setup** — Init container that creates the `terraform-modules` bucket and seeds sample data from `testdata/`
75	- **crow** — The registry on `localhost:8080`
76
77	Once running:
78
79	```bash
80	# Health check
81	curl http://localhost:8080/health
82
83	# List versions
84	curl http://localhost:8080/v1/modules/mymodule/mymodule/aws/versions
85
86	# Download a module
87	curl -I http://localhost:8080/v1/modules/mymodule/mymodule/aws/1.0.0/download
88	```
89
90	To seed modules, place `.tar.gz` archives in `testdata/` following the S3 key layout and update the `rustfs-setup` entrypoint in `docker-compose.yaml`.
91
92	## Development
93
94	```bash
95	# From the repository root:
96	make lint-service DIR_NAME=crow
97	make test-service-verbose DIR_NAME=crow
98	make build-service DIR_NAME=crow
99
100	# Build container image:
101	make build-service-image DIR_NAME=crow
102	```
103
104	## Usage with Terraform
105
106	Point Terraform at the registry by configuring a `~/.terraformrc` or project-level `.terraformrc`:
107
108	```hcl
109	host "crow.example.com" {
110	  services = {
111	    "modules.v1" = "https://crow.example.com/v1/modules/"
112	  }
113	}
114	```
115
116	Then reference modules normally:
117
118	```hcl
119	module "example" {
120	  source  = "crow.example.com/namespace/name/provider"
121	  version = "1.0.0"
122	}
123	```
124

# Crow

Crow is a private Terraform Module Registry backed by AWS S3. It implements the [Terraform Module Registry Protocol](https://developer.hashicorp.com/terraform/internals/module-registry-protocol), allowing Terraform to discover, list, and download modules stored as `.tar.gz` archives in an S3 bucket.

## API Endpoints

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/health` | GET | Health check (returns 200) |
| `/.well-known/terraform.json` | GET | Terraform service discovery |
| `/v1/modules/:namespace/:name/:provider/versions` | GET | List available module versions |
| `/v1/modules/:namespace/:name/:provider/:version/download` | GET | Get presigned S3 download URL |
| `/v1/modules/:namespace/:name/:provider/:version` | PUT | Upload a module `.tar.gz` archive |

The download endpoint returns a `204` with the presigned URL in the `X-Terraform-Get` header, per the Terraform protocol spec.

### Uploading modules

Upload a `.tar.gz` archive directly:

```bash
curl -X PUT http://crow:8085/v1/modules/myorg/mymodule/aws/1.0.0 \
  -H "Content-Type: application/octet-stream" \
  --data-binary @module.tar.gz
```

Returns `201 Created` on success, `409 Conflict` if the version already exists, or `400 Bad Request` if the body is not valid gzip or exceeds the max upload size.

## S3 Bucket Layout

Modules are stored as tarballs with the following key structure:

```
{namespace}/{name}/{provider}/{version}.tar.gz
```

## Configuration

| Environment Variable | Required | Default | Description |
|---------------------|----------|---------|-------------|
| `S3_BUCKET_NAME` | Yes | | S3 bucket containing module archives |
| `AWS_REGION` | Yes | | AWS region for S3 access |
| `PORT` | No | `8080` | HTTP listen port |
| `PRESIGN_EXPIRY` | No | `15m` | Presigned URL expiration (Go duration) |
| `S3_PATH_STYLE` | No | `false` | Use path-style S3 addressing (required for non-AWS backends) |
| `AWS_ENDPOINT_URL` | No | | Custom S3-compatible endpoint URL |
| `S3_PRESIGN_ENDPOINT` | No | | Endpoint used in presigned download URLs (see below) |
| `MAX_UPLOAD_SIZE` | No | `10485760` (10MB) | Maximum upload body size in bytes |

AWS credentials are loaded via the standard SDK default chain (IAM roles, environment variables, credential files).

### Split endpoint for presigned URLs

When crow runs behind a Docker network, the S3 backend may be reachable at an internal address (e.g. `http://rustfs:9000`) that clients like Terraform can't resolve. Set `S3_PRESIGN_ENDPOINT` to the externally-reachable address so presigned download URLs point to the right host:

```
AWS_ENDPOINT_URL=http://rustfs:9000          # crow → S3 (internal)
S3_PRESIGN_ENDPOINT=http://localhost:9000     # client → S3 (external)
```

If `S3_PRESIGN_ENDPOINT` is not set, presigned URLs use the same endpoint as `AWS_ENDPOINT_URL`.

## Local Development with Docker Compose

A `docker-compose.yaml` is provided for local testing using [RustFS](https://github.com/rustfs/rustfs) as an S3-compatible backend.

```bash
docker compose up --build
```

This starts three services:

- **rustfs** — S3-compatible object storage on `localhost:9000` (API) and `localhost:9001` (console, credentials: `rustfsadmin`/`rustfsadmin`)
- **rustfs-setup** — Init container that creates the `terraform-modules` bucket and seeds sample data from `testdata/`
- **crow** — The registry on `localhost:8080`

Once running:

```bash
# Health check
curl http://localhost:8080/health

# List versions
curl http://localhost:8080/v1/modules/mymodule/mymodule/aws/versions

# Download a module
curl -I http://localhost:8080/v1/modules/mymodule/mymodule/aws/1.0.0/download
```

To seed modules, place `.tar.gz` archives in `testdata/` following the S3 key layout and update the `rustfs-setup` entrypoint in `docker-compose.yaml`.

## Development

```bash
# From the repository root:
make lint-service DIR_NAME=crow
make test-service-verbose DIR_NAME=crow
make build-service DIR_NAME=crow

# Build container image:
make build-service-image DIR_NAME=crow
```

## Usage with Terraform

Point Terraform at the registry by configuring a `~/.terraformrc` or project-level `.terraformrc`:

```hcl
host "crow.example.com" {
  services = {
    "modules.v1" = "https://crow.example.com/v1/modules/"
  }
}
```

Then reference modules normally:

```hcl
module "example" {
  source  = "crow.example.com/namespace/name/provider"
  version = "1.0.0"
}
```


List crow internal/pkg and crowctl/birdseye dirs


/Users/steve.briggs/projects/operations/apps/crowctl/:
drwxr-xr-x steve.briggs staff  256 B  Tue May 12 16:37:45 2026 cmd
.rw-r--r-- steve.briggs staff  400 B  Tue May 12 16:37:45 2026 go.mod
.rw-r--r-- steve.briggs staff 1010 B  Tue May 12 16:37:45 2026 go.sum
drwxr-xr-x steve.briggs staff  128 B  Tue May 12 16:37:45 2026 internal
drwxr-xr-x steve.briggs staff  224 B  Tue May 12 16:37:45 2026 .
.rw-r--r-- steve.briggs staff  3.2 KB Tue May 12 16:37:45 2026 main.go
drwxr-xr-x steve.briggs staff  288 B  Tue May 12 17:10:06 2026 ..

/Users/steve.briggs/projects/operations/services/crow/internal/:
drwxr-xr-x steve.briggs staff 128 B Tue May 12 16:37:45 2026 config
drwxr-xr-x steve.briggs staff 128 B Tue May 12 16:37:45 2026 registry
drwxr-xr-x steve.briggs staff 160 B Tue May 12 16:37:45 2026 .
drwxr-xr-x steve.briggs staff 192 B Tue May 12 16:37:45 2026 storage
drwxr-xr-x steve.briggs staff 416 B Tue May 12 16:37:45 2026 ..

/Users/steve.briggs/projects/operations/services/crow/pkg/:
drwxr-xr-x steve.briggs staff 416 B Tue May 12 16:37:45 2026 ..
drwxr-xr-x steve.briggs staff 192 B Tue May 12 16:37:45 2026 api
drwxr-xr-x steve.briggs staff 128 B Tue May 12 16:37:45 2026 .
drwxr-xr-x steve.briggs staff 128 B Tue May 12 16:37:45 2026 client

/Users/steve.briggs/projects/operations/apps/birdseye/:
drwxr-xr-x steve.briggs staff  64 B Tue May 12 17:07:23 2026 .
drwxr-xr-x steve.briggs staff 288 B Tue May 12 17:10:06 2026 ..


Let me check what the crow client package and crowctl already provide.


List relevant subdirs


/Users/steve.briggs/projects/operations/apps/crowctl/cmd/:
.rw-r--r-- steve.briggs staff 1.3 KB Tue May 12 16:37:45 2026 list_versions.go
.rw-r--r-- steve.briggs staff 1.8 KB Tue May 12 16:37:45 2026 list_versions_test.go
.rw-r--r-- steve.briggs staff 1.9 KB Tue May 12 16:37:45 2026 upload.go
.rw-r--r-- steve.briggs staff 3.3 KB Tue May 12 16:37:45 2026 upload_test.go
.rw-r--r-- steve.briggs staff 2.0 KB Tue May 12 16:37:45 2026 validate.go
drwxr-xr-x steve.briggs staff 256 B  Tue May 12 16:37:45 2026 .
.rw-r--r-- steve.briggs staff 3.7 KB Tue May 12 16:37:45 2026 validate_test.go
drwxr-xr-x steve.briggs staff 224 B  Tue May 12 16:37:45 2026 ..

/Users/steve.briggs/projects/operations/apps/crowctl/internal/:
drwxr-xr-x steve.briggs staff 128 B Tue May 12 16:37:45 2026 archive
drwxr-xr-x steve.briggs staff 128 B Tue May 12 16:37:45 2026 .
drwxr-xr-x steve.briggs staff 128 B Tue May 12 16:37:45 2026 module
drwxr-xr-x steve.briggs staff 224 B Tue May 12 16:37:45 2026 ..

/Users/steve.briggs/projects/operations/services/crow/pkg/api/:
.rw-r--r-- steve.briggs staff 3.2 KB Tue May 12 16:37:45 2026 paths.go
.rw-r--r-- steve.briggs staff 3.7 KB Tue May 12 16:37:45 2026 paths_test.go
.rw-r--r-- steve.briggs staff 1.1 KB Tue May 12 16:37:45 2026 types.go
drwxr-xr-x steve.briggs staff 192 B  Tue May 12 16:37:45 2026 .
.rw-r--r-- steve.briggs staff 1.0 KB Tue May 12 16:37:45 2026 types_test.go
drwxr-xr-x steve.briggs staff 128 B  Tue May 12 16:37:45 2026 ..

/Users/steve.briggs/projects/operations/services/crow/pkg/client/:
drwxr-xr-x steve.briggs staff 128 B  Tue May 12 16:37:45 2026 ..
.rw-r--r-- steve.briggs staff 3.7 KB Tue May 12 16:37:45 2026 client.go
drwxr-xr-x steve.briggs staff 128 B  Tue May 12 16:37:45 2026 .
.rw-r--r-- steve.briggs staff 5.4 KB Tue May 12 16:37:45 2026 client_test.go


1	// Command crowctl drives the Crow Terraform-module registry from CI.
2	//
3	// Subcommands:
4	//   - validate:      check that each module in the changeset has a coherent
5	//     VERSION bump and that its version is not already
6	//     published in Crow. Used on PRs.
7	//   - upload:        tar each module in the changeset and PUT it to Crow.
8	//     Used on pushes to master.
9	//   - list-versions: list the published versions for a single module.
10	//     Intended for manual / debugging use.
11	//
12	// Environment:
13	//
14	//	CROW_URL  overrides the default https://crow.infrastructure.immuta.io.
15	//
16	// Logs go to stderr; the --json flag emits them as structured slog JSON.
17	package main
18
19	import (
20		"context"
21		"errors"
22		"log/slog"
23		"os"
24		"time"
25
26		"github.com/immuta/operations/apps/crowctl/cmd"
27		"github.com/immuta/operations/libs/clog"
28		"github.com/urfave/cli/v3"
29	)
30
31	func main() {
32		app := &cli.Command{
33			Name:  "crowctl",
34			Usage: "Validate, upload, and inspect Terraform modules in Crow",
35			Flags: []cli.Flag{
36				&cli.StringFlag{
37					Name:    "crow-url",
38					Usage:   "Crow base URL",
39					Value:   "https://crow.infrastructure.immuta.io",
40					Sources: cli.EnvVars("CROW_URL"),
41				},
42				&cli.BoolFlag{
43					Name:  "json",
44					Usage: "emit structured JSON logs",
45				},
46				&cli.DurationFlag{
47					Name:  "timeout",
48					Usage: "per-operation HTTP timeout",
49					Value: 30 * time.Second,
50				},
51			},
52			Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
53				if cmd.Bool("json") {
54					slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil)))
55					return ctx, nil
56				}
57				ctx = clog.AddLoggerToContext(ctx, os.Stderr, clog.LogOpts{
58					Level: slog.LevelInfo,
59				})
60				slog.SetDefault(clog.GetLoggerFromContext(ctx))
61				return ctx, nil
62			},
63			Commands: []*cli.Command{
64				{
65					Name:      "validate",
66					Usage:     "Check that changed modules' versions are not already published in Crow",
67					ArgsUsage: "<changed-file> [changed-file...]",
68					Action: func(ctx context.Context, c *cli.Command) error {
69						return cmd.Validate(ctx, cmd.ValidateOptions{
70							CrowURL:      c.String("crow-url"),
71							Timeout:      c.Duration("timeout"),
72							ChangedFiles: c.Args().Slice(),
73						})
74					},
75				},
76				{
77					Name:      "upload",
78					Usage:     "Tar each changed module and PUT it to Crow",
79					ArgsUsage: "<changed-file> [changed-file...]",
80					Action: func(ctx context.Context, c *cli.Command) error {
81						return cmd.Upload(ctx, cmd.UploadOptions{
82							CrowURL:      c.String("crow-url"),
83							Timeout:      c.Duration("timeout"),
84							ChangedFiles: c.Args().Slice(),
85						})
86					},
87				},
88				{
89					Name:      "list-versions",
90					Usage:     "List published versions for a module",
91					ArgsUsage: "<namespace> <name> <provider>",
92					Action: func(ctx context.Context, c *cli.Command) error {
93						args := c.Args().Slice()
94						if len(args) != 3 {
95							return errors.New("list-versions requires 3 args: <namespace> <name> <provider>")
96						}
97						return cmd.ListVersions(ctx, cmd.ListVersionsOptions{
98							CrowURL:   c.String("crow-url"),
99							Timeout:   c.Duration("timeout"),
100							Namespace: args[0],
101							Name:      args[1],
102							Provider:  args[2],
103						})
104					},
105				},
106			},
107		}
108
109		if err := app.Run(context.Background(), os.Args); err != nil {
110			slog.Error("crowctl failed", "error", err)
111			os.Exit(1)
112		}
113	}
114

// Command crowctl drives the Crow Terraform-module registry from CI.
//
// Subcommands:
//   - validate:      check that each module in the changeset has a coherent
//     VERSION bump and that its version is not already
//     published in Crow. Used on PRs.
//   - upload:        tar each module in the changeset and PUT it to Crow.
//     Used on pushes to master.
//   - list-versions: list the published versions for a single module.
//     Intended for manual / debugging use.
//
// Environment:
//
//	CROW_URL  overrides the default https://crow.infrastructure.immuta.io.
//
// Logs go to stderr; the --json flag emits them as structured slog JSON.
package main

import (
	"context"
	"errors"
	"log/slog"
	"os"
	"time"

	"github.com/immuta/operations/apps/crowctl/cmd"
	"github.com/immuta/operations/libs/clog"
	"github.com/urfave/cli/v3"
)

func main() {
	app := &cli.Command{
		Name:  "crowctl",
		Usage: "Validate, upload, and inspect Terraform modules in Crow",
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:    "crow-url",
				Usage:   "Crow base URL",
				Value:   "https://crow.infrastructure.immuta.io",
				Sources: cli.EnvVars("CROW_URL"),
			},
			&cli.BoolFlag{
				Name:  "json",
				Usage: "emit structured JSON logs",
			},
			&cli.DurationFlag{
				Name:  "timeout",
				Usage: "per-operation HTTP timeout",
				Value: 30 * time.Second,
			},
		},
		Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
			if cmd.Bool("json") {
				slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil)))
				return ctx, nil
			}
			ctx = clog.AddLoggerToContext(ctx, os.Stderr, clog.LogOpts{
				Level: slog.LevelInfo,
			})
			slog.SetDefault(clog.GetLoggerFromContext(ctx))
			return ctx, nil
		},
		Commands: []*cli.Command{
			{
				Name:      "validate",
				Usage:     "Check that changed modules' versions are not already published in Crow",
				ArgsUsage: "<changed-file> [changed-file...]",
				Action: func(ctx context.Context, c *cli.Command) error {
					return cmd.Validate(ctx, cmd.ValidateOptions{
						CrowURL:      c.String("crow-url"),
						Timeout:      c.Duration("timeout"),
						ChangedFiles: c.Args().Slice(),
					})
				},
			},
			{
				Name:      "upload",
				Usage:     "Tar each changed module and PUT it to Crow",
				ArgsUsage: "<changed-file> [changed-file...]",
				Action: func(ctx context.Context, c *cli.Command) error {
					return cmd.Upload(ctx, cmd.UploadOptions{
						CrowURL:      c.String("crow-url"),
						Timeout:      c.Duration("timeout"),
						ChangedFiles: c.Args().Slice(),
					})
				},
			},
			{
				Name:      "list-versions",
				Usage:     "List published versions for a module",
				ArgsUsage: "<namespace> <name> <provider>",
				Action: func(ctx context.Context, c *cli.Command) error {
					args := c.Args().Slice()
					if len(args) != 3 {
						return errors.New("list-versions requires 3 args: <namespace> <name> <provider>")
					}
					return cmd.ListVersions(ctx, cmd.ListVersionsOptions{
						CrowURL:   c.String("crow-url"),
						Timeout:   c.Duration("timeout"),
						Namespace: args[0],
						Name:      args[1],
						Provider:  args[2],
					})
				},
			},
		},
	}

	if err := app.Run(context.Background(), os.Args); err != nil {
		slog.Error("crowctl failed", "error", err)
		os.Exit(1)
	}
}


Let me create tasks and peek at the client to know what's already available.


ToolSearch

Question: select:TaskCreate,TaskUpdate


Question: select:TaskCreate,TaskUpdate


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

// Package client is a reusable Crow HTTP client. It is used by apps/crowctl
// and is safe to use from tests (point it at an httptest.Server via
// WithHTTPClient or a localhost baseURL).
package client

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"

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

type Client struct {
	httpClient *http.Client
	baseURL    string
}

type Option func(*Client)

// WithHTTPClient overrides the default *http.Client (useful for tests and
// for callers that want custom timeouts or transport).
func WithHTTPClient(h *http.Client) Option {
	return func(c *Client) { c.httpClient = h }
}

// New constructs a Client pointed at baseURL (e.g. "https://crow.infrastructure.immuta.io").
// baseURL must not end in "/".
func New(baseURL string, opts ...Option) *Client {
	c := &Client{baseURL: baseURL, httpClient: http.DefaultClient}
	for _, opt := range opts {
		opt(c)
	}
	return c
}

// Typed errors consumers can match with errors.Is.
var (
	ErrAlreadyExists = errors.New("module version already exists")
	ErrNotFound      = errors.New("module version not found")
)

// Upload PUTs the given gzipped-tar body for the module version. It returns
// ErrAlreadyExists on HTTP 409, nil on 201, and a generic error otherwise.
func (c *Client) Upload(ctx context.Context, m api.Module, body io.Reader) error {
	if err := m.Validate(); err != nil {
		return err
	}
	if err := api.ValidateVersion(m.Version); err != nil {
		return err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPut, c.baseURL+m.UploadPath(), body)
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/octet-stream")
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	switch resp.StatusCode {
	case http.StatusCreated:
		return nil
	case http.StatusConflict:
		return ErrAlreadyExists
	default:
		return fmt.Errorf("unexpected status %d uploading %s", resp.StatusCode, m.UploadPath())
	}
}

// Exists returns (true, nil) on 200 or 204, (false, nil) on 404, and an
// error for any other status.
func (c *Client) Exists(ctx context.Context, m api.Module) (bool, error) {
	if err := m.Validate(); err != nil {
		return false, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodHead, c.baseURL+m.DownloadPath(), nil)
	if err != nil {
		return false, err
	}
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return false, err
	}
	defer resp.Body.Close()
	switch resp.StatusCode {
	case http.StatusOK, http.StatusNoContent:
		return true, nil
	case http.StatusNotFound:
		return false, nil
	default:
		return false, fmt.Errorf("unexpected status %d checking %s", resp.StatusCode, m.DownloadPath())
	}
}

// ListVersions returns the module's version list in server order.
// Returns ErrNotFound when the module has no published versions.
func (c *Client) ListVersions(ctx context.Context, id api.ModuleIdentifier) ([]string, error) {
	if err := id.Validate(); err != nil {
		return nil, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+id.VersionsPath(), nil)
	if err != nil {
		return nil, err
	}
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	switch resp.StatusCode {
	case http.StatusOK:
		var body api.ListVersionsResponse
		if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
			return nil, fmt.Errorf("decoding response: %w", err)
		}
		if len(body.Modules) == 0 {
			return nil, ErrNotFound
		}
		out := make([]string, len(body.Modules[0].Versions))
		for i, v := range body.Modules[0].Versions {
			out[i] = v.Version
		}
		return out, nil
	case http.StatusNotFound:
		return nil, ErrNotFound
	default:
		return nil, fmt.Errorf("unexpected status %d listing %s", resp.StatusCode, id.VersionsPath())
	}
}


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

// Package api declares the Crow HTTP wire protocol: request/response types
// and segment/path helpers. Both the Crow server and its clients import this
// package so they cannot drift on what a valid module path or response shape
// looks like.
package api

type ModuleIdentifier struct {
	Namespace string
	Name      string
	Provider  string
}

type Module struct {
	ModuleIdentifier
	Version string
}

type VersionEntry struct {
	Version string `json:"version"`
}

type ModuleVersions struct {
	Versions []VersionEntry `json:"versions"`
}

type ListVersionsResponse struct {
	Modules []ModuleVersions `json:"modules"`
}

type ErrorResponse struct {
	Error string `json:"error"`
}

// ServiceDiscoveryResponse is the body of /.well-known/terraform.json. The
// "modules.v1" key is dictated by HashiCorp's Terraform service discovery
// protocol and cannot be renamed to camelCase.
// See https://developer.hashicorp.com/terraform/internals/remote-service-discovery
type ServiceDiscoveryResponse struct {
	ModulesV1 string `json:"modules.v1"` //nolint:tagliatelle // external wire format
}


Got the lay of the land, Sir Charles. Let me set up tasks for the brainstorming flow.


TaskCreate

Explore project context

Read crow service, crowctl, client/api packages to understand reusable bits


Task #1 created successfully: Explore project context

Explore project context


Ask clarifying questions

One at a time - purpose, success criteria, scope constraints for birdseye


Task #2 created successfully: Ask clarifying questions

Ask clarifying questions


Propose 2-3 architectural approaches

Present trade-offs with a recommendation


Task #3 created successfully: Propose 2-3 architectural approaches

Propose 2-3 architectural approaches


Present design sections for approval

Architecture, components, data flow, error handling, testing - approval at each section


Task #4 created successfully: Present design sections for approval

Present design sections for approval


Write and self-review design doc

Save to docs/superpowers/specs/, self-review for placeholders/contradictions, commit


Task #5 created successfully: Write and self-review design doc

Write and self-review design doc


User reviews written spec

Hand off for user spec review before invoking writing-plans


Task #6 created successfully: User reviews written spec

User reviews written spec


Invoke writing-plans skill

Transition to implementation plan once spec is approved


Task #7 created successfully: Invoke writing-plans skill

Invoke writing-plans skill


Updated task #1 status


Updated task #2 status


I've got a clear picture: crow stores `.tar.gz` modules at `namespace/name/provider/version`, and `services/crow/pkg/client` already provides `ListVersions`/`Exists`/`Upload` — but no `Download` (we'll need to add one or fetch the presigned URL ourselves).

Time for clarifying questions, one at a time. Starting with the most foundational: **how should the user invoke a diff**, because that shapes everything else (subcommand layout, TUI scope, flag design).


AskUserQuestion

Question: How should the user invoke a diff with birdseye?

Choices:
birdseye diff <namespace>/<name>/<provider> <vA> <vB> — fast for scripts/CI; if args omitted, drop into TUI picker.
Bare `birdseye` launches TUI that walks you through module → versionA → versionB → diff. CLI args are an escape hatch.
birdseye diff --module=ns/name/aws --from=1.0.0 --to=1.1.0 — verbose but unambiguous and friendly to shell completion.


User has answered your questions: "How should the user invoke a diff with birdseye?"="Positional args (one-shot)". You can now continue with the user's answers in mind.

Question: How should the user invoke a diff with birdseye?

Choices:
birdseye diff <namespace>/<name>/<provider> <vA> <vB> — fast for scripts/CI; if args omitted, drop into TUI picker.
Bare `birdseye` launches TUI that walks you through module → versionA → versionB → diff. CLI args are an escape hatch.
birdseye diff --module=ns/name/aws --from=1.0.0 --to=1.1.0 — verbose but unambiguous and friendly to shell completion.


Good — positional one-shot with TUI fallback when args are missing. Now the next big fork:


Question: What kind of diff should birdseye produce?

Choices:
Per-file line-by-line diff like `git diff`. Handles .tf, .tfvars, README, anything. Simple, robust, ships fast. Aligns with TF module workflow since most changes ARE textual.
Parse HCL and report resources/variables/outputs/providers added/removed/changed. More useful but significantly more work — needs hclparse, schema awareness, etc.
Ship textual first then layer on optional semantic mode. Best of both worlds but doubles the scope of v1.


User has answered your questions: "What kind of diff should birdseye produce?"="Both — textual default, --semantic flag". You can now continue with the user's answers in mind.

Question: What kind of diff should birdseye produce?

Choices:
Per-file line-by-line diff like `git diff`. Handles .tf, .tfvars, README, anything. Simple, robust, ships fast. Aligns with TF module workflow since most changes ARE textual.
Parse HCL and report resources/variables/outputs/providers added/removed/changed. More useful but significantly more work — needs hclparse, schema awareness, etc.
Ship textual first then layer on optional semantic mode. Best of both worlds but doubles the scope of v1.


Bold choice — both modes. Now the TUI scope:


Question: How rich should the bubbletea TUI be?

Choices:
Left: file tree of changed files. Right: unified diff for selected file. Footer: status/keybinds. If --semantic was used, an additional summary tab. Feels like lazygit/k9s.
TUI just provides scrolling/search over a single rendered diff blob — like piping into `less -R`. Easy to ship, less wow factor.
Just print colorized diff to stdout (with pager autodetect via $PAGER). Ship the cobra/diff plumbing first, add TUI in v2.


User has answered your questions: "How rich should the bubbletea TUI be?"="Rich multi-pane (Recommended)". You can now continue with the user's answers in mind.

Question: How rich should the bubbletea TUI be?

Choices:
Left: file tree of changed files. Right: unified diff for selected file. Footer: status/keybinds. If --semantic was used, an additional summary tab. Feels like lazygit/k9s.
TUI just provides scrolling/search over a single rendered diff blob — like piping into `less -R`. Easy to ship, less wow factor.
Just print colorized diff to stdout (with pager autodetect via $PAGER). Ship the cobra/diff plumbing first, add TUI in v2.


Multi-pane it is — lazygit-style. One reality check before I sketch architecture:


Question: Crow's API has no "list modules" endpoint — only "list versions for a known module." In the no-arg TUI flow, how should the user pick which module to diff?

Choices:
Type module address in TUI (Recommended)
TUI shows a textinput for `namespace/name/provider`, then once typed we hit /versions and let them pick two versions. No crow change needed.
Add a new endpoint on crow (e.g., GET /v1/modules) backed by an S3 ListObjects call, then TUI shows a real picker. Wider blast radius — changes a service in production.
TUI activates only AFTER `birdseye diff ns/name/provider` — module must always be on CLI. Two missing versions launch version pickers, but module is never interactive.


User has answered your questions: "Crow's API has no "list modules" endpoint — only "list versions for a known module." In the no-arg TUI flow, how should the user pick which module to diff?"="Type module address in TUI (Recommended)". You can now continue with the user's answers in mind.

Question: Crow's API has no "list modules" endpoint — only "list versions for a known module." In the no-arg TUI flow, how should the user pick which module to diff?

Choices:
Type module address in TUI (Recommended)
TUI shows a textinput for `namespace/name/provider`, then once typed we hit /versions and let them pick two versions. No crow change needed.
Add a new endpoint on crow (e.g., GET /v1/modules) backed by an S3 ListObjects call, then TUI shows a real picker. Wider blast radius — changes a service in production.
TUI activates only AFTER `birdseye diff ns/name/provider` — module must always be on CLI. Two missing versions launch version pickers, but module is never interactive.

Type module address in TUI (Recommended)


Got it — no crow changes needed. I have enough now to sketch architectural approaches. Updating tasks.


Updated task #3 status


## Three approaches for code organization

**Recommendation: Approach A — Reuse crow's `pkg/client`, extend it with a `Download` method.** This matches how `crowctl` already consumes it, keeps wire-format invariants in one place, and the new method is ~20 lines (HEAD-then-redirect via `X-Terraform-Get`). The skill emphasizes "accept interfaces, return concrete types" and small interfaces — adding one method to `Client` is a clean fit. Birdseye stays focused on the diff/TUI value it adds.

**Approach B — Birdseye reimplements its own HTTP client.** Pure independence, no contribution to crow. Downside: duplicates wire-format knowledge (paths, error semantics, `X-Terraform-Get` redirect) — exactly what `pkg/api` exists to prevent drift on.

**Approach C — Add `Download` AND a `list-modules`/cache layer.** Most ambitious. Adds a new crow endpoint, plus on-disk cache for module tarballs in `~/.cache/birdseye/`. Cache part is fine and cheap; new endpoint is service-change scope creep we explicitly ruled out earlier.

### Libraries I'd reach for (calling these out now so we can argue before the design doc)

- **CLI:** `cobra` (per your request) — the existing crowctl uses `urfave/cli/v3`, but you asked for cobra; new app justifies the divergence.
- **Textual diff:** `github.com/hexops/gotextdiff` — this is what `gopls` uses internally; clean API, produces standard unified diffs, ~one third-party dep. Alternative: `github.com/pmezard/go-difflib/difflib` (used by testify, more battle-worn). Either is fine.
- **HCL semantic diff:** `github.com/hashicorp/hcl/v2/hclparse` + `hclwrite` — first-party HashiCorp, only real choice.
- **TUI:** `github.com/charmbracelet/bubbletea` + `bubbles` (filepicker/list/viewport/textinput) + `lipgloss` (styling). Standard charmbracelet stack.
- **Archive extraction:** stdlib `archive/tar` + `compress/gzip`. No third-party needed.

Does Approach A and that library stack work for you, or want to push back on any choice?


afplay ~/.claude/sounds/mario/stop/stomp.wav 2>/dev/null || true


this looks good, lets continue


Excellent. Moving to design presentation.


Updated task #4 status


## Section 1 — Architecture & directory layout

`apps/birdseye/` becomes a standalone Go module (own `go.mod`, mirroring `crowctl`). It depends on `github.com/immuta/operations/services/crow/pkg/{api,client}` for wire-protocol-correct registry talk.

```
apps/birdseye/
  main.go                  # cobra root + flag wiring, hands off to subcommand actions
  Makefile                 # build target, mirrors immanager's Makefile pattern
  cmd/
    root.go                # rootCmd: --crow-url, --timeout, --json flags
    diff.go                # diffCmd: positional args, --semantic, --no-tui, --plain
    versions.go            # versionsCmd: prints version list for a module (bonus, mirrors crowctl)
    diff_test.go
  internal/
    registry/
      registry.go          # tiny wrapper over crow client adding Download + retry/cache
      cache.go             # on-disk cache at ~/.cache/birdseye/<ns>/<name>/<provider>/<ver>.tar.gz
    archive/
      extract.go           # tar.gz -> map[relpath][]byte in memory (modules are <10MB)
    diff/
      textual.go           # gotextdiff per file -> []FileDiff{Path, Hunks, AddCount, DelCount}
      semantic.go          # HCL-aware: parse .tf files, build ModuleSummary, diff summaries
      types.go             # FileDiff, ModuleSummary, SemanticDiff shared types
    tui/
      app.go               # bubbletea program + top-level Model (state machine)
      picker.go            # textinput for module, list pickers for versions
      browser.go           # multi-pane: filetree (list) + viewport(diff)
      styles.go            # lipgloss styles, one place to tweak theme
  testdata/
    moduleA-1.0.0.tar.gz   # fixtures for tests
    moduleA-1.1.0.tar.gz
```

**Contribution to crow's client:** one new method on `*client.Client`:

```go
// Download fetches the module tarball bytes, following the X-Terraform-Get redirect.
func (c *Client) Download(ctx context.Context, m api.Module) (io.ReadCloser, error)
```

Returns `ErrNotFound` on 404. Goes into `services/crow/pkg/client/client.go` with a matching test using `httptest`.

**Boundaries / why these splits:**
- `registry` owns "talk to crow + cache" — diff/TUI don't know about HTTP
- `archive` is a pure function: bytes → file map. No I/O after extraction
- `diff` is pure: two file maps → diff types. Easy to unit-test
- `tui` is pure presentation: it accepts diff types as input, never makes network calls itself. The TUI's `Init()` may dispatch a `tea.Cmd` that calls into registry/diff, but the rendering layer is decoupled

Does this layout look right, or want me to adjust anything before I move on to the data flow section?


yes this looks right


## Section 2 — Data flow

Two main paths through the system, sharing the same `internal/` packages.

### Path A: one-shot CLI (args provided, stdout is a TTY)

```
$ birdseye diff immuta/eks-cluster/aws 1.0.0 1.2.0
```

```
cobra diffCmd.Run
  -> registry.GetTwo(ctx, modID, vA, vB)        // parallel: cache hit OR client.Download
       -> client.ListVersions  (validate both versions exist; nice error otherwise)
       -> client.Download x2   (concurrent, errgroup)
       -> cache writes both    (atomic rename via os.Rename)
  -> archive.Extract x2        (in-memory map[path][]byte; cap 50MB safety)
  -> diff.Textual(a, b)        (-> []FileDiff)
  -> if --semantic: diff.Semantic(a, b)  (-> SemanticDiff alongside)
  -> if isatty(stdout) && !--plain: tui.Run(diffResult)   // interactive browser
     else: render plain colorized unified diff to stdout
```

### Path B: no-args / interactive

```
$ birdseye diff
```

```
cobra diffCmd.Run with empty args
  -> tui.RunPicker(initialState=AskModule)
       state machine:
         AskModule    (textinput "namespace/name/provider")
           -> validate format, then client.ListVersions
           -> if 0 versions or error, show inline error, stay in state
         AskVersionA  (bubbles/list of versions, newest first)
         AskVersionB  (same list, A excluded)
         Loading      (spinner; tea.Cmd fans out registry.GetTwo)
         Browser      (file tree + diff pane — same view as Path A's TUI)
       q exits at any state; esc backs up one state
```

The `Browser` state is **identical code** to the one-shot path — it's a sub-Model the picker hands off to. So we only build the diff-viewer once.

### TTY / output mode decision matrix

| Invocation | TUI? | Output |
|---|---|---|
| `birdseye diff ns/n/p A B` (TTY) | yes (Browser only) | full-screen browser |
| `birdseye diff ns/n/p A B --plain` | no | unified diff to stdout, ANSI color if `$TERM` |
| `birdseye diff ns/n/p A B \| less` (pipe) | no | unified diff, no ANSI |
| `birdseye diff` (TTY) | yes (Picker → Browser) | full TUI flow |
| `birdseye diff` (no TTY) | no | error: "interactive mode requires a TTY" |
| `--json` flag (with args) | no | JSON diff result for scripting |

`--plain` forces plain output; pipe detection auto-flips it. `--json` is a freebie — `diff.FileDiff` already serializes cleanly.

### Errors as data flow

- `client.ErrNotFound` on either version → exit 2, "version X.Y.Z not published for ns/n/p" (and list nearest 5 versions)
- Network/timeout → exit 3, "failed talking to crow at $CROW_URL: <wrapped>"
- Bad module format → cobra usage error, exit 1
- Identical versions → exit 0, "no differences"

Looks right, or any tweaks to the flow?


looks good


## Section 3 — Component interfaces (the public shape of each internal package)

### `internal/registry`

```go
type Registry struct {
    client *client.Client
    cache  Cache
}

type Cache interface {
    Get(api.Module) ([]byte, bool, error)
    Put(api.Module, []byte) error
}

func New(c *client.Client, cache Cache) *Registry

// Fetch returns the gzipped tarball bytes for one module version, cache-first.
func (r *Registry) Fetch(ctx context.Context, m api.Module) ([]byte, error)

// FetchPair fans Fetch out concurrently; cancels both on first error.
func (r *Registry) FetchPair(ctx context.Context, a, b api.Module) (aBytes, bBytes []byte, err error)
```

`Cache` is a small interface so tests inject `memCache{}`; production uses `diskCache{dir}`.

### `internal/archive`

```go
// Module is the in-memory representation of an extracted tarball.
// Keys are forward-slash relative paths; symlinks are resolved or rejected.
type Module struct {
    Files map[string][]byte
}

func Extract(tarball []byte, maxBytes int64) (Module, error)
```

`maxBytes` guards against gzip bombs (default 50MB). Symlinks pointing outside the archive root are rejected with an error — standard untar hardening.

### `internal/diff`

```go
type FileDiff struct {
    Path     string
    Status   FileStatus  // Added | Removed | Modified | Renamed (renames detected by content hash; v1 can skip and just report Added+Removed)
    Unified  string      // pre-rendered unified diff (gotextdiff output)
    Adds     int
    Dels     int
}

type ModuleSummary struct {
    Resources map[string]hcl.Block  // key = "type.name"
    Variables map[string]hcl.Block
    Outputs   map[string]hcl.Block
    Providers map[string]hcl.Block
    Locals    map[string]hcl.Block
    Data      map[string]hcl.Block
    Modules   map[string]hcl.Block
}

type SemanticChange struct {
    Kind   string  // "resource" | "variable" | ...
    Name   string
    Status FileStatus
    Detail string  // optional human description of what changed
}

type Result struct {
    Files    []FileDiff       // always populated
    Semantic []SemanticChange // populated only when --semantic
}

func Textual(a, b archive.Module) []FileDiff
func Semantic(a, b archive.Module) ([]SemanticChange, error)
```

v1 keeps `Status: Renamed` out — keeps the heuristic out of scope. We can add it later by hashing identical-content adds/removes.

### `internal/tui`

```go
// Run launches the full TUI starting from a picker; returns final exit error.
func Run(ctx context.Context, reg *registry.Registry, initial api.ModuleIdentifier) error

// RunBrowser launches just the diff browser with a pre-computed Result.
func RunBrowser(ctx context.Context, result diff.Result) error
```

Two entry points so the one-shot path can skip the picker entirely. Both return when user hits `q`. The Model graph is a single `tea.Model` with an internal state enum (`statePickModule | statePickVersionA | statePickVersionB | stateLoading | stateBrowser`) — no nested bubbletea programs.

### `cmd` package

```go
type diffOpts struct {
    CrowURL  string
    Timeout  time.Duration
    Semantic bool
    Plain    bool
    JSON     bool
    CacheDir string
}

func newDiffCmd() *cobra.Command   // returns configured cobra.Command
func newRootCmd() *cobra.Command   // wires global flags + subcommands
```

Functions return `*cobra.Command` instead of using package-level vars — easier to test (each test builds a fresh command tree, no flag-state leakage between tests).

Want me to push on any interface, or move on to error handling + testing?


move on to error handling and testing


## Section 4 — Error handling

**Error wrapping rules (Go idiomatic, matches existing crow/crowctl style):**

- All errors get context via `fmt.Errorf("doing X: %w", err)` at each layer boundary
- Typed sentinel errors at package boundaries so callers can `errors.Is` without string-matching:
  - `registry.ErrCacheMiss`, `registry.ErrNetwork` (wraps), `registry.ErrCorruptCache`
  - `archive.ErrTooLarge`, `archive.ErrUnsafePath` (zip-slip / symlink escape)
  - `diff.ErrParseFailed` (HCL parse errors in semantic mode)
- Reuse `client.ErrNotFound` and `client.ErrAlreadyExists` from crow's package — don't redefine them

**Exit codes (`cmd` package owns the mapping):**

| Code | Meaning |
|------|---------|
| 0 | Success (including "no differences") |
| 1 | Usage error (bad args, bad module format) — cobra handles most of this |
| 2 | Version not found in registry |
| 3 | Network/registry communication failure |
| 4 | Archive corrupt or unsafe |
| 5 | HCL parse error in `--semantic` mode |
| 130 | User cancelled (Ctrl+C in TUI, picks up `SIGINT`) |

The mapping lives in one `func exitCode(err error) int` in `cmd/root.go` using `errors.Is`. `main.go` calls it.

**User-facing error rendering:**

- One-shot mode: `fmt.Fprintln(os.Stderr, "birdseye: " + err.Error())`. No stack traces in user output.
- `--json` mode: structured `{"error": "...", "code": N}` to stderr, still exit non-zero.
- TUI mode: errors that happen during a `tea.Cmd` flow through to a `stateError` view with a "press q to quit, r to retry" footer. Crashes (panic) in TUI restore terminal via `defer p.RestoreTerminal()` before re-panicking — standard bubbletea pattern.

**Cancellation:**

- Root `context.Context` from `cmd.ExecuteContext` is wired through every `Fetch`, every HTTP request, every long op
- TUI traps `tea.Interrupt` and propagates to context cancel
- Concurrent `FetchPair` uses `errgroup.WithContext` so first failure cancels the sibling

**Validation:**

- Module address parsed by `api.ModuleIdentifier{...}.Validate()` (already exists in crow's `pkg/api`)
- Version strings validated by `api.ValidateVersion` (already exists) — semver, no leading `v`
- These give us clean errors before any network call

---

## Section 5 — Testing strategy

**TDD per your global preferences. Race detector on for every `go test` invocation in CI.**

### Unit tests (the bulk of coverage)

| Package | What we test | How |
|---|---|---|
| `internal/registry` | cache hit/miss, FetchPair concurrent error, retry behavior | Inject mock `Cache` + `httptest.Server` returning canned bytes or errors |
| `internal/archive` | normal extract, zip-slip path, symlink escape, gzip bomb (size cap), empty archive | Build tarballs in-test using `archive/tar` |
| `internal/diff/textual` | added/removed/modified files, identical files = empty result, binary file handling | Hand-built `archive.Module` fixtures |
| `internal/diff/semantic` | resource added/removed/changed, variable type change, malformed HCL → typed err | Inline `.tf` string literals built into archive.Module fixtures |
| `cmd` | flag parsing, arg validation, exit code mapping, JSON output shape | `cobra.Command.SetArgs(...)` + buffer capture for stdout/stderr |
| `internal/tui` | state transitions (model unit tests) | Use `tea.Model.Update(msg)` directly without launching a Program — bubbletea models are pure functions of (state, msg) |

The TUI tests are the secret weapon: because `tea.Model` is `(Model, Cmd) ← Update(Msg)`, we can drive the whole state machine in unit tests by feeding `tea.KeyMsg{Type: tea.KeyEnter}` etc. and asserting on the returned state. No terminal emulator needed.

### Integration tests

One `cmd_e2e_test.go` that:
1. Spins up `httptest.Server` impersonating crow (returns canned `versions.json` and tar.gz bodies)
2. Invokes the cobra root command with `--plain --crow-url=<test server>`
3. Asserts on stdout/stderr/exit code

Avoids real crow, real S3, real network. Hermetic.

### Testdata fixtures

Two real `.tar.gz` tarballs of a synthetic terraform module under `apps/birdseye/testdata/` — one with `main.tf` defining an `aws_s3_bucket` "alpha", and a v2 that renames to "beta" + adds a variable. Reused by archive, diff, and e2e tests.

### Coverage targets

- `internal/*` packages: ≥85% — pure, no I/O, easy to hit
- `cmd`: ≥70% — error paths and flag combos
- `tui`: ≥60% — view rendering is partly visual; we cover state transitions and key handling

### What I'm intentionally NOT testing

- Live crow service (that's crow's job)
- Real S3 backends (likewise)
- Visual snapshot tests of TUI output — too brittle, charm itself doesn't test that way
- Performance/benchmarks — module tarballs are <10MB, no perf concern in v1

Look right? Anything to push on before I write the design doc?


Writing the design doc now. Let me check whether the docs path exists.


Updated task #5 status


Bash

ls /Users/steve.briggs/projects/operations/docs/superpowers/specs/ 2>&1 || ls /Users/steve.briggs/projects/operations/docs/ 2>&1 || echo "no docs dir"

Check spec dir


lsd: /Users/steve.briggs/projects/operations/docs/superpowers/specs/: No such file or directory (os error 2).

drwxr-xr-x steve.briggs staff 288 B Tue May 12 16:37:45 2026 deployment
drwxr-xr-x steve.briggs staff 192 B Tue May 12 16:37:45 2026 design
drwxr-xr-x steve.briggs staff 160 B Tue May 12 16:37:45 2026 .
drwxr-xr-x steve.briggs staff 160 B Tue May 12 16:37:45 2026 sre
drwxr-xr-x steve.briggs staff 736 B Tue May 12 16:37:45 2026 ..


mkdir -p /Users/steve.briggs/projects/operations/docs/superpowers/specs && ls /Users/steve.briggs/projects/operations/docs/superpowers/specs

Create specs dir


drwxr-xr-x steve.briggs staff 64 B Tue May 12 17:44:23 2026 .
drwxr-xr-x steve.briggs staff 96 B Tue May 12 17:44:23 2026 ..