forgedock 1.1.6 → 1.1.8

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 (43) hide show
  1. package/README.md +3 -1
  2. package/bin/engine/invariants.mjs +361 -0
  3. package/bin/hooks/pre-tool-use.mjs +182 -0
  4. package/bin/journey.mjs +77 -7
  5. package/bin/labels.json +10 -0
  6. package/bin/recall.mjs +540 -0
  7. package/commands/autopilot.md +959 -78
  8. package/commands/cleanup.md +52 -3
  9. package/commands/optimize.md +585 -0
  10. package/commands/orchestrate/phase-1-resolve.md +35 -25
  11. package/commands/orchestrate/phase-3-dependency.md +465 -34
  12. package/commands/orchestrate/phase-4-execution.md +448 -29
  13. package/commands/orchestrate/phase-6-report.md +198 -9
  14. package/commands/pipeline-health/phase-2-metrics.md +305 -1
  15. package/commands/pipeline-health/phase-4-proposals.md +127 -0
  16. package/commands/quality-gate.md +417 -0
  17. package/commands/review-pr.md +391 -13
  18. package/commands/spec-doctor.md +429 -0
  19. package/commands/validate.md +52 -0
  20. package/commands/work-on/build/architect.md +77 -0
  21. package/commands/work-on/build/context.md +357 -22
  22. package/commands/work-on/build/implement.md +2 -0
  23. package/commands/work-on/build/validate.md +28 -0
  24. package/commands/work-on/build.md +44 -0
  25. package/commands/work-on/close.md +621 -8
  26. package/commands/work-on/investigate.md +221 -0
  27. package/commands/work-on.md +15 -14
  28. package/docs/FORGE-PROTOCOL.md +101 -0
  29. package/docs/design/awesome-list-submissions.md +105 -0
  30. package/docs/design/f5bot-monitoring-setup.md +43 -0
  31. package/docs/design/growth-strategy-2026.md +579 -0
  32. package/docs/spec/forge-protocol-v1.md +97 -5
  33. package/docs/spec/label-state-machine.md +6 -1
  34. package/package.json +2 -2
  35. package/packages/protocol/src/cli.js +374 -84
  36. package/packages/protocol/src/types.js +75 -1
  37. package/scripts/build-knowledge-index.mjs +1761 -0
  38. package/scripts/calibration.mjs +996 -0
  39. package/scripts/danger-zones.mjs +782 -0
  40. package/scripts/doctor-pipeline-state.sh +88 -2
  41. package/scripts/refresh-bot-token.sh +97 -0
  42. package/scripts/select-fix-targets.sh +234 -0
  43. package/scripts/transition-label.sh +157 -6
@@ -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
@@ -1586,16 +1843,22 @@ if [ "$ATTRIBUTION_PR_FOOTER" = "true" ]; then
1586
1843
  > Orchestrated with [ForgeDock](https://github.com/RapierCraftStudios/ForgeDock) — state, scheduling, review, and memory on GitHub."
1587
1844
  fi
1588
1845
 
1846
+ # Build trust annotation line for verdict body — always included so the decision is auditable.
1847
+ # INTENSITY_TIER and TABLE_CELL_KEY are set in Phase 3B.5 (defaults: SHADOW / unavailable).
1848
+ TRUST_ANNOTATION="**Intensity tier**: ${INTENSITY_TIER:-SHADOW} | **Cell**: \`${TABLE_CELL_KEY:-(unavailable)}\` | **Shadow mode**: ${TRUST_SHADOW_MODE:-true}"
1849
+
1589
1850
  # Stale review:
1590
1851
  gh pr review $ARGUMENTS --comment --body "Review of commit $REVIEW_SHA_SHORT is stale — PR HEAD changed. Re-run /review-pr."
1591
1852
 
1592
1853
  # Clean (no blocking issues):
1593
1854
  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.
1855
+ ${TRUST_ANNOTATION}
1594
1856
  $([ "$MERGE_HEALTH" = "UNKNOWN" ] && echo "
1595
1857
  ⚠ Mergeability: GitHub is still computing merge state (UNKNOWN after retries). Verify manually before merging.")${ATTRIBUTION_FOOTER_LINE}"
1596
1858
 
1597
1859
  # Blocking issues (including merge conflicts and purpose regressions):
1598
1860
  gh pr review $ARGUMENTS --comment --body "CHANGES REQUESTED: commit $REVIEW_SHA_SHORT — [N] blocking issues found. See GitHub issues.
1861
+ ${TRUST_ANNOTATION}
1599
1862
  $([ "$HAS_MERGE_CONFLICT" = "true" ] && echo "
1600
1863
  🔴 Merge Conflict: ${MERGE_CONFLICT_MSG}")
1601
1864
  $([ "$HAS_PURPOSE_REGRESSION" = "true" ] && echo "
@@ -1604,23 +1867,112 @@ $([ "$HAS_PURPOSE_REGRESSION" = "true" ] && echo "
1604
1867
 
1605
1868
  ---
1606
1869
 
1870
+ ## Phase 7B.5: Calibration Threshold Consultation (Conditional) <!-- Added: forge#1741 -->
1871
+
1872
+ **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).
1873
+
1874
+ **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.
1875
+
1876
+ **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.
1877
+
1878
+ ```bash
1879
+ # Phase 7B.5: Calibration threshold consultation
1880
+ CALIBRATION_NEEDS_HUMAN=false
1881
+ CALIBRATION_CELL=""
1882
+ CALIBRATION_NOTE=""
1883
+
1884
+ # Read task type from FORGE:INVESTIGATOR on the linked issue
1885
+ # (MERGE_ISSUE is the issue number; passed from --auto-merge args)
1886
+ ISSUE_NUMBER="${MERGE_ISSUE:-}"
1887
+
1888
+ if [ -n "$ISSUE_NUMBER" ]; then
1889
+ INVESTIGATOR_BODY=$(gh api "repos/{GH_REPO}/issues/${ISSUE_NUMBER}/comments" \
1890
+ --jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR"))] | last | .body // ""' 2>/dev/null || echo '')
1891
+ TASK_TYPE=$(echo "$INVESTIGATOR_BODY" | grep -oP '(?<=\*\*Task Type\*\*: )[^\n]+' | head -1 | tr -d ' \r')
1892
+ CONFIDENCE=$(echo "$INVESTIGATOR_BODY" | grep -oP '(?<=\*\*Confidence\*\*: )[^\n]+' | head -1 | tr -d ' \r')
1893
+ echo "Phase 7B.5: task_type=${TASK_TYPE:-unknown} confidence=${CONFIDENCE:-unknown} (from issue #${ISSUE_NUMBER})"
1894
+ fi
1895
+
1896
+ # Read calibration table from forge-knowledge branch (fail-safe: any error → skip)
1897
+ if [ -n "${TASK_TYPE:-}" ] && [ -n "${CONFIDENCE:-}" ]; then
1898
+ CALIB_RAW=$(git show "origin/forge-knowledge:calibration/table.json" 2>/dev/null || echo '')
1899
+
1900
+ if [ -n "$CALIB_RAW" ]; then
1901
+ # Look up the task-type × confidence cell
1902
+ CALIB_CELL=$(echo "$CALIB_RAW" | jq -r --arg tt "$TASK_TYPE" --arg c "$CONFIDENCE" \
1903
+ '.rows[] | select(.taskType == $tt and .confidence == $c and .trusted == true)' 2>/dev/null || echo '')
1904
+
1905
+ if [ -n "$CALIB_CELL" ]; then
1906
+ SURVIVAL_RATE=$(echo "$CALIB_CELL" | jq -r '.survivalRate // "null"' 2>/dev/null || echo 'null')
1907
+ SAMPLE_COUNT=$(echo "$CALIB_CELL" | jq -r '.sampleCount // 0' 2>/dev/null || echo '0')
1908
+ CELL_FLAG=$(echo "$CALIB_CELL" | jq -r '.flag // "null"' 2>/dev/null || echo 'null')
1909
+
1910
+ CALIBRATION_CELL="${TASK_TYPE} × ${CONFIDENCE}: survival=${SURVIVAL_RATE} (n=${SAMPLE_COUNT})"
1911
+ echo "Phase 7B.5: calibration cell found — ${CALIBRATION_CELL} flag=${CELL_FLAG}"
1912
+
1913
+ # Check overconfidence threshold: HIGH confidence with survival < 0.8
1914
+ if [ "$CONFIDENCE" = "HIGH" ] && [ "$CELL_FLAG" = "overconfidence" ]; then
1915
+ CALIBRATION_NEEDS_HUMAN=true
1916
+ 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."
1917
+ echo "Phase 7B.5: CALIBRATION_NEEDS_HUMAN=true — ${CALIBRATION_NOTE}"
1918
+ else
1919
+ CALIBRATION_NOTE="Calibration cell: ${CALIBRATION_CELL} — within acceptable threshold"
1920
+ echo "Phase 7B.5: CALIBRATION_NEEDS_HUMAN=false — ${CALIBRATION_NOTE}"
1921
+ fi
1922
+ else
1923
+ # Cell not found or not trusted (below min-samples) — static behavior applies
1924
+ CALIBRATION_NOTE="Calibration cell for ${TASK_TYPE} × ${CONFIDENCE} is absent or untrusted — using static behavior"
1925
+ echo "Phase 7B.5: no trusted cell — ${CALIBRATION_NOTE}"
1926
+ fi
1927
+ else
1928
+ CALIBRATION_NOTE="forge-knowledge:calibration/table.json not found — using static behavior"
1929
+ echo "Phase 7B.5: calibration table unavailable — ${CALIBRATION_NOTE}"
1930
+ fi
1931
+ else
1932
+ CALIBRATION_NOTE="task type or confidence not resolved from FORGE:INVESTIGATOR — using static behavior"
1933
+ echo "Phase 7B.5: could not resolve task type/confidence — ${CALIBRATION_NOTE}"
1934
+ fi
1935
+
1936
+ # Log threshold decision in TRAJECTORY (append to existing issue comment or note for Phase 6)
1937
+ # This satisfies the acceptance criterion: "Threshold adjustments appear in TRAJECTORY with the cell that justified them"
1938
+ if [ -n "$ISSUE_NUMBER" ]; then
1939
+ gh issue comment "${ISSUE_NUMBER}" {MERGE_GH_FLAG} --body "<!-- FORGE:CALIBRATION_CHECK -->
1940
+ **Phase 7B.5 — Calibration Threshold Check**
1941
+ **Cell**: ${CALIBRATION_CELL:-not found}
1942
+ **CALIBRATION_NEEDS_HUMAN**: ${CALIBRATION_NEEDS_HUMAN}
1943
+ **Note**: ${CALIBRATION_NOTE}
1944
+
1945
+ **Phase 3B.5 — Provenance Trust Decision**
1946
+ **Intensity tier**: ${INTENSITY_TIER:-SHADOW}
1947
+ **Provenance cell**: \`${TABLE_CELL_KEY:-(unavailable)}\`
1948
+ **Shadow mode**: ${TRUST_SHADOW_MODE:-true}
1949
+ **TRUST_NEEDS_HUMAN**: ${TRUST_NEEDS_HUMAN:-false}
1950
+ **Timestamp**: $(date -u +%Y-%m-%dT%H:%M:%SZ)" 2>/dev/null || true
1951
+ fi
1952
+ ```
1953
+
1954
+ ---
1955
+
1607
1956
  ## Phase 8: Auto-Merge (Conditional)
1608
1957
 
1609
1958
  **Skip if** `AUTO_MERGE=false`.
1610
1959
 
1611
1960
  ```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
1961
+ # §7B verdict + purpose-regression + calibration + trust-escalation guard — check before any merge attempt <!-- Added: forge#1601, forge#1741, forge#1745 -->
1962
+ # HARD RULE 3 requires that VERDICT=CHANGES REQUESTED, HAS_PURPOSE_REGRESSION=true,
1963
+ # CALIBRATION_NEEDS_HUMAN=true, AND TRUST_NEEDS_HUMAN=true all block merge, including under --auto-merge.
1964
+ # These vars are set in Phase 7A/7B/7B.5/3B.5 earlier in the same agent session.
1965
+ # An unset/empty VERDICT is safe — it evaluates to "" which does not equal "CHANGES REQUESTED".
1966
+ # TRUST_NEEDS_HUMAN: set to true by Phase 3B.5 when INTENSITY_TIER=NOVEL_NEEDS_HUMAN AND shadow mode is off.
1967
+ if [ "$VERDICT" = "CHANGES REQUESTED" ] || [ "$HAS_PURPOSE_REGRESSION" = "true" ] || [ "$CALIBRATION_NEEDS_HUMAN" = "true" ] || [ "${TRUST_NEEDS_HUMAN:-false}" = "true" ]; then
1618
1968
  BLOCK_REASON=""
1619
1969
  [ "$VERDICT" = "CHANGES REQUESTED" ] && BLOCK_REASON="review verdict is CHANGES REQUESTED (blocking finding confirmed by Phase 7B)"
1620
1970
  [ "$HAS_PURPOSE_REGRESSION" = "true" ] && BLOCK_REASON="${BLOCK_REASON:+${BLOCK_REASON}; }purpose regression detected by Phase 7A (\`HAS_PURPOSE_REGRESSION=true\`)"
1971
+ [ "$CALIBRATION_NEEDS_HUMAN" = "true" ] && BLOCK_REASON="${BLOCK_REASON:+${BLOCK_REASON}; }calibration threshold: ${CALIBRATION_NOTE}"
1972
+ [ "${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
1973
  gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "⛔ Auto-merge aborted for PR #{PR_NUMBER}: ${BLOCK_REASON}. Manual review required before merging."
1622
1974
  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
1975
+ # STOP — do not attempt gh pr merge when §7B/7B.5 blocking conditions are active
1624
1976
  else
1625
1977
 
1626
1978
  # Pre-merge mergeability guard — re-fetch fresh state before attempting merge <!-- Added: forge#194 -->
@@ -1635,6 +1987,31 @@ if [ "$PRE_MERGE_HEALTH" = "CONFLICTING" ] || [ "$PRE_MERGE_HEALTH_STATE" = "DIR
1635
1987
  gh issue edit {MERGE_ISSUE} {MERGE_GH_FLAG} --add-label "needs-human" 2>/dev/null || true
1636
1988
  # STOP — do not attempt gh pr merge on a CONFLICTING/DIRTY PR
1637
1989
  else
1990
+
1991
+ # Previously-escalated re-review guard <!-- Added: forge#1810 -->
1992
+ # If the linked issue currently carries needs-human, this PR was escalated at some
1993
+ # earlier point (VERDICT/purpose-regression/calibration/trust/mergeability) and has
1994
+ # now been remediated + re-reviewed back to a clean, mergeable APPROVED state above.
1995
+ # The "auto-land bar" (≥2 independent adversarial approvals + domain gates — see
1996
+ # #1809 Q1) is not yet implemented (future #1809 sub-issues B/C/D), so the safe
1997
+ # default here is to land on workflow:awaiting-merge (clearing needs-human) rather
1998
+ # than silently auto-merging a PR that was once flagged human-risk.
1999
+ PREVIOUSLY_ESCALATED=$(gh issue view {MERGE_ISSUE} {MERGE_GH_FLAG} --json labels \
2000
+ --jq '[.labels[].name] | any(. == "needs-human")' 2>/dev/null || echo "false")
2001
+
2002
+ if [ "$PREVIOUSLY_ESCALATED" = "true" ]; then
2003
+ 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\`."
2004
+ RESOLUTION=$(resolve_script 'transition-label')
2005
+ TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
2006
+ case "$TIER" in
2007
+ adaptive|universal) bash "$SCRIPT_PATH" {MERGE_ISSUE} {MERGE_GH_FLAG} awaiting-merge ;;
2008
+ prose)
2009
+ gh issue edit {MERGE_ISSUE} {MERGE_GH_FLAG} --add-label "workflow:awaiting-merge" \
2010
+ --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
2011
+ ;;
2012
+ esac
2013
+ # STOP — do not attempt gh pr merge; a human decides the merge from here
2014
+ else
1638
2015
  # Checkpoint comment on issue
1639
2016
  gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "Review complete for PR #{PR_NUMBER}. Verdict: ${VERDICT:-APPROVED}. Proceeding to merge."
1640
2017
 
@@ -1646,6 +2023,7 @@ else
1646
2023
  [ "$MERGE_STATE" != "MERGED" ] && gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "PR #{PR_NUMBER} merge failed. State: $MERGE_STATE."
1647
2024
  fi
1648
2025
  fi
2026
+ fi
1649
2027
  ```
1650
2028
 
1651
2029
  **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.