forgedock 1.0.16 → 1.0.18

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 (190) hide show
  1. package/README.md +54 -3
  2. package/bin/cross-publish.mjs +344 -0
  3. package/bin/forgedock.mjs +2205 -541
  4. package/bin/github-app-manifest.json.license +2 -0
  5. package/bin/hooks/session-start.mjs +4 -5
  6. package/bin/registry.mjs +20 -0
  7. package/bin/tui.mjs +172 -96
  8. package/commands/adopt.md +433 -0
  9. package/commands/analytics.md +2 -0
  10. package/commands/audit-agents.md +2 -0
  11. package/commands/audit.md +2 -0
  12. package/commands/autopilot.md +77 -0
  13. package/commands/changelog.md +354 -0
  14. package/commands/cleanup.md +2 -0
  15. package/commands/compat-audit.md +330 -0
  16. package/commands/deploy-info.md +6 -5
  17. package/commands/design-bench.md +176 -0
  18. package/commands/design-render-critique-loop.md +103 -0
  19. package/commands/design.md +259 -0
  20. package/commands/diagnose.md +483 -0
  21. package/commands/explain.md +349 -0
  22. package/commands/forge-stats.md +2 -0
  23. package/commands/forgedock-init.md +2 -0
  24. package/commands/geo-audit.md +2 -0
  25. package/commands/incident-response.md +2 -0
  26. package/commands/issue.md +9 -3
  27. package/commands/milestone.md +2 -0
  28. package/commands/optimize.md +411 -0
  29. package/commands/orchestrate.md +239 -0
  30. package/commands/pipeline-health.md +60 -1
  31. package/commands/qa-sweep.md +39 -2
  32. package/commands/quality-gate.md +67 -0
  33. package/commands/replay.md +263 -0
  34. package/commands/resume.md +264 -0
  35. package/commands/review-pr-agents.md +5 -1
  36. package/commands/review-pr-staging.md +160 -2
  37. package/commands/review-pr.md +85 -3
  38. package/commands/rollback.md +2 -0
  39. package/commands/scope.md +280 -0
  40. package/commands/security-audit.md +2 -0
  41. package/commands/status.md +262 -0
  42. package/commands/sync-ecosystem.md +2 -0
  43. package/commands/test-gate.md +929 -0
  44. package/commands/validate.md +2 -0
  45. package/commands/work-on/build/architect.md +128 -18
  46. package/commands/work-on/build/context.md +130 -17
  47. package/commands/work-on/build/implement.md +7 -3
  48. package/commands/work-on/build/validate.md +2 -0
  49. package/commands/work-on/build.md +32 -7
  50. package/commands/work-on/close.md +85 -0
  51. package/commands/work-on/decompose.md +2 -0
  52. package/commands/work-on/investigate.md +30 -3
  53. package/commands/work-on/review.md +13 -1
  54. package/commands/work-on-monolithic.md +2 -0
  55. package/commands/work-on.md +676 -38
  56. package/docs/BRAND.md +51 -0
  57. package/docs/CI.md +177 -0
  58. package/docs/CONFIG.md +94 -0
  59. package/docs/FORGE-PROTOCOL.md +324 -0
  60. package/docs/articles/b-vs-c-baseline.md +210 -0
  61. package/docs/articles/full-abc-benchmark.md +205 -0
  62. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +174 -0
  63. package/docs/articles/selective-spec-loading-token-savings.md +140 -0
  64. package/docs/claude-breakpoints.json +47 -0
  65. package/docs/design/abc-benchmark.md +195 -0
  66. package/docs/design/design-architect-rationale.md +123 -0
  67. package/docs/design/design-memory.md +61 -0
  68. package/docs/design/design-spec-schema.md +248 -0
  69. package/docs/design/design-system-lint.md +150 -0
  70. package/docs/design/divergent-generation.md +106 -0
  71. package/docs/design/effects-appropriateness.md +133 -0
  72. package/docs/design/fixtures/briefs/README.md +47 -0
  73. package/docs/design/fixtures/briefs/cadence.md +39 -0
  74. package/docs/design/fixtures/briefs/plume.md +39 -0
  75. package/docs/design/fixtures/briefs/slipstream.md +39 -0
  76. package/docs/design/fixtures/briefs/tender.md +39 -0
  77. package/docs/design/fixtures/briefs/voltage.md +39 -0
  78. package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-1.html +364 -0
  79. package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-2.html +420 -0
  80. package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-3.html +506 -0
  81. package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-1.html +611 -0
  82. package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-2.html +594 -0
  83. package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-3.html +817 -0
  84. package/docs/design/fixtures/runs/b-vs-c-baseline/runs.json +175 -0
  85. package/docs/design/fixtures/runs/b-vs-c-baseline/scorecard.json +138 -0
  86. package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-1.html +373 -0
  87. package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-2.html +398 -0
  88. package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-3.html +595 -0
  89. package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-1.html +531 -0
  90. package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-2.html +565 -0
  91. package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-3.html +674 -0
  92. package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-1.html +388 -0
  93. package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-2.html +463 -0
  94. package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-3.html +568 -0
  95. package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-1.html +562 -0
  96. package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-2.html +618 -0
  97. package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-3.html +618 -0
  98. package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-1.html +364 -0
  99. package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-2.html +420 -0
  100. package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-3.html +506 -0
  101. package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-1.html +527 -0
  102. package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-2.html +445 -0
  103. package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-3.html +483 -0
  104. package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-1.html +611 -0
  105. package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-2.html +594 -0
  106. package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-3.html +817 -0
  107. package/docs/design/fixtures/runs/full-abc/runs.json +190 -0
  108. package/docs/design/fixtures/runs/full-abc/scorecard.json +144 -0
  109. package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-1.html +496 -0
  110. package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-2.html +447 -0
  111. package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-3.html +564 -0
  112. package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-1.html +373 -0
  113. package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-2.html +398 -0
  114. package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-3.html +595 -0
  115. package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-1.html +622 -0
  116. package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-2.html +475 -0
  117. package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-3.html +660 -0
  118. package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-1.html +531 -0
  119. package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-2.html +565 -0
  120. package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-3.html +674 -0
  121. package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-1.html +525 -0
  122. package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-2.html +446 -0
  123. package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-3.html +463 -0
  124. package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-1.html +388 -0
  125. package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-2.html +463 -0
  126. package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-3.html +568 -0
  127. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-1.html +1068 -0
  128. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-2.html +1423 -0
  129. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-3.html +1245 -0
  130. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-1.html +305 -0
  131. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-2.html +327 -0
  132. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-3.html +428 -0
  133. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-1.html +1270 -0
  134. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-2.html +1172 -0
  135. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-3.html +1532 -0
  136. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-1.html +315 -0
  137. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-2.html +342 -0
  138. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-3.html +353 -0
  139. package/docs/design/fixtures/runs/full-abc-opus/runs.json +190 -0
  140. package/docs/design/fixtures/runs/full-abc-opus/scorecard.json +144 -0
  141. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-1.html +345 -0
  142. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-2.html +396 -0
  143. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-3.html +502 -0
  144. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-1.html +408 -0
  145. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-2.html +446 -0
  146. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-3.html +496 -0
  147. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-1.html +813 -0
  148. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-2.html +711 -0
  149. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-3.html +721 -0
  150. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-1.html +280 -0
  151. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-2.html +291 -0
  152. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-3.html +366 -0
  153. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-1.html +1010 -0
  154. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-2.html +836 -0
  155. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-3.html +930 -0
  156. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-1.html +407 -0
  157. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-2.html +396 -0
  158. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-3.html +529 -0
  159. package/docs/design/foundations/brutalist.css +294 -0
  160. package/docs/design/foundations/editorial.css +300 -0
  161. package/docs/design/foundations/minimal-luxury.css +295 -0
  162. package/docs/design/foundations/technical.css +293 -0
  163. package/docs/design/foundations/warm-photo.css +299 -0
  164. package/docs/design/reference-corpus.md +1726 -0
  165. package/docs/design/render-critique-loop.md +57 -0
  166. package/docs/site/getting-started.md +21 -0
  167. package/docs/spec-graph-schema.md +397 -0
  168. package/package.json +3 -3
  169. package/scripts/bench-scorecard.mjs +239 -0
  170. package/scripts/build-spec-graph.mjs +509 -0
  171. package/scripts/classify-lane.sh +138 -0
  172. package/scripts/design-system-lint.mjs +993 -0
  173. package/scripts/graph-query.sh +486 -0
  174. package/scripts/transition-label.sh +169 -0
  175. package/scripts/validate-pr-target.sh +30 -0
  176. package/scripts/validate-spec-graph.sh +423 -0
  177. package/templates/devdocs/README.md +72 -0
  178. package/templates/devdocs/agent/using-forgedock.md +3 -0
  179. package/templates/devdocs/agent/using-github.md +3 -0
  180. package/templates/devdocs/index.yaml +162 -0
  181. package/templates/devdocs/project/architecture.md +3 -0
  182. package/templates/devdocs/project/conventions.md +3 -0
  183. package/templates/devdocs/project/custom-instructions.md +3 -0
  184. package/templates/devdocs/project/glossary.md +3 -0
  185. package/templates/devdocs/project/stack.md +3 -0
  186. package/templates/devdocs/templates/commit-convention.md +3 -0
  187. package/templates/devdocs/templates/issue.md +3 -0
  188. package/templates/devdocs/templates/pull-request.md +3 -0
  189. package/templates/devdocs/templates/review-finding.md +3 -0
  190. package/templates/workflows/forgedock-review.yml +96 -0
@@ -0,0 +1,411 @@
1
+ ---
2
+ description: Analyze this repo and generate per-repo adaptive scripts from learned patterns, git history, and existing configuration
3
+ argument-hint: [--dry-run | --force | --no-registry]
4
+ ---
5
+
6
+ # /optimize — Per-Repo Adaptive Script Generation
7
+
8
+ **Input**: $ARGUMENTS
9
+
10
+ Proactively analyze the current repository and generate `.forgedock/scripts/` entries from `forge.yaml → learned:`, git commit history, and pipeline annotations — without waiting for a correction event. This is the proactive half of the learning loop; reactive capture happens in `/work-on` Phase 1D.
11
+
12
+ **Agent model policy**: Default `model: "sonnet"`. Fallback: `model: "opus"` if Sonnet is rate-limited.
13
+ **NEVER use plan mode (EnterPlanMode).**
14
+
15
+ ---
16
+
17
+ ## Argument Parsing
18
+
19
+ | Flag | Effect |
20
+ |------|--------|
21
+ | (none) | Full run: analyze → generate → registry → report |
22
+ | `--dry-run` | Analyze and report what would be generated; write nothing to disk |
23
+ | `--force` | Overwrite scripts even if confidence < 0.7 |
24
+ | `--no-registry` | Skip `registry.json` update |
25
+
26
+ Parse `$ARGUMENTS` and set:
27
+ ```
28
+ DRY_RUN = true if --dry-run present, else false
29
+ FORCE = true if --force present, else false
30
+ NO_REG = true if --no-registry present, else false
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Prerequisites
36
+
37
+ ### P1: Read forge.yaml
38
+
39
+ ```bash
40
+ REPO_PATH=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
41
+ FORGE_YAML="$REPO_PATH/forge.yaml"
42
+ GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$FORGE_YAML" 2>/dev/null)
43
+ SCRIPTS_DIR="$REPO_PATH/.forgedock/scripts"
44
+ ADAPTIVE_ENABLED=$(yq '.adaptive_scripts.enabled // true' "$FORGE_YAML" 2>/dev/null || echo "true")
45
+ ADAPTIVE_COMMIT=$(yq '.adaptive_scripts.commit // false' "$FORGE_YAML" 2>/dev/null || echo "false")
46
+ ```
47
+
48
+ If `$FORGE_YAML` is missing:
49
+ ```
50
+ STOP: forge.yaml not found. Run `npx forgedock init` to generate it, then re-run /optimize.
51
+ ```
52
+
53
+ If `ADAPTIVE_ENABLED` is `false`:
54
+ ```
55
+ STOP: adaptive_scripts.enabled is false in forge.yaml. Set it to true to use /optimize.
56
+ ```
57
+
58
+ ### P2: Ensure scripts directory exists
59
+
60
+ ```bash
61
+ mkdir -p "$SCRIPTS_DIR"
62
+ ```
63
+
64
+ If `.forgedock/scripts/` is not gitignored, note in the report (non-blocking).
65
+
66
+ ---
67
+
68
+ ## Phase 1: Pattern Analysis
69
+
70
+ Read all available signal sources. Build a `PATTERNS` map — keyed by script name, value is `{evidence, confidence, value}`.
71
+
72
+ **Confidence scale**:
73
+ - `1.0` — explicit value in `forge.yaml → learned:` (authoritative)
74
+ - `0.85` — value confirmed by 3+ independent signals (git history, FORGE:LEARNED annotations)
75
+ - `0.7` — value confirmed by 2 independent signals
76
+ - `0.5` — single signal (heuristic only)
77
+ - `<0.5` — insufficient evidence; skip unless `--force`
78
+
79
+ ### 1A: Read forge.yaml → learned: section
80
+
81
+ ```bash
82
+ LEARNED=$(yq '.learned // {}' "$FORGE_YAML" 2>/dev/null || echo '{}')
83
+ echo "Learned section: $LEARNED"
84
+ ```
85
+
86
+ Extract each known key:
87
+ ```bash
88
+ LEARNED_STAGING=$(yq '.learned.branch_targets.staging // ""' "$FORGE_YAML" 2>/dev/null)
89
+ LEARNED_TEST_CMDS=$(yq '.learned.test_commands // []' "$FORGE_YAML" -o json 2>/dev/null)
90
+ LEARNED_TEST_LOCS=$(yq '.learned.test_locations // []' "$FORGE_YAML" -o json 2>/dev/null)
91
+ LEARNED_LABEL_MAP=$(yq '.learned.label_map // {}' "$FORGE_YAML" -o json 2>/dev/null)
92
+ LEARNED_COMMIT_STYLE=$(yq '.learned.commit_style // ""' "$FORGE_YAML" 2>/dev/null)
93
+ ```
94
+
95
+ For each non-empty learned key, add to PATTERNS with confidence `1.0`:
96
+
97
+ | Non-empty key | Script to generate |
98
+ |---------------|--------------------|
99
+ | `branch_targets.staging` | `branch-targets.sh` |
100
+ | `test_commands` (non-empty array) | `run-tests.sh` |
101
+ | `test_locations` (non-empty array) | `find-tests.sh` |
102
+ | `label_map` (non-empty object) | `label-map.sh` |
103
+ | `commit_style` | `format-commit.sh` |
104
+
105
+ ### 1B: Analyze git commit history
106
+
107
+ ```bash
108
+ cd "$REPO_PATH"
109
+
110
+ # Sample last 50 commits for style analysis
111
+ COMMIT_SUBJECTS=$(git log --oneline -50 --format="%s" 2>/dev/null | head -50)
112
+ echo "$COMMIT_SUBJECTS"
113
+ ```
114
+
115
+ **Detect commit style**:
116
+ ```bash
117
+ CONV_COUNT=$(echo "$COMMIT_SUBJECTS" | grep -cE '^(feat|fix|docs|chore|refactor|test|style|ci|build|perf)\(' 2>/dev/null || echo 0)
118
+ CONV_SIMPLE=$(echo "$COMMIT_SUBJECTS" | grep -cE '^(feat|fix|docs|chore|refactor|test|style|ci|build|perf):' 2>/dev/null || echo 0)
119
+ TOTAL=$(echo "$COMMIT_SUBJECTS" | wc -l | tr -d ' ')
120
+
121
+ CONV_WITH_SCOPE_PCT=$(( (CONV_COUNT * 100) / (TOTAL + 1) ))
122
+ CONV_SIMPLE_PCT=$(( (CONV_SIMPLE * 100) / (TOTAL + 1) ))
123
+ ```
124
+
125
+ Determine `DETECTED_COMMIT_STYLE`:
126
+ - If `CONV_WITH_SCOPE_PCT >= 60` → `"conventional-with-scope"`, confidence `0.85`
127
+ - If `CONV_SIMPLE_PCT >= 60` → `"conventional"`, confidence `0.85`
128
+ - If `(CONV_COUNT + CONV_SIMPLE) < 20%` → `"plain"`, confidence `0.7`
129
+ - Otherwise → insufficient signal, skip `format-commit.sh` unless already in `learned:`
130
+
131
+ **If `LEARNED_COMMIT_STYLE` is non-empty**: use it (confidence `1.0`), skip git analysis for this signal.
132
+
133
+ **Detect test locations** (if `LEARNED_TEST_LOCS` is empty):
134
+ ```bash
135
+ # Look for test directories with actual test files
136
+ TEST_DIRS=""
137
+ for candidate in tests/ __tests__/ test/ spec/ src/__tests__/; do
138
+ COUNT=$(git ls-files "$candidate" 2>/dev/null | wc -l | tr -d ' ')
139
+ if [ "$COUNT" -gt "0" ]; then
140
+ TEST_DIRS="$TEST_DIRS $candidate"
141
+ fi
142
+ done
143
+ TEST_DIRS=$(echo "$TEST_DIRS" | xargs) # trim
144
+ ```
145
+
146
+ If `TEST_DIRS` is non-empty and differs from the default (`tests/`): add to PATTERNS for `find-tests.sh` with confidence `0.7`.
147
+
148
+ ### 1C: Mine FORGE:LEARNED annotations from closed issues
149
+
150
+ ```bash
151
+ # Read closed issues for repeated FORGE:LEARNED annotations (last 30 closed issues)
152
+ FORGE_LEARNED_BODIES=$(gh api "repos/$GH_REPO/issues?state=closed&per_page=30" \
153
+ --jq '[.[] | .number]' 2>/dev/null | jq -r '.[]' 2>/dev/null | \
154
+ while read -r n; do
155
+ gh api "repos/$GH_REPO/issues/$n/comments" \
156
+ --jq '.[] | select(.body | contains("FORGE:LEARNED")) | .body' 2>/dev/null
157
+ done | head -200)
158
+ echo "$FORGE_LEARNED_BODIES" | head -50
159
+ ```
160
+
161
+ **Extract corroborating signals from annotations**:
162
+ - Count occurrences of each staging branch override, test command, label remap, commit style across annotations
163
+ - Each repeated annotation (≥2 occurrences) raises confidence by `+0.15` for the matching pattern
164
+ - Cap confidence at `1.0`
165
+
166
+ **Practical limit**: Process at most 30 issues. If `gh` rate-limits, skip 1C and note in report.
167
+
168
+ ---
169
+
170
+ ## Phase 2: Script Generation
171
+
172
+ For each pattern in PATTERNS with `confidence >= 0.7` (or all if `--force`):
173
+
174
+ **Overwrite mode**: if a script already exists, read it first, then overwrite — never append. Log "updated" vs "created" in the report.
175
+
176
+ ### 2A: branch-targets.sh
177
+
178
+ Trigger: `branch_targets.staging` set in `learned:` (confidence `1.0`), OR git history shows consistent use of a non-default staging branch.
179
+
180
+ ```bash
181
+ cat > "$SCRIPTS_DIR/branch-targets.sh" << 'SCRIPT_EOF'
182
+ #!/usr/bin/env bash
183
+ # Auto-generated by /optimize — branch target overrides for this repo.
184
+ # Source: forge.yaml → learned.branch_targets
185
+ # Edit manually if your branch structure changes.
186
+
187
+ # STAGING_BRANCH: override the default staging branch for fast-lane PRs
188
+ echo "STAGING_BRANCH={LEARNED_STAGING_VALUE}"
189
+ SCRIPT_EOF
190
+ chmod +x "$SCRIPTS_DIR/branch-targets.sh"
191
+ ```
192
+
193
+ Replace `{LEARNED_STAGING_VALUE}` with the actual value from `learned.branch_targets.staging`.
194
+
195
+ ### 2B: run-tests.sh
196
+
197
+ Trigger: `test_commands` array non-empty in `learned:` (confidence `1.0`), OR FORGE:LEARNED annotations contain repeated test command corrections.
198
+
199
+ ```bash
200
+ cat > "$SCRIPTS_DIR/run-tests.sh" << 'SCRIPT_EOF'
201
+ #!/usr/bin/env bash
202
+ # Auto-generated by /optimize — test command sequence for this repo.
203
+ # Source: forge.yaml → learned.test_commands
204
+ # Edit manually if your test setup changes.
205
+
206
+ set -e
207
+
208
+ {LEARNED_TEST_COMMANDS_EXPANDED}
209
+ SCRIPT_EOF
210
+ chmod +x "$SCRIPTS_DIR/run-tests.sh"
211
+ ```
212
+
213
+ Expand `{LEARNED_TEST_COMMANDS_EXPANDED}` to one command per line, in array order.
214
+
215
+ ### 2C: find-tests.sh
216
+
217
+ Trigger: `test_locations` array non-empty in `learned:` (confidence `1.0`), OR git ls-files shows non-default test directories (confidence `0.7`).
218
+
219
+ ```bash
220
+ cat > "$SCRIPTS_DIR/find-tests.sh" << 'SCRIPT_EOF'
221
+ #!/usr/bin/env bash
222
+ # Auto-generated by /optimize — test file discovery for this repo.
223
+ # Source: forge.yaml → learned.test_locations
224
+ # Returns a list of test files matching this repo's actual test directory layout.
225
+
226
+ set -e
227
+
228
+ {FIND_COMMANDS_EXPANDED}
229
+ SCRIPT_EOF
230
+ chmod +x "$SCRIPTS_DIR/find-tests.sh"
231
+ ```
232
+
233
+ Expand `{FIND_COMMANDS_EXPANDED}` to `find {dir} -name "*.test.*" -o -name "*.spec.*" 2>/dev/null` for each location, plus glob patterns if specified. Patterns ending in `*` are expanded with `git ls-files`.
234
+
235
+ ### 2D: label-map.sh
236
+
237
+ Trigger: `label_map` non-empty in `learned:` (confidence `1.0`).
238
+
239
+ ```bash
240
+ cat > "$SCRIPTS_DIR/label-map.sh" << 'SCRIPT_EOF'
241
+ #!/usr/bin/env bash
242
+ # Auto-generated by /optimize — label name remapping for this repo.
243
+ # Source: forge.yaml → learned.label_map
244
+ # Usage: source this file, then use map_label() to resolve canonical → repo label.
245
+
246
+ # Associative array: canonical ForgeDock label → repo-specific label
247
+ declare -A LABEL_MAP=(
248
+ {LABEL_MAP_ENTRIES}
249
+ )
250
+
251
+ map_label() {
252
+ local canonical="$1"
253
+ echo "${LABEL_MAP[$canonical]:-$canonical}"
254
+ }
255
+ SCRIPT_EOF
256
+ chmod +x "$SCRIPTS_DIR/label-map.sh"
257
+ ```
258
+
259
+ Expand `{LABEL_MAP_ENTRIES}` to `["canonical"]="repo-specific"` pairs.
260
+
261
+ ### 2E: format-commit.sh
262
+
263
+ Trigger: `commit_style` set in `learned:` (confidence `1.0`), OR git history analysis produces `confidence >= 0.7`.
264
+
265
+ ```bash
266
+ cat > "$SCRIPTS_DIR/format-commit.sh" << 'SCRIPT_EOF'
267
+ #!/usr/bin/env bash
268
+ # Auto-generated by /optimize — commit message style enforcer for this repo.
269
+ # Source: forge.yaml → learned.commit_style (or git history analysis)
270
+ # Usage: ./format-commit.sh "type" "scope" "description" "issue_number"
271
+ # type: conventional prefix (feat, fix, refactor, docs, chore, etc.)
272
+ # scope: component or area being changed (e.g. auth, pipeline, commands)
273
+ # description: short imperative summary of the change
274
+ # issue_number: GitHub issue number (without #)
275
+ # Outputs: the correctly-formatted commit message for this repo.
276
+
277
+ set -e
278
+
279
+ STYLE="{COMMIT_STYLE}"
280
+ TYPE="${1:-}"
281
+ SCOPE="${2:-}"
282
+ DESCRIPTION="${3:-}"
283
+ ISSUE="${4:-}"
284
+
285
+ case "$STYLE" in
286
+ conventional-with-scope)
287
+ if [ -n "$SCOPE" ]; then
288
+ echo "${TYPE}(${SCOPE}): ${DESCRIPTION} (#${ISSUE})"
289
+ else
290
+ echo "${TYPE:-chore}: ${DESCRIPTION} (#${ISSUE})"
291
+ fi
292
+ ;;
293
+ conventional)
294
+ echo "${TYPE:-chore}: ${DESCRIPTION} (#${ISSUE})"
295
+ ;;
296
+ plain)
297
+ echo "${DESCRIPTION} (#${ISSUE})"
298
+ ;;
299
+ *)
300
+ echo "${DESCRIPTION}"
301
+ ;;
302
+ esac
303
+ SCRIPT_EOF
304
+ chmod +x "$SCRIPTS_DIR/format-commit.sh"
305
+ ```
306
+
307
+ Replace `{COMMIT_STYLE}` with the detected or learned value.
308
+
309
+ ---
310
+
311
+ ## Phase 3: Registry Update
312
+
313
+ Skip if `--no-registry`.
314
+
315
+ Write (overwrite) `.forgedock/scripts/registry.json`:
316
+
317
+ ```bash
318
+ GENERATED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
319
+
320
+ cat > "$SCRIPTS_DIR/registry.json" << JSON_EOF
321
+ {
322
+ "schema_version": 1,
323
+ "generated_at": "${GENERATED_AT}",
324
+ "generated_by": "/optimize",
325
+ "scripts": {
326
+ {REGISTRY_ENTRIES}
327
+ }
328
+ }
329
+ JSON_EOF
330
+ ```
331
+
332
+ Build `{REGISTRY_ENTRIES}` from the PATTERNS map. For each generated script:
333
+ ```json
334
+ "branch-targets": {
335
+ "path": "branch-targets.sh",
336
+ "confidence": 1.0,
337
+ "evidence": "forge.yaml → learned.branch_targets.staging",
338
+ "generated_at": "2026-03-15T10:30:00Z"
339
+ }
340
+ ```
341
+
342
+ Comma-separate all entries. If `--dry-run`: print the registry JSON to stdout instead of writing it.
343
+
344
+ ---
345
+
346
+ ## Phase 4: Report
347
+
348
+ Output a human-readable summary. Do NOT commit anything — `adaptive_scripts.commit` is respected:
349
+
350
+ ```bash
351
+ echo ""
352
+ echo "=== /optimize Report ==="
353
+ echo "Run at: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
354
+ echo "Repo: $GH_REPO"
355
+ echo "Scripts dir: $SCRIPTS_DIR"
356
+ if [ "$DRY_RUN" = "true" ]; then
357
+ echo "Mode: DRY RUN — no files written"
358
+ fi
359
+ echo ""
360
+ echo "--- Patterns detected ---"
361
+ ```
362
+
363
+ For each pattern in PATTERNS (sorted by confidence descending):
364
+ ```
365
+ {script_name}.sh confidence={confidence} source={evidence} action={created|updated|skipped|would-create}
366
+ ```
367
+
368
+ For patterns with `confidence < 0.7` that were skipped:
369
+ ```
370
+ {script_name}.sh SKIPPED (confidence={confidence} < 0.7 — run with --force to generate)
371
+ ```
372
+
373
+ Then:
374
+ ```
375
+ --- Summary ---
376
+ Scripts created: N
377
+ Scripts updated: N
378
+ Scripts skipped: N
379
+ Registry: {written|skipped}
380
+ Commit: NOT done (adaptive_scripts.commit=false — review scripts before committing)
381
+
382
+ --- Next steps ---
383
+ 1. Review generated scripts in .forgedock/scripts/
384
+ 2. Run each script manually to verify output
385
+ 3. If correct: git add .forgedock/scripts/ && git commit -m "feat(scripts): adaptive scripts from /optimize"
386
+ 4. If not: edit scripts directly or update forge.yaml → learned: and re-run /optimize
387
+ ```
388
+
389
+ If no patterns were detected at all:
390
+ ```
391
+ No patterns detected with sufficient confidence.
392
+
393
+ Possible reasons:
394
+ - forge.yaml → learned: is empty (no corrections captured yet)
395
+ - Git history is too short (< 10 commits) to detect commit style
396
+ - Test directories match the default (tests/) — no override needed
397
+
398
+ Run /work-on on a few issues to let the pipeline capture corrections, then re-run /optimize.
399
+ ```
400
+
401
+ ---
402
+
403
+ ## Idempotency Guarantee
404
+
405
+ Running `/optimize` twice produces the same output. Specifically:
406
+ - Scripts are always overwritten, never appended to
407
+ - `registry.json` is always overwritten with current analysis results
408
+ - Generated timestamps in scripts and registry will update on each run (this is expected)
409
+ - Content of scripts changes only if the underlying patterns change
410
+
411
+ To verify: run `/optimize` twice and compare outputs — the only differences should be the `generated_at` timestamps.
@@ -2,6 +2,8 @@
2
2
  description: Orchestrate parallel work on multiple issues or an entire milestone — spawns sub-agents that each run the full /work-on pipeline
3
3
  argument-hint: [milestone <slug> | #1 #2 #3 | next <N> | fast-lane | priority:P0]
4
4
  ---
5
+ <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
6
+ <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
5
7
 
6
8
  # /orchestrate — Multi-Issue Parallel Orchestrator
7
9
 
@@ -550,6 +552,103 @@ If `UNEXPECTED_GROWTH > 500`, report the alert clearly before launching the next
550
552
 
551
553
  ---
552
554
 
555
+ ### Step 4A.pre.0: Pre-create milestone branches for the wave (MANDATORY before classify-lane) <!-- Added: forge#901 -->
556
+
557
+ **WHY THIS EXISTS**: Feature-lane milestone branches were created lazily — by whichever feature-lane agent reached its build phase first. When a wave launches N feature-lane agents in parallel, they each run the lane check at roughly the same time. Every agent that runs before the branch is first pushed observes "branch absent" and is misrouted (hard-fail / `needs-human`, or fallback to staging in older code paths). The result is a single milestone's PRs scattered across the milestone branch and staging — a branch-routing nondeterminism that recurs under parallelism.
558
+
559
+ The fix is deterministic: create every milestone branch the wave will target **once, up front, before any agent runs `classify-lane.sh`**. After this step, every agent's lane check sees the branch and routes consistently.
560
+
561
+ **When to run**: Before the classify-lane loop in Step 4A.pre, for every wave. The step is a no-op for pure fast-lane waves (no issue in the wave has a milestone).
562
+
563
+ **Requires bash 4+**: This snippet uses an associative array (`declare -A SEEN_MILESTONE_SLUG`) to de-dupe milestone slugs, so it must run under bash 4 or newer. Under a non-bash POSIX shell (`sh`/dash), `declare -A` fails and the de-dupe silently no-ops. This degrades gracefully — branch creation stays correct because the `git ls-remote --exit-code` exists-check below still skips any milestone branch that already exists; the only effect is redundant, idempotent `ls-remote`/`push` attempts for milestones referenced by more than one issue in the wave. Run this command's blocks under bash 4+. <!-- Added: forge#901 -->
564
+
565
+ ```bash
566
+ # Pre-create the origin milestone branch for every distinct milestone referenced by the wave.
567
+ # Slugification MUST byte-match scripts/classify-lane.sh — otherwise a branch is created that
568
+ # the classifier will not select. Keep these two slug pipelines identical.
569
+ git fetch origin
570
+
571
+ # Collect distinct milestone titles among the wave's issues
572
+ declare -A SEEN_MILESTONE_SLUG
573
+ for NUM in {wave_issue_numbers}; do
574
+ MILESTONE_TITLE=$(gh issue view "$NUM" -R {GH_REPO} --json milestone --jq '.milestone.title // empty' 2>/dev/null || echo "")
575
+ [ -z "$MILESTONE_TITLE" ] && continue # fast-lane issue — no milestone branch needed
576
+
577
+ # Slugify — IDENTICAL to classify-lane.sh: lowercase → spaces-to-hyphens →
578
+ # strip non-[a-z0-9-] → collapse hyphens → strip leading/trailing hyphens.
579
+ SLUG=$(echo "$MILESTONE_TITLE" \
580
+ | tr '[:upper:]' '[:lower:]' \
581
+ | tr ' ' '-' \
582
+ | tr -cd 'a-z0-9-' \
583
+ | sed 's/--*/-/g' \
584
+ | sed 's/^-//;s/-$//')
585
+
586
+ # Empty-slug guard (matches classify-lane.sh): a title with no ASCII letters/digits/hyphens
587
+ # would produce "milestone/", an invalid ref. Skip and let classify-lane surface the error.
588
+ if [ -z "$SLUG" ]; then
589
+ echo "WARN: milestone title '$MILESTONE_TITLE' (issue #$NUM) produced an empty slug — skipping pre-creation; classify-lane will hard-fail." >&2
590
+ continue
591
+ fi
592
+
593
+ # De-dupe: only attempt creation once per milestone
594
+ [ -n "${SEEN_MILESTONE_SLUG[$SLUG]:-}" ] && continue
595
+ SEEN_MILESTONE_SLUG[$SLUG]=1
596
+
597
+ LANE="milestone/$SLUG"
598
+ if git ls-remote --exit-code origin "$LANE" >/dev/null 2>&1; then
599
+ echo "Milestone branch '$LANE' already exists on origin — no action."
600
+ continue
601
+ fi
602
+
603
+ # Create-if-absent from the default branch (matches /milestone create).
604
+ # $DEFAULT_BRANCH is resolved from forge.yaml at the top of this command.
605
+ echo "Pre-creating milestone branch '$LANE' from origin/$DEFAULT_BRANCH …"
606
+ if git push origin "origin/$DEFAULT_BRANCH:refs/heads/$LANE" 2>/dev/null; then
607
+ echo "Created milestone branch '$LANE'."
608
+ elif git ls-remote --exit-code origin "$LANE" >/dev/null 2>&1; then
609
+ # A concurrent orchestrator (or an agent) created it first — harmless. Never force-push.
610
+ echo "Milestone branch '$LANE' was created concurrently — proceeding with the existing branch."
611
+ else
612
+ echo "ERROR: failed to pre-create milestone branch '$LANE' from origin/$DEFAULT_BRANCH." >&2
613
+ echo " classify-lane.sh will hard-fail for issues in this milestone until the branch exists." >&2
614
+ fi
615
+ done
616
+ ```
617
+
618
+ This step is the deterministic counterpart to fix #2 (atomic create-if-absent in the classifier): by guaranteeing the branch exists before the wave's lane checks run, no agent can observe a missing branch. `classify-lane.sh`'s hard-fail is intentionally preserved as the phantom-slug gate for any path that bypasses this step.
619
+
620
+ ### Step 4A.pre: Classify lane for each issue (MANDATORY before spawning agents)
621
+
622
+ Before building agent prompts, run `classify-lane.sh` for every issue in the current wave to compute `{LANE}` and `{PR_BASE}` deterministically. The script output is authoritative — the LLM MUST NOT override or reason around it.
623
+
624
+ ```bash
625
+ # Requires classify-lane.sh to be available at ~/.claude/scripts/classify-lane.sh
626
+ # (installed by `npx forgedock` — see bin/forgedock.mjs linkScripts step)
627
+ # Fallback: bash "$FORGE_HOME/scripts/classify-lane.sh" if ~/.claude/scripts/ is unavailable
628
+
629
+ declare -A ISSUE_LANE
630
+ declare -A ISSUE_PR_BASE
631
+
632
+ for NUM in {wave_issue_numbers}; do
633
+ PR_BASE=$(bash ~/.claude/scripts/classify-lane.sh "$NUM" -R {GH_REPO}) || {
634
+ echo "ERROR: classify-lane.sh failed for #$NUM — adding needs-human label and skipping" >&2
635
+ gh issue edit "$NUM" -R {GH_REPO} --add-label "needs-human" 2>/dev/null || true
636
+ continue
637
+ }
638
+ # Derive LANE label from PR_BASE
639
+ if [ "$PR_BASE" = "staging" ]; then
640
+ LANE="fast-lane"
641
+ else
642
+ LANE="feature-lane"
643
+ fi
644
+ ISSUE_LANE[$NUM]="$LANE"
645
+ ISSUE_PR_BASE[$NUM]="$PR_BASE"
646
+ echo "#$NUM → lane=$LANE, PR_BASE=$PR_BASE"
647
+ done
648
+ ```
649
+
650
+ Use `${ISSUE_LANE[$NUM]}` and `${ISSUE_PR_BASE[$NUM]}` to populate `{LANE}` and `{PR_BASE}` in the agent template below. Never substitute prose guesses for these values — the script output is the only valid source. <!-- Added: forge#677 -->
651
+
553
652
  ### Step 4A: Launch a wave
554
653
 
555
654
  **REMINDER: You MUST use the template below verbatim. 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.**
@@ -694,6 +793,90 @@ You will be automatically notified when each background agent completes. **Do NO
694
793
  - Reading agent JSONL output files to check progress — use GitHub labels as the source of truth
695
794
  - Polling the same status check repeatedly on a timer
696
795
 
796
+ ### Step 4B.5: Time-Based Stall Detection
797
+
798
+ **Purpose**: Catches agents that have stopped responding WITHOUT exiting (e.g., rate-limited, context-frozen, or silently hung). The reactive check in Step 4B only fires on agent completion — this check catches agents that never complete at all.
799
+
800
+ **When to run** (NOT a sleep loop — two trigger points only):
801
+ 1. On every background agent completion event (run BEFORE the terminal-state check in Step 4B)
802
+ 2. Before posting any "waiting for wave..." status update to the user
803
+
804
+ **Do NOT poll on a timer. Do NOT use sleep. Run at these two trigger points only.**
805
+
806
+ **Read stall timeout from config**:
807
+ ```bash
808
+ STALL_TIMEOUT=$(yq '.pipeline.stall_timeout_minutes // 15' forge.yaml 2>/dev/null || echo 15)
809
+ ```
810
+
811
+ **For each non-terminal agent in the current wave**:
812
+ ```bash
813
+ for NUM in {active_wave_issue_numbers}; do
814
+ # Skip issues already in terminal state
815
+ TERMINAL=$(gh issue view $NUM -R {GH_REPO} --json labels \
816
+ --jq '[.labels[].name | select(. == "workflow:merged" or . == "workflow:invalid" or . == "needs-human")] | length')
817
+ [ "$TERMINAL" -gt 0 ] && continue
818
+
819
+ # Get last activity timestamp — prefer last comment (catches FORGE:HEARTBEAT updates)
820
+ LAST_ACTIVITY=$(gh api repos/{GH_REPO}/issues/${NUM}/comments \
821
+ --jq '.[-1].updated_at // empty' 2>/dev/null)
822
+ # Fall back to issue updated_at if no comments
823
+ if [ -z "$LAST_ACTIVITY" ]; then
824
+ LAST_ACTIVITY=$(gh issue view $NUM -R {GH_REPO} --json updatedAt --jq '.updatedAt')
825
+ fi
826
+
827
+ # Compute elapsed minutes (GNU date — adjust for macOS: date -j -f "%Y-%m-%dT%H:%M:%SZ")
828
+ LAST_EPOCH=$(date -d "$LAST_ACTIVITY" +%s 2>/dev/null \
829
+ || date -j -f "%Y-%m-%dT%H:%M:%SZ" "$LAST_ACTIVITY" +%s 2>/dev/null)
830
+ NOW_EPOCH=$(date +%s)
831
+ ELAPSED_MIN=$(( (NOW_EPOCH - LAST_EPOCH) / 60 ))
832
+
833
+ if [ "$ELAPSED_MIN" -gt "$STALL_TIMEOUT" ]; then
834
+ # Count prior stall events on this issue
835
+ STALL_COUNT=$(gh api repos/{GH_REPO}/issues/${NUM}/comments \
836
+ --jq '[.[] | select(.body | contains("FORGE:STALL_DETECTED"))] | length')
837
+
838
+ CURRENT_STATE=$(gh issue view $NUM -R {GH_REPO} --json labels \
839
+ --jq '[.labels[].name | select(startswith("workflow:"))] | .[0] // "unknown"')
840
+
841
+ if [ "$STALL_COUNT" -lt 2 ]; then
842
+ # Auto-resume: post stall annotation and re-invoke /work-on
843
+ RESUME_ATTEMPT=$(( STALL_COUNT + 1 ))
844
+ gh issue comment $NUM -R {GH_REPO} --body "<!-- FORGE:STALL_DETECTED -->
845
+ ## Stall Detected
846
+
847
+ **Issue**: #${NUM}
848
+ **Elapsed since last activity**: ${ELAPSED_MIN} min (threshold: ${STALL_TIMEOUT} min)
849
+ **Current workflow state**: ${CURRENT_STATE}
850
+ **Auto-resume attempt**: ${RESUME_ATTEMPT} of 2
851
+ **Timestamp**: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
852
+
853
+ # Resume the agent — collect all resumes and launch in a single message (see Step 4B rule)
854
+ # STALL_RESUME_LIST is accumulated and launched in parallel after the loop
855
+ STALL_RESUME_LIST="$STALL_RESUME_LIST $NUM"
856
+ else
857
+ # 2+ prior stalls — auto-resume exhausted, escalate to needs-human
858
+ gh issue edit $NUM -R {GH_REPO} --add-label "needs-human"
859
+ gh issue comment $NUM -R {GH_REPO} --body "<!-- FORGE:STALL_DETECTED -->
860
+ ## Stall Escalated — Needs Human Intervention
861
+
862
+ Issue #${NUM} has been auto-resumed ${STALL_COUNT} times without reaching a terminal state. Auto-resume limit (2) exhausted. Manual intervention required.
863
+
864
+ **Last workflow state**: ${CURRENT_STATE}
865
+ **Total elapsed since last activity**: ${ELAPSED_MIN} min
866
+ **Timestamp**: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
867
+ echo "STALL ESCALATED: #{NUM} → needs-human (${STALL_COUNT} prior resumes)"
868
+ fi
869
+ fi
870
+ done
871
+
872
+ # Launch all stall resumes in parallel (single message — same rule as Step 4B)
873
+ # For each NUM in $STALL_RESUME_LIST, call Agent(resume="{AGENT_ID}", run_in_background=true, ...)
874
+ ```
875
+
876
+ **Resume all stalled agents in a single message** (parallel). Use the same `Agent(resume=...)` pattern as Step 4B — do not wait between individual resumes.
877
+
878
+ **Track stall resume cycles separately** from completion-event resumes (Step 4B). If the same issue accumulates ≥ 2 `FORGE:STALL_DETECTED` comments AND still hasn't reached terminal state, do not resume again — the `needs-human` label is already set.
879
+
697
880
  ### Step 4C: Collect review-finding issues from completed wave
698
881
 
699
882
  When ALL agents in the current wave have completed, check if any `/work-on` runs spawned review-finding issues during their review phase. These are new work items that should be folded into the remaining waves.
@@ -814,6 +997,62 @@ They remain open in GitHub — they will be picked up by future `/orchestrate` o
814
997
 
815
998
  **If no review-finding issues were spawned:** Continue to next wave normally.
816
999
 
1000
+ ### Step 4C.5: Milestone lane-consistency check (run after each wave) <!-- Added: forge#901 -->
1001
+
1002
+ **WHY THIS EXISTS**: A milestone's feature-lane PRs must all target the same milestone branch. If a branch-routing race ever scatters them — some on the milestone branch, some on staging — the milestone branch becomes incomplete relative to staging, and the split is otherwise invisible until the milestone tries to ship. Step 4A.pre.0 prevents the split deterministically; this check detects any residual split so it surfaces immediately instead of at ship time.
1003
+
1004
+ **When to run**: After each wave completes, for any batch where at least one issue has a milestone. Skip for pure fast-lane batches. This check is **non-blocking** — it alerts; it does not auto-resolve or stop the pipeline.
1005
+
1006
+ ```bash
1007
+ # For each distinct milestone in the batch, assert all of its feature-lane PRs share one base.
1008
+ for NUM in {all_batch_issue_numbers}; do
1009
+ MILESTONE_TITLE=$(gh issue view "$NUM" -R {GH_REPO} --json milestone --jq '.milestone.title // empty' 2>/dev/null || echo "")
1010
+ [ -z "$MILESTONE_TITLE" ] && continue
1011
+
1012
+ SLUG=$(echo "$MILESTONE_TITLE" \
1013
+ | tr '[:upper:]' '[:lower:]' \
1014
+ | tr ' ' '-' \
1015
+ | tr -cd 'a-z0-9-' \
1016
+ | sed 's/--*/-/g' \
1017
+ | sed 's/^-//;s/-$//')
1018
+ [ -z "$SLUG" ] && continue
1019
+ EXPECTED_BASE="milestone/$SLUG"
1020
+
1021
+ # Collect the base branch of every PR that closes an issue in this milestone.
1022
+ # Iterate the milestone's issues and read each one's linked PR base.
1023
+ # Exclude CLOSED-unmerged PRs: a closed-but-not-merged PR is a superseded/abandoned
1024
+ # routing attempt and does NOT reflect the live lane. Keep only OPEN (in-flight) and
1025
+ # MERGED (landed) PRs. `gh pr list --state` cannot combine open+merged, so query all
1026
+ # and drop CLOSED in jq.
1027
+ BASES=$(gh pr list -R {GH_REPO} --state all --search "milestone:\"$MILESTONE_TITLE\"" \
1028
+ --json baseRefName,state --jq '.[] | select(.state != "CLOSED") | .baseRefName' 2>/dev/null | sort -u)
1029
+ # Fallback: if PR search by milestone is unavailable, derive from the issues' linked PRs.
1030
+ if [ -z "$BASES" ]; then
1031
+ BASES=$(for IN in {all_batch_issue_numbers}; do
1032
+ IM=$(gh issue view "$IN" -R {GH_REPO} --json milestone --jq '.milestone.title // empty' 2>/dev/null)
1033
+ [ "$IM" = "$MILESTONE_TITLE" ] || continue
1034
+ gh pr list -R {GH_REPO} --state all --search "$IN in:body" \
1035
+ --json baseRefName,state --jq '.[] | select(.state != "CLOSED") | .baseRefName' 2>/dev/null
1036
+ done | sort -u)
1037
+ fi
1038
+
1039
+ STRAY_BASES=$(echo "$BASES" | grep -v "^${EXPECTED_BASE}\$" | grep -v '^$' || true)
1040
+ if [ -n "$STRAY_BASES" ]; then
1041
+ echo "ALERT: milestone '$MILESTONE_TITLE' has feature-lane PRs split across multiple base branches." >&2
1042
+ echo " Expected base: $EXPECTED_BASE" >&2
1043
+ echo " Found bases:" >&2
1044
+ echo "$BASES" | sed 's/^/ - /' >&2
1045
+ echo " This indicates a branch-routing split — reconcile the stray PRs onto $EXPECTED_BASE" >&2
1046
+ echo " (rebase/cherry-pick the stray branch onto the milestone branch) before the milestone ships." >&2
1047
+ # Do NOT auto-stop or auto-resolve — surface the alert and let the user decide.
1048
+ else
1049
+ echo "Lane-consistency OK: all '$MILESTONE_TITLE' PRs target $EXPECTED_BASE."
1050
+ fi
1051
+ done
1052
+ ```
1053
+
1054
+ Report any `ALERT` lines prominently before advancing to the next wave. Reconciliation of an existing split is a manual/`/milestone`-assisted step — this check only ensures the split is never silent.
1055
+
817
1056
  ### Step 4D: Milestone integration build gate (MANDATORY between waves)
818
1057
 
819
1058
  **WHY THIS EXISTS**: Session Intelligence milestone shipped 116 PRs across multiple waves with zero integration testing. Each PR built in isolation — type errors from cross-PR interactions (wrong prop types, missing components, incompatible interfaces) were invisible until the milestone→staging merge broke the build with 4 distinct errors. This gate catches those failures early.