Skip to content

feat(brainstormer): periodic backlog audit — demote cosmetic tickets to loop:cold #125

@hadamrd

Description

@hadamrd

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

Acceptance criteria

  • New method Brainstormer.audit_backlog(repo: str) -> AuditOutcome in src/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.
  • Demotion rule (BOTH branches must be implemented):
    1. Issue has no axis:* label → demote with reason "missing axis citation".
    2. 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_settings_field_default_is_10BrainstormerSettings().audit_every_n_ticks == 10.
  • test_settings_zero_disables_audit — verify _tick short-circuit path doesn't call audit_backlog when setting is 0.

Integration tests:

  • test_tick_runs_audit_on_nth_tick — fake clock + mocked gh adapter; assert audit_backlog invoked on tick 10, 20, but NOT on tick 1-9 or 11.
  • test_audit_emits_typed_event — assert brainstormer_audit_done event lands in events file with correct schema.

Adversarial / sad-path (REQUIRED):

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.
  • Changing the brainstormer's forward (new-ticket-creation) path — that is epic: customer-tunable Product Brainstormer / PO Master (vision + axes drive every ticket) #121's territory.
  • Notifying Slack / external systems on demotion — event emission only.
  • A new CLI subcommand to trigger the audit on-demand (separate ticket if needed; do NOT add forge-loop audit-backlog).
  • Changing priority:* labels on demoted issues.
  • Touching the worker dispatch path or attempt-fingerprint logic.

File pointers

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

Original report

Parent

Part of #121.

What

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.

File pointers

  • src/forge_loop/brainstormer.py — add audit_backlog
  • src/forge_loop/settings.py — new BrainstormerSettings group
  • tests/test_brainstormer_audit.py (new)

Metadata

Metadata

Assignees

No one assigned

    Labels

    loop:readyLoop runner will autonomously attempt this issuepo: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