@windyroad/itil 0.26.0 → 0.27.0-preview.296

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.
@@ -0,0 +1,374 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # @rfc RFC-002 T3 — Bats fixture audit + dual-tolerant assertions
4
+ # @adr ADR-031 (Problem-ticket directory layout — per-state subdirs)
5
+ # @adr ADR-051 (load-bearing-from-the-start — each SKILL-prescribed
6
+ # enumeration pipeline ships with a behavioural enforcement test
7
+ # exercised against per-state-layout synthetic fixtures, not later
8
+ # by graceful drift discovery during the T5 migration cutover)
9
+ # @adr ADR-052 (behavioural-bats default — these tests run the actual
10
+ # shell pipelines the SKILL.md sites prescribe against synthetic
11
+ # fixtures and assert observable enumeration. They do NOT structurally
12
+ # grep SKILL.md prose for the dual-pattern string, which would be
13
+ # P081-class structural-test-disguised-as-behavioural and was
14
+ # explicitly excluded from T2 per architect finding 3)
15
+ # @adr ADR-014 (single-purpose: one mechanical contract — the
16
+ # SKILL-prescribed pipelines compose with per-state-layout fixtures)
17
+ # @adr ADR-060 (Phase 1 Slice 5 forward-dogfood — T3 commit grain)
18
+ # @problem P069 (driving — flat layout unskimmable; the migration this
19
+ # contract guards is the relief)
20
+ # @problem P081 (no structural-grep on SKILL.md content — this test
21
+ # is the behavioural alternative)
22
+ # @jtbd JTBD-001 (extended scope — multi-commit RFC-grain coordinated
23
+ # change governance; T3 is one of 11 RFC-002 sub-tasks; behavioural
24
+ # coverage is how per-edit governance stays trustworthy across the
25
+ # migration window)
26
+ # @jtbd JTBD-006 (work-backlog-AFK — dual-tolerant pipelines preserve
27
+ # AFK-loop continuity during the T2-to-T6 migration window; without
28
+ # this contract, mid-migration AFK iterations silently miss tickets
29
+ # in the un-migrated layout half)
30
+ # @jtbd JTBD-008 (decompose-fix-into-coordinated-changes — RFC-002 T3
31
+ # is the load-bearing test artefact for the coordinated-change
32
+ # sub-workstream; visible as an RFC-002-T3 entity rather than
33
+ # diffusing across 14 existing files per JTBD review)
34
+ # @jtbd JTBD-101 (extend-the-suite — adopter projects consuming
35
+ # @windyroad/itil at the T2-shipped state must enumerate correctly
36
+ # against their flat-layout tickets AND post-auto-migration per-state
37
+ # tickets; T3 proves both halves)
38
+ #
39
+ # Contract: every SKILL.md call site updated in T2 (commit `0795e91`,
40
+ # 14 SKILL.md surfaces) prescribes a shell pipeline of canonical shape:
41
+ #
42
+ # ls docs/problems/*.<state>.md docs/problems/<state>/*.md 2>/dev/null
43
+ # ls docs/problems/*.md docs/problems/*/*.md 2>/dev/null
44
+ # ls docs/problems/<ID>-*.md docs/problems/*/<ID>-*.md 2>/dev/null
45
+ #
46
+ # T2's `dual-tolerant-glob-rfc-002-t2.bats` exercises the canonical
47
+ # pattern shapes generically. T3 extends that coverage to the
48
+ # end-to-end SKILL-prescribed PIPELINES — the next-ID compute pipeline
49
+ # (`ls X Y | sed | grep -oE | sort -n | tail -1`), the multi-state
50
+ # union form (4-pathspec for open + known-error backlog scan), the
51
+ # verifying-state filter as run-retro Step 4a dispatches it, and the
52
+ # brace-expansion ID + state-set form report-upstream uses. Each test
53
+ # runs the pipeline against three synthetic fixture shapes (flat-only,
54
+ # per-state-only, mixed) and asserts observable enumeration.
55
+ #
56
+ # T6 (post-T5 verification) drops the flat-layout half. This test
57
+ # updates at T6 to single-pattern (per-state only), NOT removed — the
58
+ # contract narrows but the behavioural enforcement remains.
59
+ #
60
+ # CONTRACT NOTE: when one half of the dual-pattern has zero matches
61
+ # in the current fixture (single-layout fixtures), `ls X Y 2>/dev/null`
62
+ # exits nonzero — the unmatched literal pathname propagates to ls's
63
+ # argv and `2>/dev/null` only suppresses the stderr noise, not the
64
+ # exit code. SKILL.md call sites MUST treat STDOUT emptiness as the
65
+ # canonical "no tickets" signal, NOT exit code zero. Test assertions
66
+ # probe stdout content via `run` (which absorbs the exit code into
67
+ # `$status`); `$status` is asserted only in the empty-fixture and
68
+ # missing-ID cases where nonzero exit is the intended contract.
69
+
70
+ setup() {
71
+ REPO_ROOT="$(mktemp -d)"
72
+ cd "$REPO_ROOT"
73
+ mkdir -p docs/problems
74
+ }
75
+
76
+ teardown() {
77
+ cd /
78
+ rm -rf "$REPO_ROOT"
79
+ }
80
+
81
+ # ── fixture builders ─────────────────────────────────────────────────────────
82
+
83
+ build_flat_layout() {
84
+ cat > docs/problems/100-foo.open.md <<'EOF'
85
+ # Problem 100: Foo
86
+ **Status**: Open
87
+ **WSJF**: 5.0
88
+ EOF
89
+ cat > docs/problems/101-bar.known-error.md <<'EOF'
90
+ # Problem 101: Bar
91
+ **Status**: Known Error
92
+ **WSJF**: 4.0
93
+ EOF
94
+ cat > docs/problems/102-baz.verifying.md <<'EOF'
95
+ # Problem 102: Baz
96
+ **Status**: Verification Pending
97
+ EOF
98
+ cat > docs/problems/103-qux.parked.md <<'EOF'
99
+ # Problem 103: Qux
100
+ **Status**: Parked
101
+ EOF
102
+ cat > docs/problems/104-quux.closed.md <<'EOF'
103
+ # Problem 104: Quux
104
+ **Status**: Closed
105
+ EOF
106
+ }
107
+
108
+ build_per_state_layout() {
109
+ mkdir -p docs/problems/open docs/problems/known-error docs/problems/verifying docs/problems/parked docs/problems/closed
110
+ cat > docs/problems/open/200-foo2.md <<'EOF'
111
+ # Problem 200: Foo2
112
+ **Status**: Open
113
+ **WSJF**: 6.0
114
+ EOF
115
+ cat > docs/problems/known-error/201-bar2.md <<'EOF'
116
+ # Problem 201: Bar2
117
+ **Status**: Known Error
118
+ **WSJF**: 3.5
119
+ EOF
120
+ cat > docs/problems/verifying/202-baz2.md <<'EOF'
121
+ # Problem 202: Baz2
122
+ **Status**: Verification Pending
123
+ EOF
124
+ cat > docs/problems/parked/203-qux2.md <<'EOF'
125
+ # Problem 203: Qux2
126
+ **Status**: Parked
127
+ EOF
128
+ cat > docs/problems/closed/204-quux2.md <<'EOF'
129
+ # Problem 204: Quux2
130
+ **Status**: Closed
131
+ EOF
132
+ }
133
+
134
+ build_mixed_layout() {
135
+ build_flat_layout
136
+ build_per_state_layout
137
+ }
138
+
139
+ # ── Pipeline 1: Next-ID compute (manage-problem Step 3 + capture-problem Step 3)
140
+ #
141
+ # SKILL.md prescribes the recursive local_max formula:
142
+ # ls docs/problems/*.md docs/problems/*/*.md 2>/dev/null \
143
+ # | sed 's|.*/||' \
144
+ # | grep -oE '^[0-9]+' \
145
+ # | sort -n | tail -1
146
+ #
147
+ # Architect finding 2 (T2): the recursive enumeration MUST contribute
148
+ # tickets from BOTH layouts to max-ID, otherwise a per-state ticket at
149
+ # ID 204 is invisible to a flat-only-enumerating capture-problem and
150
+ # the next ID re-allocates an already-taken slot. T3 exercises the
151
+ # pipeline against per-state-only AND mixed fixtures to prove the
152
+ # dual-pathspec composes with the downstream sed/grep/sort pipeline.
153
+ # ──────────────────────────────────────────────────────────────────────────────
154
+
155
+ @test "next-ID pipeline: flat-only fixture yields max ID 104" {
156
+ build_flat_layout
157
+ run bash -c "ls docs/problems/*.md docs/problems/*/*.md 2>/dev/null | sed 's|.*/||' | grep -oE '^[0-9]+' | sort -n | tail -1"
158
+ [ "$output" = "104" ]
159
+ }
160
+
161
+ @test "next-ID pipeline: per-state-only fixture yields max ID 204" {
162
+ build_per_state_layout
163
+ run bash -c "ls docs/problems/*.md docs/problems/*/*.md 2>/dev/null | sed 's|.*/||' | grep -oE '^[0-9]+' | sort -n | tail -1"
164
+ [ "$output" = "204" ]
165
+ }
166
+
167
+ @test "next-ID pipeline: mixed fixture yields max ID 204 (recursive enumeration spans both layouts)" {
168
+ # Architect finding 2: a per-state ticket at ID 204 MUST contribute
169
+ # to max-ID even when flat-layout 104 also exists. Drop the per-state
170
+ # half of the dual-pathspec and this test fails — capture-problem
171
+ # would re-allocate ID 105 instead of advancing to 205.
172
+ build_mixed_layout
173
+ run bash -c "ls docs/problems/*.md docs/problems/*/*.md 2>/dev/null | sed 's|.*/||' | grep -oE '^[0-9]+' | sort -n | tail -1"
174
+ [ "$output" = "204" ]
175
+ }
176
+
177
+ @test "next-ID pipeline: empty fixture yields empty result" {
178
+ run bash -c "ls docs/problems/*.md docs/problems/*/*.md 2>/dev/null | sed 's|.*/||' | grep -oE '^[0-9]+' | sort -n | tail -1"
179
+ [ -z "$output" ]
180
+ }
181
+
182
+ # ── Pipeline 2: Open + known-error multi-state union
183
+ # (work-problems Step 1, list-problems live scan)
184
+ #
185
+ # SKILL.md prescribes the 4-pathspec form:
186
+ # ls docs/problems/*.open.md docs/problems/*.known-error.md \
187
+ # docs/problems/open/*.md docs/problems/known-error/*.md 2>/dev/null
188
+ #
189
+ # This is wider than T2's single-state filter — it unions two states
190
+ # across two layouts in one ls invocation. The prove-out shape: the
191
+ # union enumerates open + known-error from BOTH layouts and excludes
192
+ # verifying / parked / closed from BOTH layouts.
193
+ # ──────────────────────────────────────────────────────────────────────────────
194
+
195
+ @test "open+known-error union: per-state-only fixture enumerates 200 and 201" {
196
+ build_per_state_layout
197
+ run bash -c 'ls docs/problems/*.open.md docs/problems/*.known-error.md docs/problems/open/*.md docs/problems/known-error/*.md 2>/dev/null'
198
+ [[ "$output" == *"open/200-foo2.md"* ]]
199
+ [[ "$output" == *"known-error/201-bar2.md"* ]]
200
+ }
201
+
202
+ @test "open+known-error union: per-state-only fixture excludes verifying/parked/closed" {
203
+ build_per_state_layout
204
+ run bash -c 'ls docs/problems/*.open.md docs/problems/*.known-error.md docs/problems/open/*.md docs/problems/known-error/*.md 2>/dev/null'
205
+ [[ "$output" != *"202-baz2"* ]]
206
+ [[ "$output" != *"203-qux2"* ]]
207
+ [[ "$output" != *"204-quux2"* ]]
208
+ }
209
+
210
+ @test "open+known-error union: mixed fixture enumerates all four (100, 101, 200, 201)" {
211
+ build_mixed_layout
212
+ run bash -c 'ls docs/problems/*.open.md docs/problems/*.known-error.md docs/problems/open/*.md docs/problems/known-error/*.md 2>/dev/null'
213
+ [[ "$output" == *"100-foo.open.md"* ]]
214
+ [[ "$output" == *"101-bar.known-error.md"* ]]
215
+ [[ "$output" == *"open/200-foo2.md"* ]]
216
+ [[ "$output" == *"known-error/201-bar2.md"* ]]
217
+ }
218
+
219
+ @test "open+known-error union: mixed fixture excludes verifying/parked/closed from BOTH layouts" {
220
+ build_mixed_layout
221
+ run bash -c 'ls docs/problems/*.open.md docs/problems/*.known-error.md docs/problems/open/*.md docs/problems/known-error/*.md 2>/dev/null'
222
+ [[ "$output" != *"102-baz"* ]]
223
+ [[ "$output" != *"103-qux"* ]]
224
+ [[ "$output" != *"104-quux"* ]]
225
+ [[ "$output" != *"202-baz2"* ]]
226
+ [[ "$output" != *"203-qux2"* ]]
227
+ [[ "$output" != *"204-quux2"* ]]
228
+ }
229
+
230
+ # ── Pipeline 3: Verifying-state filter (run-retro Step 4a)
231
+ #
232
+ # SKILL.md prescribes:
233
+ # ls docs/problems/*.verifying.md docs/problems/verifying/*.md 2>/dev/null
234
+ #
235
+ # run-retro Step 4a uses this to surface verification-close candidates
236
+ # from the session-context evidence. T3 proves the pipeline finds the
237
+ # verifying ticket in BOTH layouts independently.
238
+ # ──────────────────────────────────────────────────────────────────────────────
239
+
240
+ @test "verifying-state pipeline: flat-only fixture finds 102" {
241
+ build_flat_layout
242
+ run bash -c 'ls docs/problems/*.verifying.md docs/problems/verifying/*.md 2>/dev/null'
243
+ [[ "$output" == *"102-baz.verifying.md"* ]]
244
+ }
245
+
246
+ @test "verifying-state pipeline: per-state-only fixture finds 202" {
247
+ build_per_state_layout
248
+ run bash -c 'ls docs/problems/*.verifying.md docs/problems/verifying/*.md 2>/dev/null'
249
+ [[ "$output" == *"verifying/202-baz2.md"* ]]
250
+ }
251
+
252
+ @test "verifying-state pipeline: mixed fixture finds 102 AND 202" {
253
+ build_mixed_layout
254
+ run bash -c 'ls docs/problems/*.verifying.md docs/problems/verifying/*.md 2>/dev/null'
255
+ [[ "$output" == *"102-baz.verifying.md"* ]]
256
+ [[ "$output" == *"verifying/202-baz2.md"* ]]
257
+ }
258
+
259
+ # ── Pipeline 4: ID-anchored ticket lookup
260
+ # (manage-problem ticket-by-ID, link-incident, close-incident,
261
+ # transition-problem Step 2, capture-problem Step 2 dup-detect)
262
+ #
263
+ # SKILL.md prescribes:
264
+ # ls docs/problems/<ID>-*.md docs/problems/*/<ID>-*.md 2>/dev/null
265
+ #
266
+ # T3 exercises lookup of a known-existing ID across both layouts and
267
+ # the missing-ID case (asserts empty stdout + nonzero exit).
268
+ # ──────────────────────────────────────────────────────────────────────────────
269
+
270
+ @test "ID-anchored pipeline: per-state-only fixture finds 200 in subdir" {
271
+ build_per_state_layout
272
+ run bash -c 'ls docs/problems/200-*.md docs/problems/*/200-*.md 2>/dev/null'
273
+ [[ "$output" == *"open/200-foo2.md"* ]]
274
+ }
275
+
276
+ @test "ID-anchored pipeline: mixed fixture finds 100 (flat) and 200 (per-state)" {
277
+ build_mixed_layout
278
+ run bash -c 'ls docs/problems/100-*.md docs/problems/*/100-*.md 2>/dev/null'
279
+ [[ "$output" == *"100-foo.open.md"* ]]
280
+ run bash -c 'ls docs/problems/200-*.md docs/problems/*/200-*.md 2>/dev/null'
281
+ [[ "$output" == *"open/200-foo2.md"* ]]
282
+ }
283
+
284
+ @test "ID-anchored pipeline: missing ID yields empty stdout + nonzero exit" {
285
+ build_per_state_layout
286
+ run bash -c 'ls docs/problems/999-*.md docs/problems/*/999-*.md 2>/dev/null'
287
+ [ -z "$output" ]
288
+ [ "$status" -ne 0 ]
289
+ }
290
+
291
+ # ── Pipeline 5: Brace-expansion ID + state-set (report-upstream)
292
+ #
293
+ # SKILL.md prescribes:
294
+ # ls docs/problems/<ID>-*.{open,known-error,verifying,closed}.md \
295
+ # docs/problems/*/<ID>-*.md 2>/dev/null
296
+ #
297
+ # The flat half restricts to a state-set (excludes parked); the
298
+ # per-state half is unrestricted (no state filter). T3 proves the
299
+ # brace expansion composes with the per-state pathspec without false
300
+ # positives across the migration window.
301
+ # ──────────────────────────────────────────────────────────────────────────────
302
+
303
+ @test "brace-id-state pipeline: per-state-only fixture finds 200 via per-state half" {
304
+ build_per_state_layout
305
+ run bash -c 'ls docs/problems/200-*.{open,known-error,verifying,closed}.md docs/problems/*/200-*.md 2>/dev/null'
306
+ [[ "$output" == *"open/200-foo2.md"* ]]
307
+ }
308
+
309
+ @test "brace-id-state pipeline: mixed fixture finds flat 100 (open) and per-state 200 (open)" {
310
+ build_mixed_layout
311
+ run bash -c 'ls docs/problems/100-*.{open,known-error,verifying,closed}.md docs/problems/*/100-*.md 2>/dev/null'
312
+ [[ "$output" == *"100-foo.open.md"* ]]
313
+ run bash -c 'ls docs/problems/200-*.{open,known-error,verifying,closed}.md docs/problems/*/200-*.md 2>/dev/null'
314
+ [[ "$output" == *"open/200-foo2.md"* ]]
315
+ }
316
+
317
+ @test "brace-id-state pipeline: parked ticket excluded from flat half but matched in per-state half" {
318
+ # Flat 103 is parked (excluded by the brace state-set); per-state 203
319
+ # is parked but the per-state pathspec is state-unrestricted, so it
320
+ # MUST surface. This is the contract architect finding 2 codifies.
321
+ build_mixed_layout
322
+ run bash -c 'ls docs/problems/103-*.{open,known-error,verifying,closed}.md docs/problems/*/103-*.md 2>/dev/null'
323
+ [[ "$output" != *"103-qux.parked.md"* ]]
324
+ run bash -c 'ls docs/problems/203-*.{open,known-error,verifying,closed}.md docs/problems/*/203-*.md 2>/dev/null'
325
+ [[ "$output" == *"parked/203-qux2.md"* ]]
326
+ }
327
+
328
+ # ── Pipeline 6: Closed-ticket ID-anchored lookup
329
+ # (review-problems Step 5: closed-section rendering uses
330
+ # `docs/problems/*.closed.md docs/problems/closed/*.md`)
331
+ # ──────────────────────────────────────────────────────────────────────────────
332
+
333
+ @test "closed-ticket pipeline: mixed fixture enumerates flat 104 and per-state 204" {
334
+ build_mixed_layout
335
+ run bash -c 'ls docs/problems/*.closed.md docs/problems/closed/*.md 2>/dev/null'
336
+ [[ "$output" == *"104-quux.closed.md"* ]]
337
+ [[ "$output" == *"closed/204-quux2.md"* ]]
338
+ }
339
+
340
+ @test "closed-ticket pipeline: per-state-only fixture excludes other-state subdirs" {
341
+ build_per_state_layout
342
+ run bash -c 'ls docs/problems/*.closed.md docs/problems/closed/*.md 2>/dev/null'
343
+ [[ "$output" == *"closed/204-quux2.md"* ]]
344
+ [[ "$output" != *"open/200"* ]]
345
+ [[ "$output" != *"known-error/201"* ]]
346
+ [[ "$output" != *"verifying/202"* ]]
347
+ [[ "$output" != *"parked/203"* ]]
348
+ }
349
+
350
+ # ── Pipeline 7: Parked-state filter (review-problems Step 3 parked section)
351
+ # ──────────────────────────────────────────────────────────────────────────────
352
+
353
+ @test "parked-state pipeline: mixed fixture enumerates flat 103 and per-state 203" {
354
+ build_mixed_layout
355
+ run bash -c 'ls docs/problems/*.parked.md docs/problems/parked/*.md 2>/dev/null'
356
+ [[ "$output" == *"103-qux.parked.md"* ]]
357
+ [[ "$output" == *"parked/203-qux2.md"* ]]
358
+ }
359
+
360
+ # ── Composition: ls-with-2>/dev/null exit-code semantics
361
+ # (mirrors T2's empty-fixture contract; proves the SKILL.md contract
362
+ # remains "stdout content is the signal, not exit code")
363
+ # ──────────────────────────────────────────────────────────────────────────────
364
+
365
+ @test "all pipelines: empty fixture produces empty stdout across every shape" {
366
+ run bash -c "ls docs/problems/*.md docs/problems/*/*.md 2>/dev/null | sed 's|.*/||' | grep -oE '^[0-9]+' | sort -n | tail -1"
367
+ [ -z "$output" ]
368
+ run bash -c 'ls docs/problems/*.open.md docs/problems/*.known-error.md docs/problems/open/*.md docs/problems/known-error/*.md 2>/dev/null'
369
+ [ -z "$output" ]
370
+ run bash -c 'ls docs/problems/*.verifying.md docs/problems/verifying/*.md 2>/dev/null'
371
+ [ -z "$output" ]
372
+ run bash -c 'ls docs/problems/100-*.md docs/problems/*/100-*.md 2>/dev/null'
373
+ [ -z "$output" ]
374
+ }
@@ -23,7 +23,7 @@ This skill is the foreground-lightweight-capture variant of `/wr-itil:manage-pro
23
23
 
24
24
  ## Rule 6 audit (per ADR-032 + ADR-013)
25
25
 
26
- This skill has **zero AskUserQuestion branches** by design. Each potentially-interactive decision is framework-mediated per ADR-044:
26
+ This skill has **one classification-only AskUserQuestion (type-tag, taste authority per ADR-044 category 5) and zero control-flow branches keyed on the answer**. Each potentially-interactive decision is framework-mediated per ADR-044:
27
27
 
28
28
  | Decision | Resolution |
29
29
  |----------|-----------|
@@ -32,8 +32,9 @@ This skill has **zero AskUserQuestion branches** by design. Each potentially-int
32
32
  | Effort default | Framework-policy: `M` flagged "deferred — re-rate at next /wr-itil:review-problems". |
33
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
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
+ | Type classification (P170 / ADR-060 item 8c) | Taste authority per ADR-044 category 5. AskUserQuestion fires for `type` ∈ {`technical`, `user-business`} when no caller-side flag pre-resolved it. `--type=<value>` flag pre-resolves the answer (silent-proceed). `--no-prompt` flag defaults to `technical` (silent-proceed). Maintainer-side ONLY: this prompt is paired with JTBD-301 protection — `.github/ISSUE_TEMPLATE/problem-report.yml` (plugin-user-side intake) MUST NOT carry an equivalent type selector; triage assigns the type during `/wr-itil:manage-problem` ingestion of user-reported issues. **I2 invariant** (ADR-060 line 98): the prompt is a classification facet, not a workflow split — Steps 0-7 control-flow is identical regardless of the chosen `type_value`; only the substituted value in the Step 4 skeleton template differs. |
35
36
 
36
- Per ADR-013 Rule 6 fail-safe: every branch above resolves without user input, so AFK and interactive contexts behave identically.
37
+ Per ADR-013 Rule 6 fail-safe: every decision above resolves without interactive user input in non-interactive contexts (the type-tag carve-out resolves to `technical` via `--no-prompt` or `--type=` caller-side pre-resolution). AFK orchestrators MUST pass `--no-prompt` or `--type=<value>` per JTBD-006 § Persona Constraints; AFK callers that omit both flags violate the caller-side contract. Interactive and pre-resolved AFK paths produce identical observable outputs except for the `**Type**:` field value, satisfying the I2 invariant by construction.
37
38
 
38
39
  ## Steps
39
40
 
@@ -54,18 +55,42 @@ if [ "$reconcile_exit" -eq 1 ]; then
54
55
  fi
55
56
  ```
56
57
 
57
- ### 1. Parse the description from `$ARGUMENTS`
58
+ ### 1. Parse the description and flags from `$ARGUMENTS`
58
59
 
59
- The description is the full free-text payload from `$ARGUMENTS`. Empty arguments halts per the Rule 6 audit above.
60
+ `$ARGUMENTS` may carry up to two leading flags before the free-text description (caller-side pre-resolution per ADR-044 silent-proceed shape):
61
+
62
+ | Flag | Effect on Step 1.5 |
63
+ |------|-------------------|
64
+ | `--type=technical` | Pre-resolves type to `technical`; Step 1.5 skips the AskUserQuestion. |
65
+ | `--type=user-business` | Pre-resolves type to `user-business`; Step 1.5 skips the AskUserQuestion. |
66
+ | `--no-prompt` | Pre-resolves type to `technical` (default); Step 1.5 skips the AskUserQuestion. |
67
+
68
+ Strip recognised leading flags from `$ARGUMENTS`; the remainder (after flags) is the free-text description. If both `--type=<value>` and `--no-prompt` are present, `--type=<value>` wins (more specific). Unknown leading flags halt-with-stderr-directive: print "capture-problem: unknown flag '<flag>' — recognised flags: --type=technical, --type=user-business, --no-prompt" and exit.
69
+
70
+ Empty description (post-flag-strip) halts per the Rule 6 audit above.
60
71
 
61
72
  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
73
 
74
+ ### 1.5 Type classification (taste authority per ADR-044 category 5)
75
+
76
+ Resolve `type_value` ∈ {`technical`, `user-business`} per the following framework-mediated dispatch:
77
+
78
+ 1. **If `--type=<value>` was set in Step 1**: use that value; do NOT fire AskUserQuestion (silent-proceed per ADR-013 Rule 5).
79
+ 2. **Else if `--no-prompt` was set in Step 1**: default `type_value = technical`; do NOT fire AskUserQuestion. JTBD-006 protection: AFK orchestrators MUST pass this flag (or `--type=<value>`).
80
+ 3. **Else** (interactive context, no caller-side pre-resolution): fire AskUserQuestion with options `technical` (default) and `user-business`. Question text: *"What type of problem is this?"* Per-option descriptions:
81
+ - `technical` — *"Bug, defect, broken behaviour, framework drift — root cause sits in code or process."*
82
+ - `user-business` — *"Missing capability, UX gap, adopter friction, JTBD-shaped need — root cause sits in unmet user need."*
83
+
84
+ **I2 invariant guard (ADR-060 line 98)**: the resolved `type_value` is used at Step 4 ONLY as a substituted string in the skeleton template's `**Type**:` body field. Steps 2, 3, 4 (other than the `**Type**:` substitution), 5, 6, 7 execute identically regardless of `type_value`. The skill carries NO control-flow branch keyed on `type` — that would convert classification into a workflow split and violate I2. Pure-bash supporting-script enforcement of this invariant lives in `packages/itil/scripts/test/i2-no-type-branching.bats`; the SKILL.md surface coverage gap is named at P176 (descendant of P012 master harness).
85
+
86
+ **JTBD-301 scope guard**: this prompt fires on the maintainer-side `/wr-itil:capture-problem` skill only. The plugin-user-side intake (`.github/ISSUE_TEMPLATE/problem-report.yml`) MUST NOT carry an equivalent type selector — plugin-user persona constraint is "no pre-classification". Triage assigns `type` during `/wr-itil:manage-problem` ingestion of user-reported issues, not at user-report time.
87
+
63
88
  ### 2. Minimal-grep duplicate check (3-keyword title-only)
64
89
 
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):
90
+ Extract up to **3 distinct kebab-cased non-stopword keywords** from the description. Grep the **filenames** of `docs/problems/*.md` AND `docs/problems/<state>/*.md` (NOT bodies — title-only is the conservative threshold per architect verdict on Q1; dual-tolerant per RFC-002 migration window):
66
91
 
67
92
  ```bash
68
- match_count=$(ls docs/problems/*.md 2>/dev/null \
93
+ match_count=$(ls docs/problems/*.md docs/problems/*/*.md 2>/dev/null \
69
94
  | grep -ciE 'kw1|kw2|kw3' || true)
70
95
  ```
71
96
 
@@ -90,8 +115,16 @@ The marker is shared between `manage-problem` and `capture-problem` per ADR-032
90
115
  Same P056-safe local_max + origin_max formula as `/wr-itil:manage-problem` Step 3:
91
116
 
92
117
  ```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)
118
+ # Dual-tolerant ticket enumeration (RFC-002 migration window). Both
119
+ # halves of the OR contribute to next-ID compute flat-layout 104 +
120
+ # per-state 204 BOTH appear in `local_max` so the next-ID compute
121
+ # never re-allocates an already-taken ID. Architect finding 2 (RFC-002
122
+ # T2) — capture-problem's next-ID surface is a separate ADR-031
123
+ # contract from generic enumeration; missing the per-state half
124
+ # regresses ID allocation. The `git ls-tree -r` recursive flag
125
+ # extends the same coverage to the origin tree.
126
+ local_max=$(ls docs/problems/*.md docs/problems/*/*.md 2>/dev/null | sed 's|.*/||' | grep -oE '^[0-9]+' | sort -n | tail -1)
127
+ origin_max=$(git ls-tree -r --name-only origin/main docs/problems/ 2>/dev/null | sed 's|.*/||' | grep -oE '^[0-9]+' | sort -n | tail -1)
95
128
  next=$(printf '%03d' $(( $(echo -e "${local_max:-0}\n${origin_max:-0}" | sort -n | tail -1) + 1 )))
96
129
  ```
97
130
 
@@ -110,10 +143,11 @@ Log the renumber decision in the operation report if origin and local diverged.
110
143
  **Reported**: <YYYY-MM-DD>
111
144
  **Priority**: 3 (Medium) — Impact: 3 x Likelihood: 1 (deferred — re-rate at next /wr-itil:review-problems)
112
145
  **Effort**: M (deferred — re-rate at next /wr-itil:review-problems)
146
+ **Type**: <type_value>
113
147
 
114
148
  ## Description
115
149
 
116
- <full description from $ARGUMENTS>
150
+ <full description from $ARGUMENTS, with leading recognised flags stripped>
117
151
 
118
152
  ## Symptoms
119
153
 
@@ -192,7 +226,9 @@ The trailing pointer is **not optional** — it is the user-visible signal that
192
226
  |---------|----------------|-----------------|
193
227
  | Duplicate-check | Wide-net grep + AskUserQuestion branch on matches | 3-keyword title-only grep, list-only (no branch) |
194
228
  | 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 |
229
+ | Skeleton-fill | Full-intake; AskUserQuestion for missing fields | Deferred-placeholder pattern + one classification-only AskUserQuestion (type-tag) |
230
+ | Type-tag prompt | Step 4-equivalent AskUserQuestion fires alongside other intake fields | Step 1.5 classification-only AskUserQuestion; `--type=` and `--no-prompt` flags pre-resolve for non-interactive callers; I2 invariant: no control-flow branch keyed on type |
231
+ | AskUserQuestion authority | Multiple branches (deviation-approval / direction-setting / taste / mechanical) | Exactly one classification-only fire (taste, ADR-044 cat. 5); zero control-flow branches |
196
232
  | README refresh | P094 inline (regenerate + stage in same commit) | Deferred to next `/wr-itil:review-problems` |
197
233
  | Status transitions | Step 7 owns Open → Known Error → Verifying → Closed | Out of scope (creation only) |
198
234
  | Commit grain | One commit per intake (or per split-concern set) | One commit per capture |
@@ -206,12 +242,17 @@ The two skills share the `/tmp/manage-problem-grep-${SESSION_ID}` create-gate ma
206
242
  - **P014** (`docs/problems/014-aside-invocation-for-governance-skills.open.md`) — parent / master tracker.
207
243
  - **P078** — capture-on-correction OFFER pattern; depends on capture-problem shipping.
208
244
  - **P119** — manage-problem create-gate hook; capture-problem composes with the same marker.
245
+ - **P170** (`docs/problems/170-...open.md`) — RFC framework driver; Slice 4 B7.T3 / item 8c authored the type-classification prompt at Step 1.5.
246
+ - **P176** — agent-side I2 (no type-branching) coverage gap on the SKILL.md surface (this file's surface); descendant of P012 master harness ticket. The Step 1.5 I2 invariant guard is enforced by audit-trailed prose here per ADR-052 § Surface 2 escape-hatch contract; behavioural enforcement awaits the master harness.
209
247
  - **ADR-032** (`docs/decisions/032-governance-skill-invocation-patterns.proposed.md`) — foreground-lightweight-capture variant amendment.
210
248
  - **ADR-038** — progressive-disclosure pattern (SKILL.md + REFERENCE.md split).
211
- - **ADR-044** — decision-delegation contract (framework-mediated mechanical-stage carve-outs).
249
+ - **ADR-044** — decision-delegation contract; type classification is taste authority per category 5; `--no-prompt` / `--type=<value>` are policy-authorised silent-proceed shapes per category 4.
212
250
  - **ADR-049** — bin/ on PATH; capture-problem reuses the existing `wr-itil-reconcile-readme` shim.
213
- - **ADR-052** — behavioural-tests-default for skill testing.
251
+ - **ADR-052** — behavioural-tests-default for skill testing; SKILL.md I2 surface coverage gap is named, not silent (P176 + ADR-052 § Surface 2).
252
+ - **ADR-060** (`docs/decisions/060-...accepted.md`) — Phase 1 item 8c authored Step 1.5 here; I2 invariant (line 98) governs the no-control-flow-branch contract; line 132 names the maintainer-side-only / JTBD-301-protection scope; line 160 (Confirmation criterion 4) gates the type-prompt placement.
253
+ - **JTBD-301** (`docs/jtbd/plugin-user/JTBD-301-...md`) — plugin-user no-pre-classification persona constraint; protected by the Step 1.5 maintainer-side scope guard.
214
254
  - `packages/itil/skills/manage-problem/SKILL.md` — heavyweight intake counterpart.
215
255
  - `packages/itil/skills/review-problems/SKILL.md` — re-rates the deferred placeholders + refreshes README.md.
256
+ - `packages/itil/scripts/test/i2-no-type-branching.bats` — pure-bash supporting-script enforcement of the I2 invariant; this SKILL.md change does not affect any pure-bash script and so does not change the bats outcome (still green).
216
257
 
217
258
  $ARGUMENTS
@@ -89,8 +89,10 @@ Derive a kebab-case title slug from the first 8-10 non-stopword tokens of `$desc
89
89
  For each `P<NNN>` in the trace list:
90
90
 
91
91
  ```bash
92
- # Check existence in any lifecycle status
93
- trace_files=$(ls docs/problems/<NNN>-*.md 2>/dev/null)
92
+ # Check existence in any lifecycle status (dual-tolerant — RFC-002
93
+ # migration window covers BOTH flat `docs/problems/<NNN>-<title>.<state>.md`
94
+ # AND per-state subdir `docs/problems/<state>/<NNN>-<title>.md` layouts).
95
+ trace_files=$(ls docs/problems/<NNN>-*.md docs/problems/*/<NNN>-*.md 2>/dev/null)
94
96
  ```
95
97
 
96
98
  **I1 hard-block (per ADR-060 § Confirmation criterion 1)**:
@@ -198,7 +200,8 @@ For each problem ID in `$problem_trace`, invoke the helper before commit:
198
200
  ```bash
199
201
  for pid_token in $(echo "$problem_trace" | tr ',' ' '); do
200
202
  pid_num="${pid_token#P}"
201
- problem_file=$(ls docs/problems/${pid_num}-*.md 2>/dev/null | head -1)
203
+ # Dual-tolerant ticket discovery (RFC-002 migration window).
204
+ problem_file=$(ls docs/problems/${pid_num}-*.md docs/problems/*/${pid_num}-*.md 2>/dev/null | head -1)
202
205
  [ -z "$problem_file" ] && continue
203
206
  bash "$(wr-itil-script-path 2>/dev/null || echo packages/itil/scripts)/update-problem-rfcs-section.sh" "$problem_file" docs/rfcs
204
207
  git add "$problem_file"
@@ -63,10 +63,10 @@ Branch on which section is present:
63
63
  **Case A — `## Linked Problem` present**:
64
64
 
65
65
  1. Extract the problem ID `P<NNN>` from the section.
66
- 2. Locate the problem file:
66
+ 2. Locate the problem file. Dual-tolerant lookup spans flat layout AND per-state subdir layout (RFC-002 migration window):
67
67
 
68
68
  ```bash
69
- ls docs/problems/<NNN>-*.md 2>/dev/null | head -1
69
+ ls docs/problems/<NNN>-*.md docs/problems/*/<NNN>-*.md 2>/dev/null | head -1
70
70
  ```
71
71
 
72
72
  Accept both `P<NNN>-*.md` and `<NNN>-*.md` naming conventions for robustness.
@@ -42,11 +42,13 @@ The link operation works on any incident status — investigating, mitigating, r
42
42
 
43
43
  ### 3. Locate the problem file
44
44
 
45
+ Dual-tolerant lookup spans flat layout AND per-state subdir layout (RFC-002 migration window):
46
+
45
47
  ```bash
46
- ls docs/problems/<MMM>-*.md 2>/dev/null | head -1
48
+ ls docs/problems/<MMM>-*.md docs/problems/*/<MMM>-*.md 2>/dev/null | head -1
47
49
  ```
48
50
 
49
- Accept any lifecycle suffix: `.open.md`, `.known-error.md`, `.verifying.md`, `.closed.md`.
51
+ Accept any lifecycle suffix: `.open.md`, `.known-error.md`, `.verifying.md`, `.closed.md` in flat layout; bare `<MMM>-<title>.md` under `docs/problems/<state>/` in per-state layout.
50
52
 
51
53
  - If no file matches, report "No problem `P<MMM>` found. Check `/wr-itil:list-problems` for the current backlog." and exit.
52
54
  - Read the problem file's title from the `# Problem <MMM>: <Title>` header line.
@@ -12,15 +12,16 @@ This skill is the P071 phased-landing split of `/wr-itil:manage-problem list` pe
12
12
 
13
13
  ## Scope
14
14
 
15
- Included in the ranking table:
16
- - `docs/problems/*.open.md` — open tickets (under investigation)
17
- - `docs/problems/*.known-error.md` — known errors (root cause confirmed, fix NOT yet released)
15
+ Included in the ranking table (RFC-002 migration window — each glob is dual-tolerant, covering BOTH the flat `docs/problems/<NNN>-<title>.<state>.md` filename-suffix layout AND the per-state subdir `docs/problems/<state>/<NNN>-<title>.md` layout):
16
+
17
+ - `docs/problems/*.open.md` + `docs/problems/open/*.md` open tickets (under investigation)
18
+ - `docs/problems/*.known-error.md` + `docs/problems/known-error/*.md` — known errors (root cause confirmed, fix NOT yet released)
18
19
 
19
20
  Shown in separate sections, excluded from the dev-work WSJF ranking per ADR-022:
20
- - `docs/problems/*.verifying.md` — Verification Pending (fix released, awaiting user verification; WSJF multiplier 0)
21
- - `docs/problems/*.parked.md` — Parked on upstream or user-suspended (WSJF multiplier 0)
21
+ - `docs/problems/*.verifying.md` + `docs/problems/verifying/*.md` — Verification Pending (fix released, awaiting user verification; WSJF multiplier 0)
22
+ - `docs/problems/*.parked.md` + `docs/problems/parked/*.md` — Parked on upstream or user-suspended (WSJF multiplier 0)
22
23
 
23
- `docs/problems/*.closed.md` is omitted entirely (the view is of active backlog, not the closed archive).
24
+ `docs/problems/*.closed.md` + `docs/problems/closed/*.md` is omitted entirely (the view is of active backlog, not the closed archive).
24
25
 
25
26
  ## Steps
26
27
 
@@ -31,7 +32,10 @@ Reuse the same `git log`-based freshness test as `/wr-itil:manage-problem review
31
32
  ```bash
32
33
  readme_commit=$(git log -1 --format=%H -- docs/problems/README.md 2>/dev/null)
33
34
  if [ -z "$readme_commit" ] || \
34
- git log --oneline "${readme_commit}..HEAD" -- 'docs/problems/*.md' ':!docs/problems/README.md' 2>/dev/null | grep -q .; then
35
+ git log --oneline "${readme_commit}..HEAD" -- 'docs/problems/*.md' 'docs/problems/*/*.md' ':!docs/problems/README.md' 2>/dev/null | grep -q .; then
36
+ # Pathspec pair `'docs/problems/*.md' 'docs/problems/*/*.md'` is the
37
+ # RFC-002 dual-tolerant transitional shape — covers BOTH the flat
38
+ # layout AND the per-state subdir layout.
35
39
  echo "stale"
36
40
  fi
37
41
  ```
@@ -42,12 +46,12 @@ fi
42
46
 
43
47
  ### 2. Live scan (cache-stale fallback)
44
48
 
45
- Enumerate the backlog files via glob:
49
+ Enumerate the backlog files via dual-tolerant globs (RFC-002 migration window — each line covers BOTH the flat `<NNN>-<title>.<state>.md` filename-suffix layout AND the per-state subdir `<state>/<NNN>-<title>.md` layout):
46
50
 
47
51
  ```bash
48
- ls docs/problems/*.open.md docs/problems/*.known-error.md 2>/dev/null
49
- ls docs/problems/*.verifying.md 2>/dev/null # for Verification Queue section
50
- ls docs/problems/*.parked.md 2>/dev/null # for Parked section
52
+ ls docs/problems/*.open.md docs/problems/*.known-error.md docs/problems/open/*.md docs/problems/known-error/*.md 2>/dev/null
53
+ ls docs/problems/*.verifying.md docs/problems/verifying/*.md 2>/dev/null # for Verification Queue section
54
+ ls docs/problems/*.parked.md docs/problems/parked/*.md 2>/dev/null # for Parked section
51
55
  ```
52
56
 
53
57
  For each `.open.md` and `.known-error.md` file, read the `**Status**`, `**Priority**`, `**Effort**`, and `**WSJF**` lines from the frontmatter section. Compute WSJF if missing: `WSJF = (Severity × StatusMultiplier) / EffortDivisor` per `/wr-itil:manage-problem` WSJF Prioritisation. Default to M (divisor 2) when Effort is absent; flag missing scores so the user knows a review is overdue.