forgedock 1.0.21 → 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 (44) hide show
  1. package/README.md +134 -80
  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/cleanup.md +85 -39
  7. package/commands/deploy-info.md +6 -2
  8. package/commands/forge-stats.md +1 -1
  9. package/commands/optimize.md +1 -1
  10. package/commands/orchestrate.md +320 -26
  11. package/commands/quality-gate.md +91 -49
  12. package/commands/review-pr-agents.md +62 -38
  13. package/commands/review-pr.md +33 -2
  14. package/commands/scope.md +11 -3
  15. package/commands/security-audit.md +24 -16
  16. package/commands/sync-ecosystem.md +2 -2
  17. package/commands/work-on/build/architect.md +47 -3
  18. package/commands/work-on/build/context.md +36 -5
  19. package/commands/work-on/build/validate.md +16 -7
  20. package/commands/work-on/close.md +129 -1
  21. package/commands/work-on/investigate.md +24 -1
  22. package/commands/work-on.md +226 -8
  23. package/docs/site/annotations-explained.md +55 -0
  24. package/docs/site/command-learning-path.md +104 -0
  25. package/docs/site/command-reference.md +3 -0
  26. package/docs/site/getting-started.md +36 -14
  27. package/docs/site/troubleshooting.md +208 -0
  28. package/docs/spec/forge-protocol-v1.md +768 -0
  29. package/examples/forgedock-demo/README.md +113 -0
  30. package/examples/forgedock-demo/bootstrap.sh +125 -0
  31. package/examples/forgedock-demo/issues/01-bug-delete-missing-auth.md +37 -0
  32. package/examples/forgedock-demo/issues/02-feature-filter-notes.md +37 -0
  33. package/examples/forgedock-demo/issues/03-refactor-router-table.md +33 -0
  34. package/examples/forgedock-demo/issues/04-perf-index-lookup.md +35 -0
  35. package/examples/forgedock-demo/issues/05-docs-api-reference.md +39 -0
  36. package/examples/forgedock-demo/labels.json +21 -0
  37. package/examples/forgedock-demo/package.json +16 -0
  38. package/examples/forgedock-demo/scripts/smoke.js +61 -0
  39. package/examples/forgedock-demo/src/auth.js +28 -0
  40. package/examples/forgedock-demo/src/db.js +66 -0
  41. package/examples/forgedock-demo/src/routes/notes.js +64 -0
  42. package/examples/forgedock-demo/src/server.js +79 -0
  43. package/package.json +5 -1
  44. 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)
@@ -853,32 +1060,46 @@ If the label is NOT terminal (e.g., `workflow:investigating`, `workflow:ready-to
853
1060
  )
854
1061
  ```
855
1062
 
856
- **`{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 -->
857
1064
 
858
1065
  ```bash
859
1066
  # Build GIST_CONTEXT for an issue
860
1067
  GIST_CONTEXT=""
861
- PARENT_INV=$(gh issue view {NUMBER} -R {GH_REPO} --json body --jq '.body' \
862
- | grep -oP '(?i)parent[: ]*#\K\d+|spawned from[: ]*#\K\d+' | head -1)
863
1068
 
864
- 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.
865
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="
866
1086
  **CONTEXT FROM PRIOR INVESTIGATION**: Investigation #${PARENT_INV} produced Knowledge Gist(s) with findings relevant to this issue:
867
1087
  $(echo "${INVESTIGATION_GISTS[$PARENT_INV]}" | while IFS= read -r url; do echo "- ${url}"; done)
868
1088
  Fetch the Gist content during the context-gathering phase for implementation guidance."
869
- fi
1089
+ fi
870
1090
 
871
- # Include milestone index URL if available (from Step 2C.5)
872
- if [ -n "$MILESTONE_INDEX_URL" ]; then
873
- 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}
874
1094
 
875
1095
  **MILESTONE KNOWLEDGE INDEX**: All investigation findings for this milestone are aggregated in a single index Gist:
876
1096
  - ${MILESTONE_INDEX_URL}
877
1097
  The context-gathering phase can fetch this index to discover all investigation Gists for the milestone."
1098
+ fi
878
1099
  fi
879
1100
  ```
880
1101
 
881
- 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 -->
882
1103
 
883
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:
884
1105
 
@@ -1438,7 +1659,11 @@ if [ ${#SWEEP_EXECUTE[@]} -gt 0 ]; then
1438
1659
 
1439
1660
  FINDING_TITLE=$(gh issue view "$FINDING_NUM" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "")
1440
1661
 
1441
- # Build GIST_CONTEXT for sweep finding — same as Step 4A generation block
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.
1442
1667
  GIST_CONTEXT=""
1443
1668
  PARENT_INV=$(gh issue view "$FINDING_NUM" -R {GH_REPO} --json body --jq '.body' \
1444
1669
  | grep -oP '(?i)parent[: ]*#\K\d+|spawned from[: ]*#\K\d+' | head -1)
@@ -1552,7 +1777,7 @@ This will:
1552
1777
  - Fix stale workflow labels on any issues the agents left behind
1553
1778
  - Close orphaned open issues whose PRs were merged (common when merging to `staging`)
1554
1779
  - Remove worktrees created by agents in this batch
1555
- - 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
1556
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.
1557
1782
  - Sync Project board state
1558
1783
 
@@ -1590,6 +1815,22 @@ done
1590
1815
 
1591
1816
  Aggregate into the batch-level analytics for Step 6B.
1592
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
+
1593
1834
  ### Step 6B: Present consolidated report
1594
1835
 
1595
1836
  ```
@@ -1672,7 +1913,8 @@ Aggregate into the batch-level analytics for Step 6B.
1672
1913
  | Contracts posted | {N} |
1673
1914
  | Contract→code divergences | {N} (agents that updated contract mid-build) |
1674
1915
  | Review findings created | {N_total} |
1675
- | 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) |
1676
1918
  | Findings validated | {N} |
1677
1919
  | False positives | {N} ({%}) |
1678
1920
  | Anomalies flagged | {N} |
@@ -1680,6 +1922,8 @@ Aggregate into the batch-level analytics for Step 6B.
1680
1922
  **Domain breakdown**: {N} scraping, {N} frontend, {N} billing, ...
1681
1923
  **Routing**: {N} fast-lane, {N} feature-lane
1682
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
+
1683
1927
  **Systematic issues** (flag if detected):
1684
1928
  - False positive rate > 30% → review agents may need tuning
1685
1929
  - Invalidation rate < 10% or > 35% → investigation calibration off
@@ -1694,6 +1938,56 @@ Aggregate into the batch-level analytics for Step 6B.
1694
1938
  {If fast-lane: "All fixes merged to staging. Merge staging → main via GitHub web UI when ready to deploy."}
1695
1939
  ```
1696
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
+
1697
1991
  ---
1698
1992
 
1699
1993
  ## Safety Rules
@@ -1706,7 +2000,7 @@ Aggregate into the batch-level analytics for Step 6B.
1706
2000
  6. **Respect existing work** — skip issues already being worked on (`workflow:building`, `workflow:in-review`)
1707
2001
  7. **Dependency failures cascade** — if A fails and B depends on A, all transitive dependents of A are skipped (not attempted)
1708
2002
  8. **Always run post-batch cleanup** — Phase 5 is mandatory. Never skip `/cleanup all` after orchestration.
1709
- 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 -->
1710
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.
1711
2005
 
1712
2006
  ---