@windyroad/itil 0.23.1-preview.251 → 0.23.1-preview.252

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.
@@ -117,5 +117,20 @@ if check_create_gate "$SESSION_ID"; then
117
117
  exit 0
118
118
  fi
119
119
 
120
- 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)"
120
+ # P144 / ADR-048: gate-misfire recovery hint. When SOME marker exists (for
121
+ # any SID) but the gate denies, the agent is likely hitting the P124 Phase 3
122
+ # helper regression — `mark_step2_complete` succeeded but the marker landed
123
+ # under the wrong UUID. Append a recovery pointer to the deny message so
124
+ # the agent finds the documented two-tier procedure in SKILL.md Step 2
125
+ # substep 7 instead of reaching for the brute-force-marker anti-pattern
126
+ # (139-marker incident, 2026-04-28 P144 driver evidence).
127
+ #
128
+ # Routine first-creation deny (no marker for ANY SID in this session)
129
+ # leaves the deny message unchanged — the helper-bug signal is conditional.
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}"
121
136
  exit 0
@@ -186,3 +186,64 @@ set_marker() {
186
186
  [ "$status" -eq 0 ]
187
187
  [[ "$output" != *"BLOCKED"* ]]
188
188
  }
189
+
190
+ # --- P144 / ADR-048: gate-misfire recovery hint on deny message ---
191
+ #
192
+ # When the deny fires AND any /tmp/manage-problem-grep-* marker exists for
193
+ # SOME SID, that's the helper-bug signal (P124 Phase 3 regression — helper
194
+ # returned wrong SID, marker exists but doesn't match runtime hook stdin).
195
+ # The deny message appends a recovery pointer to direct the agent at the
196
+ # documented two-tier procedure in SKILL.md Step 2 substep 7.
197
+ #
198
+ # Routine first-creation deny (no marker exists for any SID at all) is
199
+ # unchanged — recovery hint MUST NOT appear.
200
+
201
+ setup_other_sid_marker() {
202
+ OTHER_SID="other-sid-$$-$RANDOM"
203
+ : > "/tmp/manage-problem-grep-${OTHER_SID}"
204
+ }
205
+
206
+ teardown_other_sid_marker() {
207
+ if [ -n "${OTHER_SID:-}" ]; then
208
+ rm -f "/tmp/manage-problem-grep-${OTHER_SID}"
209
+ fi
210
+ }
211
+
212
+ @test "deny without ANY /tmp/manage-problem-grep-* marker → deny message OMITS recovery hint" {
213
+ # Scrub any markers so the helper-bug signal cannot fire.
214
+ rm -f /tmp/manage-problem-grep-*
215
+ run run_write_hook "$PWD/docs/problems/999-foo.open.md" "$SID"
216
+ [ "$status" -eq 0 ]
217
+ [[ "$output" == *"BLOCKED"* ]]
218
+ # No marker exists for any SID → routine first-creation deny → no recovery hint.
219
+ [[ "$output" != *"SID mismatch"* ]]
220
+ [[ "$output" != *"Step 2 substep 7"* ]]
221
+ }
222
+
223
+ @test "deny with /tmp/manage-problem-grep-* marker for OTHER SID → deny message INCLUDES recovery hint" {
224
+ # Scrub other markers first, then set a marker for a different SID.
225
+ rm -f /tmp/manage-problem-grep-*
226
+ setup_other_sid_marker
227
+ run run_write_hook "$PWD/docs/problems/999-foo.open.md" "$SID"
228
+ status=$?
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".
241
+ rm -f /tmp/manage-problem-grep-*
242
+ setup_other_sid_marker
243
+ run run_write_hook "$PWD/docs/problems/999-foo.open.md" "$SID"
244
+ status=$?
245
+ teardown_other_sid_marker
246
+ [ "$status" -eq 0 ]
247
+ [[ "$output" == *"BLOCKED"* ]]
248
+ [[ "$output" != *"P124-Phase-3-regression"* ]]
249
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.23.1-preview.251",
3
+ "version": "0.23.1-preview.252",
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"
@@ -269,6 +269,47 @@ Before creating, search existing problems for similar issues. The user may not k
269
269
 
270
270
  **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.
271
271
 
272
+ <!-- supersedes-when: P142 ships -->
273
+ **Recovery if hook denial persists** (P144 / ADR-048 — auto-supersedes when P142 ships):
274
+
275
+ 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.
276
+
277
+ **Gate-misfire signal** — recovery applies ONLY when ALL three conditions hold:
278
+ 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).
279
+ 2. `mark_step2_complete` succeeded (the helper exited zero — no SID-discovery error).
280
+ 3. The subsequent `Write` to the new `.<status>.md` file is denied by the P119 hook.
281
+
282
+ 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).
283
+
284
+ **First-tier recovery — announce-marker scrape**:
285
+
286
+ ```bash
287
+ # Discover the orchestrator session UUID via the most-reliable per-session announce marker.
288
+ # The orchestrator SID is what the runtime hook stdin contains in the common subprocess case.
289
+ sid=$(ls -t /tmp/itil-assistant-gate-announced-* 2>/dev/null | head -1 | sed 's|.*itil-assistant-gate-announced-||')
290
+ [ -n "$sid" ] && touch "/tmp/manage-problem-grep-${sid}"
291
+ # Retry the Write.
292
+ ```
293
+
294
+ **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):
295
+
296
+ ```bash
297
+ # The hook is PreToolUse:Write; python3-in-Bash is not a Write tool call,
298
+ # so the hook never fires. Use only when first-tier fails.
299
+ python3 -c "from pathlib import Path; Path('docs/problems/<NNN>-<title>.open.md').write_text('''<file body>''')"
300
+ ```
301
+
302
+ **Audit-trail-preservation test** — the second-tier procedure is sanctioned ONLY in the audit-trail-preserved branch:
303
+
304
+ - ✅ **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.
305
+ - ❌ **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.
306
+
307
+ **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.
308
+
309
+ **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.
310
+
311
+ **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).
312
+
272
313
  **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.
273
314
 
274
315
  **Hook contract (P119)**: writing a `.open.md` (or any `.<status>.md`) file under `docs/problems/` without first running this Step 2 grep + marker-touch is blocked by the `manage-problem-enforce-create.sh` PreToolUse hook with a `permissionDecision: deny` directing the agent back to this skill. Agents that try to bypass the skill (e.g. mid-retrospective inline capture, post-mortem wrap-up, or any "I'll just write it directly" shortcut) will hit the deny and be redirected here. Do not work around the deny by setting the marker manually — the marker exists to record that this Step 2 ran, and a marker without a grep is the audit-trail gap P119 closes.
@@ -0,0 +1,165 @@
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
+ }