forgedock 1.1.8 → 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +10 -33
  2. package/bin/cinema.mjs +23 -5
  3. package/bin/engine-cli.mjs +137 -11
  4. package/bin/env-detect.mjs +249 -0
  5. package/bin/forge-utils.mjs +364 -4
  6. package/bin/forgedock.mjs +1064 -135
  7. package/bin/hooks/session-start.mjs +68 -3
  8. package/bin/init-detect.mjs +101 -12
  9. package/bin/journey.mjs +917 -16
  10. package/bin/recall.mjs +24 -7
  11. package/bin/registry.mjs +91 -3
  12. package/bin/runner.mjs +38 -2
  13. package/bin/tui.mjs +49 -3
  14. package/commands/adopt.md +1 -1
  15. package/commands/analytics.md +8 -8
  16. package/commands/audit.md +1 -1
  17. package/commands/autopilot.md +1 -1
  18. package/commands/ci-audit.md +1 -1
  19. package/commands/cleanup.md +87 -52
  20. package/commands/compat-audit.md +1 -1
  21. package/commands/deploy-info.md +1 -1
  22. package/commands/deploy-pr.md +1 -1
  23. package/commands/diagnose.md +1 -1
  24. package/commands/explain.md +1 -1
  25. package/commands/fix-ci.md +1 -1
  26. package/commands/forgedock-init.md +1 -1
  27. package/commands/incident-response.md +3 -3
  28. package/commands/issue.md +1 -1
  29. package/commands/optimize.md +1 -1
  30. package/commands/orchestrate/config.md +13 -3
  31. package/commands/orchestrate/phase-1-resolve.md +1 -1
  32. package/commands/orchestrate/phase-2-triage.md +1 -1
  33. package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
  34. package/commands/orchestrate/phase-3-dependency.md +128 -20
  35. package/commands/orchestrate/phase-4-execution.md +598 -43
  36. package/commands/orchestrate/phase-5-cleanup.md +1 -1
  37. package/commands/orchestrate/phase-6-report.md +51 -3
  38. package/commands/orchestrate/safety.md +2 -2
  39. package/commands/qa-sweep.md +1 -1
  40. package/commands/quality-gate.md +1 -1
  41. package/commands/recover-orphans.md +5 -5
  42. package/commands/replay.md +1 -1
  43. package/commands/review-pr-agents/api.md +2 -2
  44. package/commands/review-pr-agents/auth.md +2 -2
  45. package/commands/review-pr-agents/billing.md +2 -2
  46. package/commands/review-pr-agents/concurrency.md +2 -2
  47. package/commands/review-pr-agents/database.md +2 -2
  48. package/commands/review-pr-agents/frontend.md +2 -2
  49. package/commands/review-pr-agents/infra.md +2 -2
  50. package/commands/review-pr-agents/protocols.md +1 -1
  51. package/commands/review-pr-agents/scraper.md +2 -2
  52. package/commands/review-pr-agents/security.md +2 -2
  53. package/commands/review-pr-staging.md +41 -3
  54. package/commands/review-pr.md +71 -6
  55. package/commands/rollback.md +1 -1
  56. package/commands/scope.md +1 -1
  57. package/commands/security-audit.md +1 -1
  58. package/commands/test-gate.md +1 -1
  59. package/commands/validate.md +1 -1
  60. package/commands/work-on/build/architect.md +1 -1
  61. package/commands/work-on/build/context.md +9 -6
  62. package/commands/work-on/build/implement.md +1 -1
  63. package/commands/work-on/build/validate.md +1 -1
  64. package/commands/work-on/build.md +31 -6
  65. package/commands/work-on/close.md +3 -2
  66. package/commands/work-on/decompose.md +1 -1
  67. package/commands/work-on/investigate.md +14 -5
  68. package/commands/work-on/remediate.md +331 -0
  69. package/commands/work-on/review.md +1 -1
  70. package/commands/work-on-monolithic.md +1 -1
  71. package/commands/work-on.md +194 -56
  72. package/docs/CI.md +2 -2
  73. package/docs/CODEX.md +1 -1
  74. package/docs/CONFIG.md +186 -0
  75. package/docs/site/command-reference.md +31 -1
  76. package/docs/site/getting-started.md +9 -34
  77. package/docs/site/index.md +1 -1
  78. package/docs/spec/label-state-machine.md +1 -1
  79. package/package.json +2 -1
  80. package/packages/protocol/src/types.js +1 -1
  81. package/scripts/check-command-docs-drift.sh +4 -2
  82. package/scripts/danger-zones.mjs +19 -12
  83. package/scripts/transition-label.sh +101 -13
  84. package/scripts/refresh-bot-token.sh +0 -97
@@ -1,5 +1,5 @@
1
1
  ---
2
- install: internal
2
+ install: core
3
3
  ---
4
4
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
5
5
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
@@ -8,6 +8,8 @@ install: internal
8
8
 
9
9
  ## Phase 4: Streaming DAG Execution
10
10
 
11
+ **Entry requires `phase-3-dependency.md`'s Step 3D.6 completion gate to have passed.** <!-- Added: forge#1913 --> Every step below (budget init, dispatch, dependency resolution) reads `ISSUES[]`, `PREDECESSORS[]`, `ISSUE_DOMAIN[]`, `ISSUE_SCORE[]`, `ISSUE_COST_ESTIMATE[]`, and `ISSUE_HAS_PRIOR[]` as authoritative Phase 3 output — none of these are re-derived here. If Phase 4 is being entered without having actually run Phase 3's Steps 3A–3E.5 in this session (e.g. a fresh session resuming mid-batch), reconstruct from GitHub via the "Orchestrator state reconstruction on wake / after compaction" procedure in `phase-3-dependency.md` rather than assuming these variables are already populated.
12
+
11
13
  ### Step 4A-pre.0: Budget initialization (MANDATORY when --budget is set) <!-- Added: forge#1743 -->
12
14
 
13
15
  Initialize budget tracking state before the first dispatch. Read `--budget N` from the orchestrator's argument list (passed from the top-level `/orchestrate` invocation). When `--budget` is not set, `BUDGET_LIMIT` is `Infinity` (uncapped — current default behavior preserved).
@@ -95,6 +97,47 @@ should_dispatch() {
95
97
 
96
98
  **When `BUDGET_LIMIT = Infinity`**: skip this check and report entirely — uncapped behavior.
97
99
 
100
+ ### Step 4A-pre.0.2: Concurrency gate initialization (MANDATORY) <!-- Added: forge#1912 -->
101
+
102
+ **WHY THIS EXISTS** <!-- Added: forge#1912 -->: Phase 4's dispatch loops previously launched every DAG-ready issue in one shot — the engine-first bash loop backgrounded every `should_dispatch()`-passing issue with no count limit, and the Agent-spawn-fallback path was explicitly instructed to "launch all ready agents simultaneously." On a large ready set this saturates the Anthropic API rate limit in one burst, causing cascading failures across most of the batch. This is a distinct, count-denominated gate from the dollar-denominated `--budget` gate above (Step 4A-pre.0) — an issue can be deferred for budget reasons, concurrency reasons, or both; each gate accumulates its own deferred list independently.
103
+
104
+ Initialize the concurrency cap and its batch-scope tracking state before the first dispatch:
105
+
106
+ ```bash
107
+ # --- Concurrency gate initialization ---
108
+ MAX_CONCURRENT=$(yq '.orchestration.max_concurrent // 12' forge.yaml 2>/dev/null || echo 12)
109
+
110
+ # Validate MAX_CONCURRENT is a bare positive integer before it is ever used in arithmetic
111
+ # expansion or array-slice-length position below. Bash's `$(( ))` and `${arr[@]:off:len}`
112
+ # both re-evaluate an unquoted variable's contents as an expression — an unvalidated value
113
+ # (e.g. a malicious or malformed forge.yaml, or a stray non-numeric string) can inject
114
+ # arbitrary command substitution into the arithmetic context, and 0/negative values make
115
+ # the chunked dispatch loop below (Step 4A) never advance its index, spinning forever.
116
+ # Same validation convention as BUDGET_LIMIT in Step 4A-pre.0 above. <!-- Added: forge#1912 -->
117
+ if [ "$MAX_CONCURRENT" = "null" ] || ! echo "$MAX_CONCURRENT" | grep -qP '^[1-9][0-9]*$'; then
118
+ echo "WARNING: forge.yaml → orchestration.max_concurrent is not a positive integer (\"${MAX_CONCURRENT}\") — falling back to default 12"
119
+ MAX_CONCURRENT=12
120
+ fi
121
+
122
+ ACTIVE_DISPATCH_COUNT=0 # in-flight dispatched-but-not-yet-completed agents, this batch
123
+ DEFERRED_CONCURRENCY_ISSUES=() # ready issues held back because no headroom was available
124
+
125
+ echo "Concurrency gate initialized: MAX_CONCURRENT=${MAX_CONCURRENT} (forge.yaml → orchestration.max_concurrent; default 12 when unset or invalid)"
126
+ # --- End concurrency gate initialization ---
127
+ ```
128
+
129
+ **Headroom helper** (used by every dispatch site below — Step 4A's engine-first loop, Step 4A's Agent-spawn-fallback path, and Step 4B's newly-ready dispatch):
130
+
131
+ ```bash
132
+ dispatch_headroom() {
133
+ local HEADROOM=$((MAX_CONCURRENT - ACTIVE_DISPATCH_COUNT))
134
+ [ "$HEADROOM" -lt 0 ] && HEADROOM=0
135
+ echo "$HEADROOM"
136
+ }
137
+ ```
138
+
139
+ **This is a hard cap, not a suggestion.** No dispatch site in Phase 4 may background/spawn more than `dispatch_headroom` new agents without first waiting for in-flight agents to complete. Issues that cannot be dispatched due to the cap go into `DEFERRED_CONCURRENCY_ISSUES[]` and are released — in the order they were deferred — as running agents complete (Step 4B), the same event-driven model (no sleep/poll loops) the file already uses for DAG-readiness and budget gating.
140
+
98
141
  ### Step 4A-pre: Staging baseline tracking (MANDATORY — continuous)
99
142
 
100
143
  **WHY THIS EXISTS**: Milestone-code-onto-staging contamination incidents (see issue #150) produce unexpected growth on the staging branch that is otherwise invisible until after a deploy. In the streaming DAG model, there are no discrete wave boundaries — instead, track a running baseline and check after each agent completion.
@@ -222,7 +265,7 @@ Before building agent prompts, run `classify-lane.sh` for every issue in the cur
222
265
 
223
266
  ```bash
224
267
  # Requires classify-lane.sh to be available at ~/.claude/scripts/classify-lane.sh
225
- # (installed by `npx forgedock` — see bin/forgedock.mjs linkScripts step)
268
+ # (installed by `npx forgedock` — see bin/journey.mjs: forge()'s linkPipelineScripts() step)
226
269
  # Fallback: bash "$FORGE_HOME/scripts/classify-lane.sh" if ~/.claude/scripts/ is unavailable
227
270
 
228
271
  declare -A ISSUE_LANE
@@ -236,6 +279,43 @@ QUEUED_FINDINGS=()
236
279
  declare -A DEFERRED_REASONS
237
280
  declare -A AGENT_ISSUE_MAP
238
281
 
282
+ # Same-file current-state brief forwarding (forge#1860). Populated by the core streaming
283
+ # dispatch loop below (Step 4B) whenever a Layer 1/2/3 structural predecessor edge (see
284
+ # EDGE_KIND/EDGE_FILES from phase-3-dependency.md Step 3C) resolves; consumed by Step 4A's
285
+ # {GIST_CONTEXT} generation. EDGE_BRIEFED guards against re-appending the same predecessor's
286
+ # brief on every completion cycle this loop re-runs before BLOCKED_NUM actually dispatches.
287
+ declare -A SAME_FILE_BRIEF
288
+ declare -A EDGE_BRIEFED
289
+
290
+ # Human-gated idle/backpressure flag (Step 4B item 6.7, forge#1814). Starts false —
291
+ # recomputed every completion cycle over {all_batch_issue_numbers}. Declared at batch
292
+ # scope (not per-agent) so Step 4C can read the latest value on every iteration.
293
+ BATCH_FULLY_GATED=false
294
+
295
+ # Per-batch token budget for Step 4C's review-finding cascade control (forge#1858).
296
+ # Distinct from the $-denominated `--budget N` flag (economic scheduling, forge#1743,
297
+ # Step 4A-pre.0 above) — that mechanism is opt-in and gates the *original* issue
298
+ # dispatch order; this ceiling is always-on, token-denominated, and scopes ONLY to
299
+ # Step 4C's cascade dispatch of review-finding issues. Declared once here (batch scope,
300
+ # not per-agent) so BATCH_TOKEN_SPEND accumulates correctly across every Step 4C run
301
+ # this session performs.
302
+ TOKEN_BUDGET=$(yq '.pipeline.token_budget_per_batch // 900000' forge.yaml 2>/dev/null || echo 900000)
303
+ TOKEN_ESTIMATE_PER_FINDING=$(yq '.pipeline.token_estimate_per_finding // 150000' forge.yaml 2>/dev/null || echo 150000)
304
+ BATCH_TOKEN_SPEND=0
305
+ TOKEN_DEFERRED=() # findings deferred by the token-budget rule this run (re-evaluable in Step 4F.2.6)
306
+
307
+ # Surface-area batching accumulators (forge#1818), promoted to batch scope here so the
308
+ # Step 6B report (forge#1858) sees the full-run total instead of only the last Step 4C
309
+ # completion cycle to touch them. SURFACE_FILE_MEMBERS (the per-cycle file->members grouping
310
+ # map used inside Step 4C) is rebuilt from the current QUEUED_FINDINGS every cycle and must
311
+ # carry no cross-cycle state of its own — but `declare -A` alone does NOT guarantee this: on
312
+ # an already-declared associative array it is a no-op on existing contents. Step 4C enforces
313
+ # the freshness guarantee explicitly via `unset SURFACE_FILE_MEMBERS` immediately before its
314
+ # `declare -A SURFACE_FILE_MEMBERS` each cycle (see below) — do not rely on re-declaration
315
+ # alone. <!-- Added: forge#1909 -->
316
+ SURFACE_BATCHED_FINDINGS=() # all member issue numbers absorbed into a batch across the run
317
+ SURFACE_BATCH_COUNT=0 # count of batch issues created across the run
318
+
239
319
  for NUM in {ready_issue_numbers}; do
240
320
  PR_BASE=$(bash ~/.claude/scripts/classify-lane.sh "$NUM" -R {GH_REPO}) || {
241
321
  echo "ERROR: classify-lane.sh failed for #$NUM — adding needs-human label and skipping" >&2
@@ -266,24 +346,38 @@ Use `${ISSUE_LANE[$NUM]}` and `${ISSUE_PR_BASE[$NUM]}` to populate `{LANE}` and
266
346
  FORGEDOCK_AVAILABLE=$(command -v forgedock >/dev/null 2>&1 && echo "true" || echo "false")
267
347
 
268
348
  if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
349
+ # Build the budget-gated dispatch queue first (forge#1743's should_dispatch() still applies
350
+ # per-issue, independent of the concurrency cap below).
351
+ DISPATCH_QUEUE=()
269
352
  for NUM in "${SORTED_READY_SET[@]:-{ready_issue_numbers}}"; do
270
- # Budget gate (forge#1743): skip dispatch if projected spend would exceed budget ceiling.
271
- # should_dispatch() also handles ε-reserve (no-prior issues get guaranteed slot).
272
- if ! should_dispatch "$NUM"; then
273
- continue # Issue added to DEFERRED_BUDGET_ISSUES[] by should_dispatch()
274
- fi
275
-
276
- LANE="${ISSUE_LANE[$NUM]}"
277
- PR_BASE="${ISSUE_PR_BASE[$NUM]}"
278
- COST="${ISSUE_COST_ESTIMATE[$NUM]:-0.35}"
279
-
280
- # Advance PROJECTED_SPEND before forking so subsequent iterations see the updated total
281
- PROJECTED_SPEND=$(echo "scale=4; $PROJECTED_SPEND + $COST" | bc 2>/dev/null || echo "$PROJECTED_SPEND")
353
+ if should_dispatch "$NUM"; then
354
+ DISPATCH_QUEUE+=("$NUM")
355
+ fi # else: added to DEFERRED_BUDGET_ISSUES[] by should_dispatch()
356
+ done
282
357
 
283
- echo "Dispatching #$NUM via forgedock run-issue --lane $PR_BASE (score=${ISSUE_SCORE[$NUM]:-?} est_cost=\$${COST} projected_total=\$${PROJECTED_SPEND})"
284
- forgedock run-issue "$NUM" --lane "$PR_BASE" &
358
+ # Concurrency gate (forge#1912): dispatch in chunks of MAX_CONCURRENT, waiting for each
359
+ # chunk to finish before starting the next. This is the code-form equivalent of the
360
+ # Agent-spawn-fallback cap below — never more than MAX_CONCURRENT `forgedock run-issue`
361
+ # processes in flight at once, regardless of how many issues are ready.
362
+ IDX=0
363
+ while [ "$IDX" -lt "${#DISPATCH_QUEUE[@]}" ]; do
364
+ CHUNK=("${DISPATCH_QUEUE[@]:$IDX:$MAX_CONCURRENT}")
365
+ echo "Dispatching chunk of ${#CHUNK[@]} issue(s) — concurrency cap ${MAX_CONCURRENT}"
366
+
367
+ for NUM in "${CHUNK[@]}"; do
368
+ LANE="${ISSUE_LANE[$NUM]}"
369
+ PR_BASE="${ISSUE_PR_BASE[$NUM]}"
370
+ COST="${ISSUE_COST_ESTIMATE[$NUM]:-0.35}"
371
+
372
+ # Advance PROJECTED_SPEND before forking so subsequent iterations see the updated total
373
+ PROJECTED_SPEND=$(echo "scale=4; $PROJECTED_SPEND + $COST" | bc 2>/dev/null || echo "$PROJECTED_SPEND")
374
+
375
+ echo "Dispatching #$NUM via forgedock run-issue --lane $PR_BASE (score=${ISSUE_SCORE[$NUM]:-?} est_cost=\$${COST} projected_total=\$${PROJECTED_SPEND})"
376
+ forgedock run-issue "$NUM" --lane "$PR_BASE" &
377
+ done
378
+ wait # cap enforced here — no more than MAX_CONCURRENT processes run concurrently
379
+ IDX=$((IDX + MAX_CONCURRENT))
285
380
  done
286
- wait
287
381
  echo "Engine dispatch complete — advancing to Step 4B (completion sweep)"
288
382
  else
289
383
  echo "INFO: Using agent dispatch mode (forgedock CLI not in PATH — run \`npm install -g forgedock\` for engine-mode dispatch)"
@@ -305,7 +399,7 @@ For each **ready** issue (all predecessors resolved or no predecessors), spawn a
305
399
  ```
306
400
  Agent(
307
401
  subagent_type="general-purpose",
308
- model="sonnet",
402
+ model="{SUBAGENT_MODEL}",
309
403
  description="Work on {PROJECT_PREFIX}#{NUMBER}",
310
404
  run_in_background=true,
311
405
  prompt="You are working on GitHub issue #{NUMBER} for the {PROJECT_NAME} project.
@@ -322,15 +416,16 @@ Agent(
322
416
 
323
417
  **IMPORTANT RULES**:
324
418
  - **MANDATORY**: You MUST use the Skill tool to invoke 'work-on' with args '{PROJECT_PREFIX}{NUMBER}'. Do NOT implement manually — /work-on handles the full pipeline including label state machine (workflow:investigating → workflow:building → workflow:in-review → workflow:merged), investigation reports, PR creation, and cleanup.
325
- - For default repo issues: `Skill(skill='work-on', args='{NUMBER}')`
326
- - For satellite repo issues: `Skill(skill='work-on', args='{SATELLITE_PREFIX}:{NUMBER}')` (prefix from forge.yaml → repos.satellites)
419
+ - For default repo issues: `Skill(skill='work-on', args='{NUMBER} --under-orchestration')`
420
+ - For satellite repo issues: `Skill(skill='work-on', args='{SATELLITE_PREFIX}:{NUMBER} --under-orchestration')` (prefix from forge.yaml → repos.satellites)
421
+ - The `--under-orchestration` flag tells `/work-on` to post its phase-entry `FORGE:HEARTBEAT` comments (Phases 0/1/3/5) — this orchestrator's Step 4B.5 stall detector depends on those timestamps. A solo `/work-on` run omits the flag and skips those writes entirely (see `commands/work-on.md` → Orchestration Flag).
327
422
  - NEVER bypass /work-on with manual git/gh commands — the label updates and structured comments are critical for tracking
328
423
  - NEVER target `main` for PRs targeting the default repo. Use `{STAGING_BRANCH}` for fast-lane issues, or `milestone/{slug}` for milestone issues.
329
424
  - Satellite repos (MCP, n8n) have no staging branch — fast-lane PRs go to `main` for those.
330
425
  - If the issue is INVALID after investigation, close it with a comment explaining why
331
426
  - If you hit merge conflicts or blockers, post a comment on the issue and STOP — do not force anything
332
427
  - Do not interact with the user — you are running autonomously in the background
333
- - **NEVER ask the user questions** — you are a background agent. If review finds issues, auto-fix simple ones and proceed. For complex findings on **low-risk domains**, merge anyway and create follow-up issues. For complex findings on **high-risk domains** (AUTH, BILLING, DATABASE, or any domain tagged as security-critical in Step 3B), add the `needs-human` label and stop — do NOT merge. High-risk `needs-human` halts are surfaced as `⚠ Blocked` in the Phase 5 completion report.
428
+ - **NEVER ask the user questions** — you are a background agent. If review finds issues, auto-fix simple ones and proceed, then let `/review-pr`'s own verdict decide: APPROVED (no unresolved CONFIRMED HIGH/CRITICAL finding) → merge to `staging` and create follow-up issues for the rest, **regardless of domain**. Domain alone (AUTH, BILLING, DATABASE, or any domain tagged as security-critical in Step 3B) is NOT a reason to add `needs-human` and stop — `staging` is reversible; the real human deploy gate is `staging → main`, not this merge. <!-- Added: forge#1815 --> `needs-human` is reserved for what the pipeline genuinely cannot do itself: spend/procurement decisions, real-environment validation it has no access to, product/architecture judgment calls a human must make, or `/review-pr`'s existing evidence-based escalations (spec-evolution guard, novel task-type/module-combo trust escalation, calibration-based overconfidence routing). An unresolved CONFIRMED HIGH/CRITICAL finding is `/review-pr`'s own withheld-APPROVED case, not a domain-driven `needs-human` halt — `/review-pr` already refuses to return APPROVED when that's true, so there is no separate domain check to perform here.
334
429
 
335
430
  **LABEL-STATE LOOP CONTRACT — enforce after EVERY Skill return**:
336
431
  After EVERY `Skill(skill='work-on', ...)` call returns, immediately check the issue's current workflow label:
@@ -340,7 +435,7 @@ gh issue view {NUMBER} -R {GH_REPO} --json labels --jq '[.labels[].name | select
340
435
  **Terminal labels** (only these allow you to stop): `workflow:merged`, `workflow:invalid`
341
436
  **Terminal condition also**: `needs-human` label present, `workflow:awaiting-merge` label present, OR issue state is `closed`
342
437
  `needs-human` and `workflow:awaiting-merge` are terminal-FOR-THIS-AGENT (this individual `/work-on` run stops here — a human decision or merge is now the blocking step) but are NOT "done" from the DAG's point of view; see Predecessor Classification in Step 4B for how the orchestrator's dependency logic treats them (`GATED`, not `DONE`).
343
- If the label is NOT terminal (e.g., `workflow:investigating`, `workflow:ready-to-build`, `workflow:building`, `workflow:in-review`), invoke `Skill(skill='work-on', args='{NUMBER}')` again immediately. The `/work-on` skill will re-read GitHub state and advance to the next phase. Do NOT output a summary, do NOT pause, do NOT ask for confirmation — just invoke it again.
438
+ If the label is NOT terminal (e.g., `workflow:investigating`, `workflow:ready-to-build`, `workflow:building`, `workflow:in-review`), invoke `Skill(skill='work-on', args='{NUMBER} --under-orchestration')` again immediately. The `/work-on` skill will re-read GitHub state and advance to the next phase. Do NOT output a summary, do NOT pause, do NOT ask for confirmation — just invoke it again.
344
439
 
345
440
  **CRITICAL — SOURCE BRANCH DETECTION**:
346
441
  - If the issue has the `review-finding` label, read the issue body for `**Code branch**: \`{branch}\``
@@ -374,7 +469,11 @@ if [ -n "$SYNTHESIS_BRIEF" ]; then
374
469
  ${SYNTHESIS_BRIEF}"
375
470
  else
376
471
  # Fallback: Phase 2.5 did not run (0/1 investigations). Use the raw gist behavior.
472
+ # Markdown emphasis markers (**bold**, __bold__, *italic*) are stripped before matching,
473
+ # since sub-issue bodies commonly render the label as "**Parent**: #NNN" and the bare
474
+ # label alternation below would otherwise fail to match past the emphasis characters.
377
475
  PARENT_INV=$(gh issue view {NUMBER} -R {GH_REPO} --json body --jq '.body' \
476
+ | sed -E 's/[*_]+//g' \
378
477
  | grep -oP '(?i)parent[: ]*#\K\d+|spawned from[: ]*#\K\d+' | head -1)
379
478
 
380
479
  if [ -n "$PARENT_INV" ] && [ -n "${INVESTIGATION_GISTS[$PARENT_INV]:-}" ]; then
@@ -446,6 +545,20 @@ fi
446
545
 
447
546
  If the issue has no parent reference, or the parent has no `FORGE:CONTRACT` annotation, this block produces no output and `GIST_CONTEXT` is unchanged. The hot-copy is an optimization — the durable annotation on the parent issue remains the authoritative record for compaction recovery.
448
547
 
548
+ **Same-file current-state brief injection** <!-- Added: forge#1860 --> — a separate, parallel mechanism to the `FORGE:SYNTHESIS_BRIEF` handling above: it does not replace or interact with Phase 2.5 investigation→implementation forwarding. When this issue was dispatched because a Layer 1/2/3 structural predecessor edge just resolved, Step 4B's core streaming dispatch loop has already populated `SAME_FILE_BRIEF[{NUMBER}]` with a short excerpt of what each such predecessor changed in the shared file/directory/module. Append it here:
549
+
550
+ ```bash
551
+ # Append same-file/directory/shared-module briefs from resolved structural-edge predecessors.
552
+ if [ -n "${SAME_FILE_BRIEF[{NUMBER}]:-}" ]; then
553
+ GIST_CONTEXT="${GIST_CONTEXT}
554
+
555
+ **SAME-FILE STATE BRIEF (structural DAG predecessor)**: One or more predecessors in this batch were serialized against you because they touch the same file/directory/module (Step 3C Layer 1/2/3 — not an explicit \`Depends on\`). Here is what they just changed — use this as your starting understanding of the file's current state; do not re-investigate it cold:
556
+ ${SAME_FILE_BRIEF[{NUMBER}]}"
557
+ fi
558
+ ```
559
+
560
+ This block is a no-op — `GIST_CONTEXT` resolves exactly as before — whenever `SAME_FILE_BRIEF[{NUMBER}]` is unset: the issue had no predecessors, its only predecessors were explicit-dependency/DATABASE-chain/Layer 4/Layer 5 edges (none of which populate `SAME_FILE_BRIEF`), or the orchestrator session was compacted and restarted between Phase 3 and this dispatch (in which case `EDGE_KIND`/`EDGE_FILES`/`SAME_FILE_BRIEF` are in-memory-only and are not reconstructed by the wake/compaction recovery in `phase-3-dependency.md` — the dispatched agent simply proceeds without the brief, exactly as it would have before this mechanism existed).
561
+
449
562
  **Capture agent IDs after the batch spawn (MANDATORY)**: Each `Agent(...)` call returns an agent ID. Store each returned ID in `AGENT_ISSUE_MAP` keyed by issue number. This map is the only way to resume a stalled agent by ID in Steps 4B and 4B.5:
450
563
 
451
564
  ```
@@ -455,12 +568,43 @@ AGENT_ISSUE_MAP[{NUMBER}] = <agent_id returned by Agent()>
455
568
 
456
569
  `AGENT_ISSUE_MAP` starts empty and accumulates entries as agents are spawned. For parallel dispatch (all Agent() calls in one message), capture the returned IDs from the batch response — one entry per issue — before entering Step 4B's monitoring loop. Without this capture, `resume=` calls in Steps 4B and 4B.5 will have no agent ID to reference and the resume will fail. <!-- Added: forge#1083 -->
457
570
 
458
- **Launch all ready agents simultaneously** by putting multiple Agent tool calls in a single message. Use `run_in_background=true` so they execute in parallel.
571
+ **Launch only up to the concurrency cap** (forge#1912) — never put more than `dispatch_headroom` (`MAX_CONCURRENT - ACTIVE_DISPATCH_COUNT`, from Step 4A-pre.0.2) Agent tool calls into a single message. Use `run_in_background=true` so the dispatched agents execute in parallel with each other, within the cap.
572
+
573
+ ```bash
574
+ # Compute this dispatch batch (forge#1912): merge any previously-deferred concurrency
575
+ # issues (oldest first, for fairness) with the current ready set, then take only as many
576
+ # as headroom allows. The remainder is re-queued, not dropped.
577
+ HEADROOM=$(dispatch_headroom)
578
+ CANDIDATES=("${DEFERRED_CONCURRENCY_ISSUES[@]}" {ready_issue_numbers})
579
+ DEFERRED_CONCURRENCY_ISSUES=()
580
+
581
+ DISPATCH_NOW=()
582
+ for NUM in "${CANDIDATES[@]}"; do
583
+ if [ "${#DISPATCH_NOW[@]}" -lt "$HEADROOM" ]; then
584
+ DISPATCH_NOW+=("$NUM")
585
+ else
586
+ DEFERRED_CONCURRENCY_ISSUES+=("$NUM")
587
+ fi
588
+ done
589
+
590
+ if [ "${#DEFERRED_CONCURRENCY_ISSUES[@]}" -gt 0 ]; then
591
+ echo "CONCURRENCY DEFER: ${#DEFERRED_CONCURRENCY_ISSUES[@]} ready issue(s) held back — ${ACTIVE_DISPATCH_COUNT}/${MAX_CONCURRENT} already in flight. Will dispatch as slots free up: ${DEFERRED_CONCURRENCY_ISSUES[*]}"
592
+ fi
593
+ echo "Dispatching ${#DISPATCH_NOW[@]} issue(s) this message (headroom was ${HEADROOM})"
594
+ ```
595
+
596
+ Spawn `Agent()` for every issue in `DISPATCH_NOW` (not the raw ready set) using the template above. After the batch spawn, increment `ACTIVE_DISPATCH_COUNT` by `${#DISPATCH_NOW[@]}` — this happens in addition to, not instead of, capturing each returned agent ID into `AGENT_ISSUE_MAP` above.
597
+
598
+ If `DISPATCH_NOW` is empty (headroom is 0), do not spawn any agents this cycle — wait for the next `agent_completed` notification, which frees a slot and re-triggers this dispatch computation (Step 4B).
459
599
 
460
600
  ### Step 4B: Monitor completions and dispatch newly ready issues
461
601
 
462
602
  You will be automatically notified when each background agent completes. **Do NOT use `sleep` loops to poll for completion.** Instead, wait for the automatic notification. When you receive a notification that an agent completed, immediately process it.
463
603
 
604
+ **Concurrency slot release (MANDATORY — first action on every completion)** <!-- Added: forge#1912 -->: The instant an `agent_completed` notification arrives, decrement `ACTIVE_DISPATCH_COUNT` by 1 — a worker slot has just freed, regardless of what terminal state the issue ended up in. Do this before any stall-recovery/resume logic below. If that agent is then resumed (item 2 below, or Step 4B.5's stall recovery) because it stalled mid-pipeline rather than truly finishing, re-increment `ACTIVE_DISPATCH_COUNT` when the `Agent(resume=...)` call is issued — a resume re-occupies a worker slot exactly like a fresh dispatch does.
605
+
606
+ **Ordering requirement (MANDATORY — prevents transient over-cap)**: For every agent that completed in this notification batch, finish its terminal-state check and, if applicable, its resume re-increment (items 1-2 below) BEFORE computing `dispatch_headroom` for item 5's newly-ready-issue dispatch. Computing headroom before a to-be-resumed agent's re-increment would let a genuinely-still-running agent's freed slot be double-booked — once by a fresh dispatch, once by the resume itself — transiently exceeding `MAX_CONCURRENT`. Process every completed agent's items 1-2 to a decision (terminal vs. resumed) first; only then compute headroom once for the batch's item 5 dispatch.
607
+
464
608
  **Successor dispatch latency is measured from `agent_completed`, not from orchestrator polling.** The moment you receive an `agent_completed` notification for issue N, that is t=0 for dispatching N's successors. Any successor whose predecessors are all now terminal MUST be dispatched in the same response that processes the notification — not after a poll cycle, not after a sleep. This is the design property that makes streaming DAG execution faster than wave-based execution. <!-- Added: forge#1251 -->
465
609
 
466
610
  **Predecessor Classification (DONE / GATED / FAILED)** <!-- Added: forge#1812 --> — every check in this file that asks "is predecessor X resolved enough for its dependents to proceed" MUST classify X into exactly one of three states below — never a single binary terminal/non-terminal grep. Earlier versions of this file independently patched `grep -qE 'workflow:merged|workflow:invalid|needs-human|CLOSED'` in multiple places, and the copies drifted: the readiness check (this step) treated `needs-human` as done-enough-to-dispatch-through, while the failure handler (item 6 below) treated the identical label as a hard failure that skips dependents. Both cannot be right at once — `needs-human` means the predecessor's code is paused pending a human decision and is NOT yet in the base branch, so dispatching a dependent against it is unsafe, but permanently skipping the dependent is also wrong once the human resolves the block. The fix is a third state:
@@ -501,6 +645,94 @@ classify_predecessor_state() {
501
645
 
502
646
  A GATED predecessor whose PR later merges reclassifies to DONE the next time `classify_predecessor_state` runs (its label flips to `workflow:merged`) — this is exactly what the merge-triggered wake check (item 6.6 below) relies on.
503
647
 
648
+ **File-overlap edge re-verification** <!-- Added: forge#1904 --> — `classify_predecessor_state()` answers "is the predecessor resolved enough to proceed," but it says nothing about whether a specific Layer 1/2/3 file-overlap edge (`EDGE_KIND`/`EDGE_FILES`, tagged in `phase-3-dependency.md` Step 3C/3D) was ever real. Both were built from a **pre-build guess** — the predecessor's `FORGE:INVESTIGATOR` "Affected Files" list, or a raw issue-body parse if no investigation existed yet. Once a predecessor reaches FAILED or GATED, that guess must be checked against ground truth (the predecessor's actual PR diff, or the absence of any PR) before the edge is allowed to keep blocking a dependent. Without this, a predecessor that reaches `needs-human` or `workflow:invalid` having never touched the guessed shared file (or never opened a PR at all) leaves its dependents gated/skipped on a conflict that never existed.
649
+
650
+ ```bash
651
+ verify_file_overlap_edge() {
652
+ local PRED="$1"
653
+ local DEP="$2"
654
+ local EDGE_TYPE="${EDGE_KIND["${PRED}:${DEP}"]:-}"
655
+
656
+ # Only Layer 1/2/3 structural edges (same-file / directory / shared-module, tagged in
657
+ # phase-3-dependency.md Step 3C) are eligible for this check. Explicit `Depends on`, the
658
+ # DATABASE domain chain, Layer 4 conservative-fallback edges, and Layer 5 co-change edges
659
+ # never populate EDGE_KIND for a pair — they encode a declared or historical dependency,
660
+ # not a guessed file list, and are NEVER dropped here.
661
+ if [ -z "$EDGE_TYPE" ]; then
662
+ echo "KEEP"
663
+ return
664
+ fi
665
+
666
+ # Resolve PRED's PR using the same anchored search already used elsewhere in this file
667
+ # (forge#1634/#1646/#1830 precedent — a bare "#${PRED}" substring grep would false-match
668
+ # #50/#500 for predecessor #5, so this always goes through gh's search query, never a
669
+ # hand-rolled grep against comment/PR body text).
670
+ #
671
+ # Exclude CLOSED-unmerged PRs (same precedent as the milestone-base-consistency check
672
+ # elsewhere in this file: "a closed-but-not-merged PR is a superseded/abandoned routing
673
+ # attempt and does NOT reflect the live lane"). For a FAILED (workflow:invalid) or GATED
674
+ # (needs-human/workflow:awaiting-merge) predecessor this can only ever surface an OPEN PR —
675
+ # a MERGED PR would already classify the predecessor DONE, not FAILED/GATED — so this filter
676
+ # also guarantees `gh pr diff` below targets a live, still-open branch rather than an
677
+ # abandoned PR whose branch may since have been force-deleted (e.g. by /cleanup).
678
+ local PRED_PR
679
+ PRED_PR=$(gh pr list -R {GH_REPO} --state all --search "\"Closes #${PRED}\" in:body" \
680
+ --json number,state --jq '[.[] | select(.state != "CLOSED")][0].number // empty' 2>/dev/null || echo "")
681
+
682
+ if [ -z "$PRED_PR" ]; then
683
+ # Predecessor reached a terminal/gated state having never opened a PR (or only ever had
684
+ # an abandoned/closed-unmerged one) — there is no live code that could possibly conflict
685
+ # with DEP's files. The edge was based purely on the pre-build guess and never materialized.
686
+ echo "DROP"
687
+ return
688
+ fi
689
+
690
+ # A PR exists — compare the file(s) that actually triggered this edge (EDGE_FILES, set at
691
+ # Layer 1/2/3 in phase-3-dependency.md) against the PR's real changed-file list.
692
+ local ACTUAL_FILES
693
+ local DIFF_EXIT
694
+ ACTUAL_FILES=$(gh pr diff "$PRED_PR" -R {GH_REPO} --name-only 2>/dev/null)
695
+ DIFF_EXIT=$?
696
+ if [ "$DIFF_EXIT" -ne 0 ]; then
697
+ # Fail-safe: could not fetch the actual diff (transient API error, rate limit, etc.) —
698
+ # an empty result here must NOT be conflated with "confirmed no overlap." Keep the edge
699
+ # blocking rather than risk dropping a real conflict on a fetch failure.
700
+ echo "KEEP"
701
+ return
702
+ fi
703
+ local EDGE_FILE_LIST="${EDGE_FILES["${PRED}:${DEP}"]:-}"
704
+
705
+ local OVERLAP_FOUND=false
706
+ for EF in $EDGE_FILE_LIST; do
707
+ [ -z "$EF" ] && continue
708
+ # Strip a trailing slash (Layer 2 directory-type edges store a bare directory path) and
709
+ # escape regex metacharacters, then anchor the match to a path-component boundary. A raw
710
+ # `grep -qF` substring test would let a short directory name like "utils" false-match an
711
+ # unrelated path such as "some_utils_helper.py" or "shared_utils/" — anchoring on `/`
712
+ # boundaries requires the shared file/directory to appear as a whole path segment.
713
+ EF_CLEAN="${EF%/}"
714
+ EF_ESCAPED=$(printf '%s' "$EF_CLEAN" | sed 's/[.[\*^$()+?{|]/\\&/g')
715
+ if echo "$ACTUAL_FILES" | grep -qE "(^|/)${EF_ESCAPED}(/|$)"; then
716
+ OVERLAP_FOUND=true
717
+ break
718
+ fi
719
+ done
720
+
721
+ if [ "$OVERLAP_FOUND" = "true" ]; then
722
+ echo "KEEP" # PR's actual diff confirms the shared file was really touched — real conflict
723
+ else
724
+ echo "DROP" # PR exists but its actual diff never touched the guessed shared file(s) —
725
+ # the Layer 1/2/3 guess was wrong for this predecessor; the edge is spurious
726
+ fi
727
+ }
728
+ ```
729
+
730
+ **Call sites**: item 6 (FAILED handling, below) and item 6.5 (GATED handling, below) both call this helper — once per `EDGE_KIND`-tagged predecessor edge — before cascading a skip or tracking `blocked-on-human-merge`. `phase-3-dependency.md`'s wake/compaction reconstruction block calls the identical logic (mirrored verbatim, not re-derived) for the case where the predecessor resolves after the orchestrator session has already ended — see that file's "Orchestrator state reconstruction on wake / after compaction" section. This mirroring is deliberate: keeping the check in exactly one function definition, called (not reimplemented) from both files, avoids the same drift class forge#1812 had to fix once already for DONE/GATED/FAILED classification itself.
731
+
732
+ **Recursive cascade note (item 6 only)**: when a dependent `DEP` is itself marked "skipped — dependency failed" (edge confirmed `KEEP`, not dropped), `DEP` becomes the new FAILED anchor for its own direct dependents — re-run this same `verify_file_overlap_edge` check at that next hop too, rather than assuming every transitive descendant is unconditionally skipped. A grandchild dependent whose only path to the failure runs through a spurious (droppable) edge one hop down must not be skipped just because its parent was.
733
+
734
+ **Never called from**: the DONE branch (that predecessor's code is already merged — the edge, if real, is already resolved) or the `IN_PROGRESS` branch (nothing to re-verify yet — the predecessor hasn't concluded).
735
+
504
736
  **Core streaming dispatch loop**: After processing each agent completion, check the DAG for newly unblocked issues. If any issue now has all predecessors classified `DONE`, dispatch it immediately (run Steps 4A.pre.0, 4A.pre, and 4A for the newly ready issues). This is the key difference from the wave model — issues dispatch as soon as their specific predecessors complete, not after an entire group finishes.
505
737
 
506
738
  ```bash
@@ -512,7 +744,50 @@ for BLOCKED_NUM in {all_blocked_issue_numbers}; do
512
744
  for PRED in {predecessors_of_BLOCKED_NUM}; do
513
745
  PRED_STATE=$(classify_predecessor_state "$PRED")
514
746
  case "$PRED_STATE" in
515
- DONE) ;; # satisfied — no action
747
+ DONE)
748
+ # Same-file current-state brief forwarding (forge#1860): only fires when the
749
+ # RESOLVED edge from this specific PRED to this specific BLOCKED_NUM is a
750
+ # Layer 1/2/3 structural edge (EDGE_KIND set to same-file/directory/shared-module
751
+ # in phase-3-dependency.md Step 3C) — never for explicit `Depends on`, the
752
+ # DATABASE domain chain, Layer 4 conservative-fallback, or Layer 5 co-change edges,
753
+ # since those never populate EDGE_KIND for this pair.
754
+ EDGE_TYPE="${EDGE_KIND["${PRED}:${BLOCKED_NUM}"]:-}"
755
+ if [ -n "$EDGE_TYPE" ] && [ -z "${EDGE_BRIEFED["${PRED}:${BLOCKED_NUM}"]:-}" ]; then
756
+ EDGE_BRIEFED["${PRED}:${BLOCKED_NUM}"]=1
757
+ SHARED_FILE="${EDGE_FILES["${PRED}:${BLOCKED_NUM}"]:-}"
758
+
759
+ # Prefer FORGE:BUILDER (the concrete "what changed" record). Fall back to
760
+ # FORGE:ARCHITECT, then FORGE:INVESTIGATOR, for predecessors that never reached
761
+ # implementation (e.g. closed workflow:invalid, or resolved via Phase 2 decomposition).
762
+ BRIEF_SRC=$(gh api repos/{GH_REPO}/issues/${PRED}/comments \
763
+ --jq '[.[] | select(.body | contains("FORGE:BUILDER"))] | last | .body // ""' 2>/dev/null)
764
+ BRIEF_SRC_LABEL="FORGE:BUILDER"
765
+ if [ -z "$BRIEF_SRC" ]; then
766
+ BRIEF_SRC=$(gh api repos/{GH_REPO}/issues/${PRED}/comments \
767
+ --jq '[.[] | select(.body | contains("FORGE:ARCHITECT"))] | last | .body // ""' 2>/dev/null)
768
+ BRIEF_SRC_LABEL="FORGE:ARCHITECT"
769
+ fi
770
+ if [ -z "$BRIEF_SRC" ]; then
771
+ BRIEF_SRC=$(gh api repos/{GH_REPO}/issues/${PRED}/comments \
772
+ --jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR"))] | last | .body // ""' 2>/dev/null)
773
+ BRIEF_SRC_LABEL="FORGE:INVESTIGATOR"
774
+ fi
775
+
776
+ if [ -n "$BRIEF_SRC" ]; then
777
+ # Scope the excerpt to the shared file — a few lines, never a raw diff or
778
+ # full comment dump. Fall back to the comment's Approach/Changes section if
779
+ # the file's basename doesn't literally appear (e.g. a directory-level edge).
780
+ FIRST_SHARED_FILE=$(echo "$SHARED_FILE" | awk '{print $1}')
781
+ EXCERPT=""
782
+ if [ -n "$FIRST_SHARED_FILE" ]; then
783
+ EXCERPT=$(echo "$BRIEF_SRC" | grep -iF "$(basename "$FIRST_SHARED_FILE" 2>/dev/null)" | head -3)
784
+ fi
785
+ [ -z "$EXCERPT" ] && EXCERPT=$(echo "$BRIEF_SRC" | sed -n '/### Approach/,/^### /p' | head -6)
786
+ SAME_FILE_BRIEF["$BLOCKED_NUM"]="${SAME_FILE_BRIEF["$BLOCKED_NUM"]:-}
787
+ - **#${PRED}** (${EDGE_TYPE} edge, from its ${BRIEF_SRC_LABEL} comment, \`${SHARED_FILE:-shared file}\`): ${EXCERPT:-see #${PRED}'s ${BRIEF_SRC_LABEL} comment for details}"
788
+ fi
789
+ fi
790
+ ;; # satisfied — no dispatch-readiness action beyond the brief above
516
791
  FAILED) ALL_PREDS_DONE=false ;; # handled by item 6 (skip dependents)
517
792
  GATED)
518
793
  ALL_PREDS_DONE=false
@@ -530,7 +805,11 @@ for BLOCKED_NUM in {all_blocked_issue_numbers}; do
530
805
  # Do NOT dispatch. Do NOT mark skipped. Tracked via item 6.5.
531
806
  fi
532
807
  done
533
- # Run Steps 4A.pre.0 → 4A.pre → 4A for newly ready issues (batch them in a single message)
808
+ # Run Steps 4A.pre.0 → 4A.pre → 4A for newly ready issues. Step 4A's own dispatch-batch
809
+ # computation (the HEADROOM/DISPATCH_NOW logic, forge#1912) is what actually caps this —
810
+ # "newly ready" here just means "eligible," not "guaranteed to dispatch this cycle." Any
811
+ # issue that doesn't fit current headroom lands in DEFERRED_CONCURRENCY_ISSUES and is
812
+ # retried automatically on the next completion, same as every other concurrency-deferred issue.
534
813
  ```
535
814
 
536
815
  **CRITICAL — Stall detection and recovery**: Background agents sometimes stop mid-pipeline (`stop_reason=end_turn`) after completing a sub-phase (e.g., investigation completes but build never starts). This causes the agent to "complete" from the Agent tool's perspective even though the `/work-on` pipeline is only partially done. When you receive a completion notification:
@@ -552,10 +831,10 @@ done
552
831
  resume=AGENT_ISSUE_MAP[{NUMBER}],
553
832
  description="Resume #{NUMBER} pipeline",
554
833
  run_in_background=true,
555
- prompt="The previous /work-on invocation stopped before completing the full pipeline. The issue is currently at {CURRENT_WORKFLOW_STATE}. Continue — invoke Skill(skill='work-on', args='{NUMBER}') to resume the routing loop from the current state. /work-on will re-read GitHub state and pick up where it left off."
834
+ prompt="The previous /work-on invocation stopped before completing the full pipeline. The issue is currently at {CURRENT_WORKFLOW_STATE}. Continue — invoke Skill(skill='work-on', args='{NUMBER} --under-orchestration') to resume the routing loop from the current state. /work-on will re-read GitHub state and pick up where it left off."
556
835
  )
557
836
  ```
558
- **Resume ALL stalled agents in a single message** (parallel resume). Do not wait between resumes.
837
+ **Resume ALL stalled agents in a single message** (parallel resume). Do not wait between resumes. Each resume re-occupies a worker slot — increment `ACTIVE_DISPATCH_COUNT` by 1 per agent resumed here (it was already decremented by the "Concurrency slot release" rule above when the stall was first observed as a completion). <!-- Added: forge#1912 -->
559
838
 
560
839
  3. **Track resume cycles per agent.** If an agent has been resumed 2+ times and still hasn't reached a terminal state, report it as a failure — do not resume again.
561
840
 
@@ -601,11 +880,78 @@ done
601
880
 
602
881
  5. **Record completed results**: Success (PR merged), Invalid (issue closed), Blocked (needs human), Awaiting-merge (`workflow:awaiting-merge` — remediated + re-reviewed to APPROVED after an earlier `needs-human` escalation; needs only a human merge, not diagnosis — keep distinct from Blocked in any status output, see item 8), or Error
603
882
 
604
- 5. **Check for newly unblocked issues** — run the DAG readiness check above. If any issues are now ready, dispatch them immediately (Steps 4A.pre.0 → 4A.pre → 4A). Batch all newly ready issues into a single dispatch message.
883
+ 5. **Check for newly unblocked issues** — run the DAG readiness check above. If any issues are now ready, run Steps 4A.pre.0 → 4A.pre → 4A for them. **Step 4A's own headroom computation is the cap** (forge#1912) — "batch all newly ready issues into a single dispatch message" means one message covers `DISPATCH_NOW` (ready issues that fit current headroom), not necessarily every newly-ready issue; any that don't fit go to `DEFERRED_CONCURRENCY_ISSUES` and retry on the next completion.
884
+
885
+ 6. **Handle predecessor failures** — if a completed agent's issue classifies as `FAILED` (`workflow:invalid`, or an explicit build/test error — see Predecessor Classification above; `needs-human` and `workflow:awaiting-merge` are GATED, not FAILED — see item 6.5), check for dependent issues in the DAG.
886
+
887
+ **Edge re-verification before cascading the skip** <!-- Added: forge#1904 -->: For each direct dependent `DEP` of the FAILED predecessor `PRED`, call `verify_file_overlap_edge "$PRED" "$DEP"` (defined above, alongside `classify_predecessor_state`). If the pair has no `EDGE_KIND` entry (explicit dependency, DATABASE chain, Layer 4/5 edge), the helper returns `KEEP` immediately and behavior is unchanged — cascade the skip as before. If the pair IS an `EDGE_KIND` edge (Layer 1/2/3) and the helper returns `DROP` (FAILED predecessor never opened a PR, or its PR's actual diff never touched the guessed shared file), do NOT cascade the skip through that specific predecessor for that specific dependent — remove `PRED` from `DEP`'s predecessor set instead. If `DEP` has other still-unresolved predecessors it continues waiting on them normally; if `PRED` was `DEP`'s only predecessor, `DEP` becomes immediately ready (run Steps 4A.pre.0 → 4A.pre → 4A for it this cycle). Only when the helper returns `KEEP` (or there is no `EDGE_KIND` entry) does the "skipped — dependency #{X} failed" outcome apply, unchanged from prior behavior.
888
+
889
+ Report every dependent whose skip was avoided this way — e.g. "#{DEP} — predecessor #{PRED} failed but never touched the shared file(s); edge dropped, #{DEP} not skipped." For all remaining dependents (real `EDGE_KIND` overlap confirmed, or a non-`EDGE_KIND` edge type), mark them "skipped — dependency #{X} failed" and report them. Do NOT dispatch them.
890
+
891
+ 6.4. **Auto-dispatch remediation against a `needs-human`-gated predecessor's own PR** <!-- Added: forge#1813 --> — item 6.5 below tracks the *dependents* of a `GATED` predecessor; this item handles the predecessor's own PR, which item 6.5/6.6 never re-drive on their own. Run this check whenever a completed agent's issue classifies `GATED` **specifically via `needs-human`** — NOT `workflow:awaiting-merge`. That second state already means "remediated and re-reviewed to a clean verdict, just needs a human's merge click" (see forge#1810's guard) — dispatching remediation again would be redundant, not just wasteful, since there is nothing left to fix.
892
+
893
+ ```bash
894
+ PRED_CURRENT_LABEL=$(gh issue view "$PRED" -R {GH_REPO} --json labels \
895
+ --jq '[.labels[].name | select(. == "needs-human" or . == "workflow:awaiting-merge")] | .[0] // empty' 2>/dev/null)
896
+
897
+ if [ "$PRED_CURRENT_LABEL" = "needs-human" ]; then
898
+ # Resolve PRED's open PR using the anchored search (forge#1634/#1646 precedent —
899
+ # never a bare-number search, which would misattribute an unrelated PR).
900
+ GATING_PR=$(gh pr list -R {GH_REPO} --state open --search "\"Closes #${PRED}\" in:body" \
901
+ --json number --jq '.[0].number // empty' 2>/dev/null || echo "")
902
+
903
+ if [ -n "$GATING_PR" ]; then
904
+ # Idempotency guard: only one remediation attempt per PR, ever (single-attempt
905
+ # semantics — remediate.md's own Phase M0 enforces this too, but checking here
906
+ # avoids spawning a redundant agent that would immediately no-op on entry).
907
+ ALREADY_REMEDIATED=$(gh api repos/{GH_REPO}/issues/${GATING_PR}/comments \
908
+ --jq '[.[] | select(.body | contains("FORGE:REMEDIATION"))] | length' 2>/dev/null || echo "0")
909
+
910
+ if [ "$ALREADY_REMEDIATED" -eq 0 ]; then
911
+ echo "Dispatching remediation for #{PRED}'s gating PR #{GATING_PR} (needs-human)"
912
+ # Same Agent-spawn-fallback style as Step 4A's template — one background agent,
913
+ # whose sole job is to invoke /work-on in remediation mode and let it run to
914
+ # completion (AUTO-LANDED, HELD-AWAITING-MERGE, RE-ESCALATED, or UNFIXABLE — all
915
+ # are terminal-for-this-agent; see work-on/remediate.md Output).
916
+ Agent(
917
+ subagent_type="general-purpose",
918
+ model="{SUBAGENT_MODEL}",
919
+ description="Remediate PR #{GATING_PR} (needs-human, blocks #{PRED})",
920
+ run_in_background=true,
921
+ prompt="You are remediating GitHub PR #{GATING_PR} for the {PROJECT_NAME} project (repo: {GH_REPO}), which is currently held at `needs-human` on its linked issue #{PRED}.
922
+
923
+ **YOUR MISSION**: Invoke `Skill(skill='work-on', args='{GATING_PR} --remediate --issue {PRED} --repo {GH_REPO} --gh-flag {GH_FLAG}')` and let it run to completion. This is a self-contained flow: it checks out the PR branch, fixes any fixable review findings, re-reviews, and either auto-lands the PR, holds it at `workflow:awaiting-merge` for a human, re-escalates back to `needs-human`, or reports the block as policy-level and unfixable. Do NOT intervene manually — do not run raw git/gh commands yourself.
924
+
925
+ **DO NOT STOP EARLY**: if the Skill call returns without a terminal `REMEDIATE_RESULT.status`, invoke it again — it re-reads GitHub state and resumes. Terminal statuses are: `COMPLETE`, `ALREADY_DONE`, `UNFIXABLE`, `BLOCKED`.
926
+
927
+ Do not ask the user questions — you are running autonomously in the background."
928
+ )
929
+ fi
930
+ fi
931
+ fi
932
+ ```
605
933
 
606
- 6. **Handle predecessor failures** — if a completed agent's issue classifies as `FAILED` (`workflow:invalid`, or an explicit build/test error — see Predecessor Classification above; `needs-human` and `workflow:awaiting-merge` are GATED, not FAILED — see item 6.5), check for dependent issues in the DAG. Mark all transitive dependents as "skipped — dependency #{X} failed" and report them. Do NOT dispatch them.
934
+ This satisfies #1809 Q2 (the orchestrator auto-dispatches remediation against the gated predecessor itself — the exact gap forge#1812's item 6.5/6.6 left open, since those items only ever track and wake *dependents*, never the gated PR's own remediation). The remediation agent's outcome is picked up on the **next** completion-monitoring cycle of this same Step 4B loop: if it lands (`workflow:merged`), item 6.6 below fires normally and wakes any `blocked-on-human-merge` dependents; if it holds/re-escalates, the predecessor simply remains `GATED` and item 6.5 continues tracking its dependents unchanged.
607
935
 
608
936
  6.5. **Handle predecessor gating** (`GATED` — `needs-human` or `workflow:awaiting-merge`) <!-- Added: forge#1812 --> — if a completed agent's issue classifies as `GATED`, its direct dependents are neither dispatched nor marked failed/skipped. For each direct dependent `DEP` of the gated predecessor `PRED`:
937
+
938
+ **Edge re-verification gate (run FIRST, before any tracking)** <!-- Added: forge#1904 -->:
939
+ ```bash
940
+ EDGE_VERDICT=$(verify_file_overlap_edge "$PRED" "$DEP")
941
+ if [ "$EDGE_VERDICT" = "DROP" ]; then
942
+ echo "#{DEP} — predecessor #{PRED} is GATED but its EDGE_KIND file-overlap edge does not hold (no PR ever opened, or its actual diff never touched the guessed shared file(s)). Dropping the edge — not tracking #{DEP} as blocked-on-human-merge for this predecessor."
943
+ # Remove PRED from DEP's predecessor set. If DEP has other unresolved predecessors it
944
+ # keeps waiting on those normally. If PRED was DEP's only predecessor, DEP is immediately
945
+ # ready — run Steps 4A.pre.0 → 4A.pre → 4A for it this cycle (same as any other newly
946
+ # ready issue in the core streaming dispatch loop above).
947
+ continue # skip the tracking block below entirely for this (PRED, DEP) pair
948
+ fi
949
+ # EDGE_VERDICT = KEEP (real EDGE_KIND overlap confirmed, or no EDGE_KIND entry at all —
950
+ # explicit dependency / DATABASE chain / Layer 4-5 edges always fall through to KEEP and
951
+ # proceed with tracking exactly as before this fix).
952
+ ```
953
+
954
+ **Existing tracking logic (unchanged for the KEEP case)**:
609
955
  ```bash
610
956
  # Resolve PRED's open PR, if any, using the anchored search (forge#1634/#1646 precedent —
611
957
  # do NOT fall back to a bare-number search here; a stale unrelated PR would misattribute gating).
@@ -654,6 +1000,56 @@ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was
654
1000
  ```
655
1001
  This satisfies the live-session case. For the case where the gating PR merges after the orchestrator session has already ended, the equivalent check runs in `phase-3-dependency.md`'s wake/compaction reconstruction on the next `/orchestrate` invocation — see that file's "Orchestrator state reconstruction on wake / after compaction" section.
656
1002
 
1003
+ 6.7. **Human-gated idle/backpressure check** (`BATCH_FULLY_GATED`) <!-- Added: forge#1814 --> — run this after every completion cycle, once the per-issue classification above (items 5-6.6) has been applied for this cycle. It answers a different question than the paused-drain/blocked-on-human-merge tracking above: those items handle *individual* gated predecessors and their *direct* dependents; this check asks whether the **entire original batch** has now exhausted into human-gated states, which is the condition under which continuing to dispatch cascade-spawned review findings (Step 4C) produces net-negative churn — closing 1 issue while opening 2-4 more, with the real blockers (the GATED issues) unresolved:
1004
+
1005
+ ```bash
1006
+ # BATCH_FULLY_GATED is computed over {all_batch_issue_numbers} — the ORIGINAL batch issues
1007
+ # this /orchestrate invocation was given, NOT cascade-spawned review-finding issues (those are
1008
+ # a separate, currently-unbounded-looking stream that this check exists to cap). Cascade
1009
+ # findings are excluded here because they are the SYMPTOM (Step 4C keeps producing them);
1010
+ # counting them as "still IN_PROGRESS" would make this check permanently false and defeat
1011
+ # its own purpose.
1012
+ ANY_ORIGINAL_IN_PROGRESS=false
1013
+ ANY_ORIGINAL_GATED=false
1014
+ for ORIG_NUM in {all_batch_issue_numbers}; do
1015
+ ORIG_STATE=$(classify_predecessor_state "$ORIG_NUM")
1016
+ case "$ORIG_STATE" in
1017
+ IN_PROGRESS) ANY_ORIGINAL_IN_PROGRESS=true ;;
1018
+ GATED) ANY_ORIGINAL_GATED=true ;;
1019
+ DONE|FAILED) ;; # exhausted — no action
1020
+ esac
1021
+ done
1022
+
1023
+ if [ "$ANY_ORIGINAL_IN_PROGRESS" = "false" ] && [ "$ANY_ORIGINAL_GATED" = "true" ]; then
1024
+ BATCH_FULLY_GATED=true
1025
+ else
1026
+ BATCH_FULLY_GATED=false
1027
+ fi
1028
+ ```
1029
+
1030
+ - **`BATCH_FULLY_GATED=true`** requires BOTH: no original-batch issue is still `IN_PROGRESS` (i.e. nothing from the original scope will complete on its own without a human), AND at least one original-batch issue is `GATED` (`needs-human`/`workflow:awaiting-merge`, or a dependent already tracked `blocked-on-human-merge`). A batch that finishes entirely `DONE`/`FAILED` with zero `GATED` issues is NOT idle — it is simply complete; do not confuse the two.
1031
+ - **This is a live, recomputed flag, not a one-way latch.** Re-run it every completion cycle. If a gating PR merges (item 6.6 fires) and unblocks an original-batch dependent that becomes dispatchable again, `ANY_ORIGINAL_IN_PROGRESS` flips back to `true` on the next cycle and `BATCH_FULLY_GATED` flips back to `false` — normal dispatch resumes automatically. This is what prevents a permanent idle state and satisfies "no regression: when productive non-gated work remains, the orchestrator continues normally."
1032
+ - **Effect when true**: Step 4C's cascade-finding dispatch (the "For queued (non-deferred) findings" block) is suppressed — see the `BATCH_FULLY_GATED` check added there. The first time the flag flips from `false`/unset to `true` in a completion cycle, print the idle report below and stop actively dispatching new cascade work; the batch remains resumable exactly as item 6.6 and `phase-3-dependency.md`'s wake reconstruction already guarantee.
1033
+
1034
+ **Idle report** (print once, the cycle `BATCH_FULLY_GATED` first becomes true):
1035
+ ```
1036
+ ⏸ Orchestrator Idle — Waiting on N Merge(s)
1037
+
1038
+ The remaining batch is fully human-gated: every original issue is either merged/invalid, or
1039
+ blocked on a human decision/merge. No further autonomous progress is possible until one of the
1040
+ PRs below is merged. Newly-spawned review-finding issues are being deferred (not dispatched) so
1041
+ the open-issue count does not inflate while nothing productive can close.
1042
+
1043
+ {reuse the Merge-Ready table computation from phase-6-report.md Step 6A.5 (MERGE_READY_PRS) and
1044
+ the Blocked-on-Merge table from Step 6A.6 (BLOCKED_ON_MERGE) — both already anchor their PR
1045
+ lookups on "Closes #N" in:body per forge#1634/#1646/#1822, so this reuses that logic verbatim
1046
+ rather than re-implementing a parallel PR-resolution path}
1047
+
1048
+ Findings deferred (idle policy): {count of newly-queued findings deferred this cycle}
1049
+ ```
1050
+
1051
+ This report is an interim, in-progress print — it does NOT replace the final consolidated report from Phase 6, which runs once the session actually ends or the next `/orchestrate` invocation picks the batch back up; see `phase-6-report.md` Step 6B for the corresponding "Orchestration Paused — Idle" header.
1052
+
657
1053
  7. **Verify pipeline compliance** — for each truly completed issue, check that the agent used `/work-on`:
658
1054
  ```bash
659
1055
  LABELS=$(gh issue view $NUM -R {GH_REPO} --json labels --jq '[.labels[].name | select(startswith("workflow:"))] | length')
@@ -664,7 +1060,9 @@ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was
664
1060
  ```
665
1061
  If an agent bypassed the pipeline, report it as a **failure** regardless of whether a PR exists.
666
1062
 
667
- 8. **Post a brief status update** to the user after each agent reaches terminal state:
1063
+ 8. **Post a status update** to the user after each agent reaches terminal state. Format is gated by `{NARRATION_MODE}` (resolved in `config.md` from `pipeline.narration`, default `"terse"`):
1064
+
1065
+ **`terse` (default)** — one line per completion, no table, nothing repeated between completions:
668
1066
  ```
669
1067
  ✓ #{NUMBER} — {title} → PR #{PR} merged to {target}
670
1068
  ✗ #{NUMBER} — {title} → {reason for failure}
@@ -675,6 +1073,17 @@ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was
675
1073
  → Dispatched #{NEWLY_READY} (predecessor #{PRED} completed)
676
1074
  ```
677
1075
 
1076
+ **`verbose`** — same one-liners, plus a running per-completion recap table (title, PR, target, elapsed):
1077
+ ```
1078
+ ✓ #{NUMBER} — {title} → PR #{PR} merged to {target}
1079
+
1080
+ | # | Title | PR | Target | Elapsed |
1081
+ |---|-------|----|--------|---------|
1082
+ {one row per issue completed so far in this batch}
1083
+ ```
1084
+
1085
+ **This gate is cosmetic only** — it changes what prints to the terminal between completions, never which phases run or what gets committed. Step 6B's full tables (Implementation Results, Review-Spawned Issues, Batch Trajectory Analytics) always render once at the end regardless of `{NARRATION_MODE}`; terse mode just skips the redundant per-completion recap, since Step 6B already aggregates everything.
1086
+
678
1087
  `⏸` (awaiting-merge) is deliberately distinct from `⚠` (blocked/bypass) — do not collapse the
679
1088
  two. `⚠` means the pipeline hit something it cannot resolve and a human must diagnose it;
680
1089
  `⏸` means the PR already cleared re-review and only needs a merge click. `🔗` (blocked-on-human-merge,
@@ -686,6 +1095,10 @@ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was
686
1095
 
687
1096
  **Termination condition**: All issues in the DAG have reached `DONE` or `FAILED` (merged, invalid, or skipped due to dependency failure) — OR are `blocked-on-human-merge` (item 6.5) with no further dispatchable work remaining in the batch. These two outcomes are reported differently: a batch where every issue is `DONE`/`FAILED` is a **clean drain**; a batch where one or more issues remain `blocked-on-human-merge` is a **paused drain** — the active dispatch loop stops (there is nothing left to do until a human merges a gating PR) but this MUST be reported as paused, not as fully complete (see `phase-6-report.md`'s `🔗 Blocked-on-Merge` section). `needs-human` predecessors with no open PR are neither — they remain GATED indefinitely until either a PR appears (dependent moves to blocked-on-human-merge) or the predecessor itself resolves; do not treat isolated `needs-human` issues with no dependents as blocking termination. When either drain condition is met, check whether deferred review-spawned findings exist (accumulated in `DEFERRED_FINDINGS` during Step 4C). If deferred findings exist → proceed to Step 4F (Completion Sweep). If no deferred findings → proceed to Phase 5.
688
1097
 
1098
+ **Relationship to `DEFERRED_CONCURRENCY_ISSUES` (forge#1912)**: A non-empty `DEFERRED_CONCURRENCY_ISSUES[]` never satisfies either termination condition above — it means dispatchable work exists but is temporarily held back by the concurrency cap, not that the DAG has stalled. Unlike `blocked-on-human-merge`, this is never reported as a "paused drain" requiring human action — it self-resolves automatically the moment any in-flight agent completes and frees a slot (Step 4A's `dispatch_headroom` recomputes every cycle). Only treat the DAG as drained once `DEFERRED_CONCURRENCY_ISSUES` is also empty.
1099
+
1100
+ **Relationship to `BATCH_FULLY_GATED` (item 6.7, forge#1814)**: A paused drain (above) describes the *original batch DAG* reaching a stable, non-progressing state. `BATCH_FULLY_GATED` is the mechanism that keeps that stable state from being masked by cascade churn — without it, Step 4C would keep dispatching new review-finding issues indefinitely (each with its own predecessors/dependents), so the DAG would never actually look "drained" even though the original batch's productive work stopped the moment the last non-gated issue completed. Once `BATCH_FULLY_GATED` is true, Step 4C stops adding new dispatchable work (rule 0 defers all newly-spawned findings), which lets the batch actually reach the paused-drain termination condition above instead of chasing an ever-growing cascade tail. Report this state using the idle report from item 6.7, not a plain "waiting for agents" message — the whole point is to make the pause visible and actionable (which PR(s) to merge), not silent.
1101
+
689
1102
  **Anti-pattern — DO NOT DO THIS:**
690
1103
  - `sleep 60/120/180/300` loops to check status — you will be notified automatically
691
1104
  - Spawning separate "progress check" agents — they waste tokens and add noise
@@ -805,16 +1218,18 @@ gh issue list -R {GH_REPO} --state open --label "review-finding" --limit 20 \
805
1218
  For each spawned finding, determine whether it should be **executed** or **deferred**:
806
1219
 
807
1220
  **Evaluation order** (first matching rule wins):
1221
+ 0. **Batch fully human-gated** (`BATCH_FULLY_GATED == true`, always defer, even for P1/P2) <!-- Added: forge#1814 -->: The original batch (see Step 4B item 6.7) has exhausted into DONE/FAILED/GATED with nothing left `IN_PROGRESS` — the real blockers are the GATED issues, not a lack of dispatchable findings. Dispatching a new review-finding here cannot produce net batch progress; it only inflates the open-issue count while the productive path waits on a human merge. Always defer, checked before generation and priority. Rationale: this is the idle/backpressure policy this issue adds — without it, rule 2 (below) unconditionally executes P1/P2 findings regardless of how gated the rest of the batch is, which is the root cause of the net-negative churn this policy exists to stop.
808
1222
  1. **Generation ≥ 2** (always defer, even for P1/P2): Finding was spawned by an issue that was itself a review-finding. Check the source issue's labels for `review-finding` — if the source has that label, the new finding is generation 2. Always defer. Rationale: gen-2+ cascade is theoretically unbounded — cap it here.
809
1223
  2. **Priority override** (P1 or P2 → always execute): If the finding is labeled P1 or P2, skip all remaining heuristics and execute. Rationale: high-priority findings must never be suppressed by keyword matching.
810
1224
  3. **Comment/typo heuristic** (P3 and below only): Finding title contains the word "comment" or "typo" (case-insensitive). These are 1-line cosmetic fixes that do not block other work.
811
1225
  4. **P3 + same-file overlap**: Finding is labeled `P3` AND the file it targets overlaps with ANY file already in the current batch (active or queued in the DAG). Rationale: same-file P3 findings add predecessor edges that serialize agents — one finding per original issue increases wall-clock time with no proportional value.
1226
+ 5. **Per-batch token budget** (P3 and below only, applied AFTER surface-area batching below — see "Per-batch token budget gate") <!-- Added: forge#1858 -->: Once `BATCH_TOKEN_SPEND` would exceed `TOKEN_BUDGET`, additional P3-and-below units (an unclubbed finding, or an already-clubbed batch issue) defer rather than dispatch. This is NOT part of the per-finding rule 0-4 chain immediately below — it is a quantity gate applied to the POST-clubbing `QUEUED_FINDINGS` list, so a same-run surface-area batch issue (surface-area batching below) is charged once for the whole cluster, not once per member. P1/P2 are NEVER gated by it (they are excluded by rule 2 before reaching this gate, same as they are excluded from rules 3-4).
812
1227
 
813
- **Defer** (do NOT add to the DAG) if rules 1, 3, or 4 match.
1228
+ **Defer** (do NOT add to the DAG) if rules 0, 1, 3, 4, or 5 match.
814
1229
 
815
1230
  **Execute** (add to the DAG) if:
816
- - Rule 2 matches (P1 or P2)
817
- - None of the defer rules matched (generation 1, P3 with no file overlap, not a keyword match)
1231
+ - Rule 2 matches (P1 or P2) — AND rule 0 did not already match (rule 0 is checked first and overrides rule 2)
1232
+ - None of the defer rules matched (generation 1, P3 with no file overlap, not a keyword match, batch not fully gated) AND rule 5's token budget still has headroom for this unit
818
1233
 
819
1234
  **Before running the loop, build the batch file list (MANDATORY for Heuristic 3):**
820
1235
 
@@ -850,8 +1265,13 @@ for FINDING_NUM in {spawned_finding_numbers}; do
850
1265
  PRIORITY=$(echo "$FINDING_DATA" | jq -r '.labels[] | select(startswith("priority:P")) | ltrimstr("priority:")' | head -1)
851
1266
  TITLE=$(echo "$FINDING_DATA" | jq -r '.title')
852
1267
 
1268
+ # Rule 0: Batch fully human-gated — checked FIRST, overrides even the P1/P2 priority
1269
+ # override below. BATCH_FULLY_GATED is computed once per completion cycle in Step 4B
1270
+ # item 6.7; read it here, do not recompute. <!-- Added: forge#1814 -->
1271
+ if [ "${BATCH_FULLY_GATED:-false}" = "true" ]; then
1272
+ DEFER=true; DEFER_REASON="batch fully human-gated — idle policy"
853
1273
  # Heuristic 1: Generation check — source issue has review-finding label (always defer, even for P1/P2)
854
- if SOURCE_NUM=$(echo "$FINDING_DATA" | jq -r '.body' | grep -oP '(?i)spawned from issue #\K\d+|source issue[: #]+\K\d+' | head -1) && \
1274
+ elif SOURCE_NUM=$(echo "$FINDING_DATA" | jq -r '.body' | grep -oP '(?i)spawned from issue #\K\d+|source issue[: #]+\K\d+' | head -1) && \
855
1275
  [ -n "$SOURCE_NUM" ] && \
856
1276
  gh issue view $SOURCE_NUM -R {GH_REPO} --json labels --jq '[.labels[].name]' 2>/dev/null | grep -q "review-finding"; then
857
1277
  DEFER=true; DEFER_REASON="generation >= 2 (source #${SOURCE_NUM} is also a review-finding)"
@@ -893,8 +1313,14 @@ Cascade-spawned findings collected within a single `/orchestrate` run never pass
893
1313
  # exclusions as phase-1-resolve.md. Both sites go through jq test() (Oniguruma)
894
1314
  # with identical patterns — NOT grep ERE — so the two batching checks cannot
895
1315
  # classify the same issue body differently. <!-- forge#1837 -->
1316
+ # NOTE: SURFACE_BATCHED_FINDINGS and SURFACE_BATCH_COUNT are declared once in Step 4A.pre
1317
+ # (batch scope) — do NOT re-initialize them here, this block runs per-agent-completion cycle.
1318
+ # SURFACE_FILE_MEMBERS itself must NOT survive across cycles — `declare -A` on an
1319
+ # already-declared associative array is a no-op on its existing contents in bash, so an
1320
+ # explicit `unset` is required to actually clear it before each cycle repopulates it.
1321
+ # <!-- Added: forge#1909 -->
1322
+ unset SURFACE_FILE_MEMBERS
896
1323
  declare -A SURFACE_FILE_MEMBERS
897
- SURFACE_BATCHED_FINDINGS=()
898
1324
 
899
1325
  # Defensive cap on gh issue view fan-out. QUEUED_FINDINGS is already bounded by
900
1326
  # upstream cascade control; this cap holds even if that bound is later loosened,
@@ -983,6 +1409,7 @@ BATCH_EOF
983
1409
  # with the single batch issue, so the dispatch step below operates on the
984
1410
  # batch unit and skips the individual members.
985
1411
  SURFACE_BATCHED_FINDINGS+=("${CHUNK[@]}")
1412
+ [ -n "$BATCH_ISSUE_NUM" ] && SURFACE_BATCH_COUNT=$((SURFACE_BATCH_COUNT + 1)) # forge#1858 Step 6B counter
986
1413
  QUEUED_FINDINGS=($(printf '%s\n' "${QUEUED_FINDINGS[@]}" | grep -vxF -f <(printf '%s\n' "${CHUNK[@]}") || true))
987
1414
  [ -n "$BATCH_ISSUE_NUM" ] && QUEUED_FINDINGS+=("$BATCH_ISSUE_NUM")
988
1415
  echo "Batched ${#CHUNK[@]} findings into #${BATCH_ISSUE_NUM}; members removed from QUEUED_FINDINGS and the DAG."
@@ -992,6 +1419,44 @@ done
992
1419
 
993
1420
  Findings clustered here are replaced by their batch issue in `QUEUED_FINDINGS` (and therefore the DAG, which is built from `QUEUED_FINDINGS` in the dispatch step below) — the individual member issues in `SURFACE_BATCHED_FINDINGS` are never dispatched. Findings that remain ungrouped (fewer than 2 sharing a file in this collection round) stay individually queued below; they retain default-batchable eligibility and will be picked up by the next `/orchestrate` invocation's Phase 1 resolve if a same-file or leaf-directory cluster later forms across runs.
994
1421
 
1422
+ **Per-batch token budget gate (P3-and-below only — MANDATORY, runs on the POST-clubbing `QUEUED_FINDINGS` list):** <!-- Added: forge#1858 -->
1423
+
1424
+ Rule 5 from the evaluation order above. Charges ONE `TOKEN_ESTIMATE_PER_FINDING` per unit remaining in `QUEUED_FINDINGS` at this point — a same-run surface-area-clubbed batch issue counts as a single unit (its members were already replaced by the batch issue number above), never once per member. `TOKEN_BUDGET`, `TOKEN_ESTIMATE_PER_FINDING`, and `BATCH_TOKEN_SPEND` were declared in Step 4A.pre and persist across every Step 4C run this session performs — do NOT re-initialize `BATCH_TOKEN_SPEND` here. This is a DIFFERENT mechanism from the `--budget N` dollar-cost flag (economic scheduling, forge#1743, `should_dispatch()` in Step 4A-pre.0): that one is opt-in, dollar-denominated, and gates only the *original* `SORTED_READY_SET` dispatch loop — it is never consulted here.
1425
+
1426
+ ```bash
1427
+ TOKEN_BUDGET_DEFERRED_THIS_RUN=()
1428
+
1429
+ for FINDING_NUM in "${QUEUED_FINDINGS[@]}"; do
1430
+ FINDING_PRIORITY=$(gh issue view "$FINDING_NUM" -R {GH_REPO} --json labels \
1431
+ --jq '([.labels[].name | select(startswith("priority:P"))] | .[0] // "priority:P3") | ltrimstr("priority:")' 2>/dev/null || echo "P3")
1432
+
1433
+ # P1/P2 are NEVER gated by the token budget — they were queued by rule 2 above
1434
+ # regardless of budget headroom; skip them here and leave them in QUEUED_FINDINGS.
1435
+ if [ "$FINDING_PRIORITY" = "P1" ] || [ "$FINDING_PRIORITY" = "P2" ]; then
1436
+ continue
1437
+ fi
1438
+
1439
+ PROJECTED_TOKEN_SPEND=$((BATCH_TOKEN_SPEND + TOKEN_ESTIMATE_PER_FINDING))
1440
+ if [ "$PROJECTED_TOKEN_SPEND" -gt "$TOKEN_BUDGET" ]; then
1441
+ TOKEN_DEFERRED+=("$FINDING_NUM")
1442
+ TOKEN_BUDGET_DEFERRED_THIS_RUN+=("$FINDING_NUM")
1443
+ DEFERRED_REASONS[$FINDING_NUM]="token budget exceeded (est. ${TOKEN_ESTIMATE_PER_FINDING} tokens would push batch spend to ${PROJECTED_TOKEN_SPEND} > ceiling ${TOKEN_BUDGET})"
1444
+ echo "TOKEN BUDGET DEFER: #${FINDING_NUM} (est. ${TOKEN_ESTIMATE_PER_FINDING} tokens, projected total ${PROJECTED_TOKEN_SPEND} > ${TOKEN_BUDGET})"
1445
+ else
1446
+ BATCH_TOKEN_SPEND=$PROJECTED_TOKEN_SPEND
1447
+ fi
1448
+ done
1449
+
1450
+ # Remove budget-deferred units from QUEUED_FINDINGS — they move to DEFERRED_FINDINGS/
1451
+ # TOKEN_DEFERRED instead of dispatching this cycle. Re-evaluable in Step 4F.2.6, never
1452
+ # permanent (distinct from the generation>=2 bucket in Step 4F.1).
1453
+ if [ "${#TOKEN_BUDGET_DEFERRED_THIS_RUN[@]}" -gt 0 ]; then
1454
+ QUEUED_FINDINGS=($(printf '%s\n' "${QUEUED_FINDINGS[@]}" | grep -vxF -f <(printf '%s\n' "${TOKEN_BUDGET_DEFERRED_THIS_RUN[@]}") || true))
1455
+ DEFERRED_FINDINGS+=("${TOKEN_BUDGET_DEFERRED_THIS_RUN[@]}")
1456
+ echo "Token budget: deferred ${#TOKEN_BUDGET_DEFERRED_THIS_RUN[@]} P3-and-below unit(s) this cycle — batch spend now ${BATCH_TOKEN_SPEND}/${TOKEN_BUDGET}"
1457
+ fi
1458
+ ```
1459
+
995
1460
  **For queued (non-deferred) findings:**
996
1461
 
997
1462
  1. **Add them to the dependency DAG.** They are implementation issues — same as issues spawned by investigations in Phase 2. Compute their predecessor sets using the same conflict detection (Step 3C Layers 1-4) against all remaining blocked/active issues.
@@ -1000,7 +1465,7 @@ Findings clustered here are replaced by their batch issue in `QUEUED_FINDINGS` (
1000
1465
  ```
1001
1466
  Agent #{COMPLETED} spawned {count} new finding issues: #{A}, #{B}
1002
1467
  Added to DAG: #{A} (predecessors: {}), #{B} (predecessors: {#{X}})
1003
- Deferred (cascade control): #{C} (P3 same-file), #{D} (comment heuristic)
1468
+ Deferred (cascade control): #{C} (P3 same-file), #{D} (comment heuristic), #{E} (token budget)
1004
1469
  ```
1005
1470
  4. **Re-run file-overlap detection** (Step 3C) on the expanded issue set — finding issues may conflict with active or queued issues that touch the same files. Ready findings dispatch immediately via the standard Step 4B dispatch loop.
1006
1471
 
@@ -1162,11 +1627,13 @@ If an agent reports failure or error:
1162
1627
 
1163
1628
  **WHY THIS EXISTS**: Deferred findings accumulate during the batch because of file-overlap and cascade-control heuristics (Step 4C). But once the DAG drains, the conditions that caused deferral often no longer apply — completed issues no longer occupy files, so same-file overlap vanishes. Without this sweep, deferred findings silently pile up across runs and never get resolved.
1164
1629
 
1165
- **Step 4F.1: Classify deferred findings into permanent vs re-evaluable**
1630
+ **Step 4F.1: Classify deferred findings into permanent vs re-evaluable vs idle-gated vs token-gated**
1166
1631
 
1167
1632
  ```bash
1168
1633
  PERMANENT_DEFERRED=()
1169
1634
  SWEEP_CANDIDATES=()
1635
+ IDLE_DEFERRED=() # <!-- Added: forge#1814 -->
1636
+ TOKEN_GATED=() # <!-- Added: forge#1858 -->
1170
1637
 
1171
1638
  for FINDING_NUM in "${DEFERRED_FINDINGS[@]}"; do
1172
1639
  DEFER_REASON="${DEFERRED_REASONS[$FINDING_NUM]}"
@@ -1174,13 +1641,28 @@ for FINDING_NUM in "${DEFERRED_FINDINGS[@]}"; do
1174
1641
  # Generation >= 2 deferrals are PERMANENT — unbounded cascade prevention
1175
1642
  if echo "$DEFER_REASON" | grep -qi "generation"; then
1176
1643
  PERMANENT_DEFERRED+=($FINDING_NUM)
1644
+ # "Batch fully human-gated" deferrals (forge#1814) are their OWN bucket — they must NOT be
1645
+ # re-evaluated by the file-overlap logic in Step 4F.2 below, because the reason they were
1646
+ # deferred has nothing to do with file overlap. Re-evaluating them the same way as
1647
+ # comment/typo or P3-same-file deferrals would silently undo the idle policy: a sweep can
1648
+ # run while the batch is still a "paused drain" (Step 4B's Termination condition explicitly
1649
+ # allows Step 4F to run in that state), and BATCH_FULLY_GATED would still be true at sweep
1650
+ # time unless a human has actually merged a gating PR in the meantime.
1651
+ elif echo "$DEFER_REASON" | grep -qi "batch fully human-gated"; then
1652
+ IDLE_DEFERRED+=($FINDING_NUM)
1653
+ # "Token budget exceeded" deferrals (forge#1858) are ALSO their own bucket — re-evaluating
1654
+ # them via the file-overlap logic in Step 4F.2 would incorrectly clear them (they were never
1655
+ # deferred for file-overlap reasons) without checking whether token headroom actually exists.
1656
+ # See Step 4F.2.6 below, which mirrors the Step 4F.2.5 idle-gated pattern.
1657
+ elif echo "$DEFER_REASON" | grep -qi "token budget"; then
1658
+ TOKEN_GATED+=($FINDING_NUM)
1177
1659
  else
1178
1660
  # All other deferrals (comment/typo, P3 same-file) are re-evaluable
1179
1661
  SWEEP_CANDIDATES+=($FINDING_NUM)
1180
1662
  fi
1181
1663
  done
1182
1664
 
1183
- echo "Completion sweep: ${#SWEEP_CANDIDATES[@]} re-evaluable, ${#PERMANENT_DEFERRED[@]} permanent"
1665
+ echo "Completion sweep: ${#SWEEP_CANDIDATES[@]} re-evaluable, ${#PERMANENT_DEFERRED[@]} permanent, ${#IDLE_DEFERRED[@]} idle-gated, ${#TOKEN_GATED[@]} token-gated"
1184
1666
  ```
1185
1667
 
1186
1668
  **Step 4F.2: Re-evaluate sweep candidates**
@@ -1216,6 +1698,70 @@ for FINDING_NUM in "${SWEEP_CANDIDATES[@]}"; do
1216
1698
  done
1217
1699
  ```
1218
1700
 
1701
+ **Step 4F.2.5: Re-evaluate idle-gated deferrals** <!-- Added: forge#1814 -->
1702
+
1703
+ Recompute `BATCH_FULLY_GATED` fresh at sweep time (same check as Step 4B item 6.7, over `{all_batch_issue_numbers}`) — do NOT reuse a stale value captured when the finding was originally deferred. If a human has merged a gating PR since the finding was deferred, the original batch is no longer fully gated and the finding is safe to execute; otherwise it stays deferred.
1704
+
1705
+ ```bash
1706
+ # Skip the recompute entirely if nothing was idle-gated this run — no need to spend API
1707
+ # calls re-classifying the original batch for a bucket with zero members.
1708
+ if [ "${#IDLE_DEFERRED[@]}" -gt 0 ]; then
1709
+ ANY_ORIGINAL_IN_PROGRESS=false
1710
+ ANY_ORIGINAL_GATED=false
1711
+ for ORIG_NUM in {all_batch_issue_numbers}; do
1712
+ ORIG_STATE=$(classify_predecessor_state "$ORIG_NUM")
1713
+ case "$ORIG_STATE" in
1714
+ IN_PROGRESS) ANY_ORIGINAL_IN_PROGRESS=true ;;
1715
+ GATED) ANY_ORIGINAL_GATED=true ;;
1716
+ DONE|FAILED) ;;
1717
+ esac
1718
+ done
1719
+ if [ "$ANY_ORIGINAL_IN_PROGRESS" = "false" ] && [ "$ANY_ORIGINAL_GATED" = "true" ]; then
1720
+ BATCH_FULLY_GATED=true
1721
+ else
1722
+ BATCH_FULLY_GATED=false
1723
+ fi
1724
+ fi
1725
+
1726
+ for FINDING_NUM in "${IDLE_DEFERRED[@]}"; do
1727
+ if [ "${BATCH_FULLY_GATED:-false}" = "true" ]; then
1728
+ SWEEP_STILL_DEFERRED+=($FINDING_NUM)
1729
+ echo "Sweep: #${FINDING_NUM} still deferred (batch still fully human-gated — idle policy)"
1730
+ else
1731
+ SWEEP_EXECUTE+=($FINDING_NUM)
1732
+ echo "Sweep: #${FINDING_NUM} cleared for execution (batch no longer fully gated — a gating PR merged)"
1733
+ fi
1734
+ done
1735
+ ```
1736
+
1737
+ **Step 4F.2.6: Re-evaluate token-gated deferrals** <!-- Added: forge#1858 -->
1738
+
1739
+ The original batch has now drained — the token spend that caused these deferrals belongs to a batch that is no longer competing for dispatch. Re-evaluate `TOKEN_GATED` against a **fresh, sweep-scoped** token allowance (`SWEEP_TOKEN_SPEND`, separate from the drained `BATCH_TOKEN_SPEND` — do NOT reuse or reset the original batch counter, which other reporting still reads in Step 6B). This mirrors the file-overlap re-check in Step 4F.2 (which also benefits from the drained-DAG state) rather than the idle-gated re-check in Step 4F.2.5 (which re-tests an external condition, not a resettable budget).
1740
+
1741
+ ```bash
1742
+ # Skip entirely if nothing was token-gated this run.
1743
+ if [ "${#TOKEN_GATED[@]}" -gt 0 ]; then
1744
+ SWEEP_TOKEN_SPEND=0
1745
+
1746
+ for FINDING_NUM in "${TOKEN_GATED[@]}"; do
1747
+ STATE=$(gh issue view "$FINDING_NUM" -R {GH_REPO} --json state --jq '.state' 2>/dev/null || echo "")
1748
+ [ "$STATE" = "CLOSED" ] && continue # resolved by another process — drop silently
1749
+
1750
+ PROJECTED_SWEEP_SPEND=$((SWEEP_TOKEN_SPEND + TOKEN_ESTIMATE_PER_FINDING))
1751
+ if [ "$PROJECTED_SWEEP_SPEND" -gt "$TOKEN_BUDGET" ]; then
1752
+ SWEEP_STILL_DEFERRED+=($FINDING_NUM)
1753
+ echo "Sweep: #${FINDING_NUM} still deferred (token budget — sweep allowance ${TOKEN_BUDGET} would be exceeded at ${PROJECTED_SWEEP_SPEND})"
1754
+ else
1755
+ SWEEP_TOKEN_SPEND=$PROJECTED_SWEEP_SPEND
1756
+ SWEEP_EXECUTE+=($FINDING_NUM)
1757
+ echo "Sweep: #${FINDING_NUM} cleared for execution (fits fresh sweep token allowance — ${SWEEP_TOKEN_SPEND}/${TOKEN_BUDGET})"
1758
+ fi
1759
+ done
1760
+ fi
1761
+ ```
1762
+
1763
+ Findings that still don't fit the fresh sweep allowance remain deferred — never permanent (unlike `generation >= 2`). They are re-evaluated again on the next `/orchestrate` invocation or completion sweep, exactly like comment/typo and P3-same-file deferrals.
1764
+
1219
1765
  **Step 4F.3: Dispatch cleared findings**
1220
1766
 
1221
1767
  For each finding in `SWEEP_EXECUTE`, add it to a fresh sweep DAG and dispatch using the same Steps 4A.pre.0, 4A.pre, and 4A logic. Run file-overlap detection between the swept findings themselves (they may conflict with each other).
@@ -1275,7 +1821,10 @@ if [ ${#SWEEP_EXECUTE[@]} -gt 0 ]; then
1275
1821
  # synthesis brief (Phase 2.5 runs only over the original batch's investigations), so
1276
1822
  # there is nothing to prefer. Keep this block in sync with 4A's fallback branch only.
1277
1823
  GIST_CONTEXT=""
1824
+ # Markdown emphasis markers (**bold**, __bold__, *italic*) are stripped before matching —
1825
+ # kept in sync with 4A's fallback branch above.
1278
1826
  PARENT_INV=$(gh issue view "$FINDING_NUM" -R {GH_REPO} --json body --jq '.body' \
1827
+ | sed -E 's/[*_]+//g' \
1279
1828
  | grep -oP '(?i)parent[: ]*#\K\d+|spawned from[: ]*#\K\d+' | head -1)
1280
1829
 
1281
1830
  if [ -n "$PARENT_INV" ] && [ -n "${INVESTIGATION_GISTS[$PARENT_INV]:-}" ]; then
@@ -1297,7 +1846,7 @@ The context-gathering phase can fetch this index to discover all investigation G
1297
1846
  # the complete pipeline contract. Keep in sync with Step 4A when the template changes.
1298
1847
  Agent(
1299
1848
  subagent_type="general-purpose",
1300
- model="sonnet",
1849
+ model="{SUBAGENT_MODEL}",
1301
1850
  description="Work on {PROJECT_PREFIX}#${FINDING_NUM}",
1302
1851
  run_in_background=true,
1303
1852
  prompt="You are working on GitHub issue #${FINDING_NUM} for the {PROJECT_NAME} project.
@@ -1314,15 +1863,15 @@ The context-gathering phase can fetch this index to discover all investigation G
1314
1863
 
1315
1864
  **IMPORTANT RULES**:
1316
1865
  - **MANDATORY**: You MUST use the Skill tool to invoke 'work-on' with args '${FINDING_NUM}'. Do NOT implement manually — /work-on handles the full pipeline including label state machine (workflow:investigating → workflow:building → workflow:in-review → workflow:merged), investigation reports, PR creation, and cleanup.
1317
- - For default repo issues: \`Skill(skill='work-on', args='${FINDING_NUM}')\`
1318
- - For satellite repo issues: \`Skill(skill='work-on', args='{SATELLITE_PREFIX}:${FINDING_NUM}')\` (prefix from forge.yaml → repos.satellites)
1866
+ - For default repo issues: \`Skill(skill='work-on', args='${FINDING_NUM} --under-orchestration')\`
1867
+ - For satellite repo issues: \`Skill(skill='work-on', args='{SATELLITE_PREFIX}:${FINDING_NUM} --under-orchestration')\` (prefix from forge.yaml → repos.satellites)
1319
1868
  - NEVER bypass /work-on with manual git/gh commands — the label updates and structured comments are critical for tracking
1320
1869
  - NEVER target \`main\` for PRs targeting the default repo. Use \`{STAGING_BRANCH}\` for fast-lane issues, or \`milestone/{slug}\` for milestone issues.
1321
1870
  - Satellite repos (MCP, n8n) have no staging branch — fast-lane PRs go to \`main\` for those.
1322
1871
  - If the issue is INVALID after investigation, close it with a comment explaining why
1323
1872
  - If you hit merge conflicts or blockers, post a comment on the issue and STOP — do not force anything
1324
1873
  - Do not interact with the user — you are running autonomously in the background
1325
- - **NEVER ask the user questions** — you are a background agent. If review finds issues, auto-fix simple ones and proceed. For complex findings on **low-risk domains**, merge anyway and create follow-up issues. For complex findings on **high-risk domains** (AUTH, BILLING, DATABASE, or any domain tagged as security-critical in Step 3B), add the `needs-human` label and stop — do NOT merge. High-risk `needs-human` halts are surfaced as `⚠ Blocked` in the Phase 5 completion report.
1874
+ - **NEVER ask the user questions** — you are a background agent. If review finds issues, auto-fix simple ones and proceed, then let `/review-pr`'s own verdict decide: APPROVED (no unresolved CONFIRMED HIGH/CRITICAL finding) → merge to `staging` and create follow-up issues for the rest, **regardless of domain**. Domain alone (AUTH, BILLING, DATABASE, or any domain tagged as security-critical in Step 3B) is NOT a reason to add `needs-human` and stop — `staging` is reversible; the real human deploy gate is `staging → main`, not this merge. <!-- Added: forge#1815 --> `needs-human` is reserved for what the pipeline genuinely cannot do itself: spend/procurement decisions, real-environment validation it has no access to, product/architecture judgment calls a human must make, or `/review-pr`'s existing evidence-based escalations (spec-evolution guard, novel task-type/module-combo trust escalation, calibration-based overconfidence routing). An unresolved CONFIRMED HIGH/CRITICAL finding is `/review-pr`'s own withheld-APPROVED case, not a domain-driven `needs-human` halt — `/review-pr` already refuses to return APPROVED when that's true, so there is no separate domain check to perform here.
1326
1875
 
1327
1876
  **LABEL-STATE LOOP CONTRACT — enforce after EVERY Skill return**:
1328
1877
  After EVERY \`Skill(skill='work-on', ...)\` call returns, immediately check the issue's current workflow label:
@@ -1331,7 +1880,7 @@ gh issue view ${FINDING_NUM} -R {GH_REPO} --json labels --jq '[.labels[].name |
1331
1880
  \`\`\`
1332
1881
  **Terminal labels** (only these allow you to stop): \`workflow:merged\`, \`workflow:invalid\`
1333
1882
  **Terminal condition also**: \`needs-human\` label present, \`workflow:awaiting-merge\` label present, OR issue state is \`closed\`
1334
- If the label is NOT terminal (e.g., \`workflow:investigating\`, \`workflow:ready-to-build\`, \`workflow:building\`, \`workflow:in-review\`), invoke \`Skill(skill='work-on', args='${FINDING_NUM}')\` again immediately. The \`/work-on\` skill will re-read GitHub state and advance to the next phase. Do NOT output a summary, do NOT pause, do NOT ask for confirmation — just invoke it again.
1883
+ If the label is NOT terminal (e.g., \`workflow:investigating\`, \`workflow:ready-to-build\`, \`workflow:building\`, \`workflow:in-review\`), invoke \`Skill(skill='work-on', args='${FINDING_NUM} --under-orchestration')\` again immediately. The \`/work-on\` skill will re-read GitHub state and advance to the next phase. Do NOT output a summary, do NOT pause, do NOT ask for confirmation — just invoke it again.
1335
1884
 
1336
1885
  **CRITICAL — SOURCE BRANCH DETECTION**:
1337
1886
  - If the issue has the \`review-finding\` label, read the issue body for \`**Code branch**: \\\`{branch}\\\`\`
@@ -1360,6 +1909,10 @@ Completion Sweep Results:
1360
1909
  Dispatched: #{A}, #{B} (file overlap cleared after DAG drain)
1361
1910
  Still deferred (cosmetic): #{C} (comment/typo)
1362
1911
  Permanently deferred (gen2): #{D} (generation >= 2 cascade cap)
1912
+ Idle-gated — still deferred: #{E} (batch still fully human-gated — waiting on a merge)
1913
+ Idle-gated — cleared: #{F} (a gating PR merged since deferral — no longer idle)
1914
+ Token-gated — cleared: #{G} (fits fresh sweep token allowance — batch spend ${SWEEP_TOKEN_SPEND}/${TOKEN_BUDGET})
1915
+ Token-gated — still deferred: #{H} (sweep allowance also exhausted — re-evaluable next run)
1363
1916
  ```
1364
1917
 
1365
1918
  **After sweep agents complete** (or if no findings were dispatched): output the budget deferred-issues report (if applicable), then proceed to Phase 5.
@@ -1368,6 +1921,8 @@ Completion Sweep Results:
1368
1921
  - Re-sweeping findings spawned during the sweep itself — this creates unbounded recursion. Sweep is a single pass.
1369
1922
  - Overriding generation >= 2 deferrals — the cascade cap is absolute.
1370
1923
  - Skipping the sweep because "there are only a few" deferred findings — even one deferred finding represents unresolved work.
1924
+ - Clearing an idle-gated deferral (forge#1814) without recomputing `BATCH_FULLY_GATED` fresh at sweep time — a stale "not gated" read would re-introduce the exact net-negative churn this policy exists to stop.
1925
+ - Clearing a token-gated deferral (forge#1858) by reusing the drained `BATCH_TOKEN_SPEND` instead of the fresh `SWEEP_TOKEN_SPEND` allowance — the whole point of Step 4F.2.6 is that the *original* batch's spend is no longer relevant once it has drained.
1371
1926
 
1372
1927
  ### Step 4F.5: Budget Deferred-Issues Report (conditional) <!-- Added: forge#1743 -->
1373
1928