forgedock 1.1.7 → 1.1.8

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 (37) hide show
  1. package/bin/engine/invariants.mjs +361 -0
  2. package/bin/hooks/pre-tool-use.mjs +182 -0
  3. package/bin/labels.json +10 -0
  4. package/bin/recall.mjs +540 -0
  5. package/commands/autopilot.md +957 -76
  6. package/commands/cleanup.md +52 -3
  7. package/commands/optimize.md +585 -0
  8. package/commands/orchestrate/phase-1-resolve.md +35 -25
  9. package/commands/orchestrate/phase-3-dependency.md +465 -34
  10. package/commands/orchestrate/phase-4-execution.md +447 -28
  11. package/commands/orchestrate/phase-6-report.md +198 -9
  12. package/commands/pipeline-health/phase-2-metrics.md +305 -1
  13. package/commands/pipeline-health/phase-4-proposals.md +127 -0
  14. package/commands/quality-gate.md +417 -0
  15. package/commands/review-pr.md +391 -13
  16. package/commands/spec-doctor.md +429 -0
  17. package/commands/validate.md +52 -0
  18. package/commands/work-on/build/architect.md +77 -0
  19. package/commands/work-on/build/context.md +357 -22
  20. package/commands/work-on/build/implement.md +2 -0
  21. package/commands/work-on/build/validate.md +28 -0
  22. package/commands/work-on/build.md +44 -0
  23. package/commands/work-on/close.md +621 -8
  24. package/commands/work-on/investigate.md +221 -0
  25. package/commands/work-on.md +15 -14
  26. package/docs/FORGE-PROTOCOL.md +101 -0
  27. package/docs/spec/forge-protocol-v1.md +97 -5
  28. package/docs/spec/label-state-machine.md +6 -1
  29. package/package.json +2 -2
  30. package/packages/protocol/src/cli.js +374 -84
  31. package/packages/protocol/src/types.js +75 -1
  32. package/scripts/build-knowledge-index.mjs +1761 -0
  33. package/scripts/calibration.mjs +996 -0
  34. package/scripts/danger-zones.mjs +782 -0
  35. package/scripts/doctor-pipeline-state.sh +88 -2
  36. package/scripts/select-fix-targets.sh +234 -0
  37. package/scripts/transition-label.sh +157 -6
@@ -8,6 +8,93 @@ install: internal
8
8
 
9
9
  ## Phase 4: Streaming DAG Execution
10
10
 
11
+ ### Step 4A-pre.0: Budget initialization (MANDATORY when --budget is set) <!-- Added: forge#1743 -->
12
+
13
+ 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).
14
+
15
+ ```bash
16
+ # --- Budget initialization ---
17
+ # Parse --budget N from ARGUMENTS (e.g. /orchestrate fast-lane --budget 5.00)
18
+ BUDGET_LIMIT=$(echo "${ARGUMENTS:-}" | grep -oP '(?<=--budget )\S+' | head -1 || echo "")
19
+ if [ -z "$BUDGET_LIMIT" ] || ! echo "$BUDGET_LIMIT" | grep -qP '^\d+(\.\d+)?$'; then
20
+ BUDGET_LIMIT="Infinity"
21
+ fi
22
+
23
+ PROJECTED_SPEND="0" # sum of ISSUE_COST_ESTIMATE[] for dispatched issues
24
+ ACTUAL_SPEND="0" # sum of actual cost reported by completed agents (best-effort)
25
+ DEFERRED_BUDGET_ISSUES=() # issues deferred because projected spend would exceed budget
26
+ EPSILON_DISPATCHED=false # true once at least one ε-reserve issue has been dispatched
27
+
28
+ if [ "$BUDGET_LIMIT" != "Infinity" ]; then
29
+ EPSILON_BUDGET=$(echo "scale=4; $BUDGET_LIMIT * 0.10" | bc 2>/dev/null || echo "0")
30
+ echo "Budget initialized: BUDGET_LIMIT=\$${BUDGET_LIMIT} EPSILON_BUDGET=\$${EPSILON_BUDGET} (10% ε-reserve)"
31
+ echo "Issues without cost priors (ε-reserve eligible): ${NO_PRIOR_ISSUES[*]:-none}"
32
+ else
33
+ EPSILON_BUDGET="0"
34
+ echo "Budget: uncapped (no --budget flag) — dispatching all ready issues by score order"
35
+ fi
36
+ # --- End budget initialization ---
37
+ ```
38
+
39
+ **Budget halt condition** (checked in Step 4A before each dispatch):
40
+
41
+ Before dispatching issue `NUM`, check whether its estimated cost would exceed the remaining budget:
42
+
43
+ ```bash
44
+ # Check budget before dispatching NUM
45
+ should_dispatch() {
46
+ local NUM="$1"
47
+ local COST="${ISSUE_COST_ESTIMATE[$NUM]:-0.35}"
48
+
49
+ if [ "$BUDGET_LIMIT" = "Infinity" ]; then
50
+ return 0 # uncapped — always dispatch
51
+ fi
52
+
53
+ NEW_PROJECTED=$(echo "scale=4; $PROJECTED_SPEND + $COST" | bc 2>/dev/null || echo "$PROJECTED_SPEND")
54
+ MAIN_CEILING=$(echo "scale=4; $BUDGET_LIMIT - $EPSILON_BUDGET" | bc 2>/dev/null || echo "$BUDGET_LIMIT")
55
+
56
+ # ε-reserve logic: if this is a no-prior issue AND ε-budget has not yet been
57
+ # used AND the no-prior issue has NOT been dispatched yet, allow it even if
58
+ # the main ceiling is hit (up to BUDGET_LIMIT total).
59
+ if [ "${ISSUE_HAS_PRIOR[$NUM]:-false}" = "false" ] && [ "$EPSILON_DISPATCHED" = "false" ]; then
60
+ if echo "$NEW_PROJECTED $BUDGET_LIMIT" | awk '{exit ($1 <= $2) ? 0 : 1}' 2>/dev/null; then
61
+ EPSILON_DISPATCHED=true
62
+ echo "ε-reserve: dispatching #${NUM} (no-prior issue) from exploration reserve"
63
+ return 0
64
+ fi
65
+ fi
66
+
67
+ # Main budget check: defer if projected would exceed main ceiling
68
+ if echo "$NEW_PROJECTED $MAIN_CEILING" | awk '{exit ($1 <= $2) ? 0 : 1}' 2>/dev/null; then
69
+ DEFERRED_BUDGET_ISSUES+=("$NUM")
70
+ echo "BUDGET DEFER: #${NUM} (est. \$${COST}) would push projected spend to \$${NEW_PROJECTED} > main ceiling \$${MAIN_CEILING}"
71
+ return 1
72
+ fi
73
+
74
+ return 0
75
+ }
76
+ ```
77
+
78
+ **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):
79
+
80
+ ```
81
+ ## Budget Report
82
+
83
+ **Budget limit**: $${BUDGET_LIMIT}
84
+ **Projected spend (dispatched issues)**: $${PROJECTED_SPEND}
85
+ **ε-reserve used**: ${EPSILON_DISPATCHED} (10% = $${EPSILON_BUDGET})
86
+
87
+ ### Deferred Issues (budget exhausted — never silently dropped)
88
+
89
+ | Issue | Title | Score | Est. Cost | Reason |
90
+ |-------|-------|-------|-----------|--------|
91
+ | #{N} | {title} | {score} | ${cost} | Budget ceiling reached |
92
+
93
+ **Action**: Re-run `/orchestrate {deferred_issue_numbers} [--budget N]` to process deferred issues, or increase `--budget`.
94
+ ```
95
+
96
+ **When `BUDGET_LIMIT = Infinity`**: skip this check and report entirely — uncapped behavior.
97
+
11
98
  ### Step 4A-pre: Staging baseline tracking (MANDATORY — continuous)
12
99
 
13
100
  **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.
@@ -174,14 +261,26 @@ Use `${ISSUE_LANE[$NUM]}` and `${ISSUE_PR_BASE[$NUM]}` to populate `{LANE}` and
174
261
  **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
262
 
176
263
  ```bash
177
- # Engine-first dispatch: check CLI availability, then dispatch ready issues in parallel
264
+ # Engine-first dispatch: check CLI availability, then dispatch ready issues in score order
265
+ # Uses SORTED_READY_SET[] from Step 3E.5 (descending value/cost) and budget gate from Step 4A-pre.0
178
266
  FORGEDOCK_AVAILABLE=$(command -v forgedock >/dev/null 2>&1 && echo "true" || echo "false")
179
267
 
180
268
  if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
181
- for NUM in {ready_issue_numbers}; do
269
+ for NUM in "${SORTED_READY_SET[@]:-{ready_issue_numbers}}"; do
270
+ # Budget gate (forge#1743): skip dispatch if projected spend would exceed budget ceiling.
271
+ # should_dispatch() also handles ε-reserve (no-prior issues get guaranteed slot).
272
+ if ! should_dispatch "$NUM"; then
273
+ continue # Issue added to DEFERRED_BUDGET_ISSUES[] by should_dispatch()
274
+ fi
275
+
182
276
  LANE="${ISSUE_LANE[$NUM]}"
183
277
  PR_BASE="${ISSUE_PR_BASE[$NUM]}"
184
- echo "Dispatching #$NUM via forgedock run-issue --lane $PR_BASE"
278
+ COST="${ISSUE_COST_ESTIMATE[$NUM]:-0.35}"
279
+
280
+ # Advance PROJECTED_SPEND before forking so subsequent iterations see the updated total
281
+ PROJECTED_SPEND=$(echo "scale=4; $PROJECTED_SPEND + $COST" | bc 2>/dev/null || echo "$PROJECTED_SPEND")
282
+
283
+ echo "Dispatching #$NUM via forgedock run-issue --lane $PR_BASE (score=${ISSUE_SCORE[$NUM]:-?} est_cost=\$${COST} projected_total=\$${PROJECTED_SPEND})"
185
284
  forgedock run-issue "$NUM" --lane "$PR_BASE" &
186
285
  done
187
286
  wait
@@ -217,7 +316,7 @@ Agent(
217
316
 
218
317
  **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
318
 
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.
319
+ **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
320
 
222
321
  **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
322
 
@@ -239,7 +338,8 @@ After EVERY `Skill(skill='work-on', ...)` call returns, immediately check the is
239
338
  gh issue view {NUMBER} -R {GH_REPO} --json labels --jq '[.labels[].name | select(startswith("workflow:"))]'
240
339
  ```
241
340
  **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`
341
+ **Terminal condition also**: `needs-human` label present, `workflow:awaiting-merge` label present, OR issue state is `closed`
342
+ `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`).
243
343
  If the label is NOT terminal (e.g., `workflow:investigating`, `workflow:ready-to-build`, `workflow:building`, `workflow:in-review`), invoke `Skill(skill='work-on', args='{NUMBER}')` again immediately. The `/work-on` skill will re-read GitHub state and advance to the next phase. Do NOT output a summary, do NOT pause, do NOT ask for confirmation — just invoke it again.
244
344
 
245
345
  **CRITICAL — SOURCE BRANCH DETECTION**:
@@ -297,6 +397,30 @@ fi
297
397
 
298
398
  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
399
 
400
+ **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.
401
+
402
+ ```bash
403
+ # Inject coordination issue URL if claims board was created in Step 3D.1
404
+ if [ -n "${FORGE_COORD_ISSUE:-}" ]; then
405
+ GIST_CONTEXT="${GIST_CONTEXT}
406
+
407
+ **ORCHESTRATION CLAIMS BOARD**: This agent is running under an orchestration batch.
408
+ Claims board issue URL: ${FORGE_COORD_ISSUE}
409
+
410
+ On build start (Phase B2 / Phase 3C of /work-on), post a FORGE:CLAIM annotation on the
411
+ coordination issue above. Required fields:
412
+ Holder: ##{NUMBER} / batch-$(date -u +%Y%m%dT%H%M%S)
413
+ Files: (list of files from your FORGE:CONTRACT deliverables table, one per line)
414
+ Interfaces: (public function/type signatures you will modify or that callers must preserve)
415
+ TTL: terminal state of Holder issue ##{NUMBER}
416
+
417
+ On reaching terminal state (workflow:merged, workflow:invalid, needs-human, or workflow:awaiting-merge), post
418
+ <!-- FORGE:CLAIM_RELEASED --> on the coordination issue to release your claim.
419
+
420
+ Set FORGE_COORD_ISSUE=${FORGE_COORD_ISSUE} in your environment so /work-on phases can read it."
421
+ fi
422
+ ```
423
+
300
424
  **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
425
 
302
426
  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.
@@ -339,23 +463,71 @@ You will be automatically notified when each background agent completes. **Do NO
339
463
 
340
464
  **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
465
 
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.
466
+ **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:
467
+
468
+ ```bash
469
+ classify_predecessor_state() {
470
+ local PRED="$1"
471
+ local PRED_INFO
472
+ # NOTE: the `workflow` array below deliberately also keeps `needs-human` — that label has NO
473
+ # `workflow:` prefix (see bin/labels.json), so a bare `select(startswith("workflow:"))` would
474
+ # drop it and the GATED branch's `needs-human` case would be dead code (forge#1812 primary case).
475
+ PRED_INFO=$(gh issue view "$PRED" -R {GH_REPO} --json labels,state \
476
+ --jq '{state: .state, workflow: [.labels[].name | select(startswith("workflow:") or . == "needs-human")]}' 2>/dev/null || echo '{}')
477
+ local PRED_STATE PRED_LABELS
478
+ PRED_STATE=$(echo "$PRED_INFO" | jq -r '.state // "OPEN"')
479
+ PRED_LABELS=$(echo "$PRED_INFO" | jq -r '.workflow[]?' 2>/dev/null)
480
+
481
+ if echo "$PRED_LABELS" | grep -qx "workflow:invalid"; then
482
+ echo "FAILED"
483
+ elif echo "$PRED_LABELS" | grep -qx "workflow:merged"; then
484
+ echo "DONE"
485
+ elif echo "$PRED_LABELS" | grep -qxE "needs-human|workflow:awaiting-merge"; then
486
+ echo "GATED"
487
+ elif [ "$PRED_STATE" = "CLOSED" ]; then
488
+ # Closed with no workflow:invalid/merged label (e.g. closed-not-planned) — treat as DONE,
489
+ # not a new deadlock state; there is no pending code for dependents to wait on.
490
+ echo "DONE"
491
+ else
492
+ echo "IN_PROGRESS"
493
+ fi
494
+ }
495
+ ```
496
+
497
+ - **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.
498
+ - **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).
499
+ - **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.
500
+ - **IN_PROGRESS** — predecessor is still mid-pipeline (`investigating`/`ready-to-build`/`building`/`in-review`). Dependent simply continues waiting; no special tracking needed.
501
+
502
+ A GATED predecessor whose PR later merges reclassifies to DONE the next time `classify_predecessor_state` runs (its label flips to `workflow:merged`) — this is exactly what the merge-triggered wake check (item 6.6 below) relies on.
503
+
504
+ **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
505
 
344
506
  ```bash
345
507
  # After each agent completion, check for newly ready issues:
346
508
  for BLOCKED_NUM in {all_blocked_issue_numbers}; do
347
509
  ALL_PREDS_DONE=true
510
+ ANY_PRED_GATED=false
511
+ GATING_PREDS=()
348
512
  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
513
+ PRED_STATE=$(classify_predecessor_state "$PRED")
514
+ case "$PRED_STATE" in
515
+ DONE) ;; # satisfied — no action
516
+ FAILED) ALL_PREDS_DONE=false ;; # handled by item 6 (skip dependents)
517
+ GATED)
518
+ ALL_PREDS_DONE=false
519
+ ANY_PRED_GATED=true
520
+ GATING_PREDS+=("$PRED")
521
+ ;; # handled by item 6.5 (blocked-on-human-merge)
522
+ IN_PROGRESS|*) ALL_PREDS_DONE=false ;; # just keep waiting
523
+ esac
355
524
  done
356
525
  if [ "$ALL_PREDS_DONE" = "true" ]; then
357
- echo "#{BLOCKED_NUM} is now READY — all predecessors resolved. Dispatching."
526
+ echo "#{BLOCKED_NUM} is now READY — all predecessors DONE (merged/resolved). Dispatching."
358
527
  # Add to dispatch batch for this completion cycle
528
+ elif [ "$ANY_PRED_GATED" = "true" ]; then
529
+ echo "#{BLOCKED_NUM} is BLOCKED-ON-HUMAN-MERGE — gated by: ${GATING_PREDS[*]}. See item 6.5."
530
+ # Do NOT dispatch. Do NOT mark skipped. Tracked via item 6.5.
359
531
  fi
360
532
  done
361
533
  # Run Steps 4A.pre.0 → 4A.pre → 4A for newly ready issues (batch them in a single message)
@@ -365,12 +537,16 @@ done
365
537
 
366
538
  1. **Check if the agent completed the FULL pipeline** — not just one phase:
367
539
  ```bash
368
- # Check final workflow state — only workflow:merged or workflow:invalid means truly done
540
+ # Check final workflow state — workflow:merged, workflow:invalid, needs-human, and
541
+ # workflow:awaiting-merge all mean this agent's own /work-on run has stopped (the last
542
+ # two are human-gated pauses, not completions). See Predecessor Classification above for
543
+ # how the DAG's readiness/failure logic treats these same labels differently from "this
544
+ # agent is done running."
369
545
  FINAL_STATE=$(gh issue view $NUM -R {GH_REPO} --json labels,state --jq '{state: .state, workflow: [.labels[].name | select(startswith("workflow:"))]}')
370
546
  echo "#{NUM}: $FINAL_STATE"
371
547
  ```
372
548
 
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**:
549
+ 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
550
  ```
375
551
  Agent(
376
552
  resume=AGENT_ISSUE_MAP[{NUMBER}],
@@ -383,11 +559,100 @@ done
383
559
 
384
560
  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
561
 
386
- 4. **Record completed results**: Success (PR merged), Invalid (issue closed), Blocked (needs human), or Error
562
+ 4. **Post CLAIM_RELEASED on coordination issue** (when `FORGE_COORD_ISSUE` is set): <!-- Added: forge#1736 -->
563
+ ```bash
564
+ # After verifying terminal state for issue NUM, release its claim on the coordination issue
565
+ if [ -n "${FORGE_COORD_ISSUE:-}" ]; then
566
+ COORD_NUM=$(echo "$FORGE_COORD_ISSUE" | grep -oE '[0-9]+$')
567
+ if [ -n "$COORD_NUM" ]; then
568
+ gh issue comment "$COORD_NUM" -R {GH_REPO} --body "<!-- FORGE:CLAIM_RELEASED -->
569
+ **Holder**: #${NUM} — reached terminal state: ${FINAL_WORKFLOW_STATE}
570
+ **Released**: $(date -u +%Y-%m-%dT%H:%M:%SZ)" 2>/dev/null || true
571
+ echo "CLAIM_RELEASED posted for #${NUM} on coordination issue #${COORD_NUM}"
572
+ fi
573
+ fi
574
+ ```
575
+
576
+ **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 -->
577
+
578
+ ```bash
579
+ # After CLAIM_RELEASED for issue NUM:
580
+ # Read all active FORGE:CLAIM annotations from coordination issue
581
+ if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_NUM:-}" ]; then
582
+ ACTIVE_CLAIMS=$(gh api repos/{GH_REPO}/issues/${COORD_NUM}/comments \
583
+ --jq '[.[] | select(.body | contains("<!-- FORGE:CLAIM -->")) |
584
+ select(.body | contains("<!-- FORGE:CLAIM_RELEASED -->") | not)] |
585
+ map({holder: (.body | capture("\\*\\*Holder\\*\\*: (?P<h>[^\\n]+)").h),
586
+ files: (.body | capture("\\*\\*Files\\*\\*: (?P<f>[^\\n]+)").f)})' 2>/dev/null || echo '[]')
587
+ # For each still-blocked issue in a Layer-2/4 pair: check if its claim's file set
588
+ # is disjoint from all remaining active claims. If so, mark it ready.
589
+ # (Layer-1 and Layer-3 edges are never relaxed — this check is Layer-2/4 only.)
590
+ for BLOCKED_NUM in {layer_2_4_blocked_issues}; do
591
+ BLOCKED_CLAIM_FILES=$(echo "$ACTIVE_CLAIMS" \
592
+ | jq -r --arg h "#${BLOCKED_NUM}" '.[] | select(.holder | startswith($h)) | .files' 2>/dev/null || echo "")
593
+ if [ -n "$BLOCKED_CLAIM_FILES" ]; then
594
+ # Compare file set with all other active claims — if disjoint, downgrade to ready
595
+ echo "Claims-board relaxation: checking if #${BLOCKED_NUM} can be unblocked based on disjoint claims"
596
+ # (Implementer: build a set-intersection check here using sorted file lists)
597
+ fi
598
+ done
599
+ fi
600
+ ```
601
+
602
+ 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
387
603
 
388
604
  5. **Check for newly unblocked issues** — run the DAG readiness check above. If any issues are now ready, dispatch them immediately (Steps 4A.pre.0 → 4A.pre → 4A). Batch all newly ready issues into a single dispatch message.
389
605
 
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.
606
+ 6. **Handle predecessor failures** — if a completed agent's issue classifies as `FAILED` (`workflow:invalid`, or an explicit build/test error — see Predecessor Classification above; `needs-human` and `workflow:awaiting-merge` are GATED, not FAILED — see item 6.5), check for dependent issues in the DAG. Mark all transitive dependents as "skipped — dependency #{X} failed" and report them. Do NOT dispatch them.
607
+
608
+ 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`:
609
+ ```bash
610
+ # Resolve PRED's open PR, if any, using the anchored search (forge#1634/#1646 precedent —
611
+ # do NOT fall back to a bare-number search here; a stale unrelated PR would misattribute gating).
612
+ GATING_PR=$(gh pr list -R {GH_REPO} --state open --search "\"Closes #${PRED}\" in:body" \
613
+ --json number --jq '.[0].number // empty' 2>/dev/null || echo "")
614
+ PRED_LABEL=$(gh issue view "$PRED" -R {GH_REPO} --json labels \
615
+ --jq '[.labels[].name | select(. == "needs-human" or . == "workflow:awaiting-merge")] | .[0] // "needs-human"' 2>/dev/null)
616
+
617
+ # Self-heal the label if it hasn't been bootstrapped yet (same pattern as review-pr.md 6C —
618
+ # colors match the canonical manifest bin/labels.json; --force makes this idempotent/cheap).
619
+ 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
620
+
621
+ # Idempotency: only post/label if not already tracked for this specific predecessor.
622
+ # Anchor on the exact "**Gating predecessor**: #N" label with a word boundary —
623
+ # a bare contains("#N") substring would false-match #50/#500 for predecessor #5. <!-- forge#1830 -->
624
+ ALREADY_TRACKED=$(gh api repos/{GH_REPO}/issues/${DEP}/comments \
625
+ --jq --arg prednum "${PRED}" '[.[] | select(.body | contains("FORGE:BLOCKED_ON_HUMAN_MERGE") and test("Gating predecessor\\*\\*: #" + $prednum + "\\b"))] | length' 2>/dev/null || echo "0")
626
+ if [ "$ALREADY_TRACKED" -eq 0 ]; then
627
+ gh issue comment "$DEP" -R {GH_REPO} --body "<!-- FORGE:BLOCKED_ON_HUMAN_MERGE -->
628
+ **Gating predecessor**: #${PRED} (state: \`${PRED_LABEL}\`${GATING_PR:+, open PR #${GATING_PR}})
629
+ **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\`."
630
+ gh issue edit "$DEP" -R {GH_REPO} --add-label "blocked-on-human-merge" 2>/dev/null || true
631
+ fi
632
+ ```
633
+ 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.
634
+
635
+ 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:
636
+ ```bash
637
+ WOKEN=$(gh issue list -R {GH_REPO} --state open --label "blocked-on-human-merge" --json number \
638
+ --jq '.[].number' 2>/dev/null || echo "")
639
+ for DEP in $WOKEN; do
640
+ IS_GATED_BY_THIS=$(gh api repos/{GH_REPO}/issues/${DEP}/comments \
641
+ --jq --arg prednum "${NUM}" '[.[] | select(.body | contains("FORGE:BLOCKED_ON_HUMAN_MERGE") and test("Gating predecessor\\*\\*: #" + $prednum + "\\b"))] | length' 2>/dev/null || echo "0")
642
+ [ "$IS_GATED_BY_THIS" -gt 0 ] || continue
643
+ # Idempotency: only dispatch if DEP hasn't already been dispatched by another path.
644
+ DEP_ALREADY_DISPATCHED=$(gh issue view "$DEP" -R {GH_REPO} --json labels \
645
+ --jq '[.labels[].name | select(startswith("workflow:"))] | length' 2>/dev/null || echo "0")
646
+ if [ "$DEP_ALREADY_DISPATCHED" -eq 0 ]; then
647
+ gh issue edit "$DEP" -R {GH_REPO} --remove-label "blocked-on-human-merge" 2>/dev/null || true
648
+ gh issue comment "$DEP" -R {GH_REPO} --body "<!-- FORGE:UNBLOCKED -->
649
+ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was tracked via a prior FORGE:BLOCKED_ON_HUMAN_MERGE comment.)"
650
+ echo "#{DEP} unblocked by #{NUM} merge — dispatching immediately (Steps 4A.pre.0 → 4A.pre → 4A)."
651
+ # Add DEP to the same-response dispatch batch
652
+ fi
653
+ done
654
+ ```
655
+ 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.
391
656
 
392
657
  7. **Verify pipeline compliance** — for each truly completed issue, check that the agent used `/work-on`:
393
658
  ```bash
@@ -404,13 +669,22 @@ done
404
669
  ✓ #{NUMBER} — {title} → PR #{PR} merged to {target}
405
670
  ✗ #{NUMBER} — {title} → {reason for failure}
406
671
  ⚠ #{NUMBER} — {title} → PIPELINE BYPASS (no /work-on — PR invalid)
672
+ ⏸ #{NUMBER} — {title} → PR #{PR} awaiting-merge (remediated + re-approved — human merge only, no diagnosis needed)
673
+ 🔗 #{NUMBER} — {title} → blocked-on-human-merge (gated by #{PRED}, will auto-dispatch on #{PRED} merge)
407
674
  ⏳ Progress: {completed}/{total} complete, {active} active, {blocked} blocked
408
675
  → Dispatched #{NEWLY_READY} (predecessor #{PRED} completed)
409
676
  ```
410
677
 
678
+ `⏸` (awaiting-merge) is deliberately distinct from `⚠` (blocked/bypass) — do not collapse the
679
+ two. `⚠` means the pipeline hit something it cannot resolve and a human must diagnose it;
680
+ `⏸` means the PR already cleared re-review and only needs a merge click. `🔗` (blocked-on-human-merge,
681
+ forge#1812) is distinct from both: it marks a DEPENDENT of a GATED predecessor (item 6.5) — the
682
+ dependent itself has no problem at all, it is simply waiting on someone else's merge. See Phase 6's
683
+ "Merge-Ready" report section (`phase-6-report.md` Step 6A.5/6B) for the batch-level rollup.
684
+
411
685
  9. **Run staging integrity check** (from Step 4A-pre) if the completed agent merged a PR targeting staging.
412
686
 
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.
687
+ **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.
414
688
 
415
689
  **Anti-pattern — DO NOT DO THIS:**
416
690
  - `sleep 60/120/180/300` loops to check status — you will be notified automatically
@@ -437,9 +711,12 @@ STALL_TIMEOUT=$(yq '.pipeline.stall_timeout_minutes // 15' forge.yaml 2>/dev/nul
437
711
  **For each non-terminal agent in the current batch**:
438
712
  ```bash
439
713
  for NUM in {active_issue_numbers}; do
440
- # Skip issues already in terminal state
714
+ # Skip issues already in a terminal-for-this-agent state (merged/invalid/needs-human/awaiting-merge).
715
+ # workflow:awaiting-merge MUST be included here (forge#1812) — otherwise the stall detector
716
+ # re-escalates an already-remediated-and-re-approved PR back to needs-human after STALL_TIMEOUT,
717
+ # silently collapsing the Awaiting-Merge/Blocked distinction forge#1811 introduced.
441
718
  TERMINAL=$(gh issue view $NUM -R {GH_REPO} --json labels \
442
- --jq '[.labels[].name | select(. == "workflow:merged" or . == "workflow:invalid" or . == "needs-human")] | length')
719
+ --jq '[.labels[].name | select(. == "workflow:merged" or . == "workflow:invalid" or . == "needs-human" or . == "workflow:awaiting-merge")] | length')
443
720
  [ "$TERMINAL" -gt 0 ] && continue
444
721
 
445
722
  # Get last activity timestamp — prefer last comment (catches FORGE:HEARTBEAT updates)
@@ -607,6 +884,114 @@ for FINDING_NUM in {spawned_finding_numbers}; do
607
884
  done
608
885
  ```
609
886
 
887
+ **Surface-area batching for queued P3 findings (MANDATORY check before dispatch):** <!-- Added: forge#1818 -->
888
+
889
+ 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:
890
+
891
+ ```bash
892
+ # Group QUEUED_FINDINGS by exact affected file, reusing the SAME safety
893
+ # exclusions as phase-1-resolve.md. Both sites go through jq test() (Oniguruma)
894
+ # with identical patterns — NOT grep ERE — so the two batching checks cannot
895
+ # classify the same issue body differently. <!-- forge#1837 -->
896
+ declare -A SURFACE_FILE_MEMBERS
897
+ SURFACE_BATCHED_FINDINGS=()
898
+
899
+ # Defensive cap on gh issue view fan-out. QUEUED_FINDINGS is already bounded by
900
+ # upstream cascade control; this cap holds even if that bound is later loosened,
901
+ # so the loop can never scale API calls linearly with cascade-seeded findings. <!-- forge#1836 -->
902
+ MAX_BATCH_SCAN=50
903
+ SCANNED=0
904
+
905
+ for FINDING_NUM in "${QUEUED_FINDINGS[@]}"; do
906
+ SCANNED=$((SCANNED + 1))
907
+ if [ "$SCANNED" -gt "$MAX_BATCH_SCAN" ]; then
908
+ echo "Surface-area batching: reached MAX_BATCH_SCAN=$MAX_BATCH_SCAN — remaining findings stay individually queued"
909
+ break
910
+ fi
911
+
912
+ FINDING_DATA=$(gh issue view $FINDING_NUM -R {GH_REPO} --json title,body,labels \
913
+ --jq '{title: .title, body: .body, labels: [.labels[].name]}')
914
+
915
+ # Safety exclusions — never batch, at any priority. Same jq test() engine and
916
+ # patterns as phase-1-resolve.md's batching rule (single shared mechanism).
917
+ echo "$FINDING_DATA" | jq -e '
918
+ (.title | test("security|billing|anti-bot|auth"; "i"))
919
+ or (.body | test("## Problem[\\s\\S]{0,500}(security|billing|anti-bot|auth)"; "i"))
920
+ or ([.labels[]] | any(. == "security" or . == "billing" or . == "anti-bot" or . == "auth"))
921
+ ' >/dev/null && continue
922
+
923
+ # Only P3 findings are eligible (P1/P2 already dispatched individually above)
924
+ echo "$FINDING_DATA" | jq -e '[.labels[]] | any(. == "priority:P3")' >/dev/null || continue
925
+
926
+ FINDING_FILE=$(echo "$FINDING_DATA" | jq -r '.body' | grep -oE '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|sh|md)`' | head -1 | tr -d '`')
927
+ [ -z "$FINDING_FILE" ] && continue
928
+
929
+ SURFACE_FILE_MEMBERS["$FINDING_FILE"]="${SURFACE_FILE_MEMBERS[$FINDING_FILE]} $FINDING_NUM"
930
+ done
931
+
932
+ # For each same-file cluster of 2+, actually CREATE the batch issue (executable —
933
+ # mirrors phase-1-resolve.md's "Batch creation rule") and REPLACE the members with
934
+ # the batch issue in QUEUED_FINDINGS so the dispatch step below never double-dispatches
935
+ # them. This is what makes SURFACE_BATCHED_FINDINGS a live control, not dead wiring. <!-- forge#1832, forge#1834 -->
936
+ for FILE in "${!SURFACE_FILE_MEMBERS[@]}"; do
937
+ MEMBERS=(${SURFACE_FILE_MEMBERS[$FILE]})
938
+ [ "${#MEMBERS[@]}" -ge 2 ] || continue
939
+
940
+ # Sanitize the affected-file path before interpolating it into the issue title/body.
941
+ # Git filenames can legally carry shell metacharacters (`$()`, backticks, quotes);
942
+ # restrict to a validated charset so the value cannot break the gh argument
943
+ # boundary from an untrusted issue body. Shared guard with phase-1-resolve.md. <!-- forge#1833, forge#1835 -->
944
+ SAFE_SURFACE_AREA=$(printf '%s' "$FILE" | tr -cd 'A-Za-z0-9._/-')
945
+
946
+ echo "Same-run surface-area cluster: ${#MEMBERS[@]} P3 findings share $FILE — creating batch issue(s)"
947
+
948
+ # Cap at 8 members per batch (phase-1-resolve.md limit); split into batches of <=8.
949
+ for START in $(seq 0 8 $(( ${#MEMBERS[@]} - 1 ))); do
950
+ CHUNK=("${MEMBERS[@]:$START:8}")
951
+ [ "${#CHUNK[@]}" -ge 2 ] || continue
952
+
953
+ MEMBER_LINES=""
954
+ for M in "${CHUNK[@]}"; do
955
+ MTITLE=$(gh issue view "$M" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "")
956
+ MEMBER_LINES="${MEMBER_LINES}- [ ] #${M}: ${MTITLE}"$'\n'
957
+ done
958
+
959
+ BATCH_ISSUE_NUM=$(gh issue create {GH_FLAG} \
960
+ --title "fix(batch): P3 review findings — ${SAFE_SURFACE_AREA} (same-run batch)" \
961
+ --label "review-finding,priority:P3,batch" \
962
+ --body "$(cat <<BATCH_EOF
963
+ ## Problem
964
+
965
+ 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.
966
+
967
+ ## Member Findings
968
+
969
+ <!-- FORGE:BATCH_MEMBERS -->
970
+ ${MEMBER_LINES}<!-- /FORGE:BATCH_MEMBERS -->
971
+
972
+ ## Acceptance Criteria
973
+
974
+ - [ ] All member findings addressed or closed as false-positive
975
+ - [ ] Member issues auto-closed with reference to this batch PR on merge
976
+ - [ ] No security, billing, anti-bot, or auth paths touched (validated before batching)
977
+
978
+ <!-- FORGE:BATCHABLE -->
979
+ BATCH_EOF
980
+ )" --json number --jq '.number')
981
+
982
+ # Consume the cluster: record members and REPLACE them in QUEUED_FINDINGS
983
+ # with the single batch issue, so the dispatch step below operates on the
984
+ # batch unit and skips the individual members.
985
+ SURFACE_BATCHED_FINDINGS+=("${CHUNK[@]}")
986
+ QUEUED_FINDINGS=($(printf '%s\n' "${QUEUED_FINDINGS[@]}" | grep -vxF -f <(printf '%s\n' "${CHUNK[@]}") || true))
987
+ [ -n "$BATCH_ISSUE_NUM" ] && QUEUED_FINDINGS+=("$BATCH_ISSUE_NUM")
988
+ echo "Batched ${#CHUNK[@]} findings into #${BATCH_ISSUE_NUM}; members removed from QUEUED_FINDINGS and the DAG."
989
+ done
990
+ done
991
+ ```
992
+
993
+ Findings clustered here are replaced by their batch issue in `QUEUED_FINDINGS` (and therefore the DAG, which is built from `QUEUED_FINDINGS` in the dispatch step below) — the individual member issues in `SURFACE_BATCHED_FINDINGS` are never dispatched. Findings that remain ungrouped (fewer than 2 sharing a file in this collection round) stay individually queued below; they retain default-batchable eligibility and will be picked up by the next `/orchestrate` invocation's Phase 1 resolve if a same-file or leaf-directory cluster later forms across runs.
994
+
610
995
  **For queued (non-deferred) findings:**
611
996
 
612
997
  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.
@@ -761,11 +1146,13 @@ fi
761
1146
  ### Step 4E: Handle individual agent failures
762
1147
 
763
1148
  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"
1149
+ - **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.
1150
+ - **Invalid issue**: Already handled by the agent (closed with comment) — just report it. This classifies as **FAILED**.
1151
+ - **Build/test failure**: Report the error, suggest manual intervention. This classifies as **FAILED**.
1152
+ - **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.
1153
+ - **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:
1154
+ - 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).
1155
+ - 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
1156
 
770
1157
  **Do NOT retry failed agents automatically.** Report the failure and let the user decide.
771
1158
 
@@ -921,7 +1308,7 @@ The context-gathering phase can fetch this index to discover all investigation G
921
1308
 
922
1309
  **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
1310
 
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.
1311
+ **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
1312
 
926
1313
  **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
1314
 
@@ -943,7 +1330,7 @@ After EVERY \`Skill(skill='work-on', ...)\` call returns, immediately check the
943
1330
  gh issue view ${FINDING_NUM} -R {GH_REPO} --json labels --jq '[.labels[].name | select(startswith(\"workflow:\"))]'
944
1331
  \`\`\`
945
1332
  **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\`
1333
+ **Terminal condition also**: \`needs-human\` label present, \`workflow:awaiting-merge\` label present, OR issue state is \`closed\`
947
1334
  If the label is NOT terminal (e.g., \`workflow:investigating\`, \`workflow:ready-to-build\`, \`workflow:building\`, \`workflow:in-review\`), invoke \`Skill(skill='work-on', args='${FINDING_NUM}')\` again immediately. The \`/work-on\` skill will re-read GitHub state and advance to the next phase. Do NOT output a summary, do NOT pause, do NOT ask for confirmation — just invoke it again.
948
1335
 
949
1336
  **CRITICAL — SOURCE BRANCH DETECTION**:
@@ -975,12 +1362,44 @@ Completion Sweep Results:
975
1362
  Permanently deferred (gen2): #{D} (generation >= 2 cascade cap)
976
1363
  ```
977
1364
 
978
- **After sweep agents complete** (or if no findings were dispatched): proceed to Phase 5.
1365
+ **After sweep agents complete** (or if no findings were dispatched): output the budget deferred-issues report (if applicable), then proceed to Phase 5.
979
1366
 
980
1367
  **Anti-patterns — DO NOT DO THIS:**
981
1368
  - Re-sweeping findings spawned during the sweep itself — this creates unbounded recursion. Sweep is a single pass.
982
1369
  - Overriding generation >= 2 deferrals — the cascade cap is absolute.
983
1370
  - Skipping the sweep because "there are only a few" deferred findings — even one deferred finding represents unresolved work.
984
1371
 
1372
+ ### Step 4F.5: Budget Deferred-Issues Report (conditional) <!-- Added: forge#1743 -->
1373
+
1374
+ **Run only when `BUDGET_LIMIT != "Infinity"` AND `${#DEFERRED_BUDGET_ISSUES[@]} > 0`.**
1375
+
1376
+ 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.
1377
+
1378
+ ```bash
1379
+ if [ "$BUDGET_LIMIT" != "Infinity" ] && [ "${#DEFERRED_BUDGET_ISSUES[@]}" -gt 0 ]; then
1380
+ echo ""
1381
+ echo "## Budget Report"
1382
+ echo ""
1383
+ echo "**Budget limit**: \$${BUDGET_LIMIT}"
1384
+ echo "**Projected spend (dispatched issues)**: \$${PROJECTED_SPEND}"
1385
+ echo "**Actual spend (completed issues, best-effort)**: \$${ACTUAL_SPEND}"
1386
+ echo "**ε-reserve used**: ${EPSILON_DISPATCHED} (10% = \$${EPSILON_BUDGET})"
1387
+ echo ""
1388
+ echo "### Deferred Issues (budget exhausted — never silently dropped)"
1389
+ echo ""
1390
+ echo "| Issue | Title | Score | Est. Cost | Reason |"
1391
+ echo "|-------|-------|-------|-----------|--------|"
1392
+ for DNUM in "${DEFERRED_BUDGET_ISSUES[@]}"; do
1393
+ DTITLE=$(gh issue view "$DNUM" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "(unknown)")
1394
+ DSCORE="${ISSUE_SCORE[$DNUM]:-?}"
1395
+ DCOST="${ISSUE_COST_ESTIMATE[$DNUM]:-?}"
1396
+ echo "| #${DNUM} | ${DTITLE} | ${DSCORE} | \$${DCOST} | Budget ceiling reached |"
1397
+ done
1398
+ echo ""
1399
+ DEFERRED_LIST=$(IFS=' '; echo "${DEFERRED_BUDGET_ISSUES[*]}")
1400
+ echo "**Action**: Re-run \`/orchestrate ${DEFERRED_LIST} [--budget N]\` to process deferred issues, or increase \`--budget\`."
1401
+ fi
1402
+ ```
1403
+
985
1404
  ---
986
1405