@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -14
- package/bin/{wr-itil-check-afk-accept-eligible → wr-itil-render-story-map} +2 -2
- package/bin/wr-itil-story-map-edit +51 -0
- package/bin/wr-itil-story-map-query +51 -0
- package/hooks/itil-no-implement-draft-gate.sh +19 -28
- package/lib/story-oversight.sh +154 -60
- package/package.json +2 -1
- package/scripts/check-rfc-stories-ratified.sh +15 -9
- package/scripts/detect-unratified-stories-maps.sh +15 -32
- package/scripts/mark-story-oversight-confirmed.sh +80 -31
- package/scripts/migrate-story-status-mirror.sh +7 -3
- package/scripts/reconcile-stories.sh +26 -5
- package/scripts/render-story-map.mjs +1057 -0
- package/scripts/render-story-map.sh +15 -0
- package/scripts/story-map-edit.mjs +280 -0
- package/scripts/story-map-edit.sh +10 -0
- package/scripts/story-map-query.mjs +227 -0
- package/scripts/story-map-query.sh +68 -0
- package/scripts/update-story-references-section.sh +6 -1
- package/skills/capture-rfc/SKILL.md +1 -1
- package/skills/capture-story/SKILL.md +16 -8
- package/skills/capture-story-map/SKILL.md +102 -59
- package/skills/list-stories/SKILL.md +16 -10
- package/skills/list-story-maps/SKILL.md +1 -1
- package/skills/manage-rfc/SKILL.md +1 -1
- package/skills/manage-story/SKILL.md +19 -26
- package/skills/manage-story-map/SKILL.md +19 -17
- package/skills/reconcile-stories/SKILL.md +5 -2
- package/skills/work-problems/SKILL.md +2 -2
- package/templates/story-map.css +195 -0
- package/templates/story-map.html +23 -0
- package/scripts/check-afk-accept-eligible.sh +0 -230
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-
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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).
|
|
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.
|
|
18
|
-
#
|
|
19
|
-
#
|
|
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)
|
|
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-
|
|
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
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
package/lib/story-oversight.sh
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
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
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
|
|
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-
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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.
|
|
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-
|
|
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
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
-
|
|
55
|
-
|
|
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
|
|
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
|