forgedock 1.0.22 → 1.1.2
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 +255 -211
- package/bin/batch-runner.mjs +339 -0
- package/bin/cinema.mjs +320 -0
- package/bin/demo.mjs +92 -7
- package/bin/engine/phases.mjs +168 -0
- package/bin/engine/projector.mjs +32 -0
- package/bin/engine/reconcile.mjs +11 -0
- package/bin/engine/runlog.mjs +79 -0
- package/bin/engine/state.mjs +32 -0
- package/bin/engine-cli.mjs +222 -0
- package/bin/engine.mjs +138 -0
- package/bin/forgedock.mjs +896 -1540
- package/bin/hooks/interactive-engine.mjs +509 -0
- package/bin/hooks/pre-tool-use.mjs +498 -0
- package/bin/hooks/subagent-stop-enforce.mjs +197 -0
- package/bin/journey.mjs +841 -0
- package/bin/registry.mjs +139 -0
- package/bin/report.mjs +447 -0
- package/bin/runner.mjs +136 -20
- package/bin/settings-hook.mjs +346 -0
- package/bin/tui.mjs +6 -3
- package/commands/adopt.md +4 -1
- package/commands/analytics.md +4 -1
- package/commands/audit-agents.md +110 -27
- package/commands/audit.md +20 -1
- package/commands/autopilot.md +84 -12
- package/commands/changelog.md +2 -1
- package/commands/ci-audit.md +2 -1
- package/commands/cleanup.md +161 -1
- package/commands/compat-audit.md +2 -1
- package/commands/deploy-info.md +63 -4
- package/commands/design-bench.md +6 -1
- package/commands/design-render-critique-loop.md +2 -1
- package/commands/design.md +1 -0
- package/commands/diagnose.md +2 -1
- package/commands/explain.md +2 -1
- package/commands/forge-stats.md +122 -2
- package/commands/forgedock-init.md +1 -1
- package/commands/geo-audit.md +3 -0
- package/commands/incident-response.md +2 -1
- package/commands/issue.md +24 -3
- package/commands/milestone.md +77 -12
- package/commands/optimize.md +2 -1
- package/commands/orchestrate/config.md +67 -0
- package/commands/orchestrate/phase-1-resolve.md +159 -0
- package/commands/orchestrate/phase-2-triage.md +126 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
- package/commands/orchestrate/phase-3-dependency.md +591 -0
- package/commands/orchestrate/phase-4-execution.md +962 -0
- package/commands/orchestrate/phase-5-cleanup.md +47 -0
- package/commands/orchestrate/phase-6-report.md +198 -0
- package/commands/orchestrate/safety.md +62 -0
- package/commands/orchestrate.md +25 -2027
- package/commands/pipeline-health/config.md +34 -0
- package/commands/pipeline-health/phase-1-context.md +180 -0
- package/commands/pipeline-health/phase-2-metrics.md +1729 -0
- package/commands/pipeline-health/phase-3-analyze.md +204 -0
- package/commands/pipeline-health/phase-4-proposals.md +117 -0
- package/commands/pipeline-health/phase-5-report.md +284 -0
- package/commands/pipeline-health/phase-6-summary.md +52 -0
- package/commands/pipeline-health.md +25 -2401
- package/commands/pipeline-resume.md +24 -1
- package/commands/pipeline-status.md +65 -0
- package/commands/qa-sweep.md +4 -1
- package/commands/quality-gate.md +326 -42
- package/commands/replay.md +2 -1
- package/commands/review-pr-agents/api.md +135 -0
- package/commands/review-pr-agents/auth.md +137 -0
- package/commands/review-pr-agents/billing.md +101 -0
- package/commands/review-pr-agents/concurrency.md +145 -0
- package/commands/review-pr-agents/database.md +103 -0
- package/commands/review-pr-agents/frontend.md +89 -0
- package/commands/review-pr-agents/infra.md +385 -0
- package/commands/review-pr-agents/protocols.md +185 -0
- package/commands/review-pr-agents/scraper.md +185 -0
- package/commands/review-pr-agents/security.md +406 -0
- package/commands/review-pr-agents.md +45 -1722
- package/commands/review-pr-staging.md +196 -40
- package/commands/review-pr.md +612 -56
- package/commands/rollback.md +2 -1
- package/commands/scope.md +2 -1
- package/commands/security-audit.md +2 -1
- package/commands/signal-planner.md +623 -0
- package/commands/sync-ecosystem.md +1 -0
- package/commands/test-gate.md +17 -1
- package/commands/validate.md +2 -1
- package/commands/work-on/build/architect.md +28 -0
- package/commands/work-on/build/context.md +2 -0
- package/commands/work-on/build/implement.md +19 -5
- package/commands/work-on/build/validate.md +62 -6
- package/commands/work-on/build.md +187 -30
- package/commands/work-on/close.md +132 -5
- package/commands/work-on/decompose.md +19 -1
- package/commands/work-on/investigate.md +117 -2
- package/commands/work-on/review.md +48 -4
- package/commands/work-on-monolithic.md +8 -2
- package/commands/work-on.md +247 -7
- package/docs/BRAND.md +16 -5
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +36 -1
- package/docs/CONFIG.md +44 -42
- package/docs/FORGE-PROTOCOL.md +15 -1064
- package/docs/anthropic-devrel-outreach.md +2 -2
- package/docs/articles/command-decomposition-token-savings.md +214 -0
- package/docs/articles/model-release-playbook.md +131 -0
- package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
- package/docs/claude-breakpoints.json +75 -5
- package/docs/design/abc-benchmark.md +1 -1
- package/docs/design/design-spec-schema.md +1 -1
- package/docs/design/divergent-generation.md +1 -1
- package/docs/design/eval-gate-runbook.md +178 -0
- package/docs/design/show-hn-draft.md +26 -1
- package/docs/design/token-cost-baseline-1298.md +59 -0
- package/docs/eval/README.md +117 -0
- package/docs/eval/corpus/seed-v1.json +6 -0
- package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
- package/docs/site/annotations-explained.md +24 -0
- package/docs/site/command-learning-path.md +51 -14
- package/docs/site/command-reference.md +339 -113
- package/docs/site/for-companies.md +95 -0
- package/docs/site/forge-annotation-protocol.md +12 -256
- package/docs/site/getting-started.md +58 -12
- package/docs/site/github-is-the-memory.md +113 -0
- package/docs/site/how-it-works.md +3 -13
- package/docs/site/index.md +10 -2
- package/docs/spec/eval-run-result.md +147 -0
- package/docs/spec/forge-protocol-v1.md +441 -18
- package/docs/spec/label-state-machine.md +55 -0
- package/docs/spec/review-protocol.md +158 -0
- package/docs/spec-graph-schema.md +28 -0
- package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
- package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
- package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
- package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
- package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
- package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
- package/examples/forgedock-demo/README.md +41 -22
- package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
- package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
- package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
- package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
- package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
- package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
- package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
- package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
- package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
- package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
- package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
- package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
- package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
- package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
- package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
- package/examples/forgedock-demo/scripts/smoke.js +18 -1
- package/examples/forgedock-demo/src/db.js +36 -11
- package/examples/forgedock-demo/src/routes/notes.js +106 -9
- package/examples/forgedock-demo/src/server.js +4 -1
- package/package.json +12 -4
- package/packages/protocol/LICENSE +21 -0
- package/packages/protocol/src/cli.js +135 -0
- package/packages/protocol/src/emit.js +146 -0
- package/packages/protocol/src/index.js +43 -0
- package/packages/protocol/src/parse.js +229 -0
- package/packages/protocol/src/types.js +171 -0
- package/packages/protocol/src/validate.js +122 -0
- package/scripts/bench-topology-cost.mjs +372 -0
- package/scripts/check-command-docs-drift.sh +198 -0
- package/scripts/check-protocol-restatements.sh +176 -0
- package/scripts/check-registry/README.md +72 -0
- package/scripts/check-registry/manifest.json +6 -0
- package/scripts/code-index.sh +454 -0
- package/scripts/conformance-check.mjs +168 -0
- package/scripts/doctor-pipeline-state.sh +484 -0
- package/scripts/eval-gate-baseline.json +17 -0
- package/scripts/eval-gate-scorecard.mjs +242 -0
- package/scripts/eval-scorecard-aggregator.mjs +252 -0
- package/scripts/flaky-quarantine.sh +241 -0
- package/scripts/forge-annotation.sh +567 -0
- package/scripts/issue-dedup.sh +236 -0
- package/scripts/validate-annotation-node.mjs +233 -0
- package/scripts/verify-env-vars.sh +18 -2
- package/scripts/verify-host-headers.sh +4 -4
- package/scripts/verify-route-registration.sh +2 -2
- package/scripts/worktree-lifecycle.sh +202 -0
- package/templates/devdocs/agent/using-forgedock.md +14 -0
package/commands/review-pr.md
CHANGED
|
@@ -11,8 +11,32 @@ allowed-tools: Task, Bash, Read, Grep, Glob, WebFetch, Skill
|
|
|
11
11
|
**Input**: $ARGUMENTS
|
|
12
12
|
|
|
13
13
|
**NEVER use plan mode (EnterPlanMode)** during review — it breaks execution context.
|
|
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).
|
|
14
15
|
|
|
15
|
-
**Agent model policy**:
|
|
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.
|
|
17
|
+
|
|
18
|
+
<!-- FORGE:SPEC_LOADED — review-pr.md loaded and active. Agent is bound by this spec. -->
|
|
19
|
+
|
|
20
|
+
## HARD RULES — READ BEFORE ANYTHING ELSE
|
|
21
|
+
|
|
22
|
+
1. **Use `Task(...)` for ALL domain agent launches.** Never substitute `Agent(...)`. Task agents run in a constrained context, post findings to the PR via `gh pr comment`, and their output is structured. Agent spawns opaque subprocesses outside allowed-tools.
|
|
23
|
+
|
|
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
|
+
|
|
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`.
|
|
27
|
+
|
|
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
|
+
|
|
30
|
+
## Forbidden Tools Self-Check
|
|
31
|
+
|
|
32
|
+
**Before executing any phase**, verify you are NOT using any of these tools:
|
|
33
|
+
|
|
34
|
+
| Tool | Status | Reason |
|
|
35
|
+
|------|--------|--------|
|
|
36
|
+
| `Agent` | **FORBIDDEN** | Spawns opaque subprocesses outside allowed-tools; bypasses spec workflow; cannot post structured findings |
|
|
37
|
+
| `EnterPlanMode` | **FORBIDDEN** | Breaks execution context; must run phases, not plan them |
|
|
38
|
+
|
|
39
|
+
If you find yourself about to call `Agent(...)`, stop and use `Task(...)` instead. If you find yourself about to use `EnterPlanMode`, stop and execute the next phase directly.
|
|
16
40
|
|
|
17
41
|
## Architecture — How This Command Works
|
|
18
42
|
|
|
@@ -22,7 +46,8 @@ This is the **orchestrator**. It routes to the right review mode, runs automated
|
|
|
22
46
|
|
|
23
47
|
| File | What | How to invoke |
|
|
24
48
|
|------|------|---------------|
|
|
25
|
-
| `$FORGE_HOME/commands/review-pr-agents.md` |
|
|
49
|
+
| `$FORGE_HOME/commands/review-pr-agents/protocols.md` | Shared review protocols (Evidence-Based + Structured Findings + Input Scoping) | `Read` tool during Phase 3C (always) |
|
|
50
|
+
| `$FORGE_HOME/commands/review-pr-agents/<persona>.md` | Per-persona agent prompt templates (9 files) | `Read` tool during Phase 3C (selected agents only) |
|
|
26
51
|
| `$FORGE_HOME/commands/review-pr-staging.md` | Full staging→main review pipeline | `Skill("review-pr-staging", ...)` during Phase 0 |
|
|
27
52
|
|
|
28
53
|
`$FORGE_HOME` defaults to `~/.claude` (the directory where `npx forgedock` symlinks commands). Override by setting `FORGE_HOME` in your environment.
|
|
@@ -49,6 +74,55 @@ Extract: `PR_NUMBER`, `AUTO_MERGE=true`, `MERGE_ISSUE`, `MERGE_BASE`, `MERGE_GH_
|
|
|
49
74
|
|
|
50
75
|
If `--auto-merge` is NOT present, `AUTO_MERGE=false` — Phase 8 (Auto-Merge) will be skipped.
|
|
51
76
|
|
|
77
|
+
### Thoroughness Flag
|
|
78
|
+
|
|
79
|
+
If `$ARGUMENTS` contains `--thorough`, set `THOROUGH=true`. This restores full union-dispatch (all matched domain agents run) for release-critical PRs. Default is `THOROUGH=false` — risk-scaled dispatch (2-3 agents).
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
THOROUGH=false
|
|
83
|
+
echo "$ARGUMENTS" | grep -q "\-\-thorough" && THOROUGH=true
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Phase -1: Route Assertion
|
|
89
|
+
|
|
90
|
+
**This phase is MANDATORY and must execute BEFORE Phase 0. No phase may be skipped.**
|
|
91
|
+
|
|
92
|
+
Resolve the PR number and post a routing marker immediately. This creates an audit trail — if a PR has no `FORGE:REVIEW_ROUTE` comment after a review command was run, the review was bypassed or never started.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Determine REVIEW_MODE from $ARGUMENTS before any routing decision
|
|
96
|
+
REVIEW_MODE_RAW="$ARGUMENTS"
|
|
97
|
+
if echo "$ARGUMENTS" | grep -qE '^(staging|feature|staging:feature)$'; then
|
|
98
|
+
REVIEW_MODE="staging-keyword"
|
|
99
|
+
ROUTE_PR_NUMBER="(resolved by staging sub-command)"
|
|
100
|
+
elif echo "$ARGUMENTS" | grep -qE '^(open|all)$'; then
|
|
101
|
+
REVIEW_MODE="multi-pr"
|
|
102
|
+
ROUTE_PR_NUMBER="(list mode)"
|
|
103
|
+
else
|
|
104
|
+
# Single PR number or URL — resolve HEAD/BASE now
|
|
105
|
+
PR_ROUTE_INFO=$(gh pr view $ARGUMENTS --json number,baseRefName,headRefName --jq '{number:.number,base:.baseRefName,head:.headRefName}')
|
|
106
|
+
ROUTE_PR_NUMBER=$(echo "$PR_ROUTE_INFO" | jq -r '.number')
|
|
107
|
+
ROUTE_HEAD=$(echo "$PR_ROUTE_INFO" | jq -r '.head')
|
|
108
|
+
ROUTE_BASE=$(echo "$PR_ROUTE_INFO" | jq -r '.base')
|
|
109
|
+
if [ "$ROUTE_HEAD" = "staging" ] && [ "$ROUTE_BASE" = "main" ] || [ "$ROUTE_HEAD" = "feature" ] && [ "$ROUTE_BASE" = "main" ]; then
|
|
110
|
+
REVIEW_MODE="staging-auto"
|
|
111
|
+
else
|
|
112
|
+
REVIEW_MODE="single-pr"
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
REVIEW_SHA_ROUTE=$(gh pr view ${ROUTE_PR_NUMBER:-$ARGUMENTS} --json headRefOid --jq '.headRefOid' 2>/dev/null | cut -c1-7 || echo "n/a")
|
|
117
|
+
|
|
118
|
+
# Post the routing assertion marker to the PR (skip for list/keyword modes where no PR# is known yet)
|
|
119
|
+
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} -->"
|
|
121
|
+
fi
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Invariant**: After this phase, `REVIEW_MODE` and (where applicable) `ROUTE_PR_NUMBER` are set. Any sub-invocation of `Skill("review-pr-staging", ...)` should immediately post its own `FORGE:REVIEW_ROUTE` marker scoped to the PR it resolves.
|
|
125
|
+
|
|
52
126
|
---
|
|
53
127
|
|
|
54
128
|
## Phase 0: Route to Review Mode
|
|
@@ -137,17 +211,19 @@ FILES=$(gh pr diff $ARGUMENTS --name-only)
|
|
|
137
211
|
DIFF=$(gh pr diff $ARGUMENTS)
|
|
138
212
|
|
|
139
213
|
echo "=== SERVICES ==="
|
|
140
|
-
|
|
141
|
-
echo "$FILES" | grep -
|
|
142
|
-
echo "$FILES" | grep -
|
|
143
|
-
echo "$FILES" | grep -
|
|
144
|
-
echo "$FILES" | grep -cE "^
|
|
214
|
+
# Service detection is path-agnostic — matches project-specific and generic structures
|
|
215
|
+
echo "$FILES" | grep -cE "(^services/api/|/api/|/backend/)" && echo "API" || true
|
|
216
|
+
echo "$FILES" | grep -cE "(^services/worker/|/worker/|/jobs/|/tasks/)" && echo "WORKER" || true
|
|
217
|
+
echo "$FILES" | grep -cE "(^web/|/frontend/|/app/|\.tsx?$|\.jsx?$)" && echo "WEB" || true
|
|
218
|
+
echo "$FILES" | grep -cE "(^shared/|/shared/|/common/|/lib/)" && echo "SHARED" || true
|
|
219
|
+
echo "$FILES" | grep -cE "^(docker|infra/|\.github|Makefile|traefik|k8s/|terraform/)" && echo "INFRA" || true
|
|
145
220
|
|
|
146
221
|
echo "=== DOMAINS ==="
|
|
147
|
-
echo "$DIFF" | grep -cE "
|
|
148
|
-
echo "$DIFF" | grep -cE "credit|balance|debit|reconcil|
|
|
149
|
-
|
|
150
|
-
echo "$DIFF" | grep -cE "
|
|
222
|
+
echo "$DIFF" | grep -cE "get_current_user|jwt|oauth|login|logout|require_auth|authenticated_user|x.forwarded.for|x_forwarded_for|forwarded_for|rate.limit.*ip|ip.*rate.limit|algorithm.*HS256|algorithm.*RS256|NEXTAUTH_SECRET|JWT_SECRET" && echo "AUTH" || true
|
|
223
|
+
echo "$DIFF" | grep -cE "credit|balance|debit|reconcil|pricing|charge|refund|stripe|subscription|payment" && echo "BILLING" || true
|
|
224
|
+
# SCRAPING: only match browser-automation/scraping keywords, NOT bare "playwright" (avoids E2E test repos)
|
|
225
|
+
echo "$DIFF" | grep -cE "scrape|tier.*escalat|anti_bot|stealth|playwright.*scrape|scrape.*playwright|playbook_min_tier|browser.*pool|proxy.*scrape|web.*scrape" && echo "SCRAPING" || true
|
|
226
|
+
echo "$DIFF" | grep -cE "FOR UPDATE|atomic|transaction|pipeline|MULTI|distributed_lock|acquire_lock|reserved_by|promo.*claim|voucher.*redeem" && echo "CONCURRENCY" || true
|
|
151
227
|
echo "$FILES" | grep -cE "migration|\.sql$" && echo "DATABASE" || true
|
|
152
228
|
echo "$DIFF" | grep -cE "create_async_engine|AsyncSession|connect_args|pool_size|prepared_statement|engine_from_config|sessionmaker" && echo "DB_CONFIG" || true
|
|
153
229
|
echo "$FILES" | grep -cE "router|routes" && echo "API_DESIGN" || true
|
|
@@ -263,7 +339,21 @@ git diff origin/main...HEAD -- "**/package.json" | grep -E "^\+" | grep -v "^\+\
|
|
|
263
339
|
|
|
264
340
|
### 2H: Tests
|
|
265
341
|
|
|
266
|
-
Read `forge.yaml → verification.commands.{lang}.test` for the project's test command
|
|
342
|
+
Read `forge.yaml → verification.commands.{lang}.test` for the project's test command.
|
|
343
|
+
|
|
344
|
+
**Before running tests**: check the quarantine manifest for known pre-broken and flaky tests:
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
# Load quarantine manifest if present — used below to suppress known-bad tests from blocking.
|
|
348
|
+
QUARANTINE_MANIFEST=".forgedock/quarantine.jsonl"
|
|
349
|
+
QUARANTINED_TESTS=""
|
|
350
|
+
if [ -f "$QUARANTINE_MANIFEST" ]; then
|
|
351
|
+
QUARANTINED_TESTS=$(grep -oP '"test":"[^"]*"' "$QUARANTINE_MANIFEST" 2>/dev/null | sed 's/"test":"//;s/"//' | sort -u)
|
|
352
|
+
QUARANTINE_COUNT=$(echo "$QUARANTINED_TESTS" | grep -c . || echo 0)
|
|
353
|
+
echo "Quarantine manifest: ${QUARANTINE_COUNT} known-bad test(s) — these will not block the review if they fail."
|
|
354
|
+
echo "$QUARANTINED_TESTS" | sed 's/^/ - /'
|
|
355
|
+
fi
|
|
356
|
+
```
|
|
267
357
|
|
|
268
358
|
```bash
|
|
269
359
|
# Check each configured language for a test command
|
|
@@ -273,7 +363,29 @@ for lang in python typescript go rust; do
|
|
|
273
363
|
echo "=== Running tests (${lang}): ${TEST_CMD} ==="
|
|
274
364
|
eval "$TEST_CMD" 2>&1 | tail -30
|
|
275
365
|
TEST_EXIT=$?
|
|
276
|
-
[ "$TEST_EXIT" -ne 0 ]
|
|
366
|
+
if [ "$TEST_EXIT" -ne 0 ]; then
|
|
367
|
+
# Attempt to cross-reference with quarantine manifest.
|
|
368
|
+
# If ALL failing test names appear in $QUARANTINED_TESTS, suppress the block.
|
|
369
|
+
# Without per-test granularity from the test runner, fall back to treating
|
|
370
|
+
# any failure as blocking unless the classifier script is available.
|
|
371
|
+
CLASSIFIER="${FORGEDOCK_SCRIPTS:-scripts}/flaky-quarantine.sh"
|
|
372
|
+
[ -f "$CLASSIFIER" ] || CLASSIFIER="scripts/flaky-quarantine.sh"
|
|
373
|
+
if [ -f "$CLASSIFIER" ]; then
|
|
374
|
+
CL_RESULT=$(bash "$CLASSIFIER" \
|
|
375
|
+
--test "$TEST_CMD" \
|
|
376
|
+
--base "$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null | sed 's|origin/||' || echo main)" \
|
|
377
|
+
--retries 3 2>&1)
|
|
378
|
+
echo "$CL_RESULT"
|
|
379
|
+
CL=$(echo "$CL_RESULT" | grep '^CLASSIFICATION:' | awk '{print $2}')
|
|
380
|
+
if [ "$CL" = "PRE_BROKEN" ] || [ "$CL" = "FLAKY" ]; then
|
|
381
|
+
echo "ADVISORY (not blocking): ${lang} tests classified ${CL} — quarantined, does not block this review"
|
|
382
|
+
else
|
|
383
|
+
echo "BLOCKING: ${lang} tests failed (exit $TEST_EXIT) — classified REAL"
|
|
384
|
+
fi
|
|
385
|
+
else
|
|
386
|
+
echo "BLOCKING: ${lang} tests failed (exit $TEST_EXIT)"
|
|
387
|
+
fi
|
|
388
|
+
fi
|
|
277
389
|
fi
|
|
278
390
|
done
|
|
279
391
|
|
|
@@ -284,7 +396,7 @@ if [ -z "$PYTHON_TEST" ] && [ -z "$TS_TEST" ]; then
|
|
|
284
396
|
echo "SKIPPED — no test commands configured in verification.commands"
|
|
285
397
|
fi
|
|
286
398
|
```
|
|
287
|
-
**BLOCKING
|
|
399
|
+
**BLOCKING only for REAL test failures** (deterministically caused by this PR). Pre-broken and flaky failures are surfaced as advisories and do not block approval.
|
|
288
400
|
|
|
289
401
|
### 2I: Build Verification (MANDATORY for staging→main AND milestone→staging)
|
|
290
402
|
|
|
@@ -397,7 +509,7 @@ For each changed file, identify its **activation path** — how does execution r
|
|
|
397
509
|
|
|
398
510
|
Map each changed file to its activation requirements.
|
|
399
511
|
|
|
400
|
-
**Layout path resolution** — Before applying the table below, read your project's layout from `forge.yaml → review.layout` and substitute the values into the pattern column. Defaults (used when the key is absent)
|
|
512
|
+
**Layout path resolution** — Before applying the table below, read your project's layout from `forge.yaml → review.layout` and substitute the values into the pattern column. Defaults (used when the key is absent) are the ForgeDock install defaults:
|
|
401
513
|
|
|
402
514
|
| `forge.yaml` key | Default | Used in table as |
|
|
403
515
|
|-----------------|---------|-----------------|
|
|
@@ -461,6 +573,23 @@ if [ "$BASH_AVAILABLE" = "true" ]; then
|
|
|
461
573
|
# Output is structured: "BLOCKING: ...", "WARNING: ...", "OK: ..." per line.
|
|
462
574
|
|
|
463
575
|
# 1. Route/router/middleware/shared-module/component registration
|
|
576
|
+
# Export forge.yaml layout overrides so verify-route-registration.sh uses project-configured
|
|
577
|
+
# paths instead of project defaults. The script supports these env vars (lines 36-44 of
|
|
578
|
+
# verify-route-registration.sh) but requires the caller to set them. <!-- Added: forge#1349 -->
|
|
579
|
+
if [ -f "$REPO_ROOT/forge.yaml" ]; then
|
|
580
|
+
_PAGES_ROOT=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
|
|
581
|
+
| grep -E '^\s*pages:' | head -1 | sed 's/.*pages:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
|
|
582
|
+
_API_ROUTERS=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
|
|
583
|
+
| grep -E '^\s*api_routers_dir:' | head -1 | sed 's/.*api_routers_dir:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
|
|
584
|
+
_API_MAIN=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
|
|
585
|
+
| grep -E '^\s*api_main:' | head -1 | sed 's/.*api_main:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
|
|
586
|
+
_API_MIDDLEWARE=$(grep -A10 'layout:' "$REPO_ROOT/forge.yaml" 2>/dev/null \
|
|
587
|
+
| grep -E '^\s*api_middleware_dir:' | head -1 | sed 's/.*api_middleware_dir:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
|
|
588
|
+
[ -n "$_PAGES_ROOT" ] && export FORGE_PAGES_ROOT="$_PAGES_ROOT"
|
|
589
|
+
[ -n "$_API_ROUTERS" ] && export FORGE_API_ROUTERS_DIR="$_API_ROUTERS"
|
|
590
|
+
[ -n "$_API_MAIN" ] && export FORGE_API_MAIN="$_API_MAIN"
|
|
591
|
+
[ -n "$_API_MIDDLEWARE" ] && export FORGE_API_MIDDLEWARE_DIR="$_API_MIDDLEWARE"
|
|
592
|
+
fi
|
|
464
593
|
echo "=== Running: verify-route-registration.sh ==="
|
|
465
594
|
bash "$FORGE_HOME/scripts/verify-route-registration.sh" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
|
|
466
595
|
|
|
@@ -612,10 +741,10 @@ Format: `INTEG-N|CONFIRMED|HIGH|file:line|Changed code may be unreachable: {reas
|
|
|
612
741
|
DIFF=$(gh pr diff $ARGUMENTS)
|
|
613
742
|
FILES=$(gh pr diff $ARGUMENTS --name-only)
|
|
614
743
|
echo "=== RISK SIGNALS ==="
|
|
615
|
-
echo "$DIFF" | grep -cE "subprocess|exec|eval|system\(|popen|heredoc
|
|
744
|
+
echo "$DIFF" | grep -cE "subprocess|exec|eval|system\(|popen|heredoc" && echo " UNTRUSTED_INPUT_PROCESSING" || true
|
|
616
745
|
echo "$DIFF" | grep -cE "\.sh$|bash|shell|cron" && echo " SHELL_SCRIPT" || true
|
|
617
746
|
echo "$DIFF" | grep -cE "credit|balance|debit|charge|refund|stripe" && echo " FINANCIAL" || true
|
|
618
|
-
echo "$DIFF" | grep -cE "
|
|
747
|
+
echo "$DIFF" | grep -cE "jwt|oauth|password|token|secret|x.forwarded.for|x_forwarded_for|forwarded_for|rate.limit.*ip|ip.*rate.limit|algorithm.*HS256|algorithm.*RS256|NEXTAUTH_SECRET|JWT_SECRET|get_current_user|require_auth" && echo " AUTH_SENSITIVE" || true
|
|
619
748
|
echo "$DIFF" | grep -cE "\.sql$|migration|DROP|ALTER|DELETE FROM" && echo " DATABASE_MUTATION" || true
|
|
620
749
|
echo "$DIFF" | grep -cE "docker|deploy|traefik|nginx|\.yml.*service" && echo " INFRASTRUCTURE" || true
|
|
621
750
|
echo "$DIFF" | grep -cE "docker-compose.*postgres|docker-compose.*redis|postgres.*command:|redis.*command:|image:.*postgres|image:.*redis" && echo " DATABASE_CONTAINER" || true
|
|
@@ -654,43 +783,232 @@ echo "$CHURN_CONTEXT"
|
|
|
654
783
|
|
|
655
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.
|
|
656
785
|
|
|
657
|
-
### 3B: Select Agents
|
|
786
|
+
### 3B: Select Agents — Risk-Scaled Dispatch
|
|
658
787
|
|
|
659
|
-
|
|
660
|
-
|-------------|----------------|
|
|
661
|
-
| UNTRUSTED_INPUT_PROCESSING | Security (deep) + relevant domain |
|
|
662
|
-
| SHELL_SCRIPT | Security (deep) + Infrastructure |
|
|
663
|
-
| FINANCIAL | Security + Billing + Concurrency |
|
|
664
|
-
| AUTH_SENSITIVE | Security + Auth Conventions |
|
|
665
|
-
| DATABASE_MUTATION | Security + Database |
|
|
666
|
-
| DB_CONFIG | Security + Database + Infrastructure |
|
|
667
|
-
| INFRASTRUCTURE | Security + Infrastructure |
|
|
668
|
-
| DATABASE_CONTAINER | Security + Infrastructure (escalate to HIGH risk — stateful container restart) |
|
|
669
|
-
| CODE_EXECUTION | Security (deep) |
|
|
670
|
-
| SDK_OPENAPI | Security + API Design & Consistency (runs cross-PR schema check #10) |
|
|
671
|
-
| None | Security + all matching domain agents |
|
|
788
|
+
**Default: 2-3 agents.** General Security always runs. Select the top 1-2 domain agents by relevance score, then apply escalation triggers to add more only when signals warrant it. Use `--thorough` to restore full union dispatch for release-critical PRs.
|
|
672
789
|
|
|
673
|
-
|
|
790
|
+
#### Step 1: Score Each Domain by Signal Strength
|
|
674
791
|
|
|
675
|
-
|
|
792
|
+
Assign a relevance score to each domain based on signals detected in Phase 3A. Higher score = higher priority for inclusion.
|
|
676
793
|
|
|
677
|
-
|
|
794
|
+
```bash
|
|
795
|
+
# Relevance scoring — accumulate points per domain from Phase 3A signals
|
|
796
|
+
SCORE_AUTH=0
|
|
797
|
+
SCORE_BILLING=0
|
|
798
|
+
SCORE_CONCURRENCY=0
|
|
799
|
+
SCORE_DATABASE=0
|
|
800
|
+
SCORE_INFRA=0
|
|
801
|
+
SCORE_SECURITY=0
|
|
802
|
+
SCORE_SCRAPING=0
|
|
803
|
+
SCORE_FRONTEND=0
|
|
804
|
+
SCORE_API=0
|
|
805
|
+
|
|
806
|
+
# AUTH domain signals
|
|
807
|
+
echo "$DIFF" | grep -qE "jwt|oauth|login|logout|password|NEXTAUTH_SECRET|JWT_SECRET|auth_token|access_token|refresh_token" && SCORE_AUTH=$((SCORE_AUTH + 3))
|
|
808
|
+
echo "$DIFF" | grep -qE "get_current_user|Depends\(get_|x.forwarded.for|forwarded_for|rate.limit.*ip|ip.*rate.limit" && SCORE_AUTH=$((SCORE_AUTH + 2))
|
|
809
|
+
echo "$DIFF" | grep -qE "algorithm.*HS256|algorithm.*RS256|admin.proxy" && SCORE_AUTH=$((SCORE_AUTH + 2))
|
|
810
|
+
|
|
811
|
+
# BILLING domain signals
|
|
812
|
+
echo "$DIFF" | grep -qE "credit|balance|debit|charge|refund|stripe|subscription" && SCORE_BILLING=$((SCORE_BILLING + 3))
|
|
813
|
+
echo "$DIFF" | grep -qE "pricing|tier_cost|reconcil" && SCORE_BILLING=$((SCORE_BILLING + 2))
|
|
814
|
+
|
|
815
|
+
# CONCURRENCY domain signals
|
|
816
|
+
echo "$DIFF" | grep -qE "FOR UPDATE|atomic|transaction|pipeline|MULTI|distributed_lock|acquire_lock" && SCORE_CONCURRENCY=$((SCORE_CONCURRENCY + 3))
|
|
817
|
+
echo "$DIFF" | grep -qE "reserved_by|promo.*claim|voucher.*redeem" && SCORE_CONCURRENCY=$((SCORE_CONCURRENCY + 2))
|
|
818
|
+
|
|
819
|
+
# DATABASE domain signals
|
|
820
|
+
echo "$FILES" | grep -qE "migration|\.sql$" && SCORE_DATABASE=$((SCORE_DATABASE + 3))
|
|
821
|
+
echo "$DIFF" | grep -qE "DROP|ALTER|DELETE FROM" && SCORE_DATABASE=$((SCORE_DATABASE + 2))
|
|
822
|
+
echo "$DIFF" | grep -qE "create_async_engine|AsyncSession|pool_size|prepared_statement|sessionmaker" && SCORE_DATABASE=$((SCORE_DATABASE + 2))
|
|
823
|
+
|
|
824
|
+
# INFRASTRUCTURE domain signals
|
|
825
|
+
echo "$DIFF" | grep -qE "docker-compose.*postgres|docker-compose.*redis|postgres.*command:|redis.*command:" && SCORE_INFRA=$((SCORE_INFRA + 3))
|
|
826
|
+
echo "$DIFF" | grep -qE "docker|deploy|traefik|nginx" && SCORE_INFRA=$((SCORE_INFRA + 1))
|
|
827
|
+
echo "$FILES" | grep -qE "^\.github/workflows/" && SCORE_INFRA=$((SCORE_INFRA + 2))
|
|
828
|
+
echo "$FILES" | grep -qE "Dockerfile|docker-compose" && SCORE_INFRA=$((SCORE_INFRA + 2))
|
|
829
|
+
|
|
830
|
+
# SECURITY domain signals (always baseline)
|
|
831
|
+
SCORE_SECURITY=1
|
|
832
|
+
echo "$DIFF" | grep -qE "subprocess|exec|eval|system\(|popen" && SCORE_SECURITY=$((SCORE_SECURITY + 3))
|
|
833
|
+
echo "$DIFF" | grep -qE "os\.system|eval\(|exec\(|pickle|yaml\.load[^_]" && SCORE_SECURITY=$((SCORE_SECURITY + 3))
|
|
834
|
+
echo "$DIFF" | grep -qE "\.sh$|bash|shell|cron" && SCORE_SECURITY=$((SCORE_SECURITY + 2))
|
|
835
|
+
|
|
836
|
+
# SCRAPING domain signals — only match browser-automation/scraping context, NOT bare "playwright"
|
|
837
|
+
echo "$DIFF" | grep -qE "scrape|tier.*escalat|anti_bot|stealth|playwright.*scrape|scrape.*playwright|playbook_min_tier|browser.*pool|web.*scrape" && SCORE_SCRAPING=$((SCORE_SCRAPING + 3))
|
|
838
|
+
|
|
839
|
+
# FRONTEND domain signals
|
|
840
|
+
echo "$FILES" | grep -qE "^web/src/" && SCORE_FRONTEND=$((SCORE_FRONTEND + 1))
|
|
841
|
+
echo "$FILES" | grep -qcE "^web/src/app/|^web/src/components/" | grep -qv "^0$" && SCORE_FRONTEND=$((SCORE_FRONTEND + 2))
|
|
842
|
+
|
|
843
|
+
# API domain signals
|
|
844
|
+
echo "$FILES" | grep -qE "router|routes" && SCORE_API=$((SCORE_API + 2))
|
|
845
|
+
echo "$FILES" | grep -qE "^sdk/|openapi.*\.json$|openapi-versions/" && SCORE_API=$((SCORE_API + 3))
|
|
846
|
+
|
|
847
|
+
echo "=== DOMAIN SCORES ==="
|
|
848
|
+
echo "AUTH=$SCORE_AUTH BILLING=$SCORE_BILLING CONCURRENCY=$SCORE_CONCURRENCY DATABASE=$SCORE_DATABASE INFRA=$SCORE_INFRA SECURITY=$SCORE_SECURITY SCRAPING=$SCORE_SCRAPING FRONTEND=$SCORE_FRONTEND API=$SCORE_API"
|
|
849
|
+
```
|
|
678
850
|
|
|
679
|
-
|
|
680
|
-
- AUTH + BILLING → Security (deep) + Auth + Billing + Concurrency
|
|
681
|
-
- AUTH + DATABASE_MUTATION → Security (deep) + Auth + Database
|
|
682
|
-
- FINANCIAL + CONCURRENCY → Security (deep) + Billing + Concurrency + Database
|
|
683
|
-
- AUTH + CODE_EXECUTION → Security (deep) + Auth
|
|
851
|
+
#### Step 2: Read Architect CONTRACT for Risk Flags (if PR is from /work-on)
|
|
684
852
|
|
|
685
|
-
|
|
853
|
+
```bash
|
|
854
|
+
CONTRACT_RISK_FLAGS=""
|
|
855
|
+
ISSUE_NUM=$(gh pr view $ARGUMENTS --json body --jq '.body | gsub("(?s).*?(?:Closes #|#)(?<n>[0-9]+).*"; "\(.n)") // empty' 2>/dev/null | head -1)
|
|
856
|
+
if [ -n "$ISSUE_NUM" ]; then
|
|
857
|
+
CONTRACT_BODY=$(gh api repos/${REPO}/issues/${ISSUE_NUM}/comments \
|
|
858
|
+
--jq '[.[] | select(.body | contains("FORGE:CONTRACT"))] | .[0].body' 2>/dev/null || echo "")
|
|
859
|
+
# Extract risk level from CONTRACT — look for "HIGH" or "CRITICAL" risk markers
|
|
860
|
+
if echo "$CONTRACT_BODY" | grep -qiE "\*\*Risk\*\*.*HIGH|\*\*Risk\*\*.*CRITICAL|Risk.*:\s*(HIGH|CRITICAL)"; then
|
|
861
|
+
CONTRACT_RISK_FLAGS="HIGH_RISK"
|
|
862
|
+
fi
|
|
863
|
+
# Extract cross-domain touches flagged in CONTRACT
|
|
864
|
+
echo "$CONTRACT_BODY" | grep -qiE "auth|session|jwt" && CONTRACT_RISK_FLAGS="${CONTRACT_RISK_FLAGS} CONTRACT_AUTH"
|
|
865
|
+
echo "$CONTRACT_BODY" | grep -qiE "billing|credit|payment" && CONTRACT_RISK_FLAGS="${CONTRACT_RISK_FLAGS} CONTRACT_BILLING"
|
|
866
|
+
echo "$CONTRACT_BODY" | grep -qiE "migration|schema|database" && CONTRACT_RISK_FLAGS="${CONTRACT_RISK_FLAGS} CONTRACT_DATABASE"
|
|
867
|
+
fi
|
|
868
|
+
echo "=== CONTRACT FLAGS: ${CONTRACT_RISK_FLAGS:-none} ==="
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
#### Step 3: Compute CHURN_ESCALATION
|
|
872
|
+
|
|
873
|
+
Check whether churn hot-spots exist (computed in Phase 3A `$CHURN_CONTEXT`):
|
|
874
|
+
|
|
875
|
+
```bash
|
|
876
|
+
CHURN_ESCALATION=false
|
|
877
|
+
echo "$CHURN_CONTEXT" | grep -q "HOT" && CHURN_ESCALATION=true
|
|
878
|
+
echo "=== CHURN_ESCALATION: $CHURN_ESCALATION ==="
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
#### Step 4: Build Selected Agent Roster
|
|
882
|
+
|
|
883
|
+
**If `THOROUGH=true`** (user passed `--thorough`, or `IS_MILESTONE_TO_STAGING=true`): run full union dispatch — all agents matching any signal. Skip to "Full union dispatch" below.
|
|
884
|
+
|
|
885
|
+
**Default (THOROUGH=false):**
|
|
886
|
+
|
|
887
|
+
Start with the baseline roster: `SELECTED_AGENTS="Security"` (General Security always runs).
|
|
888
|
+
|
|
889
|
+
Pick the top 1-2 domain agents by score:
|
|
890
|
+
|
|
891
|
+
```bash
|
|
892
|
+
# Build sorted list: "score:domain"
|
|
893
|
+
DOMAIN_SCORES="$SCORE_AUTH:AUTH $SCORE_BILLING:BILLING $SCORE_CONCURRENCY:CONCURRENCY $SCORE_DATABASE:DATABASE $SCORE_INFRA:INFRA $SCORE_SCRAPING:SCRAPING $SCORE_FRONTEND:FRONTEND $SCORE_API:API"
|
|
894
|
+
|
|
895
|
+
# Sort descending by score, pick top 2 with score > 0
|
|
896
|
+
TOP_DOMAINS=$(echo "$DOMAIN_SCORES" | tr ' ' '\n' | sort -t: -k1 -rn | head -2 | awk -F: '$1 > 0 {print $2}' | tr '\n' ' ')
|
|
897
|
+
for DOMAIN in $TOP_DOMAINS; do
|
|
898
|
+
SELECTED_AGENTS="$SELECTED_AGENTS $DOMAIN"
|
|
899
|
+
done
|
|
900
|
+
echo "=== BASELINE ROSTER (top domains): $SELECTED_AGENTS ==="
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
**Apply escalation triggers** — each adds agents if not already included:
|
|
904
|
+
|
|
905
|
+
| Trigger | Condition | Added Agents |
|
|
906
|
+
|---------|-----------|--------------|
|
|
907
|
+
| Critical auth path | `SCORE_AUTH >= 3` | Auth (if not already selected) |
|
|
908
|
+
| Critical billing path | `SCORE_BILLING >= 3` | Billing + Concurrency |
|
|
909
|
+
| Migration/schema change | `SCORE_DATABASE >= 3` | Database |
|
|
910
|
+
| Stateful container change | `SCORE_INFRA >= 3` AND `DATABASE_CONTAINER` signal | Infrastructure |
|
|
911
|
+
| Code execution risk | `SCORE_SECURITY >= 4` (deep scan signals) | Security runs in deep mode (already selected) |
|
|
912
|
+
| Churn hot-spot | `CHURN_ESCALATION=true` AND top-scoring domain | Top domain gets added if not already in roster |
|
|
913
|
+
| CONTRACT high-risk | `CONTRACT_RISK_FLAGS` contains `HIGH_RISK` | All CONTRACT-flagged domain agents |
|
|
914
|
+
| First-pass finding severity | Phase 3 re-entry after a CONFIRMED/HIGH finding posted to PR | Add all agents for the domain of the finding |
|
|
915
|
+
| Scraping domain | `SCORE_SCRAPING >= 3` AND `review.domains.scraping` is set in forge.yaml | Scraping (optional domain pack — never in default catalog) |
|
|
916
|
+
| Cross-critical domains | `SCORE_AUTH >= 2` AND `SCORE_BILLING >= 2` | Auth + Billing + Concurrency |
|
|
917
|
+
| Cross-critical domains | `SCORE_AUTH >= 2` AND `SCORE_DATABASE >= 3` | Auth + Database |
|
|
918
|
+
| Cross-critical domains | `SCORE_BILLING >= 2` AND `SCORE_CONCURRENCY >= 2` | Billing + Concurrency + Database |
|
|
919
|
+
|
|
920
|
+
```bash
|
|
921
|
+
# Apply escalation triggers
|
|
922
|
+
add_agent() {
|
|
923
|
+
local AGENT="$1"
|
|
924
|
+
echo "$SELECTED_AGENTS" | grep -qw "$AGENT" || SELECTED_AGENTS="$SELECTED_AGENTS $AGENT"
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
[ "$SCORE_AUTH" -ge 3 ] && add_agent "Auth"
|
|
928
|
+
[ "$SCORE_BILLING" -ge 3 ] && { add_agent "Billing"; add_agent "Concurrency"; }
|
|
929
|
+
[ "$SCORE_DATABASE" -ge 3 ] && add_agent "Database"
|
|
930
|
+
[ "$SCORE_INFRA" -ge 3 ] && echo "$DIFF" | grep -qE "docker-compose.*postgres|docker-compose.*redis|postgres.*command:|redis.*command:" && add_agent "Infrastructure"
|
|
931
|
+
# Scraping agent is opt-in: only spawns when review.domains.scraping is configured in forge.yaml
|
|
932
|
+
[ "$SCORE_SCRAPING" -ge 3 ] && [ -n "$DOMAIN_CONTEXT_SCRAPING" ] && add_agent "Scraping"
|
|
933
|
+
[ "$CHURN_ESCALATION" = "true" ] && {
|
|
934
|
+
# Add the top-scoring domain for deeper churn scrutiny if not already selected
|
|
935
|
+
TOP_CHURN_DOMAIN=$(echo "$DOMAIN_SCORES" | tr ' ' '\n' | sort -t: -k1 -rn | head -1 | awk -F: '{print $2}')
|
|
936
|
+
[ -n "$TOP_CHURN_DOMAIN" ] && add_agent "$TOP_CHURN_DOMAIN"
|
|
937
|
+
}
|
|
938
|
+
echo "$CONTRACT_RISK_FLAGS" | grep -q "HIGH_RISK" && {
|
|
939
|
+
echo "$CONTRACT_RISK_FLAGS" | grep -q "CONTRACT_AUTH" && add_agent "Auth"
|
|
940
|
+
echo "$CONTRACT_RISK_FLAGS" | grep -q "CONTRACT_BILLING" && { add_agent "Billing"; add_agent "Concurrency"; }
|
|
941
|
+
echo "$CONTRACT_RISK_FLAGS" | grep -q "CONTRACT_DATABASE" && add_agent "Database"
|
|
942
|
+
}
|
|
943
|
+
# Cross-critical domain escalation
|
|
944
|
+
{ [ "$SCORE_AUTH" -ge 2 ] && [ "$SCORE_BILLING" -ge 2 ]; } && { add_agent "Auth"; add_agent "Billing"; add_agent "Concurrency"; }
|
|
945
|
+
{ [ "$SCORE_AUTH" -ge 2 ] && [ "$SCORE_DATABASE" -ge 3 ]; } && { add_agent "Auth"; add_agent "Database"; }
|
|
946
|
+
{ [ "$SCORE_BILLING" -ge 2 ] && [ "$SCORE_CONCURRENCY" -ge 2 ]; } && { add_agent "Billing"; add_agent "Concurrency"; add_agent "Database"; }
|
|
947
|
+
# SDK/OpenAPI always adds API agent
|
|
948
|
+
echo "$FILES" | grep -qE "^sdk/|openapi.*\.json$|openapi-versions/" && add_agent "API"
|
|
949
|
+
|
|
950
|
+
echo "=== FINAL ROSTER (after escalation): $SELECTED_AGENTS ==="
|
|
951
|
+
AGENT_COUNT=$(echo "$SELECTED_AGENTS" | wc -w)
|
|
952
|
+
echo "=== AGENT COUNT: $AGENT_COUNT ==="
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
**Full union dispatch (THOROUGH=true or IS_MILESTONE_TO_STAGING=true):**
|
|
956
|
+
|
|
957
|
+
```bash
|
|
958
|
+
if [ "$THOROUGH" = "true" ] || [ "$IS_MILESTONE_TO_STAGING" = "true" ]; then
|
|
959
|
+
SELECTED_AGENTS="Security"
|
|
960
|
+
[ "$SCORE_AUTH" -gt 0 ] && SELECTED_AGENTS="$SELECTED_AGENTS Auth"
|
|
961
|
+
[ "$SCORE_BILLING" -gt 0 ] && SELECTED_AGENTS="$SELECTED_AGENTS Billing Concurrency"
|
|
962
|
+
[ "$SCORE_DATABASE" -gt 0 ] && SELECTED_AGENTS="$SELECTED_AGENTS Database"
|
|
963
|
+
[ "$SCORE_INFRA" -gt 0 ] && SELECTED_AGENTS="$SELECTED_AGENTS Infrastructure"
|
|
964
|
+
# Scraping agent only added in thorough mode when review.domains.scraping is configured
|
|
965
|
+
SCRAPING_ENABLED=$(yq '.review.domains.scraping' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
966
|
+
[ "$SCORE_SCRAPING" -gt 0 ] && [ -n "$SCRAPING_ENABLED" ] && SELECTED_AGENTS="$SELECTED_AGENTS Scraping"
|
|
967
|
+
[ "$SCORE_FRONTEND" -gt 0 ] && SELECTED_AGENTS="$SELECTED_AGENTS Frontend"
|
|
968
|
+
[ "$SCORE_API" -gt 0 ] && SELECTED_AGENTS="$SELECTED_AGENTS API"
|
|
969
|
+
# Deduplicate
|
|
970
|
+
SELECTED_AGENTS=$(echo "$SELECTED_AGENTS" | tr ' ' '\n' | sort -u | tr '\n' ' ')
|
|
971
|
+
echo "=== THOROUGH mode: FULL UNION DISPATCH — $SELECTED_AGENTS ==="
|
|
972
|
+
fi
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
**Why cross-critical domain pairs always escalate**: A 2-file PR touching both `services/api/app/core/auth.py` and `services/api/app/routers/billing.py` creates interaction bugs that single-domain reviewers cannot catch. Never rely on a single agent for multi-domain risk.
|
|
976
|
+
|
|
977
|
+
**General Security agent ALWAYS runs.** If BILLING is selected, Concurrency is always added. If SHARED module is touched, add agents for all importing services.
|
|
978
|
+
|
|
979
|
+
#### Domain-to-File Mapping (for diff slicing in Phase 3C)
|
|
980
|
+
|
|
981
|
+
Each agent receives only its domain-relevant diff slice rather than the full changeset. Compute per-domain file filters:
|
|
982
|
+
|
|
983
|
+
```bash
|
|
984
|
+
# Domain file patterns — used in Phase 3C to scope each agent's input
|
|
985
|
+
DOMAIN_FILES_AUTH=$(echo "$FILES" | grep -iE "auth|session|login|logout|jwt|oauth|token|user|permission|middleware" || echo "")
|
|
986
|
+
DOMAIN_FILES_BILLING=$(echo "$FILES" | grep -iE "billing|credit|payment|charge|refund|subscription|pricing|tier" || echo "")
|
|
987
|
+
DOMAIN_FILES_CONCURRENCY=$(echo "$FILES" | grep -iE "lock|queue|atomic|transaction|worker|job|task|celery|redis" || echo "")
|
|
988
|
+
DOMAIN_FILES_DATABASE=$(echo "$FILES" | grep -iE "migration|\.sql$|model|schema|db|database|alembic" || echo "")
|
|
989
|
+
DOMAIN_FILES_INFRA=$(echo "$FILES" | grep -iE "docker|nginx|traefik|\.github|Makefile|deploy|infra" || echo "")
|
|
990
|
+
DOMAIN_FILES_SCRAPING=$(echo "$FILES" | grep -iE "scrape|crawler|stealth|browser.*pool|headless|anti.bot|detection.*keyword|captcha" || echo "")
|
|
991
|
+
DOMAIN_FILES_FRONTEND=$(echo "$FILES" | grep -iE "^web/|\.tsx?$|\.jsx?$|component|page|layout|style|css" || echo "")
|
|
992
|
+
DOMAIN_FILES_API=$(echo "$FILES" | grep -iE "router|route|endpoint|openapi|sdk|api" || echo "")
|
|
993
|
+
|
|
994
|
+
# Fallback: if a domain has no specific files matched, give it the full file list
|
|
995
|
+
# (happens for Security — which reviews everything — and for edge cases)
|
|
996
|
+
[ -z "$DOMAIN_FILES_AUTH" ] && DOMAIN_FILES_AUTH="$FILES"
|
|
997
|
+
[ -z "$DOMAIN_FILES_BILLING" ] && DOMAIN_FILES_BILLING="$FILES"
|
|
998
|
+
[ -z "$DOMAIN_FILES_DATABASE" ] && DOMAIN_FILES_DATABASE="$FILES"
|
|
999
|
+
```
|
|
686
1000
|
|
|
687
1001
|
### 3C: Load Agent Templates & Launch
|
|
688
1002
|
|
|
689
|
-
**>>> INVOCATION: Read
|
|
1003
|
+
**>>> INVOCATION: Read shared protocols + selected persona files:**
|
|
690
1004
|
```
|
|
691
|
-
Read
|
|
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)
|
|
692
1007
|
```
|
|
693
1008
|
|
|
1009
|
+
Persona file names: `security.md` (always), `auth.md`, `billing.md`, `concurrency.md`, `scraper.md`, `frontend.md`, `api.md`, `database.md`, `infra.md`.
|
|
1010
|
+
See `review-pr-agents.md` for the full routing table mapping domains → persona files.
|
|
1011
|
+
|
|
694
1012
|
(`$FORGE_HOME` defaults to `~/.claude`)
|
|
695
1013
|
|
|
696
1014
|
**>>> LOAD CONFIG: Read forge.yaml for project context:**
|
|
@@ -699,30 +1017,193 @@ Read the file: $FORGE_HOME/commands/review-pr-agents.md
|
|
|
699
1017
|
FORGE_YAML="${FORGE_CONFIG:-$(git rev-parse --show-toplevel 2>/dev/null)/forge.yaml}"
|
|
700
1018
|
PROJECT_NAME=$(yq '.project.name' "$FORGE_YAML" 2>/dev/null || echo "this project")
|
|
701
1019
|
PROJECT_CONTEXT=$(yq '.review.context' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
702
|
-
TECH_STACK=$(yq '.review.tech_stack' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
703
1020
|
# Domain-specific context (keyed by agent name)
|
|
704
1021
|
DOMAIN_CONTEXT_AUTH=$(yq '.review.domains.auth' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
705
1022
|
DOMAIN_CONTEXT_BILLING=$(yq '.review.domains.billing' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
1023
|
+
DOMAIN_CONTEXT_CONCURRENCY=$(yq '.review.domains.concurrency' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
706
1024
|
DOMAIN_CONTEXT_INFRA=$(yq '.review.domains.infra' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
707
1025
|
DOMAIN_CONTEXT_DATABASE=$(yq '.review.domains.database' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
708
1026
|
DOMAIN_CONTEXT_FRONTEND=$(yq '.review.domains.frontend' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
709
1027
|
DOMAIN_CONTEXT_SECURITY=$(yq '.review.domains.security' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
710
1028
|
DOMAIN_CONTEXT_API=$(yq '.review.domains.api' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
1029
|
+
# Scraping domain context also gates spawning: agent only runs when this key is set
|
|
1030
|
+
DOMAIN_CONTEXT_SCRAPING=$(yq '.review.domains.scraping' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
711
1031
|
```
|
|
712
1032
|
|
|
713
1033
|
If `forge.yaml` is absent or a field is empty/null, agents fall back to generic checks (no project-specific context injected — agents still function correctly, just without project conventions).
|
|
714
1034
|
|
|
715
|
-
|
|
716
|
-
|
|
1035
|
+
**Code Index Slice (inject per agent — replaces full-repo search space with domain-scoped facts):**
|
|
1036
|
+
|
|
1037
|
+
Before launching agents, query the code index to build a domain-scoped file list and symbol map for each agent's diff domain. This replaces full-repo search with deterministic pre-computed data:
|
|
1038
|
+
|
|
1039
|
+
```bash
|
|
1040
|
+
REPO_PATH=$(yq '.paths.root' "$FORGE_YAML" 2>/dev/null || git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
1041
|
+
CODE_INDEX_SCRIPT="${REPO_PATH}/scripts/code-index.sh"
|
|
1042
|
+
|
|
1043
|
+
# Ensure index is current (cache-hit on unchanged HEAD — instant if already built)
|
|
1044
|
+
if [[ -x "$CODE_INDEX_SCRIPT" ]]; then
|
|
1045
|
+
bash "$CODE_INDEX_SCRIPT" --repo-path "$REPO_PATH" 2>/dev/null || true
|
|
1046
|
+
fi
|
|
1047
|
+
|
|
1048
|
+
# Build domain-scoped index slices for each selected agent's domain
|
|
1049
|
+
# Replace {DOMAIN} with the agent's domain label (auth, billing, database, api, frontend, etc.)
|
|
1050
|
+
build_index_slice() {
|
|
1051
|
+
local domain="$1"
|
|
1052
|
+
if [[ -x "$CODE_INDEX_SCRIPT" ]]; then
|
|
1053
|
+
local files
|
|
1054
|
+
files=$(bash "$CODE_INDEX_SCRIPT" query --domain "$domain" --repo-path "$REPO_PATH" 2>/dev/null | awk -F'\t' '{print $1}' | head -30 | tr '\n' ' ')
|
|
1055
|
+
echo "Domain files (${domain}): ${files:-none}"
|
|
1056
|
+
else
|
|
1057
|
+
echo "Code index not available — agent will use grep exploration"
|
|
1058
|
+
fi
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
# Compute slices for selected agents (only the domains that are actually running)
|
|
1062
|
+
INDEX_SLICE_AUTH=$(build_index_slice "auth")
|
|
1063
|
+
INDEX_SLICE_BILLING=$(build_index_slice "billing")
|
|
1064
|
+
INDEX_SLICE_DATABASE=$(build_index_slice "database")
|
|
1065
|
+
INDEX_SLICE_API=$(build_index_slice "api")
|
|
1066
|
+
INDEX_SLICE_FRONTEND=$(build_index_slice "frontend")
|
|
1067
|
+
INDEX_SLICE_INFRA=$(build_index_slice "infrastructure")
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
**Absence is not an error**: If `scripts/code-index.sh` is absent, `INDEX_SLICE_*` variables will contain the fallback message and agents proceed with their standard grep-based exploration.
|
|
1071
|
+
|
|
1072
|
+
**>>> COMPUTE: Per-domain diff slices (input-scoping — do this BEFORE launching agents):**
|
|
1073
|
+
|
|
1074
|
+
Each domain agent receives only the diff slice relevant to its domain, not the full PR changeset. This caps per-child input cost on large PRs. Compute slices once here; substitute `[DOMAIN_DIFF_SLICE]` per agent below.
|
|
1075
|
+
|
|
1076
|
+
```bash
|
|
1077
|
+
# Full diff fetched once — agents do NOT re-fetch it
|
|
1078
|
+
FULL_DIFF=$(gh pr diff $ARGUMENTS)
|
|
1079
|
+
FULL_FILES=$(gh pr diff $ARGUMENTS --name-only)
|
|
1080
|
+
|
|
1081
|
+
# --- Security agent: always receives the full diff (cross-cutting domain) ---
|
|
1082
|
+
DIFF_SLICE_SECURITY="$FULL_DIFF"
|
|
1083
|
+
|
|
1084
|
+
# --- Auth agent: auth, session, jwt, oauth, middleware, permission files ---
|
|
1085
|
+
DIFF_SLICE_AUTH=$(echo "$FULL_DIFF" | awk '
|
|
1086
|
+
/^diff --git/ { in_block=0 }
|
|
1087
|
+
/^diff --git.*\/(auth|session|jwt|oauth|permission|middleware|login|token)/ { in_block=1 }
|
|
1088
|
+
in_block { print }
|
|
1089
|
+
')
|
|
1090
|
+
[ -z "$DIFF_SLICE_AUTH" ] && DIFF_SLICE_AUTH="$FULL_DIFF"
|
|
1091
|
+
|
|
1092
|
+
# --- Billing agent: billing, payment, credit, pricing, stripe, subscription files ---
|
|
1093
|
+
DIFF_SLICE_BILLING=$(echo "$FULL_DIFF" | awk '
|
|
1094
|
+
/^diff --git/ { in_block=0 }
|
|
1095
|
+
/^diff --git.*\/(billing|payment|credit|pricing|stripe|subscription|invoice|charge|refund)/ { in_block=1 }
|
|
1096
|
+
in_block { print }
|
|
1097
|
+
')
|
|
1098
|
+
[ -z "$DIFF_SLICE_BILLING" ] && DIFF_SLICE_BILLING="$FULL_DIFF"
|
|
1099
|
+
|
|
1100
|
+
# --- Concurrency agent: transaction, lock, queue, async, worker, race-condition files ---
|
|
1101
|
+
DIFF_SLICE_CONCURRENCY=$(echo "$FULL_DIFF" | awk '
|
|
1102
|
+
/^diff --git/ { in_block=0 }
|
|
1103
|
+
/^diff --git.*\/(worker|queue|task|async|lock|transaction|pipeline|job)/ { in_block=1 }
|
|
1104
|
+
in_block { print }
|
|
1105
|
+
')
|
|
1106
|
+
[ -z "$DIFF_SLICE_CONCURRENCY" ] && DIFF_SLICE_CONCURRENCY="$FULL_DIFF"
|
|
1107
|
+
|
|
1108
|
+
# --- Database agent: migration, model, schema, ORM, SQL files ---
|
|
1109
|
+
DIFF_SLICE_DATABASE=$(echo "$FULL_DIFF" | awk '
|
|
1110
|
+
/^diff --git/ { in_block=0 }
|
|
1111
|
+
/^diff --git.*\/(migration|model|schema|\.sql$|orm|database|db)/ { in_block=1 }
|
|
1112
|
+
in_block { print }
|
|
1113
|
+
')
|
|
1114
|
+
[ -z "$DIFF_SLICE_DATABASE" ] && DIFF_SLICE_DATABASE="$FULL_DIFF"
|
|
1115
|
+
|
|
1116
|
+
# --- Frontend agent: web/src, components, pages, styles, tsx/jsx files ---
|
|
1117
|
+
DIFF_SLICE_FRONTEND=$(echo "$FULL_DIFF" | awk '
|
|
1118
|
+
/^diff --git/ { in_block=0 }
|
|
1119
|
+
/^diff --git.*(web\/src|components|pages|styles|\.tsx|\.jsx|\.css|\.scss)/ { in_block=1 }
|
|
1120
|
+
in_block { print }
|
|
1121
|
+
')
|
|
1122
|
+
[ -z "$DIFF_SLICE_FRONTEND" ] && DIFF_SLICE_FRONTEND="$FULL_DIFF"
|
|
1123
|
+
|
|
1124
|
+
# --- API Design agent: router, endpoint, openapi, schema, serializer files ---
|
|
1125
|
+
DIFF_SLICE_API=$(echo "$FULL_DIFF" | awk '
|
|
1126
|
+
/^diff --git/ { in_block=0 }
|
|
1127
|
+
/^diff --git.*\/(router|route|endpoint|openapi|serializer|schema|api)/ { in_block=1 }
|
|
1128
|
+
in_block { print }
|
|
1129
|
+
')
|
|
1130
|
+
[ -z "$DIFF_SLICE_API" ] && DIFF_SLICE_API="$FULL_DIFF"
|
|
1131
|
+
|
|
1132
|
+
# --- Infrastructure agent: docker, nginx, ci, deploy, infra, workflow files ---
|
|
1133
|
+
DIFF_SLICE_INFRA=$(echo "$FULL_DIFF" | awk '
|
|
1134
|
+
/^diff --git/ { in_block=0 }
|
|
1135
|
+
/^diff --git.*\/(docker|nginx|infra|\.github|deploy|ci|traefik|k8s|helm)/ { in_block=1 }
|
|
1136
|
+
in_block { print }
|
|
1137
|
+
')
|
|
1138
|
+
[ -z "$DIFF_SLICE_INFRA" ] && DIFF_SLICE_INFRA="$FULL_DIFF"
|
|
1139
|
+
|
|
1140
|
+
# --- Domain Logic agent: scraping/browser-automation files (does not match bare playwright/E2E test files) ---
|
|
1141
|
+
DIFF_SLICE_SCRAPER=$(echo "$FULL_DIFF" | awk '
|
|
1142
|
+
/^diff --git/ { in_block=0 }
|
|
1143
|
+
/^diff --git.*\/(scrape|scraper|stealth|browser_pool|headless|anti_bot|captcha|crawl)/ { in_block=1 }
|
|
1144
|
+
in_block { print }
|
|
1145
|
+
')
|
|
1146
|
+
[ -z "$DIFF_SLICE_SCRAPER" ] && DIFF_SLICE_SCRAPER="$FULL_DIFF"
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
**Tool-result truncation discipline**: All `gh pr diff` and file-read outputs piped to agents MUST be capped at ~100K characters. This mirrors the runner's built-in 100K-char tool-result cap (`bin/runner.mjs`). Any diff slice exceeding 100K chars must be truncated before substitution — agents that receive oversized context perform worse, not better.
|
|
1150
|
+
|
|
1151
|
+
```bash
|
|
1152
|
+
# Truncate any slice exceeding 100K chars before passing to agent
|
|
1153
|
+
truncate_slice() {
|
|
1154
|
+
local slice="$1"
|
|
1155
|
+
local limit=102400
|
|
1156
|
+
if [ "${#slice}" -gt "$limit" ]; then
|
|
1157
|
+
echo "${slice:0:$limit}"
|
|
1158
|
+
echo "... [TRUNCATED — diff exceeded 100K chars; focus on the files listed above]"
|
|
1159
|
+
else
|
|
1160
|
+
echo "$slice"
|
|
1161
|
+
fi
|
|
1162
|
+
}
|
|
1163
|
+
DIFF_SLICE_SECURITY=$(truncate_slice "$DIFF_SLICE_SECURITY")
|
|
1164
|
+
DIFF_SLICE_AUTH=$(truncate_slice "$DIFF_SLICE_AUTH")
|
|
1165
|
+
DIFF_SLICE_BILLING=$(truncate_slice "$DIFF_SLICE_BILLING")
|
|
1166
|
+
DIFF_SLICE_CONCURRENCY=$(truncate_slice "$DIFF_SLICE_CONCURRENCY")
|
|
1167
|
+
DIFF_SLICE_DATABASE=$(truncate_slice "$DIFF_SLICE_DATABASE")
|
|
1168
|
+
DIFF_SLICE_FRONTEND=$(truncate_slice "$DIFF_SLICE_FRONTEND")
|
|
1169
|
+
DIFF_SLICE_API=$(truncate_slice "$DIFF_SLICE_API")
|
|
1170
|
+
DIFF_SLICE_INFRA=$(truncate_slice "$DIFF_SLICE_INFRA")
|
|
1171
|
+
DIFF_SLICE_SCRAPER=$(truncate_slice "$DIFF_SLICE_SCRAPER")
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
The `protocols.md` file contains the Evidence-Based Review Protocol, Structured Findings Protocol, Per-Agent Input Scoping rules, and Tool-Result Truncation Discipline. Each persona file contains that agent's full prompt template. For each agent in `$SELECTED_AGENTS` (computed in Phase 3B):
|
|
1175
|
+
1. Extract its template from the persona file (`review-pr-agents/<persona>.md`)
|
|
717
1176
|
2. Substitute: `[PR_NUMBER]`, `[REVIEW_SHA]`, `[REVIEW_SHA_SHORT]`, `[TITLE]`, relevant files list
|
|
718
|
-
3. Substitute: `[PROJECT_NAME]` → `$PROJECT_NAME`, `[PROJECT_CONTEXT]` → `$PROJECT_CONTEXT
|
|
719
|
-
4. Substitute per-agent domain context: `[DOMAIN_CONTEXT]` → the agent's matching key from `forge.yaml → review.domains` (e.g., `$DOMAIN_CONTEXT_AUTH` for the auth agent, `$DOMAIN_CONTEXT_BILLING` for the billing agent)
|
|
1177
|
+
3. Substitute: `[PROJECT_NAME]` → `$PROJECT_NAME`, `[PROJECT_CONTEXT]` → `$PROJECT_CONTEXT`
|
|
1178
|
+
4. Substitute per-agent domain context: `[DOMAIN_CONTEXT]` → the agent's matching key from `forge.yaml → review.domains` (e.g., `$DOMAIN_CONTEXT_AUTH` for the auth agent, `$DOMAIN_CONTEXT_BILLING` for the billing agent, `$DOMAIN_CONTEXT_CONCURRENCY` for the concurrency agent, `$DOMAIN_CONTEXT_SCRAPING` for the scraping agent)
|
|
720
1179
|
5. Substitute the shared hot-spot prior: `[CHURN_CONTEXT]` → `$CHURN_CONTEXT` (computed once in Phase 3A, same value for every agent — this is a PR-level fact, not a per-domain config value, so it is NOT read from `forge.yaml`)
|
|
721
|
-
6.
|
|
722
|
-
7.
|
|
1180
|
+
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
|
+
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
|
+
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)
|
|
723
1184
|
|
|
724
1185
|
**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`.
|
|
725
1186
|
|
|
1187
|
+
#### Domain Diff Slicing
|
|
1188
|
+
|
|
1189
|
+
Each agent's prompt receives `[FILE_LIST]` scoped to its domain-relevant files (computed in Phase 3B as `DOMAIN_FILES_*`). This reduces per-agent token cost — each agent reads only the diff slice relevant to its domain, not the full changeset.
|
|
1190
|
+
|
|
1191
|
+
- **Security agent**: receives the full file list and full diff (it must see everything)
|
|
1192
|
+
- **Domain agents**: receive `DOMAIN_FILES_<DOMAIN>` as `[FILE_LIST]`; if a domain's file list is empty (fallback), pass the full list
|
|
1193
|
+
|
|
1194
|
+
When substituting `[FILE_LIST]` in each agent's template:
|
|
1195
|
+
- Auth agent → `$DOMAIN_FILES_AUTH`
|
|
1196
|
+
- Billing agent → `$DOMAIN_FILES_BILLING`
|
|
1197
|
+
- Concurrency agent → `$DOMAIN_FILES_CONCURRENCY`
|
|
1198
|
+
- Database agent → `$DOMAIN_FILES_DATABASE`
|
|
1199
|
+
- Infrastructure agent → `$DOMAIN_FILES_INFRA`
|
|
1200
|
+
- Scraping agent → `$DOMAIN_FILES_SCRAPING`
|
|
1201
|
+
- Frontend agent → `$DOMAIN_FILES_FRONTEND`
|
|
1202
|
+
- API agent → `$DOMAIN_FILES_API`
|
|
1203
|
+
- Security agent → `$FILES` (full list)
|
|
1204
|
+
|
|
1205
|
+
**Note**: Domain agents still run `gh pr diff $PR_NUMBER` inside their execution context. The scoped `[FILE_LIST]` tells each agent which files are most relevant to its domain — it is a focused starting point, not a hard restriction. Agents should follow code paths beyond their slice if those paths are needed to complete a trace.
|
|
1206
|
+
|
|
726
1207
|
---
|
|
727
1208
|
|
|
728
1209
|
## Phase 4: Wait for Agents
|
|
@@ -829,8 +1310,26 @@ fi
|
|
|
829
1310
|
|
|
830
1311
|
**Dedup against existing issues (MANDATORY before creating):**
|
|
831
1312
|
|
|
1313
|
+
Run the deterministic dedup script first, then fall through to the line-range check:
|
|
1314
|
+
|
|
1315
|
+
```bash
|
|
1316
|
+
# Step 0: Deterministic title dedup — catches near-duplicates before line-range check
|
|
1317
|
+
# See scripts/issue-dedup.sh for the token-overlap algorithm. <!-- Added: forge#1335 -->
|
|
1318
|
+
FINDING_TITLE_DEDUP="fix: brief description of finding (review finding — PR #${PR_NUMBER})"
|
|
1319
|
+
DEDUP_RESULT=$(scripts/issue-dedup.sh "$FINDING_TITLE_DEDUP" "$GH_FLAG" 2>&1)
|
|
1320
|
+
DEDUP_EXIT=$?
|
|
1321
|
+
if [ "$DEDUP_EXIT" -eq 1 ]; then
|
|
1322
|
+
echo "DEDUP: Skipping — $DEDUP_RESULT"
|
|
1323
|
+
# Skip this finding — do NOT create a duplicate issue
|
|
1324
|
+
# Add a comment on the existing issue referencing this recurrence in PR #${PR_NUMBER}
|
|
1325
|
+
elif [ "$DEDUP_EXIT" -eq 2 ]; then
|
|
1326
|
+
echo "DEDUP: Usage error — $DEDUP_RESULT"
|
|
1327
|
+
# Skip this finding — do NOT fall through to gh issue create on a usage error
|
|
1328
|
+
fi
|
|
1329
|
+
```
|
|
1330
|
+
|
|
832
1331
|
```bash
|
|
833
|
-
# For each finding
|
|
1332
|
+
# For each finding (that passes the title dedup above), check line-range overlap
|
|
834
1333
|
FINDING_FILE="path/to/file.py"
|
|
835
1334
|
FINDING_LINE="123"
|
|
836
1335
|
FINDING_TITLE="fix: brief description of finding (review finding — PR #${PR_NUMBER})"
|
|
@@ -908,6 +1407,11 @@ ISSUE_NUM=$(gh issue create \
|
|
|
908
1407
|
**Root cause**: [one sentence — why the bug occurs mechanically]
|
|
909
1408
|
**Prevention**: [one sentence — what the builder must do to avoid this class of bug]
|
|
910
1409
|
|
|
1410
|
+
<!-- FORGE:PATTERN: [pattern-slug] -->
|
|
1411
|
+
<!-- This machine-readable tag is used by pipeline-health Phase 4A to count pattern recurrences.
|
|
1412
|
+
When this slug appears on 3+ findings, a check-promotion issue is automatically filed.
|
|
1413
|
+
Keep the slug consistent across all findings for the same defect class. --> <!-- Added: forge#1331 -->
|
|
1414
|
+
|
|
911
1415
|
## Affected Files
|
|
912
1416
|
|
|
913
1417
|
Files that need changes:
|
|
@@ -934,12 +1438,39 @@ Files that need changes:
|
|
|
934
1438
|
- [ ] Trace code path to verify issue
|
|
935
1439
|
- [ ] Check existing mitigations
|
|
936
1440
|
- [ ] Reproduce or construct proof-of-concept
|
|
1441
|
+
[BATCHABLE_ANNOTATION]
|
|
937
1442
|
ISSUE_EOF
|
|
938
1443
|
)" --json number --jq '.number')
|
|
939
1444
|
```
|
|
940
1445
|
|
|
941
1446
|
Labels: `review-finding` + `needs-validation` + priority (`priority:P1` CONFIRMED, `priority:P2` LIKELY, `priority:P3` POSSIBLE).
|
|
942
1447
|
|
|
1448
|
+
**P3 batchable annotation** — <!-- Added: forge#1333 --> When priority resolves to `priority:P3` (POSSIBLE confidence), check whether the finding qualifies for batching. If the affected file does NOT touch a security or billing path, substitute `[BATCHABLE_ANNOTATION]` with the `<!-- FORGE:BATCHABLE -->` marker. This signals the orchestrator's Phase 1 batching rule that this finding can be grouped with other P3s in the same domain:
|
|
1449
|
+
|
|
1450
|
+
```bash
|
|
1451
|
+
# Determine batchable eligibility for this finding
|
|
1452
|
+
FINDING_PRIORITY="{priority}" # priority:P1 / priority:P2 / priority:P3
|
|
1453
|
+
BATCHABLE_ANNOTATION=""
|
|
1454
|
+
|
|
1455
|
+
if [ "$FINDING_PRIORITY" = "priority:P3" ]; then
|
|
1456
|
+
# Check for security/billing exclusions
|
|
1457
|
+
IS_SECURITY_PATH=0
|
|
1458
|
+
IS_BILLING_PATH=0
|
|
1459
|
+
# Test affected file path
|
|
1460
|
+
echo "$FINDING_FILE" | grep -qiE 'security|billing|payment|stripe|charge|invoice' && IS_SECURITY_PATH=1
|
|
1461
|
+
echo "$FINDING_TITLE" | grep -qiE 'security|billing|payment|stripe|charge|invoice' && IS_BILLING_PATH=1
|
|
1462
|
+
|
|
1463
|
+
if [ "$IS_SECURITY_PATH" -eq 0 ] && [ "$IS_BILLING_PATH" -eq 0 ]; then
|
|
1464
|
+
BATCHABLE_ANNOTATION="<!-- FORGE:BATCHABLE -->"
|
|
1465
|
+
echo "INFO: P3 finding eligible for batching — appending FORGE:BATCHABLE annotation"
|
|
1466
|
+
else
|
|
1467
|
+
BATCHABLE_ANNOTATION=""
|
|
1468
|
+
echo "INFO: P3 finding touches security/billing path — NOT batchable, keeping individual pipeline"
|
|
1469
|
+
fi
|
|
1470
|
+
fi
|
|
1471
|
+
# When priority is P1 or P2: BATCHABLE_ANNOTATION stays empty (never batched)
|
|
1472
|
+
```
|
|
1473
|
+
|
|
943
1474
|
**Add to project board:**
|
|
944
1475
|
```bash
|
|
945
1476
|
for FINDING_NUM in {numbers}; do
|
|
@@ -1046,20 +1577,29 @@ if [ "$MERGE_HEALTH" = "CONFLICTING" ] || [ "$MERGE_HEALTH_STATE" = "DIRTY" ] ||
|
|
|
1046
1577
|
MERGE_CONFLICT_MSG="Merge conflict with \`${BASE}\`. Rebase \`${HEAD}\` onto \`origin/${BASE}\`, resolve the conflicting files, then re-run /review-pr."
|
|
1047
1578
|
fi
|
|
1048
1579
|
|
|
1580
|
+
# Resolve attribution footer (forge.yaml → attribution.pr_footer)
|
|
1581
|
+
ATTRIBUTION_PR_FOOTER=$(grep -A5 "^attribution:" forge.yaml 2>/dev/null | grep "pr_footer:" | awk '{print $2}' | tr -d '"' || echo "false")
|
|
1582
|
+
ATTRIBUTION_FOOTER_LINE=""
|
|
1583
|
+
if [ "$ATTRIBUTION_PR_FOOTER" = "true" ]; then
|
|
1584
|
+
ATTRIBUTION_FOOTER_LINE="
|
|
1585
|
+
---
|
|
1586
|
+
> Orchestrated with [ForgeDock](https://github.com/RapierCraftStudios/ForgeDock) — state, scheduling, review, and memory on GitHub."
|
|
1587
|
+
fi
|
|
1588
|
+
|
|
1049
1589
|
# Stale review:
|
|
1050
1590
|
gh pr review $ARGUMENTS --comment --body "Review of commit $REVIEW_SHA_SHORT is stale — PR HEAD changed. Re-run /review-pr."
|
|
1051
1591
|
|
|
1052
1592
|
# Clean (no blocking issues):
|
|
1053
1593
|
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.
|
|
1054
1594
|
$([ "$MERGE_HEALTH" = "UNKNOWN" ] && echo "
|
|
1055
|
-
⚠ Mergeability: GitHub is still computing merge state (UNKNOWN after retries). Verify manually before merging.")"
|
|
1595
|
+
⚠ Mergeability: GitHub is still computing merge state (UNKNOWN after retries). Verify manually before merging.")${ATTRIBUTION_FOOTER_LINE}"
|
|
1056
1596
|
|
|
1057
1597
|
# Blocking issues (including merge conflicts and purpose regressions):
|
|
1058
1598
|
gh pr review $ARGUMENTS --comment --body "CHANGES REQUESTED: commit $REVIEW_SHA_SHORT — [N] blocking issues found. See GitHub issues.
|
|
1059
1599
|
$([ "$HAS_MERGE_CONFLICT" = "true" ] && echo "
|
|
1060
1600
|
🔴 Merge Conflict: ${MERGE_CONFLICT_MSG}")
|
|
1061
1601
|
$([ "$HAS_PURPOSE_REGRESSION" = "true" ] && echo "
|
|
1062
|
-
⚠ Purpose Regression: [N] finding(s) contradict the milestone's stated goal and are automatically blocking regardless of runtime impact. See: ${PURPOSE_REGRESSION_FINDINGS[@]}")"
|
|
1602
|
+
⚠ Purpose Regression: [N] finding(s) contradict the milestone's stated goal and are automatically blocking regardless of runtime impact. See: ${PURPOSE_REGRESSION_FINDINGS[@]}")${ATTRIBUTION_FOOTER_LINE}"
|
|
1063
1603
|
```
|
|
1064
1604
|
|
|
1065
1605
|
---
|
|
@@ -1069,6 +1609,20 @@ $([ "$HAS_PURPOSE_REGRESSION" = "true" ] && echo "
|
|
|
1069
1609
|
**Skip if** `AUTO_MERGE=false`.
|
|
1070
1610
|
|
|
1071
1611
|
```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
|
|
1618
|
+
BLOCK_REASON=""
|
|
1619
|
+
[ "$VERDICT" = "CHANGES REQUESTED" ] && BLOCK_REASON="review verdict is CHANGES REQUESTED (blocking finding confirmed by Phase 7B)"
|
|
1620
|
+
[ "$HAS_PURPOSE_REGRESSION" = "true" ] && BLOCK_REASON="${BLOCK_REASON:+${BLOCK_REASON}; }purpose regression detected by Phase 7A (\`HAS_PURPOSE_REGRESSION=true\`)"
|
|
1621
|
+
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "⛔ Auto-merge aborted for PR #{PR_NUMBER}: ${BLOCK_REASON}. Manual review required before merging."
|
|
1622
|
+
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
|
|
1624
|
+
else
|
|
1625
|
+
|
|
1072
1626
|
# Pre-merge mergeability guard — re-fetch fresh state before attempting merge <!-- Added: forge#194 -->
|
|
1073
1627
|
# A PR that was MERGEABLE at Phase 1A may have become CONFLICTING if the base branch
|
|
1074
1628
|
# received commits while the review was running. Re-check before every auto-merge.
|
|
@@ -1082,7 +1636,7 @@ if [ "$PRE_MERGE_HEALTH" = "CONFLICTING" ] || [ "$PRE_MERGE_HEALTH_STATE" = "DIR
|
|
|
1082
1636
|
# STOP — do not attempt gh pr merge on a CONFLICTING/DIRTY PR
|
|
1083
1637
|
else
|
|
1084
1638
|
# Checkpoint comment on issue
|
|
1085
|
-
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "Review complete for PR #{PR_NUMBER}. Verdict: {VERDICT}. Proceeding to merge."
|
|
1639
|
+
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "Review complete for PR #{PR_NUMBER}. Verdict: ${VERDICT:-APPROVED}. Proceeding to merge."
|
|
1086
1640
|
|
|
1087
1641
|
# Merge
|
|
1088
1642
|
gh pr merge {PR_NUMBER} {MERGE_GH_FLAG} --merge
|
|
@@ -1091,9 +1645,10 @@ else
|
|
|
1091
1645
|
MERGE_STATE=$(gh pr view {PR_NUMBER} {MERGE_GH_FLAG} --json state --jq '.state')
|
|
1092
1646
|
[ "$MERGE_STATE" != "MERGED" ] && gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "PR #{PR_NUMBER} merge failed. State: $MERGE_STATE."
|
|
1093
1647
|
fi
|
|
1648
|
+
fi
|
|
1094
1649
|
```
|
|
1095
1650
|
|
|
1096
|
-
**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`
|
|
1651
|
+
**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.
|
|
1097
1652
|
|
|
1098
1653
|
### 8B: Post-Merge Review Finding Demilestoning (Milestone PRs Only)
|
|
1099
1654
|
|
|
@@ -1194,7 +1749,7 @@ gh pr comment $ARGUMENTS --body "$(cat <<'EOF'
|
|
|
1194
1749
|
## Verdict: [APPROVE / CHANGES REQUESTED / NEEDS RE-REVIEW]
|
|
1195
1750
|
|
|
1196
1751
|
## Context-Aware Review
|
|
1197
|
-
**Domains**: [list] | **Agents**: [N] ([names])
|
|
1752
|
+
**Domains**: [list] | **Agents**: [N] ([names]) | **Dispatch mode**: [risk-scaled (default) / thorough (--thorough flag) / full-union (milestone PR)]
|
|
1198
1753
|
|
|
1199
1754
|
## Integration Checks (Phase 2.5)
|
|
1200
1755
|
**Code registration**: [pass / N broken activation paths found]
|
|
@@ -1226,9 +1781,10 @@ Notify user:
|
|
|
1226
1781
|
```
|
|
1227
1782
|
Review complete for PR #X. Verdict: [VERDICT].
|
|
1228
1783
|
- Domains: [list]
|
|
1229
|
-
- Agents: [N] ([names])
|
|
1784
|
+
- Agents: [N] ([names]) — [dispatch mode: risk-scaled / thorough / full-union]
|
|
1230
1785
|
- Integration checks: [pass/N broken paths found]
|
|
1231
1786
|
- Issues created: [M]
|
|
1232
1787
|
{IF AUTO_MERGE: "PR merged and issue closed." / "Merge FAILED — see issue."}
|
|
1233
1788
|
{IF IS_MILESTONE_TO_STAGING: "Review findings demilestoned: [N] issues moved to fast lane." / ""}
|
|
1789
|
+
{Tip if risk-scaled: "Run with --thorough to enable full union dispatch for release-critical reviews."}
|
|
1234
1790
|
```
|