Parent
Part of #121.
Problem
The brainstormer/PO produces issues across many concerns (CLI, runner, dispatch, observability, etc.), but the loop has no way to group or focus work by topic. Operators staring at forge-loop status see a flat list of loop:ready issues and cannot tell whether the backlog is dominated by, say, "dispatch" work or "observability" work. Likewise, forge-loop run greedily grabs whatever is ready, so an operator who wants to grind a single area for a sprint has no knob.
Concrete example: today an operator has 14 open loop:ready issues — 6 touch dispatcher internals, 5 touch the CLI, 3 are docs. They want to spend the afternoon hardening dispatch. There is no way to tell the dispatcher "only pull dispatch-tagged issues," and no way to see at a glance that 6/14 are dispatch.
Solution: introduce an axis:<name> GitHub label namespace, surface it in status, and let run filter by it.
Acceptance criteria
- A new label namespace
axis:<name> is documented (README or docs/): any lowercase slug is valid; PO/brainstormer may attach one or more to an issue.
forge-loop status (human + --json) groups open loop:ready issues by their axis:* label(s). Issues with no axis:* are bucketed as unaligned and a soft warning is emitted (yellow line in human output; "unaligned_count" field in JSON).
- Issues carrying multiple
axis:* labels appear under each axis (de-duplicated in totals).
forge-loop run --axis <name> filters dispatch so only issues whose labels include axis:<name> are eligible. The flag is optional; omitting it preserves today's behavior exactly.
--axis accepts repeated use (--axis dispatch --axis cli) → union filter. Unknown axis values are allowed (no validation against a fixed list) but logged at INFO.
forge-loop status --axis <name> likewise narrows the grouped view to that axis (sanity-check before running).
- The dispatcher logs the active axis filter at startup so operator-facing logs make the focused-sprint mode auditable.
- All existing
status and dispatch tests continue to pass unchanged.
Test matrix
Unit
tests/test_cli_axis_filter.py (new):
status groups issues by axis:* label; snapshot the rendered table.
status --json returns { "axes": { "<name>": [...], "unaligned": [...] }, "unaligned_count": N }.
status emits the unaligned warning iff unaligned_count > 0.
- Multi-axis issue appears in each bucket; total count reflects unique issues.
tests/test_dispatch_axis_filter.py (new) or extend existing dispatch test:
run --axis dispatch against MockGhClient only picks up issues labelled axis:dispatch.
run --axis dispatch --axis cli picks up the union.
run (no flag) behavior is byte-identical to today (regression guard).
- Issue labelled
axis:dispatch but missing loop:ready is not picked (ready-gate still wins).
Integration
- End-to-end CLI invocation in a temp git repo using the MockGhClient: seed 6 issues across 2 axes + 1 unaligned, run
status --json, assert structure; run run --axis dispatch --dry-run (if dry-run exists; otherwise mock the worker spawn), assert only dispatch issues are claimed.
Adversarial / sad path
- Issue with malformed label
axis: (empty slug) → treated as unaligned, warning logged, no crash.
--axis with an axis that matches zero issues → dispatcher exits cleanly with a one-line "no eligible issues for axis=X" message (exit code 0, not an error).
- Label list with mixed case
Axis:Dispatch vs axis:dispatch → normalize to lowercase before matching.
Out of scope
- Do not auto-assign axis labels from issue text (that is a separate brainstormer enhancement; this ticket is plumbing only).
- Do not introduce a closed enum / registry of allowed axis names. Free-form slugs.
- Do not change the priority label namespace (
priority:*) or loop:* namespace.
- Do not persist axis filter into config/state — it is a per-invocation flag only.
- No TUI changes (
cli_tui.py) in this ticket; CLI surface only.
- No multi-repo / multi-host changes.
File pointers
src/forge_loop/cli.py — extend _cmd_status and the run command handler; add --axis option to both.
src/forge_loop/runner/dispatch.py — accept and apply an axis_filter: list[str] | None argument; filter eligible issues before claim.
src/forge_loop/gh_client.py — (investigate) if issue listing currently strips labels, ensure labels survive to the dispatcher.
src/forge_loop/_testing/ — (investigate) extend MockGhClient fixtures to carry axis:* labels for tests.
tests/test_cli_axis_filter.py (new).
tests/test_dispatch_axis_filter.py (new) — or fold into existing dispatch test if one exists (investigate).
README.md or docs/ — document the axis:* namespace (one short section).
Worker note
AC is wide — touches CLI (status + run), dispatcher, gh client, mock fixtures, two new test files, AND docs. You are at high risk of running out of turns before pushing. Apply COMMIT DISCIPLINE (wip-commit every ~20 turns or every 5 file-edits) aggressively from the start. Suggested commit order: (1) label-parsing helper + unit test, (2) status grouping + tests, (3) run --axis filter + tests, (4) docs. Run the EXIT CHECKLIST even if docs feel incomplete — partial progress committed is better than a silent dry-exit.
Original report
What
Add axis:<name> label namespace. forge-loop status shows backlog grouped by axis. Dispatcher can optionally filter --axis <name> to grind one axis at a time.
Acceptance
forge-loop status groups open loop:ready issues by their axis:* label; counts unlabeled as "unaligned" with a warning.
forge-loop run --axis <name> filters dispatch to issues carrying axis:<name>. Useful for focused sprints.
- Tests: status output snapshots; dispatch filter against MockGhClient.
File pointers
src/forge_loop/cli.py — extend status + run
src/forge_loop/runner/dispatch.py — axis filter
tests/test_cli_axis_filter.py (new)
Parent
Part of #121.
Problem
The brainstormer/PO produces issues across many concerns (CLI, runner, dispatch, observability, etc.), but the loop has no way to group or focus work by topic. Operators staring at
forge-loop statussee a flat list ofloop:readyissues and cannot tell whether the backlog is dominated by, say, "dispatch" work or "observability" work. Likewise,forge-loop rungreedily grabs whatever is ready, so an operator who wants to grind a single area for a sprint has no knob.Concrete example: today an operator has 14 open
loop:readyissues — 6 touch dispatcher internals, 5 touch the CLI, 3 are docs. They want to spend the afternoon hardening dispatch. There is no way to tell the dispatcher "only pull dispatch-tagged issues," and no way to see at a glance that 6/14 are dispatch.Solution: introduce an
axis:<name>GitHub label namespace, surface it instatus, and letrunfilter by it.Acceptance criteria
axis:<name>is documented (README ordocs/): any lowercase slug is valid; PO/brainstormer may attach one or more to an issue.forge-loop status(human +--json) groups openloop:readyissues by theiraxis:*label(s). Issues with noaxis:*are bucketed asunalignedand a soft warning is emitted (yellow line in human output;"unaligned_count"field in JSON).axis:*labels appear under each axis (de-duplicated in totals).forge-loop run --axis <name>filters dispatch so only issues whose labels includeaxis:<name>are eligible. The flag is optional; omitting it preserves today's behavior exactly.--axisaccepts repeated use (--axis dispatch --axis cli) → union filter. Unknown axis values are allowed (no validation against a fixed list) but logged at INFO.forge-loop status --axis <name>likewise narrows the grouped view to that axis (sanity-check before running).statusand dispatch tests continue to pass unchanged.Test matrix
Unit
tests/test_cli_axis_filter.py(new):statusgroups issues byaxis:*label; snapshot the rendered table.status --jsonreturns{ "axes": { "<name>": [...], "unaligned": [...] }, "unaligned_count": N }.statusemits the unaligned warning iffunaligned_count > 0.tests/test_dispatch_axis_filter.py(new) or extend existing dispatch test:run --axis dispatchagainstMockGhClientonly picks up issues labelledaxis:dispatch.run --axis dispatch --axis clipicks up the union.run(no flag) behavior is byte-identical to today (regression guard).axis:dispatchbut missingloop:readyis not picked (ready-gate still wins).Integration
status --json, assert structure; runrun --axis dispatch --dry-run(if dry-run exists; otherwise mock the worker spawn), assert only dispatch issues are claimed.Adversarial / sad path
axis:(empty slug) → treated as unaligned, warning logged, no crash.--axiswith an axis that matches zero issues → dispatcher exits cleanly with a one-line "no eligible issues for axis=X" message (exit code 0, not an error).Axis:Dispatchvsaxis:dispatch→ normalize to lowercase before matching.Out of scope
priority:*) orloop:*namespace.cli_tui.py) in this ticket; CLI surface only.File pointers
src/forge_loop/cli.py— extend_cmd_statusand theruncommand handler; add--axisoption to both.src/forge_loop/runner/dispatch.py— accept and apply anaxis_filter: list[str] | Noneargument; filter eligible issues before claim.src/forge_loop/gh_client.py— (investigate) if issue listing currently strips labels, ensure labels survive to the dispatcher.src/forge_loop/_testing/— (investigate) extendMockGhClientfixtures to carryaxis:*labels for tests.tests/test_cli_axis_filter.py(new).tests/test_dispatch_axis_filter.py(new) — or fold into existing dispatch test if one exists (investigate).README.mdordocs/— document theaxis:*namespace (one short section).Worker note
AC is wide — touches CLI (status + run), dispatcher, gh client, mock fixtures, two new test files, AND docs. You are at high risk of running out of turns before pushing. Apply COMMIT DISCIPLINE (wip-commit every ~20 turns or every 5 file-edits) aggressively from the start. Suggested commit order: (1) label-parsing helper + unit test, (2)
statusgrouping + tests, (3)run --axisfilter + tests, (4) docs. Run the EXIT CHECKLIST even if docs feel incomplete — partial progress committed is better than a silent dry-exit.Original report
What
Add
axis:<name>label namespace.forge-loop statusshows backlog grouped by axis. Dispatcher can optionally filter--axis <name>to grind one axis at a time.Acceptance
forge-loop statusgroups openloop:readyissues by theiraxis:*label; counts unlabeled as "unaligned" with a warning.forge-loop run --axis <name>filters dispatch to issues carryingaxis:<name>. Useful for focused sprints.File pointers
src/forge_loop/cli.py— extendstatus+runsrc/forge_loop/runner/dispatch.py— axis filtertests/test_cli_axis_filter.py(new)