forgedock 1.15.0 → 1.16.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # forge-root.test.sh — guards the canonical FORGE_ROOT bootstrap (forge#3098).
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); trap 'rm -rf "$T"' EXIT
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
- 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/review-pr.md commands/work-on/remediate.md commands/orchestrate/phase-1-resolve.md commands/orchestrate/phase-4-execution.md"
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 $SITES; do
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/bin/engine"
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" "$T/nowhere" "$(run "$T/h1" FORGEDOCK_HOME="$T/nowhere")"
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
- grep -hE '^[[:space:]]*GATE_SCRIPT=""; \[ -n "\$FORGE_ROOT" \]' "$ROOT/commands/work-on/remediate.md" "$ROOT/commands/review-pr.md" | sed 's/^[[:space:]]*//' | sort -u > "$T/gs"
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
- gate_run() { ( cd "$T/consumer" && env -i PATH="$PATH" HOME="$1" "$SH" -c "$(cat "$T/canon"); $(cat "$T/gs"); printf %s \"\$GATE_SCRIPT\"" ); }
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 with only one of the two needed scripts is rejected
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 (missing lint script) rejected" "" "$(run "$T/h4")"
143
- # stale install: has the two original gate scripts but lacks is-docs-only.sh / engine modules => rejected
144
- for missing in scripts/is-docs-only.sh bin/engine/resolve.mjs bin/engine/orchestrate-canary.mjs bin/engine/admission.mjs; do
145
- rm -rf "$T/h12"; mkscripts "$T/h12/.claude/plugins/cache/forgedock/forgedock/1.0.0"; rm -f "$T/h12/.claude/plugins/cache/forgedock/forgedock/1.0.0/$missing"
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
- expect "newer-core pre-release beats older release" "$T/h7/.claude/plugins/cache/forgedock/forgedock/2.0.0-rc1" "$(run "$T/h7")"
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 is normalized (/c/...), relative stays rejected
181
- rm -rf "$T/win"; mkscripts "$T/win/c/forge"
182
- expect "C:/ FORGEDOCK_HOME authoritative (no cygpath, bogus path)" "/c/nowhere/forge" "$(run "$T/h1" FORGEDOCK_HOME='C:/nowhere/forge')"
183
- expect "C:\\ FORGEDOCK_HOME normalized" "/d/x/y" "$(run "$T/h1" FORGEDOCK_HOME='D:\x\y')"
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,21 @@ 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 (textually substituted) outranks an exported FORGE_HOME that also has the scripts:
222
- # the running plugin's own files win over a stale clone named by a global env var (forge#3147 field test)
223
- mkscripts "$T/h11/stale-clone"; mkscripts "$T/h11/running-plugin"
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; once loaded, the helper's own tier order applies, 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 )"
228
199
  # the env var alone (no substitution) must NOT be relied on: Claude Code does not export it to Bash
229
200
  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
201
  done
231
- # the canonical block must use the exact substitutable spelling, single-quoted (no :- form, which Claude Code leaves verbatim)
232
- grep -qF "'\${CLAUDE_PLUGIN_ROOT}'" "$T/canon.code" && ok || bad "canonical bootstrap lacks the substitutable '\${CLAUDE_PLUGIN_ROOT}' candidate"
233
- grep -qF 'CLAUDE_PLUGIN_ROOT:-' "$T/canon.code" && bad "canonical bootstrap uses \${CLAUDE_PLUGIN_ROOT:-}, which Claude Code never substitutes" || ok
202
+ # the pinned locator must use the exact substitutable spelling, single-quoted (no :- form, which Claude Code leaves verbatim)
203
+ grep -qF "'\${CLAUDE_PLUGIN_ROOT}'" "$T/locator.sh" && ok || bad "stub locator lacks the substitutable '\${CLAUDE_PLUGIN_ROOT}' candidate"
204
+ grep -qF 'CLAUDE_PLUGIN_ROOT:-' "$T/locator.sh" && bad "stub locator uses \${CLAUDE_PLUGIN_ROOT:-}, which Claude Code never substitutes" || ok
234
205
 
235
206
  # 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 $SITES commands/work-on/close.md; do
207
+ for f in $STUB_SITES; do
237
208
  grep -nE 'file://\$\{?(FORGE_ROOT|\(pwd\))' "$ROOT/$f" >/dev/null && bad "concatenated file:// URL in $f" || ok
238
209
  done
239
210
  if command -v node >/dev/null 2>&1; then
@@ -263,7 +234,7 @@ while IFS= read -r f; do
263
234
  arms=$(grep -c 'trusted-comments.sh" \]' "$f" || true)
264
235
  [ "$arms" = "$(cat "$T/tc_n")" ] || { TC_STRAY=$((TC_STRAY + 1)); echo " stray resolver in $f: $arms arms vs $(cat "$T/tc_n") blocks"; }
265
236
  done < <(grep -rl 'trusted-comments.sh" \]' "$ROOT/commands")
266
- expect "trusted-comments.sh resolver blocks discovered (review-pr 11 + review + remediate x2 + phase-4 + review-pr-staging 7)" 22 "$TC_BLOCKS"
237
+ 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
238
  expect "trusted-comments.sh resolvers without a canonical block" 0 "$TC_STRAY"
268
239
  expect "trusted-comments.sh resolvers byte-identical" 1 "$(sort -u "$T/tc_all" | wc -l | tr -d ' ')"
269
240
  grep -qF 'plugins/cache/forgedock/forgedock' "$T/tc_all" && ok || bad 'trusted-comments.sh resolver lacks the plugin-cache scan'
@@ -292,14 +263,18 @@ extract_helper_block() { # file var -> block on stdout
292
263
  extract_helper_fn() { # file -> function on stdout
293
264
  awk '/^resolve_extract_affected_files\(\) \{/ {inb=1} inb {print} inb && /^\}$/ {exit}' "$1"
294
265
  }
266
+ # review-pr.md resolves its helpers through the forge_resolve stub (#3780): the pinned locator plus the call line, taken from the spec.
267
+ stub_block() { # file var -> locator + the first `var=$(forge_resolve ...)` line, indentation stripped
268
+ sed 's/^[[:space:]]*//' "$1" | grep -m1 '^type forge_resolve >/dev/null'; sed 's/^[[:space:]]*//' "$1" | grep -m1 "^$2=\$(forge_resolve "
269
+ }
295
270
  HR_CLASSIFY="$ROOT/commands/review-pr.md"
296
271
  HR_AMP="$ROOT/commands/orchestrate/phase-4-execution.md"
297
272
  HR_EX3="$ROOT/commands/orchestrate/phase-3-dependency.md"
298
- extract_helper_block "$HR_CLASSIFY" CLASSIFY_SCRIPT > "$T/hr_classify.sh"
273
+ stub_block "$HR_CLASSIFY" CLASSIFY_SCRIPT > "$T/hr_classify.sh"
299
274
  extract_helper_block "$HR_AMP" AMP_SCRIPT > "$T/hr_amp.sh"
300
275
  extract_helper_fn "$HR_AMP" > "$T/hr_ex4.sh"
301
276
  extract_helper_fn "$HR_EX3" > "$T/hr_ex3.sh"
302
- expect "CLASSIFY_SCRIPT resolver block found" 1 "$(grep -c '^CLASSIFY_SCRIPT=""' "$T/hr_classify.sh")"
277
+ expect "CLASSIFY_SCRIPT resolver stub found" 1 "$(grep -c '^CLASSIFY_SCRIPT=\$(forge_resolve classify-finding.sh)$' "$T/hr_classify.sh")"
303
278
  expect "AMP_SCRIPT resolver block found" 1 "$(grep -c '^AMP_SCRIPT=""' "$T/hr_amp.sh")"
304
279
  expect "extract-affected-files resolver found in phase-4" 1 "$(grep -c 'resolve_extract_affected_files() {' "$T/hr_ex4.sh")"
305
280
  expect "extract-affected-files resolver found in phase-3" 1 "$(grep -c 'resolve_extract_affected_files() {' "$T/hr_ex3.sh")"
@@ -313,7 +288,8 @@ done
313
288
  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
289
 
315
290
  # 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"
291
+ mkdir -p "$T/hrwork/scripts" "$T/hrhome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts/lib"
292
+ cp "$ROOT/scripts/lib/forge-resolve.sh" "$T/hrhome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts/lib/forge-resolve.sh"
317
293
  for h in classify-finding amplification-breaker extract-affected-files; do
318
294
  : > "$T/hrwork/scripts/$h.sh"; : > "$T/hrhome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts/$h.sh"
319
295
  done
@@ -339,23 +315,20 @@ hr_check_block() { # label file var script
339
315
  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
316
  grep -qF "scripts/$4" "$T/hr_blk.sh" && ok || bad "$1: $3 resolver does not resolve scripts/$4"
341
317
  }
342
- hr_check_block review-pr "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT derive-finding-milestone.sh
343
- hr_check_block review-pr "$HR_CLASSIFY" PRIORITY_SCRIPT severity-to-priority.sh
344
- hr_check_block review-pr "$HR_CLASSIFY" CODE_INDEX_SCRIPT code-index.sh
345
- hr_check_block review-pr "$HR_CLASSIFY" DEDUP_SCRIPT issue-dedup.sh
318
+ hr_check_stub() { # label file var script: the spec calls the script through the forge_resolve stub (locator in the same fence)
319
+ stub_block "$2" "$3" > "$T/hr_blk.sh"
320
+ expect "$1: $3 resolver stub found" "$3=\$(forge_resolve $4)" "$(sed -n 2p "$T/hr_blk.sh")"
321
+ grep -qE '\$PWD|\$REPO_PATH|\$\{PWD|\$\{REPO_PATH' "$T/hr_blk.sh" && bad "$1: $3 resolver lists the working directory or repo path" || ok
322
+ local needle="$3=\$(forge_resolve " # kept out of the awk command substitution: bash 3.2 (macOS) mis-parses the nested quote and paren
323
+ 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")"
324
+ }
325
+ hr_check_stub review-pr "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT derive-finding-milestone.sh
326
+ hr_check_stub review-pr "$HR_CLASSIFY" PRIORITY_SCRIPT severity-to-priority.sh
327
+ hr_check_stub review-pr "$HR_CLASSIFY" CODE_INDEX_SCRIPT code-index.sh
328
+ hr_check_stub review-pr "$HR_CLASSIFY" DEDUP_SCRIPT issue-dedup.sh
346
329
  hr_check_block issue "$ROOT/commands/issue.md" DEDUP_SCRIPT issue-dedup.sh
347
- hr_check_block review-pr-staging "$HR_STG" DERIVE_MILESTONE_SCRIPT derive-finding-milestone.sh
348
- hr_check_block review-pr-staging "$HR_STG" PRIORITY_SCRIPT severity-to-priority.sh
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'
330
+ stub_block "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT > "$T/hr_dm.sh"
331
+ stub_block "$HR_CLASSIFY" PRIORITY_SCRIPT > "$T/hr_pr.sh"
359
332
  # no bare cwd-relative helper call in either review spec
360
333
  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
334
 
@@ -400,6 +373,8 @@ done
400
373
  # tiers: plugin-cache scan and CLAUDE_CONFIG_DIR present, no "$PWD"/"$REPO_PATH" tier. Passes with 0 blocks today.
401
374
  RD_BLOCKS=0
402
375
  while IFS= read -r f; do
376
+ # a forge_resolve stub (scripts/lib/forge-resolve.sh owns the tiers) satisfies the guard
377
+ if grep -qE '^[[:space:]]*REVIEW_DELTA_SCRIPT=\$\(forge_resolve review-delta\.sh\)$' "$f"; then RD_BLOCKS=$((RD_BLOCKS + 1)); continue; fi
403
378
  awk '/^[ ]*# HELPER_SCRIPT resolver \(/ && index($0, "(REVIEW_DELTA_SCRIPT;") {inb=1} inb {sub(/^[ ]+/, ""); print} inb && /^done <<< "\$_tc"$/ {exit}' "$f" > "$T/rd_block.sh"
404
379
  [ -s "$T/rd_block.sh" ] || { bad "review-delta.sh referenced in $f without a canonical REVIEW_DELTA_SCRIPT resolver block"; continue; }
405
380
  RD_BLOCKS=$((RD_BLOCKS + 1))
@@ -418,5 +393,149 @@ expect "review-delta.sh without --trusted-script prints FULL (cwd trust script i
418
393
  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
394
  expect "review-delta.sh with a relative cwd --trusted-script prints FULL" "FULL" "$out"
420
395
 
396
+
397
+ # Guard (#3778): scripts/lib/forge-resolve.sh owns the install-root tiers; review-pr-staging.md carries only byte-identical
398
+ # stubs. The locator line must be identical everywhere, resolve from trusted tiers only (no $PWD, no repo under review),
399
+ # and every fence stub must fail closed when the helper is unresolved.
400
+ FR="$ROOT/scripts/lib/forge-resolve.sh"; FB="$ROOT/scripts/lib/fence-bootstrap.sh"
401
+ [ -f "$FR" ] && [ -f "$FB" ] && ok || bad "scripts/lib/forge-resolve.sh and scripts/lib/fence-bootstrap.sh must exist"
402
+ sed 's/^[[:space:]]*//' "$STG_SPEC" | grep '^type forge_resolve >/dev/null' | sort | uniq -c > "$T/loc_uniq"
403
+ [ "$(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"
404
+ [ "$(awk '{print $1}' "$T/loc_uniq")" -ge 30 ] && ok || bad "expected a locator on every bootstrap (21) and resolver (11) stub"
405
+ grep -qE 'PWD|\$\(pwd\)|REPO_PATH' "$T/locator.sh" && bad "stub locator must not consult the cwd or repo under review" || ok
406
+ [ "$(grep -c '# FENCE_BOOTSTRAP$' "$STG_SPEC")" = 21 ] && ok || bad "expected 21 fence bootstrap stubs"
407
+ [ "$(grep '# FENCE_BOOTSTRAP$' "$STG_SPEC" | sort -u | wc -l | tr -d ' ')" = 1 ] && ok || bad "fence bootstrap stubs must be byte-identical"
408
+ 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"
409
+ # the spec carries none of the old inlined blocks
410
+ grep -qE '^[[:space:]]*(TRUSTED_SCRIPT|[A-Z_]+_SCRIPT)=""$|_tc="\$\(printf' "$STG_SPEC" && bad "review-pr-staging.md still carries an inlined resolver block" || ok
411
+ # no $PWD / $(pwd) tier in the helper (comments aside)
412
+ grep -nE '\$PWD|\$\(pwd\)|\$\{PWD' "$FR" | grep -vE '^[0-9]+:[[:space:]]*#' | grep -q . && bad "forge-resolve.sh must have no PWD tier" || ok
413
+
414
+ # 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.
415
+ REM_SPEC="$ROOT/commands/work-on/remediate.md"
416
+ sed 's/^[[:space:]]*//' "$REM_SPEC" | grep '^type forge_resolve >/dev/null' | sort | uniq -c > "$T/rem_uniq"
417
+ [ "$(wc -l < "$T/rem_uniq" | tr -d ' ')" = 1 ] && ok || bad "remediate.md locator lines must be byte-identical"
418
+ expect "remediate.md locator count (one per fence that resolves a script)" 7 "$(awk '{print $1}' "$T/rem_uniq")"
419
+ [ "$(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"
420
+ expect "remediate.md inlined FORGE_ROOT bootstrap copies" 0 "$(grep -c '# FORGE_ROOT bootstrap' "$REM_SPEC" || true)"
421
+ 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
422
+ # every forge_resolve call in remediate.md must sit in a fence that already defined it via the locator
423
+ 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")"
424
+
425
+ # Guard (#3780): review-pr.md carries the same byte-identical locator stub (one per fence that resolves a script) and none of the
426
+ # old inlined blocks: no FORGE_ROOT bootstrap, no `_tc` resolver, no `*_SCRIPT=""` accumulator. Review-delta mode lives in
427
+ # scripts/lib/review-delta-mode.sh (pinned by review-pr-delta-wiring.test.sh).
428
+ RP_SPEC="$ROOT/commands/review-pr.md"
429
+ sed 's/^[[:space:]]*//' "$RP_SPEC" | grep '^type forge_resolve >/dev/null' | sort | uniq -c > "$T/rp_uniq"
430
+ [ "$(wc -l < "$T/rp_uniq" | tr -d ' ')" = 1 ] && ok || bad "review-pr.md locator lines must be byte-identical"
431
+ [ "$(awk '{print $1}' "$T/rp_uniq")" -ge 15 ] && ok || bad "expected a locator in every review-pr.md fence that resolves a script (>=15)"
432
+ [ "$(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"
433
+ expect "review-pr.md inlined FORGE_ROOT bootstrap copies" 0 "$(grep -c 'FORGE_ROOT bootstrap' "$RP_SPEC" || true)"
434
+ # (SCOPE_SCRIPT="" is a sibling path derived from the resolved trusted script's own directory, not a resolver accumulator)
435
+ 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
436
+ 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")"
437
+ grep -nE '\$FORGE_ROOT|\$\{FORGE_ROOT:\+' "$RP_SPEC" | grep -q . && bad "review-pr.md consumes \$FORGE_ROOT (resolve through forge_resolve instead)" || ok
438
+
439
+ # Guard (#3781): every spec that needs FORGE_ROOT resolves it through the pinned locator + `FORGE_ROOT="$(forge_root ...)"` and carries none of
440
+ # the old inlined bootstrap (no marker comment, no `FORGE_ROOT="$_c"` candidate loop, no sentinel-file probes). Per site:
441
+ for f in $STUB_SITES; do
442
+ sed 's/^[[:space:]]*//' "$ROOT/$f" | grep '^type forge_resolve >/dev/null' | sort -u > "$T/site_loc"
443
+ [ "$(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"
444
+ expect "$f: inlined FORGE_ROOT bootstrap markers" 0 "$(grep -c '# FORGE_ROOT bootstrap' "$ROOT/$f" || true)"
445
+ expect "$f: inlined FORGE_ROOT candidate loops" 0 "$(grep -c 'FORGE_ROOT="\$_c"' "$ROOT/$f" || true)"
446
+ grep -qE 'verify-phase-trail\.sh" \] && \[ -f' "$ROOT/$f" && bad "$f: still probes the sentinel files of the old bootstrap" || ok
447
+ # 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
448
+ 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)"
449
+ 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")"
450
+ done
451
+ # the spec-count of stub resolutions pins the conversion (a copy dropped or re-inlined changes it)
452
+ 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
453
+ f="commands/${pair%%:*}"; n="${pair##*:}"
454
+ expect "$f: FORGE_ROOT stub resolutions" "$n" "$(grep -cF "$FR_LINE" "$ROOT/$f" || true)"
455
+ done
456
+
457
+ # Behaviour: run the REAL stub locator + forge_resolve in a clean env over each install layout.
458
+ # Paths are compared canonically: macOS resolves /var -> /private/var and Git Bash maps /tmp -> /c/Users/..., so the literal
459
+ # 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.
460
+ canon() { # path -> canonical spelling, empty stays empty
461
+ [ -n "$1" ] || return 0
462
+ if command -v cygpath >/dev/null 2>&1; then cygpath -ml "$1" 2>/dev/null || printf '%s' "$1"
463
+ else ( cd "$(dirname "$1")" 2>/dev/null && printf '%s/%s' "$(pwd -P)" "$(basename "$1")" ) || printf '%s' "$1"; fi
464
+ }
465
+ expect_path() { expect "$1" "$(canon "$2")" "$(canon "$3")"; }
466
+ mkdir -p "$T/fr/cwdplant/scripts/lib" "$T/fr/ws"
467
+ cp "$FR" "$T/fr/cwdplant/scripts/lib/forge-resolve.sh"; printf 'PLANT\n' > "$T/fr/cwdplant/scripts/trusted-comments.sh"
468
+ mk_install() { # dir: a minimal install root that carries the helpers and trusted-comments.sh
469
+ 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"
470
+ }
471
+ fr_run_sh() { # shell home cmd [extra env assignments...] -> stdout; cwd is a planted repo that must never be trusted
472
+ local sh="$1" h="$2" c="$3"; shift 3
473
+ ( cd "$T/fr/cwdplant" && env -i PATH="$PATH" HOME="$h" "$@" "$sh" -c "$(cat "$T/locator.sh"); $c" 2>/dev/null )
474
+ }
475
+ fr_run() { fr_run_sh bash "$@"; }
476
+ mk_install "$T/fr/npm"
477
+ 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")"
478
+ expect_path "forge_root under FORGEDOCK_HOME" "$T/fr/npm" "$(fr_run "$T/fr/h0" 'forge_root' FORGEDOCK_HOME="$T/fr/npm")"
479
+ mk_install "$T/fr/other"
480
+ 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")"
481
+ CFG="$T/fr/plugonly/.claude/plugins/cache/forgedock/forgedock"
482
+ mk_install "$CFG/1.9.0"; mk_install "$CFG/1.12.0"; : > "$CFG/1.12.0/scripts/review-delta.sh"
483
+ 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')"
484
+ 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')"
485
+ # 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
486
+ CFG2="$T/fr/lexical/.claude/plugins/cache/forgedock/forgedock"
487
+ mk_install "$CFG2/1.16.0"; mk_install "$CFG2/2.0.0"; : > "$CFG2/2.0.0/scripts/review-delta.sh"
488
+ 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')"
489
+ 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')"
490
+ mk_install "$T/fr/subst"
491
+ 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'|" "$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)"
492
+ mk_install "$T/fr/repo"; mkdir -p "$T/fr/instl/.claude/commands"
493
+ 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
494
+ expect_path "install.sh layout: commands symlink resolves to the repo root" "$T/fr/repo" "$(fr_run "$T/fr/instl" 'forge_root')"
495
+ else echo "SKIP: symlinks unavailable here"; SKIPPED=$((SKIPPED+1)); fi
496
+ mk_install "$T/fr/relocated/cfg/plugins/cache/forgedock/forgedock/1.12.0"
497
+ 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")"
498
+ # INFRA-2: CLAUDE_CONFIG_DIR is set (and empty) while the plugin cache lives under ~/.claude
499
+ mkdir -p "$T/fr/emptycfg"
500
+ 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")"
501
+ # INFRA-1: Codex / legacy variable layouts with no ~/.claude at all
502
+ 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")"
503
+ 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")"
504
+ mkdir -p "$T/fr/codexhome"; printf '%s\n' "$T/fr/repo" > "$T/fr/codexhome/forge-home"
505
+ 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")"
506
+ expect "relative FORGE_HOME is rejected" "" "$(fr_run "$T/fr/h0" 'forge_resolve trusted-comments.sh' FORGE_HOME=fr/cwdplant)"
507
+ expect "unresolved layout prints nothing (cwd plant ignored)" "" "$(fr_run "$T/fr/h0" 'forge_resolve trusted-comments.sh')"
508
+ 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')"
509
+ 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')"
510
+ expect "relative FORGEDOCK_HOME is rejected" "" "$(fr_run "$T/fr/h0" 'forge_resolve trusted-comments.sh' FORGEDOCK_HOME=fr/cwdplant)"
511
+ 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")"
512
+ # #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)
513
+ sed 's/^[[:space:]]*//' "$REM_SPEC" | grep '^type forge_resolve >/dev/null' | sed 1q > "$T/rem_locator.sh"
514
+ 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"
515
+ 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 ); }
516
+ 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")"
517
+ : > "$T/fr/cwdplant/scripts/resolve-deploy-gate.sh"
518
+ expect "remediate stub: empty HOME leaves the gate script empty (strict), cwd plant ignored" "" "$(rem_run "$T/fr/h0")"
519
+ rm -f "$T/fr/cwdplant/scripts/resolve-deploy-gate.sh"
520
+ # INFRA-8: the stub itself must survive errexit in bash (setopt is zsh-only and must not abort a set -e shell)
521
+ 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)"
522
+ # INFRA-5: errexit callers must survive a resolver miss (helper sourced, and helper never found) and reach their own empty-output branch
523
+ 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")"
524
+ 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')"
525
+ # #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)
526
+ for opts in "-e" "-eo pipefail" "-euo pipefail"; do
527
+ 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)"
528
+ 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)"
529
+ done
530
+ 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)"
531
+ # INFRA-4: under zsh an unmatched glob in the locator must not abort, and there is no cwd-derived self root
532
+ if command -v zsh >/dev/null 2>&1; then
533
+ 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")"
534
+ expect "zsh: unresolved layout prints nothing and never trusts the cwd plant" "" "$(fr_run_sh zsh "$T/fr/h0" 'forge_resolve trusted-comments.sh')"
535
+ expect_path "zsh: numeric-newest cache wins" "$CFG2/2.0.0" "$(fr_run_sh zsh "$T/fr/lexical" 'forge_root')"
536
+ else echo "SKIP: zsh not installed"; SKIPPED=$((SKIPPED+1)); fi
537
+ # INFRA-6: a Windows drive-letter FORGEDOCK_HOME is normalized by the helper (cygpath where present, else the /c/x spelling)
538
+ 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||')"
539
+
421
540
  echo "forge-root tests: pass=$PASS fail=$FAILN skipped=$SKIPPED"
422
541
  [ "$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."
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env bash
2
+ # forge-resolve.sh — sourced helper: locate ForgeDock's OWN install root and resolve its scripts.
3
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+ #
6
+ # Source it, never exec it: . "$_r/scripts/lib/forge-resolve.sh"
7
+ # forge_root prints the first trusted install root that carries scripts/lib/forge-resolve.sh
8
+ # forge_resolve <name> prints the absolute path of scripts/<name> from the first trusted root that has it,
9
+ # or prints nothing (status 0). Callers fail closed on empty output.
10
+ #
11
+ # Trust boundary: roots come only from the engine (FORGEDOCK_HOME, authoritative when set), the plugin runtime
12
+ # (CLAUDE_PLUGIN_ROOT), the legacy FORGE_ROOT/FORGE_HOME variables, the install.sh symlink, the Codex
13
+ # $CODEX_HOME/forge-home file, the newest pinned plugin cache and the marketplace checkout under CLAUDE_CONFIG_DIR
14
+ # then ~/.claude, and last the directory this file was sourced from. The consumer's working directory and the repo
15
+ # under review are author-controlled and are never consulted, and every root must be an absolute path.
16
+ #
17
+ # Order (matches docs/CONFIG.md "Install root resolution"): FORGEDOCK_HOME (when set, nothing else is consulted) >
18
+ # CLAUDE_PLUGIN_ROOT > FORGE_ROOT > FORGE_HOME > install symlink > Codex forge-home > newest cache > marketplace > self.
19
+ # The self root is last on purpose: the stub locator that sourced this file picks a cache directory by glob order
20
+ # (lexical), so the helper re-resolves by the numeric-newest tier instead of trusting the copy that happened to load.
21
+
22
+ # Root this file was sourced from (scripts/lib/.. /..), captured now because the cwd changes later. Under zsh
23
+ # BASH_SOURCE is empty, so the sourced path comes from ${(%):-%x} (eval'd: bash cannot parse that expansion). An empty
24
+ # path would make dirname "." (a cwd-derived root), so there is no self root when neither shell reports one.
25
+ _FORGE_SELF_ROOT=""; _forge_src=""
26
+ if [ -n "${BASH_SOURCE[0]:-}" ]; then _forge_src="${BASH_SOURCE[0]}"
27
+ elif [ -n "${ZSH_VERSION:-}" ]; then eval '_forge_src="${(%):-%x}"' 2>/dev/null || _forge_src=""; fi
28
+ [ -n "$_forge_src" ] && _FORGE_SELF_ROOT="$(cd "$(dirname "$_forge_src")/../.." 2>/dev/null && pwd -P)"
29
+ unset _forge_src
30
+
31
+ # Windows drive-letter path (C:/x or C:\x) -> /c/x (cygpath when present); anything else is printed unchanged.
32
+ _forge_posix() {
33
+ local _w="${1:-}" _u
34
+ case "$_w" in
35
+ [A-Za-z]:[/\\]*)
36
+ _u="$(cygpath -u "$_w" 2>/dev/null || true)"
37
+ [ -n "$_u" ] || _u="/$(printf %s "$_w" | cut -c1 | tr 'A-Z' 'a-z')$(printf %s "${_w#??}" | tr '\\' '/')"
38
+ printf '%s\n' "$_u" ;;
39
+ *) printf '%s\n' "$_w" ;;
40
+ esac
41
+ }
42
+
43
+ # One absolute candidate root per line, highest priority first.
44
+ _forge_candidates() {
45
+ local _l _cfg _mk _x _cx
46
+ if [ -n "${FORGEDOCK_HOME:-}" ]; then _forge_posix "$FORGEDOCK_HOME"; return 0; fi
47
+ _mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
48
+ printf '%s\n' "${CLAUDE_PLUGIN_ROOT:-}" "${FORGE_ROOT:-}" "${FORGE_HOME:-}"
49
+ for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
50
+ [ -n "$_cfg" ] || continue
51
+ _l="$(readlink -f "$_cfg/commands/work-on.md" 2>/dev/null || true)"
52
+ [ -n "$_l" ] && printf '%s\n' "${_l%/commands/work-on.md}"
53
+ done
54
+ # Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path).
55
+ _cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) head -n 1 "$_cx/forge-home" 2>/dev/null || true; echo ;; esac
56
+ for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
57
+ [ -n "$_cfg" ] || continue
58
+ find -L "$_cfg/plugins/cache/$_mk/forgedock" -mindepth 1 -maxdepth 1 -type d 2>/dev/null \
59
+ | 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)}' \
60
+ | sort -k1,1nr -k2,2nr -k3,3nr | cut -d' ' -f4- || true
61
+ done
62
+ for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
63
+ [ -n "$_cfg" ] && printf '%s\n' "$_cfg/plugins/marketplaces/$_mk"
64
+ done
65
+ printf '%s\n' "$_FORGE_SELF_ROOT"
66
+ }
67
+
68
+ # forge_resolve <path under scripts/>: print the first match; print nothing when unresolved. Always returns 0, so a
69
+ # command substitution survives `set -e` and the caller's own empty-output check is the fail-closed branch.
70
+ forge_resolve() {
71
+ local _n="${1:-}" _c _all
72
+ case "$_n" in ''|/*|*..*) return 0 ;; esac
73
+ # Materialize the list first: returning early from a `< <(...)` loop would close the pipe under the generator (EPIPE noise).
74
+ _all="$(_forge_candidates)"
75
+ while IFS= read -r _c; do
76
+ case "$_c" in /*) [ -f "$_c/scripts/$_n" ] && { printf '%s\n' "$_c/scripts/$_n"; return 0; } ;; esac
77
+ done <<< "$_all"
78
+ return 0
79
+ }
80
+
81
+ # forge_root: print the first trusted install root (the one that carries this helper); prints nothing when unresolved.
82
+ forge_root() {
83
+ local _f
84
+ _f="$(forge_resolve lib/forge-resolve.sh)"
85
+ [ -n "$_f" ] && printf '%s\n' "${_f%/scripts/lib/forge-resolve.sh}"
86
+ return 0
87
+ }