@windyroad/itil 0.27.1 → 0.28.0-preview.303

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.
Files changed (43) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +9 -1
  3. package/bin/wr-itil-reconcile-stories +2 -0
  4. package/bin/wr-itil-reconcile-story-maps +2 -0
  5. package/hooks/hooks.json +4 -0
  6. package/hooks/itil-readme-refresh-discipline.sh +118 -0
  7. package/hooks/lib/readme-refresh-detect.sh +161 -0
  8. package/hooks/test/itil-readme-refresh-discipline.bats +261 -0
  9. package/lib/migrate-problems-layout.sh +128 -0
  10. package/package.json +1 -1
  11. package/scripts/reconcile-stories.sh +236 -0
  12. package/scripts/reconcile-story-maps.sh +98 -0
  13. package/scripts/test/reconcile-stories.bats +173 -0
  14. package/scripts/test/reconcile-story-maps.bats +74 -0
  15. package/scripts/test/rfc-stories-extension.bats +173 -0
  16. package/scripts/test/update-problem-references-section.bats +195 -0
  17. package/scripts/test/update-references-section-sibling-helpers.bats +80 -0
  18. package/scripts/test/working-the-problem-traversal.bats +109 -0
  19. package/scripts/update-jtbd-references-section.sh +131 -0
  20. package/scripts/update-problem-references-section.sh +284 -0
  21. package/scripts/update-rfc-references-section.sh +152 -0
  22. package/scripts/update-story-references-section.sh +128 -0
  23. package/skills/capture-rfc/SKILL.md +28 -3
  24. package/skills/capture-story/SKILL.md +373 -0
  25. package/skills/capture-story/test/capture-story-behavioural.bats +227 -0
  26. package/skills/capture-story-map/SKILL.md +229 -0
  27. package/skills/capture-story-map/test/capture-story-map-behavioural.bats +98 -0
  28. package/skills/list-stories/SKILL.md +151 -0
  29. package/skills/list-stories/test/list-stories-contract.bats +127 -0
  30. package/skills/list-story-maps/SKILL.md +93 -0
  31. package/skills/list-story-maps/test/list-story-maps-contract.bats +46 -0
  32. package/skills/manage-problem/SKILL.md +42 -4
  33. package/skills/manage-problem/test/manage-problem-auto-migrate-step.bats +53 -0
  34. package/skills/manage-rfc/SKILL.md +12 -0
  35. package/skills/manage-story/SKILL.md +242 -0
  36. package/skills/manage-story/test/manage-story-contract.bats +171 -0
  37. package/skills/manage-story-map/SKILL.md +158 -0
  38. package/skills/manage-story-map/test/manage-story-map-contract.bats +63 -0
  39. package/skills/reconcile-stories/SKILL.md +110 -0
  40. package/skills/reconcile-story-maps/SKILL.md +70 -0
  41. package/skills/work-problem/SKILL.md +1 -1
  42. package/skills/work-problems/SKILL.md +25 -0
  43. package/skills/work-problems/test/work-problems-auto-migrate-step.bats +57 -0
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env bash
2
+ # packages/itil/scripts/update-story-references-section.sh
3
+ #
4
+ # Generalised reverse-trace section updater for story files. Lookup
5
+ # table supports ## RFCs, ## Story Maps sections on STORY-NNN-*.md
6
+ # files (a story's parent RFC + parent story-map traces).
7
+ #
8
+ # Per ADR-060 § Phase 2 encoding amendment 2026-05-12 architect
9
+ # finding 4: no per-section-name branching; lookup-table dispatch.
10
+ #
11
+ # @adr ADR-060 (Phase 2 encoding amendment 2026-05-12)
12
+ # @problem P170 (Phase 2 Slice 2b)
13
+
14
+ set -uo pipefail
15
+
16
+ STORY_FILE="${1:-}"
17
+ SECTION_NAME="${2:-}"
18
+
19
+ [ -n "$STORY_FILE" ] || { echo "ERROR: missing story-file argument" >&2; exit 1; }
20
+ [ -n "$SECTION_NAME" ] || { echo "ERROR: missing section-name argument" >&2; exit 1; }
21
+ [ -f "$STORY_FILE" ] || { echo "ERROR: story file not found: $STORY_FILE" >&2; exit 1; }
22
+
23
+ declare -A SECTION_GLOB SECTION_MODE SECTION_ID_PATTERN
24
+
25
+ SECTION_GLOB["RFCs"]="docs/rfcs/RFC-*.md"
26
+ SECTION_MODE["RFCs"]="markdown-frontmatter-stories"
27
+ SECTION_ID_PATTERN["RFCs"]="RFC-[0-9]+"
28
+
29
+ SECTION_GLOB["Story Maps"]="docs/story-maps/*/STORY-MAP-*.html"
30
+ SECTION_MODE["Story Maps"]="html-data-story-id"
31
+ SECTION_ID_PATTERN["Story Maps"]="STORY-MAP-[0-9]+"
32
+
33
+ glob_pattern="${SECTION_GLOB[$SECTION_NAME]:-}"
34
+ extraction_mode="${SECTION_MODE[$SECTION_NAME]:-}"
35
+ id_pattern="${SECTION_ID_PATTERN[$SECTION_NAME]:-}"
36
+
37
+ [ -n "$glob_pattern" ] || { echo "ERROR: unknown section-name '$SECTION_NAME'. Supported: RFCs, Story Maps" >&2; exit 1; }
38
+
39
+ story_basename=$(basename "$STORY_FILE")
40
+ story_id=$(echo "$story_basename" | grep -oE '^STORY-[0-9]+' | head -1)
41
+ [ -n "$story_id" ] || { echo "ERROR: cannot extract STORY ID from filename: $story_basename" >&2; exit 1; }
42
+
43
+ declare -a matched_ids=() matched_titles=() matched_statuses=()
44
+
45
+ extract_from_markdown_frontmatter_stories() {
46
+ local file="$1"
47
+ awk '/^---$/{f=!f;next} f && /^stories:/' "$file" | head -1 | grep -qE "\\b${story_id}\\b"
48
+ }
49
+
50
+ extract_from_html_data_story_id() {
51
+ local file="$1"
52
+ # Story maps reference stories via <a data-story-id="STORY-NNN"> per ADR-060
53
+ # amendment schema; grep on the literal attribute match.
54
+ grep -qE "data-story-id=\"${story_id}\"" "$file"
55
+ }
56
+
57
+ extract_id_from_filename() { basename "$1" | grep -oE "$id_pattern" | head -1; }
58
+ extract_title_md() { awk '/^# / { sub(/^# /, ""); print; exit }' "$1"; }
59
+ extract_title_html() { grep -oE '<title>[^<]+</title>' "$1" | head -1 | sed -E 's|<title>([^<]+)</title>|\1|'; }
60
+ extract_status_md() { awk '/^---$/{f=!f;next} f && /^status:/{ sub(/^status:[[:space:]]*/, ""); gsub(/"/, ""); print; exit }' "$1"; }
61
+ extract_status_html() { grep -oE '<meta[[:space:]]+name="status"[[:space:]]+content="[^"]+"' "$1" | head -1 | sed -E 's|.*content="([^"]+)".*|\1|'; }
62
+
63
+ case "$extraction_mode" in
64
+ markdown-frontmatter-stories)
65
+ extract_match=extract_from_markdown_frontmatter_stories
66
+ extract_title=extract_title_md
67
+ extract_status=extract_status_md
68
+ ;;
69
+ html-data-story-id)
70
+ extract_match=extract_from_html_data_story_id
71
+ extract_title=extract_title_html
72
+ extract_status=extract_status_html
73
+ ;;
74
+ *)
75
+ echo "ERROR: unknown extraction-mode '$extraction_mode'" >&2
76
+ exit 1
77
+ ;;
78
+ esac
79
+
80
+ shopt -s nullglob
81
+ for artefact in $glob_pattern; do
82
+ [ -e "$artefact" ] || continue
83
+ if "$extract_match" "$artefact"; then
84
+ aid=$(extract_id_from_filename "$artefact")
85
+ [ -n "$aid" ] || continue
86
+ matched_ids+=("$aid")
87
+ matched_titles+=("$("$extract_title" "$artefact" 2>/dev/null || echo "")")
88
+ matched_statuses+=("$("$extract_status" "$artefact" 2>/dev/null || echo "unknown")")
89
+ fi
90
+ done
91
+ shopt -u nullglob
92
+
93
+ new_section=""
94
+ if [ ${#matched_ids[@]} -gt 0 ]; then
95
+ new_section="## ${SECTION_NAME}"$'\n\n| ID | Title | Status |\n|----|-------|--------|\n'
96
+ for i in "${!matched_ids[@]}"; do
97
+ new_section+="| ${matched_ids[$i]} | ${matched_titles[$i]} | ${matched_statuses[$i]} |"$'\n'
98
+ done
99
+ fi
100
+
101
+ tmp_file="$(mktemp)"
102
+ awk -v sec="## $SECTION_NAME" '
103
+ BEGIN { in_target=0; blank_buffer="" }
104
+ $0 == sec { in_target=1; blank_buffer=""; next }
105
+ in_target && /^## / && $0 != sec { in_target=0 }
106
+ !in_target {
107
+ if ($0 ~ /^[[:space:]]*$/) { if (blank_buffer == "") blank_buffer="\n"; next }
108
+ if (blank_buffer != "") { printf "%s", blank_buffer; blank_buffer="" }
109
+ print
110
+ }
111
+ END { if (blank_buffer != "") printf "%s", blank_buffer }
112
+ ' "$STORY_FILE" > "$tmp_file"
113
+
114
+ tmp_file2="$(mktemp)"
115
+ awk 'BEGIN{c=0} /^[[:space:]]*$/{c++; next} {for(i=0;i<c;i++)print ""; c=0; print} END{print ""}' "$tmp_file" > "$tmp_file2"
116
+ mv "$tmp_file2" "$tmp_file"
117
+
118
+ if [ -n "$new_section" ]; then
119
+ printf '\n%s' "$new_section" >> "$tmp_file"
120
+ fi
121
+
122
+ if ! cmp -s "$tmp_file" "$STORY_FILE"; then
123
+ mv "$tmp_file" "$STORY_FILE"
124
+ else
125
+ rm -f "$tmp_file"
126
+ fi
127
+
128
+ exit 0
@@ -27,9 +27,12 @@ This skill is one half of the capture-then-manage RFC framework introduced by AD
27
27
 
28
28
  **Positional**: `<problem-trace> <description>` where `<problem-trace>` is `P<NNN>` or `P<NNN>,P<NNN>,...` (no spaces inside the trace; multiple problems comma-separated).
29
29
 
30
+ **Optional flag (Phase 2)**: `--stories STORY-<NNN>,STORY-<NNN>,...` — ORDERED execution sequence per ADR-060 line 262. Cardinality 0..N: atomic RFCs OMIT the flag and capture-rfc populates `stories: []` in frontmatter (JTBD-101 friction guard — atomic RFCs are first-class); story-decomposed RFCs supply the ordered list. The flag accepts STORY-IDs that don't yet resolve to files (forward-reference is permitted at capture; the existence check happens at `manage-rfc <NNN> accepted` transition per ADR-060 working-the-problem flow line 304).
31
+
30
32
  ```
31
33
  /wr-itil:capture-rfc P168 Pipeline consume-catalog and bootstrap-from-reports — multi-commit retrofit
32
34
  /wr-itil:capture-rfc P038,P064 Voice-and-tone gates on external comms — coordinated rollout across changeset/PR/release-notes
35
+ /wr-itil:capture-rfc P170 --stories STORY-007,STORY-010,STORY-011 Phase 2 story-tier framework — capture-story + list-stories + RFC frontmatter extension
33
36
  ```
34
37
 
35
38
  **ADR-060 § Phase 1 item 2 phrasing footnote**: ADR-060 names "mandatory `--problem P<NNN>` flag" verbatim. This skill uses the **positional** form (no `--problem` prefix) to match the lightweight aside-invocation grammar of `capture-problem` (per ADR-032) and because Claude Code skill arguments don't carry a proper CLI flag parser. The hard-block intent (ADR-060 § Confirmation criterion 1: "without a problem trace") is preserved verbatim — only the surface syntax differs. The `--problem` phrasing in ADR-060 reads as exemplar, not contract.
@@ -75,13 +78,24 @@ fi
75
78
  The arguments must begin with a problem-trace token (`P<NNN>` or comma-separated `P<NNN>,P<NNN>,...`). The remainder is the description.
76
79
 
77
80
  ```bash
78
- # Tokenise: first token = problem-trace; rest = description
79
- problem_trace="$1"; shift
80
- description="$*"
81
+ # Tokenise: first non-flag token = problem-trace; rest = description.
82
+ # Optional --stories STORY-NNN,STORY-NNN,... flag may appear anywhere.
83
+ stories_trace=""
84
+ positional=()
85
+ while [ $# -gt 0 ]; do
86
+ case "$1" in
87
+ --stories) stories_trace="$2"; shift 2 ;;
88
+ *) positional+=("$1"); shift ;;
89
+ esac
90
+ done
91
+ problem_trace="${positional[0]}"
92
+ description="${positional[*]:1}"
81
93
  ```
82
94
 
83
95
  If `$problem_trace` does not match `^P[0-9]{3}(,P[0-9]{3})*$` (regex), this is an I1 violation — go to Step 2's deny path. If `$description` is empty, halt with the empty-arguments directive from the Rule 6 audit table above.
84
96
 
97
+ If `$stories_trace` is non-empty, validate the format matches `^STORY-[0-9]{3}(,STORY-[0-9]{3})*$`. Forward-reference is permitted (STORY-IDs that don't yet resolve to files at capture time) — the existence check happens at `manage-rfc <NNN> accepted` transition. Malformed format (e.g. `--stories foo`) is an argument error: halt with stderr directive naming the expected shape.
98
+
85
99
  Derive a kebab-case title slug from the first 8-10 non-stopword tokens of `$description` (matching `capture-problem` slug derivation).
86
100
 
87
101
  ### 2. Validate problem trace + I1 hard-block enforcement
@@ -152,6 +166,7 @@ decision-makers: [<git config user.name>]
152
166
  problems: [P<NNN>, P<NNN>, ...]
153
167
  adrs: []
154
168
  jtbd: []
169
+ stories: [<from --stories flag — ordered execution sequence; or [] if --stories absent>]
155
170
  ---
156
171
 
157
172
  # RFC-<NNN>: <Title>
@@ -210,6 +225,16 @@ done
210
225
 
211
226
  The helper (`packages/itil/scripts/update-problem-rfcs-section.sh`) is idempotent: running over a current section is a no-op. Lazy-empty discipline applies (zero traced RFCs → section absent) — capture-rfc invocations always have ≥ 1 trace at this step, so this surface always emits a populated section. The `git add` is conditional on the helper actually modifying the file — `cmp -s` no-op-on-current is the helper's idempotency contract; `git add` of an unchanged file is also a no-op.
212
227
 
228
+ **Phase 2 — render `## Stories` body section on the new RFC** (when `--stories` was provided): the just-written RFC file carries `stories: [STORY-NNN, ...]` in frontmatter; the helper `update-rfc-references-section.sh <rfc-file> "Stories"` renders the forward-trace `## Stories` body section from that frontmatter array in execution order per ADR-060 line 270. Lazy-empty discipline applies — when `stories: []` (atomic RFC, JTBD-101 friction guard), the helper omits the section entirely:
229
+
230
+ ```bash
231
+ if [ -n "$stories_trace" ]; then
232
+ bash "$(wr-itil-script-path 2>/dev/null || echo packages/itil/scripts)/update-rfc-references-section.sh" "docs/rfcs/RFC-${next}-${slug}.proposed.md" "Stories"
233
+ fi
234
+ ```
235
+
236
+ (`git add` of the new RFC file in the next step picks up the section render.)
237
+
213
238
  Stage the new RFC file:
214
239
 
215
240
  ```bash
@@ -0,0 +1,373 @@
1
+ ---
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.
4
+ allowed-tools: Read, Write, Edit, Bash, Grep, Glob
5
+ ---
6
+
7
+ # Capture Story Skill
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).
10
+
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
+
13
+ **Related JTBDs**: JTBD-008 (primary — Decompose a Fix Into Coordinated Changes; story is the INVEST-shaped sub-workstream entity JTBD-008 line 20 names), JTBD-001 (extended scope — story-level governance via INVEST gates at acceptance + auto-transition on RFC-closes + acceptance-criteria all-ticked), JTBD-101 (atomic-fix-adopter friction guard — capture-story remains opt-in; atomic-RFC fallback per ADR-060 line 262 means atomic adopters never invoke capture-story).
14
+
15
+ ## When to invoke
16
+
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).
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
+
22
+ **Use `/wr-itil:manage-story` instead** when:
23
+ - The work is advancing an existing story through its lifecycle (draft → accepted → in-progress → done).
24
+ - The user wants the full INVEST intake flow with structured value-statement + acceptance-criteria + effort-estimation prompts.
25
+ - The story needs the I10 INVEST shape behavioural test to fire at accepted.
26
+
27
+ ## Argument grammar
28
+
29
+ **Positional (both mandatory)**: `<problem-trace> <jtbd-trace> <description>` where:
30
+ - `<problem-trace>` is `P<NNN>` or comma-separated `P<NNN>,P<NNN>,...` (no spaces inside the trace; multiple problems comma-separated).
31
+ - `<jtbd-trace>` is `JTBD-<NNN>` or comma-separated `JTBD-<NNN>,JTBD-<NNN>,...`.
32
+
33
+ **Optional flags** (any order, before or after positional args):
34
+ - `--rfc RFC-<NNN>[,RFC-<NNN>,...]` — RFC(s) this story will be referenced by once design firms up.
35
+ - `--story-map STORY-MAP-<NNN>[,STORY-MAP-<NNN>,...]` — story-map(s) this story will be placed on once design firms up.
36
+
37
+ ```
38
+ /wr-itil:capture-story P170 JTBD-008 Build /wr-itil:capture-story-map skill scaffold
39
+ /wr-itil:capture-story P170 JTBD-008,JTBD-001 --rfc RFC-002 Ship hook exemption globs across 4 enforce-edit hooks
40
+ /wr-itil:capture-story P170 JTBD-008 --story-map STORY-MAP-001 --rfc RFC-002 Extract Slice 5 T7 shared-migration-routine into STORY-NNN
41
+ ```
42
+
43
+ **ADR-060 § Skills line 291 phrasing footnote**: ADR-060 names "Mandatory: ≥1 problem trace, ≥1 JTBD trace" verbatim. This skill uses the **positional** form (no `--problem` / `--jtbd` prefix on the mandatory pair) to match the lightweight aside-invocation grammar of `capture-rfc` (per ADR-032) and because Claude Code skill arguments don't carry a proper CLI flag parser. The optional `--rfc` / `--story-map` flags exist BECAUSE they are optional — fully positional would require sentinel values for absent traces. The hard-block intent (ADR-060 § Confirmation criterion implied by I6 + I9 — capture fails without both mandatory traces) is preserved verbatim — only the surface syntax differs.
44
+
45
+ ## Rule 6 audit (per ADR-032 + ADR-013 + ADR-060)
46
+
47
+ This skill has **two direction-setting AskUserQuestion fires** (problem-trace AND JTBD-trace, when arguments are non-empty but malformed) and **one optional taste AskUserQuestion** (title/scope summary, silent-default if unavailable). Every other potentially-interactive decision is framework-mediated per ADR-044:
48
+
49
+ | Decision | Resolution | Authority class |
50
+ |----------|-----------|-----------------|
51
+ | Problem trace presence | I6 hard-block — refuse on missing trace; emit deny log + halt-with-stderr-directive | direction-setting |
52
+ | Problem trace validation | Mechanical: each `P<NNN>` must exist in `docs/problems/`. Open / Known Error / Verifying = pass; Closed / Parked = advisory-warn but proceed (bounded-escape carve-out — see Step 2) | silent-mechanical |
53
+ | JTBD trace presence | I9 hard-block — refuse on missing JTBD trace; emit deny log + halt-with-stderr-directive | direction-setting |
54
+ | JTBD trace validation | Mechanical: each `JTBD-<NNN>` must resolve to a file under `docs/jtbd/<persona>/JTBD-<NNN>-*.md` (any lifecycle status) | silent-mechanical |
55
+ | Optional `--rfc` trace validation | Mechanical: each provided `RFC-<NNN>` must resolve to a file under `docs/rfcs/`; advisory-warn on `proposed` / `verifying` lifecycle states; missing entirely = hard-block on the provided arg (the absence-from-args case is the "optional" path — the malformed-arg case is not) | silent-mechanical |
56
+ | Optional `--story-map` trace validation | Same mechanical pattern against `docs/story-maps/*/STORY-MAP-*.html` (HTML data-attribute existence check); advisory-warn on `draft` / `in-progress` story-maps | silent-mechanical |
57
+ | STORY ID allocation | Mechanical: `max(local, origin) + 1`, three-digit padded; enumerates `docs/stories/*/STORY-*.md` + `git ls-tree origin/main docs/stories/`. ADR-019 collision-guard inline per Slice 3 design review architect approval (finding 3 option a — inline-only path) | silent-mechanical |
58
+ | Title kebab-slug | Mechanical: first 8-10 non-stopword tokens of description | silent-mechanical |
59
+ | Title prose / scope summary refinement | Optional `AskUserQuestion`; silent-default to derived form when unavailable | taste |
60
+ | File write / frontmatter | Mechanical: shape per `docs/stories/README.md` § Frontmatter shape + ADR-060 lines 220-228 | silent-mechanical |
61
+ | Single commit | Mechanical: `feat(itil): capture STORY-<NNN> <title>` + `Refs: STORY-<NNN>` trailer | silent-mechanical |
62
+ | Empty arguments | Halt-with-stderr-directive: print "capture-story requires `<problem-trace> <jtbd-trace> <description>` — invoke /wr-itil:manage-story instead for the full intake flow" and exit. AFK orchestrators MUST NOT invoke capture-story with empty arguments. | n/a |
63
+
64
+ Per ADR-013 Rule 6 fail-safe + ADR-044 + P132 + inverse-P078: every silent-mechanical branch above resolves without user input, so AFK and interactive contexts behave identically modulo the optional taste prompt.
65
+
66
+ ## Steps
67
+
68
+ ### 0. Preflight (Phase 2 cross-directory)
69
+
70
+ This skill's preflight uses `wr-itil-reconcile-readme docs/problems` (the existing problems-README reconciliation contract per P118). Sibling reconcile-stories + reconcile-story-maps scripts land in Slice 5 + Slice 9 of P170 Phase 2 — once those ship, swap this preflight to call all three reconciliations (cross-tier integrity holds at all three surfaces).
71
+
72
+ ```bash
73
+ wr-itil-reconcile-readme docs/problems > /tmp/wr-itil-drift-$$.txt
74
+ reconcile_exit=$?
75
+ if [ "$reconcile_exit" -eq 1 ]; then
76
+ wr-itil-classify-readme-drift /tmp/wr-itil-drift-$$.txt docs/problems
77
+ classify_exit=$?
78
+ rm -f /tmp/wr-itil-drift-$$.txt
79
+ # classify_exit 0 (INLINE_REFRESH): proceed (no inline refresh in this skill).
80
+ # classify_exit 1 (HALT_ROUTE_RECONCILE): halt; invoke /wr-itil:reconcile-readme.
81
+ # classify_exit 2 (parse error): conservative halt-and-route.
82
+ fi
83
+ ```
84
+
85
+ ### 1. Parse arguments
86
+
87
+ Tokenise the argument string. Optional flags (`--rfc <ids>`, `--story-map <ids>`) may appear in any position. The remaining positional tokens are `<problem-trace> <jtbd-trace> <description>` in that order.
88
+
89
+ ```bash
90
+ # Pseudo:
91
+ rfc_trace=""
92
+ story_map_trace=""
93
+ positional=()
94
+ while [ $# -gt 0 ]; do
95
+ case "$1" in
96
+ --rfc) rfc_trace="$2"; shift 2 ;;
97
+ --story-map) story_map_trace="$2"; shift 2 ;;
98
+ *) positional+=("$1"); shift ;;
99
+ esac
100
+ done
101
+ problem_trace="${positional[0]}"
102
+ jtbd_trace="${positional[1]}"
103
+ description="${positional[*]:2}"
104
+ ```
105
+
106
+ If `$problem_trace` does not match `^P[0-9]{3}(,P[0-9]{3})*$` (regex), this is an I6 violation — go to Step 2's deny path. If `$jtbd_trace` does not match `^JTBD-[0-9]{3}(,JTBD-[0-9]{3})*$`, this is an I9 violation. If `$description` is empty, halt with the empty-arguments directive from the Rule 6 audit table.
107
+
108
+ Derive a kebab-case title slug from the first 8-10 non-stopword tokens of `$description` (matching `capture-rfc` slug derivation).
109
+
110
+ ### 2. Validate problem trace + I6 hard-block enforcement
111
+
112
+ For each `P<NNN>` in the trace list:
113
+
114
+ ```bash
115
+ # Dual-tolerant ticket discovery (RFC-002 migration window):
116
+ # BOTH flat `docs/problems/<NNN>-<title>.<state>.md` AND per-state
117
+ # subdir `docs/problems/<state>/<NNN>-<title>.md` layouts.
118
+ trace_files=$(ls docs/problems/<NNN>-*.md docs/problems/*/<NNN>-*.md 2>/dev/null)
119
+ ```
120
+
121
+ **I6 hard-block (per ADR-060 line 248)**:
122
+
123
+ - **Trace token absent OR malformed**: emit deny log entry + halt with stderr directive:
124
+ ```bash
125
+ mkdir -p logs
126
+ printf '{"timestamp":"%s","session_id":"%s","reason":"%s","args":%s}\n' \
127
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$(get_current_session_id 2>/dev/null || echo unknown)" \
128
+ "<missing|malformed|unresolved>-trace" \
129
+ "$(printf '%s' "$ARGUMENTS" | jq -Rs .)" \
130
+ >> logs/story-capture-denials.jsonl
131
+ echo "/wr-itil:capture-story requires a leading problem-trace argument (P<NNN> or P<NNN>,P<NNN>...). Open the driving problem via /wr-itil:capture-problem first, then re-invoke capture-story with the trace." >&2
132
+ exit 1
133
+ ```
134
+ The deny log feeds the trace-violation-rate reassessment criterion (sibling to RFC's `logs/rfc-capture-denials.jsonl`).
135
+
136
+ - **Each `P<NNN>` must resolve to a file in `docs/problems/`**. If any does not, emit deny log entry with `reason: unresolved-trace` + the unresolved IDs, halt, exit 1.
137
+
138
+ **Bounded-escape carve-out for Closed/Verifying/Parked traces**: classify by suffix or path; `.open.md` / `.known-error.md` (or `open/` / `known-error/` subdirs) pass silently; `.verifying.md` (or `verifying/` subdir) passes with advisory note; `.closed.md` (or `closed/` subdir) and `.parked.md` (or `parked/` subdir) pass with advisory-warn (story may be a retrospective extraction).
139
+
140
+ ### 2.5. Validate JTBD trace + I9 hard-block enforcement
141
+
142
+ For each `JTBD-<NNN>` in the JTBD trace list:
143
+
144
+ ```bash
145
+ jtbd_file=$(ls docs/jtbd/*/JTBD-<NNN>-*.md 2>/dev/null | head -1)
146
+ [ -z "$jtbd_file" ] && unresolved_jtbds+=("JTBD-<NNN>")
147
+ ```
148
+
149
+ **I9 hard-block (per ADR-060 line 251)**:
150
+
151
+ - **JTBD trace token absent OR malformed**: emit deny log entry + halt with directive: `/wr-itil:capture-story requires a JTBD trace argument (JTBD-<NNN> or JTBD-<NNN>,JTBD-<NNN>...). Author the driving JTBD via /wr-jtbd:update-guide first, then re-invoke capture-story.`
152
+ - **Each `JTBD-<NNN>` must resolve to a file in `docs/jtbd/`**. If any does not, emit deny log with `reason: unresolved-jtbd-trace`, halt, exit 1.
153
+
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
+
156
+ ### 2.6. Validate optional `--rfc` and `--story-map` traces
157
+
158
+ If `$rfc_trace` is non-empty, for each `RFC-<NNN>`:
159
+
160
+ ```bash
161
+ rfc_file=$(ls docs/rfcs/RFC-<NNN>-*.md 2>/dev/null | head -1)
162
+ [ -z "$rfc_file" ] && unresolved_rfcs+=("RFC-<NNN>")
163
+ ```
164
+
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.
171
+
172
+ ### 3. Compute next STORY ID
173
+
174
+ Inline `max(local, origin) + 1` formula (per Slice 3 design review architect finding 3 option a — inline-only path; no separate `check-id-collision.sh` script per capture-rfc / capture-problem precedent):
175
+
176
+ ```bash
177
+ local_max=$(ls docs/stories/*/STORY-*.md 2>/dev/null | sed 's|.*/STORY-||;s|-.*||' | grep -oE '^[0-9]+' | sort -n | tail -1)
178
+ origin_max=$(git ls-tree -r --name-only origin/main docs/stories/ 2>/dev/null | sed 's|.*/STORY-||;s|-.*||' | grep -oE '^[0-9]+' | sort -n | tail -1)
179
+ next=$(printf '%03d' $(( 10#$(echo -e "${local_max:-0}\n${origin_max:-0}" | sort -n | tail -1) + 1 )))
180
+ ```
181
+
182
+ Log the renumber decision in the operation report if origin and local diverged. The `git ls-tree -r` recursive flag enumerates the per-state subdir layout — `docs/stories/draft/`, `docs/stories/accepted/`, etc.
183
+
184
+ ### 4. Optional taste prompt for title / scope summary
185
+
186
+ If interactive (AskUserQuestion available) AND the description is short enough that the derived title slug may not capture intent, fire one `AskUserQuestion` with `header: "Story title"` offering: (a) the derived kebab-slug as default, (b) "edit". This is **taste** authority per ADR-044 — silent-default to (a) when AskUserQuestion is unavailable.
187
+
188
+ ### 5. Write the story file
189
+
190
+ **File path**: `docs/stories/draft/STORY-<NNN>-<kebab-title>.md`
191
+
192
+ **Template** (mirrors `docs/stories/README.md` § Story frontmatter + body structure):
193
+
194
+ ```markdown
195
+ ---
196
+ status: draft
197
+ story-id: <kebab-slug>
198
+ reported: <YYYY-MM-DD>
199
+ decision-makers: [<git config user.name>]
200
+ problems: [P<NNN>, P<NNN>, ...]
201
+ jtbd: [JTBD-<NNN>, JTBD-<NNN>, ...]
202
+ rfcs: [<RFC-<NNN>, ...> or empty]
203
+ story-maps: [<STORY-MAP-<NNN>, ...> or empty]
204
+ estimated-effort: deferred
205
+ ---
206
+
207
+ # STORY-<NNN>: <Title>
208
+
209
+ **Status**: draft
210
+ **Reported**: <YYYY-MM-DD>
211
+ **Problems**: <P<NNN> [, P<NNN>, ...]>
212
+ **JTBD**: <JTBD-<NNN> [, ...]>
213
+ **RFCs**: <RFC-<NNN> [, ...]> or (none — populate at accepted transition per I7)
214
+ **Story Maps**: <STORY-MAP-<NNN> [, ...]> or (none — populate at accepted transition per I8)
215
+ **Estimated effort**: deferred (populate at accepted transition per I10 INVEST Estimable)
216
+
217
+ ## User value (required, INVEST Valuable)
218
+
219
+ (populate at /wr-itil:manage-story accepted transition — one-paragraph user-facing value statement)
220
+
221
+ ## Acceptance criteria (accepted-gate, INVEST Testable)
222
+
223
+ - [ ] (populate at /wr-itil:manage-story accepted transition — observable behavioural criteria)
224
+
225
+ ## Driving problem trace (required — I6 invariant)
226
+
227
+ <description from arguments — one-line summary linking the story scope to the problem's symptom or RCA finding for each driving problem>
228
+
229
+ ## JTBD trace (required — I9 invariant)
230
+
231
+ <one-line summary linking each JTBD-<NNN> to the persona-job's desired outcome that this story serves>
232
+
233
+ ## Implementation notes (optional)
234
+
235
+ (deferred — populate at /wr-itil:manage-story accepted transition or during implementation)
236
+
237
+ ## Dependencies
238
+
239
+ - **Blocks**: (none — populate at /wr-itil:manage-story if applicable)
240
+ - **Blocked by**: (none — populate at /wr-itil:manage-story; Phase 2 I-invariant prohibits Blocked-by references to unaccepted stories at acceptance time per INVEST Independent)
241
+
242
+ ## Related
243
+
244
+ (captured via /wr-itil:capture-story; expand at next /wr-itil:manage-story invocation)
245
+ ```
246
+
247
+ The deferred-section pattern matches `capture-rfc`'s placeholder approach — the captured story is intentionally minimal; full INVEST shape lands at the manage-story accepted-transition step.
248
+
249
+ ### 6. Single commit — `## Stories` reverse-trace refresh; no stories README refresh
250
+
251
+ **Stage list**: the new story file PLUS each driving problem ticket file (refresh `## Stories` reverse-trace section) PLUS each driving JTBD file (refresh `## Stories` reverse-trace section) PLUS each driving RFC file IF `--rfc` was provided (refresh `## Stories` reverse-trace section). **Do NOT** stage `docs/stories/README.md` (deferred). **Do NOT** stage any story-map HTML files — story-maps are spatially-authored HTML; new stories must be placed on the relevant map manually via `/wr-itil:manage-story-map` (when that skill lands per Slice 4 of P170 Phase 2). Capture-story emits an advisory stderr line naming the unplaced-on-map state.
252
+
253
+ The reverse-trace refresh on driving artefacts IS in-commit per ADR-014 single-commit grain — the cross-tier `## Stories` table on a problem / JTBD / RFC must stay current the moment a new story traces it. The same justification as capture-rfc's inline `## RFCs` refresh applies.
254
+
255
+ For each problem ID in `$problem_trace`:
256
+
257
+ ```bash
258
+ for pid_token in $(echo "$problem_trace" | tr ',' ' '); do
259
+ pid_num="${pid_token#P}"
260
+ # Dual-tolerant ticket discovery (RFC-002 migration window).
261
+ problem_file=$(ls docs/problems/${pid_num}-*.md docs/problems/*/${pid_num}-*.md 2>/dev/null | head -1)
262
+ [ -z "$problem_file" ] && continue
263
+ bash "$(wr-itil-script-path 2>/dev/null || echo packages/itil/scripts)/update-problem-references-section.sh" "$problem_file" "Stories"
264
+ git add "$problem_file"
265
+ done
266
+ ```
267
+
268
+ Same shape for each JTBD in `$jtbd_trace`:
269
+
270
+ ```bash
271
+ for jid_token in $(echo "$jtbd_trace" | tr ',' ' '); do
272
+ jid_num="${jid_token#JTBD-}"
273
+ jtbd_file=$(ls docs/jtbd/*/JTBD-${jid_num}-*.md 2>/dev/null | head -1)
274
+ [ -z "$jtbd_file" ] && continue
275
+ bash "$(wr-itil-script-path 2>/dev/null || echo packages/itil/scripts)/update-jtbd-references-section.sh" "$jtbd_file" "Stories"
276
+ git add "$jtbd_file"
277
+ done
278
+ ```
279
+
280
+ Same shape for each RFC in `$rfc_trace` (only if non-empty):
281
+
282
+ ```bash
283
+ for rid_token in $(echo "$rfc_trace" | tr ',' ' '); do
284
+ [ -z "$rid_token" ] && continue
285
+ rfc_file=$(ls docs/rfcs/${rid_token}-*.md 2>/dev/null | head -1)
286
+ [ -z "$rfc_file" ] && continue
287
+ bash "$(wr-itil-script-path 2>/dev/null || echo packages/itil/scripts)/update-rfc-references-section.sh" "$rfc_file" "Stories"
288
+ git add "$rfc_file"
289
+ done
290
+ ```
291
+
292
+ The helpers (`update-problem-references-section.sh`, `update-jtbd-references-section.sh`, `update-rfc-references-section.sh`) all support `"Stories"` as a section-name token per Slice 2a/2b verified lookup tables. Each helper is idempotent: a no-op section is a no-op stage.
293
+
294
+ Stage the new story file:
295
+
296
+ ```bash
297
+ git add docs/stories/draft/STORY-<NNN>-<slug>.md
298
+ ```
299
+
300
+ Satisfy the commit gate per ADR-014:
301
+
302
+ - **Primary**: delegate to subagent type `wr-risk-scorer:pipeline` via the Agent tool.
303
+ - **Fallback**: invoke `/wr-risk-scorer:assess-release` via the Skill tool when the subagent type is unavailable.
304
+
305
+ Commit message:
306
+
307
+ ```
308
+ feat(itil): capture STORY-<NNN> <title>
309
+
310
+ Refs: STORY-<NNN>
311
+ ```
312
+
313
+ The `capture` verb mirrors `capture-rfc`'s audit signal (lightweight aside path vs. heavyweight `manage-story` intake). The single `Refs: STORY-<NNN>` trailer is the universal story-trailer vocabulary per ADR-060 line 307 + amendment 2026-05-10 nitpick N2 — capture commits and implementation commits both use `Refs:`; the manage-story skill's `draft → in-progress` auto-transition trigger discriminates by "is this the capture commit (subject starts with `capture`) or a subsequent commit" rather than by trailer verb.
314
+
315
+ ### 7. Report
316
+
317
+ After the commit, report:
318
+
319
+ - The new story file path and ID.
320
+ - The traced problems with their lifecycle states.
321
+ - The traced JTBDs with their lifecycle states.
322
+ - Any traced RFCs / story-maps if provided, with lifecycle-state advisory warnings.
323
+ - Any unplaced-on-story-map advisory (always emit when `--story-map STORY-MAP-<NNN>` was provided — the HTML placement is manual per architect finding 2 on Slice 7).
324
+ - Trailing pointer: `Run /wr-itil:manage-story <STORY-<NNN>> next to populate User value + Acceptance criteria + Estimated effort, then advance draft → accepted; refresh docs/stories/README.md.`
325
+
326
+ The trailing pointer is **not optional** — it is the user-visible signal that the story is intentionally skeleton-only and how to advance it.
327
+
328
+ ## Composition with manage-story
329
+
330
+ | Concern | manage-story | capture-story |
331
+ |---------|--------------|---------------|
332
+ | Problem-trace I6 enforcement | Re-validated at every lifecycle transition | Hard-block at capture-time; deny logged to `logs/story-capture-denials.jsonl` |
333
+ | JTBD-trace I9 enforcement | Re-validated at every lifecycle transition | Hard-block at capture-time |
334
+ | 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 |
335
+ | Story-map-trace I8 enforcement | Hard-block at `accepted` transition | Same advisory-warn pattern at capture-time |
336
+ | INVEST shape (I10) | Behavioural checks at `accepted` transition | Out of scope: capture produces a skeleton with deferred-placeholder sections |
337
+ | Skeleton-fill | Full-intake; AskUserQuestion for User value + Acceptance criteria + Estimated effort | Deferred-placeholder pattern; one optional taste prompt only |
338
+ | Status transitions | Step 7 owns draft → accepted → in-progress → done | Out of scope (creation only) |
339
+ | `## Stories` README refresh | P094 / P062 inline (regenerate + stage in same commit) | Deferred to `/wr-itil:manage-story review` or `wr-itil-reconcile-stories` (Slice 9) |
340
+ | Commit grain | One commit per intake / per transition | One commit per capture |
341
+ | Use case | Full lifecycle management | Aside-invocation; capture-and-continue |
342
+
343
+ The two skills share the `/tmp/wr-itil-story-capture-grep-${SESSION_ID}` create-gate marker (sibling to the capture-rfc marker per architect verdict on capture-rfc sub-decision (a)).
344
+
345
+ ## Related
346
+
347
+ - **ADR-060** — Problem-RFC-Story framework with mandatory problem-trace and unified problem ontology + Phase 2 amendment 2026-05-12 (story tier).
348
+ - **ADR-060 lines 220-228** — Story frontmatter shape spec.
349
+ - **ADR-060 lines 248-253** — I6-I11 story-tier invariants.
350
+ - **ADR-060 line 291** — capture-story description (this skill's source-of-truth contract).
351
+ - **ADR-060 line 307 + amendment 2026-05-10 nitpick N2** — single-trailer vocabulary (`Refs: STORY-<NNN>`).
352
+ - **P170** — driver problem ticket.
353
+ - **JTBD-008** — Decompose a Fix Into Coordinated Changes. Primary persona-anchor.
354
+ - **JTBD-001** (extended scope) — change-set-level governance composition.
355
+ - **JTBD-101** (atomic-fix-adopter friction guard) — capture-story remains opt-in aside-invocation; atomic-RFC fallback per ADR-060 line 262.
356
+ - **`docs/stories/README.md`** — story tier lifecycle index + frontmatter/body shape spec (P170 Phase 2 Slice 1 — committed `8562bbc`).
357
+ - **ADR-010** — amended skill-granularity: capture-story + manage-story are two skills, not one.
358
+ - **ADR-014** — single-commit grain per capture. Commit-message convention.
359
+ - **ADR-022** — problem lifecycle conventions; story lifecycle mirrors (draft / accepted / in-progress / done / archived).
360
+ - **ADR-032** — governance-skill aside-invocation pattern. Lightweight + heavyweight split.
361
+ - **ADR-038** — progressive disclosure. SKILL.md (this file) + future REFERENCE.md split deferred per ADR-054.
362
+ - **ADR-044** — decision delegation contract. Authority classes named in the Rule 6 audit table.
363
+ - **ADR-049** — plugin-bundled scripts via `bin/` on `$PATH`. `wr-itil-reconcile-stories` shim follows this grammar (Slice 9).
364
+ - **ADR-051** — load-bearing-from-the-start. I6 + I9 hard-block ship behaviourally on day one.
365
+ - **ADR-052** — behavioural-tests default. Bats coverage at `packages/itil/skills/capture-story/test/capture-story-behavioural.bats` (this slice).
366
+ - **ADR-060 Phase 2 Slice 2a/2b reverse-trace helpers** — `update-problem-references-section.sh`, `update-jtbd-references-section.sh`, `update-rfc-references-section.sh` all support `"Stories"` section-name token (verified lookup-table entries).
367
+ - **Capture-rfc precedent** — `packages/itil/skills/capture-rfc/SKILL.md` — sibling skill at the RFC tier; structurally near-identical surface.
368
+ - **P078** capture-on-correction — capture-story may be the correct response to a strong-signal user correction that names a single INVEST-shaped sub-workstream within an existing RFC.
369
+ - **P132 + inverse-P078** — mechanical-stage carve-outs prevent over-asking; named in the Rule 6 audit table.
370
+
371
+ ## Phase-out-of-order note
372
+
373
+ 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.