feat(deps): auto-detect blocker closures + ready-to-assign comment (#196) #198
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
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!198
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/196"
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?
Closes #196.
Summary
When an
issues.closedwebhook fires (i.e. a PR merged and closed its source issue), walk every other open issue that was blocked on it. If all blockers are now closed:handleIssueAssigneddispatch (no comment).✅ **Ready to assign**comment with a suggested agent type + one-line heuristic reasoning. No auto-assign — keeps the operator in the triage seat./holdoverride) → skip silently.Fire-and-forget so the webhook response is never delayed.
Dependency model
Two sources, union for correctness, native wins on disagreement:
/repos/{repo}/issues/{N}/dependencies+/blocksREST API. Authoritative when the operator wires it up (UI or breakdown skill).apps/server/src/deps.ts::parseBlockersFromBodyrecognisesBlocks on #N,Blocked by #N,Depends on #N, the breakdown skill's bullet-bold shape, and comma-list continuations. Covers the M19 backlog that was authored in prose.Deltas between the two are logged so the breakdown skill can be tightened over time.
just deps-backfill <repo>promotes body-parsed edges to native in one idempotent pass.Suggested-assignee heuristic
Priority-ordered in
deps.ts::suggestAssignee:area:design→designer,area:design-review→design-reviewer,area:security/security→reviewer.area:infra+ body hints →boss,type:chore→dev,area:dashboardscaled by body size,area:webhook/area:agents→boss.boss(safer thandev).Reasoning string is rendered inline so the operator sees why — trust-but-verify.
New surface
GET /issues/ready— same set the propagator would have commented on, suitable for the M18-7 board's "Ready" column. Read-only, no cache./hold+/no-ready+/readyslash commands inissue_comment— trust-gated same as/breakdown, persist in the newhold_issuesSQLite table.just deps-backfill <owner>/<repo>— CLI that callsaddIssueDependencyfor every body-parsed edge not already native.skills/breakdown.md— register native deps alongside the prose on every created issue.Implementation
New module
apps/server/src/deps.tsowns the propagator, parser, heuristic, renderer,/issues/readyquery, and the backfill CLI.handleIssueClosedgains a fire-and-forget propagation phase;handleIssueCommentrecognises/hold//ready.hold_issuesSQLite table lives alongside the existingagentstable (no separate DB).Test plan
deps.test.ts(51 tests) — body parser across every phrasing, heuristic matrix, native + body-parser union, classifyBlockers, findDependents, full propagator flows (ready-comment, assigned dispatch, partial closure, held, dedup),listReadyIssues,backfillDependencies.webhook-handlers.test.ts—/hold//readyfrom trusted + untrusted users, DB row write-through.webhook.test.ts—issues.closedcleanup still runs, propagation-phase fetches don't confuse the "cleanup is fetch-free" assertion.just test— 840 tests pass, no regressions.just qa(typecheck + biome check + format) clean.🤖 Generated with Claude Code
Review — Policy change requested
Everything here is sound — the dependency model, body parser, heuristic,
/holdhandling, SQLite table,/issues/readyendpoint, backfill CLI, breakdown-skill integration, and 51deps.test.tscases are all in great shape. CI is green, tests pass, architecture is clean. This is a high-quality implementation of the spec.However — the operator reversed the core policy after this PR was opened: the
unassigned + all-blockers-closedbranch should auto-assign based on the heuristic, not post a comment for the operator to action.Rationale from the operator: the comment-and-wait flow still requires a manual tap per unblock, which defeats the point of automation at a single-operator scale. The heuristic is good enough to try auto-assign directly, with a cheap override if wrong.
All the heuristic logic + dependency machinery you've built is reused verbatim — only the action at the end of the pipeline changes.
Required changes
1. Pivot the
handleUnassignedReadypathInstead of
postReadyComment(...), call the Forgejo issue-update endpoint:The assign call triggers the existing
issues.assignedwebhook →dispatchIssueForAgent→ agent picks up work. The short audit comment is the paper trail so the operator can see why and override cheaply.2. Add a
/unassignslash commandMirror the existing
/holdmachinery inhandleIssueComment. On/unassignfrom the operator (trust-gated same as/hold):updateIssueAssignees(repo, issue.number, [], token)to clear assignees.Unassigned. Re-add a blocker + close it to re-trigger auto-assign, or assign manually.Keep
/holdfor the case where the operator wants to suppress all automation on an issue;/unassignis narrower ("this particular routing was wrong").3. Keep
/issues/readyread-only endpointStill useful for the M18-7 assignment board's "Ready" column — returns issues that just became ready (recent-window? or snapshot?) so the board can animate the transition. Even with auto-assign, the board wants a brief "Ready → assigned" flash. Keep the endpoint as-is.
4. Remove the
postReadyCommentrendererDelete the full ready-to-assign comment template and its renderer. The audit comment in step 1 replaces it. Reduces surface area.
5. Update tests
deps.test.tspropagator cases:unassigned + all-blockers-closedshould assertupdateIssueAssigneescalled, notcreateIssueCommentfor the ready template. The short audit comment can be asserted separately./unassignslash command clears assignees, trust-gated./holdtests.6. Spec amendment on #196
Please edit the issue body to reflect the new policy. The "Out of scope: auto-assigning on ready" line should be removed; replace with "Out of scope: transitive dependency surfacing" (unchanged) and a new note: "Manual override via
/unassign".What stays
suggestAssignee./hold//readymachinery./issues/readyendpoint.just deps-backfillCLI.handleIssueClosed.hold_issuesSQLite table + the trust-gated comment pattern.Test against #170
Once this lands, #170 M18-9 Sunset becomes the live canary — when #167 (its last remaining blocker) closes, we should see boss auto-assigned + dispatched on #170 within seconds, not a ready-comment waiting for a tap.
Review: feat(deps): auto-detect blocker closures + ready-to-assign comment (#196)
CI: ✅ green (run #1816, sha
4b896cf, 3m48s)Round: 1 (first review)
Verdict: APPROVED
Solid implementation. All acceptance criteria from #196 are met, the test suite is comprehensive (51 tests in
deps.test.ts+ handler/webhook test expansions), and no functional bugs found.Findings
Minor: CLAUDE.md says "native wins on disagreement" but code correctly does union
File:
apps/server/src/deps.ts—fetchUnionBlockersFile:
CLAUDE.md— "Issue dependencies" sectionThe PR description, issue AC, and CLAUDE.md all say "When native and body disagree, native wins + log the delta." The code does union instead — any blocker present in either source is retained. The code comment explains this is deliberate:
Union is actually the safer choice here: it prevents false "all blockers closed" positives when the native API lags behind the body text. The delta logging is still present for the breakdown-skill tightening purpose. No functional issue — the code is correct — but CLAUDE.md currently documents the wrong behaviour. Consider updating the "native wins" sentence to "union is used for the all-blockers-closed check; the delta is logged so the breakdown skill can be tightened over time."
Everything else looks good
propagateDependencyClosurefire-and-forget wiring inhandleIssueClosedis correct — async, non-blocking, errors caught and logged per-dependent.probeToken()reuse for propagation is consistent with the rest of the handlers./holdtrust gate mirrors the/breakdowntrust gate — only configured agent users and repo owners can suppress notifications. Tested inwebhook-handlers.test.ts.hold_issuesschema uses composite PK(repo, issue_number)withINSERT OR REPLACEsemantics — idempotent double-hold is correct.READY_COMMENT_MARKERprefix is structural and not content-dependent — operator edits to the comment body won't break it.findDependentscross-repo guard (refRepo && refRepo !== repo) correctly excludes native entries from other repos.#M19-3prose refs correctly not parsed (not a numeric ID) — documented by the test case, intentional./issues/readyendpoint is uncached by design ("One Forgejo round-trip per issue — cheap for the M19 volume (tens of issues)"). Acceptable at current scale.classifyBlockers,findDependents, full propagator flows (ready-comment, assigned dispatch, partial closure, held, dedup),listReadyIssues,backfillDependencies,/hold+/readyCRUD.Policy pivot landed in
81035d8. Summary of what changed, mapped to the review checklist:1.
handleUnassignedReadypivoted —deps.ts::propagateDependencyClosurenow callsupdateIssueAssignees(repo, depNum, [type], token)in the unassigned+ready branch, then stamps a one-line🤖 Auto-assigned to **{type}** (heuristic: …). Reply \/unassign` to reroute.audit comment. Forgejo firesissues.assignedon the PATCH →handleIssueAssigneddispatches. TheonAssignedReady` callback is no longer fired from the unassigned branch (the real webhook re-entry is what dispatches).2.
/unassignslash command added —parseUnassignCommand+applyUnassignCommandindeps.ts; wired intohandleIssueCommentalongside/hold//ready. Trust-gated the same way. Narrower than/hold— clears assignees only, no DB flag toggled. Ack comment:Unassigned. Re-add a blocker + close it to re-trigger auto-assign, or assign an agent manually to dispatch now.3.
/issues/readyendpoint kept as-is. Doc comment updated to call out that post-pivot the window an issue spends in this list is narrow (until the PATCH triggers Forgejo's webhook), which is the "Ready → assigned" flash the M18-7 board uses.4.
postReadyComment/renderReadyCommentremoved. The oldREADY_COMMENT_MARKER+hasReadyComment+ClearedBlockersurface was replaced with the narrowerAUTO_ASSIGN_AUDIT_MARKER+hasAutoAssignAudit+renderAutoAssignAudit. Dedup is kept as belt-and-braces for the rare "re-close after/unassign" loop — the stale audit comment on the issue blocks a second auto-assign, forcing the operator to either assign manually or go through a clean/readycycle.5. Tests updated.
deps.test.ts: propagator "unassigned + all closed" assertsPATCH /issues/201withassignees:["dev"]plus the audit comment body. Dedup test uses the new marker. New suites forparseUnassignCommand+applyUnassignCommand(happy path + PATCH-failure-aborts-ack). 60 tests pass.webhook-handlers.test.ts: three new/unassigntests — trusted user PATCHes + posts ack, untrusted is ignored (no fetch),/unassigndoes NOT set the hold flag. 36 tests pass.just test: 852 pass, 0 fail.6. Issue #196 body amended. "Out of scope: auto-assigning on ready" removed; replaced with the new "Dispatch action" block (auto-assign PATCH + audit comment) and the three-command override matrix (
/hold//ready//unassign). Added a "Policy pivot note" up top explaining why the spec changed mid-flight.Docs.
CLAUDE.md::Issue dependenciesrewritten to describe the new auto-assign policy and the override surface.skills/breakdown.md,forgejo-api.ts,db.tscomment touch-ups to match the new vocabulary.#170 canary — ready to run once this ships. When #167 (its last remaining blocker) closes, the propagator should auto-assign boss on #170 and the dispatch should fire within seconds of the webhook.
just qaclean (typecheck + biome check + format).