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.
Files changed (49) hide show
  1. package/README.md +146 -77
  2. package/bin/demo.mjs +386 -0
  3. package/bin/forgedock.mjs +357 -30
  4. package/bin/init-detect.mjs +95 -0
  5. package/bin/runner.mjs +780 -0
  6. package/commands/ci-audit.md +469 -0
  7. package/commands/cleanup.md +85 -39
  8. package/commands/deploy-info.md +6 -2
  9. package/commands/forge-stats.md +1 -1
  10. package/commands/optimize.md +1 -1
  11. package/commands/orchestrate.md +684 -33
  12. package/commands/quality-gate.md +166 -46
  13. package/commands/review-pr-agents.md +106 -34
  14. package/commands/review-pr.md +33 -2
  15. package/commands/scope.md +11 -3
  16. package/commands/security-audit.md +24 -16
  17. package/commands/sync-ecosystem.md +2 -2
  18. package/commands/work-on/build/architect.md +47 -3
  19. package/commands/work-on/build/context.md +36 -5
  20. package/commands/work-on/build/validate.md +16 -7
  21. package/commands/work-on/close.md +129 -1
  22. package/commands/work-on/investigate.md +24 -1
  23. package/commands/work-on.md +226 -8
  24. package/docs/comparison.md +135 -0
  25. package/docs/design/developer-objections-ai-context-tools.md +244 -0
  26. package/docs/design/messaging-angles-research.md +179 -0
  27. package/docs/design/show-hn-draft.md +86 -0
  28. package/docs/site/annotations-explained.md +55 -0
  29. package/docs/site/command-learning-path.md +104 -0
  30. package/docs/site/command-reference.md +3 -0
  31. package/docs/site/getting-started.md +36 -14
  32. package/docs/site/troubleshooting.md +208 -0
  33. package/docs/spec/forge-protocol-v1.md +768 -0
  34. package/examples/forgedock-demo/README.md +113 -0
  35. package/examples/forgedock-demo/bootstrap.sh +125 -0
  36. package/examples/forgedock-demo/issues/01-bug-delete-missing-auth.md +37 -0
  37. package/examples/forgedock-demo/issues/02-feature-filter-notes.md +37 -0
  38. package/examples/forgedock-demo/issues/03-refactor-router-table.md +33 -0
  39. package/examples/forgedock-demo/issues/04-perf-index-lookup.md +35 -0
  40. package/examples/forgedock-demo/issues/05-docs-api-reference.md +39 -0
  41. package/examples/forgedock-demo/labels.json +21 -0
  42. package/examples/forgedock-demo/package.json +16 -0
  43. package/examples/forgedock-demo/scripts/smoke.js +61 -0
  44. package/examples/forgedock-demo/src/auth.js +28 -0
  45. package/examples/forgedock-demo/src/db.js +66 -0
  46. package/examples/forgedock-demo/src/routes/notes.js +64 -0
  47. package/examples/forgedock-demo/src/server.js +79 -0
  48. package/package.json +6 -2
  49. package/templates/forge.yaml.minimal +44 -0
@@ -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
- **For issues WITHOUT an investigation comment**, fall back to parsing the issue body for file paths:
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
- ```bash
322
- gh issue view $NUM --json body --jq '.body' | grep -oP '`[^`]*\.(py|tsx?|jsx?|sql|json|ya?ml)`' | sort -u
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 four layers:
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
- - **No artificial concurrency limit** — all issues with empty predecessor sets dispatch simultaneously. The only constraints are file overlap and explicit dependencies.
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, check if it was spawned by an investigation that has a Knowledge Gist (from Step 2C.5). If so, include the Gist URL(s) in the agent prompt:
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
- if [ -n "$PARENT_INV" ] && [ -n "${INVESTIGATION_GISTS[$PARENT_INV]:-}" ]; then
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
- GIST_CONTEXT="${GIST_CONTEXT}
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 or milestone index found), the variable resolves to a blank line in the template — no impact on the agent prompt. <!-- Updated: forge#341 -->
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="{AGENT_ID}",
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 3+ times and still hasn't reached a terminal state, report it as a failure — do not resume again.
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="{AGENT_ID}", run_in_background=true, ...)
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
- They remain open in GitHub — they will be picked up by future `/orchestrate` or `/work-on` runs. Log them in the final batch summary under "deferred" so they are visible. Do NOT close or label them — leave them for the next pipeline pass.
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 for merged PRs
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) / ⏳ Open (deferred) |
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 synthesis | {N} (deduplicated/arbitrated) |
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
  ---