forgedock 1.0.20 → 1.0.22
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 +146 -77
- package/bin/demo.mjs +386 -0
- package/bin/forgedock.mjs +357 -30
- package/bin/init-detect.mjs +95 -0
- package/bin/runner.mjs +780 -0
- package/commands/ci-audit.md +469 -0
- package/commands/cleanup.md +85 -39
- package/commands/deploy-info.md +6 -2
- package/commands/forge-stats.md +1 -1
- package/commands/optimize.md +1 -1
- package/commands/orchestrate.md +684 -33
- package/commands/quality-gate.md +166 -46
- package/commands/review-pr-agents.md +106 -34
- package/commands/review-pr.md +33 -2
- package/commands/scope.md +11 -3
- package/commands/security-audit.md +24 -16
- package/commands/sync-ecosystem.md +2 -2
- package/commands/work-on/build/architect.md +47 -3
- package/commands/work-on/build/context.md +36 -5
- package/commands/work-on/build/validate.md +16 -7
- package/commands/work-on/close.md +129 -1
- package/commands/work-on/investigate.md +24 -1
- package/commands/work-on.md +226 -8
- package/docs/comparison.md +135 -0
- package/docs/design/developer-objections-ai-context-tools.md +244 -0
- package/docs/design/messaging-angles-research.md +179 -0
- package/docs/design/show-hn-draft.md +86 -0
- package/docs/site/annotations-explained.md +55 -0
- package/docs/site/command-learning-path.md +104 -0
- package/docs/site/command-reference.md +3 -0
- package/docs/site/getting-started.md +36 -14
- package/docs/site/troubleshooting.md +208 -0
- package/docs/spec/forge-protocol-v1.md +768 -0
- package/examples/forgedock-demo/README.md +113 -0
- package/examples/forgedock-demo/bootstrap.sh +125 -0
- package/examples/forgedock-demo/issues/01-bug-delete-missing-auth.md +37 -0
- package/examples/forgedock-demo/issues/02-feature-filter-notes.md +37 -0
- package/examples/forgedock-demo/issues/03-refactor-router-table.md +33 -0
- package/examples/forgedock-demo/issues/04-perf-index-lookup.md +35 -0
- package/examples/forgedock-demo/issues/05-docs-api-reference.md +39 -0
- package/examples/forgedock-demo/labels.json +21 -0
- package/examples/forgedock-demo/package.json +16 -0
- package/examples/forgedock-demo/scripts/smoke.js +61 -0
- package/examples/forgedock-demo/src/auth.js +28 -0
- package/examples/forgedock-demo/src/db.js +66 -0
- package/examples/forgedock-demo/src/routes/notes.js +64 -0
- package/examples/forgedock-demo/src/server.js +79 -0
- package/package.json +6 -2
- package/templates/forge.yaml.minimal +44 -0
package/commands/orchestrate.md
CHANGED
|
@@ -255,6 +255,154 @@ Proceeding to dependency analysis with the expanded issue set...
|
|
|
255
255
|
|
|
256
256
|
---
|
|
257
257
|
|
|
258
|
+
## Phase 2.5: Investigation Synthesis
|
|
259
|
+
|
|
260
|
+
<!-- Added: forge#1192 -->
|
|
261
|
+
|
|
262
|
+
**Purpose**: Reconcile *competing recommendations* across investigation outputs BEFORE they fan out to implementation agents. Phase 3's conflict detection (Step 3C) deconflicts issues at the **file** layer only — it prevents git merge conflicts. It performs **zero semantic deconfliction**: two issues can touch entirely different files while proposing **contradictory approaches to the same problem**, and the file-overlap detector passes both straight to parallel dispatch. This phase closes that gap by clustering investigations semantically (by target subsystem, not by file) and arbitrating incompatible plans into a single decision — or serializing them so the second agent inherits the first's decision.
|
|
263
|
+
|
|
264
|
+
**This phase operates ONLY on FORGE annotations and issue bodies. It NEVER reads code, and it NEVER closes, skips, or merges issues.** Reconciling plans/annotations is distinct from adjudicating *duplicate validity* (Safety Rule 9, the #3842/#4039 scar): the anti-dedup rule forbids the orchestrator from deciding two issues are the same bug and closing one — that call belongs to `/work-on` investigation agents examining actual code. Plan reconciliation touches neither code nor issue state; it only writes a synthesis brief annotation and adds `Depends on #X` serialization edges. It therefore does not violate Hard Rule 2 (dispatcher, not builder) or Safety Rule 9.
|
|
265
|
+
|
|
266
|
+
**No-op guard**: This entire phase is skipped when the batch contains **0 or 1 investigations** — there is nothing to reconcile against. Proceed directly to Phase 3.
|
|
267
|
+
|
|
268
|
+
### Step 2.5A: No-op guard
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
# Count investigations that completed in Wave 0 (Phase 2), regardless of whether each
|
|
272
|
+
# emitted a Knowledge Gist. Use the full completed-investigation set (the same
|
|
273
|
+
# {investigation_numbers} that Steps 2C.5, 2.5B and 2.5C iterate) — NOT the
|
|
274
|
+
# INVESTIGATION_GISTS map, which only holds gist-producing investigations and would
|
|
275
|
+
# undercount when a genuine investigation reached a recommendation without a gist.
|
|
276
|
+
# If < 2, skip synthesis entirely.
|
|
277
|
+
INVESTIGATION_NUMS=( {investigation_numbers} )
|
|
278
|
+
INVESTIGATION_COUNT=${#INVESTIGATION_NUMS[@]}
|
|
279
|
+
if [ "$INVESTIGATION_COUNT" -lt 2 ]; then
|
|
280
|
+
echo "Phase 2.5 skipped: ${INVESTIGATION_COUNT} investigation(s) in batch — nothing to reconcile. Proceeding to Phase 3."
|
|
281
|
+
SYNTHESIS_RAN=false
|
|
282
|
+
RECONCILED_COUNT=0
|
|
283
|
+
# Skip to Phase 3.
|
|
284
|
+
else
|
|
285
|
+
SYNTHESIS_RAN=true
|
|
286
|
+
fi
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
If `SYNTHESIS_RAN` is false, do NOT execute Steps 2.5B–2.5D — proceed directly to Phase 3. Step 4A's `{GIST_CONTEXT}` generation will fall back to the existing raw-gist behavior (no synthesis brief exists).
|
|
290
|
+
|
|
291
|
+
### Step 2.5B: Cluster investigations by target subsystem
|
|
292
|
+
|
|
293
|
+
For each investigation that completed in Wave 0, read its `FORGE:INVESTIGATOR` comment (and the newly spawned implementation issue bodies from Step 2D) and extract its **Recommendation** and **Affected Files / target subsystem** — NOT to compare files for merge conflicts, but to group investigations that operate on the **same conceptual surface** (e.g. "auth session lifecycle", "credit metering", "orchestrate DAG construction").
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
# For each investigation, pull its recommendation + affected-files block (annotations only — no code reads)
|
|
297
|
+
declare -A INV_RECOMMENDATION
|
|
298
|
+
declare -A INV_SUBSYSTEM
|
|
299
|
+
for INV_NUM in {investigation_numbers}; do
|
|
300
|
+
INV_BODY=$(gh api repos/{GH_REPO}/issues/${INV_NUM}/comments \
|
|
301
|
+
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body' 2>/dev/null | head -1)
|
|
302
|
+
# Extract the Recommendation section (annotation prose only)
|
|
303
|
+
INV_RECOMMENDATION[$INV_NUM]=$(echo "$INV_BODY" | awk '/^### Recommendation/{p=1;next}/^### /{p=0}p')
|
|
304
|
+
# Derive a coarse subsystem tag from Affected Files directories + title keywords
|
|
305
|
+
INV_SUBSYSTEM[$INV_NUM]=$(echo "$INV_BODY" \
|
|
306
|
+
| grep -oP '`[^`]+/[^`]+`' | xargs -r -n1 dirname 2>/dev/null | sort | uniq -c | sort -rn | head -1)
|
|
307
|
+
done
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Cluster rule**: Two investigations are in the same cluster when they share a target subsystem (overlapping affected-file directories OR the same domain tag from Step 3B applied to their recommendations). Clustering is by **conceptual surface**, deliberately coarser than Step 3C's file-level analysis — the goal is to surface plan-level contradictions the file layer cannot see.
|
|
311
|
+
|
|
312
|
+
**Forward reference — related future signal** <!-- Added: forge#1196 -->: This clustering step (Step 2.5B) runs before Phase 3, so no Layer 5 data exists yet at this point — do not treat it as an input here. For readers extending this clustering rule in the future: Step 3C Layer 5 (historical co-change coupling, computed later in Phase 3) is a related signal worth reusing — two investigations whose affected files have historically co-changed would be good candidates for the same subsystem cluster. This is purely a forward reference, not a functional dependency or phase-reordering.
|
|
313
|
+
|
|
314
|
+
### Step 2.5C: Detect and resolve competing recommendations
|
|
315
|
+
|
|
316
|
+
Within each cluster (2+ investigations on the same subsystem), compare the **Recommendation** sections for incompatibility — e.g. one recommends adding a cache layer while another recommends removing caching from the same path; one proposes a new abstraction another proposes to delete. This is a semantic comparison of *proposed approaches*, read purely from the annotation prose.
|
|
317
|
+
|
|
318
|
+
For each detected conflict, resolve it in exactly ONE of two ways:
|
|
319
|
+
|
|
320
|
+
1. **Arbitration decision** — When the two recommendations are directly incompatible and one is clearly correct given the combined evidence, record a single deconflicted decision for BOTH issues. State which approach wins and why. This decision is written into each affected issue's `FORGE:SYNTHESIS_BRIEF` (Step 2.5D) — it does NOT close either issue; both still run, but against a reconciled plan.
|
|
321
|
+
2. **Serialization edge** — When the approaches are interdependent (the second issue's correct approach depends on what the first decides) or arbitration cannot pick a winner from annotations alone, add a `Depends on #{FIRST}` marker to the SECOND issue's body so the two serialize. The second agent then inherits the first's merged result during its own investigation/context phase.
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
# Serialization is expressed as a standard "Depends on #X" edge so Step 3A consumes it
|
|
325
|
+
# with no new plumbing (see Step 3A dependency-marker parsing).
|
|
326
|
+
RECONCILED_COUNT=0
|
|
327
|
+
N_ARBITRATED=0
|
|
328
|
+
N_SERIALIZED=0
|
|
329
|
+
for CONFLICT in "${DETECTED_CONFLICTS[@]}"; do
|
|
330
|
+
# CONFLICT = "FIRST SECOND RESOLUTION" where RESOLUTION is "arbitrate" or "serialize"
|
|
331
|
+
set -- $CONFLICT; FIRST=$1; SECOND=$2; RESOLUTION=$3
|
|
332
|
+
if [ "$RESOLUTION" = "serialize" ]; then
|
|
333
|
+
# Reverse-direction cycle guard: before adding "#SECOND depends on #FIRST", check
|
|
334
|
+
# whether #FIRST already declares "Depends on #SECOND". If it does, the requested
|
|
335
|
+
# edge would close a 2-node cycle (#FIRST -> #SECOND -> #FIRST) that Step 3D.5's
|
|
336
|
+
# cycle detector would later have to exclude from dispatch entirely (both issues
|
|
337
|
+
# stuck behind needs-human). Skip the edge and fall back to arbitration-in-place
|
|
338
|
+
# instead, so both issues still run.
|
|
339
|
+
FIRST_BODY=$(gh issue view $FIRST -R {GH_REPO} --json body --jq '.body')
|
|
340
|
+
if echo "$FIRST_BODY" | grep -qiE "depends on #${SECOND}\b"; then
|
|
341
|
+
echo "Phase 2.5: skipping serialization edge #${FIRST} -> #${SECOND}: reverse edge #${SECOND} -> #${FIRST} already exists (would create a cycle). Falling back to arbitration-in-place."
|
|
342
|
+
RESOLUTION="arbitrate"
|
|
343
|
+
else
|
|
344
|
+
SECOND_BODY=$(gh issue view $SECOND -R {GH_REPO} --json body --jq '.body')
|
|
345
|
+
if ! echo "$SECOND_BODY" | grep -qiE "depends on #${FIRST}\b"; then
|
|
346
|
+
gh issue edit $SECOND -R {GH_REPO} \
|
|
347
|
+
--body "${SECOND_BODY}
|
|
348
|
+
|
|
349
|
+
Depends on #${FIRST}
|
|
350
|
+
<!-- Serialized by orchestrate Phase 2.5: competing recommendation reconciled via dependency edge. -->"
|
|
351
|
+
fi
|
|
352
|
+
fi
|
|
353
|
+
fi
|
|
354
|
+
# Re-check RESOLUTION (may have been downgraded from "serialize" to "arbitrate" above)
|
|
355
|
+
# so the breakdown counters and the Step 2.5D per-issue decision recording both reflect
|
|
356
|
+
# the resolution that was actually applied, not the one originally proposed.
|
|
357
|
+
if [ "$RESOLUTION" = "serialize" ]; then
|
|
358
|
+
N_SERIALIZED=$((N_SERIALIZED + 1))
|
|
359
|
+
else
|
|
360
|
+
N_ARBITRATED=$((N_ARBITRATED + 1))
|
|
361
|
+
fi
|
|
362
|
+
RECONCILED_COUNT=$((RECONCILED_COUNT + 1))
|
|
363
|
+
done
|
|
364
|
+
echo "Phase 2.5 reconciled ${RECONCILED_COUNT} competing recommendation(s) (${N_ARBITRATED} arbitrated, ${N_SERIALIZED} serialized)."
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**MUST NOT**: close, skip, or merge any issue on the basis of a detected conflict. Two issues with competing recommendations are BOTH valid work items — Phase 2.5 makes their plans coherent, it does not eliminate either. (This is the Safety Rule 9 boundary — see the Purpose note above.)
|
|
368
|
+
|
|
369
|
+
### Step 2.5D: Emit one deconflicted brief per issue
|
|
370
|
+
|
|
371
|
+
For each implementation issue about to be dispatched, write a single `FORGE:SYNTHESIS_BRIEF` annotation containing ONLY the reconciled context relevant to *that* issue — the arbitration decisions affecting it and pointers to the specific sibling investigation Gists it actually needs. This replaces injecting the entire aggregated milestone-index gist (which forces each agent to independently re-arbitrate the same contradictions, wasting tokens and producing nondeterministic cross-PR incoherence).
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
for ISSUE_NUM in {implementation_issue_numbers}; do
|
|
375
|
+
# Assemble the per-issue brief: arbitration decisions touching this issue's subsystem +
|
|
376
|
+
# only the relevant sibling gist URLs (not the full milestone-index dump)
|
|
377
|
+
BRIEF_BODY="Reconciled context for this issue (see orchestrate Phase 2.5):
|
|
378
|
+
${PER_ISSUE_DECISIONS[$ISSUE_NUM]}"
|
|
379
|
+
gh issue comment $ISSUE_NUM -R {GH_REPO} --body "<!-- FORGE:SYNTHESIS_BRIEF -->
|
|
380
|
+
## Synthesis Brief
|
|
381
|
+
|
|
382
|
+
${BRIEF_BODY}
|
|
383
|
+
|
|
384
|
+
<!-- FORGE:SYNTHESIS_BRIEF:COMPLETE -->"
|
|
385
|
+
done
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
The `FORGE:SYNTHESIS_BRIEF` annotation is consumed by Step 4A's `{GIST_CONTEXT}` generation (which prefers it over the raw milestone-index gist when present) and its reconciled count (`RECONCILED_COUNT`) feeds the Step 6B `Competing recommendations reconciled (Phase 2.5)` metric.
|
|
389
|
+
|
|
390
|
+
**Report**: Post a brief Phase 2.5 summary to the user before proceeding to Phase 3:
|
|
391
|
+
|
|
392
|
+
```
|
|
393
|
+
## Phase 2.5: Investigation Synthesis
|
|
394
|
+
|
|
395
|
+
**Investigations reconciled**: {INVESTIGATION_COUNT}
|
|
396
|
+
**Competing recommendations detected**: {RECONCILED_COUNT}
|
|
397
|
+
- Arbitrated in place: {N_ARBITRATED} (includes any serialization edges downgraded by the reverse-cycle guard)
|
|
398
|
+
- Serialized via dependency edge: {N_SERIALIZED}
|
|
399
|
+
**Per-issue synthesis briefs emitted**: {N_briefs}
|
|
400
|
+
|
|
401
|
+
Proceeding to dependency analysis with a deconflicted plan set...
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
258
406
|
## Phase 3: Dependency Analysis & Execution Plan
|
|
259
407
|
|
|
260
408
|
### Step 3A: Analyze explicit dependencies
|
|
@@ -305,21 +453,30 @@ Domain estimation (above) catches broad category overlap but misses cases where
|
|
|
305
453
|
|
|
306
454
|
#### Layer 1: Explicit file-overlap extraction
|
|
307
455
|
|
|
308
|
-
**For issues that already have an INVESTIGATOR comment** (from Wave 0 or a prior session), extract their Affected Files list:
|
|
456
|
+
**For issues that already have an INVESTIGATOR comment** (from Wave 0 or a prior session), extract their Affected Files list. **For issues WITHOUT an investigation comment**, fall back to parsing the issue body for file paths. Both code paths accumulate into a single `LAYER1_FILES` array (declared once, before the loop) — this is the batch-wide file set that Layer 5's co-change query (below) reuses, per the "file list already extracted in Layer 1" reference in Layer 2 and Layer 5:
|
|
309
457
|
|
|
310
458
|
```bash
|
|
459
|
+
LAYER1_FILES=()
|
|
311
460
|
for NUM in {issue_numbers}; do
|
|
312
461
|
echo "=== #$NUM ==="
|
|
313
|
-
gh api repos/{GH_REPO}/issues/${NUM}/comments \
|
|
462
|
+
FILES_FOR_NUM=$(gh api repos/{GH_REPO}/issues/${NUM}/comments \
|
|
314
463
|
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body' 2>/dev/null \
|
|
315
|
-
| grep -oP '`[^`]*\.(py|tsx?|jsx?|sql|json|ya?ml)`' | sort -u
|
|
316
|
-
done
|
|
317
|
-
```
|
|
464
|
+
| grep -oP '`[^`]*\.(py|tsx?|jsx?|sql|json|ya?ml)`' | tr -d '`' | sort -u)
|
|
318
465
|
|
|
319
|
-
|
|
466
|
+
# Fall back to parsing the issue body directly when no INVESTIGATOR comment exists yet.
|
|
467
|
+
if [ -z "$FILES_FOR_NUM" ]; then
|
|
468
|
+
FILES_FOR_NUM=$(gh issue view $NUM --json body --jq '.body' \
|
|
469
|
+
| grep -oP '`[^`]*\.(py|tsx?|jsx?|sql|json|ya?ml)`' | tr -d '`' | sort -u)
|
|
470
|
+
fi
|
|
320
471
|
|
|
321
|
-
|
|
322
|
-
|
|
472
|
+
echo "$FILES_FOR_NUM"
|
|
473
|
+
|
|
474
|
+
# Accumulate into the batch-wide array — read line-by-line so each extracted path
|
|
475
|
+
# becomes one array element (paths here don't contain spaces, but this stays robust).
|
|
476
|
+
while IFS= read -r f; do
|
|
477
|
+
[ -n "$f" ] && LAYER1_FILES+=("$f")
|
|
478
|
+
done <<< "$FILES_FOR_NUM"
|
|
479
|
+
done
|
|
323
480
|
```
|
|
324
481
|
|
|
325
482
|
**Cross-reference all extracted file lists:**
|
|
@@ -400,22 +557,71 @@ When file extraction yields **fewer than 2 file paths** for an issue (common for
|
|
|
400
557
|
|
|
401
558
|
**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.
|
|
402
559
|
|
|
560
|
+
#### Layer 5: Historical co-change coupling <!-- Added: forge#1196 --> <!-- Empty-set guard: forge#1206 -->
|
|
561
|
+
|
|
562
|
+
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"). This is the same category of operation already established as compliant in `commands/work-on/investigate.md` and `commands/work-on/build/context.md`, which mine `git log` for issue cross-referencing.
|
|
563
|
+
|
|
564
|
+
**Bounded query** — restrict both the time window and the file set so this stays a small, deterministic, single-shot lookup (never a full-repo co-change matrix). An empty `ALL_AFFECTED_FILES` set MUST short-circuit before the query runs — passing an empty array to `git log --` does not mean "match nothing", it means "no pathspec restriction", which would silently widen the query to the entire repo:
|
|
565
|
+
|
|
566
|
+
```bash
|
|
567
|
+
# Union of affected files across all issues in the CURRENT batch only (already
|
|
568
|
+
# extracted per-issue in Layer 1) — never the whole repo. Built as an array
|
|
569
|
+
# (not a newline-joined scalar) so each path survives as a single pathspec
|
|
570
|
+
# argument below — a plain string here would be word-split and glob-expanded
|
|
571
|
+
# by the shell when handed to `git log --`, silently mangling or dropping any
|
|
572
|
+
# path containing a space or glob metacharacter.
|
|
573
|
+
mapfile -t ALL_AFFECTED_FILES < <(printf '%s\n' "${LAYER1_FILES[@]}" | sort -u)
|
|
574
|
+
|
|
575
|
+
# Guard: an empty array expands to nothing after `--`, which git interprets as
|
|
576
|
+
# "no pathspec restriction" (i.e. the whole repo) rather than "match nothing".
|
|
577
|
+
# Skip the query entirely in that case instead of letting it silently widen to
|
|
578
|
+
# a full-repo scan — this can happen when upstream file-extraction (Layer 1)
|
|
579
|
+
# yields zero paths for every issue in the batch.
|
|
580
|
+
if [ "${#ALL_AFFECTED_FILES[@]}" -eq 0 ]; then
|
|
581
|
+
echo "Layer 5: no affected files extracted by Layer 1 for this batch — skipping co-change query, falling back to Layers 1-4."
|
|
582
|
+
else
|
|
583
|
+
# Bounded window: last 90 days, capped at 200 commits — whichever is smaller.
|
|
584
|
+
# Each commit's file list is delimited by a marker so co-occurring files can be
|
|
585
|
+
# grouped per-commit in a single pass. The array is expanded quoted
|
|
586
|
+
# ("${ALL_AFFECTED_FILES[@]}") so every path is passed as one literal argument.
|
|
587
|
+
git log --name-only --since="90 days ago" --max-count=200 \
|
|
588
|
+
--pretty=format:'---%H---' -- "${ALL_AFFECTED_FILES[@]}" \
|
|
589
|
+
> /tmp/cochange_log.txt
|
|
590
|
+
|
|
591
|
+
# Parse into commit → file-set groups, then increment a co-occurrence counter
|
|
592
|
+
# for every unordered pair of files that appear in the SAME commit's file list.
|
|
593
|
+
# (Illustrative — an agent executing this reads /tmp/cochange_log.txt and tallies
|
|
594
|
+
# pairs; no separate script is shipped, matching the pseudo-code style of Layers 1-4.)
|
|
595
|
+
fi
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
**Scoring rule**: A file pair is **co-change coupled** when it appears together in **3 or more** of the commits captured by the bounded query above. A pair with **zero** co-occurrences across the entire window is **verified independent**.
|
|
599
|
+
|
|
600
|
+
**Apply the signal:**
|
|
601
|
+
- **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).
|
|
602
|
+
- **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`).
|
|
603
|
+
- If `ALL_AFFECTED_FILES` is empty, the guard above skips the query and Layer 5 contributes nothing for the entire batch. If the bounded query runs but returns no commits (e.g. brand-new files, or window/pair-set too small to have history) → Layer 5 contributes nothing; fall back silently to Layers 1-4's existing verdict for that pair.
|
|
604
|
+
|
|
605
|
+
**Rationale**: Empirical co-change is a strictly stronger signal than directory proximity or naming convention — it is the actual observed outcome the other layers are trying to approximate. Bounding the window and pair-set keeps the query cheap and deterministic while still catching the cross-directory conflicts Layers 1-4 structurally cannot see.
|
|
606
|
+
|
|
403
607
|
#### Combining all layers
|
|
404
608
|
|
|
405
|
-
Build the final conflict graph by merging signals from all
|
|
609
|
+
Build the final conflict graph by merging signals from all five layers:
|
|
406
610
|
|
|
407
611
|
| Signal | Strength | Action |
|
|
408
612
|
|--------|----------|--------|
|
|
409
613
|
| Layer 1: Same file | **Hard conflict** | Always serialize |
|
|
410
614
|
| Layer 2: Same small directory | **Probable conflict** | Serialize |
|
|
411
615
|
| Layer 2: Same broad directory + same domain | **Probable conflict** | Serialize |
|
|
412
|
-
| Layer 2: Same broad directory + different domain | **Possible conflict** | Parallelize (accept risk) |
|
|
616
|
+
| Layer 2: Same broad directory + different domain | **Possible conflict** | Parallelize (accept risk) — unless Layer 5 shows high co-change, then serialize |
|
|
413
617
|
| Layer 3: High-fan-in file touched | **Probable conflict** | Serialize with same-service issues |
|
|
414
618
|
| Layer 3: Shared model/init pattern | **Probable conflict** | Serialize |
|
|
415
619
|
| Layer 4: Low confidence + same domain | **Conservative** | Serialize |
|
|
416
|
-
| Layer 4: Low confidence + no domain | **Conservative** | Add predecessor edge to most recent issue |
|
|
620
|
+
| Layer 4: Low confidence + no domain | **Conservative** | Add predecessor edge to most recent issue — unless Layer 5 shows verified independence, then parallelize |
|
|
621
|
+
| Layer 5: High co-change (3+ shared commits, cross-issue file pair) | **Probable conflict** | Serialize (or route to Phase 2.5 arbitration if a competing-recommendation conflict is also present) |
|
|
622
|
+
| Layer 5: Verified independent (zero shared commits) | **Downgrade signal** | Permits downgrading a Layer 2 "broad directory + different domain" or Layer 4 verdict to parallel — never overrides Layer 1 or Layer 3 |
|
|
417
623
|
|
|
418
|
-
**This supplements, not replaces, the domain keyword estimation.** Domain tags still help with broad sequencing decisions. Multi-layer conflict detection catches the specific cases keywords miss (e.g., two issues that both modify files in `services/api/app/models/` where one is labeled WORKER and the other BILLING — Layer 2 catches this even though Layer 1 shows no direct file overlap).
|
|
624
|
+
**This supplements, not replaces, the domain keyword estimation.** Domain tags still help with broad sequencing decisions. Multi-layer conflict detection catches the specific cases keywords miss (e.g., two issues that both modify files in `services/api/app/models/` where one is labeled WORKER and the other BILLING — Layer 2 catches this even though Layer 1 shows no direct file overlap; or two issues touching files in unrelated directories that Layer 5 shows have co-changed in 4 of the last 12 commits touching either file).
|
|
419
625
|
|
|
420
626
|
### Step 3D: Build the dependency DAG
|
|
421
627
|
|
|
@@ -428,7 +634,8 @@ Build a **directed acyclic graph (DAG)** of per-issue dependencies. Each issue g
|
|
|
428
634
|
- **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.
|
|
429
635
|
- **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.
|
|
430
636
|
- **Conservative fallback edges**: Low-confidence issues (Layer 4) get edges to same-domain issues as per Step 3C rules.
|
|
431
|
-
- **
|
|
637
|
+
- **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.
|
|
638
|
+
- **No artificial concurrency limit** — all issues with empty predecessor sets dispatch simultaneously. The only constraints are file overlap, explicit dependencies, and co-change coupling.
|
|
432
639
|
|
|
433
640
|
**Terminology:**
|
|
434
641
|
- **Ready issues**: Issues whose predecessor set is empty (all predecessors have reached terminal state or were never added)
|
|
@@ -453,6 +660,108 @@ Initial dispatch: #2633, #2636, #2645, #2646 (all ready — launched simultaneou
|
|
|
453
660
|
|
|
454
661
|
**Key advantage over waves**: When #2633 completes, #2634 dispatches immediately — it does not wait for #2636, #2645, or #2646 to finish. Similarly, when #2645 completes, #2647 dispatches immediately regardless of other issues' status.
|
|
455
662
|
|
|
663
|
+
### Step 3D.5: Cycle Detection (MANDATORY) <!-- Added: forge#1085 -->
|
|
664
|
+
|
|
665
|
+
**Run immediately after Step 3D's DAG edge construction, before presenting the plan (Step 3E).** This step validates that the predecessor graph is acyclic. Without it, mutual `Depends on` declarations (e.g., A depends on B AND B depends on A) cause both issues to remain permanently blocked in Step 4B's dispatch loop — no error, no timeout, indefinite deadlock.
|
|
666
|
+
|
|
667
|
+
**Algorithm**: Kahn's topological sort. Runs in O(V+E) — negligible overhead for typical batch sizes.
|
|
668
|
+
|
|
669
|
+
```bash
|
|
670
|
+
# --- Step 3D.5: Cycle Detection ---
|
|
671
|
+
# Inputs:
|
|
672
|
+
# ISSUES[] — array of all issue numbers in the DAG
|
|
673
|
+
# PREDECESSORS[N] — space-separated list of predecessor issue numbers for issue N
|
|
674
|
+
|
|
675
|
+
# Step 1: Compute in-degree for each issue
|
|
676
|
+
declare -A IN_DEGREE
|
|
677
|
+
for NUM in "${ISSUES[@]}"; do
|
|
678
|
+
IN_DEGREE[$NUM]=0
|
|
679
|
+
done
|
|
680
|
+
for NUM in "${ISSUES[@]}"; do
|
|
681
|
+
for PRED in ${PREDECESSORS[$NUM]:-}; do
|
|
682
|
+
IN_DEGREE[$NUM]=$(( ${IN_DEGREE[$NUM]:-0} + 1 ))
|
|
683
|
+
done
|
|
684
|
+
done
|
|
685
|
+
|
|
686
|
+
# Step 2: Seed the queue with zero-in-degree issues (no predecessors)
|
|
687
|
+
KAHN_QUEUE=()
|
|
688
|
+
for NUM in "${ISSUES[@]}"; do
|
|
689
|
+
[ "${IN_DEGREE[$NUM]}" -eq 0 ] && KAHN_QUEUE+=("$NUM")
|
|
690
|
+
done
|
|
691
|
+
|
|
692
|
+
# Step 3: Process queue — reduce successor in-degrees, enqueue newly freed issues
|
|
693
|
+
PROCESSED_COUNT=0
|
|
694
|
+
PROCESSED_ORDER=()
|
|
695
|
+
while [ "${#KAHN_QUEUE[@]}" -gt 0 ]; do
|
|
696
|
+
# Dequeue
|
|
697
|
+
CURRENT="${KAHN_QUEUE[0]}"
|
|
698
|
+
KAHN_QUEUE=("${KAHN_QUEUE[@]:1}")
|
|
699
|
+
PROCESSED_ORDER+=("$CURRENT")
|
|
700
|
+
PROCESSED_COUNT=$(( PROCESSED_COUNT + 1 ))
|
|
701
|
+
|
|
702
|
+
# Reduce in-degree of all issues that depend on CURRENT (i.e., CURRENT is in their PREDECESSORS)
|
|
703
|
+
for SUCCESSOR in "${ISSUES[@]}"; do
|
|
704
|
+
for PRED in ${PREDECESSORS[$SUCCESSOR]:-}; do
|
|
705
|
+
if [ "$PRED" = "$CURRENT" ]; then
|
|
706
|
+
IN_DEGREE[$SUCCESSOR]=$(( ${IN_DEGREE[$SUCCESSOR]} - 1 ))
|
|
707
|
+
[ "${IN_DEGREE[$SUCCESSOR]}" -eq 0 ] && KAHN_QUEUE+=("$SUCCESSOR")
|
|
708
|
+
fi
|
|
709
|
+
done
|
|
710
|
+
done
|
|
711
|
+
done
|
|
712
|
+
|
|
713
|
+
# Step 4: Any issue not processed has in-degree > 0 — part of a cycle
|
|
714
|
+
CYCLE_ISSUES=()
|
|
715
|
+
EXCLUDED_CYCLE=()
|
|
716
|
+
for NUM in "${ISSUES[@]}"; do
|
|
717
|
+
FOUND=false
|
|
718
|
+
for P in "${PROCESSED_ORDER[@]}"; do [ "$P" = "$NUM" ] && FOUND=true && break; done
|
|
719
|
+
[ "$FOUND" = "false" ] && CYCLE_ISSUES+=("$NUM")
|
|
720
|
+
done
|
|
721
|
+
|
|
722
|
+
# Step 5: Handle cycles
|
|
723
|
+
if [ "${#CYCLE_ISSUES[@]}" -gt 0 ]; then
|
|
724
|
+
echo "CYCLE DETECTED in dependency graph — the following issues form a circular dependency:"
|
|
725
|
+
for C in "${CYCLE_ISSUES[@]}"; do
|
|
726
|
+
echo " #${C}: predecessors=[${PREDECESSORS[$C]}]"
|
|
727
|
+
# Label each cyclic issue needs-human
|
|
728
|
+
gh issue edit "$C" -R {GH_REPO} --add-label "needs-human" 2>/dev/null || true
|
|
729
|
+
gh issue comment "$C" -R {GH_REPO} --body "**Cycle detected by /orchestrate**: This issue is part of a circular dependency chain involving issues: ${CYCLE_ISSUES[*]/#/#}. The orchestrator cannot dispatch it automatically. Please fix the \`Depends on\` declarations so that no cycle exists, then re-run /orchestrate." 2>/dev/null || true
|
|
730
|
+
# Remove from DAG — store in EXCLUDED_CYCLE for Step 3E reporting
|
|
731
|
+
EXCLUDED_CYCLE+=("$C")
|
|
732
|
+
# Remove from ISSUES array for all downstream processing
|
|
733
|
+
# Use exact-match filter loop — pattern substitution (${array[@]/pattern}) leaves blank
|
|
734
|
+
# slots and corrupts partial matches (e.g., removing 100 changes 1000 to 0).
|
|
735
|
+
NEW_ISSUES=()
|
|
736
|
+
for I in "${ISSUES[@]}"; do
|
|
737
|
+
[ "$I" != "$C" ] && NEW_ISSUES+=("$I")
|
|
738
|
+
done
|
|
739
|
+
ISSUES=("${NEW_ISSUES[@]}")
|
|
740
|
+
done
|
|
741
|
+
echo ""
|
|
742
|
+
echo "These issues have been labeled needs-human and excluded from the DAG."
|
|
743
|
+
echo "Fix their dependency declarations and re-run /orchestrate."
|
|
744
|
+
else
|
|
745
|
+
echo "DAG cycle check: PASS — no cycles detected. Proceeding with ${#PROCESSED_ORDER[@]} issues."
|
|
746
|
+
fi
|
|
747
|
+
|
|
748
|
+
# Guard: if all issues were cyclic, ISSUES[] is now empty — abort before presenting an empty plan
|
|
749
|
+
if [ "${#ISSUES[@]}" -eq 0 ]; then
|
|
750
|
+
echo ""
|
|
751
|
+
echo "ERROR: All issues in this batch form circular dependencies and have been excluded."
|
|
752
|
+
echo "Every issue has been labeled needs-human."
|
|
753
|
+
echo "Fix the Depends on / Blocked by declarations so no cycle exists, then re-run /orchestrate."
|
|
754
|
+
exit 1
|
|
755
|
+
fi
|
|
756
|
+
# --- End Step 3D.5 ---
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
**After this step**:
|
|
760
|
+
- `ISSUES[]` contains only acyclic issues — safe to dispatch
|
|
761
|
+
- `EXCLUDED_CYCLE[]` contains cyclic issue numbers — reported in Step 3E, never dispatched
|
|
762
|
+
- If `EXCLUDED_CYCLE` is non-empty, report it clearly in the Step 3E plan before asking for user confirmation
|
|
763
|
+
- 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 -->
|
|
764
|
+
|
|
456
765
|
### Step 3E: Present the plan to the user
|
|
457
766
|
|
|
458
767
|
```
|
|
@@ -504,6 +813,18 @@ Initial dispatch: #2633, #2636, #2645, #2646 (all ready — launched simultaneou
|
|
|
504
813
|
**Excluded** (already in progress / ineligible):
|
|
505
814
|
- #{X} — {reason}
|
|
506
815
|
|
|
816
|
+
{IF EXCLUDED_CYCLE is non-empty:}
|
|
817
|
+
### ⚠ Circular Dependencies Detected — Manual Fix Required
|
|
818
|
+
|
|
819
|
+
The following issues form a circular dependency chain and **cannot be dispatched** until the cycle is resolved:
|
|
820
|
+
|
|
821
|
+
| Issue | Depends On | Problem |
|
|
822
|
+
|-------|------------|---------|
|
|
823
|
+
{rows: each EXCLUDED_CYCLE issue, its predecessor list, "mutual dependency — forms cycle with #{other}"}
|
|
824
|
+
|
|
825
|
+
**Action required**: Edit each issue's body to remove or correct the `Depends on` / `Blocked by` declarations so no cycle exists. Each issue has been labeled `needs-human`. After fixing, re-run `/orchestrate` to dispatch them.
|
|
826
|
+
{END IF}
|
|
827
|
+
|
|
507
828
|
Proceed? (yes / adjust / pick specific issues)
|
|
508
829
|
```
|
|
509
830
|
|
|
@@ -648,6 +969,14 @@ Before building agent prompts, run `classify-lane.sh` for every issue in the cur
|
|
|
648
969
|
declare -A ISSUE_LANE
|
|
649
970
|
declare -A ISSUE_PR_BASE
|
|
650
971
|
|
|
972
|
+
# Batch-level accumulators for review-finding cascade control (Step 4C) and
|
|
973
|
+
# Completion Sweep (Step 4F). Declared here so they persist across ALL agent
|
|
974
|
+
# completions — Step 4C runs per-agent and must NOT re-initialize these.
|
|
975
|
+
DEFERRED_FINDINGS=()
|
|
976
|
+
QUEUED_FINDINGS=()
|
|
977
|
+
declare -A DEFERRED_REASONS
|
|
978
|
+
declare -A AGENT_ISSUE_MAP
|
|
979
|
+
|
|
651
980
|
for NUM in {ready_issue_numbers}; do
|
|
652
981
|
PR_BASE=$(bash ~/.claude/scripts/classify-lane.sh "$NUM" -R {GH_REPO}) || {
|
|
653
982
|
echo "ERROR: classify-lane.sh failed for #$NUM — adding needs-human label and skipping" >&2
|
|
@@ -731,32 +1060,55 @@ If the label is NOT terminal (e.g., `workflow:investigating`, `workflow:ready-to
|
|
|
731
1060
|
)
|
|
732
1061
|
```
|
|
733
1062
|
|
|
734
|
-
**`{GIST_CONTEXT}` generation**: For each issue being dispatched,
|
|
1063
|
+
**`{GIST_CONTEXT}` generation**: For each issue being dispatched, build the context block. **Prefer the deconflicted `FORGE:SYNTHESIS_BRIEF` (from Phase 2.5) when one exists** — it is a per-issue, already-reconciled brief that carries only the arbitration decisions and sibling investigation Gists relevant to *this* issue. Injecting it instead of the full aggregated milestone-index gist means the agent does not re-arbitrate the same contradictions (less token spend, less nondeterminism). Only when Phase 2.5 did not run (0/1 investigations — no brief exists) does this fall back to the raw parent-investigation + milestone-index gist behavior. <!-- Added: forge#1192 -->
|
|
735
1064
|
|
|
736
1065
|
```bash
|
|
737
1066
|
# Build GIST_CONTEXT for an issue
|
|
738
1067
|
GIST_CONTEXT=""
|
|
739
|
-
PARENT_INV=$(gh issue view {NUMBER} -R {GH_REPO} --json body --jq '.body' \
|
|
740
|
-
| grep -oP '(?i)parent[: ]*#\K\d+|spawned from[: ]*#\K\d+' | head -1)
|
|
741
1068
|
|
|
742
|
-
|
|
1069
|
+
# Preferred path: a deconflicted per-issue synthesis brief from Phase 2.5.
|
|
1070
|
+
SYNTHESIS_BRIEF=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
1071
|
+
--jq '[.[] | select(.body | contains("<!-- FORGE:SYNTHESIS_BRIEF -->"))] | last | .body // ""' 2>/dev/null)
|
|
1072
|
+
|
|
1073
|
+
if [ -n "$SYNTHESIS_BRIEF" ]; then
|
|
1074
|
+
# Phase 2.5 ran and reconciled competing recommendations for this issue.
|
|
1075
|
+
# Inject the deconflicted brief INSTEAD of the raw milestone-index gist dump.
|
|
743
1076
|
GIST_CONTEXT="
|
|
1077
|
+
**RECONCILED CONTEXT (orchestrate Phase 2.5 synthesis brief)**: Competing investigation recommendations affecting this issue have already been reconciled. Use this deconflicted brief as your primary cross-investigation context — do NOT independently re-arbitrate the underlying investigations.
|
|
1078
|
+
${SYNTHESIS_BRIEF}"
|
|
1079
|
+
else
|
|
1080
|
+
# Fallback: Phase 2.5 did not run (0/1 investigations). Use the raw gist behavior.
|
|
1081
|
+
PARENT_INV=$(gh issue view {NUMBER} -R {GH_REPO} --json body --jq '.body' \
|
|
1082
|
+
| grep -oP '(?i)parent[: ]*#\K\d+|spawned from[: ]*#\K\d+' | head -1)
|
|
1083
|
+
|
|
1084
|
+
if [ -n "$PARENT_INV" ] && [ -n "${INVESTIGATION_GISTS[$PARENT_INV]:-}" ]; then
|
|
1085
|
+
GIST_CONTEXT="
|
|
744
1086
|
**CONTEXT FROM PRIOR INVESTIGATION**: Investigation #${PARENT_INV} produced Knowledge Gist(s) with findings relevant to this issue:
|
|
745
1087
|
$(echo "${INVESTIGATION_GISTS[$PARENT_INV]}" | while IFS= read -r url; do echo "- ${url}"; done)
|
|
746
1088
|
Fetch the Gist content during the context-gathering phase for implementation guidance."
|
|
747
|
-
fi
|
|
1089
|
+
fi
|
|
748
1090
|
|
|
749
|
-
# Include milestone index URL if available (from Step 2C.5)
|
|
750
|
-
if [ -n "$MILESTONE_INDEX_URL" ]; then
|
|
751
|
-
|
|
1091
|
+
# Include milestone index URL if available (from Step 2C.5)
|
|
1092
|
+
if [ -n "$MILESTONE_INDEX_URL" ]; then
|
|
1093
|
+
GIST_CONTEXT="${GIST_CONTEXT}
|
|
752
1094
|
|
|
753
1095
|
**MILESTONE KNOWLEDGE INDEX**: All investigation findings for this milestone are aggregated in a single index Gist:
|
|
754
1096
|
- ${MILESTONE_INDEX_URL}
|
|
755
1097
|
The context-gathering phase can fetch this index to discover all investigation Gists for the milestone."
|
|
1098
|
+
fi
|
|
756
1099
|
fi
|
|
757
1100
|
```
|
|
758
1101
|
|
|
759
|
-
If `GIST_CONTEXT` is empty (no parent investigation
|
|
1102
|
+
If `GIST_CONTEXT` is empty (no synthesis brief, no parent investigation, and no milestone index found), the variable resolves to a blank line in the template — no impact on the agent prompt. <!-- Updated: forge#341, forge#1192 -->
|
|
1103
|
+
|
|
1104
|
+
**Capture agent IDs after the batch spawn (MANDATORY)**: Each `Agent(...)` call returns an agent ID. Store each returned ID in `AGENT_ISSUE_MAP` keyed by issue number. This map is the only way to resume a stalled agent by ID in Steps 4B and 4B.5:
|
|
1105
|
+
|
|
1106
|
+
```
|
|
1107
|
+
# After the single-message batch spawn, capture each returned ID:
|
|
1108
|
+
AGENT_ISSUE_MAP[{NUMBER}] = <agent_id returned by Agent()>
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
`AGENT_ISSUE_MAP` starts empty and accumulates entries as agents are spawned. For parallel dispatch (all Agent() calls in one message), capture the returned IDs from the batch response — one entry per issue — before entering Step 4B's monitoring loop. Without this capture, `resume=` calls in Steps 4B and 4B.5 will have no agent ID to reference and the resume will fail. <!-- Added: forge#1083 -->
|
|
760
1112
|
|
|
761
1113
|
**Launch all ready agents simultaneously** by putting multiple Agent tool calls in a single message. Use `run_in_background=true` so they execute in parallel.
|
|
762
1114
|
|
|
@@ -798,7 +1150,7 @@ done
|
|
|
798
1150
|
2. **If the issue is NOT in a terminal state** (`workflow:merged`, `workflow:invalid`, or `needs-human`), the agent stalled mid-pipeline. **Resume it immediately**:
|
|
799
1151
|
```
|
|
800
1152
|
Agent(
|
|
801
|
-
resume=
|
|
1153
|
+
resume=AGENT_ISSUE_MAP[{NUMBER}],
|
|
802
1154
|
description="Resume #{NUMBER} pipeline",
|
|
803
1155
|
run_in_background=true,
|
|
804
1156
|
prompt="The previous /work-on invocation stopped before completing the full pipeline. The issue is currently at {CURRENT_WORKFLOW_STATE}. Continue — invoke Skill(skill='work-on', args='{NUMBER}') to resume the routing loop from the current state. /work-on will re-read GitHub state and pick up where it left off."
|
|
@@ -806,7 +1158,7 @@ done
|
|
|
806
1158
|
```
|
|
807
1159
|
**Resume ALL stalled agents in a single message** (parallel resume). Do not wait between resumes.
|
|
808
1160
|
|
|
809
|
-
3. **Track resume cycles per agent.** If an agent has been resumed
|
|
1161
|
+
3. **Track resume cycles per agent.** If an agent has been resumed 2+ times and still hasn't reached a terminal state, report it as a failure — do not resume again.
|
|
810
1162
|
|
|
811
1163
|
4. **Record completed results**: Success (PR merged), Invalid (issue closed), Blocked (needs human), or Error
|
|
812
1164
|
|
|
@@ -835,7 +1187,7 @@ done
|
|
|
835
1187
|
|
|
836
1188
|
9. **Run staging integrity check** (from Step 4A-pre) if the completed agent merged a PR targeting staging.
|
|
837
1189
|
|
|
838
|
-
**Termination condition**: All issues in the DAG are in a terminal state (merged, invalid, needs-human, or skipped due to dependency failure). When this condition is met, proceed to Phase 5.
|
|
1190
|
+
**Termination condition**: All issues in the DAG are in a terminal state (merged, invalid, needs-human, or skipped due to dependency failure). When this condition is met, check whether deferred review-spawned findings exist (accumulated in `DEFERRED_FINDINGS` during Step 4C). If deferred findings exist → proceed to Step 4F (Completion Sweep). If no deferred findings → proceed to Phase 5.
|
|
839
1191
|
|
|
840
1192
|
**Anti-pattern — DO NOT DO THIS:**
|
|
841
1193
|
- `sleep 60/120/180/300` loops to check status — you will be notified automatically
|
|
@@ -921,7 +1273,7 @@ Issue #${NUM} has been auto-resumed ${STALL_COUNT} times without reaching a term
|
|
|
921
1273
|
done
|
|
922
1274
|
|
|
923
1275
|
# Launch all stall resumes in parallel (single message — same rule as Step 4B)
|
|
924
|
-
# For each NUM in $STALL_RESUME_LIST, call Agent(resume=
|
|
1276
|
+
# For each NUM in $STALL_RESUME_LIST, call Agent(resume=AGENT_ISSUE_MAP[NUM], run_in_background=true, ...)
|
|
925
1277
|
```
|
|
926
1278
|
|
|
927
1279
|
**Resume all stalled agents in a single message** (parallel). Use the same `Agent(resume=...)` pattern as Step 4B — do not wait between individual resumes.
|
|
@@ -989,6 +1341,8 @@ ALL_BATCH_FILES=$(echo "$ALL_BATCH_FILES" | sort -u | grep -v '^$')
|
|
|
989
1341
|
|
|
990
1342
|
```bash
|
|
991
1343
|
# For each finding, check its priority label and generation
|
|
1344
|
+
# NOTE: DEFERRED_FINDINGS, QUEUED_FINDINGS, and DEFERRED_REASONS are declared at
|
|
1345
|
+
# batch scope in Step 4A.pre — do NOT re-initialize them here (Step 4C runs per-agent).
|
|
992
1346
|
for FINDING_NUM in {spawned_finding_numbers}; do
|
|
993
1347
|
FINDING_DATA=$(gh issue view $FINDING_NUM -R {GH_REPO} --json labels,title,body \
|
|
994
1348
|
--jq '{labels: [.labels[].name], title: .title, body: .body}')
|
|
@@ -1022,6 +1376,7 @@ for FINDING_NUM in {spawned_finding_numbers}; do
|
|
|
1022
1376
|
|
|
1023
1377
|
if [ "$DEFER" = "true" ]; then
|
|
1024
1378
|
DEFERRED_FINDINGS+=($FINDING_NUM)
|
|
1379
|
+
DEFERRED_REASONS[$FINDING_NUM]="$DEFER_REASON"
|
|
1025
1380
|
echo "Deferred #${FINDING_NUM}: $DEFER_REASON"
|
|
1026
1381
|
else
|
|
1027
1382
|
QUEUED_FINDINGS+=($FINDING_NUM)
|
|
@@ -1043,7 +1398,7 @@ done
|
|
|
1043
1398
|
|
|
1044
1399
|
**For deferred findings:**
|
|
1045
1400
|
|
|
1046
|
-
|
|
1401
|
+
Track them in `DEFERRED_FINDINGS` for re-evaluation in Step 4F (Completion Sweep) after the DAG drains. Do NOT close or label them yet — the sweep will determine their final disposition.
|
|
1047
1402
|
|
|
1048
1403
|
**If no review-finding issues were spawned:** Continue monitoring for the next agent completion.
|
|
1049
1404
|
|
|
@@ -1191,6 +1546,219 @@ If an agent reports failure or error:
|
|
|
1191
1546
|
|
|
1192
1547
|
**Do NOT retry failed agents automatically.** Report the failure and let the user decide.
|
|
1193
1548
|
|
|
1549
|
+
### Step 4F: Completion Sweep (deferred review-spawned findings) <!-- Added: forge#1105 -->
|
|
1550
|
+
|
|
1551
|
+
**When to run**: After all DAG issues reach terminal state AND `DEFERRED_FINDINGS` is non-empty. Skip if no findings were deferred during this batch.
|
|
1552
|
+
|
|
1553
|
+
**WHY THIS EXISTS**: Deferred findings accumulate during the batch because of file-overlap and cascade-control heuristics (Step 4C). But once the DAG drains, the conditions that caused deferral often no longer apply — completed issues no longer occupy files, so same-file overlap vanishes. Without this sweep, deferred findings silently pile up across runs and never get resolved.
|
|
1554
|
+
|
|
1555
|
+
**Step 4F.1: Classify deferred findings into permanent vs re-evaluable**
|
|
1556
|
+
|
|
1557
|
+
```bash
|
|
1558
|
+
PERMANENT_DEFERRED=()
|
|
1559
|
+
SWEEP_CANDIDATES=()
|
|
1560
|
+
|
|
1561
|
+
for FINDING_NUM in "${DEFERRED_FINDINGS[@]}"; do
|
|
1562
|
+
DEFER_REASON="${DEFERRED_REASONS[$FINDING_NUM]}"
|
|
1563
|
+
|
|
1564
|
+
# Generation >= 2 deferrals are PERMANENT — unbounded cascade prevention
|
|
1565
|
+
if echo "$DEFER_REASON" | grep -qi "generation"; then
|
|
1566
|
+
PERMANENT_DEFERRED+=($FINDING_NUM)
|
|
1567
|
+
else
|
|
1568
|
+
# All other deferrals (comment/typo, P3 same-file) are re-evaluable
|
|
1569
|
+
SWEEP_CANDIDATES+=($FINDING_NUM)
|
|
1570
|
+
fi
|
|
1571
|
+
done
|
|
1572
|
+
|
|
1573
|
+
echo "Completion sweep: ${#SWEEP_CANDIDATES[@]} re-evaluable, ${#PERMANENT_DEFERRED[@]} permanent"
|
|
1574
|
+
```
|
|
1575
|
+
|
|
1576
|
+
**Step 4F.2: Re-evaluate sweep candidates**
|
|
1577
|
+
|
|
1578
|
+
Re-run the Step 4C heuristics against the now-empty DAG. Since all original batch issues are in terminal state, the `ALL_BATCH_FILES` list for file-overlap detection is empty — P3 same-file deferrals will now pass.
|
|
1579
|
+
|
|
1580
|
+
```bash
|
|
1581
|
+
SWEEP_EXECUTE=()
|
|
1582
|
+
SWEEP_STILL_DEFERRED=()
|
|
1583
|
+
|
|
1584
|
+
for FINDING_NUM in "${SWEEP_CANDIDATES[@]}"; do
|
|
1585
|
+
FINDING_DATA=$(gh issue view $FINDING_NUM -R {GH_REPO} --json labels,title,body,state \
|
|
1586
|
+
--jq '{labels: [.labels[].name], title: .title, body: .body, state: .state}')
|
|
1587
|
+
|
|
1588
|
+
# Skip if already closed (resolved by another process)
|
|
1589
|
+
STATE=$(echo "$FINDING_DATA" | jq -r '.state')
|
|
1590
|
+
[ "$STATE" = "CLOSED" ] && continue
|
|
1591
|
+
|
|
1592
|
+
PRIORITY=$(echo "$FINDING_DATA" | jq -r '.labels[] | select(startswith("priority:P")) | ltrimstr("priority:")' | head -1)
|
|
1593
|
+
TITLE=$(echo "$FINDING_DATA" | jq -r '.title')
|
|
1594
|
+
|
|
1595
|
+
# Re-apply heuristics against the drained DAG (no active batch files)
|
|
1596
|
+
# Comment/typo heuristic still applies — these are cosmetic regardless of DAG state
|
|
1597
|
+
if echo "$TITLE" | grep -qi "comment\|typo"; then
|
|
1598
|
+
SWEEP_STILL_DEFERRED+=($FINDING_NUM)
|
|
1599
|
+
echo "Sweep: #${FINDING_NUM} still deferred (comment/typo — cosmetic)"
|
|
1600
|
+
else
|
|
1601
|
+
# P3 same-file overlap no longer applies (DAG is drained, no active files)
|
|
1602
|
+
# All other findings are safe to execute
|
|
1603
|
+
SWEEP_EXECUTE+=($FINDING_NUM)
|
|
1604
|
+
echo "Sweep: #${FINDING_NUM} cleared for execution (file overlap resolved)"
|
|
1605
|
+
fi
|
|
1606
|
+
done
|
|
1607
|
+
```
|
|
1608
|
+
|
|
1609
|
+
**Step 4F.3: Dispatch cleared findings**
|
|
1610
|
+
|
|
1611
|
+
For each finding in `SWEEP_EXECUTE`, add it to a fresh sweep DAG and dispatch using the same Steps 4A.pre.0, 4A.pre, and 4A logic. Run file-overlap detection between the swept findings themselves (they may conflict with each other).
|
|
1612
|
+
|
|
1613
|
+
**MANDATORY**: Use the full Step 4A agent template verbatim for each swept finding. Do NOT use a bare `prompt="Run /work-on N"` — that bypasses the label-state loop contract, source branch detection, and all pipeline enforcement rules. Swept findings are always `review-finding` issues that require source branch detection to route correctly.
|
|
1614
|
+
|
|
1615
|
+
**Step 4F.3.pre: Classify lane for each sweep finding (MANDATORY before dispatching)**
|
|
1616
|
+
|
|
1617
|
+
Run `classify-lane.sh` per finding — same pattern as Step 4A.pre:
|
|
1618
|
+
|
|
1619
|
+
```bash
|
|
1620
|
+
declare -A SWEEP_LANE
|
|
1621
|
+
declare -A SWEEP_PR_BASE
|
|
1622
|
+
|
|
1623
|
+
for FINDING_NUM in "${SWEEP_EXECUTE[@]}"; do
|
|
1624
|
+
SWEEP_BASE=$(bash ~/.claude/scripts/classify-lane.sh "$FINDING_NUM" -R {GH_REPO}) || {
|
|
1625
|
+
echo "ERROR: classify-lane.sh failed for #$FINDING_NUM — adding needs-human and skipping" >&2
|
|
1626
|
+
gh issue edit "$FINDING_NUM" -R {GH_REPO} --add-label "needs-human" 2>/dev/null || true
|
|
1627
|
+
continue
|
|
1628
|
+
}
|
|
1629
|
+
if [ "$SWEEP_BASE" = "staging" ]; then
|
|
1630
|
+
SWEEP_LANE[$FINDING_NUM]="fast-lane"
|
|
1631
|
+
else
|
|
1632
|
+
SWEEP_LANE[$FINDING_NUM]="feature-lane"
|
|
1633
|
+
fi
|
|
1634
|
+
SWEEP_PR_BASE[$FINDING_NUM]="$SWEEP_BASE"
|
|
1635
|
+
echo "#$FINDING_NUM → lane=${SWEEP_LANE[$FINDING_NUM]}, PR_BASE=$SWEEP_BASE"
|
|
1636
|
+
done
|
|
1637
|
+
```
|
|
1638
|
+
|
|
1639
|
+
**Step 4F.3.dispatch: Dispatch with full Step 4A template**
|
|
1640
|
+
|
|
1641
|
+
```bash
|
|
1642
|
+
if [ ${#SWEEP_EXECUTE[@]} -gt 0 ]; then
|
|
1643
|
+
echo "Completion sweep: dispatching ${#SWEEP_EXECUTE[@]} cleared findings"
|
|
1644
|
+
|
|
1645
|
+
# Build sweep DAG — same conflict detection as Step 3C Layers 1-4
|
|
1646
|
+
# but only among the swept findings (no original batch issues remain active)
|
|
1647
|
+
# Dispatch ready findings, monitor completions using the same Step 4B loop
|
|
1648
|
+
# This is a SINGLE pass — findings spawned during the sweep are NOT swept again
|
|
1649
|
+
# (they follow the standard Step 4C triage: queued or deferred for next run)
|
|
1650
|
+
|
|
1651
|
+
# NOTE: Step 4A.pre.0 (milestone branch pre-creation) is intentionally skipped here.
|
|
1652
|
+
# Swept findings are always review-finding issues that target an already-existing branch
|
|
1653
|
+
# (staging or a milestone branch). The branch was created when the original batch ran —
|
|
1654
|
+
# it always exists by the time the sweep runs.
|
|
1655
|
+
|
|
1656
|
+
for FINDING_NUM in "${SWEEP_EXECUTE[@]}"; do
|
|
1657
|
+
# Skip any finding whose classify-lane call failed (needs-human already set above)
|
|
1658
|
+
[ -z "${SWEEP_PR_BASE[$FINDING_NUM]:-}" ] && continue
|
|
1659
|
+
|
|
1660
|
+
FINDING_TITLE=$(gh issue view "$FINDING_NUM" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "")
|
|
1661
|
+
|
|
1662
|
+
# Build GIST_CONTEXT for sweep finding — same as Step 4A's *fallback* (raw-gist) path.
|
|
1663
|
+
# The Phase 2.5 FORGE:SYNTHESIS_BRIEF preference is intentionally NOT applied here:
|
|
1664
|
+
# sweep findings are freshly-created review-finding issues that never received a
|
|
1665
|
+
# synthesis brief (Phase 2.5 runs only over the original batch's investigations), so
|
|
1666
|
+
# there is nothing to prefer. Keep this block in sync with 4A's fallback branch only.
|
|
1667
|
+
GIST_CONTEXT=""
|
|
1668
|
+
PARENT_INV=$(gh issue view "$FINDING_NUM" -R {GH_REPO} --json body --jq '.body' \
|
|
1669
|
+
| grep -oP '(?i)parent[: ]*#\K\d+|spawned from[: ]*#\K\d+' | head -1)
|
|
1670
|
+
|
|
1671
|
+
if [ -n "$PARENT_INV" ] && [ -n "${INVESTIGATION_GISTS[$PARENT_INV]:-}" ]; then
|
|
1672
|
+
GIST_CONTEXT="
|
|
1673
|
+
**CONTEXT FROM PRIOR INVESTIGATION**: Investigation #${PARENT_INV} produced Knowledge Gist(s) with findings relevant to this issue:
|
|
1674
|
+
$(echo "${INVESTIGATION_GISTS[$PARENT_INV]}" | while IFS= read -r url; do echo "- ${url}"; done)
|
|
1675
|
+
Fetch the Gist content during the context-gathering phase for implementation guidance."
|
|
1676
|
+
fi
|
|
1677
|
+
|
|
1678
|
+
if [ -n "$MILESTONE_INDEX_URL" ]; then
|
|
1679
|
+
GIST_CONTEXT="${GIST_CONTEXT}
|
|
1680
|
+
|
|
1681
|
+
**MILESTONE KNOWLEDGE INDEX**: All investigation findings for this milestone are aggregated in a single index Gist:
|
|
1682
|
+
- ${MILESTONE_INDEX_URL}
|
|
1683
|
+
The context-gathering phase can fetch this index to discover all investigation Gists for the milestone."
|
|
1684
|
+
fi
|
|
1685
|
+
|
|
1686
|
+
# Use the full Step 4A template verbatim — copied here so sweep agents receive
|
|
1687
|
+
# the complete pipeline contract. Keep in sync with Step 4A when the template changes.
|
|
1688
|
+
Agent(
|
|
1689
|
+
subagent_type="general-purpose",
|
|
1690
|
+
model="sonnet",
|
|
1691
|
+
description="Work on {PROJECT_PREFIX}#${FINDING_NUM}",
|
|
1692
|
+
run_in_background=true,
|
|
1693
|
+
prompt="You are working on GitHub issue #${FINDING_NUM} for the {PROJECT_NAME} project.
|
|
1694
|
+
|
|
1695
|
+
**Project**: {PROJECT_NAME}
|
|
1696
|
+
**Repository**: {GH_REPO}
|
|
1697
|
+
**Repo path**: {REPO_PATH}
|
|
1698
|
+
|
|
1699
|
+
**YOUR MISSION**: Invoke \`/work-on\` via the Skill tool and let it run to completion. \`/work-on\` is a self-contained routing loop that handles the ENTIRE pipeline: investigate → build (context → architect → implement → validate) → review (push → PR → /review-pr --auto-merge) → close (project board → trajectory log → worktree cleanup). Do NOT intervene, compensate, or manually close issues — \`/work-on\` handles everything including issue closure and label updates in its close phase.
|
|
1700
|
+
|
|
1701
|
+
**CRITICAL — DO NOT STOP EARLY**: /work-on runs as a multi-phase routing loop. Each phase (investigate, build, review, close) returns an intermediate result — these are NOT completion signals. You are NOT done until the issue reaches a terminal state: \`workflow:merged\`, \`workflow:invalid\`, or \`needs-human\`. If /work-on returns after only one phase (e.g., investigation), you MUST invoke it again immediately — it will re-read GitHub state and continue to the next phase. Keep invoking /work-on until it reaches a terminal state. Never output 'done' or stop after an intermediate result.
|
|
1702
|
+
|
|
1703
|
+
**HOW REVIEW FINDINGS WORK**: /review-pr may create GitHub issues (with \`review-finding\` label) for findings it discovers. These are NOT blockers — they are separate work items that will go through their own /work-on pipeline later. The original PR should ALWAYS merge after review. The only exception is build errors (code doesn't compile) — those must be fixed before merging.
|
|
1704
|
+
|
|
1705
|
+
**IMPORTANT RULES**:
|
|
1706
|
+
- **MANDATORY**: You MUST use the Skill tool to invoke 'work-on' with args '${FINDING_NUM}'. Do NOT implement manually — /work-on handles the full pipeline including label state machine (workflow:investigating → workflow:building → workflow:in-review → workflow:merged), investigation reports, PR creation, and cleanup.
|
|
1707
|
+
- For default repo issues: \`Skill(skill='work-on', args='${FINDING_NUM}')\`
|
|
1708
|
+
- For satellite repo issues: \`Skill(skill='work-on', args='{SATELLITE_PREFIX}:${FINDING_NUM}')\` (prefix from forge.yaml → repos.satellites)
|
|
1709
|
+
- NEVER bypass /work-on with manual git/gh commands — the label updates and structured comments are critical for tracking
|
|
1710
|
+
- NEVER target \`main\` for PRs targeting the default repo. Use \`{STAGING_BRANCH}\` for fast-lane issues, or \`milestone/{slug}\` for milestone issues.
|
|
1711
|
+
- Satellite repos (MCP, n8n) have no staging branch — fast-lane PRs go to \`main\` for those.
|
|
1712
|
+
- If the issue is INVALID after investigation, close it with a comment explaining why
|
|
1713
|
+
- If you hit merge conflicts or blockers, post a comment on the issue and STOP — do not force anything
|
|
1714
|
+
- Do not interact with the user — you are running autonomously in the background
|
|
1715
|
+
- **NEVER ask the user questions** — you are a background agent. If review finds issues, auto-fix simple ones and proceed. For complex findings, merge anyway and create follow-up issues.
|
|
1716
|
+
|
|
1717
|
+
**LABEL-STATE LOOP CONTRACT — enforce after EVERY Skill return**:
|
|
1718
|
+
After EVERY \`Skill(skill='work-on', ...)\` call returns, immediately check the issue's current workflow label:
|
|
1719
|
+
\`\`\`bash
|
|
1720
|
+
gh issue view ${FINDING_NUM} -R {GH_REPO} --json labels --jq '[.labels[].name | select(startswith(\"workflow:\"))]'
|
|
1721
|
+
\`\`\`
|
|
1722
|
+
**Terminal labels** (only these allow you to stop): \`workflow:merged\`, \`workflow:invalid\`
|
|
1723
|
+
**Terminal condition also**: \`needs-human\` label present OR issue state is \`closed\`
|
|
1724
|
+
If the label is NOT terminal (e.g., \`workflow:investigating\`, \`workflow:ready-to-build\`, \`workflow:building\`, \`workflow:in-review\`), invoke \`Skill(skill='work-on', args='${FINDING_NUM}')\` again immediately. The \`/work-on\` skill will re-read GitHub state and advance to the next phase. Do NOT output a summary, do NOT pause, do NOT ask for confirmation — just invoke it again.
|
|
1725
|
+
|
|
1726
|
+
**CRITICAL — SOURCE BRANCH DETECTION**:
|
|
1727
|
+
- If the issue has the \`review-finding\` label, read the issue body for \`**Code branch**: \\\`{branch}\\\`\`
|
|
1728
|
+
- If found, that is the SOURCE_BRANCH — the code ONLY exists on that branch (e.g., \`staging\`), NOT on \`origin/main\`
|
|
1729
|
+
- Investigation MUST use \`git show origin/{SOURCE_BRANCH}:{filepath}\` to verify the code exists
|
|
1730
|
+
- Worktree MUST branch from \`origin/{SOURCE_BRANCH}\`, NOT \`origin/main\`
|
|
1731
|
+
- PR target is \`{SOURCE_BRANCH}\` (the fix goes back to where the code lives)
|
|
1732
|
+
|
|
1733
|
+
**LANE**: ${SWEEP_LANE[$FINDING_NUM]} (PR target: ${SWEEP_PR_BASE[$FINDING_NUM]})
|
|
1734
|
+
**Issue title**: ${FINDING_TITLE}
|
|
1735
|
+
${GIST_CONTEXT}
|
|
1736
|
+
"
|
|
1737
|
+
)
|
|
1738
|
+
done
|
|
1739
|
+
|
|
1740
|
+
# Monitor sweep agents using the same Step 4B completion loop
|
|
1741
|
+
# IMPORTANT: Findings spawned by sweep agents are NOT re-swept —
|
|
1742
|
+
# they follow standard Step 4C triage to prevent recursive cascades
|
|
1743
|
+
fi
|
|
1744
|
+
```
|
|
1745
|
+
|
|
1746
|
+
**Step 4F.4: Report sweep results**
|
|
1747
|
+
|
|
1748
|
+
```
|
|
1749
|
+
Completion Sweep Results:
|
|
1750
|
+
Dispatched: #{A}, #{B} (file overlap cleared after DAG drain)
|
|
1751
|
+
Still deferred (cosmetic): #{C} (comment/typo)
|
|
1752
|
+
Permanently deferred (gen2): #{D} (generation >= 2 cascade cap)
|
|
1753
|
+
```
|
|
1754
|
+
|
|
1755
|
+
**After sweep agents complete** (or if no findings were dispatched): proceed to Phase 5.
|
|
1756
|
+
|
|
1757
|
+
**Anti-patterns — DO NOT DO THIS:**
|
|
1758
|
+
- Re-sweeping findings spawned during the sweep itself — this creates unbounded recursion. Sweep is a single pass.
|
|
1759
|
+
- Overriding generation >= 2 deferrals — the cascade cap is absolute.
|
|
1760
|
+
- Skipping the sweep because "there are only a few" deferred findings — even one deferred finding represents unresolved work.
|
|
1761
|
+
|
|
1194
1762
|
---
|
|
1195
1763
|
|
|
1196
1764
|
## Phase 5: Post-Batch Cleanup
|
|
@@ -1209,7 +1777,7 @@ This will:
|
|
|
1209
1777
|
- Fix stale workflow labels on any issues the agents left behind
|
|
1210
1778
|
- Close orphaned open issues whose PRs were merged (common when merging to `staging`)
|
|
1211
1779
|
- Remove worktrees created by agents in this batch
|
|
1212
|
-
- Delete local/remote branches
|
|
1780
|
+
- Delete local/remote `fix/*` and `feat/*` branches whose PR has merged — detection uses merged-PR state (`gh pr list --state merged`) as the source of truth, so it covers feature-lane branches merged into a milestone branch as well as squash-merged branches, not just branches merged directly to staging
|
|
1213
1781
|
- Report milestones that hit 0 open issues — these are ready for `/milestone ship` (staging review + merge). Do NOT close them; closure happens after code reaches staging.
|
|
1214
1782
|
- Sync Project board state
|
|
1215
1783
|
|
|
@@ -1247,6 +1815,22 @@ done
|
|
|
1247
1815
|
|
|
1248
1816
|
Aggregate into the batch-level analytics for Step 6B.
|
|
1249
1817
|
|
|
1818
|
+
**Also collect the machine-readable summary cards** (`<!-- FORGE:CARD {json} -->`, embedded in
|
|
1819
|
+
each issue's `FORGE:TRAJECTORY` comment by `/work-on` close phase). These power the per-issue and
|
|
1820
|
+
batch cards in Step 6C:
|
|
1821
|
+
|
|
1822
|
+
```bash
|
|
1823
|
+
CARDS=""
|
|
1824
|
+
for NUM in {all_completed_issue_numbers}; do
|
|
1825
|
+
CARD=$(gh api repos/{GH_REPO}/issues/${NUM}/comments \
|
|
1826
|
+
--jq '.[] | select(.body | contains("FORGE:CARD")) | .body' 2>/dev/null \
|
|
1827
|
+
| sed -n 's/.*<!-- FORGE:CARD \(.*\) -->.*/\1/p' | head -1)
|
|
1828
|
+
[ -n "$CARD" ] && CARDS="${CARDS}${CARD}"$'\n'
|
|
1829
|
+
done
|
|
1830
|
+
# CARDS is now a newline-delimited list of per-issue JSON objects (skip any issue whose
|
|
1831
|
+
# card is absent — pre-card runs or non-merged terminal states degrade gracefully).
|
|
1832
|
+
```
|
|
1833
|
+
|
|
1250
1834
|
### Step 6B: Present consolidated report
|
|
1251
1835
|
|
|
1252
1836
|
```
|
|
@@ -1278,14 +1862,28 @@ Aggregate into the batch-level analytics for Step 6B.
|
|
|
1278
1862
|
{IF review findings were created during any agent run:}
|
|
1279
1863
|
| # | Finding | Source Issue | Source PR | Status |
|
|
1280
1864
|
|---|---------|-------------|-----------|--------|
|
|
1281
|
-
| #{F1} | {title} | #{A} | PR #{PR} | ✓ Merged (in-batch) / ⏳
|
|
1865
|
+
| #{F1} | {title} | #{A} | PR #{PR} | ✓ Merged (in-batch) / ✓ Swept (completion sweep) / ⏳ Deferred (cosmetic) / ⛔ Deferred (gen2 cascade cap) |
|
|
1282
1866
|
|
|
1283
1867
|
{ELSE: "No review findings created during this batch."}
|
|
1284
1868
|
|
|
1869
|
+
### Completion Sweep <!-- Added: forge#1105 -->
|
|
1870
|
+
- **Re-evaluated**: {N} deferred findings re-checked after DAG drain
|
|
1871
|
+
- **Dispatched**: {N} findings cleared and executed (file overlap resolved)
|
|
1872
|
+
- **Still deferred (cosmetic)**: {N} comment/typo findings (low-value, safe to leave)
|
|
1873
|
+
- **Permanently deferred (gen2)**: {N} generation >= 2 findings (cascade cap — requires manual `/work-on`)
|
|
1874
|
+
|
|
1875
|
+
{IF permanently deferred > 0:}
|
|
1876
|
+
**Action required**: The following findings were permanently deferred due to the generation >= 2 cascade cap. They will NOT be picked up automatically — run `/work-on #{N}` manually or include them in the next `/orchestrate` batch:
|
|
1877
|
+
{list of permanently deferred issue numbers and titles}
|
|
1878
|
+
|
|
1879
|
+
{IF cosmetic deferred > 0:}
|
|
1880
|
+
**Low-priority leftovers**: The following cosmetic findings remain open. They will be picked up by future runs or can be batched with `/orchestrate #{N1} #{N2}`:
|
|
1881
|
+
{list of cosmetic deferred issue numbers and titles}
|
|
1882
|
+
|
|
1285
1883
|
### Summary
|
|
1286
1884
|
- **Investigations**: {N} completed, spawned {M} new issues
|
|
1287
|
-
- **Review findings**: {N} spawned, {M} resolved in-batch, {K} deferred
|
|
1288
|
-
- **Succeeded**: {N} issues resolved (implementation)
|
|
1885
|
+
- **Review findings**: {N} spawned, {M} resolved in-batch, {K} swept, {J} deferred (cosmetic), {L} deferred (gen2)
|
|
1886
|
+
- **Succeeded**: {N} issues resolved (implementation + sweep)
|
|
1289
1887
|
- **Failed**: {N} issues need attention
|
|
1290
1888
|
- **Skipped**: {N} issues (dependency failures)
|
|
1291
1889
|
|
|
@@ -1315,7 +1913,8 @@ Aggregate into the batch-level analytics for Step 6B.
|
|
|
1315
1913
|
| Contracts posted | {N} |
|
|
1316
1914
|
| Contract→code divergences | {N} (agents that updated contract mid-build) |
|
|
1317
1915
|
| Review findings created | {N_total} |
|
|
1318
|
-
| Findings after
|
|
1916
|
+
| Findings queued after cascade control | {N_queued}/{N_total} ({N_deferred} deferred — see Step 4C) |
|
|
1917
|
+
| Competing recommendations reconciled (Phase 2.5) | {RECONCILED_COUNT} (investigation plans arbitrated in place + serialized) |
|
|
1319
1918
|
| Findings validated | {N} |
|
|
1320
1919
|
| False positives | {N} ({%}) |
|
|
1321
1920
|
| Anomalies flagged | {N} |
|
|
@@ -1323,6 +1922,8 @@ Aggregate into the batch-level analytics for Step 6B.
|
|
|
1323
1922
|
**Domain breakdown**: {N} scraping, {N} frontend, {N} billing, ...
|
|
1324
1923
|
**Routing**: {N} fast-lane, {N} feature-lane
|
|
1325
1924
|
|
|
1925
|
+
> `Findings queued after cascade control` reports Step 4C's defer/execute triage split (`QUEUED_FINDINGS` vs `DEFERRED_FINDINGS`) — it is NOT a dedup/arbitration computation; no such step exists for review findings. `Competing recommendations reconciled` is populated by Phase 2.5 (`RECONCILED_COUNT`), which reconciles investigation plans, not review findings. It is `0` when the batch had 0–1 investigations (synthesis is a no-op). <!-- Added: forge#1192, forge#1193 -->
|
|
1926
|
+
|
|
1326
1927
|
**Systematic issues** (flag if detected):
|
|
1327
1928
|
- False positive rate > 30% → review agents may need tuning
|
|
1328
1929
|
- Invalidation rate < 10% or > 35% → investigation calibration off
|
|
@@ -1337,6 +1938,56 @@ Aggregate into the batch-level analytics for Step 6B.
|
|
|
1337
1938
|
{If fast-lane: "All fixes merged to staging. Merge staging → main via GitHub web UI when ready to deploy."}
|
|
1338
1939
|
```
|
|
1339
1940
|
|
|
1941
|
+
### Step 6C: Pipeline Summary Cards (the shareable moment)
|
|
1942
|
+
|
|
1943
|
+
Render one compact summary card per completed issue (from the `CARDS` JSON collected in Step 6A),
|
|
1944
|
+
then a single batch-level summary card. These are the shareable artifacts a developer screenshots
|
|
1945
|
+
after an orchestration run. Print all cards to stdout.
|
|
1946
|
+
|
|
1947
|
+
**Per-issue card** — emit one for each JSON object in `CARDS`. Use the same box-drawing style and
|
|
1948
|
+
51-column inner width as `work-on/close.md` Phase C4.5b. Read fields directly from the JSON
|
|
1949
|
+
(`issue`, `title`, `pipeline`, `commits`, `additions`, `deletions`, `pr`, `pr_target`, `review`,
|
|
1950
|
+
`elapsed_seconds`). Truncate long titles with `…`; render `null` numeric fields as `—`. The
|
|
1951
|
+
`pipeline`/`status` field already encodes skipped phases (decomposed/invalid/blocked), so reflect
|
|
1952
|
+
it verbatim. Skip issues with no card (graceful — pre-card or non-merged runs).
|
|
1953
|
+
|
|
1954
|
+
**Batch summary card** — aggregate across all collected cards:
|
|
1955
|
+
|
|
1956
|
+
```
|
|
1957
|
+
╔═══════════════════════════════════════════════════╗
|
|
1958
|
+
║ ForgeDock Orchestration Complete ║
|
|
1959
|
+
╠═══════════════════════════════════════════════════╣
|
|
1960
|
+
║ ║
|
|
1961
|
+
║ Scope: {milestone / issue list} ║
|
|
1962
|
+
║ Issues: {N} merged · {M} blocked · {K} invalid ║
|
|
1963
|
+
║ Commits: {SUM_COMMITS} ({SUM_ADD} additions, {SUM_DEL} deletions) ║
|
|
1964
|
+
║ PRs: {N} merged ║
|
|
1965
|
+
║ Findings: {N} spawned · {M} resolved ║
|
|
1966
|
+
║ Time: {BATCH_ELAPSED} ║
|
|
1967
|
+
║ ║
|
|
1968
|
+
╚═══════════════════════════════════════════════════╝
|
|
1969
|
+
```
|
|
1970
|
+
|
|
1971
|
+
Aggregate with `jq` over the collected cards (every sum degrades gracefully — `null`/missing
|
|
1972
|
+
fields are treated as 0; never fabricate):
|
|
1973
|
+
|
|
1974
|
+
```bash
|
|
1975
|
+
echo "$CARDS" | grep -v '^$' | jq -s '{
|
|
1976
|
+
merged: (map(select(.status=="merged")) | length),
|
|
1977
|
+
blocked: (map(select(.status=="blocked")) | length),
|
|
1978
|
+
invalid: (map(select(.status=="invalid")) | length),
|
|
1979
|
+
commits: (map(.commits // 0) | add),
|
|
1980
|
+
adds: (map(.additions // 0) | add),
|
|
1981
|
+
dels: (map(.deletions // 0) | add),
|
|
1982
|
+
prs: (map(select(.pr != null)) | length),
|
|
1983
|
+
elapsed: (map(.elapsed_seconds // 0) | add)
|
|
1984
|
+
}'
|
|
1985
|
+
```
|
|
1986
|
+
|
|
1987
|
+
The batch card's `Findings:` line is filled from the review-finding counts already computed in
|
|
1988
|
+
the Summary section above. `BATCH_ELAPSED` is the wall-clock duration of the orchestration run
|
|
1989
|
+
(Step 6B `Duration`), not the sum of per-issue elapsed times.
|
|
1990
|
+
|
|
1340
1991
|
---
|
|
1341
1992
|
|
|
1342
1993
|
## Safety Rules
|
|
@@ -1349,7 +2000,7 @@ Aggregate into the batch-level analytics for Step 6B.
|
|
|
1349
2000
|
6. **Respect existing work** — skip issues already being worked on (`workflow:building`, `workflow:in-review`)
|
|
1350
2001
|
7. **Dependency failures cascade** — if A fails and B depends on A, all transitive dependents of A are skipped (not attempted)
|
|
1351
2002
|
8. **Always run post-batch cleanup** — Phase 5 is mandatory. Never skip `/cleanup all` after orchestration.
|
|
1352
|
-
9. **NEVER close/skip issues as duplicates** — only `/work-on` investigation agents can make that call after examining the actual code. The orchestrator delegates, it does not adjudicate.
|
|
2003
|
+
9. **NEVER close/skip issues as duplicates** — only `/work-on` investigation agents can make that call after examining the actual code. The orchestrator delegates, it does not adjudicate. **This is distinct from Phase 2.5 plan reconciliation**: reconciling *competing recommendations* across investigation annotations (arbitrating incompatible plans, adding `Depends on #X` serialization edges) operates only on FORGE annotations and issue bodies — it never reads code and never closes/skips/merges an issue. Both issues in a reconciled conflict still run. Adjudicating *duplicate validity* (deciding two issues are the same bug and closing one) remains forbidden. <!-- Added: forge#1192 -->
|
|
1353
2004
|
10. **Per-completion verification** — after each agent completes, check that it has `workflow:*` labels and structured comments. If an agent bypassed `/work-on`, report it as a pipeline failure.
|
|
1354
2005
|
|
|
1355
2006
|
---
|