forgedock 1.1.7 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -33
- package/bin/cinema.mjs +23 -5
- package/bin/engine/invariants.mjs +361 -0
- package/bin/engine-cli.mjs +137 -11
- package/bin/env-detect.mjs +249 -0
- package/bin/forge-utils.mjs +364 -4
- package/bin/forgedock.mjs +1064 -135
- package/bin/hooks/pre-tool-use.mjs +182 -0
- package/bin/hooks/session-start.mjs +68 -3
- package/bin/init-detect.mjs +101 -12
- package/bin/journey.mjs +917 -16
- package/bin/labels.json +10 -0
- package/bin/recall.mjs +557 -0
- package/bin/registry.mjs +91 -3
- package/bin/runner.mjs +38 -2
- package/bin/tui.mjs +49 -3
- package/commands/adopt.md +1 -1
- package/commands/analytics.md +8 -8
- package/commands/audit.md +1 -1
- package/commands/autopilot.md +958 -77
- package/commands/ci-audit.md +1 -1
- package/commands/cleanup.md +139 -55
- package/commands/compat-audit.md +1 -1
- package/commands/deploy-info.md +1 -1
- package/commands/deploy-pr.md +1 -1
- package/commands/diagnose.md +1 -1
- package/commands/explain.md +1 -1
- package/commands/fix-ci.md +1 -1
- package/commands/forgedock-init.md +1 -1
- package/commands/incident-response.md +3 -3
- package/commands/issue.md +1 -1
- package/commands/optimize.md +586 -1
- package/commands/orchestrate/config.md +13 -3
- package/commands/orchestrate/phase-1-resolve.md +36 -26
- package/commands/orchestrate/phase-2-triage.md +1 -1
- package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +587 -48
- package/commands/orchestrate/phase-4-execution.md +1031 -57
- package/commands/orchestrate/phase-5-cleanup.md +1 -1
- package/commands/orchestrate/phase-6-report.md +248 -11
- package/commands/orchestrate/safety.md +2 -2
- package/commands/pipeline-health/phase-2-metrics.md +305 -1
- package/commands/pipeline-health/phase-4-proposals.md +127 -0
- package/commands/qa-sweep.md +1 -1
- package/commands/quality-gate.md +418 -1
- package/commands/recover-orphans.md +5 -5
- package/commands/replay.md +1 -1
- package/commands/review-pr-agents/api.md +2 -2
- package/commands/review-pr-agents/auth.md +2 -2
- package/commands/review-pr-agents/billing.md +2 -2
- package/commands/review-pr-agents/concurrency.md +2 -2
- package/commands/review-pr-agents/database.md +2 -2
- package/commands/review-pr-agents/frontend.md +2 -2
- package/commands/review-pr-agents/infra.md +2 -2
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-agents/scraper.md +2 -2
- package/commands/review-pr-agents/security.md +2 -2
- package/commands/review-pr-staging.md +41 -3
- package/commands/review-pr.md +462 -19
- package/commands/rollback.md +1 -1
- package/commands/scope.md +1 -1
- package/commands/security-audit.md +1 -1
- package/commands/spec-doctor.md +429 -0
- package/commands/test-gate.md +1 -1
- package/commands/validate.md +53 -1
- package/commands/work-on/build/architect.md +78 -1
- package/commands/work-on/build/context.md +360 -22
- package/commands/work-on/build/implement.md +3 -1
- package/commands/work-on/build/validate.md +29 -1
- package/commands/work-on/build.md +75 -6
- package/commands/work-on/close.md +624 -10
- package/commands/work-on/decompose.md +1 -1
- package/commands/work-on/investigate.md +235 -5
- package/commands/work-on/remediate.md +331 -0
- package/commands/work-on/review.md +1 -1
- package/commands/work-on-monolithic.md +1 -1
- package/commands/work-on.md +205 -66
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +1 -1
- package/docs/CONFIG.md +186 -0
- package/docs/FORGE-PROTOCOL.md +101 -0
- package/docs/site/command-reference.md +31 -1
- package/docs/site/getting-started.md +9 -34
- package/docs/site/index.md +1 -1
- package/docs/spec/forge-protocol-v1.md +97 -5
- package/docs/spec/label-state-machine.md +6 -1
- package/package.json +3 -2
- package/packages/protocol/src/cli.js +374 -84
- package/packages/protocol/src/types.js +75 -1
- package/scripts/build-knowledge-index.mjs +1761 -0
- package/scripts/calibration.mjs +996 -0
- package/scripts/check-command-docs-drift.sh +4 -2
- package/scripts/danger-zones.mjs +789 -0
- package/scripts/doctor-pipeline-state.sh +88 -2
- package/scripts/select-fix-targets.sh +234 -0
- package/scripts/transition-label.sh +245 -6
- package/scripts/refresh-bot-token.sh +0 -97
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
install:
|
|
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
|
-
|
|
35
|
-
echo "$ISSUE" | grep -qiE "
|
|
36
|
-
echo "$ISSUE" | grep -qiE "
|
|
37
|
-
echo "$ISSUE" | grep -qiE "
|
|
38
|
-
echo "$ISSUE" | grep -qiE "
|
|
39
|
-
echo "$ISSUE" | grep -qiE "
|
|
40
|
-
echo "$ISSUE" | grep -qiE "
|
|
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
|
-
**
|
|
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)
|
|
@@ -162,11 +182,111 @@ When file extraction yields **fewer than 2 file paths** for an issue (common for
|
|
|
162
182
|
|
|
163
183
|
**Rationale**: The cost of a false-negative (two agents conflict → one fails with merge error, wasting the full agent run) far exceeds the cost of a false-positive (an issue waits for one predecessor before starting). Always err toward serialization when uncertain.
|
|
164
184
|
|
|
165
|
-
#### Layer 5: Historical co-change coupling <!-- Added: forge#1196 --> <!-- Empty-set guard: forge#1206 -->
|
|
185
|
+
#### Layer 5: Historical co-change coupling <!-- Added: forge#1196 --> <!-- Empty-set guard: forge#1206 --> <!-- Matrix lookup: forge#1738 -->
|
|
186
|
+
|
|
187
|
+
Layers 1-4 infer conflict risk from structure — path overlap, directory nesting, hard-coded high-fan-in lists. They miss the case where two files with no directory or naming relationship have historically changed together in the same commits (e.g. `models/user.py` and `services/billing/charge.py`), and they over-serialize the inverse case where files merely sit near each other but have never actually co-changed. Git commit history answers both questions directly and empirically. This layer reads **commit metadata only** — the list of files touched per commit — never file contents, so it does not violate Hard Rule 2 ("dispatcher, not a builder... never read code").
|
|
188
|
+
|
|
189
|
+
**Primary path — persisted co-change matrix** (O(1) lookup, repo-wide coverage): <!-- Added: forge#1738 -->
|
|
190
|
+
|
|
191
|
+
Check whether `~/.forge/index/cochange.jsonl` (produced by `scripts/danger-zones.mjs`) exists before
|
|
192
|
+
falling back to the live `git log` query. The matrix is repo-wide and covers file pairs outside the
|
|
193
|
+
current batch — providing broader coupling signal than the batch-scoped live query alone.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
COCHANGE_INDEX="${HOME}/.forge/index/cochange.jsonl"
|
|
197
|
+
COCHANGE_META="${HOME}/.forge/index/cochange-meta.json"
|
|
198
|
+
|
|
199
|
+
if [ -f "$COCHANGE_INDEX" ]; then
|
|
200
|
+
echo "Layer 5: co-change matrix found — using persisted index for pair lookups"
|
|
201
|
+
LAYER5_SOURCE="matrix"
|
|
202
|
+
TOTAL_COMMITS=$(cat "$COCHANGE_META" 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('totalCommits',0))" 2>/dev/null || echo "0")
|
|
203
|
+
else
|
|
204
|
+
echo "Layer 5: co-change matrix absent — falling back to live git query (run 'node scripts/danger-zones.mjs' to build)"
|
|
205
|
+
LAYER5_SOURCE="live"
|
|
206
|
+
TOTAL_COMMITS=0
|
|
207
|
+
fi
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Matrix lookup per file pair** (when `LAYER5_SOURCE=matrix`):
|
|
211
|
+
|
|
212
|
+
For each pair of files across issues in the batch, query the matrix for coupling verdict:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# Query a single file pair from the matrix
|
|
216
|
+
query_cochange_pair() {
|
|
217
|
+
local FILE_A="$1"
|
|
218
|
+
local FILE_B="$2"
|
|
219
|
+
|
|
220
|
+
# Find the record for FILE_A (matrix stores each pair once in lexicographic order,
|
|
221
|
+
# but also includes reverse-lookup entries for O(1) lookup from either side)
|
|
222
|
+
local RECORD
|
|
223
|
+
RECORD=$(grep -m1 "\"file\":\"${FILE_A}\"" "$COCHANGE_INDEX" 2>/dev/null || true)
|
|
224
|
+
if [ -z "$RECORD" ]; then
|
|
225
|
+
echo "unknown" # FILE_A not in matrix — insufficient history
|
|
226
|
+
return
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
# Extract n(A) — sum of monthly ring buffer
|
|
230
|
+
local N_A
|
|
231
|
+
N_A=$(echo "$RECORD" | python3 -c "
|
|
232
|
+
import sys, json
|
|
233
|
+
r = json.loads(sys.stdin.read())
|
|
234
|
+
n = r.get('n', [0,0,0])
|
|
235
|
+
print(sum(n))
|
|
236
|
+
" 2>/dev/null || echo "0")
|
|
237
|
+
|
|
238
|
+
# Extract c(A,B) — co-occurrence count with FILE_B
|
|
239
|
+
local C_AB
|
|
240
|
+
C_AB=$(echo "$RECORD" | python3 -c "
|
|
241
|
+
import sys, json
|
|
242
|
+
r = json.loads(sys.stdin.read())
|
|
243
|
+
partners = r.get('partners', {})
|
|
244
|
+
c = partners.get('${FILE_B}', [0,0,0])
|
|
245
|
+
print(sum(c))
|
|
246
|
+
" 2>/dev/null || echo "0")
|
|
247
|
+
|
|
248
|
+
# Apply thresholds: cold-start check (n < 5), support (c >= 3), confidence
|
|
249
|
+
if [ "$N_A" -lt 5 ]; then
|
|
250
|
+
echo "unknown" # Insufficient history for FILE_A
|
|
251
|
+
return
|
|
252
|
+
fi
|
|
166
253
|
|
|
167
|
-
|
|
254
|
+
local FILE_B_RECORD
|
|
255
|
+
FILE_B_RECORD=$(grep -m1 "\"file\":\"${FILE_B}\"" "$COCHANGE_INDEX" 2>/dev/null || true)
|
|
256
|
+
local N_B=0
|
|
257
|
+
if [ -n "$FILE_B_RECORD" ]; then
|
|
258
|
+
N_B=$(echo "$FILE_B_RECORD" | python3 -c "
|
|
259
|
+
import sys, json
|
|
260
|
+
r = json.loads(sys.stdin.read())
|
|
261
|
+
n = r.get('n', [0,0,0])
|
|
262
|
+
print(sum(n))
|
|
263
|
+
" 2>/dev/null || echo "0")
|
|
264
|
+
fi
|
|
168
265
|
|
|
169
|
-
|
|
266
|
+
if [ "$N_B" -lt 5 ]; then
|
|
267
|
+
echo "unknown" # Insufficient history for FILE_B
|
|
268
|
+
return
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
# Use danger-zones.mjs --query for the authoritative normalization verdict
|
|
272
|
+
# (handles ubiquity, directional confidence, companions — avoids re-implementing)
|
|
273
|
+
VERDICT=$(node "$(git rev-parse --show-toplevel 2>/dev/null)/scripts/danger-zones.mjs" \
|
|
274
|
+
--query "$FILE_A" 2>/dev/null \
|
|
275
|
+
| python3 -c "
|
|
276
|
+
import sys, json
|
|
277
|
+
d = json.loads(sys.stdin.read())
|
|
278
|
+
partners = d.get('cochangePartners', [])
|
|
279
|
+
for p in partners:
|
|
280
|
+
if p['file'] == '${FILE_B}':
|
|
281
|
+
print(p['verdict'])
|
|
282
|
+
sys.exit(0)
|
|
283
|
+
print('unknown')
|
|
284
|
+
" 2>/dev/null || echo "unknown")
|
|
285
|
+
echo "$VERDICT"
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Fallback path — live git query** (when matrix is absent or file pair not found in matrix):
|
|
170
290
|
|
|
171
291
|
```bash
|
|
172
292
|
# Union of affected files across all issues in the CURRENT batch only (already
|
|
@@ -204,14 +324,16 @@ else
|
|
|
204
324
|
fi
|
|
205
325
|
```
|
|
206
326
|
|
|
207
|
-
**Scoring rule**: A file pair is **co-change coupled** when it appears together in **3 or more**
|
|
327
|
+
**Scoring rule**: A file pair is **co-change coupled** when it appears together in **3 or more** commits in the window. A pair with **zero** co-occurrences across the entire window (and both files have n ≥ 5 commits) is **verified independent**. Pairs where either file has fewer than 5 commits in the window are **unknown** — the matrix must NOT be used to downgrade edges for unknown pairs.
|
|
208
328
|
|
|
209
329
|
**Apply the signal:**
|
|
210
330
|
- **High co-change pair spans two different issues in the batch** → add a serialization edge between them (same directed-edge convention as Layers 1-4: lower issue number is predecessor), OR, if the pair also carries competing investigation recommendations, flag it for Phase 2.5 arbitration instead of a blind serialization edge (see cross-reference in Step 2.5B below).
|
|
211
|
-
- **Verified-independent pair** → MAY be used to downgrade an existing Layer 2 "broad directory + different domain" or Layer 4 "conservative fallback" serialization to parallel. This downgrade is **never** applied to Layer 1 (same-file hard conflict, which is ground truth from the current batch, not historical inference) or to Layer 3 high-fan-in-file edges (a file can be structurally high-risk even with a thin history window, e.g. a newly added `main.py`).
|
|
212
|
-
- If `ALL_AFFECTED_FILES` is empty, the guard above skips the query and Layer 5 contributes nothing for the entire batch. If the
|
|
331
|
+
- **Verified-independent pair** → MAY be used to downgrade an existing Layer 2 "broad directory + different domain" or Layer 4 "conservative fallback" serialization to parallel. This downgrade is **never** applied to Layer 1 (same-file hard conflict, which is ground truth from the current batch, not historical inference) or to Layer 3 high-fan-in-file edges (a file can be structurally high-risk even with a thin history window, e.g. a newly added `main.py`). Ubiquitous-file pairs (n/N > 0.2 for either file) are **ineligible** for verified-independent downgrade even with zero co-occurrences.
|
|
332
|
+
- If `ALL_AFFECTED_FILES` is empty, the guard above skips the query and Layer 5 contributes nothing for the entire batch. If the matrix or live query returns no data for a pair → Layer 5 contributes nothing; fall back silently to Layers 1-4's existing verdict for that pair.
|
|
333
|
+
|
|
334
|
+
**Wire-through proof (mandatory check)**: When `LAYER5_SOURCE=matrix`, confirm the matrix lookup path executes on at least one pair in the batch and log the verdict. This proves the path is live, not dead code. If no pairs are in the matrix, log that the live fallback ran instead. <!-- Ref: forge#1731, forge#1230, forge#1244 — Layer 5 has had two dead-code defects; this check prevents recurrence. -->
|
|
213
335
|
|
|
214
|
-
**Rationale**:
|
|
336
|
+
**Rationale**: The persisted matrix provides broader coverage (repo-wide, not batch-scoped) and O(1) lookup vs O(batch × commits) live query. The live fallback ensures no regression when the matrix is absent (cold start or first run). Bounding the live fallback to the batch file set keeps it cheap and deterministic.
|
|
215
337
|
|
|
216
338
|
#### Combining all layers
|
|
217
339
|
|
|
@@ -242,9 +364,74 @@ Build a **directed acyclic graph (DAG)** of per-issue dependencies. Each issue g
|
|
|
242
364
|
- **Explicit dependencies**: If issue B says "Depends on #A" or "Blocked by #A", add A to B's predecessors
|
|
243
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.
|
|
244
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.
|
|
245
368
|
- **Conservative fallback edges**: Low-confidence issues (Layer 4) get edges to same-domain issues as per Step 3C rules.
|
|
246
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.
|
|
247
|
-
- **
|
|
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.
|
|
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
|
+
```
|
|
393
|
+
|
|
394
|
+
### Step 3D.1: Create coordination issue (claims board) <!-- Added: forge#1736 -->
|
|
395
|
+
|
|
396
|
+
**When to run**: Immediately after DAG construction (Step 3D), before Step 3D.5 cycle detection. Run once per orchestration batch. Skip if `FORGE_COORD_ISSUE` is already set (e.g., resumed session).
|
|
397
|
+
|
|
398
|
+
**Purpose**: Create a dedicated GitHub issue that serves as the shared claims board for the batch. Agents post `FORGE:CLAIM` annotations here when they begin implementation; they post `FORGE:CLAIM_RELEASED` when they reach a terminal state. The orchestrator reads active claims during the Layer-2/4 relaxation sweep (Step 4B) to determine whether serialized pairs can now run in parallel.
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
# Create coordination issue for this orchestration batch
|
|
402
|
+
BATCH_ISSUE_COUNT="${#ISSUES[@]}"
|
|
403
|
+
BATCH_ID="$(date -u +%Y%m%dT%H%M%S)-$$"
|
|
404
|
+
|
|
405
|
+
COORD_ISSUE_BODY="## Orchestration Batch Claims Board
|
|
406
|
+
|
|
407
|
+
This issue is the claims board for an orchestration batch of ${BATCH_ISSUE_COUNT} issues.
|
|
408
|
+
Agents post \`FORGE:CLAIM\` here on build start and \`FORGE:CLAIM_RELEASED\` on terminal state.
|
|
409
|
+
|
|
410
|
+
**Batch ID**: ${BATCH_ID}
|
|
411
|
+
**Issues in batch**: ${ISSUES[*]/#/#}
|
|
412
|
+
**Created**: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
413
|
+
|
|
414
|
+
<!-- FORGE:COORD_ISSUE -->
|
|
415
|
+
<!-- FORGE:BATCH_ID: ${BATCH_ID} -->"
|
|
416
|
+
|
|
417
|
+
COORD_ISSUE_URL=$(gh issue create -R {GH_REPO} \
|
|
418
|
+
--title "orchestrate: claims board for batch ${BATCH_ID}" \
|
|
419
|
+
--body "$COORD_ISSUE_BODY" \
|
|
420
|
+
--label "automation" 2>/dev/null || echo "")
|
|
421
|
+
|
|
422
|
+
if [ -z "$COORD_ISSUE_URL" ]; then
|
|
423
|
+
echo "WARNING: failed to create coordination issue — claims board disabled for this batch. Layer-2/4 relaxation will not run."
|
|
424
|
+
FORGE_COORD_ISSUE=""
|
|
425
|
+
else
|
|
426
|
+
COORD_ISSUE_NUMBER=$(echo "$COORD_ISSUE_URL" | grep -oE '[0-9]+$')
|
|
427
|
+
FORGE_COORD_ISSUE="$COORD_ISSUE_URL"
|
|
428
|
+
echo "Coordination issue created: ${COORD_ISSUE_URL} (#${COORD_ISSUE_NUMBER})"
|
|
429
|
+
export FORGE_COORD_ISSUE
|
|
430
|
+
export COORD_ISSUE_NUMBER
|
|
431
|
+
fi
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Idempotency**: If `FORGE_COORD_ISSUE` is already set in the environment (e.g., after a compaction / orchestrator restart), skip creation and use the existing URL. The coordination issue persists for the lifetime of the batch.
|
|
248
435
|
|
|
249
436
|
**Terminology:**
|
|
250
437
|
- **Ready issues**: Issues whose predecessor set is empty (all predecessors have reached terminal state or were never added)
|
|
@@ -370,9 +557,263 @@ fi
|
|
|
370
557
|
- `EXCLUDED_CYCLE[]` contains cyclic issue numbers — reported in Step 3E, never dispatched
|
|
371
558
|
- If `EXCLUDED_CYCLE` is non-empty, report it clearly in the Step 3E plan before asking for user confirmation
|
|
372
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.
|
|
595
|
+
|
|
596
|
+
### Step 3E.5: Value/Cost Scoring Pass (MANDATORY) <!-- Added: forge#1743 -->
|
|
597
|
+
|
|
598
|
+
**Run immediately after Step 3D.5, before presenting the plan (Step 3E).** This step scores every issue in `ISSUES[]` by its expected value/cost ratio and re-orders the ready-set (issues with an empty predecessors set) in descending value/cost order. Dependency constraints are **never overridden** — this is a reordering pass within the existing ready-set only, not an edge-insertion pass. No new edges are added; cycle detection (Step 3D.5) has already completed.
|
|
599
|
+
|
|
600
|
+
**Purpose**: Ensure that when a budget is finite (see `--budget` in Phase 4), the highest-value-per-token work dispatches first. When no budget is set (the default, uncapped behavior), dispatch order still reflects value/cost — useful for observability even without a hard cap.
|
|
601
|
+
|
|
602
|
+
#### Value function (transparent heuristic — deferral decisions must be explainable)
|
|
603
|
+
|
|
604
|
+
```
|
|
605
|
+
value(issue) = priority_weight × danger_zone_weight
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
**Priority weight** (from issue labels):
|
|
609
|
+
|
|
610
|
+
| Label | Weight |
|
|
611
|
+
|-------|--------|
|
|
612
|
+
| `priority:P0` | 4.0 |
|
|
613
|
+
| `priority:P1` | 3.0 |
|
|
614
|
+
| `priority:P2` | 2.0 |
|
|
615
|
+
| `priority:P3` | 1.0 |
|
|
616
|
+
| *(no priority label)* | 1.5 |
|
|
617
|
+
|
|
618
|
+
**Danger-zone weight** (from affected files via FORGE:INVESTIGATOR comment): Read the `### Affected Files` section and check each file path against the danger-zone list from `forge.yaml → review.danger_zones[]`. Each affected file that appears in a danger zone adds 0.5 to the weight (additive, capped at 2.0). Default (no matches): 1.0.
|
|
619
|
+
|
|
620
|
+
```
|
|
621
|
+
danger_zone_weight = min(2.0, 1.0 + 0.5 × count_of_danger_zone_files_affected)
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
If `forge.yaml → review.danger_zones` is absent: danger_zone_weight = 1.0 for all issues.
|
|
625
|
+
|
|
626
|
+
#### Cost function (fallback hierarchy)
|
|
627
|
+
|
|
628
|
+
```
|
|
629
|
+
cost_estimate(issue) → expected_spend_usd
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
Resolve in this order (use the first that produces a non-null result):
|
|
633
|
+
|
|
634
|
+
1. **Cost-prior lookup** (primary): Read `~/.forge/index/cost-priors.json`. Compute key = `task_type:module` where:
|
|
635
|
+
- `task_type` = FORGE:INVESTIGATOR `**Task Type**` field (lower-cased, spaces→hyphens). If no investigator comment: infer from issue labels (`feature` → `feature`, `bug` → `bug-fix`, else `unknown`).
|
|
636
|
+
- `module` = basename (no ext, lowercase) of the primary affected file from FORGE:INVESTIGATOR. If absent: `_unknown`.
|
|
637
|
+
- If the key exists in cost-priors.json: use `priors[key].mean`. Mark the issue as `has_prior: true` for exploration-reserve logic below.
|
|
638
|
+
|
|
639
|
+
2. **Label heuristic fallback** (when cost-priors.json absent or key not found):
|
|
640
|
+
```
|
|
641
|
+
bug/fix: $0.20 · feature: $0.40 · refactor: $0.30 · investigation: $0.50 · unknown: $0.35
|
|
642
|
+
```
|
|
643
|
+
Mark the issue as `has_prior: false`.
|
|
644
|
+
|
|
645
|
+
3. **File-count proxy** (last resort — no labels and no prior):
|
|
646
|
+
```
|
|
647
|
+
estimated_cost = 0.10 + 0.05 × count_of_affected_files
|
|
648
|
+
```
|
|
649
|
+
Mark the issue as `has_prior: false`.
|
|
650
|
+
|
|
651
|
+
#### Scoring and sorting
|
|
652
|
+
|
|
653
|
+
```bash
|
|
654
|
+
# --- Step 3E.5: Value/Cost Scoring Pass ---
|
|
655
|
+
# Requires: ISSUES[] (post-cycle-detection), PREDECESSORS[], GH_REPO
|
|
656
|
+
# Outputs: ISSUE_SCORE[], ISSUE_COST_ESTIMATE[], ISSUE_HAS_PRIOR[], SORTED_READY_SET[]
|
|
657
|
+
|
|
658
|
+
declare -A ISSUE_SCORE # issue → value/cost ratio (float)
|
|
659
|
+
declare -A ISSUE_COST_ESTIMATE # issue → estimated cost (USD float string)
|
|
660
|
+
declare -A ISSUE_HAS_PRIOR # issue → true|false
|
|
661
|
+
declare -A ISSUE_VALUE # issue → value weight (float)
|
|
662
|
+
|
|
663
|
+
COST_PRIORS_PATH="${HOME}/.forge/index/cost-priors.json"
|
|
664
|
+
|
|
665
|
+
for NUM in "${ISSUES[@]}"; do
|
|
666
|
+
# 1. Fetch issue data for scoring (labels, investigator comment)
|
|
667
|
+
ISSUE_DATA=$(gh issue view "$NUM" -R {GH_REPO} --json labels,body \
|
|
668
|
+
--jq '{labels: [.labels[].name]}' 2>/dev/null || echo '{"labels":[]}')
|
|
669
|
+
|
|
670
|
+
LABELS=$(echo "$ISSUE_DATA" | jq -r '.labels[]' 2>/dev/null || echo '')
|
|
671
|
+
|
|
672
|
+
# --- Value: priority weight ---
|
|
673
|
+
if echo "$LABELS" | grep -q "priority:P0"; then
|
|
674
|
+
PRIO_WEIGHT=4.0
|
|
675
|
+
elif echo "$LABELS" | grep -q "priority:P1"; then
|
|
676
|
+
PRIO_WEIGHT=3.0
|
|
677
|
+
elif echo "$LABELS" | grep -q "priority:P2"; then
|
|
678
|
+
PRIO_WEIGHT=2.0
|
|
679
|
+
elif echo "$LABELS" | grep -q "priority:P3"; then
|
|
680
|
+
PRIO_WEIGHT=1.0
|
|
681
|
+
else
|
|
682
|
+
PRIO_WEIGHT=1.5
|
|
683
|
+
fi
|
|
684
|
+
|
|
685
|
+
# --- Value: danger-zone weight ---
|
|
686
|
+
DANGER_WEIGHT=1.0
|
|
687
|
+
DANGER_ZONES=$(yq '.review.danger_zones[]? // ""' forge.yaml 2>/dev/null || echo '')
|
|
688
|
+
if [ -n "$DANGER_ZONES" ]; then
|
|
689
|
+
# Fetch affected files from INVESTIGATOR comment
|
|
690
|
+
AFFECTED=$(gh api repos/{GH_REPO}/issues/${NUM}/comments \
|
|
691
|
+
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body' 2>/dev/null \
|
|
692
|
+
| grep -oP '`[^`]+\.(py|mjs|ts|md|sh|yaml|yml)`' | tr -d '`' | head -20 || echo '')
|
|
693
|
+
ZONE_HIT_COUNT=0
|
|
694
|
+
while IFS= read -r dz; do
|
|
695
|
+
[ -z "$dz" ] && continue
|
|
696
|
+
if echo "$AFFECTED" | grep -q "$dz"; then
|
|
697
|
+
ZONE_HIT_COUNT=$((ZONE_HIT_COUNT + 1))
|
|
698
|
+
fi
|
|
699
|
+
done <<< "$DANGER_ZONES"
|
|
700
|
+
DZ_ADD=$(echo "scale=1; if ($ZONE_HIT_COUNT * 0.5 > 1.0) 1.0 else $ZONE_HIT_COUNT * 0.5" | bc 2>/dev/null || echo "0")
|
|
701
|
+
DANGER_WEIGHT=$(echo "scale=1; 1.0 + $DZ_ADD" | bc 2>/dev/null || echo "1.0")
|
|
702
|
+
fi
|
|
703
|
+
|
|
704
|
+
VALUE=$(echo "scale=4; $PRIO_WEIGHT * $DANGER_WEIGHT" | bc 2>/dev/null || echo "$PRIO_WEIGHT")
|
|
705
|
+
ISSUE_VALUE[$NUM]="$VALUE"
|
|
706
|
+
|
|
707
|
+
# --- Cost: prior lookup → label heuristic → file-count proxy ---
|
|
708
|
+
COST=""
|
|
709
|
+
HAS_PRIOR="false"
|
|
710
|
+
|
|
711
|
+
if [ -f "$COST_PRIORS_PATH" ]; then
|
|
712
|
+
# Derive task_type:module key
|
|
713
|
+
TASK_TYPE=$(gh api repos/{GH_REPO}/issues/${NUM}/comments \
|
|
714
|
+
--jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body] | last // ""' 2>/dev/null \
|
|
715
|
+
| grep -oP '(?<=\*\*Task Type\*\*: )\S+' | head -1 | tr '[:upper:]' '[:lower:]' \
|
|
716
|
+
| tr ' ' '-' || echo '')
|
|
717
|
+
[ -z "$TASK_TYPE" ] && {
|
|
718
|
+
if echo "$LABELS" | grep -q "^feature$"; then TASK_TYPE="feature"
|
|
719
|
+
elif echo "$LABELS" | grep -q "^bug$"; then TASK_TYPE="bug-fix"
|
|
720
|
+
else TASK_TYPE="unknown"; fi
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
PRIMARY_FILE=$(gh api repos/{GH_REPO}/issues/${NUM}/comments \
|
|
724
|
+
--jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body] | last // ""' 2>/dev/null \
|
|
725
|
+
| grep -oP '`[^`]+\.(py|mjs|ts|md|sh|yaml|yml)`' | tr -d '`' | head -1 || echo '')
|
|
726
|
+
MODULE=$(basename "${PRIMARY_FILE:-_unknown}" | sed 's/\.[^.]*$//' | tr '[:upper:]' '[:lower:]')
|
|
727
|
+
[ -z "$MODULE" ] && MODULE="_unknown"
|
|
728
|
+
|
|
729
|
+
PRIOR_KEY="${TASK_TYPE}:${MODULE}"
|
|
730
|
+
COST=$(jq -r --arg k "$PRIOR_KEY" '.priors[$k].mean // empty' "$COST_PRIORS_PATH" 2>/dev/null || echo '')
|
|
731
|
+
[ -n "$COST" ] && HAS_PRIOR="true"
|
|
732
|
+
fi
|
|
733
|
+
|
|
734
|
+
if [ -z "$COST" ]; then
|
|
735
|
+
# Label heuristic fallback
|
|
736
|
+
if echo "$LABELS" | grep -q "^feature$"; then COST="0.40"
|
|
737
|
+
elif echo "$LABELS" | grep -q "^bug$"; then COST="0.20"
|
|
738
|
+
elif echo "$LABELS" | grep -q "^refactor$"; then COST="0.30"
|
|
739
|
+
else COST="0.35"; fi
|
|
740
|
+
fi
|
|
741
|
+
|
|
742
|
+
# Score = value / cost (protected against divide-by-zero)
|
|
743
|
+
SCORE=$(echo "scale=4; if ($COST > 0) $VALUE / $COST else $VALUE / 0.01" | bc 2>/dev/null || echo "1.0")
|
|
744
|
+
|
|
745
|
+
ISSUE_SCORE[$NUM]="$SCORE"
|
|
746
|
+
ISSUE_COST_ESTIMATE[$NUM]="$COST"
|
|
747
|
+
ISSUE_HAS_PRIOR[$NUM]="$HAS_PRIOR"
|
|
748
|
+
|
|
749
|
+
echo "Score: #${NUM} value=${VALUE} cost_est=\$${COST} (prior=${HAS_PRIOR}) score=${SCORE}"
|
|
750
|
+
done
|
|
751
|
+
|
|
752
|
+
# --- ε-exploration reserve ---
|
|
753
|
+
# ε = 10% of budget allocated to no-prior issues (high-variance unknowns).
|
|
754
|
+
# A no-prior issue is guaranteed a dispatch slot within the ε reserve even if
|
|
755
|
+
# its score would otherwise place it below the budget cutoff. This prevents
|
|
756
|
+
# discovery starvation on novel modules with no cost history.
|
|
757
|
+
#
|
|
758
|
+
# Implementation: when --budget is set in Phase 4, the dispatch loop reserves
|
|
759
|
+
# EPSILON_BUDGET = 0.10 × BUDGET_LIMIT for issues where ISSUE_HAS_PRIOR[N] == "false".
|
|
760
|
+
# This step only MARKS the no-prior issues; Phase 4 reads ISSUE_HAS_PRIOR[] to apply the reserve.
|
|
761
|
+
#
|
|
762
|
+
# No-prior issues still compete in the main dispatch queue by score. The reserve
|
|
763
|
+
# acts as a safety net: if no no-prior issue has been dispatched by the time
|
|
764
|
+
# PROJECTED_SPEND reaches (BUDGET_LIMIT − EPSILON_BUDGET), the highest-scoring
|
|
765
|
+
# no-prior issue is force-dispatched from the reserve before budget cutoff.
|
|
766
|
+
|
|
767
|
+
NO_PRIOR_ISSUES=()
|
|
768
|
+
for NUM in "${ISSUES[@]}"; do
|
|
769
|
+
[ "${ISSUE_HAS_PRIOR[$NUM]:-false}" = "false" ] && NO_PRIOR_ISSUES+=("$NUM")
|
|
770
|
+
done
|
|
771
|
+
echo "Exploration reserve: ${#NO_PRIOR_ISSUES[@]} no-prior issues (ε=10% of budget reserved for these)"
|
|
772
|
+
|
|
773
|
+
# --- Sort the ready-set by descending score ---
|
|
774
|
+
# The ready-set is the subset of ISSUES with empty PREDECESSORS[].
|
|
775
|
+
# Dependency-constrained issues (non-empty PREDECESSORS[]) keep their original DAG ordering —
|
|
776
|
+
# their dispatch is triggered by predecessor completion, not by score rank.
|
|
777
|
+
# SORTED_READY_SET is consumed by Step 3E (plan) and Phase 4 (dispatch order).
|
|
778
|
+
|
|
779
|
+
READY_SET=()
|
|
780
|
+
for NUM in "${ISSUES[@]}"; do
|
|
781
|
+
[ -z "${PREDECESSORS[$NUM]:-}" ] && READY_SET+=("$NUM")
|
|
782
|
+
done
|
|
783
|
+
|
|
784
|
+
# Sort by score descending (bc-based comparison via temporary file)
|
|
785
|
+
SCORE_PAIRS=""
|
|
786
|
+
for NUM in "${READY_SET[@]}"; do
|
|
787
|
+
SCORE_PAIRS="${SCORE_PAIRS}${ISSUE_SCORE[$NUM]:-0} $NUM"$'\n'
|
|
788
|
+
done
|
|
789
|
+
SORTED_READY_SET=()
|
|
790
|
+
while IFS=' ' read -r _score num; do
|
|
791
|
+
[ -n "$num" ] && SORTED_READY_SET+=("$num")
|
|
792
|
+
done < <(echo "$SCORE_PAIRS" | sort -rn -k1,1 | grep -v '^$')
|
|
793
|
+
|
|
794
|
+
echo ""
|
|
795
|
+
echo "Ready-set dispatch order (descending value/cost):"
|
|
796
|
+
for NUM in "${SORTED_READY_SET[@]}"; do
|
|
797
|
+
PRIOR_TAG="${ISSUE_HAS_PRIOR[$NUM]:-false}"
|
|
798
|
+
[ "$PRIOR_TAG" = "false" ] && PRIOR_NOTE=" [ε-reserve eligible]" || PRIOR_NOTE=" [prior known]"
|
|
799
|
+
echo " #${NUM} score=${ISSUE_SCORE[$NUM]:-?} cost_est=\$${ISSUE_COST_ESTIMATE[$NUM]:-?}${PRIOR_NOTE}"
|
|
800
|
+
done
|
|
801
|
+
# --- End Step 3E.5 ---
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
**Output state** (consumed by Step 3E and Phase 4):
|
|
805
|
+
- `ISSUE_SCORE[N]` — value/cost ratio for each issue
|
|
806
|
+
- `ISSUE_COST_ESTIMATE[N]` — estimated spend in USD
|
|
807
|
+
- `ISSUE_HAS_PRIOR[N]` — `true` if cost-priors.json had an entry; `false` = no-prior (ε-reserve eligible)
|
|
808
|
+
- `SORTED_READY_SET[]` — ready issues sorted by descending score (used in Step 3E plan table and Phase 4 dispatch loop)
|
|
809
|
+
- `NO_PRIOR_ISSUES[]` — issues with no cost history (a subset of `SORTED_READY_SET` + blocked issues)
|
|
810
|
+
|
|
811
|
+
**Important**: `ISSUE_SCORE[]` and `ISSUE_COST_ESTIMATE[]` are also set for blocked issues (for reporting). The sorted order only affects the ready-set — blocked issues dispatch when their predecessors complete, regardless of score.
|
|
373
812
|
|
|
374
813
|
### Step 3E: Present the plan to the user
|
|
375
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
|
+
|
|
376
817
|
```
|
|
377
818
|
## Orchestration Plan
|
|
378
819
|
|
|
@@ -404,17 +845,22 @@ fi
|
|
|
404
845
|
|
|
405
846
|
### Dependency Graph
|
|
406
847
|
|
|
407
|
-
| Issue | Predecessors | Domain | Status |
|
|
408
|
-
|
|
409
|
-
| #{A} | — | FRONTEND | Ready (dispatches
|
|
410
|
-
| #{B} | — | BILLING | Ready (dispatches
|
|
411
|
-
| #{C} | — | WORKER | Ready (dispatches
|
|
412
|
-
| #{D} | #{A} | FRONTEND | Blocked (waits for #{A} only) |
|
|
413
|
-
| #{E} | — | DATABASE | Ready (dispatches
|
|
414
|
-
| #{F} | #{E} | DATABASE | Blocked (serialized — waits for #{E}) |
|
|
848
|
+
| Issue | Predecessors | Domain | Score | Est. Cost | Status |
|
|
849
|
+
|-------|-------------|--------|-------|-----------|--------|
|
|
850
|
+
| #{A} | — | FRONTEND | {score} | ${cost} | Ready (dispatches 1st by score) |
|
|
851
|
+
| #{B} | — | BILLING | {score} | ${cost} | Ready (dispatches 2nd by score) |
|
|
852
|
+
| #{C} | — | WORKER | {score} | ${cost} [ε] | Ready (dispatches 3rd — ε-reserve) |
|
|
853
|
+
| #{D} | #{A} | FRONTEND | {score} | ${cost} | Blocked (waits for #{A} only) |
|
|
854
|
+
| #{E} | — | DATABASE | {score} | ${cost} | Ready (dispatches 4th by score) |
|
|
855
|
+
| #{F} | #{E} | DATABASE | {score} | ${cost} | Blocked (serialized — waits for #{E}) |
|
|
856
|
+
|
|
857
|
+
**[ε]** = no cost prior; eligible for exploration reserve (10% of budget guaranteed for these)
|
|
858
|
+
|
|
859
|
+
**Score** = value / estimated_cost (value = priority_weight × danger_zone_weight; higher = dispatches first within the ready-set)
|
|
860
|
+
**Est. Cost** = cost-prior mean for (task_type × module), or label heuristic if no prior
|
|
415
861
|
|
|
416
862
|
**Critical path**: #{E} → #{F} (2 steps, determines minimum wall-clock time)
|
|
417
|
-
**Initial dispatch**: #{A}, #{B}, #{C}, #{E} (all predecessors resolved)
|
|
863
|
+
**Initial dispatch**: #{A}, #{B}, #{C}, #{E} (all predecessors resolved — ordered by score)
|
|
418
864
|
**Streaming**: #{D} dispatches as soon as #{A} completes — does NOT wait for #{B}, #{C}, or #{E}
|
|
419
865
|
|
|
420
866
|
**Note**: Investigations may create additional issues that will be automatically added to the dependency graph. The final graph will be confirmed after investigations complete.
|
|
@@ -477,10 +923,11 @@ On completion or failure:
|
|
|
477
923
|
scripts/worktree-lifecycle.sh cleanup <issue-number>
|
|
478
924
|
```
|
|
479
925
|
|
|
480
|
-
**Concurrency cap** (`forge.yaml → orchestration.max_concurrent`):
|
|
481
|
-
- Default:
|
|
482
|
-
- 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.
|
|
483
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.
|
|
484
931
|
|
|
485
932
|
**Rate-limit backpressure** (pre-dispatch gate):
|
|
486
933
|
|
|
@@ -504,7 +951,7 @@ fi
|
|
|
504
951
|
**Configuration reference** (`forge.yaml`):
|
|
505
952
|
```yaml
|
|
506
953
|
orchestration:
|
|
507
|
-
max_concurrent: 8 # optional; default:
|
|
954
|
+
max_concurrent: 8 # optional; default: 12
|
|
508
955
|
rate_limit_floor: 200 # optional; default: 200
|
|
509
956
|
```
|
|
510
957
|
|
|
@@ -557,39 +1004,131 @@ The orchestrator context window must stay small regardless of how many issues ha
|
|
|
557
1004
|
|
|
558
1005
|
**Contract**: After any compaction event or orchestrator wake (session resumed after idle/restart), do NOT rely on in-context variables. Instead, reconstruct the DAG dispatch state from GitHub before checking for newly ready issues:
|
|
559
1006
|
|
|
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."
|
|
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
|
+
|
|
560
1011
|
```bash
|
|
561
1012
|
# Reconstruct dispatch state from GitHub after compaction / wake
|
|
562
1013
|
# Run this block at the top of every resumed Phase 4 loop iteration.
|
|
563
1014
|
|
|
564
|
-
# 1. Re-fetch all issue labels
|
|
1015
|
+
# 1. Re-fetch all issue labels and classify each into DONE / GATED / FAILED / IN_PROGRESS
|
|
1016
|
+
# (same classify_predecessor_state() function defined in phase-4-execution.md Step 4B —
|
|
1017
|
+
# re-declare it here if this block runs in a fresh context that hasn't sourced Step 4B yet).
|
|
1018
|
+
declare -A ISSUE_CLASS
|
|
1019
|
+
DONE_ISSUES=()
|
|
1020
|
+
GATED_ISSUES=()
|
|
1021
|
+
FAILED_ISSUES=()
|
|
1022
|
+
ACTIVE_ISSUES=() # IN_PROGRESS — still mid-pipeline, not yet terminal-for-this-agent
|
|
1023
|
+
|
|
565
1024
|
for NUM in {all_issue_numbers_in_batch}; do
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
fi
|
|
1025
|
+
CLASS=$(classify_predecessor_state "$NUM")
|
|
1026
|
+
ISSUE_CLASS["$NUM"]="$CLASS"
|
|
1027
|
+
case "$CLASS" in
|
|
1028
|
+
DONE) DONE_ISSUES+=("$NUM") ;;
|
|
1029
|
+
GATED) GATED_ISSUES+=("$NUM") ;;
|
|
1030
|
+
FAILED) FAILED_ISSUES+=("$NUM") ;;
|
|
1031
|
+
*) ACTIVE_ISSUES+=("$NUM") ;;
|
|
1032
|
+
esac
|
|
575
1033
|
done
|
|
576
1034
|
|
|
577
|
-
# 2. Re-derive the ready set: any non-terminal issue whose predecessors are
|
|
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).
|
|
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.
|
|
1048
|
+
READY_ISSUES=()
|
|
1049
|
+
NEWLY_BLOCKED=() # dependents whose gating predecessor is GATED with a still-live edge — need blocked-on-human-merge tracking
|
|
578
1050
|
for NUM in "${ACTIVE_ISSUES[@]}"; do
|
|
579
1051
|
ALL_PREDS_DONE=true
|
|
1052
|
+
GATING_PRED=""
|
|
580
1053
|
for PRED in {predecessors_of_NUM}; do
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
1054
|
+
case "${ISSUE_CLASS[$PRED]:-IN_PROGRESS}" in
|
|
1055
|
+
DONE) ;;
|
|
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
|
+
;;
|
|
1069
|
+
*) ALL_PREDS_DONE=false ;;
|
|
1070
|
+
esac
|
|
585
1071
|
done
|
|
586
|
-
$ALL_PREDS_DONE
|
|
1072
|
+
if [ "$ALL_PREDS_DONE" = "true" ]; then
|
|
1073
|
+
READY_ISSUES+=("$NUM")
|
|
1074
|
+
elif [ -n "$GATING_PRED" ]; then
|
|
1075
|
+
NEWLY_BLOCKED+=("$NUM|$GATING_PRED")
|
|
1076
|
+
fi
|
|
1077
|
+
done
|
|
1078
|
+
|
|
1079
|
+
# 2.5. Track newly-blocked dependents (mirrors phase-4-execution.md Step 4B item 6.5)
|
|
1080
|
+
# Self-heal the label if not yet bootstrapped (same pattern as review-pr.md 6C / phase-4-execution.md item 6.5).
|
|
1081
|
+
gh label create "blocked-on-human-merge" --color "006B75" --description "Dependent of a gated (needs-human/awaiting-merge) predecessor. Managed by ForgeDock." --force -R {GH_REPO} 2>/dev/null
|
|
1082
|
+
for ENTRY in "${NEWLY_BLOCKED[@]:-}"; do
|
|
1083
|
+
[ -z "$ENTRY" ] && continue
|
|
1084
|
+
DEP="${ENTRY%%|*}"
|
|
1085
|
+
PRED="${ENTRY##*|}"
|
|
1086
|
+
# Anchor on the exact "**Gating predecessor**: #N" label with a word boundary —
|
|
1087
|
+
# a bare contains("#N") substring would false-match #50/#500 for predecessor #5. <!-- forge#1830 -->
|
|
1088
|
+
ALREADY_TRACKED=$(gh api repos/{GH_REPO}/issues/${DEP}/comments \
|
|
1089
|
+
--jq --arg prednum "${PRED}" '[.[] | select(.body | contains("FORGE:BLOCKED_ON_HUMAN_MERGE") and test("Gating predecessor\\*\\*: #" + $prednum + "\\b"))] | length' 2>/dev/null || echo "0")
|
|
1090
|
+
if [ "$ALREADY_TRACKED" -eq 0 ]; then
|
|
1091
|
+
GATING_PR=$(gh pr list -R {GH_REPO} --state open --search "\"Closes #${PRED}\" in:body" \
|
|
1092
|
+
--json number --jq '.[0].number // empty' 2>/dev/null || echo "")
|
|
1093
|
+
gh issue comment "$DEP" -R {GH_REPO} --body "<!-- FORGE:BLOCKED_ON_HUMAN_MERGE -->
|
|
1094
|
+
**Gating predecessor**: #${PRED} (state: \`${ISSUE_CLASS[$PRED]}\`${GATING_PR:+, open PR #${GATING_PR}})
|
|
1095
|
+
**Status**: Detected on orchestrator wake/compaction reconstruction. Ready to dispatch as soon as #${PRED} reaches \`workflow:merged\`."
|
|
1096
|
+
gh issue edit "$DEP" -R {GH_REPO} --add-label "blocked-on-human-merge" 2>/dev/null || true
|
|
1097
|
+
fi
|
|
1098
|
+
done
|
|
1099
|
+
|
|
1100
|
+
# 3. Merge-triggered wake: any issue tracked as blocked-on-human-merge whose gating predecessor
|
|
1101
|
+
# is now DONE gets un-blocked and added to the ready set. This is the wake-time equivalent of
|
|
1102
|
+
# phase-4-execution.md Step 4B item 6.6 — it is what makes "auto-dispatch on merge, no manual
|
|
1103
|
+
# /orchestrate re-run" hold true even when the merge happened after the session ended.
|
|
1104
|
+
BLOCKED_NOW=$(gh issue list -R {GH_REPO} --state open --label "blocked-on-human-merge" --json number \
|
|
1105
|
+
--jq '.[].number' 2>/dev/null || echo "")
|
|
1106
|
+
for DEP in $BLOCKED_NOW; do
|
|
1107
|
+
# Read which predecessor(s) this DEP is tracked against
|
|
1108
|
+
GATING_PREDS_RAW=$(gh api repos/{GH_REPO}/issues/${DEP}/comments \
|
|
1109
|
+
--jq '[.[] | select(.body | contains("FORGE:BLOCKED_ON_HUMAN_MERGE")) | (.body | capture("Gating predecessor\\*\\*: #(?<p>[0-9]+)").p)]' 2>/dev/null || echo '[]')
|
|
1110
|
+
STILL_GATED=false
|
|
1111
|
+
for GPRED in $(echo "$GATING_PREDS_RAW" | jq -r '.[]' 2>/dev/null); do
|
|
1112
|
+
GPRED_CLASS=$(classify_predecessor_state "$GPRED")
|
|
1113
|
+
[ "$GPRED_CLASS" != "DONE" ] && STILL_GATED=true
|
|
1114
|
+
done
|
|
1115
|
+
if [ "$STILL_GATED" = "false" ]; then
|
|
1116
|
+
gh issue edit "$DEP" -R {GH_REPO} --remove-label "blocked-on-human-merge" 2>/dev/null || true
|
|
1117
|
+
gh issue comment "$DEP" -R {GH_REPO} --body "<!-- FORGE:UNBLOCKED -->
|
|
1118
|
+
All gating predecessor(s) reached \`workflow:merged\` (detected on orchestrator wake) — dispatching now."
|
|
1119
|
+
READY_ISSUES+=("$DEP")
|
|
1120
|
+
fi
|
|
587
1121
|
done
|
|
588
1122
|
|
|
589
|
-
#
|
|
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.
|
|
590
1129
|
```
|
|
591
1130
|
|
|
592
|
-
**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 is fully on GitHub. The reconstruction above re-derives the ready set from labels alone, so the orchestrator context never needs to hold cumulative dispatch history.
|
|
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.
|
|
593
1132
|
|
|
594
1133
|
---
|
|
595
1134
|
|