forgedock 1.16.0 → 1.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/phases.mjs +69 -15
- package/bin/engine/runlog.mjs +21 -3
- package/bin/engine.mjs +43 -7
- package/bin/runner.mjs +21 -2
- package/commands/forge-stats.md +2 -2
- package/commands/orchestrate/phase-1-resolve.md +6 -40
- package/commands/orchestrate/phase-4-execution.md +26 -162
- package/commands/quality-gate.md +12 -80
- package/commands/review-pr-staging.md +67 -301
- package/commands/review-pr.md +150 -752
- package/commands/work-on/build/validate.md +6 -40
- package/commands/work-on/build.md +82 -40
- package/commands/work-on/close.md +7 -22
- package/commands/work-on/decompose.md +8 -24
- package/commands/work-on/investigate.md +18 -29
- package/commands/work-on/remediate.md +38 -147
- package/commands/work-on/review.md +25 -42
- package/commands/work-on.md +13 -63
- package/docs/CONFIG.md +10 -7
- package/docs/RELEASE-NOTES.md +6 -0
- package/docs/WORK-ON-RUNTIME.md +1 -1
- package/docs/spec/forge-protocol-v1.md +1 -1
- package/package.json +1 -1
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/check-spec-size.sh +47 -27
- package/scripts/check-spec-size.test.sh +46 -3
- package/scripts/diff-size.sh +36 -3
- package/scripts/diff-size.test.sh +47 -0
- package/scripts/forge-root.test.sh +218 -91
- package/scripts/lib/fence-bootstrap.sh +16 -0
- package/scripts/lib/forge-resolve.sh +96 -0
- package/scripts/lib/review-delta-mode.sh +41 -0
- package/scripts/lint-dispatch-prompt.test.sh +3 -3
- package/scripts/remediate-liveness.sh +96 -0
- package/scripts/review-acceptance-gate.sh +132 -0
- package/scripts/review-acceptance-gate.test.sh +90 -0
- package/scripts/review-pr-delta-wiring.test.sh +24 -7
- package/scripts/review-pr-staging-scope.test.sh +14 -7
- package/scripts/spec-no-worktree-script.test.sh +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# forge-root.test.sh — guards the
|
|
2
|
+
# forge-root.test.sh — guards the shared forge_resolve stub that resolves FORGE_ROOT in every spec (forge#3098, #3778-#3781).
|
|
3
3
|
# Plugin installs set neither FORGEDOCK_HOME nor FORGE_HOME; the specs must resolve ForgeDock's OWN
|
|
4
4
|
# install root (never the consumer repo) and fail closed when it cannot be found.
|
|
5
5
|
# Usage: bash scripts/forge-root.test.sh
|
|
@@ -7,41 +7,23 @@
|
|
|
7
7
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
8
8
|
set -uo pipefail
|
|
9
9
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$HERE/.."
|
|
10
|
-
T=$(mktemp -d);
|
|
10
|
+
T=$(mktemp -d); T="$(cd "$T" && pwd -P)" # physical path: the helper reports pwd -P roots (macOS /var, Git Bash /tmp are symlinks)
|
|
11
|
+
trap 'rm -rf "$T"' EXIT
|
|
11
12
|
PASS=0; FAILN=0; SKIPPED=0
|
|
12
13
|
ok() { PASS=$((PASS+1)); }
|
|
13
14
|
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
14
15
|
expect() { [ "$2" = "$3" ] && ok || bad "$1 (got '$3' want '$2')"; }
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
STUB_SITES="commands/work-on.md commands/work-on/build/validate.md commands/quality-gate.md commands/work-on/review.md commands/work-on/investigate.md commands/work-on/decompose.md commands/work-on/build.md commands/work-on/close.md commands/orchestrate/phase-1-resolve.md commands/orchestrate/phase-4-execution.md commands/work-on/remediate.md commands/review-pr.md"
|
|
18
|
+
# Every site above carries the #3778 forge_resolve stub (no inlined FORGE_ROOT bootstrap copies; #3779, #3780, #3781), so every per-site hygiene check below applies to all of them.
|
|
19
|
+
# The pinned locator line is the one in review-pr-staging.md; the "FORGE_ROOT stub" the specs carry is that locator plus FORGE_ROOT="$(forge_root 2>/dev/null || true)".
|
|
20
|
+
STG_SPEC="$ROOT/commands/review-pr-staging.md"
|
|
21
|
+
sed 's/^[[:space:]]*//' "$STG_SPEC" | grep '^type forge_resolve >/dev/null' | sed 1q > "$T/locator.sh"
|
|
22
|
+
[ -s "$T/locator.sh" ] && ok || bad "pinned forge_resolve locator not found in review-pr-staging.md"
|
|
23
|
+
FR_LINE='FORGE_ROOT="$(forge_root 2>/dev/null || true)"'
|
|
24
|
+
# the unit under test: the real two-line stub the specs carry
|
|
25
|
+
{ cat "$T/locator.sh"; printf '%s\n' "$FR_LINE"; } > "$T/canon"
|
|
17
26
|
|
|
18
|
-
# extract the bootstrap block (comment line .. closing top-level fi), indentation stripped
|
|
19
|
-
# Block ends at the first line (after the marker) that starts with `fi` at the marker's own indentation.
|
|
20
|
-
# extract <file> <n> prints the n-th (1-based) bootstrap copy in the file.
|
|
21
|
-
extract() { awk -v want="$2" '/# FORGE_ROOT bootstrap/{c++; if(c==want){f=1; match($0,/^[ ]*/); ind=RLENGTH}} f{l=$0; sub(/^[ ]+/,"",l); print l} f&&match($0,/^[ ]*fi$/)&&RLENGTH-2==ind{exit}' "$ROOT/$1"; }
|
|
22
|
-
count_copies() { grep -c '# FORGE_ROOT bootstrap' "$ROOT/$1"; }
|
|
23
|
-
extract commands/work-on.md 1 > "$T/canon"
|
|
24
|
-
[ -s "$T/canon" ] && ok || bad "canonical bootstrap not found in work-on.md"
|
|
25
|
-
# Expected copies per file are DERIVED from the number of FORGE_ROOT resolution loops
|
|
26
|
-
# (`FORGE_ROOT="$_c"` lines) so adding a site never needs a hardcoded count here; count_copies
|
|
27
|
-
# (marker comments) must still agree with it, which catches a loop pasted without its marker.
|
|
28
|
-
want_copies() { grep -c 'FORGE_ROOT="\$_c"' "$ROOT/$1"; }
|
|
29
|
-
for f in $SITES; do
|
|
30
|
-
n=$(count_copies "$f"); expect "exact bootstrap copy count in $f" "$(want_copies "$f")" "$n"
|
|
31
|
-
i=1
|
|
32
|
-
while [ "$i" -le "$n" ]; do # compare EVERY copy
|
|
33
|
-
extract "$f" "$i" > "$T/s"
|
|
34
|
-
cmp -s "$T/canon" "$T/s" && ok || bad "bootstrap copy $i in $f differs from canonical"
|
|
35
|
-
i=$((i+1))
|
|
36
|
-
done
|
|
37
|
-
done
|
|
38
|
-
# the canonical block must not contain the stale lexical glob / unfiltered marketplaces glob
|
|
39
|
-
grep -v '^[[:space:]]*#' "$T/canon" > "$T/canon.code" # code only: the comments name the banned constructs
|
|
40
|
-
grep -qE 'mapfile|readarray|declare -A|sort[^|]* -[a-zA-Z]*V' "$T/canon.code" && bad "canonical bootstrap uses a bash-4+/non-portable construct" || ok
|
|
41
|
-
grep -q 'sort -k1,1nr -k2,2nr -k3,3nr' "$T/canon.code" && ok || bad "canonical bootstrap lacks numeric component version sort"
|
|
42
|
-
grep -qF 'marketplaces/*; do' "$T/canon" && bad "canonical bootstrap globs all marketplaces" || ok
|
|
43
|
-
grep -qF -- "-iname '*forgedock*'" "$T/canon.code" && bad "canonical bootstrap wildcard-matches marketplaces" || ok
|
|
44
|
-
grep -qF '[A-Za-z]:' "$T/canon.code" && ok || bad "canonical bootstrap lacks drive-letter path handling"
|
|
45
27
|
# review.md must hard-exit on an unreadable trail
|
|
46
28
|
grep -qE 'TRAIL_RC.* -ge 2 .*exit 1' "$ROOT/commands/work-on/review.md" && ok || bad "work-on/review.md lacks hard exit guard for TRAIL_RC>=2"
|
|
47
29
|
# behavioral: execute the guard line itself. rc>=2 => BLOCKED on STDOUT + exit 1; rc 0/1 => falls through.
|
|
@@ -57,7 +39,7 @@ for rc in 0 1; do
|
|
|
57
39
|
done
|
|
58
40
|
|
|
59
41
|
# no consumer-repo fallback / fail-open remains in the gates
|
|
60
|
-
for f in $
|
|
42
|
+
for f in $STUB_SITES; do
|
|
61
43
|
grep -nE 'FORGE_HOME:-\$?\{?\{?REPO_PATH\}*/scripts' "$ROOT/$f" >/dev/null && bad "repo-path fallback in $f" || ok
|
|
62
44
|
done
|
|
63
45
|
for f in commands/review-pr.md commands/work-on/review.md; do
|
|
@@ -84,10 +66,8 @@ if [ -z "${FORGE_ROOT_TEST_SHELLS:-}" ] && command -v zsh >/dev/null 2>&1; then
|
|
|
84
66
|
for SH in $SHELLS; do
|
|
85
67
|
echo "== bootstrap behavior under: $SH"
|
|
86
68
|
mkdir -p "$T/consumer/scripts"; : > "$T/consumer/scripts/verify-phase-trail.sh" # hostile/lookalike consumer repo
|
|
87
|
-
mkscripts() {
|
|
88
|
-
mkdir -p "$1/scripts" "$1/
|
|
89
|
-
: > "$1/scripts/verify-phase-trail.sh"; : > "$1/scripts/lint-dispatch-prompt.sh"; : > "$1/scripts/is-docs-only.sh"
|
|
90
|
-
: > "$1/bin/engine/resolve.mjs"; : > "$1/bin/engine/orchestrate-canary.mjs"; : > "$1/bin/engine/admission.mjs"
|
|
69
|
+
mkscripts() { # a minimal install root: forge_root accepts the first root that carries scripts/lib/forge-resolve.sh
|
|
70
|
+
mkdir -p "$1/scripts/lib"; cp "$ROOT/scripts/lib/forge-resolve.sh" "$1/scripts/lib/forge-resolve.sh"
|
|
91
71
|
}
|
|
92
72
|
# plugin install: no env vars at all
|
|
93
73
|
mkscripts "$T/h1/.claude/plugins/cache/forgedock/forgedock/1.0.0"
|
|
@@ -96,8 +76,9 @@ expect "CLAUDE_PLUGIN_ROOT resolves" "$T/h1/.claude/plugins/cache/forgedock/forg
|
|
|
96
76
|
# nothing installed: empty (never the consumer repo, never a relative path)
|
|
97
77
|
expect "unresolvable stays empty" "" "$(run "$T/empty")"
|
|
98
78
|
expect "relative FORGE_HOME ignored" "" "$(run "$T/empty" FORGE_HOME=.)"
|
|
99
|
-
# explicit FORGEDOCK_HOME is authoritative, even if bogus (caller then fails closed)
|
|
100
|
-
expect "FORGEDOCK_HOME authoritative" "
|
|
79
|
+
# explicit FORGEDOCK_HOME is authoritative, even if bogus: nothing else is consulted, so the root is empty (caller then fails closed)
|
|
80
|
+
expect "FORGEDOCK_HOME authoritative (bogus => empty, no other tier)" "" "$(run "$T/h1" FORGEDOCK_HOME="$T/nowhere")"
|
|
81
|
+
mkscripts "$T/hfh/pinned"; expect "FORGEDOCK_HOME pinning a real install resolves it" "$T/hfh/pinned" "$(run "$T/h1" FORGEDOCK_HOME="$T/hfh/pinned")"
|
|
101
82
|
# symlink install (install.sh): ~/.claude/commands/work-on.md -> <clone>/commands/work-on.md
|
|
102
83
|
mkdir -p "$T/clone/commands" "$T/h2/.claude/commands"; mkscripts "$T/clone"; : > "$T/clone/commands/work-on.md"
|
|
103
84
|
# Git Bash (MSYS) makes a COPY for `ln -s` unless native symlinks are requested, which leaves readlink
|
|
@@ -114,9 +95,11 @@ fi
|
|
|
114
95
|
# forge#3758 (hgate is a fixture of its own: the loop runs once per shell and h14 is reused below with other versions): deploy-gate script resolves on a plugin-only install (cache only, no symlink, no FORGE_* env).
|
|
115
96
|
# The gate-script line is taken verbatim from the spec so the test exercises exactly what ships.
|
|
116
97
|
mkscripts "$T/hgate/.claude/plugins/cache/forgedock/forgedock/1.0.0"; : > "$T/hgate/.claude/plugins/cache/forgedock/forgedock/1.0.0/scripts/resolve-deploy-gate.sh"
|
|
117
|
-
|
|
98
|
+
# review-pr.md resolves the gate script through the forge_resolve stub (#3780): the locator and the call line are taken verbatim from the spec.
|
|
99
|
+
sed 's/^[[:space:]]*//' "$ROOT/commands/review-pr.md" | grep '^GATE_SCRIPT=\$(forge_resolve resolve-deploy-gate.sh)$' | sort -u > "$T/gs"
|
|
118
100
|
expect "gate-script resolver line is identical across sites" 1 "$(wc -l < "$T/gs" | tr -d ' ')"
|
|
119
|
-
|
|
101
|
+
{ sed 's/^[[:space:]]*//' "$ROOT/commands/review-pr.md" | grep -m1 '^type forge_resolve >/dev/null'; cat "$T/gs"; } > "$T/gs_full"
|
|
102
|
+
gate_run() { ( cd "$T/consumer" && env -i PATH="$PATH" HOME="$1" "$SH" -c "$(cat "$T/gs_full"); printf %s \"\$GATE_SCRIPT\"" ); }
|
|
120
103
|
expect "plugin-only install resolves resolve-deploy-gate.sh" "$T/hgate/.claude/plugins/cache/forgedock/forgedock/1.0.0/scripts/resolve-deploy-gate.sh" "$(gate_run "$T/hgate")"
|
|
121
104
|
expect "unresolved gate script stays empty (strict)" "" "$(gate_run "$T/empty")"
|
|
122
105
|
# cwd/consumer repo is never a candidate even when it carries the gate script
|
|
@@ -137,18 +120,12 @@ expect "space in HOME resolves" "$T/sp ace/.claude/plugins/cache/forgedock/forge
|
|
|
137
120
|
# relative FORGEDOCK_HOME / CLAUDE_PLUGIN_ROOT rejected (fail closed => empty)
|
|
138
121
|
expect "relative FORGEDOCK_HOME rejected" "" "$(run "$T/h1" FORGEDOCK_HOME=scripts/..)"
|
|
139
122
|
expect "relative CLAUDE_PLUGIN_ROOT ignored" "" "$(run "$T/empty" CLAUDE_PLUGIN_ROOT=../consumer)"
|
|
140
|
-
# a candidate
|
|
123
|
+
# a candidate without the helper is rejected (a bare scripts/ dir is not an install root)
|
|
141
124
|
mkdir -p "$T/h4/.claude/plugins/cache/forgedock/forgedock/1.0.0/scripts"; : > "$T/h4/.claude/plugins/cache/forgedock/forgedock/1.0.0/scripts/verify-phase-trail.sh"
|
|
142
|
-
expect "partial install (
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
expect "stale install missing $missing rejected" "" "$(run "$T/h12")"
|
|
147
|
-
done
|
|
148
|
-
# a stale newest version is skipped in favor of a complete older one
|
|
149
|
-
rm -rf "$T/h13"; mkscripts "$T/h13/.claude/plugins/cache/forgedock/forgedock/1.0.0"; mkscripts "$T/h13/.claude/plugins/cache/forgedock/forgedock/2.0.0"
|
|
150
|
-
rm -f "$T/h13/.claude/plugins/cache/forgedock/forgedock/2.0.0/scripts/is-docs-only.sh"
|
|
151
|
-
expect "stale newest skipped for complete older" "$T/h13/.claude/plugins/cache/forgedock/forgedock/1.0.0" "$(run "$T/h13")"
|
|
125
|
+
expect "partial install (no forge-resolve.sh helper) rejected" "" "$(run "$T/h4")"
|
|
126
|
+
# a newest version without the helper is skipped in favor of an older one that has it
|
|
127
|
+
rm -rf "$T/h13"; mkscripts "$T/h13/.claude/plugins/cache/forgedock/forgedock/1.0.0"; mkdir -p "$T/h13/.claude/plugins/cache/forgedock/forgedock/2.0.0/scripts"
|
|
128
|
+
expect "newest without the helper skipped for older with it" "$T/h13/.claude/plugins/cache/forgedock/forgedock/1.0.0" "$(run "$T/h13")"
|
|
152
129
|
# non-ForgeDock marketplace dirs are ignored; ForgeDock-named ones resolve
|
|
153
130
|
mkscripts "$T/h5/.claude/plugins/marketplaces/other-tool"
|
|
154
131
|
expect "non-ForgeDock marketplace ignored" "" "$(run "$T/h5")"
|
|
@@ -160,13 +137,13 @@ rm -rf "$T/h7" # fixture is mutated below; reset per shell
|
|
|
160
137
|
mkscripts "$T/h7/.claude/plugins/cache/forgedock/forgedock/1.9.0-rc1"; mkscripts "$T/h7/.claude/plugins/cache/forgedock/forgedock/1.9.0"
|
|
161
138
|
expect "release beats its pre-release" "$T/h7/.claude/plugins/cache/forgedock/forgedock/1.9.0" "$(run "$T/h7")"
|
|
162
139
|
mkscripts "$T/h7/.claude/plugins/cache/forgedock/forgedock/2.0.0-rc1"
|
|
163
|
-
|
|
140
|
+
# the shared helper ranks released x.y.z dirs only: a newer-core pre-release never outranks the newest release
|
|
141
|
+
expect "newer-core pre-release does not outrank the newest release" "$T/h7/.claude/plugins/cache/forgedock/forgedock/1.9.0" "$(run "$T/h7")"
|
|
164
142
|
# marketplace pinning: a higher-versioned forgedock plugin from a non-official marketplace is never used
|
|
165
143
|
mkscripts "$T/h8/.claude/plugins/cache/evil-market/forgedock/99.0.0"; mkscripts "$T/h8/.claude/plugins/cache/forgedock/forgedock/1.1.0"
|
|
166
144
|
expect "hostile marketplace at 99.0.0 ignored" "$T/h8/.claude/plugins/cache/forgedock/forgedock/1.1.0" "$(run "$T/h8")"
|
|
167
145
|
rm -rf "$T/h8b"; mkscripts "$T/h8b/.claude/plugins/cache/evil-market/forgedock/99.0.0"
|
|
168
146
|
expect "only a hostile marketplace => empty" "" "$(run "$T/h8b")"
|
|
169
|
-
expect "FORGEDOCK_MARKETPLACE pins another marketplace" "$T/h8b/.claude/plugins/cache/evil-market/forgedock/99.0.0" "$(run "$T/h8b" FORGEDOCK_MARKETPLACE=evil-market)"
|
|
170
147
|
expect "invalid FORGEDOCK_MARKETPLACE falls back to forgedock" "" "$(run "$T/h8b" FORGEDOCK_MARKETPLACE=..)"
|
|
171
148
|
rm -rf "$T/h8c"; mkscripts "$T/h8c/.claude/plugins/marketplaces/evil-forgedock-tools"
|
|
172
149
|
expect "wildcard-named marketplace dir ignored" "" "$(run "$T/h8c")"
|
|
@@ -174,21 +151,16 @@ expect "wildcard-named marketplace dir ignored" "" "$(run "$T/h8c")"
|
|
|
174
151
|
rm -rf "$T/h14"; P14="$T/h14/.claude/plugins/cache/forgedock/forgedock"
|
|
175
152
|
mkscripts "$P14/1.10.0"; mkscripts "$P14/1e5abcdef"; mkscripts "$P14/abc1234def"; mkscripts "$P14/6f3a9c0"
|
|
176
153
|
expect "SHA-named cache dirs ignored" "$P14/1.10.0" "$(run "$T/h14")"
|
|
177
|
-
rm -rf "$T/h14b"; mkscripts "$T/h14b/.claude/plugins/cache/forgedock/forgedock/abc1234def"
|
|
178
|
-
expect "only SHA-named dir => empty" "" "$(run "$T/h14b")"
|
|
179
154
|
mkscripts "$P14/1.10.0-rc1"; expect "pre-release dir name still accepted (release wins)" "$P14/1.10.0" "$(run "$T/h14")"
|
|
180
|
-
# Windows drive-letter FORGEDOCK_HOME
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
155
|
+
# Windows drive-letter FORGEDOCK_HOME normalization lives in the helper (_forge_posix, pinned in the #3778 section below)
|
|
156
|
+
# SEC-1 (#3781): the stub's own first candidate normalizes a drive-letter FORGEDOCK_HOME (cygpath), so the helper loads from that root.
|
|
157
|
+
rm -rf "$T/hwin"; mkscripts "$T/hwin/c/forge"; mkdir -p "$T/fakebin"
|
|
158
|
+
printf '#!/bin/sh\n[ "$1" = "-u" ] && case "$2" in C:/forge) echo "%s"; exit 0;; esac\nexit 1\n' "$T/hwin/c/forge" > "$T/fakebin/cygpath"; chmod +x "$T/fakebin/cygpath"
|
|
159
|
+
expect "C:/ FORGEDOCK_HOME resolves through the stub (cygpath)" "$T/hwin/c/forge" "$(run "$T/empty" PATH="$T/fakebin:$PATH" FORGEDOCK_HOME='C:/forge')"
|
|
184
160
|
# Codex: forge-home pointer file written by install-codex.sh
|
|
185
161
|
mkscripts "$T/hx/clone"; mkdir -p "$T/hx/.codex"; printf '%s\n' "$T/hx/clone" > "$T/hx/.codex/forge-home"
|
|
186
162
|
expect "codex forge-home pointer resolves" "$T/hx/clone" "$(run "$T/hx")"
|
|
187
163
|
expect "CODEX_HOME override resolves" "$T/hx/clone" "$(run "$T/empty" CODEX_HOME="$T/hx/.codex")"
|
|
188
|
-
# A relative CODEX_HOME must not resolve the pointer against the consumer cwd
|
|
189
|
-
mkdir -p "$T/consumer/.codex"; printf '%s\n' "$T/hx/clone" > "$T/consumer/.codex/forge-home"
|
|
190
|
-
expect "relative CODEX_HOME ignored (cwd-local pointer not read)" "" "$(run "$T/empty" CODEX_HOME=.codex)"
|
|
191
|
-
rm -rf "$T/consumer/.codex"
|
|
192
164
|
printf 'relative/path\n' > "$T/hx/.codex/forge-home"
|
|
193
165
|
expect "relative forge-home pointer rejected" "" "$(run "$T/hx")"
|
|
194
166
|
# install-codex.sh: FORGE_HOME is shell-escaped in env files, and absent env files are not created (forge#3241)
|
|
@@ -207,7 +179,7 @@ grep -qF "\$'" "$T/ic-home/.zshenv" && bad "env file contains bash-only \$'...'
|
|
|
207
179
|
expect "pointer file still written" "$IC_REAL" "$(cat "$T/ic-home/.codex/forge-home" 2>/dev/null)"
|
|
208
180
|
# errexit/pipefail safety: absent readlink target, absent cache dir, no ~/.claude at all
|
|
209
181
|
for opts in "-e" "-eo pipefail" "-euo pipefail"; do
|
|
210
|
-
expect "survives set $opts with nothing installed" "ok:" "$( cd "$T/consumer" && env -i PATH="$PATH" HOME="$T/empty" "$SH" -c "set $opts; $(cat "$T/canon"); printf 'ok:%s' \"\$FORGE_ROOT\"" 2>&1 )"
|
|
182
|
+
expect "survives set $opts with nothing installed (no helper at all, no stderr noise)" "ok:" "$( cd "$T/consumer" && env -i PATH="$PATH" HOME="$T/empty" "$SH" -c "set $opts; $(cat "$T/canon"); printf 'ok:%s' \"\$FORGE_ROOT\"" 2>&1 )"
|
|
211
183
|
expect "resolves under set $opts" "ok:$T/h3/.claude/plugins/cache/forgedock/forgedock/1.10.0" "$( cd "$T/consumer" && env -i PATH="$PATH" HOME="$T/h3" "$SH" -c "set $opts; $(cat "$T/canon"); printf 'ok:%s' \"\$FORGE_ROOT\"" 2>&1 )"
|
|
212
184
|
done
|
|
213
185
|
# a non-forgedock plugin in the cache is ignored even with a higher version
|
|
@@ -218,22 +190,29 @@ mkdir -p "$T/h10/.claude/plugins/cache"
|
|
|
218
190
|
expect "empty cache, no match, no abort" "" "$(run "$T/h10")"
|
|
219
191
|
expect "no ~/.claude at all, no abort" "" "$(run "$T/empty")"
|
|
220
192
|
expect "unmatched glob does not break later candidates" "$T/h1/.claude/plugins/cache/forgedock/forgedock/1.0.0" "$(run "$T/h10" CLAUDE_PLUGIN_ROOT="$T/h1/.claude/plugins/cache/forgedock/forgedock/1.0.0")"
|
|
221
|
-
# plugin root (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
expect "substituted plugin root beats FORGE_HOME" "$T/h11/running-plugin" "$(run "$T/h11" FORGE_HOME="$T/h11/stale-clone" CLAUDE_PLUGIN_ROOT="$T/h11/running-plugin")"
|
|
193
|
+
# substituted plugin root alone resolves (the locator loads that root's helper; the stub also hands it to the helper as FORGE_SOURCED_ROOT, so it outranks FORGE_HOME and any newer cache; see forge-resolve.sh)
|
|
194
|
+
mkscripts "$T/h11/running-plugin"; mkscripts "$T/h11/stale-clone"
|
|
195
|
+
expect "substituted plugin root resolves" "$T/h11/running-plugin" "$(run "$T/h11" CLAUDE_PLUGIN_ROOT="$T/h11/running-plugin")"
|
|
225
196
|
# unsubstituted placeholder (non-Claude runtime) is a literal, rejected by the /* check, never expanded
|
|
226
197
|
expect "unsubstituted placeholder ignored, FORGE_HOME used" "$T/h11/stale-clone" "$(run "$T/h11" FORGE_HOME="$T/h11/stale-clone")"
|
|
227
198
|
expect "unsubstituted placeholder safe under set -u" "" "$( cd "$T/consumer" && env -i PATH="$PATH" HOME="$T/empty" "$SH" -c "set -u; $(cat "$T/canon"); printf %s \"\$FORGE_ROOT\"" 2>&1 )"
|
|
199
|
+
# running plugin root (substituted) beats a newer cache version and an exported FORGE_HOME (version skew)
|
|
200
|
+
mkscripts "$T/h12/.claude/plugins/cache/forgedock/forgedock/1.16.0"; mkscripts "$T/h12/.claude/plugins/cache/forgedock/forgedock/1.17.0"; mkscripts "$T/h12/stale-clone"
|
|
201
|
+
expect "running plugin root beats newer cache" "$T/h12/.claude/plugins/cache/forgedock/forgedock/1.16.0" "$(run "$T/h12" CLAUDE_PLUGIN_ROOT="$T/h12/.claude/plugins/cache/forgedock/forgedock/1.16.0")"
|
|
202
|
+
expect "running plugin root beats exported FORGE_HOME" "$T/h11/running-plugin" "$(run "$T/h11" CLAUDE_PLUGIN_ROOT="$T/h11/running-plugin" FORGE_HOME="$T/h11/stale-clone")"
|
|
203
|
+
expect "running plugin root beats exported FORGE_ROOT" "$T/h11/running-plugin" "$(run "$T/h11" CLAUDE_PLUGIN_ROOT="$T/h11/running-plugin" FORGE_ROOT="$T/h11/stale-clone")"
|
|
204
|
+
expect "FORGEDOCK_HOME still authoritative over running root" "$T/h11/stale-clone" "$(run "$T/h11" CLAUDE_PLUGIN_ROOT="$T/h11/running-plugin" FORGEDOCK_HOME="$T/h11/stale-clone")"
|
|
205
|
+
expect "unsubstituted literal ignored, newest cache wins" "$T/h12/.claude/plugins/cache/forgedock/forgedock/1.17.0" "$(run "$T/h12")"
|
|
206
|
+
expect "relative running root rejected" "$T/h11/stale-clone" "$(run "$T/h11" CLAUDE_PLUGIN_ROOT="relative/running-plugin" FORGE_HOME="$T/h11/stale-clone")"
|
|
228
207
|
# the env var alone (no substitution) must NOT be relied on: Claude Code does not export it to Bash
|
|
229
208
|
expect "CLAUDE_PLUGIN_ROOT env var alone is not a candidate" "" "$( cd "$T/consumer" && env -i PATH="$PATH" HOME="$T/empty" CLAUDE_PLUGIN_ROOT="$T/h11/running-plugin" "$SH" -c "$(cat "$T/canon"); printf %s \"\$FORGE_ROOT\"" )"
|
|
230
209
|
done
|
|
231
|
-
# the
|
|
232
|
-
grep -qF "'\${CLAUDE_PLUGIN_ROOT}'" "$T/
|
|
233
|
-
grep -qF 'CLAUDE_PLUGIN_ROOT:-' "$T/
|
|
210
|
+
# the pinned locator must use the exact substitutable spelling, single-quoted (no :- form, which Claude Code leaves verbatim)
|
|
211
|
+
grep -qF "'\${CLAUDE_PLUGIN_ROOT}'" "$T/locator.sh" && ok || bad "stub locator lacks the substitutable '\${CLAUDE_PLUGIN_ROOT}' candidate"
|
|
212
|
+
grep -qF 'CLAUDE_PLUGIN_ROOT:-' "$T/locator.sh" && bad "stub locator uses \${CLAUDE_PLUGIN_ROOT:-}, which Claude Code never substitutes" || ok
|
|
234
213
|
|
|
235
214
|
# no hand-built file:// URL from FORGE_ROOT remains; imports go through pathToFileURL (a '#', '?' or space in the path must survive)
|
|
236
|
-
for f in $
|
|
215
|
+
for f in $STUB_SITES; do
|
|
237
216
|
grep -nE 'file://\$\{?(FORGE_ROOT|\(pwd\))' "$ROOT/$f" >/dev/null && bad "concatenated file:// URL in $f" || ok
|
|
238
217
|
done
|
|
239
218
|
if command -v node >/dev/null 2>&1; then
|
|
@@ -263,7 +242,7 @@ while IFS= read -r f; do
|
|
|
263
242
|
arms=$(grep -c 'trusted-comments.sh" \]' "$f" || true)
|
|
264
243
|
[ "$arms" = "$(cat "$T/tc_n")" ] || { TC_STRAY=$((TC_STRAY + 1)); echo " stray resolver in $f: $arms arms vs $(cat "$T/tc_n") blocks"; }
|
|
265
244
|
done < <(grep -rl 'trusted-comments.sh" \]' "$ROOT/commands")
|
|
266
|
-
expect "trusted-comments.sh resolver blocks discovered (review
|
|
245
|
+
expect "trusted-comments.sh resolver blocks discovered (review + phase-4; review-pr, review-pr-staging and remediate use the forge_resolve stub)" 2 "$TC_BLOCKS"
|
|
267
246
|
expect "trusted-comments.sh resolvers without a canonical block" 0 "$TC_STRAY"
|
|
268
247
|
expect "trusted-comments.sh resolvers byte-identical" 1 "$(sort -u "$T/tc_all" | wc -l | tr -d ' ')"
|
|
269
248
|
grep -qF 'plugins/cache/forgedock/forgedock' "$T/tc_all" && ok || bad 'trusted-comments.sh resolver lacks the plugin-cache scan'
|
|
@@ -292,14 +271,18 @@ extract_helper_block() { # file var -> block on stdout
|
|
|
292
271
|
extract_helper_fn() { # file -> function on stdout
|
|
293
272
|
awk '/^resolve_extract_affected_files\(\) \{/ {inb=1} inb {print} inb && /^\}$/ {exit}' "$1"
|
|
294
273
|
}
|
|
274
|
+
# review-pr.md resolves its helpers through the forge_resolve stub (#3780): the pinned locator plus the call line, taken from the spec.
|
|
275
|
+
stub_block() { # file var -> locator + the first `var=$(forge_resolve ...)` line, indentation stripped
|
|
276
|
+
sed 's/^[[:space:]]*//' "$1" | grep -m1 '^type forge_resolve >/dev/null'; sed 's/^[[:space:]]*//' "$1" | grep -m1 "^$2=\$(forge_resolve "
|
|
277
|
+
}
|
|
295
278
|
HR_CLASSIFY="$ROOT/commands/review-pr.md"
|
|
296
279
|
HR_AMP="$ROOT/commands/orchestrate/phase-4-execution.md"
|
|
297
280
|
HR_EX3="$ROOT/commands/orchestrate/phase-3-dependency.md"
|
|
298
|
-
|
|
281
|
+
stub_block "$HR_CLASSIFY" CLASSIFY_SCRIPT > "$T/hr_classify.sh"
|
|
299
282
|
extract_helper_block "$HR_AMP" AMP_SCRIPT > "$T/hr_amp.sh"
|
|
300
283
|
extract_helper_fn "$HR_AMP" > "$T/hr_ex4.sh"
|
|
301
284
|
extract_helper_fn "$HR_EX3" > "$T/hr_ex3.sh"
|
|
302
|
-
expect "CLASSIFY_SCRIPT resolver
|
|
285
|
+
expect "CLASSIFY_SCRIPT resolver stub found" 1 "$(grep -c '^CLASSIFY_SCRIPT=\$(forge_resolve classify-finding.sh)$' "$T/hr_classify.sh")"
|
|
303
286
|
expect "AMP_SCRIPT resolver block found" 1 "$(grep -c '^AMP_SCRIPT=""' "$T/hr_amp.sh")"
|
|
304
287
|
expect "extract-affected-files resolver found in phase-4" 1 "$(grep -c 'resolve_extract_affected_files() {' "$T/hr_ex4.sh")"
|
|
305
288
|
expect "extract-affected-files resolver found in phase-3" 1 "$(grep -c 'resolve_extract_affected_files() {' "$T/hr_ex3.sh")"
|
|
@@ -313,7 +296,8 @@ done
|
|
|
313
296
|
if grep -rnE 'PWD/scripts/|^\$PWD$|REPO_PATH/scripts/extract' "$ROOT/commands" >/dev/null 2>&1; then bad 'commands/ still lists a working-directory helper tier'; else ok; fi
|
|
314
297
|
|
|
315
298
|
# Behavior (#3483): a planted scripts/<helper>.sh in the cwd must never be selected; a cache-only install must resolve.
|
|
316
|
-
mkdir -p "$T/hrwork/scripts" "$T/hrhome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts"
|
|
299
|
+
mkdir -p "$T/hrwork/scripts" "$T/hrhome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts/lib"
|
|
300
|
+
cp "$ROOT/scripts/lib/forge-resolve.sh" "$T/hrhome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts/lib/forge-resolve.sh"
|
|
317
301
|
for h in classify-finding amplification-breaker extract-affected-files; do
|
|
318
302
|
: > "$T/hrwork/scripts/$h.sh"; : > "$T/hrhome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts/$h.sh"
|
|
319
303
|
done
|
|
@@ -339,23 +323,20 @@ hr_check_block() { # label file var script
|
|
|
339
323
|
grep -qE '\$PWD|\$REPO_PATH|\$\{PWD|\$\{REPO_PATH' "$T/hr_blk.sh" && bad "$1: $3 resolver lists the working directory or repo path" || ok
|
|
340
324
|
grep -qF "scripts/$4" "$T/hr_blk.sh" && ok || bad "$1: $3 resolver does not resolve scripts/$4"
|
|
341
325
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
326
|
+
hr_check_stub() { # label file var script: the spec calls the script through the forge_resolve stub (locator in the same fence)
|
|
327
|
+
stub_block "$2" "$3" > "$T/hr_blk.sh"
|
|
328
|
+
expect "$1: $3 resolver stub found" "$3=\$(forge_resolve $4)" "$(sed -n 2p "$T/hr_blk.sh")"
|
|
329
|
+
grep -qE '\$PWD|\$REPO_PATH|\$\{PWD|\$\{REPO_PATH' "$T/hr_blk.sh" && bad "$1: $3 resolver lists the working directory or repo path" || ok
|
|
330
|
+
local needle="$3=\$(forge_resolve " # kept out of the awk command substitution: bash 3.2 (macOS) mis-parses the nested quote and paren
|
|
331
|
+
expect "$1: $3 call without a locator in the same fence" 0 "$(awk -v n="$needle" '/^[ ]*```/{loc=0} /^[ ]*type forge_resolve >\/dev\/null/{loc=1} index($0, n)>0 && !loc {b++} END{print b+0}' "$2")"
|
|
332
|
+
}
|
|
333
|
+
hr_check_stub review-pr "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT derive-finding-milestone.sh
|
|
334
|
+
hr_check_stub review-pr "$HR_CLASSIFY" PRIORITY_SCRIPT severity-to-priority.sh
|
|
335
|
+
hr_check_stub review-pr "$HR_CLASSIFY" CODE_INDEX_SCRIPT code-index.sh
|
|
336
|
+
hr_check_stub review-pr "$HR_CLASSIFY" DEDUP_SCRIPT issue-dedup.sh
|
|
346
337
|
hr_check_block issue "$ROOT/commands/issue.md" DEDUP_SCRIPT issue-dedup.sh
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
extract_helper_block "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT > "$T/hr_dm.sh"
|
|
350
|
-
extract_helper_block "$HR_CLASSIFY" PRIORITY_SCRIPT > "$T/hr_pr.sh"
|
|
351
|
-
extract_helper_block "$HR_STG" DERIVE_MILESTONE_SCRIPT > "$T/hr_dm_stg.sh"
|
|
352
|
-
extract_helper_block "$HR_STG" PRIORITY_SCRIPT > "$T/hr_pr_stg.sh"
|
|
353
|
-
# only the variable-name-specific text may differ, and review-pr/review-pr-staging must agree
|
|
354
|
-
cmp -s "$T/hr_dm.sh" "$T/hr_dm_stg.sh" && ok || bad 'DERIVE_MILESTONE_SCRIPT resolver differs between review-pr and review-pr-staging'
|
|
355
|
-
cmp -s "$T/hr_pr.sh" "$T/hr_pr_stg.sh" && ok || bad 'PRIORITY_SCRIPT resolver differs between review-pr and review-pr-staging'
|
|
356
|
-
extract_helper_block "$HR_CLASSIFY" DEDUP_SCRIPT > "$T/hr_dd.sh"
|
|
357
|
-
extract_helper_block "$ROOT/commands/issue.md" DEDUP_SCRIPT > "$T/hr_dd_issue.sh"
|
|
358
|
-
cmp -s "$T/hr_dd.sh" "$T/hr_dd_issue.sh" && ok || bad 'DEDUP_SCRIPT resolver differs between review-pr and issue'
|
|
338
|
+
stub_block "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT > "$T/hr_dm.sh"
|
|
339
|
+
stub_block "$HR_CLASSIFY" PRIORITY_SCRIPT > "$T/hr_pr.sh"
|
|
359
340
|
# no bare cwd-relative helper call in either review spec
|
|
360
341
|
if grep -nE 'bash scripts/(derive-finding-milestone|severity-to-priority)\.sh' "$HR_CLASSIFY" "$HR_STG" >/dev/null 2>&1; then bad 'review spec still calls a helper by a bare scripts/ path'; else ok; fi
|
|
361
342
|
|
|
@@ -400,6 +381,8 @@ done
|
|
|
400
381
|
# tiers: plugin-cache scan and CLAUDE_CONFIG_DIR present, no "$PWD"/"$REPO_PATH" tier. Passes with 0 blocks today.
|
|
401
382
|
RD_BLOCKS=0
|
|
402
383
|
while IFS= read -r f; do
|
|
384
|
+
# a forge_resolve stub (scripts/lib/forge-resolve.sh owns the tiers) satisfies the guard
|
|
385
|
+
if grep -qE '^[[:space:]]*REVIEW_DELTA_SCRIPT=\$\(forge_resolve review-delta\.sh\)$' "$f"; then RD_BLOCKS=$((RD_BLOCKS + 1)); continue; fi
|
|
403
386
|
awk '/^[ ]*# HELPER_SCRIPT resolver \(/ && index($0, "(REVIEW_DELTA_SCRIPT;") {inb=1} inb {sub(/^[ ]+/, ""); print} inb && /^done <<< "\$_tc"$/ {exit}' "$f" > "$T/rd_block.sh"
|
|
404
387
|
[ -s "$T/rd_block.sh" ] || { bad "review-delta.sh referenced in $f without a canonical REVIEW_DELTA_SCRIPT resolver block"; continue; }
|
|
405
388
|
RD_BLOCKS=$((RD_BLOCKS + 1))
|
|
@@ -418,5 +401,149 @@ expect "review-delta.sh without --trusted-script prints FULL (cwd trust script i
|
|
|
418
401
|
out=$(cd "$T/rdwork" && bash "$ROOT/scripts/review-delta.sh" --pr 1 --head 0123456789abcdef0123456789abcdef01234567 --base main --trusted-script scripts/trusted-comments.sh --comments-file comments.json --labels "" 2>&1 | sed -n 1p)
|
|
419
402
|
expect "review-delta.sh with a relative cwd --trusted-script prints FULL" "FULL" "$out"
|
|
420
403
|
|
|
404
|
+
|
|
405
|
+
# Guard (#3778): scripts/lib/forge-resolve.sh owns the install-root tiers; review-pr-staging.md carries only byte-identical
|
|
406
|
+
# stubs. The locator line must be identical everywhere, resolve from trusted tiers only (no $PWD, no repo under review),
|
|
407
|
+
# and every fence stub must fail closed when the helper is unresolved.
|
|
408
|
+
FR="$ROOT/scripts/lib/forge-resolve.sh"; FB="$ROOT/scripts/lib/fence-bootstrap.sh"
|
|
409
|
+
[ -f "$FR" ] && [ -f "$FB" ] && ok || bad "scripts/lib/forge-resolve.sh and scripts/lib/fence-bootstrap.sh must exist"
|
|
410
|
+
sed 's/^[[:space:]]*//' "$STG_SPEC" | grep '^type forge_resolve >/dev/null' | sort | uniq -c > "$T/loc_uniq"
|
|
411
|
+
[ "$(wc -l < "$T/loc_uniq" | tr -d ' ')" = 1 ] && ok || bad "forge_resolve locator line must be byte-identical across every stub in review-pr-staging.md"
|
|
412
|
+
[ "$(awk '{print $1}' "$T/loc_uniq")" -ge 30 ] && ok || bad "expected a locator on every bootstrap (21) and resolver (11) stub"
|
|
413
|
+
grep -qE 'PWD|\$\(pwd\)|REPO_PATH' "$T/locator.sh" && bad "stub locator must not consult the cwd or repo under review" || ok
|
|
414
|
+
[ "$(grep -c '# FENCE_BOOTSTRAP$' "$STG_SPEC")" = 21 ] && ok || bad "expected 21 fence bootstrap stubs"
|
|
415
|
+
[ "$(grep '# FENCE_BOOTSTRAP$' "$STG_SPEC" | sort -u | wc -l | tr -d ' ')" = 1 ] && ok || bad "fence bootstrap stubs must be byte-identical"
|
|
416
|
+
grep '# FENCE_BOOTSTRAP$' "$STG_SPEC" | sed 1q | grep -qF 'exit 1' && ok || bad "fence bootstrap stub must fail closed (exit 1) when the helper is unresolved"
|
|
417
|
+
# the spec carries none of the old inlined blocks
|
|
418
|
+
grep -qE '^[[:space:]]*(TRUSTED_SCRIPT|[A-Z_]+_SCRIPT)=""$|_tc="\$\(printf' "$STG_SPEC" && bad "review-pr-staging.md still carries an inlined resolver block" || ok
|
|
419
|
+
# no $PWD / $(pwd) tier in the helper (comments aside)
|
|
420
|
+
grep -nE '\$PWD|\$\(pwd\)|\$\{PWD' "$FR" | grep -vE '^[0-9]+:[[:space:]]*#' | grep -q . && bad "forge-resolve.sh must have no PWD tier" || ok
|
|
421
|
+
|
|
422
|
+
# Guard (#3779): remediate.md carries the same byte-identical locator stub (7 fences: 5 gate sites, trusted-comments, CI gate) and none of the old inlined resolvers.
|
|
423
|
+
REM_SPEC="$ROOT/commands/work-on/remediate.md"
|
|
424
|
+
sed 's/^[[:space:]]*//' "$REM_SPEC" | grep '^type forge_resolve >/dev/null' | sort | uniq -c > "$T/rem_uniq"
|
|
425
|
+
[ "$(wc -l < "$T/rem_uniq" | tr -d ' ')" = 1 ] && ok || bad "remediate.md locator lines must be byte-identical"
|
|
426
|
+
expect "remediate.md locator count (one per fence that resolves a script)" 7 "$(awk '{print $1}' "$T/rem_uniq")"
|
|
427
|
+
[ "$(sed 's/^[[:space:]]*//' "$REM_SPEC" | grep '^type forge_resolve >/dev/null' | sed 1q)" = "$(cat "$T/locator.sh")" ] && ok || bad "remediate.md locator differs from the pinned review-pr-staging.md locator"
|
|
428
|
+
expect "remediate.md inlined FORGE_ROOT bootstrap copies" 0 "$(grep -c '# FORGE_ROOT bootstrap' "$REM_SPEC" || true)"
|
|
429
|
+
grep -qE '^[[:space:]]*(TRUSTED_SCRIPT|[A-Z_]+_SCRIPT)=""|_tc="\$\(printf|^[[:space:]]*GATE_SCRIPT=""' "$REM_SPEC" && bad "remediate.md still carries an inlined resolver block" || ok
|
|
430
|
+
# every forge_resolve call in remediate.md must sit in a fence that already defined it via the locator
|
|
431
|
+
expect "remediate.md: forge_resolve calls without a preceding locator in the same fence" 0 "$(awk '/^[ ]*```/{loc=0} /^[ ]*type forge_resolve >\/dev\/null/{loc=1} /(GATE|TRUSTED|CI_GATE)_SCRIPT=\$\(forge_resolve /&&!loc{bad++} END{print bad+0}' "$REM_SPEC")"
|
|
432
|
+
|
|
433
|
+
# Guard (#3780): review-pr.md carries the same byte-identical locator stub (one per fence that resolves a script) and none of the
|
|
434
|
+
# old inlined blocks: no FORGE_ROOT bootstrap, no `_tc` resolver, no `*_SCRIPT=""` accumulator. Review-delta mode lives in
|
|
435
|
+
# scripts/lib/review-delta-mode.sh (pinned by review-pr-delta-wiring.test.sh).
|
|
436
|
+
RP_SPEC="$ROOT/commands/review-pr.md"
|
|
437
|
+
sed 's/^[[:space:]]*//' "$RP_SPEC" | grep '^type forge_resolve >/dev/null' | sort | uniq -c > "$T/rp_uniq"
|
|
438
|
+
[ "$(wc -l < "$T/rp_uniq" | tr -d ' ')" = 1 ] && ok || bad "review-pr.md locator lines must be byte-identical"
|
|
439
|
+
[ "$(awk '{print $1}' "$T/rp_uniq")" -ge 15 ] && ok || bad "expected a locator in every review-pr.md fence that resolves a script (>=15)"
|
|
440
|
+
[ "$(sed 's/^[[:space:]]*//' "$RP_SPEC" | grep '^type forge_resolve >/dev/null' | sed 1q)" = "$(cat "$T/locator.sh")" ] && ok || bad "review-pr.md locator differs from the pinned review-pr-staging.md locator"
|
|
441
|
+
expect "review-pr.md inlined FORGE_ROOT bootstrap copies" 0 "$(grep -c 'FORGE_ROOT bootstrap' "$RP_SPEC" || true)"
|
|
442
|
+
# (SCOPE_SCRIPT="" is a sibling path derived from the resolved trusted script's own directory, not a resolver accumulator)
|
|
443
|
+
grep -vE '^[[:space:]]*SCOPE_SCRIPT=""$' "$RP_SPEC" | grep -qE '^[[:space:]]*(TRUSTED_SCRIPT|[A-Z_]+_SCRIPT)=""|_tc="\$\(printf|TRUSTED_SCRIPT resolver \(canonical' && bad "review-pr.md still carries an inlined resolver block" || ok
|
|
444
|
+
expect "review-pr.md: forge_resolve calls without a preceding locator in the same fence" 0 "$(awk '/^[ ]*```/{loc=0} /^[ ]*type forge_resolve >\/dev\/null/{loc=1} /=\$\(forge_resolve /&&!loc{bad++} END{print bad+0}' "$RP_SPEC")"
|
|
445
|
+
grep -nE '\$FORGE_ROOT|\$\{FORGE_ROOT:\+' "$RP_SPEC" | grep -q . && bad "review-pr.md consumes \$FORGE_ROOT (resolve through forge_resolve instead)" || ok
|
|
446
|
+
|
|
447
|
+
# Guard (#3781): every spec that needs FORGE_ROOT resolves it through the pinned locator + `FORGE_ROOT="$(forge_root ...)"` and carries none of
|
|
448
|
+
# the old inlined bootstrap (no marker comment, no `FORGE_ROOT="$_c"` candidate loop, no sentinel-file probes). Per site:
|
|
449
|
+
for f in $STUB_SITES; do
|
|
450
|
+
sed 's/^[[:space:]]*//' "$ROOT/$f" | grep '^type forge_resolve >/dev/null' | sort -u > "$T/site_loc"
|
|
451
|
+
[ "$(wc -l < "$T/site_loc" | tr -d ' ')" = 1 ] && [ "$(cat "$T/site_loc")" = "$(cat "$T/locator.sh")" ] && ok || bad "$f: locator lines are not byte-identical to the pinned locator"
|
|
452
|
+
expect "$f: inlined FORGE_ROOT bootstrap markers" 0 "$(grep -c '# FORGE_ROOT bootstrap' "$ROOT/$f" || true)"
|
|
453
|
+
expect "$f: inlined FORGE_ROOT candidate loops" 0 "$(grep -c 'FORGE_ROOT="\$_c"' "$ROOT/$f" || true)"
|
|
454
|
+
grep -qE 'verify-phase-trail\.sh" \] && \[ -f' "$ROOT/$f" && bad "$f: still probes the sentinel files of the old bootstrap" || ok
|
|
455
|
+
# every FORGE_ROOT resolution is the stub line (shell state does not persist between Bash calls, so each fence repeats it) and follows a locator in its fence
|
|
456
|
+
expect "$f: FORGE_ROOT resolutions that are not the stub line" 0 "$(grep -E '^[[:space:]]*FORGE_ROOT="\$\(forge_root' "$ROOT/$f" | grep -cvF "$FR_LINE" || true)"
|
|
457
|
+
expect "$f: forge_root calls without a preceding locator in the same fence" 0 "$(awk '/^[ ]*```/{loc=0} /^[ ]*type forge_resolve >\/dev\/null/{loc=1} /FORGE_ROOT="\$\(forge_root /&&!loc{bad++} END{print bad+0}' "$ROOT/$f")"
|
|
458
|
+
done
|
|
459
|
+
# the spec-count of stub resolutions pins the conversion (a copy dropped or re-inlined changes it)
|
|
460
|
+
for pair in work-on.md:3 work-on/review.md:2 work-on/build/validate.md:2 work-on/investigate.md:1 work-on/decompose.md:1 work-on/close.md:1 work-on/build.md:1 quality-gate.md:4 orchestrate/phase-1-resolve.md:2 orchestrate/phase-4-execution.md:8; do
|
|
461
|
+
f="commands/${pair%%:*}"; n="${pair##*:}"
|
|
462
|
+
expect "$f: FORGE_ROOT stub resolutions" "$n" "$(grep -cF "$FR_LINE" "$ROOT/$f" || true)"
|
|
463
|
+
done
|
|
464
|
+
|
|
465
|
+
# Behaviour: run the REAL stub locator + forge_resolve in a clean env over each install layout.
|
|
466
|
+
# Paths are compared canonically: macOS resolves /var -> /private/var and Git Bash maps /tmp -> /c/Users/..., so the literal
|
|
467
|
+
# spelling of a temp path differs from what readlink/pwd -P report (and 8.3 short names like RUNNER~1 differ from long ones). cygpath -ml (Git Bash) or dirname+pwd -P (POSIX) agree.
|
|
468
|
+
canon() { # path -> canonical spelling, empty stays empty
|
|
469
|
+
[ -n "$1" ] || return 0
|
|
470
|
+
if command -v cygpath >/dev/null 2>&1; then cygpath -ml "$1" 2>/dev/null || printf '%s' "$1"
|
|
471
|
+
else ( cd "$(dirname "$1")" 2>/dev/null && printf '%s/%s' "$(pwd -P)" "$(basename "$1")" ) || printf '%s' "$1"; fi
|
|
472
|
+
}
|
|
473
|
+
expect_path() { expect "$1" "$(canon "$2")" "$(canon "$3")"; }
|
|
474
|
+
mkdir -p "$T/fr/cwdplant/scripts/lib" "$T/fr/ws"
|
|
475
|
+
cp "$FR" "$T/fr/cwdplant/scripts/lib/forge-resolve.sh"; printf 'PLANT\n' > "$T/fr/cwdplant/scripts/trusted-comments.sh"
|
|
476
|
+
mk_install() { # dir: a minimal install root that carries the helpers and trusted-comments.sh
|
|
477
|
+
mkdir -p "$1/scripts/lib" "$1/commands"; cp "$FR" "$1/scripts/lib/"; cp "$FB" "$1/scripts/lib/"; : > "$1/scripts/trusted-comments.sh"; : > "$1/commands/work-on.md"
|
|
478
|
+
}
|
|
479
|
+
fr_run_sh() { # shell home cmd [extra env assignments...] -> stdout; cwd is a planted repo that must never be trusted
|
|
480
|
+
local sh="$1" h="$2" c="$3"; shift 3
|
|
481
|
+
( cd "$T/fr/cwdplant" && env -i PATH="$PATH" HOME="$h" "$@" "$sh" -c "$(cat "$T/locator.sh"); $c" 2>/dev/null )
|
|
482
|
+
}
|
|
483
|
+
fr_run() { fr_run_sh bash "$@"; }
|
|
484
|
+
mk_install "$T/fr/npm"
|
|
485
|
+
expect_path "engine layout: FORGEDOCK_HOME resolves (cwd plant ignored)" "$T/fr/npm/scripts/trusted-comments.sh" "$(fr_run "$T/fr/h0" 'forge_resolve trusted-comments.sh' FORGEDOCK_HOME="$T/fr/npm")"
|
|
486
|
+
expect_path "forge_root under FORGEDOCK_HOME" "$T/fr/npm" "$(fr_run "$T/fr/h0" 'forge_root' FORGEDOCK_HOME="$T/fr/npm")"
|
|
487
|
+
mk_install "$T/fr/other"
|
|
488
|
+
expect "FORGEDOCK_HOME is authoritative: a valid FORGE_HOME is not consulted when it points at a bare dir" "" "$(fr_run "$T/fr/h0" 'forge_resolve trusted-comments.sh' FORGEDOCK_HOME="$T/fr/ws" FORGE_HOME="$T/fr/other")"
|
|
489
|
+
CFG="$T/fr/plugonly/.claude/plugins/cache/forgedock/forgedock"
|
|
490
|
+
mk_install "$CFG/1.9.0"; mk_install "$CFG/1.12.0"; : > "$CFG/1.12.0/scripts/review-delta.sh"
|
|
491
|
+
expect_path "plugin-only layout: newest pinned cache wins (1.12.0 over 1.9.0)" "$CFG/1.12.0" "$(fr_run "$T/fr/plugonly" 'forge_root')"
|
|
492
|
+
expect_path "plugin-only layout: script resolves from the cache" "$CFG/1.12.0/scripts/review-delta.sh" "$(fr_run "$T/fr/plugonly" 'forge_resolve review-delta.sh')"
|
|
493
|
+
# INFRA-3: a lexical glob picks 1.16.0 before 2.0.0 and a pair where 1.9.0/1.12.0 happen to agree masks it; the stub must still end on 2.0.0
|
|
494
|
+
CFG2="$T/fr/lexical/.claude/plugins/cache/forgedock/forgedock"
|
|
495
|
+
mk_install "$CFG2/1.16.0"; mk_install "$CFG2/2.0.0"; : > "$CFG2/2.0.0/scripts/review-delta.sh"
|
|
496
|
+
expect_path "lexical-vs-numeric: 2.0.0 outranks 1.16.0 (stub-loaded helper re-resolves numerically)" "$CFG2/2.0.0" "$(fr_run "$T/fr/lexical" 'forge_root')"
|
|
497
|
+
expect_path "lexical-vs-numeric: a script only in 2.0.0 resolves" "$CFG2/2.0.0/scripts/review-delta.sh" "$(fr_run "$T/fr/lexical" 'forge_resolve review-delta.sh')"
|
|
498
|
+
mk_install "$T/fr/subst"
|
|
499
|
+
expect_path "plugin-only layout: substituted plugin root is a locator tier" "$T/fr/subst/scripts/trusted-comments.sh" "$(sed "s|'\${CLAUDE_PLUGIN_ROOT}'|'$T/fr/subst'|g" "$T/locator.sh" > "$T/locator_subst.sh"; cd "$T/fr/cwdplant" && env -i PATH="$PATH" HOME="$T/fr/h0" bash -c "$(cat "$T/locator_subst.sh"); forge_resolve trusted-comments.sh" 2>/dev/null)"
|
|
500
|
+
mk_install "$T/fr/repo"; mkdir -p "$T/fr/instl/.claude/commands"
|
|
501
|
+
if ln -s "$T/fr/repo/commands/work-on.md" "$T/fr/instl/.claude/commands/work-on.md" 2>/dev/null && [ -L "$T/fr/instl/.claude/commands/work-on.md" ]; then
|
|
502
|
+
expect_path "install.sh layout: commands symlink resolves to the repo root" "$T/fr/repo" "$(fr_run "$T/fr/instl" 'forge_root')"
|
|
503
|
+
else echo "SKIP: symlinks unavailable here"; SKIPPED=$((SKIPPED+1)); fi
|
|
504
|
+
mk_install "$T/fr/relocated/cfg/plugins/cache/forgedock/forgedock/1.12.0"
|
|
505
|
+
expect_path "CLAUDE_CONFIG_DIR-relocated layout resolves" "$T/fr/relocated/cfg/plugins/cache/forgedock/forgedock/1.12.0/scripts/trusted-comments.sh" "$(fr_run "$T/fr/h0" 'forge_resolve trusted-comments.sh' CLAUDE_CONFIG_DIR="$T/fr/relocated/cfg")"
|
|
506
|
+
# INFRA-2: CLAUDE_CONFIG_DIR is set (and empty) while the plugin cache lives under ~/.claude
|
|
507
|
+
mkdir -p "$T/fr/emptycfg"
|
|
508
|
+
expect_path "CLAUDE_CONFIG_DIR set but empty, cache under ~/.claude resolves" "$CFG/1.12.0/scripts/trusted-comments.sh" "$(fr_run "$T/fr/plugonly" 'forge_resolve trusted-comments.sh' CLAUDE_CONFIG_DIR="$T/fr/emptycfg")"
|
|
509
|
+
# INFRA-1: Codex / legacy variable layouts with no ~/.claude at all
|
|
510
|
+
expect_path "FORGE_HOME-only layout resolves" "$T/fr/repo/scripts/trusted-comments.sh" "$(fr_run "$T/fr/h0" 'forge_resolve trusted-comments.sh' FORGE_HOME="$T/fr/repo")"
|
|
511
|
+
expect_path "FORGE_ROOT-only layout resolves" "$T/fr/repo/scripts/trusted-comments.sh" "$(fr_run "$T/fr/h0" 'forge_resolve trusted-comments.sh' FORGE_ROOT="$T/fr/repo")"
|
|
512
|
+
mkdir -p "$T/fr/codexhome"; printf '%s\n' "$T/fr/repo" > "$T/fr/codexhome/forge-home"
|
|
513
|
+
expect_path "Codex forge-home file layout resolves" "$T/fr/repo/scripts/trusted-comments.sh" "$(fr_run "$T/fr/h0" 'forge_resolve trusted-comments.sh' CODEX_HOME="$T/fr/codexhome")"
|
|
514
|
+
expect "relative FORGE_HOME is rejected" "" "$(fr_run "$T/fr/h0" 'forge_resolve trusted-comments.sh' FORGE_HOME=fr/cwdplant)"
|
|
515
|
+
expect "unresolved layout prints nothing (cwd plant ignored)" "" "$(fr_run "$T/fr/h0" 'forge_resolve trusted-comments.sh')"
|
|
516
|
+
expect "unresolved layout: the bootstrap stub fails closed" "BLOCKED" "$(fr_run "$T/fr/h0" "$(grep '# FENCE_BOOTSTRAP$' "$STG_SPEC" | sed 1q); echo reached" | sed -n 's/^\(BLOCKED\).*/\1/p')"
|
|
517
|
+
expect "unresolved layout: the bootstrap stub does not reach the fence body" "" "$(fr_run "$T/fr/h0" "$(grep '# FENCE_BOOTSTRAP$' "$STG_SPEC" | sed 1q); echo reached" | sed -n 's/^\(reached\)$/\1/p')"
|
|
518
|
+
expect "relative FORGEDOCK_HOME is rejected" "" "$(fr_run "$T/fr/h0" 'forge_resolve trusted-comments.sh' FORGEDOCK_HOME=fr/cwdplant)"
|
|
519
|
+
expect "path traversal in the script name is rejected" "" "$(fr_run "$T/fr/h0" 'forge_resolve ../scripts/trusted-comments.sh' FORGEDOCK_HOME="$T/fr/npm")"
|
|
520
|
+
# #3779: the REAL remediate locator + gate resolution on a plugin-only layout and on an empty HOME (empty output => the spec's strict branch fires)
|
|
521
|
+
sed 's/^[[:space:]]*//' "$REM_SPEC" | grep '^type forge_resolve >/dev/null' | sed 1q > "$T/rem_locator.sh"
|
|
522
|
+
mk_install "$T/fr/remgate/.claude/plugins/cache/forgedock/forgedock/1.0.0"; : > "$T/fr/remgate/.claude/plugins/cache/forgedock/forgedock/1.0.0/scripts/resolve-deploy-gate.sh"
|
|
523
|
+
rem_run() { ( cd "$T/fr/cwdplant" && env -i PATH="$PATH" HOME="$1" bash -c "$(cat "$T/rem_locator.sh"); GATE_SCRIPT=\$(forge_resolve resolve-deploy-gate.sh); printf %s \"\$GATE_SCRIPT\"" 2>/dev/null ); }
|
|
524
|
+
expect_path "remediate stub: plugin-only install resolves resolve-deploy-gate.sh" "$T/fr/remgate/.claude/plugins/cache/forgedock/forgedock/1.0.0/scripts/resolve-deploy-gate.sh" "$(rem_run "$T/fr/remgate")"
|
|
525
|
+
: > "$T/fr/cwdplant/scripts/resolve-deploy-gate.sh"
|
|
526
|
+
expect "remediate stub: empty HOME leaves the gate script empty (strict), cwd plant ignored" "" "$(rem_run "$T/fr/h0")"
|
|
527
|
+
rm -f "$T/fr/cwdplant/scripts/resolve-deploy-gate.sh"
|
|
528
|
+
# INFRA-8: the stub itself must survive errexit in bash (setopt is zsh-only and must not abort a set -e shell)
|
|
529
|
+
expect "errexit: the locator stub itself survives set -e with no helper" "survived" "$(cd "$T/fr/cwdplant" && env -i PATH="$PATH" HOME="$T/fr/h0" bash -c "set -e; $(cat "$T/locator.sh"); X=\$(forge_resolve trusted-comments.sh); [ -z \"\$X\" ] && echo survived" 2>/dev/null)"
|
|
530
|
+
# INFRA-5: errexit callers must survive a resolver miss (helper sourced, and helper never found) and reach their own empty-output branch
|
|
531
|
+
expect "errexit: a resolver miss with the helper sourced survives" "survived" "$(fr_run "$T/fr/h0" 'set -e; X=$(forge_resolve nothere.sh); [ -z "$X" ] && echo survived' FORGEDOCK_HOME="$T/fr/npm")"
|
|
532
|
+
expect "errexit: a resolver miss with no helper at all survives" "survived" "$(fr_run "$T/fr/h0" 'set -e; X=$(forge_resolve trusted-comments.sh); [ -z "$X" ] && echo survived')"
|
|
533
|
+
# #3781: the real two-line stub (locator + FORGE_ROOT line) leaves FORGE_ROOT empty, silently, and survives errexit when nothing resolves (callers keep their own empty-root fail-closed branch)
|
|
534
|
+
for opts in "-e" "-eo pipefail" "-euo pipefail"; do
|
|
535
|
+
expect "stub: unresolved layout leaves FORGE_ROOT empty under set $opts" "ok:" "$(cd "$T/fr/cwdplant" && env -i PATH="$PATH" HOME="$T/fr/h0" "$SH" -c "set $opts; $(cat "$T/canon"); printf 'ok:%s' \"\$FORGE_ROOT\"" 2>&1)"
|
|
536
|
+
expect "stub: resolves under set $opts" "ok:$T/fr/npm" "$(cd "$T/fr/cwdplant" && env -i PATH="$PATH" HOME="$T/fr/h0" FORGEDOCK_HOME="$T/fr/npm" "$SH" -c "set $opts; $(cat "$T/canon"); printf 'ok:%s' \"\$FORGE_ROOT\"" 2>&1)"
|
|
537
|
+
done
|
|
538
|
+
expect "stub: cwd plant is never FORGE_ROOT (hostile consumer repo)" "" "$(cd "$T/fr/cwdplant" && env -i PATH="$PATH" HOME="$T/fr/h0" bash -c "$(cat "$T/canon"); printf %s \"\$FORGE_ROOT\"" 2>/dev/null)"
|
|
539
|
+
# INFRA-4: under zsh an unmatched glob in the locator must not abort, and there is no cwd-derived self root
|
|
540
|
+
if command -v zsh >/dev/null 2>&1; then
|
|
541
|
+
expect_path "zsh: locator survives no cache at all and a FORGE_HOME layout resolves" "$T/fr/repo/scripts/trusted-comments.sh" "$(fr_run_sh zsh "$T/fr/h0" 'forge_resolve trusted-comments.sh' FORGE_HOME="$T/fr/repo")"
|
|
542
|
+
expect "zsh: unresolved layout prints nothing and never trusts the cwd plant" "" "$(fr_run_sh zsh "$T/fr/h0" 'forge_resolve trusted-comments.sh')"
|
|
543
|
+
expect_path "zsh: numeric-newest cache wins" "$CFG2/2.0.0" "$(fr_run_sh zsh "$T/fr/lexical" 'forge_root')"
|
|
544
|
+
else echo "SKIP: zsh not installed"; SKIPPED=$((SKIPPED+1)); fi
|
|
545
|
+
# INFRA-6: a Windows drive-letter FORGEDOCK_HOME is normalized by the helper (cygpath where present, else the /c/x spelling)
|
|
546
|
+
expect "drive-letter FORGEDOCK_HOME normalizes to /c/x" "/c/forge/x" "$(env -i PATH="$PATH" HOME="$T/fr/h0" bash -c ". '$FR'; _forge_posix 'C:\\forge\\x'" 2>/dev/null | sed 's|^/cygdrive||')"
|
|
547
|
+
|
|
421
548
|
echo "forge-root tests: pass=$PASS fail=$FAILN skipped=$SKIPPED"
|
|
422
549
|
[ "$FAILN" -eq 0 ]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# fence-bootstrap.sh — sourced by every review-pr-staging fence that consumes cross-fence state.
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
# A fresh Bash call loses shell state: this re-derives config and the cross-fence PR state from the
|
|
7
|
+
# scratchpad. Source it (never exec it) so the variables reach the calling fence.
|
|
8
|
+
_SD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"; mkdir -p "$_SD"
|
|
9
|
+
CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
|
|
10
|
+
[ -n "${GH_REPO:-}" ] || GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE" 2>/dev/null)
|
|
11
|
+
[ -n "${DEFAULT_BRANCH:-}" ] || DEFAULT_BRANCH=$(yq '.branches.default' "$CONFIG_FILE" 2>/dev/null)
|
|
12
|
+
[ -n "${STAGING_BRANCH:-}" ] || STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE" 2>/dev/null)
|
|
13
|
+
# numeric-validated: a tampered file never reaches gh
|
|
14
|
+
[ -n "${PR_NUMBER:-}" ] || { [ -r "$_SD/staging_pr_number" ] && PR_NUMBER=$(sed -n '1{/^[0-9][0-9]*$/p;q}' "$_SD/staging_pr_number"); }
|
|
15
|
+
[ -n "${ALL_PR_NUMBERS:-}" ] || { [ -n "${PR_NUMBER:-}" ] && [ -r "$_SD/${PR_NUMBER}_bundle-prs.txt" ] && ALL_PR_NUMBERS=$(grep -E '^[0-9]+$' "$_SD/${PR_NUMBER}_bundle-prs.txt"); }
|
|
16
|
+
[ -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."
|