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.
- package/bin/engine/phases.mjs +54 -12
- package/bin/engine/runlog.mjs +12 -0
- package/bin/engine-cli.mjs +3 -1
- package/bin/engine.mjs +64 -16
- package/bin/hooks/interactive-engine.mjs +135 -212
- package/bin/hooks/pre-tool-use.mjs +16 -4
- package/bin/labels.json +5 -0
- package/bin/opencode-adapter.mjs +1 -1
- package/bin/runner.mjs +234 -21
- package/commands/autopilot.md +6 -6
- package/commands/cleanup.md +7 -4
- package/commands/orchestrate/phase-1-resolve.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +14 -19
- package/commands/orchestrate/phase-4-execution.md +50 -38
- package/commands/orchestrate/phase-6-report.md +6 -0
- package/commands/orchestrate/safety.md +1 -1
- package/commands/pipeline-health/phase-2-metrics.md +39 -0
- package/commands/pipeline-health/phase-5-report.md +1 -0
- package/commands/pipeline-resume.md +1 -1
- package/commands/pipeline-status.md +2 -2
- package/commands/review-pr-agents/api.md +1 -1
- package/commands/review-pr-agents/auth.md +1 -1
- package/commands/review-pr-agents/infra.md +3 -3
- package/commands/review-pr-agents/protocols.md +14 -5
- package/commands/review-pr-agents/scraper.md +1 -1
- package/commands/review-pr-agents/security.md +5 -5
- package/commands/review-pr-agents.md +1 -1
- package/commands/review-pr-staging.md +226 -8
- package/commands/review-pr.md +239 -29
- package/commands/work-on/build/architect.md +36 -0
- package/commands/work-on/build/validate.md +88 -4
- package/commands/work-on/build.md +95 -3
- package/commands/work-on/remediate.md +115 -13
- package/commands/work-on.md +4 -4
- package/docs/CONFIG.md +29 -0
- package/docs/RELEASE-NOTES.md +45 -0
- package/docs/spec/forge-protocol-v1.md +22 -1
- package/docs/spec/review-protocol.md +19 -4
- package/package.json +2 -1
- package/scripts/check-blast-radius.sh +164 -0
- package/scripts/check-blast-radius.test.sh +183 -0
- package/scripts/check-contract-scope.sh +137 -0
- package/scripts/check-contract-scope.test.sh +83 -0
- package/scripts/check-spec-bash.sh +60 -3
- package/scripts/check-spec-bash.test.sh +56 -0
- package/scripts/check-spec-markers.sh +29 -0
- package/scripts/check-test-wiring.sh +57 -0
- package/scripts/check-test-wiring.test.sh +70 -0
- package/scripts/classify-finding.sh +72 -8
- package/scripts/classify-finding.test.sh +77 -8
- package/scripts/close-dossier-no-main-commit.test.sh +12 -12
- package/scripts/diff-size.test.sh +3 -2
- package/scripts/doctor-pipeline-state.sh +35 -0
- package/scripts/forge-annotation.sh +3 -0
- package/scripts/forge-root.test.sh +70 -1
- package/scripts/forge-run.sh +2 -2
- package/scripts/recovery-claim-live.test.sh +10 -7
- package/scripts/remediate-basesync.test.sh +103 -0
- package/scripts/review-delta.sh +206 -0
- package/scripts/review-delta.test.sh +213 -0
- package/scripts/review-pr-gate2.test.sh +84 -0
- package/scripts/review-pr-staging-scope.test.sh +99 -0
- package/scripts/run-shell-tests.sh +115 -0
- 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 ]
|