@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.
Files changed (55) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +13 -2
  5. package/bin/install.mjs +42 -8
  6. package/hooks/codex-agent-completion.mjs +82 -0
  7. package/hooks/hooks.json +5 -2
  8. package/hooks/risk-scorer-dispatch.sh +3 -0
  9. package/hooks/risk-scorer-scaffold-nudge.sh +15 -4
  10. package/package.json +5 -2
  11. package/scripts/codex-agents.mjs +208 -0
  12. package/scripts/sync-codex-skills.mjs +8 -3
  13. package/skills/assess-external-comms/SKILL.md +5 -4
  14. package/skills/assess-inbound-report/SKILL.md +5 -4
  15. package/skills/assess-release/SKILL.md +5 -4
  16. package/skills/assess-wip/SKILL.md +5 -4
  17. package/skills/bootstrap-catalog/SKILL.md +5 -4
  18. package/skills/create-risk/SKILL.md +5 -4
  19. package/skills/external-comms/SKILL.md +5 -4
  20. package/skills/pipeline/SKILL.md +5 -4
  21. package/skills/update-policy/SKILL.md +6 -5
  22. package/skills/wip/SKILL.md +5 -4
  23. package/agents/test/inbound-report-contract.bats +0 -225
  24. package/agents/test/risk-scorer-above-appetite-stop.bats +0 -80
  25. package/agents/test/risk-scorer-catalog-consumption.bats +0 -138
  26. package/agents/test/risk-scorer-commit-verdict.bats +0 -66
  27. package/agents/test/risk-scorer-monitoring-not-a-control.bats +0 -76
  28. package/agents/test/risk-scorer-reducing-bypass-criteria.bats +0 -62
  29. package/agents/test/risk-scorer-register-hint.bats +0 -111
  30. package/agents/test/risk-scorer-structured-remediations.bats +0 -123
  31. package/agents/test/risk-scorer-user-stated-preconditions.bats +0 -89
  32. package/hooks/test/ci-status-gate.bats +0 -234
  33. package/hooks/test/external-comms-gate.bats +0 -528
  34. package/hooks/test/external-comms-key-substance.bats +0 -74
  35. package/hooks/test/git-push-gate.bats +0 -82
  36. package/hooks/test/pipeline-state-changeset-partition.bats +0 -181
  37. package/hooks/test/pipeline-state-hash.bats +0 -195
  38. package/hooks/test/plan-risk-guidance-once-per-session.bats +0 -95
  39. package/hooks/test/reducing-marker-persistence.bats +0 -236
  40. package/hooks/test/risk-gate.bats +0 -330
  41. package/hooks/test/risk-policy-project-root.bats +0 -20
  42. package/hooks/test/risk-score-commit-gate-adr-031-bypass.bats +0 -98
  43. package/hooks/test/risk-score-commit-gate-cadence-staleness.bats +0 -105
  44. package/hooks/test/risk-score-mark-external-comms-prompt-parse.bats +0 -94
  45. package/hooks/test/risk-score-mark-register-queue.bats +0 -253
  46. package/hooks/test/risk-score-mark.bats +0 -112
  47. package/hooks/test/risk-scorer-hook-dispatch.bats +0 -77
  48. package/hooks/test/risk-scorer-no-stop-hook.bats +0 -19
  49. package/hooks/test/risk-scorer-scaffold-nudge.bats +0 -132
  50. package/hooks/test/slide-marker-on-subprocess-return.bats +0 -118
  51. package/hooks/test/wip-risk-mark.bats +0 -64
  52. package/scripts/test/drain-register-queue.bats +0 -359
  53. package/scripts/test/evaluate-graduation.bats +0 -533
  54. package/scripts/test/extract-risks-from-reports.bats +0 -366
  55. 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
- }