forgedock 1.7.1 → 1.8.1

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 (48) hide show
  1. package/bin/engine/admission.mjs +242 -7
  2. package/bin/engine/admission.test.mjs +183 -2
  3. package/bin/engine/phases.mjs +31 -22
  4. package/bin/engine/resolve.mjs +8 -1
  5. package/bin/engine/resolve.test.mjs +5 -0
  6. package/bin/engine.mjs +26 -17
  7. package/bin/forgedock.mjs +15 -2
  8. package/bin/hooks/pre-tool-use.mjs +573 -2
  9. package/bin/journey.mjs +11 -7
  10. package/bin/labels.json +5 -0
  11. package/bin/opencode-adapter.mjs +47 -13
  12. package/bin/orchestrate-preflight.mjs +434 -0
  13. package/bin/runner.mjs +30 -4
  14. package/commands/audit-agents.md +75 -26
  15. package/commands/autopilot.md +4 -4
  16. package/commands/cleanup.md +10 -5
  17. package/commands/issue.md +95 -35
  18. package/commands/orchestrate/phase-1-resolve.md +70 -32
  19. package/commands/orchestrate/phase-3-dependency.md +141 -13
  20. package/commands/orchestrate/phase-4-execution.md +660 -70
  21. package/commands/orchestrate/phase-5-cleanup.md +5 -11
  22. package/commands/orchestrate/phase-6-report.md +27 -1
  23. package/commands/orchestrate/safety.md +1 -1
  24. package/commands/orchestrate.md +39 -1
  25. package/commands/quality-gate.md +30 -10
  26. package/commands/review-pr-agents/protocols.md +13 -0
  27. package/commands/review-pr-agents.md +1 -1
  28. package/commands/review-pr-staging.md +100 -18
  29. package/commands/review-pr.md +78 -24
  30. package/commands/upgrade-deps.md +1 -1
  31. package/commands/work-on/build/validate.md +25 -4
  32. package/commands/work-on/close.md +40 -10
  33. package/commands/work-on/decompose.md +33 -1
  34. package/commands/work-on/investigate.md +33 -0
  35. package/commands/work-on/remediate.md +40 -5
  36. package/commands/work-on/review.md +27 -0
  37. package/commands/work-on.md +48 -5
  38. package/docs/CONFIG.md +45 -10
  39. package/docs/OPENCODE.md +21 -6
  40. package/docs/spec/review-protocol.md +7 -0
  41. package/package.json +1 -1
  42. package/scripts/check-admission-parity.mjs +1 -0
  43. package/scripts/check-command-side-effects.sh +14 -13
  44. package/scripts/check-shell-portability.sh +36 -0
  45. package/scripts/check-spec-markers.sh +21 -11
  46. package/scripts/checkbox-sections.test.sh +67 -0
  47. package/scripts/extract-affected-files.sh +85 -5
  48. package/scripts/extract-affected-files.test.sh +154 -2
@@ -189,6 +189,8 @@ should_dispatch() {
189
189
 
190
190
  **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.
191
191
 
192
+ `MAX_CONCURRENT` caps top-level `/work-on` dispatches, not the total number of harness subagents. A normal worker can spawn context, architect, implement, validate, quality-gate, and review children. Budget **8 total subagent spawns per worker** when choosing this value: `max_concurrent <= session_subagent_budget / 8`. This is a conservative planning multiplier, not a second runtime semaphore; the harness does not expose a reliable cross-agent spawn counter to this shell-level dispatcher.
193
+
192
194
  Initialize the concurrency cap and its batch-scope tracking state before the first dispatch:
193
195
 
194
196
  ```bash
@@ -209,8 +211,12 @@ fi
209
211
 
210
212
  ACTIVE_DISPATCH_COUNT=0 # in-flight dispatched-but-not-yet-completed agents, this batch
211
213
  DEFERRED_CONCURRENCY_ISSUES=() # ready issues held back because no headroom was available
214
+ SUBAGENT_SPAWN_BUDGET_PER_WORKER=8 # /work-on + build/review fan-out planning estimate
215
+ TOP_LEVEL_DISPATCH_TOTAL=0
216
+ PLANNED_SUBAGENT_SPAWNS=0
217
+ OBSERVED_SUBAGENT_SPAWNS="unavailable" # replace only with harness telemetry; never infer
212
218
 
213
- echo "Concurrency gate initialized: MAX_CONCURRENT=${MAX_CONCURRENT} (forge.yaml → orchestration.max_concurrent; default 12 when unset or invalid)"
219
+ echo "Concurrency gate initialized: MAX_CONCURRENT=${MAX_CONCURRENT}; plan for up to $((MAX_CONCURRENT * SUBAGENT_SPAWN_BUDGET_PER_WORKER)) subagent spawns in flight (8 per worker)"
214
220
  # --- End concurrency gate initialization ---
215
221
  ```
216
222
 
@@ -226,6 +232,31 @@ dispatch_headroom() {
226
232
 
227
233
  **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.
228
234
 
235
+ **Dispatch accounting**: Immediately after each successful top-level dispatch, increment `TOP_LEVEL_DISPATCH_TOTAL` and add `SUBAGENT_SPAWN_BUDGET_PER_WORKER` to `PLANNED_SUBAGENT_SPAWNS`. When the runtime reports child-spawn telemetry, replace `OBSERVED_SUBAGENT_SPAWNS` with that measured total; otherwise leave it `unavailable` rather than fabricating a measured value.
236
+
237
+ ### Step 4A-pre.0.3: Secondary content-creation backpressure (MANDATORY)
238
+
239
+ GitHub's `resources.core` quota does not report secondary content-creation throttles. Treat any GitHub API response with HTTP `403` whose body contains `secondary rate limit` as a batch-wide backpressure signal.
240
+
241
+ ```bash
242
+ # Call from every dispatcher-owned gh write/create wrapper and from a child completion
243
+ # report that includes its final GitHub API error. Do not retry the failed request here.
244
+ SECONDARY_RATE_LIMITED=false
245
+ SECONDARY_RATE_LIMIT_MESSAGE=""
246
+
247
+ record_secondary_rate_limit() {
248
+ local STATUS="$1"
249
+ local BODY="$2"
250
+ if [ "$STATUS" = "403" ] && echo "$BODY" | grep -qi 'secondary rate limit'; then
251
+ SECONDARY_RATE_LIMITED=true
252
+ SECONDARY_RATE_LIMIT_MESSAGE="$BODY"
253
+ echo "SECONDARY RATE LIMIT: pausing all new dispatch. Do not retry GitHub content creation until an operator resumes the batch." >&2
254
+ fi
255
+ }
256
+ ```
257
+
258
+ Before every dispatch site computes headroom or launches a worker, check `SECONDARY_RATE_LIMITED`. When true, preserve the ready/deferred queues, launch no more workers, and end the active dispatch cycle. Do not poll or retry GitHub calls to discover recovery: an operator resumes the batch after the throttle clears. In-flight agents receive this contract in the dispatch prompt: on a secondary-limit 403, they must stop their GitHub write/create retry loop, report the response to the orchestrator, and exit their current phase without creating replacement work.
259
+
229
260
  ### Step 4A-pre: Staging baseline tracking (MANDATORY — continuous)
230
261
 
231
262
  **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.
@@ -409,6 +440,16 @@ echo "Step 4A.pre: using BATCH_T0=${BATCH_T0} for Step 4C's run-spawned cascade
409
440
  DEFERRED_FINDINGS=()
410
441
  QUEUED_FINDINGS=()
411
442
  declare -A DEFERRED_REASONS
443
+ # Count every finding regardless of admission, so a deferred refinement never
444
+ # makes the reported amplification ratio look better than it was.
445
+ FINDINGS_SPAWNED=0
446
+ MERGED_UNITS=0
447
+ AMPLIFICATION_RATIO_HISTORY=()
448
+ AMPLIFICATION_DEFERRED=()
449
+ declare -A FINDINGS_BY_SOURCE_PR
450
+ declare -A REFINEMENT_FINDINGS
451
+ declare -A NEW_SURFACE_FINDINGS
452
+ declare -A AMPLIFICATION_FINDING_SEEN
412
453
  declare -A AGENT_ISSUE_MAP
413
454
  # Engine-first dispatch equivalent of AGENT_ISSUE_MAP (fixed forge#2466): keyed by issue
414
455
  # number, holds the task id returned by each backgrounded `Bash(run_in_background=true,
@@ -417,9 +458,29 @@ declare -A AGENT_ISSUE_MAP
417
458
  # the same role AGENT_ISSUE_MAP plays for Agent-tool `agent_completed` notifications.
418
459
  declare -A ENGINE_DISPATCH_MAP
419
460
  # OpenCode native task equivalent of ENGINE_DISPATCH_MAP. Keys are issue numbers;
420
- # values are the session ids returned by task(background=true).
461
+ # values are the session ids returned by task(background=true). This is a live
462
+ # cache only; every entry must also be persisted as a FORGE:DISPATCH comment.
421
463
  declare -A OPENCODE_DISPATCH_MAP
422
464
 
465
+ # Reconstruct the live map after compaction/restart from the durable dispatch
466
+ # records. The issue association is the map key; only the latest running record
467
+ # is restored, because a later completed/error record has already released it.
468
+ for NUM in "${ISSUES[@]}"; do
469
+ DISPATCH_BODY=$(gh api repos/{GH_REPO}/issues/"$NUM"/comments \
470
+ --jq '[.[] | select(.body | contains("FORGE:DISPATCH")) | .body] | last // ""' 2>/dev/null || true)
471
+ TASK_ID=$(printf '%s' "$DISPATCH_BODY" | jq -Rr 'try capture("\\\"runtime\\\":\\\"opencode\\\",\\\"child_session_id\\\":\\\"(?<id>[^\\\"]+)\\\".*\\\"state\\\":\\\"running\\\"").id catch ""')
472
+ if [ -n "$TASK_ID" ]; then
473
+ OPENCODE_DISPATCH_MAP["$NUM"]="$TASK_ID"
474
+ fi
475
+ done
476
+
477
+ # Full-repository intake is intentionally separate from Step 4C's T0-scoped
478
+ # review-finding cascade. It is enabled only for an operator-authorised
479
+ # policy: all run; records every open issue already observed so CI-created
480
+ # actionable work is admitted once rather than rediscovered every cycle.
481
+ declare -A SEEN_OPEN_ISSUES
482
+ FULL_REPO_SWEEP_COUNT=0
483
+
423
484
  # Same-file current-state brief forwarding (forge#1860). Populated by the core streaming
424
485
  # dispatch loop below (Step 4B) whenever a Layer 1/2/3 structural predecessor edge (see
425
486
  # EDGE_KIND/EDGE_FILES from phase-3-dependency.md Step 3C) resolves; consumed by Step 4A's
@@ -428,6 +489,42 @@ declare -A OPENCODE_DISPATCH_MAP
428
489
  declare -A SAME_FILE_BRIEF
429
490
  declare -A EDGE_BRIEFED
430
491
 
492
+ # DONE-path edge re-verification memo (forge#2848). A conclusive re-derivation is
493
+ # memoized in EDGE_REDERIVED; inconclusive results get one retry, capped by
494
+ # EDGE_REDERIVE_ATTEMPTS. This avoids caching a transient failure while bounding
495
+ # extraction to two attempts per descendant per batch.
496
+ declare -A EDGE_REDERIVED
497
+ declare -A EDGE_REDERIVE_ATTEMPTS
498
+
499
+ # Affected-file extraction helper, resolved for the DONE-path cohort re-derivation
500
+ # (forge#2848). Same resolver precedence as phase-3-dependency.md Step 3C Layer 1 —
501
+ # ForgeDock's runtime installation before the target repository — because the
502
+ # orchestrator runs inside the project being worked on, where a bare
503
+ # `bash scripts/extract-affected-files.sh` silently fails when that project has not
504
+ # copied ForgeDock's helper scripts into its own repository (#2794/#2791).
505
+ resolve_extract_affected_files() {
506
+ local candidates=()
507
+ [ -n "${FORGE_HOME:-}" ] && candidates+=("$FORGE_HOME/scripts/extract-affected-files.sh")
508
+ [ -n "${REPO_PATH:-}" ] && candidates+=("$REPO_PATH/scripts/extract-affected-files.sh")
509
+ candidates+=("$PWD/scripts/extract-affected-files.sh")
510
+
511
+ local candidate
512
+ for candidate in "${candidates[@]}"; do
513
+ if [ -f "$candidate" ]; then
514
+ printf '%s\n' "$candidate"
515
+ return 0
516
+ fi
517
+ done
518
+ echo "ERROR: extract-affected-files.sh is not installed in any configured runtime path." >&2
519
+ return 1
520
+ }
521
+
522
+ # Non-fatal here, unlike Phase 3's hard exit: Phase 3 cannot build a DAG at all without
523
+ # this helper, whereas Phase 4 only needs it for the OPTIONAL re-derivation optimization.
524
+ # If it is missing, re-derivation is skipped and edges stay as planned — the conservative
525
+ # direction, and identical to this file's pre-forge#2848 behavior.
526
+ AFFECTED_FILES_SCRIPT=$(resolve_extract_affected_files) || AFFECTED_FILES_SCRIPT=""
527
+
431
528
  # Human-gated idle/backpressure flag (Step 4B item 6.7, forge#1814). Starts false —
432
529
  # recomputed every completion cycle over {all_batch_issue_numbers}. Declared at batch
433
530
  # scope (not per-agent) so Step 4C can read the latest value on every iteration.
@@ -452,15 +549,15 @@ CASCADE_POLICY_NAME=$(yq '.orchestration.cascade.policy // "balanced"' forge.yam
452
549
 
453
550
  case "$CASCADE_POLICY_NAME" in
454
551
  all)
455
- PRESET_MAX_GEN="unlimited"; PRESET_TOKEN_BUDGET="unlimited"; PRESET_DEFER_GATED="false"; PRESET_KEYWORD="false"; PRESET_P3_SAME_FILE="false" ;;
552
+ PRESET_MAX_GEN="unlimited"; PRESET_BATCH_MAX_GEN=2; PRESET_TOKEN_BUDGET="unlimited"; PRESET_DEFER_GATED="false"; PRESET_KEYWORD="false"; PRESET_P3_SAME_FILE="false" ;;
456
553
  conservative)
457
- PRESET_MAX_GEN=1; PRESET_TOKEN_BUDGET=450000; PRESET_DEFER_GATED="true"; PRESET_KEYWORD="true"; PRESET_P3_SAME_FILE="true" ;;
554
+ PRESET_MAX_GEN=1; PRESET_BATCH_MAX_GEN=2; PRESET_TOKEN_BUDGET=450000; PRESET_DEFER_GATED="true"; PRESET_KEYWORD="true"; PRESET_P3_SAME_FILE="true" ;;
458
555
  balanced)
459
- PRESET_MAX_GEN=1; PRESET_TOKEN_BUDGET=900000; PRESET_DEFER_GATED="true"; PRESET_KEYWORD="true"; PRESET_P3_SAME_FILE="true" ;;
556
+ PRESET_MAX_GEN=1; PRESET_BATCH_MAX_GEN=2; PRESET_TOKEN_BUDGET=900000; PRESET_DEFER_GATED="true"; PRESET_KEYWORD="true"; PRESET_P3_SAME_FILE="true" ;;
460
557
  *)
461
558
  echo "WARNING: forge.yaml → orchestration.cascade.policy \"${CASCADE_POLICY_NAME}\" is not one of: all, balanced, conservative — falling back to \"balanced\""
462
559
  CASCADE_POLICY_NAME="balanced"
463
- PRESET_MAX_GEN=1; PRESET_TOKEN_BUDGET=900000; PRESET_DEFER_GATED="true"; PRESET_KEYWORD="true"; PRESET_P3_SAME_FILE="true" ;;
560
+ PRESET_MAX_GEN=1; PRESET_BATCH_MAX_GEN=2; PRESET_TOKEN_BUDGET=900000; PRESET_DEFER_GATED="true"; PRESET_KEYWORD="true"; PRESET_P3_SAME_FILE="true" ;;
464
561
  esac
465
562
 
466
563
  # max_generation is authoritatively resolved in phase-1-resolve.md (it only governs
@@ -473,6 +570,14 @@ if [ "$MAX_GENERATION_FOR_NOTICE" != "unlimited" ] && ! echo "$MAX_GENERATION_FO
473
570
  MAX_GENERATION_FOR_NOTICE="$PRESET_MAX_GEN"
474
571
  fi
475
572
 
573
+ # P3 batching is a distinct, bounded aggregation exception to autonomous cascade
574
+ # admission. Unlike max_generation, it must always be finite, including policy: all.
575
+ BATCH_MAX_GENERATION=$(yq ".orchestration.cascade.batch_max_generation // ${PRESET_BATCH_MAX_GEN}" forge.yaml 2>/dev/null || echo "$PRESET_BATCH_MAX_GEN")
576
+ if ! echo "$BATCH_MAX_GENERATION" | grep -qE '^[1-9][0-9]*$'; then
577
+ echo "WARNING: forge.yaml → orchestration.cascade.batch_max_generation is not a positive integer (\"${BATCH_MAX_GENERATION}\") — falling back to default ${PRESET_BATCH_MAX_GEN}"
578
+ BATCH_MAX_GENERATION="$PRESET_BATCH_MAX_GEN"
579
+ fi
580
+
476
581
  # token_budget precedence: orchestration.cascade.token_budget (new home) >
477
582
  # pipeline.token_budget_per_batch (deprecated alias, forge#1858, kept working
478
583
  # unchanged) > the resolved preset default. Accepts the "unlimited" sentinel —
@@ -491,13 +596,30 @@ fi
491
596
 
492
597
  TOKEN_ESTIMATE_PER_FINDING=$(yq '.pipeline.token_estimate_per_finding // 150000' forge.yaml 2>/dev/null || echo 150000)
493
598
 
599
+ # Amplification is observational by default. The optional ceiling is evaluated
600
+ # only against same-lineage refinements in Step 4C, never against new surface.
601
+ CASCADE_MAX_AMPLIFICATION=$(yq '.orchestration.cascade.max_amplification // "off"' forge.yaml 2>/dev/null || echo "off")
602
+ if [ "$CASCADE_MAX_AMPLIFICATION" != "off" ]; then
603
+ if ! echo "$CASCADE_MAX_AMPLIFICATION" | grep -qE '^[0-9]+(\.[0-9]+)?$' || \
604
+ ! awk "BEGIN { exit !($CASCADE_MAX_AMPLIFICATION > 0) }"; then
605
+ echo "WARNING: forge.yaml → orchestration.cascade.max_amplification must be a positive number or off — disabling the bound"
606
+ CASCADE_MAX_AMPLIFICATION="off"
607
+ fi
608
+ fi
609
+ CONVERGENCE_WINDOW=$(yq '.orchestration.cascade.convergence_window // 3' forge.yaml 2>/dev/null || echo 3)
610
+ if ! echo "$CONVERGENCE_WINDOW" | grep -qE '^[1-9][0-9]*$'; then
611
+ echo "WARNING: forge.yaml → orchestration.cascade.convergence_window must be a positive integer — falling back to 3"
612
+ CONVERGENCE_WINDOW=3
613
+ fi
614
+ echo "Cascade amplification: max_amplification=${CASCADE_MAX_AMPLIFICATION} convergence_window=${CONVERGENCE_WINDOW} (off preserves current admission behavior)"
615
+
494
616
  # Independent boolean levers — each accepts an explicit granular override on
495
617
  # top of the resolved preset (preset supplies the default, not a hard value).
496
618
  CASCADE_DEFER_ON_BATCH_GATED=$(yq ".orchestration.cascade.defer_on_batch_gated // ${PRESET_DEFER_GATED}" forge.yaml 2>/dev/null || echo "$PRESET_DEFER_GATED")
497
619
  CASCADE_KEYWORD_HEURISTIC=$(yq ".orchestration.cascade.keyword_heuristic // ${PRESET_KEYWORD}" forge.yaml 2>/dev/null || echo "$PRESET_KEYWORD")
498
620
  CASCADE_P3_SAME_FILE_DEFER=$(yq ".orchestration.cascade.p3_same_file_defer // ${PRESET_P3_SAME_FILE}" forge.yaml 2>/dev/null || echo "$PRESET_P3_SAME_FILE")
499
621
 
500
- echo "Cascade admission policy resolved: policy=${CASCADE_POLICY_NAME} token_budget=${TOKEN_BUDGET} defer_on_batch_gated=${CASCADE_DEFER_ON_BATCH_GATED} keyword_heuristic=${CASCADE_KEYWORD_HEURISTIC} p3_same_file_defer=${CASCADE_P3_SAME_FILE_DEFER} (forge.yaml → orchestration.cascade; see docs/CONFIG.md)"
622
+ echo "Cascade admission policy resolved: policy=${CASCADE_POLICY_NAME} token_budget=${TOKEN_BUDGET} batch_max_generation=${BATCH_MAX_GENERATION} defer_on_batch_gated=${CASCADE_DEFER_ON_BATCH_GATED} keyword_heuristic=${CASCADE_KEYWORD_HEURISTIC} p3_same_file_defer=${CASCADE_P3_SAME_FILE_DEFER} (forge.yaml → orchestration.cascade; see docs/CONFIG.md)"
501
623
 
502
624
  # Both-uncapped notice (loud, one-time, printed once per orchestrate invocation since
503
625
  # this resolution block itself runs once at Step 4A.pre) — never a preset default except
@@ -524,6 +646,8 @@ TOKEN_DEFERRED=() # findings deferred by the token-budget rule this run (re-ev
524
646
  # alone. <!-- Added: forge#1909 -->
525
647
  SURFACE_BATCHED_FINDINGS=() # all member issue numbers absorbed into a batch across the run
526
648
  SURFACE_BATCH_COUNT=0 # count of batch issues created across the run
649
+ BATCHABLE_DEFERRED_P3=() # gen-2+ P3s eligible only for bounded P3 aggregation (forge#2849)
650
+ declare -A FINDING_GENERATIONS
527
651
 
528
652
  for NUM in {ready_issue_numbers}; do
529
653
  PR_BASE=$(bash "$CLASSIFY_LANE_SCRIPT" "$NUM" -R {GH_REPO}) || {
@@ -547,6 +671,97 @@ Use `${ISSUE_LANE[$NUM]}` and `${ISSUE_PR_BASE[$NUM]}` to populate `{LANE}` and
547
671
 
548
672
  ### Step 4A: Dispatch ready issues
549
673
 
674
+ ### Step 4A.0: Probe Knowledge Gist capability once
675
+
676
+ Probe the authenticated identity once for this orchestration run before any engine, Agent, or
677
+ OpenCode worker is dispatched. A GitHub App installation token identifies as `Bot` and cannot use
678
+ the Gists API; cache that fact rather than letting every worker rediscover it by attempting a
679
+ write. An unavailable identity probe preserves existing behavior for PAT-authenticated runs.
680
+
681
+ ```bash
682
+ if [ -z "${FORGE_GIST_CAPABLE+x}" ]; then
683
+ GIST_AUTH_TYPE=$(gh api user --jq '.type' 2>/dev/null || true)
684
+ if [ "$GIST_AUTH_TYPE" = "Bot" ]; then
685
+ FORGE_GIST_CAPABLE=false
686
+ else
687
+ FORGE_GIST_CAPABLE=true
688
+ fi
689
+ export FORGE_GIST_CAPABLE
690
+ fi
691
+
692
+ if [ "$FORGE_GIST_CAPABLE" = "true" ]; then
693
+ echo "Knowledge Gist capability available"
694
+ else
695
+ echo "INFO: Knowledge Gist subsystem unavailable for this authentication; workers will skip it"
696
+ fi
697
+ ```
698
+
699
+ Carry `FORGE_GIST_CAPABLE` unchanged through every dispatch path. Do not probe it in individual
700
+ workers dispatched by this run. Phase 6 reports a false value once at batch level.
701
+
702
+ **Claims-board dispatch gate (MANDATORY, before every individual dispatch)** <!-- Added: forge#2844 -->: The coordination issue is the durable authority for file ownership. Do not use `EDGE_FILES`, `ISSUE_FILES`, or a remembered prior read as evidence that a claim is free. Immediately before dispatching each issue, re-read the full claims board and refuse that dispatch when the issue's declared file set intersects a live claim held by another issue. This applies equally to engine, Claude Agent, and OpenCode task dispatches, including newly-ready issues and wake reconstruction.
703
+
704
+ ```bash
705
+ # Returns unreleased claims as [{holder: "123", files: "..."}]. A release is a separate,
706
+ # later comment, so it must be paired with its claim by holder rather than searched for in the
707
+ # claim comment itself. Terminal holder states also self-heal claims left behind by dead agents.
708
+ read_active_claims() {
709
+ local COORD_NUM="$1"
710
+ local CLAIMS HOLDER TERMINAL
711
+ CLAIMS=$(gh api --paginate --slurp "repos/{GH_REPO}/issues/${COORD_NUM}/comments" 2>/dev/null \
712
+ | jq -c '
713
+ flatten as $comments |
714
+ [$comments[]
715
+ | select(.body | contains("<!-- FORGE:CLAIM -->"))
716
+ | . as $claim
717
+ | ($claim.body | capture("\\*\\*Holder\\*\\*: #(?<holder>[0-9]+)").holder) as $holder
718
+ | select([$comments[]
719
+ | select(.body | contains("<!-- FORGE:CLAIM_RELEASED -->"))
720
+ | select((.body | capture("\\*\\*Holder\\*\\*: #(?<holder>[0-9]+)").holder) == $holder)
721
+ | select(.created_at > $claim.created_at)] | length == 0)
722
+ | {holder: $holder,
723
+ files: ($claim.body | capture("\\*\\*Files\\*\\*: (?<files>[\\s\\S]*?)(?:\\n\\*\\*Interfaces\\*\\*:|$)").files)}]') || return 1
724
+
725
+ for HOLDER in $(echo "$CLAIMS" | jq -r '.[].holder'); do
726
+ TERMINAL=$(gh issue view "$HOLDER" -R {GH_REPO} --json labels --jq \
727
+ '[.labels[].name | select(. == "workflow:merged" or . == "workflow:invalid" or . == "workflow:awaiting-merge" or . == "needs-human")] | length > 0' 2>/dev/null)
728
+ [ "$TERMINAL" = "true" ] && CLAIMS=$(echo "$CLAIMS" | jq --arg holder "$HOLDER" '[.[] | select(.holder != $holder)]')
729
+ done
730
+ echo "$CLAIMS"
731
+ }
732
+
733
+ claim_conflicts_with_live_holder() {
734
+ local NUM="$1" TARGET_FILES CLAIM HOLDER CLAIM_FILES OVERLAP
735
+ TARGET_FILES=$(printf '%s\n' "${ISSUE_FILES[$NUM]:-}" | sed -E '/^[[:space:]]*$/d; s/^[[:space:]]*[-*][[:space:]]*//; s/`//g' | sort -u)
736
+ [ -z "$TARGET_FILES" ] && return 1 # No declared paths: normal conservative DAG rules still apply.
737
+
738
+ ACTIVE_CLAIMS=$(read_active_claims "$COORD_ISSUE_NUMBER") || {
739
+ echo "REFUSING TO DISPATCH #${NUM}: could not read the durable claims board." >&2
740
+ return 0
741
+ }
742
+ while IFS= read -r CLAIM; do
743
+ HOLDER=$(echo "$CLAIM" | jq -r '.holder')
744
+ [ "$HOLDER" = "$NUM" ] && continue
745
+ CLAIM_FILES=$(echo "$CLAIM" | jq -r '.files' | sed -E '/^[[:space:]]*$/d; s/^[[:space:]]*[-*][[:space:]]*//; s/`//g' | sort -u)
746
+ OVERLAP=$(comm -12 <(printf '%s\n' "$TARGET_FILES") <(printf '%s\n' "$CLAIM_FILES"))
747
+ if [ -n "$OVERLAP" ]; then
748
+ echo "DEFERRING #${NUM}: declared files overlap live claim held by #${HOLDER}: ${OVERLAP//$'\n'/, }"
749
+ return 0
750
+ fi
751
+ done < <(echo "$ACTIVE_CLAIMS" | jq -c '.[]')
752
+ return 1
753
+ }
754
+
755
+ # Run this immediately before each engine Bash, Agent(), or OpenCode task call. Do not batch the
756
+ # board read: another worker can claim a file between two dispatches in the same ready set.
757
+ if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_ISSUE_NUMBER:-}" ]; then
758
+ if claim_conflicts_with_live_holder "$NUM"; then
759
+ DEFERRED_CLAIM_ISSUES+=("$NUM")
760
+ continue
761
+ fi
762
+ fi
763
+ ```
764
+
550
765
  **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.
551
766
 
552
767
  **OpenCode dispatch (runtime-neutral adapter)**: When `FORGE_RUNTIME=opencode` or
@@ -567,14 +782,26 @@ task(
567
782
  description="Work on {PROJECT_PREFIX}#{NUMBER}",
568
783
  subagent_type="general",
569
784
  background=true,
570
- prompt="Use the same Phase 4A work-on template below. Invoke Skill(skill='work-on', args='{PROJECT_PREFIX}{NUMBER} --under-orchestration') and continue until a terminal workflow state."
785
+ prompt="Use the same Phase 4A work-on template below. Before invoking it, run `export FORGE_GIST_CAPABLE={FORGE_GIST_CAPABLE}` so the cached orchestration capability is preserved. Invoke Skill(skill='work-on', args='{PROJECT_PREFIX}{NUMBER} --under-orchestration') and continue until a terminal workflow state."
571
786
  )
572
787
  ```
573
788
 
574
789
  Capture the returned `<task id="..." state="running">` id in
575
- `OPENCODE_DISPATCH_MAP[{NUMBER}]`. `background=true` is required for streaming
576
- DAG behavior; a foreground task makes the orchestrator wait for that issue and
577
- reintroduces a wave barrier. The ForgeDock OpenCode plugin opts into
790
+ `OPENCODE_DISPATCH_MAP[{NUMBER}]` and immediately persist it to the issue:
791
+
792
+ ```bash
793
+ ATTEMPT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
794
+ --jq '[.[] | select(.body | contains("FORGE:DISPATCH") and contains("\"state\":\"running\""))] | length + 1')
795
+ if [ "${DRY_RUN:-false}" = "false" ]; then
796
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DISPATCH -->
797
+ \`\`\`json
798
+ {\"runtime\":\"opencode\",\"child_session_id\":\"{TASK_ID}\",\"attempt\":${ATTEMPT},\"state\":\"running\",\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}
799
+ \`\`\`"
800
+ fi
801
+ ```
802
+
803
+ `background=true` is required for streaming DAG behavior; a foreground task
804
+ makes the orchestrator wait for that issue and reintroduces a wave barrier. The ForgeDock OpenCode plugin opts into
578
805
  `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` by default. An explicit
579
806
  `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false` is a supported degraded mode,
580
807
  but it must be reported as non-streaming rather than treated as equivalent to
@@ -667,6 +894,18 @@ if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
667
894
  if [ "${#DEFERRED_CONCURRENCY_ISSUES[@]}" -gt 0 ]; then
668
895
  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[*]}"
669
896
  fi
897
+ # Filter against the durable board once while forming this engine batch. The tool-call
898
+ # instruction below re-checks immediately before each launch to close the remaining TOCTOU gap.
899
+ CLAIM_SAFE_DISPATCH=()
900
+ for NUM in "${DISPATCH_NOW[@]}"; do
901
+ if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_ISSUE_NUMBER:-}" ] && claim_conflicts_with_live_holder "$NUM"; then
902
+ DEFERRED_CLAIM_ISSUES+=("$NUM")
903
+ continue
904
+ fi
905
+ CLAIM_SAFE_DISPATCH+=("$NUM")
906
+ done
907
+ DISPATCH_NOW=("${CLAIM_SAFE_DISPATCH[@]}")
908
+
670
909
  echo "Dispatching ${#DISPATCH_NOW[@]} issue(s) this message via forgedock run-issue (headroom was ${HEADROOM})"
671
910
 
672
911
  for NUM in "${DISPATCH_NOW[@]}"; do
@@ -703,10 +942,10 @@ fi
703
942
  fi
704
943
  ```
705
944
 
706
- **Dispatch each issue in `DISPATCH_NOW` via its own backgrounded `Bash` call (MANDATORY when `FORGEDOCK_AVAILABLE=true`) — never shell `&`/`wait`.** Issue one `Bash(...)` call per issue in `DISPATCH_NOW`, all in the same message, so they run concurrently within the headroom already computed above:
945
+ **Dispatch each issue in `DISPATCH_NOW` via its own backgrounded `Bash` call (MANDATORY when `FORGEDOCK_AVAILABLE=true`) — never shell `&`/`wait`.** Immediately before each call, run `claim_conflicts_with_live_holder "{NUM}"`; if it returns success, defer that issue rather than dispatching it. Issue one `Bash(...)` call per remaining issue in `DISPATCH_NOW`, all in the same message, so they run concurrently within the headroom already computed above:
707
946
 
708
947
  ```
709
- Bash(command="forgedock run-issue {NUM} --lane {PR_BASE}", run_in_background=true, description="Engine-drive issue #{NUM}")
948
+ Bash(command="FORGE_GIST_CAPABLE=${FORGE_GIST_CAPABLE} forgedock run-issue {NUM} --lane {PR_BASE}", run_in_background=true, description="Engine-drive issue #{NUM}")
710
949
  ```
711
950
 
712
951
  Capture the task id each call returns into `ENGINE_DISPATCH_MAP[{NUM}]` (declared alongside `AGENT_ISSUE_MAP` below — Step 4B's completion handler uses this map to identify which issue a backgrounded engine-mode `Bash` completion notification belongs to, the same role `AGENT_ISSUE_MAP` plays for `agent_completed` notifications):
@@ -723,10 +962,13 @@ If `DISPATCH_NOW` is empty (headroom is 0), do not dispatch any issues this cycl
723
962
  **Agent-spawn path (Claude fallback when forgedock CLI unavailable)**: When `FORGEDOCK_AVAILABLE=false` and the runtime is not OpenCode, spawn Agent sub-agents per issue using the template below. This preserves engine state via the SubagentStop hook even without the CLI.
724
963
 
725
964
  When the runtime is OpenCode, skip this Claude-only `Agent(...)` path entirely.
726
- Use the native `task` call above for initial, newly-ready, remediation, and
727
- stall-resume dispatches. Translate every later `Agent(...)` reference in this
728
- phase to `task(...)` with `subagent_type="general"`; preserve `background=true`
729
- and use `task_id=OPENCODE_DISPATCH_MAP[{NUMBER}]` when resuming a child.
965
+ Use the native `task` call above for initial and newly-ready issue dispatches.
966
+ Translate only those OpenCode orchestration dispatches to `task(...)` with
967
+ `subagent_type="general"` and `background=true`. Review and remediation are
968
+ load-bearing child operations and must run foreground. Do not claim task-id
969
+ resume support: after a restart, reconcile the latest `FORGE:DISPATCH` record
970
+ with durable GitHub workflow state, then dispatch a fresh work-on continuation
971
+ only when the issue is still non-terminal.
730
972
 
731
973
  **REMINDER: You MUST use the template below verbatim when on the Agent-spawn fallback path. Only fill in `{VARIABLES}`. Do NOT rewrite the agent prompt. Do NOT write custom implementation instructions. The agent MUST invoke `/work-on` via the Skill tool — this is the HARD RULE from the top of this file.**
732
974
 
@@ -748,6 +990,8 @@ Agent(
748
990
  **Repository**: {GH_REPO}
749
991
  **Repo path**: {REPO_PATH}
750
992
 
993
+ **KNOWLEDGE GIST CAPABILITY**: This orchestration already probed it: `{FORGE_GIST_CAPABLE}`. Before invoking `/work-on`, run `export FORGE_GIST_CAPABLE={FORGE_GIST_CAPABLE}`. Do not re-probe or attempt Gist creation when it is `false`.
994
+
751
995
  **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.
752
996
 
753
997
  **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.
@@ -760,7 +1004,9 @@ Agent(
760
1004
  - For satellite repo issues: `Skill(skill='work-on', args='{SATELLITE_PREFIX}:{NUMBER} --under-orchestration')` (prefix from forge.yaml → repos.satellites)
761
1005
  - 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).
762
1006
  - NEVER bypass /work-on with manual git/gh commands — the label updates and structured comments are critical for tracking
763
- - **Temp files — ALWAYS use `mktemp`, NEVER a fixed path**: You are one of several agents running concurrently on this host and you share its `/tmp` with all of them. Any time you stage content in a temp file before passing it to `gh` (e.g. `--body-file`), create that path with `mktemp` (e.g. `BODY_FILE="$(mktemp)"`) — never a fixed literal like `/tmp/body.md` or `/tmp/issue.json`. A fixed path collides with another concurrently-running agent and can silently overwrite the content you staged (or you can silently overwrite theirs) before either of you reads it back — this has caused real cross-agent GitHub issue-body corruption. `mktemp` costs nothing and eliminates the collision. (forge#2198)
1007
+ - **File-backed GitHub bodies — entity scope plus read-back is mandatory**: Never stage a `gh --body-file` body at a generic shared `/tmp` path, including one made by bare `mktemp`, and never hand-roll a root-level path such as `/tmp_invbody_31076.txt`, which can hang unattended cleanup. Prefer the session scratchpad or a repo-relative scratch directory on Windows: a native Windows `gh` may not resolve Git Bash `/tmp` reliably. Create a filename that contains the target issue/PR number and an agent-unique token, then use `mktemp` for its random suffix. Put one caller-chosen marker such as `<!-- FORGE:BODY-INTEGRITY:${NUMBER}_investigator_${AGENT_TOKEN} -->` in the body. After every `gh issue create|edit` or `gh pr create|comment` using `--body-file`, re-read the target object and assert the exact marker is present; a mismatch is a hard error. Unique names reduce collisions, but only read-back detects a collision that substitutes plausible-looking content from another agent. Do not rely on eyeballing. (forge#2843, forge#2855) <!-- allowlist:check-command-side-effects -->
1008
+ - **GitHub secondary rate limit**: If a GitHub API call returns HTTP 403 with `secondary rate limit` in its response, do NOT retry it or start a polling loop. Stop GitHub content creation for this phase, report the status and response body to the orchestrator in your final result, and wait for a later batch resume. Retrying extends the throttle for every sibling.
1009
+ - **Temp files — ALWAYS use `mktemp`, NEVER hand-roll a path**: You are one of several agents running concurrently on this host and you share its `/tmp` with all of them. Any time you stage content in a temp file before passing it to `gh` (e.g. `--body-file`), create that path with `mktemp` (e.g. `BODY_FILE="$(mktemp)"`). Never write a temp file to a single-segment root path such as `/tmp_invbody_31076.txt`: Claude Code treats removing it as dangerous and requires an explicit approval that bypass mode cannot clear, hanging unattended runs. Also never use a fixed literal such as `/tmp/body.md` or `/tmp/issue.json`: a fixed path collides with another concurrently-running agent and can silently overwrite the content you staged (or you can silently overwrite theirs) before either of you reads it back. Safe: `BODY_FILE="$(mktemp)"`. Unsafe: `/tmp_invbody_31076.txt` (a missing slash that creates a root-level path) and `/tmp/body.md` (a fixed path). `mktemp` costs nothing and prevents both failures. (forge#2198, forge#2855)
764
1010
  - **`docker cp` — NEVER write into a bind-mounted shared container**: If the project you're working on runs its dev/test containers with a bind mount to the main (non-worktree) checkout, `docker cp` into that container writes through the mount into the main checkout — not your isolated per-issue worktree. Before running `docker cp` into any container, confirm its mount source is your own worktree, not a shared/main one; if you can't confirm that, don't assume an in-container test run reflects your feature branch. (forge#2198)
765
1011
  - NEVER target `main` for PRs targeting the default repo. Use `{STAGING_BRANCH}` for fast-lane issues, or `milestone/{slug}` for milestone issues.
766
1012
  - Satellite repos (MCP, n8n) have no staging branch — fast-lane PRs go to `main` for those.
@@ -950,15 +1196,28 @@ fi
950
1196
  echo "Dispatching ${#DISPATCH_NOW[@]} issue(s) this message (headroom was ${HEADROOM})"
951
1197
  ```
952
1198
 
953
- 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.
1199
+ Immediately before every Claude `Agent()` or OpenCode `task()` call, run `claim_conflicts_with_live_holder "{NUM}"`; when it returns success, append the issue to `DEFERRED_CLAIM_ISSUES[]` and do not make that call. Spawn `Agent()` for every remaining issue in `DISPATCH_NOW` (not the raw ready set) using the template above. After the batch spawn, increment `ACTIVE_DISPATCH_COUNT` by the number actually dispatched — this happens in addition to, not instead of, capturing each returned agent ID into `AGENT_ISSUE_MAP` above.
954
1200
 
955
1201
  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).
956
1202
 
957
1203
  ### Step 4B: Monitor completions and dispatch newly ready issues
958
1204
 
959
- You will be automatically notified when each background agent completes — or, for an engine-first dispatch (Step 4A, `FORGEDOCK_AVAILABLE=true`), when a backgrounded `Bash(run_in_background=true, command="forgedock run-issue ...")` call completes. In OpenCode, a native background `task` first returns a `state="running"` result and later injects a synthetic `<task id="..." state="completed">` or `state="error"` result into this same parent session. The later result is the completion event; the initial running result is not completion. Both Claude notifications and OpenCode task-result events must trigger the same per-issue handling immediately. **Do NOT use `sleep` loops, wait for the slowest sibling, or poll before processing an event.** When one arrives, look up which issue it belongs to — `AGENT_ISSUE_MAP` for a Claude `agent_completed` notification, `ENGINE_DISPATCH_MAP` for a background-Bash notification, or `OPENCODE_DISPATCH_MAP` for an OpenCode task result — and immediately process it exactly the same way regardless of which map resolved it; every check below (`classify_predecessor_state()`, dependent dispatch, stall/staging checks) keys off GitHub labels/state, not which dispatch mechanism produced them. **Fallback**: if a runtime ever fails to deliver a background completion notification, use the documented label-state recovery path, but do not convert the normal OpenCode path to foreground tasks or a wave barrier.
1205
+ You will be automatically notified when each background agent completes — or, for an engine-first dispatch (Step 4A, `FORGEDOCK_AVAILABLE=true`), when a backgrounded `Bash(run_in_background=true, command="forgedock run-issue ...")` call completes. In OpenCode, a native background `task` first returns a `state="running"` result and later injects a synthetic `<task id="..." state="completed">` or `state="error"` result into this same parent session. The later result is the completion event; the initial running result is not completion. Both Claude notifications and OpenCode task-result events must trigger the same per-issue handling immediately. **Do NOT use `sleep` loops, wait for the slowest sibling, or poll before processing an event.** When one arrives, look up which issue it belongs to — `AGENT_ISSUE_MAP` for a Claude `agent_completed` notification, `ENGINE_DISPATCH_MAP` for a background-Bash notification, or `OPENCODE_DISPATCH_MAP` for an OpenCode task result — and immediately process it exactly the same way regardless of which map resolved it; every check below (`classify_predecessor_state()`, dependent dispatch, stall/staging checks) keys off GitHub labels/state, not which dispatch mechanism produced them. For an OpenCode completion, append this terminal record before releasing capacity:
960
1206
 
961
- **Concurrency slot release (MANDATORY — first action on every completion)** <!-- Added: forge#1912 -->: The instant a completion notification arrives — `agent_completed`, a background-Bash completion, or an OpenCode `task` result with `state="completed"`/`state="error"` — 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 or fallback-dispatched (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=...)` or OpenCode `task(task_id=..., background=true)` call is issued — either re-occupies a worker slot exactly like a fresh dispatch does. (Step 4B.5's TIME-BASED hang detector — an agent that never completes at all, as opposed to one that completes at `workflow:engine-error` — remains resume-specific and out of scope for this fix: an engine-dispatched issue that silently hangs still surfaces only via the standard stall-detection alert, and `forgedock resume-stalled` remains available as a separate manual/scripted recovery path for that case. The completion-triggered case — an engine-dispatched issue that DOES complete at `workflow:engine-error` with empty committed state — is still auto-fallen-back by item 2b below, fixed forge#2743.)
1207
+ ```bash
1208
+ if [ "${DRY_RUN:-false}" = "false" ]; then
1209
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DISPATCH -->
1210
+ \`\`\`json
1211
+ {\"runtime\":\"opencode\",\"child_session_id\":\"{TASK_ID}\",\"attempt\":${ATTEMPT},\"state\":\"{completed|error}\",\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}
1212
+ \`\`\`"
1213
+ fi
1214
+ ```
1215
+
1216
+ **Fallback**: if a runtime ever fails to deliver a background completion notification, read the latest `FORGE:DISPATCH` record and use the documented label-state recovery path, but do not convert the normal OpenCode path to foreground tasks or a wave barrier.
1217
+
1218
+ For an OpenCode completion, retain `OPENCODE_DISPATCH_MAP[{NUMBER}]` until the terminal `FORGE:DISPATCH` record above has been posted successfully. Then remove that map entry and release capacity. This makes a restart deterministic: a latest `running` record restores the task-to-issue association, while a latest `completed` or `error` record cannot release the same slot twice.
1219
+
1220
+ **Concurrency slot release (MANDATORY — first action on every completion)** <!-- Added: forge#1912 -->: The instant a completion notification arrives — `agent_completed`, a background-Bash completion, or an OpenCode `task` result with `state="completed"`/`state="error"` — 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 or fallback-dispatched (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=...)` or a fresh OpenCode `task(background=true)` continuation is issued — either re-occupies a worker slot exactly like a fresh dispatch does. (Step 4B.5's TIME-BASED hang detector — an agent that never completes at all, as opposed to one that completes at `workflow:engine-error` — remains resume-specific and out of scope for this fix: an engine-dispatched issue that silently hangs still surfaces only via the standard stall-detection alert, and `forgedock resume-stalled` remains available as a separate manual/scripted recovery path for that case. The completion-triggered case — an engine-dispatched issue that DOES complete at `workflow:engine-error` with empty committed state — is still auto-fallen-back by item 2b below, fixed forge#2743.)
962
1221
 
963
1222
  **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.
964
1223
 
@@ -1019,7 +1278,7 @@ classify_predecessor_state() {
1019
1278
 
1020
1279
  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.
1021
1280
 
1022
- **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.
1281
+ **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, GATED, **or DONE**, 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 or to inject a same-file brief. 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 — and on the happy path, an over-serialized chain is never reconciled at all and stays serialized for its entire life (forge#2848).
1023
1282
 
1024
1283
  ```bash
1025
1284
  verify_file_overlap_edge() {
@@ -1049,9 +1308,25 @@ verify_file_overlap_edge() {
1049
1308
  # a MERGED PR would already classify the predecessor DONE, not FAILED/GATED — so this filter
1050
1309
  # also guarantees `gh pr diff` below targets a live, still-open branch rather than an
1051
1310
  # abandoned PR whose branch may since have been force-deleted (e.g. by /cleanup).
1311
+ #
1312
+ # forge#2848 — the DONE call site widens what this can surface: for a DONE predecessor
1313
+ # the live PR is MERGED, not OPEN. That is the *better* case, not a problem — a merged
1314
+ # PR's diff is ground truth rather than a still-moving branch, and `gh pr diff` serves it
1315
+ # from the API, so it keeps working after the head branch is deleted on merge. The
1316
+ # CLOSED-unmerged exclusion is unchanged and still correct for all three call sites.
1052
1317
  local PRED_PR
1318
+ local PRED_PR_EXIT
1053
1319
  PRED_PR=$(gh pr list -R {GH_REPO} --state all --search "\"Closes #${PRED}\" in:body" \
1054
- --json number,state --jq '[.[] | select(.state != "CLOSED")][0].number // empty' 2>/dev/null || echo "")
1320
+ --json number,state --jq '[.[] | select(.state != "CLOSED")][0].number // empty' 2>/dev/null)
1321
+ PRED_PR_EXIT=$?
1322
+
1323
+ if [ "$PRED_PR_EXIT" -ne 0 ]; then
1324
+ # Fail-safe: could not determine whether a live predecessor PR exists (transient API
1325
+ # error, rate limit, or search-index lag). Keep the edge rather than conflating a failed
1326
+ # lookup with confirmed absence of a PR and dropping a real conflict.
1327
+ echo "KEEP"
1328
+ return
1329
+ fi
1055
1330
 
1056
1331
  if [ -z "$PRED_PR" ]; then
1057
1332
  # Predecessor reached a terminal/gated state having never opened a PR (or only ever had
@@ -1101,16 +1376,28 @@ verify_file_overlap_edge() {
1101
1376
  }
1102
1377
  ```
1103
1378
 
1104
- **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.
1379
+ **Call sites**: item 6 (FAILED handling, below), item 6.5 (GATED handling, below), and the DONE arm of the core streaming dispatch loop (below — see "DONE-path edge re-verification") all 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.
1105
1380
 
1106
1381
  **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.
1107
1382
 
1108
- **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).
1383
+ **Never called from**: the `IN_PROGRESS` branch (nothing to re-verify yet — the predecessor hasn't concluded).
1384
+
1385
+ **DONE-path edge re-verification** <!-- Added: forge#2848 --> — the DONE branch *does* call this helper, and it is the only call site that fires on the happy path. Be precise about what it buys, because the obvious reading is wrong:
1386
+
1387
+ **It does NOT release the dependent.** A DONE predecessor already satisfies its own edge — see the `;; # satisfied` arm and the `ALL_PREDS_DONE` gate below. Dropping an edge whose predecessor is already DONE releases nobody, so re-verification alone cannot re-parallelize anything. What it actually buys is two things:
1388
+
1389
+ 1. **Suppressing a spurious same-file brief.** The DONE arm spends up to three `gh api` comment fetches per edge and then injects a `SAME_FILE_BRIEF` excerpt into the dependent's dispatch context. When the predecessor's real diff never touched the guessed file, that brief is not merely wasted tokens — it actively misleads the agent into coordinating on a file the predecessor never wrote. Verifying **before** those fetches skips all three calls and the brief.
1390
+ 2. **Producing the disproof signal that enables re-derivation.** This is the part that actually re-parallelizes. A `DROP` on a `body-fallback`-provenance edge is evidence that the whole cohort's extraction is suspect: the first real diff has just contradicted the guess the chain was built on. By that point the still-blocked descendants have been investigated and possibly contracted, so re-running Layer 1 extraction against those now-higher-provenance sources and dropping edges that no longer hold is what collapses the chain. This is the step an operator performed by hand in the batch that motivated forge#2848; wiring it here makes it automatic.
1109
1391
 
1110
1392
  **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.
1111
1393
 
1112
1394
  ```bash
1113
1395
  # After each agent completion, check for newly ready issues:
1396
+ READINESS_RESCAN=true
1397
+ while [ "$READINESS_RESCAN" = "true" ]; do
1398
+ # Re-derivation can remove an unresolved predecessor from an issue that this
1399
+ # pass has already visited. Repeat the scan so it can dispatch this cycle.
1400
+ READINESS_RESCAN=false
1114
1401
  for BLOCKED_NUM in {all_blocked_issue_numbers}; do
1115
1402
  ALL_PREDS_DONE=true
1116
1403
  ANY_PRED_GATED=false
@@ -1128,6 +1415,100 @@ for BLOCKED_NUM in {all_blocked_issue_numbers}; do
1128
1415
  EDGE_TYPE="${EDGE_KIND["${PRED}:${BLOCKED_NUM}"]:-}"
1129
1416
  if [ -n "$EDGE_TYPE" ] && [ -z "${EDGE_BRIEFED["${PRED}:${BLOCKED_NUM}"]:-}" ]; then
1130
1417
  EDGE_BRIEFED["${PRED}:${BLOCKED_NUM}"]=1
1418
+
1419
+ # DONE-path edge re-verification (forge#2848). Runs FIRST — before the three
1420
+ # `gh api` brief fetches below — so a disproven edge costs zero extra API calls
1421
+ # and injects no brief. EDGE_BRIEFED is already set above, so a DROP is not
1422
+ # retried on the next completion cycle.
1423
+ #
1424
+ # Fail-safe direction is unchanged: verify_file_overlap_edge returns KEEP on any
1425
+ # fetch failure, so a transient API error can only ever preserve the brief, never
1426
+ # silently suppress a real one.
1427
+ EDGE_VERDICT=$(verify_file_overlap_edge "$PRED" "$BLOCKED_NUM")
1428
+ if [ "$EDGE_VERDICT" = "DROP" ]; then
1429
+ echo "DONE-path re-verification: edge #${PRED} → #${BLOCKED_NUM} (${EDGE_TYPE}, files: ${EDGE_FILES["${PRED}:${BLOCKED_NUM}"]:-?}) DROPPED — #${PRED}'s merged diff never touched the guessed file. Skipping same-file brief."
1430
+
1431
+ # Leave SAME_FILE_BRIEF[$BLOCKED_NUM] UNSET rather than setting it empty —
1432
+ # the dispatch-context builder treats unset as the no-op case, and an empty
1433
+ # string would render an orphaned "shared file context" heading with nothing
1434
+ # under it.
1435
+
1436
+ # Cohort re-derivation (forge#2848) — the step that actually releases
1437
+ # dependents. Gated on `body-fallback` provenance only: a DROP against a
1438
+ # contract- or investigation-derived list is a one-off miss, but a DROP
1439
+ # against a raw-issue-body scrape is evidence the whole cohort's extraction
1440
+ # is suspect (see phase-3-dependency.md Layer 4's cohort-confidence guidance).
1441
+ if [ "${FILE_SOURCE[$PRED]:-}" = "body-fallback" ] && [ -n "$AFFECTED_FILES_SCRIPT" ]; then
1442
+ # Re-derive only direct descendants of this DONE predecessor that
1443
+ # are still blocked now. This intentionally excludes unrelated
1444
+ # blocked issues and descendants whose PRED edge was already removed.
1445
+ STILL_BLOCKED_DESCENDANTS=()
1446
+ for CANDIDATE in {all_blocked_issue_numbers}; do
1447
+ case " ${PREDECESSORS[$CANDIDATE]:-} " in
1448
+ *" $PRED "*) STILL_BLOCKED_DESCENDANTS+=("$CANDIDATE") ;;
1449
+ esac
1450
+ done
1451
+ for DESC in "${STILL_BLOCKED_DESCENDANTS[@]}"; do
1452
+ # Memoize conclusive results per descendant per batch. An inconclusive
1453
+ # extraction gets one retry so a transient failure or pre-contract
1454
+ # attempt does not permanently foreclose re-derivation. The two-attempt
1455
+ # cap preserves an O(descendants) API budget for the hot dispatch loop.
1456
+ [ -n "${EDGE_REDERIVED[$DESC]:-}" ] && continue
1457
+ REDERIVE_ATTEMPTS=${EDGE_REDERIVE_ATTEMPTS[$DESC]:-0}
1458
+ [ "$REDERIVE_ATTEMPTS" -ge 2 ] && continue
1459
+ EDGE_REDERIVE_ATTEMPTS[$DESC]=$((REDERIVE_ATTEMPTS + 1))
1460
+
1461
+ # Re-run Layer 1 extraction. By now DESC has been investigated and
1462
+ # possibly contracted, so this typically returns a higher-provenance
1463
+ # list than the body scrape the original edge was built from. Resolve
1464
+ # the helper through the runtime path, never a bare `bash scripts/...`
1465
+ # (#2794) — same resolver precedence as phase-3-dependency.md Layer 1.
1466
+ REDERIVE_OUT=$(bash "$AFFECTED_FILES_SCRIPT" "$DESC" -R {GH_REPO})
1467
+ REDERIVE_PROV=$(echo "$REDERIVE_OUT" | head -1 | sed 's/^PROVENANCE=//')
1468
+ REDERIVE_FILES=$(echo "$REDERIVE_OUT" | tail -n +2)
1469
+
1470
+ # Fail-safe: only act on a CONFIRMED-empty overlap. `error` means the
1471
+ # extraction was inconclusive (a `gh` call failed — forge#2504), and
1472
+ # `none`/`body-fallback` mean we learned nothing better than what we
1473
+ # already had. In all three cases keep every edge untouched.
1474
+ case "$REDERIVE_PROV" in
1475
+ contract-deliverables|affected-files-section) EDGE_REDERIVED[$DESC]=1 ;;
1476
+ *) echo " re-derivation for #${DESC} inconclusive (provenance: ${REDERIVE_PROV}) — keeping all edges"; continue ;;
1477
+ esac
1478
+
1479
+ # Re-derive only PRED's edge into DESC. PRED is the DONE predecessor
1480
+ # whose merged diff just disproved this edge; other predecessors may
1481
+ # still be building and must retain their serialization edges.
1482
+ for DESC_PRED in "$PRED"; do
1483
+ [ -z "${EDGE_KIND["${DESC_PRED}:${DESC}"]:-}" ] && continue
1484
+ STILL_OVERLAPS=false
1485
+ for EF in ${EDGE_FILES["${DESC_PRED}:${DESC}"]:-}; do
1486
+ [ -z "$EF" ] && continue
1487
+ EF_CLEAN="${EF%/}"
1488
+ EF_ESCAPED=$(printf '%s' "$EF_CLEAN" | sed 's/[.[\*^$()+?{|]/\\&/g')
1489
+ if echo "$REDERIVE_FILES" | grep -qE "(^|/)${EF_ESCAPED}(/|$)"; then
1490
+ STILL_OVERLAPS=true
1491
+ break
1492
+ fi
1493
+ done
1494
+ if [ "$STILL_OVERLAPS" = "false" ]; then
1495
+ echo " re-derived edge #${DESC_PRED} → #${DESC}: DROPPED — '${EDGE_FILES["${DESC_PRED}:${DESC}"]}' absent from #${DESC}'s refreshed ${REDERIVE_PROV} file list"
1496
+ unset 'EDGE_KIND['"${DESC_PRED}:${DESC}"']'
1497
+ unset 'EDGE_FILES['"${DESC_PRED}:${DESC}"']'
1498
+ # Remove DESC_PRED from PREDECESSORS[$DESC] so the ALL_PREDS_DONE
1499
+ # gate below stops waiting on it. DESC becomes dispatch-eligible on
1500
+ # this same cycle if that was its last outstanding predecessor.
1501
+ PREDECESSORS[$DESC]=$(echo "${PREDECESSORS[$DESC]}" | tr ' ' '\n' | grep -vx "$DESC_PRED" | tr '\n' ' ')
1502
+ READINESS_RESCAN=true
1503
+ fi
1504
+ done
1505
+ done
1506
+ fi
1507
+
1508
+ # Edge disproven — nothing further to do for this (PRED, BLOCKED_NUM) pair.
1509
+ continue
1510
+ fi
1511
+
1131
1512
  SHARED_FILE="${EDGE_FILES["${PRED}:${BLOCKED_NUM}"]:-}"
1132
1513
 
1133
1514
  # Prefer FORGE:BUILDER (the concrete "what changed" record). Fall back to
@@ -1179,6 +1560,7 @@ for BLOCKED_NUM in {all_blocked_issue_numbers}; do
1179
1560
  # Do NOT dispatch. Do NOT mark skipped. Tracked via item 6.5.
1180
1561
  fi
1181
1562
  done
1563
+ done
1182
1564
  # Run Steps 4A.pre.0 → 4A.pre → 4A for newly ready issues. Step 4A's own dispatch-batch
1183
1565
  # computation (the HEADROOM/DISPATCH_NOW logic, forge#1912) is what actually caps this —
1184
1566
  # "newly ready" here just means "eligible," not "guaranteed to dispatch this cycle." Any
@@ -1338,11 +1720,7 @@ done
1338
1720
  # After CLAIM_RELEASED for issue NUM:
1339
1721
  # Read all active FORGE:CLAIM annotations from coordination issue
1340
1722
  if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_NUM:-}" ]; then
1341
- ACTIVE_CLAIMS=$(gh api repos/{GH_REPO}/issues/${COORD_NUM}/comments \
1342
- --jq '[.[] | select(.body | contains("<!-- FORGE:CLAIM -->")) |
1343
- select(.body | contains("<!-- FORGE:CLAIM_RELEASED -->") | not)] |
1344
- map({holder: (.body | capture("\\*\\*Holder\\*\\*: (?P<h>[^\\n]+)").h),
1345
- files: (.body | capture("\\*\\*Files\\*\\*: (?P<f>[^\\n]+)").f)})' 2>/dev/null || echo '[]')
1723
+ ACTIVE_CLAIMS=$(read_active_claims "$COORD_NUM" 2>/dev/null || echo '[]')
1346
1724
  # For each still-blocked issue in a Layer-2/4 pair: check if its claim's file set
1347
1725
  # is disjoint from all remaining active claims. If so, mark it ready.
1348
1726
  # (Layer-1 and Layer-3 edges are never relaxed — this check is Layer-2/4 only.)
@@ -1406,7 +1784,7 @@ done
1406
1784
  run_in_background=true,
1407
1785
  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}.
1408
1786
 
1409
- **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.
1787
+ **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 classifies the gate first, replaces `needs-human` with `workflow:in-review` only for fixable remediation, 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.
1410
1788
 
1411
1789
  **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`.
1412
1790
 
@@ -1552,6 +1930,7 @@ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was
1552
1930
  ```
1553
1931
  ✓ #{NUMBER} — {title} → PR #{PR} merged to {target}
1554
1932
  ✗ #{NUMBER} — {title} → {reason for failure}
1933
+ ⚠ #{NUMBER} — {title} → PR #{PR} blocked: review panel degraded (full fresh-session re-review required)
1555
1934
  ⚠ #{NUMBER} — {title} → PIPELINE BYPASS (no /work-on — PR invalid)
1556
1935
  ⏸ #{NUMBER} — {title} → PR #{PR} awaiting-merge (remediated + re-approved, `main`/deploy-gate base — human merge only, no diagnosis needed; non-`main` bases now auto-land to `workflow:merged` via remediate.md M7, forge#2570)
1557
1936
  🔗 #{NUMBER} — {title} → blocked-on-human-merge (gated by #{PRED}, will auto-dispatch on #{PRED} merge)
@@ -1577,6 +1956,8 @@ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was
1577
1956
  dependent itself has no problem at all, it is simply waiting on someone else's merge. See Phase 6's
1578
1957
  "Merge-Ready" report section (`phase-6-report.md` Step 6A.5/6B) for the batch-level rollup.
1579
1958
 
1959
+ Before printing a terminal status for a PR, inspect its labels. If it has `review-degraded`, print the dedicated blocked line above and retain the issue's `needs-human` state. Never report a degraded review as merged or awaiting-merge.
1960
+
1580
1961
  9. **Run staging integrity check** (from Step 4A-pre) if the completed agent merged a PR targeting staging.
1581
1962
 
1582
1963
  **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 → **call `release_orchestrator_lease()` (Step 4A-pre.-1)** — this is a clean/paused drain of this session's own dispatch loop, so the lease should not be left held for a now-idle session — then proceed to Phase 5. <!-- Added: forge#2627 -->
@@ -1737,6 +2118,56 @@ that mode only exists as the explicit, one-shot `--include-backlog` opt-in at Ph
1737
2118
  never silently widen to the full backlog regardless of any flag, since nothing here is a
1738
2119
  human-in-the-loop request. <!-- Added: forge#2628 -->
1739
2120
 
2121
+ ### Step 4C.0: Full-repository actionable-issue intake (`policy: all` only)
2122
+
2123
+ An operator-authorised `orchestration.cascade.policy: all` run must also check for newly-created
2124
+ open issues that its own agents did not report and that lack `review-finding`. Run this sweep
2125
+ after every five terminal completions, before the next dispatch cycle. It is not a cascade
2126
+ replacement: Step 4C's `BATCH_T0` filter still governs review-finding recursion.
2127
+
2128
+ At batch start, seed `SEEN_OPEN_ISSUES` with the initial resolved issue set. On each sweep, list
2129
+ the complete open set (`gh issue list --state open --limit 500 --json number,title,labels`), diff
2130
+ it against `SEEN_OPEN_ISSUES`, then mark every returned number seen whether or not it is admitted.
2131
+ For each newly-seen issue, add it to the normal Phase 3 DAG intake only when it has no terminal or
2132
+ in-progress workflow label, `needs-human`, or `workflow:decomposed`; preserve its labels and
2133
+ record why every excluded issue was skipped. Do not filter on `review-finding`, author, creation
2134
+ time, or agent trajectory. This is how CI-created issues enter an all-policy run without silently
2135
+ adopting pre-existing work in other policies.
2136
+
2137
+ ```bash
2138
+ if [ "$CASCADE_POLICY_NAME" = "all" ] && [ "$((TERMINAL_COMPLETIONS % 5))" -eq 0 ] && \
2139
+ [ "$TERMINAL_COMPLETIONS" -gt "$FULL_REPO_SWEEP_COUNT" ]; then
2140
+ FULL_REPO_SWEEP_COUNT=$TERMINAL_COMPLETIONS
2141
+ FULL_REPO_OPEN=$(gh issue list -R {GH_REPO} --state open --limit 500 --json number,title,labels)
2142
+ while IFS= read -r ISSUE; do
2143
+ NUM=$(echo "$ISSUE" | jq -r '.number')
2144
+ [ -n "${SEEN_OPEN_ISSUES[$NUM]:-}" ] && continue
2145
+ SEEN_OPEN_ISSUES[$NUM]=1
2146
+ LABELS=$(echo "$ISSUE" | jq -r '[.labels[].name] | join(",")')
2147
+ if echo "$LABELS" | grep -qE '(^|,)(workflow:(merged|invalid|awaiting-merge|building|in-review|decomposed)|needs-human)(,|$)'; then
2148
+ echo "Full-repo intake: #${NUM} seen but not admitted (terminal, in-progress, or human-gated)"
2149
+ continue
2150
+ fi
2151
+ FULL_REPO_INTAKE+=("$NUM")
2152
+ echo "Full-repo intake: admitted newly-seen actionable issue #${NUM}"
2153
+ done < <(echo "$FULL_REPO_OPEN" | jq -c '.[]')
2154
+ # Feed FULL_REPO_INTAKE through the existing Phase 3 extraction/DAG/claims gates.
2155
+ fi
2156
+ ```
2157
+
2158
+ ### Step 4C.1: Mechanical automated-alert deduplication (`policy: all` opt-in)
2159
+
2160
+ `orchestration.cascade.dedup_automated: true` permits a narrow exception to the normal
2161
+ no-dedup rule for newly-seen automated alerts. Select one lowest-numbered canonical issue and
2162
+ close another issue only when all of these are true: both authors are bot/app accounts; normalized
2163
+ titles are identical; the generator identity is identical; the triggering condition is identical;
2164
+ and the closing comment links the retained canonical issue. Never apply title similarity to a
2165
+ human-authored report, or when generator/trigger provenance is absent or differs. The reference
2166
+ predicate is `canDeduplicateAutomatedAlert()` in `bin/engine/admission.mjs`.
2167
+
2168
+ This opt-in handles mechanically identical alerts, not substantive bug adjudication. All other
2169
+ possible duplicates remain separate and go through investigation.
2170
+
1740
2171
  ```bash
1741
2172
  # Method 1: Read TRAJECTORY comments from completed issues for "Finding issues" row
1742
2173
  for NUM in {completed_issue_number}; do
@@ -1757,13 +2188,37 @@ gh issue list -R {GH_REPO} --state open --search "label:review-finding created:>
1757
2188
 
1758
2189
  **If review-finding issues were spawned:**
1759
2190
 
2191
+ **Amplification accounting (MANDATORY, before cascade admission):** Count every finding created by this batch, including findings later deferred by any cascade rule. After each merged unit, compute `FINDINGS_SPAWNED / MERGED_UNITS`, append the value to `AMPLIFICATION_RATIO_HISTORY`, and print a convergence warning when the latest `CONVERGENCE_WINDOW` observations are all `>= 1.0`. A high ratio is a signal, not a failure: valuable deep review findings can legitimately raise it.
2192
+
2193
+ For each finding, extract its source PR from `**Source**: PR #N` and increment `FINDINGS_BY_SOURCE_PR[N]`. Classify it as a **same-lineage refinement** only when that source PR closes a `review-finding` issue and both that parent finding and this finding name the same affected file; otherwise classify it as **new surface**. Record the finding number in `REFINEMENT_FINDINGS` or `NEW_SURFACE_FINDINGS` respectively. If provenance or a file path cannot be established, classify as new surface conservatively; never suppress it as a refinement.
2194
+
2195
+ When `CASCADE_MAX_AMPLIFICATION` is not `off`, and the current ratio is greater than that ceiling, defer only newly discovered entries in `REFINEMENT_FINDINGS` with reason `amplification bound exceeded (same-lineage refinement)`. Add them to `DEFERRED_FINDINGS` and `DEFERRED_REASONS` so Step 4F re-evaluates them after the batch drains. Do not apply this bound to new-surface findings, P1/P2 findings, or any finding when the option is `off`.
2196
+
2197
+ ```bash
2198
+ # Run once for each completed issue before collecting its findings. Only merged
2199
+ # units advance the denominator; invalid/skipped units do not imply delivered work.
2200
+ if gh issue view "$NUM" -R {GH_REPO} --json labels \
2201
+ --jq '[.labels[].name | select(. == "workflow:merged")] | length' | grep -qx '1'; then
2202
+ MERGED_UNITS=$((MERGED_UNITS + 1))
2203
+ AMPLIFICATION_RATIO=$(awk "BEGIN { printf \"%.2f\", $FINDINGS_SPAWNED / $MERGED_UNITS }")
2204
+ AMPLIFICATION_RATIO_HISTORY+=("$AMPLIFICATION_RATIO")
2205
+ if [ "${#AMPLIFICATION_RATIO_HISTORY[@]}" -ge "$CONVERGENCE_WINDOW" ]; then
2206
+ RECENT_RATIOS=("${AMPLIFICATION_RATIO_HISTORY[@]: -$CONVERGENCE_WINDOW}")
2207
+ if printf '%s\n' "${RECENT_RATIOS[@]}" | awk '$1 < 1 { exit 1 }'; then
2208
+ echo "CONVERGENCE WARNING: amplification has remained >= 1.0 for ${CONVERGENCE_WINDOW} merged units (${AMPLIFICATION_RATIO} current). This can be productive review refinement, but the batch is not shrinking."
2209
+ fi
2210
+ fi
2211
+ fi
2212
+
2213
+ ```
2214
+
1760
2215
  **Cascade control (MANDATORY — run before folding findings into the DAG):**
1761
2216
 
1762
2217
  For each spawned finding, determine whether it should be **executed** or **deferred**:
1763
2218
 
1764
2219
  **Evaluation order** (first matching rule wins):
1765
2220
  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. **Configurable** via `orchestration.cascade.defer_on_batch_gated` (default `true`; `false` under `policy: all` — forge#2234).
1766
- 1. **Generation ≥ 2** (always defer, even for P1/P2, for autonomous mid-run cascade — see scope note below): 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 within this Step 4C triage pass. Rationale: gen-2+ cascade is theoretically unbounded — cap it here. **Scope**: this rule caps *autonomous* cascade — findings discovered and re-triaged automatically during an unattended run. It is not a cap on what a human can explicitly request. When an operator directly asks for cascade/review-finding work via `phase-1-resolve.md`'s dedicated `cascade`/`review-findings`/`findings` resolution (with `--include-deferred`/`--allow-gen2`, or the `orchestration.cascade.max_generation` config lever from #2234), those findings enter the DAG through Phase 1 resolution, not through this Step 4C mid-run triage — this rule still defers anything Step 4C itself discovers mid-run, including inside a human-requested gen≥2 batch (see recursion safety below). **Not configurable here** — `orchestration.cascade.max_generation` governs Phase 1 resolve-time admission only (see `phase-1-resolve.md` "Cascade / Review-Finding Resolution"); this Step 4C rule stays an absolute autonomous-cascade cap regardless of policy, including `policy: all`. <!-- Added: forge#2231 -->
2221
+ 1. **Generation ≥ 2** (always defer from individual autonomous dispatch, even for P1/P2, for mid-run cascade — see scope note below): Finding was spawned by an issue that was itself a review-finding. A P3 finding at or below the finite `orchestration.cascade.batch_max_generation` ceiling (default 2) is retained only as a candidate for the bounded P3 batch pass; it remains deferred if no batch forms. A formed batch records `FORGE:BATCH_MAX_GENERATION` and its generation-2+ members. No P1/P2, or finding above that ceiling, takes this exception. Rationale: gen-2+ individual cascade is theoretically unbounded — cap it here while permitting one auditable aggregation unit. **Scope**: this rule caps *autonomous* cascade — findings discovered and re-triaged automatically during an unattended run. It is not a cap on what a human can explicitly request. When an operator directly asks for cascade/review-finding work via `phase-1-resolve.md`'s dedicated `cascade`/`review-findings`/`findings` resolution (with `--include-deferred`/`--allow-gen2`, or the `orchestration.cascade.max_generation` config lever from #2234), those findings enter the DAG through Phase 1 resolution, not through this Step 4C mid-run triage. `batch_max_generation` is separate from `max_generation`, and remains finite even under `policy: all`. <!-- Added: forge#2231, forge#2849 -->
1767
2222
  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.
1768
2223
  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. **Configurable** via `orchestration.cascade.keyword_heuristic` (default `true`; `false` under `policy: all` — forge#2234).
1769
2224
  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. **Configurable** via `orchestration.cascade.p3_same_file_defer` (default `true`; `false` under `policy: all` — forge#2234).
@@ -1799,6 +2254,22 @@ ALL_BATCH_FILES=$(echo "$ALL_BATCH_FILES" | sort -u | grep -v '^$')
1799
2254
  ```
1800
2255
 
1801
2256
  ```bash
2257
+ # Use the same bounded ancestor walk as Phase 1 so batching records actual depth,
2258
+ # not just whether the immediate source is a review finding.
2259
+ compute_finding_generation() {
2260
+ local body="$1" generation=1 hops=0 source_num source_data
2261
+ while [ "$hops" -lt 10 ]; do
2262
+ source_num=$(echo "$body" | grep -ioE 'spawned from issue #[0-9]+|source issue[: #]+[0-9]+' | head -1 | grep -oE '[0-9]+$')
2263
+ [ -z "$source_num" ] && break
2264
+ source_data=$(gh issue view "$source_num" -R {GH_REPO} --json labels,body 2>/dev/null) || break
2265
+ echo "$source_data" | jq -e '[.labels[].name] | index("review-finding")' >/dev/null 2>&1 || break
2266
+ generation=$((generation + 1))
2267
+ body=$(echo "$source_data" | jq -r '.body')
2268
+ hops=$((hops + 1))
2269
+ done
2270
+ echo "$generation"
2271
+ }
2272
+
1802
2273
  # For each finding, check its priority label and generation
1803
2274
  # NOTE: DEFERRED_FINDINGS, QUEUED_FINDINGS, and DEFERRED_REASONS are declared at
1804
2275
  # batch scope in Step 4A.pre — do NOT re-initialize them here (Step 4C runs per-agent).
@@ -1806,6 +2277,45 @@ for FINDING_NUM in {spawned_finding_numbers}; do
1806
2277
  FINDING_DATA=$(gh issue view $FINDING_NUM -R {GH_REPO} --json labels,title,body,updatedAt \
1807
2278
  --jq '{labels: [.labels[].name], title: .title, body: .body, updatedAt: .updatedAt}')
1808
2279
 
2280
+ # Count each finding once and retain source provenance for the final per-PR
2281
+ # breakdown. Missing source/file evidence fails open as new surface.
2282
+ FINDING_BODY=$(echo "$FINDING_DATA" | jq -r '.body')
2283
+ SOURCE_PR=""
2284
+ if [[ "$FINDING_BODY" =~ \*\*[Ss]ource\*\*:[[:space:]]*[Pp][Rr][[:space:]]*\#([0-9]+) ]]; then
2285
+ SOURCE_PR="${BASH_REMATCH[1]}"
2286
+ fi
2287
+ if [ -z "${AMPLIFICATION_FINDING_SEEN[$FINDING_NUM]:-}" ]; then
2288
+ AMPLIFICATION_FINDING_SEEN[$FINDING_NUM]=1
2289
+ FINDINGS_SPAWNED=$((FINDINGS_SPAWNED + 1))
2290
+ [ -n "$SOURCE_PR" ] && FINDINGS_BY_SOURCE_PR[$SOURCE_PR]=$(( ${FINDINGS_BY_SOURCE_PR[$SOURCE_PR]:-0} + 1 ))
2291
+ fi
2292
+ FINDING_IS_REFINEMENT=false
2293
+ FINDING_FILE_FOR_LINEAGE=$(echo "$FINDING_BODY" | grep -oE '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|sh|md)`' | head -1 | tr -d '`')
2294
+ if [ -n "$SOURCE_PR" ] && [ -n "$FINDING_FILE_FOR_LINEAGE" ]; then
2295
+ SOURCE_ISSUE=""
2296
+ SOURCE_PR_BODY=$(gh pr view "$SOURCE_PR" -R {GH_REPO} --json body --jq '.body' 2>/dev/null || echo "")
2297
+ SOURCE_PR_BODY_LOWER=$(echo "$SOURCE_PR_BODY" | tr '[:upper:]' '[:lower:]')
2298
+ if [[ "$SOURCE_PR_BODY_LOWER" =~ (close[sd]?|fix(e[sd])?|resolve[sd]?)[[:space:]]+\#([0-9]+) ]]; then
2299
+ SOURCE_ISSUE="${BASH_REMATCH[3]}"
2300
+ fi
2301
+ if [ -n "$SOURCE_ISSUE" ]; then
2302
+ PARENT_DATA=$(gh issue view "$SOURCE_ISSUE" -R {GH_REPO} --json labels,body \
2303
+ --jq '{labels: [.labels[].name], body: .body}' 2>/dev/null || echo '{}')
2304
+ PARENT_FILE=$(echo "$PARENT_DATA" | jq -r '.body // ""' | grep -oE '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|sh|md)`' | head -1 | tr -d '`')
2305
+ if echo "$PARENT_DATA" | jq -e '[.labels[] | select(. == "review-finding")] | length > 0' >/dev/null && \
2306
+ [ "$PARENT_FILE" = "$FINDING_FILE_FOR_LINEAGE" ]; then
2307
+ FINDING_IS_REFINEMENT=true
2308
+ REFINEMENT_FINDINGS[$FINDING_NUM]="$SOURCE_PR"
2309
+ else
2310
+ NEW_SURFACE_FINDINGS[$FINDING_NUM]="$SOURCE_PR"
2311
+ fi
2312
+ else
2313
+ NEW_SURFACE_FINDINGS[$FINDING_NUM]="$SOURCE_PR"
2314
+ fi
2315
+ else
2316
+ NEW_SURFACE_FINDINGS[$FINDING_NUM]="${SOURCE_PR:-unknown}"
2317
+ fi
2318
+
1809
2319
  # Code-branch repair guard (MANDATORY — before priority/defer heuristics below).
1810
2320
  # A review-finding with no **Code branch** annotation, whose parent PR's base is
1811
2321
  # not the staging fast lane, would otherwise silently fall through to
@@ -1870,30 +2380,33 @@ for FINDING_NUM in {spawned_finding_numbers}; do
1870
2380
  # as its own outcome and fail closed, mirroring the concurrent-edit branch below
1871
2381
  # rather than silently falling through to the unprotected write.
1872
2382
  echo "REPAIR: #${FINDING_NUM} skipped — freshness re-fetch failed (network error or API rate limit); cannot verify no concurrent edit occurred, failing closed instead of risking a silent clobber"
1873
- # forge#2584: stage the body in a mktemp file and deliver it via --body-file
1874
- # instead of interpolating finding-derived variables (${FINDING_NUM},
1875
- # ${REPAIR_SOURCE_PR}, ${REPAIR_PARENT_BASE}, ${FINDING_UPDATED_AT_SNAPSHOT})
1876
- # directly into a double-quoted --body argument at the gh call site — mirrors
1877
- # REPAIRED_BODY's existing printf-into-variable approach and the repo-wide
1878
- # mktemp + --body-file convention (forge#2198). mktemp avoids /tmp path
1879
- # collisions with other concurrently-running orchestrated agents.
1880
- GATE_BODY_TMPFILE="$(mktemp)"
2383
+ # Scope the scratch name to both this finding and this orchestrator process.
2384
+ # The marker read-back below detects a plausible-looking body substitution.
2385
+ SCRATCHPAD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"
2386
+ AGENT_TOKEN="${AGENT_ID:-${HOSTNAME:-orchestrator}-$$}"
2387
+ mkdir -p "$SCRATCHPAD"
2388
+ GATE_BODY_MARKER="FORGE:BODY-INTEGRITY:${FINDING_NUM}_freshness-gate_${AGENT_TOKEN}"
2389
+ GATE_BODY_TMPFILE="$(mktemp "$SCRATCHPAD/${FINDING_NUM}_freshness-gate_${AGENT_TOKEN}.XXXXXX.md")"
1881
2390
  printf '%s' "<!-- FORGE:GATE_FAILURE -->
1882
2391
  ## Code Branch Repair Skipped — Freshness Re-fetch Failed
1883
2392
 
1884
- Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${REPAIR_SOURCE_PR} bases on \`${REPAIR_PARENT_BASE}\` — not the staging fast lane. Automatic repair was skipped because the freshness re-fetch (\`gh issue view --json updatedAt\`) failed, so the concurrency guard could not confirm the issue body is still at the snapshot captured earlier in this iteration (\`${FINDING_UPDATED_AT_SNAPSHOT}\`). Proceeding with the write here would risk silently clobbering a concurrent edit with no way to verify. Human review required to confirm the current body state and, if still needed, re-apply the Code branch stamp manually. <!-- forge#2565 -->" > "$GATE_BODY_TMPFILE"
2393
+ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${REPAIR_SOURCE_PR} bases on \`${REPAIR_PARENT_BASE}\` — not the staging fast lane. Automatic repair was skipped because the freshness re-fetch (\`gh issue view --json updatedAt\`) failed, so the concurrency guard could not confirm the issue body is still at the snapshot captured earlier in this iteration (\`${FINDING_UPDATED_AT_SNAPSHOT}\`). Proceeding with the write here would risk silently clobbering a concurrent edit with no way to verify. Human review required to confirm the current body state and, if still needed, re-apply the Code branch stamp manually. <!-- forge#2565 -->
2394
+ <!-- ${GATE_BODY_MARKER} -->" > "$GATE_BODY_TMPFILE"
1885
2395
  gh issue comment "$FINDING_NUM" -R {GH_REPO} --body-file "$GATE_BODY_TMPFILE" 2>/dev/null || true
2396
+ gh api "repos/{GH_REPO}/issues/${FINDING_NUM}/comments" --jq '.[].body' | grep -Fqx "<!-- ${GATE_BODY_MARKER} -->" || { echo "ERROR: freshness-gate comment marker missing" >&2; exit 1; }
1886
2397
  rm -f "$GATE_BODY_TMPFILE"
1887
2398
  gh issue edit "$FINDING_NUM" -R {GH_REPO} --add-label needs-human 2>/dev/null || true # freshness-recheck-failure path
1888
2399
  elif [ "$FINDING_UPDATED_AT_CURRENT" != "$FINDING_UPDATED_AT_SNAPSHOT" ]; then
1889
2400
  echo "REPAIR: #${FINDING_NUM} skipped — concurrent edit detected (updatedAt changed from ${FINDING_UPDATED_AT_SNAPSHOT} to ${FINDING_UPDATED_AT_CURRENT} since this iteration's initial read); flagging instead of repairing"
1890
- # forge#2584: mktemp + --body-file, same rationale as the freshness-recheck-failure path above.
1891
- CONCURRENT_EDIT_BODY_TMPFILE="$(mktemp)"
2401
+ CONCURRENT_EDIT_BODY_MARKER="FORGE:BODY-INTEGRITY:${FINDING_NUM}_concurrent-edit_${AGENT_TOKEN}"
2402
+ CONCURRENT_EDIT_BODY_TMPFILE="$(mktemp "$SCRATCHPAD/${FINDING_NUM}_concurrent-edit_${AGENT_TOKEN}.XXXXXX.md")"
1892
2403
  printf '%s' "<!-- FORGE:GATE_FAILURE -->
1893
2404
  ## Code Branch Repair Skipped — Concurrent Edit Detected
1894
2405
 
1895
- Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${REPAIR_SOURCE_PR} bases on \`${REPAIR_PARENT_BASE}\` — not the staging fast lane. Automatic repair was skipped because the issue body was edited concurrently (\`updatedAt\` changed from \`${FINDING_UPDATED_AT_SNAPSHOT}\` to \`${FINDING_UPDATED_AT_CURRENT}\` between this run's initial read and the repair write). Overwriting the body now would have silently discarded that concurrent edit. Human review required to reconcile and, if still needed, re-apply the Code branch stamp manually. <!-- forge#2512 -->" > "$CONCURRENT_EDIT_BODY_TMPFILE"
2406
+ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${REPAIR_SOURCE_PR} bases on \`${REPAIR_PARENT_BASE}\` — not the staging fast lane. Automatic repair was skipped because the issue body was edited concurrently (\`updatedAt\` changed from \`${FINDING_UPDATED_AT_SNAPSHOT}\` to \`${FINDING_UPDATED_AT_CURRENT}\` between this run's initial read and the repair write). Overwriting the body now would have silently discarded that concurrent edit. Human review required to reconcile and, if still needed, re-apply the Code branch stamp manually. <!-- forge#2512 -->
2407
+ <!-- ${CONCURRENT_EDIT_BODY_MARKER} -->" > "$CONCURRENT_EDIT_BODY_TMPFILE"
1896
2408
  gh issue comment "$FINDING_NUM" -R {GH_REPO} --body-file "$CONCURRENT_EDIT_BODY_TMPFILE" 2>/dev/null || true
2409
+ gh api "repos/{GH_REPO}/issues/${FINDING_NUM}/comments" --jq '.[].body' | grep -Fqx "<!-- ${CONCURRENT_EDIT_BODY_MARKER} -->" || { echo "ERROR: concurrent-edit comment marker missing" >&2; exit 1; }
1897
2410
  rm -f "$CONCURRENT_EDIT_BODY_TMPFILE"
1898
2411
  gh issue edit "$FINDING_NUM" -R {GH_REPO} --add-label needs-human 2>/dev/null || true # concurrent-edit path
1899
2412
  else
@@ -1904,13 +2417,15 @@ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${R
1904
2417
  FINDING_DATA=$(gh issue view $FINDING_NUM -R {GH_REPO} --json labels,title,body,updatedAt \
1905
2418
  --jq '{labels: [.labels[].name], title: .title, body: .body, updatedAt: .updatedAt}')
1906
2419
  else
1907
- # forge#2584: mktemp + --body-file, same rationale as the two failure paths above.
1908
- REPAIR_FAILED_BODY_TMPFILE="$(mktemp)"
2420
+ REPAIR_FAILED_BODY_MARKER="FORGE:BODY-INTEGRITY:${FINDING_NUM}_repair-failure_${AGENT_TOKEN}"
2421
+ REPAIR_FAILED_BODY_TMPFILE="$(mktemp "$SCRATCHPAD/${FINDING_NUM}_repair-failure_${AGENT_TOKEN}.XXXXXX.md")"
1909
2422
  printf '%s' "<!-- FORGE:GATE_FAILURE -->
1910
2423
  ## Code Branch Repair Failed
1911
2424
 
1912
- Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${REPAIR_SOURCE_PR} bases on \`${REPAIR_PARENT_BASE}\` — not the staging fast lane. Automatic repair (\`gh issue edit --body\`) failed. Without this annotation, \`/work-on\`'s investigation phase may look for the code on the wrong branch (staging, where it is absent) and misclassify this confirmed finding as invalid. Human review required. <!-- forge#2443 -->" > "$REPAIR_FAILED_BODY_TMPFILE"
2425
+ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${REPAIR_SOURCE_PR} bases on \`${REPAIR_PARENT_BASE}\` — not the staging fast lane. Automatic repair (\`gh issue edit --body\`) failed. Without this annotation, \`/work-on\`'s investigation phase may look for the code on the wrong branch (staging, where it is absent) and misclassify this confirmed finding as invalid. Human review required. <!-- forge#2443 -->
2426
+ <!-- ${REPAIR_FAILED_BODY_MARKER} -->" > "$REPAIR_FAILED_BODY_TMPFILE"
1913
2427
  gh issue comment "$FINDING_NUM" -R {GH_REPO} --body-file "$REPAIR_FAILED_BODY_TMPFILE" 2>/dev/null || true
2428
+ gh api "repos/{GH_REPO}/issues/${FINDING_NUM}/comments" --jq '.[].body' | grep -Fqx "<!-- ${REPAIR_FAILED_BODY_MARKER} -->" || { echo "ERROR: repair-failure comment marker missing" >&2; exit 1; }
1914
2429
  rm -f "$REPAIR_FAILED_BODY_TMPFILE"
1915
2430
  gh issue edit "$FINDING_NUM" -R {GH_REPO} --add-label needs-human 2>/dev/null || true # repair-failure path
1916
2431
  fi
@@ -1947,7 +2462,8 @@ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${R
1947
2462
  # all" sets this to false so an operator draining a backlog is never idle-gated.
1948
2463
  if [ "$CASCADE_DEFER_ON_BATCH_GATED" = "true" ] && [ "${BATCH_FULLY_GATED:-false}" = "true" ]; then
1949
2464
  DEFER=true; DEFER_REASON="batch fully human-gated — idle policy"
1950
- # Heuristic 1: Generation check — source issue has review-finding label (always defer, even for P1/P2)
2465
+ # Heuristic 1: Generation check — source issue has review-finding label. Always defer
2466
+ # individual dispatch; only bounded P3 aggregation can consume an eligible member.
1951
2467
  # NOT gated by orchestration.cascade.max_generation — this is Step 4C's autonomous
1952
2468
  # mid-run cascade cap, which stays absolute regardless of config (see forge#2231's
1953
2469
  # scope note above and phase-1-resolve.md's Cascade / Review-Finding Resolution
@@ -1956,9 +2472,17 @@ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${R
1956
2472
  elif SOURCE_NUM=$(echo "$FINDING_DATA" | jq -r '.body' | grep -oP '(?i)spawned from issue #\K\d+|source issue[: #]+\K\d+' | head -1) && \
1957
2473
  [ -n "$SOURCE_NUM" ] && \
1958
2474
  gh issue view $SOURCE_NUM -R {GH_REPO} --json labels --jq '[.labels[].name]' 2>/dev/null | grep -q "review-finding"; then
1959
- DEFER=true; DEFER_REASON="generation >= 2 (source #${SOURCE_NUM} is also a review-finding)"
1960
- # Priority override: P1 or P2 always execute — skip remaining heuristics
1961
- elif [ "$PRIORITY" = "P1" ] || [ "$PRIORITY" = "P2" ]; then
2475
+ FINDING_GENERATION=$(compute_finding_generation "$(echo "$FINDING_DATA" | jq -r '.body')")
2476
+ FINDING_GENERATIONS[$FINDING_NUM]="$FINDING_GENERATION"
2477
+ DEFER=true; DEFER_REASON="generation ${FINDING_GENERATION} (source #${SOURCE_NUM} is also a review-finding)"
2478
+ if [ "$PRIORITY" = "P3" ] && [ "$FINDING_GENERATION" -le "$BATCH_MAX_GENERATION" ]; then
2479
+ # Keep this out of individual cascade dispatch, but offer it to the P3 batching
2480
+ # pass below. A singleton remains deferred; only a formed batch is admitted.
2481
+ BATCHABLE_DEFERRED_P3+=("$FINDING_NUM")
2482
+ DEFER_REASON="generation ${FINDING_GENERATION} — eligible for bounded P3 batching only"
2483
+ fi
2484
+ # Priority override: P0/P1 always execute; P2 is eligible for batch planning.
2485
+ elif [ "$PRIORITY" = "P0" ] || [ "$PRIORITY" = "P1" ]; then
1962
2486
  DEFER=false
1963
2487
  # Heuristic 2: Comment/typo keyword (only applies to P3 and below)
1964
2488
  # Gated by CASCADE_KEYWORD_HEURISTIC (orchestration.cascade, forge#2234).
@@ -1978,6 +2502,20 @@ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${R
1978
2502
  DEFER=false
1979
2503
  fi
1980
2504
 
2505
+ [ -n "${FINDING_GENERATIONS[$FINDING_NUM]:-}" ] || FINDING_GENERATIONS[$FINDING_NUM]=1
2506
+
2507
+ # Opt-in only: never defer a new-surface or P1/P2 finding because another
2508
+ # lineage is noisy. Step 4F re-evaluates this explicit deferral after drain.
2509
+ AMPLIFICATION_RATIO=$(awk "BEGIN { if ($MERGED_UNITS) printf \"%.2f\", $FINDINGS_SPAWNED / $MERGED_UNITS; else print 0 }")
2510
+ if [ "$DEFER" = "false" ] && [ "$FINDING_IS_REFINEMENT" = "true" ] && \
2511
+ [ "$PRIORITY" != "P1" ] && [ "$PRIORITY" != "P2" ] && \
2512
+ [ "$CASCADE_MAX_AMPLIFICATION" != "off" ] && \
2513
+ awk "BEGIN { exit !($AMPLIFICATION_RATIO > $CASCADE_MAX_AMPLIFICATION) }"; then
2514
+ DEFER=true
2515
+ DEFER_REASON="amplification bound exceeded (same-lineage refinement; ${AMPLIFICATION_RATIO} > ${CASCADE_MAX_AMPLIFICATION})"
2516
+ AMPLIFICATION_DEFERRED+=("$FINDING_NUM")
2517
+ fi
2518
+
1981
2519
  if [ "$DEFER" = "true" ]; then
1982
2520
  DEFERRED_FINDINGS+=($FINDING_NUM)
1983
2521
  DEFERRED_REASONS[$FINDING_NUM]="$DEFER_REASON"
@@ -1988,9 +2526,11 @@ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${R
1988
2526
  done
1989
2527
  ```
1990
2528
 
1991
- **Surface-area batching for queued P3 findings (MANDATORY check before dispatch):** <!-- Added: forge#1818 -->
2529
+ **Concern-level batching for queued P3 findings (MANDATORY check before dispatch):** <!-- Added: forge#1818 -->
2530
+
2531
+ Cascade-spawned findings collected within a single `/orchestrate` run must be reconsidered against the complete open, unbatched, undispatched candidate registry, not only `QUEUED_FINDINGS` from this completion cycle. At initial resolution and after every five completions (or whenever `DEFERRED_FINDINGS` reaches `MAX_CONCURRENT`), collect retained ungrouped candidates, new findings, and open batch membership; call `planP3BatchGroups()` with that registry and `batchExclusionReason()` danger-zone inputs. Execute returned `extensions` before new groups, retain `ungrouped` members for the next sweep, and record group kinds or exclusion predicates in the run summary. <!-- Added: forge#2858; extended: forge#2851, forge#2852 -->
1992
2532
 
1993
- 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:
2533
+ The per-cycle `QUEUED_FINDINGS` loop below remains the action-creation mechanism, but it MUST consume the complete-sweep planner output rather than decide groups from its local same-file map. Do not dispatch a locally ungrouped member until the next periodic sweep has considered it against the retained candidate set.
1994
2534
 
1995
2535
  ```bash
1996
2536
  # Group QUEUED_FINDINGS by exact affected file, reusing the SAME safety
@@ -2006,13 +2546,17 @@ Cascade-spawned findings collected within a single `/orchestrate` run never pass
2006
2546
  unset SURFACE_FILE_MEMBERS
2007
2547
  declare -A SURFACE_FILE_MEMBERS
2008
2548
 
2009
- # Defensive cap on gh issue view fan-out. QUEUED_FINDINGS is already bounded by
2549
+ # Include generation-2 P3 findings only as batch candidates. They remain deferred
2550
+ # unless this pass actually forms a bounded batch; no member is individually queued.
2551
+ BATCHING_CANDIDATES=("${QUEUED_FINDINGS[@]}" "${BATCHABLE_DEFERRED_P3[@]}")
2552
+
2553
+ # Defensive cap on gh issue view fan-out. BATCHING_CANDIDATES is already bounded by
2010
2554
  # upstream cascade control; this cap holds even if that bound is later loosened,
2011
2555
  # so the loop can never scale API calls linearly with cascade-seeded findings. <!-- forge#1836 -->
2012
2556
  MAX_BATCH_SCAN=50
2013
2557
  SCANNED=0
2014
2558
 
2015
- for FINDING_NUM in "${QUEUED_FINDINGS[@]}"; do
2559
+ for FINDING_NUM in "${BATCHING_CANDIDATES[@]}"; do
2016
2560
  SCANNED=$((SCANNED + 1))
2017
2561
  if [ "$SCANNED" -gt "$MAX_BATCH_SCAN" ]; then
2018
2562
  echo "Surface-area batching: reached MAX_BATCH_SCAN=$MAX_BATCH_SCAN — remaining findings stay individually queued"
@@ -2022,7 +2566,8 @@ for FINDING_NUM in "${QUEUED_FINDINGS[@]}"; do
2022
2566
  FINDING_DATA=$(gh issue view $FINDING_NUM -R {GH_REPO} --json title,body,labels \
2023
2567
  --jq '{title: .title, body: .body, labels: [.labels[].name]}')
2024
2568
 
2025
- # Safety exclusions — never batch, at any priority. Same jq test() engine and
2569
+ # Billing is never batched. Security findings are classified below and may
2570
+ # batch only with their exact same class. Same jq test() engine and
2026
2571
  # patterns as phase-1-resolve.md's batching rule (single shared mechanism).
2027
2572
  # Word-boundary anchored (not bare substrings) and attribution-boilerplate
2028
2573
  # (**Confidence**/**Severity**/**Review comment** — see forge#2477 note below
@@ -2050,9 +2595,9 @@ for FINDING_NUM in "${QUEUED_FINDINGS[@]}"; do
2050
2595
  # keyword is not auto-batched) for closing a real bypass — the safe
2051
2596
  # direction for a security-relevant exclusion. <!-- forge#2477 -->
2052
2597
  echo "$FINDING_DATA" | jq -e '
2053
- (.title | test("\\b(security|billing|anti-bot|auth|authentication|authorization|authn|authz)\\b"; "i"))
2054
- or ((.body | gsub("(?m)^\\*\\*(?:Confidence\\*\\*: (?:CONFIRMED|LIKELY|POSSIBLE)|Severity\\*\\*: (?:CRITICAL|HIGH|MEDIUM|LOW|INFO)|Review comment\\*\\*: https?://\\S+)$"; "")) | test("## Problem[\\s\\S]{0,500}\\b(security|billing|anti-bot|auth|authentication|authorization|authn|authz)\\b"; "i"))
2055
- or ([.labels[]] | any(. == "security" or . == "billing" or . == "anti-bot" or . == "auth"))
2598
+ (.title | test("\\b(billing|operator-only|manual action required|human action required)\\b"; "i"))
2599
+ or ((.body | gsub("(?m)^\\*\\*(?:Confidence\\*\\*: (?:CONFIRMED|LIKELY|POSSIBLE)|Severity\\*\\*: (?:CRITICAL|HIGH|MEDIUM|LOW|INFO)|Review comment\\*\\*: https?://\\S+)$"; "")) | test("## Problem[\\s\\S]{0,500}\\b(billing|operator-only|manual action required|human action required)\\b"; "i"))
2600
+ or ([.labels[]] | any(. == "billing" or . == "needs-human" or . == "blocked" or . == "operator-only"))
2056
2601
  ' >/dev/null && continue
2057
2602
 
2058
2603
  # Only P3 findings are eligible (P1/P2 already dispatched individually above).
@@ -2062,7 +2607,11 @@ for FINDING_NUM in "${QUEUED_FINDINGS[@]}"; do
2062
2607
  FINDING_FILE=$(echo "$FINDING_DATA" | jq -r '.body' | grep -oE '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|sh|md)`' | head -1 | tr -d '`')
2063
2608
  [ -z "$FINDING_FILE" ] && continue
2064
2609
 
2065
- SURFACE_FILE_MEMBERS["$FINDING_FILE"]="${SURFACE_FILE_MEMBERS[$FINDING_FILE]} $FINDING_NUM"
2610
+ SAFETY_CLASS=$(node -e 'import("./bin/engine/admission.mjs").then(({ classifyBatchSafety }) => process.stdout.write(classifyBatchSafety(process.argv[1]) || "routine"))' "$(echo "$FINDING_DATA" | jq -r '.title + "\n" + .body + "\n" + (.labels | join(" "))')")
2611
+ # Same file is not enough for security work: the class key prevents a
2612
+ # credential finding from sharing a batch with injection/auth hardening.
2613
+ SURFACE_KEY="${SAFETY_CLASS}:${FINDING_FILE}"
2614
+ SURFACE_FILE_MEMBERS["$SURFACE_KEY"]="${SURFACE_FILE_MEMBERS[$SURFACE_KEY]} $FINDING_NUM"
2066
2615
  done
2067
2616
 
2068
2617
  # For each same-file cluster of 2+, actually CREATE the batch issue (executable —
@@ -2073,23 +2622,39 @@ for FILE in "${!SURFACE_FILE_MEMBERS[@]}"; do
2073
2622
  MEMBERS=(${SURFACE_FILE_MEMBERS[$FILE]})
2074
2623
  [ "${#MEMBERS[@]}" -ge 2 ] || continue
2075
2624
 
2625
+ SAFETY_CLASS=${FILE%%:*}
2626
+ SURFACE_FILE=${FILE#*:}
2627
+
2076
2628
  # Sanitize the affected-file path before interpolating it into the issue title/body.
2077
2629
  # Git filenames can legally carry shell metacharacters (`$()`, backticks, quotes);
2078
2630
  # restrict to a validated charset so the value cannot break the gh argument
2079
2631
  # boundary from an untrusted issue body. Shared guard with phase-1-resolve.md. <!-- forge#1833, forge#1835 -->
2080
- SAFE_SURFACE_AREA=$(printf '%s' "$FILE" | tr -cd 'A-Za-z0-9._/-')
2632
+ SAFE_SURFACE_AREA=$(printf '%s' "$SURFACE_FILE" | tr -cd 'A-Za-z0-9._/-')
2081
2633
 
2082
- echo "Same-run surface-area cluster: ${#MEMBERS[@]} P3 findings share $FILE — creating batch issue(s)"
2634
+ echo "Same-run surface-area cluster: ${#MEMBERS[@]} ${SAFETY_CLASS} findings share $SURFACE_FILE — creating batch issue(s)"
2083
2635
 
2084
- # Cap at 8 members per batch (phase-1-resolve.md limit); split into batches of <=8.
2085
- for START in $(seq 0 8 $(( ${#MEMBERS[@]} - 1 ))); do
2086
- CHUNK=("${MEMBERS[@]:$START:8}")
2636
+ # Routine batches cap at 8; security-class batches cap at 3 and their body
2637
+ # must state a live-vector or defence-in-depth verdict for every member.
2638
+ BATCH_CAP=8
2639
+ [ "$SAFETY_CLASS" != "routine" ] && BATCH_CAP=3
2640
+ for START in $(seq 0 "$BATCH_CAP" $(( ${#MEMBERS[@]} - 1 ))); do
2641
+ CHUNK=("${MEMBERS[@]:$START:$BATCH_CAP}")
2087
2642
  [ "${#CHUNK[@]}" -ge 2 ] || continue
2088
2643
 
2089
2644
  MEMBER_LINES=""
2645
+ BATCH_MEMBER_GENERATION=1
2646
+ GEN2_MEMBER_LINES=""
2090
2647
  for M in "${CHUNK[@]}"; do
2091
2648
  MTITLE=$(gh issue view "$M" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "")
2092
2649
  MEMBER_LINES="${MEMBER_LINES}- [ ] #${M}: ${MTITLE}"$'\n'
2650
+ MEMBER_GENERATION="${FINDING_GENERATIONS[$M]:-1}"
2651
+ if [ "$MEMBER_GENERATION" -gt "$BATCH_MEMBER_GENERATION" ]; then
2652
+ BATCH_MEMBER_GENERATION="$MEMBER_GENERATION"
2653
+ fi
2654
+ if [ "$MEMBER_GENERATION" -ge 2 ]; then
2655
+ GEN2_MEMBER_LINES="${GEN2_MEMBER_LINES}- #${M}: generation ${MEMBER_GENERATION}"$'\n'
2656
+ fi
2657
+ [ "$SAFETY_CLASS" = "routine" ] || MEMBER_LINES="${MEMBER_LINES} - **Verdict**: [ ] live vector [ ] defence-in-depth"$'\n'
2093
2658
  done
2094
2659
 
2095
2660
  # Dedup-check with member exclusion (MANDATORY — forge#2432, identical mechanism to
@@ -2115,10 +2680,8 @@ for FILE in "${!SURFACE_FILE_MEMBERS[@]}"; do
2115
2680
  continue
2116
2681
  fi
2117
2682
 
2118
- BATCH_ISSUE_NUM=$(gh issue create {GH_FLAG} \
2119
- --title "$PROPOSED_BATCH_TITLE" \
2120
- --label "review-finding,priority:P3,batch" \
2121
- --body "$(cat <<BATCH_EOF
2683
+ CREATE_TOKEN="forge-create-$(date -u +%Y%m%dT%H%M%SZ)-$$-$RANDOM"
2684
+ CREATE_BODY="$(cat <<BATCH_EOF
2122
2685
  ## Problem
2123
2686
 
2124
2687
  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.
@@ -2128,6 +2691,12 @@ Batch of P3 review findings in **${SAFE_SURFACE_AREA}** (same file), clustered m
2128
2691
  <!-- FORGE:BATCH_MEMBERS -->
2129
2692
  ${MEMBER_LINES}<!-- /FORGE:BATCH_MEMBERS -->
2130
2693
 
2694
+ **Maximum member generation**: ${BATCH_MEMBER_GENERATION}
2695
+ <!-- FORGE:BATCH_MAX_GENERATION: ${BATCH_MEMBER_GENERATION} --> <!-- allowlist:check-spec-markers -->
2696
+
2697
+ **Generation >= 2 members admitted by bounded batching**:
2698
+ ${GEN2_MEMBER_LINES:-none}
2699
+
2131
2700
  ## Acceptance Criteria
2132
2701
 
2133
2702
  - [ ] All member findings addressed or closed as false-positive
@@ -2136,7 +2705,26 @@ ${MEMBER_LINES}<!-- /FORGE:BATCH_MEMBERS -->
2136
2705
 
2137
2706
  <!-- FORGE:BATCHABLE -->
2138
2707
  BATCH_EOF
2139
- )" --json number --jq '.number')
2708
+ )"
2709
+ CREATE_BODY="${CREATE_BODY}
2710
+
2711
+ <!-- issue-create-token:${CREATE_TOKEN} -->"
2712
+ CREATE_RESPONSE=$(gh api "repos/{GH_REPO}/issues" --method POST \
2713
+ -f title="$PROPOSED_BATCH_TITLE" -f body="$CREATE_BODY" \
2714
+ -f 'labels[]=review-finding' -f 'labels[]=priority:P3' -f 'labels[]=batch') || {
2715
+ echo "ERROR: GitHub rejected same-run batch creation; members remain queued." >&2
2716
+ continue
2717
+ }
2718
+ BATCH_ISSUE_NUM=$(echo "$CREATE_RESPONSE" | jq -r '.number // empty')
2719
+ if [ -z "$BATCH_ISSUE_NUM" ]; then
2720
+ echo "ERROR: same-run batch creation returned no issue number; members remain queued." >&2
2721
+ continue
2722
+ fi
2723
+ CREATED_BODY=$(gh issue view "$BATCH_ISSUE_NUM" -R {GH_REPO} --json body --jq '.body') || continue
2724
+ if ! printf '%s' "$CREATED_BODY" | grep -qF "issue-create-token:${CREATE_TOKEN}"; then
2725
+ echo "ERROR: batch issue #${BATCH_ISSUE_NUM} failed create-token read-back; members remain queued." >&2
2726
+ continue
2727
+ fi
2140
2728
 
2141
2729
  # Consume the cluster: record members and REPLACE them in QUEUED_FINDINGS
2142
2730
  # with the single batch issue, so the dispatch step below operates on the
@@ -2635,6 +3223,8 @@ The context-gathering phase can fetch this index to discover all investigation G
2635
3223
  **Repository**: {GH_REPO}
2636
3224
  **Repo path**: {REPO_PATH}
2637
3225
 
3226
+ **KNOWLEDGE GIST CAPABILITY**: This orchestration already probed it: `${FORGE_GIST_CAPABLE}`. Before invoking `/work-on`, run `export FORGE_GIST_CAPABLE=${FORGE_GIST_CAPABLE}`. Do not re-probe or attempt Gist creation when it is `false`.
3227
+
2638
3228
  **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.
2639
3229
 
2640
3230
  **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.