@windyroad/itil 0.30.2-preview.317 → 0.30.3-preview.319

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "wr-itil",
3
- "version": "0.30.2",
3
+ "version": "0.30.3",
4
4
  "description": "ITIL-aligned IT service management for Claude Code"
5
5
  }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/../scripts/plugin-exercise-index.sh" "$@"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/../scripts/skill-invocations.sh" "$@"
package/hooks/hooks.json CHANGED
@@ -52,7 +52,8 @@
52
52
  }
53
53
  ],
54
54
  "Stop": [
55
- { "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-assistant-output-review.sh" }] }
55
+ { "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-assistant-output-review.sh" }] },
56
+ { "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-mid-loop-ask-detect.sh" }] }
56
57
  ]
57
58
  }
58
59
  }
@@ -0,0 +1,142 @@
1
+ #!/bin/bash
2
+ # P132 Phase 2b — wr-itil Stop hook.
3
+ #
4
+ # Detects orchestrator main-turn `AskUserQuestion` calls fired mid-loop
5
+ # inside `/wr-itil:work-problems` — the regression class P132 captures
6
+ # (2026-05-17 reopen evidence: orchestrator asked iter-target selection
7
+ # between iters 3 and 4; halted the AFK loop for hours of user time).
8
+ #
9
+ # Detection signal (all three must hold):
10
+ # 1. Last assistant turn in the transcript contains an
11
+ # AskUserQuestion tool_use.
12
+ # 2. Earlier in the transcript an assistant message issued a `Skill`
13
+ # tool_use to `wr-itil:work-problems` (orchestrator activation).
14
+ # 3. No `ALL_DONE` or `## Work Problems Summary` terminal marker has
15
+ # been emitted since the orchestrator activated (the orchestrator
16
+ # is still mid-loop, not in its post-loop wrap-up).
17
+ #
18
+ # When all three match the hook emits a structured `stopReason`
19
+ # advisory citing P130 + the Mid-loop ask discipline subsection of
20
+ # `packages/itil/skills/work-problems/SKILL.md`. Advisory ONLY — the
21
+ # hook NEVER blocks. The next assistant turn reads the stopReason in
22
+ # its context and self-corrects (queue the question to
23
+ # outstanding_questions and continue iterating).
24
+ #
25
+ # Mirrors the sibling `itil-assistant-output-review.sh` Stop hook
26
+ # precedent (P085 prose-ask detection) on transcript-extraction shape
27
+ # and stopReason emit format.
28
+ #
29
+ # References:
30
+ # P132 — this hook (Phase 2b structural enforcement).
31
+ # P130 — orchestrator presence-aware dispatch; named in advisory.
32
+ # ADR-013 — Rule 1 (interactive default) + Rule 6 (AFK fail-safe).
33
+ # ADR-044 — framework-resolution boundary; named in advisory.
34
+ # ADR-045 — hook injection budget; advisory honour-system band.
35
+ # ADR-052 — behavioural tests default.
36
+ # ADR-005 — plugin testing strategy.
37
+
38
+ # Per-surface configuration. Extending coverage to other orchestrators
39
+ # (run-retro Step 4b Stage 1, /install-updates Step 6a) is a future
40
+ # Phase 2c/2d variant — copy this hook + retarget these two vars.
41
+ ORCHESTRATOR_SKILL="wr-itil:work-problems"
42
+ TERMINAL_MARKER_RE='ALL_DONE|## Work Problems Summary'
43
+
44
+ INPUT=$(cat)
45
+ TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null || echo "")
46
+
47
+ # Graceful fallback: no transcript_path or file missing means nothing
48
+ # to inspect. Exit clean — hook is advisory, never blocking.
49
+ if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
50
+ exit 0
51
+ fi
52
+
53
+ # Empty transcript → silent exit.
54
+ [ -s "$TRANSCRIPT_PATH" ] || exit 0
55
+
56
+ # Identify the LAST assistant turn. Each transcript line is a JSON
57
+ # object {type, message, ...}; assistant turns have type=="assistant".
58
+ # Malformed lines are silently tolerated by jq's `-c` + `2>/dev/null`.
59
+ LAST_ASSISTANT=$(grep -E '"type"[[:space:]]*:[[:space:]]*"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -n 1 || true)
60
+ if [ -z "$LAST_ASSISTANT" ]; then
61
+ exit 0
62
+ fi
63
+
64
+ # Signal 1: last assistant turn contains AskUserQuestion tool_use.
65
+ if ! echo "$LAST_ASSISTANT" | jq -e '
66
+ .message.content
67
+ | if type == "array" then
68
+ map(select(.type == "tool_use" and .name == "AskUserQuestion")) | length > 0
69
+ else false end
70
+ ' >/dev/null 2>&1; then
71
+ exit 0
72
+ fi
73
+
74
+ # Signal 2: any earlier assistant message contains a Skill tool_use to
75
+ # the configured orchestrator skill. Iterate every assistant line; on
76
+ # the first match set ORCH_LINE_NUM to its 1-based line index.
77
+ ORCH_LINE_NUM=0
78
+ LINE_NUM=0
79
+ while IFS= read -r line || [ -n "$line" ]; do
80
+ LINE_NUM=$((LINE_NUM + 1))
81
+ # Skip non-assistant + malformed lines.
82
+ echo "$line" | grep -qE '"type"[[:space:]]*:[[:space:]]*"assistant"' || continue
83
+ echo "$line" | jq -e . >/dev/null 2>&1 || continue
84
+ if echo "$line" | jq -e --arg s "$ORCHESTRATOR_SKILL" '
85
+ .message.content
86
+ | if type == "array" then
87
+ map(select(.type == "tool_use" and .name == "Skill" and (.input.skill // .input.skillName // "") == $s)) | length > 0
88
+ else false end
89
+ ' >/dev/null 2>&1; then
90
+ ORCH_LINE_NUM=$LINE_NUM
91
+ break
92
+ fi
93
+ done < "$TRANSCRIPT_PATH"
94
+
95
+ if [ "$ORCH_LINE_NUM" -eq 0 ]; then
96
+ exit 0
97
+ fi
98
+
99
+ # Signal 3: no terminal marker emitted AFTER orchestrator activation.
100
+ # Scan lines strictly after ORCH_LINE_NUM up to the line BEFORE the
101
+ # last assistant turn (the AskUserQuestion turn itself shouldn't be
102
+ # considered a terminal-marker source — its content is a tool_use, not
103
+ # the orchestrator's final summary). If the AskUserQuestion turn IS
104
+ # the same turn that emits ALL_DONE, that's structurally implausible
105
+ # (you don't ask while wrapping up) — the conservative read is to
106
+ # scan up to and including all prior turns.
107
+ TOTAL_LINES=$(wc -l < "$TRANSCRIPT_PATH" | tr -d ' ')
108
+ # Slice: from ORCH_LINE_NUM+1 to TOTAL_LINES-1 (exclude the final
109
+ # assistant turn carrying the AskUserQuestion). Edge case: if the
110
+ # orchestrator-activation line IS the last-assistant line, the
111
+ # AskUserQuestion would be in the same turn as the Skill call —
112
+ # impossible in practice. In that degenerate case the slice is empty
113
+ # and no terminal marker is seen → advise.
114
+ SLICE_END=$((TOTAL_LINES - 1))
115
+ TERMINAL_SEEN="no"
116
+ if [ "$SLICE_END" -ge "$ORCH_LINE_NUM" ]; then
117
+ if sed -n "$((ORCH_LINE_NUM + 1)),${SLICE_END}p" "$TRANSCRIPT_PATH" \
118
+ | grep -qE "$TERMINAL_MARKER_RE" 2>/dev/null; then
119
+ TERMINAL_SEEN="yes"
120
+ fi
121
+ fi
122
+
123
+ if [ "$TERMINAL_SEEN" = "yes" ]; then
124
+ exit 0
125
+ fi
126
+
127
+ # All three signals present → emit advisory stopReason. The next
128
+ # assistant turn reads this in its context and self-corrects.
129
+ # Voice-tone target ~600 bytes; ADR-045 honour-system slack < 1000.
130
+ jq -n '{
131
+ stopReason: (
132
+ "MID-LOOP ASK DETECTED: AskUserQuestion fired inside /wr-itil:work-problems orchestrator main turn. " +
133
+ "Per P130 + Mid-loop ask discipline subsection of work-problems SKILL.md, mid-loop AskUserQuestion is " +
134
+ "forbidden except at framework-prescribed halt points (Step 0 session-continuity, Step 2.5/2.5b loop-end emit, " +
135
+ "Step 6.5 above-appetite Rule 5 / CI failure, Step 6.75 dirty halt). " +
136
+ "If at a halt point, this advisory is inapplicable. " +
137
+ "If mid-loop, queue the question to outstanding_questions and continue iterating. " +
138
+ "See ADR-044 framework-resolution boundary."
139
+ )
140
+ }'
141
+
142
+ exit 0
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # P132 Phase 2b: itil-mid-loop-ask-detect.sh Stop hook detects
4
+ # orchestrator main-turn AskUserQuestion calls fired mid-loop inside
5
+ # /wr-itil:work-problems — the regression class P132 captures
6
+ # (2026-05-17 reopen: orchestrator asked iter-target selection between
7
+ # iters 3 and 4; halted the loop for hours of AFK time).
8
+ #
9
+ # Detection signal:
10
+ # 1. Last assistant turn contains an AskUserQuestion tool_use
11
+ # 2. Earlier transcript contains a Skill tool_use to wr-itil:work-problems
12
+ # 3. No `ALL_DONE` / `## Work Problems Summary` marker has been emitted
13
+ # since the skill activation (mid-loop, not post-loop wrap)
14
+ #
15
+ # When all three match the hook emits a structured `stopReason`
16
+ # advisory citing P130 + the Mid-loop ask discipline subsection of
17
+ # work-problems SKILL.md. Advisory only — never blocks. Mirrors the
18
+ # itil-assistant-output-review.sh Stop hook precedent (P085 prose-ask
19
+ # detection) but on a different signal class.
20
+ #
21
+ # Per ADR-005 / ADR-052 — bats live under packages/<plugin>/hooks/test/
22
+ # and assert behaviour on emitted JSON, not source-content. Per
23
+ # feedback_behavioural_tests.md (P081) — no source-grep on hook text.
24
+
25
+ setup() {
26
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
27
+ HOOK="$REPO_ROOT/packages/itil/hooks/itil-mid-loop-ask-detect.sh"
28
+ TMPDIR_="$(mktemp -d)"
29
+ TRANSCRIPT="$TMPDIR_/transcript.jsonl"
30
+ }
31
+
32
+ teardown() {
33
+ rm -rf "$TMPDIR_"
34
+ }
35
+
36
+ # Helper: emit the JSONL transcript line for an assistant turn
37
+ # containing a Skill tool_use to wr-itil:work-problems.
38
+ emit_orchestrator_activation() {
39
+ cat <<'JSON'
40
+ {"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","name":"Skill","input":{"skill":"wr-itil:work-problems"}}]}}
41
+ JSON
42
+ }
43
+
44
+ # Helper: emit a benign assistant text turn (no tool_use).
45
+ emit_text_turn() {
46
+ local text="$1"
47
+ printf '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":%s}]}}\n' \
48
+ "$(printf '%s' "$text" | jq -Rs .)"
49
+ }
50
+
51
+ # Helper: emit an assistant turn containing an AskUserQuestion tool_use.
52
+ emit_ask_turn() {
53
+ local header="${1:-Next problem}"
54
+ cat <<JSON
55
+ {"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","name":"AskUserQuestion","input":{"questions":[{"question":"Pick next iter","header":"${header}","options":[{"label":"A","description":"x"},{"label":"B","description":"y"}]}]}}]}}
56
+ JSON
57
+ }
58
+
59
+ run_hook() {
60
+ echo "{\"session_id\":\"mid-loop-test\",\"transcript_path\":$(printf '%s' "$TRANSCRIPT" | jq -Rs .)}" | bash "$HOOK"
61
+ }
62
+
63
+ # --- Positive detection ---
64
+
65
+ @test "detect: orchestrator activation + final AskUserQuestion + no terminal marker emits stopReason" {
66
+ {
67
+ printf '{"type":"user","message":{"role":"user","content":"/wr-itil:work-problems"}}\n'
68
+ emit_orchestrator_activation
69
+ emit_text_turn "Iter 1 complete. Dispatching iter 2."
70
+ emit_text_turn "Iter 2 complete. Dispatching iter 3."
71
+ emit_ask_turn "Pick next iter"
72
+ } > "$TRANSCRIPT"
73
+ run run_hook
74
+ [ "$status" -eq 0 ]
75
+ [[ "$output" == *"stopReason"* ]]
76
+ }
77
+
78
+ @test "detect: stopReason cites P130 + framework-prescribed halt points" {
79
+ {
80
+ printf '{"type":"user","message":{"role":"user","content":"/wr-itil:work-problems"}}\n'
81
+ emit_orchestrator_activation
82
+ emit_text_turn "Iter 1 done."
83
+ emit_ask_turn "Next problem"
84
+ } > "$TRANSCRIPT"
85
+ run run_hook
86
+ [ "$status" -eq 0 ]
87
+ [[ "$output" == *"P130"* ]]
88
+ [[ "$output" == *"halt point"* ]]
89
+ [[ "$output" == *"outstanding_questions"* ]]
90
+ }
91
+
92
+ @test "detect: stopReason cites ADR-044 framework-resolution boundary" {
93
+ {
94
+ printf '{"type":"user","message":{"role":"user","content":"/wr-itil:work-problems"}}\n'
95
+ emit_orchestrator_activation
96
+ emit_text_turn "Iter 1 done."
97
+ emit_ask_turn "Pick"
98
+ } > "$TRANSCRIPT"
99
+ run run_hook
100
+ [ "$status" -eq 0 ]
101
+ [[ "$output" == *"ADR-044"* ]]
102
+ }
103
+
104
+ # --- Negative paths: silent exit ---
105
+
106
+ @test "allow: transcript with no orchestrator activation exits silently" {
107
+ # User asked a question outside /wr-itil:work-problems context;
108
+ # AskUserQuestion in this case is unrelated to mid-loop discipline.
109
+ {
110
+ printf '{"type":"user","message":{"role":"user","content":"help me design a feature"}}\n'
111
+ emit_text_turn "Two paths come to mind."
112
+ emit_ask_turn "Design choice"
113
+ } > "$TRANSCRIPT"
114
+ run run_hook
115
+ [ "$status" -eq 0 ]
116
+ [[ "$output" != *"stopReason"* ]]
117
+ }
118
+
119
+ @test "allow: orchestrator activated then ALL_DONE emitted (post-loop wrap)" {
120
+ # The orchestrator has emitted its terminal summary; subsequent
121
+ # AskUserQuestion is post-loop interactive follow-up, not mid-loop.
122
+ {
123
+ printf '{"type":"user","message":{"role":"user","content":"/wr-itil:work-problems"}}\n'
124
+ emit_orchestrator_activation
125
+ emit_text_turn "Iter 1 complete."
126
+ emit_text_turn "## Work Problems Summary
127
+ All iters complete.
128
+ ALL_DONE"
129
+ emit_ask_turn "Follow-up question"
130
+ } > "$TRANSCRIPT"
131
+ run run_hook
132
+ [ "$status" -eq 0 ]
133
+ [[ "$output" != *"stopReason"* ]]
134
+ }
135
+
136
+ @test "allow: orchestrator activated then Work Problems Summary header emitted (post-loop)" {
137
+ {
138
+ printf '{"type":"user","message":{"role":"user","content":"/wr-itil:work-problems"}}\n'
139
+ emit_orchestrator_activation
140
+ emit_text_turn "## Work Problems Summary
141
+
142
+ Completed: 3 iters."
143
+ emit_ask_turn "Next steps?"
144
+ } > "$TRANSCRIPT"
145
+ run run_hook
146
+ [ "$status" -eq 0 ]
147
+ [[ "$output" != *"stopReason"* ]]
148
+ }
149
+
150
+ @test "allow: last assistant turn does not contain AskUserQuestion" {
151
+ {
152
+ printf '{"type":"user","message":{"role":"user","content":"/wr-itil:work-problems"}}\n'
153
+ emit_orchestrator_activation
154
+ emit_text_turn "Iter 2 dispatched."
155
+ } > "$TRANSCRIPT"
156
+ run run_hook
157
+ [ "$status" -eq 0 ]
158
+ [[ "$output" != *"stopReason"* ]]
159
+ }
160
+
161
+ @test "allow: missing transcript_path exits silently" {
162
+ run bash -c 'echo "{\"session_id\":\"sid\"}" | bash "$1"' -- "$HOOK"
163
+ [ "$status" -eq 0 ]
164
+ [[ "$output" != *"stopReason"* ]]
165
+ }
166
+
167
+ @test "allow: non-existent transcript file exits silently" {
168
+ run bash -c "echo '{\"session_id\":\"sid\",\"transcript_path\":\"/tmp/does-not-exist-$RANDOM\"}' | bash '$HOOK'"
169
+ [ "$status" -eq 0 ]
170
+ [[ "$output" != *"stopReason"* ]]
171
+ }
172
+
173
+ @test "allow: empty transcript exits silently" {
174
+ : > "$TRANSCRIPT"
175
+ run run_hook
176
+ [ "$status" -eq 0 ]
177
+ [[ "$output" != *"stopReason"* ]]
178
+ }
179
+
180
+ @test "allow: malformed JSONL lines do not crash the hook" {
181
+ {
182
+ echo "not-json"
183
+ emit_orchestrator_activation
184
+ emit_text_turn "Iter 2"
185
+ } > "$TRANSCRIPT"
186
+ run run_hook
187
+ [ "$status" -eq 0 ]
188
+ # Either silent OR stopReason — but never a crash. We assert no
189
+ # non-zero exit; the silent-vs-stopReason split is incidental here.
190
+ }
191
+
192
+ # --- Advisory budget per ADR-045 ---
193
+
194
+ @test "advisory output stays under ADR-045 800-byte honour-system band" {
195
+ {
196
+ printf '{"type":"user","message":{"role":"user","content":"/wr-itil:work-problems"}}\n'
197
+ emit_orchestrator_activation
198
+ emit_text_turn "Iter 1 done."
199
+ emit_ask_turn "Pick"
200
+ } > "$TRANSCRIPT"
201
+ run run_hook
202
+ [ "$status" -eq 0 ]
203
+ [[ "$output" == *"stopReason"* ]]
204
+ [ "${#output}" -lt 1000 ]
205
+ }
206
+
207
+ # --- Distinguishing-marker: tool_name for AskUserQuestion --
208
+
209
+ @test "detect: matches even when AskUserQuestion is intermixed with text blocks" {
210
+ {
211
+ printf '{"type":"user","message":{"role":"user","content":"/wr-itil:work-problems"}}\n'
212
+ emit_orchestrator_activation
213
+ cat <<'JSON'
214
+ {"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Two iter targets are tied on WSJF:"},{"type":"tool_use","name":"AskUserQuestion","input":{"questions":[{"question":"Pick next iter","header":"Next iter","options":[{"label":"P132","description":"x"},{"label":"P130","description":"y"}]}]}}]}}
215
+ JSON
216
+ } > "$TRANSCRIPT"
217
+ run run_hook
218
+ [ "$status" -eq 0 ]
219
+ [[ "$output" == *"stopReason"* ]]
220
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.30.2-preview.317",
3
+ "version": "0.30.3-preview.319",
4
4
  "description": "ITIL-aligned IT service management for Claude Code (problem, and future incident/change skills)",
5
5
  "bin": {
6
6
  "windyroad-itil": "./bin/install.mjs"