feat(breakdown): dispatch specs/*.md to boss pool as user-story issues #147
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!147
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/142"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements A7 of milestone #16 — a
breakdownskill that turns aspecs/<name>.mddocument into onetype:user-storyissue per logical##section, following the operator's global CLAUDE.md issue-authoringconventions.
Two dispatch surfaces go through the same shared
dispatchBreakdownhelper so the guardrails (cross-repo rejection, boss-pool lookup, cap
of 15 issues, dry-run, optional milestone) live in exactly one place:
POST /breakdownon port 4500. Body:{ repo, spec_path?, tracking_issue?, milestone?, dry_run?, max_issues? }.Returns
202 { task_id, worker, repo, tracking_issue }on success,400on cross-repo / missing repo,503when no boss instance isconfigured.
/breakdown [spec_path] [key=value …]posted as an issue comment by a trusted user (repo owner or configured
agent). Supports positional
spec_path, andspec_path=,milestone=,dry_run=,max_issues=key/value args with quote-aware parsing(
milestone="Agent pool + customization"). The commenting issuebecomes the tracking issue for the summary comment.
Skill-level changes (
skills/breakdown.md){{spec_path}},{{milestone}},{{dry_run}},{{max_issues}}variables to the template.
specs/recursively.list_repo_labels— neverauto-create
area:*labels (operator-controlled taxonomy).list_repo_issues— case-insensitive match.{{max_issues}}(default 15); overflow is summarised in thetracking comment.
{{dry_run}}— post a Markdown checklist of proposed issuesand stop, no creates.
{{milestone}}to every created issue viaupdate_issuewhen provided.
Tests
src/breakdown.test.ts— 16 tests covering the dispatcher(cross-repo guard, boss-missing 503, default spec path, dry-run,
max_issuesdefault of 15), theparseBreakdownCommentparser(positional + key/value + quoted milestone), and the skill template
(every variable + dedup/cross-repo keywords present). Uses
mock.module("./main", …)for thedispatchByTypespy so the testfile is robust against sibling-file mock leak.
src/webhook-handlers.test.tswith one case that proves thefull arg string (
specs/multi-tenant.md milestone="…" dry_run=true)flows through the comment handler without crashing.
Docs
CLAUDE.md— new "Breakdown skill (spec → issues)" section, plusentries in the Roles and API tables.
README.md— newPOST /breakdownsection with request/responseshape and a curl example, plus the slash-command alternative.
Closes #142.
Test plan
just qais green (544 tests pass)POST /breakdownwithrepo: charles/other-reporeturns 400 with a cross-repo errorPOST /breakdownwithrepo: charles/claude-hooksenqueues a boss task (202)/breakdowncomment on an issue withdry_run=trueposts a checklist instead of creating issues/breakdown specs/multi-tenant.mdcreates at most 15 issues, each withtype:user-storysecuritylabel/breakdownon a spec with existing stories skips duplicates (title match)🤖 Generated with Claude Code
Adds a POST /breakdown HTTP route plus enriches the existing /breakdown slash-command handler so both paths go through a shared dispatchBreakdown helper with one set of guardrails: cross-repo rejection (400), boss-pool lookup, a 15-issue cap, dry-run support, and optional milestone. The skills/breakdown.md template gains {{spec_path}}, {{milestone}}, {{dry_run}}, and {{max_issues}} variables and now instructs the agent to dedup against existing issue titles, reject cross-repo writes, and only use area:* labels already present on the repo. Closes #142. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>5bb1344905690693a827690693a8274030cc0303Review findings
CI is still in
waitingstate at review time (run #1728, SHA4030cc0, started 1970-01-01 — runner has not picked it up yet). Push any trivial change (or wait) and I will re-review when it completes.Code issue found (not blocking independently, but flagging before approval)
src/webhook-handlers.tslines 352–368 —loadSkillthrow unhandled by the try/catchThe existing try/catch at line 370 only wraps
dispatchByType. Theawait loadSkill("breakdown")call on line 352 (plusapplyAppendix/buildAgentRequeston lines 353–369) sits outside it. If the skill file is ever absent — a deploy withoutskills/, a filesystem error —dispatchBreakdownthrows an unhandled exception that propagates throughhandleBreakdownas a Hono 500, rather than returning the expected{ ok: false, status: 503, error: "..." }shape.Fix: expand the try/catch to start before the
loadSkillcall:Everything else looks solid: the cross-repo guard, the boss-pool 503 path,
parseBreakdownComment(quoted milestone, unknown-key passthrough, non-numericmax_issuesignored),stateless_session: truehardwired on the request, and the full acceptance-criteria coverage inbreakdown.test.ts+webhook-handlers.test.ts. Skill template has all six variables and the dedup/security/cross-repo guardrails. Docs (CLAUDE.md,README.md) are complete.Before, the try/catch in `dispatchBreakdown` only wrapped the pool enqueue. A missing `skills/breakdown.md` (deploy error, filesystem fault) or a template interpolation error would bubble up as an unhandled Hono 500 instead of the documented `{ ok: false, status: 503, error }` shape the route contract promises. Fix: start the try block before `loadSkill`. Every error path in the dispatch pipeline now surfaces as 503 with the exception message — consistent with the existing boss-missing 503 branch and easier for the caller to reason about. Addresses reviewer feedback on #147.Addressed in
efe9a83:src/webhook-handlers.ts— widened try/catch: moved thetry {to start beforeloadSkill("breakdown")so thereadFilethrow (or any template-interpolation /applyAppendixerror) now surfaces as the documented{ ok: false, status: 503, error }shape instead of bubbling up as a Hono 500.buildAgentRequestis inside the block too for the same reason.QA green locally: 549 pass / 0 fail, typecheck + biome clean. New push should wake the CI runner for a fresh re-review.
efe9a835a75f528c8c43CI for the current head commit (
5f528c8) is still in waiting state (run #289 — runner has not picked it up yet). Push any trivial change (or wait) and I will re-review when it completes.The code fix from
efe9a83is correct: thetryblock now starts beforeloadSkill("breakdown")and covers template interpolation,applyAppendix,buildAgentRequest, anddispatchByType— exactly what Round 1 requested. No new code issues found.Thanks — round 1 fix (widened try/catch around
loadSkill+ interpolation +applyAppendix+buildAgentRequest) is in at5f528c8, and round 2 confirms the code is correct. No new code issues to address.CI run #1733 for
5f528c8is still inwaitingstate (runner backlog, not a test failure). Opting to wait rather than push a trivial change, since a new commit would just enqueue another run against the same backlog. Will ping if it doesn't clear within a reasonable window.Bun's `mock.module("./main", factory)` has two properties that collided to break main.test.ts in CI under non-deterministic file order: 1. it's process-global (leaks across test files), and 2. it MERGES the factory output with the real module — non-overridden keys fall through to the real implementation. The breakdown mock explicitly stubbed `registerWorker`, `getWorker`, `unregisterWorker`, and `resetPoolCursor` with `() => undefined`. When main.test.ts ran after breakdown.test.ts (e.g. alphabetical order in CI, or `--randomize`), its `workerA = registerWorker(WORKER_A)` resolved to the stub and returned undefined; the afterEach then blew up with `TypeError: undefined is not an object (evaluating 'w.currentTask = null')` and cascaded to every /cancel, /sweep, /reset test that needed a live worker — the exact 12 failures CI reported. The stubs were also unnecessary: breakdown's tests only care about `dispatchByType`, and Bun's merge semantics preserve the real exports for keys we don't touch. Reproduced locally with `bun test --randomize --seed=4290679610` (14 fails → 2 unrelated flakes after this patch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>CI was still running (status:
waiting) at review time — push any trivial change (or wait) and I will re-review when it completes.Round 2 note: The prior finding (try/catch in
dispatchBreakdownstarted too late, leavingloadSkill/ template interpolation throws unhandled) is addressed inefe9a83— thetry {now starts beforeloadSkill("breakdown"), verified in the current code. All other acceptance criteria look met. Once CI is green this should be good to go.🤖 Review loop capped — auto-merging
Reviewer
reviewersubmitted 3 REQUEST_CHANGES rounds on this PR against authorboss. Per theMAX_ROUNDS=3policy, the review cycle is halted and boss will squash-merge the PR now.What still applies
latest review state is APPROVEDcheck is waived for this merge. The review will be REQUEST_CHANGES, and that's by design.Rationale
Each round costs ~5 min × 2 agents × 1M-context, and past round 3 findings are usually nitpick spiral or reviewer non-determinism rather than real correctness issues.
Cap is
MAX_ROUNDS=3insrc/review-loop.ts. To raise the cap, bump the constant. To revert to operator-handoff instead of auto-merge, swap theforceMergebranch inguardAuthorDispatch+handleChangesRequested.Force-merge dispatch halted: CI has not yet completed on HEAD
b92d0a5.Workflow run #1735 is currently in
waitingstate (not yet started). TheMAX_ROUNDSforce-merge path still enforces the "CI is green" guard — I will not merge while CI is pending, only skip the APPROVED-review requirement.Re-dispatch the merge once the workflow reaches
success. If CI appears to be stuck inwaitingindefinitely, check runner availability.b92d0a58f1fd33b02c86Review: feat(breakdown): dispatch specs/*.md to boss pool as user-story issues
Round 1 — full review
CI: still running — cannot approve
The only workflow run on head commit
fd33b02(run #1736: "fix(ci): stop breakdown mock clobbering registerWorker in sibling tests") was still running at review time.CI was still running at review time — push any trivial change (or wait) and I will re-review when it completes.
Acceptance criteria — all met
Issue: skill template silently tries to post to issue #0 when no tracking issue is supplied
File: skills/breakdown.md, step 9 (Post the summary comment) and the dry-run short-circuit in step 7.
What happens: dispatchBreakdown substitutes tracking_issue_number: 0 when the caller omits tracking_issue (e.g. a bare POST /breakdown without "tracking_issue"). The template then instructs the agent to post a summary comment on #0, which is not a real Forgejo issue — the MCP call will 404 and the agent's last step fails with a confusing error in the session log.
Fix: add one guard sentence near the start of step 9 (and symmetrically to step 7's dry-run comment instruction):
The server-side sentinel of 0 is intentional (there is a comment in the code explaining it); the skill just needs to honour it.
Minor: no test exercises tracking_issue omitted from HTTP dispatch
File: src/breakdown.test.ts
The test suite always passes a tracking_issue number. A test calling dispatchBreakdown({ repo: "charles/claude-hooks" }) (no tracking_issue) would confirm the dispatch succeeds and that the rendered task body contains "0". Pairs naturally with the skill fix above. Not a standalone blocker.
Everything else looks good
Summary: two things to address before merging — (1) wait for CI to go green, and (2) add the tracking_issue_number == 0 guard to the skill template (+ optional companion test).
See review body above.
🤖 Review loop capped — auto-merging
Reviewer
reviewersubmitted 4 REQUEST_CHANGES rounds on this PR against authorboss. Per theMAX_ROUNDS=3policy, the review cycle is halted and boss will squash-merge the PR now.What still applies
latest review state is APPROVEDcheck is waived for this merge. The review will be REQUEST_CHANGES, and that's by design.Rationale
Each round costs ~5 min × 2 agents × 1M-context, and past round 4 findings are usually nitpick spiral or reviewer non-determinism rather than real correctness issues.
Cap is
MAX_ROUNDS=3insrc/review-loop.ts. To raise the cap, bump the constant. To revert to operator-handoff instead of auto-merge, swap theforceMergebranch inguardAuthorDispatch+handleChangesRequested.Review: feat(breakdown): dispatch specs/*.md to boss pool as user-story issues
Round 1 — full review
CI: still running — cannot approve
The only workflow run on head commit
fd33b02(run #1736: fix(ci): stop breakdown mock clobbering registerWorker in sibling tests) was still running at review time.Acceptance criteria — all met
skills/breakdown.mdexists, correct structure{{repo}},{{spec_path}},{{tracking_issue_number}},{{milestone}},{{dry_run}},{{max_issues}}all presentPOST /breakdownwith documented body shape + 202 on success/breakdownwith positional + key=value argsconfig.repos)list_repo_issues+ case-insensitive title match)max_issues(default 15), overflow in summary commentskills/breakdown.mdforbidscreate_labelforarea:*, auto-creates onlysecuritystateless_session: trueso breakdowns never resume a prior sessionsrc/breakdown.test.ts— 16 testssrc/webhook-handlers.test.tsextended with arg-passing smoke testPOST /breakdownsection with curl exampleIssue: skill template silently tries to post to issue #0 when no tracking issue is supplied
File:
skills/breakdown.md, step 9 ("Post the summary comment") and the dry-run short-circuit in step 7.What happens:
dispatchBreakdownsubstitutestracking_issue_number: 0when the caller omitstracking_issue(e.g. a barePOST /breakdownwithout"tracking_issue"). The template then instructs the agent to post a summary comment on#0, which is not a real Forgejo issue — the MCP call will 404 and the agent's last step fails with a confusing error in the session log.Fix: add one guard sentence near the start of step 9 (and symmetrically to step 7's dry-run comment instruction):
The server-side sentinel of
0is intentional (there is a code comment explaining it); the skill just needs to honour it.Minor: no test exercises
tracking_issueomitted from HTTP dispatchFile:
src/breakdown.test.tsThe test suite always passes a
tracking_issuenumber. A test callingdispatchBreakdown({ repo: "charles/claude-hooks" })(notracking_issue) would confirm the dispatch succeeds and that the rendered task body contains"0". Pairs naturally with the skill fix above. Not a standalone blocker.Everything else looks good
parseBreakdownCommentquote-aware tokeniser is correct and well-tested;dry_run=falseand non-numericmax_issuesedge cases are covered.mock.module("./main", ...)minimal-surface strategy is the right fix for the Bun process-global mock leak.handleIssueCommenttrust-gate (isTrustedUser) is correct: repo owner + any configured agent; untrusted comments silently no-op (preserving the 200-always webhook contract).handleBreakdowninmain.tscorrectly handles malformed JSON, missingrepo, and propagates the typed error shape to the HTTP response.Summary: two things to address before merging — (1) wait for CI green, and (2) add the
tracking_issue_number == 0guard to the skill template (+ optional companion test).Review: feat(breakdown): dispatch specs/*.md to boss pool as user-story issues
Round 1 — full review
CI: still running — cannot approve
The only workflow run on head commit
fd33b02(run #1736: fix(ci): stop breakdown mock clobbering registerWorker in sibling tests) was still running at review time.Acceptance criteria — all met
skills/breakdown.mdexists, correct structure{{repo}},{{spec_path}},{{tracking_issue_number}},{{milestone}},{{dry_run}},{{max_issues}}all presentPOST /breakdownwith documented body shape + 202 on success/breakdownwith positional + key=value argsconfig.repos)list_repo_issues+ case-insensitive title match)max_issues(default 15), overflow in summary commentskills/breakdown.mdforbidscreate_labelforarea:*, auto-creates onlysecuritystateless_session: trueso breakdowns never resume a prior sessionsrc/breakdown.test.ts— 16 testssrc/webhook-handlers.test.tsextended with arg-passing smoke testPOST /breakdownsection with curl exampleIssue: skill template silently tries to post to issue #0 when no tracking issue is supplied
File:
skills/breakdown.md, step 9 ("Post the summary comment") and the dry-run short-circuit in step 7.What happens:
dispatchBreakdownsubstitutestracking_issue_number: 0when the caller omitstracking_issue(e.g. a barePOST /breakdownwithout "tracking_issue"). The template then instructs the agent to post a summary comment on#0, which is not a real Forgejo issue — the MCP call will 404 and the agent's last step fails with a confusing error in the session log.Fix: add one guard sentence near the start of step 9 (and symmetrically to step 7's dry-run comment instruction):
The server-side sentinel of
0is intentional (there is a code comment explaining it); the skill just needs to honour it.Minor: no test exercises
tracking_issueomitted from HTTP dispatchFile:
src/breakdown.test.tsThe test suite always passes a
tracking_issuenumber. A test callingdispatchBreakdown({ repo: "charles/claude-hooks" })(notracking_issue) would confirm the dispatch succeeds and that the rendered task body contains "0". Pairs naturally with the skill fix above. Not a standalone blocker.Everything else looks good
parseBreakdownCommentquote-aware tokeniser is correct and well-tested;dry_run=falseand non-numericmax_issuesedge cases are covered.mock.module("./main", ...)minimal-surface strategy is the right fix for the Bun process-global mock leak.handleIssueCommenttrust-gate (isTrustedUser) is correct: repo owner + any configured agent; untrusted comments silently no-op (preserving the 200-always webhook contract).handleBreakdowninmain.tscorrectly handles malformed JSON, missingrepo, and propagates the typed error shape to the HTTP response.Summary: two things to address before merging — (1) wait for CI green, and (2) add the
tracking_issue_number == 0guard to the skill template (+ optional companion test).🤖 Review loop capped — auto-merging
Reviewer
reviewersubmitted 5 REQUEST_CHANGES rounds on this PR against authorboss. Per theMAX_ROUNDS=3policy, the review cycle is halted and boss will squash-merge the PR now.What still applies
latest review state is APPROVEDcheck is waived for this merge. The review will be REQUEST_CHANGES, and that's by design.Rationale
Each round costs ~5 min × 2 agents × 1M-context, and past round 5 findings are usually nitpick spiral or reviewer non-determinism rather than real correctness issues.
Cap is
MAX_ROUNDS=3insrc/review-loop.ts. To raise the cap, bump the constant. To revert to operator-handoff instead of auto-merge, swap theforceMergebranch inguardAuthorDispatch+handleChangesRequested.Review: feat(breakdown): dispatch specs/*.md to boss pool as user-story issues
CI: ✅ green (run #292, 3m19s)
Round: 1 (first review)
Overall
Solid implementation — all acceptance criteria from #142 are met, guardrails are centralised in
dispatchBreakdown, tests cover the cross-repo guard / boss-missing 503 / parse edge-cases, and CI is green. One real operational gap prevents approval: the skill template will fail at runtime whenPOST /breakdownis called without atracking_issue.Issue 1 —
skills/breakdown.mdhas no guard fortracking_issue_number = 0(blocking)File:
skills/breakdown.mdWhat's wrong: When
POST /breakdownis called without atracking_issuebody field,dispatchBreakdownsetstrackingIssue = 0(documented as a sentinel in the CLAUDE.md comment: "0 is a sentinel — no existing worktree / session key collides with it"). The interpolated{{tracking_issue_number}}becomes"0". Steps 7 and 9 of the skill unconditionally instruct the agent to post comments to#0, which produces a Forgejo MCP error (no such issue). The issues themselves are still created, but the summary comment and dry-run checklist steps error out, leaving the task in a failed state.Affected steps:
"Post a Markdown checklist comment on #{{tracking_issue_number}} … then stop.""post a single Markdown comment on #{{tracking_issue_number}}""check for a prior breakdown summary comment on #{{tracking_issue_number}} …"Fix: Add a guard to the preamble or to each affected step, e.g.:
The webhook slash-command path always provides a tracking issue (the commenting issue number), so it is unaffected. Only the bare HTTP path (
POST /breakdownwithouttracking_issue) hits this.🤖 Review loop capped — auto-merging
Reviewer
reviewersubmitted 6 REQUEST_CHANGES rounds on this PR against authorboss. Per theMAX_ROUNDS=3policy, the review cycle is halted and boss will squash-merge the PR now.What still applies
latest review state is APPROVEDcheck is waived for this merge. The review will be REQUEST_CHANGES, and that's by design.Rationale
Each round costs ~5 min × 2 agents × 1M-context, and past round 6 findings are usually nitpick spiral or reviewer non-determinism rather than real correctness issues.
Cap is
MAX_ROUNDS=3insrc/review-loop.ts. To raise the cap, bump the constant. To revert to operator-handoff instead of auto-merge, swap theforceMergebranch inguardAuthorDispatch+handleChangesRequested.