forgedock 1.15.0 → 1.16.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/phases.mjs +21 -4
- package/bin/engine/runlog.mjs +10 -1
- package/bin/engine.mjs +2 -0
- package/commands/forge-stats.md +10 -0
- package/commands/review-pr-staging.md +12 -2
- package/package.json +1 -1
- package/scripts/check-spec-size.sh +102 -0
- package/scripts/check-spec-size.test.sh +85 -0
- package/scripts/review-pr-staging-scope.test.sh +28 -0
package/bin/engine/phases.mjs
CHANGED
|
@@ -218,6 +218,15 @@ function hasPhaseComplete(comments) {
|
|
|
218
218
|
return hasMarker(comments, "FORGE:PHASE:COMPLETE", "FORGE:PHASE:COMPLETE");
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
/**
|
|
222
|
+
* close.md's own idempotency key: a trusted FORGE:TRAJECTORY comment. Shared by close.reconcile
|
|
223
|
+
* and close.detectOutcome so the two cannot drift. Throws on fetch error; callers fail closed.
|
|
224
|
+
*/
|
|
225
|
+
async function hasTrajectory(issue, io) {
|
|
226
|
+
const { comments } = await issueMarkers(issue, io);
|
|
227
|
+
return comments.some((b) => startsWithHeader(b, "FORGE:TRAJECTORY"));
|
|
228
|
+
}
|
|
229
|
+
|
|
221
230
|
/**
|
|
222
231
|
* Trusted comment bodies, chronological — the same single fetch + trust predicate
|
|
223
232
|
* as issueMarkers() (one path, forge#3542). Fail closed: a fetch error yields `[]` here (issueMarkers itself propagates it).
|
|
@@ -960,8 +969,7 @@ export const PHASES = [
|
|
|
960
969
|
// proof close.md ran (board, summary, trajectory). Its own idempotency key is the
|
|
961
970
|
// FORGE:TRAJECTORY comment; only that makes this satisfied.
|
|
962
971
|
try {
|
|
963
|
-
|
|
964
|
-
return { satisfied: comments.some((b) => startsWithHeader(b, "FORGE:TRAJECTORY")), terminalReason: "investigation" };
|
|
972
|
+
return { satisfied: await hasTrajectory(state.issue, io), terminalReason: "investigation" };
|
|
965
973
|
} catch { return { satisfied: false }; }
|
|
966
974
|
}
|
|
967
975
|
if (snap.state === "CLOSED") return { satisfied: true };
|
|
@@ -992,8 +1000,17 @@ export const PHASES = [
|
|
|
992
1000
|
if (snap.labels.includes(PHASE_MARKERS.close.completionLabel))
|
|
993
1001
|
return { status: "committed", terminalReason: "merged", outputs: {} };
|
|
994
1002
|
// forge#3534: an investigation exit closes the issue itself; keep that reason honest.
|
|
995
|
-
if (snap.state === "CLOSED")
|
|
996
|
-
|
|
1003
|
+
if (snap.state === "CLOSED") {
|
|
1004
|
+
if (state.terminalReason !== "investigation")
|
|
1005
|
+
return { status: "committed", terminalReason: "invalid", outputs: {} };
|
|
1006
|
+
// The issue is always already CLOSED for an investigation run, so require the same
|
|
1007
|
+
// positive evidence as reconcile (a trusted FORGE:TRAJECTORY); fail closed on error.
|
|
1008
|
+
let ran = false;
|
|
1009
|
+
try { ran = await hasTrajectory(state.issue, io); } catch { ran = false; }
|
|
1010
|
+
return ran
|
|
1011
|
+
? { status: "committed", terminalReason: "investigation", outputs: {} }
|
|
1012
|
+
: { status: "failed", detail: "investigation close: no FORGE:TRAJECTORY" };
|
|
1013
|
+
}
|
|
997
1014
|
// forge#3545: PHASE_COMPLETE is a valid close exit (multi-phase issue: PR merged,
|
|
998
1015
|
// issue deliberately left OPEN). Only the current cycle's marker counts.
|
|
999
1016
|
if (snap.state === "OPEN") {
|
package/bin/engine/runlog.mjs
CHANGED
|
@@ -61,6 +61,8 @@ export function deriveState(events) {
|
|
|
61
61
|
remediationKind: null };
|
|
62
62
|
// forge#3528: handoff reason of the LAST PHASE_COMMIT only (replaced, not sticky).
|
|
63
63
|
let lastCommitReason = null;
|
|
64
|
+
// forge#3754: phase of the LAST PHASE_COMMIT, so a reopen can tell a remediate-terminal run.
|
|
65
|
+
let lastCommitPhase = null;
|
|
64
66
|
for (const e of events) {
|
|
65
67
|
switch (e.event) {
|
|
66
68
|
case "RUN_START":
|
|
@@ -75,6 +77,7 @@ export function deriveState(events) {
|
|
|
75
77
|
// remediate instead of defaulting to a "merged" close.
|
|
76
78
|
if (e.terminalReason) s.terminalReason = e.terminalReason;
|
|
77
79
|
lastCommitReason = e.terminalReason ?? null;
|
|
80
|
+
lastCommitPhase = e.phase ?? null;
|
|
78
81
|
// forge#3530: the review's remediation kind must survive resume. A later review
|
|
79
82
|
// commit replaces it; a committed remediate consumes it, so it is never reused.
|
|
80
83
|
if (e.phase === "review") s.remediationKind = e.outputs?.remediation ?? null;
|
|
@@ -91,6 +94,12 @@ export function deriveState(events) {
|
|
|
91
94
|
// retry re-enters remediate/decompose instead of close/build.
|
|
92
95
|
case "RUN_REOPEN":
|
|
93
96
|
s.terminal = false; s.terminalReason = lastCommitReason; s.v = e.seq;
|
|
97
|
+
// forge#3754: a remediate that ended needs-human is re-run after a human clears the
|
|
98
|
+
// label. Un-commit it (new array, no in-place mutation) so the forge#3506 resume guard
|
|
99
|
+
// does not re-terminate and remediate's entryCondition re-selects it.
|
|
100
|
+
if (lastCommitPhase === "remediate" && lastCommitReason === "needs-human") {
|
|
101
|
+
s.committed = s.committed.filter((p) => p !== "remediate");
|
|
102
|
+
}
|
|
94
103
|
break;
|
|
95
104
|
// forge#3562: an explicit `run-issue --retry` on a `phase-complete` run
|
|
96
105
|
// (multi-phase issue: this phase merged, more remain) starts a NEW cycle.
|
|
@@ -102,7 +111,7 @@ export function deriveState(events) {
|
|
|
102
111
|
case "RUN_NEXT_CYCLE":
|
|
103
112
|
s.terminal = false; s.terminalReason = null; s.committed = [];
|
|
104
113
|
s.branch = null; s.pr = null; s.remediationKind = null;
|
|
105
|
-
lastCommitReason = null; s.v = e.seq;
|
|
114
|
+
lastCommitReason = null; lastCommitPhase = null; s.v = e.seq;
|
|
106
115
|
break;
|
|
107
116
|
// forge#2524: a session-limit pause is purely informational — it does
|
|
108
117
|
// NOT touch committed/terminal/terminalReason. The phase that hit the
|
package/bin/engine.mjs
CHANGED
|
@@ -514,6 +514,8 @@ export async function runIssue(opts) {
|
|
|
514
514
|
// must honour that reason instead of picking the next phase (close would throw in buildArgs
|
|
515
515
|
// and end as engine-error; build would run on a decomposed parent). Review's `needs-human`
|
|
516
516
|
// is a handoff to remediate, so only these committed phases trigger the guard.
|
|
517
|
+
// forge#3754: a reopened (--retry) remediate-terminal run has `remediate` un-committed by the
|
|
518
|
+
// run-log reducer, so this guard does not fire and remediate re-runs.
|
|
517
519
|
if (state.terminalReason && TERMINAL_REASONS.includes(state.terminalReason) &&
|
|
518
520
|
((state.committed.includes("remediate") && !["merged", "decomposed", "invalid"].includes(state.terminalReason)) ||
|
|
519
521
|
(state.committed.includes("decompose") && state.terminalReason === "decomposed") ||
|
package/commands/forge-stats.md
CHANGED
|
@@ -52,6 +52,16 @@ Commands are classified by role. Each role has a max line count — beyond this,
|
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
55
|
+
## CI Spec-Size Budget
|
|
56
|
+
|
|
57
|
+
`scripts/check-spec-size.sh` (job `spec-size-budget` in `check-commands.yml`, PRs only) enforces a budget that is independent of the line-count `.baselines`; the baseline is refreshed on release via `/forge-stats baseline`. It compares bytes against the PR base:
|
|
58
|
+
|
|
59
|
+
- `review-pr.md`, `review-pr-staging.md`, `work-on/remediate.md`, `work-on/review.md`, `work-on.md` may not grow by more than 2% or 4 KB. Shrinking always passes.
|
|
60
|
+
- No `commands/**` file may gain a copy of a canonical block (FORGE_ROOT bootstrap, TRUSTED_SCRIPT/HELPER_SCRIPT resolver, FENCE_BOOTSTRAP anchor); the per-file marker count may not rise. Tighten to an outright ban once the shared helper (#3493) lands.
|
|
61
|
+
- Exemption: the `spec-size-exempt` PR label, or a `<!-- spec-size-exempt: <reason> -->` line in the PR body (non-empty reason). The job summary lists the per-file delta and any waiver.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
55
65
|
## Mode: `diff` (default)
|
|
56
66
|
|
|
57
67
|
Compare current line counts against the last git commit. Shows what grew and what shrank.
|
|
@@ -1190,7 +1190,11 @@ if [ -n "$REVIEW_SHA_FULL" ] && [ -n "$TRUSTED_SCRIPT" ]; then
|
|
|
1190
1190
|
| jq -r 'scan("^<!-- FORGE:REVIEW-AGENT:([a-z0-9-]+) -->") | .[0]' 2>/dev/null | sort -u | grep -c . || echo 0)
|
|
1191
1191
|
fi
|
|
1192
1192
|
case "$ACTUAL_AGENT_COUNT" in ''|*[!0-9]*) ACTUAL_AGENT_COUNT=0 ;; esac # fail closed: unreadable or untrusted counts as not posted
|
|
1193
|
-
|
|
1193
|
+
# Fresh shell: SELECTED_AGENT_COUNT is not carried over. Reload it from the PR-keyed launched-agent file, then fail closed when it is empty, non-integer or zero (a staging review always selects at least one reviewer).
|
|
1194
|
+
[ -n "${SELECTED_AGENT_COUNT:-}" ] || { [ -n "${PR_NUMBER:-}" ] && [ -r "$_SD/${PR_NUMBER}_launched_review_agents" ] && SELECTED_AGENT_COUNT=$(grep -E '^[a-z0-9-]+$' "$_SD/${PR_NUMBER}_launched_review_agents" | sort -u | grep -c .); }
|
|
1195
|
+
SELECTED_COUNT_INVALID=false
|
|
1196
|
+
case "${SELECTED_AGENT_COUNT:-}" in ''|*[!0-9]*|0) SELECTED_COUNT_INVALID=true; SELECTED_AGENT_COUNT=0 ;; esac
|
|
1197
|
+
if [ "${DISPATCH_FAILED:-}" = "true" ] || [ "$SELECTED_COUNT_INVALID" = "true" ] || [ "$ACTUAL_AGENT_COUNT" -lt "$SELECTED_AGENT_COUNT" ]; then
|
|
1194
1198
|
gh label create "review-degraded" --color "E4E669" --description "PR review panel was incomplete; re-review required before deployment. Managed by ForgeDock." --force -R "$GH_REPO" 2>/dev/null || true
|
|
1195
1199
|
gh pr edit "$PR_NUMBER" -R "$GH_REPO" --add-label "review-degraded" --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1196
1200
|
gh pr comment "$PR_NUMBER" -R "$GH_REPO" --body "<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity -->
|
|
@@ -1222,6 +1226,12 @@ CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}" # FENCE_BOOTSTRAP
|
|
|
1222
1226
|
[ -n "${PR_NUMBER:-}" ] || echo "WARNING: PR_NUMBER is empty after reload (no staging_pr_number in $_SD): this fence cannot key its scratch files or post to a PR." # FENCE_BOOTSTRAP
|
|
1223
1227
|
# Reload the launched-agent list persisted at dispatch (validated: lowercase marker domains only).
|
|
1224
1228
|
[ -n "${LAUNCHED_REVIEW_AGENTS:-}" ] || { [ -r "$_SD/${PR_NUMBER}_launched_review_agents" ] && LAUNCHED_REVIEW_AGENTS=$(grep -E '^[a-z0-9-]+$' "$_SD/${PR_NUMBER}_launched_review_agents" | sort -u | tr '\n' ' '); }
|
|
1229
|
+
# Fail closed: a staging review always launches at least the Phase 2 material-change agent, so an empty list means lost state or skipped bookkeeping, never "nothing to verify".
|
|
1230
|
+
if [ -z "${PR_NUMBER:-}" ] || [ -z "${LAUNCHED_REVIEW_AGENTS:-}" ]; then
|
|
1231
|
+
echo "REVIEW DELIVERY FAILURE: launched-agent list is empty or unreadable (PR_NUMBER='${PR_NUMBER:-}')"
|
|
1232
|
+
echo "Recover the launched reviewer domains from the PR's FORGE:REVIEW-AGENT comments, re-append them to the launched-agent file, and rerun."
|
|
1233
|
+
exit 1
|
|
1234
|
+
fi
|
|
1225
1235
|
MISSING_AGENT_COMMENTS=""
|
|
1226
1236
|
# Each Bash call is a fresh shell: re-resolve the full head SHA if it is not set, and refuse when it is not a commit id
|
|
1227
1237
|
# (an empty SHA would make the Reviewed-SHA match accept any head).
|
|
@@ -1256,7 +1266,7 @@ if [ -n "$MISSING_AGENT_COMMENTS" ]; then
|
|
|
1256
1266
|
fi
|
|
1257
1267
|
```
|
|
1258
1268
|
|
|
1259
|
-
Missing comments are an explicit failure, never evidence of no findings. Do not triage or synthesize a deploy verdict while any launched agent is unaccounted for.
|
|
1269
|
+
An empty or unreadable launched-agent list is also a delivery failure (the fence exits 1), never evidence that nothing needed verifying; recover the list from the PR comments and rerun. Missing comments are an explicit failure, never evidence of no findings. Do not triage or synthesize a deploy verdict while any launched agent is unaccounted for.
|
|
1260
1270
|
|
|
1261
1271
|
### 7A: Extract Findings
|
|
1262
1272
|
Only findings inside trusted, current-head reviewer comments count: the head SHA is public, so an untrusted commenter must never be able to inject `FINDING` markers that get filed as issues. Read every page of comments, fail closed when the trust script or comments are unreadable (never fall through to "no findings").
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgedock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Autonomous development pipeline for coding agents. GitHub as institutional memory, with native Claude Code and OpenCode entrypoints.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forgedock": "bin/forgedock.mjs"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-spec-size.sh — PR spec-size budget (forge#3762).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
# Usage: check-spec-size.sh [base_ref] (default: $BASE_REF, else origin/staging)
|
|
7
|
+
# Env: PR_LABELS comma-separated PR label names (untrusted; grep-matched only)
|
|
8
|
+
# PR_BODY PR body text (untrusted; grep-matched only)
|
|
9
|
+
# GITHUB_STEP_SUMMARY summary file (stdout when unset)
|
|
10
|
+
# Exit: 0 pass/exempt, 1 budget violation, 2 usage/base-ref error (fail closed)
|
|
11
|
+
#
|
|
12
|
+
# Rules (against the PR base, compared in bytes):
|
|
13
|
+
# 1. Budgeted specs may not grow by more than 2% or more than 4096 bytes.
|
|
14
|
+
# 2. No commands/**/*.md file may gain a canonical block copy (per-file marker
|
|
15
|
+
# count may not rise). Count-based until the shared helper lands (#3493).
|
|
16
|
+
# Shrinking or equal size always passes.
|
|
17
|
+
# Exemption: `spec-size-exempt` label, or a `<!-- spec-size-exempt: <reason> -->`
|
|
18
|
+
# line with a non-empty reason in the PR body. Failures are reported, exit 0.
|
|
19
|
+
# Bash 3.2 portable: no assoc arrays, mapfile, grep -P.
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
|
|
22
|
+
BASE="${1:-${BASE_REF:-origin/staging}}"
|
|
23
|
+
PCT_NUM=2 # 2%
|
|
24
|
+
ABS_MAX=4096 # bytes
|
|
25
|
+
BUDGETED="commands/review-pr.md commands/review-pr-staging.md commands/work-on/remediate.md commands/work-on/review.md commands/work-on.md"
|
|
26
|
+
|
|
27
|
+
SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/stdout}"
|
|
28
|
+
out() { printf '%s\n' "$*" >> "$SUMMARY"; }
|
|
29
|
+
|
|
30
|
+
if ! git rev-parse --verify -q "$BASE^{commit}" >/dev/null; then
|
|
31
|
+
echo "ERROR: base ref '$BASE' cannot be resolved (shallow checkout? need fetch-depth: 0)" >&2
|
|
32
|
+
exit 2
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# --- exemption -------------------------------------------------------------
|
|
36
|
+
EXEMPT_REASON=""
|
|
37
|
+
if printf '%s' "${PR_LABELS:-}" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -qxF 'spec-size-exempt'; then
|
|
38
|
+
EXEMPT_REASON="label spec-size-exempt"
|
|
39
|
+
else
|
|
40
|
+
R=$(printf '%s\n' "${PR_BODY:-}" | tr -d '\r' \
|
|
41
|
+
| sed -n 's/^[[:space:]]*<!--[[:space:]]*spec-size-exempt:[[:space:]]*\(.*\)-->[[:space:]]*$/\1/p' \
|
|
42
|
+
| head -n 1 | sed 's/[[:space:]]*$//')
|
|
43
|
+
[ -n "$R" ] && EXEMPT_REASON="PR body marker: $R"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
FAILURES=0
|
|
47
|
+
base_size() { git cat-file -s "$BASE:$1" 2>/dev/null || echo 0; }
|
|
48
|
+
MARKERS='FORGE_ROOT bootstrap \(canonical|TRUSTED_SCRIPT=""|HELPER_SCRIPT=""|mkdir -p "\$_SD".*# FENCE_BOOTSTRAP'
|
|
49
|
+
|
|
50
|
+
out "## Spec-size budget (base: \`$BASE\`)"
|
|
51
|
+
out ""
|
|
52
|
+
out "| File | Base | Head | Delta | Delta % | Status |"
|
|
53
|
+
out "|------|-----:|-----:|------:|--------:|--------|"
|
|
54
|
+
for f in $BUDGETED; do
|
|
55
|
+
[ -f "$f" ] || [ "$(base_size "$f")" != 0 ] || continue
|
|
56
|
+
b=$(base_size "$f")
|
|
57
|
+
h=0; [ -f "$f" ] && h=$(wc -c < "$f" | tr -d ' ')
|
|
58
|
+
g=$((h - b))
|
|
59
|
+
status="ok"
|
|
60
|
+
if [ "$g" -gt 0 ] && { [ $((g * 100)) -gt $((b * PCT_NUM)) ] || [ "$g" -gt "$ABS_MAX" ]; }; then
|
|
61
|
+
status="OVER BUDGET"; FAILURES=$((FAILURES + 1))
|
|
62
|
+
fi
|
|
63
|
+
if [ "$b" -gt 0 ]; then
|
|
64
|
+
p=$(awk -v g="$g" -v b="$b" 'BEGIN{printf "%+.2f%%", g*100/b}')
|
|
65
|
+
else p="n/a"; fi
|
|
66
|
+
out "| \`$f\` | $b | $h | $g | $p | $status |"
|
|
67
|
+
done
|
|
68
|
+
|
|
69
|
+
DUP_ROWS=""
|
|
70
|
+
# Every commands/**/*.md present at head (new files have base count 0).
|
|
71
|
+
for f in $(git ls-files 'commands/*.md' 'commands/**/*.md' | sort -u); do
|
|
72
|
+
[ -f "$f" ] || continue
|
|
73
|
+
hc=$(grep -cE "$MARKERS" "$f" 2>/dev/null || true); hc=${hc:-0}
|
|
74
|
+
bc=$(git show "$BASE:$f" 2>/dev/null | grep -cE "$MARKERS" 2>/dev/null || true); bc=${bc:-0}
|
|
75
|
+
if [ "$hc" -gt "$bc" ]; then
|
|
76
|
+
DUP_ROWS="$DUP_ROWS| \`$f\` | $bc | $hc | NEW CANONICAL BLOCK COPY |
|
|
77
|
+
"
|
|
78
|
+
FAILURES=$((FAILURES + 1))
|
|
79
|
+
fi
|
|
80
|
+
done
|
|
81
|
+
if [ -n "$DUP_ROWS" ]; then
|
|
82
|
+
out ""
|
|
83
|
+
out "### Canonical block copies added (source the shared helper instead)"
|
|
84
|
+
out ""
|
|
85
|
+
out "| File | Base markers | Head markers | Status |"
|
|
86
|
+
out "|------|-----:|-----:|--------|"
|
|
87
|
+
printf '%s' "$DUP_ROWS" >> "$SUMMARY"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
out ""
|
|
91
|
+
if [ "$FAILURES" -eq 0 ]; then
|
|
92
|
+
out "Result: PASS"
|
|
93
|
+
exit 0
|
|
94
|
+
fi
|
|
95
|
+
if [ -n "$EXEMPT_REASON" ]; then
|
|
96
|
+
out "Result: EXEMPT ($FAILURES violation(s) waived: $EXEMPT_REASON)"
|
|
97
|
+
echo "spec-size: $FAILURES violation(s) waived: $EXEMPT_REASON"
|
|
98
|
+
exit 0
|
|
99
|
+
fi
|
|
100
|
+
out "Result: FAIL ($FAILURES violation(s)). Shrink the spec, or add the spec-size-exempt label / a \`<!-- spec-size-exempt: <reason> -->\` line to the PR body."
|
|
101
|
+
echo "spec-size: FAIL ($FAILURES violation(s)); see job summary" >&2
|
|
102
|
+
exit 1
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-spec-size.test.sh — cases for scripts/check-spec-size.sh (forge#3762).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
S="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/check-spec-size.sh"
|
|
7
|
+
PASS=0; FAILN=0
|
|
8
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
9
|
+
ok() { PASS=$((PASS+1)); }
|
|
10
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
11
|
+
|
|
12
|
+
git init -q --bare "$TMP/origin.git"
|
|
13
|
+
git clone -q "$TMP/origin.git" "$TMP/w" 2>/dev/null
|
|
14
|
+
cd "$TMP/w" || exit 1
|
|
15
|
+
git config user.email t@example.com; git config user.name t
|
|
16
|
+
mkdir -p commands/work-on
|
|
17
|
+
gen() { awk -v n="$1" 'BEGIN{for(i=0;i<n;i++) print "line of spec text number " i}'; }
|
|
18
|
+
gen 4000 > commands/review-pr.md # ~110 KB: 2% ~ 2.2 KB < 4 KB cap
|
|
19
|
+
gen 20000 > commands/work-on.md # ~560 KB: 2% ~ 11 KB, so the 4 KB cap binds
|
|
20
|
+
gen 100 > commands/other.md
|
|
21
|
+
gen 100 > commands/work-on/remediate.md
|
|
22
|
+
git add -A; git commit -qm base; git branch -M staging; git push -q origin staging 2>/dev/null
|
|
23
|
+
git checkout -q -b feat
|
|
24
|
+
|
|
25
|
+
reset() { git reset -q --hard origin/staging; git clean -qfd; }
|
|
26
|
+
run() { # name want_rc [env...]
|
|
27
|
+
local name="$1" want="$2"; shift 2
|
|
28
|
+
local sum="$TMP/summary"; : > "$sum"
|
|
29
|
+
env GITHUB_STEP_SUMMARY="$sum" PR_LABELS="" PR_BODY="" "$@" bash "$S" origin/staging >"$TMP/out" 2>"$TMP/err"
|
|
30
|
+
local rc=$?
|
|
31
|
+
if [ "$rc" = "$want" ]; then ok; else bad "$name (rc=$rc want $want)"; cat "$TMP/err"; fi
|
|
32
|
+
}
|
|
33
|
+
has() { grep -qF "$2" "$TMP/summary" && ok || bad "$1 (summary lacks: $2)"; }
|
|
34
|
+
|
|
35
|
+
reset; run "no change passes" 0
|
|
36
|
+
has "summary shows delta table" "| File | Base | Head | Delta |"
|
|
37
|
+
|
|
38
|
+
reset; gen 100 >> commands/review-pr.md # +~2.8 KB on ~110 KB = ~2.5% (>2%, <4 KB)
|
|
39
|
+
run "over 2% fails" 1; has "over-budget flagged" "OVER BUDGET"
|
|
40
|
+
|
|
41
|
+
reset; gen 200 >> commands/work-on.md # +~5.6 KB (<2% of 560 KB, >4 KB)
|
|
42
|
+
run "over 4 KB fails" 1
|
|
43
|
+
|
|
44
|
+
reset; gen 5 >> commands/review-pr.md # small growth under both limits
|
|
45
|
+
run "small growth passes" 0
|
|
46
|
+
|
|
47
|
+
reset; gen 5000 >> commands/other.md # not a budgeted spec
|
|
48
|
+
run "non-budgeted growth passes" 0
|
|
49
|
+
|
|
50
|
+
reset; gen 100 > commands/review-pr.md # shrink
|
|
51
|
+
run "shrink passes" 0
|
|
52
|
+
|
|
53
|
+
reset; printf 'TRUSTED_SCRIPT=""\n' >> commands/other.md
|
|
54
|
+
run "new TRUSTED_SCRIPT copy fails" 1; has "dup flagged" "NEW CANONICAL BLOCK COPY"
|
|
55
|
+
|
|
56
|
+
reset; printf '_SD=x; mkdir -p "$_SD" # FENCE_BOOTSTRAP\n' >> commands/other.md
|
|
57
|
+
run "new FENCE_BOOTSTRAP anchor fails" 1
|
|
58
|
+
|
|
59
|
+
reset; printf 'echo hi # FENCE_BOOTSTRAP\n' >> commands/other.md
|
|
60
|
+
run "non-anchor FENCE_BOOTSTRAP tag passes" 0
|
|
61
|
+
|
|
62
|
+
reset; printf '# FORGE_ROOT bootstrap (canonical)\n' > commands/new-spec.md; git add -A
|
|
63
|
+
run "new spec file with block fails" 1
|
|
64
|
+
|
|
65
|
+
# Existing block, count unchanged or reduced, passes
|
|
66
|
+
reset; printf 'HELPER_SCRIPT=""\n' >> commands/other.md; git commit -qam x; git push -q origin HEAD:staging 2>/dev/null
|
|
67
|
+
git fetch -q origin staging
|
|
68
|
+
run "unchanged count passes" 0
|
|
69
|
+
: > commands/other.md; git add -A
|
|
70
|
+
run "reduced count passes" 0
|
|
71
|
+
|
|
72
|
+
# Exemption
|
|
73
|
+
reset; gen 100 >> commands/review-pr.md
|
|
74
|
+
run "exempt by label" 0 PR_LABELS="bug,spec-size-exempt"; has "label reported" "EXEMPT"
|
|
75
|
+
run "exempt by body marker" 0 PR_BODY=$'text\n<!-- spec-size-exempt: security fix -->\nmore'; has "reason reported" "security fix"
|
|
76
|
+
run "empty-reason marker not exempt" 1 PR_BODY='<!-- spec-size-exempt: -->'
|
|
77
|
+
run "label substring not exempt" 1 PR_LABELS="spec-size-exempt-not"
|
|
78
|
+
|
|
79
|
+
# Fail closed on unresolvable base
|
|
80
|
+
: > "$TMP/summary"
|
|
81
|
+
GITHUB_STEP_SUMMARY="$TMP/summary" bash "$S" origin/does-not-exist >/dev/null 2>&1; rc=$?
|
|
82
|
+
[ "$rc" = 2 ] && ok || bad "unresolved base fails closed (rc=$rc)"
|
|
83
|
+
|
|
84
|
+
echo "check-spec-size.test: pass=$PASS fail=$FAILN"
|
|
85
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -113,6 +113,34 @@ grep -q 'test_gate_verdict' "$SPEC" && grep -q 'launched_review_agents' "$SPEC"
|
|
|
113
113
|
if grep -qE '\$_SD/(test_gate_verdict|test_gate_reason|launched_review_agents)|\.forge-scratch\}/launched_review_agents' "$SPEC"; then bad "gate/launched-agent scratch files must be keyed by \${PR_NUMBER}_"; else ok; fi
|
|
114
114
|
grep -qF ': > "$_SD/${PR_NUMBER}_launched_review_agents"' "$SPEC" && grep -qF 'rm -f "$_SD/${PR_NUMBER}_test_gate_verdict" "$_SD/${PR_NUMBER}_test_gate_reason"' "$SPEC" && ok || bad "Phase 2 must truncate and Phase 6.5 must rm -f the PR-keyed scratch files in executable commands"
|
|
115
115
|
if awk '/^### 7E:/,/^### 7F:/' "$SPEC" | grep -qF '{GH_REPO}'; then bad "7E must not use the {GH_REPO} template token"; else ok; fi
|
|
116
|
+
# (8b) forge#3733: Phase 6.75 fails closed on an empty/absent launched-agent list or empty PR_NUMBER; Phase 6 fails closed on a bad SELECTED_AGENT_COUNT
|
|
117
|
+
F675=$(awk '/^### Phase 6.75/,/^### 7A/' "$SPEC" | awk '/^```bash/{f=1;next} /^```/{if(f)exit} f')
|
|
118
|
+
GUARD_LN=$(printf '%s\n' "$F675" | grep -n 'launched-agent list is empty or unreadable' | head -1 | cut -d: -f1)
|
|
119
|
+
LOOP_LN=$(printf '%s\n' "$F675" | grep -n '^for AGENT_DOMAIN in' | head -1 | cut -d: -f1)
|
|
120
|
+
[ -n "$GUARD_LN" ] && [ -n "$LOOP_LN" ] && [ "$GUARD_LN" -lt "$LOOP_LN" ] && ok || bad "6.75 empty-list guard must precede the for AGENT_DOMAIN loop"
|
|
121
|
+
printf '%s\n' "$F675" | sed -n "1,$((LOOP_LN-1))p" | grep -v '^[[:space:]]*gh ' > "$TMP/f675.sh"
|
|
122
|
+
mkdir -p "$TMP/sd9/empty" "$TMP/sd9/full" "$TMP/bin9"
|
|
123
|
+
printf '#!/bin/sh\necho stub-value\n' > "$TMP/bin9/yq"; printf '#!/bin/sh\nexit 0\n' > "$TMP/bin9/gh"; chmod +x "$TMP/bin9/yq" "$TMP/bin9/gh"
|
|
124
|
+
: > "$TMP/sd9/empty/42_launched_review_agents"
|
|
125
|
+
printf 'security\n' > "$TMP/sd9/full/42_launched_review_agents"
|
|
126
|
+
run675() { ( unset LAUNCHED_REVIEW_AGENTS REVIEW_SHA_FULL; export PATH="$TMP/bin9:$PATH" FORGE_SCRATCHPAD="$1" HOME="$TMP/nohome"; [ -n "$2" ] && export PR_NUMBER="$2"; bash "$TMP/f675.sh" 2>&1; echo "rc=$?" ); }
|
|
127
|
+
for _case in "$TMP/sd9/empty|42" "$TMP/sd9/none|42" "$TMP/sd9/full|"; do
|
|
128
|
+
OUT9=$(run675 "${_case%%|*}" "${_case##*|}")
|
|
129
|
+
case "$OUT9" in *"REVIEW DELIVERY FAILURE: launched-agent list is empty or unreadable"*"rc=1"*) ok ;; *) bad "6.75 must exit 1 for case '$_case' (got '$OUT9')" ;; esac
|
|
130
|
+
done
|
|
131
|
+
OUT9=$(run675 "$TMP/sd9/full" 42)
|
|
132
|
+
case "$OUT9" in *"launched-agent list is empty"*) bad "6.75 guard must not fire for a non-empty list (got '$OUT9')" ;; *) ok ;; esac
|
|
133
|
+
# Phase 6 SELECTED_AGENT_COUNT validation: static order + behaviour of the reload/validate lines
|
|
134
|
+
F6=$(awk '/Selected isolated reviewers/{exit} {print}' "$SPEC" | awk '/^```bash/{b=""; f=1; next} /^```/{f=0; next} f{b=b $0 "\n"; last=b} END{printf "%s", last}')
|
|
135
|
+
printf '%s' "$F6" | grep -qF 'SELECTED_COUNT_INVALID' && ok || bad "Phase 6 fence must validate SELECTED_AGENT_COUNT (SELECTED_COUNT_INVALID)"
|
|
136
|
+
printf '%s' "$F6" | grep -F 'SELECTED_AGENT_COUNT=' | grep -F 'launched_review_agents' > "$TMP/sel.sh"
|
|
137
|
+
printf '%s\n' 'SELECTED_COUNT_INVALID=false' "$(printf '%s' "$F6" | grep -F 'case "${SELECTED_AGENT_COUNT:-}"')" > "$TMP/selcase.sh"
|
|
138
|
+
for _c in "$TMP/sd9/empty|42" "$TMP/sd9/none|42" "$TMP/sd9/full|42"; do
|
|
139
|
+
OUT9=$( ( unset SELECTED_AGENT_COUNT; _SD="${_c%%|*}"; PR_NUMBER="${_c##*|}"; . "$TMP/sel.sh"; . "$TMP/selcase.sh"; echo "$SELECTED_COUNT_INVALID|$SELECTED_AGENT_COUNT" ) 2>&1 )
|
|
140
|
+
case "${_c%%|*}" in */full) [ "$OUT9" = "false|1" ] && ok || bad "Phase 6: one launched agent must give SELECTED_AGENT_COUNT=1 (got '$OUT9')" ;; *) [ "$OUT9" = "true|0" ] && ok || bad "Phase 6: empty/absent list must be invalid (got '$OUT9')" ;; esac
|
|
141
|
+
done
|
|
142
|
+
OUT9=$( ( SELECTED_AGENT_COUNT="abc"; . "$TMP/selcase.sh"; echo "$SELECTED_COUNT_INVALID" ) 2>&1 )
|
|
143
|
+
[ "$OUT9" = "true" ] && ok || bad "Phase 6: non-integer SELECTED_AGENT_COUNT must be invalid (got '$OUT9')"
|
|
116
144
|
mkdir -p "$TMP/sd8/full" "$TMP/sd8/empty" "$TMP/sd8/bad" "$TMP/bin"
|
|
117
145
|
printf '#!/bin/sh\necho stub-value\n' > "$TMP/bin/yq"; chmod +x "$TMP/bin/yq"
|
|
118
146
|
printf '42\n' > "$TMP/sd8/full/staging_pr_number"; printf '7\n8\n' > "$TMP/sd8/full/42_bundle-prs.txt"
|