@windyroad/itil 0.23.0 → 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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "wr-itil",
3
- "version": "0.23.0",
3
+ "version": "0.23.1",
4
4
  "description": "ITIL-aligned IT service management for Claude Code"
5
5
  }
@@ -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.0",
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
+ }
@@ -415,9 +415,36 @@ After the iteration's commit lands but before starting the next iteration, check
415
415
  2. If `.changeset/` is non-empty after push, run `npm run release:watch` (merge the release PR + wait for npm publish).
416
416
  3. Resume the loop only after the release lands on npm.
417
417
 
418
- **Failure handling**: If `release:watch` fails (CI failure, publish failure), stop the loop and report the failure in the AFK summary. Do not retry non-interactively the user must intervene. **Step 2.5b cross-reference (P126)**: before emitting the final AFK summary for a Failure handling / CI failure / release:watch halt, run Step 2.5b's surfacing routine. The routine is gated on ≥1 accumulated user-answerable skip; this halt path empirically frequently has accumulated skips from prior iters (the original P126 surface), so the gate is normally satisfied and Step 2.5b's AskUserQuestion-default branch fires (`halt-paths-must-route-design-questions-through-Step-2.5b`). The CI-failure cause itself remains a halt with bug-signal Step 2.5b surfaces *prior-iter accumulated user-answerable skips only*; it does NOT ask the user how to remediate the CI failure (that requires the user to inspect the failing CI run on return).
418
+ **Failure handling (P140)**: When `push:watch` or `release:watch` reports a CI failure or publish failure, the orchestrator follows a diagnose-then-classify routing fix-and-continue for the documented mechanically-fixable allow-list, halt for everything else. The previous uniform halt rule converted mechanically-fixable failures (1-line stale-grep-string updates, transient flakes) into ~45min queue stalls, regressing JTBD-006 "Progress the Backlog While I'm Away" without any governance benefit.
419
419
 
420
- `push:watch` and `release:watch` are policy-authorised actions when residual risk is within appetite per RISK-POLICY.md, so no `AskUserQuestion` is required for the drain itself (ADR-013 Rule 5).
420
+ **Diagnostic preamble (ADR-026 grounding)**: orchestrator MUST first fetch the failed CI log via `gh run view <run-id> --log-failed` (or `gh run view --log-failed` against the most recent failure). Read the failure output and classify into ONE of the buckets below. Cite the failed test output verbatim in the fix-and-continue commit message or halt summary so future readers can audit the classification.
421
+
422
+ **Fixable-in-iter allow-list (closed)**: the following classes are policy-authorised silent fix-and-continue per ADR-013 Rule 5. The list is **closed** — adding a new class is itself a deviation-candidate per ADR-044's framework-resolution boundary (surface to user via Step 2.5b's AskUserQuestion-default branch; do NOT auto-extend at agent discretion).
423
+
424
+ - **P081-class stale-grep-string** — structural test runs `grep -F '<literal>'` (or `grep -nE '<pattern>'`) against a SKILL.md / ADR / source file; non-zero return because source was edited and the test's grep string was not. Fix: update the grep string to current source phrasing. Composes with P081 (structural-tests-are-wasteful root cause); fix-and-continue is the stop-gap, P081's full retrofit is the structural elimination.
425
+ - **Hook stub mismatch** — test's mock-stdin field doesn't match current hook expectation (e.g. renamed JSON key, renamed event type). Fix: update the stub.
426
+ - **Test ID drift** — assertion message grep doesn't match a recently-renamed function or symbol. Fix: sed in the test.
427
+ - **Environmental flake** — CI runner intermittent issue (npm registry timeout, GitHub API rate limit, transient infra). Fix: re-trigger the workflow.
428
+
429
+ **Ambiguous classification defaults to halt.** If the failure does not unambiguously match one of the above, the orchestrator halts. No diagnose-then-guess.
430
+
431
+ **Fix-and-continue branch**: for a fixable class:
432
+
433
+ 1. Apply the fix (typically a single `Edit` change).
434
+ 2. Commit the fix through the **standard ADR-014 commit gate flow** — architect / JTBD / risk-scorer review per retry. A gate rejection routes to the halt branch (no retry budget restoration). Each fix-and-continue commit is its own discrete unit of work and rides its own commit through gates per ADR-014 + ADR-042 Rule 3 precedent (retries each ride their own commit).
435
+ 3. `git push` and re-run `npm run push:watch` (or `release:watch` if the failure was on the release-PR side) to wait for CI re-trigger.
436
+ 4. If CI passes, resume the loop (Step 6.75).
437
+ 5. If CI fails again, increment the per-iteration retry counter and return to step 1.
438
+
439
+ **3-retry cap (per iteration, not per failure-class)**: after 3 fix-and-continue attempts in a single Step 6.5 invocation, the orchestrator routes to the halt branch regardless of failure class. Repeated failures of the "same" class are evidence the diagnosis was wrong; halt and surface for user judgment. The cap is per-iteration — a 4th distinct fixable failure in the same drain still halts.
440
+
441
+ **Halt branch (genuinely unrecoverable)**: halt the loop and report the failure in the AFK summary. Do not retry non-interactively. Genuinely-unrecoverable classes include: auth failure (npm token, GitHub credentials), npm publish rejection (version conflict, package access denied), semantic test failure requiring user judgment (not literal-string drift), repeated transient failures (3+ retries, per the cap above), and any failure outside the fixable-in-iter allow-list.
442
+
443
+ **Step 2.5b cross-reference (P126)**: before emitting the final AFK summary for a Failure handling / CI failure / release:watch halt, run Step 2.5b's surfacing routine. The routine is gated on ≥1 accumulated user-answerable skip; this halt path empirically frequently has accumulated skips from prior iters (the original P126 surface), so the gate is normally satisfied and Step 2.5b's AskUserQuestion-default branch fires (`halt-paths-must-route-design-questions-through-Step-2.5b`). The CI-failure cause itself remains a halt with bug-signal — Step 2.5b surfaces *prior-iter accumulated user-answerable skips only*; it does NOT ask the user how to remediate the CI failure (that requires the user to inspect the failing CI run on return).
444
+
445
+ `push:watch` and `release:watch` are policy-authorised actions when residual risk is within appetite per RISK-POLICY.md, so no `AskUserQuestion` is required for the drain itself (ADR-013 Rule 5). The fix-and-continue branch is itself policy-authorised by the closed allow-list above, satisfying ADR-013 Rule 5 without an `AskUserQuestion` round-trip.
446
+
447
+ **Composition notes**: fix-and-continue is the inverse of P132 (over-ask in interactive sessions) on the failure-handling surface — both arise from over-defensive uniform routing where a documented class-policy would empower silent action. Composes with P130 (orchestrator main-turn ask discipline — fix-and-continue does NOT introduce mid-iter asks; the closed allow-list resolves the decision per ADR-044). Cross-references: P081 (stop-gap composition — most fixables are P081-class), P135 (decision-delegation contract — the closed allow-list IS the framework-resolved policy).
421
448
 
422
449
  #### Above-appetite branch (per ADR-042)
423
450
 
@@ -497,6 +524,7 @@ When `AskUserQuestion` is unavailable or the user is AFK, the skill (and the del
497
524
  | Commit when risk within appetite | Auto-commit (manage-problem step 9e fallback) |
498
525
  | Commit when risk above appetite | Skip commit, report uncommitted state |
499
526
  | Pipeline risk at appetite (push or release = 4/25) | Drain release queue (`push:watch` then `release:watch`) before next iteration — per ADR-018 (Step 6.5) |
527
+ | CI failure during Step 6.5 drain (within-appetite branch) | Diagnose via `gh run view --log-failed`, classify against the closed fixable-in-iter allow-list (P081-class stale-grep-string, hook stub mismatch, test ID drift, environmental flake), fix-and-continue for fixable classes (each retry rides its own ADR-014 commit gate), 3-retry cap per iteration, halt for unrecoverable classes. Ambiguous classification defaults to halt. ADR-013 Rule 5 policy-authorised. Per ADR-026 grounding + ADR-044 framework-resolution boundary + P140 (Step 6.5 Failure handling). |
500
528
  | Pipeline risk above appetite (push or release >= 5/25) | Auto-apply scorer remediations incrementally (ADR-042 Rule 2). The agent reads suggestions and decides what to do. Re-score after each apply; drain when within appetite. **Never release above appetite** (ADR-042 Rule 1) — no AskUserQuestion shortcut. Halt the loop with `outcome: halted-above-appetite` if the loop exhausts without convergence (ADR-042 Rule 5). Verification Pending commits excluded from auto-revert (Rule 2b). Per ADR-042 (Step 6.5 Above-appetite branch). |
501
529
  | Origin diverged before start | Pull `--ff-only` if trivial; stop with report (`git log HEAD..origin/<base>` and reverse) if non-fast-forward — per ADR-019 (Step 0) |
502
530
  | Prior-session partial work detected at start (session-continuity dirty: untracked `docs/decisions/*.proposed.md` / `docs/problems/*.md`, `.afk-run-state/iter-*.json` with `is_error: true` or `api_error_status >= 400`, stale `.claude/worktrees/*`, uncommitted SKILL.md/source/ADR edits) | Halt the loop with a structured Prior-Session State report in the AFK summary. Do NOT attempt non-interactive resume. Interactive invocations prompt via `AskUserQuestion` with 4 options (resume / discard / leave-and-lower-priority / halt). Per P109 + ADR-013 Rule 6 (Step 0 session-continuity detection pass). |
@@ -517,7 +545,7 @@ The orchestrator MUST NOT call `AskUserQuestion` between iterations except at th
517
545
  - **Step 0 fetch-failure halt** — `git fetch origin` network failure; halt-with-report so the user retries on return.
518
546
  - **Step 2.5 / Step 2.5b loop-end emit** — accumulated `outstanding_questions` queue presented as batched `AskUserQuestion` (or fallback Outstanding Design Questions table per ADR-013 Rule 6). This is the framework's prescribed user-interaction point; do NOT dilute it by asking earlier.
519
547
  - **Step 6.5 above-appetite Rule 5 halt** — auto-apply loop exhausted without convergence; halt-with-batched-questions per the Step 2.5b cross-reference (Step 2.5b surfaces *prior-iter accumulated user-answerable skips only* — the halt-causing scorer-gap remains a halt-with-bug-signal per ADR-042 Rule 5).
520
- - **Step 6.5 CI-failure / `release:watch` failure halt** — push:watch or release:watch failed; halt-with-batched-questions per the Step 2.5b cross-reference.
548
+ - **Step 6.5 CI-failure / `release:watch` failure halt** — push:watch or release:watch failed AND the failure is genuinely-unrecoverable (outside the fixable-in-iter allow-list, or 3-retry cap reached); halt-with-batched-questions per the Step 2.5b cross-reference. Failures inside the closed allow-list route to fix-and-continue per Step 6.5 Failure handling (P140), not this halt point.
521
549
  - **Step 6.75 dirty-for-unknown-reason halt** — `git status --porcelain` divergence; halt-with-batched-questions per the Step 2.5b cross-reference.
522
550
 
523
551
  **No mid-iter ask points.** Every other point in the orchestrator's main turn (between Step 5 dispatch completing and Step 6.5 release-cadence check; between Step 6.75 verification and Step 7 loop-back; between Step 7 and Step 1 next-iteration; between consecutive iters generally) is a mechanical-stage transition that the framework has already resolved. Do NOT introduce ad-hoc `AskUserQuestion` calls at those points to confirm "is it OK to proceed?" or "want me to start the next iter?" — proceeding IS the framework-resolved default. Continue iterating until quota or stop-condition #1/#2/#3 fires.
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # P140: /wr-itil:work-problems Step 6.5 Failure handling subsection must
4
+ # document diagnose-then-classify routing — fix-and-continue for the
5
+ # documented mechanically-fixable allow-list, halt for everything else.
6
+ #
7
+ # Prior behaviour was a uniform halt-on-CI-failure rule that converted
8
+ # 1-line stale-grep-string updates and transient flakes into ~45min queue
9
+ # stalls, regressing JTBD-006 "Progress the Backlog While I'm Away"
10
+ # without any governance benefit. P140's Phase 1 amendment replaces that
11
+ # uniform rule with a closed allow-list policy authorising silent
12
+ # fix-and-continue per ADR-013 Rule 5, capped at 3 retries per iteration
13
+ # before falling back to the halt branch.
14
+ #
15
+ # Doc-lint contract assertions per ADR-037 Permitted Exception
16
+ # (contract-assertion class — same shape as the P130 / P126 / P135
17
+ # sibling fixtures). The asserted prose IS the load-bearing policy
18
+ # surface — re-reading the SKILL.md is the only way an AFK reader (and
19
+ # the iteration subprocess) learns the fixable-class taxonomy and the
20
+ # retry cap. Behavioural verification is impossible until Phase 2's
21
+ # advisory classifier ships (deferred per the ticket Fix Strategy —
22
+ # observe over 30 days).
23
+ #
24
+ # @problem P140
25
+ # @adr ADR-013 (Rule 5 — policy-authorised silent action)
26
+ # @adr ADR-014 (one-commit-per-iter; retries each ride their own commit)
27
+ # @adr ADR-018 (inter-iteration release cadence; this refines its
28
+ # Failure handling clause)
29
+ # @adr ADR-026 (agent output grounding — diagnostic preamble citation)
30
+ # @adr ADR-037 (skill-testing strategy — contract-assertion class)
31
+ # @adr ADR-042 (above-appetite branch — Rule 3 commit-gate-per-retry
32
+ # precedent composes with this fix-and-continue branch)
33
+ # @adr ADR-044 (decision-delegation contract — framework-resolution
34
+ # boundary; closed allow-list extensions are deviation-candidates)
35
+ # @jtbd JTBD-006 (Progress the Backlog While I'm Away — primary)
36
+ # @jtbd JTBD-001 (Enforce Governance Without Slowing Down — composes;
37
+ # per-retry gates preserve governance)
38
+
39
+ setup() {
40
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../../.." && pwd)"
41
+ SKILL_MD="$REPO_ROOT/packages/itil/skills/work-problems/SKILL.md"
42
+ }
43
+
44
+ @test "work-problems P140: SKILL.md exists" {
45
+ [ -f "$SKILL_MD" ]
46
+ }
47
+
48
+ # ── Failure handling subsection identity ───────────────────────────────────
49
+
50
+ @test "work-problems P140: Step 6.5 Failure handling subsection cites P140" {
51
+ # The amendment must self-identify so future readers tracing back from
52
+ # the ticket find the load-bearing prose without keyword-guessing.
53
+ run grep -nE 'Failure handling.*P140|P140.*Failure handling' "$SKILL_MD"
54
+ [ "$status" -eq 0 ]
55
+ }
56
+
57
+ # ── Diagnostic preamble (ADR-026 grounding) ────────────────────────────────
58
+
59
+ @test "work-problems P140: Failure handling cites gh run view --log-failed as the diagnostic preamble" {
60
+ # ADR-026 grounding: the orchestrator MUST read the actual failure
61
+ # output before classifying. Without this, classification degrades to
62
+ # guess-from-context.
63
+ run grep -nE 'gh run view.*--log-failed' "$SKILL_MD"
64
+ [ "$status" -eq 0 ]
65
+ }
66
+
67
+ @test "work-problems P140: Failure handling cites ADR-026 (grounding) on the diagnostic preamble" {
68
+ # The grounding requirement should cite ADR-026 explicitly so the
69
+ # connection is auditable.
70
+ run grep -nE 'ADR-026' "$SKILL_MD"
71
+ [ "$status" -eq 0 ]
72
+ }
73
+
74
+ # ── Fixable-in-iter allow-list (closed) ────────────────────────────────────
75
+
76
+ @test "work-problems P140: Failure handling names P081-class stale-grep-string as a fixable class" {
77
+ run grep -nE 'P081-class stale-grep-string|stale-grep-string' "$SKILL_MD"
78
+ [ "$status" -eq 0 ]
79
+ }
80
+
81
+ @test "work-problems P140: Failure handling names hook stub mismatch as a fixable class" {
82
+ run grep -niE 'hook stub mismatch' "$SKILL_MD"
83
+ [ "$status" -eq 0 ]
84
+ }
85
+
86
+ @test "work-problems P140: Failure handling names test ID drift as a fixable class" {
87
+ run grep -niE 'test ID drift' "$SKILL_MD"
88
+ [ "$status" -eq 0 ]
89
+ }
90
+
91
+ @test "work-problems P140: Failure handling names environmental flake as a fixable class" {
92
+ run grep -niE 'environmental flake' "$SKILL_MD"
93
+ [ "$status" -eq 0 ]
94
+ }
95
+
96
+ @test "work-problems P140: allow-list is framed as 'closed' (not extensible at agent discretion)" {
97
+ # JTBD review guard-rail: persona could misread "fix-and-continue" as
98
+ # "auto-fix anything" without the closed framing. Future agent edits
99
+ # must not drift the allow-list open without explicit user direction.
100
+ run grep -niE 'allow-list.*closed|closed.*allow-list' "$SKILL_MD"
101
+ [ "$status" -eq 0 ]
102
+ }
103
+
104
+ @test "work-problems P140: extending the allow-list is framed as a deviation-candidate per ADR-044" {
105
+ # ADR-044 framework-resolution boundary: the closed list IS the
106
+ # framework-resolved policy. Adding a class is a direction-setting
107
+ # decision, not a mechanical fix.
108
+ run grep -niE 'deviation-candidate.*ADR-044|ADR-044.*deviation' "$SKILL_MD"
109
+ [ "$status" -eq 0 ]
110
+ }
111
+
112
+ @test "work-problems P140: ambiguous classification defaults to halt (no diagnose-then-guess)" {
113
+ # JTBD review guard-rail (b): without this, the persona-misread risk
114
+ # of "auto-fix anything" re-enters via fuzzy classification.
115
+ run grep -niE 'Ambiguous classification defaults to halt|ambiguous.*halt' "$SKILL_MD"
116
+ [ "$status" -eq 0 ]
117
+ }
118
+
119
+ # ── Fix-and-continue branch ────────────────────────────────────────────────
120
+
121
+ @test "work-problems P140: Failure handling documents a fix-and-continue branch" {
122
+ run grep -niE 'Fix-and-continue branch|fix-and-continue branch' "$SKILL_MD"
123
+ [ "$status" -eq 0 ]
124
+ }
125
+
126
+ @test "work-problems P140: each fix-and-continue retry rides standard ADR-014 commit gate flow (architect / JTBD / risk-scorer)" {
127
+ # Architect-flagged invariant: governance gates MUST run on every
128
+ # retry. The fix-and-continue branch does NOT bypass gates.
129
+ run grep -niE 'standard ADR-014 commit gate flow|ADR-014.*commit gate' "$SKILL_MD"
130
+ [ "$status" -eq 0 ]
131
+ }
132
+
133
+ @test "work-problems P140: ADR-042 Rule 3 commit-gate-per-retry precedent is cross-referenced" {
134
+ # ADR-042 already establishes that retries each ride their own
135
+ # commit through full gate flow. P140 composes with that precedent
136
+ # rather than inventing a new commit-cardinality rule.
137
+ run grep -niE 'ADR-042 Rule 3' "$SKILL_MD"
138
+ [ "$status" -eq 0 ]
139
+ }
140
+
141
+ # ── 3-retry cap (per iteration) ────────────────────────────────────────────
142
+
143
+ @test "work-problems P140: Failure handling caps fix-and-continue at 3 retries" {
144
+ run grep -niE '3-retry cap|3 retr|three retr' "$SKILL_MD"
145
+ [ "$status" -eq 0 ]
146
+ }
147
+
148
+ @test "work-problems P140: 3-retry cap is per-iteration, not per-failure-class" {
149
+ # Without this clarification, an agent could reset the counter on
150
+ # each new failure class and drain budget indefinitely.
151
+ run grep -niE 'per[- ]iteration, not per[- ]failure[- ]class|cap is per[- ]iteration' "$SKILL_MD"
152
+ [ "$status" -eq 0 ]
153
+ }
154
+
155
+ # ── Halt branch preserved ──────────────────────────────────────────────────
156
+
157
+ @test "work-problems P140: Halt branch preserved for genuinely-unrecoverable failures" {
158
+ run grep -niE 'genuinely-unrecoverable|genuinely unrecoverable' "$SKILL_MD"
159
+ [ "$status" -eq 0 ]
160
+ }
161
+
162
+ @test "work-problems P140: Halt branch enumerates auth failure / npm publish rejection / semantic test as unrecoverable" {
163
+ # The halt branch's allow-list mirror — naming the unrecoverable
164
+ # classes makes the boundary auditable.
165
+ run grep -niE 'auth failure|npm publish rejection|semantic test.*judgment' "$SKILL_MD"
166
+ [ "$status" -eq 0 ]
167
+ }
168
+
169
+ # ── Step 2.5b cross-reference preserved (P126) ─────────────────────────────
170
+
171
+ @test "work-problems P140: Halt branch routes through Step 2.5b surfacing routine (P126 preserved)" {
172
+ # The halt branch's existing P126 cross-reference must survive the
173
+ # amendment — surfacing accumulated user-answerable skips before
174
+ # emitting the halt summary remains the contract.
175
+ run grep -nE 'Step 2\.5b cross-reference \(P126\)' "$SKILL_MD"
176
+ [ "$status" -eq 0 ]
177
+ }
178
+
179
+ # ── ADR-013 Rule 5 policy-authorised silent action ─────────────────────────
180
+
181
+ @test "work-problems P140: fix-and-continue branch is policy-authorised per ADR-013 Rule 5" {
182
+ # ADR-044's framework-mediated surface includes "policy-authorised
183
+ # silent proceed" — the closed allow-list IS the policy. Future
184
+ # readers must find the citation to confirm this is not an ad-hoc
185
+ # bypass of Rule 1.
186
+ run grep -nE 'ADR-013 Rule 5|Rule 5 policy-authorised|policy-authorised.*ADR-013' "$SKILL_MD"
187
+ [ "$status" -eq 0 ]
188
+ }
189
+
190
+ # ── Composition cross-references ───────────────────────────────────────────
191
+
192
+ @test "work-problems P140: Failure handling cross-references P081 (stop-gap composition)" {
193
+ # P081 is the structural-tests-are-wasteful root cause. Most
194
+ # P081-class stale-grep-string failures are P081's territory.
195
+ # Fix-and-continue is the stop-gap; P081's full retrofit is the
196
+ # structural elimination.
197
+ run grep -nE 'P081' "$SKILL_MD"
198
+ [ "$status" -eq 0 ]
199
+ }
200
+
201
+ @test "work-problems P140: Failure handling cross-references P135 (decision-delegation contract)" {
202
+ # P135 + ADR-044 frame the closed allow-list as the
203
+ # framework-resolved policy.
204
+ run grep -nE 'P135' "$SKILL_MD"
205
+ [ "$status" -eq 0 ]
206
+ }
207
+
208
+ @test "work-problems P140: Failure handling cross-references P130 (orchestrator main-turn ask discipline)" {
209
+ # P130 ensures fix-and-continue does NOT introduce mid-iter asks —
210
+ # the closed allow-list resolves the decision per ADR-044's
211
+ # framework-resolution boundary.
212
+ run grep -nE 'P130' "$SKILL_MD"
213
+ [ "$status" -eq 0 ]
214
+ }
215
+
216
+ @test "work-problems P140: Failure handling cross-references P132 (over-ask in interactive sessions)" {
217
+ # P140 is the inverse of P132 on the failure-handling surface — both
218
+ # arise from over-defensive uniform routing. Naming the symmetry
219
+ # protects against future drift.
220
+ run grep -nE 'P132' "$SKILL_MD"
221
+ [ "$status" -eq 0 ]
222
+ }
223
+
224
+ # ── Mid-loop ask discipline halt-point bullet narrowed ─────────────────────
225
+
226
+ @test "work-problems P140: Step 6.5 CI-failure halt-point bullet narrows to outside-allow-list / cap-reached scope" {
227
+ # The Mid-loop ask discipline subsection enumerates Step 6.5 CI-
228
+ # failure as a halt point. After P140 the halt fires only on
229
+ # unrecoverable failures — the bullet must reflect that narrower
230
+ # scope, otherwise future readers conclude all CI failures still
231
+ # halt.
232
+ run grep -nE 'fixable-in-iter allow-list|3-retry cap reached|outside the.*allow-list' "$SKILL_MD"
233
+ [ "$status" -eq 0 ]
234
+ }
235
+
236
+ # ── Non-Interactive Decision Making table row ──────────────────────────────
237
+
238
+ @test "work-problems P140: Decision Making table carries a CI-failure-during-Step-6.5-drain row" {
239
+ # The decision table is the AFK reader's quick-reference; without a
240
+ # row here the failure-handling refinement is buried 80 lines up in
241
+ # Step 6.5.
242
+ run grep -nE '\| CI failure during Step 6\.5 drain' "$SKILL_MD"
243
+ [ "$status" -eq 0 ]
244
+ }
245
+
246
+ @test "work-problems P140: Decision Making table row cites the closed fixable-in-iter allow-list" {
247
+ run grep -nE 'closed fixable-in-iter allow-list' "$SKILL_MD"
248
+ [ "$status" -eq 0 ]
249
+ }
250
+
251
+ @test "work-problems P140: Decision Making table row cites the 3-retry cap" {
252
+ run grep -nE 'CI failure during Step 6\.5.*3-retry cap|3-retry cap.*CI failure' "$SKILL_MD"
253
+ [ "$status" -eq 0 ]
254
+ }