forgedock 1.1.7 → 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 (97) hide show
  1. package/README.md +10 -33
  2. package/bin/cinema.mjs +23 -5
  3. package/bin/engine/invariants.mjs +361 -0
  4. package/bin/engine-cli.mjs +137 -11
  5. package/bin/env-detect.mjs +249 -0
  6. package/bin/forge-utils.mjs +364 -4
  7. package/bin/forgedock.mjs +1064 -135
  8. package/bin/hooks/pre-tool-use.mjs +182 -0
  9. package/bin/hooks/session-start.mjs +68 -3
  10. package/bin/init-detect.mjs +101 -12
  11. package/bin/journey.mjs +917 -16
  12. package/bin/labels.json +10 -0
  13. package/bin/recall.mjs +557 -0
  14. package/bin/registry.mjs +91 -3
  15. package/bin/runner.mjs +38 -2
  16. package/bin/tui.mjs +49 -3
  17. package/commands/adopt.md +1 -1
  18. package/commands/analytics.md +8 -8
  19. package/commands/audit.md +1 -1
  20. package/commands/autopilot.md +958 -77
  21. package/commands/ci-audit.md +1 -1
  22. package/commands/cleanup.md +139 -55
  23. package/commands/compat-audit.md +1 -1
  24. package/commands/deploy-info.md +1 -1
  25. package/commands/deploy-pr.md +1 -1
  26. package/commands/diagnose.md +1 -1
  27. package/commands/explain.md +1 -1
  28. package/commands/fix-ci.md +1 -1
  29. package/commands/forgedock-init.md +1 -1
  30. package/commands/incident-response.md +3 -3
  31. package/commands/issue.md +1 -1
  32. package/commands/optimize.md +586 -1
  33. package/commands/orchestrate/config.md +13 -3
  34. package/commands/orchestrate/phase-1-resolve.md +36 -26
  35. package/commands/orchestrate/phase-2-triage.md +1 -1
  36. package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
  37. package/commands/orchestrate/phase-3-dependency.md +587 -48
  38. package/commands/orchestrate/phase-4-execution.md +1031 -57
  39. package/commands/orchestrate/phase-5-cleanup.md +1 -1
  40. package/commands/orchestrate/phase-6-report.md +248 -11
  41. package/commands/orchestrate/safety.md +2 -2
  42. package/commands/pipeline-health/phase-2-metrics.md +305 -1
  43. package/commands/pipeline-health/phase-4-proposals.md +127 -0
  44. package/commands/qa-sweep.md +1 -1
  45. package/commands/quality-gate.md +418 -1
  46. package/commands/recover-orphans.md +5 -5
  47. package/commands/replay.md +1 -1
  48. package/commands/review-pr-agents/api.md +2 -2
  49. package/commands/review-pr-agents/auth.md +2 -2
  50. package/commands/review-pr-agents/billing.md +2 -2
  51. package/commands/review-pr-agents/concurrency.md +2 -2
  52. package/commands/review-pr-agents/database.md +2 -2
  53. package/commands/review-pr-agents/frontend.md +2 -2
  54. package/commands/review-pr-agents/infra.md +2 -2
  55. package/commands/review-pr-agents/protocols.md +1 -1
  56. package/commands/review-pr-agents/scraper.md +2 -2
  57. package/commands/review-pr-agents/security.md +2 -2
  58. package/commands/review-pr-staging.md +41 -3
  59. package/commands/review-pr.md +462 -19
  60. package/commands/rollback.md +1 -1
  61. package/commands/scope.md +1 -1
  62. package/commands/security-audit.md +1 -1
  63. package/commands/spec-doctor.md +429 -0
  64. package/commands/test-gate.md +1 -1
  65. package/commands/validate.md +53 -1
  66. package/commands/work-on/build/architect.md +78 -1
  67. package/commands/work-on/build/context.md +360 -22
  68. package/commands/work-on/build/implement.md +3 -1
  69. package/commands/work-on/build/validate.md +29 -1
  70. package/commands/work-on/build.md +75 -6
  71. package/commands/work-on/close.md +624 -10
  72. package/commands/work-on/decompose.md +1 -1
  73. package/commands/work-on/investigate.md +235 -5
  74. package/commands/work-on/remediate.md +331 -0
  75. package/commands/work-on/review.md +1 -1
  76. package/commands/work-on-monolithic.md +1 -1
  77. package/commands/work-on.md +205 -66
  78. package/docs/CI.md +2 -2
  79. package/docs/CODEX.md +1 -1
  80. package/docs/CONFIG.md +186 -0
  81. package/docs/FORGE-PROTOCOL.md +101 -0
  82. package/docs/site/command-reference.md +31 -1
  83. package/docs/site/getting-started.md +9 -34
  84. package/docs/site/index.md +1 -1
  85. package/docs/spec/forge-protocol-v1.md +97 -5
  86. package/docs/spec/label-state-machine.md +6 -1
  87. package/package.json +3 -2
  88. package/packages/protocol/src/cli.js +374 -84
  89. package/packages/protocol/src/types.js +75 -1
  90. package/scripts/build-knowledge-index.mjs +1761 -0
  91. package/scripts/calibration.mjs +996 -0
  92. package/scripts/check-command-docs-drift.sh +4 -2
  93. package/scripts/danger-zones.mjs +789 -0
  94. package/scripts/doctor-pipeline-state.sh +88 -2
  95. package/scripts/select-fix-targets.sh +234 -0
  96. package/scripts/transition-label.sh +245 -6
  97. 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,136 @@ 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
+
13
+ ### Step 4A-pre.0: Budget initialization (MANDATORY when --budget is set) <!-- Added: forge#1743 -->
14
+
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).
16
+
17
+ ```bash
18
+ # --- Budget initialization ---
19
+ # Parse --budget N from ARGUMENTS (e.g. /orchestrate fast-lane --budget 5.00)
20
+ BUDGET_LIMIT=$(echo "${ARGUMENTS:-}" | grep -oP '(?<=--budget )\S+' | head -1 || echo "")
21
+ if [ -z "$BUDGET_LIMIT" ] || ! echo "$BUDGET_LIMIT" | grep -qP '^\d+(\.\d+)?$'; then
22
+ BUDGET_LIMIT="Infinity"
23
+ fi
24
+
25
+ PROJECTED_SPEND="0" # sum of ISSUE_COST_ESTIMATE[] for dispatched issues
26
+ ACTUAL_SPEND="0" # sum of actual cost reported by completed agents (best-effort)
27
+ DEFERRED_BUDGET_ISSUES=() # issues deferred because projected spend would exceed budget
28
+ EPSILON_DISPATCHED=false # true once at least one ε-reserve issue has been dispatched
29
+
30
+ if [ "$BUDGET_LIMIT" != "Infinity" ]; then
31
+ EPSILON_BUDGET=$(echo "scale=4; $BUDGET_LIMIT * 0.10" | bc 2>/dev/null || echo "0")
32
+ echo "Budget initialized: BUDGET_LIMIT=\$${BUDGET_LIMIT} EPSILON_BUDGET=\$${EPSILON_BUDGET} (10% ε-reserve)"
33
+ echo "Issues without cost priors (ε-reserve eligible): ${NO_PRIOR_ISSUES[*]:-none}"
34
+ else
35
+ EPSILON_BUDGET="0"
36
+ echo "Budget: uncapped (no --budget flag) — dispatching all ready issues by score order"
37
+ fi
38
+ # --- End budget initialization ---
39
+ ```
40
+
41
+ **Budget halt condition** (checked in Step 4A before each dispatch):
42
+
43
+ Before dispatching issue `NUM`, check whether its estimated cost would exceed the remaining budget:
44
+
45
+ ```bash
46
+ # Check budget before dispatching NUM
47
+ should_dispatch() {
48
+ local NUM="$1"
49
+ local COST="${ISSUE_COST_ESTIMATE[$NUM]:-0.35}"
50
+
51
+ if [ "$BUDGET_LIMIT" = "Infinity" ]; then
52
+ return 0 # uncapped — always dispatch
53
+ fi
54
+
55
+ NEW_PROJECTED=$(echo "scale=4; $PROJECTED_SPEND + $COST" | bc 2>/dev/null || echo "$PROJECTED_SPEND")
56
+ MAIN_CEILING=$(echo "scale=4; $BUDGET_LIMIT - $EPSILON_BUDGET" | bc 2>/dev/null || echo "$BUDGET_LIMIT")
57
+
58
+ # ε-reserve logic: if this is a no-prior issue AND ε-budget has not yet been
59
+ # used AND the no-prior issue has NOT been dispatched yet, allow it even if
60
+ # the main ceiling is hit (up to BUDGET_LIMIT total).
61
+ if [ "${ISSUE_HAS_PRIOR[$NUM]:-false}" = "false" ] && [ "$EPSILON_DISPATCHED" = "false" ]; then
62
+ if echo "$NEW_PROJECTED $BUDGET_LIMIT" | awk '{exit ($1 <= $2) ? 0 : 1}' 2>/dev/null; then
63
+ EPSILON_DISPATCHED=true
64
+ echo "ε-reserve: dispatching #${NUM} (no-prior issue) from exploration reserve"
65
+ return 0
66
+ fi
67
+ fi
68
+
69
+ # Main budget check: defer if projected would exceed main ceiling
70
+ if echo "$NEW_PROJECTED $MAIN_CEILING" | awk '{exit ($1 <= $2) ? 0 : 1}' 2>/dev/null; then
71
+ DEFERRED_BUDGET_ISSUES+=("$NUM")
72
+ echo "BUDGET DEFER: #${NUM} (est. \$${COST}) would push projected spend to \$${NEW_PROJECTED} > main ceiling \$${MAIN_CEILING}"
73
+ return 1
74
+ fi
75
+
76
+ return 0
77
+ }
78
+ ```
79
+
80
+ **Budget deferred-issues report** (output when `BUDGET_LIMIT` is finite and `DEFERRED_BUDGET_ISSUES` is non-empty — print at end of Phase 4, before Phase 5):
81
+
82
+ ```
83
+ ## Budget Report
84
+
85
+ **Budget limit**: $${BUDGET_LIMIT}
86
+ **Projected spend (dispatched issues)**: $${PROJECTED_SPEND}
87
+ **ε-reserve used**: ${EPSILON_DISPATCHED} (10% = $${EPSILON_BUDGET})
88
+
89
+ ### Deferred Issues (budget exhausted — never silently dropped)
90
+
91
+ | Issue | Title | Score | Est. Cost | Reason |
92
+ |-------|-------|-------|-----------|--------|
93
+ | #{N} | {title} | {score} | ${cost} | Budget ceiling reached |
94
+
95
+ **Action**: Re-run `/orchestrate {deferred_issue_numbers} [--budget N]` to process deferred issues, or increase `--budget`.
96
+ ```
97
+
98
+ **When `BUDGET_LIMIT = Infinity`**: skip this check and report entirely — uncapped behavior.
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
+
11
141
  ### Step 4A-pre: Staging baseline tracking (MANDATORY — continuous)
12
142
 
13
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.
@@ -135,7 +265,7 @@ Before building agent prompts, run `classify-lane.sh` for every issue in the cur
135
265
 
136
266
  ```bash
137
267
  # Requires classify-lane.sh to be available at ~/.claude/scripts/classify-lane.sh
138
- # (installed by `npx forgedock` — see bin/forgedock.mjs linkScripts step)
268
+ # (installed by `npx forgedock` — see bin/journey.mjs: forge()'s linkPipelineScripts() step)
139
269
  # Fallback: bash "$FORGE_HOME/scripts/classify-lane.sh" if ~/.claude/scripts/ is unavailable
140
270
 
141
271
  declare -A ISSUE_LANE
@@ -149,6 +279,43 @@ QUEUED_FINDINGS=()
149
279
  declare -A DEFERRED_REASONS
150
280
  declare -A AGENT_ISSUE_MAP
151
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
+
152
319
  for NUM in {ready_issue_numbers}; do
153
320
  PR_BASE=$(bash ~/.claude/scripts/classify-lane.sh "$NUM" -R {GH_REPO}) || {
154
321
  echo "ERROR: classify-lane.sh failed for #$NUM — adding needs-human label and skipping" >&2
@@ -174,17 +341,43 @@ Use `${ISSUE_LANE[$NUM]}` and `${ISSUE_PR_BASE[$NUM]}` to populate `{LANE}` and
174
341
  **Engine-first dispatch (default)**: When `forgedock` is in PATH, dispatch each ready issue via the durable engine rather than spawning prose Agent sub-agents. The engine's phase table enforces gate semantics in code — its fail-closed review gate and deterministic phase ordering are not subject to LLM interpretation.
175
342
 
176
343
  ```bash
177
- # Engine-first dispatch: check CLI availability, then dispatch ready issues in parallel
344
+ # Engine-first dispatch: check CLI availability, then dispatch ready issues in score order
345
+ # Uses SORTED_READY_SET[] from Step 3E.5 (descending value/cost) and budget gate from Step 4A-pre.0
178
346
  FORGEDOCK_AVAILABLE=$(command -v forgedock >/dev/null 2>&1 && echo "true" || echo "false")
179
347
 
180
348
  if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
181
- for NUM in {ready_issue_numbers}; do
182
- LANE="${ISSUE_LANE[$NUM]}"
183
- PR_BASE="${ISSUE_PR_BASE[$NUM]}"
184
- echo "Dispatching #$NUM via forgedock run-issue --lane $PR_BASE"
185
- forgedock run-issue "$NUM" --lane "$PR_BASE" &
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=()
352
+ for NUM in "${SORTED_READY_SET[@]:-{ready_issue_numbers}}"; do
353
+ if should_dispatch "$NUM"; then
354
+ DISPATCH_QUEUE+=("$NUM")
355
+ fi # else: added to DEFERRED_BUDGET_ISSUES[] by should_dispatch()
356
+ done
357
+
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))
186
380
  done
187
- wait
188
381
  echo "Engine dispatch complete — advancing to Step 4B (completion sweep)"
189
382
  else
190
383
  echo "INFO: Using agent dispatch mode (forgedock CLI not in PATH — run \`npm install -g forgedock\` for engine-mode dispatch)"
@@ -206,7 +399,7 @@ For each **ready** issue (all predecessors resolved or no predecessors), spawn a
206
399
  ```
207
400
  Agent(
208
401
  subagent_type="general-purpose",
209
- model="sonnet",
402
+ model="{SUBAGENT_MODEL}",
210
403
  description="Work on {PROJECT_PREFIX}#{NUMBER}",
211
404
  run_in_background=true,
212
405
  prompt="You are working on GitHub issue #{NUMBER} for the {PROJECT_NAME} project.
@@ -217,21 +410,22 @@ Agent(
217
410
 
218
411
  **YOUR MISSION**: Invoke `/work-on` via the Skill tool and let it run to completion. `/work-on` is a self-contained routing loop that handles the ENTIRE pipeline: investigate → build (context → architect → implement → validate) → review (push → PR → /review-pr --auto-merge) → close (project board → trajectory log → worktree cleanup). Do NOT intervene, compensate, or manually close issues — `/work-on` handles everything including issue closure and label updates in its close phase.
219
412
 
220
- **CRITICAL — DO NOT STOP EARLY**: /work-on runs as a multi-phase routing loop. Each phase (investigate, build, review, close) returns an intermediate result — these are NOT completion signals. You are NOT done until the issue reaches a terminal state: `workflow:merged`, `workflow:invalid`, or `needs-human`. If /work-on returns after only one phase (e.g., investigation), you MUST invoke it again immediately — it will re-read GitHub state and continue to the next phase. Keep invoking /work-on until it reaches a terminal state. Never output 'done' or stop after an intermediate result.
413
+ **CRITICAL — DO NOT STOP EARLY**: /work-on runs as a multi-phase routing loop. Each phase (investigate, build, review, close) returns an intermediate result — these are NOT completion signals. You are NOT done until the issue reaches a terminal state: `workflow:merged`, `workflow:invalid`, `needs-human`, or `workflow:awaiting-merge`. If /work-on returns after only one phase (e.g., investigation), you MUST invoke it again immediately — it will re-read GitHub state and continue to the next phase. Keep invoking /work-on until it reaches a terminal state. Never output 'done' or stop after an intermediate result.
221
414
 
222
415
  **HOW REVIEW FINDINGS WORK**: /review-pr may create GitHub issues (with `review-finding` label) for findings it discovers. These are NOT blockers — they are separate work items that will go through their own /work-on pipeline later. The original PR should ALWAYS merge after review. The only exception is build errors (code doesn't compile) — those must be fixed before merging.
223
416
 
224
417
  **IMPORTANT RULES**:
225
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.
226
- - For default repo issues: `Skill(skill='work-on', args='{NUMBER}')`
227
- - 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).
228
422
  - NEVER bypass /work-on with manual git/gh commands — the label updates and structured comments are critical for tracking
229
423
  - NEVER target `main` for PRs targeting the default repo. Use `{STAGING_BRANCH}` for fast-lane issues, or `milestone/{slug}` for milestone issues.
230
424
  - Satellite repos (MCP, n8n) have no staging branch — fast-lane PRs go to `main` for those.
231
425
  - If the issue is INVALID after investigation, close it with a comment explaining why
232
426
  - If you hit merge conflicts or blockers, post a comment on the issue and STOP — do not force anything
233
427
  - Do not interact with the user — you are running autonomously in the background
234
- - **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.
235
429
 
236
430
  **LABEL-STATE LOOP CONTRACT — enforce after EVERY Skill return**:
237
431
  After EVERY `Skill(skill='work-on', ...)` call returns, immediately check the issue's current workflow label:
@@ -239,8 +433,9 @@ After EVERY `Skill(skill='work-on', ...)` call returns, immediately check the is
239
433
  gh issue view {NUMBER} -R {GH_REPO} --json labels --jq '[.labels[].name | select(startswith("workflow:"))]'
240
434
  ```
241
435
  **Terminal labels** (only these allow you to stop): `workflow:merged`, `workflow:invalid`
242
- **Terminal condition also**: `needs-human` label present OR issue state is `closed`
243
- 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.
436
+ **Terminal condition also**: `needs-human` label present, `workflow:awaiting-merge` label present, OR issue state is `closed`
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`).
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.
244
439
 
245
440
  **CRITICAL — SOURCE BRANCH DETECTION**:
246
441
  - If the issue has the `review-finding` label, read the issue body for `**Code branch**: \`{branch}\``
@@ -274,7 +469,11 @@ if [ -n "$SYNTHESIS_BRIEF" ]; then
274
469
  ${SYNTHESIS_BRIEF}"
275
470
  else
276
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.
277
475
  PARENT_INV=$(gh issue view {NUMBER} -R {GH_REPO} --json body --jq '.body' \
476
+ | sed -E 's/[*_]+//g' \
278
477
  | grep -oP '(?i)parent[: ]*#\K\d+|spawned from[: ]*#\K\d+' | head -1)
279
478
 
280
479
  if [ -n "$PARENT_INV" ] && [ -n "${INVESTIGATION_GISTS[$PARENT_INV]:-}" ]; then
@@ -297,6 +496,30 @@ fi
297
496
 
298
497
  If `GIST_CONTEXT` is empty (no synthesis brief, no parent investigation, and no milestone index found), the variable resolves to a blank line in the template — no impact on the agent prompt. <!-- Updated: forge#341, forge#1192 -->
299
498
 
499
+ **Claims board context injection** <!-- Added: forge#1736 -->: When a coordination issue exists for this batch (`FORGE_COORD_ISSUE` is set), append the claims board URL and the active-claims check instruction to the agent's context. This enables each `/work-on` agent to post its `FORGE:CLAIM` on build start.
500
+
501
+ ```bash
502
+ # Inject coordination issue URL if claims board was created in Step 3D.1
503
+ if [ -n "${FORGE_COORD_ISSUE:-}" ]; then
504
+ GIST_CONTEXT="${GIST_CONTEXT}
505
+
506
+ **ORCHESTRATION CLAIMS BOARD**: This agent is running under an orchestration batch.
507
+ Claims board issue URL: ${FORGE_COORD_ISSUE}
508
+
509
+ On build start (Phase B2 / Phase 3C of /work-on), post a FORGE:CLAIM annotation on the
510
+ coordination issue above. Required fields:
511
+ Holder: ##{NUMBER} / batch-$(date -u +%Y%m%dT%H%M%S)
512
+ Files: (list of files from your FORGE:CONTRACT deliverables table, one per line)
513
+ Interfaces: (public function/type signatures you will modify or that callers must preserve)
514
+ TTL: terminal state of Holder issue ##{NUMBER}
515
+
516
+ On reaching terminal state (workflow:merged, workflow:invalid, needs-human, or workflow:awaiting-merge), post
517
+ <!-- FORGE:CLAIM_RELEASED --> on the coordination issue to release your claim.
518
+
519
+ Set FORGE_COORD_ISSUE=${FORGE_COORD_ISSUE} in your environment so /work-on phases can read it."
520
+ fi
521
+ ```
522
+
300
523
  **Hot-copy CONTRACT context** (extends `{GIST_CONTEXT}` for milestone-lane issues where the parent issue already carries a `FORGE:CONTRACT` annotation): <!-- Added: forge#1277 -->
301
524
 
302
525
  When a DAG node issue was spawned from a decomposition (parent issue has `workflow:decomposed` label and the child issue body references `**Parent**: #NNN`), the parent issue may already have a `FORGE:CONTRACT` annotation that was posted before decomposition. Inject a scoped excerpt into the child's prompt so the child does not re-fetch it.
@@ -322,6 +545,20 @@ fi
322
545
 
323
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.
324
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
+
325
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:
326
563
 
327
564
  ```
@@ -331,63 +568,487 @@ AGENT_ISSUE_MAP[{NUMBER}] = <agent_id returned by Agent()>
331
568
 
332
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 -->
333
570
 
334
- **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).
335
599
 
336
600
  ### Step 4B: Monitor completions and dispatch newly ready issues
337
601
 
338
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.
339
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
+
340
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 -->
341
609
 
342
- **Core streaming dispatch loop**: After processing each agent completion, check the DAG for newly unblocked issues. If any issue now has all predecessors in a terminal state, 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.
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:
611
+
612
+ ```bash
613
+ classify_predecessor_state() {
614
+ local PRED="$1"
615
+ local PRED_INFO
616
+ # NOTE: the `workflow` array below deliberately also keeps `needs-human` — that label has NO
617
+ # `workflow:` prefix (see bin/labels.json), so a bare `select(startswith("workflow:"))` would
618
+ # drop it and the GATED branch's `needs-human` case would be dead code (forge#1812 primary case).
619
+ PRED_INFO=$(gh issue view "$PRED" -R {GH_REPO} --json labels,state \
620
+ --jq '{state: .state, workflow: [.labels[].name | select(startswith("workflow:") or . == "needs-human")]}' 2>/dev/null || echo '{}')
621
+ local PRED_STATE PRED_LABELS
622
+ PRED_STATE=$(echo "$PRED_INFO" | jq -r '.state // "OPEN"')
623
+ PRED_LABELS=$(echo "$PRED_INFO" | jq -r '.workflow[]?' 2>/dev/null)
624
+
625
+ if echo "$PRED_LABELS" | grep -qx "workflow:invalid"; then
626
+ echo "FAILED"
627
+ elif echo "$PRED_LABELS" | grep -qx "workflow:merged"; then
628
+ echo "DONE"
629
+ elif echo "$PRED_LABELS" | grep -qxE "needs-human|workflow:awaiting-merge"; then
630
+ echo "GATED"
631
+ elif [ "$PRED_STATE" = "CLOSED" ]; then
632
+ # Closed with no workflow:invalid/merged label (e.g. closed-not-planned) — treat as DONE,
633
+ # not a new deadlock state; there is no pending code for dependents to wait on.
634
+ echo "DONE"
635
+ else
636
+ echo "IN_PROGRESS"
637
+ fi
638
+ }
639
+ ```
640
+
641
+ - **DONE** — predecessor's code is in the base branch (`workflow:merged`), or the predecessor is closed with no pending code. Safe for dependents to dispatch.
642
+ - **GATED** — predecessor is paused pending a human decision (`needs-human`) or pending only a human merge click (`workflow:awaiting-merge`). Its code is NOT yet in the base branch. Dependents are neither dispatched nor skipped — they move to the `blocked-on-human-merge` tracked state (item 6.5 below).
643
+ - **FAILED** — predecessor was closed as `workflow:invalid`, or the agent explicitly reported a build/test error. Dependents are marked "skipped — dependency failed" (item 6 below) — unchanged from prior behavior.
644
+ - **IN_PROGRESS** — predecessor is still mid-pipeline (`investigating`/`ready-to-build`/`building`/`in-review`). Dependent simply continues waiting; no special tracking needed.
645
+
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.
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
+
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.
343
737
 
344
738
  ```bash
345
739
  # After each agent completion, check for newly ready issues:
346
740
  for BLOCKED_NUM in {all_blocked_issue_numbers}; do
347
741
  ALL_PREDS_DONE=true
742
+ ANY_PRED_GATED=false
743
+ GATING_PREDS=()
348
744
  for PRED in {predecessors_of_BLOCKED_NUM}; do
349
- PRED_STATE=$(gh issue view $PRED -R {GH_REPO} --json labels,state --jq '{state: .state, workflow: [.labels[].name | select(startswith("workflow:"))]}')
350
- # Terminal states: workflow:merged, workflow:invalid, needs-human, or state=CLOSED
351
- if ! echo "$PRED_STATE" | grep -qE 'workflow:merged|workflow:invalid|needs-human|CLOSED'; then
352
- ALL_PREDS_DONE=false
353
- break
354
- fi
745
+ PRED_STATE=$(classify_predecessor_state "$PRED")
746
+ case "$PRED_STATE" in
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
791
+ FAILED) ALL_PREDS_DONE=false ;; # handled by item 6 (skip dependents)
792
+ GATED)
793
+ ALL_PREDS_DONE=false
794
+ ANY_PRED_GATED=true
795
+ GATING_PREDS+=("$PRED")
796
+ ;; # handled by item 6.5 (blocked-on-human-merge)
797
+ IN_PROGRESS|*) ALL_PREDS_DONE=false ;; # just keep waiting
798
+ esac
355
799
  done
356
800
  if [ "$ALL_PREDS_DONE" = "true" ]; then
357
- echo "#{BLOCKED_NUM} is now READY — all predecessors resolved. Dispatching."
801
+ echo "#{BLOCKED_NUM} is now READY — all predecessors DONE (merged/resolved). Dispatching."
358
802
  # Add to dispatch batch for this completion cycle
803
+ elif [ "$ANY_PRED_GATED" = "true" ]; then
804
+ echo "#{BLOCKED_NUM} is BLOCKED-ON-HUMAN-MERGE — gated by: ${GATING_PREDS[*]}. See item 6.5."
805
+ # Do NOT dispatch. Do NOT mark skipped. Tracked via item 6.5.
359
806
  fi
360
807
  done
361
- # 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.
362
813
  ```
363
814
 
364
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:
365
816
 
366
817
  1. **Check if the agent completed the FULL pipeline** — not just one phase:
367
818
  ```bash
368
- # Check final workflow state — only workflow:merged or workflow:invalid means truly done
819
+ # Check final workflow state — workflow:merged, workflow:invalid, needs-human, and
820
+ # workflow:awaiting-merge all mean this agent's own /work-on run has stopped (the last
821
+ # two are human-gated pauses, not completions). See Predecessor Classification above for
822
+ # how the DAG's readiness/failure logic treats these same labels differently from "this
823
+ # agent is done running."
369
824
  FINAL_STATE=$(gh issue view $NUM -R {GH_REPO} --json labels,state --jq '{state: .state, workflow: [.labels[].name | select(startswith("workflow:"))]}')
370
825
  echo "#{NUM}: $FINAL_STATE"
371
826
  ```
372
827
 
373
- 2. **If the issue is NOT in a terminal state** (`workflow:merged`, `workflow:invalid`, or `needs-human`), the agent stalled mid-pipeline. **Resume it immediately**:
828
+ 2. **If the issue is NOT in a terminal-for-this-agent state** (`workflow:merged`, `workflow:invalid`, `needs-human`, or `workflow:awaiting-merge`), the agent stalled mid-pipeline. **Resume it immediately**:
374
829
  ```
375
830
  Agent(
376
831
  resume=AGENT_ISSUE_MAP[{NUMBER}],
377
832
  description="Resume #{NUMBER} pipeline",
378
833
  run_in_background=true,
379
- 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."
380
835
  )
381
836
  ```
382
- **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 -->
383
838
 
384
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.
385
840
 
386
- 4. **Record completed results**: Success (PR merged), Invalid (issue closed), Blocked (needs human), or Error
841
+ 4. **Post CLAIM_RELEASED on coordination issue** (when `FORGE_COORD_ISSUE` is set): <!-- Added: forge#1736 -->
842
+ ```bash
843
+ # After verifying terminal state for issue NUM, release its claim on the coordination issue
844
+ if [ -n "${FORGE_COORD_ISSUE:-}" ]; then
845
+ COORD_NUM=$(echo "$FORGE_COORD_ISSUE" | grep -oE '[0-9]+$')
846
+ if [ -n "$COORD_NUM" ]; then
847
+ gh issue comment "$COORD_NUM" -R {GH_REPO} --body "<!-- FORGE:CLAIM_RELEASED -->
848
+ **Holder**: #${NUM} — reached terminal state: ${FINAL_WORKFLOW_STATE}
849
+ **Released**: $(date -u +%Y-%m-%dT%H:%M:%SZ)" 2>/dev/null || true
850
+ echo "CLAIM_RELEASED posted for #${NUM} on coordination issue #${COORD_NUM}"
851
+ fi
852
+ fi
853
+ ```
854
+
855
+ **Claims-board relaxation sweep** (run after posting CLAIM_RELEASED): When a claim is released, check all remaining Layer-2/4-serialized issue pairs. If the now-released Holder's claimed files were the *only* conflict reason for a still-blocked issue, and that blocked issue already has an active `FORGE:CLAIM` with a disjoint file set, the blocking edge MAY be relaxed (blocked issue becomes ready). <!-- Added: forge#1736 -->
856
+
857
+ ```bash
858
+ # After CLAIM_RELEASED for issue NUM:
859
+ # Read all active FORGE:CLAIM annotations from coordination issue
860
+ if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_NUM:-}" ]; then
861
+ ACTIVE_CLAIMS=$(gh api repos/{GH_REPO}/issues/${COORD_NUM}/comments \
862
+ --jq '[.[] | select(.body | contains("<!-- FORGE:CLAIM -->")) |
863
+ select(.body | contains("<!-- FORGE:CLAIM_RELEASED -->") | not)] |
864
+ map({holder: (.body | capture("\\*\\*Holder\\*\\*: (?P<h>[^\\n]+)").h),
865
+ files: (.body | capture("\\*\\*Files\\*\\*: (?P<f>[^\\n]+)").f)})' 2>/dev/null || echo '[]')
866
+ # For each still-blocked issue in a Layer-2/4 pair: check if its claim's file set
867
+ # is disjoint from all remaining active claims. If so, mark it ready.
868
+ # (Layer-1 and Layer-3 edges are never relaxed — this check is Layer-2/4 only.)
869
+ for BLOCKED_NUM in {layer_2_4_blocked_issues}; do
870
+ BLOCKED_CLAIM_FILES=$(echo "$ACTIVE_CLAIMS" \
871
+ | jq -r --arg h "#${BLOCKED_NUM}" '.[] | select(.holder | startswith($h)) | .files' 2>/dev/null || echo "")
872
+ if [ -n "$BLOCKED_CLAIM_FILES" ]; then
873
+ # Compare file set with all other active claims — if disjoint, downgrade to ready
874
+ echo "Claims-board relaxation: checking if #${BLOCKED_NUM} can be unblocked based on disjoint claims"
875
+ # (Implementer: build a set-intersection check here using sorted file lists)
876
+ fi
877
+ done
878
+ fi
879
+ ```
880
+
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
882
+
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
+ ```
933
+
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.
935
+
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)**:
955
+ ```bash
956
+ # Resolve PRED's open PR, if any, using the anchored search (forge#1634/#1646 precedent —
957
+ # do NOT fall back to a bare-number search here; a stale unrelated PR would misattribute gating).
958
+ GATING_PR=$(gh pr list -R {GH_REPO} --state open --search "\"Closes #${PRED}\" in:body" \
959
+ --json number --jq '.[0].number // empty' 2>/dev/null || echo "")
960
+ PRED_LABEL=$(gh issue view "$PRED" -R {GH_REPO} --json labels \
961
+ --jq '[.labels[].name | select(. == "needs-human" or . == "workflow:awaiting-merge")] | .[0] // "needs-human"' 2>/dev/null)
962
+
963
+ # Self-heal the label if it hasn't been bootstrapped yet (same pattern as review-pr.md 6C —
964
+ # colors match the canonical manifest bin/labels.json; --force makes this idempotent/cheap).
965
+ gh label create "blocked-on-human-merge" --color "006B75" --description "Dependent of a gated (needs-human/awaiting-merge) predecessor. Managed by ForgeDock." --force -R {GH_REPO} 2>/dev/null
966
+
967
+ # Idempotency: only post/label if not already tracked for this specific predecessor.
968
+ # Anchor on the exact "**Gating predecessor**: #N" label with a word boundary —
969
+ # a bare contains("#N") substring would false-match #50/#500 for predecessor #5. <!-- forge#1830 -->
970
+ ALREADY_TRACKED=$(gh api repos/{GH_REPO}/issues/${DEP}/comments \
971
+ --jq --arg prednum "${PRED}" '[.[] | select(.body | contains("FORGE:BLOCKED_ON_HUMAN_MERGE") and test("Gating predecessor\\*\\*: #" + $prednum + "\\b"))] | length' 2>/dev/null || echo "0")
972
+ if [ "$ALREADY_TRACKED" -eq 0 ]; then
973
+ gh issue comment "$DEP" -R {GH_REPO} --body "<!-- FORGE:BLOCKED_ON_HUMAN_MERGE -->
974
+ **Gating predecessor**: #${PRED} (state: \`${PRED_LABEL}\`${GATING_PR:+, open PR #${GATING_PR}})
975
+ **Status**: This issue is ready to dispatch as soon as #${PRED}'s gating PR merges. No action needed — the orchestrator (live session via item 6.6, or the next \`/orchestrate\` invocation via phase-3-dependency.md's wake reconstruction) will auto-dispatch it the moment #${PRED} reaches \`workflow:merged\`."
976
+ gh issue edit "$DEP" -R {GH_REPO} --add-label "blocked-on-human-merge" 2>/dev/null || true
977
+ fi
978
+ ```
979
+ Do NOT dispatch `DEP`. Do NOT mark it skipped — it remains visibly tracked as `blocked-on-human-merge` in the DAG, re-evaluated on the next completion event, stall-detection pass, or session wake.
980
+
981
+ 6.6. **Merge-triggered wake for blocked-on-human-merge dependents** <!-- Added: forge#1812 --> — whenever a completed agent's issue classifies as `DONE` via `workflow:merged` (i.e. it just merged), check whether any other issue is tracked as blocked on it — this makes gated dependents dispatch the instant the gating PR merges, with no manual `/orchestrate` re-run required:
982
+ ```bash
983
+ WOKEN=$(gh issue list -R {GH_REPO} --state open --label "blocked-on-human-merge" --json number \
984
+ --jq '.[].number' 2>/dev/null || echo "")
985
+ for DEP in $WOKEN; do
986
+ IS_GATED_BY_THIS=$(gh api repos/{GH_REPO}/issues/${DEP}/comments \
987
+ --jq --arg prednum "${NUM}" '[.[] | select(.body | contains("FORGE:BLOCKED_ON_HUMAN_MERGE") and test("Gating predecessor\\*\\*: #" + $prednum + "\\b"))] | length' 2>/dev/null || echo "0")
988
+ [ "$IS_GATED_BY_THIS" -gt 0 ] || continue
989
+ # Idempotency: only dispatch if DEP hasn't already been dispatched by another path.
990
+ DEP_ALREADY_DISPATCHED=$(gh issue view "$DEP" -R {GH_REPO} --json labels \
991
+ --jq '[.labels[].name | select(startswith("workflow:"))] | length' 2>/dev/null || echo "0")
992
+ if [ "$DEP_ALREADY_DISPATCHED" -eq 0 ]; then
993
+ gh issue edit "$DEP" -R {GH_REPO} --remove-label "blocked-on-human-merge" 2>/dev/null || true
994
+ gh issue comment "$DEP" -R {GH_REPO} --body "<!-- FORGE:UNBLOCKED -->
995
+ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was tracked via a prior FORGE:BLOCKED_ON_HUMAN_MERGE comment.)"
996
+ echo "#{DEP} unblocked by #{NUM} merge — dispatching immediately (Steps 4A.pre.0 → 4A.pre → 4A)."
997
+ # Add DEP to the same-response dispatch batch
998
+ fi
999
+ done
1000
+ ```
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.
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.
387
1042
 
388
- 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.
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}
389
1047
 
390
- 6. **Handle predecessor failures** — if a completed agent's issue FAILED (needs-human, invalid, or error), check for dependent issues in the DAG. Mark all transitive dependents as "skipped — dependency #{X} failed" and report them. Do NOT dispatch them.
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.
391
1052
 
392
1053
  7. **Verify pipeline compliance** — for each truly completed issue, check that the agent used `/work-on`:
393
1054
  ```bash
@@ -399,18 +1060,44 @@ done
399
1060
  ```
400
1061
  If an agent bypassed the pipeline, report it as a **failure** regardless of whether a PR exists.
401
1062
 
402
- 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:
403
1066
  ```
404
1067
  ✓ #{NUMBER} — {title} → PR #{PR} merged to {target}
405
1068
  ✗ #{NUMBER} — {title} → {reason for failure}
406
1069
  ⚠ #{NUMBER} — {title} → PIPELINE BYPASS (no /work-on — PR invalid)
1070
+ ⏸ #{NUMBER} — {title} → PR #{PR} awaiting-merge (remediated + re-approved — human merge only, no diagnosis needed)
1071
+ 🔗 #{NUMBER} — {title} → blocked-on-human-merge (gated by #{PRED}, will auto-dispatch on #{PRED} merge)
407
1072
  ⏳ Progress: {completed}/{total} complete, {active} active, {blocked} blocked
408
1073
  → Dispatched #{NEWLY_READY} (predecessor #{PRED} completed)
409
1074
  ```
410
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
+
1087
+ `⏸` (awaiting-merge) is deliberately distinct from `⚠` (blocked/bypass) — do not collapse the
1088
+ two. `⚠` means the pipeline hit something it cannot resolve and a human must diagnose it;
1089
+ `⏸` means the PR already cleared re-review and only needs a merge click. `🔗` (blocked-on-human-merge,
1090
+ forge#1812) is distinct from both: it marks a DEPENDENT of a GATED predecessor (item 6.5) — the
1091
+ dependent itself has no problem at all, it is simply waiting on someone else's merge. See Phase 6's
1092
+ "Merge-Ready" report section (`phase-6-report.md` Step 6A.5/6B) for the batch-level rollup.
1093
+
411
1094
  9. **Run staging integrity check** (from Step 4A-pre) if the completed agent merged a PR targeting staging.
412
1095
 
413
- **Termination condition**: All issues in the DAG are in a terminal state (merged, invalid, needs-human, or skipped due to dependency failure). When this 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.
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.
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.
414
1101
 
415
1102
  **Anti-pattern — DO NOT DO THIS:**
416
1103
  - `sleep 60/120/180/300` loops to check status — you will be notified automatically
@@ -437,9 +1124,12 @@ STALL_TIMEOUT=$(yq '.pipeline.stall_timeout_minutes // 15' forge.yaml 2>/dev/nul
437
1124
  **For each non-terminal agent in the current batch**:
438
1125
  ```bash
439
1126
  for NUM in {active_issue_numbers}; do
440
- # Skip issues already in terminal state
1127
+ # Skip issues already in a terminal-for-this-agent state (merged/invalid/needs-human/awaiting-merge).
1128
+ # workflow:awaiting-merge MUST be included here (forge#1812) — otherwise the stall detector
1129
+ # re-escalates an already-remediated-and-re-approved PR back to needs-human after STALL_TIMEOUT,
1130
+ # silently collapsing the Awaiting-Merge/Blocked distinction forge#1811 introduced.
441
1131
  TERMINAL=$(gh issue view $NUM -R {GH_REPO} --json labels \
442
- --jq '[.labels[].name | select(. == "workflow:merged" or . == "workflow:invalid" or . == "needs-human")] | length')
1132
+ --jq '[.labels[].name | select(. == "workflow:merged" or . == "workflow:invalid" or . == "needs-human" or . == "workflow:awaiting-merge")] | length')
443
1133
  [ "$TERMINAL" -gt 0 ] && continue
444
1134
 
445
1135
  # Get last activity timestamp — prefer last comment (catches FORGE:HEARTBEAT updates)
@@ -528,16 +1218,18 @@ gh issue list -R {GH_REPO} --state open --label "review-finding" --limit 20 \
528
1218
  For each spawned finding, determine whether it should be **executed** or **deferred**:
529
1219
 
530
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.
531
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.
532
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.
533
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.
534
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).
535
1227
 
536
- **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.
537
1229
 
538
1230
  **Execute** (add to the DAG) if:
539
- - Rule 2 matches (P1 or P2)
540
- - 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
541
1233
 
542
1234
  **Before running the loop, build the batch file list (MANDATORY for Heuristic 3):**
543
1235
 
@@ -573,8 +1265,13 @@ for FINDING_NUM in {spawned_finding_numbers}; do
573
1265
  PRIORITY=$(echo "$FINDING_DATA" | jq -r '.labels[] | select(startswith("priority:P")) | ltrimstr("priority:")' | head -1)
574
1266
  TITLE=$(echo "$FINDING_DATA" | jq -r '.title')
575
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"
576
1273
  # Heuristic 1: Generation check — source issue has review-finding label (always defer, even for P1/P2)
577
- 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) && \
578
1275
  [ -n "$SOURCE_NUM" ] && \
579
1276
  gh issue view $SOURCE_NUM -R {GH_REPO} --json labels --jq '[.labels[].name]' 2>/dev/null | grep -q "review-finding"; then
580
1277
  DEFER=true; DEFER_REASON="generation >= 2 (source #${SOURCE_NUM} is also a review-finding)"
@@ -607,6 +1304,159 @@ for FINDING_NUM in {spawned_finding_numbers}; do
607
1304
  done
608
1305
  ```
609
1306
 
1307
+ **Surface-area batching for queued P3 findings (MANDATORY check before dispatch):** <!-- Added: forge#1818 -->
1308
+
1309
+ Cascade-spawned findings collected within a single `/orchestrate` run never pass back through Phase 1's batching rule — Phase 1 only runs once, at the start. Without a check here, same-file P3 findings spawned mid-run always dispatch individually, defeating the batching policy for exactly the findings it exists to catch. Apply the same grouping rule from `commands/orchestrate/phase-1-resolve.md` ("P3 Review-Finding Batching") to `QUEUED_FINDINGS` before the dispatch step below:
1310
+
1311
+ ```bash
1312
+ # Group QUEUED_FINDINGS by exact affected file, reusing the SAME safety
1313
+ # exclusions as phase-1-resolve.md. Both sites go through jq test() (Oniguruma)
1314
+ # with identical patterns — NOT grep ERE — so the two batching checks cannot
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
1323
+ declare -A SURFACE_FILE_MEMBERS
1324
+
1325
+ # Defensive cap on gh issue view fan-out. QUEUED_FINDINGS is already bounded by
1326
+ # upstream cascade control; this cap holds even if that bound is later loosened,
1327
+ # so the loop can never scale API calls linearly with cascade-seeded findings. <!-- forge#1836 -->
1328
+ MAX_BATCH_SCAN=50
1329
+ SCANNED=0
1330
+
1331
+ for FINDING_NUM in "${QUEUED_FINDINGS[@]}"; do
1332
+ SCANNED=$((SCANNED + 1))
1333
+ if [ "$SCANNED" -gt "$MAX_BATCH_SCAN" ]; then
1334
+ echo "Surface-area batching: reached MAX_BATCH_SCAN=$MAX_BATCH_SCAN — remaining findings stay individually queued"
1335
+ break
1336
+ fi
1337
+
1338
+ FINDING_DATA=$(gh issue view $FINDING_NUM -R {GH_REPO} --json title,body,labels \
1339
+ --jq '{title: .title, body: .body, labels: [.labels[].name]}')
1340
+
1341
+ # Safety exclusions — never batch, at any priority. Same jq test() engine and
1342
+ # patterns as phase-1-resolve.md's batching rule (single shared mechanism).
1343
+ echo "$FINDING_DATA" | jq -e '
1344
+ (.title | test("security|billing|anti-bot|auth"; "i"))
1345
+ or (.body | test("## Problem[\\s\\S]{0,500}(security|billing|anti-bot|auth)"; "i"))
1346
+ or ([.labels[]] | any(. == "security" or . == "billing" or . == "anti-bot" or . == "auth"))
1347
+ ' >/dev/null && continue
1348
+
1349
+ # Only P3 findings are eligible (P1/P2 already dispatched individually above)
1350
+ echo "$FINDING_DATA" | jq -e '[.labels[]] | any(. == "priority:P3")' >/dev/null || continue
1351
+
1352
+ FINDING_FILE=$(echo "$FINDING_DATA" | jq -r '.body' | grep -oE '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|sh|md)`' | head -1 | tr -d '`')
1353
+ [ -z "$FINDING_FILE" ] && continue
1354
+
1355
+ SURFACE_FILE_MEMBERS["$FINDING_FILE"]="${SURFACE_FILE_MEMBERS[$FINDING_FILE]} $FINDING_NUM"
1356
+ done
1357
+
1358
+ # For each same-file cluster of 2+, actually CREATE the batch issue (executable —
1359
+ # mirrors phase-1-resolve.md's "Batch creation rule") and REPLACE the members with
1360
+ # the batch issue in QUEUED_FINDINGS so the dispatch step below never double-dispatches
1361
+ # them. This is what makes SURFACE_BATCHED_FINDINGS a live control, not dead wiring. <!-- forge#1832, forge#1834 -->
1362
+ for FILE in "${!SURFACE_FILE_MEMBERS[@]}"; do
1363
+ MEMBERS=(${SURFACE_FILE_MEMBERS[$FILE]})
1364
+ [ "${#MEMBERS[@]}" -ge 2 ] || continue
1365
+
1366
+ # Sanitize the affected-file path before interpolating it into the issue title/body.
1367
+ # Git filenames can legally carry shell metacharacters (`$()`, backticks, quotes);
1368
+ # restrict to a validated charset so the value cannot break the gh argument
1369
+ # boundary from an untrusted issue body. Shared guard with phase-1-resolve.md. <!-- forge#1833, forge#1835 -->
1370
+ SAFE_SURFACE_AREA=$(printf '%s' "$FILE" | tr -cd 'A-Za-z0-9._/-')
1371
+
1372
+ echo "Same-run surface-area cluster: ${#MEMBERS[@]} P3 findings share $FILE — creating batch issue(s)"
1373
+
1374
+ # Cap at 8 members per batch (phase-1-resolve.md limit); split into batches of <=8.
1375
+ for START in $(seq 0 8 $(( ${#MEMBERS[@]} - 1 ))); do
1376
+ CHUNK=("${MEMBERS[@]:$START:8}")
1377
+ [ "${#CHUNK[@]}" -ge 2 ] || continue
1378
+
1379
+ MEMBER_LINES=""
1380
+ for M in "${CHUNK[@]}"; do
1381
+ MTITLE=$(gh issue view "$M" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "")
1382
+ MEMBER_LINES="${MEMBER_LINES}- [ ] #${M}: ${MTITLE}"$'\n'
1383
+ done
1384
+
1385
+ BATCH_ISSUE_NUM=$(gh issue create {GH_FLAG} \
1386
+ --title "fix(batch): P3 review findings — ${SAFE_SURFACE_AREA} (same-run batch)" \
1387
+ --label "review-finding,priority:P3,batch" \
1388
+ --body "$(cat <<BATCH_EOF
1389
+ ## Problem
1390
+
1391
+ Batch of P3 review findings in **${SAFE_SURFACE_AREA}** (same file), clustered mid-run by phase-4-execution.md to reduce per-finding pipeline overhead.
1392
+
1393
+ ## Member Findings
1394
+
1395
+ <!-- FORGE:BATCH_MEMBERS -->
1396
+ ${MEMBER_LINES}<!-- /FORGE:BATCH_MEMBERS -->
1397
+
1398
+ ## Acceptance Criteria
1399
+
1400
+ - [ ] All member findings addressed or closed as false-positive
1401
+ - [ ] Member issues auto-closed with reference to this batch PR on merge
1402
+ - [ ] No security, billing, anti-bot, or auth paths touched (validated before batching)
1403
+
1404
+ <!-- FORGE:BATCHABLE -->
1405
+ BATCH_EOF
1406
+ )" --json number --jq '.number')
1407
+
1408
+ # Consume the cluster: record members and REPLACE them in QUEUED_FINDINGS
1409
+ # with the single batch issue, so the dispatch step below operates on the
1410
+ # batch unit and skips the individual members.
1411
+ SURFACE_BATCHED_FINDINGS+=("${CHUNK[@]}")
1412
+ [ -n "$BATCH_ISSUE_NUM" ] && SURFACE_BATCH_COUNT=$((SURFACE_BATCH_COUNT + 1)) # forge#1858 Step 6B counter
1413
+ QUEUED_FINDINGS=($(printf '%s\n' "${QUEUED_FINDINGS[@]}" | grep -vxF -f <(printf '%s\n' "${CHUNK[@]}") || true))
1414
+ [ -n "$BATCH_ISSUE_NUM" ] && QUEUED_FINDINGS+=("$BATCH_ISSUE_NUM")
1415
+ echo "Batched ${#CHUNK[@]} findings into #${BATCH_ISSUE_NUM}; members removed from QUEUED_FINDINGS and the DAG."
1416
+ done
1417
+ done
1418
+ ```
1419
+
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.
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
+
610
1460
  **For queued (non-deferred) findings:**
611
1461
 
612
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.
@@ -615,7 +1465,7 @@ done
615
1465
  ```
616
1466
  Agent #{COMPLETED} spawned {count} new finding issues: #{A}, #{B}
617
1467
  Added to DAG: #{A} (predecessors: {}), #{B} (predecessors: {#{X}})
618
- Deferred (cascade control): #{C} (P3 same-file), #{D} (comment heuristic)
1468
+ Deferred (cascade control): #{C} (P3 same-file), #{D} (comment heuristic), #{E} (token budget)
619
1469
  ```
620
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.
621
1471
 
@@ -761,11 +1611,13 @@ fi
761
1611
  ### Step 4E: Handle individual agent failures
762
1612
 
763
1613
  If an agent reports failure or error:
764
- - **Merge conflict**: Report to user, mark issue as needing human attention
765
- - **Invalid issue**: Already handled by the agent (closed with comment) — just report it
766
- - **Build/test failure**: Report the error, suggest manual intervention
767
- - **Agent timeout**: Report which issue timed out, suggest re-running with `/work-on #{N}`
768
- - **Dependency cascade**: Mark all transitive dependents in the DAG as "skipped — dependency #{X} failed"
1614
+ - **Merge conflict**: Report to user, mark issue as needing human attention (`needs-human`). This classifies as **GATED**, not FAILED — see Predecessor Classification in Step 4B. Its dependents follow the Dependency cascade rule below, not a hard skip.
1615
+ - **Invalid issue**: Already handled by the agent (closed with comment) — just report it. This classifies as **FAILED**.
1616
+ - **Build/test failure**: Report the error, suggest manual intervention. This classifies as **FAILED**.
1617
+ - **Agent timeout**: Report which issue timed out, suggest re-running with `/work-on #{N}`. Not yet terminal — leave dependents in `IN_PROGRESS` wait, no cascade action.
1618
+ - **Dependency cascade** <!-- Updated: forge#1812 -->: Re-run `classify_predecessor_state` (Step 4B) for the failed/gated issue before cascading — do NOT assume every entry above is a hard failure:
1619
+ - If it classifies **FAILED** (invalid, or build/test failure): mark all transitive dependents in the DAG as "skipped — dependency #{X} failed" (same as Step 4B item 6).
1620
+ - If it classifies **GATED** (merge conflict → `needs-human`, or `workflow:awaiting-merge`): do NOT mark dependents skipped. Instead apply Step 4B item 6.5 — track each direct dependent as `blocked-on-human-merge` against this predecessor, so it auto-dispatches via item 6.6 the moment the predecessor reaches `workflow:merged`.
769
1621
 
770
1622
  **Do NOT retry failed agents automatically.** Report the failure and let the user decide.
771
1623
 
@@ -775,11 +1627,13 @@ If an agent reports failure or error:
775
1627
 
776
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.
777
1629
 
778
- **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**
779
1631
 
780
1632
  ```bash
781
1633
  PERMANENT_DEFERRED=()
782
1634
  SWEEP_CANDIDATES=()
1635
+ IDLE_DEFERRED=() # <!-- Added: forge#1814 -->
1636
+ TOKEN_GATED=() # <!-- Added: forge#1858 -->
783
1637
 
784
1638
  for FINDING_NUM in "${DEFERRED_FINDINGS[@]}"; do
785
1639
  DEFER_REASON="${DEFERRED_REASONS[$FINDING_NUM]}"
@@ -787,13 +1641,28 @@ for FINDING_NUM in "${DEFERRED_FINDINGS[@]}"; do
787
1641
  # Generation >= 2 deferrals are PERMANENT — unbounded cascade prevention
788
1642
  if echo "$DEFER_REASON" | grep -qi "generation"; then
789
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)
790
1659
  else
791
1660
  # All other deferrals (comment/typo, P3 same-file) are re-evaluable
792
1661
  SWEEP_CANDIDATES+=($FINDING_NUM)
793
1662
  fi
794
1663
  done
795
1664
 
796
- 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"
797
1666
  ```
798
1667
 
799
1668
  **Step 4F.2: Re-evaluate sweep candidates**
@@ -829,6 +1698,70 @@ for FINDING_NUM in "${SWEEP_CANDIDATES[@]}"; do
829
1698
  done
830
1699
  ```
831
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
+
832
1765
  **Step 4F.3: Dispatch cleared findings**
833
1766
 
834
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).
@@ -888,7 +1821,10 @@ if [ ${#SWEEP_EXECUTE[@]} -gt 0 ]; then
888
1821
  # synthesis brief (Phase 2.5 runs only over the original batch's investigations), so
889
1822
  # there is nothing to prefer. Keep this block in sync with 4A's fallback branch only.
890
1823
  GIST_CONTEXT=""
1824
+ # Markdown emphasis markers (**bold**, __bold__, *italic*) are stripped before matching —
1825
+ # kept in sync with 4A's fallback branch above.
891
1826
  PARENT_INV=$(gh issue view "$FINDING_NUM" -R {GH_REPO} --json body --jq '.body' \
1827
+ | sed -E 's/[*_]+//g' \
892
1828
  | grep -oP '(?i)parent[: ]*#\K\d+|spawned from[: ]*#\K\d+' | head -1)
893
1829
 
894
1830
  if [ -n "$PARENT_INV" ] && [ -n "${INVESTIGATION_GISTS[$PARENT_INV]:-}" ]; then
@@ -910,7 +1846,7 @@ The context-gathering phase can fetch this index to discover all investigation G
910
1846
  # the complete pipeline contract. Keep in sync with Step 4A when the template changes.
911
1847
  Agent(
912
1848
  subagent_type="general-purpose",
913
- model="sonnet",
1849
+ model="{SUBAGENT_MODEL}",
914
1850
  description="Work on {PROJECT_PREFIX}#${FINDING_NUM}",
915
1851
  run_in_background=true,
916
1852
  prompt="You are working on GitHub issue #${FINDING_NUM} for the {PROJECT_NAME} project.
@@ -921,21 +1857,21 @@ The context-gathering phase can fetch this index to discover all investigation G
921
1857
 
922
1858
  **YOUR MISSION**: Invoke \`/work-on\` via the Skill tool and let it run to completion. \`/work-on\` is a self-contained routing loop that handles the ENTIRE pipeline: investigate → build (context → architect → implement → validate) → review (push → PR → /review-pr --auto-merge) → close (project board → trajectory log → worktree cleanup). Do NOT intervene, compensate, or manually close issues — \`/work-on\` handles everything including issue closure and label updates in its close phase.
923
1859
 
924
- **CRITICAL — DO NOT STOP EARLY**: /work-on runs as a multi-phase routing loop. Each phase (investigate, build, review, close) returns an intermediate result — these are NOT completion signals. You are NOT done until the issue reaches a terminal state: \`workflow:merged\`, \`workflow:invalid\`, or \`needs-human\`. If /work-on returns after only one phase (e.g., investigation), you MUST invoke it again immediately — it will re-read GitHub state and continue to the next phase. Keep invoking /work-on until it reaches a terminal state. Never output 'done' or stop after an intermediate result.
1860
+ **CRITICAL — DO NOT STOP EARLY**: /work-on runs as a multi-phase routing loop. Each phase (investigate, build, review, close) returns an intermediate result — these are NOT completion signals. You are NOT done until the issue reaches a terminal state: \`workflow:merged\`, \`workflow:invalid\`, \`needs-human\`, or \`workflow:awaiting-merge\`. If /work-on returns after only one phase (e.g., investigation), you MUST invoke it again immediately — it will re-read GitHub state and continue to the next phase. Keep invoking /work-on until it reaches a terminal state. Never output 'done' or stop after an intermediate result.
925
1861
 
926
1862
  **HOW REVIEW FINDINGS WORK**: /review-pr may create GitHub issues (with \`review-finding\` label) for findings it discovers. These are NOT blockers — they are separate work items that will go through their own /work-on pipeline later. The original PR should ALWAYS merge after review. The only exception is build errors (code doesn't compile) — those must be fixed before merging.
927
1863
 
928
1864
  **IMPORTANT RULES**:
929
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.
930
- - For default repo issues: \`Skill(skill='work-on', args='${FINDING_NUM}')\`
931
- - 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)
932
1868
  - NEVER bypass /work-on with manual git/gh commands — the label updates and structured comments are critical for tracking
933
1869
  - NEVER target \`main\` for PRs targeting the default repo. Use \`{STAGING_BRANCH}\` for fast-lane issues, or \`milestone/{slug}\` for milestone issues.
934
1870
  - Satellite repos (MCP, n8n) have no staging branch — fast-lane PRs go to \`main\` for those.
935
1871
  - If the issue is INVALID after investigation, close it with a comment explaining why
936
1872
  - If you hit merge conflicts or blockers, post a comment on the issue and STOP — do not force anything
937
1873
  - Do not interact with the user — you are running autonomously in the background
938
- - **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.
939
1875
 
940
1876
  **LABEL-STATE LOOP CONTRACT — enforce after EVERY Skill return**:
941
1877
  After EVERY \`Skill(skill='work-on', ...)\` call returns, immediately check the issue's current workflow label:
@@ -943,8 +1879,8 @@ After EVERY \`Skill(skill='work-on', ...)\` call returns, immediately check the
943
1879
  gh issue view ${FINDING_NUM} -R {GH_REPO} --json labels --jq '[.labels[].name | select(startswith(\"workflow:\"))]'
944
1880
  \`\`\`
945
1881
  **Terminal labels** (only these allow you to stop): \`workflow:merged\`, \`workflow:invalid\`
946
- **Terminal condition also**: \`needs-human\` label present OR issue state is \`closed\`
947
- 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.
1882
+ **Terminal condition also**: \`needs-human\` label present, \`workflow:awaiting-merge\` label present, OR issue state is \`closed\`
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.
948
1884
 
949
1885
  **CRITICAL — SOURCE BRANCH DETECTION**:
950
1886
  - If the issue has the \`review-finding\` label, read the issue body for \`**Code branch**: \\\`{branch}\\\`\`
@@ -973,14 +1909,52 @@ Completion Sweep Results:
973
1909
  Dispatched: #{A}, #{B} (file overlap cleared after DAG drain)
974
1910
  Still deferred (cosmetic): #{C} (comment/typo)
975
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)
976
1916
  ```
977
1917
 
978
- **After sweep agents complete** (or if no findings were dispatched): proceed to Phase 5.
1918
+ **After sweep agents complete** (or if no findings were dispatched): output the budget deferred-issues report (if applicable), then proceed to Phase 5.
979
1919
 
980
1920
  **Anti-patterns — DO NOT DO THIS:**
981
1921
  - Re-sweeping findings spawned during the sweep itself — this creates unbounded recursion. Sweep is a single pass.
982
1922
  - Overriding generation >= 2 deferrals — the cascade cap is absolute.
983
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.
1926
+
1927
+ ### Step 4F.5: Budget Deferred-Issues Report (conditional) <!-- Added: forge#1743 -->
1928
+
1929
+ **Run only when `BUDGET_LIMIT != "Infinity"` AND `${#DEFERRED_BUDGET_ISSUES[@]} > 0`.**
1930
+
1931
+ Deferred issues are issues that were not dispatched because their estimated cost would have pushed projected spend past the budget ceiling (after reserving ε for no-prior issues). They are **never silently dropped** — this report makes them visible and actionable.
1932
+
1933
+ ```bash
1934
+ if [ "$BUDGET_LIMIT" != "Infinity" ] && [ "${#DEFERRED_BUDGET_ISSUES[@]}" -gt 0 ]; then
1935
+ echo ""
1936
+ echo "## Budget Report"
1937
+ echo ""
1938
+ echo "**Budget limit**: \$${BUDGET_LIMIT}"
1939
+ echo "**Projected spend (dispatched issues)**: \$${PROJECTED_SPEND}"
1940
+ echo "**Actual spend (completed issues, best-effort)**: \$${ACTUAL_SPEND}"
1941
+ echo "**ε-reserve used**: ${EPSILON_DISPATCHED} (10% = \$${EPSILON_BUDGET})"
1942
+ echo ""
1943
+ echo "### Deferred Issues (budget exhausted — never silently dropped)"
1944
+ echo ""
1945
+ echo "| Issue | Title | Score | Est. Cost | Reason |"
1946
+ echo "|-------|-------|-------|-----------|--------|"
1947
+ for DNUM in "${DEFERRED_BUDGET_ISSUES[@]}"; do
1948
+ DTITLE=$(gh issue view "$DNUM" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "(unknown)")
1949
+ DSCORE="${ISSUE_SCORE[$DNUM]:-?}"
1950
+ DCOST="${ISSUE_COST_ESTIMATE[$DNUM]:-?}"
1951
+ echo "| #${DNUM} | ${DTITLE} | ${DSCORE} | \$${DCOST} | Budget ceiling reached |"
1952
+ done
1953
+ echo ""
1954
+ DEFERRED_LIST=$(IFS=' '; echo "${DEFERRED_BUDGET_ISSUES[*]}")
1955
+ echo "**Action**: Re-run \`/orchestrate ${DEFERRED_LIST} [--budget N]\` to process deferred issues, or increase \`--budget\`."
1956
+ fi
1957
+ ```
984
1958
 
985
1959
  ---
986
1960