Skip to content

feat(brainstormer): forge-loop brainstorm CLI (dry-run + --apply) #124

@hadamrd

Description

@hadamrd

Problem

Issue #122 introduced the brainstormer module, and #123 introduced GhClient. There is still no operator-facing way to actually invoke the brainstormer from the command line. Right now an operator who wants to seed the backlog from a ProductVision has to drop into Python or write an ad-hoc script, and there's no safe preview before issues are filed on GitHub. That defeats the sprint loop's promise of "vision → tickets" in one command.

Concrete example: an operator updates forge-loop.yaml with two new axes ("billing", "observability") and wants to see what epics + tickets the brainstormer would propose before anything lands on the repo. Today they cannot. We need a CLI that defaults to a dry-run (prints the BrainstormReport as YAML to stdout) and only files issues when --apply is passed.

Acceptance criteria

  • forge-loop brainstorm (no flags) loads the configured ProductVision, invokes Brainstormer.run(), and prints the resulting BrainstormReport as YAML to stdout. Exit code 0. No GitHub calls made.
  • forge-loop brainstorm --apply files each proposed epic and ticket via GhClient.create_issue (issue refactor(gh): migrate from gh-CLI subprocess to githubkit SDK — typed, async-capable, no subprocess plumbing #83 client). Exit code 0 on full success.
  • Each filed epic carries labels: axis:<name> + epic. Body contains the epic's customer story and acceptance criteria as rendered Markdown.
  • Each filed ticket carries labels: axis:<name> + loop:ready. Body contains a Parent: #<epic-number> cross-link to the epic that was just filed in the same run (so epics must be filed first and their returned issue numbers threaded into ticket bodies).
  • Missing or invalid ProductVision (no forge-loop.yaml, empty axes, or ProductVision validation error) → exit code 2 with a clear stderr message. No partial state.
  • Partial failure during --apply (some issues filed, some GhClient calls raised) → exit code 1. Stdout/stderr report which titles succeeded with their issue numbers and which failed with the error.
  • New subcommand wired into the existing Typer app in src/forge_loop/cli.py alongside the other @app.command definitions (do not invent a new dispatch pattern).
  • --apply is opt-in only; never default to writing GitHub state.

Test matrix

Unit tests (in tests/test_cli_brainstorm.py, using typer.testing.CliRunner + MockGhClient from forge_loop.gh_client):

  • test_brainstorm_dry_run_prints_yaml: stub Brainstormer.run to return a fixed BrainstormReport with 1 epic + 2 tickets; assert YAML output parses back to the same shape and MockGhClient.create_issue was never called.
  • test_brainstorm_apply_files_epics_first: assert epic is filed before its tickets and ticket bodies contain #<epic-number> matching the epic's mocked return.
  • test_brainstorm_apply_labels_epic: filed epic has both axis:<name> and epic labels.
  • test_brainstorm_apply_labels_ticket: filed ticket has both axis:<name> and loop:ready labels.
  • test_brainstorm_missing_vision_exits_2: no vision file present → exit 2, no create_issue calls.
  • test_brainstorm_invalid_vision_exits_2: malformed vision (empty axes) → exit 2.
  • Adversarial / sad-path: test_brainstorm_partial_failure_exits_1: MockGhClient.create_issue raises on the 2nd ticket; assert exit code 1, the successful issues are reported on stdout, the failing title + error are reported on stderr, and the epic+first-ticket were still filed (no rollback expected).
  • test_brainstorm_apply_no_proposals: brainstormer returns empty report → exit 0, zero create_issue calls, helpful message printed.

Integration: extend tests/test_cli.py only if needed to register the new subcommand in the existing CLI smoke test. No live-GitHub e2e in this ticket.

Out of scope

File pointers

  • src/forge_loop/cli.py — add a new @app.command("brainstorm") function next to the other top-level commands. Follow the existing _cmd_* + Typer-callback pattern visible in _cmd_init and _cmd_run.
  • src/forge_loop/brainstormer.py — read Brainstormer, BrainstormReport, ProposedEpic, ProposedTicket (already exported).
  • src/forge_loop/gh_client.py — use GhClient.create_issue and MockGhClient for tests. Helper list_open_tickets already exists if needed.
  • src/forge_loop/product_vision.pyProductVision loader / validator (use existing entry point; do not re-parse YAML inline).
  • src/forge_loop/config.py — for resolving the vision path from config (investigate the existing cfg accessor used by _cmd_init).
  • tests/test_cli_brainstorm.py — new test module.
  • tests/test_cli.py — only touch if the CLI smoke test enumerates subcommands.

Original report

Parent

Part of #121. Depends on #123.

What

New forge-loop brainstorm CLI subcommand. Dry-run by default (prints proposed epics + tickets to stdout). --apply actually files them on GitHub.

Acceptance

  • forge-loop brainstorm reads vision, runs brainstormer, prints BrainstormReport as YAML.
  • forge-loop brainstorm --apply files epics + tickets via GhClient (issue refactor(gh): migrate from gh-CLI subprocess to githubkit SDK — typed, async-capable, no subprocess plumbing #83 client). Each filed issue carries: axis:<name> label, epic label (for epics) or loop:ready label (for tickets), parent-epic cross-link in the body.
  • Exit code: 0 on success, 2 on missing/invalid vision, 1 on partial failure (some tickets filed, some not).
  • Tests: CliRunner against MockGhClient; assert filed issues have correct labels + cross-links.

File pointers

  • src/forge_loop/cli.py — add subcommand
  • tests/test_cli_brainstorm.py (new)

Metadata

Metadata

Assignees

No one assigned

    Labels

    po:expandedPO subagent expanded the issue bodypriority:p2Nice to have, opportunistic

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions