forgedock 1.8.4 → 1.9.1
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 +359 -111
- package/commands/orchestrate/phase-5-cleanup.md +17 -0
- package/commands/orchestrate/phase-6-report.md +1 -1
- package/commands/quality-gate.md +80 -0
- package/commands/recover-orphans.md +3 -3
- package/commands/review-pr.md +98 -16
- package/commands/work-on/build/context.md +25 -4
- package/commands/work-on/build/validate.md +16 -9
- 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 +61 -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-bash.sh +120 -0
- package/scripts/check-spec-bash.test.sh +90 -0
- package/scripts/check-spec-markers.sh +67 -3
- package/scripts/classify-finding.sh +123 -0
- package/scripts/classify-finding.test.sh +77 -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
|
|
|
@@ -273,7 +277,9 @@ the gate rather than leaving the workflow blocked.
|
|
|
273
277
|
- **2R (Registry checks)**: ALWAYS run — executes all promoted checks from `scripts/check-registry/manifest.json` <!-- Added: forge#1331 -->
|
|
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 -->
|
|
280
|
+
- **2G.10 (Spec bash syntax)**: Run if `FORGE_GRAPH` in DOMAINS and any `commands/**/*.md` changed — `bash -n` on changed ```bash blocks in specs (HIGH); shellcheck advisory
|
|
276
281
|
- **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 -->
|
|
282
|
+
- **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
283
|
- **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
284
|
- **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
285
|
|
|
@@ -713,6 +719,34 @@ fi
|
|
|
713
719
|
|
|
714
720
|
Do NOT suppress the finding by making the check less strict or adding fake no-op tests. The intent is a real proof that the path is reachable. <!-- Added: forge#1731 -->
|
|
715
721
|
|
|
722
|
+
### 2G.10: Spec bash syntax check (FORGE_GRAPH domain)
|
|
723
|
+
|
|
724
|
+
**Triggered when**: changed files include `commands/**/*.md`. Skips silently in any project without `scripts/check-spec-bash.sh`.
|
|
725
|
+
|
|
726
|
+
**Why this matters**: in ForgeDock the pipeline code is bash inside Markdown specs. The executable-file checks above exclude `.md` files, and acceptance checks are mostly greps, so spec bash used to merge without ever being parsed. Reviewers were then the first to run it, which drove most review findings (2026-10-08 cascade audit). This check runs `bash -n` on every ```bash block that contains a changed line, after neutralising `{PLACEHOLDER}` notation. Blocks with pseudo tool calls (`Skill(`/`Agent(`/`Task(`) and blocks preceded by `<!-- allowlist:check-spec-bash -->` are skipped. Pre-existing blocks are never checked, so the baseline cannot block a change. `shellcheck -S error` results are advisory only.
|
|
727
|
+
|
|
728
|
+
```bash
|
|
729
|
+
SPEC_BASH_CHECKER="$FORGEDOCK_SCRIPTS/check-spec-bash.sh"
|
|
730
|
+
[ -f "$SPEC_BASH_CHECKER" ] || SPEC_BASH_CHECKER="{WORKTREE_PATH}/scripts/check-spec-bash.sh"
|
|
731
|
+
SPEC_MD_FILES=$(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '^commands/.*\.md$' || true)
|
|
732
|
+
if [ -f "$SPEC_BASH_CHECKER" ] && [ -n "$SPEC_MD_FILES" ]; then
|
|
733
|
+
PR_BASE="${PR_BASE:-staging}"
|
|
734
|
+
SPEC_BASH_BASE=$(git -C "{WORKTREE_PATH}" merge-base "origin/${PR_BASE}" HEAD 2>/dev/null || true)
|
|
735
|
+
if [ -z "$SPEC_BASH_BASE" ]; then
|
|
736
|
+
echo "FORGE_GRAPH | HIGH | spec-bash | cannot resolve merge-base with origin/${PR_BASE} — spec bash not checked (fail closed)"
|
|
737
|
+
else
|
|
738
|
+
# shellcheck disable=SC2086 # SPEC_MD_FILES is a newline list of repo-relative paths without spaces
|
|
739
|
+
SPEC_BASH_REPORT=$(cd "{WORKTREE_PATH}" && bash "$SPEC_BASH_CHECKER" --base "$SPEC_BASH_BASE" --shellcheck $SPEC_MD_FILES 2>&1)
|
|
740
|
+
echo "$SPEC_BASH_REPORT"
|
|
741
|
+
echo "$SPEC_BASH_REPORT" | grep -E '^FAIL ' | while IFS= read -r hit; do
|
|
742
|
+
echo "FORGE_GRAPH | HIGH | spec-bash | ${hit#FAIL }"
|
|
743
|
+
done
|
|
744
|
+
fi
|
|
745
|
+
fi
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
Report each `FAIL` line as **HIGH**: the changed block does not parse, so any agent that runs it hits a syntax error. Fix the bash. If the block is an intentional fragment (for example an `if` opened in one block and closed in the next), put `<!-- allowlist:check-spec-bash -->` on the line before the fence. `WARN` (shellcheck) lines are advisory and go in the report without blocking.
|
|
749
|
+
|
|
716
750
|
### 2G.9: Danger-zone recurrence check (FORGE_GRAPH domain) <!-- Added: forge#1744 -->
|
|
717
751
|
|
|
718
752
|
**Triggered when**: `FORGE_GRAPH` in DOMAINS (changed files include `commands/*.md` specs or `scripts/*` files). Skips silently when the FORGE:CONTEXT comment on the issue contains no `### Danger-Zone Rule Cards` section (i.e., Phase C0.5 injected zero cards — nothing to cross-reference).
|
|
@@ -1298,6 +1332,51 @@ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
|
1298
1332
|
|
|
1299
1333
|
**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
1334
|
|
|
1335
|
+
### 2U: Coverage-reduction check (ALWAYS — a red check must be fixed, not silenced)
|
|
1336
|
+
|
|
1337
|
+
<!-- Added: forge#3257 -->
|
|
1338
|
+
|
|
1339
|
+
**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.
|
|
1340
|
+
|
|
1341
|
+
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).
|
|
1342
|
+
|
|
1343
|
+
```bash
|
|
1344
|
+
cd {WORKTREE_PATH}
|
|
1345
|
+
PR_BASE="${PR_BASE:-staging}"
|
|
1346
|
+
if ! git rev-parse --verify --quiet "origin/$PR_BASE^{commit}" >/dev/null; then
|
|
1347
|
+
echo "COVERAGE-ERR | HIGH | quality-gate | base ref origin/$PR_BASE not found — fetch it; coverage-reduction check could not run"
|
|
1348
|
+
else
|
|
1349
|
+
BASE_REF=$(git merge-base "origin/$PR_BASE" HEAD) || { echo "COVERAGE-ERR | HIGH | quality-gate | merge-base failed for origin/$PR_BASE"; BASE_REF=""; }
|
|
1350
|
+
fi
|
|
1351
|
+
if [ -n "${BASE_REF:-}" ]; then
|
|
1352
|
+
TEST_PATH_RE='(^|/)(test_[^/]*|[^/]*_test\.[^/]*|[^/]*\.test\.[^/]*|[^/]*\.spec\.[^/]*)$|(^|/)(tests|__tests__)/'
|
|
1353
|
+
|
|
1354
|
+
# (a) deleted test files (renames excluded via --find-renames)
|
|
1355
|
+
git diff --find-renames --diff-filter=D --name-only "$BASE_REF" | grep -E "$TEST_PATH_RE" \
|
|
1356
|
+
&& echo "COVERAGE-1: deleted test file(s)"
|
|
1357
|
+
|
|
1358
|
+
# (b) removed test cases with no added counterpart, (c) newly added skip markers
|
|
1359
|
+
git diff -U0 "$BASE_REF" -- . | grep -E '^-[[:space:]]*(it|test|describe)\(|^-[[:space:]]*(async )?def test_' \
|
|
1360
|
+
&& echo "COVERAGE-1: removed test case line(s) — confirm each has an added counterpart in the diff"
|
|
1361
|
+
git diff -U0 "$BASE_REF" -- . | grep -E '^\+.*(\.skip\(|@pytest\.mark\.skip|@pytest\.mark\.xfail|xfail|continue-on-error:[[:space:]]*true|if:[[:space:]]*false)' \
|
|
1362
|
+
&& echo "COVERAGE-1: newly added skip/xfail/continue-on-error/if: false marker"
|
|
1363
|
+
|
|
1364
|
+
# (d) removed or disabled workflow test steps
|
|
1365
|
+
git diff -U0 "$BASE_REF" -- .github/workflows | grep -E '^-.*run:.*(test|pytest|jest|vitest|mocha|\.test\.sh|npm t|go test|cargo test)' \
|
|
1366
|
+
&& echo "COVERAGE-1: removed workflow test step"
|
|
1367
|
+
fi
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1370
|
+
Also treat a test command that is commented out (`+#` of a previously live `run:` test line) as a removal.
|
|
1371
|
+
|
|
1372
|
+
**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.
|
|
1373
|
+
|
|
1374
|
+
**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).
|
|
1375
|
+
|
|
1376
|
+
**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.
|
|
1377
|
+
|
|
1378
|
+
---
|
|
1379
|
+
|
|
1301
1380
|
### 2R: Registry checks (ALWAYS — promoted patterns from recurring review findings)
|
|
1302
1381
|
|
|
1303
1382
|
<!-- Added: forge#1331 -->
|
|
@@ -1698,6 +1777,7 @@ OR:
|
|
|
1698
1777
|
6. SHELL-1 | HIGH | deploy.sh | Hardcoded 'localhost' in DB connection function — use ${POSTGRES_HOST:-localhost} pattern
|
|
1699
1778
|
7. INFRA-1 | HIGH | Dockerfile | Filesystem write ops found under named volume mount — add chown before privilege drop in entrypoint
|
|
1700
1779
|
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
|
|
1780
|
+
9. COVERAGE-1 | HIGH | .github/workflows/ci.yml | coverage reduction: removed workflow test step — restore it and fix the code under test
|
|
1701
1781
|
```
|
|
1702
1782
|
|
|
1703
1783
|
**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,40 @@ 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) AND is MEDIUM+ or CONFIRMED — a LOW/POSSIBLE finding stays a NOTE whichever reviewer raised it. **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, FORGE_ROOT, FORGEDOCK_HOME, newest pinned forgedock plugin cache
|
|
1701
|
+
# under the active config dir (CLAUDE_CONFIG_DIR) then ~/.claude, then the repo's own scripts/ (ForgeDock itself).
|
|
1702
|
+
# The plugin-root placeholder is not always substituted in forked review runs, so the cache scan is what makes
|
|
1703
|
+
# consumer repos resolve the script instead of falling back to classifier=manual.
|
|
1704
|
+
CLASSIFY_SCRIPT=""
|
|
1705
|
+
_cands="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}")"
|
|
1706
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
1707
|
+
[ -n "$_cfg" ] || continue
|
|
1708
|
+
_cands="$_cands
|
|
1709
|
+
$(find -L "$_cfg/plugins/cache/forgedock/forgedock" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | awk -F/ '$NF ~ /^[0-9]+\.[0-9]+\.[0-9]+$/{split($NF,a,".");printf "%d %d %d %s\n",a[1],a[2],a[3],$0}' | sort -k1,1nr -k2,2nr -k3,3nr | cut -d' ' -f4- || true)"
|
|
1710
|
+
done
|
|
1711
|
+
_cands="$_cands
|
|
1712
|
+
$PWD"
|
|
1713
|
+
while IFS= read -r _c; do
|
|
1714
|
+
case "$_c" in /*) [ -z "$CLASSIFY_SCRIPT" ] && [ -f "$_c/scripts/classify-finding.sh" ] && CLASSIFY_SCRIPT="$_c/scripts/classify-finding.sh" ;; esac
|
|
1715
|
+
done <<< "$_cands"
|
|
1716
|
+
FINDING_LINEAGE="none"
|
|
1717
|
+
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
|
|
1718
|
+
FINDING_LINEAGE="review-finding"
|
|
1719
|
+
fi
|
|
1720
|
+
# Per deduped finding (FINDING_TEXT_FILE = mktemp file holding title + body + affected paths):
|
|
1721
|
+
# DISPOSITION=$(bash "$CLASSIFY_SCRIPT" --severity "$FINDING_SEVERITY" --confidence "$FINDING_CONFIDENCE" \
|
|
1722
|
+
# --agent "$FINDING_AGENT" --lineage "$FINDING_LINEAGE" --text-file "$FINDING_TEXT_FILE")
|
|
1723
|
+
# case "$DISPOSITION" in ISSUE*) ;; NOTE*) ;; esac # first word is the class, the rest is the reason
|
|
1724
|
+
# If CLASSIFY_SCRIPT is empty, apply the four rules above by hand and record classifier=manual below.
|
|
1725
|
+
```
|
|
1699
1726
|
|
|
1700
1727
|
Each NOTE gets exactly one disposition, in this preference order:
|
|
1701
1728
|
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 +1733,15 @@ Each NOTE gets exactly one disposition, in this preference order:
|
|
|
1706
1733
|
|
|
1707
1734
|
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
1735
|
|
|
1736
|
+
**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.
|
|
1737
|
+
|
|
1738
|
+
```bash
|
|
1739
|
+
CLASSIFIER_MODE=$([ -n "$CLASSIFY_SCRIPT" ] && echo script || echo manual)
|
|
1740
|
+
# NOTE_LIST_FILE: mktemp file named for the PR number, holding one line per NOTE (id, file:line, disposition, reason).
|
|
1741
|
+
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} -->
|
|
1742
|
+
$(cat "$NOTE_LIST_FILE" 2>/dev/null)" # allowlist:check-command-side-effects
|
|
1743
|
+
```
|
|
1744
|
+
|
|
1709
1745
|
### 6C: Create Issues
|
|
1710
1746
|
|
|
1711
1747
|
```bash
|
|
@@ -2071,6 +2107,8 @@ Verdict determined by standard blocking criteria.
|
|
|
2071
2107
|
4. `MERGE_HEALTH == "CONFLICTING"` OR `MERGE_HEALTH_STATE` in {`DIRTY`, `BLOCKED`} — PR cannot be merged cleanly into its base branch <!-- Added: forge#194 -->
|
|
2072
2108
|
- Verdict: CHANGES REQUESTED. Message: "Merge conflict with `{base}`. Rebase `{head}` onto `origin/{base}`, resolve the conflicting files, then re-run /review-pr."
|
|
2073
2109
|
- 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).
|
|
2110
|
+
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 -->
|
|
2111
|
+
- 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
2112
|
|
|
2075
2113
|
```bash
|
|
2076
2114
|
# 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 +2242,25 @@ fi
|
|
|
2204
2242
|
|
|
2205
2243
|
**Skip if** `AUTO_MERGE=false`.
|
|
2206
2244
|
|
|
2245
|
+
**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.
|
|
2246
|
+
|
|
2247
|
+
```bash
|
|
2248
|
+
DISPO_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null) || DISPO_JSON=""
|
|
2249
|
+
if [ -z "$DISPO_JSON" ]; then
|
|
2250
|
+
echo "NOTE DISPOSITION: could not read PR comments — refusing to merge (fail closed)" >&2
|
|
2251
|
+
# STOP — return REVIEW_RESULT: status: BLOCKED, blocker: "note disposition unreadable".
|
|
2252
|
+
exit 1
|
|
2253
|
+
fi
|
|
2254
|
+
FINDING_COUNT=$(printf '%s' "$DISPO_JSON" | jq -s '[.[][] | select(.body | test("<!-- FINDING:"))] | length')
|
|
2255
|
+
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')
|
|
2256
|
+
if [ "${FINDING_COUNT:-0}" -gt 0 ] && [ "${DISPOSITION_COUNT:-0}" -eq 0 ]; then
|
|
2257
|
+
echo "NOTE DISPOSITION: ${FINDING_COUNT} finding comment(s) but no FORGE:NOTE_DISPOSITION record — run §6B.5 before merging" >&2
|
|
2258
|
+
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
|
|
2259
|
+
# STOP — return REVIEW_RESULT: status: BLOCKED, blocker: "note disposition missing".
|
|
2260
|
+
exit 1
|
|
2261
|
+
fi
|
|
2262
|
+
```
|
|
2263
|
+
|
|
2207
2264
|
**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
2265
|
|
|
2209
2266
|
```bash
|
|
@@ -2216,14 +2273,20 @@ else
|
|
|
2216
2273
|
# Same resolution as work-on/review.md Phase R1.5: the verifier ships with ForgeDock, not the consumer repo.
|
|
2217
2274
|
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
2218
2275
|
FORGE_ROOT=""
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2276
|
+
# Windows drive-letter FORGEDOCK_HOME (C:/x or C:\x) is normalized to /c/x (cygpath when present); relative values stay rejected.
|
|
2277
|
+
_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
|
|
2278
|
+
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
2279
|
+
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
2280
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
2281
|
+
# 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.
|
|
2282
|
+
_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")")"
|
|
2283
|
+
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
2284
|
+
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
2285
|
+
# 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)
|
|
2286
|
+
_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)"
|
|
2287
|
+
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
2225
2288
|
# '${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")"
|
|
2289
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
2227
2290
|
while IFS= read -r _c; do
|
|
2228
2291
|
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
2292
|
done <<< "$_k"
|
|
@@ -2247,7 +2310,20 @@ else
|
|
|
2247
2310
|
echo "PHASE TRAIL: verify-phase-trail.sh not resolvable (set FORGEDOCK_HOME to the ForgeDock install) — refusing to merge" >&2
|
|
2248
2311
|
TRAIL="PHASE_TRAIL: ERROR (verifier not resolvable)"; TRAIL_RC=127
|
|
2249
2312
|
else
|
|
2250
|
-
|
|
2313
|
+
# 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).
|
|
2314
|
+
PR_HEAD_SHA=$(gh api "repos/{GH_REPO}/pulls/{PR_NUMBER}" --jq .head.sha 2>/dev/null) || PR_HEAD_SHA=""
|
|
2315
|
+
HEAD_SHA_ARGS=()
|
|
2316
|
+
if [[ "$PR_HEAD_SHA" =~ ^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$ ]]; then HEAD_SHA_ARGS=(--head-sha "$PR_HEAD_SHA"); fi
|
|
2317
|
+
TRAIL=$(bash "$TRAIL_SCRIPT" "$MERGE_ISSUE" -R {GH_REPO} $DOCS_ONLY_FLAG $CODE_DIFF_FLAG ${HEAD_SHA_ARGS[@]+"${HEAD_SHA_ARGS[@]}"}); TRAIL_RC=$?
|
|
2318
|
+
if [ "$TRAIL_RC" -eq 0 ]; then
|
|
2319
|
+
# An `OVERRIDE:` line means the verifier accepted a human override. Record it on the PR from the verifier's output (never agent prose).
|
|
2320
|
+
OVR=$(printf '%s\n' "$TRAIL" | sed -n 's/^OVERRIDE: //p' | sed -n '1p')
|
|
2321
|
+
OVERRIDE_BOUND_HEAD=""
|
|
2322
|
+
if [ -n "$OVR" ]; then
|
|
2323
|
+
OVERRIDE_BOUND_HEAD="$PR_HEAD_SHA" # forge#3273: the merge below must land exactly this commit
|
|
2324
|
+
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 -->
|
|
2325
|
+
fi
|
|
2326
|
+
fi
|
|
2251
2327
|
fi
|
|
2252
2328
|
if [ "$TRAIL_RC" -ge 2 ]; then
|
|
2253
2329
|
# rc 2 (trail unreadable) / 127 (verifier not resolvable) is an infrastructure failure, NOT missing phases:
|
|
@@ -2407,6 +2483,12 @@ else
|
|
|
2407
2483
|
fi
|
|
2408
2484
|
fi
|
|
2409
2485
|
fi
|
|
2486
|
+
# WIRE:PROVEN — spec prose; OVERRIDE_BOUND_HEAD is set in the verifier block above and consumed here (reviewed by trace)
|
|
2487
|
+
# forge#3273: an accepted break-glass override is bound to one exact head. If the commit being merged
|
|
2488
|
+
# differs (a push after the verifier ran, even a docs-only one), the override does not cover it.
|
|
2489
|
+
if [ "$CI_GATE_RC" -eq 0 ] && [ -n "${OVERRIDE_BOUND_HEAD:-}" ] && [ "$GATED_HEAD" != "$OVERRIDE_BOUND_HEAD" ]; then
|
|
2490
|
+
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}"
|
|
2491
|
+
fi
|
|
2410
2492
|
if [ "$CI_GATE_RC" -eq 4 ]; then
|
|
2411
2493
|
# Not a human problem: the caller re-runs /review-pr on the new head (work-on/review.md R4).
|
|
2412
2494
|
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
|
|
|
@@ -149,7 +150,7 @@ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-com
|
|
|
149
150
|
|
|
150
151
|
## Phase V2: Format and Verify
|
|
151
152
|
|
|
152
|
-
Run after quality gate passes. All tool commands are read from `forge.yaml → verification.commands
|
|
153
|
+
Run after quality gate passes. All tool commands are read from `forge.yaml → verification.commands`. `forge.yaml` is usually gitignored, so it is absent from the worktree: every block below resolves `FORGE_CFG` to the worktree copy if present, else the main checkout's `forge.yaml` (via `git rev-parse --git-common-dir`). Before this, every verification command, learned test command and the SOPS chain check were silently skipped in worktree builds; each step logs `SKIPPED — not configured` when the corresponding key is absent rather than silently passing.
|
|
153
154
|
|
|
154
155
|
**Track skipped checks** — initialize before any check runs:
|
|
155
156
|
```bash
|
|
@@ -177,8 +178,9 @@ run_verification_command() {
|
|
|
177
178
|
**Python**:
|
|
178
179
|
```bash
|
|
179
180
|
cd {WORKTREE_PATH}
|
|
181
|
+
FORGE_CFG=forge.yaml; [ -f "$FORGE_CFG" ] || { _gcd=$(git rev-parse --git-common-dir 2>/dev/null) && _gcd=$(cd "$_gcd" 2>/dev/null && pwd) && [ -f "${_gcd%/.git}/forge.yaml" ] && FORGE_CFG="${_gcd%/.git}/forge.yaml"; } # gitignored forge.yaml exists only in the main checkout, not in worktrees
|
|
180
182
|
|
|
181
|
-
PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""'
|
|
183
|
+
PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' "$FORGE_CFG" 2>/dev/null || echo '')
|
|
182
184
|
if [ -n "$PYTHON_FORMAT" ]; then
|
|
183
185
|
run_verification_command "python.format" "$PYTHON_FORMAT"
|
|
184
186
|
else
|
|
@@ -194,10 +196,11 @@ Failures in `py_compile` are BLOCKING — fix before continuing.
|
|
|
194
196
|
**TypeScript**:
|
|
195
197
|
```bash
|
|
196
198
|
cd {WORKTREE_PATH}
|
|
199
|
+
FORGE_CFG=forge.yaml; [ -f "$FORGE_CFG" ] || { _gcd=$(git rev-parse --git-common-dir 2>/dev/null) && _gcd=$(cd "$_gcd" 2>/dev/null && pwd) && [ -f "${_gcd%/.git}/forge.yaml" ] && FORGE_CFG="${_gcd%/.git}/forge.yaml"; } # gitignored forge.yaml exists only in the main checkout, not in worktrees
|
|
197
200
|
|
|
198
|
-
TS_FORMAT=$(yq '.verification.commands.typescript.format // ""'
|
|
199
|
-
TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""'
|
|
200
|
-
TS_BUILD=$(yq '.verification.commands.typescript.build // ""'
|
|
201
|
+
TS_FORMAT=$(yq '.verification.commands.typescript.format // ""' "$FORGE_CFG" 2>/dev/null || echo '')
|
|
202
|
+
TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' "$FORGE_CFG" 2>/dev/null || echo '')
|
|
203
|
+
TS_BUILD=$(yq '.verification.commands.typescript.build // ""' "$FORGE_CFG" 2>/dev/null || echo '')
|
|
201
204
|
|
|
202
205
|
if [ -n "$TS_FORMAT" ]; then
|
|
203
206
|
run_verification_command "typescript.format" "$TS_FORMAT"
|
|
@@ -233,8 +236,9 @@ Before running any test command below, check it against `verification.known_slow
|
|
|
233
236
|
|
|
234
237
|
```bash
|
|
235
238
|
cd {WORKTREE_PATH}
|
|
239
|
+
FORGE_CFG=forge.yaml; [ -f "$FORGE_CFG" ] || { _gcd=$(git rev-parse --git-common-dir 2>/dev/null) && _gcd=$(cd "$_gcd" 2>/dev/null && pwd) && [ -f "${_gcd%/.git}/forge.yaml" ] && FORGE_CFG="${_gcd%/.git}/forge.yaml"; } # gitignored forge.yaml exists only in the main checkout, not in worktrees
|
|
236
240
|
# Read directly from forge.yaml (static, operator-declared config).
|
|
237
|
-
KNOWN_SLOW_TESTS=$(yq -o=json -I=0 '.verification.known_slow_tests // []'
|
|
241
|
+
KNOWN_SLOW_TESTS=$(yq -o=json -I=0 '.verification.known_slow_tests // []' "$FORGE_CFG" 2>/dev/null || echo '[]')
|
|
238
242
|
|
|
239
243
|
# apply_known_slow_filter <cmd> — echoes the command to actually run, or "" to
|
|
240
244
|
# skip it entirely. Matching is substring match of `pattern` against the full
|
|
@@ -273,7 +277,8 @@ After all `verification.commands` steps complete, run any commands from `forge.y
|
|
|
273
277
|
|
|
274
278
|
```bash
|
|
275
279
|
cd {WORKTREE_PATH}
|
|
276
|
-
|
|
280
|
+
FORGE_CFG=forge.yaml; [ -f "$FORGE_CFG" ] || { _gcd=$(git rev-parse --git-common-dir 2>/dev/null) && _gcd=$(cd "$_gcd" 2>/dev/null && pwd) && [ -f "${_gcd%/.git}/forge.yaml" ] && FORGE_CFG="${_gcd%/.git}/forge.yaml"; } # gitignored forge.yaml exists only in the main checkout, not in worktrees
|
|
281
|
+
LEARNED_TEST_COMMANDS=$(yq -o=json -I=0 '.learned.test_commands // []' "$FORGE_CFG" 2>/dev/null || echo '[]')
|
|
277
282
|
LEARNED_FAILED=0
|
|
278
283
|
if [ -n "$LEARNED_TEST_COMMANDS" ] && [ "$LEARNED_TEST_COMMANDS" != "[]" ] && [ "$LEARNED_TEST_COMMANDS" != "null" ]; then
|
|
279
284
|
echo "Running learned test commands..."
|
|
@@ -367,7 +372,8 @@ After static proxy checks, run a lightweight live browser check using Playwright
|
|
|
367
372
|
|
|
368
373
|
```bash
|
|
369
374
|
cd {WORKTREE_PATH}
|
|
370
|
-
|
|
375
|
+
FORGE_CFG=forge.yaml; [ -f "$FORGE_CFG" ] || { _gcd=$(git rev-parse --git-common-dir 2>/dev/null) && _gcd=$(cd "$_gcd" 2>/dev/null && pwd) && [ -f "${_gcd%/.git}/forge.yaml" ] && FORGE_CFG="${_gcd%/.git}/forge.yaml"; } # gitignored forge.yaml exists only in the main checkout, not in worktrees
|
|
376
|
+
APP_URL=$(yq '.services.app_url // ""' "$FORGE_CFG" 2>/dev/null || echo '')
|
|
371
377
|
if [ -z "$APP_URL" ]; then
|
|
372
378
|
echo "SKIPPED — services.app_url not configured in forge.yaml (browser signal check requires a running app URL)"
|
|
373
379
|
else
|
|
@@ -441,7 +447,8 @@ ADDED_LINES=$(git diff HEAD -- {CHANGED_FILES} | grep -E '^\+' | grep -v '^+++')
|
|
|
441
447
|
|
|
442
448
|
```bash
|
|
443
449
|
cd {WORKTREE_PATH}
|
|
444
|
-
|
|
450
|
+
FORGE_CFG=forge.yaml; [ -f "$FORGE_CFG" ] || { _gcd=$(git rev-parse --git-common-dir 2>/dev/null) && _gcd=$(cd "$_gcd" 2>/dev/null && pwd) && [ -f "${_gcd%/.git}/forge.yaml" ] && FORGE_CFG="${_gcd%/.git}/forge.yaml"; } # gitignored forge.yaml exists only in the main checkout, not in worktrees
|
|
451
|
+
SECRETS_BACKEND=$(yq '.deploy.secrets_backend // ""' "$FORGE_CFG" 2>/dev/null || echo '')
|
|
445
452
|
if [ "$SECRETS_BACKEND" != "sops" ]; then
|
|
446
453
|
echo 'SKIP: SOPS chain check — deploy.secrets_backend is not "sops". Configure deploy.secrets_backend in forge.yaml to enable.'
|
|
447
454
|
fi
|