You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Today the brainstormer (epic #121) files axis-aligned tickets under loop:ready, but nothing prunes the backlog as it ages. Cosmetic tickets that humans file by hand (or that slipped through earlier brainstormer versions) keep their loop:ready label forever and get picked up by workers, burning loop turns on work the product owner already decided is out-of-scope.
Concrete example: an operator opens feat(ui): add a sparkline to the status table with loop:ready but no axis:* label. The dispatch loop happily ships it next tick. Per the axes rubric in .forge/axes.yaml (see #121), this matches rejected_as_cosmetic: "Visual polish without a UX defect" and should never have entered the queue. We need a periodic janitor pass that re-applies the rubric to the existing backlog, not just to newly-proposed tickets.
Demotion rule (BOTH branches must be implemented):
Issue has no axis:* label → demote with reason "missing axis citation".
Issue title OR body matches any regex in any axis's rejected_as_cosmetic list (case-insensitive, anchored via re.search) → demote with reason "matches cosmetic pattern: <quoted-rule>" naming the offending axis + rule.
Demotion side-effects per issue: add loop:cold label, remove loop:ready label, post one comment quoting the failed-rubric reason and linking back to .forge/axes.yaml. All three side-effects go through existing forge_loop.gh helpers (label, unlabel, comment) — do not shell out to gh directly.
New settings field BrainstormerSettings.audit_every_n_ticks: int = 10 in src/forge_loop/settings.py. Value 0 disables the audit entirely (matches the convention used by SchedulingSettings.maintenance_every_n_ticks).
Tick integration: in src/forge_loop/runner/tick.py, when cfg.brainstormer.audit_every_n_ticks > 0 and tick % N == 0, call audit_backlog BEFORE the normal worker dispatch path. Emit a typed event brainstormer_audit_done with tick, demoted: list[int], kept: list[int], duration_s (register it in src/forge_loop/events.py per issue refactor(events): typed discriminated-union schema for events.jsonl + one emit() helper #88 conventions).
Idempotent: re-running audit on an already-demoted issue (no loop:ready, has loop:cold) is a no-op and posts NO duplicate comment.
All GitHub mutations are wrapped in try/except and logged; one failure on issue N must not abort the audit for issue N+1.
Test matrix
Unit tests (tests/test_brainstormer_audit.py, new):
test_demotes_issue_missing_axis_label — fixture issue with loop:ready but no axis:* → asserts loop:ready removed, loop:cold added, comment posted, reason = "missing axis citation".
test_demotes_issue_matching_cosmetic_regex — fixture issue with axis:golden-path-e2e but body contains text matching that axis's rejected_as_cosmetic pattern → demoted with reason naming the offending pattern.
test_keeps_clean_issue — fixture issue with axis:* label and body that matches an acceptable_work example → NOT demoted, no comment posted, no label change.
test_audit_is_idempotent_on_already_cold_issue — run twice; second run posts no comment and changes no labels.
test_malformed_axes_yaml_does_not_crash_tick — if .forge/axes.yaml is missing or invalid, audit logs and skips; the tick continues into normal dispatch.
Out of scope
Re-promoting loop:cold issues back to loop:ready (one-way demotion only; humans re-promote manually).
Closing demoted issues — they stay open under loop:cold for human triage.
src/forge_loop/settings.py — add BrainstormerSettings group near SchedulingSettings (line ~319) following the same BaseSettings + SettingsConfigDict(extra="ignore") pattern. Wire it into the top-level Settings class.
src/forge_loop/runner/tick.py — add the audit short-circuit branch alongside the existing maintenance_every_n_ticks branch (see _tick around line 333). Mirror that branch's structure exactly (write_state → emit start event → call → emit done event → write_state → short_sleep → return).
src/forge_loop/events.py — register BrainstormerAuditDoneEvent and BrainstormerAuditPartialFailureEvent following the TickStartEvent pattern (line ~126).
src/forge_loop/gh.py — re-use existing label, unlabel, and (investigate) comment helpers. Do not add new GitHub primitives.
AC is wide — touches brainstormer.py + settings.py + runner/tick.py + events.py + new test file (4 modules + tests), and depends on #121 having landed the brainstormer module skeleton. Worker, you are at high risk of running out of turns before pushing. Apply COMMIT DISCIPLINE (wip-commit every 20 turns / 5 file-edits) aggressively from the start. If #121's module shape is unclear, do the settings + events + test-skeleton commits FIRST (they don't depend on #121), then layer the audit_backlog implementation on top. Run the EXIT CHECKLIST even if implementation feels incomplete — a half-feature pushed to a PR is recoverable; an un-pushed worktree is not.
Every Nth tick (default 10), the brainstormer scans existing loop:ready issues for the repo and demotes any whose body lacks an axis citation OR matches a rejected_as_cosmetic pattern to loop:cold + posts a comment explaining why.
Acceptance
New Brainstormer.audit_backlog() method runs against open loop:ready issues.
Demotion rule: missing axis:* label OR title/body matches a regex from any axis's rejected_as_cosmetic list.
Demoted issues get: loop:cold label, loop:ready removed, a comment quoting the failed-rubric reason.
Driven by settings.brainstormer.audit_every_n_ticks (default 10) — new Settings field.
Tests: fixtures of mixed clean/cosmetic backlog; assert correct issues demoted.
Problem
Today the brainstormer (epic #121) files axis-aligned tickets under
loop:ready, but nothing prunes the backlog as it ages. Cosmetic tickets that humans file by hand (or that slipped through earlier brainstormer versions) keep theirloop:readylabel forever and get picked up by workers, burning loop turns on work the product owner already decided is out-of-scope.Concrete example: an operator opens
feat(ui): add a sparkline to the status tablewithloop:readybut noaxis:*label. The dispatch loop happily ships it next tick. Per the axes rubric in.forge/axes.yaml(see #121), this matchesrejected_as_cosmetic: "Visual polish without a UX defect"and should never have entered the queue. We need a periodic janitor pass that re-applies the rubric to the existing backlog, not just to newly-proposed tickets.Acceptance criteria
Brainstormer.audit_backlog(repo: str) -> AuditOutcomeinsrc/forge_loop/brainstormer.py(or the brainstormer package created by epic: customer-tunable Product Brainstormer / PO Master (vision + axes drive every ticket) #121). Returns a structured outcome listing demoted issue numbers, kept issue numbers, and per-issue reasons.axis:*label → demote with reason"missing axis citation".rejected_as_cosmeticlist (case-insensitive, anchored viare.search) → demote with reason"matches cosmetic pattern: <quoted-rule>"naming the offending axis + rule.loop:coldlabel, removeloop:readylabel, post one comment quoting the failed-rubric reason and linking back to.forge/axes.yaml. All three side-effects go through existingforge_loop.ghhelpers (label,unlabel,comment) — do not shell out toghdirectly.BrainstormerSettings.audit_every_n_ticks: int = 10insrc/forge_loop/settings.py. Value0disables the audit entirely (matches the convention used bySchedulingSettings.maintenance_every_n_ticks).src/forge_loop/runner/tick.py, whencfg.brainstormer.audit_every_n_ticks > 0 and tick % N == 0, callaudit_backlogBEFORE the normal worker dispatch path. Emit a typed eventbrainstormer_audit_donewithtick,demoted: list[int],kept: list[int],duration_s(register it insrc/forge_loop/events.pyper issue refactor(events): typed discriminated-union schema for events.jsonl + one emit() helper #88 conventions).loop:ready, hasloop:cold) is a no-op and posts NO duplicate comment.try/exceptand logged; one failure on issue N must not abort the audit for issue N+1.Test matrix
Unit tests (
tests/test_brainstormer_audit.py, new):test_demotes_issue_missing_axis_label— fixture issue withloop:readybut noaxis:*→ assertsloop:readyremoved,loop:coldadded, comment posted, reason ="missing axis citation".test_demotes_issue_matching_cosmetic_regex— fixture issue withaxis:golden-path-e2ebut body contains text matching that axis'srejected_as_cosmeticpattern → demoted with reason naming the offending pattern.test_keeps_clean_issue— fixture issue withaxis:*label and body that matches anacceptable_workexample → NOT demoted, no comment posted, no label change.test_settings_field_default_is_10—BrainstormerSettings().audit_every_n_ticks == 10.test_settings_zero_disables_audit— verify_tickshort-circuit path doesn't callaudit_backlogwhen setting is0.Integration tests:
test_tick_runs_audit_on_nth_tick— fake clock + mockedghadapter; assertaudit_backloginvoked on tick 10, 20, but NOT on tick 1-9 or 11.test_audit_emits_typed_event— assertbrainstormer_audit_doneevent lands in events file with correct schema.Adversarial / sad-path (REQUIRED):
test_audit_continues_after_single_issue_failure— fixture of 3 issues; mockgh.labelto raiseRuntimeErroron issue feat(worker): migrate subprocess('claude') to Claude Agent SDK with typed events #2 only. Assert issues EPIC: harden forge-loop into a respectable OSS product (8 sub-issues) #1 and feat(briefs): externalize PO + worker + critic briefs as overridable .md templates #3 still processed correctly and abrainstormer_audit_partial_failureevent is emitted naming issue feat(worker): migrate subprocess('claude') to Claude Agent SDK with typed events #2.test_audit_is_idempotent_on_already_cold_issue— run twice; second run posts no comment and changes no labels.test_malformed_axes_yaml_does_not_crash_tick— if.forge/axes.yamlis missing or invalid, audit logs and skips; the tick continues into normal dispatch.Out of scope
loop:coldissues back toloop:ready(one-way demotion only; humans re-promote manually).loop:coldfor human triage.forge-loop audit-backlog).priority:*labels on demoted issues.File pointers
src/forge_loop/brainstormer.py(orsrc/forge_loop/brainstormer/package created by epic: customer-tunable Product Brainstormer / PO Master (vision + axes drive every ticket) #121) — addaudit_backlogmethod andAuditOutcomedataclass. If the module doesn't exist yet because epic: customer-tunable Product Brainstormer / PO Master (vision + axes drive every ticket) #121 hasn't landed, stop and post a comment on this issue saying "blocked by epic: customer-tunable Product Brainstormer / PO Master (vision + axes drive every ticket) #121" rather than inventing the module shape.src/forge_loop/settings.py— addBrainstormerSettingsgroup nearSchedulingSettings(line ~319) following the sameBaseSettings+SettingsConfigDict(extra="ignore")pattern. Wire it into the top-levelSettingsclass.src/forge_loop/runner/tick.py— add the audit short-circuit branch alongside the existingmaintenance_every_n_ticksbranch (see_tickaround line 333). Mirror that branch's structure exactly (write_state → emit start event → call → emit done event → write_state → short_sleep → return).src/forge_loop/events.py— registerBrainstormerAuditDoneEventandBrainstormerAuditPartialFailureEventfollowing theTickStartEventpattern (line ~126).src/forge_loop/gh.py— re-use existinglabel,unlabel, and (investigate)commenthelpers. Do not add new GitHub primitives.tests/test_brainstormer_audit.py(new) — follow fixture conventions in existingtests/test_maintenance*.pyortests/test_brainstormer*.pyonce epic: customer-tunable Product Brainstormer / PO Master (vision + axes drive every ticket) #121 lands.Worker note
AC is wide — touches
brainstormer.py+settings.py+runner/tick.py+events.py+ new test file (4 modules + tests), and depends on #121 having landed the brainstormer module skeleton. Worker, you are at high risk of running out of turns before pushing. Apply COMMIT DISCIPLINE (wip-commit every 20 turns / 5 file-edits) aggressively from the start. If #121's module shape is unclear, do the settings + events + test-skeleton commits FIRST (they don't depend on #121), then layer theaudit_backlogimplementation on top. Run the EXIT CHECKLIST even if implementation feels incomplete — a half-feature pushed to a PR is recoverable; an un-pushed worktree is not.Original report
Parent
Part of #121.
What
Every Nth tick (default 10), the brainstormer scans existing
loop:readyissues for the repo and demotes any whose body lacks an axis citation OR matches arejected_as_cosmeticpattern toloop:cold+ posts a comment explaining why.Acceptance
Brainstormer.audit_backlog()method runs against openloop:readyissues.axis:*label OR title/body matches a regex from any axis'srejected_as_cosmeticlist.loop:coldlabel,loop:readyremoved, a comment quoting the failed-rubric reason.settings.brainstormer.audit_every_n_ticks(default 10) — new Settings field.File pointers
src/forge_loop/brainstormer.py— addaudit_backlogsrc/forge_loop/settings.py— newBrainstormerSettingsgrouptests/test_brainstormer_audit.py(new)