@windyroad/itil 0.57.3-preview.933 → 0.58.0-preview.939

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.
@@ -497,5 +497,5 @@
497
497
  }
498
498
  },
499
499
  "name": "wr-itil",
500
- "version": "0.57.3"
500
+ "version": "0.58.0"
501
501
  }
package/hooks/hooks.json CHANGED
@@ -37,6 +37,10 @@
37
37
  "matcher": "Bash",
38
38
  "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/p057-staging-trap-detect.sh" }]
39
39
  },
40
+ {
41
+ "matcher": "Bash",
42
+ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-no-implement-draft-gate.sh" }]
43
+ },
40
44
  {
41
45
  "matcher": "Bash",
42
46
  "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-bash-polling-antipattern-detect.sh" }]
@@ -17,7 +17,8 @@
17
17
  # - is NOT the artefact's capture commit (subject does not start with
18
18
  # `docs(rfcs): capture RFC-` / `feat(itil): capture STORY-`), AND
19
19
  # - the referenced artefact is in a pre-in-progress status
20
- # (RFC: .proposed/.accepted ; story: .draft) on disk.
20
+ # (RFC: .proposed/.accepted ; story: .accepted per ADR-096 a draft
21
+ # story is blocked from implementation, not advised to advance) on disk.
21
22
  # → emit a stderr advisory naming the transition command. Silent otherwise.
22
23
  #
23
24
  # Advisory-only (ADR-013 Rule 6 fail-open; ADR-045 ≤300-byte band; exit 0).
@@ -69,9 +70,14 @@ while IFS= read -r id; do
69
70
  ;;
70
71
  STORY-*)
71
72
  [ -d "./docs/stories" ] || continue
72
- shopt -s nullglob; files=(./docs/stories/draft/${id}-*.md ./docs/stories/${id}-*.draft.md); shopt -u nullglob
73
+ # ADR-096: a draft story is NEVER implementable — the PreToolUse
74
+ # itil-no-implement-draft-gate blocks a commit referencing a draft story.
75
+ # So this advisory now fires for an ACCEPTED story carrying its first
76
+ # implementing commit: advise accepted -> in-progress (draft -> in-progress
77
+ # no longer exists; a draft story must be accepted first).
78
+ shopt -s nullglob; files=(./docs/stories/accepted/${id}-*.md); shopt -u nullglob
73
79
  [ ${#files[@]} -gt 0 ] || continue
74
- advise "${id} carries a non-capture commit but is still draft. It should advance to in-progress — run /wr-itil:manage-story ${id} in-progress. Bypass: BYPASS_TRANSITION_ADVISORY=1."
80
+ advise "${id} carries a non-capture implementing commit and is accepted. It should advance to in-progress — run /wr-itil:manage-story ${id} in-progress. Bypass: BYPASS_TRANSITION_ADVISORY=1."
75
81
  ;;
76
82
  esac
77
83
  done <<< "$TRAILERS"
@@ -0,0 +1,69 @@
1
+ #!/bin/bash
2
+ # wr-itil — PreToolUse:Bash gate (ADR-096 / P404). A story in `draft` is NEVER
3
+ # implementable. Blocks a `git commit` whose `Refs: STORY-NNN` trailer names a
4
+ # story still in docs/stories/draft/ — implementation requires `accepted` (where
5
+ # the INVEST + RFC-trace gates + ADR-090 ratification fire). Capture commits are
6
+ # exempt (they CREATE the draft story). Bootstrap-exempt commits bypass
7
+ # (ADR-060 A4). Fail-open on every abnormal path (ADR-013 Rule 6).
8
+ #
9
+ # This is the enforcement locus the architect named as the ONLY one that catches
10
+ # the exact P404 bypass — a direct implementing commit against a draft story,
11
+ # whether from the orchestrator or by hand.
12
+ #
13
+ # Bypass: BYPASS_NO_IMPLEMENT_DRAFT=1.
14
+ #
15
+ # @adr ADR-096 (no-implement-while-draft) ADR-060 (lifecycle) ADR-013 (Rule 6)
16
+ # ADR-095 (sibling capture-time gates) ADR-052 (bats)
17
+ # @problem P404
18
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19
+ # shellcheck source=lib/command-detect.sh
20
+ source "$SCRIPT_DIR/lib/command-detect.sh" 2>/dev/null || exit 0
21
+
22
+ INPUT=$(cat)
23
+ TOOL_NAME=$(printf '%s' "$INPUT" | python3 -c "import sys,json
24
+ try: print(json.load(sys.stdin).get('tool_name',''))
25
+ except: print('')" 2>/dev/null || echo "")
26
+ [ "$TOOL_NAME" = "Bash" ] || exit 0
27
+
28
+ COMMAND=$(printf '%s' "$INPUT" | python3 -c "import sys,json
29
+ try: print(json.load(sys.stdin).get('tool_input',{}).get('command',''))
30
+ except: print('')" 2>/dev/null || echo "")
31
+ command_invokes_git_commit "$COMMAND" || exit 0
32
+ [ "${BYPASS_NO_IMPLEMENT_DRAFT:-}" = "1" ] && exit 0
33
+
34
+ # Capture commits create the draft story — exempt. Bootstrap migrations bypass.
35
+ case "$COMMAND" in
36
+ *"capture STORY-"*) exit 0 ;;
37
+ *"bootstrap-exempt"*) exit 0 ;;
38
+ esac
39
+
40
+ # Extract `Refs: STORY-NNN` trailers from the command text (present literally
41
+ # regardless of -m / heredoc form).
42
+ STORIES=$(printf '%s' "$COMMAND" | grep -oE 'Refs:[[:space:]]*STORY-[0-9]{3}' | grep -oE 'STORY-[0-9]{3}' | sort -u)
43
+ [ -n "$STORIES" ] || exit 0
44
+
45
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
46
+ [ -d "./docs/stories" ] || exit 0
47
+
48
+ deny() {
49
+ cat <<JSON
50
+ {
51
+ "hookSpecificOutput": {
52
+ "hookEventName": "PreToolUse",
53
+ "permissionDecision": "deny",
54
+ "permissionDecisionReason": "$1"
55
+ }
56
+ }
57
+ JSON
58
+ exit 0
59
+ }
60
+
61
+ while IFS= read -r id; do
62
+ [ -n "$id" ] || continue
63
+ shopt -s nullglob; draftfiles=(./docs/stories/draft/${id}-*.md); shopt -u nullglob
64
+ if [ ${#draftfiles[@]} -gt 0 ]; then
65
+ deny "BLOCKED (ADR-096 / P404): this commit references ${id} via a Refs: trailer, but ${id} is still in draft. A draft story cannot be implemented — accept it first via /wr-itil:manage-story ${id} accepted (it runs the INVEST + RFC-trace gates + ratification), then re-commit. Bypass: BYPASS_NO_IMPLEMENT_DRAFT=1."
66
+ fi
67
+ done <<< "$STORIES"
68
+
69
+ exit 0
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bats
2
+ # Behavioural tests for itil-no-implement-draft-gate.sh (ADR-096 / P404).
3
+ # PreToolUse:Bash — reads a JSON envelope on stdin, emits permissionDecision:deny
4
+ # (exit 0, deny in the JSON) when a commit references a DRAFT story.
5
+ # @adr ADR-096 @adr ADR-052 @problem P404
6
+
7
+ setup() {
8
+ HOOK="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/itil-no-implement-draft-gate.sh"
9
+ TMP="$(mktemp -d)"; cd "$TMP"; git init -q .
10
+ mkdir -p docs/stories/draft docs/stories/accepted
11
+ }
12
+ teardown() { cd /; rm -rf "$TMP"; }
13
+
14
+ # write env.json carrying the given command string
15
+ env_cmd() { python3 -c 'import json,sys; open("env.json","w").write(json.dumps({"tool_name":"Bash","tool_input":{"command":sys.argv[1]}}))' "$1"; }
16
+
17
+ @test "blocks a commit referencing a DRAFT story" {
18
+ touch docs/stories/draft/STORY-042-foo.md
19
+ env_cmd 'git commit -m "fix: thing
20
+
21
+ Refs: STORY-042"'
22
+ run bash -c "bash '$HOOK' < env.json"
23
+ [ "$status" -eq 0 ]
24
+ echo "$output" | grep -q '"permissionDecision": "deny"'
25
+ echo "$output" | grep -q 'STORY-042'
26
+ }
27
+
28
+ @test "allows a commit referencing an ACCEPTED story" {
29
+ touch docs/stories/accepted/STORY-042-foo.md
30
+ env_cmd 'git commit -m "fix: thing
31
+
32
+ Refs: STORY-042"'
33
+ run bash -c "bash '$HOOK' < env.json"
34
+ [ "$status" -eq 0 ]
35
+ ! echo "$output" | grep -q 'deny'
36
+ }
37
+
38
+ @test "exempts a capture commit even for a draft story" {
39
+ touch docs/stories/draft/STORY-042-foo.md
40
+ env_cmd 'git commit -m "feat(itil): capture STORY-042 foo
41
+
42
+ Refs: STORY-042"'
43
+ run bash -c "bash '$HOOK' < env.json"
44
+ [ "$status" -eq 0 ]
45
+ ! echo "$output" | grep -q 'deny'
46
+ }
47
+
48
+ @test "bootstrap-exempt commit bypasses" {
49
+ touch docs/stories/draft/STORY-042-foo.md
50
+ env_cmd 'git commit -m "migrate
51
+
52
+ Refs: STORY-042
53
+ bootstrap-exempt: STORY-MAP-001"'
54
+ run bash -c "bash '$HOOK' < env.json"
55
+ [ "$status" -eq 0 ]
56
+ ! echo "$output" | grep -q 'deny'
57
+ }
58
+
59
+ @test "non-commit command is a no-op" {
60
+ touch docs/stories/draft/STORY-042-foo.md
61
+ env_cmd 'echo Refs: STORY-042'
62
+ run bash -c "bash '$HOOK' < env.json"
63
+ [ "$status" -eq 0 ]
64
+ ! echo "$output" | grep -q 'deny'
65
+ }
66
+
67
+ @test "commit with no story trailer is a no-op" {
68
+ env_cmd 'git commit -m "fix: no story ref"'
69
+ run bash -c "bash '$HOOK' < env.json"
70
+ [ "$status" -eq 0 ]
71
+ ! echo "$output" | grep -q 'deny'
72
+ }
73
+
74
+ @test "BYPASS env allows the commit" {
75
+ touch docs/stories/draft/STORY-042-foo.md
76
+ env_cmd 'git commit -m "fix
77
+
78
+ Refs: STORY-042"'
79
+ BYPASS_NO_IMPLEMENT_DRAFT=1 run bash -c "BYPASS_NO_IMPLEMENT_DRAFT=1 bash '$HOOK' < env.json"
80
+ [ "$status" -eq 0 ]
81
+ ! echo "$output" | grep -q 'deny'
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.57.3-preview.933",
3
+ "version": "0.58.0-preview.939",
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"
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: wr-itil:capture-story
3
- description: Lightweight story-capture skill for aside-invocation during foreground work — mandatory leading problem-trace AND JTBD-trace per ADR-060 I6 + I9 invariants, optional `--rfc` and `--story-map` flag args (I7 + I8 enforce at `accepted` transition not at capture), skeleton story file at `docs/stories/draft/STORY-NNN-<slug>.md`, single commit per capture, no inline README refresh. Defers full INVEST shape + acceptance transition to /wr-itil:manage-story. Use when the user (or agent) wants to capture a story quickly with clear problem + JTBD anchoring. For full lifecycle management, use /wr-itil:manage-story.
3
+ description: Lightweight story-capture skill for aside-invocation during foreground work — mandatory leading problem-trace AND JTBD-trace per ADR-060 I6 + I9 invariants, mandatory `--story-map` trace (I8 enforced AT CAPTURE per ADR-095, refuse-and-route if absent) + optional `--rfc` (I7 at accepted) + real user-value + >=1 acceptance criterion at capture (I10 content subset / ADR-095), skeleton story file at `docs/stories/draft/STORY-NNN-<slug>.md`, single commit per capture, no inline README refresh. Defers full INVEST shape + acceptance transition to /wr-itil:manage-story. Use when the user (or agent) wants to capture a story quickly with clear problem + JTBD anchoring. For full lifecycle management, use /wr-itil:manage-story.
4
4
  allowed-tools: Read, Write, Edit, Bash, Grep, Glob
5
5
  ---
6
6
 
7
7
  # Capture Story Skill
8
8
 
9
- Capture an INVEST-shaped story ticket quickly during foreground work. Lightweight aside-invocation surface that complements the heavyweight `/wr-itil:manage-story` flow. Mirrors `/wr-itil:capture-rfc` shape per ADR-032 lightweight + heavyweight skill split, extended for the story tier's stricter trace-mandate (both problem AND JTBD at capture; RFC AND story-map deferred to accepted).
9
+ Capture an INVEST-shaped story ticket quickly during foreground work. Lightweight aside-invocation surface that complements the heavyweight `/wr-itil:manage-story` flow. Mirrors `/wr-itil:capture-rfc` shape per ADR-032 lightweight + heavyweight skill split, extended for the story tier's stricter trace-mandate (problem, JTBD, AND story-map at capture I8 enforced at capture per ADR-095; RFC deferred to accepted).
10
10
 
11
11
  This skill is one half of the capture-then-manage story framework introduced by ADR-060 (Problem-RFC-Story framework with mandatory problem-trace and unified problem ontology, accepted 2026-05-05; Phase 2 amendment 2026-05-12 introducing the story tier). The other half is `/wr-itil:manage-story` (heavyweight intake + INVEST-gated lifecycle management).
12
12
 
@@ -15,8 +15,8 @@ This skill is one half of the capture-then-manage story framework introduced by
15
15
  ## When to invoke
16
16
 
17
17
  - **Slicing an RFC into INVEST-shaped sub-workstreams**: agent / user has captured an RFC and is now decomposing its scope into the ordered `stories:` array per ADR-060's working-the-problem flow (line 300-320). Each slice on a story-map's backbone → ribs → slices grid becomes one story.
18
- - **Capturing a story before its placement on a story-map**: per ADR-060 line 291, RFC + story-map traces are optional at capture; I7 / I8 enforce only at the `draft → accepted` transition. Draft stories may exist with NO `rfcs:` and NO `story-maps:` until the design firms up.
19
- - **Retrospective bootstrap migration** (Slice 15 of P170 Phase 2): extracting existing slices from `docs/plans/170-rfc-framework-story-map.md` into individual story files. The bounded-escape carve-out for I7 / I8 enforce-at-accepted permits the retrospective sequence (capture draft, design fills in `rfcs:` + `story-maps:`, manage-story <NNN> accepted gate fires).
18
+ - **Placing a story on its map at capture**: per ADR-095 a story is born on a story map — `--story-map` is MANDATORY at capture (I8 refuse-and-route if absent). `--rfc` stays optional (I7 enforces at the `draft → accepted` transition); a draft story may exist with NO `rfcs:` until the design firms up, but NEVER with an empty `story-maps:`.
19
+ - **Retrospective bootstrap migration** (Slice 15 of P170 Phase 2): extracting existing slices from `docs/plans/170-rfc-framework-story-map.md` into individual story files. The bootstrap-exempt marker (ADR-060 A4 / ADR-053) permits the retrospective sequence to bypass the capture-time I8 hard-block for migration stories ONLY; `rfcs:` still fills in before the manage-story <NNN> accepted gate (I7).
20
20
  - **Forward dogfood capture**: a new story for in-flight work, captured at the start of implementation, runs to `done` via `Refs: STORY-NNN` trailer detection + acceptance-criteria all-ticked.
21
21
 
22
22
  **Use `/wr-itil:manage-story` instead** when:
@@ -153,21 +153,25 @@ jtbd_file=$(ls docs/jtbd/*/JTBD-<NNN>-*.md 2>/dev/null | head -1)
153
153
 
154
154
  JTBD lifecycle states (`.proposed.md` / `.accepted.md` / `.archived.md`) all pass silently — a story may anchor on a proposed JTBD per the dogfood pattern (Phase 2 itself is being captured against proposed JTBDs).
155
155
 
156
- ### 2.6. Validate optional `--rfc` and `--story-map` traces
156
+ ### 2.6. Validate story-map trace (I8 hard-block AT CAPTURE per ADR-095) + optional `--rfc`
157
157
 
158
- If `$rfc_trace` is non-empty, for each `RFC-<NNN>`:
158
+ **I8 story-map membership is enforced at CAPTURE (ADR-095) a story is born on a map.** `--story-map` is MANDATORY:
159
+
160
+ - **`--story-map` absent**: hard-block with **refuse-and-route** (parity with the I6/I9 mandatory-trace gates). Do NOT scaffold `story-maps: []`. Emit the deny log (`reason: missing-story-map-trace`) and halt with:
161
+ > `/wr-itil:capture-story` requires a story-map trace (ADR-095 / I8): every story is born on a story map. Create a map first via `/wr-itil:capture-story-map` (or extend one via `/wr-itil:manage-story-map`), then re-invoke capture-story with `--story-map STORY-MAP-<NNN>`.
162
+
163
+ AFK orchestrators author the map first (born `human-oversight: unconfirmed` per ADR-090, drained later), then capture the story onto it — nothing halts silently; the map is a prerequisite step.
164
+ - **`--story-map` present**: for each `STORY-MAP-<NNN>`, existence check `ls docs/story-maps/*/STORY-MAP-<NNN>-*.html 2>/dev/null`. Malformed or unresolved → hard-block (`reason: unresolved-story-map-trace`). Lifecycle advisory-warn on `draft` / `in-progress` maps; pass silently on `accepted` / `completed`.
165
+ - **Bootstrap exemption (ADR-060 A4 / ADR-053)**: a capture carrying the inline `<!-- bootstrap-exempt: ... -->` marker bypasses the I8 hard-block for migration stories ONLY. A **non-bootstrap** capture carrying the marker is rejected (the marker is not a general capture-time escape hatch — behavioural test asserts).
166
+
167
+ `--rfc` stays **OPTIONAL** at capture (I7 enforces at the `accepted` transition — a story can legitimately precede its RFC firming up):
159
168
 
160
169
  ```bash
161
170
  rfc_file=$(ls docs/rfcs/RFC-<NNN>-*.md 2>/dev/null | head -1)
162
171
  [ -z "$rfc_file" ] && unresolved_rfcs+=("RFC-<NNN>")
163
172
  ```
164
173
 
165
- - Token absent: skip (the "optional" path).
166
- - Token present but malformed (`--rfc RFC-99`, etc.) OR resolves to no file: hard-block. Emit deny log with `reason: unresolved-rfc-trace`. The optional-vs-malformed distinction is load-bearing — absence is permitted, malformed input is not.
167
-
168
- Same shape for `--story-map`. Story-maps are HTML; existence check uses `ls docs/story-maps/*/STORY-MAP-<NNN>-*.html 2>/dev/null`.
169
-
170
- Lifecycle classification on resolved files: advisory-warn on `proposed` / `draft` / `in-progress` states (the design isn't firm yet — captured story will reference work that may itself drift); pass silently on `accepted` / `closed` / `verifying` states.
174
+ - `--rfc` token absent: skip (the optional path). Present-but-malformed OR resolves to no file: hard-block (`reason: unresolved-rfc-trace`).
171
175
 
172
176
  ### 3. Compute next STORY ID
173
177
 
@@ -200,7 +204,7 @@ decision-makers: [<git config user.name>]
200
204
  problems: [P<NNN>, P<NNN>, ...]
201
205
  jtbd: [JTBD-<NNN>, JTBD-<NNN>, ...]
202
206
  rfcs: [<RFC-<NNN>, ...> or empty]
203
- story-maps: [<STORY-MAP-<NNN>, ...> or empty]
207
+ story-maps: [<STORY-MAP-<NNN>, ...>] # >=1 REQUIRED at capture (I8 / ADR-095)
204
208
  estimated-effort: <S|M|L|XL — derived at capture per ADR-067 (real best-effort value, no deferral marker)>
205
209
  human-oversight: unconfirmed
206
210
  ---
@@ -212,16 +216,16 @@ human-oversight: unconfirmed
212
216
  **Problems**: <P<NNN> [, P<NNN>, ...]>
213
217
  **JTBD**: <JTBD-<NNN> [, ...]>
214
218
  **RFCs**: <RFC-<NNN> [, ...]> or (none — populate at accepted transition per I7)
215
- **Story Maps**: <STORY-MAP-<NNN> [, ...]> or (none populate at accepted transition per I8)
219
+ **Story Maps**: <STORY-MAP-<NNN> [, ...]> (>=1 required at capture I8 / ADR-095)
216
220
  **Estimated effort**: <S|M|L|XL> — derived at capture as a real best-effort value (P375 / ADR-032 amendment 2026-06-24; ADR-067 silent-derivation). NO `deferred` default and no "not estimated" marker — those are deferrals (user correction 2026-06-24). Confirmed/refined at the accepted transition per I10 INVEST Estimable.
217
221
 
218
222
  ## User value (required, INVEST Valuable)
219
223
 
220
- (populate at /wr-itil:manage-story accepted transition one-paragraph user-facing value statement)
224
+ <REQUIRED at capture (I10 Valuable subset / ADR-095): a real one-paragraph value-first user-facing statement — NOT a placeholder. "In order to <value>, as a <persona>, I want <capability>.">
221
225
 
222
226
  ## Acceptance criteria (accepted-gate, INVEST Testable)
223
227
 
224
- - [ ] (populate at /wr-itil:manage-story accepted transition observable behavioural criteria)
228
+ - [ ] <REQUIRED at capture (I10 Testable subset / ADR-095): >=1 real observable behavioural acceptance criterion — NOT a placeholder>
225
229
 
226
230
  ## Driving problem trace (required — I6 invariant)
227
231
 
@@ -335,7 +339,7 @@ The trailing pointer is **not optional** — it is the user-visible signal that
335
339
  | Problem-trace I6 enforcement | Re-validated at every lifecycle transition | Hard-block at capture-time; deny logged to `logs/story-capture-denials.jsonl` |
336
340
  | JTBD-trace I9 enforcement | Re-validated at every lifecycle transition | Hard-block at capture-time |
337
341
  | RFC-trace I7 enforcement | Hard-block at `accepted` transition (allows draft stories to exist before RFC reference firms up) | Advisory-warn at capture-time if `--rfc` provided and resolves to draft/proposed lifecycle |
338
- | Story-map-trace I8 enforcement | Hard-block at `accepted` transition | Same advisory-warn pattern at capture-time |
342
+ | Story-map-trace I8 enforcement | Hard-block AT CAPTURE refuse-and-route to `/wr-itil:capture-story-map` if absent (ADR-095) | direction-setting (caller must supply or author a map first) |
339
343
  | INVEST shape (I10) | Behavioural checks at `accepted` transition | Out of scope: capture produces a skeleton with deferred-placeholder sections |
340
344
  | Skeleton-fill | Full-intake; AskUserQuestion for User value + Acceptance criteria + Estimated effort | Deferred-placeholder pattern; one optional taste prompt only |
341
345
  | Status transitions | Step 7 owns draft → accepted → in-progress → done | Out of scope (creation only) |
@@ -373,4 +377,4 @@ The two skills share the `/tmp/wr-itil-story-capture-grep-${SESSION_ID}` create-
373
377
 
374
378
  ## Phase-out-of-order note
375
379
 
376
- This skill ships BEFORE `/wr-itil:capture-story-map` (Slice 3 of P170 Phase 2) due to the voice-tone-hook-on-HTML blocker documented at P170 line 297. Building capture-story first is structurally permitted per ADR-060 line 291 (story-map traces optional at capture; I8 enforce only at accepted transition). When Slices 3-6 eventually ship the story-map skills, `manage-story <NNN> accepted` will validate the I8 invariant against the then-existing story-map corpus. The deviation from ADR-060's recommended commit-grain order (line 449-454 — sub-slice 3 story-map skills then sub-slice 4 story skills) is auditable here and in this commit's Slice 7 commit message.
380
+ This skill ships BEFORE `/wr-itil:capture-story-map` (Slice 3 of P170 Phase 2) due to the voice-tone-hook-on-HTML blocker documented at P170 line 297. Building capture-story first was structurally permitted at the time (story-map traces were then optional at capture). **Superseded by ADR-095** — I8 now hard-blocks at capture, and `/wr-itil:capture-story-map` has since shipped, so a map must exist before a story is captured. When Slices 3-6 eventually ship the story-map skills, `manage-story <NNN> accepted` will validate the I8 invariant against the then-existing story-map corpus. The deviation from ADR-060's recommended commit-grain order (line 449-454 — sub-slice 3 story-map skills then sub-slice 4 story skills) is auditable here and in this commit's Slice 7 commit message.
@@ -197,7 +197,7 @@ decision-makers: [Test]
197
197
  problems: [P170]
198
198
  jtbd: [JTBD-008]
199
199
  rfcs: []
200
- story-maps: []
200
+ story-maps: [STORY-MAP-001]
201
201
  estimated-effort: M
202
202
  ---
203
203
 
@@ -212,6 +212,27 @@ EOF
212
212
  done
213
213
  }
214
214
 
215
+ @test "capture-story: I8-at-capture invariant — a valid captured story has a NON-EMPTY story-maps: (ADR-095)" {
216
+ mkdir -p docs/stories/draft
217
+ cat > docs/stories/draft/STORY-001-mapped.md <<EOF
218
+ ---
219
+ status: draft
220
+ story-maps: [STORY-MAP-002]
221
+ ---
222
+ EOF
223
+ run grep -E '^story-maps: \[STORY-MAP-[0-9]' docs/stories/draft/STORY-001-mapped.md
224
+ [ "$status" -eq 0 ]
225
+ run grep -E '^story-maps: \[\]' docs/stories/draft/STORY-001-mapped.md
226
+ [ "$status" -ne 0 ]
227
+ }
228
+
229
+ @test "capture-story: SKILL prescribes refuse-and-route on a missing story-map trace (I8/ADR-095)" {
230
+ run grep -E 'I8 hard-block AT CAPTURE|refuse-and-route|missing-story-map-trace' "$SKILL_FILE"
231
+ [ "$status" -eq 0 ]
232
+ run grep -E 'story-map traces are optional at capture' "$SKILL_FILE"
233
+ [ "$status" -ne 0 ]
234
+ }
235
+
215
236
  @test "capture-story: captured story file lands at docs/stories/draft/ per ADR-060 lines 145-147" {
216
237
  # Story-maps use HTML and lifecycle subdirs (draft/accepted/in-progress/
217
238
  # completed/archived); stories use markdown and lifecycle subdirs