forgedock 1.11.0 → 1.12.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.
Files changed (39) hide show
  1. package/bin/engine/phases-args.test.mjs +113 -0
  2. package/bin/engine/phases.mjs +433 -109
  3. package/bin/engine/runlog.mjs +21 -1
  4. package/bin/engine-cli.mjs +14 -2
  5. package/bin/engine.mjs +150 -19
  6. package/bin/forgedock.mjs +2 -2
  7. package/bin/hooks/interactive-engine.mjs +3 -0
  8. package/bin/labels.json +5 -0
  9. package/bin/orchestrate-preflight.mjs +1 -1
  10. package/bin/runner.mjs +111 -12
  11. package/bin/watch-utils.mjs +1 -0
  12. package/commands/cleanup.md +1 -1
  13. package/commands/orchestrate/phase-4-execution.md +58 -24
  14. package/commands/orchestrate/phase-6-report.md +29 -0
  15. package/commands/quality-gate.md +96 -12
  16. package/commands/recover-orphans.md +5 -5
  17. package/commands/review-pr-agents/protocols.md +1 -1
  18. package/commands/review-pr-agents.md +11 -11
  19. package/commands/review-pr.md +227 -33
  20. package/commands/work-on/build/implement.md +1 -1
  21. package/commands/work-on/build/validate.md +58 -7
  22. package/commands/work-on/build.md +11 -4
  23. package/commands/work-on/decompose.md +2 -2
  24. package/commands/work-on/investigate.md +14 -4
  25. package/commands/work-on/remediate.md +109 -19
  26. package/commands/work-on/review.md +79 -25
  27. package/commands/work-on.md +23 -15
  28. package/docs/WORK-ON-RUNTIME.md +2 -2
  29. package/docs/spec/label-state-machine.md +6 -1
  30. package/docs/spec/review-protocol.md +1 -1
  31. package/package.json +1 -1
  32. package/packages/protocol/src/phases.js +27 -1
  33. package/scripts/check-branch-ancestry.sh +82 -0
  34. package/scripts/check-branch-ancestry.test.sh +78 -0
  35. package/scripts/check-spec-markers.sh +2 -0
  36. package/scripts/forge-root.test.sh +36 -15
  37. package/scripts/review-pr-domain-map.test.sh +87 -0
  38. package/scripts/spec-no-worktree-script.test.sh +46 -0
  39. package/scripts/transition-label.sh +3 -1
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ # Fixtures for scripts/check-branch-ancestry.sh. Run: bash scripts/check-branch-ancestry.test.sh (bash 3.2 compatible)
6
+
7
+ set -u
8
+ DIR=$(cd "$(dirname "$0")" && pwd)
9
+ S="$DIR/check-branch-ancestry.sh"
10
+ T=$(mktemp -d "${TMPDIR:-/tmp}/check-branch-ancestry-test.XXXXXX")
11
+ trap 'rm -rf "$T"' EXIT
12
+ pass=0; fail=0
13
+ OUT=""
14
+
15
+ g() { git -C "$T/r" -c user.name=t -c user.email=t@example.com -c commit.gpgsign=false "$@" >/dev/null 2>&1; }
16
+ commit() { echo "$1" > "$T/r/$1.txt"; g add -A; g commit -m "$1"; }
17
+ # run <branch> <base>: sets OUT and RC
18
+ run() { OUT=$(cd "$T/r" && bash "$S" "$@" 2>&1); RC=$?; }
19
+ expect() { # expect <want_rc> <desc> [want_substring]
20
+ if [ "$RC" = "$1" ] && { [ -z "${3:-}" ] || case "$OUT" in *"$3"*) true ;; *) false ;; esac; }; then
21
+ pass=$((pass+1))
22
+ else
23
+ fail=$((fail+1)); echo "FAIL: $2 (rc=$RC, want $1)"; printf '%s\n' "$OUT" | sed 's/^/ /'
24
+ fi
25
+ }
26
+
27
+ mkdir "$T/r"; git -C "$T/r" init -q -b staging >/dev/null 2>&1 || { git -C "$T/r" init -q; g checkout -b staging; }
28
+ commit base
29
+ # milestone diverges from staging after staging2
30
+ commit staging2
31
+ g checkout -b milestone/m1; commit ms1
32
+ g checkout staging
33
+ # feature branch off the original base commit
34
+ g checkout -b feat HEAD~1; commit feat1
35
+
36
+ # 1. no merges
37
+ run feat staging; expect 0 "no merges passes"
38
+
39
+ # 2. base-sync merge passes (staging advances, then is merged into feat)
40
+ g merge --no-ff -m "Merge staging into feat" staging
41
+ run feat staging; expect 0 "base-sync merge passes"
42
+ run HEAD staging; expect 0 "HEAD form passes"
43
+
44
+ # 3. staging advances further and is synced again: still passes
45
+ g checkout -q staging; commit staging3; g checkout -q feat
46
+ g merge --no-ff -m "Merge staging again" staging
47
+ run feat staging; expect 0 "repeated base-sync merges pass"
48
+
49
+ # 4. foreign merge fails, foreign parent printed
50
+ MS=$(git -C "$T/r" rev-parse milestone/m1)
51
+ g merge --no-ff -m "Merge milestone into feat" milestone/m1
52
+ run feat staging; expect 1 "foreign merge fails" "$MS"
53
+ case "$OUT" in *"Merge milestone into feat"*) pass=$((pass+1)) ;; *) fail=$((fail+1)); echo "FAIL: subject not printed"; echo "$OUT" ;; esac
54
+
55
+ # 5. octopus with one foreign parent fails
56
+ g checkout -q -b feat2 staging; commit f2
57
+ g checkout -q staging; g checkout -q -b side; commit side1
58
+ g checkout -q feat2
59
+ g merge --no-ff -m "octopus" side milestone/m1
60
+ run feat2 staging; expect 1 "octopus with foreign parent fails" "$MS"
61
+
62
+ # 5b. branch cut from a milestone line, then syncing the base: foreign first-parent history fails
63
+ MS1=$(git -C "$T/r" rev-parse milestone/m1)
64
+ g checkout -q -b feat3 milestone/m1; commit f3
65
+ g merge --no-ff -m "Merge staging into feat3" staging
66
+ run feat3 staging; expect 1 "milestone-cut branch with base sync fails" "$MS1"
67
+
68
+ # 5c. milestone-cut branch with no merge at all also fails
69
+ g checkout -q -b feat4 milestone/m1; commit f4
70
+ run feat4 staging; expect 1 "milestone-cut branch without merges fails" "$MS1"
71
+
72
+ # 6. bad refs fail closed (exit 2)
73
+ run nosuchbranch staging; expect 2 "bad branch ref exits 2"
74
+ run feat nosuchbase; expect 2 "bad base ref exits 2"
75
+ run feat; expect 2 "missing arg exits 2"
76
+
77
+ echo "check-branch-ancestry tests: $pass passed, $fail failed"
78
+ [ "$fail" -eq 0 ]
@@ -84,6 +84,8 @@ MARKER_REGISTRY="
84
84
  AUTOPILOT_IMPACT
85
85
  BATCHABLE
86
86
  BATCH_ID
87
+ BASESYNC_FAILED
88
+ BASESYNC_REMEDIATION
87
89
  BATCH_MEMBERS
88
90
  BENCH_SCORECARD
89
91
  BLOCKED_ON_HUMAN_MERGE
@@ -13,7 +13,7 @@ ok() { PASS=$((PASS+1)); }
13
13
  bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
14
14
  expect() { [ "$2" = "$3" ] && ok || bad "$1 (got '$3' want '$2')"; }
15
15
 
16
- SITES="commands/work-on.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/orchestrate/phase-1-resolve.md commands/orchestrate/phase-4-execution.md"
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/orchestrate/phase-1-resolve.md commands/orchestrate/phase-4-execution.md"
17
17
 
18
18
  # extract the bootstrap block (comment line .. closing top-level fi), indentation stripped
19
19
  # Block ends at the first line (after the marker) that starts with `fi` at the marker's own indentation.
@@ -229,20 +229,41 @@ if command -v node >/dev/null 2>&1; then
229
229
  done
230
230
  fi
231
231
 
232
- # Guard (#3400): gate-helper (trusted-comments.sh) resolution loops must never list "$PWD" — the repo
233
- # under review is author-controlled. Exact copy counts + normalized identity prevent drift.
234
- TC_LOOPS="commands/review-pr.md:7 commands/work-on/review.md:1"
235
- : > "$T/tc_all"
236
- for e in $TC_LOOPS; do
237
- f="${e%%:*}"; want="${e##*:}"
238
- n=$(grep -c 'trusted-comments.sh" \]' "$ROOT/$f"); expect "trusted-comments.sh loop count in $f" "$want" "$n"
239
- grep -n 'for _c in' "$ROOT/$f" | while IFS=: read -r ln _; do
240
- sed -n "$((ln+1))p" "$ROOT/$f" | grep -q 'trusted-comments.sh' && sed -n "${ln}p" "$ROOT/$f" | sed 's/^[ ]*//' >> "$T/tc_all"
241
- done
242
- done
243
- expect "trusted-comments.sh loops total" 8 "$(wc -l < "$T/tc_all" | tr -d ' ')"
244
- expect "trusted-comments.sh loops byte-identical" 1 "$(sort -u "$T/tc_all" | wc -l | tr -d ' ')"
245
- grep -qF '"$PWD"' "$T/tc_all" && bad 'trusted-comments.sh loop lists "$PWD"' || ok
232
+ # Guard (#3400, #3454): every gate-helper (trusted-comments.sh) resolver block under commands/ must be
233
+ # byte-identical, must scan the plugin cache (the plugin-root placeholder is not always substituted in
234
+ # forked runs, so without the scan a cache-only install never resolves the script), and must never list the
235
+ # working directory (the repo under review is author-controlled). Blocks are discovered, not hard-coded, so a
236
+ # new copy cannot drift unnoticed. A block runs from its '# TRUSTED_SCRIPT resolver' header to 'done <<< "$_tc"'.
237
+ : > "$T/tc_all"; TC_BLOCKS=0; TC_STRAY=0
238
+ while IFS= read -r f; do
239
+ awk -v out="$T/tc_all" '
240
+ /^[ ]*# TRUSTED_SCRIPT resolver/ {inb=1; line=""}
241
+ inb { sub(/^[ ]+/, ""); line = line $0 "\\n" }
242
+ inb && /^done <<< "\$_tc"$/ { print line >> out; inb=0; n++ }
243
+ END { print n+0 }' "$f" > "$T/tc_n"
244
+ TC_BLOCKS=$((TC_BLOCKS + $(cat "$T/tc_n")))
245
+ # every trusted-comments.sh resolution case-arm must belong to a block (no stray short copies)
246
+ arms=$(grep -c 'trusted-comments.sh" \]' "$f" || true)
247
+ [ "$arms" = "$(cat "$T/tc_n")" ] || { TC_STRAY=$((TC_STRAY + 1)); echo " stray resolver in $f: $arms arms vs $(cat "$T/tc_n") blocks"; }
248
+ done < <(grep -rl 'trusted-comments.sh" \]' "$ROOT/commands")
249
+ expect "trusted-comments.sh resolver blocks discovered (review-pr 7 + review + remediate + phase-4)" 10 "$TC_BLOCKS"
250
+ expect "trusted-comments.sh resolvers without a canonical block" 0 "$TC_STRAY"
251
+ expect "trusted-comments.sh resolvers byte-identical" 1 "$(sort -u "$T/tc_all" | wc -l | tr -d ' ')"
252
+ grep -qF 'plugins/cache/forgedock/forgedock' "$T/tc_all" && ok || bad 'trusted-comments.sh resolver lacks the plugin-cache scan'
253
+ grep -qF 'CLAUDE_CONFIG_DIR' "$T/tc_all" && ok || bad 'trusted-comments.sh resolver lacks CLAUDE_CONFIG_DIR'
254
+ grep -qF '"$PWD"' "$T/tc_all" && bad 'trusted-comments.sh resolver lists "$PWD"' || ok
255
+
256
+ # Behavior (#3454): a cache-only install with an unsubstituted plugin-root placeholder must resolve the script.
257
+ # Run the real block from review.md against a fake HOME that has only plugins/cache/forgedock/forgedock/<semver>.
258
+ awk '/^[ ]*# TRUSTED_SCRIPT resolver/ {inb=1} inb {sub(/^[ ]+/, ""); print} inb && /^done <<< "\$_tc"$/ {exit}' "$ROOT/commands/work-on/review.md" > "$T/tc_block.sh"
259
+ mkdir -p "$T/tchome/.claude/plugins/cache/forgedock/forgedock/1.9.0/scripts" "$T/tchome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts" "$T/tcwork"
260
+ : > "$T/tchome/.claude/plugins/cache/forgedock/forgedock/1.9.0/scripts/trusted-comments.sh"
261
+ : > "$T/tchome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts/trusted-comments.sh"
262
+ mkdir -p "$T/tcwork/scripts"; : > "$T/tcwork/scripts/trusted-comments.sh" # author-controlled cwd copy must NOT win
263
+ out=$(cd "$T/tcwork" && env -i PATH="$PATH" HOME="$T/tchome" bash -c 'source "$1"; printf %s "$TRUSTED_SCRIPT"' _ "$T/tc_block.sh" 2>&1)
264
+ expect "cache-only install resolves newest cached trusted-comments.sh" "$T/tchome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts/trusted-comments.sh" "$out"
265
+ out=$(cd "$T/tcwork" && env -i PATH="$PATH" HOME="$T/nohome" bash -c 'source "$1"; printf %s "$TRUSTED_SCRIPT"' _ "$T/tc_block.sh" 2>&1)
266
+ expect "no install resolves empty (cwd copy never used)" "" "$out"
246
267
 
247
268
  echo "forge-root tests: pass=$PASS fail=$FAILN skipped=$SKIPPED"
248
269
  [ "$FAILN" -eq 0 ]
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ # Guards the review-pr roster-label to marker-domain mapping: every roster spelling collapses to one marker
6
+ # domain, the helper is identical wherever it is inlined, the roster dedups across spellings, and every
7
+ # persona file is covered by the table and the catalog. Run: bash scripts/review-pr-domain-map.test.sh
8
+ # (bash 3.2 compatible)
9
+
10
+ set -u
11
+ DIR=$(cd "$(dirname "$0")" && pwd)
12
+ ROOT=$(cd "$DIR/.." && pwd)
13
+ SPEC="$ROOT/commands/review-pr.md"
14
+ CATALOG="$ROOT/commands/review-pr-agents.md"
15
+ PERSONAS="$ROOT/commands/review-pr-agents"
16
+ pass=0; fail=0
17
+ ok() { pass=$((pass+1)); }
18
+ bad() { fail=$((fail+1)); echo "FAIL: $1"; }
19
+
20
+ WORK=$(mktemp -d "${TMPDIR:-/tmp}/review-pr-domain-map.XXXXXX")
21
+ trap 'rm -rf "$WORK"' EXIT
22
+
23
+ # Test 1: helper copies. Every inline definition must be byte-identical (each Bash call is a fresh shell).
24
+ awk -v dir="$WORK" '
25
+ /^# BEGIN review-pr-agent-domain/ {n++; out=dir "/helper." n; on=1}
26
+ on {print > out}
27
+ /^# END review-pr-agent-domain/ {on=0; close(out)}
28
+ ' "$SPEC"
29
+ copies=$(ls "$WORK"/helper.* 2>/dev/null | wc -l | tr -d ' ')
30
+ if [ "$copies" -ge 4 ]; then ok; else bad "expected >=4 inline agent_domain copies (roster, canonicalize, dispatch, phase 4), found $copies"; fi
31
+ for f in "$WORK"/helper.*; do
32
+ [ -f "$f" ] || continue
33
+ if cmp -s "$f" "$WORK/helper.1"; then ok; else bad "$(basename "$f") differs from helper.1"; fi
34
+ done
35
+
36
+ # Test 2: no site derives the marker by lowercasing the roster label directly
37
+ if grep -nE "AGENT_DOMAIN=.*tr '\[:upper:\]' '\[:lower:\]'" "$SPEC" >/dev/null; then bad "direct lowercase marker derivation remains"; else ok; fi
38
+ sites=$(grep -c 'AGENT_DOMAIN=$(agent_domain "$AGENT")' "$SPEC")
39
+ if [ "$sites" -ge 2 ]; then ok; else bad "expected >=2 agent_domain derivation sites, found $sites"; fi
40
+
41
+ # Test 3: spelling collapse
42
+ # shellcheck disable=SC1090
43
+ . "$WORK/helper.1"
44
+ check() { got=$(agent_domain "$1"); if [ "$got" = "$2" ]; then ok; else bad "agent_domain '$1' = '$got', want '$2'"; fi; }
45
+ for l in INFRA Infrastructure infra infrastructure; do check "$l" infra; done
46
+ for l in SCRAPING Scraping scraper Scraper scraping; do check "$l" scraper; done
47
+ for l in FRONTEND Frontend frontend Web; do check "$l" frontend; done
48
+ for l in Security SECURITY; do check "$l" security; done
49
+ for l in AUTH Auth; do check "$l" auth; done
50
+ for l in BILLING Billing; do check "$l" billing; done
51
+ for l in CONCURRENCY Concurrency; do check "$l" concurrency; done
52
+ for l in DATABASE Database; do check "$l" database; done
53
+ for l in API Api; do check "$l" api; done
54
+ check "CustomDomain" customdomain
55
+
56
+ # Test 4: add_agent dedups across spellings; canonicalization block yields one entry per persona
57
+ ADD=$(awk '/^add_agent\(\) \{/{on=1} on{print} on && /^\}/{exit}' "$SPEC")
58
+ if [ -n "$ADD" ]; then ok; else bad "add_agent not found in spec"; fi
59
+ eval "$ADD"
60
+ SELECTED_AGENTS="Security INFRA SCRAPING"
61
+ add_agent "Infrastructure"; add_agent "Scraping"; add_agent "Auth"; add_agent "AUTH"; add_agent "security"
62
+ want=$(printf '%s' "Security INFRA SCRAPING auth")
63
+ if [ "$SELECTED_AGENTS" = "$want" ]; then ok; else bad "add_agent roster '$SELECTED_AGENTS', want '$want'"; fi
64
+
65
+ CANON=$(awk '/^# END review-pr-agent-domain/{seen++} seen && /^CANON_ROSTER=/{on=1} on{print} on && /^SELECTED_AGENTS=/{exit}' "$SPEC" | head -20)
66
+ SELECTED_AGENTS="Security AUTH INFRA Infrastructure SCRAPING Scraping Frontend API api"
67
+ eval "$CANON"
68
+ want="security auth infra scraper frontend api"
69
+ if [ "$SELECTED_AGENTS" = "$want" ]; then ok; else bad "canonical roster '$SELECTED_AGENTS', want '$want'"; fi
70
+ cnt=$(printf '%s' "$SELECTED_AGENTS" | tr ' ' '\n' | grep -c '.')
71
+ if [ "$cnt" -eq 6 ]; then ok; else bad "roster count $cnt, want 6"; fi
72
+
73
+ # Test 5: every persona file (minus protocols and spec-cli) has a table row in the spec and a catalog row
74
+ for pf in "$PERSONAS"/*.md; do
75
+ name=$(basename "$pf" .md)
76
+ case "$name" in protocols|spec-cli) continue ;; esac
77
+ if grep -qE "^\| .*\| \`$name\` \| \`$name\.md\` \|" "$SPEC"; then ok; else bad "spec table has no row for persona $name"; fi
78
+ if grep -qE "^\| .*\| \`$name\` \| \`review-pr-agents/$name\.md\` \|" "$CATALOG"; then ok; else bad "catalog has no marker-domain row for persona $name"; fi
79
+ got=$(agent_domain "$name")
80
+ if [ "$got" = "$name" ]; then ok; else bad "persona $name maps to $got"; fi
81
+ done
82
+
83
+ # Test 6: the protocol defines the marker domain through the mapping
84
+ if grep -qF 'marker domain' "$PERSONAS/protocols.md"; then ok; else bad "protocols.md does not define the marker domain"; fi
85
+
86
+ echo "review-pr-domain-map: $pass passed, $fail failed"
87
+ [ "$fail" -eq 0 ]
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bash
2
+ # spec-no-worktree-script.test.sh — guard/gate scripts named in specs must resolve from ForgeDock's
3
+ # install root only. A candidate under {WORKTREE_PATH}/scripts, {REPO_PATH}/scripts or a cwd-relative
4
+ # scripts/ is author-controlled (the PR branch under audit) and would let a branch supply the script
5
+ # that gates it. Read-only data uses (grep of a file's contents, JSON manifest reads) are not matched.
6
+ # Usage: bash scripts/spec-no-worktree-script.test.sh
7
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
8
+ # SPDX-License-Identifier: AGPL-3.0-or-later
9
+ set -uo pipefail
10
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$HERE/.."
11
+ PASS=0; FAILN=0
12
+ ok() { PASS=$((PASS+1)); }
13
+ bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
14
+
15
+ # Execution-resolution patterns: a fallback assignment or candidate list pointing at a repo-controlled scripts dir.
16
+ PATTERNS=(
17
+ '\|\| [A-Za-z_]+="\{(WORKTREE_PATH|REPO_PATH)\}/scripts/[A-Za-z0-9_.-]+\.sh"'
18
+ 'for _d in [^;]*\{(WORKTREE_PATH|REPO_PATH)\}/scripts'
19
+ '\$\{FORGEDOCK_SCRIPTS:-scripts\}'
20
+ '\|\| [A-Za-z_]+="scripts/[A-Za-z0-9_.-]+\.sh"'
21
+ )
22
+ FILES=$(cd "$ROOT" && find commands -name '*.md' | sort)
23
+ for p in "${PATTERNS[@]}"; do
24
+ hits=$(cd "$ROOT" && grep -nE "$p" $FILES 2>/dev/null || true)
25
+ [ -z "$hits" ] && ok || bad "repo-controlled script resolution matches /$p/: $hits"
26
+ done
27
+
28
+ # Representative swept instances must each carry the install-root-only resolution.
29
+ chk() { grep -qF "$2" "$ROOT/$1" && ok || bad "$1 lacks: $2"; }
30
+ chk commands/work-on/build/validate.md 'ANCESTRY_SCRIPT="${FORGE_ROOT:+$FORGE_ROOT/scripts/check-branch-ancestry.sh}"'
31
+ chk commands/quality-gate.md 'VALIDATOR="${FORGE_ROOT:+$FORGE_ROOT/scripts/validate-spec-graph.sh}"'
32
+ chk commands/quality-gate.md 'CONFLICT_CHECKER="${FORGE_ROOT:+$FORGE_ROOT/scripts/check-native-conflicts.sh}"'
33
+ chk commands/quality-gate.md 'SPEC_BASH_CHECKER="${FORGE_ROOT:+$FORGE_ROOT/scripts/check-spec-bash.sh}"'
34
+ chk commands/quality-gate.md 'CLASSIFIER="${FORGE_ROOT:+$FORGE_ROOT/scripts/flaky-quarantine.sh}"'
35
+ chk commands/review-pr.md 'CLASSIFIER="${FORGE_ROOT:+$FORGE_ROOT/scripts/flaky-quarantine.sh}"'
36
+ # validate.md must not name the worktree scripts dir at all (ac-1)
37
+ grep -qF '{WORKTREE_PATH}/scripts' "$ROOT/commands/work-on/build/validate.md" && bad "validate.md references {WORKTREE_PATH}/scripts" || ok
38
+
39
+ # Behavioral: the validate.md ancestry resolver with FORGE_ROOT unset/empty must resolve to nothing (never /scripts or ./scripts).
40
+ for fr in "" "/nonexistent-root"; do
41
+ out=$(FORGE_ROOT="$fr" bash -c 'ANCESTRY_SCRIPT="${FORGE_ROOT:+$FORGE_ROOT/scripts/check-branch-ancestry.sh}"; [ -f "$ANCESTRY_SCRIPT" ] || ANCESTRY_SCRIPT=""; printf %s "$ANCESTRY_SCRIPT"')
42
+ [ -z "$out" ] && ok || bad "resolver yielded '$out' for FORGE_ROOT='$fr'"
43
+ done
44
+
45
+ echo "spec-no-worktree-script tests: pass=$PASS fail=$FAILN"
46
+ [ "$FAILN" -eq 0 ]
@@ -8,7 +8,8 @@
8
8
  # GH_FLAG Repository flag passed to gh (e.g. -R RapierCraftStudios/forgedock)
9
9
  # May be multiple tokens — pass before TARGET_STATE
10
10
  # TARGET_STATE One of: investigating, ready-to-build, building, in-review,
11
- # merged, invalid, decomposed, awaiting-merge
11
+ # merged, invalid, decomposed, awaiting-merge,
12
+ # remediating (label workflow:remediating: autonomous remediation pending, not a human gate)
12
13
  #
13
14
  # --validate Sub-command mode: translate an investigation verdict into a
14
15
  # finding-lifecycle label (needs-validation → validated/false-positive).
@@ -184,6 +185,7 @@ VALID_STATES=(
184
185
  "invalid"
185
186
  "decomposed"
186
187
  "awaiting-merge"
188
+ "remediating"
187
189
  )
188
190
 
189
191
  # ---------------------------------------------------------------------------