forgedock 1.16.2 → 1.18.0

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 (114) hide show
  1. package/bin/engine/outcome-codes.mjs +114 -0
  2. package/bin/engine/phases.mjs +211 -100
  3. package/bin/engine/projector.mjs +48 -1
  4. package/bin/engine/runlog.mjs +20 -5
  5. package/bin/engine/telemetry-seam.mjs +62 -0
  6. package/bin/engine/telemetry-seam.test.mjs +60 -0
  7. package/bin/engine/telemetry.mjs +148 -0
  8. package/bin/engine/telemetry.test.mjs +120 -0
  9. package/bin/engine-cli.mjs +17 -1
  10. package/bin/engine.mjs +227 -48
  11. package/bin/forgedock.mjs +8 -2
  12. package/bin/hooks/interactive-engine.mjs +10 -0
  13. package/bin/hooks/pre-tool-use.mjs +220 -0
  14. package/bin/runner.mjs +70 -4
  15. package/bin/stats/aggregate.mjs +231 -0
  16. package/bin/stats/cache.mjs +40 -0
  17. package/bin/stats/github-source.mjs +211 -0
  18. package/bin/stats/index.mjs +154 -0
  19. package/bin/stats/local-source.mjs +79 -0
  20. package/bin/stats/package.json +6 -0
  21. package/bin/stats/render.mjs +82 -0
  22. package/bin/stats/stats.test.mjs +312 -0
  23. package/commands/cleanup.md +90 -2
  24. package/commands/deploy-info.md +8 -4
  25. package/commands/deploy-pr.md +64 -0
  26. package/commands/fix-ci.md +20 -6
  27. package/commands/milestone.md +34 -17
  28. package/commands/orchestrate/phase-1-resolve.md +4 -1
  29. package/commands/orchestrate/phase-4-execution.md +28 -6
  30. package/commands/orchestrate/phase-5-cleanup.md +18 -2
  31. package/commands/pipeline-health/phase-1-context.md +2 -0
  32. package/commands/pipeline-health/phase-2-metrics.md +12 -5
  33. package/commands/pipeline-status.md +21 -1
  34. package/commands/review-pr-agents/protocols.md +1 -0
  35. package/commands/review-pr-agents.md +10 -2
  36. package/commands/review-pr-staging.md +196 -496
  37. package/commands/review-pr.md +512 -3134
  38. package/commands/test-gate.md +25 -10
  39. package/commands/work-on/build/architect.md +13 -1
  40. package/commands/work-on/build/implement.md +28 -6
  41. package/commands/work-on/build/validate.md +5 -2
  42. package/commands/work-on/build.md +197 -145
  43. package/commands/work-on/close.md +66 -20
  44. package/commands/work-on/decompose.md +113 -34
  45. package/commands/work-on/investigate.md +15 -14
  46. package/commands/work-on/remediate.md +49 -19
  47. package/commands/work-on/review.md +7 -5
  48. package/commands/work-on.md +1 -2
  49. package/docs/CONFIG.md +76 -24
  50. package/docs/OUTCOME-CODES.md +53 -0
  51. package/docs/RELEASE-NOTES.md +24 -0
  52. package/docs/STATS.md +63 -0
  53. package/docs/TELEMETRY.md +65 -0
  54. package/docs/WORK-ON-RUNTIME.md +19 -0
  55. package/docs/site/command-reference.md +1 -1
  56. package/docs/spec/forge-protocol-v1.md +2 -2
  57. package/docs/spec/label-state-machine.md +1 -1
  58. package/package.json +3 -2
  59. package/packages/protocol/src/phases.js +5 -5
  60. package/scripts/builder-resume-match.test.sh +58 -0
  61. package/scripts/cap-findings.sh +110 -0
  62. package/scripts/cap-findings.test.sh +130 -0
  63. package/scripts/check-spec-bash.sh +5 -1
  64. package/scripts/check-spec-bash.test.sh +20 -0
  65. package/scripts/check-spec-markers.sh +2 -0
  66. package/scripts/classify-finding.sh +65 -16
  67. package/scripts/classify-finding.test.sh +43 -1
  68. package/scripts/classify-grep-exit.sh +20 -0
  69. package/scripts/classify-grep-exit.test.sh +46 -0
  70. package/scripts/close-parent-c3.test.sh +123 -0
  71. package/scripts/cost-report.sh +146 -0
  72. package/scripts/cost-report.test.sh +80 -0
  73. package/scripts/diff-size.sh +97 -3
  74. package/scripts/diff-size.test.sh +59 -2
  75. package/scripts/doctor-pipeline-state.sh +43 -0
  76. package/scripts/file-deferred-followups.sh +159 -0
  77. package/scripts/file-deferred-followups.test.sh +106 -0
  78. package/scripts/fixtures/cost-report/a-merged1.jsonl +6 -0
  79. package/scripts/fixtures/cost-report/a-merged2-remediated.jsonl +7 -0
  80. package/scripts/fixtures/cost-report/a-needs-human.jsonl +4 -0
  81. package/scripts/fixtures/cost-report/b-merged.jsonl +3 -0
  82. package/scripts/fixtures/cost-report/c-merged-attempt-usage.jsonl +8 -0
  83. package/scripts/fixtures/cost-report/hydrated-no-usage.jsonl +3 -0
  84. package/scripts/fixtures/cost-report/old-log-no-version.jsonl +3 -0
  85. package/scripts/fixtures/cost-report/unfinished-no-terminal.jsonl +2 -0
  86. package/scripts/fixtures/cost-report/unfinished-reopened.jsonl +5 -0
  87. package/scripts/forge-root.test.sh +18 -11
  88. package/scripts/lib/fence-bootstrap.sh +4 -3
  89. package/scripts/lib/pr-issue-number.sh +18 -0
  90. package/scripts/lib/review-template-resolve.sh +41 -0
  91. package/scripts/lib/telemetry.sh +168 -0
  92. package/scripts/publish-staging-sync.test.sh +48 -0
  93. package/scripts/remediate-liveness.sh +42 -6
  94. package/scripts/resolve-deploy-gate.sh +32 -6
  95. package/scripts/resolve-deploy-gate.test.sh +42 -1
  96. package/scripts/review-acceptance-gate.sh +16 -7
  97. package/scripts/review-acceptance-gate.test.sh +13 -0
  98. package/scripts/review-finalize.sh +1427 -0
  99. package/scripts/review-finalize.test.sh +833 -0
  100. package/scripts/review-pr-delta-wiring.test.sh +19 -72
  101. package/scripts/review-pr-domain-map.test.sh +23 -13
  102. package/scripts/review-pr-gate2.test.sh +24 -65
  103. package/scripts/review-pr-staging-finding-filter.test.sh +44 -0
  104. package/scripts/review-pr-staging-scope.test.sh +56 -80
  105. package/scripts/review-prepare.sh +553 -0
  106. package/scripts/review-prepare.test.sh +288 -0
  107. package/scripts/review-roster.sh +186 -0
  108. package/scripts/review-roster.test.sh +185 -0
  109. package/scripts/run-shell-tests.sh +3 -0
  110. package/scripts/telemetry.test.sh +106 -0
  111. package/scripts/trusted-comments.test.sh +9 -2
  112. package/scripts/wait-ci-green.sh +8 -1
  113. package/scripts/wait-ci-green.test.sh +3 -3
  114. package/templates/forge.yaml.minimal +9 -0
@@ -412,11 +412,18 @@ Before shipping, ensure the milestone branch is up to date with `staging` (picks
412
412
  ```bash
413
413
  cd {REPO_PATH}
414
414
  git fetch origin {STAGING_BRANCH} milestone/{slug}
415
- git checkout milestone/{slug}
416
- git merge origin/{STAGING_BRANCH} --no-edit
417
- # If conflicts, report to user and STOP — do not auto-resolve
418
- git push origin milestone/{slug}
419
- git checkout {STAGING_BRANCH}
415
+ # Never mutate the main checkout: sync in a detached worktree at the milestone tip
416
+ SYNC_WT=$(mktemp -d)
417
+ git worktree add --detach "$SYNC_WT" origin/milestone/{slug}
418
+ if git -C "$SYNC_WT" merge origin/{STAGING_BRANCH} --no-edit; then
419
+ git -C "$SYNC_WT" push origin HEAD:refs/heads/milestone/{slug}
420
+ git worktree remove --force "$SYNC_WT"
421
+ else
422
+ # Conflicts: report to user and STOP — do not auto-resolve
423
+ git -C "$SYNC_WT" merge --abort 2>/dev/null || true
424
+ git worktree remove --force "$SYNC_WT"
425
+ echo "STOP: merge conflict syncing milestone/{slug} with {STAGING_BRANCH}"; exit 1
426
+ fi
420
427
  ```
421
428
 
422
429
  ### Step 2.5: Pre-Merge Hunk-Loss Audit (MANDATORY before creating PR)
@@ -425,6 +432,7 @@ git checkout {STAGING_BRANCH}
425
432
 
426
433
  This step identifies "at-risk hunks" — staging-only content in files the milestone also modifies — and absorbs them into the milestone branch via rebase before the PR is created. After the rebase, even a squash merge will preserve all staging content because it's now part of the milestone branch's commits.
427
434
 
435
+ <!-- allowlist:check-spec-bash -->
428
436
  ```bash
429
437
  cd {REPO_PATH}
430
438
  git fetch origin {STAGING_BRANCH} milestone/{slug}
@@ -453,18 +461,19 @@ else
453
461
  echo "$AT_RISK" | grep '^--- a/' | sed 's|^--- a/||'
454
462
 
455
463
  echo "Rebasing milestone/{slug} onto origin/{STAGING_BRANCH} to absorb at-risk hunks..."
456
- git checkout milestone/{slug}
457
- git rebase origin/{STAGING_BRANCH}
464
+ REBASE_WT=$(mktemp -d)
465
+ git worktree add --detach "$REBASE_WT" origin/milestone/{slug}
466
+ git -C "$REBASE_WT" rebase origin/{STAGING_BRANCH}
458
467
 
459
468
  REBASE_EXIT=$?
460
469
  if [ "$REBASE_EXIT" -ne 0 ]; then
461
470
  # Capture conflicting files BEFORE aborting (after abort, unmerged state is gone)
462
- CONFLICTING_FILES=$(git diff --name-only --diff-filter=U 2>/dev/null)
471
+ CONFLICTING_FILES=$(git -C "$REBASE_WT" diff --name-only --diff-filter=U 2>/dev/null)
463
472
  [ -z "$CONFLICTING_FILES" ] && CONFLICTING_FILES="unknown — check git status after resolving manually"
464
473
 
465
474
  # Abort the failed rebase and return to a clean state
466
- git rebase --abort 2>/dev/null || true
467
- git checkout {STAGING_BRANCH}
475
+ git -C "$REBASE_WT" rebase --abort 2>/dev/null || true
476
+ git worktree remove --force "$REBASE_WT"
468
477
 
469
478
  # Truncate AT_RISK to safe size for issue body (avoid shell quoting issues with large diffs)
470
479
  AT_RISK_SNIPPET=$(echo "$AT_RISK" | head -60 | sed "s/'/'\\\\''/g")
@@ -519,6 +528,7 @@ ISSUE_EOF
519
528
  ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"fix(milestone): rebase conflict during hunk-loss audit for milestone/{slug}\" --body-file \"${REBASE_ISSUE_BODY_FILE}\" --label \"bug\" --label \"priority:P0\" --label \"needs-human\""))
520
529
  ```
521
530
 
531
+ <!-- allowlist:check-spec-bash -->
522
532
  ```bash
523
533
  echo "STOP: Rebase conflict. Created issue for manual resolution (see /issue output above). Do NOT proceed to PR creation."
524
534
  echo "Run '/milestone ship {slug}' again after resolving the conflict."
@@ -526,9 +536,9 @@ ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"fi
526
536
  fi
527
537
 
528
538
  # Rebase succeeded — push the rebased branch
529
- git push origin milestone/{slug} --force-with-lease
539
+ git -C "$REBASE_WT" push origin HEAD:refs/heads/milestone/{slug} --force-with-lease
540
+ git worktree remove --force "$REBASE_WT"
530
541
  echo "Rebase complete. Milestone branch now includes all $AT_RISK_COUNT at-risk staging hunks."
531
- git checkout {STAGING_BRANCH}
532
542
  fi
533
543
  fi
534
544
  ```
@@ -716,11 +726,18 @@ Syncs the milestone branch with the latest default branch to pick up fast-lane f
716
726
  cd {REPO_PATH}
717
727
  DEFAULT_BRANCH=$(yq '.branches.default' "$CONFIG_FILE")
718
728
  git fetch origin $DEFAULT_BRANCH milestone/{slug}
719
- git checkout milestone/{slug}
720
- git merge origin/$DEFAULT_BRANCH --no-edit
721
- # If conflicts, report to user and STOP
722
- git push origin milestone/{slug}
723
- git checkout $DEFAULT_BRANCH
729
+ # Never mutate the main checkout: sync in a detached worktree at the milestone tip
730
+ SYNC_WT=$(mktemp -d)
731
+ git worktree add --detach "$SYNC_WT" origin/milestone/{slug}
732
+ if git -C "$SYNC_WT" merge origin/$DEFAULT_BRANCH --no-edit; then
733
+ git -C "$SYNC_WT" push origin HEAD:refs/heads/milestone/{slug}
734
+ git worktree remove --force "$SYNC_WT"
735
+ else
736
+ # If conflicts, report to user and STOP
737
+ git -C "$SYNC_WT" merge --abort 2>/dev/null || true
738
+ git worktree remove --force "$SYNC_WT"
739
+ echo "STOP: merge conflict syncing milestone/{slug} with $DEFAULT_BRANCH"; exit 1
740
+ fi
724
741
  ```
725
742
 
726
743
  Report: "Synced milestone/{slug} with latest `{DEFAULT_BRANCH}`. {N} new commits incorporated."
@@ -260,7 +260,7 @@ gh issue list -R {SATELLITE_REPO} --state open --limit 500 --json number,title,l
260
260
  ### Filter out ineligible issues
261
261
 
262
262
  Remove issues that should NOT be worked on:
263
- - Already closed or has `workflow:merged` label
263
+ - Already closed or has `workflow:merged` label (exception: an explicitly requested closed `workflow:decomposed` parent is expanded first, see below)
264
264
  - Has `workflow:invalid` label
265
265
  - Has `needs-human` label (requires manual action)
266
266
  - Has `workflow:decomposed` label (parent tracker — its sub-issues should be picked up instead)
@@ -269,6 +269,9 @@ Remove issues that should NOT be worked on:
269
269
 
270
270
  If a `workflow:decomposed` issue is found, automatically expand it to its open sub-issues instead.
271
271
 
272
+ <!-- Added: forge#3736 -->
273
+ **Closed decomposed parents**: with `decomposition.close_parent` enabled, a decomposed parent is CLOSED at decomposition and keeps `workflow:decomposed`. Run this expansion BEFORE the "already closed" filter: when an explicitly requested issue is a closed `workflow:decomposed` parent, expand it to its open children (parse the `- [ ] #N` and `- [x] #N` lines of the Sub-Issue Tracker in its body, keep those whose state is OPEN) instead of dropping it as closed. A closed decomposed parent with no open children is reported as complete and skipped. A closed parent without `workflow:decomposed` is still dropped.
274
+
272
275
  ### Source-PR Triage Hint (pre-flight, non-binding)
273
276
 
274
277
  <!-- Added: forge#2351 -->
@@ -53,6 +53,8 @@ fi
53
53
  # --- End lease gate ---
54
54
  ```
55
55
 
56
+ **Board closure ownership** <!-- Added: forge#3699 -->: lease takeover never reclaims or closes a claims board (each batch creates its own, Step 3D.1). Closing it is owned by Phase 5 Step 5C and, for batches that never reach it, `/cleanup` Phase 2C.
57
+
56
58
  **Lease release**: `LEASE_RELEASED_THIS_SESSION` (declared just below, alongside `release_orchestrator_lease()`) guards against double-posting `FORGE:LEASE_RELEASED`. The release call is invoked from both the **Step 4A-pre.-0.5 "Stopping the orchestrator"** procedure (interrupted path) and the **Termination condition** at the end of Step 4B (normal clean/paused drain) — both exit paths route through the same idempotent function so the lease is never left dangling either way.
57
59
 
58
60
  ```bash
@@ -1609,6 +1611,12 @@ classify_predecessor_state() {
1609
1611
  PRED_LABELS=$(echo "$PRED_INFO" | jq -r '.workflow[]?' 2>/dev/null)
1610
1612
  fi
1611
1613
 
1614
+ # forge#3566: age bound for the remediating / engine-error IN_PROGRESS arms below. Resolved here, not once at
1615
+ # Step 4 setup, because wake reconstruction calls this helper from a fresh shell. Non-numeric falls back to 48.
1616
+ local IN_PROGRESS_MAX_HOURS
1617
+ IN_PROGRESS_MAX_HOURS=$(yq '.orchestration.in_progress_max_hours // 48' forge.yaml 2>/dev/null || echo 48)
1618
+ case "$IN_PROGRESS_MAX_HOURS" in ''|*[!0-9]*) IN_PROGRESS_MAX_HOURS=48 ;; esac
1619
+
1612
1620
  # reverify_merged_trail: re-run the phase-trail verifier for a merged predecessor that has NO escalation
1613
1621
  # record (forge#3156), so wake reconstruction does not trust the `workflow:merged` label alone. Prints OK
1614
1622
  # when the trail verifies, UNVERIFIED otherwise. Fails closed: a wake runs in a fresh shell where FORGE_ROOT is
@@ -1671,7 +1679,10 @@ classify_predecessor_state() {
1671
1679
  # forge#3541: autonomous remediation (ci-gate / in-pr-fix / base-sync) is pending or running. No human decision is
1672
1680
  # needed, so this is NOT GATED: dependents keep waiting exactly as for any mid-pipeline predecessor, and item 6.4
1673
1681
  # (or the engine handoff) drives the fix. A concurrent `needs-human` is matched by the GATED arm above first.
1674
- echo "IN_PROGRESS"
1682
+ # Time-boxed (forge#3566): past orchestration.in_progress_max_hours (default 48) [IN_PROGRESS_MAX_HOURS, resolved from forge.yaml] the state is stranded, not in flight.
1683
+ PRED_AGE_H=$(gh issue view "$PRED" -R {GH_REPO} --json updatedAt --jq '((now - (.updatedAt | fromdateiso8601)) / 3600 | floor)' 2>/dev/null || echo "")
1684
+ case "$PRED_AGE_H" in ''|*[!0-9]*) PRED_AGE_H=0 ;; esac # unreadable age keeps IN_PROGRESS (fail safe)
1685
+ if [ "$PRED_AGE_H" -ge "${IN_PROGRESS_MAX_HOURS:-48}" ]; then echo "GATED"; else echo "IN_PROGRESS"; fi
1675
1686
  elif echo "$PRED_LABELS" | grep -qx "workflow:engine-error"; then
1676
1687
  # forge#2261: the engine/tool itself broke on this run (e.g. a fail-fast
1677
1688
  # CLI_BACKEND_FAILED, or an exhausted retry loop where the runner never
@@ -1688,7 +1699,11 @@ classify_predecessor_state() {
1688
1699
  # ENGINE_DISPATCH_MAP-tracked issues (fresh Agent-spawn fallback on empty
1689
1700
  # committed state) — 2a's Agent(resume=...) alone cannot reach them, since
1690
1701
  # they were never in AGENT_ISSUE_MAP.
1691
- echo "IN_PROGRESS"
1702
+ # Time-boxed (forge#3566): past orchestration.in_progress_max_hours (default 48) [IN_PROGRESS_MAX_HOURS, resolved from forge.yaml] auto-resume has evidently
1703
+ # not recovered it; classify GATED so a human is told instead of dependents waiting forever.
1704
+ PRED_AGE_H=$(gh issue view "$PRED" -R {GH_REPO} --json updatedAt --jq '((now - (.updatedAt | fromdateiso8601)) / 3600 | floor)' 2>/dev/null || echo "")
1705
+ case "$PRED_AGE_H" in ''|*[!0-9]*) PRED_AGE_H=0 ;; esac # unreadable age keeps IN_PROGRESS (fail safe)
1706
+ if [ "$PRED_AGE_H" -ge "${IN_PROGRESS_MAX_HOURS:-48}" ]; then echo "GATED"; else echo "IN_PROGRESS"; fi
1692
1707
  elif [ "$PRED_STATE" = "CLOSED" ]; then
1693
1708
  # Closed with no workflow:invalid/merged label (e.g. closed-not-planned) — treat as DONE,
1694
1709
  # not a new deadlock state; there is no pending code for dependents to wait on.
@@ -3928,7 +3943,10 @@ if [ -n "$TS_CHANGED" ]; then
3928
3943
  echo "=== Integration Build Gate (TypeScript): batch checkpoint ==="
3929
3944
  cd {REPO_PATH}
3930
3945
  git fetch origin ${MILESTONE_BRANCH}
3931
- git checkout origin/${MILESTONE_BRANCH} --detach 2>/dev/null
3946
+ # Detached worktree: never check out in the main checkout
3947
+ GATE_WT=$(mktemp -d)
3948
+ git worktree add --detach "$GATE_WT" "origin/${MILESTONE_BRANCH}" 2>/dev/null
3949
+ cd "$GATE_WT"
3932
3950
 
3933
3951
  if [ -n "$TS_TYPECHECK" ]; then
3934
3952
  eval "$TS_TYPECHECK" 2>&1 | head -30
@@ -3946,7 +3964,8 @@ if [ -n "$TS_CHANGED" ]; then
3946
3964
  BUILD_EXIT=0
3947
3965
  fi
3948
3966
 
3949
- git checkout - 2>/dev/null
3967
+ cd {REPO_PATH}
3968
+ git worktree remove --force "$GATE_WT" 2>/dev/null || true
3950
3969
 
3951
3970
  if [ "$TSC_EXIT" -ne 0 ]; then
3952
3971
  echo "BLOCKING: TypeScript errors on ${MILESTONE_BRANCH} after batch checkpoint."
@@ -3962,7 +3981,9 @@ PY_CHANGED=$(git diff origin/{DEFAULT_BRANCH}...origin/${MILESTONE_BRANCH} --nam
3962
3981
  if [ -n "$PY_CHANGED" ]; then
3963
3982
  echo "=== Integration Build Gate (Python): batch checkpoint ==="
3964
3983
  cd {REPO_PATH}
3965
- git checkout origin/${MILESTONE_BRANCH} --detach 2>/dev/null
3984
+ GATE_WT=$(mktemp -d)
3985
+ git worktree add --detach "$GATE_WT" "origin/${MILESTONE_BRANCH}" 2>/dev/null
3986
+ cd "$GATE_WT"
3966
3987
 
3967
3988
  if [ -n "$PYTHON_FORMAT" ]; then
3968
3989
  eval "$PYTHON_FORMAT" 2>&1 | tail -10
@@ -3972,7 +3993,8 @@ if [ -n "$PY_CHANGED" ]; then
3972
3993
  FORMAT_EXIT=0
3973
3994
  fi
3974
3995
 
3975
- git checkout - 2>/dev/null
3996
+ cd {REPO_PATH}
3997
+ git worktree remove --force "$GATE_WT" 2>/dev/null || true
3976
3998
 
3977
3999
  if [ "$FORMAT_EXIT" -ne 0 ]; then
3978
4000
  echo "WARNING: Python formatting issues on ${MILESTONE_BRANCH} after batch checkpoint."
@@ -44,13 +44,24 @@ Include the audit summary in the final report (Phase 6). Key metrics to surface:
44
44
 
45
45
  ### Step 5C: Close the coordination issue (claims board) <!-- Added: forge#2072 -->
46
46
 
47
- **Why this step exists**: Step 3D.1 (`phase-3-dependency.md`) creates a per-batch coordination issue (the claims board) and exports `FORGE_COORD_ISSUE`/`COORD_ISSUE_NUMBER`. Step 5A's `/cleanup` sweep only closes orphaned issues whose PR merged — the claims board has no PR of its own, so that heuristic never matches it. Without this step, one `automation`-labelled issue leaks per orchestration batch.
47
+ **Why this step exists**: Step 3D.1 (`phase-3-dependency.md`) creates a per-batch coordination issue (the claims board) and exports `FORGE_COORD_ISSUE`/`COORD_ISSUE_NUMBER`. Step 5A's `/cleanup` orphan sweep (Phase 2A/2B) only closes issues whose PR merged — the claims board has no PR of its own, so that heuristic never matches it; `/cleanup` Phase 2C is the separate safety net that closes boards this step missed. Without this step, one `automation`-labelled issue leaks per orchestration batch.
48
48
 
49
49
  **Run after Step 5B, before Step 5D.** No-ops cleanly if the claims board was never created this run (`COORD_ISSUE_NUMBER` unset/empty — e.g. Step 3D.1's `gh issue create` failed, or this batch never reached DAG construction). Tolerates GitHub API failures without aborting the rest of Phase 5 — the same tolerant-failure convention (`2>/dev/null || echo ...`, `|| true`) used throughout `phase-3-dependency.md`/`phase-4-execution.md`.
50
50
 
51
51
  **Orchestrator lease should already be released by this point** (forge#2627): `phase-4-execution.md`'s Termination condition (end of Step 4B) calls `release_orchestrator_lease()` before handing off to Phase 5 on a normal drain, and the "Stopping the orchestrator" procedure (Step 4A-pre.-0.5) does the same on an interrupted exit. Closing the coordination issue here does not depend on lease state either way — an unreleased lease on an issue about to be closed is harmless (a future batch creates its own new coordination issue per Step 3D.1), but if `ACTIVE_CLAIMS_NOTE` logic is ever extended to also surface lease state, prefer checking for a still-unreleased `FORGE:LEASE` the same way `ACTIVE_CLAIMS` is computed above.
52
52
 
53
+ **Recover the board when the variable is lost** <!-- Added: forge#3699 -->: `COORD_ISSUE_NUMBER` is a shell variable and does not survive compaction, `--retry` or a fresh-context resume. When it is unset but `BATCH_ID` is known, look the board up on GitHub by the exact `<!-- FORGE:BATCH_ID: ${BATCH_ID} -->` body marker (never a prefix, so another live batch's board is never matched). If `BATCH_ID` is also lost, fall through to the no-op and leave the board to `/cleanup` Phase 2C.
54
+
53
55
  ```bash
56
+ if [ -z "${COORD_ISSUE_NUMBER:-}" ] && [ -n "${BATCH_ID:-}" ]; then
57
+ COORD_ISSUE_NUMBER=$(gh issue list -R {GH_REPO} --state open --label automation --limit 100 \
58
+ --json number,body 2>/dev/null \
59
+ | jq -r --arg marker "<!-- FORGE:BATCH_ID: ${BATCH_ID} -->" \
60
+ '[.[] | select(.body | contains("<!-- FORGE:COORD_ISSUE -->") and contains($marker))] | first | .number // empty' \
61
+ 2>/dev/null || echo "")
62
+ [ -n "$COORD_ISSUE_NUMBER" ] && echo "Recovered coordination issue #${COORD_ISSUE_NUMBER} from FORGE:BATCH_ID marker"
63
+ fi
64
+
54
65
  if [ -n "${COORD_ISSUE_NUMBER:-}" ]; then
55
66
  # Use Step 4A's read_active_claims() helper (re-declare it verbatim here when this
56
67
  # phase runs in a fresh context). Releases are separate later comments, so checking
@@ -66,15 +77,20 @@ $(echo "$ACTIVE_CLAIMS" | jq -r '.[] | "- " + .' 2>/dev/null)"
66
77
  ACTIVE_CLAIMS_NOTE="No active (unreleased) claims at close time."
67
78
  fi
68
79
 
80
+ # Outcome counters come from Phase 4/6 state; tolerate unset values (default n/a when the orchestrator did not track them).
81
+ MERGED_COUNT="${MERGED_COUNT:-n/a}"; GATED_COUNT="${GATED_COUNT:-n/a}"; FAILED_COUNT="${FAILED_COUNT:-n/a}"
82
+
69
83
  gh issue close "$COORD_ISSUE_NUMBER" -R {GH_REPO} --comment "Batch complete — closing claims board.
70
84
 
85
+ **Summary**: merged ${MERGED_COUNT} / gated ${GATED_COUNT} / failed ${FAILED_COUNT}
86
+
71
87
  ${ACTIVE_CLAIMS_NOTE}
72
88
 
73
89
  Closed automatically by \`/orchestrate\` Phase 5 Step 5C." 2>/dev/null \
74
90
  && echo "Closed coordination issue #${COORD_ISSUE_NUMBER}" \
75
91
  || echo "WARNING: failed to close coordination issue #${COORD_ISSUE_NUMBER} — non-fatal, continuing Phase 5"
76
92
  else
77
- echo "No coordination issue for this batch (FORGE_COORD_ISSUE unset) — Step 5C is a no-op"
93
+ echo "No coordination issue for this batch (unset and not recoverable via BATCH_ID) — Step 5C is a no-op; /cleanup Phase 2C will sweep it if it exists"
78
94
  fi
79
95
  ```
80
96
 
@@ -36,6 +36,8 @@ else
36
36
  fi
37
37
  ```
38
38
 
39
+ **Decomposition trackers** <!-- Added: forge#3736 -->: issues labelled `workflow:decomposed` are trackers (open, or closed at decomposition when `decomposition.close_parent` is true), not work. Phase 2 excludes `workflow:decomposed` from every open-work and issue-flow count and reports them separately as `trackers (N)`.
40
+
39
41
  ### 1C: Load Prior Health Report
40
42
 
41
43
  Fetch the most recent `health-report` issue for the same repo and parse its metrics for trend computation. This must run before Phase 2 so that PRIOR_* variables are available when Phase 3A builds the trend column.
@@ -323,7 +323,7 @@ INVALID_ISSUES=$(gh issue list -R $REPO --state closed --label "workflow:invalid
323
323
  --jq "[.[] | select(.createdAt > \"$SINCE\" and (.labels | map(.name) | index(\"review-finding\") | not))] | length")
324
324
 
325
325
  TOTAL_INVESTIGATED=$(gh issue list -R $REPO --state all --limit 500 --json number,createdAt,labels \
326
- --jq "[.[] | select(.createdAt > \"$SINCE\" and (.labels | map(.name) | any(. == \"workflow:investigating\" or . == \"workflow:building\" or . == \"workflow:in-review\" or . == \"workflow:merged\" or . == \"workflow:invalid\"))) ] | length")
326
+ --jq "[.[] | select(.createdAt > \"$SINCE\" and (.labels | map(.name) | index(\"workflow:decomposed\") | not) and (.labels | map(.name) | any(. == \"workflow:investigating\" or . == \"workflow:building\" or . == \"workflow:in-review\" or . == \"workflow:merged\" or . == \"workflow:invalid\"))) ] | length")
327
327
 
328
328
  echo "Investigation invalidation rate: $INVALID_ISSUES / $TOTAL_INVESTIGATED"
329
329
  # Target: < 15%
@@ -354,15 +354,22 @@ echo "Re-investigation count: $REINVESTIGATED"
354
354
  # 4. Issue structure compliance (/issue adoption proxy)
355
355
  # Issues created in window that have an "Affected Files" section in their body
356
356
  TOTAL_ISSUES=$(gh issue list -R $REPO --state all --limit 500 --json number,createdAt,labels \
357
- --jq "[.[] | select(.createdAt > \"$SINCE\" and (.labels | map(.name) | index(\"review-finding\") | not))] | length")
357
+ --jq "[.[] | select(.createdAt > \"$SINCE\" and (.labels | map(.name) | index(\"review-finding\") | not) and (.labels | map(.name) | index(\"workflow:decomposed\") | not))] | length")
358
358
 
359
359
  STRUCTURED_ISSUES=$(gh issue list -R $REPO --state all --limit 500 --json number,createdAt,body,labels \
360
- --jq "[.[] | select(.createdAt > \"$SINCE\" and (.labels | map(.name) | index(\"review-finding\") | not) and (.body | contains(\"Affected Files\")))] | length")
360
+ --jq "[.[] | select(.createdAt > \"$SINCE\" and (.labels | map(.name) | index(\"review-finding\") | not) and (.labels | map(.name) | index(\"workflow:decomposed\") | not) and (.body | contains(\"Affected Files\")))] | length")
361
361
 
362
362
  echo "Issue structure compliance: $STRUCTURED_ISSUES / $TOTAL_ISSUES"
363
363
  # Target: > 80%. Issues without Affected Files are harder to investigate and more likely to produce ISSUE_SPEC failures.
364
364
  ```
365
365
 
366
+ **Decomposition trackers** <!-- Added: forge#3736 -->: issues labelled `workflow:decomposed` are trackers, not work. `TOTAL_INVESTIGATED`, `TOTAL_ISSUES`, `STRUCTURED_ISSUES` and both stall-rate queries exclude them with an `index("workflow:decomposed") | not` filter, so a parent closed at decomposition (`decomposition.close_parent`) and an open one are treated alike. Report them separately as `trackers (N)`:
367
+
368
+ ```bash
369
+ TRACKER_COUNT=$(gh issue list -R $REPO --state all --label "workflow:decomposed" --limit 500 --json number --jq 'length' 2>/dev/null || echo 0)
370
+ echo "trackers ($TRACKER_COUNT) — decomposed parents, excluded from work counts"
371
+ ```
372
+
366
373
  **Issue quality score** = weighted average of sub-metrics:
367
374
  - Invalidation rate: 40% weight (direct signal — these issues failed)
368
375
  - ISSUE_SPEC audit findings: 30% weight (downstream cost signal)
@@ -496,14 +503,14 @@ Query issues that received a `<!-- FORGE:STALL_DETECTED -->` comment within the
496
503
  BATCH_TOTAL=$(gh issue list -R $GH_REPO \
497
504
  --state all --limit 500 \
498
505
  --json number,createdAt,labels \
499
- --jq "[.[] | select(.createdAt > \"$SINCE\") | select(.labels | map(.name) | any(startswith(\"workflow:\")))] | length" \
506
+ --jq "[.[] | select(.createdAt > \"$SINCE\") | select(.labels | map(.name) | any(startswith(\"workflow:\"))) | select(.labels | map(.name) | index(\"workflow:decomposed\") | not)] | length" \
500
507
  2>/dev/null || echo 0)
501
508
 
502
509
  # Enumerate pipeline-active issue numbers for per-issue comment queries
503
510
  PIPELINE_ISSUE_NUMS=$(gh issue list -R $GH_REPO \
504
511
  --state all --limit 500 \
505
512
  --json number,createdAt,labels \
506
- --jq "[.[] | select(.createdAt > \"$SINCE\") | select(.labels | map(.name) | any(startswith(\"workflow:\"))) | .number]" \
513
+ --jq "[.[] | select(.createdAt > \"$SINCE\") | select(.labels | map(.name) | any(startswith(\"workflow:\"))) | select(.labels | map(.name) | index(\"workflow:decomposed\") | not) | .number]" \
507
514
  2>/dev/null || echo "[]")
508
515
 
509
516
  # Count issues with at least one FORGE:STALL_DETECTED comment
@@ -100,6 +100,26 @@ if [ -n "$UNTRACKED" ] && [ "$UNTRACKED_COUNT" -gt 0 ]; then
100
100
  done
101
101
  echo ""
102
102
  fi
103
+
104
+ # Decomposition trackers (workflow:decomposed) are NOT work: they are excluded from every
105
+ # work count in this report and listed separately. With decomposition.close_parent a tracker
106
+ # is CLOSED at decomposition, so query --state all and keep only trackers with unfinished
107
+ # `- [ ]` checklist items. <!-- Added: forge#3736 -->
108
+ TRACKERS=$(gh issue list $GH_FLAG \
109
+ --state all \
110
+ --label "workflow:decomposed" \
111
+ --limit 100 \
112
+ --json number,title,state,body \
113
+ --jq '.[] | select(.body | test("(?m)^[[:space:]]*[-*+] \\[ \\]")) | "#\(.number) \(.title[:60]) [\(.state | ascii_downcase)]"' \
114
+ 2>/dev/null || echo "")
115
+ TRACKER_COUNT=$(printf '%s' "$TRACKERS" | grep -c '#' 2>/dev/null || true)
116
+ echo "[trackers] (${TRACKER_COUNT:-0} — decomposed parents awaiting children; not counted as work)"
117
+ if [ -n "$TRACKERS" ]; then
118
+ echo "$TRACKERS" | while IFS= read -r line; do
119
+ echo " $line"
120
+ done
121
+ fi
122
+ echo ""
103
123
  ```
104
124
 
105
125
  ---
@@ -295,7 +315,7 @@ echo ""
295
315
  INVESTIGATING=$(gh issue list $GH_FLAG --state open --label "workflow:investigating" --limit 100 --json number --jq '. | length' 2>/dev/null || echo 0)
296
316
  BUILDING=$(gh issue list $GH_FLAG --state open --label "workflow:building" --limit 100 --json number --jq '. | length' 2>/dev/null || echo 0)
297
317
  IN_REVIEW=$(gh issue list $GH_FLAG --state open --label "workflow:in-review" --limit 100 --json number --jq '. | length' 2>/dev/null || echo 0)
298
- BLOCKED=$(gh issue list $GH_FLAG --state open --label "needs-human" --limit 100 --json number --jq '. | length' 2>/dev/null || echo 0)
318
+ BLOCKED=$(gh issue list $GH_FLAG --state open --label "needs-human" --limit 100 --json number,labels --jq '[.[] | select(.labels | map(.name) | index("workflow:decomposed") | not)] | length' 2>/dev/null || echo 0) # workflow:decomposed trackers are excluded from work counts
299
319
  READY=$(gh issue list $GH_FLAG --state open --label "workflow:ready-to-build" --limit 100 --json number --jq '. | length' 2>/dev/null || echo 0)
300
320
 
301
321
  TOTAL_INFLIGHT=$((INVESTIGATING + BUILDING + IN_REVIEW + READY))
@@ -192,6 +192,7 @@ Append this block at the very end of your comment (after the `---` footer line,
192
192
  9. **Agent marker**: Include exactly one `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker in the persisted body, where `{domain}` is the marker domain the orchestrator passes you (the canonical value from the roster-to-marker table in `review-pr.md`, e.g. `infra` and `scraper`; never a lowercased display name such as `infrastructure` or `scraping`).
193
193
  10. **Reviewed SHA**: Include exactly one line `Reviewed-SHA: [REVIEW_SHA]` (the full 40-hex head you were pinned to) in the persisted body, outside the marker. `/review-pr` scopes its completion wait and idempotent re-entry to this line, so a comment without the current head does not count as a completed review. Do NOT put the SHA inside the `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker.
194
194
  11. **Failure scenario and scope (provenance inputs)**: every finding's body section (the prose above the findings block) MUST carry a `**Failure scenario**` line — concrete inputs leading to a wrong output or behaviour — and a `**Scope**: changed` or `**Scope**: pre-existing` line (`changed` = the cited line is added or modified by this PR; `pre-existing` = it was already there). `/review-pr` §6B.4 reads these: a finding without a failure scenario is demoted to a note (unless CRITICAL/HIGH, any confidence), and a MEDIUM or lower finding whose cited line is not in the PR diff is routed as pre-existing (CRITICAL/HIGH never are; see §5). The `**Scope**` tag is informational and never gates routing. Speculative hardening with no failure scenario is a note, not a finding. The one-line `<!-- FINDING:... -->` grammar above is unchanged. <!-- Added: forge#3452 -->
195
+ 12. **Fix efficacy (linked-issue contradiction)**: when a finding says the PR's linked issue is NOT fixed, or that the PR re-introduces the defect it claims to fix, its body section MUST carry a `**Fix efficacy**: fails` (the fix does not work) or `**Fix efficacy**: regresses` (the PR re-introduces the defect) line, and the prose may include the token on its own line (`FIX_EFFICACY: fails|regresses`; line-anchored, a mid-sentence mention does not count). Opt-in on the tag, never on keywords. `/review-pr` §6B.5 passes it to `scripts/classify-finding.sh --fix-efficacy`, which exempts a MEDIUM finding from the fix-PR damper. Use it only for the linked issue's own fix; never for unrelated defects. The one-line `<!-- FINDING:... -->` grammar is unchanged. <!-- Added: forge#3693 -->
195
196
 
196
197
  ### Domain Prefixes
197
198
 
@@ -35,6 +35,12 @@ post `FORGE:REVIEW_BLOCKED` and never replace the isolated review with inline wo
35
35
 
36
36
  ---
37
37
 
38
+ ## Read-Only Git Rule (all reviewer agents)
39
+
40
+ Every reviewer agent is read-only with respect to the main checkout (the repo's shared primary working tree). It MUST NOT run `git checkout`, `git switch`, `git reset`, `git stash`, or `git merge` there: those race with the operator and other agents. Read PR code with `git show <sha>:<path>`, `gh pr diff`, `gh api`, or a per-PR worktree (`git worktree add --detach`) that the agent removes afterward. The orchestrator appends this rule to every dispatched prompt (review-pr.md Phase 3C). <!-- Added: forge#3650 -->
41
+
42
+ ---
43
+
38
44
  ## How to Load Agents (Phase 3C)
39
45
 
40
46
  For each agent selected in Phase 3B, read **only** the files relevant to that agent:
@@ -49,14 +55,16 @@ For each agent selected in Phase 3B, read **only** the files relevant to that ag
49
55
  | Agent | Trigger | Marker domain | File |
50
56
  |-------|---------|---------------|------|
51
57
  | General Security & Quality Scan | ALWAYS RUNS | `security` | `review-pr-agents/security.md` |
52
- | Auth Conventions Auditor | AUTH domain | `auth` | `review-pr-agents/auth.md` |
58
+ | Auth Conventions Auditor | Auth/session/trust-filter files changed (path-gated) | `auth` | `review-pr-agents/auth.md` |
53
59
  | Billing Integrity Auditor | BILLING domain | `billing` | `review-pr-agents/billing.md` |
54
60
  | Concurrency & Race Condition Auditor | CONCURRENCY or BILLING domain | `concurrency` | `review-pr-agents/concurrency.md` |
55
61
  | Domain Logic Auditor | SCRAPING domain (requires `review.domains.scraping` in forge.yaml) | `scraper` | `review-pr-agents/scraper.md` |
56
62
  | Frontend Quality Auditor | WEB service touched | `frontend` | `review-pr-agents/frontend.md` |
57
63
  | API Design & Consistency Auditor | New/modified routers or SDK/OpenAPI files | `api` | `review-pr-agents/api.md` |
58
64
  | Database & Migration Auditor | DATABASE domain | `database` | `review-pr-agents/database.md` |
59
- | Infrastructure & Deploy Safety Auditor | INFRA service touched | `infra` | `review-pr-agents/infra.md` |
65
+ | Infrastructure & Deploy Safety Auditor | Docker, CI, deploy or infra files changed (path-gated) | `infra` | `review-pr-agents/infra.md` |
66
+
67
+ The table is applied by `scripts/review-roster.sh` (Phase 3B Step 4), which tiers the roster by diff size and fix-PR lineage: small and fix PRs run Security only unless a path trigger fires, and Auth and Infra are selected by file path, never by diff-text score. `--thorough` and milestone-to-staging always run the full union. The tier and reason are recorded in `FORGE:REVIEW_TIER`.
60
68
 
61
69
  ## Domain Prefixes (for Structured Findings)
62
70