forgedock 1.1.8 → 1.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +10 -33
  2. package/bin/cinema.mjs +23 -5
  3. package/bin/engine-cli.mjs +137 -11
  4. package/bin/env-detect.mjs +249 -0
  5. package/bin/forge-utils.mjs +364 -4
  6. package/bin/forgedock.mjs +1064 -135
  7. package/bin/hooks/session-start.mjs +68 -3
  8. package/bin/init-detect.mjs +101 -12
  9. package/bin/journey.mjs +935 -19
  10. package/bin/recall.mjs +24 -7
  11. package/bin/registry.mjs +91 -3
  12. package/bin/runner.mjs +38 -2
  13. package/bin/tui.mjs +49 -3
  14. package/commands/adopt.md +1 -1
  15. package/commands/analytics.md +8 -8
  16. package/commands/audit.md +1 -1
  17. package/commands/autopilot.md +1 -1
  18. package/commands/ci-audit.md +1 -1
  19. package/commands/cleanup.md +87 -52
  20. package/commands/compat-audit.md +1 -1
  21. package/commands/deploy-info.md +1 -1
  22. package/commands/deploy-pr.md +1 -1
  23. package/commands/diagnose.md +1 -1
  24. package/commands/explain.md +1 -1
  25. package/commands/fix-ci.md +1 -1
  26. package/commands/forgedock-init.md +1 -1
  27. package/commands/incident-response.md +3 -3
  28. package/commands/issue.md +1 -1
  29. package/commands/optimize.md +1 -1
  30. package/commands/orchestrate/config.md +13 -3
  31. package/commands/orchestrate/phase-1-resolve.md +1 -1
  32. package/commands/orchestrate/phase-2-triage.md +1 -1
  33. package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
  34. package/commands/orchestrate/phase-3-dependency.md +128 -20
  35. package/commands/orchestrate/phase-4-execution.md +598 -43
  36. package/commands/orchestrate/phase-5-cleanup.md +1 -1
  37. package/commands/orchestrate/phase-6-report.md +51 -3
  38. package/commands/orchestrate/safety.md +2 -2
  39. package/commands/qa-sweep.md +1 -1
  40. package/commands/quality-gate.md +1 -1
  41. package/commands/recover-orphans.md +5 -5
  42. package/commands/replay.md +1 -1
  43. package/commands/review-pr-agents/api.md +2 -2
  44. package/commands/review-pr-agents/auth.md +2 -2
  45. package/commands/review-pr-agents/billing.md +2 -2
  46. package/commands/review-pr-agents/concurrency.md +2 -2
  47. package/commands/review-pr-agents/database.md +2 -2
  48. package/commands/review-pr-agents/frontend.md +2 -2
  49. package/commands/review-pr-agents/infra.md +2 -2
  50. package/commands/review-pr-agents/protocols.md +1 -1
  51. package/commands/review-pr-agents/scraper.md +2 -2
  52. package/commands/review-pr-agents/security.md +2 -2
  53. package/commands/review-pr-staging.md +41 -3
  54. package/commands/review-pr.md +71 -6
  55. package/commands/rollback.md +1 -1
  56. package/commands/scope.md +1 -1
  57. package/commands/security-audit.md +1 -1
  58. package/commands/test-gate.md +1 -1
  59. package/commands/validate.md +1 -1
  60. package/commands/work-on/build/architect.md +1 -1
  61. package/commands/work-on/build/context.md +9 -6
  62. package/commands/work-on/build/implement.md +1 -1
  63. package/commands/work-on/build/validate.md +1 -1
  64. package/commands/work-on/build.md +31 -6
  65. package/commands/work-on/close.md +3 -2
  66. package/commands/work-on/decompose.md +1 -1
  67. package/commands/work-on/investigate.md +14 -5
  68. package/commands/work-on/remediate.md +331 -0
  69. package/commands/work-on/review.md +1 -1
  70. package/commands/work-on-monolithic.md +1 -1
  71. package/commands/work-on.md +194 -56
  72. package/docs/CI.md +2 -2
  73. package/docs/CODEX.md +1 -1
  74. package/docs/CONFIG.md +186 -0
  75. package/docs/site/command-reference.md +31 -1
  76. package/docs/site/getting-started.md +9 -34
  77. package/docs/site/index.md +1 -1
  78. package/docs/spec/label-state-machine.md +1 -1
  79. package/package.json +2 -1
  80. package/packages/protocol/src/types.js +1 -1
  81. package/scripts/check-command-docs-drift.sh +4 -2
  82. package/scripts/danger-zones.mjs +19 -12
  83. package/scripts/transition-label.sh +101 -13
  84. package/scripts/refresh-bot-token.sh +0 -97
@@ -1,5 +1,5 @@
1
1
  ---
2
- install: internal
2
+ install: core
3
3
  ---
4
4
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
5
5
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
@@ -28,17 +28,28 @@ done
28
28
  For each issue, estimate which domains it touches based on title, body, and labels. This improves DAG construction — issues in the same domain likely touch the same files and should be serialized (one becomes a predecessor of the other).
29
29
 
30
30
  ```bash
31
+ declare -A ISSUE_DOMAIN # issue → comma-separated matched domain tags, or "NONE" (forge#1913)
31
32
  for NUM in {issue_numbers}; do
32
33
  ISSUE=$(gh issue view $NUM --json title,body,labels --jq '{title: .title, labels: [.labels[].name], body: (.body[:300])}')
33
34
  echo "=== #$NUM ==="
34
- echo "$ISSUE" | grep -qiE "credit|billing|pricing|stripe|charge|refund" && echo " BILLING" || true
35
- echo "$ISSUE" | grep -qiE "auth|session|jwt|login|permission|oauth" && echo " AUTH" || true
36
- echo "$ISSUE" | grep -qiE "worker|queue|job|task|background|consumer" && echo " WORKER" || true
37
- echo "$ISSUE" | grep -qiE "migration|\.sql|database|postgres|alembic" && echo " DATABASE" || true
38
- echo "$ISSUE" | grep -qiE "component|page|layout|dashboard|ui|ux|frontend|web/src" && echo " FRONTEND" || true
39
- echo "$ISSUE" | grep -qiE "docker|deploy|traefik|nginx|ci|cd|infra|github.action" && echo " INFRA" || true
40
- echo "$ISSUE" | grep -qiE "llm|extract|schema|format|embedding|model" && echo " AI" || true
35
+ MATCHED_DOMAINS=()
36
+ if echo "$ISSUE" | grep -qiE "credit|billing|pricing|stripe|charge|refund"; then echo " BILLING"; MATCHED_DOMAINS+=("BILLING"); fi
37
+ if echo "$ISSUE" | grep -qiE "auth|session|jwt|login|permission|oauth"; then echo " AUTH"; MATCHED_DOMAINS+=("AUTH"); fi
38
+ if echo "$ISSUE" | grep -qiE "worker|queue|job|task|background|consumer"; then echo " WORKER"; MATCHED_DOMAINS+=("WORKER"); fi
39
+ if echo "$ISSUE" | grep -qiE "migration|\.sql|database|postgres|alembic"; then echo " DATABASE"; MATCHED_DOMAINS+=("DATABASE"); fi
40
+ if echo "$ISSUE" | grep -qiE "component|page|layout|dashboard|ui|ux|frontend|web/src"; then echo " FRONTEND"; MATCHED_DOMAINS+=("FRONTEND"); fi
41
+ if echo "$ISSUE" | grep -qiE "docker|deploy|traefik|nginx|ci|cd|infra|github.action"; then echo " INFRA"; MATCHED_DOMAINS+=("INFRA"); fi
42
+ if echo "$ISSUE" | grep -qiE "llm|extract|schema|format|embedding|model"; then echo " AI"; MATCHED_DOMAINS+=("AI"); fi
41
43
  # For project-specific domains, configure keywords in forge.yaml → review.domains and extend above
44
+
45
+ # Materialize the result — every issue gets an explicit ISSUE_DOMAIN entry, even when no
46
+ # keyword matched ("NONE"). This is what Step 3D.6's completion gate checks for key presence
47
+ # (not truthiness) — an un-set key means this loop never ran for that issue. <!-- forge#1913 -->
48
+ if [ "${#MATCHED_DOMAINS[@]}" -eq 0 ]; then
49
+ ISSUE_DOMAIN[$NUM]="NONE"
50
+ else
51
+ ISSUE_DOMAIN[$NUM]=$(IFS=,; echo "${MATCHED_DOMAINS[*]}")
52
+ fi
42
53
  done
43
54
  ```
44
55
 
@@ -48,7 +59,7 @@ done
48
59
  - BILLING + AUTH issues should be prioritized early (security-critical)
49
60
  - **DATABASE issues are ALWAYS serialized — hard rule, no exceptions.** Multiple agents writing migrations simultaneously will produce duplicate migration numbers (e.g., two `0067_*.sql` files), which breaks the migration runner. DATABASE issues form a linear predecessor chain in the DAG. If 3 DATABASE issues are in a batch: A has no predecessors, B has {A} as predecessor, C has {B} as predecessor.
50
61
 
51
- **Store domain tags per issue** for use in the plan presentation (Step 3E).
62
+ **Domain tags are stored in `ISSUE_DOMAIN[$NUM]`** (materialized above) for use in the plan presentation (Step 3E) and the Step 3D.6 completion gate.
52
63
 
53
64
  ### Step 3C: Multi-layer conflict detection
54
65
 
@@ -60,6 +71,8 @@ Domain estimation (above) catches broad category overlap but misses cases where
60
71
 
61
72
  ```bash
62
73
  LAYER1_FILES=()
74
+ declare -A EDGE_KIND # "{PRED}:{SUCCESSOR}" → same-file | directory | shared-module (forge#1860)
75
+ declare -A EDGE_FILES # "{PRED}:{SUCCESSOR}" → the specific file(s) that triggered the edge (forge#1860)
63
76
  for NUM in {issue_numbers}; do
64
77
  echo "=== #$NUM ==="
65
78
  FILES_FOR_NUM=$(gh api repos/{GH_REPO}/issues/${NUM}/comments \
@@ -86,6 +99,7 @@ done
86
99
  - If two issues share ANY affected file → one MUST be a predecessor of the other (serialized)
87
100
  - The issue with lower issue number goes first (stable ordering), unless an explicit `Depends on #` says otherwise
88
101
  - Add a conflict note to the DAG plan: "#{A} and #{B} both modify `{file}` — #{A} is predecessor of #{B}"
102
+ - **Record the edge kind and shared file(s)** <!-- Added: forge#1860 -->: alongside the predecessor relationship, set `EDGE_KIND["${A}:${B}"]="same-file"` and `EDGE_FILES["${A}:${B}"]="{file}"` (space-separated if multiple files overlap). Layer 1 is the only point where the specific overlapping file is known — Step 3D's DAG only records that a predecessor relationship exists, not why. `EDGE_KIND`/`EDGE_FILES` have two consumers in `phase-4-execution.md` Step 4B: (1) the DONE-case "same-file current-state brief" forwarding (forge#1860, unchanged), and (2) `verify_file_overlap_edge()` (forge#1904), which re-checks this guessed file list against the predecessor's *actual* PR diff once it reaches FAILED or GATED — since Layer 1's file list is extracted from a pre-build investigation guess or a raw issue-body parse, never from real code, it can be wrong, and a predecessor that concludes without ever touching the guessed file should not keep a dependent gated/skipped on it. See `phase-4-execution.md`'s "File-overlap edge re-verification" section for the full mechanism.
89
103
 
90
104
  #### Layer 2: Directory-proximity detection
91
105
 
@@ -112,6 +126,7 @@ done
112
126
  - `web/src/lib/` (shared utilities)
113
127
  - `shared/` (volume-mounted, affects all services)
114
128
  - `infra/migrations/` (already covered by DATABASE hard rule, but explicit here too)
129
+ - **Record the edge kind and shared directory** <!-- Added: forge#1860 -->: whenever this layer actually serializes a pair (small-directory match, broad-directory + same-domain match, or a known high-conflict directory), set `EDGE_KIND["${A}:${B}"]="directory"` and `EDGE_FILES["${A}:${B}"]="{shared_directory}"`. Same consumption contract as Layer 1 — read by Step 4B's same-file brief forwarding.
115
130
 
116
131
  #### Layer 3: Shared-module inference
117
132
 
@@ -149,6 +164,11 @@ HIGH_FAN_IN = [
149
164
 
150
165
  # For each issue, check if affected files include a high-fan-in file
151
166
  # If yes: that issue cannot be parallelized with any other issue touching the same service
167
+
168
+ # Record edge metadata for any pair actually serialized by this layer <!-- Added: forge#1860 -->:
169
+ # EDGE_KIND["${A}:${B}"]="shared-module"
170
+ # EDGE_FILES["${A}:${B}"]="{the shared high-fan-in file, or the inferred barrel/init/registry module}"
171
+ # Same consumption contract as Layers 1-2 — read by Step 4B's same-file brief forwarding.
152
172
  ```
153
173
 
154
174
  #### Layer 4: Conservative fallback (low-confidence cases)
@@ -344,10 +364,32 @@ Build a **directed acyclic graph (DAG)** of per-issue dependencies. Each issue g
344
364
  - **Explicit dependencies**: If issue B says "Depends on #A" or "Blocked by #A", add A to B's predecessors
345
365
  - **File-conflict edges**: If two issues share affected files (from Step 3C Layer 1), add a directed edge: lower issue number → higher issue number (unless explicit deps say otherwise). The later issue has the earlier issue in its predecessors.
346
366
  - **Domain serialization edges**: DATABASE issues form a linear chain (each has the previous DATABASE issue as its predecessor). Same-small-directory issues (Layer 2) and high-fan-in file issues (Layer 3) get directed edges as per Step 3C rules.
367
+ - **Edge-kind tagging for same-file/directory/shared-module briefing and re-verification** <!-- Added: forge#1860; extended forge#1904 -->: Layer 1/2/3 edges are additionally tagged with `EDGE_KIND` (`same-file` / `directory` / `shared-module`) and `EDGE_FILES` at the point they're added (see Step 3C). Explicit-dependency edges, the DATABASE domain chain, Layer 4 conservative-fallback edges, and Layer 5 co-change edges deliberately do NOT receive one of these three `EDGE_KIND` values. This is what lets Step 4B distinguish "predecessor edge came from Step 3C Layer 1/2/3" from every other edge type, for two purposes: a same-file current-state brief (DONE case, forge#1860) and edge re-verification against the predecessor's actual PR diff once it reaches FAILED or GATED (`verify_file_overlap_edge()`, forge#1904 — see `phase-4-execution.md` Step 4B). Non-`EDGE_KIND` edges are eligible for neither — they always `KEEP` in the re-verification check and are never candidates for the same-file brief.
347
368
  - **Conservative fallback edges**: Low-confidence issues (Layer 4) get edges to same-domain issues as per Step 3C rules.
348
369
  - **Co-change coupling edges** <!-- Added: forge#1196 -->: High co-change file pairs (Layer 5, 3+ shared commits in the bounded window) that span two different issues get a directed edge using the same lower-issue-number-is-predecessor convention as Layer 1. Verified-independent pairs (Layer 5, zero shared commits) may instead REMOVE an edge that Layer 2 or Layer 4 would otherwise have added for that pair — Layer 1 and Layer 3 edges are never removed by a Layer 5 downgrade.
349
370
  - **Claims-board downgrade (Layer 2/4 edges only)** <!-- Added: forge#1736 -->: After dispatch begins (Phase 4A), when both issues in a Layer-2 or Layer-4 serialized pair post `FORGE:CLAIM` annotations on the coordination issue and their claimed file sets are **disjoint** (no path appears in both claims), the serialization edge for that pair MAY be relaxed — the blocked issue becomes ready. This downgrade is **never** applied to Layer-1 (same-file) or Layer-3 (high-fan-in) edges. See Step 4B: Claims-board relaxation sweep for the runtime check.
350
- - **No artificial concurrency limit by default** — all issues with empty predecessor sets dispatch simultaneously. The only constraints are file overlap, explicit dependencies, and co-change coupling. When `forge.yaml → orchestration.max_concurrent` is set, the dispatch loop queues excess ready issues and releases them as running workers complete (see Engine mode § Concurrency model).
371
+ - **Concurrency is capped by default** <!-- Updated: forge#1912 --> — issues with empty predecessor sets are still all *eligible* to dispatch simultaneously (file overlap, explicit dependencies, and co-change coupling remain the only DAG-ordering constraints), but Phase 4's dispatch loop holds at most `MAX_CONCURRENT` in flight at once (default 12; `forge.yaml → orchestration.max_concurrent` overrides). Ready issues beyond the cap queue and dispatch as running workers complete (see Engine mode § Concurrency model, and `phase-4-execution.md` Step 4A-pre.0.2).
372
+
373
+ **Materialize the DAG** <!-- Added: forge#1913 -->: The rules above describe how edges are derived, but they must be applied into real, checkable data structures — not carried in prose or reconstructed from memory later. Step 3D.1 (coordination issue), Step 3D.5 (cycle detection), Step 3E.5 (scoring), and Step 3E (plan presentation) all read `ISSUES[]` and `PREDECESSORS[]` as if this already happened; this is the one place they're actually built:
374
+
375
+ ```bash
376
+ # --- Step 3D: Materialize the DAG ---
377
+ ISSUES=({issue_numbers}) # all issues in this batch, excluding Phase 2 investigations
378
+ declare -A PREDECESSORS # issue → space-separated predecessor issue numbers ("" = no predecessors)
379
+
380
+ for NUM in "${ISSUES[@]}"; do
381
+ PREDECESSORS[$NUM]="" # every issue gets an explicit entry — even an empty one — so
382
+ # Step 3D.6's gate can tell "no predecessors" (valid) apart from
383
+ # "never processed" (the bug this gate exists to catch)
384
+ done
385
+
386
+ # Apply edges in the order described above — explicit deps, then Layer 1/2/3 conflicts
387
+ # (Step 3C), then the DATABASE domain chain, then Layer 4 conservative fallback, then
388
+ # Layer 5 co-change coupling/downgrades. Each edge appends the predecessor's number to
389
+ # the successor's entry:
390
+ # PREDECESSORS[$SUCCESSOR]="${PREDECESSORS[$SUCCESSOR]:+${PREDECESSORS[$SUCCESSOR]} }$PREDECESSOR_NUM"
391
+ # --- End Step 3D materialization ---
392
+ ```
351
393
 
352
394
  ### Step 3D.1: Create coordination issue (claims board) <!-- Added: forge#1736 -->
353
395
 
@@ -515,6 +557,41 @@ fi
515
557
  - `EXCLUDED_CYCLE[]` contains cyclic issue numbers — reported in Step 3E, never dispatched
516
558
  - If `EXCLUDED_CYCLE` is non-empty, report it clearly in the Step 3E plan before asking for user confirmation
517
559
  - If `ISSUES[]` is empty after cycle exclusion (all issues were cyclic), the guard above aborts with `exit 1` — Step 3E is never reached with an empty plan <!-- Added: forge#1110 -->
560
+ - Proceed next to **Step 3D.6** before Step 3E.5's scoring pass. <!-- Added: forge#1913 -->
561
+
562
+ ### Step 3D.6: Phase 3 Completion Gate (MANDATORY) <!-- Added: forge#1913 -->
563
+
564
+ **Run immediately after Step 3D.5's cycle detection, before Step 3E.5's scoring pass and Step 3E's plan presentation.** Steps 3D.1, 3D.5, 3E.5, and 3E all read `ISSUES[]`, `PREDECESSORS[]`, and `ISSUE_DOMAIN[]` as if Step 3B/3D already built them for real. Without this gate, an orchestrator under time or context pressure (e.g. a 73-issue batch) can skip the actual extraction loops in Steps 3A–3D and hand-write a plausible-looking DAG from memory instead — Step 3E.5's scoring and Phase 4's dispatch would then run against fabricated data with no error, because the `${PREDECESSORS[$NUM]:-}` defaulting used throughout Step 3D.5 silently tolerates a missing entry instead of failing loudly.
565
+
566
+ **Check**: every issue in `ISSUES[]` must have an explicit key in `PREDECESSORS` and in `ISSUE_DOMAIN` — checked by **key presence**, not by whether the value is non-empty. An issue with zero real predecessors (`PREDECESSORS[$NUM]=""`) or no matched domain (`ISSUE_DOMAIN[$NUM]="NONE"`) is a valid, common outcome and must NOT trip this gate — only a genuinely *absent* key means the upstream step never ran for that issue.
567
+
568
+ ```bash
569
+ # --- Step 3D.6: Phase 3 Completion Gate ---
570
+ MISSING_PREDECESSORS=()
571
+ MISSING_DOMAIN=()
572
+ for NUM in "${ISSUES[@]}"; do
573
+ # Key-presence test (${arr[key]+x}), NOT emptiness — PREDECESSORS[$NUM]="" and
574
+ # ISSUE_DOMAIN[$NUM]="NONE" are valid values and must pass this check.
575
+ [ -z "${PREDECESSORS[$NUM]+x}" ] && MISSING_PREDECESSORS+=("$NUM")
576
+ [ -z "${ISSUE_DOMAIN[$NUM]+x}" ] && MISSING_DOMAIN+=("$NUM")
577
+ done
578
+
579
+ if [ "${#MISSING_PREDECESSORS[@]}" -gt 0 ] || [ "${#MISSING_DOMAIN[@]}" -gt 0 ]; then
580
+ echo "FATAL: Phase 3 DAG construction is incomplete — cannot proceed to Step 3E.5, Step 3E, or Phase 4."
581
+ [ "${#MISSING_PREDECESSORS[@]}" -gt 0 ] && echo " Missing PREDECESSORS[] entry for: ${MISSING_PREDECESSORS[*]/#/#}"
582
+ [ "${#MISSING_DOMAIN[@]}" -gt 0 ] && echo " Missing ISSUE_DOMAIN[] entry for: ${MISSING_DOMAIN[*]/#/#}"
583
+ echo "This means Step 3B (domain estimation) and/or Step 3D (DAG edge construction) were"
584
+ echo "skipped, or did not run for every issue in the batch. Do NOT hand-write the missing"
585
+ echo "entries from memory or by re-reading issue titles — go back and run Steps 3A-3D for"
586
+ echo "the listed issues, then re-run this gate before proceeding."
587
+ exit 1
588
+ else
589
+ echo "Phase 3 completion gate: PASS — ${#ISSUES[@]} issues all have PREDECESSORS[] and ISSUE_DOMAIN[] entries."
590
+ fi
591
+ # --- End Step 3D.6 ---
592
+ ```
593
+
594
+ **After this gate passes**: Step 3E.5 (scoring) and Step 3E (plan presentation) may proceed. Do not present the Step 3E plan, run Step 3E.5, or hand off to Phase 4 while this gate is failing.
518
595
 
519
596
  ### Step 3E.5: Value/Cost Scoring Pass (MANDATORY) <!-- Added: forge#1743 -->
520
597
 
@@ -735,6 +812,8 @@ done
735
812
 
736
813
  ### Step 3E: Present the plan to the user
737
814
 
815
+ **Populate every row below by dereferencing `PREDECESSORS[$NUM]`, `ISSUE_DOMAIN[$NUM]`, `ISSUE_SCORE[$NUM]`, and `ISSUE_COST_ESTIMATE[$NUM]` computed in Steps 3B–3E.5 — do not fabricate values from memory or by re-reading issue titles at this point.** The Step 3D.6 gate having passed only guarantees the data structures exist; it does not substitute for actually reading them here. <!-- Added: forge#1913 -->
816
+
738
817
  ```
739
818
  ## Orchestration Plan
740
819
 
@@ -844,10 +923,11 @@ On completion or failure:
844
923
  scripts/worktree-lifecycle.sh cleanup <issue-number>
845
924
  ```
846
925
 
847
- **Concurrency cap** (`forge.yaml → orchestration.max_concurrent`):
848
- - Default: uncapped — all DAG-ready issues dispatch simultaneously (preserves current behaviour).
849
- - When `max_concurrent: N` is set, the dispatch loop holds at most N in-flight workers. Newly ready issues queue and start as running workers complete.
926
+ **Concurrency cap** (`forge.yaml → orchestration.max_concurrent`): <!-- Updated: forge#1912 -->
927
+ - Default: **12** — the dispatch loop holds at most 12 in-flight workers unless overridden. This is an enforced default, not opt-in: earlier revisions defaulted to uncapped, which let a large ready set (e.g. 40+ issues) dispatch in one burst and saturate the Anthropic API rate limit.
928
+ - When `max_concurrent: N` is set, the dispatch loop holds at most N in-flight workers instead of the default 12. Newly ready issues queue and start as running workers complete.
850
929
  - Prevents wave-triggered rate-limit storms on large batches (e.g., 40-issue milestone dispatches).
930
+ - See `phase-4-execution.md` Step 4A-pre.0.2 for the concrete initialization and headroom-gated dispatch logic that enforces this cap on both the engine-first and Agent-spawn-fallback dispatch paths.
851
931
 
852
932
  **Rate-limit backpressure** (pre-dispatch gate):
853
933
 
@@ -871,7 +951,7 @@ fi
871
951
  **Configuration reference** (`forge.yaml`):
872
952
  ```yaml
873
953
  orchestration:
874
- max_concurrent: 8 # optional; default: uncapped
954
+ max_concurrent: 8 # optional; default: 12
875
955
  rate_limit_floor: 200 # optional; default: 200
876
956
  ```
877
957
 
@@ -926,6 +1006,8 @@ The orchestrator context window must stay small regardless of how many issues ha
926
1006
 
927
1007
  This reconstruction MUST use the same three-way **DONE / GATED / FAILED** predecessor classification defined in `phase-4-execution.md` Step 4B ("Predecessor Classification") — not a binary terminal/non-terminal grep. A binary grep is exactly the bug forge#1812 fixed: it let `needs-human` simultaneously satisfy "predecessor is done, dispatch the successor" (this block, pre-fix) and "predecessor failed, skip the successor" (Step 4B's failure handler, pre-fix) — with no way to represent "predecessor is human-gated, its PR is still open, and its dependents should wait but not be abandoned." That third case is exactly what wake/compaction reconstruction hits most often, since a merge approved by a human typically happens *after* the orchestrator session that dispatched the predecessor has already ended — this block, not the live Step 4B loop, is the realistic trigger point for "gating PR merged while nobody was watching."
928
1008
 
1009
+ For the same reason, this reconstruction also MUST call `verify_file_overlap_edge()` (also defined in `phase-4-execution.md` Step 4B, alongside `classify_predecessor_state()` — re-declare it here too if this block runs in a fresh context) before treating a GATED or FAILED predecessor's `EDGE_KIND` edge as still blocking. <!-- Added: forge#1904 --> A predecessor that reached `needs-human`/`workflow:invalid` with no PR, or whose PR never actually touched the guessed shared file, most realistically gets *discovered* at wake time — the session that would have caught it live has already ended. Re-verifying only in the live Step 4B loop and not here would leave this exact wake-time case unfixed.
1010
+
929
1011
  ```bash
930
1012
  # Reconstruct dispatch state from GitHub after compaction / wake
931
1013
  # Run this block at the top of every resumed Phase 4 loop iteration.
@@ -950,17 +1032,40 @@ for NUM in {all_issue_numbers_in_batch}; do
950
1032
  esac
951
1033
  done
952
1034
 
953
- # 2. Re-derive the ready set: any non-terminal issue whose predecessors are ALL classified DONE.
1035
+ # 2. Re-derive the ready set: any non-terminal issue whose predecessors are ALL classified DONE
1036
+ # (or GATED-but-edge-dropped — see the re-verification gate below).
954
1037
  # A GATED predecessor blocks dispatch but does NOT fail the dependent — see step 2.5 below.
1038
+ #
1039
+ # Edge re-verification (forge#1904): a GATED (or FAILED) predecessor's file-overlap edge is only
1040
+ # a real block if `verify_file_overlap_edge()` (phase-4-execution.md Step 4B, defined alongside
1041
+ # `classify_predecessor_state()`) confirms it. This block is the wake/compaction-time mirror of
1042
+ # phase-4-execution.md item 6.5's live-session check — it MUST call the identical function, not a
1043
+ # re-derived equivalent, to avoid the drift class forge#1812/#1837 already had to fix once for
1044
+ # classification and regex tooling respectively. This is what handles the case where a GATED
1045
+ # predecessor with no PR (or a PR that never touched the guessed shared file) resolves the
1046
+ # `needs-human`/`workflow:invalid` state AFTER the orchestrator session that dispatched it has
1047
+ # already ended — the realistic trigger point named in the "Why this matters" note above.
955
1048
  READY_ISSUES=()
956
- NEWLY_BLOCKED=() # dependents whose gating predecessor is GATED — need blocked-on-human-merge tracking
1049
+ NEWLY_BLOCKED=() # dependents whose gating predecessor is GATED with a still-live edge — need blocked-on-human-merge tracking
957
1050
  for NUM in "${ACTIVE_ISSUES[@]}"; do
958
1051
  ALL_PREDS_DONE=true
959
1052
  GATING_PRED=""
960
1053
  for PRED in {predecessors_of_NUM}; do
961
1054
  case "${ISSUE_CLASS[$PRED]:-IN_PROGRESS}" in
962
1055
  DONE) ;;
963
- GATED) ALL_PREDS_DONE=false; GATING_PRED="$PRED" ;;
1056
+ GATED|FAILED)
1057
+ EDGE_VERDICT=$(verify_file_overlap_edge "$PRED" "$NUM")
1058
+ if [ "$EDGE_VERDICT" = "DROP" ]; then
1059
+ echo "Edge re-verification: #${PRED} → #${NUM} dropped (GATED/FAILED predecessor never opened a PR, or its actual diff never touched the guessed shared file). Treating this predecessor as resolved for #${NUM}."
1060
+ # Do NOT set ALL_PREDS_DONE=false for this predecessor — the guessed edge never
1061
+ # materialized into a real conflict, so it does not gate #${NUM}.
1062
+ elif [ "${ISSUE_CLASS[$PRED]}" = "GATED" ]; then
1063
+ ALL_PREDS_DONE=false
1064
+ GATING_PRED="$PRED"
1065
+ else
1066
+ ALL_PREDS_DONE=false # FAILED with a confirmed real edge — dependent stays blocked/skipped, handled by existing FAILED-cascade logic elsewhere
1067
+ fi
1068
+ ;;
964
1069
  *) ALL_PREDS_DONE=false ;;
965
1070
  esac
966
1071
  done
@@ -1015,9 +1120,12 @@ All gating predecessor(s) reached \`workflow:merged\` (detected on orchestrator
1015
1120
  fi
1016
1121
  done
1017
1122
 
1018
- # 4. Dispatch the reconstructed ready set (DONE_ISSUES-unblocked + merge-triggered-woken) via the
1019
- # standard Step 4A.pre.0 → 4A.pre → 4A flow. FAILED_ISSUES' transitive dependents remain marked
1020
- # "skipped — dependency failed" per phase-4-execution.md Step 4B item 6 — do not re-add them here.
1123
+ # 4. Dispatch the reconstructed ready set (DONE_ISSUES-unblocked + merge-triggered-woken +
1124
+ # edge-dropped-into-READY_ISSUES from step 2 above) via the standard Step 4A.pre.0 → 4A.pre →
1125
+ # 4A flow. FAILED_ISSUES' transitive dependents remain marked "skipped — dependency failed" per
1126
+ # phase-4-execution.md Step 4B item 6 — do not re-add them here — UNLESS step 2's
1127
+ # `verify_file_overlap_edge()` check already placed them in READY_ISSUES because the FAILED
1128
+ # predecessor's edge dropped (forge#1904); that case is legitimately ready, not a re-add.
1021
1129
  ```
1022
1130
 
1023
1131
  **Why this keeps context small**: Each `Agent()` call returns an agent ID stored only in `AGENT_ISSUE_MAP`, which is rebuilt per Step 4A.pre dispatch batch. After compaction, the map is gone — but the DAG state, including `blocked-on-human-merge` tracking (a durable `FORGE:BLOCKED_ON_HUMAN_MERGE` comment plus label, not an in-context variable), is fully on GitHub. The reconstruction above re-derives the ready set, the gated set, and the blocked-on-human-merge set from labels and comments alone, so the orchestrator context never needs to hold cumulative dispatch history.