@windyroad/itil 0.23.2 → 0.23.3
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/.claude-plugin/plugin.json +1 -1
- package/hooks/hooks.json +4 -0
- package/hooks/itil-runtime-sid-marker.sh +87 -0
- package/hooks/lib/runtime-sid.sh +61 -0
- package/hooks/lib/session-id.sh +24 -0
- package/hooks/manage-problem-enforce-create.sh +10 -16
- package/hooks/test/manage-problem-enforce-create.bats +24 -30
- package/hooks/test/runtime-sid-marker.bats +90 -0
- package/hooks/test/session-id.bats +80 -0
- package/package.json +1 -1
- package/skills/manage-problem/SKILL.md +1 -40
- package/skills/manage-problem/test/manage-problem-p119-recovery-path.bats +0 -165
package/hooks/hooks.json
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-correction-detect.sh" }] }
|
|
9
9
|
],
|
|
10
10
|
"PreToolUse": [
|
|
11
|
+
{
|
|
12
|
+
"matcher": "Bash|Write|Edit|Read",
|
|
13
|
+
"hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-runtime-sid-marker.sh" }]
|
|
14
|
+
},
|
|
11
15
|
{
|
|
12
16
|
"matcher": "Write",
|
|
13
17
|
"hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/manage-problem-enforce-create.sh" }]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# P142 / ADR-050: runtime-SID instrumentation PreToolUse hook.
|
|
3
|
+
#
|
|
4
|
+
# Captures the runtime stdin `session_id` from Claude Code's PreToolUse
|
|
5
|
+
# JSON payload and writes it to a per-machine, per-user, per-project
|
|
6
|
+
# marker file. The `get_current_session_id` helper (lib/session-id.sh)
|
|
7
|
+
# reads this marker as the authoritative current-session UUID, replacing
|
|
8
|
+
# the Phase 3 mtime-based announce-marker selection that misfired in
|
|
9
|
+
# orchestrator main turns AFTER subprocess dispatch (P142 ticket).
|
|
10
|
+
#
|
|
11
|
+
# Why a NEW PreToolUse hook (not an extension of an existing one):
|
|
12
|
+
# - manage-problem-enforce-create.sh already runs on PreToolUse:Write,
|
|
13
|
+
# but its perf-sensitive denial-path needs the runtime SID BEFORE
|
|
14
|
+
# this hook would write it. Writing in a separate, prior hook
|
|
15
|
+
# ensures the marker is in place by the time enforce-create reads.
|
|
16
|
+
# - The architect-enforce-edit / jtbd-enforce-edit / tdd-enforce hooks
|
|
17
|
+
# are owned by sibling plugins; cross-plugin coupling is rejected
|
|
18
|
+
# per ADR-017 (shared-code-sync).
|
|
19
|
+
# - A standalone, single-purpose hook is the cleanest fit for ADR-045
|
|
20
|
+
# Pattern 1 (silent-on-pass, side-effect-only).
|
|
21
|
+
#
|
|
22
|
+
# Matcher: PreToolUse:Bash|Write|Edit|Read covers the tool calls that
|
|
23
|
+
# may invoke `get_current_session_id` indirectly (Bash sources the
|
|
24
|
+
# helper; Write/Edit fires the create-gate that consumes the marker;
|
|
25
|
+
# Read is included for completeness — every tool call that fires a
|
|
26
|
+
# PreToolUse hook contributes a fresh marker).
|
|
27
|
+
#
|
|
28
|
+
# ADR-045 Pattern 1 binding: this hook MUST emit 0 bytes on stdout.
|
|
29
|
+
# Adding stdout output would burn the per-tool-call context budget.
|
|
30
|
+
# All side effects are filesystem writes; observability is via the
|
|
31
|
+
# marker file itself.
|
|
32
|
+
#
|
|
33
|
+
# Fail-open contract: any error path (missing jq, malformed JSON, empty
|
|
34
|
+
# session_id, write failure) exits 0 without modifying state. The hook
|
|
35
|
+
# MUST NOT block tool calls — its only role is to deposit a marker for
|
|
36
|
+
# the helper. If the marker is absent, the helper falls back to the
|
|
37
|
+
# announce-marker priority logic.
|
|
38
|
+
#
|
|
39
|
+
# References:
|
|
40
|
+
# ADR-050 — runtime-SID instrumentation surface (this hook).
|
|
41
|
+
# ADR-048 — gate-misfire recovery (superseded by ADR-050).
|
|
42
|
+
# ADR-045 — hook injection budget; Pattern 1 binding.
|
|
43
|
+
# ADR-038 — announce-marker contract (cold-path fallback consumer).
|
|
44
|
+
# P142 — the ticket this hook closes.
|
|
45
|
+
# P124 — Phase 3 helper this hook complements.
|
|
46
|
+
|
|
47
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
48
|
+
# shellcheck source=lib/runtime-sid.sh
|
|
49
|
+
source "$SCRIPT_DIR/lib/runtime-sid.sh"
|
|
50
|
+
|
|
51
|
+
INPUT=$(cat)
|
|
52
|
+
|
|
53
|
+
# Empty stdin -> no-op. Hook harnesses, manual invocation, or a
|
|
54
|
+
# malformed stdin payload all land here; fail-open per the contract.
|
|
55
|
+
if [ -z "$INPUT" ]; then
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Parse session_id with python3 (universally present on macOS + most
|
|
60
|
+
# Linux distros; also already used by manage-problem-enforce-create.sh
|
|
61
|
+
# as the JSON parser of choice in this plugin). jq fallback if python3
|
|
62
|
+
# is absent. Any parse failure -> empty SESSION_ID -> no-op below.
|
|
63
|
+
SESSION_ID=""
|
|
64
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
65
|
+
SESSION_ID=$(echo "$INPUT" | python3 -c "
|
|
66
|
+
import sys, json
|
|
67
|
+
try:
|
|
68
|
+
data = json.load(sys.stdin)
|
|
69
|
+
print(data.get('session_id', ''))
|
|
70
|
+
except Exception:
|
|
71
|
+
print('')
|
|
72
|
+
" 2>/dev/null || echo "")
|
|
73
|
+
elif command -v jq >/dev/null 2>&1; then
|
|
74
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null || echo "")
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
if [ -z "$SESSION_ID" ]; then
|
|
78
|
+
exit 0
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# Write the marker. printf (not echo) to avoid trailing newline; the
|
|
82
|
+
# helper's `cat` reads contents verbatim, and a trailing newline would
|
|
83
|
+
# corrupt the SID comparison the runtime hook performs.
|
|
84
|
+
MARKER_PATH=$(runtime_sid_path)
|
|
85
|
+
printf '%s' "$SESSION_ID" > "$MARKER_PATH" 2>/dev/null || true
|
|
86
|
+
|
|
87
|
+
exit 0
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# P142 (P124 Phase 4): runtime-SID marker path helper.
|
|
3
|
+
#
|
|
4
|
+
# Computes the per-machine, per-user, per-project marker path that the
|
|
5
|
+
# `itil-runtime-sid-marker.sh` PreToolUse hook writes the runtime
|
|
6
|
+
# `session_id` to (parsed from hook stdin JSON) and that
|
|
7
|
+
# `get_current_session_id` reads as the authoritative current-session
|
|
8
|
+
# identifier. Both producer (hook) and consumer (helper) source this
|
|
9
|
+
# lib so they agree on the path.
|
|
10
|
+
#
|
|
11
|
+
# Why this exists:
|
|
12
|
+
# The Phase 3 helper relied on within-system mtime selection across
|
|
13
|
+
# ADR-038 announce markers. In orchestrator main turns AFTER subprocess
|
|
14
|
+
# dispatch, subprocess announce markers had NEWER mtimes than the
|
|
15
|
+
# orchestrator's, so newest-mtime-wins picked the wrong UUID. No pure-
|
|
16
|
+
# helper algorithm can disambiguate orchestrator vs subprocess context
|
|
17
|
+
# from filesystem state alone (P142 ticket Investigation Tasks). The
|
|
18
|
+
# structural fix is to capture the runtime stdin SID — known with
|
|
19
|
+
# certainty by the hook on every tool call — into a discoverable file
|
|
20
|
+
# the helper can read. See ADR-050.
|
|
21
|
+
#
|
|
22
|
+
# Path scoping:
|
|
23
|
+
# When SESSION_MARKER_DIR is set (sandboxed bats per session-id.bats
|
|
24
|
+
# convention), the marker lives at "${SESSION_MARKER_DIR}/itil-runtime-sid.current"
|
|
25
|
+
# — a single fixed filename, no per-user/per-project scoping. Tests
|
|
26
|
+
# create and tear down their own SANDBOX_TMP, so cross-test pollution
|
|
27
|
+
# is impossible without further scoping.
|
|
28
|
+
#
|
|
29
|
+
# In production (no SESSION_MARKER_DIR), the path is
|
|
30
|
+
# "/tmp/itil-runtime-sid-${USER}-${proj_hash}.current" where
|
|
31
|
+
# proj_hash = cksum of $PWD. Two Claude Code sessions in DIFFERENT
|
|
32
|
+
# projects do not race (different proj_hash). Two sessions in the
|
|
33
|
+
# SAME project on the same machine still race; per ADR-050 this is
|
|
34
|
+
# accepted as a documented limitation — the failure mode is a hook-
|
|
35
|
+
# denied Write that the agent can recover from, not silent corruption.
|
|
36
|
+
#
|
|
37
|
+
# References:
|
|
38
|
+
# ADR-050 — runtime-SID instrumentation via PreToolUse (this surface).
|
|
39
|
+
# ADR-048 — gate-misfire recovery procedure (superseded by ADR-050 +
|
|
40
|
+
# P142 + this lib).
|
|
41
|
+
# ADR-038 — announce-marker contract (cold-path fallback consumer).
|
|
42
|
+
# ADR-009 — gate marker lifecycle.
|
|
43
|
+
# P142 — this fix's ticket.
|
|
44
|
+
|
|
45
|
+
# Echoes the runtime-SID marker path on stdout. Always exits 0.
|
|
46
|
+
#
|
|
47
|
+
# Usage:
|
|
48
|
+
# source packages/itil/hooks/lib/runtime-sid.sh
|
|
49
|
+
# path=$(runtime_sid_path)
|
|
50
|
+
runtime_sid_path() {
|
|
51
|
+
if [ -n "${SESSION_MARKER_DIR:-}" ]; then
|
|
52
|
+
echo "${SESSION_MARKER_DIR}/itil-runtime-sid.current"
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
local user="${USER:-anon}"
|
|
56
|
+
local proj_hash
|
|
57
|
+
# cksum is POSIX; portable across macOS BSD and Linux GNU.
|
|
58
|
+
# Trailing whitespace stripped via awk; first field is the checksum.
|
|
59
|
+
proj_hash=$(printf '%s' "${PWD:-/}" | cksum 2>/dev/null | awk '{print $1}')
|
|
60
|
+
echo "/tmp/itil-runtime-sid-${user}-${proj_hash:-0}.current"
|
|
61
|
+
}
|
package/hooks/lib/session-id.sh
CHANGED
|
@@ -69,6 +69,30 @@ get_current_session_id() {
|
|
|
69
69
|
return 0
|
|
70
70
|
fi
|
|
71
71
|
|
|
72
|
+
# P142 / ADR-050: runtime-SID marker. The PreToolUse hook
|
|
73
|
+
# (itil-runtime-sid-marker.sh) writes the runtime stdin session_id
|
|
74
|
+
# to a per-machine marker on EVERY tool call. The helper, running
|
|
75
|
+
# inside a Bash tool call, reads the marker that the same Bash
|
|
76
|
+
# tool call's PreToolUse hook just wrote — by construction the
|
|
77
|
+
# current session's SID. This is the authoritative path; the
|
|
78
|
+
# announce-marker fallback below is the cold-path (no PreToolUse
|
|
79
|
+
# has fired yet in this session).
|
|
80
|
+
local rt_lib_dir
|
|
81
|
+
rt_lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
|
82
|
+
if [ -f "${rt_lib_dir}/runtime-sid.sh" ]; then
|
|
83
|
+
# shellcheck source=runtime-sid.sh
|
|
84
|
+
source "${rt_lib_dir}/runtime-sid.sh"
|
|
85
|
+
local rt_path rt_sid
|
|
86
|
+
rt_path=$(runtime_sid_path)
|
|
87
|
+
if [ -s "$rt_path" ]; then
|
|
88
|
+
rt_sid=$(cat "$rt_path" 2>/dev/null)
|
|
89
|
+
if [ -n "$rt_sid" ]; then
|
|
90
|
+
echo "$rt_sid"
|
|
91
|
+
return 0
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
|
|
72
96
|
local marker_dir="${SESSION_MARKER_DIR:-/tmp}"
|
|
73
97
|
|
|
74
98
|
# Marker-system priority order. Architect first because architect-
|
|
@@ -117,20 +117,14 @@ if check_create_gate "$SESSION_ID"; then
|
|
|
117
117
|
exit 0
|
|
118
118
|
fi
|
|
119
119
|
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
RECOVERY_HINT=""
|
|
131
|
-
if compgen -G '/tmp/manage-problem-grep-*' > /dev/null 2>&1; then
|
|
132
|
-
RECOVERY_HINT=" (Helper succeeded but SID mismatch detected — see manage-problem SKILL.md Step 2 substep 7.)"
|
|
133
|
-
fi
|
|
134
|
-
|
|
135
|
-
create_gate_deny "BLOCKED: Cannot Write '${BASENAME}' under docs/problems/ without running /wr-itil:manage-problem Step 2 (duplicate-check) first. New problem tickets MUST be created via the skill so the duplicate-prevention grep fires before the file lands. Invoke the Skill tool with skill='wr-itil:manage-problem' and a description of the new problem; Step 2 will grep for related existing tickets and surface any matches via AskUserQuestion before creating the new ticket. (P119)${RECOVERY_HINT}"
|
|
120
|
+
# P142 / ADR-050: the runtime-SID instrumentation hook
|
|
121
|
+
# (itil-runtime-sid-marker.sh) writes the runtime stdin session_id to a
|
|
122
|
+
# per-machine marker on every PreToolUse:Bash|Write|Edit|Read event. The
|
|
123
|
+
# `get_current_session_id` helper reads that marker as the authoritative
|
|
124
|
+
# SID, so the marker `mark_step2_complete` writes is bound to the same
|
|
125
|
+
# session_id this hook will see on the subsequent Write. SID-mismatch
|
|
126
|
+
# denial is structurally eliminated; the only remaining deny path is
|
|
127
|
+
# the routine "Step 2 grep has not run yet for this session" case, for
|
|
128
|
+
# which the deny message stays focused and skill-pointing.
|
|
129
|
+
create_gate_deny "BLOCKED: Cannot Write '${BASENAME}' under docs/problems/ without running /wr-itil:manage-problem Step 2 (duplicate-check) first. New problem tickets MUST be created via the skill so the duplicate-prevention grep fires before the file lands. Invoke the Skill tool with skill='wr-itil:manage-problem' and a description of the new problem; Step 2 will grep for related existing tickets and surface any matches via AskUserQuestion before creating the new ticket. (P119)"
|
|
136
130
|
exit 0
|
|
@@ -187,16 +187,23 @@ set_marker() {
|
|
|
187
187
|
[[ "$output" != *"BLOCKED"* ]]
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
# ---
|
|
190
|
+
# --- P142 / ADR-050: deny-message simplicity post-supersession ---
|
|
191
191
|
#
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
#
|
|
192
|
+
# ADR-048 documented a two-tier recovery procedure for SID-mismatch denials
|
|
193
|
+
# (when the helper picked a stale subprocess SID while the runtime hook saw
|
|
194
|
+
# the orchestrator SID). The hook appended a "Helper succeeded but SID
|
|
195
|
+
# mismatch detected — see manage-problem SKILL.md Step 2 substep 7."
|
|
196
|
+
# pointer when SOME marker existed for SOME SID (the helper-bug signal).
|
|
197
197
|
#
|
|
198
|
-
#
|
|
199
|
-
#
|
|
198
|
+
# P142 / ADR-050 superseded ADR-048 by capturing the runtime stdin SID
|
|
199
|
+
# in a per-machine marker via a new PreToolUse hook
|
|
200
|
+
# (`itil-runtime-sid-marker.sh`). The helper reads the marker as
|
|
201
|
+
# authoritative; SID-mismatch is structurally impossible in routine flow.
|
|
202
|
+
# The conditional RECOVERY_HINT was removed; the deny message stays
|
|
203
|
+
# terse and skill-pointing regardless of marker presence.
|
|
204
|
+
#
|
|
205
|
+
# These tests pin that the deny message is INVARIANT of the
|
|
206
|
+
# /tmp/manage-problem-grep-* marker state (no recovery-hint branching).
|
|
200
207
|
|
|
201
208
|
setup_other_sid_marker() {
|
|
202
209
|
OTHER_SID="other-sid-$$-$RANDOM"
|
|
@@ -209,35 +216,21 @@ teardown_other_sid_marker() {
|
|
|
209
216
|
fi
|
|
210
217
|
}
|
|
211
218
|
|
|
212
|
-
@test "deny without ANY /tmp/manage-problem-grep-* marker → deny
|
|
213
|
-
# Scrub any markers so the helper-bug signal cannot fire.
|
|
219
|
+
@test "deny without ANY /tmp/manage-problem-grep-* marker → deny is terse, no recovery prose" {
|
|
214
220
|
rm -f /tmp/manage-problem-grep-*
|
|
215
221
|
run run_write_hook "$PWD/docs/problems/999-foo.open.md" "$SID"
|
|
216
222
|
[ "$status" -eq 0 ]
|
|
217
223
|
[[ "$output" == *"BLOCKED"* ]]
|
|
218
|
-
# No marker exists for any SID → routine first-creation deny → no recovery hint.
|
|
219
224
|
[[ "$output" != *"SID mismatch"* ]]
|
|
220
225
|
[[ "$output" != *"Step 2 substep 7"* ]]
|
|
221
226
|
}
|
|
222
227
|
|
|
223
|
-
@test "deny with /tmp/manage-problem-grep-* marker for OTHER SID → deny
|
|
224
|
-
#
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
teardown_other_sid_marker
|
|
230
|
-
[ "$status" -eq 0 ]
|
|
231
|
-
[[ "$output" == *"BLOCKED"* ]]
|
|
232
|
-
# Marker exists for OTHER SID → helper-bug signal → recovery hint appended.
|
|
233
|
-
[[ "$output" == *"SID mismatch"* ]]
|
|
234
|
-
[[ "$output" == *"Step 2 substep 7"* ]]
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
@test "recovery hint avoids ADR-038 jargon (no internal P-number jargon in deny string)" {
|
|
238
|
-
# ADR-038 progressive disclosure — deny stays terse + actionable. Architect
|
|
239
|
-
# advisory rejected "P124-Phase-3-regression" wording in favour of plain
|
|
240
|
-
# "Helper succeeded but SID mismatch detected".
|
|
228
|
+
@test "deny with /tmp/manage-problem-grep-* marker for OTHER SID → deny is terse (post-ADR-050; no recovery hint)" {
|
|
229
|
+
# Pre-ADR-050 contract: an other-SID marker triggered the helper-bug
|
|
230
|
+
# recovery pointer. Post-ADR-050: the runtime-SID marker prevents
|
|
231
|
+
# SID-mismatch in routine flow, so the recovery pointer is removed.
|
|
232
|
+
# The deny message is identical regardless of marker presence — the
|
|
233
|
+
# only signal that matters is "this session has not run Step 2".
|
|
241
234
|
rm -f /tmp/manage-problem-grep-*
|
|
242
235
|
setup_other_sid_marker
|
|
243
236
|
run run_write_hook "$PWD/docs/problems/999-foo.open.md" "$SID"
|
|
@@ -245,5 +238,6 @@ teardown_other_sid_marker() {
|
|
|
245
238
|
teardown_other_sid_marker
|
|
246
239
|
[ "$status" -eq 0 ]
|
|
247
240
|
[[ "$output" == *"BLOCKED"* ]]
|
|
248
|
-
[[ "$output" != *"
|
|
241
|
+
[[ "$output" != *"SID mismatch"* ]]
|
|
242
|
+
[[ "$output" != *"Step 2 substep 7"* ]]
|
|
249
243
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# P142 / ADR-050: itil-runtime-sid-marker.sh PreToolUse hook.
|
|
4
|
+
#
|
|
5
|
+
# Behavioural contract:
|
|
6
|
+
# 1. Hook receives JSON on stdin with a `session_id` field.
|
|
7
|
+
# 2. Hook writes the session_id to the runtime-SID marker path
|
|
8
|
+
# (computed by `runtime_sid_path()` in lib/runtime-sid.sh).
|
|
9
|
+
# 3. Hook emits 0 bytes on stdout (ADR-045 Pattern 1: side-effect-only,
|
|
10
|
+
# silent-on-pass — no context budget burn per tool call).
|
|
11
|
+
# 4. Hook always exits 0 (fail-open — never block a tool call on
|
|
12
|
+
# marker write).
|
|
13
|
+
# 5. Empty session_id -> hook is a no-op (marker not touched).
|
|
14
|
+
# 6. Subsequent invocations OVERWRITE the marker (so a subprocess
|
|
15
|
+
# tool call replaces the orchestrator's SID with the subprocess's
|
|
16
|
+
# SID for the duration of the subprocess; the orchestrator's
|
|
17
|
+
# next tool call after subprocess exit overwrites it back).
|
|
18
|
+
#
|
|
19
|
+
# Per feedback_behavioural_tests.md (P081): tests assert the hook's
|
|
20
|
+
# observable effects (marker contents, stdout bytes, exit code) — NOT
|
|
21
|
+
# the source content of the hook script.
|
|
22
|
+
|
|
23
|
+
setup() {
|
|
24
|
+
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
25
|
+
HOOK="$SCRIPT_DIR/itil-runtime-sid-marker.sh"
|
|
26
|
+
SANDBOX_TMP=$(mktemp -d)
|
|
27
|
+
export SESSION_MARKER_DIR="$SANDBOX_TMP"
|
|
28
|
+
MARKER_PATH="$SANDBOX_TMP/itil-runtime-sid.current"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
teardown() {
|
|
32
|
+
rm -rf "$SANDBOX_TMP"
|
|
33
|
+
unset SESSION_MARKER_DIR
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Helper: invoke the hook with a JSON stdin payload.
|
|
37
|
+
fire_hook() {
|
|
38
|
+
local json="$1"
|
|
39
|
+
echo "$json" | bash "$HOOK"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@test "hook writes session_id to runtime-SID marker" {
|
|
43
|
+
expected_uuid="aaaaaaaa-1111-2222-3333-444444444444"
|
|
44
|
+
fire_hook "{\"session_id\":\"$expected_uuid\",\"tool_name\":\"Bash\"}"
|
|
45
|
+
[ -f "$MARKER_PATH" ]
|
|
46
|
+
[ "$(cat "$MARKER_PATH")" = "$expected_uuid" ]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@test "hook is silent on stdout (ADR-045 Pattern 1)" {
|
|
50
|
+
expected_uuid="bbbbbbbb-1111-2222-3333-444444444444"
|
|
51
|
+
output=$(fire_hook "{\"session_id\":\"$expected_uuid\",\"tool_name\":\"Bash\"}")
|
|
52
|
+
[ -z "$output" ]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@test "hook exits 0 on success" {
|
|
56
|
+
expected_uuid="cccccccc-1111-2222-3333-444444444444"
|
|
57
|
+
echo "{\"session_id\":\"$expected_uuid\",\"tool_name\":\"Bash\"}" | bash "$HOOK"
|
|
58
|
+
[ "$?" -eq 0 ]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@test "hook overwrites prior marker on subsequent invocation" {
|
|
62
|
+
first_uuid="dddddddd-1111-2222-3333-444444444444"
|
|
63
|
+
second_uuid="eeeeeeee-1111-2222-3333-444444444444"
|
|
64
|
+
fire_hook "{\"session_id\":\"$first_uuid\",\"tool_name\":\"Bash\"}"
|
|
65
|
+
[ "$(cat "$MARKER_PATH")" = "$first_uuid" ]
|
|
66
|
+
fire_hook "{\"session_id\":\"$second_uuid\",\"tool_name\":\"Write\"}"
|
|
67
|
+
[ "$(cat "$MARKER_PATH")" = "$second_uuid" ]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@test "hook is a no-op when session_id is empty" {
|
|
71
|
+
fire_hook "{\"tool_name\":\"Bash\"}"
|
|
72
|
+
[ ! -f "$MARKER_PATH" ]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@test "hook is a no-op when stdin is not valid JSON" {
|
|
76
|
+
echo "not-json-at-all" | bash "$HOOK"
|
|
77
|
+
[ "$?" -eq 0 ]
|
|
78
|
+
[ ! -f "$MARKER_PATH" ]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@test "hook fail-open on jq absent (graceful degradation)" {
|
|
82
|
+
# Simulate jq absent by making PATH not include any jq binary.
|
|
83
|
+
expected_uuid="ffffffff-1111-2222-3333-444444444444"
|
|
84
|
+
result=$(echo "{\"session_id\":\"$expected_uuid\",\"tool_name\":\"Bash\"}" | env PATH="/usr/bin:/bin" bash "$HOOK"; echo "EXIT:$?")
|
|
85
|
+
# Either jq is in /usr/bin (fine — marker written), or it's absent
|
|
86
|
+
# (hook should still exit 0 without crashing). The exit-0 contract
|
|
87
|
+
# is the load-bearing assertion; marker presence is a bonus when
|
|
88
|
+
# jq is available.
|
|
89
|
+
[[ "$result" == *"EXIT:0"* ]]
|
|
90
|
+
}
|
|
@@ -176,3 +176,83 @@ mark_announced() {
|
|
|
176
176
|
[[ "$output" != *"$middle_uuid"* ]]
|
|
177
177
|
[[ "$output" == *"EXIT:0"* ]]
|
|
178
178
|
}
|
|
179
|
+
|
|
180
|
+
# --- Behavioural contract: runtime-SID marker (P142 Phase 4) ---
|
|
181
|
+
#
|
|
182
|
+
# Phase 3 mtime-based within-system selection introduced a regression
|
|
183
|
+
# in orchestrator main turns AFTER subprocess dispatch: subprocess
|
|
184
|
+
# announce markers have NEWER mtime than the orchestrator's, so the
|
|
185
|
+
# helper picked the subprocess SID while the runtime hook stdin still
|
|
186
|
+
# contained the orchestrator SID — marker landed under the wrong UUID,
|
|
187
|
+
# create-gate (P119) denied. Mirror failure mode would fire in
|
|
188
|
+
# subprocess context if the priority list were re-ordered to favour
|
|
189
|
+
# orchestrator-only announce systems (no pure-helper algorithm can
|
|
190
|
+
# distinguish "running in orchestrator main turn" from "running in
|
|
191
|
+
# subprocess" by filesystem state alone).
|
|
192
|
+
#
|
|
193
|
+
# Phase 4 structural fix: a new PreToolUse hook
|
|
194
|
+
# (`itil-runtime-sid-marker.sh`) writes the runtime stdin session_id
|
|
195
|
+
# to a per-machine marker on every tool call. The helper reads this
|
|
196
|
+
# marker FIRST as the authoritative current-session SID, falling back
|
|
197
|
+
# to the existing announce-marker priority logic when the marker is
|
|
198
|
+
# absent (cold path — first tool call of a session, before any
|
|
199
|
+
# PreToolUse fires).
|
|
200
|
+
#
|
|
201
|
+
# Sandbox path: when SESSION_MARKER_DIR is set (test override), the
|
|
202
|
+
# runtime marker lives at `${SESSION_MARKER_DIR}/itil-runtime-sid.current`
|
|
203
|
+
# — a single fixed filename, no per-user/per-project scoping. The
|
|
204
|
+
# scoping suffix used in prod (`-${USER}-${proj_hash}`) is irrelevant
|
|
205
|
+
# under sandbox because every test gets a fresh SANDBOX_TMP.
|
|
206
|
+
|
|
207
|
+
@test "runtime-SID marker present: helper returns marker contents over newer announce markers" {
|
|
208
|
+
runtime_uuid="dddddddd-dddd-dddd-dddd-dddddddddddd"
|
|
209
|
+
decoy_uuid="eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"
|
|
210
|
+
# Decoy: an architect-announced marker with a NEWER mtime than the
|
|
211
|
+
# runtime marker. The Phase 3 helper would have picked decoy_uuid;
|
|
212
|
+
# the Phase 4 helper picks runtime_uuid because the runtime-SID
|
|
213
|
+
# marker is authoritative.
|
|
214
|
+
printf '%s' "$runtime_uuid" > "$SANDBOX_TMP/itil-runtime-sid.current"
|
|
215
|
+
sleep 1
|
|
216
|
+
mark_announced "architect" "$decoy_uuid"
|
|
217
|
+
output=$(discover)
|
|
218
|
+
[[ "$output" == *"$runtime_uuid"* ]]
|
|
219
|
+
[[ "$output" != *"$decoy_uuid"* ]]
|
|
220
|
+
[[ "$output" == *"EXIT:0"* ]]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@test "runtime-SID marker empty: helper falls back to announce-marker priority" {
|
|
224
|
+
expected_uuid="ffffffff-ffff-ffff-ffff-ffffffffffff"
|
|
225
|
+
mark_announced "architect" "$expected_uuid"
|
|
226
|
+
# Empty runtime marker (zero-byte file) — helper must NOT return
|
|
227
|
+
# the empty contents; it must fall through to the announce-marker
|
|
228
|
+
# scrape. Empty marker can occur if the hook ran with empty
|
|
229
|
+
# session_id stdin (test harness, hook self-test) and would still
|
|
230
|
+
# leave the file at zero bytes per the hook's empty-input fail-open.
|
|
231
|
+
: > "$SANDBOX_TMP/itil-runtime-sid.current"
|
|
232
|
+
output=$(discover)
|
|
233
|
+
[[ "$output" == *"$expected_uuid"* ]]
|
|
234
|
+
[[ "$output" == *"EXIT:0"* ]]
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@test "runtime-SID marker absent (cold path): helper uses announce-marker priority unchanged" {
|
|
238
|
+
expected_uuid="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
239
|
+
mark_announced "architect" "$expected_uuid"
|
|
240
|
+
# No runtime marker created — cold path. Helper falls back to
|
|
241
|
+
# existing Phase 3 announce-marker priority. This test pins the
|
|
242
|
+
# backwards-compat contract: sessions whose first tool call hasn't
|
|
243
|
+
# yet fired the PreToolUse hook still discover their SID via the
|
|
244
|
+
# announce-marker fallback (the priority list is preserved as-is).
|
|
245
|
+
output=$(discover)
|
|
246
|
+
[[ "$output" == *"$expected_uuid"* ]]
|
|
247
|
+
[[ "$output" == *"EXIT:0"* ]]
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@test "env var beats runtime-SID marker (env-var fast path preserved)" {
|
|
251
|
+
env_uuid="11111111-aaaa-bbbb-cccc-222222222222"
|
|
252
|
+
marker_uuid="22222222-aaaa-bbbb-cccc-333333333333"
|
|
253
|
+
printf '%s' "$marker_uuid" > "$SANDBOX_TMP/itil-runtime-sid.current"
|
|
254
|
+
output=$(CLAUDE_SESSION_ID="$env_uuid" SESSION_MARKER_DIR="$SANDBOX_TMP" bash -c "source '$HELPER'; get_current_session_id; echo \"EXIT:\$?\"")
|
|
255
|
+
[[ "$output" == *"$env_uuid"* ]]
|
|
256
|
+
[[ "$output" != *"$marker_uuid"* ]]
|
|
257
|
+
[[ "$output" == *"EXIT:0"* ]]
|
|
258
|
+
}
|
package/package.json
CHANGED
|
@@ -271,46 +271,7 @@ Before creating, search existing problems for similar issues. The user may not k
|
|
|
271
271
|
|
|
272
272
|
**Why a helper instead of inline `${CLAUDE_SESSION_ID:-default}`**: the agent's process does NOT export `CLAUDE_SESSION_ID` today; the hook side reads `session_id` from its stdin JSON payload (per the Claude Code PreToolUse contract). The prior fallback wrote the marker under `default` while the hook checked the real UUID — mismatch caused the Write deny on every first ticket of a session until the agent ad-hoc scraped a UUID-bearing marker. The helper canonicalises that scrape so every agent context discovers the SID the same way. P124.
|
|
273
273
|
|
|
274
|
-
|
|
275
|
-
**Recovery if hook denial persists** (P144 / ADR-048 — auto-supersedes when P142 ships):
|
|
276
|
-
|
|
277
|
-
The P124 helper has a Phase 3 regression in orchestrator sessions that have dispatched subprocesses: it sometimes returns a subprocess SID instead of the orchestrator SID, while the runtime hook stdin still contains the orchestrator SID. The marker lands under the wrong UUID; the next `Write` is denied even though `mark_step2_complete` succeeded. The hook deny message includes a `(Helper succeeded but SID mismatch detected — see manage-problem SKILL.md Step 2 substep 7.)` pointer when this signal is observable.
|
|
278
|
-
|
|
279
|
-
**Gate-misfire signal** — recovery applies ONLY when ALL three conditions hold:
|
|
280
|
-
1. The agent is **already executing** `/wr-itil:manage-problem` Step 2 in this turn (i.e., the SKILL contract has just ordered the grep for THIS ticket creation — not a marker carried over from a prior unrelated invocation in the same session).
|
|
281
|
-
2. `mark_step2_complete` succeeded (the helper exited zero — no SID-discovery error).
|
|
282
|
-
3. The subsequent `Write` to the new `.<status>.md` file is denied by the P119 hook.
|
|
283
|
-
|
|
284
|
-
Routine creation flow does NOT match these conditions and MUST continue through the standard `Write` path. The recovery is mechanical (deterministic from the gate-misfire signal — no `AskUserQuestion` required, per ADR-044's framework-mediated surface catalog extension).
|
|
285
|
-
|
|
286
|
-
**First-tier recovery — announce-marker scrape**:
|
|
287
|
-
|
|
288
|
-
```bash
|
|
289
|
-
# Discover the orchestrator session UUID via the most-reliable per-session announce marker.
|
|
290
|
-
# The orchestrator SID is what the runtime hook stdin contains in the common subprocess case.
|
|
291
|
-
sid=$(ls -t /tmp/itil-assistant-gate-announced-* 2>/dev/null | head -1 | sed 's|.*itil-assistant-gate-announced-||')
|
|
292
|
-
[ -n "$sid" ] && touch "/tmp/manage-problem-grep-${sid}"
|
|
293
|
-
# Retry the Write.
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
**Second-tier recovery — python3-via-Bash file-write** (2026-04-29 evidence: runtime hook stdin SID may not be in any announce-marker class; first-tier returns the orchestrator SID, but the runtime SID is a different per-Write SID surfaced only by `architect-reviewed-*` mtime, not by any announce-marker):
|
|
297
|
-
|
|
298
|
-
```bash
|
|
299
|
-
# The hook is PreToolUse:Write; python3-in-Bash is not a Write tool call,
|
|
300
|
-
# so the hook never fires. Use only when first-tier fails.
|
|
301
|
-
python3 -c "from pathlib import Path; Path('docs/problems/<NNN>-<title>.open.md').write_text('''<file body>''')"
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
**Audit-trail-preservation test** — the second-tier procedure is sanctioned ONLY in the audit-trail-preserved branch:
|
|
305
|
-
|
|
306
|
-
- ✅ **Audit-trail-preserved**: the agent is currently executing `/wr-itil:manage-problem` Step 2 for THIS ticket creation (gate-misfire signal condition 1), AND any `/tmp/manage-problem-grep-*` marker exists. The skill flow itself is the just-ran-grep witness; the marker existence corroborates it.
|
|
307
|
-
- ❌ **Audit-trail-violated**: the agent is NOT in `/wr-itil:manage-problem` Step 2 for this ticket creation, OR no marker exists for any SID. Routine first-creation flow MUST hit the gate; the recovery procedure does NOT apply.
|
|
308
|
-
|
|
309
|
-
**Anti-pattern bound** — the loose reading "any marker from any earlier `manage-problem` invocation in this session" would let the recovery procedure apply to a fresh ticket creation that happens to reuse a stale marker from a prior unrelated invocation. That is the P131 anti-pattern surface (gate state as a workaround target instead of as a directive). The bound holds because the recovery is invoked from inside an active manage-problem flow where Step 2 has just been ordered for THIS ticket, AND the python3-via-Bash branch is named in this substep so its invocation is itself audit-trail-emitting.
|
|
310
|
-
|
|
311
|
-
**DO NOT brute-force-touch markers for every announced UUID.** That pattern (139 markers in one session, 2026-04-28 P144 evidence) satisfies the marker shape while gaming the audit trail the marker is supposed to record. The user has explicitly rejected this pattern: *"WTF? Why did you bypass instead of using the skill?"* (P144 driver correction). Brute-forcing markers for SIDs that did not run Step 2 is the canonical bypass — the recovery procedure above is the canonical use of the skill.
|
|
312
|
-
|
|
313
|
-
**Cross-references**: P124 (helper Phase 3 regression — driver of the misfire); P142 (P124 Phase 4 — structural fix that auto-supersedes this recovery when shipped); P131 (gate-exclusions-as-write-permission — adjacent anti-pattern family); ADR-048 (sanctioning + scoping ADR); ADR-009 (gate marker lifecycle); ADR-044 (mechanical-decision framework-mediated surface catalog).
|
|
274
|
+
**Phase 4 (P142 / ADR-050)** — the helper now reads the runtime stdin `session_id` from a per-machine marker written by the `itil-runtime-sid-marker.sh` PreToolUse hook on every tool call. Because every Bash call that sources the helper is itself a PreToolUse:Bash event, the marker the helper reads was written moments earlier with the same `session_id` the runtime Write hook will see — so SID-mismatch denial is structurally impossible in the routine flow. The Phase 3 announce-marker priority logic is preserved as cold-path fallback (first tool call of a session, before any PreToolUse fires).
|
|
314
275
|
|
|
315
276
|
**Search strategy**: Search problem filenames AND file content. A match on the filename (kebab-case title) or the Description/Symptoms sections counts. Cast a wide net — false positives are cheap (user chooses), but false negatives mean duplicate problems.
|
|
316
277
|
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
#
|
|
3
|
-
# packages/itil/skills/manage-problem/test/manage-problem-p119-recovery-path.bats
|
|
4
|
-
#
|
|
5
|
-
# Behavioural tests for manage-problem Step 2 substep 7's P119 hook-misfire
|
|
6
|
-
# recovery procedure (P144 / ADR-048).
|
|
7
|
-
#
|
|
8
|
-
# Step 2 substep 7 documents a two-tier recovery for the case where
|
|
9
|
-
# `mark_step2_complete` succeeded but the P119 PreToolUse:Write hook still
|
|
10
|
-
# denies the new ticket Write — typically because the P124 helper returned
|
|
11
|
-
# a subprocess SID instead of the orchestrator SID (ADR-048 Phase 3
|
|
12
|
-
# regression). Without documented recovery, the agent reaches for the
|
|
13
|
-
# brute-force-touch-every-marker anti-pattern (139-marker incident,
|
|
14
|
-
# 2026-04-28). User correction was emphatic: "WTF? Why did you bypass
|
|
15
|
-
# instead of using the skill?"
|
|
16
|
-
#
|
|
17
|
-
# This bats fixes the contract:
|
|
18
|
-
# - Sub-block names the gate-misfire signal (active flow + helper-succeeded
|
|
19
|
-
# + Write-denied conjunction).
|
|
20
|
-
# - Two-tier procedure named (first-tier announce-marker scrape; second-tier
|
|
21
|
-
# python3-via-Bash file-write).
|
|
22
|
-
# - Audit-trail-preservation test as the gate-on-sanctioning rule.
|
|
23
|
-
# - Anti-pattern call-out ("DO NOT brute-force") in durable form.
|
|
24
|
-
# - ADR-048, P124, P142 cross-references.
|
|
25
|
-
# - <!-- supersedes-when: P142 ships --> HTML comment for cleanup
|
|
26
|
-
# discoverability.
|
|
27
|
-
#
|
|
28
|
-
# tdd-review: structural-permitted (justification: skill behavioural
|
|
29
|
-
# harness pending P012 + P081 Phase 2; SKILL.md contract assertions
|
|
30
|
-
# bridge until then; expected to migrate to behavioural form once
|
|
31
|
-
# the harness exists).
|
|
32
|
-
#
|
|
33
|
-
# @problem P144
|
|
34
|
-
# @adr ADR-048 (Documented recovery from gate misfire is the prescribed surface, not bypass)
|
|
35
|
-
# @adr ADR-009 (gate marker lifecycle)
|
|
36
|
-
# @adr ADR-013 Rule 5 (policy-authorised silent proceed)
|
|
37
|
-
# @adr ADR-022 (problem lifecycle status suffixes)
|
|
38
|
-
# @adr ADR-037 / P081 (testing strategy — bridge during harness build)
|
|
39
|
-
# @adr ADR-038 (progressive disclosure — deny message terse)
|
|
40
|
-
# @adr ADR-044 (decision-delegation — recovery is mechanical)
|
|
41
|
-
# @jtbd JTBD-001 / JTBD-101 / JTBD-201
|
|
42
|
-
|
|
43
|
-
SKILL_FILE="${BATS_TEST_DIRNAME}/../SKILL.md"
|
|
44
|
-
|
|
45
|
-
setup() {
|
|
46
|
-
[ -f "$SKILL_FILE" ]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
# Bound the search to Step 2 substep 7 region (between Step 2 heading and Step 3 heading).
|
|
50
|
-
step2_text() {
|
|
51
|
-
awk '/^### 2\. /,/^### 3\. /' "$SKILL_FILE"
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
# ── Recovery sub-block presence ─────────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
@test "Step 2 SKILL.md contains a Recovery sub-block for hook-denial misfire" {
|
|
57
|
-
run step2_text
|
|
58
|
-
[ "$status" -eq 0 ]
|
|
59
|
-
[[ "$output" == *"Recovery"* ]]
|
|
60
|
-
[[ "$output" == *"hook denial"* ]] || [[ "$output" == *"hook still denies"* ]] || [[ "$output" == *"deny"* ]]
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
# ── Gate-misfire signal definition ──────────────────────────────────────────
|
|
64
|
-
|
|
65
|
-
@test "Step 2 SKILL.md names the gate-misfire signal precondition (active manage-problem flow)" {
|
|
66
|
-
run step2_text
|
|
67
|
-
[ "$status" -eq 0 ]
|
|
68
|
-
# The signal requires that the agent is already executing manage-problem
|
|
69
|
-
# Step 2 in the current turn — not just any prior session marker.
|
|
70
|
-
[[ "$output" == *"already executing"* ]] || [[ "$output" == *"active"* ]] || [[ "$output" == *"this turn"* ]]
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
@test "Step 2 SKILL.md names mark_step2_complete success as part of the misfire signal" {
|
|
74
|
-
run step2_text
|
|
75
|
-
[ "$status" -eq 0 ]
|
|
76
|
-
[[ "$output" == *"mark_step2_complete"* ]]
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
# ── Two-tier procedure ──────────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
@test "Step 2 SKILL.md names the first-tier recovery (announce-marker scrape)" {
|
|
82
|
-
run step2_text
|
|
83
|
-
[ "$status" -eq 0 ]
|
|
84
|
-
[[ "$output" == *"first-tier"* ]] || [[ "$output" == *"First-tier"* ]]
|
|
85
|
-
[[ "$output" == *"itil-assistant-gate-announced"* ]]
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
@test "Step 2 SKILL.md names the second-tier recovery (python3-via-Bash file-write)" {
|
|
89
|
-
run step2_text
|
|
90
|
-
[ "$status" -eq 0 ]
|
|
91
|
-
[[ "$output" == *"second-tier"* ]] || [[ "$output" == *"Second-tier"* ]]
|
|
92
|
-
[[ "$output" == *"python3"* ]]
|
|
93
|
-
[[ "$output" == *"Bash"* ]]
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
# ── Audit-trail-preservation test ───────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
@test "Step 2 SKILL.md states the audit-trail-preservation test as the sanctioning rule" {
|
|
99
|
-
run step2_text
|
|
100
|
-
[ "$status" -eq 0 ]
|
|
101
|
-
[[ "$output" == *"audit-trail"* ]] || [[ "$output" == *"audit trail"* ]]
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
@test "Step 2 SKILL.md names the anti-pattern bound (any-marker-anywhere is NOT the test)" {
|
|
105
|
-
# Architect advisory: the bound must rule out the loose "any marker from any
|
|
106
|
-
# earlier invocation in this session" reading — that's the P131 surface.
|
|
107
|
-
run step2_text
|
|
108
|
-
[ "$status" -eq 0 ]
|
|
109
|
-
[[ "$output" == *"this ticket"* ]] || [[ "$output" == *"THIS ticket"* ]]
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
# ── Anti-pattern call-out (durable surface) ─────────────────────────────────
|
|
113
|
-
|
|
114
|
-
@test "Step 2 SKILL.md contains the explicit DO-NOT-brute-force anti-pattern wording" {
|
|
115
|
-
run step2_text
|
|
116
|
-
[ "$status" -eq 0 ]
|
|
117
|
-
[[ "$output" == *"DO NOT brute-force"* ]] || [[ "$output" == *"do not brute-force"* ]] || [[ "$output" == *"Do not brute-force"* ]]
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
@test "Step 2 SKILL.md cites the 2026-04-28 user correction context for the anti-pattern" {
|
|
121
|
-
run step2_text
|
|
122
|
-
[ "$status" -eq 0 ]
|
|
123
|
-
[[ "$output" == *"P144"* ]]
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
# ── Cross-references ────────────────────────────────────────────────────────
|
|
127
|
-
|
|
128
|
-
@test "Step 2 SKILL.md cites ADR-048 for the recovery procedure scope" {
|
|
129
|
-
run step2_text
|
|
130
|
-
[ "$status" -eq 0 ]
|
|
131
|
-
[[ "$output" == *"ADR-048"* ]]
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
@test "Step 2 SKILL.md cites P124 as the helper-bug source" {
|
|
135
|
-
run step2_text
|
|
136
|
-
[ "$status" -eq 0 ]
|
|
137
|
-
[[ "$output" == *"P124"* ]]
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
@test "Step 2 SKILL.md cites P142 as the structural fix (supersession trigger)" {
|
|
141
|
-
run step2_text
|
|
142
|
-
[ "$status" -eq 0 ]
|
|
143
|
-
[[ "$output" == *"P142"* ]]
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
# ── Supersession comment (CI-enforced cleanup invariant) ────────────────────
|
|
147
|
-
|
|
148
|
-
@test "Step 2 SKILL.md carries the supersedes-when HTML comment so cleanup is discoverable" {
|
|
149
|
-
# ADR-048 Reassessment Criteria: when P142's resolution ADR is accepted,
|
|
150
|
-
# this comment must be removed from SKILL.md source. Today the comment
|
|
151
|
-
# is present and this assertion passes; once P142 lands, the cleanup
|
|
152
|
-
# signal lives here.
|
|
153
|
-
run step2_text
|
|
154
|
-
[ "$status" -eq 0 ]
|
|
155
|
-
[[ "$output" == *"supersedes-when"* ]]
|
|
156
|
-
[[ "$output" == *"P142"* ]]
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
# ── Mechanical (no-AskUserQuestion) per ADR-044 ─────────────────────────────
|
|
160
|
-
|
|
161
|
-
@test "Step 2 SKILL.md states the recovery is mechanical (no AskUserQuestion required)" {
|
|
162
|
-
run step2_text
|
|
163
|
-
[ "$status" -eq 0 ]
|
|
164
|
-
[[ "$output" == *"mechanical"* ]] || [[ "$output" == *"ADR-044"* ]]
|
|
165
|
-
}
|