@windyroad/itil 0.61.2 → 1.0.0

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 (33) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +2 -14
  3. package/bin/{wr-itil-check-afk-accept-eligible → wr-itil-render-story-map} +2 -2
  4. package/bin/wr-itil-story-map-edit +51 -0
  5. package/bin/wr-itil-story-map-query +51 -0
  6. package/hooks/itil-no-implement-draft-gate.sh +19 -28
  7. package/lib/story-oversight.sh +154 -60
  8. package/package.json +2 -1
  9. package/scripts/check-rfc-stories-ratified.sh +15 -9
  10. package/scripts/detect-unratified-stories-maps.sh +15 -32
  11. package/scripts/mark-story-oversight-confirmed.sh +80 -31
  12. package/scripts/migrate-story-status-mirror.sh +7 -3
  13. package/scripts/reconcile-stories.sh +26 -5
  14. package/scripts/render-story-map.mjs +1057 -0
  15. package/scripts/render-story-map.sh +15 -0
  16. package/scripts/story-map-edit.mjs +280 -0
  17. package/scripts/story-map-edit.sh +10 -0
  18. package/scripts/story-map-query.mjs +227 -0
  19. package/scripts/story-map-query.sh +68 -0
  20. package/scripts/update-story-references-section.sh +6 -1
  21. package/skills/capture-rfc/SKILL.md +1 -1
  22. package/skills/capture-story/SKILL.md +16 -8
  23. package/skills/capture-story-map/SKILL.md +102 -59
  24. package/skills/list-stories/SKILL.md +16 -10
  25. package/skills/list-story-maps/SKILL.md +1 -1
  26. package/skills/manage-rfc/SKILL.md +1 -1
  27. package/skills/manage-story/SKILL.md +19 -26
  28. package/skills/manage-story-map/SKILL.md +19 -17
  29. package/skills/reconcile-stories/SKILL.md +5 -2
  30. package/skills/work-problems/SKILL.md +2 -2
  31. package/templates/story-map.css +195 -0
  32. package/templates/story-map.html +23 -0
  33. package/scripts/check-afk-accept-eligible.sh +0 -230
@@ -497,5 +497,5 @@
497
497
  }
498
498
  },
499
499
  "name": "wr-itil",
500
- "version": "0.61.2"
500
+ "version": "1.0.0"
501
501
  }
package/README.md CHANGED
@@ -93,7 +93,7 @@ See [ADR-011](../../docs/decisions/011-manage-incident-skill.proposed.md) for th
93
93
  | `/wr-itil:capture-story` | Lightweight story-capture skill — mandatory problem-trace AND JTBD-trace per ADR-060 I6 + I9 invariants; optional `--rfc` / `--story-map` flags (I7 + I8 enforce at `accepted` transition); drafts an INVEST-shaped sub-workstream entity under a parent RFC (Phase 2 of the Problem-RFC-Story framework, P170 / ADR-060) | Experimental |
94
94
  | `/wr-itil:list-stories` | Read-only display of stories grouped by lifecycle state, with optional `--rfc RFC-<NNN>` filter rendering the RFC's ordered story list per ADR-060 line 259 (Phase 2 / P170) | Experimental |
95
95
  | `/wr-itil:reconcile-stories` | Detect and correct drift between `docs/stories/README.md` and on-disk story inventory + reverse-trace `## Stories` sections on driving problems / RFCs / JTBDs (Phase 2 / P170) | Experimental |
96
- | `/wr-itil:manage-story` | Heavyweight story lifecycle management — draft → accepted → in-progress → done → archived; I7+I8+I10+I12 hard-block at accepted transition; INVEST 4-axis check; auto-transitions on `Refs: STORY-NNN` commit trailer + linked RFC closure (Phase 2 / P170). **I12 (ADR-101)**: `accepted` requires a recorded ratification basis, and an implementing commit against an unratified story is blocked. The basis is a human ratification event by default; an `oversight-basis: pure-decomposition` marker is written instead only where the project has opted in AND the story decomposes nothing but already-confirmed substance see the config key below | Experimental |
96
+ | `/wr-itil:manage-story` | Heavyweight story lifecycle management — draft → accepted → in-progress → done → archived; I7+I8+I10+I12 hard-block at accepted transition; INVEST 4-axis check; auto-transitions on `Refs: STORY-NNN` commit trailer + linked RFC closure (Phase 2 / P170). **I12 (ADR-103)**: `accepted` requires the story to be approved, and an implementing commit against an unapproved story is blocked. Approval is the story map's ratify the map and every story on it is approved with it | Experimental |
97
97
  | `/wr-itil:capture-story-map` | Lightweight story-map-capture skill — mandatory problem-trace AND JTBD-trace per ADR-060 I3 + I4 invariants; HTML skeleton at `docs/story-maps/draft/STORY-MAP-NNN-<slug>.html` per ADR-060 § Phase 2 encoding amendment 2026-05-12 (Phase 2 / P170) | Experimental |
98
98
  | `/wr-itil:manage-story-map` | Heavyweight story-map lifecycle management — draft → accepted → in-progress → completed → archived; backbone/ribs/slices authoring guidance; reverse-trace `## Story Maps` refresh on driving problems + JTBDs (Phase 2 / P170) | Experimental |
99
99
  | `/wr-itil:reconcile-story-maps` | Detect and correct drift between `docs/story-maps/README.md` and on-disk story-map HTML inventory (Phase 2 / P170) | Experimental |
@@ -105,19 +105,7 @@ See [ADR-011](../../docs/decisions/011-manage-incident-skill.proposed.md) for th
105
105
 
106
106
  ## Configuration
107
107
 
108
- Optional. Create `.claude/itil.config.json` in your project (or `~/.claude/itil.config.json` for all your projects); a project key wins over a machine key, and both are optional.
109
-
110
- | Key | Default | What it does |
111
- |---|---|---|
112
- | `afk_accept_pure_decomposition` | `false` | Lets an unattended run accept and implement a story **without** asking you first — but only when that story adds no new thinking. Every decision, job and map it draws on must already be one you confirmed, and each of its acceptance criteria must name the confirmed clause it breaks down. Anything that introduces a new design choice, persona or decision is still held for you. Off unless you turn it on. |
113
-
114
- ```json
115
- { "afk_accept_pure_decomposition": true }
116
- ```
117
-
118
- Only the literal `true` turns it on. Stories accepted this way are marked `oversight-basis: pure-decomposition`, listed by `wr-itil-detect-unratified-stories-maps --with-afk-accepted`, and shown distinctly by `/wr-itil:list-stories`, so you can review them afterwards. Nothing ever writes this file for you. Background: ADR-101.
119
-
120
- Independently of this setting, an implementing commit against a story you have not ratified is blocked for everyone.
108
+ An implementing commit against a story you have not approved is blocked. A story's approval is its story map's: ratify the map and every story on it is approved with it.
121
109
 
122
110
  ## Updating and Uninstalling
123
111
 
@@ -30,7 +30,7 @@ SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
30
30
 
31
31
  # Source-repo guard: own parent dir is NOT semver → dispatch to own scripts/.
32
32
  if ! [[ "$OWN_VERSION_NAME" =~ $SEMVER_RE ]]; then
33
- exec "$SHIM_DIR/../scripts/check-afk-accept-eligible.sh" "$@"
33
+ exec "$SHIM_DIR/../scripts/render-story-map.sh" "$@"
34
34
  fi
35
35
 
36
36
  # Cache execution: pick the highest-semver sibling under CACHE_PARENT.
@@ -48,4 +48,4 @@ if [[ -z "$HIGHEST" ]]; then
48
48
  exit 127
49
49
  fi
50
50
 
51
- exec "$CACHE_PARENT/$HIGHEST/scripts/check-afk-accept-eligible.sh" "$@"
51
+ exec "$CACHE_PARENT/$HIGHEST/scripts/render-story-map.sh" "$@"
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ # Generated by scripts/sync-shim-wrappers.sh from
3
+ # packages/shared/lib/shim-wrapper-template.sh. DO NOT EDIT individual
4
+ # shim files in packages/*/bin/wr-* directly; edit the template + run
5
+ # `npm run sync:shim-wrappers` to regenerate.
6
+ #
7
+ # Resolution (ADR-080):
8
+ # 1. If the wrapper's parent dir is semver-shaped, treat as installed-
9
+ # cache execution and resolve to the highest-version sibling's
10
+ # scripts/ entry below.
11
+ # 2. Otherwise (parent dir is e.g. `architect`), treat as source-
12
+ # monorepo execution and dispatch to own scripts/. The source-repo-
13
+ # guard `exec` is the anchor parsed by
14
+ # packages/retrospective/scripts/check-tarball-shipped-shims.sh.
15
+ # 3. If the cache parent contains zero semver-shaped siblings, exit
16
+ # 127 with a stderr message naming the cache parent (per SQ-080-2).
17
+ #
18
+ # @adr ADR-080 (highest-version-wins shim wrapper plugin scaffold)
19
+ # @adr ADR-049 (plugin-bundled scripts resolve via bin/ on $PATH — amended)
20
+ # @problem P343 (mid-session staleness window)
21
+
22
+ set -euo pipefail
23
+
24
+ SHIM_DIR="$(cd "$(dirname "$0")" && pwd)"
25
+ OWN_VERSION_DIR="$(dirname "$SHIM_DIR")"
26
+ OWN_VERSION_NAME="$(basename "$OWN_VERSION_DIR")"
27
+ CACHE_PARENT="$(dirname "$OWN_VERSION_DIR")"
28
+
29
+ SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
30
+
31
+ # Source-repo guard: own parent dir is NOT semver → dispatch to own scripts/.
32
+ if ! [[ "$OWN_VERSION_NAME" =~ $SEMVER_RE ]]; then
33
+ exec "$SHIM_DIR/../scripts/story-map-edit.sh" "$@"
34
+ fi
35
+
36
+ # Cache execution: pick the highest-semver sibling under CACHE_PARENT.
37
+ HIGHEST=""
38
+ while IFS= read -r dir; do
39
+ name="$(basename "$dir")"
40
+ [[ "$name" =~ $SEMVER_RE ]] || continue
41
+ if [[ -z "$HIGHEST" ]] || [[ "$(printf '%s\n%s\n' "$HIGHEST" "$name" | sort -V | tail -1)" == "$name" ]]; then
42
+ HIGHEST="$name"
43
+ fi
44
+ done < <(find "$CACHE_PARENT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
45
+
46
+ if [[ -z "$HIGHEST" ]]; then
47
+ printf 'wr-shim: no cached versions in %s\n' "$CACHE_PARENT" >&2
48
+ exit 127
49
+ fi
50
+
51
+ exec "$CACHE_PARENT/$HIGHEST/scripts/story-map-edit.sh" "$@"
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ # Generated by scripts/sync-shim-wrappers.sh from
3
+ # packages/shared/lib/shim-wrapper-template.sh. DO NOT EDIT individual
4
+ # shim files in packages/*/bin/wr-* directly; edit the template + run
5
+ # `npm run sync:shim-wrappers` to regenerate.
6
+ #
7
+ # Resolution (ADR-080):
8
+ # 1. If the wrapper's parent dir is semver-shaped, treat as installed-
9
+ # cache execution and resolve to the highest-version sibling's
10
+ # scripts/ entry below.
11
+ # 2. Otherwise (parent dir is e.g. `architect`), treat as source-
12
+ # monorepo execution and dispatch to own scripts/. The source-repo-
13
+ # guard `exec` is the anchor parsed by
14
+ # packages/retrospective/scripts/check-tarball-shipped-shims.sh.
15
+ # 3. If the cache parent contains zero semver-shaped siblings, exit
16
+ # 127 with a stderr message naming the cache parent (per SQ-080-2).
17
+ #
18
+ # @adr ADR-080 (highest-version-wins shim wrapper plugin scaffold)
19
+ # @adr ADR-049 (plugin-bundled scripts resolve via bin/ on $PATH — amended)
20
+ # @problem P343 (mid-session staleness window)
21
+
22
+ set -euo pipefail
23
+
24
+ SHIM_DIR="$(cd "$(dirname "$0")" && pwd)"
25
+ OWN_VERSION_DIR="$(dirname "$SHIM_DIR")"
26
+ OWN_VERSION_NAME="$(basename "$OWN_VERSION_DIR")"
27
+ CACHE_PARENT="$(dirname "$OWN_VERSION_DIR")"
28
+
29
+ SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
30
+
31
+ # Source-repo guard: own parent dir is NOT semver → dispatch to own scripts/.
32
+ if ! [[ "$OWN_VERSION_NAME" =~ $SEMVER_RE ]]; then
33
+ exec "$SHIM_DIR/../scripts/story-map-query.sh" "$@"
34
+ fi
35
+
36
+ # Cache execution: pick the highest-semver sibling under CACHE_PARENT.
37
+ HIGHEST=""
38
+ while IFS= read -r dir; do
39
+ name="$(basename "$dir")"
40
+ [[ "$name" =~ $SEMVER_RE ]] || continue
41
+ if [[ -z "$HIGHEST" ]] || [[ "$(printf '%s\n%s\n' "$HIGHEST" "$name" | sort -V | tail -1)" == "$name" ]]; then
42
+ HIGHEST="$name"
43
+ fi
44
+ done < <(find "$CACHE_PARENT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
45
+
46
+ if [[ -z "$HIGHEST" ]]; then
47
+ printf 'wr-shim: no cached versions in %s\n' "$CACHE_PARENT" >&2
48
+ exit 127
49
+ fi
50
+
51
+ exec "$CACHE_PARENT/$HIGHEST/scripts/story-map-query.sh" "$@"
@@ -4,7 +4,8 @@
4
4
  # story still in docs/stories/draft/ — implementation requires `accepted` (where
5
5
  # the INVEST + RFC-trace gates + ADR-090 ratification fire). Capture commits are
6
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).
7
+ # (ADR-060 A4). Degrades to a no-op on abnormal paths characterised, not
8
+ # authorised; see the header note below.
8
9
  #
9
10
  # This is the enforcement locus the architect named as the ONLY one that catches
10
11
  # the exact P404 bypass — a direct implementing commit against a draft story,
@@ -14,25 +15,27 @@
14
15
  # (P465): a commit against an `accepted`/`in-progress` story is blocked unless
15
16
  # that story is ratified. This half is UNCONDITIONAL — a pure tightening, no
16
17
  # config, no opt-in — because putting an ADR-090-mandated check behind a flag
17
- # would be the decision conflict. Where the marker was machine-written under the
18
- # ADR-101 pure-decomposition carve-out, the gate additionally re-asserts the
19
- # story-LOCAL structure of that carve-out. It never re-evaluates the carve-out's
20
- # shared-artefact conditions: those are accept-time only, because unrelated churn
21
- # on a shared story map must not block an unrelated story (that is P456's shape).
18
+ # would be the decision conflict. Under ADR-103 the approval surface is the story
19
+ # MAP: a story carries no oversight marker of its own, and is approved when every
20
+ # map in its `story-maps:` field is ratified.
22
21
  #
23
22
  # Bypass: BYPASS_NO_IMPLEMENT_DRAFT=1.
24
23
  #
25
- # @adr ADR-096 (no-implement-while-draft) ADR-060 (lifecycle) ADR-013 (Rule 6)
24
+ # @adr ADR-096 (no-implement-while-draft) ADR-060 (lifecycle)
26
25
  # ADR-095 (sibling capture-time gates) ADR-052 (bats)
27
- # ADR-090 (drift-invalidated ratification) ADR-101 (AFK carve-out)
26
+ # ADR-090 (drift-invalidated ratification) ADR-103 (map is the approval surface)
28
27
  # @problem P404 P465 P456
29
28
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
30
29
  # shellcheck source=lib/command-detect.sh
31
30
  source "$SCRIPT_DIR/lib/command-detect.sh" 2>/dev/null || exit 0
32
- # Fail-open per ADR-013 Rule 6. A missing story-oversight lib degrades to the
33
- # draft-only gate; a missing command-detect lib (sourced above with the same
34
- # guard) exits before the draft check too, so that path degrades to no gate at
35
- # all. Both fail in the safe direction never blocking every commit.
31
+ # Degrades to a no-op on a missing lib. NOT authorised by any in-force decision:
32
+ # ADR-013 (Structured user interaction for governance-skill decisions) Rule 6
33
+ # governs a skill that cannot reach AskUserQuestion, and its Continue clause
34
+ # reads "Do NOT silently fail-soft-skip". This posture is CHARACTERISED, not
35
+ # endorsed — see "the gate degrades to a no-op when its predicate lib is
36
+ # missing" in hooks/test/. Both source guards are terminal and both precede
37
+ # every check, so a missing lib — either one — degrades to no gate at all.
38
+ # Open question queued at P369.
36
39
  # shellcheck source=../lib/story-oversight.sh
37
40
  source "$SCRIPT_DIR/../lib/story-oversight.sh" 2>/dev/null || exit 0
38
41
 
@@ -92,22 +95,10 @@ while IFS= read -r id; do
92
95
  [ ${#livefiles[@]} -gt 0 ] || continue
93
96
  story="${livefiles[0]}"
94
97
 
95
- if ! is_story_map_ratified "$story"; then
96
- deny "BLOCKED (ADR-090 / ADR-096 / P465): this commit references ${id}, but ${id} is not ratified — it carries no confirmed human-oversight marker, or its content has drifted since it was ratified. ADR-096 requires ratification before any implementation. Ratify it via /wr-itil:manage-story ${id} ratify, then re-commit. Bypass: BYPASS_NO_IMPLEMENT_DRAFT=1."
97
- fi
98
-
99
- # ADR-101 belt-and-braces. The load-bearing catch for a post-accept edit is
100
- # the hash above (both `afk-accept:` and `## Decomposition basis` sit inside
101
- # it). This adds a specific, actionable deny reason instead of a generic drift
102
- # deny, on a story-local structure no shared-artefact churn can invalidate.
103
- if oversight_is_pure_decomposition "$story"; then
104
- oversight_declares_pure_decomposition "$story" || deny \
105
- "BLOCKED (ADR-101): ${id} carries a machine-written pure-decomposition marker but no longer declares \`afk-accept: pure-decomposition\`. Re-accept it, or ratify it as a human via /wr-itil:manage-story ${id} ratify."
106
- crit=$(awk '/^##[[:space:]]+Acceptance criteria/{i=1;next} i&&/^##[[:space:]]/{exit} i' "$story" | grep -cE '^- \[[ xX]\]')
107
- basis=$(awk '/^##[[:space:]]+Decomposition basis/{i=1;next} i&&/^##[[:space:]]/{exit} i' "$story" | grep -cE '^- ')
108
- if [ "$crit" -eq 0 ] || [ "$basis" -ne "$crit" ]; then
109
- deny "BLOCKED (ADR-101): ${id} was AFK-accepted as pure decomposition, but its \`## Decomposition basis\` (${basis} entries) no longer matches its acceptance criteria (${crit}). Every criterion must name the already-confirmed clause it decomposes. Re-run /wr-itil:manage-story ${id} accepted, or ratify it as a human."
110
- fi
98
+ # ADR-103: a story's approval is its story MAP's. The story itself carries no
99
+ # oversight marker, so there is one question to ask and one place to fix it.
100
+ if ! story_is_approved "$story"; then
101
+ deny "BLOCKED (ADR-103 / ADR-096 / P465): this commit references ${id}, but ${id} is not approved. A story is approved by its story map: every map in its \`story-maps:\` field must be ratified, and ${id} either names no map or names one that is unratified or has drifted since it was ratified. Ratify the map via /wr-itil:manage-story-map <MAP-ID> ratify, then re-commit. Bypass: BYPASS_NO_IMPLEMENT_DRAFT=1."
111
102
  fi
112
103
  done <<< "$STORIES"
113
104
 
@@ -5,7 +5,7 @@
5
5
  # mark-story-oversight-confirmed.sh so all three agree on ONE hash definition
6
6
  # (if they diverged, a freshly-ratified artefact would read as drifted forever).
7
7
  #
8
- # A story/map is RATIFIED when it carries a `confirmed` human-oversight marker
8
+ # A story MAP is RATIFIED when it carries a `confirmed` human-oversight marker
9
9
  # AND a stored oversight-hash that matches a fresh hash of its content-minus-
10
10
  # marker. Any content edit changes the hash → the artefact reads as drifted /
11
11
  # unratified until re-ratified. This is ADR-090's drift-invalidation (ADR-009
@@ -19,6 +19,12 @@
19
19
  # encodings in one filter:
20
20
  # - markdown: `human-oversight:` / `oversight-hash:` frontmatter lines
21
21
  # - HTML: <meta name="human-oversight" ...> / <meta name="oversight-hash" ...>
22
+ # - ADR-102 data island: the same fields as JSON keys, `"humanOversight":` /
23
+ # `"oversightHash":` / `"oversightDate":` / `"oversightNote":`. Since ADR-102
24
+ # scopes a map's hash to the island, the marker now lives INSIDE the hashed
25
+ # region — so without this third spelling, ratifying a map mutated the very
26
+ # bytes it was fingerprinting and the stored hash could never match. Every
27
+ # map would have read as drifted the instant it was ratified.
22
28
  # THE single definition of what the fingerprint ignores. Reads stdin, writes the
23
29
  # normalised stream. Both hash functions route through this and neither carries
24
30
  # its own copy — that duplication is why the P474 `**Status**:` mirror had to be
@@ -40,7 +46,7 @@
40
46
  # content — and `itil-no-implement-draft-gate` sources this lib under
41
47
  # `2>/dev/null || exit 0`, so that failure would remove the gate silently.
42
48
  _oversight_filter() {
43
- grep -vE '^(human-oversight|oversight-hash|oversight-basis|status):|<meta[^>]*name="(human-oversight|oversight-hash|oversight-basis|status)"' \
49
+ grep -vE '^(human-oversight|oversight-hash|oversight-basis|status):|<meta[^>]*name="(human-oversight|oversight-hash|oversight-basis|status)"|^[[:space:]]*"(humanOversight|oversightHash|oversightBasis|oversightDate|oversightNote|status)"[[:space:]]*:' \
44
50
  | sed -E 's/- \[[ xX]\]/- [ ]/g; s/data-status="[^"]*"/data-status=""/g'
45
51
  }
46
52
 
@@ -51,77 +57,113 @@ oversight_excluded_keys() {
51
57
  printf '%s\n' human-oversight oversight-hash oversight-basis status
52
58
  }
53
59
 
54
- oversight_content_hash() {
55
- # Input path: the file is fed to the filter directly, so trailing blank lines
56
- # are PRESERVED. Deliberately NOT unified with the map variant below — see the
57
- # note there. Changing this changes every stored fingerprint.
58
- _oversight_filter < "$1" | shasum -a 256 | awk '{print $1}'
59
- }
60
-
61
- # Hash a story MAP's content while EXCLUDING the single-line card elements whose
62
- # data-story-id is in the caller-supplied set (ADR-101 condition (a), map leg).
60
+ # The MAP-SUBSTANCE key set: the island keys a story map's ratification hangs on.
63
61
  #
64
- # ADR-095 requires story-map membership at capture, so authoring a story ALWAYS
65
- # adds a card to its map which, under ADR-090 drift-invalidation, re-opens the
66
- # map's ratification by construction. Requiring a hash-matching map would make
67
- # the AFK-accept carve-out unsatisfiable: capturing the story would break the
68
- # very condition the story must satisfy.
62
+ # Editing this set here or in the SUBSTANCE tuple below — changes ADR-090's
63
+ # Decision Outcome as amended by ADR-103, not merely a mechanism. It IS the drift
64
+ # trigger. A change here needs an ADR-090 amendment, and the bidirectional test
65
+ # will NOT catch that: it asserts the accessor and the tuple agree, so it passes
66
+ # whenever both move together, which is the shape of exactly that edit. The
67
+ # sibling notice on _oversight_filter says the same thing for the same reason.
69
68
  #
70
- # This COARSENS the drift trigger to a coherent edit-set — the remedy ADR-090's
71
- # own Reassessment Criteria authorises rather than dropping to write-once,
72
- # which ADR-090 explicitly forbids. Any map edit OTHER than adding the named
73
- # cards still drifts the hash, so the condition stays load-bearing.
74
- oversight_content_hash_excluding_stories() {
75
- local f="$1"; shift
76
- local filtered id
77
- filtered="$(cat "$f")"
78
- for id in "$@"; do
79
- [ -n "$id" ] || continue
80
- # Anchored on the closing quote so STORY-05 cannot strip STORY-054's card.
81
- filtered="$(printf '%s\n' "$filtered" | grep -vF "data-story-id=\"${id}\"" || true)"
82
- done
83
- # Input path: `$(cat)` above stripped ALL trailing newlines and this `printf`
84
- # re-adds exactly one, so trailing blank lines are COLLAPSED here where
85
- # `oversight_content_hash` preserves them. That divergence is pre-existing and
86
- # is preserved deliberately — unifying the two input paths would silently change
87
- # one function's hash and un-ratify every stored fingerprint at once. A
88
- # consequence worth knowing: with zero ids this does NOT equal
89
- # `oversight_content_hash` for an artefact with trailing blank lines, which is
90
- # recorded as its own P474 task because it makes ADR-101's map leg
91
- # unsatisfiable for such a map even with the right card excluded.
92
- printf '%s\n' "$filtered" | _oversight_filter | shasum -a 256 | awk '{print $1}'
69
+ # SCOPE: island keys only. Unlike oversight_excluded_keys, which strips keys from
70
+ # every artefact in three encodings, this set is meaningless for anything with no
71
+ # island — stories and pre-ADR-102 maps fall through to whole-file bytes. The two
72
+ # are not complements; `excluded substance` is not the key space.
73
+ #
74
+ # THIS IS THE ONLY ENUMERATION. Six documents used to restate it (ADR-090,
75
+ # ADR-103, ADR-105, capture-story-map, and manage-story-map twice) and by
76
+ # 2026-08-08 it had drifted in both directions at once: the ADRs still named
77
+ # `lead` and `traceProse`, dead the day before, while manage-story-map had lost
78
+ # `storyMapId` and `secondaryPersona`, both live. The prose now states the rule
79
+ # and points here which is what the excluded-key set has always done, and it
80
+ # has never drifted.
81
+ #
82
+ # Consumers DERIVE from this rather than restating it, the way the corpus lint
83
+ # derives its pattern from oversight_excluded_keys. A consumer that hardcodes the
84
+ # list starts a seventh enumeration.
85
+ oversight_map_substance_keys() {
86
+ printf '%s\n' storyMapId title persona secondaryPersona traces backbone caption
93
87
  }
94
88
 
95
- # True (0) if this artefact's `confirmed` marker was written by the ADR-101 AFK
96
- # pure-decomposition carve-out rather than by a human ratification event.
97
- # BSD grep has no \s use [[:space:]] (the P334 portability class).
98
- oversight_is_pure_decomposition() {
99
- grep -qE '^oversight-basis:[[:space:]]*pure-decomposition([[:space:]]|$)' "$1" 2>/dev/null
89
+ # Emit the bytes a fingerprint should cover.
90
+ #
91
+ # For a story map under ADR-102 that is the DATA ISLAND alone, not the whole
92
+ # file: the grid, the <style> block and the <meta> block are all generated from
93
+ # the island, so hashing the whole file would let a template change regenerate
94
+ # every map, drift every stored fingerprint, and silently revoke ratification
95
+ # across the corpus (ADR-090's marker is drift-invalidated). A presentation
96
+ # change must never revoke a substance approval.
97
+ #
98
+ # Anything with no island — stories (.md), and maps predating ADR-102 — falls
99
+ # through to whole-file bytes, so the story leg and the legacy corpus are
100
+ # untouched.
101
+ _oversight_hashable() {
102
+ local f="$1"
103
+ if grep -qF '<script id="story-map-data"' "$f" 2>/dev/null; then
104
+ # ADR-103: the fingerprint covers SUBSTANCE only — the map's identity and
105
+ # prose, its traces, and its backbone (the activity columns). Releases and
106
+ # the cards in them are SCHEDULING: drawing a row, or putting a story in
107
+ # one, is not a revision of what a human approved.
108
+ #
109
+ # This is what retires the ADR-101 carve-out. That existed because
110
+ # capturing a story onto its map drifted the map's hash, which broke the
111
+ # very condition the story had to satisfy. With cards outside the basis,
112
+ # the drift never happens and the carve-out has nothing to carve.
113
+ #
114
+ # NOT a sed range and not a JS reimplementation — see the awk note below
115
+ # and story-map-query.sh's header.
116
+ awk '
117
+ !inside && index($0, "<script id=\"story-map-data\"") { inside=1; next }
118
+ inside && index($0, "</script>") { exit }
119
+ inside { print }
120
+ ' "$f" | python3 -c '
121
+ import json, sys
122
+ try:
123
+ d = json.loads(sys.stdin.read().replace("\\u003c", "<"))
124
+ except Exception:
125
+ sys.exit(0) # unparseable island → empty basis, reads as drifted
126
+ # Only keys PRESENT in the island are included, so a key listed here that no
127
+ # map carries contributes nothing — which is why `lead` and `traceProse` could
128
+ # be removed on 2026-08-08 without moving a single stored hash. They had left
129
+ # the format the day before and were a live re-introduction vector: a field
130
+ # nothing documents would still have been ratification-bearing.
131
+ SUBSTANCE = ("storyMapId", "title", "persona", "secondaryPersona",
132
+ "traces", "backbone", "caption")
133
+ print(json.dumps({k: d[k] for k in SUBSTANCE if k in d}, sort_keys=True, indent=2))
134
+ '
135
+ else
136
+ cat "$f"
137
+ fi
100
138
  }
101
139
 
102
- # True (0) if the story DECLARES itself eligible for the ADR-101 carve-out.
103
- # Unlike `oversight-basis:` (marker-adjacent, excluded from the hash), this is an
104
- # AUTHORED claim and stays INSIDE the hash editing it re-opens ratification,
105
- # and it cannot be stripped to hide the story from the post-hoc drain.
106
- oversight_declares_pure_decomposition() {
107
- grep -qE '^afk-accept:[[:space:]]*pure-decomposition([[:space:]]|$)' "$1" 2>/dev/null
140
+ oversight_content_hash() {
141
+ # Input path: bytes are fed to the filter directly, so trailing blank lines
142
+ # are PRESERVED. Deliberately NOT unified with the map variant below see the
143
+ # note there. Changing this changes every stored fingerprint.
144
+ _oversight_hashable "$1" | _oversight_filter | shasum -a 256 | awk '{print $1}'
108
145
  }
109
146
 
110
- # ADR-101 map leg. Satisfied when the map is fully ratified (card already present
111
- # at ratification time), OR when it is `confirmed` and its stored hash matches the
112
- # content hash with THIS story's card excluded (card added after ratification).
113
- oversight_map_leg_ok() {
114
- local map="$1" story_id="$2"
115
- is_story_map_ratified "$map" && return 0
116
- oversight_is_confirmed "$map" || return 1
117
- [ "$(oversight_stored_hash "$map")" = "$(oversight_content_hash_excluding_stories "$map" "$story_id")" ]
118
- }
147
+ # ADR-103 retired the ADR-101 AFK pure-decomposition carve-out and its map leg.
148
+ # The carve-out existed because ADR-095 compels a card onto the map at capture,
149
+ # which under ADR-090 drift-invalidation re-opened the map's ratification by
150
+ # construction — the condition a story had to satisfy was broken by authoring the
151
+ # story. ADR-103 took cards out of the fingerprint basis entirely, so that no
152
+ # longer happens and there is nothing left to exclude. Removed with it:
153
+ # oversight_content_hash_excluding_stories, oversight_map_leg_ok,
154
+ # oversight_is_pure_decomposition, oversight_declares_pure_decomposition.
119
155
 
120
- # Echo the stored oversight-hash (md frontmatter OR HTML meta), empty if none.
156
+ # Echo the stored oversight-hash, empty if none. Three spellings carry the same
157
+ # fact: md frontmatter, an HTML <meta>, and — for an ADR-102 map — the data
158
+ # island, which is where mark-story-oversight-confirmed actually writes. The
159
+ # island MUST be read here: the <meta> is a projection the renderer regenerates
160
+ # from it, so a map marked but not yet re-rendered would otherwise read as
161
+ # unratified the instant it was ratified.
121
162
  oversight_stored_hash() {
122
163
  local h
123
164
  h="$(grep -oE '^oversight-hash:[[:space:]]*[a-f0-9]{64}' "$1" 2>/dev/null | grep -oE '[a-f0-9]{64}' | head -1)"
124
165
  [ -z "$h" ] && h="$(grep -oE '<meta[^>]*name="oversight-hash"[^>]*content="[a-f0-9]{64}"' "$1" 2>/dev/null | grep -oE '[a-f0-9]{64}' | head -1)"
166
+ [ -z "$h" ] && h="$(grep -oE '"oversightHash"[[:space:]]*:[[:space:]]*"[a-f0-9]{64}"' "$1" 2>/dev/null | grep -oE '[a-f0-9]{64}' | head -1)"
125
167
  printf '%s' "$h"
126
168
  }
127
169
 
@@ -129,6 +171,8 @@ oversight_stored_hash() {
129
171
  oversight_is_confirmed() {
130
172
  grep -qiE '^human-oversight:[[:space:]]*confirmed([[:space:]]|$)' "$1" && return 0
131
173
  grep -qiE '<meta[^>]*name="human-oversight"[^>]*content="confirmed"' "$1" && return 0
174
+ # ADR-102 island spelling — see oversight_stored_hash for why this is load-bearing.
175
+ grep -qiE '"humanOversight"[[:space:]]*:[[:space:]]*"confirmed"' "$1" && return 0
132
176
  return 1
133
177
  }
134
178
 
@@ -142,3 +186,53 @@ is_story_map_ratified() {
142
186
  [ -z "$stored" ] && return 1
143
187
  [ "$stored" = "$(oversight_content_hash "$f")" ]
144
188
  }
189
+
190
+ # Is this STORY approved? ADR-103 made the story map the approval surface, and a
191
+ # story carries no oversight marker of its own — not even one saying it inherits.
192
+ # A story is approved exactly when every map it names is ratified.
193
+ #
194
+ # Any `human-oversight:` line left on a story is legacy and is IGNORED here. It
195
+ # is not consulted as a fallback: a story-level marker that could still approve a
196
+ # story on an unratified map would keep alive the second approval surface this
197
+ # decision removed, and the drain would never finish.
198
+ #
199
+ # A story naming no map is NOT approved — otherwise dropping the `story-maps:`
200
+ # field would be a way to self-approve.
201
+ story_is_approved() {
202
+ local story="$1" maps_root="${2:-docs/story-maps}" line ids id f found=0
203
+
204
+ # `story-maps:` in inline `[A, B]` or block `- A` form.
205
+ line="$(awk '/^story-maps:/{print; exit}' "$story")"
206
+ ids="$(printf '%s' "$line" | grep -oE 'STORY-MAP-[0-9]+' || true)"
207
+ [ -n "$ids" ] || ids="$(awk '/^story-maps:/{g=1;next} g&&/^[[:space:]]*-/{print} g&&/^[^[:space:]-]/{exit}' "$story" \
208
+ | grep -oE 'STORY-MAP-[0-9]+' || true)"
209
+
210
+ for id in $ids; do
211
+ # Exactly one file must match under a lifecycle directory. This USED to be
212
+ # `ls ... | head -1`, which arbitrary-picks when an id resolves in more than
213
+ # one of them — silently, and in the permissive direction: the one branch in
214
+ # this predicate that could grant an approval nobody gave. Which copy wins is
215
+ # just glob sort order, so whether it lands on the live map or a stale one is
216
+ # luck. An ambiguous id is a corpus defect; deny and let it surface.
217
+ #
218
+ # Known gap, pre-existing and unreachable today (no map sits at the top
219
+ # level of docs/story-maps/): the detector walks BOTH "$MAPS_DIR"/*.html and
220
+ # "$MAPS_DIR"/*/*.html, so a top-level map is visible to it but invisible
221
+ # here, and a story naming one would read unapproved forever.
222
+ #
223
+ # No `shopt nullglob` here on purpose. This function is sourced, so toggling
224
+ # it would mutate the CALLER's shell — and one caller
225
+ # (detect-unratified-stories-maps.sh) sets nullglob once at the top and
226
+ # relies on it for a later glob. The `-e` test does the same job locally: an
227
+ # unmatched glob stays literal, and a literal path does not exist.
228
+ local cand n=0 m=""
229
+ for cand in "$maps_root"/*/"${id}"-*.html; do
230
+ [ -e "$cand" ] || continue
231
+ n=$((n + 1)); m="$cand"
232
+ done
233
+ [ "$n" -eq 1 ] || return 1
234
+ is_story_map_ratified "$m" || return 1
235
+ found=1
236
+ done
237
+ [ "$found" = 1 ]
238
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.61.2",
3
+ "version": "1.0.0",
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"
@@ -25,6 +25,7 @@
25
25
  "hooks/",
26
26
  "skills/",
27
27
  "scripts/",
28
+ "templates/",
28
29
  ".claude-plugin/",
29
30
  "lib/",
30
31
  "!skills/*/eval/",
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env bash
2
- # check-rfc-stories-ratified.sh — ADR-090 (an RFC may reference only RATIFIED stories).
2
+ # check-rfc-stories-ratified.sh — ADR-103 (an RFC may reference only APPROVED stories).
3
3
  #
4
4
  # For each STORY-NNN in the RFC's `stories:` frontmatter, resolve the story file
5
- # under <stories-root>/*/STORY-NNN-*.md and verify it carries
6
- # `human-oversight: confirmed`. Exit non-zero (with a stderr directive naming the
7
- # offending stories) if any listed story is unratified, unconfirmed, or missing.
5
+ # under <stories-root>/*/STORY-NNN-*.md and ask `story_is_approved`: is every map
6
+ # in that story's `story-maps:` field ratified? A story carries no oversight
7
+ # marker of its own (ADR-103), so the remedy for a failure is always to ratify
8
+ # the MAP. Exit non-zero, with a stderr directive naming the offending stories,
9
+ # if any listed story is unapproved or missing.
8
10
  #
9
11
  # Composes with check-rfc-has-stories.sh (ADR-089): has-stories checks >=1 story
10
12
  # exists; this checks each listed story is ratified. An empty `stories:` passes
@@ -14,7 +16,7 @@
14
16
  # Exit: 0 = all listed stories ratified (or none listed); 1 = >=1 unratified/missing;
15
17
  # 2 = usage / file error.
16
18
  #
17
- # Authority: ADR-090. Driver: P404 Phase 2. Test: check-rfc-stories-ratified.bats.
19
+ # Authority: ADR-090, ADR-103. Driver: P404 Phase 2. Test: check-rfc-stories-ratified.bats.
18
20
  set -euo pipefail
19
21
 
20
22
  # Adopter-safe: source the shared lazy-fingerprint lib RELATIVE TO THIS SCRIPT (P317).
@@ -25,8 +27,9 @@ source "$LIB/story-oversight.sh"
25
27
 
26
28
  rfc="${1:-}"
27
29
  stories_root="${2:-docs/stories}"
30
+ maps_root="${3:-docs/story-maps}"
28
31
  if [ -z "$rfc" ]; then
29
- echo "check-rfc-stories-ratified: usage: check-rfc-stories-ratified.sh <rfc-file> [stories-root]" >&2
32
+ echo "check-rfc-stories-ratified: usage: check-rfc-stories-ratified.sh <rfc-file> [stories-root] [maps-root]" >&2
30
33
  exit 2
31
34
  fi
32
35
  if [ ! -f "$rfc" ]; then
@@ -51,13 +54,16 @@ for id in $ids; do
51
54
  unratified="$unratified $id(missing)"
52
55
  continue
53
56
  fi
54
- if ! is_story_map_ratified "$f"; then
55
- unratified="$unratified $id(unratified)"
57
+ # ADR-103: approval reaches a story through its map. Judging it by the story
58
+ # predicate alone would re-create the P456 deadlock one tier up — the RFC tier
59
+ # would block on a story ADR-103 approves.
60
+ if ! story_is_approved "$f" "$maps_root"; then
61
+ unratified="$unratified $id(unapproved)"
56
62
  fi
57
63
  done
58
64
 
59
65
  if [ -n "$unratified" ]; then
60
- echo "check-rfc-stories-ratified: $rfc references unratified stories:$unratified — ADR-090: an RFC may reference only ratified (human-oversight: confirmed) stories. Ratify them first." >&2
66
+ echo "check-rfc-stories-ratified: $rfc references unapproved stories:$unratified — ADR-103: an RFC may reference only approved stories, and a story is approved by its story MAP. Ratify the map named in the story's story-maps: field via /wr-itil:manage-story-map <MAP-ID> ratify — a story carries no oversight marker of its own." >&2
61
67
  exit 1
62
68
  fi
63
69
  exit 0