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.
- package/README.md +10 -33
- package/bin/cinema.mjs +23 -5
- package/bin/engine/invariants.mjs +361 -0
- package/bin/engine-cli.mjs +137 -11
- package/bin/env-detect.mjs +249 -0
- package/bin/forge-utils.mjs +364 -4
- package/bin/forgedock.mjs +1064 -135
- package/bin/hooks/pre-tool-use.mjs +182 -0
- package/bin/hooks/session-start.mjs +68 -3
- package/bin/init-detect.mjs +101 -12
- package/bin/journey.mjs +917 -16
- package/bin/labels.json +10 -0
- package/bin/recall.mjs +557 -0
- package/bin/registry.mjs +91 -3
- package/bin/runner.mjs +38 -2
- package/bin/tui.mjs +49 -3
- package/commands/adopt.md +1 -1
- package/commands/analytics.md +8 -8
- package/commands/audit.md +1 -1
- package/commands/autopilot.md +958 -77
- package/commands/ci-audit.md +1 -1
- package/commands/cleanup.md +139 -55
- package/commands/compat-audit.md +1 -1
- package/commands/deploy-info.md +1 -1
- package/commands/deploy-pr.md +1 -1
- package/commands/diagnose.md +1 -1
- package/commands/explain.md +1 -1
- package/commands/fix-ci.md +1 -1
- package/commands/forgedock-init.md +1 -1
- package/commands/incident-response.md +3 -3
- package/commands/issue.md +1 -1
- package/commands/optimize.md +586 -1
- package/commands/orchestrate/config.md +13 -3
- package/commands/orchestrate/phase-1-resolve.md +36 -26
- package/commands/orchestrate/phase-2-triage.md +1 -1
- package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +587 -48
- package/commands/orchestrate/phase-4-execution.md +1031 -57
- package/commands/orchestrate/phase-5-cleanup.md +1 -1
- package/commands/orchestrate/phase-6-report.md +248 -11
- package/commands/orchestrate/safety.md +2 -2
- package/commands/pipeline-health/phase-2-metrics.md +305 -1
- package/commands/pipeline-health/phase-4-proposals.md +127 -0
- package/commands/qa-sweep.md +1 -1
- package/commands/quality-gate.md +418 -1
- package/commands/recover-orphans.md +5 -5
- package/commands/replay.md +1 -1
- package/commands/review-pr-agents/api.md +2 -2
- package/commands/review-pr-agents/auth.md +2 -2
- package/commands/review-pr-agents/billing.md +2 -2
- package/commands/review-pr-agents/concurrency.md +2 -2
- package/commands/review-pr-agents/database.md +2 -2
- package/commands/review-pr-agents/frontend.md +2 -2
- package/commands/review-pr-agents/infra.md +2 -2
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-agents/scraper.md +2 -2
- package/commands/review-pr-agents/security.md +2 -2
- package/commands/review-pr-staging.md +41 -3
- package/commands/review-pr.md +462 -19
- package/commands/rollback.md +1 -1
- package/commands/scope.md +1 -1
- package/commands/security-audit.md +1 -1
- package/commands/spec-doctor.md +429 -0
- package/commands/test-gate.md +1 -1
- package/commands/validate.md +53 -1
- package/commands/work-on/build/architect.md +78 -1
- package/commands/work-on/build/context.md +360 -22
- package/commands/work-on/build/implement.md +3 -1
- package/commands/work-on/build/validate.md +29 -1
- package/commands/work-on/build.md +75 -6
- package/commands/work-on/close.md +624 -10
- package/commands/work-on/decompose.md +1 -1
- package/commands/work-on/investigate.md +235 -5
- package/commands/work-on/remediate.md +331 -0
- package/commands/work-on/review.md +1 -1
- package/commands/work-on-monolithic.md +1 -1
- package/commands/work-on.md +205 -66
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +1 -1
- package/docs/CONFIG.md +186 -0
- package/docs/FORGE-PROTOCOL.md +101 -0
- package/docs/site/command-reference.md +31 -1
- package/docs/site/getting-started.md +9 -34
- package/docs/site/index.md +1 -1
- package/docs/spec/forge-protocol-v1.md +97 -5
- package/docs/spec/label-state-machine.md +6 -1
- package/package.json +3 -2
- package/packages/protocol/src/cli.js +374 -84
- package/packages/protocol/src/types.js +75 -1
- package/scripts/build-knowledge-index.mjs +1761 -0
- package/scripts/calibration.mjs +996 -0
- package/scripts/check-command-docs-drift.sh +4 -2
- package/scripts/danger-zones.mjs +789 -0
- package/scripts/doctor-pipeline-state.sh +88 -2
- package/scripts/select-fix-targets.sh +234 -0
- package/scripts/transition-label.sh +245 -6
- package/scripts/refresh-bot-token.sh +0 -97
package/commands/review-pr.md
CHANGED
|
@@ -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: "
|
|
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
|
-
|
|
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
|
|
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}
|
|
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.
|
|
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
|
-
**>>>
|
|
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: $
|
|
1006
|
-
Read: $
|
|
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
|
|
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:
|
|
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
|
|
1614
|
-
#
|
|
1615
|
-
#
|
|
1616
|
-
# "" which does not equal "CHANGES REQUESTED"
|
|
1617
|
-
|
|
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]
|
package/commands/rollback.md
CHANGED
|
@@ -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: "
|
|
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
|
|