forgedock 1.12.0 → 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-args.test.mjs +2 -1
- package/bin/engine/phases.mjs +349 -90
- package/bin/engine/runlog.mjs +25 -2
- package/bin/engine-cli.mjs +4 -1
- package/bin/engine.mjs +72 -19
- package/bin/hooks/interactive-engine.mjs +135 -209
- package/bin/hooks/pre-tool-use.mjs +16 -4
- package/bin/labels.json +10 -0
- package/bin/opencode-adapter.mjs +1 -1
- package/bin/orchestrate-preflight.mjs +1 -1
- package/bin/runner.mjs +258 -23
- package/bin/watch-utils.mjs +1 -0
- package/commands/autopilot.md +6 -6
- package/commands/cleanup.md +8 -5
- 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 +80 -53
- 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/recover-orphans.md +5 -5
- 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 +15 -6
- package/commands/review-pr-agents/scraper.md +1 -1
- package/commands/review-pr-agents/security.md +5 -5
- package/commands/review-pr-agents.md +12 -12
- package/commands/review-pr-staging.md +226 -8
- package/commands/review-pr.md +394 -53
- package/commands/work-on/build/architect.md +36 -0
- package/commands/work-on/build/implement.md +1 -1
- package/commands/work-on/build/validate.md +88 -4
- package/commands/work-on/build.md +106 -7
- package/commands/work-on/decompose.md +2 -2
- package/commands/work-on/investigate.md +14 -4
- package/commands/work-on/remediate.md +137 -25
- package/commands/work-on/review.md +25 -9
- package/commands/work-on.md +15 -15
- 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/label-state-machine.md +6 -1
- package/docs/spec/review-protocol.md +20 -5
- package/package.json +2 -1
- package/packages/protocol/src/phases.js +21 -0
- 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 +103 -13
- 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-domain-map.test.sh +87 -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
- package/scripts/transition-label.sh +3 -1
|
@@ -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 ]
|
|
@@ -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
|