forgedock 1.1.1 → 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 +53 -8
- package/bin/batch-runner.mjs +339 -0
- package/bin/demo.mjs +92 -7
- package/bin/engine/phases.mjs +20 -0
- package/bin/engine/state.mjs +8 -3
- package/bin/engine-cli.mjs +177 -5
- package/bin/forgedock.mjs +409 -26
- 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 +72 -14
- package/bin/registry.mjs +139 -0
- package/bin/report.mjs +447 -0
- package/bin/runner.mjs +68 -19
- package/bin/settings-hook.mjs +235 -1
- 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 +80 -8
- 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 +58 -1
- package/commands/design-bench.md +5 -1
- package/commands/design-render-critique-loop.md +1 -1
- package/commands/diagnose.md +2 -1
- package/commands/explain.md +2 -1
- package/commands/forge-stats.md +121 -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 +23 -2
- 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 -2045
- 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 +248 -10
- 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 +591 -52
- 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 +3 -1
- package/commands/work-on/build/validate.md +26 -7
- package/commands/work-on/build.md +163 -24
- package/commands/work-on/close.md +130 -3
- package/commands/work-on/decompose.md +19 -1
- package/commands/work-on/investigate.md +117 -2
- package/commands/work-on/review.md +24 -4
- package/commands/work-on-monolithic.md +7 -2
- package/commands/work-on.md +202 -4
- package/docs/BRAND.md +14 -7
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +36 -1
- package/docs/CONFIG.md +5 -8
- 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 +43 -4
- package/docs/site/github-is-the-memory.md +113 -0
- package/docs/site/how-it-works.md +3 -13
- package/docs/site/index.md +9 -1
- 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/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 +3 -1
- 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/worktree-lifecycle.sh +3 -3
- package/templates/devdocs/agent/using-forgedock.md +14 -0
- package/docs/site/.vitepress/config.mjs +0 -85
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,16 +211,18 @@ 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
|
-
|
|
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
|
|
150
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
|
|
@@ -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
|
|
|
@@ -632,7 +744,7 @@ echo "=== RISK SIGNALS ==="
|
|
|
632
744
|
echo "$DIFF" | grep -cE "subprocess|exec|eval|system\(|popen|heredoc" && echo " UNTRUSTED_INPUT_PROCESSING" || true
|
|
633
745
|
echo "$DIFF" | grep -cE "\.sh$|bash|shell|cron" && echo " SHELL_SCRIPT" || true
|
|
634
746
|
echo "$DIFF" | grep -cE "credit|balance|debit|charge|refund|stripe" && echo " FINANCIAL" || true
|
|
635
|
-
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
|
|
636
748
|
echo "$DIFF" | grep -cE "\.sql$|migration|DROP|ALTER|DELETE FROM" && echo " DATABASE_MUTATION" || true
|
|
637
749
|
echo "$DIFF" | grep -cE "docker|deploy|traefik|nginx|\.yml.*service" && echo " INFRASTRUCTURE" || true
|
|
638
750
|
echo "$DIFF" | grep -cE "docker-compose.*postgres|docker-compose.*redis|postgres.*command:|redis.*command:|image:.*postgres|image:.*redis" && echo " DATABASE_CONTAINER" || true
|
|
@@ -671,43 +783,232 @@ echo "$CHURN_CONTEXT"
|
|
|
671
783
|
|
|
672
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.
|
|
673
785
|
|
|
674
|
-
### 3B: Select Agents
|
|
786
|
+
### 3B: Select Agents — Risk-Scaled Dispatch
|
|
787
|
+
|
|
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.
|
|
789
|
+
|
|
790
|
+
#### Step 1: Score Each Domain by Signal Strength
|
|
791
|
+
|
|
792
|
+
Assign a relevance score to each domain based on signals detected in Phase 3A. Higher score = higher priority for inclusion.
|
|
793
|
+
|
|
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
|
+
```
|
|
850
|
+
|
|
851
|
+
#### Step 2: Read Architect CONTRACT for Risk Flags (if PR is from /work-on)
|
|
852
|
+
|
|
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
|
|
675
872
|
|
|
676
|
-
|
|
677
|
-
|-------------|----------------|
|
|
678
|
-
| UNTRUSTED_INPUT_PROCESSING | Security (deep) + relevant domain |
|
|
679
|
-
| SHELL_SCRIPT | Security (deep) + Infrastructure |
|
|
680
|
-
| FINANCIAL | Security + Billing + Concurrency |
|
|
681
|
-
| AUTH_SENSITIVE | Security + Auth Conventions |
|
|
682
|
-
| DATABASE_MUTATION | Security + Database |
|
|
683
|
-
| DB_CONFIG | Security + Database + Infrastructure |
|
|
684
|
-
| INFRASTRUCTURE | Security + Infrastructure |
|
|
685
|
-
| DATABASE_CONTAINER | Security + Infrastructure (escalate to HIGH risk — stateful container restart) |
|
|
686
|
-
| CODE_EXECUTION | Security (deep) |
|
|
687
|
-
| SDK_OPENAPI | Security + API Design & Consistency (runs cross-PR schema check #10) |
|
|
688
|
-
| None | Security + all matching domain agents |
|
|
873
|
+
Check whether churn hot-spots exist (computed in Phase 3A `$CHURN_CONTEXT`):
|
|
689
874
|
|
|
690
|
-
|
|
875
|
+
```bash
|
|
876
|
+
CHURN_ESCALATION=false
|
|
877
|
+
echo "$CHURN_CONTEXT" | grep -q "HOT" && CHURN_ESCALATION=true
|
|
878
|
+
echo "=== CHURN_ESCALATION: $CHURN_ESCALATION ==="
|
|
879
|
+
```
|
|
691
880
|
|
|
692
|
-
|
|
881
|
+
#### Step 4: Build Selected Agent Roster
|
|
693
882
|
|
|
694
|
-
|
|
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.
|
|
695
884
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
+
```
|
|
701
902
|
|
|
702
|
-
|
|
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
|
+
```
|
|
703
1000
|
|
|
704
1001
|
### 3C: Load Agent Templates & Launch
|
|
705
1002
|
|
|
706
|
-
**>>> INVOCATION: Read
|
|
1003
|
+
**>>> INVOCATION: Read shared protocols + selected persona files:**
|
|
707
1004
|
```
|
|
708
|
-
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)
|
|
709
1007
|
```
|
|
710
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
|
+
|
|
711
1012
|
(`$FORGE_HOME` defaults to `~/.claude`)
|
|
712
1013
|
|
|
713
1014
|
**>>> LOAD CONFIG: Read forge.yaml for project context:**
|
|
@@ -716,30 +1017,193 @@ Read the file: $FORGE_HOME/commands/review-pr-agents.md
|
|
|
716
1017
|
FORGE_YAML="${FORGE_CONFIG:-$(git rev-parse --show-toplevel 2>/dev/null)/forge.yaml}"
|
|
717
1018
|
PROJECT_NAME=$(yq '.project.name' "$FORGE_YAML" 2>/dev/null || echo "this project")
|
|
718
1019
|
PROJECT_CONTEXT=$(yq '.review.context' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
719
|
-
TECH_STACK=$(yq '.review.tech_stack' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
720
1020
|
# Domain-specific context (keyed by agent name)
|
|
721
1021
|
DOMAIN_CONTEXT_AUTH=$(yq '.review.domains.auth' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
722
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 "")
|
|
723
1024
|
DOMAIN_CONTEXT_INFRA=$(yq '.review.domains.infra' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
724
1025
|
DOMAIN_CONTEXT_DATABASE=$(yq '.review.domains.database' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
725
1026
|
DOMAIN_CONTEXT_FRONTEND=$(yq '.review.domains.frontend' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
726
1027
|
DOMAIN_CONTEXT_SECURITY=$(yq '.review.domains.security' "$FORGE_YAML" 2>/dev/null || echo "")
|
|
727
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 "")
|
|
728
1031
|
```
|
|
729
1032
|
|
|
730
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).
|
|
731
1034
|
|
|
732
|
-
|
|
733
|
-
|
|
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`)
|
|
734
1176
|
2. Substitute: `[PR_NUMBER]`, `[REVIEW_SHA]`, `[REVIEW_SHA_SHORT]`, `[TITLE]`, relevant files list
|
|
735
|
-
3. Substitute: `[PROJECT_NAME]` → `$PROJECT_NAME`, `[PROJECT_CONTEXT]` → `$PROJECT_CONTEXT
|
|
736
|
-
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)
|
|
737
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`)
|
|
738
|
-
6.
|
|
739
|
-
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)
|
|
740
1184
|
|
|
741
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`.
|
|
742
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
|
+
|
|
743
1207
|
---
|
|
744
1208
|
|
|
745
1209
|
## Phase 4: Wait for Agents
|
|
@@ -846,8 +1310,26 @@ fi
|
|
|
846
1310
|
|
|
847
1311
|
**Dedup against existing issues (MANDATORY before creating):**
|
|
848
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
|
+
|
|
849
1331
|
```bash
|
|
850
|
-
# For each finding
|
|
1332
|
+
# For each finding (that passes the title dedup above), check line-range overlap
|
|
851
1333
|
FINDING_FILE="path/to/file.py"
|
|
852
1334
|
FINDING_LINE="123"
|
|
853
1335
|
FINDING_TITLE="fix: brief description of finding (review finding — PR #${PR_NUMBER})"
|
|
@@ -925,6 +1407,11 @@ ISSUE_NUM=$(gh issue create \
|
|
|
925
1407
|
**Root cause**: [one sentence — why the bug occurs mechanically]
|
|
926
1408
|
**Prevention**: [one sentence — what the builder must do to avoid this class of bug]
|
|
927
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
|
+
|
|
928
1415
|
## Affected Files
|
|
929
1416
|
|
|
930
1417
|
Files that need changes:
|
|
@@ -951,12 +1438,39 @@ Files that need changes:
|
|
|
951
1438
|
- [ ] Trace code path to verify issue
|
|
952
1439
|
- [ ] Check existing mitigations
|
|
953
1440
|
- [ ] Reproduce or construct proof-of-concept
|
|
1441
|
+
[BATCHABLE_ANNOTATION]
|
|
954
1442
|
ISSUE_EOF
|
|
955
1443
|
)" --json number --jq '.number')
|
|
956
1444
|
```
|
|
957
1445
|
|
|
958
1446
|
Labels: `review-finding` + `needs-validation` + priority (`priority:P1` CONFIRMED, `priority:P2` LIKELY, `priority:P3` POSSIBLE).
|
|
959
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
|
+
|
|
960
1474
|
**Add to project board:**
|
|
961
1475
|
```bash
|
|
962
1476
|
for FINDING_NUM in {numbers}; do
|
|
@@ -1063,20 +1577,29 @@ if [ "$MERGE_HEALTH" = "CONFLICTING" ] || [ "$MERGE_HEALTH_STATE" = "DIRTY" ] ||
|
|
|
1063
1577
|
MERGE_CONFLICT_MSG="Merge conflict with \`${BASE}\`. Rebase \`${HEAD}\` onto \`origin/${BASE}\`, resolve the conflicting files, then re-run /review-pr."
|
|
1064
1578
|
fi
|
|
1065
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
|
+
|
|
1066
1589
|
# Stale review:
|
|
1067
1590
|
gh pr review $ARGUMENTS --comment --body "Review of commit $REVIEW_SHA_SHORT is stale — PR HEAD changed. Re-run /review-pr."
|
|
1068
1591
|
|
|
1069
1592
|
# Clean (no blocking issues):
|
|
1070
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.
|
|
1071
1594
|
$([ "$MERGE_HEALTH" = "UNKNOWN" ] && echo "
|
|
1072
|
-
⚠ 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}"
|
|
1073
1596
|
|
|
1074
1597
|
# Blocking issues (including merge conflicts and purpose regressions):
|
|
1075
1598
|
gh pr review $ARGUMENTS --comment --body "CHANGES REQUESTED: commit $REVIEW_SHA_SHORT — [N] blocking issues found. See GitHub issues.
|
|
1076
1599
|
$([ "$HAS_MERGE_CONFLICT" = "true" ] && echo "
|
|
1077
1600
|
🔴 Merge Conflict: ${MERGE_CONFLICT_MSG}")
|
|
1078
1601
|
$([ "$HAS_PURPOSE_REGRESSION" = "true" ] && echo "
|
|
1079
|
-
⚠ 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}"
|
|
1080
1603
|
```
|
|
1081
1604
|
|
|
1082
1605
|
---
|
|
@@ -1086,6 +1609,20 @@ $([ "$HAS_PURPOSE_REGRESSION" = "true" ] && echo "
|
|
|
1086
1609
|
**Skip if** `AUTO_MERGE=false`.
|
|
1087
1610
|
|
|
1088
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
|
+
|
|
1089
1626
|
# Pre-merge mergeability guard — re-fetch fresh state before attempting merge <!-- Added: forge#194 -->
|
|
1090
1627
|
# A PR that was MERGEABLE at Phase 1A may have become CONFLICTING if the base branch
|
|
1091
1628
|
# received commits while the review was running. Re-check before every auto-merge.
|
|
@@ -1099,7 +1636,7 @@ if [ "$PRE_MERGE_HEALTH" = "CONFLICTING" ] || [ "$PRE_MERGE_HEALTH_STATE" = "DIR
|
|
|
1099
1636
|
# STOP — do not attempt gh pr merge on a CONFLICTING/DIRTY PR
|
|
1100
1637
|
else
|
|
1101
1638
|
# Checkpoint comment on issue
|
|
1102
|
-
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."
|
|
1103
1640
|
|
|
1104
1641
|
# Merge
|
|
1105
1642
|
gh pr merge {PR_NUMBER} {MERGE_GH_FLAG} --merge
|
|
@@ -1108,6 +1645,7 @@ else
|
|
|
1108
1645
|
MERGE_STATE=$(gh pr view {PR_NUMBER} {MERGE_GH_FLAG} --json state --jq '.state')
|
|
1109
1646
|
[ "$MERGE_STATE" != "MERGED" ] && gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "PR #{PR_NUMBER} merge failed. State: $MERGE_STATE."
|
|
1110
1647
|
fi
|
|
1648
|
+
fi
|
|
1111
1649
|
```
|
|
1112
1650
|
|
|
1113
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.
|
|
@@ -1211,7 +1749,7 @@ gh pr comment $ARGUMENTS --body "$(cat <<'EOF'
|
|
|
1211
1749
|
## Verdict: [APPROVE / CHANGES REQUESTED / NEEDS RE-REVIEW]
|
|
1212
1750
|
|
|
1213
1751
|
## Context-Aware Review
|
|
1214
|
-
**Domains**: [list] | **Agents**: [N] ([names])
|
|
1752
|
+
**Domains**: [list] | **Agents**: [N] ([names]) | **Dispatch mode**: [risk-scaled (default) / thorough (--thorough flag) / full-union (milestone PR)]
|
|
1215
1753
|
|
|
1216
1754
|
## Integration Checks (Phase 2.5)
|
|
1217
1755
|
**Code registration**: [pass / N broken activation paths found]
|
|
@@ -1243,9 +1781,10 @@ Notify user:
|
|
|
1243
1781
|
```
|
|
1244
1782
|
Review complete for PR #X. Verdict: [VERDICT].
|
|
1245
1783
|
- Domains: [list]
|
|
1246
|
-
- Agents: [N] ([names])
|
|
1784
|
+
- Agents: [N] ([names]) — [dispatch mode: risk-scaled / thorough / full-union]
|
|
1247
1785
|
- Integration checks: [pass/N broken paths found]
|
|
1248
1786
|
- Issues created: [M]
|
|
1249
1787
|
{IF AUTO_MERGE: "PR merged and issue closed." / "Merge FAILED — see issue."}
|
|
1250
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."}
|
|
1251
1790
|
```
|