feat(deps): auto-detect blocker closures + ready-to-assign comment #196

Closed
opened 2026-04-20 22:30:57 +00:00 by claude-desktop · 0 comments
Collaborator

Goal

When a PR merges and closes its source issue, detect every issue that was blocked on it, and — once all their blockers are closed — auto-assign each dependent to the heuristic's pick and stamp a one-line audit comment so the operator can see why and override with /unassign if wrong.

Today the operator (or I) manually track this chain: "OK #175 merged → which of M19-3/4/5/6 unblocks → assign one or more". A handful of dispatches per day, each with a cognitive tax and an opportunity to miss a downstream unblock. Automating detection + routing in one shot keeps the operator-in-the-loop model (override is cheap: /unassign clears the assignment) but removes the per-unblock tap.

Policy pivot note: the original spec called for a "Ready to assign" comment and no auto-assign. After more thought this still required a manual tap per unblock, which defeats the point of automation at a single-operator scale. The heuristic is good enough to try direct auto-assign with a cheap override. The review of PR #198 captured the reasoning and flipped the policy; all the heuristic logic + dependency machinery is reused verbatim — only the final action changes.

Acceptance criteria

Dependency model (source of truth)

  • Primary: Forgejo's native issue-dependency API (POST/GET /repos/{repo}/issues/{index}/dependencies). The claude-hooks repo already has enable_issue_dependencies: true.
  • Fallback: body parser scanning for Blocks on #N / Dependencies:\s*.*#N / - \*\*Blocks on #M19-\d+\*\* patterns — covers every issue already filed without native deps set (the breakdown skill writes these in prose).
  • When native and body disagree, native wins + log a warning so the breakdown skill can be tightened later.

Webhook trigger

  • Extend handleIssueClosed (apps/server/src/webhook-handlers.ts) with a dependency-propagation phase. Fires after the existing cleanup:
    1. Fetch every open issue in the repo where the closed issue is a blocker (native API + body scan).
    2. For each, check if all blockers are now closed.
    3. Dispatch action based on assignment state (see next section).
  • Runs fire-and-forget; a slow dep-check must not hold up the webhook response.

Dispatch action

  • Assigned + all blockers closed → fire the existing issues.assigned dispatch logic (reuse handleIssueAssigned). No comment — the task just starts. The operator was already in the loop when they pre-assigned.
  • Unassigned + all blockers closed → PATCH assignees to [<heuristic pick>] via Forgejo's edit-issue route, then stamp a one-line audit comment. Forgejo fires issues.assigned on the PATCH which our own webhook re-enters and dispatches via handleIssueAssigned. No manual tap required.
  • Some blockers still open → no-op (re-evaluate on the next issues.closed).

Audit comment

One-line template (rendered markdown on the dependent issue):

🤖 Auto-assigned to **{agent_type}** (heuristic: {one-line reasoning}). Reply `/unassign` to reroute.

Structural dedup via the 🤖 Auto-assigned to prefix — on a rare re-close loop where the issue is still unassigned and an audit comment is already present, the propagator skips rather than re-assigning over the operator's unassignment.

Suggested-assignee heuristic

  • Label-based routing is authoritative for its carved-out cases: area:design → designer, area:design-review → design-reviewer, area:security → reviewer-security.
  • Otherwise, fall back to a small rules table:
    • area:infra + body mentions "systemd / docker / reconcile" → boss
    • type:choredev
    • area:dashboard + body length > 2 KB → boss (heavy)
    • area:dashboard + body length ≤ 2 KB → dev
    • area:webhook / area:agentsboss (architecture-touching)
    • Default → boss (safer default than dev)
  • Heuristic reasoning is rendered inline in the audit comment so the operator sees why — trust-but-verify.

Operator overrides

  • /hold (or /no-ready) on the issue inserts a row in the hold_issues SQLite table. The propagator skips that issue on every future blocker closure — no dispatch, no auto-assign — until cleared. Use when parking an issue deliberately.
  • /ready clears the hold row. Next blocker closure re-triggers auto-assign.
  • /unassign is narrower than /hold: it just clears the issue's assignee list via PATCH /issues/{N} and posts a one-line ack. Intended for "this particular auto-assign route was wrong". Does NOT toggle the hold flag. Future blocker-close cycles still evaluate the heuristic.
  • All three slash commands are trust-gated (same as /breakdown).

Integration with breakdown skill

  • Extend skills/breakdown.md: when creating an issue with a Blocks on #N body section, also call POST /repos/.../dependencies to set the native dep. Phases out the body-parser fallback over time.
  • One-time backfill script: just deps-backfill <repo> parses every open issue's body, finds Blocks on #N patterns, and sets them natively if missing. Idempotent.

Observability

  • GET /issues/ready endpoint returns open issues whose blockers are all closed AND that haven't been silenced via /hold. Feeds the M18-7 assignment board's "Ready" column so it can animate the brief "Ready → assigned" transition as the propagator PATCHes assignees.
  • Log line per detection: [deps] #M unblocked by #N — auto-assigned to <agent> (<reasoning>).

Tests

  • deps.test.ts: graph traversal — native deps + body-parser fallback + mixed.
  • deps.test.ts: all-blockers-closed detection — single dep, multi-dep, partial-closed.
  • deps.test.ts: suggested-assignee heuristic over a matrix of label / milestone / body combos.
  • deps.test.ts: propagateDependencyClosure unassigned branch PATCHes assignees:[<type>] and posts the audit comment.
  • webhook-handlers.test.ts: issues.closed fires dispatch when assignee is set, auto-assigns when unassigned, no-ops when blockers remain.
  • webhook-handlers.test.ts: /unassign clears assignees + posts ack (trust-gated).
  • deps.test.ts: /hold comment suppresses; /ready re-enables.

Docs

  • CLAUDE.md "Issue dependencies" section describing the native + fallback model, the auto-assign policy, the heuristic, and /hold / /ready / /unassign.
  • /issues/ready endpoint remains for the M18-7 board.
  • skills/breakdown.md updated to prefer native deps.

Out of scope

  • Cross-repo dependencies.
  • Transitive dependency surfacing ("#A blocks #B blocks #C, and #C blocks 5 things") — report only immediate dependents on each close; transitive unblocks will cascade naturally as each intermediate issue closes.
  • A visual dependency graph in the dashboard — would fit M18-7 / M19 pipeline view, not this story.

Override channel for a mis-routed auto-assign: /unassign (narrow) or /hold (wholesale suppression).

Dependencies

  • Depends on nothing in flight. Can land any time.
  • Complements #168 M18-7 assignment board — that story's "Ready" column queries the GET /issues/ready endpoint this issue ships.

References

  • Forgejo issue-dependencies API: POST/GET /repos/{repo}/issues/{index}/dependencies (enabled on claude-hooks via enable_issue_dependencies: true).
  • Existing label-based auto-dispatch: apps/server/src/webhook-handlers.ts::handleIssueLabeled.
  • Existing cleanup-on-close: apps/server/src/webhook-handlers.ts::handleIssueClosed — this story extends it.
  • Breakdown skill: skills/breakdown.md — primary writer of dep-carrying issue bodies.
## Goal When a PR merges and closes its source issue, detect every issue that was blocked on it, and — once *all* their blockers are closed — **auto-assign** each dependent to the heuristic's pick and stamp a one-line audit comment so the operator can see *why* and override with `/unassign` if wrong. Today the operator (or I) manually track this chain: "OK #175 merged → which of M19-3/4/5/6 unblocks → assign one or more". A handful of dispatches per day, each with a cognitive tax and an opportunity to miss a downstream unblock. Automating *detection + routing* in one shot keeps the operator-in-the-loop model (override is cheap: `/unassign` clears the assignment) but removes the per-unblock tap. **Policy pivot note**: the original spec called for a "Ready to assign" comment and *no* auto-assign. After more thought this still required a manual tap per unblock, which defeats the point of automation at a single-operator scale. The heuristic is good enough to try direct auto-assign with a cheap override. The review of PR #198 captured the reasoning and flipped the policy; all the heuristic logic + dependency machinery is reused verbatim — only the final action changes. ## Acceptance criteria ### Dependency model (source of truth) - [x] Primary: **Forgejo's native issue-dependency API** (`POST/GET /repos/{repo}/issues/{index}/dependencies`). The claude-hooks repo already has `enable_issue_dependencies: true`. - [x] Fallback: **body parser** scanning for `Blocks on #N` / `Dependencies:\s*.*#N` / `- \*\*Blocks on #M19-\d+\*\*` patterns — covers every issue already filed without native deps set (the breakdown skill writes these in prose). - [x] When native and body disagree, native wins + log a warning so the breakdown skill can be tightened later. ### Webhook trigger - [x] Extend `handleIssueClosed` (`apps/server/src/webhook-handlers.ts`) with a **dependency-propagation phase**. Fires after the existing cleanup: 1. Fetch every open issue in the repo where the closed issue is a blocker (native API + body scan). 2. For each, check if *all* blockers are now closed. 3. Dispatch action based on assignment state (see next section). - [x] Runs fire-and-forget; a slow dep-check must not hold up the webhook response. ### Dispatch action - [x] **Assigned + all blockers closed** → fire the existing `issues.assigned` dispatch logic (reuse `handleIssueAssigned`). No comment — the task just starts. The operator was already in the loop when they pre-assigned. - [x] **Unassigned + all blockers closed** → PATCH `assignees` to `[<heuristic pick>]` via Forgejo's edit-issue route, then stamp a one-line audit comment. Forgejo fires `issues.assigned` on the PATCH which our own webhook re-enters and dispatches via `handleIssueAssigned`. No manual tap required. - [x] **Some blockers still open** → no-op (re-evaluate on the next `issues.closed`). ### Audit comment One-line template (rendered markdown on the dependent issue): ```markdown 🤖 Auto-assigned to **{agent_type}** (heuristic: {one-line reasoning}). Reply `/unassign` to reroute. ``` Structural dedup via the `🤖 Auto-assigned to` prefix — on a rare re-close loop where the issue is still unassigned and an audit comment is already present, the propagator skips rather than re-assigning over the operator's unassignment. ### Suggested-assignee heuristic - [x] Label-based routing is authoritative for its carved-out cases: `area:design` → designer, `area:design-review` → design-reviewer, `area:security` → reviewer-security. - [x] Otherwise, fall back to a small rules table: - `area:infra` + body mentions "systemd / docker / reconcile" → **boss** - `type:chore` → **dev** - `area:dashboard` + body length > 2 KB → **boss** (heavy) - `area:dashboard` + body length ≤ 2 KB → **dev** - `area:webhook` / `area:agents` → **boss** (architecture-touching) - Default → **boss** (safer default than dev) - [x] Heuristic reasoning is rendered inline in the audit comment so the operator sees *why* — trust-but-verify. ### Operator overrides - [x] **`/hold`** (or `/no-ready`) on the issue inserts a row in the `hold_issues` SQLite table. The propagator skips that issue on every future blocker closure — no dispatch, no auto-assign — until cleared. Use when parking an issue deliberately. - [x] **`/ready`** clears the hold row. Next blocker closure re-triggers auto-assign. - [x] **`/unassign`** is narrower than `/hold`: it just clears the issue's assignee list via `PATCH /issues/{N}` and posts a one-line ack. Intended for "this particular auto-assign route was wrong". Does NOT toggle the hold flag. Future blocker-close cycles still evaluate the heuristic. - [x] All three slash commands are trust-gated (same as `/breakdown`). ### Integration with breakdown skill - [x] Extend `skills/breakdown.md`: when creating an issue with a `Blocks on #N` body section, *also* call `POST /repos/.../dependencies` to set the native dep. Phases out the body-parser fallback over time. - [x] One-time backfill script: `just deps-backfill <repo>` parses every open issue's body, finds `Blocks on #N` patterns, and sets them natively if missing. Idempotent. ### Observability - [x] `GET /issues/ready` endpoint returns open issues whose blockers are all closed AND that haven't been silenced via `/hold`. Feeds the M18-7 assignment board's "Ready" column so it can animate the brief "Ready → assigned" transition as the propagator PATCHes assignees. - [x] Log line per detection: `[deps] #M unblocked by #N — auto-assigned to <agent> (<reasoning>)`. ### Tests - [x] `deps.test.ts`: graph traversal — native deps + body-parser fallback + mixed. - [x] `deps.test.ts`: all-blockers-closed detection — single dep, multi-dep, partial-closed. - [x] `deps.test.ts`: suggested-assignee heuristic over a matrix of label / milestone / body combos. - [x] `deps.test.ts`: `propagateDependencyClosure` unassigned branch PATCHes `assignees:[<type>]` and posts the audit comment. - [x] `webhook-handlers.test.ts`: `issues.closed` fires dispatch when assignee is set, auto-assigns when unassigned, no-ops when blockers remain. - [x] `webhook-handlers.test.ts`: `/unassign` clears assignees + posts ack (trust-gated). - [x] `deps.test.ts`: `/hold` comment suppresses; `/ready` re-enables. ### Docs - [x] CLAUDE.md "Issue dependencies" section describing the native + fallback model, the auto-assign policy, the heuristic, and `/hold` / `/ready` / `/unassign`. - [x] `/issues/ready` endpoint remains for the M18-7 board. - [x] `skills/breakdown.md` updated to prefer native deps. ## Out of scope - Cross-repo dependencies. - Transitive dependency surfacing ("#A blocks #B blocks #C, and #C blocks 5 things") — report only immediate dependents on each close; transitive unblocks will cascade naturally as each intermediate issue closes. - A visual dependency graph in the dashboard — would fit M18-7 / M19 pipeline view, not this story. Override channel for a mis-routed auto-assign: `/unassign` (narrow) or `/hold` (wholesale suppression). ## Dependencies - Depends on nothing in flight. Can land any time. - Complements **#168 M18-7 assignment board** — that story's "Ready" column queries the `GET /issues/ready` endpoint this issue ships. ## References - Forgejo issue-dependencies API: `POST/GET /repos/{repo}/issues/{index}/dependencies` (enabled on claude-hooks via `enable_issue_dependencies: true`). - Existing label-based auto-dispatch: `apps/server/src/webhook-handlers.ts::handleIssueLabeled`. - Existing cleanup-on-close: `apps/server/src/webhook-handlers.ts::handleIssueClosed` — this story extends it. - Breakdown skill: `skills/breakdown.md` — primary writer of dep-carrying issue bodies.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks#196
No description provided.