forgedock 1.1.7 → 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +10 -33
  2. package/bin/cinema.mjs +23 -5
  3. package/bin/engine/invariants.mjs +361 -0
  4. package/bin/engine-cli.mjs +137 -11
  5. package/bin/env-detect.mjs +249 -0
  6. package/bin/forge-utils.mjs +364 -4
  7. package/bin/forgedock.mjs +1064 -135
  8. package/bin/hooks/pre-tool-use.mjs +182 -0
  9. package/bin/hooks/session-start.mjs +68 -3
  10. package/bin/init-detect.mjs +101 -12
  11. package/bin/journey.mjs +917 -16
  12. package/bin/labels.json +10 -0
  13. package/bin/recall.mjs +557 -0
  14. package/bin/registry.mjs +91 -3
  15. package/bin/runner.mjs +38 -2
  16. package/bin/tui.mjs +49 -3
  17. package/commands/adopt.md +1 -1
  18. package/commands/analytics.md +8 -8
  19. package/commands/audit.md +1 -1
  20. package/commands/autopilot.md +958 -77
  21. package/commands/ci-audit.md +1 -1
  22. package/commands/cleanup.md +139 -55
  23. package/commands/compat-audit.md +1 -1
  24. package/commands/deploy-info.md +1 -1
  25. package/commands/deploy-pr.md +1 -1
  26. package/commands/diagnose.md +1 -1
  27. package/commands/explain.md +1 -1
  28. package/commands/fix-ci.md +1 -1
  29. package/commands/forgedock-init.md +1 -1
  30. package/commands/incident-response.md +3 -3
  31. package/commands/issue.md +1 -1
  32. package/commands/optimize.md +586 -1
  33. package/commands/orchestrate/config.md +13 -3
  34. package/commands/orchestrate/phase-1-resolve.md +36 -26
  35. package/commands/orchestrate/phase-2-triage.md +1 -1
  36. package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
  37. package/commands/orchestrate/phase-3-dependency.md +587 -48
  38. package/commands/orchestrate/phase-4-execution.md +1031 -57
  39. package/commands/orchestrate/phase-5-cleanup.md +1 -1
  40. package/commands/orchestrate/phase-6-report.md +248 -11
  41. package/commands/orchestrate/safety.md +2 -2
  42. package/commands/pipeline-health/phase-2-metrics.md +305 -1
  43. package/commands/pipeline-health/phase-4-proposals.md +127 -0
  44. package/commands/qa-sweep.md +1 -1
  45. package/commands/quality-gate.md +418 -1
  46. package/commands/recover-orphans.md +5 -5
  47. package/commands/replay.md +1 -1
  48. package/commands/review-pr-agents/api.md +2 -2
  49. package/commands/review-pr-agents/auth.md +2 -2
  50. package/commands/review-pr-agents/billing.md +2 -2
  51. package/commands/review-pr-agents/concurrency.md +2 -2
  52. package/commands/review-pr-agents/database.md +2 -2
  53. package/commands/review-pr-agents/frontend.md +2 -2
  54. package/commands/review-pr-agents/infra.md +2 -2
  55. package/commands/review-pr-agents/protocols.md +1 -1
  56. package/commands/review-pr-agents/scraper.md +2 -2
  57. package/commands/review-pr-agents/security.md +2 -2
  58. package/commands/review-pr-staging.md +41 -3
  59. package/commands/review-pr.md +462 -19
  60. package/commands/rollback.md +1 -1
  61. package/commands/scope.md +1 -1
  62. package/commands/security-audit.md +1 -1
  63. package/commands/spec-doctor.md +429 -0
  64. package/commands/test-gate.md +1 -1
  65. package/commands/validate.md +53 -1
  66. package/commands/work-on/build/architect.md +78 -1
  67. package/commands/work-on/build/context.md +360 -22
  68. package/commands/work-on/build/implement.md +3 -1
  69. package/commands/work-on/build/validate.md +29 -1
  70. package/commands/work-on/build.md +75 -6
  71. package/commands/work-on/close.md +624 -10
  72. package/commands/work-on/decompose.md +1 -1
  73. package/commands/work-on/investigate.md +235 -5
  74. package/commands/work-on/remediate.md +331 -0
  75. package/commands/work-on/review.md +1 -1
  76. package/commands/work-on-monolithic.md +1 -1
  77. package/commands/work-on.md +205 -66
  78. package/docs/CI.md +2 -2
  79. package/docs/CODEX.md +1 -1
  80. package/docs/CONFIG.md +186 -0
  81. package/docs/FORGE-PROTOCOL.md +101 -0
  82. package/docs/site/command-reference.md +31 -1
  83. package/docs/site/getting-started.md +9 -34
  84. package/docs/site/index.md +1 -1
  85. package/docs/spec/forge-protocol-v1.md +97 -5
  86. package/docs/spec/label-state-machine.md +6 -1
  87. package/package.json +3 -2
  88. package/packages/protocol/src/cli.js +374 -84
  89. package/packages/protocol/src/types.js +75 -1
  90. package/scripts/build-knowledge-index.mjs +1761 -0
  91. package/scripts/calibration.mjs +996 -0
  92. package/scripts/check-command-docs-drift.sh +4 -2
  93. package/scripts/danger-zones.mjs +789 -0
  94. package/scripts/doctor-pipeline-state.sh +88 -2
  95. package/scripts/select-fix-targets.sh +234 -0
  96. package/scripts/transition-label.sh +245 -6
  97. package/scripts/refresh-bot-token.sh +0 -97
@@ -13,7 +13,7 @@ allowed-tools: Task, Bash, Read, Grep, Glob, WebFetch, Skill
13
13
  **NEVER use plan mode (EnterPlanMode)** during review — it breaks execution context.
14
14
  **NEVER use the Agent tool** — review-pr dispatches domain agents via `Task` tool only. `Agent` spawns opaque subprocesses that bypass the allowed-tools constraint and cannot post structured findings to the PR. Always use `Task(...)` for review agent launch (Phase 3C).
15
15
 
16
- **Agent model policy**: `model: "sonnet"` (standard tier); the General Security & Quality reviewer spawned as always-runs Task uses `effort: xhigh` (deep tier). Fallback: `model: "opus"` if rate-limited. User can override with `--model <name>`. Pass model and effort explicitly in every `Task` tool call. Feature gate: pass `effort` only on Claude Code >= 2.1.154.
16
+ **Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier); the General Security & Quality reviewer spawned as always-runs Task uses `effort: xhigh` (deep tier). Fallback: `model: "opus"` if rate-limited. User can override with `--model <name>`. Pass model and effort explicitly in every `Task` tool call. Feature gate: pass `effort` only on Claude Code >= 2.1.154. **The domain-review Task agents this file dispatches** (per `commands/review-pr-agents/*.md`) resolve via `model: "{SUBAGENT_MODEL}"` — forge.yaml `agents.subagent_model`, else `agents.default_model`, else `"sonnet"` — not `{DEFAULT_MODEL}` directly.
17
17
 
18
18
  <!-- FORGE:SPEC_LOADED — review-pr.md loaded and active. Agent is bound by this spec. -->
19
19
 
@@ -23,10 +23,12 @@ allowed-tools: Task, Bash, Read, Grep, Glob, WebFetch, Skill
23
23
 
24
24
  2. **Post the FORGE:REVIEW verdict regardless of finding severity.** A review that completes but posts no `<!-- FORGE:REVIEW -->` comment is invisible to the pipeline. Even a PASS verdict must be posted.
25
25
 
26
- 3. **Review findings do NOT block merge UNLESS they meet the Blocking Criteria in §7B** (a CONFIRMED HIGH/CRITICAL finding, a purpose regression, a merge conflict, or a build/type/test failure). File every finding as a GitHub issue with the `review-finding` label regardless of severity. Minor/style findings never block; §7B's blocking conditions always do — including under `--auto-merge`.
26
+ 3. **Review findings do NOT block merge UNLESS they meet the Blocking Criteria in §7B** (a CONFIRMED HIGH/CRITICAL finding, a purpose regression, a merge conflict, or a build/type/test failure) **or the calibration threshold check in §7B.5 sets `CALIBRATION_NEEDS_HUMAN=true`** (HIGH-confidence task type with historical survival < 80%). File every finding as a GitHub issue with the `review-finding` label regardless of severity. Minor/style findings never block; §7B's and §7B.5's blocking conditions always do — including under `--auto-merge`. <!-- forge#1741 -->
27
27
 
28
28
  4. **Route correctly at Phase 0.** If the input is "staging" or the PR targets `main`, invoke `Skill("review-pr-staging", ...)` — do NOT run the standard PR review pipeline against a staging→main PR.
29
29
 
30
+ 5. **`spec-evolution` PRs are NEVER auto-merged.** When a PR carries the `spec-evolution` label (created by `/spec-doctor`), Phase -1 MUST set `AUTO_MERGE=false` and add `needs-human` before any other processing. This cannot be overridden by the caller — the eval gate plus human review are the only permitted merge path. See Phase -1 `spec-evolution guard` block. <!-- Added: forge#1742 -->
31
+
30
32
  ## Forbidden Tools Self-Check
31
33
 
32
34
  **Before executing any phase**, verify you are NOT using any of these tools:
@@ -115,9 +117,43 @@ fi
115
117
 
116
118
  REVIEW_SHA_ROUTE=$(gh pr view ${ROUTE_PR_NUMBER:-$ARGUMENTS} --json headRefOid --jq '.headRefOid' 2>/dev/null | cut -c1-7 || echo "n/a")
117
119
 
120
+ # spec-evolution guard — MUST run before posting FORGE:REVIEW_ROUTE <!-- Added: forge#1742 -->
121
+ # spec-evolution PRs are produced by /spec-doctor and require eval-gate + human review.
122
+ # Auto-merge is structurally excluded regardless of how review-pr was invoked.
123
+ # This guard activates for single-PR mode only (staging/multi-pr modes don't auto-merge anyway).
124
+ IS_SPEC_EVOLUTION=false
125
+ if [ "$REVIEW_MODE" = "single-pr" ] && [ -n "$ROUTE_PR_NUMBER" ] && [ "$ROUTE_PR_NUMBER" != "(list mode)" ] && [ "$ROUTE_PR_NUMBER" != "(resolved by staging sub-command)" ]; then
126
+ SPEC_EVOL_CHECK=$(gh pr view "$ROUTE_PR_NUMBER" --json labels \
127
+ --jq '[.labels[].name] | any(. == "spec-evolution")' 2>/dev/null || echo "false")
128
+ if [ "$SPEC_EVOL_CHECK" = "true" ]; then
129
+ IS_SPEC_EVOLUTION=true
130
+ AUTO_MERGE=false
131
+ # Add needs-human label to the associated issue (if --issue was passed in $ARGUMENTS)
132
+ SPEC_EVOL_ISSUE=$(echo "$ARGUMENTS" | grep -oP '(?<=--issue )\d+' || echo "")
133
+ if [ -n "$SPEC_EVOL_ISSUE" ]; then
134
+ GH_FLAG_SPEC=$(echo "$ARGUMENTS" | grep -oP '(?<=-R )\S+' | head -1 | sed 's/^/-R /' || echo "")
135
+ gh issue edit "$SPEC_EVOL_ISSUE" $GH_FLAG_SPEC --add-label "needs-human" 2>/dev/null || true
136
+ fi
137
+ echo "SPEC-EVOLUTION GUARD: PR #${ROUTE_PR_NUMBER} carries 'spec-evolution' label."
138
+ echo "AUTO_MERGE forced to false. Human review + eval gate required before merge."
139
+ echo "HARD RULE 5: This cannot be overridden by the caller."
140
+ fi
141
+ fi
142
+
118
143
  # Post the routing assertion marker to the PR (skip for list/keyword modes where no PR# is known yet)
144
+ # CODEC PATH (forge#1727): REVIEW_ROUTE is a custom (non-RESERVED) annotation type; emit() tolerates
145
+ # unknown types. Use the codec CLI to produce the opening tag for any FORGE: annotation.
146
+ # For single-line control markers like REVIEW_ROUTE, the inline heredoc is acceptable as long
147
+ # as field values contain no untrusted content (here: REVIEW_MODE and REVIEW_SHA_ROUTE are
148
+ # pipeline-internal values, not user-supplied text). For annotations with user-supplied fields,
149
+ # route through: forge-annotation.sh write REVIEWER --field Verdict=APPROVED ...
119
150
  if [ "$REVIEW_MODE" != "staging-keyword" ] && [ "$REVIEW_MODE" != "multi-pr" ]; then
120
- gh pr comment "$ROUTE_PR_NUMBER" --body "<!-- FORGE:REVIEW_ROUTE mode=${REVIEW_MODE} spec=review-pr.md sha=${REVIEW_SHA_ROUTE} -->"
151
+ if [ "$IS_SPEC_EVOLUTION" = "true" ]; then
152
+ gh pr comment "$ROUTE_PR_NUMBER" --body "<!-- FORGE:REVIEW_ROUTE mode=spec-evolution-blocked spec=review-pr.md sha=${REVIEW_SHA_ROUTE} -->
153
+ ⚠️ **spec-evolution PR**: auto-merge is structurally excluded (HARD RULE 5). Eval gate must pass and a human must review and merge. See \`docs/design/eval-gate-runbook.md\`."
154
+ else
155
+ gh pr comment "$ROUTE_PR_NUMBER" --body "<!-- FORGE:REVIEW_ROUTE mode=${REVIEW_MODE} spec=review-pr.md sha=${REVIEW_SHA_ROUTE} -->"
156
+ fi
121
157
  fi
122
158
  ```
123
159
 
@@ -753,35 +789,71 @@ echo "$DIFF" | grep -cE "subprocess|os\.system|eval\(|exec\(|pickle|yaml\.load[^
753
789
  echo "$FILES" | grep -cE "^sdk/|openapi.*\.json$|openapi-versions/" && echo " SDK_OPENAPI" || true
754
790
  ```
755
791
 
756
- **Churn (hot-spot) signal**: The signals above are all derived from the current diff content — none of them measure historical change frequency, which is one of the strongest empirical predictors of defect density. Compute a bounded per-file churn tier for the PR's changed files only (never repo-wide) and carry it forward as `CHURN_CONTEXT` for Phase 3C:
792
+ **Churn (hot-spot) signal**: The signals above are all derived from the current diff content — none of them measure historical change frequency or defect density. Compute both commit-churn tier and finding-density tier for the PR's changed files and carry them forward as `CHURN_CONTEXT` for Phase 3C: <!-- Finding-density signal: forge#1738 -->
757
793
 
758
794
  ```bash
759
795
  CHURN_WINDOW="90 days ago" # named constant — must match the same window used in architect.md Phase A5
760
796
  CHURN_CONTEXT=""
797
+
798
+ # Load danger-zones index for finding-density signal (non-blocking — absent index is not an error).
799
+ # The index is produced by scripts/danger-zones.mjs and updated on each merge via
800
+ # build-knowledge-index.mjs --with-danger-zones. When absent, only commit-count signal is used.
801
+ DANGER_ZONES_INDEX="${HOME}/.forge/index/danger-zones.json"
802
+ DZ_DATA=""
803
+ if [ -f "$DANGER_ZONES_INDEX" ]; then
804
+ DZ_DATA=$(cat "$DANGER_ZONES_INDEX" 2>/dev/null || true)
805
+ fi
806
+
761
807
  # Herestring (not a piped `| while read`) — a pipe would run the loop body in a
762
808
  # subshell in bash, silently discarding CHURN_CONTEXT once the loop exits. $FILES
763
809
  # is one path per line (gh pr diff --name-only), so `read -r` per line is safe
764
810
  # even when a path contains embedded spaces.
765
811
  while IFS= read -r FILE; do
766
812
  [ -z "$FILE" ] && continue
813
+
814
+ # Commit-churn signal (unchanged from original)
767
815
  COMMITS=$(git log --oneline --since="$CHURN_WINDOW" -- "$FILE" 2>/dev/null | wc -l)
816
+ COMMIT_TAG=""
768
817
  if [ "$COMMITS" -ge 15 ]; then
818
+ COMMIT_TAG=" — HOT (${COMMITS} commits/90d)"
819
+ fi
820
+
821
+ # Finding-density signal (new — reads from danger-zones index when available)
822
+ FINDING_TAG=""
823
+ if [ -n "$DZ_DATA" ]; then
824
+ FINDING_COUNT=$(echo "$DZ_DATA" | python3 -c "
825
+ import sys, json
826
+ d = json.loads(sys.stdin.read())
827
+ files = d.get('files', {})
828
+ entry = files.get('${FILE}', None)
829
+ if entry:
830
+ print(entry.get('findingCount90d', 0))
831
+ else:
832
+ print(0)
833
+ " 2>/dev/null || echo "0")
834
+ if [ "${FINDING_COUNT:-0}" -ge 3 ]; then
835
+ FINDING_TAG=" — HOT-FINDINGS (${FINDING_COUNT} findings/90d)"
836
+ fi
837
+ fi
838
+
839
+ # Append to CHURN_CONTEXT when either signal fires
840
+ if [ -n "$COMMIT_TAG" ] || [ -n "$FINDING_TAG" ]; then
769
841
  # Real newline appended via $'\n' (ANSI-C quoting), not a literal backslash-n.
770
842
  # A literal "\n" would only be interpreted by `echo -e` — it survives
771
843
  # unprocessed into Phase 3C's raw template substitution ([CHURN_CONTEXT] ->
772
844
  # $CHURN_CONTEXT), which does not reprocess escapes, so with 2+ HOT files
773
845
  # multi-hotspot PRs would render squished in the agent prompt. A real
774
846
  # newline here makes the variable correct for every consumer.
775
- CHURN_CONTEXT="${CHURN_CONTEXT}${FILE} (${COMMITS} commits in last 90 days — HOT)"$'\n'
847
+ CHURN_CONTEXT="${CHURN_CONTEXT}${FILE}${COMMIT_TAG}${FINDING_TAG}"$'\n'
776
848
  fi
777
849
  done <<< "$FILES"
778
850
  if [ -z "$CHURN_CONTEXT" ]; then
779
- CHURN_CONTEXT="No hot-spot files detected (all changed files under 15 commits in the last 90 days)."
851
+ CHURN_CONTEXT="No hot-spot files detected (all changed files under 15 commits and under 3 findings in the last 90 days)."
780
852
  fi
781
853
  echo "$CHURN_CONTEXT"
782
854
  ```
783
855
 
784
- Tiers (fixed thresholds, identical to `architect.md` Phase A5): **HOT** = 15+ commits / 90 days, **MEDIUM** = 5–14, **LOW** = 0–4. Only HOT-tier files are listed in `CHURN_CONTEXT` — this keeps the agent prompt signal short and high-value rather than dumping a churn count for every file. This does not change agent selection (3B) — it is a scrutiny prior surfaced to whichever agents are already selected, substituted as `[CHURN_CONTEXT]` in Phase 3C.
856
+ Tiers (fixed thresholds, identical to `architect.md` Phase A5): **HOT** = 15+ commits / 90 days, **MEDIUM** = 5–14, **LOW** = 0–4. **HOT-FINDINGS** = 3+ confirmed findings in 90 days (from Forge Ledger danger-zones index). Only files where at least one signal fires are listed in `CHURN_CONTEXT` — this keeps the agent prompt signal short and high-value. A file can appear with both tags (e.g., `commands/orchestrate.md — HOT (20 commits/90d) — HOT-FINDINGS (18 findings/90d)`). This does not change agent selection (3B) — it is a scrutiny prior surfaced to whichever agents are already selected, substituted as `[CHURN_CONTEXT]` in Phase 3C. When the danger-zones index is absent (cold start or pre-1738 install), `CHURN_CONTEXT` falls back to commit-count-only behavior (no HOT-FINDINGS tags). <!-- Added: forge#1738 -->
785
857
 
786
858
  ### 3B: Select Agents — Risk-Scaled Dispatch
787
859
 
@@ -952,6 +1024,191 @@ AGENT_COUNT=$(echo "$SELECTED_AGENTS" | wc -w)
952
1024
  echo "=== AGENT COUNT: $AGENT_COUNT ==="
953
1025
  ```
954
1026
 
1027
+ ### 3B.5: Provenance-Based Trust Escalation (Conditional)
1028
+
1029
+ **Skip if**: `THOROUGH=true` OR `IS_MILESTONE_TO_STAGING=true` — these modes already run full union dispatch; provenance de-escalation must not narrow an already-thorough review. This skip is enforced in code at the Step 4 application block (not merely relied upon via a downstream overwrite). <!-- forge#1804 -->
1030
+
1031
+
1032
+ **Purpose**: Adjust the agent roster based on the PR's verifiable track record. Proven `(task-type × module-set)` combinations may drop optional judgment agents; novel combinations escalate to full panel plus `needs-human` on merge. The hard floor (Security agent + all Phase 2 automated checks) runs at every intensity tier, unconditionally.
1033
+
1034
+ **HARD FLOOR (non-negotiable)**: Security agent always stays in `SELECTED_AGENTS`. Phase 2 automated checks (linting, typecheck, secrets scan, SQL validation) run regardless of intensity tier. Trust only modulates optional LLM judgment breadth.
1035
+
1036
+ #### Step 1: Declare defaults (fail-safe initialization)
1037
+
1038
+ ```bash
1039
+ # Default: SHADOW — no roster change, log the decision only.
1040
+ # This ensures that any error path (absent table, parse failure, git error)
1041
+ # falls back to the current static behavior — never looser, never tighter.
1042
+ INTENSITY_TIER="SHADOW"
1043
+ TABLE_CELL_KEY="(unavailable)"
1044
+ PROVENANCE_TABLE_LOADED=false
1045
+ TRUST_SHADOW_MODE=$(yq '.review.trust_shadow_mode // "true"' "${FORGE_YAML:-forge.yaml}" 2>/dev/null || echo "true")
1046
+ ```
1047
+
1048
+ #### Step 2: Read provenance table from forge-knowledge branch
1049
+
1050
+ ```bash
1051
+ # Single bounded git-show read — no network round-trip if the branch is local.
1052
+ # Fail-safe: any non-zero exit or parse error → PROVENANCE_JSON stays empty.
1053
+ PROVENANCE_JSON=""
1054
+ if git show-ref --quiet "refs/remotes/origin/forge-knowledge" 2>/dev/null || \
1055
+ git show-ref --quiet "refs/heads/forge-knowledge" 2>/dev/null; then
1056
+ PROVENANCE_JSON=$(git show "forge-knowledge:calibration/provenance.json" 2>/dev/null || \
1057
+ git show "origin/forge-knowledge:calibration/provenance.json" 2>/dev/null || echo "")
1058
+ fi
1059
+
1060
+ if [ -n "$PROVENANCE_JSON" ] && echo "$PROVENANCE_JSON" | jq -e '.rows' >/dev/null 2>&1; then
1061
+ PROVENANCE_TABLE_LOADED=true
1062
+ echo "=== PROVENANCE TABLE: loaded ($(echo "$PROVENANCE_JSON" | jq '.rows | length') cells) ==="
1063
+ else
1064
+ echo "=== PROVENANCE TABLE: absent or invalid — intensity tier defaults to SHADOW (no roster change) ==="
1065
+ fi
1066
+ ```
1067
+
1068
+ #### Step 3: Compute module key and look up cell
1069
+
1070
+ ```bash
1071
+ if [ "$PROVENANCE_TABLE_LOADED" = "true" ]; then
1072
+ # Normalize changed files to module key (mirrors normalizeModules() in calibration.mjs):
1073
+ # Take the top-level directory of each file; for two-level prefixes (services/*, apps/*,
1074
+ # packages/*, clients/*, sdk/*) include the second directory segment too.
1075
+ # Sort + deduplicate + join with "|".
1076
+ TWO_LEVEL="services apps packages clients sdk"
1077
+ RAW_PREFIXES=$(echo "$FILES" | while IFS= read -r f; do
1078
+ [ -z "$f" ] && continue
1079
+ top=$(echo "$f" | cut -d/ -f1)
1080
+ second=$(echo "$f" | cut -d/ -f2)
1081
+ two_level_match=false
1082
+ for prefix in $TWO_LEVEL; do
1083
+ [ "$top" = "$prefix" ] && two_level_match=true && break
1084
+ done
1085
+ if [ "$two_level_match" = "true" ] && [ -n "$second" ]; then
1086
+ echo "${top}/${second}"
1087
+ else
1088
+ echo "$top"
1089
+ fi
1090
+ done | sort -u | tr '\n' '|' | sed 's/|$//')
1091
+
1092
+ # Determine task type from FORGE:INVESTIGATOR annotation on the linked issue (if available)
1093
+ ISSUE_NUM_FOR_TRUST=$(gh pr view "$ARGUMENTS" --json body \
1094
+ --jq '.body | gsub("(?s).*?(?:Closes #|#)(?<n>[0-9]+).*"; "\(.n)") // ""' 2>/dev/null | head -1)
1095
+ TASK_TYPE_FOR_TRUST=""
1096
+ if [ -n "$ISSUE_NUM_FOR_TRUST" ]; then
1097
+ TASK_TYPE_FOR_TRUST=$(gh api "repos/${REPO}/issues/${ISSUE_NUM_FOR_TRUST}/comments" \
1098
+ --jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR"))] | last | .body' 2>/dev/null \
1099
+ | grep -oP '(?<=\*\*Task Type\*\*: )[\w /]+' | head -1 | xargs || echo "")
1100
+ fi
1101
+ # Fall back to title-based inference if INVESTIGATOR comment absent
1102
+ if [ -z "$TASK_TYPE_FOR_TRUST" ]; then
1103
+ PR_TITLE=$(gh pr view "$ARGUMENTS" --json title --jq '.title' 2>/dev/null || echo "")
1104
+ case "$PR_TITLE" in
1105
+ fix*|Fix*) TASK_TYPE_FOR_TRUST="Bug Fix" ;;
1106
+ feat*|Feat*) TASK_TYPE_FOR_TRUST="Feature" ;;
1107
+ refactor*) TASK_TYPE_FOR_TRUST="Refactor" ;;
1108
+ docs*|chore*)TASK_TYPE_FOR_TRUST="Maintenance" ;;
1109
+ *) TASK_TYPE_FOR_TRUST="Feature" ;;
1110
+ esac
1111
+ fi
1112
+
1113
+ TABLE_CELL_KEY="${TASK_TYPE_FOR_TRUST}::${RAW_PREFIXES}"
1114
+ echo "=== PROVENANCE CELL KEY: ${TABLE_CELL_KEY} ==="
1115
+
1116
+ # Look up the cell in the provenance table
1117
+ CELL_DATA=$(echo "$PROVENANCE_JSON" | jq -r \
1118
+ --arg key "$TABLE_CELL_KEY" \
1119
+ '.rows[] | select(.key == $key)' 2>/dev/null || echo "")
1120
+
1121
+ if [ -z "$CELL_DATA" ]; then
1122
+ INTENSITY_TIER="NOVEL"
1123
+ echo "=== PROVENANCE: cell not found — NOVEL (no prior data for this task-type × module-set) ==="
1124
+ else
1125
+ CELL_TRUSTED=$(echo "$CELL_DATA" | jq -r '.trusted' 2>/dev/null || echo "false")
1126
+ CELL_TIER=$(echo "$CELL_DATA" | jq -r '.intensityTier' 2>/dev/null || echo "NOVEL")
1127
+ CELL_SURVIVAL=$(echo "$CELL_DATA" | jq -r '.survivalRate // "null"' 2>/dev/null || echo "null")
1128
+ CELL_SAMPLES=$(echo "$CELL_DATA" | jq -r '.sampleCount' 2>/dev/null || echo "0")
1129
+
1130
+ if [ "$CELL_TRUSTED" = "true" ]; then
1131
+ INTENSITY_TIER="$CELL_TIER"
1132
+ echo "=== PROVENANCE: cell found (${CELL_SAMPLES} samples, survival ${CELL_SURVIVAL}) — tier: ${INTENSITY_TIER} ==="
1133
+ else
1134
+ INTENSITY_TIER="NOVEL"
1135
+ echo "=== PROVENANCE: cell found but not yet trusted (${CELL_SAMPLES} samples < min_samples) — tier: NOVEL ==="
1136
+ fi
1137
+ fi
1138
+ fi
1139
+ ```
1140
+
1141
+ #### Step 4: Apply intensity tier (shadow-mode gated)
1142
+
1143
+ ```bash
1144
+ echo "=== TRUST_SHADOW_MODE: ${TRUST_SHADOW_MODE} ==="
1145
+ echo "=== INTENSITY_TIER (pre-application): ${INTENSITY_TIER} ==="
1146
+
1147
+ # Explicit skip guard (enforces the "Skip if" prose above in code): in THOROUGH or
1148
+ # milestone mode the full union dispatch is authoritative — provenance MUST NOT narrow
1149
+ # the roster here. Enforcing it at the application point means correctness no longer
1150
+ # depends on a downstream overwrite re-widening SELECTED_AGENTS. <!-- forge#1804 -->
1151
+ if [ "$THOROUGH" = "true" ] || [ "$IS_MILESTONE_TO_STAGING" = "true" ]; then
1152
+ echo "=== TRUST ESCALATION: skipped (THOROUGH/milestone mode — full union dispatch is authoritative) ==="
1153
+ # Shadow mode: log the decision but do NOT modify SELECTED_AGENTS.
1154
+ # Set review.trust_shadow_mode: "false" in forge.yaml to enforce after the 30-day window.
1155
+ elif [ "$TRUST_SHADOW_MODE" != "false" ]; then
1156
+ echo "=== TRUST ESCALATION: shadow-mode active — logging decision, roster unchanged ==="
1157
+ # SELECTED_AGENTS is NOT modified — current behavior preserved
1158
+ else
1159
+ case "$INTENSITY_TIER" in
1160
+ PROVEN)
1161
+ # PROVEN tier: may drop optional judgment agents, but NEVER the hard floor.
1162
+ # The Security agent and any agents added by hard-signal escalation triggers
1163
+ # (SCORE_AUTH >= 3, SCORE_BILLING >= 3, SCORE_DATABASE >= 3, SCORE_INFRA >= 3)
1164
+ # are part of the floor — provenance trust MUST NOT remove them.
1165
+ #
1166
+ # Only agents added by the top-1/2-domain scoring (Step 1 baseline selection)
1167
+ # that are NOT flagged by a hard escalation trigger are eligible for removal.
1168
+ # For safety in the initial enforcement window, we only remove agents that
1169
+ # scored exactly 1 (informational — file-presence only) and were not triggered
1170
+ # by any cross-critical or churn escalation.
1171
+ FLOOR_AGENTS="Security"
1172
+ [ "$SCORE_AUTH" -ge 3 ] && FLOOR_AGENTS="$FLOOR_AGENTS Auth"
1173
+ [ "$SCORE_BILLING" -ge 3 ] && FLOOR_AGENTS="$FLOOR_AGENTS Billing Concurrency"
1174
+ [ "$SCORE_DATABASE" -ge 3 ] && FLOOR_AGENTS="$FLOOR_AGENTS Database"
1175
+ [ "$SCORE_INFRA" -ge 3 ] && FLOOR_AGENTS="$FLOOR_AGENTS Infrastructure"
1176
+ [ "$CHURN_ESCALATION" = "true" ] && FLOOR_AGENTS="$FLOOR_AGENTS $TOP_CHURN_DOMAIN"
1177
+ # Deduplicate floor
1178
+ FLOOR_AGENTS=$(echo "$FLOOR_AGENTS" | tr ' ' '\n' | sort -u | tr '\n' ' ')
1179
+ # PROVEN: keep only floor agents (drop optional judgment-only agents)
1180
+ SELECTED_AGENTS="$FLOOR_AGENTS"
1181
+ echo "=== TRUST ESCALATION: PROVEN — roster narrowed to floor: ${SELECTED_AGENTS} ==="
1182
+ ;;
1183
+ NOVEL|NOVEL_NEEDS_HUMAN)
1184
+ # NOVEL tier: escalate to full affected-domain panel (add all scored domains)
1185
+ for DOMAIN in AUTH BILLING CONCURRENCY DATABASE INFRA FRONTEND API; do
1186
+ SCORE_VAR="SCORE_${DOMAIN}"
1187
+ [ "${!SCORE_VAR:-0}" -gt 0 ] && add_agent "$DOMAIN"
1188
+ done
1189
+ SELECTED_AGENTS=$(echo "$SELECTED_AGENTS" | tr ' ' '\n' | sort -u | tr '\n' ' ')
1190
+ echo "=== TRUST ESCALATION: ${INTENSITY_TIER} — roster widened to full panel: ${SELECTED_AGENTS} ==="
1191
+ if [ "$INTENSITY_TIER" = "NOVEL_NEEDS_HUMAN" ]; then
1192
+ echo "=== TRUST ESCALATION: NOVEL_NEEDS_HUMAN — needs-human will be added on merge ==="
1193
+ TRUST_NEEDS_HUMAN=true
1194
+ fi
1195
+ ;;
1196
+ SHADOW|*)
1197
+ # SHADOW or unknown: no change
1198
+ echo "=== TRUST ESCALATION: ${INTENSITY_TIER} — roster unchanged ==="
1199
+ ;;
1200
+ esac
1201
+ fi
1202
+
1203
+ # Record TRUST_NEEDS_HUMAN default (set to false if not already set by NOVEL_NEEDS_HUMAN branch)
1204
+ TRUST_NEEDS_HUMAN="${TRUST_NEEDS_HUMAN:-false}"
1205
+
1206
+ echo "=== FINAL ROSTER (after trust escalation): $SELECTED_AGENTS ==="
1207
+ echo "=== INTENSITY_TIER: ${INTENSITY_TIER} | TABLE_CELL_KEY: ${TABLE_CELL_KEY} | SHADOW: ${TRUST_SHADOW_MODE} | NEEDS_HUMAN: ${TRUST_NEEDS_HUMAN} ==="
1208
+ ```
1209
+
1210
+ <!-- Added: forge#1745 -->
1211
+
955
1212
  **Full union dispatch (THOROUGH=true or IS_MILESTONE_TO_STAGING=true):**
956
1213
 
957
1214
  ```bash
@@ -1000,12 +1257,60 @@ DOMAIN_FILES_API=$(echo "$FILES" | grep -iE "router|route|endpoint|openapi|sdk|a
1000
1257
 
1001
1258
  ### 3C: Load Agent Templates & Launch
1002
1259
 
1003
- **>>> INVOCATION: Read shared protocols + selected persona files:**
1260
+ **>>> MANDATORY TEMPLATE-RESOLUTION GUARD — run BEFORE any Read of persona templates:**
1261
+
1262
+ Missing persona templates are a fatal setup error, not permission to skip multi-agent review. Resolve the template source through this ordered chain and STOP if none resolve — never fall through to reviewing inline in the main context:
1263
+
1264
+ ```bash
1265
+ # Tier 1: $FORGE_HOME (the installed location — the common case)
1266
+ TEMPLATE_BASE=""
1267
+ if [[ -f "$FORGE_HOME/commands/review-pr-agents/protocols.md" ]]; then
1268
+ TEMPLATE_BASE="$FORGE_HOME/commands/review-pr-agents"
1269
+ TEMPLATE_SOURCE="forge_home"
1270
+ else
1271
+ # Tier 2: repo-path fallback — same resolution already used for the code index below.
1272
+ # FORGE_YAML is resolved independently here (not yet loaded — that happens further
1273
+ # down in this phase) so the guard does not depend on later-phase ordering.
1274
+ FORGE_YAML="${FORGE_CONFIG:-$(git rev-parse --show-toplevel 2>/dev/null)/forge.yaml}"
1275
+ REPO_PATH=$(yq '.paths.root' "$FORGE_YAML" 2>/dev/null || git rev-parse --show-toplevel 2>/dev/null || pwd)
1276
+ if [[ -f "$REPO_PATH/commands/review-pr-agents/protocols.md" ]]; then
1277
+ TEMPLATE_BASE="$REPO_PATH/commands/review-pr-agents"
1278
+ TEMPLATE_SOURCE="repo_path"
1279
+ elif [[ -f "$REPO_PATH/commands/review-pr-agents.md" ]] && grep -q "^### Agent:" "$REPO_PATH/commands/review-pr-agents.md" 2>/dev/null; then
1280
+ # Tier 3: monolithic catalog — last resort, contains all personas + protocols in one file.
1281
+ # The content check (`### Agent:` headers) is required, not just file existence: a
1282
+ # post-split repo still ships a small router stub at this same path that only points
1283
+ # BACK to the (missing) commands/review-pr-agents/ directory — reading it would not
1284
+ # provide any actual protocol/persona content, silently reproducing the original bug
1285
+ # one tier deeper. <!-- Added: forge#1849 -->
1286
+ TEMPLATE_BASE=""
1287
+ MONOLITHIC_CATALOG="$REPO_PATH/commands/review-pr-agents.md"
1288
+ TEMPLATE_SOURCE="monolithic_catalog"
1289
+ else
1290
+ TEMPLATE_SOURCE="none"
1291
+ fi
1292
+ fi
1293
+
1294
+ if [[ "$TEMPLATE_SOURCE" == "none" ]]; then
1295
+ echo "FATAL: no review-pr-agents template source resolved (checked \$FORGE_HOME, repo-path fallback, monolithic catalog)."
1296
+ # HARD STOP — post error, add needs-human, do NOT review
1297
+ fi
1298
+ ```
1299
+
1300
+ **If `TEMPLATE_SOURCE` is `none`**: HARD STOP. Post a PR comment explaining the setup is broken (`gh pr comment $ARGUMENTS --body "..."`) instructing the user to run `npx forgedock update` to repair the install, add `needs-human`, and exit Phase 3 without posting any findings or a `FORGE:REVIEW` verdict. **NEVER perform the review inline in the main agent context as a substitute.** A degraded solo review that presents itself as complete is worse than no review — missing templates must fail loudly, not silently.
1301
+
1302
+ **If `TEMPLATE_SOURCE` is `forge_home` or `repo_path`** (the normal cases — behavior unchanged from before this guard existed):
1004
1303
  ```
1005
- Read: $FORGE_HOME/commands/review-pr-agents/protocols.md
1006
- Read: $FORGE_HOME/commands/review-pr-agents/<persona>.md (one per selected agent from Phase 3B)
1304
+ Read: $TEMPLATE_BASE/protocols.md
1305
+ Read: $TEMPLATE_BASE/<persona>.md (one per selected agent from Phase 3B)
1007
1306
  ```
1008
1307
 
1308
+ **If `TEMPLATE_SOURCE` is `monolithic_catalog`** (last-resort fallback):
1309
+ ```
1310
+ Read: $MONOLITHIC_CATALOG
1311
+ ```
1312
+ Then extract the shared protocols section and each selected persona's section from within that single file — same content, different packaging.
1313
+
1009
1314
  Persona file names: `security.md` (always), `auth.md`, `billing.md`, `concurrency.md`, `scraper.md`, `frontend.md`, `api.md`, `database.md`, `infra.md`.
1010
1315
  See `review-pr-agents.md` for the full routing table mapping domains → persona files.
1011
1316
 
@@ -1180,7 +1485,7 @@ The `protocols.md` file contains the Evidence-Based Review Protocol, Structured
1180
1485
  6. Substitute code index slice: `[INDEX_SLICE]` → the matching `$INDEX_SLICE_{DOMAIN}` variable for this agent (e.g., `$INDEX_SLICE_AUTH` for the auth agent). Agents MUST query index data first; fall back to grep only when index slice is empty or unavailable.
1181
1486
  7. Substitute per-agent diff slice: `[DOMAIN_DIFF_SLICE]` → the matching `$DIFF_SLICE_*` variable (e.g., `$DIFF_SLICE_AUTH` for the auth agent, `$DIFF_SLICE_SECURITY` for the security agent). This replaces any `gh pr diff [PR_NUMBER]` call inside the agent template — the agent works from the pre-computed slice, not the full changeset.
1182
1487
  8. If Phase 2.5 found broken assumptions, append them to the agent's prompt as "Pre-found integration issues to verify"
1183
- 9. Launch via `Task` tool with the resolved model (default `"sonnet"`, fallback `"opus"` if rate-limited)
1488
+ 9. Launch via `Task` tool with `model: "{SUBAGENT_MODEL}"` (forge.yaml `agents.subagent_model`, else `agents.default_model`, else `"sonnet"`; fallback `"opus"` if rate-limited)
1184
1489
 
1185
1490
  **CRITICAL**: Launch ALL selected agents in a SINGLE message using multiple Task tool calls. Each agent posts findings directly to the PR via `gh pr comment`.
1186
1491
 
@@ -1230,7 +1535,7 @@ AGENT_COUNT=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --jq '[.[] | s
1230
1535
  FINDING_COUNT=$(echo "$ALL_FINDINGS" | grep -c '.' || echo 0)
1231
1536
  ```
1232
1537
 
1233
- If synthesis needed, launch a `general-purpose` Task (model: resolved per policy — default sonnet, fallback opus):
1538
+ If synthesis needed, launch a `general-purpose` Task (model: `"{SUBAGENT_MODEL}"`, fallback `"opus"` if rate-limited):
1234
1539
  - Deduplicate findings by file + line range ±5 (keep higher confidence)
1235
1540
  - Resolve contradictions by reading disputed code
1236
1541
  - Dismiss false positives with evidence
@@ -1586,16 +1891,22 @@ if [ "$ATTRIBUTION_PR_FOOTER" = "true" ]; then
1586
1891
  > Orchestrated with [ForgeDock](https://github.com/RapierCraftStudios/ForgeDock) — state, scheduling, review, and memory on GitHub."
1587
1892
  fi
1588
1893
 
1894
+ # Build trust annotation line for verdict body — always included so the decision is auditable.
1895
+ # INTENSITY_TIER and TABLE_CELL_KEY are set in Phase 3B.5 (defaults: SHADOW / unavailable).
1896
+ TRUST_ANNOTATION="**Intensity tier**: ${INTENSITY_TIER:-SHADOW} | **Cell**: \`${TABLE_CELL_KEY:-(unavailable)}\` | **Shadow mode**: ${TRUST_SHADOW_MODE:-true}"
1897
+
1589
1898
  # Stale review:
1590
1899
  gh pr review $ARGUMENTS --comment --body "Review of commit $REVIEW_SHA_SHORT is stale — PR HEAD changed. Re-run /review-pr."
1591
1900
 
1592
1901
  # Clean (no blocking issues):
1593
1902
  gh pr review $ARGUMENTS --comment --body "APPROVED: commit $REVIEW_SHA_SHORT after context-aware review ([N] agents: [names]). [M] findings created as issues. Safe to merge.
1903
+ ${TRUST_ANNOTATION}
1594
1904
  $([ "$MERGE_HEALTH" = "UNKNOWN" ] && echo "
1595
1905
  ⚠ Mergeability: GitHub is still computing merge state (UNKNOWN after retries). Verify manually before merging.")${ATTRIBUTION_FOOTER_LINE}"
1596
1906
 
1597
1907
  # Blocking issues (including merge conflicts and purpose regressions):
1598
1908
  gh pr review $ARGUMENTS --comment --body "CHANGES REQUESTED: commit $REVIEW_SHA_SHORT — [N] blocking issues found. See GitHub issues.
1909
+ ${TRUST_ANNOTATION}
1599
1910
  $([ "$HAS_MERGE_CONFLICT" = "true" ] && echo "
1600
1911
  🔴 Merge Conflict: ${MERGE_CONFLICT_MSG}")
1601
1912
  $([ "$HAS_PURPOSE_REGRESSION" = "true" ] && echo "
@@ -1604,23 +1915,112 @@ $([ "$HAS_PURPOSE_REGRESSION" = "true" ] && echo "
1604
1915
 
1605
1916
  ---
1606
1917
 
1918
+ ## Phase 7B.5: Calibration Threshold Consultation (Conditional) <!-- Added: forge#1741 -->
1919
+
1920
+ **Skip if**: `AUTO_MERGE=false` AND no calibration-based routing is needed (this phase is informational even when not auto-merging — run it to populate `CALIBRATION_NEEDS_HUMAN` for Phase 8).
1921
+
1922
+ **Purpose**: Read the confidence calibration table (published to the `forge-knowledge` branch by `scripts/calibration.mjs`) and check whether the current PR's task-type × confidence combination has a survival rate below the overconfidence threshold. If the table says HIGH-confidence in this task type has historically performed poorly (< 80% survival), route to needs-human regardless of the agent verdict.
1923
+
1924
+ **Fail-safe**: ANY error reading the calibration table (branch absent, file missing, JSON parse error, git failure) MUST result in `CALIBRATION_NEEDS_HUMAN=false` and the current static blocking criteria (Phase 7B verdict) remaining authoritative. The calibration table can ONLY tighten behavior (add needs-human); it NEVER loosens behavior below the current static baseline.
1925
+
1926
+ ```bash
1927
+ # Phase 7B.5: Calibration threshold consultation
1928
+ CALIBRATION_NEEDS_HUMAN=false
1929
+ CALIBRATION_CELL=""
1930
+ CALIBRATION_NOTE=""
1931
+
1932
+ # Read task type from FORGE:INVESTIGATOR on the linked issue
1933
+ # (MERGE_ISSUE is the issue number; passed from --auto-merge args)
1934
+ ISSUE_NUMBER="${MERGE_ISSUE:-}"
1935
+
1936
+ if [ -n "$ISSUE_NUMBER" ]; then
1937
+ INVESTIGATOR_BODY=$(gh api "repos/{GH_REPO}/issues/${ISSUE_NUMBER}/comments" \
1938
+ --jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR"))] | last | .body // ""' 2>/dev/null || echo '')
1939
+ TASK_TYPE=$(echo "$INVESTIGATOR_BODY" | grep -oP '(?<=\*\*Task Type\*\*: )[^\n]+' | head -1 | tr -d ' \r')
1940
+ CONFIDENCE=$(echo "$INVESTIGATOR_BODY" | grep -oP '(?<=\*\*Confidence\*\*: )[^\n]+' | head -1 | tr -d ' \r')
1941
+ echo "Phase 7B.5: task_type=${TASK_TYPE:-unknown} confidence=${CONFIDENCE:-unknown} (from issue #${ISSUE_NUMBER})"
1942
+ fi
1943
+
1944
+ # Read calibration table from forge-knowledge branch (fail-safe: any error → skip)
1945
+ if [ -n "${TASK_TYPE:-}" ] && [ -n "${CONFIDENCE:-}" ]; then
1946
+ CALIB_RAW=$(git show "origin/forge-knowledge:calibration/table.json" 2>/dev/null || echo '')
1947
+
1948
+ if [ -n "$CALIB_RAW" ]; then
1949
+ # Look up the task-type × confidence cell
1950
+ CALIB_CELL=$(echo "$CALIB_RAW" | jq -r --arg tt "$TASK_TYPE" --arg c "$CONFIDENCE" \
1951
+ '.rows[] | select(.taskType == $tt and .confidence == $c and .trusted == true)' 2>/dev/null || echo '')
1952
+
1953
+ if [ -n "$CALIB_CELL" ]; then
1954
+ SURVIVAL_RATE=$(echo "$CALIB_CELL" | jq -r '.survivalRate // "null"' 2>/dev/null || echo 'null')
1955
+ SAMPLE_COUNT=$(echo "$CALIB_CELL" | jq -r '.sampleCount // 0' 2>/dev/null || echo '0')
1956
+ CELL_FLAG=$(echo "$CALIB_CELL" | jq -r '.flag // "null"' 2>/dev/null || echo 'null')
1957
+
1958
+ CALIBRATION_CELL="${TASK_TYPE} × ${CONFIDENCE}: survival=${SURVIVAL_RATE} (n=${SAMPLE_COUNT})"
1959
+ echo "Phase 7B.5: calibration cell found — ${CALIBRATION_CELL} flag=${CELL_FLAG}"
1960
+
1961
+ # Check overconfidence threshold: HIGH confidence with survival < 0.8
1962
+ if [ "$CONFIDENCE" = "HIGH" ] && [ "$CELL_FLAG" = "overconfidence" ]; then
1963
+ CALIBRATION_NEEDS_HUMAN=true
1964
+ CALIBRATION_NOTE="Calibration table: ${TASK_TYPE} × HIGH confidence has ${SURVIVAL_RATE} survival rate (< 0.80 threshold, n=${SAMPLE_COUNT}). Routing to needs-human per forge#1741 policy."
1965
+ echo "Phase 7B.5: CALIBRATION_NEEDS_HUMAN=true — ${CALIBRATION_NOTE}"
1966
+ else
1967
+ CALIBRATION_NOTE="Calibration cell: ${CALIBRATION_CELL} — within acceptable threshold"
1968
+ echo "Phase 7B.5: CALIBRATION_NEEDS_HUMAN=false — ${CALIBRATION_NOTE}"
1969
+ fi
1970
+ else
1971
+ # Cell not found or not trusted (below min-samples) — static behavior applies
1972
+ CALIBRATION_NOTE="Calibration cell for ${TASK_TYPE} × ${CONFIDENCE} is absent or untrusted — using static behavior"
1973
+ echo "Phase 7B.5: no trusted cell — ${CALIBRATION_NOTE}"
1974
+ fi
1975
+ else
1976
+ CALIBRATION_NOTE="forge-knowledge:calibration/table.json not found — using static behavior"
1977
+ echo "Phase 7B.5: calibration table unavailable — ${CALIBRATION_NOTE}"
1978
+ fi
1979
+ else
1980
+ CALIBRATION_NOTE="task type or confidence not resolved from FORGE:INVESTIGATOR — using static behavior"
1981
+ echo "Phase 7B.5: could not resolve task type/confidence — ${CALIBRATION_NOTE}"
1982
+ fi
1983
+
1984
+ # Log threshold decision in TRAJECTORY (append to existing issue comment or note for Phase 6)
1985
+ # This satisfies the acceptance criterion: "Threshold adjustments appear in TRAJECTORY with the cell that justified them"
1986
+ if [ -n "$ISSUE_NUMBER" ]; then
1987
+ gh issue comment "${ISSUE_NUMBER}" {MERGE_GH_FLAG} --body "<!-- FORGE:CALIBRATION_CHECK -->
1988
+ **Phase 7B.5 — Calibration Threshold Check**
1989
+ **Cell**: ${CALIBRATION_CELL:-not found}
1990
+ **CALIBRATION_NEEDS_HUMAN**: ${CALIBRATION_NEEDS_HUMAN}
1991
+ **Note**: ${CALIBRATION_NOTE}
1992
+
1993
+ **Phase 3B.5 — Provenance Trust Decision**
1994
+ **Intensity tier**: ${INTENSITY_TIER:-SHADOW}
1995
+ **Provenance cell**: \`${TABLE_CELL_KEY:-(unavailable)}\`
1996
+ **Shadow mode**: ${TRUST_SHADOW_MODE:-true}
1997
+ **TRUST_NEEDS_HUMAN**: ${TRUST_NEEDS_HUMAN:-false}
1998
+ **Timestamp**: $(date -u +%Y-%m-%dT%H:%M:%SZ)" 2>/dev/null || true
1999
+ fi
2000
+ ```
2001
+
2002
+ ---
2003
+
1607
2004
  ## Phase 8: Auto-Merge (Conditional)
1608
2005
 
1609
2006
  **Skip if** `AUTO_MERGE=false`.
1610
2007
 
1611
2008
  ```bash
1612
- # §7B verdict + purpose-regression guard — check before any merge attempt <!-- Added: forge#1601 -->
1613
- # HARD RULE 3 requires that VERDICT=CHANGES REQUESTED and HAS_PURPOSE_REGRESSION=true
1614
- # always block merge, including under --auto-merge. These vars are set in Phase 7A/7B
1615
- # earlier in the same agent session. An unset/empty VERDICT is safe — it evaluates to
1616
- # "" which does not equal "CHANGES REQUESTED", so fast-lane PRs (no 7A run) are unaffected.
1617
- if [ "$VERDICT" = "CHANGES REQUESTED" ] || [ "$HAS_PURPOSE_REGRESSION" = "true" ]; then
2009
+ # §7B verdict + purpose-regression + calibration + trust-escalation guard — check before any merge attempt <!-- Added: forge#1601, forge#1741, forge#1745 -->
2010
+ # HARD RULE 3 requires that VERDICT=CHANGES REQUESTED, HAS_PURPOSE_REGRESSION=true,
2011
+ # CALIBRATION_NEEDS_HUMAN=true, AND TRUST_NEEDS_HUMAN=true all block merge, including under --auto-merge.
2012
+ # These vars are set in Phase 7A/7B/7B.5/3B.5 earlier in the same agent session.
2013
+ # An unset/empty VERDICT is safe — it evaluates to "" which does not equal "CHANGES REQUESTED".
2014
+ # TRUST_NEEDS_HUMAN: set to true by Phase 3B.5 when INTENSITY_TIER=NOVEL_NEEDS_HUMAN AND shadow mode is off.
2015
+ if [ "$VERDICT" = "CHANGES REQUESTED" ] || [ "$HAS_PURPOSE_REGRESSION" = "true" ] || [ "$CALIBRATION_NEEDS_HUMAN" = "true" ] || [ "${TRUST_NEEDS_HUMAN:-false}" = "true" ]; then
1618
2016
  BLOCK_REASON=""
1619
2017
  [ "$VERDICT" = "CHANGES REQUESTED" ] && BLOCK_REASON="review verdict is CHANGES REQUESTED (blocking finding confirmed by Phase 7B)"
1620
2018
  [ "$HAS_PURPOSE_REGRESSION" = "true" ] && BLOCK_REASON="${BLOCK_REASON:+${BLOCK_REASON}; }purpose regression detected by Phase 7A (\`HAS_PURPOSE_REGRESSION=true\`)"
2019
+ [ "$CALIBRATION_NEEDS_HUMAN" = "true" ] && BLOCK_REASON="${BLOCK_REASON:+${BLOCK_REASON}; }calibration threshold: ${CALIBRATION_NOTE}"
2020
+ [ "${TRUST_NEEDS_HUMAN:-false}" = "true" ] && BLOCK_REASON="${BLOCK_REASON:+${BLOCK_REASON}; }provenance trust: NOVEL_NEEDS_HUMAN tier for cell \`${TABLE_CELL_KEY}\` — no sufficient prior data (forge#1745)"
1621
2021
  gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "⛔ Auto-merge aborted for PR #{PR_NUMBER}: ${BLOCK_REASON}. Manual review required before merging."
1622
2022
  gh issue edit {MERGE_ISSUE} {MERGE_GH_FLAG} --add-label "needs-human" 2>/dev/null || true
1623
- # STOP — do not attempt gh pr merge when §7B blocking conditions are active
2023
+ # STOP — do not attempt gh pr merge when §7B/7B.5 blocking conditions are active
1624
2024
  else
1625
2025
 
1626
2026
  # Pre-merge mergeability guard — re-fetch fresh state before attempting merge <!-- Added: forge#194 -->
@@ -1635,6 +2035,31 @@ if [ "$PRE_MERGE_HEALTH" = "CONFLICTING" ] || [ "$PRE_MERGE_HEALTH_STATE" = "DIR
1635
2035
  gh issue edit {MERGE_ISSUE} {MERGE_GH_FLAG} --add-label "needs-human" 2>/dev/null || true
1636
2036
  # STOP — do not attempt gh pr merge on a CONFLICTING/DIRTY PR
1637
2037
  else
2038
+
2039
+ # Previously-escalated re-review guard <!-- Added: forge#1810 -->
2040
+ # If the linked issue currently carries needs-human, this PR was escalated at some
2041
+ # earlier point (VERDICT/purpose-regression/calibration/trust/mergeability) and has
2042
+ # now been remediated + re-reviewed back to a clean, mergeable APPROVED state above.
2043
+ # The "auto-land bar" (≥2 independent adversarial approvals + domain gates — see
2044
+ # #1809 Q1) is not yet implemented (future #1809 sub-issues B/C/D), so the safe
2045
+ # default here is to land on workflow:awaiting-merge (clearing needs-human) rather
2046
+ # than silently auto-merging a PR that was once flagged human-risk.
2047
+ PREVIOUSLY_ESCALATED=$(gh issue view {MERGE_ISSUE} {MERGE_GH_FLAG} --json labels \
2048
+ --jq '[.labels[].name] | any(. == "needs-human")' 2>/dev/null || echo "false")
2049
+
2050
+ if [ "$PREVIOUSLY_ESCALATED" = "true" ]; then
2051
+ gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "🟠 PR #{PR_NUMBER} was previously escalated (\`needs-human\`) and has now been re-reviewed to \`${VERDICT:-APPROVED}\` with a clean mergeability check. The automated auto-land bar for previously-escalated PRs is not yet implemented, so this PR is held at \`workflow:awaiting-merge\` for a human merge decision instead of auto-merging. Merge manually once reviewed: \`gh pr merge {PR_NUMBER} {MERGE_GH_FLAG} --merge\`."
2052
+ RESOLUTION=$(resolve_script 'transition-label')
2053
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
2054
+ case "$TIER" in
2055
+ adaptive|universal) bash "$SCRIPT_PATH" {MERGE_ISSUE} {MERGE_GH_FLAG} awaiting-merge ;;
2056
+ prose)
2057
+ gh issue edit {MERGE_ISSUE} {MERGE_GH_FLAG} --add-label "workflow:awaiting-merge" \
2058
+ --remove-label "needs-human,workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
2059
+ ;;
2060
+ esac
2061
+ # STOP — do not attempt gh pr merge; a human decides the merge from here
2062
+ else
1638
2063
  # Checkpoint comment on issue
1639
2064
  gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "Review complete for PR #{PR_NUMBER}. Verdict: ${VERDICT:-APPROVED}. Proceeding to merge."
1640
2065
 
@@ -1646,6 +2071,7 @@ else
1646
2071
  [ "$MERGE_STATE" != "MERGED" ] && gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "PR #{PR_NUMBER} merge failed. State: $MERGE_STATE."
1647
2072
  fi
1648
2073
  fi
2074
+ fi
1649
2075
  ```
1650
2076
 
1651
2077
  **Important**: Phase 8 ONLY merges the PR. It does NOT close the issue, update labels, or clean up worktrees. When invoked via `/work-on`, those responsibilities belong to `work-on/close.md` (work-on.md Phase 6 — triggered when PR is merged and issue is still open). Doing them here would cause the issue to be closed with `workflow:merged` before Phase 6 runs, skipping the close phase entirely.
@@ -1739,6 +2165,23 @@ REVIEW_IS_STALE="false"
1739
2165
  if [ "$CURRENT_SHA" != "$REVIEW_SHA" ]; then REVIEW_IS_STALE="true"; fi
1740
2166
  ```
1741
2167
 
2168
+ **Verifiable agent count (MANDATORY — do not report a self-asserted `Agents: [N]` figure)** <!-- Added: forge#1849 -->
2169
+
2170
+ A degraded run that skipped Task-based agent dispatch must be visible from this summary alone, without interrogating the agent afterward. Compute the actual launched-agent count from the `<!-- FORGE:REVIEW-AGENT:{domain} -->` comments each agent is required to post (Phase 3C), rather than trusting a free-text tally:
2171
+
2172
+ ```bash
2173
+ ACTUAL_AGENT_DOMAINS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
2174
+ --jq '[.[].body | scan("<!-- FORGE:REVIEW-AGENT:([a-z-]+) -->") | .[0]] | unique | join(", ")' 2>/dev/null || echo "")
2175
+ # NOTE: no `|| echo 0` fallback here — grep -c always prints a count (including 0 on
2176
+ # no match) even though it exits 1 in that case. Appending `|| echo 0` after a `grep -c`
2177
+ # pipeline double-counts: the pipeline already printed "0", then the fallback prints a
2178
+ # second "0" line, corrupting the value to "0\n0" in exactly the zero-agent case this
2179
+ # check exists to detect. <!-- Added: forge#1849 -->
2180
+ ACTUAL_AGENT_COUNT=$(echo "$ACTUAL_AGENT_DOMAINS" | tr ',' '\n' | grep -c '.' 2>/dev/null)
2181
+ ```
2182
+
2183
+ Substitute `ACTUAL_AGENT_COUNT`/`ACTUAL_AGENT_DOMAINS` into the summary's `**Agents**: [N] ([names])` field below — do NOT substitute a manually-counted or remembered figure. If the agent's own recollection of what it launched disagrees with `ACTUAL_AGENT_COUNT` (e.g. it believes it ran agents but zero `FORGE:REVIEW-AGENT` comments exist), that mismatch itself is the signal this check exists to surface: report `ACTUAL_AGENT_COUNT` as-is (it is the ground truth) and add a note in the Recommendation section flagging the discrepancy — never suppress it to make the summary look complete. If `ACTUAL_AGENT_COUNT` is `0`, the review degraded to solo/inline mode (the exact failure mode this guard exists to catch) and the verdict MUST reflect that (`NEEDS RE-REVIEW`), regardless of what analysis was performed inline.
2184
+
1742
2185
  ```bash
1743
2186
  gh pr comment $ARGUMENTS --body "$(cat <<'EOF'
1744
2187
  # PR Review Summary: #[NUMBER] - [TITLE]
@@ -16,7 +16,7 @@ install: extras
16
16
 
17
17
  You are the pipeline's emergency rollback system. When a shipped feature or fix causes production issues, this command creates a revert PR and fast-tracks it through the pipeline.
18
18
 
19
- **Agent model policy**: `model: "sonnet"` (standard tier). Fallback: `model: "opus"` if rate-limited. User can override with `--model <name>`. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
19
+ **Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. User can override with `--model <name>`. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
20
20
 
21
21
  **NEVER use plan mode (EnterPlanMode)** — it breaks execution context.
22
22