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
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
# Usage:
|
|
11
11
|
# classify-finding.sh --severity <S> [--confidence <C>] [--agent <A>]
|
|
12
12
|
# [--lineage none|review-finding] [--text <T> | --text-file <F>]
|
|
13
|
+
# [--inpr-diff <F> --file <P>] [--contract-scope <F> --contract-open <F> [--pr-files <F>] [--merge-issue <N>]]
|
|
13
14
|
#
|
|
14
15
|
# --severity CRITICAL | HIGH | MEDIUM | LOW (case-insensitive). Missing or
|
|
15
16
|
# unparseable → ISSUE (fail toward filing, never drop).
|
|
@@ -27,14 +28,32 @@
|
|
|
27
28
|
# classified INPR_FIX — fix it on the PR before merge instead of filing it.
|
|
28
29
|
# Callers pass these only for the first fix round of an auto-merge review.
|
|
29
30
|
#
|
|
31
|
+
# --contract-scope <F> --contract-open <F> --file <P> Contract-declared scope gate: F is the TSV
|
|
32
|
+
# from `check-contract-scope.sh list` (disposition<TAB>path<TAB>issue). A finding
|
|
33
|
+
# whose path equals, or sits under, a `deferred` or `accepted-risk` item becomes
|
|
34
|
+
# `NOTE contract-deferred #N` / `NOTE contract-accepted-risk`. `--contract-open`
|
|
35
|
+
# lists the deferred issue numbers confirmed open, one per line; a deferred item
|
|
36
|
+
# demotes only when its number is listed (closed, unreadable or absent: no demotion).
|
|
37
|
+
# The caller lists a number only after verifying it is an open issue (not a PR) whose
|
|
38
|
+
# body carries `FORGE:DEFERRED_FROM: #<merge issue>` and the path.
|
|
39
|
+
# `--merge-issue <N>` is the PR's linked issue: `deferred → #N` never demotes (that
|
|
40
|
+
# issue is always open during review, so it is no follow-up).
|
|
41
|
+
# Never demoted: CRITICAL/HIGH, `not-affected` items, safety-exempt findings
|
|
42
|
+
# (security/billing keywords or a dedicated domain agent) under either disposition, and
|
|
43
|
+
# findings whose path is in `--inpr-diff` or `--pr-files` (this PR touched it; the
|
|
44
|
+
# contract excluded the sibling path, not regressions the PR introduced in it).
|
|
45
|
+
# `--pr-files <F>` is the PR's changed paths, one per line, supplied for this guard
|
|
46
|
+
# alone (it never triggers INPR_FIX). The gate runs after the severity and in-PR gates. Without the flags output is unchanged.
|
|
47
|
+
#
|
|
30
48
|
# Output (stdout, one line): `ISSUE <reason>`, `NOTE <reason>` or `INPR_FIX <reason>`.
|
|
31
49
|
# Exit codes: 0 classified, 2 usage error.
|
|
32
50
|
#
|
|
33
51
|
# Rules (forge#3060, tightened after the 2026-10-08 cascade audit):
|
|
34
52
|
# 1. HIGH/CRITICAL, or unparseable severity → ISSUE.
|
|
35
53
|
# 2. Safety exemption is CONTENT-based: the text matches the security/billing
|
|
36
|
-
# keyword set
|
|
37
|
-
#
|
|
54
|
+
# keyword set (a LOW finding additionally needs CONFIRMED confidence), or the
|
|
55
|
+
# finding came from a dedicated, signal-selected domain
|
|
56
|
+
# agent (Auth, Billing, Concurrency, Database) and is MEDIUM. Origin from the always-on
|
|
38
57
|
# "General Security & Quality" agent alone NO LONGER exempts a finding —
|
|
39
58
|
# that agent runs on every PR, so origin-based exemption filed nearly every
|
|
40
59
|
# LOW note it raised (50 of 60 would-be notes in the audited batch).
|
|
@@ -57,9 +76,13 @@ LINEAGE="none"
|
|
|
57
76
|
TEXT=""
|
|
58
77
|
INPR_DIFF=""
|
|
59
78
|
FINDING_FILE=""
|
|
79
|
+
CONTRACT_SCOPE=""
|
|
80
|
+
CONTRACT_OPEN=""
|
|
81
|
+
PR_FILES=""
|
|
82
|
+
MERGE_ISSUE=""
|
|
60
83
|
|
|
61
84
|
usage() {
|
|
62
|
-
echo "ERROR: Usage: classify-finding.sh --severity <S> [--confidence <C>] [--agent <A>] [--lineage none|review-finding] [--text <T> | --text-file <F>] [--inpr-diff <F> --file <P>]" >&2
|
|
85
|
+
echo "ERROR: Usage: classify-finding.sh --severity <S> [--confidence <C>] [--agent <A>] [--lineage none|review-finding] [--text <T> | --text-file <F>] [--inpr-diff <F> --file <P>] [--contract-scope <F> --contract-open <F> [--pr-files <F>] [--merge-issue <N>]]" >&2
|
|
63
86
|
exit 2
|
|
64
87
|
}
|
|
65
88
|
|
|
@@ -72,6 +95,10 @@ while [ "$#" -gt 0 ]; do
|
|
|
72
95
|
--text) [ "$#" -ge 2 ] || usage; TEXT="$2"; shift 2 ;;
|
|
73
96
|
--inpr-diff) [ "$#" -ge 2 ] || usage; INPR_DIFF="$2"; shift 2 ;;
|
|
74
97
|
--file) [ "$#" -ge 2 ] || usage; FINDING_FILE="$2"; shift 2 ;;
|
|
98
|
+
--contract-scope) [ "$#" -ge 2 ] || usage; CONTRACT_SCOPE="$2"; shift 2 ;;
|
|
99
|
+
--contract-open) [ "$#" -ge 2 ] || usage; CONTRACT_OPEN="$2"; shift 2 ;;
|
|
100
|
+
--pr-files) [ "$#" -ge 2 ] || usage; PR_FILES="$2"; shift 2 ;;
|
|
101
|
+
--merge-issue) [ "$#" -ge 2 ] || usage; MERGE_ISSUE="${2#\#}"; shift 2 ;;
|
|
75
102
|
--text-file)
|
|
76
103
|
[ "$#" -ge 2 ] || usage
|
|
77
104
|
[ -r "$2" ] || { echo "ERROR: --text-file not readable: $2" >&2; exit 2; }
|
|
@@ -89,6 +116,17 @@ if [ -n "$INPR_DIFF" ] && [ ! -r "$INPR_DIFF" ]; then
|
|
|
89
116
|
echo "ERROR: --inpr-diff not readable: $INPR_DIFF" >&2; exit 2
|
|
90
117
|
fi
|
|
91
118
|
|
|
119
|
+
if [ -n "$CONTRACT_SCOPE" ] && [ ! -r "$CONTRACT_SCOPE" ]; then
|
|
120
|
+
echo "ERROR: --contract-scope not readable: $CONTRACT_SCOPE" >&2; exit 2
|
|
121
|
+
fi
|
|
122
|
+
if [ -n "$CONTRACT_OPEN" ] && [ ! -r "$CONTRACT_OPEN" ]; then
|
|
123
|
+
echo "ERROR: --contract-open not readable: $CONTRACT_OPEN" >&2; exit 2
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
if [ -n "$PR_FILES" ] && [ ! -r "$PR_FILES" ]; then
|
|
127
|
+
echo "ERROR: --pr-files not readable: $PR_FILES" >&2; exit 2
|
|
128
|
+
fi
|
|
129
|
+
|
|
92
130
|
upper() { printf '%s' "$1" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]'; }
|
|
93
131
|
SEV=$(upper "$SEVERITY")
|
|
94
132
|
CONF=$(upper "$CONFIDENCE")
|
|
@@ -109,22 +147,48 @@ if [ -n "$INPR_DIFF" ] && [ -n "$FINDING_FILE" ] && [ "$SEV" = "MEDIUM" ] && [ "
|
|
|
109
147
|
fi
|
|
110
148
|
fi
|
|
111
149
|
|
|
112
|
-
|
|
113
150
|
# Safety exemption. Keywords match whole words, where `_`, `-`, `/` and other
|
|
114
151
|
# punctuation separate words (so `auth_service` matches, `author` does not).
|
|
115
152
|
SAFETY=""
|
|
116
153
|
KW='security|auth|authz|authn|billing|payment|stripe|charge|invoice|injection|xss|csrf|ssrf|idor|secret|secrets|credential|credentials|permission|permissions|sql|token|password|redact'
|
|
117
154
|
if printf '%s' "$TEXT" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9\n' ' ' | grep -Eqw "($KW)"; then
|
|
118
|
-
|
|
155
|
+
# A LOW finding needs the keyword AND CONFIRMED; MEDIUM keeps the keyword exemption at any confidence.
|
|
156
|
+
if [ "$SEV" != "LOW" ] || [ "$CONF" = "CONFIRMED" ]; then SAFETY="keyword"; fi
|
|
119
157
|
fi
|
|
120
|
-
# A dedicated domain agent rescues a finding only
|
|
121
|
-
#
|
|
158
|
+
# A dedicated domain agent rescues a MEDIUM finding only: a LOW finding stays a NOTE
|
|
159
|
+
# whichever reviewer raised it, even when CONFIRMED.
|
|
122
160
|
AGENT_LC=$(printf '%s' "$AGENT" | tr '[:upper:]' '[:lower:]')
|
|
123
161
|
case "$AGENT_LC" in
|
|
124
162
|
auth*|billing*|concurrency*|database*)
|
|
125
|
-
if [ "$SEV" = "MEDIUM" ]
|
|
163
|
+
if [ "$SEV" = "MEDIUM" ]; then SAFETY="${SAFETY:-domain-agent}"; fi ;;
|
|
126
164
|
esac
|
|
127
165
|
|
|
166
|
+
# Contract-declared scope gate (#3447). Fails toward filing: any missing input, closed or unlisted
|
|
167
|
+
# deferred issue, a deferral to the PR's own issue, an in-PR path, or a safety-exempt finding keeps
|
|
168
|
+
# the classification below.
|
|
169
|
+
if [ -n "$CONTRACT_SCOPE" ] && [ -n "$FINDING_FILE" ]; then
|
|
170
|
+
_fpath="${FINDING_FILE#./}"; _fpath="${_fpath%%:*}"
|
|
171
|
+
_inpr=""
|
|
172
|
+
if [ -n "$INPR_DIFF" ] && grep -Fxq -- "$_fpath" "$INPR_DIFF"; then _inpr=1; fi
|
|
173
|
+
if [ -n "$PR_FILES" ] && grep -Fxq -- "$_fpath" "$PR_FILES"; then _inpr=1; fi
|
|
174
|
+
if [ -z "$_inpr" ]; then
|
|
175
|
+
while IFS="$(printf '\t')" read -r _disp _ipath _inum; do
|
|
176
|
+
[ -n "$_ipath" ] || continue
|
|
177
|
+
_ipath="${_ipath#./}"; _ipath="${_ipath%/}"
|
|
178
|
+
[ "$_fpath" = "$_ipath" ] || case "$_fpath" in "$_ipath"/*) ;; *) continue ;; esac
|
|
179
|
+
case "$_disp" in
|
|
180
|
+
deferred)
|
|
181
|
+
if [ -z "$SAFETY" ] && [ -n "$_inum" ] && [ "$_inum" != "$MERGE_ISSUE" ] \
|
|
182
|
+
&& [ -n "$CONTRACT_OPEN" ] && grep -Fxq -- "$_inum" "$CONTRACT_OPEN"; then
|
|
183
|
+
echo "NOTE contract-deferred #$_inum"; exit 0
|
|
184
|
+
fi ;;
|
|
185
|
+
accepted-risk)
|
|
186
|
+
if [ -z "$SAFETY" ]; then echo "NOTE contract-accepted-risk"; exit 0; fi ;;
|
|
187
|
+
esac
|
|
188
|
+
done < "$CONTRACT_SCOPE"
|
|
189
|
+
fi
|
|
190
|
+
fi
|
|
191
|
+
|
|
128
192
|
if [ "$LINEAGE" = "review-finding" ]; then
|
|
129
193
|
if [ "$SEV" = "MEDIUM" ] && { [ "$CONF" = "CONFIRMED" ] || { [ -n "$SAFETY" ] && [ "$CONF" = "LIKELY" ]; }; }; then
|
|
130
194
|
echo "ISSUE lineage-medium-${CONF:-UNKNOWN}"
|
|
@@ -38,19 +38,27 @@ expect NOTE "LOW from Security agent, no keyword, is a NOTE (audit fix)" -- --s
|
|
|
38
38
|
expect NOTE "LOW from General Security & Quality, no keyword" -- --severity LOW --confidence LIKELY --agent "General Security & Quality" --text "duplicated predicate in NOTE count"
|
|
39
39
|
|
|
40
40
|
# --- Safety exemption (content-based) ---
|
|
41
|
-
expect
|
|
42
|
-
expect
|
|
43
|
-
expect
|
|
44
|
-
expect
|
|
41
|
+
expect NOTE "LOW POSSIBLE injection keyword is a NOTE" -- --severity LOW --confidence POSSIBLE --text "shell injection via unquoted var"
|
|
42
|
+
expect NOTE "LOW LIKELY auth_service path is a NOTE" -- --severity LOW --confidence LIKELY --text "src/auth_service.py:12 missing check"
|
|
43
|
+
expect NOTE "LOW LIKELY Secrets (case) is a NOTE" -- --severity LOW --confidence LIKELY --text "Secrets leak to log"
|
|
44
|
+
expect NOTE "LOW LIKELY permission-check is a NOTE" -- --severity LOW --confidence LIKELY --text "permission-check skipped"
|
|
45
45
|
expect NOTE "'author' is not 'auth'" -- --severity LOW --confidence LIKELY --text "author field unused"
|
|
46
46
|
expect NOTE "'tokenizer' is not 'token'" -- --severity LOW --confidence LIKELY --text "tokenizer splits oddly"
|
|
47
|
-
expect
|
|
47
|
+
expect NOTE "LOW CONFIRMED from Auth domain agent is a NOTE (no LOW rescue)" -- --severity LOW --confidence CONFIRMED --agent "Auth" --text "nit"
|
|
48
48
|
expect ISSUE "MEDIUM POSSIBLE from Billing agent" -- --severity MEDIUM --confidence POSSIBLE --agent "billing" --text "nit"
|
|
49
49
|
expect NOTE "LOW LIKELY from Auth domain agent is a NOTE" -- --severity LOW --confidence LIKELY --agent "Auth" --text "nit"
|
|
50
50
|
expect NOTE "LOW POSSIBLE from Database agent is a NOTE" -- --severity LOW --confidence POSSIBLE --agent "Database" --text "nit"
|
|
51
51
|
expect NOTE "LOW POSSIBLE Concurrency reviewer is a NOTE (AlterLab #34671)" -- --severity LOW --confidence POSSIBLE --agent "Concurrency reviewer" --text "lock edge cases"
|
|
52
|
-
expect
|
|
53
|
-
expect
|
|
52
|
+
expect NOTE "LOW CONFIRMED Concurrency reviewer is a NOTE" -- --severity LOW --confidence CONFIRMED --agent "Concurrency reviewer" --text "lock edge cases"
|
|
53
|
+
expect NOTE "LOW POSSIBLE domain agent + keyword is a NOTE" -- --severity LOW --confidence POSSIBLE --agent "Database" --text "sql injection"
|
|
54
|
+
# --- LOW needs keyword AND CONFIRMED; domain agents rescue MEDIUM only (#3639) ---
|
|
55
|
+
expect NOTE "LOW LIKELY + keyword is a NOTE" -- --severity LOW --confidence LIKELY --text "shell injection via unquoted var"
|
|
56
|
+
expect ISSUE "LOW CONFIRMED + keyword files" -- --severity LOW --confidence CONFIRMED --text "shell injection via unquoted var"
|
|
57
|
+
expect NOTE "LOW CONFIRMED Auth agent, no keyword" -- --severity LOW --confidence CONFIRMED --agent Auth --text "nit"
|
|
58
|
+
expect ISSUE "MEDIUM POSSIBLE + keyword still files" -- --severity MEDIUM --confidence POSSIBLE --text "sql injection"
|
|
59
|
+
expect ISSUE "MEDIUM POSSIBLE Billing agent still files" -- --severity MEDIUM --confidence POSSIBLE --agent Billing --text "nit"
|
|
60
|
+
expect ISSUE "HIGH POSSIBLE files" -- --severity HIGH --confidence POSSIBLE
|
|
61
|
+
|
|
54
62
|
|
|
55
63
|
# --- Rule 3: review-finding lineage (fix for a finding) ---
|
|
56
64
|
expect NOTE "lineage LOW CONFIRMED note" -- --severity LOW --confidence CONFIRMED --lineage review-finding --text "nit"
|
|
@@ -63,7 +71,7 @@ expect NOTE "lineage MEDIUM POSSIBLE with keyword note" -- --severity MEDIUM --
|
|
|
63
71
|
# --- text-file input ---
|
|
64
72
|
TMPF=$(mktemp "${TMPDIR:-/tmp}/classify-finding-test.XXXXXX")
|
|
65
73
|
printf 'Title: unchecked xss sink\n' > "$TMPF"
|
|
66
|
-
expect
|
|
74
|
+
expect NOTE "text-file keyword, LOW LIKELY, is a NOTE" -- --severity LOW --confidence LIKELY --text-file "$TMPF"
|
|
67
75
|
rm -f "$TMPF"
|
|
68
76
|
|
|
69
77
|
# --- In-PR fix gate (#3387, narrowed): MEDIUM CONFIRMED in the PR diff ---
|
|
@@ -83,6 +91,67 @@ expect ISSUE "prefix of a diff path does not match" -- --severity MEDIUM --con
|
|
|
83
91
|
rm -f "$DIFF"
|
|
84
92
|
expect_exit 2 "unreadable --inpr-diff" -- --severity MEDIUM --inpr-diff /nonexistent/d --file x
|
|
85
93
|
|
|
94
|
+
# --- Contract-declared scope gate (#3447) ---
|
|
95
|
+
CSCOPE=$(mktemp "${TMPDIR:-/tmp}/classify-finding-scope.XXXXXX")
|
|
96
|
+
COPEN=$(mktemp "${TMPDIR:-/tmp}/classify-finding-open.XXXXXX")
|
|
97
|
+
TAB=$(printf '\t')
|
|
98
|
+
printf 'deferred%ssrc/sync.sh%s3446\naccepted-risk%ssrc/legacy%s\nnot-affected%ssrc/engine%s\ndeferred%ssrc/closed.py%s99\ndeferred%ssrc/a/b%s3446\n' \
|
|
99
|
+
"$TAB" "$TAB" "$TAB" "$TAB" "$TAB" "$TAB" "$TAB" "$TAB" "$TAB" "$TAB" > "$CSCOPE"
|
|
100
|
+
printf '3446\n' > "$COPEN"
|
|
101
|
+
CS=(--contract-scope "$CSCOPE" --contract-open "$COPEN")
|
|
102
|
+
expect NOTE "deferred + open issue demotes" -- --severity MEDIUM --confidence CONFIRMED --text "sync gap" --file src/sync.sh "${CS[@]}"
|
|
103
|
+
got=$(bash "$S" --severity MEDIUM --confidence CONFIRMED --text "sync gap" --file src/sync.sh "${CS[@]}")
|
|
104
|
+
if [ "$got" = "NOTE contract-deferred #3446" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: deferred reason (got '$got')"; fi
|
|
105
|
+
expect NOTE "file:line form demotes" -- --severity MEDIUM --confidence LIKELY --text "gap" --file src/sync.sh:42 "${CS[@]}"
|
|
106
|
+
expect NOTE "./ prefix demotes" -- --severity MEDIUM --confidence LIKELY --text "gap" --file ./src/sync.sh "${CS[@]}"
|
|
107
|
+
expect NOTE "directory prefix demotes accepted-risk" -- --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/legacy/old.py:9 "${CS[@]}"
|
|
108
|
+
got=$(bash "$S" --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/legacy "${CS[@]}")
|
|
109
|
+
if [ "$got" = "NOTE contract-accepted-risk" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: accepted-risk reason (got '$got')"; fi
|
|
110
|
+
expect NOTE "nested deferred path demotes" -- --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/a/b/c.py "${CS[@]}"
|
|
111
|
+
expect ISSUE "sibling with shared name prefix is not under dir" -- --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/legacy2/x.py "${CS[@]}"
|
|
112
|
+
expect ISSUE "unrelated path unchanged" -- --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/other.py "${CS[@]}"
|
|
113
|
+
expect ISSUE "deferred issue closed (not listed) files" -- --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/closed.py "${CS[@]}"
|
|
114
|
+
expect ISSUE "no --contract-open means no deferred demotion" -- --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/sync.sh --contract-scope "$CSCOPE"
|
|
115
|
+
expect NOTE "accepted-risk needs no --contract-open" -- --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/legacy/x.py --contract-scope "$CSCOPE"
|
|
116
|
+
expect ISSUE "not-affected never demotes" -- --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/engine/x.py "${CS[@]}"
|
|
117
|
+
expect ISSUE "CRITICAL never demotes" -- --severity CRITICAL --confidence CONFIRMED --text "gap" --file src/sync.sh "${CS[@]}"
|
|
118
|
+
expect ISSUE "HIGH never demotes" -- --severity HIGH --confidence CONFIRMED --text "gap" --file src/legacy/x.py "${CS[@]}"
|
|
119
|
+
expect ISSUE "accepted-risk + security keyword files" -- --severity MEDIUM --confidence CONFIRMED --text "token leak in logs" --file src/legacy/x.py "${CS[@]}"
|
|
120
|
+
expect ISSUE "accepted-risk + domain agent files" -- --severity MEDIUM --confidence CONFIRMED --agent Billing --text "nit" --file src/legacy/x.py "${CS[@]}"
|
|
121
|
+
expect ISSUE "deferred + security keyword files" -- --severity MEDIUM --confidence CONFIRMED --text "token leak" --file src/sync.sh "${CS[@]}"
|
|
122
|
+
expect ISSUE "deferred + SQL injection keywords files" -- --severity MEDIUM --confidence CONFIRMED --text "SQL injection token password" --file src/sync.sh "${CS[@]}"
|
|
123
|
+
expect ISSUE "deferred + domain agent files" -- --severity MEDIUM --confidence CONFIRMED --agent Auth --text "nit" --file src/sync.sh "${CS[@]}"
|
|
124
|
+
expect ISSUE "deferred to own issue never demotes" -- --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/sync.sh --merge-issue 3446 "${CS[@]}"
|
|
125
|
+
expect ISSUE "deferred to own issue (#N form) files" -- --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/sync.sh --merge-issue "#3446" "${CS[@]}"
|
|
126
|
+
expect NOTE "deferred to another issue still demotes" -- --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/sync.sh --merge-issue 3447 "${CS[@]}"
|
|
127
|
+
expect NOTE "LOW LIKELY keyword on deferred path demotes (#3639)" -- --severity LOW --confidence LIKELY --text "token leak" --file src/sync.sh "${CS[@]}"
|
|
128
|
+
got=$(bash "$S" --severity LOW --confidence LIKELY --text "token leak" --file src/sync.sh "${CS[@]}")
|
|
129
|
+
if [ "$got" = "NOTE contract-deferred #3446" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: LOW LIKELY keyword deferred reason (got '$got')"; fi
|
|
130
|
+
expect ISSUE "LOW CONFIRMED keyword on deferred path still files (#3639)" -- --severity LOW --confidence CONFIRMED --text "token leak" --file src/sync.sh "${CS[@]}"
|
|
131
|
+
expect NOTE "accepted-risk unaffected by --merge-issue" -- --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/legacy/x.py --merge-issue 3446 "${CS[@]}"
|
|
132
|
+
DIFF2=$(mktemp "${TMPDIR:-/tmp}/classify-finding-diff.XXXXXX")
|
|
133
|
+
printf 'src/sync.sh\nsrc/legacy/touched.py\n' > "$DIFF2"
|
|
134
|
+
expect ISSUE "path in PR diff is not demoted (deferred)" -- --severity MEDIUM --confidence LIKELY --text "gap" --file src/sync.sh --inpr-diff "$DIFF2" "${CS[@]}"
|
|
135
|
+
expect ISSUE "path in PR diff is not demoted (accepted-risk)" -- --severity MEDIUM --confidence LIKELY --text "gap" --file src/legacy/touched.py --inpr-diff "$DIFF2" "${CS[@]}"
|
|
136
|
+
expect NOTE "untouched path under accepted dir demotes with diff given" -- --severity MEDIUM --confidence LIKELY --text "gap" --file src/legacy/other.py --inpr-diff "$DIFF2" "${CS[@]}"
|
|
137
|
+
rm -f "$DIFF2"
|
|
138
|
+
PRF=$(mktemp "${TMPDIR:-/tmp}/classify-finding-prf.XXXXXX")
|
|
139
|
+
printf 'src/sync.sh\nsrc/legacy/touched.py\n' > "$PRF"
|
|
140
|
+
expect ISSUE "path in --pr-files is not demoted (deferred)" -- --severity MEDIUM --confidence LIKELY --text "gap" --file src/sync.sh --pr-files "$PRF" "${CS[@]}"
|
|
141
|
+
expect ISSUE "path in --pr-files is not demoted (accepted-risk)" -- --severity MEDIUM --confidence LIKELY --text "gap" --file src/legacy/touched.py --pr-files "$PRF" "${CS[@]}"
|
|
142
|
+
expect NOTE "untouched path demotes with --pr-files given" -- --severity MEDIUM --confidence LIKELY --text "gap" --file src/legacy/other.py --pr-files "$PRF" "${CS[@]}"
|
|
143
|
+
expect ISSUE "--pr-files alone never triggers INPR_FIX" -- --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/other.py --pr-files "$PRF"
|
|
144
|
+
rm -f "$PRF"
|
|
145
|
+
expect_exit 2 "unreadable --pr-files" -- --severity MEDIUM --pr-files /nonexistent/p --file x
|
|
146
|
+
expect ISSUE "no --file: no contract demotion" -- --severity MEDIUM --confidence CONFIRMED --text "gap" "${CS[@]}"
|
|
147
|
+
expect ISSUE "no contract flags: unchanged" -- --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/sync.sh
|
|
148
|
+
expect NOTE "LOW stays NOTE through the gate" -- --severity LOW --confidence CONFIRMED --text "gap" --file src/other.py "${CS[@]}"
|
|
149
|
+
: > "$CSCOPE"
|
|
150
|
+
expect ISSUE "empty scope file: unchanged" -- --severity MEDIUM --confidence CONFIRMED --text "gap" --file src/sync.sh "${CS[@]}"
|
|
151
|
+
rm -f "$CSCOPE" "$COPEN"
|
|
152
|
+
expect_exit 2 "unreadable --contract-scope" -- --severity MEDIUM --contract-scope /nonexistent/s --file x
|
|
153
|
+
expect_exit 2 "unreadable --contract-open" -- --severity MEDIUM --contract-open /nonexistent/o --file x
|
|
154
|
+
|
|
86
155
|
# --- usage errors ---
|
|
87
156
|
expect_exit 2 "unknown flag" -- --bogus x
|
|
88
157
|
expect_exit 2 "bad lineage" -- --severity LOW --lineage P3
|
|
@@ -30,28 +30,28 @@ C17=$(sed -n '/^## Phase C1.7/,/^## Phase C1.5/p' "$CLOSE_MD")
|
|
|
30
30
|
if printf '%s\n' "$C17" | grep -nE '^\s*cd "\$\{REPO_PATH\}"' >/dev/null; then
|
|
31
31
|
bad "C1.7 changes directory into REPO_PATH"
|
|
32
32
|
else ok "C1.7 does not cd into REPO_PATH"; fi
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
grep -q 'Phase C1.7: skipped' <<< "$C17" && ok "C1.7 logs skip reasons" || bad "C1.7 missing 'Phase C1.7: skipped'"
|
|
34
|
+
grep -q 'worktree add --detach' <<< "$C17" && ok "C1.7 uses a temporary detached worktree" || bad "C1.7 missing temp worktree"
|
|
35
|
+
grep -q 'gh pr create' <<< "$C17" && ok "C1.7 opens a PR" || bad "C1.7 missing gh pr create"
|
|
36
36
|
[ "$(printf '%s\n' "$C17" | grep -c '^```bash')" -eq 1 ] && ok "C1.7 is a single bash block (no cross-block shell state)" || bad "C1.7 must be exactly one bash block"
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
grep -qE "^\s*trap .*EXIT" <<< "$C17" && ok "C1.7 registers an EXIT trap" || bad "C1.7 missing EXIT trap"
|
|
38
|
+
grep -q 'worktree remove --force' <<< "$C17" && ok "C1.7 cleanup removes the worktree" || bad "C1.7 cleanup missing worktree remove"
|
|
39
39
|
ADD_LN=$(printf '%s\n' "$C17" | grep -n 'worktree add --detach' | head -1 | cut -d: -f1)
|
|
40
40
|
TRAP_LN=$(printf '%s\n' "$C17" | grep -nE "^\s*trap .*EXIT" | head -1 | cut -d: -f1)
|
|
41
41
|
[ -n "$ADD_LN" ] && [ -n "$TRAP_LN" ] && [ "$TRAP_LN" -gt "$ADD_LN" ] && ok "EXIT trap registered after worktree add" || bad "EXIT trap must follow worktree add"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
grep -q 'force-with-lease' <<< "$C17" && ok "C1.7 pushes with --force-with-lease" || bad "C1.7 missing force-with-lease"
|
|
43
|
+
grep -qE 'push .*--force( |$)' <<< "$C17" && bad "C1.7 uses bare --force" || ok "C1.7 has no bare --force"
|
|
44
|
+
grep -q 'gh pr list' <<< "$C17" && ok "C1.7 checks for an existing open PR" || bad "C1.7 missing gh pr list"
|
|
45
45
|
grep -q 'FORGE:DOSSIER_UPDATED' "$CLOSE_MD" && ok "FORGE:DOSSIER_UPDATED marker kept" || bad "FORGE:DOSSIER_UPDATED marker missing"
|
|
46
46
|
|
|
47
47
|
# --- Dynamic: execute the REAL C1.7 block extracted from close.md (gh stubbed) ---
|
|
48
48
|
BLOCK=$(printf '%s\n' "$C17" | awk '/^```bash/{f=1;next} /^```/{f=0} f')
|
|
49
49
|
if [ -z "$BLOCK" ]; then
|
|
50
50
|
bad "could not extract the C1.7 bash block"
|
|
51
|
-
elif ! command -v perl >/dev/null 2>&1 || ! yq --version 2>&1
|
|
51
|
+
elif ! command -v perl >/dev/null 2>&1 || ! grep -q 'mikefarah' <<< "$(yq --version 2>&1)"; then
|
|
52
52
|
bad "dynamic section needs perl and mikefarah yq v4 (not silently skipped)"
|
|
53
53
|
else
|
|
54
|
-
|
|
54
|
+
grep -q '^_dossier_run()' <<< "$BLOCK" && grep -q '^_dossier_cleanup()' <<< "$BLOCK" \
|
|
55
55
|
&& ok "extracted block defines _dossier_run and _dossier_cleanup" || bad "extracted block missing _dossier_run/_dossier_cleanup"
|
|
56
56
|
|
|
57
57
|
# Substitute only the exact {NAME} placeholders; leave ${NAME} shell expansions intact.
|
|
@@ -78,7 +78,7 @@ case "$1 $2" in
|
|
|
78
78
|
printf '<!-- FORGE:BUILDER -->\n### Changes\n- `scripts/foo.sh` changed\n### Next\n' ;;
|
|
79
79
|
"pr view") echo "Test PR title" ;;
|
|
80
80
|
"pr list")
|
|
81
|
-
echo "pr-list worktrees: $(git -C "$REPO_PATH" worktree list | wc -l)" >> "$GH_LOG"
|
|
81
|
+
echo "pr-list worktrees: $(git -C "$REPO_PATH" worktree list | wc -l | tr -d ' ')" >> "$GH_LOG"
|
|
82
82
|
[ "${GH_KILL_ON_PR_LIST:-}" = 1 ] && kill -TERM "$(cat "$PIDF")" && sleep 1
|
|
83
83
|
echo "${GH_PR_LIST_URL:-}" ;;
|
|
84
84
|
"pr create") [ "${GH_FAIL_PR_CREATE:-}" = 1 ] && exit 1; echo "https://example.invalid/o/r/pull/9" ;;
|
|
@@ -107,7 +107,7 @@ run_block; RC=$?
|
|
|
107
107
|
[ "$RC" -eq 0 ] && ok "A: real block exits 0" || bad "A: real block exit $RC"
|
|
108
108
|
main_untouched && ok "A: main checkout untouched (HEAD, branch, clean tree)" || bad "A: main checkout modified"
|
|
109
109
|
git -C "$TMP/origin.git" rev-parse -q --verify refs/heads/docs/dossier-1 >/dev/null && ok "A: dossier branch pushed to origin" || bad "A: dossier branch not pushed"
|
|
110
|
-
git -C "$TMP/origin.git" show refs/heads/docs/dossier-1:devdocs/modules/m.md 2>/dev/null
|
|
110
|
+
grep -q '^## Entry' <<< "$(git -C "$TMP/origin.git" show refs/heads/docs/dossier-1:devdocs/modules/m.md 2>/dev/null)" && ok "A: dossier entry appended on pushed branch" || bad "A: no entry on pushed branch"
|
|
111
111
|
grep -q '^gh pr create' "$GH_LOG" && ok "A: gh pr create called" || bad "A: gh pr create not called"
|
|
112
112
|
grep -q 'FORGE:DOSSIER_UPDATED' "$GH_LOG" && ok "A: FORGE:DOSSIER_UPDATED comment posted" || bad "A: no FORGE:DOSSIER_UPDATED comment"
|
|
113
113
|
no_leak && ok "A: temporary worktree removed" || bad "A: temporary worktree leaked"
|
|
@@ -108,8 +108,9 @@ OUT="$(bash "$S" --repo-path "$TMP/w" 2>/dev/null)"; rc=$?
|
|
|
108
108
|
{ [ "$rc" = 2 ] && [ -z "$OUT" ]; } && ok || bad "missing base (rc=$rc)"
|
|
109
109
|
|
|
110
110
|
# SEC-1: commits landing on the base after the branch point are not counted
|
|
111
|
-
reset; git checkout -q staging; gen 3000 > base-only.txt; git add base-only.txt; git commit -qm "base moves"
|
|
112
|
-
git push -q origin staging 2>/dev/null
|
|
111
|
+
reset; git checkout -q staging; git reset -q --hard origin/staging; gen 3000 > base-only.txt; git add base-only.txt; git commit -qm "base moves"
|
|
112
|
+
git push -q origin staging 2>/dev/null && ok || bad "base-moves push rejected"
|
|
113
|
+
git checkout -q feat; gen 10 > a.txt; git add a.txt
|
|
113
114
|
OUT="$(run --threshold 100)"
|
|
114
115
|
check "moving base lines" "$OUT" diff_lines 10; check "moving base over" "$OUT" over false
|
|
115
116
|
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
# I7 Orphaned worktree branches (local branches fix/* with no open PR)
|
|
48
48
|
# I8 FORGE-referenced gists with public visibility (KNOWLEDGE_GIST, MILESTONE_INDEX,
|
|
49
49
|
# PRIOR_GIST annotations in recent issue comments)
|
|
50
|
+
# I9 Issues stuck in workflow:remediating beyond threshold (the state must
|
|
51
|
+
# never outlive its remediation run)
|
|
50
52
|
#
|
|
51
53
|
# /pipeline-health should consume this script's JSON output rather than
|
|
52
54
|
# re-discovering state from scratch.
|
|
@@ -322,6 +324,39 @@ while IFS= read -r row; do
|
|
|
322
324
|
fi
|
|
323
325
|
done < <(echo "$IN_REVIEW" | jq -c '.[]')
|
|
324
326
|
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
# I9: Issues stuck in workflow:remediating
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
# The review handoff sets workflow:remediating until remediate Phase M1 swaps it
|
|
331
|
+
# for workflow:in-review; it must never outlive the run. Same fail-inconclusive
|
|
332
|
+
# pattern as I1-I3: a failed gh call is not treated as an empty list.
|
|
333
|
+
if ! REMEDIATING=$(gh issue list $GH_FLAG \
|
|
334
|
+
--state open --label "workflow:remediating" \
|
|
335
|
+
--limit 100 \
|
|
336
|
+
--json number,title,updatedAt,labels 2>"$GH_STDERR_TMP"); then
|
|
337
|
+
echo "WARNING: gh issue list failed for workflow:remediating — I9 check inconclusive, skipping (not treated as empty): $(cat "$GH_STDERR_TMP")" >&2
|
|
338
|
+
add_skip "I9" "all" "gh issue list failed: $(bounded_reason "$(cat "$GH_STDERR_TMP")")"
|
|
339
|
+
REMEDIATING="[]"
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
while IFS= read -r row; do
|
|
343
|
+
num=$(echo "$row" | jq -r '.number')
|
|
344
|
+
labels=$(echo "$row" | jq -c '[.labels[].name]')
|
|
345
|
+
if has_terminal_label "$labels"; then
|
|
346
|
+
continue
|
|
347
|
+
fi
|
|
348
|
+
updated=$(echo "$row" | jq -r '.updatedAt')
|
|
349
|
+
hours=$(hours_since "$updated")
|
|
350
|
+
if [ "$hours" -ge "$STUCK_HOURS" ]; then
|
|
351
|
+
add_finding \
|
|
352
|
+
"stuck_remediating" "warning" \
|
|
353
|
+
"$num" "workflow:remediating" "$hours" \
|
|
354
|
+
"FORGE:REMEDIATION" \
|
|
355
|
+
"/work-on $num --resume" \
|
|
356
|
+
"Issue #$num stuck in workflow:remediating for ${hours}h (threshold: ${STUCK_HOURS}h). The state must not outlive its remediation run. Resume with /work-on $num --resume"
|
|
357
|
+
fi
|
|
358
|
+
done < <(echo "$REMEDIATING" | jq -c '.[]')
|
|
359
|
+
|
|
325
360
|
# ---------------------------------------------------------------------------
|
|
326
361
|
# I4: Issues in workflow:building with no FORGE:BUILDER annotation
|
|
327
362
|
# ---------------------------------------------------------------------------
|
|
@@ -167,6 +167,9 @@ render_template() {
|
|
|
167
167
|
|
|
168
168
|
<!-- INVESTIGATION:COMPLETE -->'
|
|
169
169
|
;;
|
|
170
|
+
# OUT_OF_SCOPE format (forge#3447): the value is `None.` (the default) or typed bullets, one per item:
|
|
171
|
+
# - `path` — deferred → #N: <why> | not-affected: <evidence> | accepted-risk: <reason>
|
|
172
|
+
# scripts/check-contract-scope.sh validates and lists them. The codec does not validate.
|
|
170
173
|
CONTRACT)
|
|
171
174
|
body='<!-- FORGE:CONTRACT -->
|
|
172
175
|
## Builder Contract
|
|
@@ -246,7 +246,7 @@ while IFS= read -r f; do
|
|
|
246
246
|
arms=$(grep -c 'trusted-comments.sh" \]' "$f" || true)
|
|
247
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
248
|
done < <(grep -rl 'trusted-comments.sh" \]' "$ROOT/commands")
|
|
249
|
-
expect "trusted-comments.sh resolver blocks discovered (review-pr
|
|
249
|
+
expect "trusted-comments.sh resolver blocks discovered (review-pr 8 + review + remediate + phase-4 + review-pr-staging)" 12 "$TC_BLOCKS"
|
|
250
250
|
expect "trusted-comments.sh resolvers without a canonical block" 0 "$TC_STRAY"
|
|
251
251
|
expect "trusted-comments.sh resolvers byte-identical" 1 "$(sort -u "$T/tc_all" | wc -l | tr -d ' ')"
|
|
252
252
|
grep -qF 'plugins/cache/forgedock/forgedock' "$T/tc_all" && ok || bad 'trusted-comments.sh resolver lacks the plugin-cache scan'
|
|
@@ -265,5 +265,74 @@ expect "cache-only install resolves newest cached trusted-comments.sh" "$T/tchom
|
|
|
265
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
266
|
expect "no install resolves empty (cwd copy never used)" "" "$out"
|
|
267
267
|
|
|
268
|
+
# Guard (#3483): the other executable-helper resolvers (classify-finding.sh, amplification-breaker.sh,
|
|
269
|
+
# extract-affected-files.sh) must use the same trusted-install tiers: no "$PWD", no "$REPO_PATH", plugin-cache
|
|
270
|
+
# scan present. Blocks are extracted from the real specs (header '# HELPER_SCRIPT resolver' to 'done <<< "$_tc"',
|
|
271
|
+
# or the resolve_extract_affected_files function) so a drifting copy cannot pass on a hand-written stand-in.
|
|
272
|
+
extract_helper_block() { # file var -> block on stdout
|
|
273
|
+
awk -v var="$2" '/^[ ]*# HELPER_SCRIPT resolver \(/ && index($0, "(" var ";") {inb=1} inb {sub(/^[ ]+/, ""); print} inb && /^done <<< "\$_tc"$/ {exit}' "$1"
|
|
274
|
+
}
|
|
275
|
+
extract_helper_fn() { # file -> function on stdout
|
|
276
|
+
awk '/^resolve_extract_affected_files\(\) \{/ {inb=1} inb {print} inb && /^\}$/ {exit}' "$1"
|
|
277
|
+
}
|
|
278
|
+
HR_CLASSIFY="$ROOT/commands/review-pr.md"
|
|
279
|
+
HR_AMP="$ROOT/commands/orchestrate/phase-4-execution.md"
|
|
280
|
+
HR_EX3="$ROOT/commands/orchestrate/phase-3-dependency.md"
|
|
281
|
+
extract_helper_block "$HR_CLASSIFY" CLASSIFY_SCRIPT > "$T/hr_classify.sh"
|
|
282
|
+
extract_helper_block "$HR_AMP" AMP_SCRIPT > "$T/hr_amp.sh"
|
|
283
|
+
extract_helper_fn "$HR_AMP" > "$T/hr_ex4.sh"
|
|
284
|
+
extract_helper_fn "$HR_EX3" > "$T/hr_ex3.sh"
|
|
285
|
+
expect "CLASSIFY_SCRIPT resolver block found" 1 "$(grep -c '^CLASSIFY_SCRIPT=""' "$T/hr_classify.sh")"
|
|
286
|
+
expect "AMP_SCRIPT resolver block found" 1 "$(grep -c '^AMP_SCRIPT=""' "$T/hr_amp.sh")"
|
|
287
|
+
expect "extract-affected-files resolver found in phase-4" 1 "$(grep -c 'resolve_extract_affected_files() {' "$T/hr_ex4.sh")"
|
|
288
|
+
expect "extract-affected-files resolver found in phase-3" 1 "$(grep -c 'resolve_extract_affected_files() {' "$T/hr_ex3.sh")"
|
|
289
|
+
cmp -s "$T/hr_ex3.sh" "$T/hr_ex4.sh" && ok || bad 'resolve_extract_affected_files copies differ between phase-3 and phase-4'
|
|
290
|
+
for hr in classify amp ex3 ex4; do
|
|
291
|
+
grep -qF 'plugins/cache/forgedock/forgedock' "$T/hr_$hr.sh" && ok || bad "$hr helper resolver lacks the plugin-cache scan"
|
|
292
|
+
grep -qF 'CLAUDE_CONFIG_DIR' "$T/hr_$hr.sh" && ok || bad "$hr helper resolver lacks CLAUDE_CONFIG_DIR"
|
|
293
|
+
grep -qE '\$PWD|\$REPO_PATH|\$\{PWD|\$\{REPO_PATH' "$T/hr_$hr.sh" && bad "$hr helper resolver lists the working directory or repo path" || ok
|
|
294
|
+
done
|
|
295
|
+
# no leftover cwd-derived helper tier anywhere under commands/
|
|
296
|
+
if grep -rnE 'PWD/scripts/|^\$PWD$|REPO_PATH/scripts/extract' "$ROOT/commands" >/dev/null 2>&1; then bad 'commands/ still lists a working-directory helper tier'; else ok; fi
|
|
297
|
+
|
|
298
|
+
# Behavior (#3483): a planted scripts/<helper>.sh in the cwd must never be selected; a cache-only install must resolve.
|
|
299
|
+
mkdir -p "$T/hrwork/scripts" "$T/hrhome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts"
|
|
300
|
+
for h in classify-finding amplification-breaker extract-affected-files; do
|
|
301
|
+
: > "$T/hrwork/scripts/$h.sh"; : > "$T/hrhome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts/$h.sh"
|
|
302
|
+
done
|
|
303
|
+
CACHE_SCRIPTS="$T/hrhome/.claude/plugins/cache/forgedock/forgedock/1.12.0/scripts"
|
|
304
|
+
hr_run() { # home blockfile expr
|
|
305
|
+
(cd "$T/hrwork" && env -i PATH="$PATH" HOME="$1" REPO_PATH="$T/hrwork" bash -c 'source "$1"; '"$3" _ "$2" 2>/dev/null)
|
|
306
|
+
}
|
|
307
|
+
expect "CLASSIFY_SCRIPT: cache-only install resolves (cwd copy ignored)" "$CACHE_SCRIPTS/classify-finding.sh" "$(hr_run "$T/hrhome" "$T/hr_classify.sh" 'printf %s "$CLASSIFY_SCRIPT"')"
|
|
308
|
+
expect "CLASSIFY_SCRIPT: no install resolves empty (cwd copy ignored)" "" "$(hr_run "$T/nohome" "$T/hr_classify.sh" 'printf %s "$CLASSIFY_SCRIPT"')"
|
|
309
|
+
expect "AMP_SCRIPT: cache-only install resolves (cwd copy ignored)" "$CACHE_SCRIPTS/amplification-breaker.sh" "$(hr_run "$T/hrhome" "$T/hr_amp.sh" 'printf %s "$AMP_SCRIPT"')"
|
|
310
|
+
expect "AMP_SCRIPT: no install resolves empty (cwd copy ignored)" "" "$(hr_run "$T/nohome" "$T/hr_amp.sh" 'printf %s "$AMP_SCRIPT"')"
|
|
311
|
+
expect "extract-affected-files: cache-only install resolves (cwd and REPO_PATH copies ignored)" "$CACHE_SCRIPTS/extract-affected-files.sh" "$(hr_run "$T/hrhome" "$T/hr_ex4.sh" 'resolve_extract_affected_files')"
|
|
312
|
+
expect "extract-affected-files: no install resolves nothing (cwd and REPO_PATH copies ignored)" "" "$(hr_run "$T/nohome" "$T/hr_ex4.sh" 'resolve_extract_affected_files')"
|
|
313
|
+
|
|
314
|
+
# Guard (#3636): review-delta.sh takes its trust script ONLY from an explicit absolute --trusted-script. Any
|
|
315
|
+
# commands/ block that resolves review-delta.sh (added by the specs that consume it) must use the trusted-install
|
|
316
|
+
# tiers: plugin-cache scan and CLAUDE_CONFIG_DIR present, no "$PWD"/"$REPO_PATH" tier. Passes with 0 blocks today.
|
|
317
|
+
RD_BLOCKS=0
|
|
318
|
+
while IFS= read -r f; do
|
|
319
|
+
awk '/^[ ]*# HELPER_SCRIPT resolver \(/ && index($0, "(REVIEW_DELTA_SCRIPT;") {inb=1} inb {sub(/^[ ]+/, ""); print} inb && /^done <<< "\$_tc"$/ {exit}' "$f" > "$T/rd_block.sh"
|
|
320
|
+
[ -s "$T/rd_block.sh" ] || { bad "review-delta.sh referenced in $f without a canonical REVIEW_DELTA_SCRIPT resolver block"; continue; }
|
|
321
|
+
RD_BLOCKS=$((RD_BLOCKS + 1))
|
|
322
|
+
grep -qF 'plugins/cache/forgedock/forgedock' "$T/rd_block.sh" && ok || bad "review-delta resolver in $f lacks the plugin-cache scan"
|
|
323
|
+
grep -qF 'CLAUDE_CONFIG_DIR' "$T/rd_block.sh" && ok || bad "review-delta resolver in $f lacks CLAUDE_CONFIG_DIR"
|
|
324
|
+
grep -qE '\$PWD|\$REPO_PATH|\$\{PWD|\$\{REPO_PATH' "$T/rd_block.sh" && bad "review-delta resolver in $f lists the working directory or repo path" || ok
|
|
325
|
+
done < <(grep -rl 'review-delta\.sh' "$ROOT/commands" 2>/dev/null)
|
|
326
|
+
|
|
327
|
+
# Behavior (#3636): a planted scripts/trusted-comments.sh in the cwd must never be used. Without --trusted-script the
|
|
328
|
+
# resolver fails closed (FULL); a stub that would trust everything cannot change that.
|
|
329
|
+
mkdir -p "$T/rdwork/scripts"
|
|
330
|
+
printf '#!/usr/bin/env bash\ncat >/dev/null\nprintf "%%s\\n" "\\"<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->\\""\n' > "$T/rdwork/scripts/trusted-comments.sh"
|
|
331
|
+
echo '[]' > "$T/rdwork/comments.json"
|
|
332
|
+
out=$(cd "$T/rdwork" && bash "$ROOT/scripts/review-delta.sh" --pr 1 --head 0123456789abcdef0123456789abcdef01234567 --base main --comments-file comments.json --labels "" 2>&1 | sed -n 1p)
|
|
333
|
+
expect "review-delta.sh without --trusted-script prints FULL (cwd trust script ignored)" "FULL" "$out"
|
|
334
|
+
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)
|
|
335
|
+
expect "review-delta.sh with a relative cwd --trusted-script prints FULL" "FULL" "$out"
|
|
336
|
+
|
|
268
337
|
echo "forge-root tests: pass=$PASS fail=$FAILN skipped=$SKIPPED"
|
|
269
338
|
[ "$FAILN" -eq 0 ]
|
package/scripts/forge-run.sh
CHANGED
|
@@ -285,7 +285,7 @@ fi
|
|
|
285
285
|
|
|
286
286
|
# Read current workflow labels
|
|
287
287
|
WORKFLOW_LABEL=""
|
|
288
|
-
for label in "workflow:in-review" "workflow:building" "workflow:ready-to-build" "workflow:investigating"; do
|
|
288
|
+
for label in "workflow:in-review" "workflow:remediating" "workflow:building" "workflow:ready-to-build" "workflow:investigating"; do
|
|
289
289
|
if printf '%s' "$LABELS_JSON" | jq -e --arg l "$label" 'contains([$l])' > /dev/null 2>&1; then
|
|
290
290
|
WORKFLOW_LABEL="$label"
|
|
291
291
|
break
|
|
@@ -299,7 +299,7 @@ ACTION=""
|
|
|
299
299
|
if [ "$REVIEW_STARTED" = "true" ]; then
|
|
300
300
|
PHASE="in-review"
|
|
301
301
|
ACTION="Review in progress — /review-pr handles merge. Check PR state."
|
|
302
|
-
elif [ "$BUILDER_COMPLETE" = "true" ] && [ "$WORKFLOW_LABEL" != "workflow:in-review" ]; then
|
|
302
|
+
elif [ "$BUILDER_COMPLETE" = "true" ] && [ "$WORKFLOW_LABEL" != "workflow:in-review" ] && [ "$WORKFLOW_LABEL" != "workflow:remediating" ]; then
|
|
303
303
|
PHASE="awaiting-pr"
|
|
304
304
|
ACTION="Builder complete. Create PR targeting the classified lane branch."
|
|
305
305
|
elif [ "$BUILDER_PRESENT" = "true" ] && [ "$BUILDER_COMPLETE" = "false" ]; then
|
|
@@ -32,10 +32,13 @@ old_iso() { date -u -d '2 hours ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u
|
|
|
32
32
|
fx() { echo "$2" > "$TMP_FX/$1.json"; echo "$TMP_FX/$1.json"; }
|
|
33
33
|
claim() { jq -cn --arg id "$1" --arg t "$2" '{body:("<!-- FORGE:RECOVERY_CLAIM -->\n**Sweep: "+$id+"**\nHolder: /recover-orphans"), updated_at:$t}'; }
|
|
34
34
|
release() { jq -cn --arg id "$1" --arg t "$2" '{body:("<!-- FORGE:RECOVERY_CLAIM_RELEASED -->\n**Sweep: "+$id+"**\nReleased"), updated_at:$t}'; }
|
|
35
|
+
# comment <marker> <second-line> <updated_at>: built by jq, never hand-escaped. macOS /bin/bash 3.2 mis-parses
|
|
36
|
+
# escaped double quotes inside "$(fx name "...")", which turned literal JSON fixtures into invalid JSON there.
|
|
37
|
+
comment() { jq -cn --arg m "$1" --arg l "$2" --arg t "$3" '{body:("<!-- "+$m+" -->\n"+$l), updated_at:$t}'; }
|
|
35
38
|
|
|
36
39
|
run() { local f="$1"; shift; OUT=$(MOCK_GH_JSON="$f" bash "$CHECK" 7 -R o/r "$@" 2>/dev/null); RC=$?; }
|
|
37
40
|
expect() { # name rc needle
|
|
38
|
-
[ "$RC" -eq "$2" ] &&
|
|
41
|
+
[ "$RC" -eq "$2" ] && grep -q "$3" <<< "$OUT" && ok "$1" || bad "$1 (rc=$RC out=$OUT)"
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
N=$(now_iso); O=$(old_iso)
|
|
@@ -44,16 +47,16 @@ run "$(fx live "[$(claim sweep-A "$N")]")"; expect "fresh c
|
|
|
44
47
|
run "$(fx exp "[$(claim sweep-A "$O")]")"; expect "expired claim is FREE" 0 "CLAIM: FREE"
|
|
45
48
|
run "$(fx rel "[$(claim sweep-A "$N"), $(release sweep-A "$N")]")"; expect "released claim is FREE" 0 "CLAIM: FREE"
|
|
46
49
|
run "$(fx relother "[$(claim sweep-A "$N"), $(release sweep-B "$N")]")"; expect "release of another sweep does not free the claim" 1 "CLAIM: LIVE sweep-A"
|
|
47
|
-
run "$(fx unkrel "[
|
|
50
|
+
run "$(fx unkrel "[$(comment FORGE:RECOVERY_CLAIM "no sweep line" "$N"), $(comment FORGE:RECOVERY_CLAIM_RELEASED "no sweep line" "$N")]")"; expect "release with no sweep id does not free an id-less claim" 1 "CLAIM: LIVE unknown"
|
|
48
51
|
run "$(fx own "[$(claim sweep-A "$N")]")" --exempt-sweep sweep-A; expect "own sweep id is exempt" 0 "CLAIM: FREE"
|
|
49
52
|
run "$(fx other "[$(claim sweep-A "$N")]")" --exempt-sweep sweep-B; expect "other sweep id is not exempt" 1 "CLAIM: LIVE sweep-A"
|
|
50
53
|
run "$(fx two "[$(claim sweep-A "$N"), $(claim sweep-B "$N")]")" --exempt-sweep sweep-A; expect "exempting A still sees live B" 1 "CLAIM: LIVE sweep-B"
|
|
51
|
-
run "$(fx unk "[
|
|
52
|
-
run "$(fx unk2 "[
|
|
54
|
+
run "$(fx unk "[$(comment FORGE:RECOVERY_CLAIM "no sweep line" "$N")]")" --exempt-sweep sweep-A; expect "claim with no sweep id is never exempt" 1 "CLAIM: LIVE"
|
|
55
|
+
run "$(fx unk2 "[$(comment FORGE:RECOVERY_CLAIM "no sweep line" "$N")]")" --exempt-sweep unknown; expect "--exempt-sweep unknown is rejected" 2 "CLAIM: ERROR"
|
|
53
56
|
run "$(fx none '[]')"; expect "no comments is FREE" 0 "CLAIM: FREE"
|
|
54
|
-
run "$(fx hb "[
|
|
55
|
-
run "$(fx bad "[
|
|
56
|
-
run "$(fx nodate "[
|
|
57
|
+
run "$(fx hb "[$(comment FORGE:HEARTBEAT "Phase 3" "$N")]")"; expect "heartbeat alone is not a recovery claim" 0 "CLAIM: FREE"
|
|
58
|
+
run "$(fx bad "[$(comment FORGE:RECOVERY_CLAIM "no sweep line" "$N")]")"; expect "malformed claim fails closed (LIVE)" 1 "CLAIM: LIVE"
|
|
59
|
+
run "$(fx nodate "[$(comment FORGE:RECOVERY_CLAIM "**Sweep: sweep-A**" garbage)]")"; expect "unparsable updated_at fails closed (LIVE)" 1 "CLAIM: LIVE"
|
|
57
60
|
|
|
58
61
|
OUT=$(MOCK_GH_FAIL=1 MOCK_GH_JSON=/dev/null bash "$CHECK" 7 -R o/r 2>/dev/null); RC=$?
|
|
59
62
|
expect "gh outage is ERROR (exit 2)" 2 "CLAIM: ERROR"
|