@windyroad/itil 0.24.1-preview.277 → 0.25.0-preview.279

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.24.1",
3
+ "version": "0.25.0",
4
4
  "description": "ITIL-aligned IT service management for Claude Code"
5
5
  }
package/README.md CHANGED
@@ -78,6 +78,7 @@ See [ADR-011](../../docs/decisions/011-manage-incident-skill.proposed.md) for th
78
78
  | Skill | Purpose |
79
79
  |-------|---------|
80
80
  | `/wr-itil:manage-problem` | Create, update, and close problem tickets through the Open → Known Error → Verifying → Closed lifecycle |
81
+ | `/wr-itil:capture-problem` | Foreground-lightweight aside-invocation variant of `manage-problem` (per ADR-032 background-capture pattern + P078 capture-on-correction) — drafts a ticket scaffold without losing the operational thread when a problem signal surfaces mid-conversation |
81
82
  | `/wr-itil:work-problem` | Pick the highest-WSJF actionable ticket and work it to completion |
82
83
  | `/wr-itil:work-problems` | AFK orchestrator — batch-work the problem backlog by WSJF priority while the user is away |
83
84
  | `/wr-itil:list-problems` | Read-only display of the open and known-error backlog sorted by WSJF |
package/hooks/hooks.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "hooks": {
3
3
  "SessionStart": [
4
- { "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/bin/check-deps.sh wr-itil wr-risk-scorer" }] }
4
+ { "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/bin/check-deps.sh wr-itil wr-risk-scorer" }] },
5
+ {
6
+ "matcher": "startup",
7
+ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-pending-questions-surface.sh" }]
8
+ }
5
9
  ],
6
10
  "UserPromptSubmit": [
7
11
  { "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-assistant-output-gate.sh" }] },
@@ -0,0 +1,125 @@
1
+ #!/bin/bash
2
+ # wr-itil — SessionStart hook (P157, ADR-032 P157 amendment, ADR-040 precedent)
3
+ #
4
+ # Surfaces accumulated `outstanding_questions` entries from the AFK loop's
5
+ # session-level queue file at .afk-run-state/outstanding-questions.jsonl
6
+ # when the user starts a new interactive session. The queue is populated
7
+ # between iters by /wr-itil:work-problems Step 5 / Step 2.5 / Step 2.5b
8
+ # (P135 Phase 3 + ADR-044 6-class taxonomy schema).
9
+ #
10
+ # Without this hook, accumulated questions persist across session boundaries
11
+ # unread when an AFK loop halts before its Step 2.5 / Step 2.5b emit fires
12
+ # (manual stop, quota exhaustion, network failure). With this hook, the
13
+ # accumulated queue surfaces deterministically on session start; the agent
14
+ # fires AskUserQuestion in batches (<=4 per call per ADR-013 Rule 1) on the
15
+ # user's first interactive turn and rewrites the queue file to remove
16
+ # resolved entries.
17
+ #
18
+ # Wired from packages/itil/hooks/hooks.json SessionStart array with
19
+ # matcher "startup" (per ADR-040 Option A). Silent exit if queue is missing,
20
+ # empty, or whitespace-only per ADR-040 Mechanism step 1.
21
+ #
22
+ # AFK-iter cross-context-leak prevention (ADR-032 line 127): when invoked
23
+ # inside a /wr-itil:work-problems iter subprocess (which inherits the
24
+ # orchestrator's queue file), the orchestrator's Step 5 dispatch block sets
25
+ # WR_SUPPRESS_PENDING_QUESTIONS=1 before each `claude -p` spawn. The hook
26
+ # self-suppresses on that env var so the orchestrator-session queue does not
27
+ # surface inside iter subprocess contexts.
28
+
29
+ set -euo pipefail
30
+
31
+ # AFK-iter self-suppress — orchestrator sets this before spawning each
32
+ # `claude -p` subprocess so the session-level queue does not leak into iter
33
+ # subprocess contexts. Only literal "1" suppresses; any other value (including
34
+ # "0", unset, empty) lets the hook proceed.
35
+ if [ "${WR_SUPPRESS_PENDING_QUESTIONS:-}" = "1" ]; then
36
+ exit 0
37
+ fi
38
+
39
+ QUEUE_FILE="${CLAUDE_PROJECT_DIR:-.}/.afk-run-state/outstanding-questions.jsonl"
40
+
41
+ # Silent-on-no-content per ADR-040 Mechanism step 1.
42
+ [ -f "$QUEUE_FILE" ] || exit 0
43
+ [ -s "$QUEUE_FILE" ] || exit 0
44
+
45
+ # ADR-044 6-class taxonomy precedence — lower rank value = higher priority.
46
+ # Strings match the JSONL schema in work-problems SKILL.md Step 5 verbatim.
47
+ rank_for_category() {
48
+ case "$1" in
49
+ deviation-approval) echo 1 ;;
50
+ direction) echo 2 ;;
51
+ one-time-override) echo 3 ;;
52
+ silent-framework) echo 4 ;;
53
+ taste) echo 5 ;;
54
+ correction-followup) echo 6 ;;
55
+ *) echo 9 ;;
56
+ esac
57
+ }
58
+
59
+ # Parse + dedupe + rank entries. Streams TSV with rank-prefix for sort.
60
+ # Tab-delimited columns: rank \t category \t ticket_id \t question_text
61
+ # (where question_text falls back to rationale for deviation-approval).
62
+ # Malformed JSON lines are silently skipped so a corrupted queue does not
63
+ # block session start.
64
+ ENTRIES_TSV=$(
65
+ while IFS= read -r line || [ -n "$line" ]; do
66
+ # Skip blank / whitespace-only lines.
67
+ [ -z "$(printf '%s' "$line" | tr -d '[:space:]')" ] && continue
68
+ # Skip non-JSON lines silently.
69
+ printf '%s' "$line" | jq -e . >/dev/null 2>&1 || continue
70
+ cat="$(printf '%s' "$line" | jq -r '.category // "unknown"')"
71
+ tid="$(printf '%s' "$line" | jq -r '.ticket_id // "—"')"
72
+ # Question text: standard-shape entries use .question; deviation-approval
73
+ # entries use .rationale (the load-bearing one-liner) since they have no
74
+ # .question field per the schema.
75
+ qtext="$(printf '%s' "$line" | jq -r '.question // .rationale // "(no question text)"')"
76
+ rank=$(rank_for_category "$cat")
77
+ printf '%s\t%s\t%s\t%s\n' "$rank" "$cat" "$tid" "$qtext"
78
+ done < "$QUEUE_FILE" \
79
+ | sort -u `# dedupe identical (rank+cat+tid+qtext)` \
80
+ | sort -t $'\t' -k1,1n -s `# stable sort by rank ascending`
81
+ )
82
+
83
+ # Empty after dedupe / parse-skip → silent exit.
84
+ [ -n "$ENTRIES_TSV" ] || exit 0
85
+
86
+ ENTRY_COUNT=$(printf '%s\n' "$ENTRIES_TSV" | wc -l | tr -d ' ')
87
+
88
+ # Emit additionalContext on stdout (ADR-040 plain-stdout shape per
89
+ # session-start-briefing.sh precedent).
90
+ {
91
+ echo "PENDING QUESTIONS FROM PRIOR AFK LOOP — accumulated outstanding_questions"
92
+ echo "queue (source: .afk-run-state/outstanding-questions.jsonl, ${ENTRY_COUNT} entries)."
93
+ echo ""
94
+ echo "These are direction / deviation-approval / one-time-override / silent-framework"
95
+ echo "/ taste / correction-followup observations queued by /wr-itil:work-problems"
96
+ echo "iters per ADR-044 6-class taxonomy. Surface them via AskUserQuestion batched"
97
+ echo "<=4 per call (sequential when >4) on the user's first interactive turn,"
98
+ echo "ranked deviation-approval > direction > one-time-override > silent-framework"
99
+ echo "> taste > correction-followup. After resolving each entry, remove the"
100
+ echo "matching line from the queue file by rewriting"
101
+ echo ".afk-run-state/outstanding-questions.jsonl with the unresolved entries"
102
+ echo "remaining. Empty queue → next session no-op."
103
+ echo ""
104
+ echo "| # | Category | Ticket | Question |"
105
+ echo "|---|----------|--------|----------|"
106
+ i=0
107
+ while IFS=$'\t' read -r _rank cat tid qtext; do
108
+ i=$((i + 1))
109
+ # Truncate question text to keep table cells bounded; agent retrieves
110
+ # full body from the queue file when constructing AskUserQuestion calls.
111
+ short_q="$(printf '%s' "$qtext" | cut -c1-160)"
112
+ [ "${#qtext}" -gt 160 ] && short_q="${short_q}..."
113
+ # Escape pipe chars in cells so the table renders.
114
+ short_q="${short_q//|/\\|}"
115
+ printf '| %d | %s | %s | %s |\n' "$i" "$cat" "$tid" "$short_q"
116
+ done <<< "$ENTRIES_TSV"
117
+ echo ""
118
+ if [ "$ENTRY_COUNT" -gt 4 ]; then
119
+ echo "Note: ${ENTRY_COUNT} entries exceeds the AskUserQuestion <=4 per-call"
120
+ echo "cap (ADR-013 Rule 1). Fire sequential calls — first 4 highest-ranked"
121
+ echo "first, then the next batch, until the queue is drained."
122
+ fi
123
+ } 2>/dev/null
124
+
125
+ exit 0
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env bats
2
+ # Behavioural fixtures for itil-pending-questions-surface.sh (P157).
3
+ #
4
+ # SessionStart hook that reads the AFK-loop-accumulated outstanding-questions
5
+ # JSONL queue at .afk-run-state/outstanding-questions.jsonl, ranks entries
6
+ # per ADR-044 6-class taxonomy, and emits an additionalContext directive on
7
+ # stdout for the agent to surface via AskUserQuestion (batched <=4).
8
+ #
9
+ # Per ADR-052 (behavioural-tests-default), these tests exercise the hook's
10
+ # observable stdout / exit-code behaviour against fixture queue files —
11
+ # NOT the prose contents of the script itself.
12
+ #
13
+ # Behavioural surfaces under test:
14
+ # 1. Silent-on-no-content per ADR-040 Mechanism step 1 — missing or empty
15
+ # queue file produces zero stdout and exits 0.
16
+ # 2. Non-empty queue produces additionalContext naming the entries.
17
+ # 3. ADR-044 6-class precedence — when multiple categories present,
18
+ # deviation-approval ranks first; correction-followup ranks last.
19
+ # 4. Deduplication — identical entries (same category + question + ticket_id)
20
+ # collapse to one.
21
+ # 5. Batching directive — when N > 4, output names the AskUserQuestion
22
+ # batched-call cap.
23
+ # 6. Cleanup directive — output instructs the agent to truncate resolved
24
+ # entries from the queue file.
25
+ # 7. AFK-iter cross-context-leak prevention — WR_SUPPRESS_PENDING_QUESTIONS=1
26
+ # env var forces silent exit even when queue is non-empty.
27
+ #
28
+ # @problem P157
29
+ # @jtbd JTBD-006 (progress backlog while AFK — surface accumulated questions
30
+ # on session resume)
31
+ # @jtbd JTBD-001 (enforce governance without slowing down — direction-class
32
+ # observations resolve before user begins foreground work)
33
+ # @jtbd JTBD-101 (extend the suite — sibling SessionStart hook reuses
34
+ # ADR-040 silent-on-no-content shape)
35
+ # @adr ADR-032 (governance skill invocation patterns — P157 amendment for
36
+ # JSONL-queue SessionStart variant)
37
+ # @adr ADR-040 (session-start briefing surface — SessionStart precedent +
38
+ # silent-on-no-content shape)
39
+ # @adr ADR-044 (decision-delegation contract — 6-class taxonomy precedence)
40
+ # @adr ADR-052 (behavioural-tests-default — these tests exercise hook
41
+ # stdout / exit-code, not script prose)
42
+
43
+ setup() {
44
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
45
+ HOOK_SCRIPT="${REPO_ROOT}/packages/itil/hooks/itil-pending-questions-surface.sh"
46
+
47
+ TMPROOT=$(mktemp -d)
48
+ mkdir -p "$TMPROOT/.afk-run-state"
49
+ QUEUE_FILE="$TMPROOT/.afk-run-state/outstanding-questions.jsonl"
50
+
51
+ export CLAUDE_PROJECT_DIR="$TMPROOT"
52
+ unset WR_SUPPRESS_PENDING_QUESTIONS
53
+ }
54
+
55
+ teardown() {
56
+ rm -rf "$TMPROOT"
57
+ unset CLAUDE_PROJECT_DIR WR_SUPPRESS_PENDING_QUESTIONS
58
+ }
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Existence — minimum surface required for hooks.json to wire it up.
62
+ # ---------------------------------------------------------------------------
63
+
64
+ @test "hook script exists and is executable" {
65
+ [ -f "$HOOK_SCRIPT" ]
66
+ [ -x "$HOOK_SCRIPT" ]
67
+ }
68
+
69
+ @test "hooks.json registers the SessionStart hook with matcher startup" {
70
+ HOOKS_JSON="${REPO_ROOT}/packages/itil/hooks/hooks.json"
71
+ run jq -r '.hooks.SessionStart[] | select(.matcher == "startup") | .hooks[].command' "$HOOKS_JSON"
72
+ [ "$status" -eq 0 ]
73
+ echo "$output" | grep -qF 'itil-pending-questions-surface.sh'
74
+ }
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Silent-on-no-content per ADR-040 Mechanism step 1.
78
+ # ---------------------------------------------------------------------------
79
+
80
+ @test "missing queue file: silent exit 0" {
81
+ # No queue file at all (typical state for projects that have never run AFK).
82
+ rm -f "$QUEUE_FILE"
83
+ run "$HOOK_SCRIPT"
84
+ [ "$status" -eq 0 ]
85
+ [ -z "$output" ]
86
+ }
87
+
88
+ @test "empty queue file: silent exit 0" {
89
+ : > "$QUEUE_FILE"
90
+ run "$HOOK_SCRIPT"
91
+ [ "$status" -eq 0 ]
92
+ [ -z "$output" ]
93
+ }
94
+
95
+ @test "queue with only whitespace lines: silent exit 0" {
96
+ printf '\n \n\t\n' > "$QUEUE_FILE"
97
+ run "$HOOK_SCRIPT"
98
+ [ "$status" -eq 0 ]
99
+ [ -z "$output" ]
100
+ }
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # Non-empty queue → additionalContext emitted.
104
+ # ---------------------------------------------------------------------------
105
+
106
+ @test "single entry: additionalContext names the question and ticket_id" {
107
+ cat > "$QUEUE_FILE" <<'JSONL'
108
+ {"category":"direction","question":"Pick A or B for the storage layer?","context":"iter1 P200","ticket_id":"P200"}
109
+ JSONL
110
+ run "$HOOK_SCRIPT"
111
+ [ "$status" -eq 0 ]
112
+ echo "$output" | grep -qF 'Pick A or B for the storage layer?'
113
+ echo "$output" | grep -qF 'P200'
114
+ }
115
+
116
+ @test "single entry: output cites the queue file path so user can inspect" {
117
+ cat > "$QUEUE_FILE" <<'JSONL'
118
+ {"category":"direction","question":"Q1","context":"c1","ticket_id":"P201"}
119
+ JSONL
120
+ run "$HOOK_SCRIPT"
121
+ [ "$status" -eq 0 ]
122
+ echo "$output" | grep -qF '.afk-run-state/outstanding-questions.jsonl'
123
+ }
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # ADR-044 6-class precedence ordering.
127
+ # ---------------------------------------------------------------------------
128
+
129
+ @test "ranking: deviation-approval ranks first among mixed categories" {
130
+ cat > "$QUEUE_FILE" <<'JSONL'
131
+ {"category":"correction-followup","question":"low-rank Q","context":"c","ticket_id":"P301"}
132
+ {"category":"direction","question":"mid-rank Q","context":"c","ticket_id":"P302"}
133
+ {"category":"deviation-approval","existing_decision":"ADR-001","contradicting_evidence":"ev","proposed_shape":"amend","rationale":"r","ticket_id":"P303"}
134
+ JSONL
135
+ run "$HOOK_SCRIPT"
136
+ [ "$status" -eq 0 ]
137
+ # The deviation-approval entry's rationale must appear before the direction Q
138
+ # in the output (precedence: deviation-approval > direction > correction-followup).
139
+ DEVIATION_LINE=$(echo "$output" | grep -n 'P303' | head -1 | cut -d: -f1)
140
+ DIRECTION_LINE=$(echo "$output" | grep -n 'mid-rank Q' | head -1 | cut -d: -f1)
141
+ CORRECTION_LINE=$(echo "$output" | grep -n 'low-rank Q' | head -1 | cut -d: -f1)
142
+ [ -n "$DEVIATION_LINE" ]
143
+ [ -n "$DIRECTION_LINE" ]
144
+ [ -n "$CORRECTION_LINE" ]
145
+ [ "$DEVIATION_LINE" -lt "$DIRECTION_LINE" ]
146
+ [ "$DIRECTION_LINE" -lt "$CORRECTION_LINE" ]
147
+ }
148
+
149
+ @test "ranking: full 6-class precedence is deviation > direction > one-time > silent-framework > taste > correction" {
150
+ cat > "$QUEUE_FILE" <<'JSONL'
151
+ {"category":"taste","question":"q-taste","context":"c","ticket_id":"P401"}
152
+ {"category":"correction-followup","question":"q-correction","context":"c","ticket_id":"P402"}
153
+ {"category":"silent-framework","question":"q-silent","context":"c","ticket_id":"P403"}
154
+ {"category":"one-time-override","question":"q-onetime","context":"c","ticket_id":"P404"}
155
+ {"category":"direction","question":"q-direction","context":"c","ticket_id":"P405"}
156
+ {"category":"deviation-approval","existing_decision":"ADR-X","contradicting_evidence":"ev","proposed_shape":"amend","rationale":"q-deviation","ticket_id":"P406"}
157
+ JSONL
158
+ run "$HOOK_SCRIPT"
159
+ [ "$status" -eq 0 ]
160
+ # Capture the line number of each category-tagged ticket marker; assert order.
161
+ L1=$(echo "$output" | grep -n 'P406' | head -1 | cut -d: -f1)
162
+ L2=$(echo "$output" | grep -n 'q-direction' | head -1 | cut -d: -f1)
163
+ L3=$(echo "$output" | grep -n 'q-onetime' | head -1 | cut -d: -f1)
164
+ L4=$(echo "$output" | grep -n 'q-silent' | head -1 | cut -d: -f1)
165
+ L5=$(echo "$output" | grep -n 'q-taste' | head -1 | cut -d: -f1)
166
+ L6=$(echo "$output" | grep -n 'q-correction' | head -1 | cut -d: -f1)
167
+ [ "$L1" -lt "$L2" ]
168
+ [ "$L2" -lt "$L3" ]
169
+ [ "$L3" -lt "$L4" ]
170
+ [ "$L4" -lt "$L5" ]
171
+ [ "$L5" -lt "$L6" ]
172
+ }
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Deduplication of identical entries.
176
+ # ---------------------------------------------------------------------------
177
+
178
+ @test "dedup: identical entries (same category+question+ticket_id) collapse to one" {
179
+ cat > "$QUEUE_FILE" <<'JSONL'
180
+ {"category":"direction","question":"Same Q","context":"c","ticket_id":"P500"}
181
+ {"category":"direction","question":"Same Q","context":"c","ticket_id":"P500"}
182
+ {"category":"direction","question":"Same Q","context":"different-context-different-iter","ticket_id":"P500"}
183
+ JSONL
184
+ run "$HOOK_SCRIPT"
185
+ [ "$status" -eq 0 ]
186
+ # "Same Q" should appear exactly once in the output (after dedup).
187
+ COUNT=$(echo "$output" | grep -cF 'Same Q')
188
+ [ "$COUNT" -eq 1 ]
189
+ }
190
+
191
+ @test "dedup: different question text on same ticket survives as two entries" {
192
+ cat > "$QUEUE_FILE" <<'JSONL'
193
+ {"category":"direction","question":"Question one","context":"c","ticket_id":"P501"}
194
+ {"category":"direction","question":"Question two","context":"c","ticket_id":"P501"}
195
+ JSONL
196
+ run "$HOOK_SCRIPT"
197
+ [ "$status" -eq 0 ]
198
+ echo "$output" | grep -qF 'Question one'
199
+ echo "$output" | grep -qF 'Question two'
200
+ }
201
+
202
+ # ---------------------------------------------------------------------------
203
+ # Batching directive — names the AskUserQuestion <=4 cap when N > 4.
204
+ # ---------------------------------------------------------------------------
205
+
206
+ @test "batching: output names AskUserQuestion when entries present" {
207
+ cat > "$QUEUE_FILE" <<'JSONL'
208
+ {"category":"direction","question":"Q1","context":"c","ticket_id":"P600"}
209
+ JSONL
210
+ run "$HOOK_SCRIPT"
211
+ [ "$status" -eq 0 ]
212
+ echo "$output" | grep -qiE 'AskUserQuestion'
213
+ }
214
+
215
+ @test "batching: directive cites the <=4-per-call cap" {
216
+ # 5 entries should trigger the batching note since AskUserQuestion caps at 4.
217
+ cat > "$QUEUE_FILE" <<'JSONL'
218
+ {"category":"direction","question":"Q1","context":"c","ticket_id":"P601"}
219
+ {"category":"direction","question":"Q2","context":"c","ticket_id":"P602"}
220
+ {"category":"direction","question":"Q3","context":"c","ticket_id":"P603"}
221
+ {"category":"direction","question":"Q4","context":"c","ticket_id":"P604"}
222
+ {"category":"direction","question":"Q5","context":"c","ticket_id":"P605"}
223
+ JSONL
224
+ run "$HOOK_SCRIPT"
225
+ [ "$status" -eq 0 ]
226
+ echo "$output" | grep -qE '(<=|≤|max(imum)?[ -]?)4|four'
227
+ }
228
+
229
+ # ---------------------------------------------------------------------------
230
+ # Cleanup-on-resolve directive.
231
+ # ---------------------------------------------------------------------------
232
+
233
+ @test "cleanup: output instructs the agent to remove resolved entries from the queue" {
234
+ cat > "$QUEUE_FILE" <<'JSONL'
235
+ {"category":"direction","question":"Q1","context":"c","ticket_id":"P700"}
236
+ JSONL
237
+ run "$HOOK_SCRIPT"
238
+ [ "$status" -eq 0 ]
239
+ # The cleanup directive must instruct removing resolved entries from the
240
+ # queue file. Match on the load-bearing words rather than exact phrasing.
241
+ echo "$output" | grep -qiE '(remove|delete|truncat|clear).*queue|outstanding-questions\.jsonl'
242
+ }
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # AFK-iter cross-context-leak prevention (architect Note 2).
246
+ # ---------------------------------------------------------------------------
247
+
248
+ @test "WR_SUPPRESS_PENDING_QUESTIONS=1 forces silent exit even when queue non-empty" {
249
+ cat > "$QUEUE_FILE" <<'JSONL'
250
+ {"category":"direction","question":"Q-should-not-leak","context":"c","ticket_id":"P800"}
251
+ JSONL
252
+ export WR_SUPPRESS_PENDING_QUESTIONS=1
253
+ run "$HOOK_SCRIPT"
254
+ [ "$status" -eq 0 ]
255
+ [ -z "$output" ]
256
+ }
257
+
258
+ @test "WR_SUPPRESS_PENDING_QUESTIONS=0 does NOT suppress (only =1 does)" {
259
+ cat > "$QUEUE_FILE" <<'JSONL'
260
+ {"category":"direction","question":"Q-must-surface","context":"c","ticket_id":"P801"}
261
+ JSONL
262
+ export WR_SUPPRESS_PENDING_QUESTIONS=0
263
+ run "$HOOK_SCRIPT"
264
+ [ "$status" -eq 0 ]
265
+ echo "$output" | grep -qF 'Q-must-surface'
266
+ }
267
+
268
+ @test "work-problems Step 5 dispatch block exports WR_SUPPRESS_PENDING_QUESTIONS=1 before claude -p" {
269
+ # The orchestrator MUST set WR_SUPPRESS_PENDING_QUESTIONS=1 before each
270
+ # iter subprocess spawn so the queue does not surface inside iter contexts
271
+ # (cross-context leak per ADR-032 line 127).
272
+ WP_SKILL="${REPO_ROOT}/packages/itil/skills/work-problems/SKILL.md"
273
+ [ -f "$WP_SKILL" ]
274
+ # Find the export line; it must come before the "claude -p" dispatch line in
275
+ # the same Step 5 dispatch block.
276
+ EXPORT_LINE=$(grep -n 'export WR_SUPPRESS_PENDING_QUESTIONS=1' "$WP_SKILL" | head -1 | cut -d: -f1)
277
+ CLAUDE_P_LINE=$(grep -n '^claude -p \\$' "$WP_SKILL" | head -1 | cut -d: -f1)
278
+ [ -n "$EXPORT_LINE" ]
279
+ [ -n "$CLAUDE_P_LINE" ]
280
+ [ "$EXPORT_LINE" -lt "$CLAUDE_P_LINE" ]
281
+ }
282
+
283
+ # ---------------------------------------------------------------------------
284
+ # Malformed input — silent skip, do not crash the SessionStart hook chain.
285
+ # ---------------------------------------------------------------------------
286
+
287
+ @test "malformed JSON line: skipped silently, well-formed lines still surface" {
288
+ cat > "$QUEUE_FILE" <<'JSONL'
289
+ {not valid json at all
290
+ {"category":"direction","question":"Valid Q","context":"c","ticket_id":"P900"}
291
+ JSONL
292
+ run "$HOOK_SCRIPT"
293
+ [ "$status" -eq 0 ]
294
+ echo "$output" | grep -qF 'Valid Q'
295
+ }
296
+
297
+ @test "all-malformed queue: silent exit 0 (do not block session start)" {
298
+ # Defensive — if the queue file is corrupted, the hook MUST NOT prevent
299
+ # the session from starting. SessionStart hook failures cascade into
300
+ # "session won't start" UX which is far worse than missing one surfacing.
301
+ cat > "$QUEUE_FILE" <<'JSONL'
302
+ {not json
303
+ also not json
304
+ JSONL
305
+ run "$HOOK_SCRIPT"
306
+ [ "$status" -eq 0 ]
307
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.24.1-preview.277",
3
+ "version": "0.25.0-preview.279",
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"
@@ -0,0 +1,137 @@
1
+ # `/wr-itil:capture-problem` Reference
2
+
3
+ This file hosts the rationale, edge cases, contract trade-offs, and ADR cross-references for the `/wr-itil:capture-problem` skill. SKILL.md is the runtime contract (~150 lines, on-topic per ADR-038 progressive disclosure); this REFERENCE.md is the on-demand expansion for maintainers and curious users.
4
+
5
+ ## Why a separate skill?
6
+
7
+ The `/wr-itil:manage-problem` flow is ~10 turns of agent work for a full new-problem intake: Step 0 README reconciliation preflight, Step 2 wide-net duplicate grep + AskUserQuestion branch, Step 3 next-ID, Step 4 information-gathering AskUserQuestion, Step 4b multi-concern split AskUserQuestion, Step 5 ticket file write + P094 README refresh, Step 11 commit gate.
8
+
9
+ That cost is correct for the canonical new-problem path — the user wants to walk the flow, see duplicate-match prompts, and place the ticket in the WSJF ranking immediately.
10
+
11
+ It is wrong for the **aside-invocation** use case. P155 surfaced three repeating patterns where the heavyweight cost is load-bearing friction:
12
+
13
+ 1. **Mid-AFK-iter sibling-findings**: agent observes a tangential ticket-worthy issue. The 10-turn ceremony breaks iter cadence — observation gets buried in `notes` field of `ITERATION_SUMMARY` and ~50% never reach the backlog.
14
+ 2. **User-initiated rapid captures**: user says "btw, this is broken too — capture it". The 10-turn ceremony breaks conversational flow.
15
+ 3. **AFK orchestrator main turn captures**: user-driven mid-loop interjections (P151 / P152 / P154 in the session that surfaced P155). Each capture took 5-15 minutes wall-clock through the heavyweight flow.
16
+
17
+ `/wr-itil:capture-problem` is the source-side fix: a lightweight skill with a deferred-placeholder pattern that captures the observation in ~3-4 turns and routes the deferred re-rating + README refresh through `/wr-itil:review-problems` at a time of the user's choosing.
18
+
19
+ ## Contract trade-offs
20
+
21
+ ### Capture-time false-positives are cheaper than false-negatives
22
+
23
+ P155 line 24: "capture-time false-positives (creating a duplicate that gets merged later) are cheaper than capture-time false-negatives (losing the observation entirely)."
24
+
25
+ This is the structural rationale for:
26
+ - **3-keyword cap on the duplicate-grep** — wider grep would surface more matches but force the user to either (a) add an AskUserQuestion branch (which capture-problem doesn't have) or (b) ignore the matches and proceed silently. A 3-keyword cap keeps the match list short and audit-able in the report.
27
+ - **Title-only filename match** — body-content matches would be too noisy at the conservative threshold. Files whose filenames have zero overlap but whose bodies mention a keyword are almost always different tickets that happen to discuss similar topics. False-positive cost would dominate.
28
+ - **No halt-on-match** — even when matches are found, capture proceeds. The duplicate gets resolved at next `/wr-itil:review-problems` (where the full-rank scan can detect and merge actual duplicates with the user in the loop).
29
+
30
+ ### Deferred-README-refresh contract
31
+
32
+ Capture-problem skips the P094 inline README refresh that `/wr-itil:manage-problem` Step 5 performs. The trade-off:
33
+
34
+ | Surface | Inline refresh (manage-problem) | Deferred refresh (capture-problem) |
35
+ |---------|---------------------------------|-------------------------------------|
36
+ | README authoritativeness | Always current at commit boundary | Lags new captures until next review |
37
+ | Capture-time turn cost | +1-2 turns (regenerate + stage) | 0 turns |
38
+ | WSJF ranking visibility | Immediate | Pending review |
39
+ | Audit trail (commit) | One commit covers ticket + README | One commit covers ticket only |
40
+ | README staleness window | None | Bounded by next review invocation |
41
+
42
+ The deferred contract is acceptable because:
43
+
44
+ 1. **The on-disk ticket inventory remains the source of truth**. README.md is a derived view — consumers that need WSJF rankings can re-derive from the ticket files (and `/wr-itil:list-problems` does exactly that on cache-stale fallback).
45
+ 2. **The trailing pointer in Step 7 is the user-visible signal** that the README is transiently stale. The user has explicit instructions for how to reconcile.
46
+ 3. **`/wr-itil:review-problems` Step 9b's auto-transition pass already re-rates deferred-placeholder tickets** (the literal string `(deferred — re-rate at next /wr-itil:review-problems)` is the keying signal). One review pass folds all captured-but-not-rated tickets into the ranking.
47
+
48
+ The bound on the staleness window is "until the next `/wr-itil:review-problems` invocation". For sessions that capture and never review, the README stays stale — but the on-disk inventory is always correct, and the next session-start `wr-itil-reconcile-readme` preflight catches drift if it propagates beyond a single session.
49
+
50
+ ### No AskUserQuestion at all
51
+
52
+ Architect Q4 + JTBD review confirmed: capture-problem is a **mechanical-stage skill** per ADR-044's framework-resolution boundary. Every potentially-interactive decision is framework-mediated:
53
+
54
+ - **Duplicate-check**: false-positive bias > false-negative bias. Mechanical rule: list matches, proceed regardless.
55
+ - **Priority**: framework-policy default `3 (Medium) — Impact 3 × Likelihood 1`, flagged for re-rate. Re-rating is mandatory before the ticket is worked, so no ticket gets ranked on a wrong default.
56
+ - **Effort**: framework-policy default `M`, flagged for re-rate. Same re-rating contract as Priority.
57
+ - **Multi-concern split**: out of scope. The user invoking capture-problem with a multi-concern observation gets one ticket with the full description as the body; they re-route to `/wr-itil:manage-problem` for the structured split.
58
+
59
+ This mirrors the mechanical-stage carve-out pattern documented in CLAUDE.md (P132 / inverse-P078 trap): when a SKILL contract names a stage as mechanical, do not ask. Per-action consent gates re-ask decisions the user already made and silently undo the load-bearing UX investment.
60
+
61
+ ## Edge cases
62
+
63
+ ### Empty `$ARGUMENTS`
64
+
65
+ Halt-with-stderr-directive. capture-problem requires a description; without one there is nothing to capture. The directive points the user to `/wr-itil:manage-problem`, which has Step 4 AskUserQuestion gathering for missing fields.
66
+
67
+ AFK orchestrators MUST NOT invoke capture-problem with empty arguments — caller-side contract. The Rule 6 audit makes this explicit so AFK-iter writers don't accidentally introduce a halt mid-loop.
68
+
69
+ ### Description is a kebab-stopword soup
70
+
71
+ If the description's first 8-10 tokens are entirely stopwords (e.g. "the and of to in"), the slug derivation falls back to the full description hash modulo a short integer. The resulting slug is non-meaningful but unique; the user re-titles at next investigation.
72
+
73
+ This is a degenerate case — real captures carry meaningful first-tokens — but the fallback prevents a malformed empty-slug filename.
74
+
75
+ ### ID collision with origin
76
+
77
+ The next-ID formula uses `git ls-tree origin/main` to read the remote-tracking ref without requiring a fetch. If a parallel session minted the same ID for a different problem and pushed it before this session captures, the local read sees the higher origin ID and increments past it.
78
+
79
+ If the local session has not fetched recently and origin has captures the local doesn't see, the formula may still collide. The renumber audit log line in Step 7 captures the resolution. P040 incident applies.
80
+
81
+ ### Cross-skill marker ordering
82
+
83
+ The `/tmp/manage-problem-grep-${SESSION_ID}` create-gate marker is shared between `manage-problem` and `capture-problem`. Whichever fires first writes the marker; subsequent calls are idempotent (`: > FILE`).
84
+
85
+ This means a session that does `manage-problem` once then `capture-problem` three times has the marker set after the first manage-problem grep, and all three captures land without re-running the grep + mark sequence. capture-problem still runs its own minimal-grep in Step 2 (because the conservative threshold + report-listing is part of the contract), but the marker write is a no-op.
86
+
87
+ ### P057 staging-trap
88
+
89
+ Not applicable. capture-problem only Writes a new file; it does not `git mv` an existing one. The P057 rule (re-stage after Edit on a `git mv`-d file) is irrelevant to this skill.
90
+
91
+ ### Multi-concern descriptions
92
+
93
+ If the user supplies a multi-concern description (e.g. "checkout flow leaks tokens AND the price calculator rounds wrong"), capture-problem creates ONE ticket with both observations in the description body. Re-routing to `/wr-itil:manage-problem` for the structured split (Step 4b) is a deliberate design choice — the heavyweight flow owns the multi-concern decision because the split prompt requires user input to confirm boundaries.
94
+
95
+ The user can manually `/wr-itil:manage-problem <NNN>` later to split a captured multi-concern ticket if needed.
96
+
97
+ ## Composition with the rest of the suite
98
+
99
+ ### `/wr-itil:review-problems`
100
+
101
+ Handles the deferred re-rating + README refresh. Step 9b's auto-transition pass keys off the deferred-placeholder string and surfaces captured tickets for re-rating. The README refresh in Step 9e regenerates the table covering all captured-but-not-rated tickets in one pass.
102
+
103
+ ### `/wr-itil:manage-problem`
104
+
105
+ Heavyweight intake counterpart. Shares the create-gate marker with capture-problem. The two skills are designed to coexist — neither supersedes the other. A user who starts with capture-problem and decides they want the structured intake flow re-invokes manage-problem on the captured ticket ID to flesh out the placeholders.
106
+
107
+ ### `/wr-itil:work-problems` (AFK orchestrator)
108
+
109
+ Iter subprocesses can invoke capture-problem to capture sibling-findings without breaking iter cadence. The AFK carve-out in ADR-032 (line 85) excludes the **background-capture** variant from AFK contexts; the **foreground-lightweight-capture** variant introduced by this skill is fine inside iter subprocesses because it has no `Agent(run_in_background: true)` invocation — it's a normal foreground-synchronous skill that happens to do less work than manage-problem.
110
+
111
+ ### `/wr-itil:capture-problem` callers
112
+
113
+ The intended invocation surface is `/wr-itil:capture-problem <description>`. The description must be a non-empty free-text payload; the skill does not branch on description shape.
114
+
115
+ ## Related ADRs
116
+
117
+ - **ADR-009** — gate-marker-lifecycle (per-session /tmp markers; capture-problem reuses the manage-problem marker).
118
+ - **ADR-013** — structured user interaction (Rule 6 fail-safe; capture-problem has no AskUserQuestion branches so Rule 6 is trivially satisfied).
119
+ - **ADR-014** — governance skills commit their own work (capture-problem owns its commit).
120
+ - **ADR-022** — verification-pending status (out of scope for capture-problem; status transitions live in transition-problem).
121
+ - **ADR-031** — problem-ticket directory layout (capture-problem matches current flat-layout production reality; auto-migration is a future ADR-031 follow-up).
122
+ - **ADR-032** — governance skill invocation patterns (this skill's parent ADR; foreground-lightweight-capture variant amendment 2026-05-03).
123
+ - **ADR-038** — progressive disclosure (SKILL.md + REFERENCE.md split shape).
124
+ - **ADR-044** — decision-delegation contract (framework-mediated mechanical-stage carve-outs).
125
+ - **ADR-049** — bin/ on PATH (capture-problem reuses existing `wr-itil-reconcile-readme` shim; no new shim).
126
+ - **ADR-052** — behavioural-tests-default for skill testing (capture-problem's bats fixtures exercise primitives, not SKILL.md prose).
127
+
128
+ ## Related problems
129
+
130
+ - **P014** — parent / master tracker.
131
+ - **P078** — capture-on-correction OFFER; depends on capture-problem.
132
+ - **P088** — settled the user-direction-scoped decision: capture-problem + capture-adr are shippable; capture-retro is deferred.
133
+ - **P119** — manage-problem create-gate; capture-problem composes with the same marker.
134
+ - **P148** — Tickets Deferred retro section (legacy when capture-problem ships).
135
+ - **P155** — driver ticket.
136
+ - **P156** — sibling capture-adr.
137
+ - **P157** — sibling pending-questions-surface hook.
@@ -0,0 +1,217 @@
1
+ ---
2
+ name: wr-itil:capture-problem
3
+ description: Lightweight problem-capture skill for aside-invocation during foreground work — minimal duplicate-check, skeleton ticket file, single commit per capture, no inline README refresh. Defers full duplicate analysis and README refresh to /wr-itil:review-problems. Use this when the user (or agent mid-iter) wants to capture an observation quickly without disrupting current task flow. For full-intake new-problem creation, use /wr-itil:manage-problem.
4
+ allowed-tools: Read, Write, Edit, Bash, Grep, Glob
5
+ ---
6
+
7
+ # Capture Problem Skill
8
+
9
+ Capture a problem ticket quickly during foreground work. Lightweight aside-invocation surface that complements the heavyweight `/wr-itil:manage-problem` flow. See `REFERENCE.md` in this directory for rationale, edge cases, contract trade-offs, and the ADR-032 foreground-lightweight-capture amendment.
10
+
11
+ This skill is the foreground-lightweight-capture variant of `/wr-itil:manage-problem`'s new-problem path per ADR-032 (P155 amendment, 2026-05-03). The deferred background-capture variant named in ADR-032's original taxonomy remains deferred per P088 settlement.
12
+
13
+ ## When to invoke
14
+
15
+ - **Mid-iter sibling-finding**: agent observes a tangential ticket-worthy issue while working on a different problem and cannot afford the 10-turn `/wr-itil:manage-problem` ceremony.
16
+ - **User-initiated rapid capture**: user says "btw, this is broken too — capture it" during retros, code reviews, or correction conversations.
17
+ - **AFK orchestrator main turn captures**: orchestrator captures user-driven mid-loop observations without breaking the iter cadence.
18
+
19
+ **Use `/wr-itil:manage-problem` instead** when:
20
+ - The user wants to walk the full intake flow (priority discussion, multi-concern split, immediate WSJF placement).
21
+ - The capture is large enough that deferred-investigation placeholders are unhelpful (the description IS the full ticket body).
22
+ - The capture needs to ride alongside an immediate fix (`fix(scope): ... (closes P<NNN>)` shape — manage-problem's Step 7 transition + Step 11 commit handles this; capture-problem does not).
23
+
24
+ ## Rule 6 audit (per ADR-032 + ADR-013)
25
+
26
+ This skill has **zero AskUserQuestion branches** by design. Each potentially-interactive decision is framework-mediated per ADR-044:
27
+
28
+ | Decision | Resolution |
29
+ |----------|-----------|
30
+ | Duplicate-check | Mechanical 3-keyword title-only grep; matches listed in report; capture proceeds regardless. False-positives are cheaper than false-negatives (P155 line 24). |
31
+ | Priority default | Framework-policy: `3 (Medium) — Impact 3 × Likelihood 1` flagged "deferred — re-rate at next /wr-itil:review-problems". |
32
+ | Effort default | Framework-policy: `M` flagged "deferred — re-rate at next /wr-itil:review-problems". |
33
+ | Multi-concern split | Out of scope: capture-problem creates one ticket per invocation. Multi-concern observations route to `/wr-itil:manage-problem` (its Step 4b owns the split). |
34
+ | Empty `$ARGUMENTS` | Halt-with-stderr-directive: print "capture-problem requires a description in $ARGUMENTS — invoke /wr-itil:manage-problem instead for the full intake flow" and exit. AFK orchestrators MUST NOT invoke capture-problem with empty arguments — caller-side contract. |
35
+
36
+ Per ADR-013 Rule 6 fail-safe: every branch above resolves without user input, so AFK and interactive contexts behave identically.
37
+
38
+ ## Steps
39
+
40
+ ### 0. README reconciliation preflight (P118)
41
+
42
+ Same as `/wr-itil:manage-problem` Step 0 — diagnose-only check. Halt-and-route on Exit 1 (committed cross-session drift); INLINE_REFRESH carve-out (P149) preserved. capture-problem itself does NOT refresh README.md (see Step 6); the preflight is purely a fail-fast on pre-existing drift.
43
+
44
+ ```bash
45
+ wr-itil-reconcile-readme docs/problems > /tmp/wr-itil-drift-$$.txt
46
+ reconcile_exit=$?
47
+ if [ "$reconcile_exit" -eq 1 ]; then
48
+ wr-itil-classify-readme-drift /tmp/wr-itil-drift-$$.txt docs/problems
49
+ classify_exit=$?
50
+ rm -f /tmp/wr-itil-drift-$$.txt
51
+ # classify_exit 0 (INLINE_REFRESH): proceed (no inline refresh in this skill).
52
+ # classify_exit 1 (HALT_ROUTE_RECONCILE): halt; invoke /wr-itil:reconcile-readme.
53
+ # classify_exit 2 (parse error): conservative halt-and-route.
54
+ fi
55
+ ```
56
+
57
+ ### 1. Parse the description from `$ARGUMENTS`
58
+
59
+ The description is the full free-text payload from `$ARGUMENTS`. Empty arguments halts per the Rule 6 audit above.
60
+
61
+ Derive a kebab-case title slug from the first 8-10 non-stopword tokens of the description (matching the existing `manage-problem` slug derivation pattern).
62
+
63
+ ### 2. Minimal-grep duplicate check (3-keyword title-only)
64
+
65
+ Extract up to **3 distinct kebab-cased non-stopword keywords** from the description. Grep the **filenames** of `docs/problems/*.md` (NOT bodies — title-only is the conservative threshold per architect verdict on Q1):
66
+
67
+ ```bash
68
+ match_count=$(ls docs/problems/*.md 2>/dev/null \
69
+ | grep -ciE 'kw1|kw2|kw3' || true)
70
+ ```
71
+
72
+ The **3-keyword cap** is a hard-coded constant. Do NOT make it env-overridable — the conservative threshold rationale (P155 line 24) is structural to the design, not a tunable knob.
73
+
74
+ **Title-only**: file bodies are intentionally NOT scanned. Body-content matches would either (a) over-prompt (capture-problem has no AskUserQuestion to surface them) or (b) get silently swallowed. Title-only matches preserve the conservative-threshold contract.
75
+
76
+ If matches are found: list them in the final report. **Do NOT halt or branch.** Capture proceeds. The user can resolve duplicates at the next `/wr-itil:review-problems` invocation (or invoke `/wr-itil:manage-problem` directly if the duplicate-check shape needs a structured branch).
77
+
78
+ **After the grep completes**, write the per-session create-gate marker so the `PreToolUse:Write` hook (P119) permits the subsequent Write of the new `.open.md` file:
79
+
80
+ ```bash
81
+ source packages/itil/hooks/lib/session-id.sh
82
+ source packages/itil/hooks/lib/create-gate.sh
83
+ sid=$(get_current_session_id) && mark_step2_complete "$sid"
84
+ ```
85
+
86
+ The marker is shared between `manage-problem` and `capture-problem` per ADR-032 amendment — same `/tmp/manage-problem-grep-${SESSION_ID}` path, idempotent across cross-skill ordering.
87
+
88
+ ### 3. Compute the next ID
89
+
90
+ Same P056-safe local_max + origin_max formula as `/wr-itil:manage-problem` Step 3:
91
+
92
+ ```bash
93
+ local_max=$(ls docs/problems/*.md 2>/dev/null | sed 's/.*\///' | grep -oE '^[0-9]+' | sort -n | tail -1)
94
+ origin_max=$(git ls-tree --name-only origin/main docs/problems/ 2>/dev/null | sed 's|^docs/problems/||' | grep -oE '^[0-9]+' | sort -n | tail -1)
95
+ next=$(printf '%03d' $(( $(echo -e "${local_max:-0}\n${origin_max:-0}" | sort -n | tail -1) + 1 )))
96
+ ```
97
+
98
+ Log the renumber decision in the operation report if origin and local diverged.
99
+
100
+ ### 4. Skeleton-fill the ticket
101
+
102
+ **File path**: `docs/problems/<NNN>-<kebab-title>.open.md`
103
+
104
+ **Template** (deferred-placeholder pattern — flag every section the capture didn't fill):
105
+
106
+ ```markdown
107
+ # Problem <NNN>: <Title>
108
+
109
+ **Status**: Open
110
+ **Reported**: <YYYY-MM-DD>
111
+ **Priority**: 3 (Medium) — Impact: 3 x Likelihood: 1 (deferred — re-rate at next /wr-itil:review-problems)
112
+ **Effort**: M (deferred — re-rate at next /wr-itil:review-problems)
113
+
114
+ ## Description
115
+
116
+ <full description from $ARGUMENTS>
117
+
118
+ ## Symptoms
119
+
120
+ (deferred to investigation)
121
+
122
+ ## Workaround
123
+
124
+ (deferred to investigation)
125
+
126
+ ## Impact Assessment
127
+
128
+ - **Who is affected**: (deferred to investigation)
129
+ - **Frequency**: (deferred to investigation)
130
+ - **Severity**: (deferred to investigation)
131
+ - **Analytics**: (deferred to investigation)
132
+
133
+ ## Root Cause Analysis
134
+
135
+ ### Investigation Tasks
136
+
137
+ - [ ] Re-rate Priority and Effort at next /wr-itil:review-problems
138
+ - [ ] Investigate root cause
139
+ - [ ] Create reproduction test
140
+
141
+ ## Dependencies
142
+
143
+ - **Blocks**: (none)
144
+ - **Blocked by**: (none)
145
+ - **Composes with**: (none)
146
+
147
+ ## Related
148
+
149
+ (captured via /wr-itil:capture-problem; expand at next investigation)
150
+ ```
151
+
152
+ The deferred-placeholder pattern is load-bearing — `/wr-itil:review-problems` keys off the literal string `(deferred — re-rate at next /wr-itil:review-problems)` to surface captured tickets for re-rating.
153
+
154
+ ### 5. Write the file
155
+
156
+ Single `Write` to `docs/problems/<NNN>-<kebab-title>.open.md`. The P119 PreToolUse hook permits the Write because Step 2 set the marker.
157
+
158
+ ### 6. Commit per ADR-014 — single commit, no README refresh
159
+
160
+ **Stage list**: ONLY the new ticket file. **Do NOT** stage `docs/problems/README.md`. The deferred-README-refresh contract is the load-bearing distinction from `/wr-itil:manage-problem` — capture-time speed depends on skipping the regenerate-and-stage cycle.
161
+
162
+ ```bash
163
+ git add docs/problems/<NNN>-<kebab-title>.open.md
164
+ ```
165
+
166
+ Satisfy the commit gate per ADR-014 — same two-path pattern as manage-problem Step 11:
167
+
168
+ - **Primary**: delegate to subagent type `wr-risk-scorer:pipeline` via the Agent tool.
169
+ - **Fallback**: invoke `/wr-risk-scorer:assess-release` via the Skill tool when the subagent type is unavailable in the current tool surface.
170
+
171
+ Commit message:
172
+
173
+ ```
174
+ docs(problems): capture P<NNN> <title>
175
+ ```
176
+
177
+ The `capture` verb in the message is the audit signal that this ticket landed via the lightweight aside path (vs. `open` for manage-problem's full intake).
178
+
179
+ ### 7. Report
180
+
181
+ After the commit, report:
182
+
183
+ - The new ticket file path and ID.
184
+ - The list of duplicate matches found (if any). If matches found, name them and remind the user to merge at next `/wr-itil:review-problems` if appropriate.
185
+ - Trailing pointer: `Run /wr-itil:review-problems next to fold P<NNN> into the WSJF rankings, re-rate the deferred placeholders, and refresh docs/problems/README.md.`
186
+
187
+ The trailing pointer is **not optional** — it is the user-visible signal that the README is transiently stale and how to reconcile it. Drift here re-opens the deferred-README-refresh contract gap.
188
+
189
+ ## Composition with manage-problem
190
+
191
+ | Concern | manage-problem | capture-problem |
192
+ |---------|----------------|-----------------|
193
+ | Duplicate-check | Wide-net grep + AskUserQuestion branch on matches | 3-keyword title-only grep, list-only (no branch) |
194
+ | Multi-concern split | Step 4b AskUserQuestion | Out of scope (one ticket per invocation) |
195
+ | Skeleton-fill | Full-intake; AskUserQuestion for missing fields | Deferred-placeholder pattern; no AskUserQuestion |
196
+ | README refresh | P094 inline (regenerate + stage in same commit) | Deferred to next `/wr-itil:review-problems` |
197
+ | Status transitions | Step 7 owns Open → Known Error → Verifying → Closed | Out of scope (creation only) |
198
+ | Commit grain | One commit per intake (or per split-concern set) | One commit per capture |
199
+ | Use case | Full-intake new problem; user wants to walk the flow | Aside-invocation; capture-and-continue |
200
+
201
+ The two skills share the `/tmp/manage-problem-grep-${SESSION_ID}` create-gate marker per P119 — calling either skill's Step 2 grep + mark sequence permits new ticket Writes for the rest of the session, regardless of which skill landed first.
202
+
203
+ ## Related
204
+
205
+ - **P155** (`docs/problems/155-ship-capture-problem-skill.open.md`) — driver ticket.
206
+ - **P014** (`docs/problems/014-aside-invocation-for-governance-skills.open.md`) — parent / master tracker.
207
+ - **P078** — capture-on-correction OFFER pattern; depends on capture-problem shipping.
208
+ - **P119** — manage-problem create-gate hook; capture-problem composes with the same marker.
209
+ - **ADR-032** (`docs/decisions/032-governance-skill-invocation-patterns.proposed.md`) — foreground-lightweight-capture variant amendment.
210
+ - **ADR-038** — progressive-disclosure pattern (SKILL.md + REFERENCE.md split).
211
+ - **ADR-044** — decision-delegation contract (framework-mediated mechanical-stage carve-outs).
212
+ - **ADR-049** — bin/ on PATH; capture-problem reuses the existing `wr-itil-reconcile-readme` shim.
213
+ - **ADR-052** — behavioural-tests-default for skill testing.
214
+ - `packages/itil/skills/manage-problem/SKILL.md` — heavyweight intake counterpart.
215
+ - `packages/itil/skills/review-problems/SKILL.md` — re-rates the deferred placeholders + refreshes README.md.
216
+
217
+ $ARGUMENTS
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env bats
2
+ # Behavioural fixtures for /wr-itil:capture-problem (P155).
3
+ #
4
+ # Per ADR-052 (Behavioural-tests-default for skill testing), these tests
5
+ # exercise the load-bearing primitives the skill dispatches and assert
6
+ # observable state — NOT the prose contents of SKILL.md.
7
+ #
8
+ # Behavioural surfaces under test:
9
+ # 1. P119 create-gate composition — capture-problem must source the
10
+ # session-id + create-gate helpers and call mark_step2_complete
11
+ # before the Write so the PreToolUse hook permits the new ticket
12
+ # file to land. Test simulates the helper-sourcing sequence and
13
+ # asserts the marker file lands in /tmp.
14
+ # 2. Skeleton-fill ticket shape — captured ticket has Description from
15
+ # $ARGUMENTS plus the deferred-placeholder fields the skill
16
+ # prescribes. Test runs the skeleton-fill command sequence against
17
+ # a fixture description and asserts the resulting file's sections.
18
+ # 3. Next-ID computation — capture-problem reuses the manage-problem
19
+ # Step 3 P056-safe local_max + origin_max formula. Test runs the
20
+ # formula against a fixture problems directory and asserts the
21
+ # computed next ID matches the expected zero-padded value.
22
+ # 4. Conservative title-only duplicate-grep — 3-keyword cap, filename
23
+ # matches only (NOT body). Test runs the grep pattern against a
24
+ # fixture and asserts the conservative match shape.
25
+ #
26
+ # @problem P155
27
+ # @jtbd JTBD-001 (enforce governance without slowing down — lightweight
28
+ # capture path)
29
+ # @jtbd JTBD-006 (progress backlog while AFK — sibling-finding capture
30
+ # in iter subprocesses)
31
+ # @jtbd JTBD-101 (extend the suite — discoverable / on / autocomplete)
32
+ # @adr ADR-032 (governance skill invocation patterns — foreground-
33
+ # lightweight-capture variant)
34
+ # @adr ADR-038 (progressive disclosure — SKILL.md + REFERENCE.md split)
35
+ # @adr ADR-049 (bin/ on PATH — capture-problem reuses existing
36
+ # wr-itil-reconcile-readme shim, no new shim needed)
37
+ # @adr ADR-052 (behavioural-tests-default — these tests exercise
38
+ # primitives, not SKILL.md prose)
39
+ # @adr ADR-119 (manage-problem create-gate — capture-problem composes
40
+ # with the same per-session marker)
41
+
42
+ setup() {
43
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../../.." && pwd)"
44
+ SKILL_DIR="${REPO_ROOT}/packages/itil/skills/capture-problem"
45
+ SKILL_FILE="${SKILL_DIR}/SKILL.md"
46
+ REF_FILE="${SKILL_DIR}/REFERENCE.md"
47
+ CREATE_GATE_LIB="${REPO_ROOT}/packages/itil/hooks/lib/create-gate.sh"
48
+
49
+ # Fresh per-test scratch directory and SESSION_ID.
50
+ TMPROOT=$(mktemp -d)
51
+ TEST_SESSION_ID="capture-problem-bats-$BATS_TEST_NUMBER-$$"
52
+ MARKER_PATH="/tmp/manage-problem-grep-${TEST_SESSION_ID}"
53
+ rm -f "$MARKER_PATH"
54
+ }
55
+
56
+ teardown() {
57
+ rm -rf "$TMPROOT"
58
+ rm -f "$MARKER_PATH"
59
+ }
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Existence / wiring tests — minimum surface required for the skill to be
63
+ # discoverable. Not structural prose-greps; these assert artefacts exist.
64
+ # ---------------------------------------------------------------------------
65
+
66
+ @test "capture-problem: SKILL.md and REFERENCE.md both exist (ADR-038 split)" {
67
+ [ -f "$SKILL_FILE" ]
68
+ [ -f "$REF_FILE" ]
69
+ }
70
+
71
+ @test "capture-problem: SKILL.md frontmatter declares wr-itil:capture-problem name" {
72
+ # Discoverable on / autocomplete depends on the canonical name.
73
+ # ADR-032 names this skill; ADR-010-amended skill-granularity rule.
74
+ run grep -E '^name: wr-itil:capture-problem$' "$SKILL_FILE"
75
+ [ "$status" -eq 0 ]
76
+ }
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # P119 create-gate composition — load-bearing behavioural primitive.
80
+ # capture-problem must call mark_step2_complete before any Write of a new
81
+ # ticket file, otherwise the PreToolUse:Write hook denies the Write.
82
+ # ---------------------------------------------------------------------------
83
+
84
+ @test "capture-problem: mark_step2_complete writes marker the P119 hook checks" {
85
+ # Source the helper the skill prescribes; call the canonical mark
86
+ # function; assert the marker file lands at the path the hook reads.
87
+ source "$CREATE_GATE_LIB"
88
+ mark_step2_complete "$TEST_SESSION_ID"
89
+ [ -f "$MARKER_PATH" ]
90
+ }
91
+
92
+ @test "capture-problem: check_create_gate returns 0 after mark_step2_complete" {
93
+ # Composes with manage-problem-enforce-create.sh which uses
94
+ # check_create_gate $SESSION_ID — exit 0 means "permit Write".
95
+ source "$CREATE_GATE_LIB"
96
+ run check_create_gate "$TEST_SESSION_ID"
97
+ [ "$status" -ne 0 ] # before mark — denied
98
+ mark_step2_complete "$TEST_SESSION_ID"
99
+ run check_create_gate "$TEST_SESSION_ID"
100
+ [ "$status" -eq 0 ] # after mark — permitted
101
+ }
102
+
103
+ @test "capture-problem: mark_step2_complete is idempotent across cross-skill order" {
104
+ # Whether manage-problem fires first then capture-problem, or vice
105
+ # versa, the marker mechanic is a no-op after the first call.
106
+ source "$CREATE_GATE_LIB"
107
+ mark_step2_complete "$TEST_SESSION_ID"
108
+ mark_step2_complete "$TEST_SESSION_ID"
109
+ mark_step2_complete "$TEST_SESSION_ID"
110
+ [ -f "$MARKER_PATH" ]
111
+ }
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Next-ID computation — capture-problem reuses manage-problem Step 3 formula
115
+ # ---------------------------------------------------------------------------
116
+
117
+ @test "capture-problem: next-ID formula is P056-safe (origin/local max + 1)" {
118
+ # Build a fixture problems directory with mixed status suffixes.
119
+ # The formula must pick the max ID across all suffixes and zero-pad.
120
+ mkdir -p "$TMPROOT/docs/problems"
121
+ : > "$TMPROOT/docs/problems/001-foo.closed.md"
122
+ : > "$TMPROOT/docs/problems/042-bar.open.md"
123
+ : > "$TMPROOT/docs/problems/099-baz.known-error.md"
124
+ : > "$TMPROOT/docs/problems/107-qux.verifying.md"
125
+
126
+ # Mirror manage-problem Step 3 local-max formula exactly.
127
+ local_max=$(ls "$TMPROOT/docs/problems"/*.md 2>/dev/null \
128
+ | sed 's/.*\///' \
129
+ | grep -oE '^[0-9]+' \
130
+ | sort -n | tail -1)
131
+ [ "$local_max" = "107" ]
132
+
133
+ # No origin available in the fixture; default to 0 then increment.
134
+ next=$(printf '%03d' $(( $(echo -e "${local_max:-0}\n0" | sort -n | tail -1) + 1 )))
135
+ [ "$next" = "108" ]
136
+ }
137
+
138
+ @test "capture-problem: next-ID handles empty problems dir (first ticket)" {
139
+ mkdir -p "$TMPROOT/docs/problems"
140
+ local_max=$(ls "$TMPROOT/docs/problems"/*.md 2>/dev/null \
141
+ | sed 's/.*\///' \
142
+ | grep -oE '^[0-9]+' \
143
+ | sort -n | tail -1)
144
+ next=$(printf '%03d' $(( $(echo -e "${local_max:-0}\n0" | sort -n | tail -1) + 1 )))
145
+ [ "$next" = "001" ]
146
+ }
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # Conservative duplicate-grep — title-only filename match, 3-keyword cap.
150
+ # Architect Q1 verdict: title-only because conservative threshold rationale
151
+ # (P155 line 24) — false-positives on body text would either over-prompt
152
+ # or be silently swallowed (capture-problem has no AskUserQuestion).
153
+ # ---------------------------------------------------------------------------
154
+
155
+ @test "capture-problem: duplicate-grep matches kebab-cased keywords in filenames" {
156
+ mkdir -p "$TMPROOT/docs/problems"
157
+ : > "$TMPROOT/docs/problems/050-checkpoint-stuck-saving.open.md"
158
+ : > "$TMPROOT/docs/problems/051-foul-drawn-garbled.closed.md"
159
+
160
+ # Description: "checkpoint stuck on save retry" — extract 3 kebab tokens.
161
+ # Title-only grep against filenames; bodies are NOT scanned (conservative).
162
+ match_count=$(ls "$TMPROOT/docs/problems"/*.md \
163
+ | grep -ciE 'checkpoint|stuck|save' || true)
164
+ [ "$match_count" -ge 1 ]
165
+ }
166
+
167
+ @test "capture-problem: duplicate-grep does NOT match keywords in body content (title-only)" {
168
+ mkdir -p "$TMPROOT/docs/problems"
169
+ # File whose title has zero overlap but whose body mentions checkpoint
170
+ cat > "$TMPROOT/docs/problems/060-unrelated.open.md" <<'EOF'
171
+ # Unrelated ticket
172
+ Body mentions checkpoint somewhere but title doesn't.
173
+ EOF
174
+
175
+ # Title-only grep on filenames must NOT match.
176
+ match_count=$(ls "$TMPROOT/docs/problems"/*.md \
177
+ | grep -ciE 'checkpoint' || true)
178
+ [ "$match_count" -eq 0 ]
179
+ }
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # Skeleton-fill ticket shape — capture-problem writes a deferred-placeholder
183
+ # ticket. Default Priority and Effort are flagged for re-rate at next review.
184
+ # ---------------------------------------------------------------------------
185
+
186
+ @test "capture-problem: skeleton-filled ticket carries the deferred-placeholder pattern" {
187
+ mkdir -p "$TMPROOT/docs/problems"
188
+ TITLE="example-aside-finding"
189
+ ID="200"
190
+ TODAY=$(date -u +%Y-%m-%d)
191
+ DESCRIPTION="Quick observation worth a ticket but not blocking."
192
+
193
+ # Mirror the SKILL.md skeleton-fill template.
194
+ cat > "$TMPROOT/docs/problems/${ID}-${TITLE}.open.md" <<EOF
195
+ # Problem ${ID}: ${TITLE}
196
+
197
+ **Status**: Open
198
+ **Reported**: ${TODAY}
199
+ **Priority**: 3 (Medium) — Impact: 3 x Likelihood: 1 (deferred — re-rate at next /wr-itil:review-problems)
200
+ **Effort**: M (deferred — re-rate at next /wr-itil:review-problems)
201
+
202
+ ## Description
203
+
204
+ ${DESCRIPTION}
205
+
206
+ ## Symptoms
207
+
208
+ (deferred to investigation)
209
+
210
+ ## Workaround
211
+
212
+ (deferred to investigation)
213
+
214
+ ## Impact Assessment
215
+
216
+ - **Who is affected**: (deferred to investigation)
217
+ - **Frequency**: (deferred to investigation)
218
+ - **Severity**: (deferred to investigation)
219
+ - **Analytics**: (deferred to investigation)
220
+
221
+ ## Root Cause Analysis
222
+
223
+ ### Investigation Tasks
224
+
225
+ - [ ] Re-rate Priority and Effort at next /wr-itil:review-problems
226
+ - [ ] Investigate root cause
227
+ - [ ] Create reproduction test
228
+
229
+ ## Dependencies
230
+
231
+ - **Blocks**: (none)
232
+ - **Blocked by**: (none)
233
+ - **Composes with**: (none)
234
+
235
+ ## Related
236
+
237
+ (captured via /wr-itil:capture-problem; expand at next investigation)
238
+ EOF
239
+
240
+ # Behavioural assertions: ticket file has the load-bearing fields.
241
+ TICKET="$TMPROOT/docs/problems/${ID}-${TITLE}.open.md"
242
+ [ -f "$TICKET" ]
243
+ run grep -F '**Status**: Open' "$TICKET"
244
+ [ "$status" -eq 0 ]
245
+ # Description survives verbatim
246
+ run grep -F "$DESCRIPTION" "$TICKET"
247
+ [ "$status" -eq 0 ]
248
+ # Deferred placeholders flag re-rating
249
+ run grep -F 'deferred — re-rate at next /wr-itil:review-problems' "$TICKET"
250
+ [ "$status" -eq 0 ]
251
+ # Investigation Tasks nudges user to re-rate
252
+ run grep -F 'Re-rate Priority and Effort at next /wr-itil:review-problems' "$TICKET"
253
+ [ "$status" -eq 0 ]
254
+ }
255
+
256
+ # ---------------------------------------------------------------------------
257
+ # Skill-allowed-tools surface contract — capture-problem MUST NOT carry
258
+ # AskUserQuestion (per design Q4 + ADR-044 framework-mediated mechanical-
259
+ # stage decisions). This is observable from the frontmatter declaration
260
+ # the runtime consumes.
261
+ # ---------------------------------------------------------------------------
262
+
263
+ @test "capture-problem: allowed-tools omits AskUserQuestion (no interactive branches)" {
264
+ # The skill's contract is NO AskUserQuestion at all — duplicate-check,
265
+ # priority-default, effort-default are framework-mediated mechanical
266
+ # stages per ADR-044. AskUserQuestion in allowed-tools would let
267
+ # future drift sneak prompts back in.
268
+ run grep -E '^allowed-tools:' "$SKILL_FILE"
269
+ [ "$status" -eq 0 ]
270
+ run grep -E '^allowed-tools:.*AskUserQuestion' "$SKILL_FILE"
271
+ [ "$status" -ne 0 ]
272
+ }
273
+
274
+ @test "capture-problem: allowed-tools includes Bash (for create-gate marker write)" {
275
+ # mark_step2_complete via Bash is the load-bearing primitive — without
276
+ # Bash in allowed-tools the skill cannot satisfy P119.
277
+ run grep -E '^allowed-tools:.*Bash' "$SKILL_FILE"
278
+ [ "$status" -eq 0 ]
279
+ }
280
+
281
+ @test "capture-problem: allowed-tools includes Write (for new ticket file)" {
282
+ run grep -E '^allowed-tools:.*Write' "$SKILL_FILE"
283
+ [ "$status" -eq 0 ]
284
+ }
285
+
286
+ # ---------------------------------------------------------------------------
287
+ # Deferred-README-refresh contract — distinguishing capture-problem from
288
+ # manage-problem. capture-problem must NOT stage docs/problems/README.md
289
+ # in its commit (deferred to /wr-itil:review-problems).
290
+ # ---------------------------------------------------------------------------
291
+
292
+ @test "capture-problem: SKILL.md prescribes deferred README refresh (no inline P094 block)" {
293
+ # The contract distinction from manage-problem: capture-problem does
294
+ # NOT regenerate README.md inline; it defers to /wr-itil:review-problems.
295
+ # This is a behavioural primitive — a future maintainer who copies the
296
+ # P094 block over would break the lightweight-capture promise.
297
+ # Asserts the SKILL.md names the deferred contract explicitly.
298
+ run grep -F '/wr-itil:review-problems' "$SKILL_FILE"
299
+ [ "$status" -eq 0 ]
300
+ }
@@ -244,6 +244,15 @@ ITER_JSON=$(mktemp)
244
244
  DISPATCH_START_EPOCH=$(date +%s)
245
245
  IDLE_TIMEOUT_S="${WORK_PROBLEMS_IDLE_TIMEOUT_S:-3600}"
246
246
 
247
+ # AFK-iter cross-context-leak guard (ADR-032 P157 amendment, line 127):
248
+ # the orchestrator-session pending-questions queue at
249
+ # .afk-run-state/outstanding-questions.jsonl is for surfacing on the user's
250
+ # next interactive session — NOT inside iter subprocess contexts. The
251
+ # itil-pending-questions-surface.sh SessionStart hook self-suppresses when
252
+ # this env var is set so the orchestrator's accumulated queue does not leak
253
+ # into iter subprocesses' first turn.
254
+ export WR_SUPPRESS_PENDING_QUESTIONS=1
255
+
247
256
  claude -p \
248
257
  --permission-mode bypassPermissions \
249
258
  --output-format json \