forgedock 1.8.4 → 1.9.0
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/bin/engine/admission.mjs +23 -4
- package/bin/engine/admission.test.mjs +37 -0
- package/commands/orchestrate/phase-1-resolve.md +40 -24
- package/commands/orchestrate/phase-3-dependency.md +56 -11
- package/commands/orchestrate/phase-4-execution.md +349 -111
- package/commands/orchestrate/phase-5-cleanup.md +17 -0
- package/commands/orchestrate/phase-6-report.md +1 -1
- package/commands/quality-gate.md +51 -0
- package/commands/recover-orphans.md +3 -3
- package/commands/review-pr.md +87 -16
- package/commands/work-on/build/context.md +25 -4
- package/commands/work-on/build/validate.md +1 -0
- package/commands/work-on/build.md +26 -9
- package/commands/work-on/close.md +104 -40
- package/commands/work-on/decompose.md +13 -7
- package/commands/work-on/investigate.md +13 -7
- package/commands/work-on/remediate.md +15 -4
- package/commands/work-on/review.md +48 -19
- package/commands/work-on.md +62 -9
- package/docs/CODEX.md +12 -0
- package/docs/CONFIG.md +49 -1
- package/docs/RELEASE-NOTES.md +52 -0
- package/package.json +1 -1
- package/scripts/amplification-breaker.sh +89 -0
- package/scripts/amplification-breaker.test.sh +39 -0
- package/scripts/check-admission-parity.mjs +74 -1
- package/scripts/check-spec-markers.sh +67 -3
- package/scripts/classify-finding.sh +120 -0
- package/scripts/classify-finding.test.sh +74 -0
- package/scripts/close-dossier-no-main-commit.test.sh +139 -0
- package/scripts/forge-root.test.sh +84 -27
- package/scripts/lint-build-validate-files.sh +29 -0
- package/scripts/lint-build-validate-files.test.sh +25 -0
- package/scripts/lint-dispatch-prompt.test.sh +2 -1
- package/scripts/recovery-claim-live.sh +73 -0
- package/scripts/recovery-claim-live.test.sh +66 -0
- package/scripts/verify-phase-trail.sh +183 -4
- package/scripts/verify-phase-trail.test.sh +211 -1
|
@@ -80,6 +80,23 @@ fi
|
|
|
80
80
|
|
|
81
81
|
**Idempotency**: `gh issue close` on an already-closed issue succeeds as a no-op (no error) — safe to re-run this step on a resumed/compacted session without an extra pre-check.
|
|
82
82
|
|
|
83
|
+
### Step 5C.5: Remove the trail cache directory <!-- Added: forge#3206 -->
|
|
84
|
+
|
|
85
|
+
Defense in depth for the per-run `$TRAIL_CACHE_DIR` (see `phase-4-execution.md` "Cycle-start cache reset"). Phase 4 already calls `cleanup_trail_cache` on Termination, Step 4F and "Stopping the orchestrator"; this step guarantees removal when Phase 5 is reached by any other path. It is idempotent and a no-op when the directory is absent. Re-declare the path helper in the same Bash block (each Bash call is a fresh shell; never trust a pre-set `TRAIL_CACHE_DIR` before `rm -rf`):
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
trail_cache_dir() {
|
|
89
|
+
printf '%s' "${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/forge-trail-$(id -u)-$(printf '%s' "{GH_REPO}" | tr -c 'A-Za-z0-9._-' '_')"
|
|
90
|
+
}
|
|
91
|
+
cleanup_trail_cache() {
|
|
92
|
+
local D; D="$(trail_cache_dir)"
|
|
93
|
+
case "$D" in */forge-trail-*) ;; *) return 0 ;; esac
|
|
94
|
+
case "$D" in *..*) return 0 ;; esac
|
|
95
|
+
rm -rf -- "$D"; unset TRAIL_CACHE_DIR; return 0
|
|
96
|
+
}
|
|
97
|
+
cleanup_trail_cache
|
|
98
|
+
```
|
|
99
|
+
|
|
83
100
|
### Step 5D: Report cleanup results
|
|
84
101
|
|
|
85
102
|
Include the cleanup summary in the final report (Phase 6), including whether the coordination issue was closed this run (see Step 5C). If cleanup found problems, call them out — they indicate agent pipeline failures that may need investigation.
|
|
@@ -130,7 +130,7 @@ for NUM in {all_batch_issue_numbers}; do
|
|
|
130
130
|
|
|
131
131
|
TITLE=$(gh issue view "$NUM" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "")
|
|
132
132
|
GATING_PRED=$(gh api repos/{GH_REPO}/issues/${NUM}/comments \
|
|
133
|
-
--jq '[.[] | select(.body | contains("FORGE:BLOCKED_ON_HUMAN_MERGE"))] | last | (.body | capture("Gating predecessor\\*\\*: #(?<p>[0-9]+)").p) // ""' 2>/dev/null || echo "")
|
|
133
|
+
--jq '[.[] | select((.body // "") | contains("FORGE:BLOCKED_ON_HUMAN_MERGE"))] | last | ((.body // "") | capture("Gating predecessor\\*\\*: #(?<p>[0-9]+)").p) // ""' 2>/dev/null || echo "")
|
|
134
134
|
GATING_PR=""
|
|
135
135
|
if [ -n "$GATING_PRED" ]; then
|
|
136
136
|
GATING_PR=$(gh pr list -R {GH_REPO} --state open --search "\"Closes #${GATING_PRED}\" in:body" \
|
package/commands/quality-gate.md
CHANGED
|
@@ -104,6 +104,7 @@ Before running checks, classify the changed files into domains. This avoids runn
|
|
|
104
104
|
| `*.py` files with new module-level `dict`, `set`, `Counter`, `Lock`, or `defaultdict` assignments | STATE |
|
|
105
105
|
| `*.py` in `services/worker/`, `infra/`, or `browser/` with new assignments to variables containing `MB`, `SIZE`, `MAX`, `LIMIT`, `THRESHOLD`, or `TIMEOUT` | CAPACITY *(scraping pack — opt-in, see 2K)* |
|
|
106
106
|
| `*.yml` in `.github/workflows/` | WORKFLOW |
|
|
107
|
+
| ANY diff (universal — every changed file set) | COVERAGE_REDUCTION *(see 2U)* <!-- Added: forge#3257 --> |
|
|
107
108
|
| `*.py` with new `from app.` import statements added outside `try:` blocks | IMPORT_RESOLUTION |
|
|
108
109
|
| `Dockerfile*` or `entrypoint*.sh` with added/changed `USER`, `su-exec`, `gosu`, or `setuid` | INFRA |
|
|
109
110
|
| `*.py` in `routers/` where the diff removes a line containing an or-gate condition (lines starting with `-` containing `if.*or`) | ROUTER_BUG |
|
|
@@ -158,6 +159,9 @@ for f in "${CHANGED_FILES_ARR[@]}"; do
|
|
|
158
159
|
esac
|
|
159
160
|
done
|
|
160
161
|
|
|
162
|
+
# COVERAGE_REDUCTION is universal: a deleted test or workflow step can ride along with any file set (see 2U)
|
|
163
|
+
DOMAINS="$DOMAINS COVERAGE_REDUCTION"
|
|
164
|
+
|
|
161
165
|
# Check for asyncio concurrency patterns in Python files
|
|
162
166
|
grep -lE "asyncio\.shield|asyncio\.wait_for|Task\.cancel" {CHANGED_FILES} 2>/dev/null | grep -qE '\.py$' && DOMAINS="$DOMAINS CONCURRENCY"
|
|
163
167
|
|
|
@@ -274,6 +278,7 @@ the gate rather than leaving the workflow blocked.
|
|
|
274
278
|
- **2R.5 (Adaptive gate.d checks)**: ALWAYS run when `adaptive_scripts.enabled` — executes `.forgedock/scripts/gate.d/*.sh`; each script is repo-specific and was promoted from a recurring finding pattern <!-- Added: forge#1739 -->
|
|
275
279
|
- **2G.8 (Wire-through proof)**: Run if `WIRE_THROUGH` in DOMAINS — demands each newly added conditional path be demonstrably reachable <!-- Added: forge#1731 -->
|
|
276
280
|
- **2G.9 (Danger-zone recurrence)**: Run if `FORGE_GRAPH` in DOMAINS — cross-references injected danger-zone rule cards from FORGE:CONTEXT against the diff; violations tagged `known-pattern-recurrence` (HIGH) <!-- Added: forge#1744 -->
|
|
281
|
+
- **2U (Coverage-reduction check)**: ALWAYS run (`COVERAGE_REDUCTION` in DOMAINS) — flags diffs that delete test files, remove test cases, add skip markers, or remove/disable workflow test steps <!-- Added: forge#3257 -->
|
|
277
282
|
- **2S (Test failure classification)**: Run if any Step 2 check invoked a test suite and it failed — classifies the failure as PRE_BROKEN, FLAKY, or REAL using `scripts/flaky-quarantine.sh` <!-- Added: forge#1336 -->
|
|
278
283
|
- **2T (Subscribed pattern card rules)**: ALWAYS run when `pattern_feeds.enabled` is true — injects Tier-B (warning-only) learned rules from subscribed exchange cards that have a `gate.d` check template matching any changed file's stack <!-- Added: forge#1746 -->
|
|
279
284
|
|
|
@@ -1298,6 +1303,51 @@ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
|
1298
1303
|
|
|
1299
1304
|
**Note**: 2Q-3 (stdlib logging check) fires on ALL changed Python files regardless of path — stdlib logger misuse is not specific to billing paths and is a universal crash class. 2Q-1 and 2Q-2 fire only on billing-domain files.
|
|
1300
1305
|
|
|
1306
|
+
### 2U: Coverage-reduction check (ALWAYS — a red check must be fixed, not silenced)
|
|
1307
|
+
|
|
1308
|
+
<!-- Added: forge#3257 -->
|
|
1309
|
+
|
|
1310
|
+
**Bug class**: a change turns a failing check green by shrinking what is checked (deleting a test file, dropping a test case, adding a skip marker, or removing/disabling a workflow test step) instead of fixing the code under test. Coverage silently disappears and nothing downstream notices.
|
|
1311
|
+
|
|
1312
|
+
Run against the diff versus the merge-base of `origin/{PR_BASE}` and `HEAD` (`PR_BASE` defaults to `staging`), so changes merged to the base after the branch point are not misread as PR-side deletions. A missing base ref is an error finding, never a silent pass. Use `--find-renames` so a rename is NOT treated as a deletion. Bash must stay portable (no `grep -P`, no GNU-only flags).
|
|
1313
|
+
|
|
1314
|
+
```bash
|
|
1315
|
+
cd {WORKTREE_PATH}
|
|
1316
|
+
PR_BASE="${PR_BASE:-staging}"
|
|
1317
|
+
if ! git rev-parse --verify --quiet "origin/$PR_BASE^{commit}" >/dev/null; then
|
|
1318
|
+
echo "COVERAGE-ERR | HIGH | quality-gate | base ref origin/$PR_BASE not found — fetch it; coverage-reduction check could not run"
|
|
1319
|
+
else
|
|
1320
|
+
BASE_REF=$(git merge-base "origin/$PR_BASE" HEAD) || { echo "COVERAGE-ERR | HIGH | quality-gate | merge-base failed for origin/$PR_BASE"; BASE_REF=""; }
|
|
1321
|
+
fi
|
|
1322
|
+
if [ -n "${BASE_REF:-}" ]; then
|
|
1323
|
+
TEST_PATH_RE='(^|/)(test_[^/]*|[^/]*_test\.[^/]*|[^/]*\.test\.[^/]*|[^/]*\.spec\.[^/]*)$|(^|/)(tests|__tests__)/'
|
|
1324
|
+
|
|
1325
|
+
# (a) deleted test files (renames excluded via --find-renames)
|
|
1326
|
+
git diff --find-renames --diff-filter=D --name-only "$BASE_REF" | grep -E "$TEST_PATH_RE" \
|
|
1327
|
+
&& echo "COVERAGE-1: deleted test file(s)"
|
|
1328
|
+
|
|
1329
|
+
# (b) removed test cases with no added counterpart, (c) newly added skip markers
|
|
1330
|
+
git diff -U0 "$BASE_REF" -- . | grep -E '^-[[:space:]]*(it|test|describe)\(|^-[[:space:]]*(async )?def test_' \
|
|
1331
|
+
&& echo "COVERAGE-1: removed test case line(s) — confirm each has an added counterpart in the diff"
|
|
1332
|
+
git diff -U0 "$BASE_REF" -- . | grep -E '^\+.*(\.skip\(|@pytest\.mark\.skip|@pytest\.mark\.xfail|xfail|continue-on-error:[[:space:]]*true|if:[[:space:]]*false)' \
|
|
1333
|
+
&& echo "COVERAGE-1: newly added skip/xfail/continue-on-error/if: false marker"
|
|
1334
|
+
|
|
1335
|
+
# (d) removed or disabled workflow test steps
|
|
1336
|
+
git diff -U0 "$BASE_REF" -- .github/workflows | grep -E '^-.*run:.*(test|pytest|jest|vitest|mocha|\.test\.sh|npm t|go test|cargo test)' \
|
|
1337
|
+
&& echo "COVERAGE-1: removed workflow test step"
|
|
1338
|
+
fi
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
Also treat a test command that is commented out (`+#` of a previously live `run:` test line) as a removal.
|
|
1342
|
+
|
|
1343
|
+
**Finding**: a `COVERAGE-ERR` line (base ref missing) is a HIGH finding returned as-is — never treat it as clean. Otherwise emit `COVERAGE-1 | HIGH | {file} | coverage reduction: {removed test | deleted test | disabled workflow test step} — restore it and fix the code under test`. Scan results are candidates, not verdicts: confirm each by reading the diff hunk before reporting.
|
|
1344
|
+
|
|
1345
|
+
**Exemption (only one)**: a legitimate deletion is allowed ONLY when the commit message or PR body gives an explicit justification tying the removal to the removal of the tested code (the code the test covered is also deleted in this diff). "Flaky", "failing", "Windows-only", or "to get CI green" are NOT justifications. When exempted, record the justification in the finding as a LOW (advisory, not returned).
|
|
1346
|
+
|
|
1347
|
+
**Fix rule**: a COVERAGE-1 finding is fixed by restoring the test or step and fixing the cause — never by suppressing the finding or re-wording the diff. If the cause cannot be fixed in scope, the caller escalates (`needs-human`) naming the failing tests/assertions.
|
|
1348
|
+
|
|
1349
|
+
---
|
|
1350
|
+
|
|
1301
1351
|
### 2R: Registry checks (ALWAYS — promoted patterns from recurring review findings)
|
|
1302
1352
|
|
|
1303
1353
|
<!-- Added: forge#1331 -->
|
|
@@ -1698,6 +1748,7 @@ OR:
|
|
|
1698
1748
|
6. SHELL-1 | HIGH | deploy.sh | Hardcoded 'localhost' in DB connection function — use ${POSTGRES_HOST:-localhost} pattern
|
|
1699
1749
|
7. INFRA-1 | HIGH | Dockerfile | Filesystem write ops found under named volume mount — add chown before privilege drop in entrypoint
|
|
1700
1750
|
8. ROUTER-1 | HIGH | routers/handler.py | Residual unfixed gate condition pattern found in sibling router file routers/other_handler.py:N — fix may be incomplete across all callers
|
|
1751
|
+
9. COVERAGE-1 | HIGH | .github/workflows/ci.yml | coverage reduction: removed workflow test step — restore it and fix the code under test
|
|
1701
1752
|
```
|
|
1702
1753
|
|
|
1703
1754
|
**Severity classification:**
|
|
@@ -317,7 +317,7 @@ done
|
|
|
317
317
|
|
|
318
318
|
For each diagnosed issue, apply the recovery action. All mutating actions are skipped when `DRY_RUN=true`.
|
|
319
319
|
|
|
320
|
-
**Claim before re-entering the pipeline** (forge#3158): the `review-pr` first-refusal resume and `create-pr` re-enter `/work-on` inline. A repeated or concurrent sweep, or a live `/orchestrate` agent, could otherwise run the pipeline twice on one issue. Every inline `/work-on` resume MUST go through `claim_orphan` first and run inside `resume_orphan_inline`, which releases the claim on every exit. A sweep is not an orchestrator, so it takes an issue-scoped `FORGE:RECOVERY_CLAIM` marker (visible to other sweeps) and defers to any live orchestrator signal (a fresh `FORGE:HEARTBEAT`, which `/work-on --under-orchestration` posts at every phase entry).
|
|
320
|
+
**Claim before re-entering the pipeline** (forge#3158): the `review-pr` first-refusal resume and `create-pr` re-enter `/work-on` inline. A repeated or concurrent sweep, or a live `/orchestrate` agent, could otherwise run the pipeline twice on one issue. Every inline `/work-on` resume MUST go through `claim_orphan` first and run inside `resume_orphan_inline`, which releases the claim on every exit. A sweep is not an orchestrator, so it takes an issue-scoped `FORGE:RECOVERY_CLAIM` marker (visible to other sweeps) and defers to any live orchestrator signal (a fresh `FORGE:HEARTBEAT`, which `/work-on --under-orchestration` posts at every phase entry). The claim is honored in the other direction too (forge#3172): `/work-on` Phase 0A.4 and `/orchestrate` Phase 4 dispatch consult the shared `scripts/recovery-claim-live.sh` and defer to a live claim, so the inline resume passes `--recovery-sweep ${SWEEP_ID}` to exempt its own claim.
|
|
321
321
|
|
|
322
322
|
**Claim refresh and guaranteed release** (forge#3171): liveness is judged from the claim comment's `updated_at`, and an inline resume posts no `FORGE:HEARTBEAT` (that is gated on `--under-orchestration`, which sweeps do not pass). So the claim is kept alive by editing it: `claim_orphan` records the claim comment id, and `start_claim_keepalive` runs `refresh_orphan` in a background loop every `RECOVERY_CLAIM_TTL_MIN / 3` minutes while the resume runs. `refresh_orphan` PATCHes the claim body, keeping the `**Sweep: id**` line and appending a changing `Refreshed: <UTC>` line, so `updated_at` always advances. Refresh is best-effort; if the keepalive is lost the claim degrades to the plain TTL behavior. `resume_orphan_inline` registers `trap ... release_orphan ... EXIT` in a subshell, so the claim is released always, on success and when the `Skill(...)` call fails, and `release_orphan` stops the keepalive first and is idempotent (it never posts the release marker twice). The keepalive and the `Skill(...)` call belong to the same sweep run. DRY_RUN makes every one of these helpers a no-op.
|
|
323
323
|
|
|
@@ -423,7 +423,7 @@ resume_orphan_inline() {
|
|
|
423
423
|
trap 'release_orphan "$num"' EXIT
|
|
424
424
|
start_claim_keepalive "$num"
|
|
425
425
|
refresh_orphan "$num"
|
|
426
|
-
Skill(skill="{FORGE_SKILL_PREFIX}work-on", args="${num}")
|
|
426
|
+
Skill(skill="{FORGE_SKILL_PREFIX}work-on", args="${num} --recovery-sweep ${SWEEP_ID}")
|
|
427
427
|
)
|
|
428
428
|
}
|
|
429
429
|
```
|
|
@@ -590,7 +590,7 @@ See the latest FORGE:PHASE_TRAIL_FAILED comment for the missing phases. Re-run e
|
|
|
590
590
|
# Branch has commits but no PR — resume /work-on to create PR
|
|
591
591
|
echo " Applying create-pr: resuming /work-on to advance from build to PR creation"
|
|
592
592
|
if [ "$DRY_RUN" = "true" ]; then
|
|
593
|
-
echo " [DRY-RUN] Would: Skill(skill='{FORGE_SKILL_PREFIX}work-on', args='$NUM')"
|
|
593
|
+
echo " [DRY-RUN] Would: Skill(skill='{FORGE_SKILL_PREFIX}work-on', args='$NUM --recovery-sweep $SWEEP_ID')"
|
|
594
594
|
RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | create-pr | Would resume /work-on — branch $BRANCH has commits, no PR |\n"
|
|
595
595
|
elif claim_orphan "$NUM"; then
|
|
596
596
|
resume_orphan_inline "$NUM" # releases the claim on every exit
|
package/commands/review-pr.md
CHANGED
|
@@ -25,7 +25,7 @@ allowed-tools: Task, Agent, Bash, Read, Grep, Glob, WebFetch, Skill
|
|
|
25
25
|
|
|
26
26
|
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.
|
|
27
27
|
|
|
28
|
-
3. **Review findings do NOT block merge UNLESS they meet the Blocking Criteria in §7B** (a CONFIRMED HIGH/CRITICAL finding, a purpose regression, a merge conflict, or a build/type/test failure) **or the calibration threshold check in §7B.5 sets `CALIBRATION_NEEDS_HUMAN=true`** (HIGH-confidence task type with historical survival < 80%). File every finding that survives the §6B.5 note disposition (
|
|
28
|
+
3. **Review findings do NOT block merge UNLESS they meet the Blocking Criteria in §7B** (a CONFIRMED HIGH/CRITICAL finding, a purpose regression, a merge conflict, or a build/type/test failure) **or the calibration threshold check in §7B.5 sets `CALIBRATION_NEEDS_HUMAN=true`** (HIGH-confidence task type with historical survival < 80%). File every finding that survives the §6B.5 note disposition (`scripts/classify-finding.sh`: HIGH+ always; MEDIUM CONFIRMED/LIKELY; LOW/POSSIBLE only when security/billing content matches; stricter on review-finding lineage) as a GitHub issue with the `review-finding` label. LOW/POSSIBLE notes are fixed in-PR, listed in the PR body, or dropped — never filed as standalone issues. Minor/style findings never block; §7B's and §7B.5's blocking conditions always do — including under `--auto-merge`. <!-- forge#1741 -->
|
|
29
29
|
|
|
30
30
|
4. **Route correctly at Phase 0.** If the input is "staging" or the PR targets `main`, invoke `Skill("{FORGE_SKILL_PREFIX}review-pr-staging", ...)` — do NOT run the standard PR review pipeline against a staging→main PR.
|
|
31
31
|
|
|
@@ -1657,7 +1657,7 @@ If synthesis needed, launch a `general-purpose` Task (model: `"{SUBAGENT_MODEL}"
|
|
|
1657
1657
|
|
|
1658
1658
|
## Phase 6: Finding Triage & Issue Creation (MANDATORY)
|
|
1659
1659
|
|
|
1660
|
-
**STOP. DO NOT skip this phase. DO NOT post summary first.** Every finding MUST become a GitHub issue BEFORE the summary.
|
|
1660
|
+
**STOP. DO NOT skip this phase. DO NOT post summary first.** Every finding that §6B.5 classifies as ISSUE MUST become a GitHub issue BEFORE the summary; every NOTE gets a recorded disposition instead (§6B.5).
|
|
1661
1661
|
|
|
1662
1662
|
### 6A: Extract Findings
|
|
1663
1663
|
|
|
@@ -1689,13 +1689,29 @@ If still 0: review is clean — skip to Phase 7.
|
|
|
1689
1689
|
|
|
1690
1690
|
### 6B.5: Non-blocking note disposition (MANDATORY before 6C — forge#3060)
|
|
1691
1691
|
|
|
1692
|
-
Filing a standalone `review-finding` issue for every LOW/POSSIBLE reviewer note makes the cascade amplify (each merged fix PR spawns its own P3 issues, which spawn more). Classify each deduped finding before 6C:
|
|
1692
|
+
Filing a standalone `review-finding` issue for every LOW/POSSIBLE reviewer note makes the cascade amplify (each merged fix PR spawns its own P3 issues, which spawn more). Classify each deduped finding before 6C with the deterministic classifier — do NOT hand-classify when the script resolves:
|
|
1693
1693
|
|
|
1694
|
-
- **
|
|
1695
|
-
- **
|
|
1696
|
-
- **
|
|
1697
|
-
- **
|
|
1698
|
-
|
|
1694
|
+
- **ISSUE always**: `**Severity**: HIGH` or `CRITICAL`, or a missing/unparseable severity.
|
|
1695
|
+
- **Safety exemption is content-based**: a LOW or POSSIBLE finding is still filed when its file path / title / body matches the security/billing keyword set (`security|auth|authz|authn|billing|payment|stripe|charge|invoice|injection|xss|csrf|ssrf|idor|secret(s)|credential(s)|permission(s)|sql|token|password|redact`, whole words, `_`/`-`/`/` separate words — so `auth_service` matches but `author`/`tokenizer` do not), or when it came from a dedicated, signal-selected domain agent (Auth, Billing, Concurrency, Database). **Origin from the always-on General Security & Quality agent alone does NOT exempt a finding**: that agent runs on every PR, so origin-based exemption filed nearly every LOW note it raised (the 2026-10-08 cascade audit: 50 of 60 would-be notes in one batch were filed only for that reason).
|
|
1696
|
+
- **Review-finding lineage** (the PR's linked issue `MERGE_ISSUE` carries the `review-finding` label, any priority — this PR is itself a fix for a finding): only MEDIUM findings that are CONFIRMED, or LIKELY and safety-exempt, become issues; LOW and POSSIBLE are always NOTEs. A fix for a finding must not mint a new generation of findings.
|
|
1697
|
+
- **Otherwise**: `**Severity**: LOW`, or `**Confidence**: POSSIBLE` below HIGH, is a NOTE unless safety-exempt; everything else (MEDIUM CONFIRMED/LIKELY) is an ISSUE.
|
|
1698
|
+
|
|
1699
|
+
```bash
|
|
1700
|
+
# Resolve the classifier: running plugin first, then the resolved ForgeDock root, then the repo copy.
|
|
1701
|
+
CLASSIFY_SCRIPT=""
|
|
1702
|
+
for _cs in '${CLAUDE_PLUGIN_ROOT}/scripts/classify-finding.sh' "${FORGE_ROOT:+$FORGE_ROOT/scripts/classify-finding.sh}" "scripts/classify-finding.sh"; do
|
|
1703
|
+
case "$_cs" in /*|scripts/*) [ -f "$_cs" ] && { CLASSIFY_SCRIPT="$_cs"; break; } ;; esac
|
|
1704
|
+
done
|
|
1705
|
+
FINDING_LINEAGE="none"
|
|
1706
|
+
if [ -n "${MERGE_ISSUE:-}" ] && gh issue view "$MERGE_ISSUE" -R {GH_REPO} --json labels --jq '.labels[].name' 2>/dev/null | grep -qx 'review-finding'; then
|
|
1707
|
+
FINDING_LINEAGE="review-finding"
|
|
1708
|
+
fi
|
|
1709
|
+
# Per deduped finding (FINDING_TEXT_FILE = mktemp file holding title + body + affected paths):
|
|
1710
|
+
# DISPOSITION=$(bash "$CLASSIFY_SCRIPT" --severity "$FINDING_SEVERITY" --confidence "$FINDING_CONFIDENCE" \
|
|
1711
|
+
# --agent "$FINDING_AGENT" --lineage "$FINDING_LINEAGE" --text-file "$FINDING_TEXT_FILE")
|
|
1712
|
+
# case "$DISPOSITION" in ISSUE*) ;; NOTE*) ;; esac # first word is the class, the rest is the reason
|
|
1713
|
+
# If CLASSIFY_SCRIPT is empty, apply the four rules above by hand and record classifier=manual below.
|
|
1714
|
+
```
|
|
1699
1715
|
|
|
1700
1716
|
Each NOTE gets exactly one disposition, in this preference order:
|
|
1701
1717
|
1. **Fix in this PR** — when the fix is cheap (a few lines, same files already in the diff, no new behaviour) and in scope, apply it as a follow-up commit on the PR branch before merge — ONLY for comment, documentation or test-only changes. Any change to executable code is not a note fix: it must be filed as an issue or fixed with a full re-review of the new HEAD before merge (never push unreviewed code under `--auto-merge`).
|
|
@@ -1706,6 +1722,15 @@ Each NOTE gets exactly one disposition, in this preference order:
|
|
|
1706
1722
|
|
|
1707
1723
|
Every NOTE disposition must be recorded (never silently dropped): list each NOTE (id, file:line, one-line reason) with its disposition in the review summary comment, including dropped ones, so a reviewer can audit the decision. NOTES are never passed to `Skill(issue)`. Record counts in the review summary (`notes_fixed`, `notes_listed`, `notes_dropped`, `findings_filed`). If no finding is an ISSUE after this step, skip 6C and continue to Phase 7.
|
|
1708
1724
|
|
|
1725
|
+
**Disposition marker (MANDATORY whenever Phase 6A extracted at least one finding):** post one PR comment whose first line is the machine-readable record below, followed by the NOTE list. Phase 8 refuses to auto-merge a PR that has `FINDING:` markers but no `FORGE:NOTE_DISPOSITION` comment — the disposition step is enforced, not advisory.
|
|
1726
|
+
|
|
1727
|
+
```bash
|
|
1728
|
+
CLASSIFIER_MODE=$([ -n "$CLASSIFY_SCRIPT" ] && echo script || echo manual)
|
|
1729
|
+
# NOTE_LIST_FILE: mktemp file named for the PR number, holding one line per NOTE (id, file:line, disposition, reason).
|
|
1730
|
+
gh pr comment {PR_NUMBER} -R {GH_REPO} --body "<!-- FORGE:NOTE_DISPOSITION: notes_fixed=${NOTES_FIXED:-0} notes_listed=${NOTES_LISTED:-0} notes_dropped=${NOTES_DROPPED:-0} findings_filed=${FINDINGS_FILED:-0} lineage=${FINDING_LINEAGE} classifier=${CLASSIFIER_MODE} -->
|
|
1731
|
+
$(cat "$NOTE_LIST_FILE" 2>/dev/null)" # allowlist:check-command-side-effects
|
|
1732
|
+
```
|
|
1733
|
+
|
|
1709
1734
|
### 6C: Create Issues
|
|
1710
1735
|
|
|
1711
1736
|
```bash
|
|
@@ -2071,6 +2096,8 @@ Verdict determined by standard blocking criteria.
|
|
|
2071
2096
|
4. `MERGE_HEALTH == "CONFLICTING"` OR `MERGE_HEALTH_STATE` in {`DIRTY`, `BLOCKED`} — PR cannot be merged cleanly into its base branch <!-- Added: forge#194 -->
|
|
2072
2097
|
- Verdict: CHANGES REQUESTED. Message: "Merge conflict with `{base}`. Rebase `{head}` onto `origin/{base}`, resolve the conflicting files, then re-run /review-pr."
|
|
2073
2098
|
- If `MERGE_HEALTH == "UNKNOWN"` after retries: emit a WARNING in the verdict body (do NOT treat as a block — GitHub may still be computing it).
|
|
2099
|
+
5. A CONFIRMED coverage reduction (deleted test, removed test case, or disabled/removed workflow test step) in a PR that responds to a red check, or that has no justification tying it to removal of the tested code <!-- Added: forge#3257 -->
|
|
2100
|
+
- Verdict: CHANGES REQUESTED. Message: "Coverage reduction: a deleted test or removed workflow test step is not a fix for a red check. Restore it and fix the code under test, or escalate naming the failing assertions." Detection follows quality-gate 2U (`COVERAGE-1`); never dedup this finding against the PR's own issue.
|
|
2074
2101
|
|
|
2075
2102
|
```bash
|
|
2076
2103
|
# Determine if mergeability is a blocker (MERGE_HEALTH/MERGE_HEALTH_STATE set in Phase 1A; BASE/HEAD set in Phase 0 Mode 3)
|
|
@@ -2204,6 +2231,25 @@ fi
|
|
|
2204
2231
|
|
|
2205
2232
|
**Skip if** `AUTO_MERGE=false`.
|
|
2206
2233
|
|
|
2234
|
+
**Note-disposition preflight (MANDATORY before any merge attempt)**: §6B.5 is what bounds the review-finding cascade, so a review that extracted findings but never recorded their disposition must not auto-merge. Fail closed on an unreadable comment list.
|
|
2235
|
+
|
|
2236
|
+
```bash
|
|
2237
|
+
DISPO_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null) || DISPO_JSON=""
|
|
2238
|
+
if [ -z "$DISPO_JSON" ]; then
|
|
2239
|
+
echo "NOTE DISPOSITION: could not read PR comments — refusing to merge (fail closed)" >&2
|
|
2240
|
+
# STOP — return REVIEW_RESULT: status: BLOCKED, blocker: "note disposition unreadable".
|
|
2241
|
+
exit 1
|
|
2242
|
+
fi
|
|
2243
|
+
FINDING_COUNT=$(printf '%s' "$DISPO_JSON" | jq -s '[.[][] | select(.body | test("<!-- FINDING:"))] | length')
|
|
2244
|
+
DISPOSITION_COUNT=$(printf '%s' "$DISPO_JSON" | jq -s '[.[][] | select(.author_association == "OWNER" or .author_association == "MEMBER" or .author_association == "COLLABORATOR") | select(.body | test("^<!-- FORGE:NOTE_DISPOSITION"))] | length')
|
|
2245
|
+
if [ "${FINDING_COUNT:-0}" -gt 0 ] && [ "${DISPOSITION_COUNT:-0}" -eq 0 ]; then
|
|
2246
|
+
echo "NOTE DISPOSITION: ${FINDING_COUNT} finding comment(s) but no FORGE:NOTE_DISPOSITION record — run §6B.5 before merging" >&2
|
|
2247
|
+
gh pr comment {PR_NUMBER} {MERGE_GH_FLAG} --body "Auto-merge skipped: review findings exist but §6B.5 note disposition was not recorded. Re-run \`/review-pr {PR_NUMBER} --auto-merge --issue <N>\`." 2>/dev/null || true # allowlist:check-command-side-effects
|
|
2248
|
+
# STOP — return REVIEW_RESULT: status: BLOCKED, blocker: "note disposition missing".
|
|
2249
|
+
exit 1
|
|
2250
|
+
fi
|
|
2251
|
+
```
|
|
2252
|
+
|
|
2207
2253
|
**Phase-trail preflight (MANDATORY before any merge attempt)** <!-- Added: forge#3061 -->: a reviewer verdict alone must not merge work whose earlier pipeline phases were skipped. Run the deterministic verifier against the linked issue; do NOT run the merge block below unless it exits 0.
|
|
2208
2254
|
|
|
2209
2255
|
```bash
|
|
@@ -2216,14 +2262,20 @@ else
|
|
|
2216
2262
|
# Same resolution as work-on/review.md Phase R1.5: the verifier ships with ForgeDock, not the consumer repo.
|
|
2217
2263
|
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
2218
2264
|
FORGE_ROOT=""
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2265
|
+
# Windows drive-letter FORGEDOCK_HOME (C:/x or C:\x) is normalized to /c/x (cygpath when present); relative values stay rejected.
|
|
2266
|
+
_h="${FORGEDOCK_HOME:-}"; case "$_h" in [A-Za-z]:[/\\]*) _w="$_h"; _h="$(cygpath -u "$_w" 2>/dev/null || true)"; [ -n "$_h" ] || _h="/$(printf %s "$_w" | cut -c1 | tr 'A-Z' 'a-z')$(printf %s "${_w#??}" | tr '\\' '/')" ;; esac
|
|
2267
|
+
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
2268
|
+
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
2269
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
2270
|
+
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match). Every assignment ends in || true so the block survives set -e / pipefail.
|
|
2271
|
+
_l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null || true)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
|
|
2272
|
+
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
2273
|
+
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
2274
|
+
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
2275
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
2276
|
+
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
2225
2277
|
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
2226
|
-
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
|
|
2278
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
2227
2279
|
while IFS= read -r _c; do
|
|
2228
2280
|
case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
|
|
2229
2281
|
done <<< "$_k"
|
|
@@ -2247,7 +2299,20 @@ else
|
|
|
2247
2299
|
echo "PHASE TRAIL: verify-phase-trail.sh not resolvable (set FORGEDOCK_HOME to the ForgeDock install) — refusing to merge" >&2
|
|
2248
2300
|
TRAIL="PHASE_TRAIL: ERROR (verifier not resolvable)"; TRAIL_RC=127
|
|
2249
2301
|
else
|
|
2250
|
-
|
|
2302
|
+
# Bind the human break-glass override (forge#3152) to the PR head. An unreadable head omits the flag, so no override is offered (fails closed).
|
|
2303
|
+
PR_HEAD_SHA=$(gh api "repos/{GH_REPO}/pulls/{PR_NUMBER}" --jq .head.sha 2>/dev/null) || PR_HEAD_SHA=""
|
|
2304
|
+
HEAD_SHA_ARGS=()
|
|
2305
|
+
if [[ "$PR_HEAD_SHA" =~ ^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$ ]]; then HEAD_SHA_ARGS=(--head-sha "$PR_HEAD_SHA"); fi
|
|
2306
|
+
TRAIL=$(bash "$TRAIL_SCRIPT" "$MERGE_ISSUE" -R {GH_REPO} $DOCS_ONLY_FLAG $CODE_DIFF_FLAG ${HEAD_SHA_ARGS[@]+"${HEAD_SHA_ARGS[@]}"}); TRAIL_RC=$?
|
|
2307
|
+
if [ "$TRAIL_RC" -eq 0 ]; then
|
|
2308
|
+
# An `OVERRIDE:` line means the verifier accepted a human override. Record it on the PR from the verifier's output (never agent prose).
|
|
2309
|
+
OVR=$(printf '%s\n' "$TRAIL" | sed -n 's/^OVERRIDE: //p' | sed -n '1p')
|
|
2310
|
+
OVERRIDE_BOUND_HEAD=""
|
|
2311
|
+
if [ -n "$OVR" ]; then
|
|
2312
|
+
OVERRIDE_BOUND_HEAD="$PR_HEAD_SHA" # forge#3273: the merge below must land exactly this commit
|
|
2313
|
+
gh pr comment {PR_NUMBER} -R {GH_REPO} --body "$(printf '<!-- FORGE:PHASE_TRAIL_OVERRIDE_APPLIED -->\nThe phase-trail gate was waived by a human break-glass override.\n\n%s\n%s\n%s\n' '```' "$OVR" '```')" # <!-- allowlist:check-command-side-effects -->
|
|
2314
|
+
fi
|
|
2315
|
+
fi
|
|
2251
2316
|
fi
|
|
2252
2317
|
if [ "$TRAIL_RC" -ge 2 ]; then
|
|
2253
2318
|
# rc 2 (trail unreadable) / 127 (verifier not resolvable) is an infrastructure failure, NOT missing phases:
|
|
@@ -2407,6 +2472,12 @@ else
|
|
|
2407
2472
|
fi
|
|
2408
2473
|
fi
|
|
2409
2474
|
fi
|
|
2475
|
+
# WIRE:PROVEN — spec prose; OVERRIDE_BOUND_HEAD is set in the verifier block above and consumed here (reviewed by trace)
|
|
2476
|
+
# forge#3273: an accepted break-glass override is bound to one exact head. If the commit being merged
|
|
2477
|
+
# differs (a push after the verifier ran, even a docs-only one), the override does not cover it.
|
|
2478
|
+
if [ "$CI_GATE_RC" -eq 0 ] && [ -n "${OVERRIDE_BOUND_HEAD:-}" ] && [ "$GATED_HEAD" != "$OVERRIDE_BOUND_HEAD" ]; then
|
|
2479
|
+
CI_GATE_RC=4; CI_GATE_OUT="STALE_REVIEW: break-glass override was bound to ${OVERRIDE_BOUND_HEAD:0:7} but the gated head is ${GATED_HEAD:0:7}"
|
|
2480
|
+
fi
|
|
2410
2481
|
if [ "$CI_GATE_RC" -eq 4 ]; then
|
|
2411
2482
|
# Not a human problem: the caller re-runs /review-pr on the new head (work-on/review.md R4).
|
|
2412
2483
|
echo "REVIEW_RESULT: status: BLOCKED, blocker: stale review — ${CI_GATE_OUT}; re-run /review-pr on the new head"
|
|
@@ -861,6 +861,7 @@ CONTEXT_BODY=$(node packages/protocol/src/cli.js emit CONTEXT)
|
|
|
861
861
|
Post the following as a GitHub comment on `{NUMBER}`:
|
|
862
862
|
|
|
863
863
|
```bash
|
|
864
|
+
# DRY_RUN: when DRY_RUN=true, print this body instead of running the command below.
|
|
864
865
|
gh issue comment {NUMBER} -R {GH_REPO} --body "<!-- FORGE:CONTEXT -->
|
|
865
866
|
## Implementation Context for #{NUMBER}
|
|
866
867
|
|
|
@@ -948,13 +949,33 @@ Skip the context gathering (print `CONTEXT_RESULT:` with `status: SKIPPED`) if:
|
|
|
948
949
|
**Minimal marker on every non-TRIVIAL skip (forge#3123)**: for STANDARD/COMPLEX the phase-trail gate (`scripts/verify-phase-trail.sh`) requires a `FORGE:CONTEXT` marker, so a skip must still post an empty marker comment (idempotent: skip if a `FORGE:CONTEXT` comment already exists), otherwise a re-run can never satisfy the gate:
|
|
949
950
|
|
|
950
951
|
```bash
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
952
|
+
# Anchored match mirrors scripts/verify-phase-trail.sh (marker must open the comment) AND its
|
|
953
|
+
# trusted-author predicate (author_association in FORGE_TRAIL_TRUSTED_ASSOCIATIONS, Bot user, or login in
|
|
954
|
+
# FORGE_TRAIL_TRUSTED_LOGINS): an untrusted commenter's pre-posted marker must not suppress the skip
|
|
955
|
+
# marker, because the gate ignores it and would then fail closed on every re-run (forge#3289).
|
|
956
|
+
# A failed lookup sets LOOKUP_FAILED and posts nothing (unknown is not zero): the
|
|
957
|
+
# phase-trail gate then flags the missing marker and a re-run retries.
|
|
958
|
+
LOOKUP_FAILED=0
|
|
959
|
+
TRUSTED_ASSOC="${FORGE_TRAIL_TRUSTED_ASSOCIATIONS-OWNER,MEMBER,COLLABORATOR}"
|
|
960
|
+
TRUSTED_LOGINS="${FORGE_TRAIL_TRUSTED_LOGINS-}"
|
|
961
|
+
EXISTING_JSON=$(set -o pipefail; gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate | jq -r \
|
|
962
|
+
--arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" '
|
|
963
|
+
($assoc | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $A
|
|
964
|
+
| ($logins | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $L
|
|
965
|
+
| [.[]
|
|
966
|
+
| select((.body // "") | startswith("<!-- FORGE:CONTEXT -->"))
|
|
967
|
+
| select(
|
|
968
|
+
((.author_association // "") as $x | $A | index($x) != null)
|
|
969
|
+
or ((.user.type // "") == "Bot")
|
|
970
|
+
or ((.user.login // "") as $x | $L | index($x) != null)
|
|
971
|
+
)] | length') || LOOKUP_FAILED=1
|
|
972
|
+
EXISTING_CONTEXT=$(printf '%s\n' "$EXISTING_JSON" | awk '{s+=$1} END {print s+0}')
|
|
954
973
|
if [ "${DRY_RUN:-false}" = "true" ]; then
|
|
955
974
|
echo "[DRY_RUN] would post minimal FORGE:CONTEXT marker on #{NUMBER}"
|
|
975
|
+
elif [ "$LOOKUP_FAILED" -eq 1 ]; then
|
|
976
|
+
echo "WARNING: FORGE:CONTEXT lookup failed (LOOKUP_FAILED=1) — not posting a marker; re-run to retry" >&2
|
|
956
977
|
elif [ "$EXISTING_CONTEXT" -eq 0 ]; then
|
|
957
|
-
gh issue comment
|
|
978
|
+
gh issue comment {NUMBER} -R {GH_REPO} --body "<!-- FORGE:CONTEXT -->
|
|
958
979
|
## Implementation Context for #{NUMBER}
|
|
959
980
|
|
|
960
981
|
Context gathering skipped: {REASON — config/docs-only edit | new files, no git history | empty affected-file list}. No institutional memory to surface.
|
|
@@ -127,6 +127,7 @@ Skill("{FORGE_SKILL_PREFIX}quality-gate", args="{CHANGED_FILES} --worktree {WORK
|
|
|
127
127
|
- Re-run after EVERY fix pass — never trust that fixes resolved findings without verification
|
|
128
128
|
- Each iteration re-scans ALL changed files — fixes can introduce new issues
|
|
129
129
|
- Only HIGH and MEDIUM findings must be fixed; LOW findings are advisory only
|
|
130
|
+
- `COVERAGE-1 | HIGH` findings (quality-gate 2U coverage reduction) are fixed by restoring the deleted/skipped test or workflow step — never by `--no-verify` or suppression <!-- Added: forge#3257 -->
|
|
130
131
|
- `TEST-QUARANTINE | LOW` findings (pre-broken or flaky tests classified by Step 2R) do **not** require fixing and do **not** count toward gate failure — include them in the V5 commit comment for reviewer visibility
|
|
131
132
|
- If the `quality-gate` skill is not found under either name, STOP and print `VALIDATE_RESULT` with `gate_passed: false`, `blocker: skill not found: quality-gate` — never run the gate inline
|
|
132
133
|
|
|
@@ -24,6 +24,8 @@ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --base {PR_BAS
|
|
|
24
24
|
|
|
25
25
|
**CRITICAL: You MUST execute ALL phases B0–B6.5 in order. Every child (B3 context, B4 architect, B5 implement, B6 validate) is invoked via `Skill(...)` — each is a forked sub-skill with its own isolated context. B3 and B4 are invoked for every complexity band except where the band is TRIVIAL or INVESTIGATION (B2/B2.1/B2.5 are also skipped for INVESTIGATION); the children post their own skip markers, so a skipped child is still visible on the issue. Skipping a child without the band justification degrades build quality and fails the phase-trail check.**
|
|
26
26
|
|
|
27
|
+
**Synchronous child consumption**: each child `Skill(...)` call (B3-B6) runs to completion in this phase's own turn, and you consume its result in that same turn. Never end or yield your turn to wait for a child, and never wait for a completion notification: notifications for forked children go to the root session, not to this phase, so a turn that yields is never resumed. A child return with no `*_RESULT:` block (running, backgrounded, empty) is not a result — re-read the GitHub markers for that child (`FORGE:CONTEXT`, `FORGE:ARCHITECT`, `FORGE:BUILDER`, `FORGE:VALIDATE`/`FORGE:QUALITY_GATE`) and apply the **bounded re-invoke rule**: re-invoke the same child with the same args at most 2 times per child on running/backgrounded/empty returns (the counter is per child and is not reset by a re-invoke). Before re-invoking, check state: if the child's marker is present, consume it instead of re-invoking; for implement and validate, also check the worktree (`git status --short` and `git log origin/{PR_BASE}..HEAD` in `{WORKTREE_PATH}`) — uncommitted in-flight edits or a fresh commit mean the original child may still be live, so wait for it by re-reading state rather than launching a second writer on the same worktree, and count that as a re-invoke attempt. After the cap is exhausted, take the Blocked exit (see Result emission and Blocked exit) with `blocker: child-stalled: <child>` (child = context|architect|implement|validate); it is terminal and is not re-invoked by the router.
|
|
28
|
+
|
|
27
29
|
<!-- FORGE:SPEC_LOADED — work-on/build.md loaded and active. Agent is bound by this spec. -->
|
|
28
30
|
|
|
29
31
|
---
|
|
@@ -69,14 +71,20 @@ fi
|
|
|
69
71
|
# ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
|
|
70
72
|
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
71
73
|
FORGE_ROOT=""
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
# Windows drive-letter FORGEDOCK_HOME (C:/x or C:\x) is normalized to /c/x (cygpath when present); relative values stay rejected.
|
|
75
|
+
_h="${FORGEDOCK_HOME:-}"; case "$_h" in [A-Za-z]:[/\\]*) _w="$_h"; _h="$(cygpath -u "$_w" 2>/dev/null || true)"; [ -n "$_h" ] || _h="/$(printf %s "$_w" | cut -c1 | tr 'A-Z' 'a-z')$(printf %s "${_w#??}" | tr '\\' '/')" ;; esac
|
|
76
|
+
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
77
|
+
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
78
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
79
|
+
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match). Every assignment ends in || true so the block survives set -e / pipefail.
|
|
80
|
+
_l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null || true)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
|
|
81
|
+
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
82
|
+
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
83
|
+
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
84
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
85
|
+
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
78
86
|
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
79
|
-
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
|
|
87
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
80
88
|
while IFS= read -r _c; do
|
|
81
89
|
case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
|
|
82
90
|
done <<< "$_k"
|
|
@@ -466,6 +474,7 @@ If `FUNCTION_NAMES` is empty, omit `--functions`. The child posts its own `FORGE
|
|
|
466
474
|
- `status: COMPLETE | PARTIAL | SKIPPED` → continue to B4
|
|
467
475
|
- No `CONTEXT_RESULT:` block, timeout or error → log a warning and continue to B4 (context is advisory and non-blocking)
|
|
468
476
|
- Skill not found → Blocked exit, blocker "skill not found: work-on:build:context"
|
|
477
|
+
- Returned running/backgrounded/empty (no `CONTEXT_RESULT:`) → do not end the turn; re-read for a `FORGE:CONTEXT` marker, re-invoke the same child if absent, per the bounded re-invoke rule (at most 2; check the marker before re-invoking) (advisory rules above still apply once it returns)
|
|
469
478
|
# MUST CONTINUE to Phase B4 — context result is intermediate, NOT terminal.
|
|
470
479
|
|
|
471
480
|
---
|
|
@@ -485,6 +494,7 @@ The child posts its own `FORGE:ARCHITECT` comment (a "Skipped" marker + `:COMPLE
|
|
|
485
494
|
- `status: BLOCKED` (conflicting constraints that cannot be resolved) → Blocked exit with the child's `blocker`
|
|
486
495
|
- No `ARCHITECT_RESULT:` block → re-read the issue: if a `FORGE:ARCHITECT:COMPLETE` marker exists continue to B5, otherwise Blocked exit "architect produced no result"
|
|
487
496
|
- Skill not found → Blocked exit, blocker "skill not found: work-on:build:architect"
|
|
497
|
+
- Returned running/backgrounded/empty → do not end the turn to wait; re-read markers and re-invoke the same child per the bounded re-invoke rule (at most 2; the marker fallback above applies only after a real return)
|
|
488
498
|
# MUST CONTINUE to Phase B5 — architect result is intermediate, NOT terminal.
|
|
489
499
|
|
|
490
500
|
---
|
|
@@ -503,6 +513,7 @@ Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo
|
|
|
503
513
|
- `status: INVESTIGATION_COMPLETE` → issues were created as deliverables and the original closed; print `BUILD_RESULT: status: INVESTIGATION_COMPLETE` (skip B6/B6.5)
|
|
504
514
|
- `status: BLOCKED` → Blocked exit with the child's `blocker`
|
|
505
515
|
- Skill not found → Blocked exit, blocker "skill not found: work-on:build:implement"
|
|
516
|
+
- Returned running/backgrounded/empty (no `IMPLEMENT_RESULT:`) → do not end the turn to wait; re-read the `FORGE:BUILDER` comment and the worktree state; if `FORGE:BUILDER:COMPLETE` is present consume it, and if the worktree shows in-flight edits or a fresh commit treat the child as possibly live; before re-invoking, apply the bounded re-invoke rule (at most 2, then `child-stalled: implement`)
|
|
506
517
|
# MUST CONTINUE to Phase B6 — implement result is intermediate, NOT terminal (validation still required).
|
|
507
518
|
|
|
508
519
|
---
|
|
@@ -522,9 +533,10 @@ CHANGED_FILES=$(git -C "{WORKTREE_PATH}" diff --name-only "origin/{PR_BASE}...HE
|
|
|
522
533
|
```
|
|
523
534
|
|
|
524
535
|
**After subcommand returns** (read its `VALIDATE_RESULT:` block):
|
|
525
|
-
- `gate_passed: true` → verify the `FORGE:QUALITY_GATE` marker exists on the issue (posted by validate V5; docs-only changes exempt). If absent, re-invoke validate once; a missing marker is not a pass. Then continue to Phase B6.5 (acceptance gate)
|
|
536
|
+
- `gate_passed: true` → verify the `FORGE:QUALITY_GATE` marker exists on the issue (posted by validate V5; docs-only changes exempt). If absent, re-derive `{CHANGED_FILES}` with the B6 command above and re-invoke validate once with the full B6 `Skill(...)` call, including `--files "{CHANGED_FILES}"` (never omit `--files`; an empty list falls back to the git diff); a missing marker is not a pass. Then continue to Phase B6.5 (acceptance gate)
|
|
526
537
|
- `gate_passed: false` → the subcommand has already posted its comment and added `needs-human`; print `BUILD_RESULT: status: BLOCKED` with the child's `blocker` (run the Blocked exit if no comment was posted)
|
|
527
538
|
- Skill not found → Blocked exit, blocker "skill not found: work-on:build:validate"
|
|
539
|
+
- Returned running/backgrounded/empty (no `VALIDATE_RESULT:`) → do not end the turn to wait; re-read the `FORGE:QUALITY_GATE` marker (consume it if present) and the worktree state before re-invoking, then re-invoke the same child under the bounded re-invoke rule (at most 2, then `child-stalled: validate`); this cap is separate from the single marker-absent re-invoke above
|
|
528
540
|
|
|
529
541
|
---
|
|
530
542
|
|
|
@@ -683,7 +695,12 @@ If `GATE_PASS = false`, the build repairs itself **once** before anything escala
|
|
|
683
695
|
1. Count `<!-- FORGE:ACCEPTANCE_REPAIR: issue={NUMBER} -->` comments on the issue. If one already exists, skip to step 4.
|
|
684
696
|
2. Post that marker (with the failed check ids), remove `needs-human` if this gate added it, then invoke
|
|
685
697
|
`Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE} --fix-acceptance \"<failed check ids and details>\"")`
|
|
686
|
-
|
|
698
|
+
then re-derive `{CHANGED_FILES}` from the repaired worktree (never reuse the pre-repair value, and never omit `--files`; an empty list falls back to this diff):
|
|
699
|
+
```bash
|
|
700
|
+
CHANGED_FILES=$(git -C "{WORKTREE_PATH}" diff --name-only "origin/{PR_BASE}...HEAD" 2>/dev/null | tr '\n' ' ' | xargs)
|
|
701
|
+
```
|
|
702
|
+
and invoke validate explicitly (the new code gets a fresh quality gate and commit):
|
|
703
|
+
`Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:validate", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE} --files \"{CHANGED_FILES}\"")`
|
|
687
704
|
3. Re-run this whole B6.5 gate. PASS → continue to the checkpoint.
|
|
688
705
|
4. Still failing (or already repaired once) → leave `needs-human` and print `BUILD_RESULT: status: BLOCKED`, blocker: "Acceptance gate failed after one repair — see FORGE:ACCEPTANCE_GATE comment".
|
|
689
706
|
|