forgedock 1.12.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/bin/engine/phases.mjs +54 -12
  2. package/bin/engine/runlog.mjs +12 -0
  3. package/bin/engine-cli.mjs +3 -1
  4. package/bin/engine.mjs +64 -16
  5. package/bin/hooks/interactive-engine.mjs +135 -212
  6. package/bin/hooks/pre-tool-use.mjs +16 -4
  7. package/bin/labels.json +5 -0
  8. package/bin/opencode-adapter.mjs +1 -1
  9. package/bin/runner.mjs +234 -21
  10. package/commands/autopilot.md +6 -6
  11. package/commands/cleanup.md +7 -4
  12. package/commands/orchestrate/phase-1-resolve.md +1 -1
  13. package/commands/orchestrate/phase-3-dependency.md +14 -19
  14. package/commands/orchestrate/phase-4-execution.md +50 -38
  15. package/commands/orchestrate/phase-6-report.md +6 -0
  16. package/commands/orchestrate/safety.md +1 -1
  17. package/commands/pipeline-health/phase-2-metrics.md +39 -0
  18. package/commands/pipeline-health/phase-5-report.md +1 -0
  19. package/commands/pipeline-resume.md +1 -1
  20. package/commands/pipeline-status.md +2 -2
  21. package/commands/review-pr-agents/api.md +1 -1
  22. package/commands/review-pr-agents/auth.md +1 -1
  23. package/commands/review-pr-agents/infra.md +3 -3
  24. package/commands/review-pr-agents/protocols.md +14 -5
  25. package/commands/review-pr-agents/scraper.md +1 -1
  26. package/commands/review-pr-agents/security.md +5 -5
  27. package/commands/review-pr-agents.md +1 -1
  28. package/commands/review-pr-staging.md +226 -8
  29. package/commands/review-pr.md +239 -29
  30. package/commands/work-on/build/architect.md +36 -0
  31. package/commands/work-on/build/validate.md +88 -4
  32. package/commands/work-on/build.md +95 -3
  33. package/commands/work-on/remediate.md +115 -13
  34. package/commands/work-on.md +4 -4
  35. package/docs/CONFIG.md +29 -0
  36. package/docs/RELEASE-NOTES.md +45 -0
  37. package/docs/spec/forge-protocol-v1.md +22 -1
  38. package/docs/spec/review-protocol.md +19 -4
  39. package/package.json +2 -1
  40. package/scripts/check-blast-radius.sh +164 -0
  41. package/scripts/check-blast-radius.test.sh +183 -0
  42. package/scripts/check-contract-scope.sh +137 -0
  43. package/scripts/check-contract-scope.test.sh +83 -0
  44. package/scripts/check-spec-bash.sh +60 -3
  45. package/scripts/check-spec-bash.test.sh +56 -0
  46. package/scripts/check-spec-markers.sh +29 -0
  47. package/scripts/check-test-wiring.sh +57 -0
  48. package/scripts/check-test-wiring.test.sh +70 -0
  49. package/scripts/classify-finding.sh +72 -8
  50. package/scripts/classify-finding.test.sh +77 -8
  51. package/scripts/close-dossier-no-main-commit.test.sh +12 -12
  52. package/scripts/diff-size.test.sh +3 -2
  53. package/scripts/doctor-pipeline-state.sh +35 -0
  54. package/scripts/forge-annotation.sh +3 -0
  55. package/scripts/forge-root.test.sh +70 -1
  56. package/scripts/forge-run.sh +2 -2
  57. package/scripts/recovery-claim-live.test.sh +10 -7
  58. package/scripts/remediate-basesync.test.sh +103 -0
  59. package/scripts/review-delta.sh +206 -0
  60. package/scripts/review-delta.test.sh +213 -0
  61. package/scripts/review-pr-gate2.test.sh +84 -0
  62. package/scripts/review-pr-staging-scope.test.sh +99 -0
  63. package/scripts/run-shell-tests.sh +115 -0
  64. package/scripts/select-fix-targets.sh +17 -5
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ # check-contract-scope.sh — Parser/validator for the `### Out of Scope` section of a
6
+ # FORGE:CONTRACT comment. One grammar shared by work-on:build
7
+ # (validate before posting) and review-pr (list, to demote findings
8
+ # that match a contract-declared item).
9
+ #
10
+ # Usage:
11
+ # check-contract-scope.sh validate [<file>] # exit 0 valid, 1 invalid (one line per bad item on stderr)
12
+ # check-contract-scope.sh list [<file>] # TSV: disposition<TAB>path<TAB>issue, one row per path
13
+ # Input is the contract comment body, from <file> or stdin.
14
+ #
15
+ # Grammar. The section is either the single line `None.` or a list of bullets, each naming at
16
+ # least one backticked path or symbol followed by exactly one disposition:
17
+ # - `path` — deferred → #N: <why> (a follow-up issue exists; `->` is accepted for `→`)
18
+ # - `path` — not-affected: <evidence>
19
+ # - `path` — accepted-risk: <reason>
20
+ # `deferred` without an issue number, an item without a backticked path, an item with no
21
+ # disposition, and a missing or empty section are all invalid.
22
+ #
23
+ # `list` prints disposition as one of: deferred, not-affected, accepted-risk. The issue column is
24
+ # the number for deferred rows and empty otherwise. A trailing `/` is stripped from paths. `*`
25
+ # is not a glob: consumers match literally (equal, or under the path as a directory prefix).
26
+ # `list` on an invalid section prints nothing and exits 1 (fail toward filing in review).
27
+ #
28
+ # Exit: 0 ok; 1 invalid section; 2 usage error. Portable: bash 3.2, no network.
29
+
30
+ set -u
31
+
32
+ usage() { echo "ERROR: Usage: check-contract-scope.sh validate|list [<file>]" >&2; exit 2; }
33
+
34
+ MODE="${1:-}"
35
+ case "$MODE" in validate|list) ;; *) usage ;; esac
36
+ [ "$#" -le 2 ] || usage
37
+ if [ "$#" -eq 2 ]; then
38
+ [ -r "$2" ] || { echo "ERROR: not readable: $2" >&2; exit 2; }
39
+ BODY=$(cat "$2")
40
+ else
41
+ BODY=$(cat)
42
+ fi
43
+
44
+ SECTION=$(printf '%s\n' "$BODY" | tr -d '\r' | awk '
45
+ /^### Out of Scope[[:space:]]*$/ { p = 1; found = 1; next }
46
+ p && /^### / { p = 0 }
47
+ p && /^> Pipeline powered by/ { p = 0 }
48
+ p && /^<!--/ { p = 0 }
49
+ p { print }
50
+ END { if (!found) exit 3 }
51
+ ')
52
+ RC=$?
53
+ if [ "$RC" -ne 0 ]; then
54
+ echo "invalid: no '### Out of Scope' section" >&2
55
+ exit 1
56
+ fi
57
+
58
+ ROWS=""
59
+ ERRS=""
60
+ COUNT=0
61
+ NONE=0
62
+ ITEM=""
63
+
64
+ flush_item() {
65
+ [ -n "$ITEM" ] || return 0
66
+ COUNT=$((COUNT + 1))
67
+ _text=$(printf '%s' "$ITEM" | sed -E 's/deferred[[:space:]]*(→|->)[[:space:]]*/deferred → /g')
68
+ # Earliest disposition keyword wins; the prefix before it holds the paths.
69
+ _pre_n="${_text%%not-affected:*}"
70
+ _pre_a="${_text%%accepted-risk:*}"
71
+ _pre_d="${_text%%deferred → *}"
72
+ _disp=""; _pre="$_text"
73
+ if [ "$_pre_n" != "$_text" ]; then _disp="not-affected"; _pre="$_pre_n"; fi
74
+ if [ "$_pre_a" != "$_text" ] && { [ -z "$_disp" ] || [ "${#_pre_a}" -lt "${#_pre}" ]; }; then _disp="accepted-risk"; _pre="$_pre_a"; fi
75
+ if [ "$_pre_d" != "$_text" ] && { [ -z "$_disp" ] || [ "${#_pre_d}" -lt "${#_pre}" ]; }; then _disp="deferred"; _pre="$_pre_d"; fi
76
+ _issue=""
77
+ if [ -z "$_disp" ]; then
78
+ ERRS="${ERRS}untyped item (needs deferred → #N, not-affected:, or accepted-risk:): ${ITEM}
79
+ "
80
+ ITEM=""; return 0
81
+ fi
82
+ if [ "$_disp" = "deferred" ]; then
83
+ _rest="${_text#*deferred → }"
84
+ if [[ "$_rest" =~ ^#([0-9]+) ]]; then
85
+ _issue="${BASH_REMATCH[1]}"
86
+ else
87
+ ERRS="${ERRS}deferred item has no issue number (needs deferred → #N): ${ITEM}
88
+ "
89
+ ITEM=""; return 0
90
+ fi
91
+ fi
92
+ _paths=$(printf '%s' "$_pre" | grep -o '`[^`][^`]*`' | tr -d '`')
93
+ if [ -z "$_paths" ]; then
94
+ ERRS="${ERRS}item names no backticked path or symbol: ${ITEM}
95
+ "
96
+ ITEM=""; return 0
97
+ fi
98
+ while IFS= read -r _p; do
99
+ [ -n "$_p" ] || continue
100
+ _p="${_p%/}"
101
+ ROWS="${ROWS}${_disp} ${_p} ${_issue}
102
+ "
103
+ done <<EOP
104
+ $_paths
105
+ EOP
106
+ ITEM=""
107
+ }
108
+
109
+ while IFS= read -r line; do
110
+ trimmed=$(printf '%s' "$line" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
111
+ [ -n "$trimmed" ] || continue
112
+ case "$trimmed" in
113
+ "None."|"- None."|"* None.") NONE=1; continue ;;
114
+ "- "*|"* "*)
115
+ flush_item
116
+ ITEM=$(printf '%s' "$trimmed" | sed -E 's/^[-*][[:space:]]+//') ;;
117
+ *)
118
+ if [ -n "$ITEM" ]; then ITEM="${ITEM} ${trimmed}"
119
+ else ERRS="${ERRS}unexpected text outside a bullet: ${trimmed}
120
+ "; fi ;;
121
+ esac
122
+ done <<EOS
123
+ $SECTION
124
+ EOS
125
+ flush_item
126
+
127
+ if [ "$COUNT" -eq 0 ] && [ "$NONE" -eq 0 ] && [ -z "$ERRS" ]; then
128
+ ERRS="empty '### Out of Scope' section (write None. or typed items)
129
+ "
130
+ fi
131
+
132
+ if [ -n "$ERRS" ]; then
133
+ printf 'invalid: %s' "$ERRS" >&2
134
+ exit 1
135
+ fi
136
+ [ "$MODE" = "list" ] && printf '%s' "$ROWS"
137
+ exit 0
@@ -0,0 +1,83 @@
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-contract-scope.sh (typed `### Out of Scope` grammar).
6
+ # Run: bash scripts/check-contract-scope.test.sh (bash 3.2 compatible)
7
+
8
+ set -u
9
+ DIR=$(cd "$(dirname "$0")" && pwd)
10
+ S="$DIR/check-contract-scope.sh"
11
+ pass=0; fail=0
12
+ TAB=$(printf '\t')
13
+
14
+ mk() { printf '## Builder Contract\n\n### Deliverables\n\n| a | b |\n\n### Out of Scope\n\n%s\n\n> Pipeline powered by [ForgeDock](https://example.invalid)\n' "$1"; }
15
+
16
+ # check <valid|invalid> <description> <section-text>
17
+ check() {
18
+ want="$1"; desc="$2"; body=$(mk "$3")
19
+ printf '%s\n' "$body" | bash "$S" validate >/dev/null 2>&1; got=$?
20
+ if { [ "$want" = valid ] && [ "$got" = 0 ]; } || { [ "$want" = invalid ] && [ "$got" = 1 ]; }; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: $desc (want $want, exit $got)"; fi
21
+ }
22
+ # list_is <description> <section-text> <expected TSV (printf-style, \t and \n)>
23
+ list_is() {
24
+ desc="$1"; body=$(mk "$2"); want=$(printf "$3")
25
+ got=$(printf '%s\n' "$body" | bash "$S" list 2>/dev/null)
26
+ if [ "$got" = "$want" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: $desc"; echo " want: $want"; echo " got: $got"; fi
27
+ }
28
+
29
+ check valid "None. accepted" 'None.'
30
+ check valid "deferred with issue" '- `scripts/a.sh` — deferred → #12: tracked there'
31
+ check valid "not-affected with evidence" '- `bin/engine/` — not-affected: no reader found by grep'
32
+ check valid "accepted-risk with reason" '- `pkg/types.js` — accepted-risk: body-only format'
33
+ check valid "ASCII arrow accepted" '- `a.sh` — deferred -> #7: later'
34
+ check valid "no-space arrow accepted" '- `a.sh` — deferred→#7: later'
35
+ check valid "three mixed items" '- `a/` — not-affected: x
36
+ - `b.js` — accepted-risk: y
37
+ - `c.md` — deferred → #3446: z'
38
+ check valid "continuation line joins item" '- `a.sh` — deferred → #5:
39
+ because of reasons'
40
+ check invalid "untyped bullet rejected" '- `a.sh` — out of scope for now'
41
+ check invalid "deferred without number rejected" '- `a.sh` — deferred: later'
42
+ check invalid "deferred arrow without number" '- `a.sh` — deferred → TBD: later'
43
+ check invalid "no backticked path rejected" '- the sync script — deferred → #9: later'
44
+ check invalid "bare prose rejected" 'Nothing relevant.'
45
+ check invalid "one bad item among good ones" '- `a` — not-affected: ok
46
+ - `b` — nope'
47
+ check invalid "empty section rejected" ''
48
+
49
+ # Missing section
50
+ printf '## Builder Contract\n\n### Deliverables\n\nx\n' | bash "$S" validate >/dev/null 2>&1; got=$?
51
+ if [ "$got" = 1 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: missing section (want exit 1, got $got)"; fi
52
+
53
+ list_is "list rows for three forms" '- `a/` — not-affected: x
54
+ - `b.js` — accepted-risk: y
55
+ - `c.md` — deferred → #3446: z' "not-affected${TAB}a${TAB}\naccepted-risk${TAB}b.js${TAB}\ndeferred${TAB}c.md${TAB}3446"
56
+ list_is "multi-path item emits one row per path" '- `x.sh`, `y.sh` — deferred → #8: both' "deferred${TAB}x.sh${TAB}8\ndeferred${TAB}y.sh${TAB}8"
57
+ list_is "why-text with other keywords does not change disposition" '- `a` — accepted-risk: not-affected elsewhere, deferred → #1 is separate' "accepted-risk${TAB}a${TAB}"
58
+ list_is "None. lists nothing" 'None.' ""
59
+ list_is "invalid section lists nothing" '- `a` — untyped' ""
60
+
61
+ # Section ends at the next ### heading
62
+ printf '### Out of Scope\n\n- `a` — not-affected: x\n\n### Quality\n\n- plain bullet\n' | bash "$S" validate >/dev/null 2>&1; got=$?
63
+ if [ "$got" = 0 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: section must end at next ### (exit $got)"; fi
64
+
65
+ # Live-shaped contract (mixed items, trailing attribution line)
66
+ LIVE=$(mk '- `bin/engine/` — not-affected: no engine module reads the contract Out of Scope section (grep in investigation).
67
+ - `packages/protocol/src/types.js` — accepted-risk: CONTRACT requiredFields stays [Task type].
68
+ - `commands/work-on/build/architect.md` — deferred → #3446: blast-radius manifest is tracked there.')
69
+ got=$(printf '%s\n' "$LIVE" | bash "$S" list 2>/dev/null | wc -l | tr -d ' ')
70
+ if [ "$got" = 3 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: live-shaped contract rows (want 3, got $got)"; fi
71
+
72
+ # File argument and usage errors
73
+ TMPF=$(mktemp "${TMPDIR:-/tmp}/ccs.XXXXXX"); printf '%s\n' "$LIVE" > "$TMPF"
74
+ bash "$S" validate "$TMPF" >/dev/null 2>&1; got=$?
75
+ if [ "$got" = 0 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: file argument (exit $got)"; fi
76
+ rm -f "$TMPF"
77
+ bash "$S" validate /nonexistent/x >/dev/null 2>&1; got=$?
78
+ if [ "$got" = 2 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: unreadable file (want 2, got $got)"; fi
79
+ bash "$S" bogus </dev/null >/dev/null 2>&1; got=$?
80
+ if [ "$got" = 2 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: bad mode (want 2, got $got)"; fi
81
+
82
+ echo "check-contract-scope.test.sh: passed=$pass failed=$fail"
83
+ [ "$fail" -eq 0 ]
@@ -13,6 +13,7 @@
13
13
  # Usage:
14
14
  # check-spec-bash.sh [--base <git-ref>] [--shellcheck] <file.md>...
15
15
  # check-spec-bash.sh --positional [<file.md>...]
16
+ # check-spec-bash.sh --fence-state <VAR>... -- <file.md>...
16
17
  # check-spec-bash.sh (no args: same as --positional over all Skill-loaded commands/**/*.md,
17
18
  # excluding orchestrate/**, pipeline-health/**, review-pr-agents/**)
18
19
  #
@@ -25,6 +26,13 @@
25
26
  # Scope the file list to Skill-loaded specs (Read-loaded orchestrate/**,
26
27
  # pipeline-health/** and the review-pr-agents catalog are not substituted).
27
28
  # Output: `FAIL <file>:<line> positional arg ...` then `SPEC-POSITIONAL: ...`.
29
+ # --fence-state Different check, opt-in: every Bash tool call is a fresh shell, so a variable bound
30
+ # in one fence is empty in the next. Fail any ```bash/sh fence that reads `$VAR` or
31
+ # `${VAR...}` for a listed VAR without assigning it in the same fence (`VAR=`,
32
+ # `export VAR=`, `read VAR`, `for VAR in`) at or before its first read. Comment-only lines are ignored; `$REPO`
33
+ # does not match `$REPO_FLAG`. Suppress a deliberate read by putting
34
+ # `allowlist:fence-state` on the same line. Pseudo-tool-call fences are scanned too.
35
+ # Output: `FAIL <file>:<line> fence-state ...` then `SPEC-FENCE-STATE: ...`.
28
36
  # --base <ref> Only check blocks that contain a line changed relative to <ref>
29
37
  # (`git diff -U0 <ref> -- <file>`), so pre-existing blocks never fail
30
38
  # a new change. Without --base every block is checked.
@@ -45,14 +53,22 @@
45
53
 
46
54
  set -u
47
55
 
48
- BASE=""; SHELLCHECK=0; POSITIONAL=0; FILES=""
49
- usage() { echo "ERROR: Usage: check-spec-bash.sh [--base <git-ref>] [--shellcheck] <file.md>... | --positional [<file.md>...]" >&2; exit 2; }
56
+ BASE=""; SHELLCHECK=0; POSITIONAL=0; FILES=""; FENCESTATE=0; FSVARS=""
57
+ usage() { echo "ERROR: Usage: check-spec-bash.sh [--base <git-ref>] [--shellcheck] <file.md>... | --positional [<file.md>...] | --fence-state <VAR>... -- <file.md>..." >&2; exit 2; }
50
58
 
51
59
  while [ "$#" -gt 0 ]; do
52
60
  case "$1" in
53
61
  --base) [ "$#" -ge 2 ] || usage; BASE="$2"; shift 2 ;;
54
62
  --shellcheck) SHELLCHECK=1; shift ;;
55
63
  --positional) POSITIONAL=1; shift ;;
64
+ --fence-state)
65
+ FENCESTATE=1; shift
66
+ while [ "$#" -gt 0 ] && [ "$1" != "--" ]; do
67
+ case "$1" in -*|*[!A-Za-z0-9_]*) usage ;; esac
68
+ FSVARS="$FSVARS $1"; shift
69
+ done
70
+ [ "$#" -gt 0 ] && [ -n "$FSVARS" ] || usage
71
+ shift ;;
56
72
  --*) usage ;;
57
73
  *) FILES="$FILES
58
74
  $1"; shift ;;
@@ -62,7 +78,7 @@ done
62
78
  # No file arguments: default to the --positional check over every Skill-loaded spec under commands/
63
79
  # (orchestrate/**, pipeline-health/** and the review-pr-agents catalog are Read-loaded, not substituted).
64
80
  # Resolved relative to this script, so it works from any cwd and from linked worktrees.
65
- if [ -z "$FILES" ] && { [ "$#" -eq 0 ] && [ "$POSITIONAL" = 0 ] && [ -z "$BASE" ] && [ "$SHELLCHECK" = 0 ] || [ "$POSITIONAL" = 1 ]; }; then
81
+ if [ -z "$FILES" ] && [ "$FENCESTATE" = 0 ] && { [ "$#" -eq 0 ] && [ "$POSITIONAL" = 0 ] && [ -z "$BASE" ] && [ "$SHELLCHECK" = 0 ] || [ "$POSITIONAL" = 1 ]; }; then
66
82
  POSITIONAL=1
67
83
  cd "$(dirname "$0")/.." || exit 2
68
84
  FILES=$(find commands -name '*.md' -not -path 'commands/orchestrate/*' \
@@ -70,6 +86,47 @@ if [ -z "$FILES" ] && { [ "$#" -eq 0 ] && [ "$POSITIONAL" = 0 ] && [ -z "$BASE"
70
86
  fi
71
87
  [ -n "$FILES" ] || usage
72
88
 
89
+ if [ "$FENCESTATE" = 1 ]; then
90
+ sfail=0; sfiles=0
91
+ while IFS= read -r f; do
92
+ [ -n "$f" ] || continue
93
+ case "$f" in *.md) ;; *) continue ;; esac
94
+ [ -f "$f" ] || { echo "SKIP $f missing"; continue; }
95
+ sfiles=$((sfiles + 1))
96
+ hits=$(awk -v f="$f" -v vars="$FSVARS" '
97
+ function flush( i, v, ln) {
98
+ for (i = 1; i <= nv; i++) {
99
+ v = vn[i]
100
+ if (first[v] != "" && !asg[v]) print "FAIL " f ":" first[v] " fence-state: $" v " read in a fence before it is assigned (fresh shell per Bash call): " firsttxt[v]
101
+ }
102
+ }
103
+ function reset( i) { for (i = 1; i <= nv; i++) { asg[vn[i]] = 0; first[vn[i]] = ""; firsttxt[vn[i]] = "" } }
104
+ BEGIN { nv = split(vars, vn, " ") }
105
+ !inb && /^[ \t]*```(bash|sh)[ \t]*$/ { inb = 1; reset(); next }
106
+ !inb && /^[ \t]*```/ { other = !other; next }
107
+ other { next }
108
+ inb && /^[ \t]*```[ \t]*$/ { flush(); inb = 0; next }
109
+ inb {
110
+ if ($0 ~ /^[ \t]*#/) next
111
+ for (i = 1; i <= nv; i++) {
112
+ v = vn[i]
113
+ if (first[v] == "" && ($0 ~ ("(^|[;&|( \t])(export[ \t]+|local[ \t]+|declare[ \t]+(-[A-Za-z]+[ \t]+)?|readonly[ \t]+)?" v "=") ||
114
+ $0 ~ ("(^|[;&|( \t])read[ \t]+(-[A-Za-z]+[ \t]+)*([A-Za-z0-9_]+[ \t]+)*" v "([ \t;]|$)") ||
115
+ $0 ~ ("(^|[;&|( \t])for[ \t]+" v "[ \t]+in[ \t]"))) asg[v] = 1
116
+ if (first[v] == "" && $0 !~ /allowlist:fence-state/ &&
117
+ ($0 ~ ("\\$" v "([^A-Za-z0-9_]|$)") || $0 ~ ("\\$\\{" v "([^A-Za-z0-9_]|$)"))) { first[v] = NR; firsttxt[v] = $0 }
118
+ }
119
+ }
120
+ ' "$f")
121
+ if [ -n "$hits" ]; then printf '%s\n' "$hits"; sfail=$((sfail + $(printf '%s\n' "$hits" | wc -l))); fi
122
+ done <<EOS
123
+ $FILES
124
+ EOS
125
+ echo "SPEC-FENCE-STATE: files=$sfiles violations=$sfail"
126
+ [ "$sfail" -eq 0 ]
127
+ exit $?
128
+ fi
129
+
73
130
  if [ "$POSITIONAL" = 1 ]; then
74
131
  pfail=0; pfiles=0
75
132
  while IFS= read -r f; do
@@ -109,5 +109,61 @@ check 1 "FAIL pos-bad.md:3" "--positional flags shell \$1"
109
109
  check 1 "violations=2" "--positional counts violations" -- --positional pos-bad.md
110
110
  check 0 "violations=0" "--positional passes safe forms, prose, allowlist" -- --positional pos-good.md
111
111
 
112
+ # --fence-state: a listed var read in a fence without a prior assignment in that same fence is flagged.
113
+ cat > "$T/fs-bad.md" <<EOF
114
+ ${FENCE}bash
115
+ gh pr view "\$PR_NUMBER" -R "\$REPO" --json state
116
+ ${FENCE}
117
+ ${FENCE}bash
118
+ X=\$(gh pr diff \${PR_NUMBER} --name-only)
119
+ ${FENCE}
120
+ ${FENCE}bash
121
+ echo "\${REPO:-none}"
122
+ ${FENCE}
123
+ ${FENCE}bash
124
+ gh pr view "\$PR_NUMBER"
125
+ PR_NUMBER="{PR_NUMBER}"
126
+ ${FENCE}
127
+ EOF
128
+ cat > "$T/fs-good.md" <<EOF
129
+ Prose mentions \$PR_NUMBER and \$REPO freely.
130
+ ${FENCE}bash
131
+ # comment reads \$PR_NUMBER and \$REPO
132
+ PR_NUMBER="{PR_NUMBER}"; REPO="{GH_REPO}"
133
+ gh pr view "\$PR_NUMBER" -R "\$REPO"
134
+ ${FENCE}
135
+ ${FENCE}bash
136
+ export REPO="{GH_REPO}"
137
+ for PR_NUMBER in 1 2; do echo "\${PR_NUMBER}"; done
138
+ echo "\$REPO"
139
+ ${FENCE}
140
+ ${FENCE}bash
141
+ REPO="\${REPO:-\$(gh repo view)}"
142
+ ${FENCE}
143
+ ${FENCE}bash
144
+ gh pr view \${REPO_FLAG} "\$OTHER"
145
+ ${FENCE}
146
+ ${FENCE}bash
147
+ echo "\$PR_NUMBER" # allowlist:fence-state
148
+ ${FENCE}
149
+ ${FENCE}text
150
+ echo \$PR_NUMBER \$REPO
151
+ ${FENCE}
152
+ ${FENCE}bash
153
+ PR_NUMBER="{PR_NUMBER}"
154
+ OUT=\$(Skill(skill="x", args="\$PR_NUMBER"))
155
+ ${FENCE}
156
+ EOF
157
+ check 1 "FAIL fs-bad.md:2 fence-state: \$PR_NUMBER" "--fence-state flags \$VAR read" -- --fence-state PR_NUMBER REPO -- fs-bad.md
158
+ check 1 "FAIL fs-bad.md:2 fence-state: \$REPO" "--fence-state flags each listed var" -- --fence-state PR_NUMBER REPO -- fs-bad.md
159
+ check 1 "FAIL fs-bad.md:5 fence-state: \$PR_NUMBER" "--fence-state flags \${VAR} form" -- --fence-state PR_NUMBER REPO -- fs-bad.md
160
+ check 1 "FAIL fs-bad.md:8 fence-state: \$REPO" "--fence-state flags \${VAR:-default}" -- --fence-state PR_NUMBER REPO -- fs-bad.md
161
+ check 1 "FAIL fs-bad.md:11 fence-state: \$PR_NUMBER" "--fence-state flags read before assignment" -- --fence-state PR_NUMBER REPO -- fs-bad.md
162
+ check 1 "violations=5" "--fence-state counts violations" -- --fence-state PR_NUMBER REPO -- fs-bad.md
163
+ check 0 "violations=0" "--fence-state ignores unlisted vars" -- --fence-state UNLISTED -- fs-bad.md
164
+ check 0 "violations=0" "--fence-state passes declared, loop, export, REPO_FLAG, allowlist, comments, prose, non-bash fences" -- --fence-state PR_NUMBER REPO -- fs-good.md
165
+ check 2 "Usage" "--fence-state without -- is a usage error" -- --fence-state PR_NUMBER fs-good.md
166
+ check 2 "Usage" "--fence-state without vars is a usage error" -- --fence-state -- fs-good.md
167
+
112
168
  echo "check-spec-bash.test.sh: passed=$pass failed=$fail"
113
169
  [ "$fail" -eq 0 ]
@@ -88,6 +88,7 @@ MARKER_REGISTRY="
88
88
  BASESYNC_REMEDIATION
89
89
  BATCH_MEMBERS
90
90
  BENCH_SCORECARD
91
+ BLAST_RADIUS
91
92
  BLOCKED_ON_HUMAN_MERGE
92
93
  BODY
93
94
  BUILDER
@@ -106,6 +107,7 @@ MARKER_REGISTRY="
106
107
  DECISION_RECORD
107
108
  DECOMPOSED
108
109
  DECOMPOSE_BLOCKED
110
+ DEFERRED_FROM
109
111
  DESIGN
110
112
  DIFF_SIZE
111
113
  DISPATCH
@@ -416,6 +418,33 @@ if [ -f "$WORKON_SPEC" ]; then
416
418
  fi
417
419
  fi
418
420
 
421
+ # ---------------------------------------------------------------------------
422
+ # Re-review claim id must be printed by both producers (forge#3472)
423
+ # ---------------------------------------------------------------------------
424
+ # A claim id generated in one bash block and compared in a later one is lost unless the producer prints it.
425
+ # Orchestrator: the Step 2 claim block's final echo must carry CLAIM_ID=. Router: Claim pre-check step 3 must
426
+ # require printing CLAIM_ID=. Scoped to the Step 2 / Claim pre-check text only; full-line HTML comments skipped.
427
+ # WIRE:PROVEN: manual mutation in a temp copy: removing CLAIM_ID= from the Step 2 echo fired the orchestrator check, removing it from router step 3 fired the router check, clean tree passes
428
+ P4_SPEC="$COMMANDS_DIR/orchestrate/phase-4-execution.md"
429
+ if [ -f "$P4_SPEC" ]; then
430
+ STEP2_ECHO=$(awk '
431
+ /Step 2 — dispatch/ { on = 1 }
432
+ /Step 3 — outcome/ { on = 0 }
433
+ on && !/^[[:space:]]*<!--.*-->[[:space:]]*$/ && /^[[:space:]]*echo "REREVIEW_MARKER=/ { print }
434
+ ' "$P4_SPEC" || true)
435
+ if ! printf '%s\n' "$STEP2_ECHO" | grep -qF 'CLAIM_ID='; then
436
+ echo "HIGH | $P4_SPEC | Step 2 claim block's final echo does not print CLAIM_ID= (own claim id is lost across bash blocks)" >&2
437
+ VIOLATIONS=$((VIOLATIONS + 1))
438
+ fi
439
+ fi
440
+ if [ -f "$WORKON_SPEC" ]; then
441
+ ROUTER_STEP3=$(printf '%s\n' "${PRECHECK_STEPS:-}" | grep '^STEP3:' || true)
442
+ if [ -z "$ROUTER_STEP3" ] || [[ "$ROUTER_STEP3" != *CLAIM_ID=* ]]; then
443
+ echo "HIGH | $WORKON_SPEC | claim pre-check step 3 does not require printing CLAIM_ID= (router own claim id is lost across bash blocks)" >&2
444
+ VIOLATIONS=$((VIOLATIONS + 1))
445
+ fi
446
+ fi
447
+
419
448
  # ---------------------------------------------------------------------------
420
449
  # Summary
421
450
  # ---------------------------------------------------------------------------
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ # Lint: every scripts/*.test.sh must be referenced by a workflow in .github/workflows/*.yml.
6
+ # An unreferenced test never runs in CI, so regressions in the script under test merge silently.
7
+ # Lists EVERY unwired test before exiting non-zero. bash 3.2 portable (no bash-4 builtins, no PCRE grep).
8
+ #
9
+ # Usage: bash scripts/check-test-wiring.sh [<scripts_dir> [<workflows_dir>]]
10
+ # defaults: scripts .github/workflows (relative to the current directory)
11
+ # Exit: 0 all wired | 1 one or more unwired, or workflows dir missing/empty | 2 bad scripts dir
12
+
13
+ set -u
14
+ SCRIPTS_DIR="${1:-scripts}"
15
+ WORKFLOWS_DIR="${2:-.github/workflows}"
16
+
17
+ [ -d "$SCRIPTS_DIR" ] || { echo "check-test-wiring: scripts dir not found: $SCRIPTS_DIR" >&2; exit 2; }
18
+ [ -d "$WORKFLOWS_DIR" ] || { echo "check-test-wiring: workflows dir not found: $WORKFLOWS_DIR" >&2; exit 1; }
19
+
20
+ wf_files=""
21
+ for w in "$WORKFLOWS_DIR"/*.yml "$WORKFLOWS_DIR"/*.yaml; do
22
+ [ -f "$w" ] && wf_files="$wf_files
23
+ $w"
24
+ done
25
+ if [ -z "$wf_files" ]; then
26
+ echo "check-test-wiring: no workflow files in $WORKFLOWS_DIR" >&2
27
+ exit 1
28
+ fi
29
+
30
+ total=0
31
+ unwired=0
32
+ for t in "$SCRIPTS_DIR"/*.test.sh; do
33
+ [ -f "$t" ] || continue
34
+ total=$((total+1))
35
+ base=$(basename "$t")
36
+ # Escape '.' for ERE; names are [A-Za-z0-9._-]. Delimiter-aware: the name must not be a
37
+ # fragment of a longer filename (foo.test.sh must not be satisfied by barfoo.test.sh).
38
+ esc=$(printf '%s' "$base" | sed 's/\./\\./g')
39
+ found=0
40
+ while IFS= read -r w; do
41
+ [ -n "$w" ] || continue
42
+ if grep -Eq "(^|[^A-Za-z0-9._-])${esc}([^A-Za-z0-9._-]|\$)" "$w" 2>/dev/null; then found=1; break; fi
43
+ done <<EOF2
44
+ $wf_files
45
+ EOF2
46
+ if [ "$found" -eq 0 ]; then
47
+ echo "UNWIRED: $base is not referenced by any workflow in $WORKFLOWS_DIR"
48
+ unwired=$((unwired+1))
49
+ fi
50
+ done
51
+
52
+ if [ "$unwired" -gt 0 ]; then
53
+ echo "check-test-wiring: $unwired of $total test script(s) unwired — add each to a workflow (e.g. shell-compat.yml)" >&2
54
+ exit 1
55
+ fi
56
+ echo "check-test-wiring: all $total test script(s) wired"
57
+ exit 0
@@ -0,0 +1,70 @@
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-test-wiring.sh. Run: bash scripts/check-test-wiring.test.sh (bash 3.2 compatible)
6
+
7
+ set -u
8
+ DIR=$(cd "$(dirname "$0")" && pwd)
9
+ S="$DIR/check-test-wiring.sh"
10
+ T=$(mktemp -d "${TMPDIR:-/tmp}/check-test-wiring-test.XXXXXX")
11
+ trap 'rm -rf "$T"' EXIT
12
+ pass=0; fail=0
13
+
14
+ # check <want_rc> <want_substring|-> <unwanted_substring|-> <desc> <scripts_dir> <workflows_dir>
15
+ check() {
16
+ want_rc="$1"; want="$2"; unwant="$3"; desc="$4"; sd="$5"; wd="$6"
17
+ out=$(bash "$S" "$sd" "$wd" 2>&1); rc=$?
18
+ ok=1
19
+ if [ "$want" != "-" ]; then case "$out" in *"$want"*) ;; *) ok=0 ;; esac; fi
20
+ if [ "$unwant" != "-" ]; then case "$out" in *"$unwant"*) ok=0 ;; esac; fi
21
+ if [ "$rc" = "$want_rc" ] && [ "$ok" = 1 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: $desc (rc=$rc)"; printf '%s\n' "$out" | sed 's/^/ /'; fi
22
+ }
23
+
24
+ mk() { mkdir -p "$T/$1/scripts" "$T/$1/wf"; }
25
+
26
+ # 1. all wired -> pass
27
+ mk ok
28
+ for n in a b c; do : > "$T/ok/scripts/$n.test.sh"; done
29
+ printf 'steps:\n - run: bash scripts/a.test.sh\n - run: |\n /bin/bash scripts/b.test.sh\n bash scripts/c.test.sh\n' > "$T/ok/wf/x.yml"
30
+ check 0 "all 3 test script(s) wired" - "all wired passes" "$T/ok/scripts" "$T/ok/wf"
31
+
32
+ # 2. two+ unwired -> fail, both listed, wired one not listed
33
+ mk bad
34
+ for n in a b c d; do : > "$T/bad/scripts/$n.test.sh"; done
35
+ printf 'run: bash scripts/a.test.sh\n' > "$T/bad/wf/x.yml"
36
+ check 1 "UNWIRED: b.test.sh" "UNWIRED: a.test.sh" "unwired b reported, wired a not" "$T/bad/scripts" "$T/bad/wf"
37
+ check 1 "UNWIRED: c.test.sh" - "unwired c reported too" "$T/bad/scripts" "$T/bad/wf"
38
+ check 1 "UNWIRED: d.test.sh" - "unwired d reported too" "$T/bad/scripts" "$T/bad/wf"
39
+ check 1 "3 of 4" - "summary counts every offender" "$T/bad/scripts" "$T/bad/wf"
40
+
41
+ # 3. substring of a longer name does not satisfy the match
42
+ mk sub
43
+ : > "$T/sub/scripts/foo.test.sh"
44
+ printf 'run: bash scripts/barfoo.test.sh\nrun: bash scripts/foo.test.sh.bak\nrun: bash scripts/foo-test-sh\n' > "$T/sub/wf/x.yml"
45
+ check 1 "UNWIRED: foo.test.sh" - "longer-name substring is not a match" "$T/sub/scripts" "$T/sub/wf"
46
+
47
+ # 4. '.' is literal, not a regex wildcard
48
+ mk dot
49
+ : > "$T/dot/scripts/foo.test.sh"
50
+ printf 'run: bash scripts/fooXtestYsh\n' > "$T/dot/wf/x.yml"
51
+ check 1 "UNWIRED: foo.test.sh" - "dot is not a wildcard" "$T/dot/scripts" "$T/dot/wf"
52
+
53
+ # 5. reference in a second workflow file counts
54
+ mk multi
55
+ : > "$T/multi/scripts/a.test.sh"
56
+ printf 'name: one\n' > "$T/multi/wf/one.yml"
57
+ printf 'run: bash scripts/a.test.sh\n' > "$T/multi/wf/two.yaml"
58
+ check 0 "wired" - "reference in any workflow counts" "$T/multi/scripts" "$T/multi/wf"
59
+
60
+ # 6. empty / missing workflows dir fails
61
+ mk empty
62
+ : > "$T/empty/scripts/a.test.sh"
63
+ check 1 "no workflow files" - "empty workflows dir fails" "$T/empty/scripts" "$T/empty/wf"
64
+ check 1 "workflows dir not found" - "missing workflows dir fails" "$T/empty/scripts" "$T/empty/nope"
65
+
66
+ # 7. missing scripts dir is a usage error
67
+ check 2 "scripts dir not found" - "missing scripts dir fails" "$T/nope" "$T/empty/wf"
68
+
69
+ echo "check-test-wiring.test.sh: passed=$pass failed=$fail"
70
+ [ "$fail" -eq 0 ]