@windyroad/risk-scorer 0.17.4 → 0.18.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/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +13 -2
- package/bin/install.mjs +42 -8
- package/hooks/codex-agent-completion.mjs +82 -0
- package/hooks/hooks.json +5 -2
- package/hooks/risk-scorer-dispatch.sh +3 -0
- package/hooks/risk-scorer-scaffold-nudge.sh +15 -4
- package/package.json +5 -2
- package/scripts/codex-agents.mjs +208 -0
- package/scripts/sync-codex-skills.mjs +8 -3
- package/skills/assess-external-comms/SKILL.md +5 -4
- package/skills/assess-inbound-report/SKILL.md +5 -4
- package/skills/assess-release/SKILL.md +5 -4
- package/skills/assess-wip/SKILL.md +5 -4
- package/skills/bootstrap-catalog/SKILL.md +5 -4
- package/skills/create-risk/SKILL.md +5 -4
- package/skills/external-comms/SKILL.md +5 -4
- package/skills/pipeline/SKILL.md +5 -4
- package/skills/update-policy/SKILL.md +6 -5
- package/skills/wip/SKILL.md +5 -4
- package/agents/test/inbound-report-contract.bats +0 -225
- package/agents/test/risk-scorer-above-appetite-stop.bats +0 -80
- package/agents/test/risk-scorer-catalog-consumption.bats +0 -138
- package/agents/test/risk-scorer-commit-verdict.bats +0 -66
- package/agents/test/risk-scorer-monitoring-not-a-control.bats +0 -76
- package/agents/test/risk-scorer-reducing-bypass-criteria.bats +0 -62
- package/agents/test/risk-scorer-register-hint.bats +0 -111
- package/agents/test/risk-scorer-structured-remediations.bats +0 -123
- package/agents/test/risk-scorer-user-stated-preconditions.bats +0 -89
- package/hooks/test/ci-status-gate.bats +0 -234
- package/hooks/test/external-comms-gate.bats +0 -528
- package/hooks/test/external-comms-key-substance.bats +0 -74
- package/hooks/test/git-push-gate.bats +0 -82
- package/hooks/test/pipeline-state-changeset-partition.bats +0 -181
- package/hooks/test/pipeline-state-hash.bats +0 -195
- package/hooks/test/plan-risk-guidance-once-per-session.bats +0 -95
- package/hooks/test/reducing-marker-persistence.bats +0 -236
- package/hooks/test/risk-gate.bats +0 -330
- package/hooks/test/risk-policy-project-root.bats +0 -20
- package/hooks/test/risk-score-commit-gate-adr-031-bypass.bats +0 -98
- package/hooks/test/risk-score-commit-gate-cadence-staleness.bats +0 -105
- package/hooks/test/risk-score-mark-external-comms-prompt-parse.bats +0 -94
- package/hooks/test/risk-score-mark-register-queue.bats +0 -253
- package/hooks/test/risk-score-mark.bats +0 -112
- package/hooks/test/risk-scorer-hook-dispatch.bats +0 -77
- package/hooks/test/risk-scorer-no-stop-hook.bats +0 -19
- package/hooks/test/risk-scorer-scaffold-nudge.bats +0 -132
- package/hooks/test/slide-marker-on-subprocess-return.bats +0 -118
- package/hooks/test/wip-risk-mark.bats +0 -64
- package/scripts/test/drain-register-queue.bats +0 -359
- package/scripts/test/evaluate-graduation.bats +0 -533
- package/scripts/test/extract-risks-from-reports.bats +0 -366
- package/scripts/test/restage-commit.bats +0 -137
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# Tests for slide_marker_on_subprocess_return helper (P111).
|
|
4
|
-
#
|
|
5
|
-
# Behavioural contract:
|
|
6
|
-
# - Slides an existing marker forward (touch) on PostToolUse:Agent|Bash
|
|
7
|
-
# completion, treating subprocess wall-clock as continuous parent-session
|
|
8
|
-
# work for TTL purposes.
|
|
9
|
-
# - Never CREATES a marker (creating requires a real gate review).
|
|
10
|
-
# - Skips slide on subprocess error (tool_response.is_error=true) so a failed
|
|
11
|
-
# subprocess does NOT extend the parent's trust window (ADR-009 amendment).
|
|
12
|
-
# - No-op when no marker exists or session_id is empty (fail-safe).
|
|
13
|
-
|
|
14
|
-
setup() {
|
|
15
|
-
HOOKS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
16
|
-
source "$HOOKS_DIR/lib/gate-helpers.sh"
|
|
17
|
-
|
|
18
|
-
TEST_SESSION="bats-slide-$$-${BATS_TEST_NUMBER}"
|
|
19
|
-
MARKER="/tmp/architect-reviewed-${TEST_SESSION}"
|
|
20
|
-
rm -f "$MARKER"
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
teardown() {
|
|
24
|
-
rm -f "$MARKER"
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
# Helper: backdate file mtime by N seconds (portable between macOS and Linux)
|
|
28
|
-
_backdate() {
|
|
29
|
-
local file="$1" seconds="$2"
|
|
30
|
-
local stamp
|
|
31
|
-
stamp=$(date -v-${seconds}S +%Y%m%d%H%M.%S 2>/dev/null \
|
|
32
|
-
|| date -d "${seconds} seconds ago" +%Y%m%d%H%M.%S 2>/dev/null)
|
|
33
|
-
touch -t "$stamp" "$file"
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
@test "slide: existing marker is touched on success response" {
|
|
37
|
-
touch "$MARKER"
|
|
38
|
-
_backdate "$MARKER" 60
|
|
39
|
-
BEFORE=$(_mtime "$MARKER")
|
|
40
|
-
_HOOK_INPUT='{"tool_response":{"content":[]}}'
|
|
41
|
-
slide_marker_on_subprocess_return "$MARKER"
|
|
42
|
-
AFTER=$(_mtime "$MARKER")
|
|
43
|
-
[ "$AFTER" -gt "$BEFORE" ]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
@test "slide: long-running subprocess does NOT cause parent marker expiry on return (P111 reproduction)" {
|
|
47
|
-
# Simulate the P111 failure mode: parent's marker is set, then a long
|
|
48
|
-
# subprocess runs (we backdate the marker to simulate elapsed wall-clock),
|
|
49
|
-
# then PostToolUse fires with a successful tool_response. The marker mtime
|
|
50
|
-
# must be refreshed so the parent's NEXT PreToolUse gate check (which
|
|
51
|
-
# compares NOW - mtime against TTL) sees a fresh marker.
|
|
52
|
-
touch "$MARKER"
|
|
53
|
-
# Marker is 50 minutes old — under default 60-min TTL but close to expiry.
|
|
54
|
-
# Without the slide on subprocess return, a subsequent 15-min subprocess
|
|
55
|
-
# would push the mtime past TTL and the next PreToolUse would deny.
|
|
56
|
-
_backdate "$MARKER" 3000
|
|
57
|
-
BEFORE=$(_mtime "$MARKER")
|
|
58
|
-
_HOOK_INPUT='{"tool_response":{"content":[{"type":"text","text":"OK"}]}}'
|
|
59
|
-
slide_marker_on_subprocess_return "$MARKER"
|
|
60
|
-
AFTER=$(_mtime "$MARKER")
|
|
61
|
-
NOW=$(date +%s)
|
|
62
|
-
[ "$AFTER" -gt "$BEFORE" ]
|
|
63
|
-
# And the new mtime is approximately NOW (within 5 seconds of slide call)
|
|
64
|
-
AGE=$((NOW - AFTER))
|
|
65
|
-
[ "$AGE" -lt 5 ]
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
@test "slide: does NOT touch marker when tool_response.is_error=true" {
|
|
69
|
-
touch "$MARKER"
|
|
70
|
-
_backdate "$MARKER" 60
|
|
71
|
-
BEFORE=$(_mtime "$MARKER")
|
|
72
|
-
_HOOK_INPUT='{"tool_response":{"is_error":true,"content":[]}}'
|
|
73
|
-
slide_marker_on_subprocess_return "$MARKER"
|
|
74
|
-
AFTER=$(_mtime "$MARKER")
|
|
75
|
-
[ "$BEFORE" = "$AFTER" ]
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
@test "slide: no-op when marker does not exist (never creates)" {
|
|
79
|
-
[ ! -f "$MARKER" ]
|
|
80
|
-
_HOOK_INPUT='{"tool_response":{"content":[]}}'
|
|
81
|
-
slide_marker_on_subprocess_return "$MARKER"
|
|
82
|
-
[ ! -f "$MARKER" ]
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@test "slide: no-op when marker path argument is empty" {
|
|
86
|
-
_HOOK_INPUT='{"tool_response":{"content":[]}}'
|
|
87
|
-
run slide_marker_on_subprocess_return ""
|
|
88
|
-
[ "$status" -eq 0 ]
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
@test "slide: malformed hook input is fail-safe (no slide)" {
|
|
92
|
-
touch "$MARKER"
|
|
93
|
-
_backdate "$MARKER" 60
|
|
94
|
-
BEFORE=$(_mtime "$MARKER")
|
|
95
|
-
_HOOK_INPUT='not valid json'
|
|
96
|
-
slide_marker_on_subprocess_return "$MARKER"
|
|
97
|
-
AFTER=$(_mtime "$MARKER")
|
|
98
|
-
# Fail-safe: when the hook input cannot be parsed, treat as error and skip
|
|
99
|
-
[ "$BEFORE" = "$AFTER" ]
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
@test "slide: triggers correctly on Skill tool_response shape (P213, ADR-009 2026-06-08 amendment)" {
|
|
103
|
-
# hooks.json matcher expansion Agent|Bash → Agent|Bash|Skill (P213 Option D)
|
|
104
|
-
# widens slide-marker coverage to PostToolUse:Skill completions (e.g. the
|
|
105
|
-
# /wr-risk-scorer:assess-* sibling assessor SKILLs run as long subprocesses
|
|
106
|
-
# by the AFK orchestrator). The Skill tool_response shape is identical to
|
|
107
|
-
# Agent|Bash (Claude Code's uniform PostToolUse contract), so the matcher-
|
|
108
|
-
# agnostic helper composes without code changes. This test documents that
|
|
109
|
-
# contract explicitly so a future hook_input shape divergence regression
|
|
110
|
-
# surfaces here rather than at the gate-denial site.
|
|
111
|
-
touch "$MARKER"
|
|
112
|
-
_backdate "$MARKER" 60
|
|
113
|
-
BEFORE=$(_mtime "$MARKER")
|
|
114
|
-
_HOOK_INPUT='{"tool_name":"Skill","tool_response":{"content":[{"type":"text","text":"OK"}]}}'
|
|
115
|
-
slide_marker_on_subprocess_return "$MARKER"
|
|
116
|
-
AFTER=$(_mtime "$MARKER")
|
|
117
|
-
[ "$AFTER" -gt "$BEFORE" ]
|
|
118
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
setup() {
|
|
4
|
-
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
|
|
5
|
-
HOOK="$REPO_ROOT/packages/risk-scorer/hooks/wip-risk-mark.sh"
|
|
6
|
-
ORIG_DIR="$PWD"
|
|
7
|
-
TEST_DIR="$(mktemp -d)"
|
|
8
|
-
TMPDIR="$TEST_DIR/tmp"
|
|
9
|
-
export TMPDIR
|
|
10
|
-
mkdir -p "$TMPDIR"
|
|
11
|
-
mkdir -p "$TEST_DIR/repo"
|
|
12
|
-
cd "$TEST_DIR/repo"
|
|
13
|
-
git init -q
|
|
14
|
-
git config user.email test@example.com
|
|
15
|
-
git config user.name "Test User"
|
|
16
|
-
printf 'base\n' > base.txt
|
|
17
|
-
git add base.txt
|
|
18
|
-
git commit -q -m initial
|
|
19
|
-
SESSION_ID="wip-mark-$$"
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
teardown() {
|
|
23
|
-
cd "$ORIG_DIR"
|
|
24
|
-
rm -rf "$TEST_DIR"
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
run_hook() {
|
|
28
|
-
python3 - "$SESSION_ID" <<'PY' | bash "$HOOK"
|
|
29
|
-
import json
|
|
30
|
-
import sys
|
|
31
|
-
print(json.dumps({
|
|
32
|
-
"tool_name": "Write",
|
|
33
|
-
"session_id": sys.argv[1],
|
|
34
|
-
"tool_input": {"file_path": "src/example.txt"},
|
|
35
|
-
}))
|
|
36
|
-
PY
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
@test "wip-risk-mark nudges once when accumulated WIP crosses the small-batch threshold" {
|
|
40
|
-
mkdir -p src
|
|
41
|
-
printf 'one\n' > src/one.txt
|
|
42
|
-
printf 'two\n' > src/two.txt
|
|
43
|
-
printf 'three\n' > src/three.txt
|
|
44
|
-
|
|
45
|
-
run run_hook
|
|
46
|
-
[ "$status" -eq 0 ]
|
|
47
|
-
[[ "$output" == *"Batching risk rising: 3 changed files"* ]]
|
|
48
|
-
[[ "$output" == *"committing a coherent slice now"* ]]
|
|
49
|
-
[[ "$output" == *"push and release small batches"* ]]
|
|
50
|
-
|
|
51
|
-
run run_hook
|
|
52
|
-
[ "$status" -eq 0 ]
|
|
53
|
-
[ -z "$output" ]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
@test "wip-risk-mark stays silent below threshold" {
|
|
57
|
-
mkdir -p src
|
|
58
|
-
printf 'one\n' > src/one.txt
|
|
59
|
-
printf 'two\n' > src/two.txt
|
|
60
|
-
|
|
61
|
-
run run_hook
|
|
62
|
-
[ "$status" -eq 0 ]
|
|
63
|
-
[ -z "$output" ]
|
|
64
|
-
}
|
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
# Behavioural-fixture coverage for packages/risk-scorer/scripts/drain-register-queue.sh
|
|
3
|
-
# per ADR-052 (behavioural tests default) and ADR-056 (Phase 2b drain contract).
|
|
4
|
-
#
|
|
5
|
-
# The drain script consumes .afk-run-state/risk-register-queue.jsonl (produced
|
|
6
|
-
# by risk-score-mark.sh per ADR-056 Phase 2a) and materialises register entries
|
|
7
|
-
# in docs/risks/. The script is invoked by consumer skills (this iter:
|
|
8
|
-
# /wr-itil:work-problems Step 6.4); subsequent iters wire additional consumers.
|
|
9
|
-
|
|
10
|
-
setup() {
|
|
11
|
-
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
|
|
12
|
-
SCRIPT="$REPO_ROOT/packages/risk-scorer/scripts/drain-register-queue.sh"
|
|
13
|
-
SHIM="$REPO_ROOT/packages/risk-scorer/bin/wr-risk-scorer-drain-register-queue"
|
|
14
|
-
WORK_DIR="$(mktemp -d)"
|
|
15
|
-
cd "$WORK_DIR"
|
|
16
|
-
# Minimal git setup — drain script does origin-max lookup via git ls-tree
|
|
17
|
-
git init --quiet
|
|
18
|
-
git config user.email "drain-test@example.com"
|
|
19
|
-
git config user.name "Drain Test"
|
|
20
|
-
git commit --quiet --allow-empty -m "init"
|
|
21
|
-
# Seed README + a single R-file fixture matching the canonical .active.md
|
|
22
|
-
# shape. P171 resolved 2026-05-31: drain script's vestigial TEMPLATE.md gate
|
|
23
|
-
# was removed (it was a pre-wipe-direction residual). Canonical docs/risks/
|
|
24
|
-
# has NO TEMPLATE.md per the 2026-05-04 user direction (commit 8edaf7b) +
|
|
25
|
-
# the canonical .active.md suffix per commit 9b52610. Seeded R-file uses the
|
|
26
|
-
# canonical shape; tests no longer synthesize a fixture-local TEMPLATE.md.
|
|
27
|
-
mkdir -p docs/risks .afk-run-state
|
|
28
|
-
cp "$REPO_ROOT/docs/risks/README.md" docs/risks/README.md
|
|
29
|
-
cat > docs/risks/R001-confidential-info-leak-via-public-repo-push.active.md <<'R001_EOF'
|
|
30
|
-
# Risk R001: Confidential info leak via public repo push
|
|
31
|
-
|
|
32
|
-
**Status**: Active
|
|
33
|
-
**Category**: information-disclosure
|
|
34
|
-
**Identified**: 2026-04-17
|
|
35
|
-
**Owner**: maintainer
|
|
36
|
-
|
|
37
|
-
## Description
|
|
38
|
-
|
|
39
|
-
Test fixture for drain-register-queue dedupe path — slug
|
|
40
|
-
`confidential-info-leak-via-public-repo-push` matches an existing R-file
|
|
41
|
-
with `## Evidence Log` semantics.
|
|
42
|
-
|
|
43
|
-
## Evidence Log
|
|
44
|
-
|
|
45
|
-
- 2026-04-17: seeded fixture entry
|
|
46
|
-
|
|
47
|
-
## Change Log
|
|
48
|
-
|
|
49
|
-
- 2026-04-17: created (test fixture)
|
|
50
|
-
R001_EOF
|
|
51
|
-
git add docs/risks
|
|
52
|
-
git commit --quiet -m "seed risks"
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
teardown() {
|
|
56
|
-
cd /
|
|
57
|
-
rm -rf "$WORK_DIR"
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
@test "shim wrapper exists and is executable" {
|
|
61
|
-
[ -x "$SHIM" ]
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
@test "shim resolves canonical script (not exit 127)" {
|
|
65
|
-
run "$SHIM" "$WORK_DIR"
|
|
66
|
-
[ "$status" -ne 127 ]
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
@test "empty queue → no-op, exit 0, no writes (ADR-056 idempotent)" {
|
|
70
|
-
: > .afk-run-state/risk-register-queue.jsonl
|
|
71
|
-
before_count=$(find docs/risks -name 'R*.active.md' 2>/dev/null | wc -l | tr -d ' ')
|
|
72
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
73
|
-
[ "$status" -eq 0 ]
|
|
74
|
-
echo "$output" | grep -q '^entries_drained=0$'
|
|
75
|
-
echo "$output" | grep -q '^next_action=none$'
|
|
76
|
-
after_count=$(find docs/risks -name 'R*.active.md' 2>/dev/null | wc -l | tr -d ' ')
|
|
77
|
-
[ "$before_count" = "$after_count" ]
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
@test "missing queue file → no-op, exit 0" {
|
|
81
|
-
rm -f .afk-run-state/risk-register-queue.jsonl
|
|
82
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
83
|
-
[ "$status" -eq 0 ]
|
|
84
|
-
echo "$output" | grep -q '^entries_drained=0$'
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
@test "missing docs/risks/ → no-op, exit 0 (Phase 1 scaffold not yet fired)" {
|
|
88
|
-
rm -rf docs/risks
|
|
89
|
-
cat > .afk-run-state/risk-register-queue.jsonl <<EOF
|
|
90
|
-
{"ts":"2026-05-03T14:00:00Z","session_id":"s1","report_path":".risk-reports/x.md","reason_tag":"above-appetite-residual","risk_slug":"foo","slug_source":"agent","prefill":"prose"}
|
|
91
|
-
EOF
|
|
92
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
93
|
-
[ "$status" -eq 0 ]
|
|
94
|
-
echo "$output" | grep -q '^entries_drained=0$'
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
@test "single hint, no existing match → creates R<NNN>-<slug>.active.md" {
|
|
98
|
-
cat > .afk-run-state/risk-register-queue.jsonl <<EOF
|
|
99
|
-
{"ts":"2026-05-03T14:00:00Z","session_id":"s1","report_path":".risk-reports/2026-05-03T14-00-00-commit.md","reason_tag":"above-appetite-residual","risk_slug":"cumulative-residual-commit","slug_source":"agent","prefill":"Cumulative residual at commit hit High band."}
|
|
100
|
-
EOF
|
|
101
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
102
|
-
[ "$status" -eq 0 ]
|
|
103
|
-
echo "$output" | grep -q '^entries_drained=1$'
|
|
104
|
-
echo "$output" | grep -q '^new_risks_created=1$'
|
|
105
|
-
echo "$output" | grep -q '^next_action=commit-staged$'
|
|
106
|
-
# R002 because R001 already exists in the seeded README
|
|
107
|
-
[ -f docs/risks/R002-cumulative-residual-commit.active.md ]
|
|
108
|
-
grep -q 'Status.*Active.*auto-scaffolded.*pending review' docs/risks/R002-cumulative-residual-commit.active.md
|
|
109
|
-
grep -q 'Curation.*pending review' docs/risks/R002-cumulative-residual-commit.active.md
|
|
110
|
-
grep -q 'not estimated.*no prior data' docs/risks/R002-cumulative-residual-commit.active.md
|
|
111
|
-
grep -q 'Cumulative residual at commit hit High band' docs/risks/R002-cumulative-residual-commit.active.md
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
@test "single hint creates README Register table row (ADR-056 step 3d)" {
|
|
115
|
-
cat > .afk-run-state/risk-register-queue.jsonl <<EOF
|
|
116
|
-
{"ts":"2026-05-03T14:00:00Z","session_id":"s1","report_path":".risk-reports/x.md","reason_tag":"above-appetite-residual","risk_slug":"my-test-risk","slug_source":"agent","prefill":"Test risk prose."}
|
|
117
|
-
EOF
|
|
118
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
119
|
-
[ "$status" -eq 0 ]
|
|
120
|
-
# README must contain a row for the new risk in the Register table
|
|
121
|
-
grep -qE '\| \[R002\]\(R002-my-test-risk\.active\.md\) \|' docs/risks/README.md
|
|
122
|
-
# Stub scoring renders as em-dash columns
|
|
123
|
-
grep -qE 'R002.*my-test-risk.*\|.*—.*\|.*—.*\|.*pending' docs/risks/README.md
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
@test "multiple hints with same slug → one register file, multiple Evidence Log lines" {
|
|
127
|
-
cat > .afk-run-state/risk-register-queue.jsonl <<EOF
|
|
128
|
-
{"ts":"2026-05-03T14:00:00Z","session_id":"s1","report_path":".risk-reports/r1.md","reason_tag":"above-appetite-residual","risk_slug":"shared-slug","slug_source":"agent","prefill":"First mention."}
|
|
129
|
-
{"ts":"2026-05-03T14:01:00Z","session_id":"s1","report_path":".risk-reports/r2.md","reason_tag":"above-appetite-residual","risk_slug":"shared-slug","slug_source":"agent","prefill":"Second mention."}
|
|
130
|
-
{"ts":"2026-05-03T14:02:00Z","session_id":"s2","report_path":".risk-reports/r3.md","reason_tag":"above-appetite-residual","risk_slug":"shared-slug","slug_source":"agent","prefill":"Third mention."}
|
|
131
|
-
EOF
|
|
132
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
133
|
-
[ "$status" -eq 0 ]
|
|
134
|
-
echo "$output" | grep -q '^entries_drained=3$'
|
|
135
|
-
echo "$output" | grep -q '^new_risks_created=1$'
|
|
136
|
-
# Exactly one register file
|
|
137
|
-
[ "$(find docs/risks -name 'R*-shared-slug.active.md' | wc -l | tr -d ' ')" = "1" ]
|
|
138
|
-
# Evidence Log section cites all three reports
|
|
139
|
-
grep -q '.risk-reports/r1.md' docs/risks/R*-shared-slug.active.md
|
|
140
|
-
grep -q '.risk-reports/r2.md' docs/risks/R*-shared-slug.active.md
|
|
141
|
-
grep -q '.risk-reports/r3.md' docs/risks/R*-shared-slug.active.md
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
@test "two distinct slugs in same queue → two register files with sequential IDs" {
|
|
145
|
-
cat > .afk-run-state/risk-register-queue.jsonl <<EOF
|
|
146
|
-
{"ts":"2026-05-03T14:00:00Z","session_id":"s1","report_path":".risk-reports/r1.md","reason_tag":"above-appetite-residual","risk_slug":"first-slug","slug_source":"agent","prefill":"First risk."}
|
|
147
|
-
{"ts":"2026-05-03T14:01:00Z","session_id":"s1","report_path":".risk-reports/r2.md","reason_tag":"confidentiality-disclosure","risk_slug":"second-slug","slug_source":"agent","prefill":"Second risk."}
|
|
148
|
-
EOF
|
|
149
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
150
|
-
[ "$status" -eq 0 ]
|
|
151
|
-
echo "$output" | grep -q '^entries_drained=2$'
|
|
152
|
-
echo "$output" | grep -q '^new_risks_created=2$'
|
|
153
|
-
[ -f docs/risks/R002-first-slug.active.md ]
|
|
154
|
-
[ -f docs/risks/R003-second-slug.active.md ]
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
@test "existing match → appends Evidence Log only, no new file, no scoring change (ADR-056 step 3b)" {
|
|
158
|
-
# Pre-seed an existing risk file with this slug
|
|
159
|
-
cat > docs/risks/R042-known-risk.active.md <<'EOF'
|
|
160
|
-
# Risk R042: Known Risk
|
|
161
|
-
|
|
162
|
-
**Status**: Active
|
|
163
|
-
**Category**: operational
|
|
164
|
-
**Identified**: 2026-04-01
|
|
165
|
-
**Owner**: solo-developer
|
|
166
|
-
**Last reviewed**: 2026-04-01
|
|
167
|
-
**Next review**: 2026-10-01
|
|
168
|
-
|
|
169
|
-
## Description
|
|
170
|
-
|
|
171
|
-
Pre-existing curated risk.
|
|
172
|
-
|
|
173
|
-
## Inherent Risk
|
|
174
|
-
|
|
175
|
-
- **Impact**: 3 (Moderate)
|
|
176
|
-
- **Likelihood**: 2 (Unlikely)
|
|
177
|
-
- **Inherent Score**: 6
|
|
178
|
-
- **Inherent Band**: Medium
|
|
179
|
-
|
|
180
|
-
## Controls
|
|
181
|
-
|
|
182
|
-
- **control-x** — does the thing. Implemented in path/x.
|
|
183
|
-
|
|
184
|
-
## Residual Risk
|
|
185
|
-
|
|
186
|
-
- **Impact**: 2 (Minor)
|
|
187
|
-
- **Likelihood**: 2 (Unlikely)
|
|
188
|
-
- **Residual Score**: 4
|
|
189
|
-
- **Residual Band**: Low
|
|
190
|
-
- **Within appetite?**: Yes
|
|
191
|
-
|
|
192
|
-
## Treatment
|
|
193
|
-
|
|
194
|
-
Mitigate. Justified.
|
|
195
|
-
|
|
196
|
-
## Monitoring
|
|
197
|
-
|
|
198
|
-
- **Trigger to re-assess**: never
|
|
199
|
-
- **Metrics**: none
|
|
200
|
-
|
|
201
|
-
## Related
|
|
202
|
-
|
|
203
|
-
- Criteria: `RISK-POLICY.md`
|
|
204
|
-
|
|
205
|
-
## Change Log
|
|
206
|
-
|
|
207
|
-
- 2026-04-01: Initial identification.
|
|
208
|
-
EOF
|
|
209
|
-
|
|
210
|
-
cat > .afk-run-state/risk-register-queue.jsonl <<EOF
|
|
211
|
-
{"ts":"2026-05-03T14:00:00Z","session_id":"s1","report_path":".risk-reports/new-fire.md","reason_tag":"above-appetite-residual","risk_slug":"known-risk","slug_source":"agent","prefill":"Fired again."}
|
|
212
|
-
EOF
|
|
213
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
214
|
-
[ "$status" -eq 0 ]
|
|
215
|
-
echo "$output" | grep -q '^entries_drained=1$'
|
|
216
|
-
echo "$output" | grep -q '^new_risks_created=0$'
|
|
217
|
-
echo "$output" | grep -q '^evidence_appended=1$'
|
|
218
|
-
# Existing file untouched on scoring lines
|
|
219
|
-
grep -q 'Inherent Score.*: 6$' docs/risks/R042-known-risk.active.md
|
|
220
|
-
grep -q 'Residual Score.*: 4$' docs/risks/R042-known-risk.active.md
|
|
221
|
-
# Evidence Log section now exists
|
|
222
|
-
grep -q '.risk-reports/new-fire.md' docs/risks/R042-known-risk.active.md
|
|
223
|
-
# No R<NNN+1> file created
|
|
224
|
-
[ ! -f docs/risks/R002-known-risk.active.md ]
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
@test "queue truncated on success" {
|
|
228
|
-
cat > .afk-run-state/risk-register-queue.jsonl <<EOF
|
|
229
|
-
{"ts":"2026-05-03T14:00:00Z","session_id":"s1","report_path":".risk-reports/r1.md","reason_tag":"above-appetite-residual","risk_slug":"truncate-test","slug_source":"agent","prefill":"prose."}
|
|
230
|
-
EOF
|
|
231
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
232
|
-
[ "$status" -eq 0 ]
|
|
233
|
-
# Queue file is empty after success
|
|
234
|
-
[ ! -s .afk-run-state/risk-register-queue.jsonl ]
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
@test "queue NOT truncated on no-op (no docs/risks/ dir)" {
|
|
238
|
-
rm -rf docs/risks
|
|
239
|
-
cat > .afk-run-state/risk-register-queue.jsonl <<EOF
|
|
240
|
-
{"ts":"2026-05-03T14:00:00Z","session_id":"s1","report_path":".risk-reports/r1.md","reason_tag":"above-appetite-residual","risk_slug":"preserve-on-skip","slug_source":"agent","prefill":"prose."}
|
|
241
|
-
EOF
|
|
242
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
243
|
-
[ "$status" -eq 0 ]
|
|
244
|
-
# Queue preserved when drain skips — Phase 1 scaffolding may land later
|
|
245
|
-
[ -s .afk-run-state/risk-register-queue.jsonl ]
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
@test "stdout key=value shape (caller-parseable)" {
|
|
249
|
-
cat > .afk-run-state/risk-register-queue.jsonl <<EOF
|
|
250
|
-
{"ts":"2026-05-03T14:00:00Z","session_id":"s1","report_path":".risk-reports/r.md","reason_tag":"above-appetite-residual","risk_slug":"shape-test","slug_source":"agent","prefill":"prose."}
|
|
251
|
-
EOF
|
|
252
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
253
|
-
[ "$status" -eq 0 ]
|
|
254
|
-
# All four required keys present
|
|
255
|
-
echo "$output" | grep -qE '^entries_drained=[0-9]+$'
|
|
256
|
-
echo "$output" | grep -qE '^new_risks_created=[0-9]+$'
|
|
257
|
-
echo "$output" | grep -qE '^evidence_appended=[0-9]+$'
|
|
258
|
-
echo "$output" | grep -qE '^next_action=(commit-staged|none)$'
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
@test "files staged after successful drain (ready for caller commit)" {
|
|
262
|
-
cat > .afk-run-state/risk-register-queue.jsonl <<EOF
|
|
263
|
-
{"ts":"2026-05-03T14:00:00Z","session_id":"s1","report_path":".risk-reports/r.md","reason_tag":"above-appetite-residual","risk_slug":"stage-test","slug_source":"agent","prefill":"prose."}
|
|
264
|
-
EOF
|
|
265
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
266
|
-
[ "$status" -eq 0 ]
|
|
267
|
-
# Caller should be able to git commit immediately
|
|
268
|
-
staged=$(git diff --cached --name-only)
|
|
269
|
-
echo "$staged" | grep -q 'docs/risks/R002-stage-test.active.md'
|
|
270
|
-
echo "$staged" | grep -q 'docs/risks/README.md'
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
@test "origin-max collision avoidance (ADR-019 ticket-creator dual-source ID)" {
|
|
274
|
-
# Simulate origin/main having higher R-numbers than local. The drain script
|
|
275
|
-
# MUST consult origin-max so parallel adopter sessions don't mint duplicate IDs.
|
|
276
|
-
# We mock by creating a branch with R099 file then resetting local but keeping
|
|
277
|
-
# the ref reachable as origin/main.
|
|
278
|
-
cat > docs/risks/R099-future-risk.active.md <<'EOF'
|
|
279
|
-
# Risk R099: Future risk
|
|
280
|
-
EOF
|
|
281
|
-
git add docs/risks/R099-future-risk.active.md
|
|
282
|
-
git commit --quiet -m "high-id"
|
|
283
|
-
git update-ref refs/remotes/origin/main HEAD
|
|
284
|
-
git rm --quiet docs/risks/R099-future-risk.active.md
|
|
285
|
-
git commit --quiet -m "remove from local"
|
|
286
|
-
# Now local-max sees only R001 (from seeded README) but origin-max should see R099
|
|
287
|
-
cat > .afk-run-state/risk-register-queue.jsonl <<EOF
|
|
288
|
-
{"ts":"2026-05-03T14:00:00Z","session_id":"s1","report_path":".risk-reports/r.md","reason_tag":"above-appetite-residual","risk_slug":"collision-guard","slug_source":"agent","prefill":"prose."}
|
|
289
|
-
EOF
|
|
290
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
291
|
-
[ "$status" -eq 0 ]
|
|
292
|
-
# Next ID must be R100, not R002
|
|
293
|
-
[ -f docs/risks/R100-collision-guard.active.md ]
|
|
294
|
-
[ ! -f docs/risks/R002-collision-guard.active.md ]
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
@test "malformed JSONL line skipped, valid lines processed" {
|
|
298
|
-
cat > .afk-run-state/risk-register-queue.jsonl <<EOF
|
|
299
|
-
not-json-at-all
|
|
300
|
-
{"ts":"2026-05-03T14:00:00Z","session_id":"s1","report_path":".risk-reports/r.md","reason_tag":"above-appetite-residual","risk_slug":"good-line","slug_source":"agent","prefill":"valid prose."}
|
|
301
|
-
{"ts":"bad","incomplete":true}
|
|
302
|
-
EOF
|
|
303
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
304
|
-
[ "$status" -eq 0 ]
|
|
305
|
-
echo "$output" | grep -q '^new_risks_created=1$'
|
|
306
|
-
[ -f docs/risks/R002-good-line.active.md ]
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
@test "P309/P171 — three unrepresented slugs drain to three register files + queue truncated" {
|
|
310
|
-
# P309 regression coverage (fold-fixed by P171, commit 9e91508).
|
|
311
|
-
# Replays the original P309 observation: a 3-entry queue with three slugs
|
|
312
|
-
# that have no matching docs/risks/ register file. Prior to the P171 fix,
|
|
313
|
-
# this returned entries_drained=0 / new_risks_created=0 / next_action=none
|
|
314
|
-
# AND did not truncate the queue — entries accumulated indefinitely off-
|
|
315
|
-
# ledger. Post-fix, the drain MUST materialise all three register files,
|
|
316
|
-
# truncate the queue, and emit next_action=commit-staged.
|
|
317
|
-
rm -f docs/risks/TEMPLATE.md
|
|
318
|
-
cat > .afk-run-state/risk-register-queue.jsonl <<EOF
|
|
319
|
-
{"ts":"2026-05-24T10:00:00Z","session_id":"s1","report_path":".risk-reports/2026-05-24-r1.md","reason_tag":"above-appetite-residual","risk_slug":"p309-unrepresented-slug-a","slug_source":"agent","prefill":"First unrepresented slug — should mint a new register entry."}
|
|
320
|
-
{"ts":"2026-05-25T10:00:00Z","session_id":"s2","report_path":".risk-reports/2026-05-25-r2.md","reason_tag":"above-appetite-residual","risk_slug":"p309-unrepresented-slug-b","slug_source":"agent","prefill":"Second unrepresented slug — should mint a second new register entry."}
|
|
321
|
-
{"ts":"2026-05-25T11:00:00Z","session_id":"s2","report_path":".risk-reports/2026-05-25-r3.md","reason_tag":"above-appetite-residual","risk_slug":"p309-unrepresented-slug-c","slug_source":"agent","prefill":"Third unrepresented slug — should mint a third new register entry."}
|
|
322
|
-
EOF
|
|
323
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
324
|
-
[ "$status" -eq 0 ]
|
|
325
|
-
echo "$output" | grep -q '^entries_drained=3$'
|
|
326
|
-
echo "$output" | grep -q '^new_risks_created=3$'
|
|
327
|
-
echo "$output" | grep -q '^evidence_appended=0$'
|
|
328
|
-
echo "$output" | grep -q '^next_action=commit-staged$'
|
|
329
|
-
# Three register files materialised with sequential IDs (R002, R003, R004
|
|
330
|
-
# because R001 is in the seeded README fixture).
|
|
331
|
-
[ -f docs/risks/R002-p309-unrepresented-slug-a.active.md ]
|
|
332
|
-
[ -f docs/risks/R003-p309-unrepresented-slug-b.active.md ]
|
|
333
|
-
[ -f docs/risks/R004-p309-unrepresented-slug-c.active.md ]
|
|
334
|
-
# Queue truncated — entries no longer accumulate off-ledger.
|
|
335
|
-
[ ! -s .afk-run-state/risk-register-queue.jsonl ]
|
|
336
|
-
# README Register table picks up all three new rows.
|
|
337
|
-
grep -q 'R002-p309-unrepresented-slug-a' docs/risks/README.md
|
|
338
|
-
grep -q 'R003-p309-unrepresented-slug-b' docs/risks/README.md
|
|
339
|
-
grep -q 'R004-p309-unrepresented-slug-c' docs/risks/README.md
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
@test "drain succeeds against canonical (post-wipe) docs/risks/ with NO TEMPLATE.md (P171)" {
|
|
343
|
-
# P171 regression coverage. The 2026-05-04 wipe direction (commit 8edaf7b)
|
|
344
|
-
# removed TEMPLATE.md from canonical docs/risks/; commit 9b52610 then re-
|
|
345
|
-
# canonicalized the R-file suffix to .active.md. The drain script previously
|
|
346
|
-
# gated on TEMPLATE.md existence and would silent-no-op against the canonical
|
|
347
|
-
# (TEMPLATE.md-absent) state. This test asserts the gate is gone: a queue with
|
|
348
|
-
# one hint MUST materialize a register entry even without TEMPLATE.md.
|
|
349
|
-
rm -f docs/risks/TEMPLATE.md
|
|
350
|
-
cat > .afk-run-state/risk-register-queue.jsonl <<EOF
|
|
351
|
-
{"ts":"2026-05-03T14:00:00Z","session_id":"s1","report_path":".risk-reports/p171.md","reason_tag":"above-appetite-residual","risk_slug":"p171-canonical-fire","slug_source":"agent","prefill":"Canonical post-wipe drain works without TEMPLATE.md."}
|
|
352
|
-
EOF
|
|
353
|
-
run bash "$SCRIPT" "$WORK_DIR"
|
|
354
|
-
[ "$status" -eq 0 ]
|
|
355
|
-
echo "$output" | grep -q '^entries_drained=1$'
|
|
356
|
-
echo "$output" | grep -q '^new_risks_created=1$'
|
|
357
|
-
echo "$output" | grep -q '^next_action=commit-staged$'
|
|
358
|
-
[ -f docs/risks/R002-p171-canonical-fire.active.md ]
|
|
359
|
-
}
|