@windyroad/itil 0.59.2 → 0.60.0-preview.1031

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -497,5 +497,5 @@
497
497
  }
498
498
  },
499
499
  "name": "wr-itil",
500
- "version": "0.59.2"
500
+ "version": "0.60.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 hard-block at accepted transition; INVEST 4-axis check; auto-transitions on `Refs: STORY-NNN` commit trailer + linked RFC closure (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 |
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 |
@@ -103,6 +103,22 @@ See [ADR-011](../../docs/decisions/011-manage-incident-skill.proposed.md) for th
103
103
  | `/wr-itil:mitigate-incident` / `/wr-itil:restore-incident` / `/wr-itil:close-incident` / `/wr-itil:link-incident` | Incident lifecycle transitions (ADR-011) | Experimental |
104
104
  | `/wr-itil:scaffold-intake` | Scaffold OSS intake surfaces (`.github/ISSUE_TEMPLATE/`, `SECURITY.md`, `SUPPORT.md`, `CONTRIBUTING.md`) for downstream adopters (ADR-036) | Experimental |
105
105
 
106
+ ## Configuration
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.
121
+
106
122
  ## Updating and Uninstalling
107
123
 
108
124
  ```bash
@@ -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/check-afk-accept-eligible.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/check-afk-accept-eligible.sh" "$@"
@@ -10,14 +10,31 @@
10
10
  # the exact P404 bypass — a direct implementing commit against a draft story,
11
11
  # whether from the orchestrator or by hand.
12
12
  #
13
+ # It ALSO enforces the ADR-090 ratification ADR-096 names but never implemented
14
+ # (P465): a commit against an `accepted`/`in-progress` story is blocked unless
15
+ # that story is ratified. This half is UNCONDITIONAL — a pure tightening, no
16
+ # 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).
22
+ #
13
23
  # Bypass: BYPASS_NO_IMPLEMENT_DRAFT=1.
14
24
  #
15
25
  # @adr ADR-096 (no-implement-while-draft) ADR-060 (lifecycle) ADR-013 (Rule 6)
16
26
  # ADR-095 (sibling capture-time gates) ADR-052 (bats)
17
- # @problem P404
27
+ # ADR-090 (drift-invalidated ratification) ADR-101 (AFK carve-out)
28
+ # @problem P404 P465 P456
18
29
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19
30
  # shellcheck source=lib/command-detect.sh
20
31
  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.
36
+ # shellcheck source=../lib/story-oversight.sh
37
+ source "$SCRIPT_DIR/../lib/story-oversight.sh" 2>/dev/null || exit 0
21
38
 
22
39
  INPUT=$(cat)
23
40
  TOOL_NAME=$(printf '%s' "$INPUT" | python3 -c "import sys,json
@@ -64,6 +81,34 @@ while IFS= read -r id; do
64
81
  if [ ${#draftfiles[@]} -gt 0 ]; then
65
82
  deny "BLOCKED (ADR-096 / P404): this commit references ${id} via a Refs: trailer, but ${id} is still in draft. A draft story cannot be implemented — accept it first via /wr-itil:manage-story ${id} accepted (it runs the INVEST + RFC-trace gates + ratification), then re-commit. Bypass: BYPASS_NO_IMPLEMENT_DRAFT=1."
66
83
  fi
84
+
85
+ # ADR-090 ratification at the implementation locus (P465). ADR-096's Decision
86
+ # Outcome claims no unratified story can ever be implemented; nothing enforced
87
+ # it until here. Scoped to accepted/in-progress — done/archived stories are
88
+ # past implementation and are not gated.
89
+ shopt -s nullglob
90
+ livefiles=(./docs/stories/accepted/${id}-*.md ./docs/stories/in-progress/${id}-*.md)
91
+ shopt -u nullglob
92
+ [ ${#livefiles[@]} -gt 0 ] || continue
93
+ story="${livefiles[0]}"
94
+
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
111
+ fi
67
112
  done <<< "$STORIES"
68
113
 
69
114
  exit 0
@@ -6,9 +6,17 @@
6
6
 
7
7
  setup() {
8
8
  HOOK="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/itil-no-implement-draft-gate.sh"
9
+ MARK="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../scripts" && pwd)/mark-story-oversight-confirmed.sh"
9
10
  TMP="$(mktemp -d)"; cd "$TMP"; git init -q .
10
- mkdir -p docs/stories/draft docs/stories/accepted
11
+ mkdir -p docs/stories/draft docs/stories/accepted docs/stories/in-progress
11
12
  }
13
+
14
+ # Seed a minimally well-formed story. Since ADR-101 the gate is ratification-
15
+ # aware, so a bare `touch` no longer produces a story it will pass.
16
+ seed_story() { printf -- '---\nstatus: accepted\n---\n\n# %s\n' "$1" > "$2"; }
17
+ # Ratify via the real write path, never a hand-written marker — the hash has one
18
+ # definition and a hand-rolled fixture would drift from it.
19
+ ratify() { bash "$MARK" "$@" >/dev/null 2>&1; }
12
20
  teardown() { cd /; rm -rf "$TMP"; }
13
21
 
14
22
  # write env.json carrying the given command string
@@ -25,8 +33,128 @@ Refs: STORY-042"'
25
33
  echo "$output" | grep -q 'STORY-042'
26
34
  }
27
35
 
28
- @test "allows a commit referencing an ACCEPTED story" {
29
- touch docs/stories/accepted/STORY-042-foo.md
36
+ # This case previously seeded an EMPTY, unratified accepted story and asserted
37
+ # allow — the P465 hole encoded as a test rather than behaviour anyone chose.
38
+ # ADR-096 always claimed no unratified story could be implemented; nothing
39
+ # enforced it. Split into both directions, flipped in the slice that ships the
40
+ # behaviour (the ADR-089 precedent).
41
+
42
+ @test "allows a commit referencing an ACCEPTED and RATIFIED story" {
43
+ seed_story STORY-042 docs/stories/accepted/STORY-042-foo.md
44
+ ratify docs/stories/accepted/STORY-042-foo.md
45
+ env_cmd 'git commit -m "fix: thing
46
+
47
+ Refs: STORY-042"'
48
+ run bash -c "bash '$HOOK' < env.json"
49
+ [ "$status" -eq 0 ]
50
+ ! echo "$output" | grep -q 'deny'
51
+ }
52
+
53
+ @test "blocks a commit referencing an ACCEPTED but UNRATIFIED story (P465)" {
54
+ seed_story STORY-042 docs/stories/accepted/STORY-042-foo.md
55
+ env_cmd 'git commit -m "fix: thing
56
+
57
+ Refs: STORY-042"'
58
+ run bash -c "bash '$HOOK' < env.json"
59
+ [ "$status" -eq 0 ]
60
+ echo "$output" | grep -q '"permissionDecision": "deny"'
61
+ echo "$output" | grep -q 'not ratified'
62
+ }
63
+
64
+ @test "blocks a commit referencing an IN-PROGRESS story whose content has drifted since ratification" {
65
+ seed_story STORY-042 docs/stories/in-progress/STORY-042-foo.md
66
+ ratify docs/stories/in-progress/STORY-042-foo.md
67
+ printf -- '\nA substance edit made after ratification.\n' >> docs/stories/in-progress/STORY-042-foo.md
68
+ env_cmd 'git commit -m "fix: thing
69
+
70
+ Refs: STORY-042"'
71
+ run bash -c "bash '$HOOK' < env.json"
72
+ [ "$status" -eq 0 ]
73
+ echo "$output" | grep -q '"permissionDecision": "deny"'
74
+ }
75
+
76
+ @test "ticking an acceptance criterion is progress, not drift — commit still allowed" {
77
+ cat > docs/stories/in-progress/STORY-042-foo.md <<'EOF'
78
+ ---
79
+ status: in-progress
80
+ ---
81
+
82
+ # STORY-042
83
+
84
+ ## Acceptance criteria (accepted-gate, INVEST Testable)
85
+
86
+ - [ ] The step happens.
87
+ EOF
88
+ ratify docs/stories/in-progress/STORY-042-foo.md
89
+ sed -i.bak 's/- \[ \] The step happens./- [x] The step happens./' docs/stories/in-progress/STORY-042-foo.md
90
+ rm -f docs/stories/in-progress/*.bak
91
+ env_cmd 'git commit -m "fix: thing
92
+
93
+ Refs: STORY-042"'
94
+ run bash -c "bash '$HOOK' < env.json"
95
+ [ "$status" -eq 0 ]
96
+ ! echo "$output" | grep -q 'deny'
97
+ }
98
+
99
+ @test "ADR-101: a pure-decomposition story whose basis no longer matches its criteria is blocked" {
100
+ cat > docs/stories/accepted/STORY-042-foo.md <<'EOF'
101
+ ---
102
+ status: accepted
103
+ afk-accept: pure-decomposition
104
+ ---
105
+
106
+ # STORY-042
107
+
108
+ ## Acceptance criteria (accepted-gate, INVEST Testable)
109
+
110
+ - [ ] One.
111
+ - [ ] Two.
112
+
113
+ ## Decomposition basis
114
+
115
+ - One decomposes ADR-900.
116
+ EOF
117
+ bash "$MARK" --pure-decomposition docs/stories/accepted/STORY-042-foo.md >/dev/null 2>&1
118
+ env_cmd 'git commit -m "fix: thing
119
+
120
+ Refs: STORY-042"'
121
+ run bash -c "bash '$HOOK' < env.json"
122
+ [ "$status" -eq 0 ]
123
+ echo "$output" | grep -q '"permissionDecision": "deny"'
124
+ echo "$output" | grep -q 'Decomposition basis'
125
+ }
126
+
127
+ @test "ADR-101: a well-formed pure-decomposition story is allowed" {
128
+ cat > docs/stories/accepted/STORY-042-foo.md <<'EOF'
129
+ ---
130
+ status: accepted
131
+ afk-accept: pure-decomposition
132
+ ---
133
+
134
+ # STORY-042
135
+
136
+ ## Acceptance criteria (accepted-gate, INVEST Testable)
137
+
138
+ - [ ] One.
139
+ - [ ] Two.
140
+
141
+ ## Decomposition basis
142
+
143
+ - One decomposes ADR-900.
144
+ - Two decomposes ADR-900.
145
+ EOF
146
+ bash "$MARK" --pure-decomposition docs/stories/accepted/STORY-042-foo.md >/dev/null 2>&1
147
+ env_cmd 'git commit -m "fix: thing
148
+
149
+ Refs: STORY-042"'
150
+ run bash -c "bash '$HOOK' < env.json"
151
+ [ "$status" -eq 0 ]
152
+ ! echo "$output" | grep -q 'deny'
153
+ }
154
+
155
+ @test "a DONE story is past implementation and is not ratification-gated" {
156
+ mkdir -p docs/stories/done
157
+ touch docs/stories/done/STORY-042-foo.md
30
158
  env_cmd 'git commit -m "fix: thing
31
159
 
32
160
  Refs: STORY-042"'
@@ -25,11 +25,64 @@ oversight_content_hash() {
25
25
  # ONLY a SUBSTANCE change re-opens ratification. Ticking a criterion or advancing
26
26
  # status/slice-progress is progress, not a change to what the user ratified; the
27
27
  # value statement, criterion TEXT, and structure still drift the hash.
28
- grep -vE '^(human-oversight|oversight-hash|status):|<meta[^>]*name="(human-oversight|oversight-hash|status)"' "$1" \
28
+ grep -vE '^(human-oversight|oversight-hash|oversight-basis|status):|<meta[^>]*name="(human-oversight|oversight-hash|oversight-basis|status)"' "$1" \
29
29
  | sed -E 's/- \[[ xX]\]/- [ ]/g; s/data-status="[^"]*"/data-status=""/g' \
30
30
  | shasum -a 256 | awk '{print $1}'
31
31
  }
32
32
 
33
+ # Hash a story MAP's content while EXCLUDING the single-line card elements whose
34
+ # data-story-id is in the caller-supplied set (ADR-101 condition (a), map leg).
35
+ #
36
+ # ADR-095 requires story-map membership at capture, so authoring a story ALWAYS
37
+ # adds a card to its map — which, under ADR-090 drift-invalidation, re-opens the
38
+ # map's ratification by construction. Requiring a hash-matching map would make
39
+ # the AFK-accept carve-out unsatisfiable: capturing the story would break the
40
+ # very condition the story must satisfy.
41
+ #
42
+ # This COARSENS the drift trigger to a coherent edit-set — the remedy ADR-090's
43
+ # own Reassessment Criteria authorises — rather than dropping to write-once,
44
+ # which ADR-090 explicitly forbids. Any map edit OTHER than adding the named
45
+ # cards still drifts the hash, so the condition stays load-bearing.
46
+ oversight_content_hash_excluding_stories() {
47
+ local f="$1"; shift
48
+ local filtered id
49
+ filtered="$(cat "$f")"
50
+ for id in "$@"; do
51
+ [ -n "$id" ] || continue
52
+ # Anchored on the closing quote so STORY-05 cannot strip STORY-054's card.
53
+ filtered="$(printf '%s\n' "$filtered" | grep -vF "data-story-id=\"${id}\"" || true)"
54
+ done
55
+ printf '%s\n' "$filtered" \
56
+ | grep -vE '^(human-oversight|oversight-hash|oversight-basis|status):|<meta[^>]*name="(human-oversight|oversight-hash|oversight-basis|status)"' \
57
+ | sed -E 's/- \[[ xX]\]/- [ ]/g; s/data-status="[^"]*"/data-status=""/g' \
58
+ | shasum -a 256 | awk '{print $1}'
59
+ }
60
+
61
+ # True (0) if this artefact's `confirmed` marker was written by the ADR-101 AFK
62
+ # pure-decomposition carve-out rather than by a human ratification event.
63
+ # BSD grep has no \s — use [[:space:]] (the P334 portability class).
64
+ oversight_is_pure_decomposition() {
65
+ grep -qE '^oversight-basis:[[:space:]]*pure-decomposition([[:space:]]|$)' "$1" 2>/dev/null
66
+ }
67
+
68
+ # True (0) if the story DECLARES itself eligible for the ADR-101 carve-out.
69
+ # Unlike `oversight-basis:` (marker-adjacent, excluded from the hash), this is an
70
+ # AUTHORED claim and stays INSIDE the hash — editing it re-opens ratification,
71
+ # and it cannot be stripped to hide the story from the post-hoc drain.
72
+ oversight_declares_pure_decomposition() {
73
+ grep -qE '^afk-accept:[[:space:]]*pure-decomposition([[:space:]]|$)' "$1" 2>/dev/null
74
+ }
75
+
76
+ # ADR-101 map leg. Satisfied when the map is fully ratified (card already present
77
+ # at ratification time), OR when it is `confirmed` and its stored hash matches the
78
+ # content hash with THIS story's card excluded (card added after ratification).
79
+ oversight_map_leg_ok() {
80
+ local map="$1" story_id="$2"
81
+ is_story_map_ratified "$map" && return 0
82
+ oversight_is_confirmed "$map" || return 1
83
+ [ "$(oversight_stored_hash "$map")" = "$(oversight_content_hash_excluding_stories "$map" "$story_id")" ]
84
+ }
85
+
33
86
  # Echo the stored oversight-hash (md frontmatter OR HTML meta), empty if none.
34
87
  oversight_stored_hash() {
35
88
  local h
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.59.2",
3
+ "version": "0.60.0-preview.1031",
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"
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env bash
2
+ # check-afk-accept-eligible.sh — ADR-101 AFK pure-decomposition carve-out.
3
+ #
4
+ # Decides whether an AFK loop may transition a story `draft -> accepted` with a
5
+ # machine-written `human-oversight: confirmed` marker, and implement it, WITHOUT
6
+ # a fresh human ratification. Opt-in and fail-closed: absent the project opt-in
7
+ # or the story's own declaration, the answer is always "not eligible".
8
+ #
9
+ # Eligible IFF the project has opted in AND the story declares
10
+ # `afk-accept: pure-decomposition` AND both conditions hold:
11
+ #
12
+ # (a) PARENT SUBSTANCE CONFIRMED — every `adrs:` / `jtbd:` entry resolves and
13
+ # carries `human-oversight: confirmed` (persona too); every `rfcs:` entry
14
+ # resolves and every ADR in THAT RFC's `adrs:` is confirmed; every
15
+ # `problems:` entry resolves; every `story-maps:` entry satisfies the
16
+ # ADR-101 map leg (fully ratified, OR confirmed with this story's own card
17
+ # excluded from the hash — ADR-095 compels that card, so it cannot be the
18
+ # thing that disqualifies the story). Map LIFECYCLE status is out of scope.
19
+ # The RFC tier holds no independent decisions per ADR-070 and so has no
20
+ # oversight marker of its own; condition (a) proxies through its `adrs:`.
21
+ #
22
+ # (b) NO NEW SUBSTANCE, ESTABLISHED POSITIVELY — a whitelist, not a novelty
23
+ # blacklist, because "introduces no new design choice" is a negative
24
+ # existential an agent cannot discharge. `## Decomposition basis` must
25
+ # carry exactly one entry per acceptance criterion, each naming the
26
+ # artefact whose already-confirmed clause that criterion decomposes, and
27
+ # every cited ID must be a member of the set (a) PROVED ratified —
28
+ # frontmatter membership is not ratification. Zero criteria is FAIL-CLOSED.
29
+ # A secondary blacklist rejects open-decision markers outside fenced or
30
+ # backticked spans.
31
+ #
32
+ # The failure direction is deliberate: conservatively hold a genuine
33
+ # decomposition rather than silently accept an embedded decision.
34
+ #
35
+ # Usage: check-afk-accept-eligible.sh <story-file> [maps-root] [rfcs-root]
36
+ # [jtbd-root] [problems-root] [decisions-root]
37
+ # Exit: 0 = eligible; 1 = not eligible (reasons on stderr); 2 = usage / file error.
38
+ #
39
+ # @adr ADR-101 (AFK pure-decomposition carve-out) ADR-090 (drift-invalidated
40
+ # oversight) ADR-095 ADR-096 ADR-060 ADR-070 ADR-098 (config shape)
41
+ # ADR-013 (Rule 6) ADR-052 (behavioural bats)
42
+ # @problem P456 (AFK iter cannot land a fix) P465 (accepted gate unenforced)
43
+ # @jtbd JTBD-006 (Progress the Backlog While I'm Away)
44
+ # @jtbd JTBD-003 (Compose Only the Guardrails I Need)
45
+ set -uo pipefail
46
+
47
+ # Adopter-safe: source the shared lazy-fingerprint lib RELATIVE TO THIS SCRIPT
48
+ # (P317), never repo-relative.
49
+ LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)" || {
50
+ echo "check-afk-accept-eligible: cannot locate lib dir" >&2; exit 2; }
51
+ # shellcheck source=/dev/null
52
+ source "$LIB/story-oversight.sh"
53
+
54
+ story="${1:-}"
55
+ maps_root="${2:-docs/story-maps}"
56
+ rfcs_root="${3:-docs/rfcs}"
57
+ jtbd_root="${4:-docs/jtbd}"
58
+ problems_root="${5:-docs/problems}"
59
+ decisions_root="${6:-docs/decisions}"
60
+
61
+ if [ -z "$story" ]; then
62
+ echo "check-afk-accept-eligible: usage: check-afk-accept-eligible.sh <story-file> [maps-root] [rfcs-root] [jtbd-root] [problems-root] [decisions-root]" >&2
63
+ exit 2
64
+ fi
65
+ [ -f "$story" ] || { echo "check-afk-accept-eligible: file not found: $story" >&2; exit 2; }
66
+
67
+ fail() { echo "check-afk-accept-eligible: NOT ELIGIBLE — $1" >&2; exit 1; }
68
+
69
+ # --- Project opt-in (ADR-101 split; config shape mirrors ADR-098) ------------
70
+ # Resolution FAILS OPEN to the built-in default (this never errors); because the
71
+ # built-in default is `false`, the POLICY outcome of every degraded path — no
72
+ # jq, no file, unreadable, unparseable, non-boolean — is not-eligible.
73
+ # Env trumps both files: a loosening override for bats and one-off use, NOT a
74
+ # project-configuration surface.
75
+ afk_accept_enabled() {
76
+ local v
77
+ case "${WR_ITIL_AFK_ACCEPT:-}" in
78
+ 1|true) return 0 ;;
79
+ 0|false) return 1 ;;
80
+ esac
81
+ command -v jq >/dev/null 2>&1 || return 1
82
+ for cfg in ".claude/itil.config.json" "$HOME/.claude/itil.config.json"; do
83
+ [ -r "$cfg" ] || continue
84
+ # Per-KEY fallback per ADR-098: a project file that simply omits the key
85
+ # must not strand the machine layer. Only a file that actually sets it
86
+ # decides the answer.
87
+ jq -e 'has("afk_accept_pure_decomposition")' "$cfg" >/dev/null 2>&1 || continue
88
+ # Strict: only the literal JSON `true` enables. The string form, 1, yes and
89
+ # null do not — a typo must never open a permission-loosening carve-out.
90
+ v="$(jq -r 'if .afk_accept_pure_decomposition == true then "true" else "false" end' "$cfg" 2>/dev/null || echo false)"
91
+ [ "$v" = "true" ] && return 0
92
+ return 1
93
+ done
94
+ return 1
95
+ }
96
+
97
+ afk_accept_enabled || fail "the project has not opted in to the ADR-101 carve-out (set \"afk_accept_pure_decomposition\": true in .claude/itil.config.json)"
98
+
99
+ # --- Declaration ------------------------------------------------------------
100
+ oversight_declares_pure_decomposition "$story" \
101
+ || fail "$story does not declare \`afk-accept: pure-decomposition\` — the carve-out is opt-in per story, never a default"
102
+
103
+ story_id="$(basename "$story" | grep -oE 'STORY-[0-9]+' | head -1)"
104
+ [ -n "$story_id" ] || fail "cannot resolve a STORY-NNN id from $story"
105
+
106
+ # Read one frontmatter list field (inline `[A, B]` or block `- A` form).
107
+ trace_ids() {
108
+ local field="$1" line ids
109
+ line="$(awk -v f="^${field}:" '$0 ~ f {print; exit}' "$story")"
110
+ ids="$(printf '%s' "$line" | grep -oE '(ADR|JTBD|RFC|STORY-MAP|P)-?[0-9]+' || true)"
111
+ if [ -z "$ids" ]; then
112
+ ids="$(awk -v f="^${field}:" '$0 ~ f {g=1;next} g&&/^[[:space:]]*-/{print} g&&/^[^[:space:]-]/{exit}' "$story" \
113
+ | grep -oE '(ADR|JTBD|RFC|STORY-MAP|P)-?[0-9]+' || true)"
114
+ fi
115
+ printf '%s' "$ids"
116
+ }
117
+
118
+ # The set condition (a) PROVES ratified. Condition (b) intersects against this,
119
+ # so a basis entry can never cite an artefact (a) never verified (B4).
120
+ RATIFIED_SET=""
121
+ add_ratified() { RATIFIED_SET="$RATIFIED_SET $1"; }
122
+
123
+ # (a) ADR parents — write-once oversight per ADR-066.
124
+ for id in $(trace_ids adrs); do
125
+ n="${id#ADR-}"
126
+ f="$(ls "$decisions_root"/"$n"-*.md 2>/dev/null | head -1)"
127
+ [ -n "$f" ] || fail "$id does not resolve under $decisions_root — a new decision is new substance"
128
+ oversight_is_confirmed "$f" || fail "$id is not \`human-oversight: confirmed\` — condition (a) requires every parent decision be human-confirmed"
129
+ add_ratified "$id"
130
+ done
131
+
132
+ # (a) JTBD parents + their personas — ADR-068 oversight sibling.
133
+ for id in $(trace_ids jtbd); do
134
+ n="${id#JTBD-}"
135
+ f="$(ls "$jtbd_root"/*/"JTBD-$n"-*.md 2>/dev/null | head -1)"
136
+ [ -n "$f" ] || fail "$id does not resolve under $jtbd_root — a new job is new substance"
137
+ oversight_is_confirmed "$f" || fail "$id is not \`human-oversight: confirmed\`"
138
+ p="$(dirname "$f")/persona.md"
139
+ if [ -f "$p" ]; then
140
+ oversight_is_confirmed "$p" || fail "the persona for $id ($p) is not \`human-oversight: confirmed\` — a new or unconfirmed persona is new substance"
141
+ fi
142
+ add_ratified "$id"
143
+ done
144
+
145
+ # (a) RFC parents — no oversight tier of their own (ADR-070); proxy via `adrs:`.
146
+ for id in $(trace_ids rfcs); do
147
+ n="${id#RFC-}"
148
+ f="$(ls "$rfcs_root"/"RFC-$n"-*.md 2>/dev/null | head -1)"
149
+ [ -n "$f" ] || fail "$id does not resolve under $rfcs_root"
150
+ rfc_adrs="$(awk '/^adrs:/{print; exit}' "$f" | grep -oE 'ADR-[0-9]+' || true)"
151
+ for a in $rfc_adrs; do
152
+ af="$(ls "$decisions_root"/"${a#ADR-}"-*.md 2>/dev/null | head -1)"
153
+ [ -n "$af" ] || fail "$id depends on $a which does not resolve under $decisions_root"
154
+ oversight_is_confirmed "$af" || fail "$id depends on $a which is not \`human-oversight: confirmed\`"
155
+ add_ratified "$a"
156
+ done
157
+ add_ratified "$id"
158
+ done
159
+
160
+ # (a) Problem parents — resolve only; problems carry no oversight tier.
161
+ for id in $(trace_ids problems); do
162
+ n="${id#P}"
163
+ f="$(ls "$problems_root"/"$n"-*.md "$problems_root"/*/"$n"-*.md 2>/dev/null | head -1)"
164
+ [ -n "$f" ] || fail "$id does not resolve under $problems_root"
165
+ add_ratified "$id"
166
+ done
167
+
168
+ # (a) Story-map parents — the ADR-101 map leg.
169
+ for id in $(trace_ids story-maps); do
170
+ n="${id#STORY-MAP-}"
171
+ f="$(ls "$maps_root"/*/"STORY-MAP-$n"-*.html 2>/dev/null | head -1)"
172
+ [ -n "$f" ] || fail "$id does not resolve under $maps_root"
173
+ oversight_map_leg_ok "$f" "$story_id" \
174
+ || fail "$id fails the ADR-101 map leg — it is not human-confirmed, or it has drifted for a reason other than adding ${story_id}'s own card. Ratify the map."
175
+ add_ratified "$id"
176
+ done
177
+
178
+ # --- (b) NO NEW SUBSTANCE, established positively ---------------------------
179
+ # Acceptance criteria: prefix-anchored section (headings carry trailing
180
+ # qualifiers like `(accepted-gate, INVEST Testable)`), terminated by the next
181
+ # `##`. Tick-insensitive so implementation progress never flips the check, and
182
+ # the criterion literal is aligned with oversight_content_hash's normaliser so
183
+ # every counted line is a normalised line.
184
+ section_of() {
185
+ awk -v h="$1" '
186
+ $0 ~ h {inside=1; next}
187
+ inside && /^##[[:space:]]/ {exit}
188
+ inside {print}
189
+ ' "$story"
190
+ }
191
+
192
+ # NO `\b` IN THESE ANCHORS, and do not re-add it. Two independent reasons:
193
+ # (1) awk escape-processes a `-v` assignment value, so `\b` arrives as a literal
194
+ # BACKSPACE (0x08) — the pattern then never matches a real heading, which
195
+ # silently makes this whole check inert. It fails CLOSED, so there is no
196
+ # permission hole, but the carve-out can never return eligible for anyone.
197
+ # (2) POSIX ERE has no `\b` at all, so it is equally wrong in the commit-locus
198
+ # copy of this count in itil-no-implement-draft-gate.sh, where the failure
199
+ # would be silent in a different way. Those two loci MUST stay aligned.
200
+ # Prefix matching is the documented intent anyway (ADR-101): corpus headings
201
+ # carry trailing qualifiers like `(accepted-gate, INVEST Testable)`.
202
+ criteria_count="$(section_of '^##[[:space:]]+Acceptance criteria' | grep -cE '^- \[[ xX]\]' || true)"
203
+ [ "${criteria_count:-0}" -gt 0 ] \
204
+ || fail "no top-level \`- [ ]\` criterion lines found in the acceptance-criteria section — condition (b) is fail-closed on zero, since an absent section is indistinguishable from absent criteria"
205
+
206
+ basis="$(section_of '^##[[:space:]]+Decomposition basis')"
207
+ [ -n "$(printf '%s' "$basis" | tr -d '[:space:]')" ] \
208
+ || fail "\`## Decomposition basis\` is missing or empty — condition (b) requires each criterion to name the already-confirmed clause it decomposes"
209
+
210
+ basis_count="$(printf '%s\n' "$basis" | grep -cE '^- ' || true)"
211
+ [ "${basis_count:-0}" -eq "$criteria_count" ] \
212
+ || fail "\`## Decomposition basis\` has $basis_count entries but there are $criteria_count acceptance criteria — every criterion must name the confirmed clause it decomposes"
213
+
214
+ # Every cited ID must be in the set condition (a) proved ratified (B4).
215
+ for cited in $(printf '%s' "$basis" | grep -oE '(ADR|JTBD|RFC|STORY-MAP)-[0-9]+' | sort -u); do
216
+ case " $RATIFIED_SET " in
217
+ *" $cited "*) ;;
218
+ *) fail "\`## Decomposition basis\` cites $cited, which condition (a) never verified as ratified — cite only traced, confirmed parents" ;;
219
+ esac
220
+ done
221
+
222
+ # Secondary blacklist. Fenced and backticked spans are stripped first so a story
223
+ # may DESCRIBE the blocked vocabulary (this one does) without tripping on it.
224
+ prose="$(awk '/^```/{f=!f;next} !f' "$story" | sed -E 's/`[^`]*`//g')"
225
+ if printf '%s' "$prose" | grep -qiE '\[Unratified Dependency\]|\bTBD\b|to be decided|open question|decision needed'; then
226
+ fail "an open-decision marker appears outside a fenced or backticked span — the story is not pure decomposition"
227
+ fi
228
+
229
+ echo "check-afk-accept-eligible: ELIGIBLE — $story_id is pure decomposition of confirmed substance ($criteria_count criteria, all basis citations ratified)" >&2
230
+ exit 0
@@ -11,7 +11,15 @@
11
11
  # - drift-reopened (marker says confirmed, but content changed → hash mismatch)
12
12
  # - legacy confirmed (confirmed but no fingerprint yet — needs one re-ratify)
13
13
  #
14
- # Usage: detect-unratified-stories-maps.sh [STORIES_DIR=docs/stories] [MAPS_DIR=docs/story-maps]
14
+ # With `--with-afk-accepted` (ADR-101) it ALSO lists, on stderr, stories whose
15
+ # `confirmed` marker was machine-written by the AFK pure-decomposition carve-out
16
+ # rather than by a human — they are awaiting post-hoc human ratification. The
17
+ # listing keys on the UNION of `afk-accept:` and `oversight-basis:`, because the
18
+ # latter is excluded from the content hash and could otherwise be stripped to
19
+ # hide a machine-accepted story from this drain; `afk-accept:` stays in the hash.
20
+ # stdout is untouched by the flag, so the Step 2.4 drain consumer is unaffected.
21
+ #
22
+ # Usage: detect-unratified-stories-maps.sh [--with-afk-accepted] [STORIES_DIR=docs/stories] [MAPS_DIR=docs/story-maps]
15
23
  # Output: one unratified artefact path per line, sorted. Empty = all ratified.
16
24
  # Always exits 0 (detector, not a gate). Consumed by the work-problems Step 2.4
17
25
  # oversight-unconfirmed drain.
@@ -23,6 +31,12 @@ LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)" || {
23
31
  # shellcheck source=/dev/null
24
32
  source "$LIB/story-oversight.sh"
25
33
 
34
+ WITH_AFK=0
35
+ if [ "${1:-}" = "--with-afk-accepted" ]; then
36
+ WITH_AFK=1
37
+ shift
38
+ fi
39
+
26
40
  STORIES_DIR="${1:-docs/stories}"
27
41
  MAPS_DIR="${2:-docs/story-maps}"
28
42
 
@@ -42,4 +56,14 @@ shopt -s nullglob
42
56
  fi
43
57
  } | sort
44
58
 
59
+ # ADR-101 post-hoc drain — stderr only, so the stdout path contract is preserved.
60
+ if [ "$WITH_AFK" = "1" ] && [ -d "$STORIES_DIR" ]; then
61
+ for f in "$STORIES_DIR"/*.md "$STORIES_DIR"/*/*.md; do
62
+ [ "$(basename "$f")" = "README.md" ] && continue
63
+ if oversight_declares_pure_decomposition "$f" || oversight_is_pure_decomposition "$f"; then
64
+ echo "AFK-ACCEPTED (awaiting post-hoc human ratification): $f" >&2
65
+ fi
66
+ done
67
+ fi
68
+
45
69
  exit 0
@@ -8,10 +8,19 @@
8
8
  # stored fingerprint no longer matches → the artefact reads as drifted /
9
9
  # unratified until re-ratified (ADR-090 drift-invalidation).
10
10
  #
11
- # Usage: mark-story-oversight-confirmed.sh <story-or-map-file>
11
+ # With `--pure-decomposition` (ADR-101) it additionally records
12
+ # `oversight-basis: pure-decomposition` — the marker was written by an AFK loop
13
+ # under the carve-out, not by a human ratification event. That field is
14
+ # markdown-story-only (a map is always human-ratified and never carries a
15
+ # decomposition basis) and is EXCLUDED from the content hash, exactly like
16
+ # `oversight-hash`; the authored `afk-accept:` declaration stays INSIDE the hash.
17
+ # Without the flag the basis line is dropped, so re-ratifying by hand clears a
18
+ # stale basis rather than leaving a human-ratified story labelled AFK-accepted.
19
+ #
20
+ # Usage: mark-story-oversight-confirmed.sh [--pure-decomposition] <story-or-map-file>
12
21
  # Exit: 0 = ratified; 2 = usage / file error.
13
22
  #
14
- # Authority: ADR-090. Driver: P404 Phase 2. Test: mark-story-oversight-confirmed.bats.
23
+ # Authority: ADR-090, ADR-101. Driver: P404 Phase 2, P456. Test: mark-story-oversight-confirmed.bats.
15
24
  set -euo pipefail
16
25
 
17
26
  # Adopter-safe: source the shared hash lib RELATIVE TO THIS SCRIPT (P317), never
@@ -21,9 +30,15 @@ LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)" || {
21
30
  # shellcheck source=/dev/null
22
31
  source "$LIB/story-oversight.sh"
23
32
 
33
+ basis=""
34
+ if [ "${1:-}" = "--pure-decomposition" ]; then
35
+ basis="pure-decomposition"
36
+ shift
37
+ fi
38
+
24
39
  f="${1:-}"
25
40
  if [ -z "$f" ]; then
26
- echo "mark-story-oversight-confirmed: usage: mark-story-oversight-confirmed.sh <file>" >&2
41
+ echo "mark-story-oversight-confirmed: usage: mark-story-oversight-confirmed.sh [--pure-decomposition] <file>" >&2
27
42
  exit 2
28
43
  fi
29
44
  [ -f "$f" ] || { echo "mark-story-oversight-confirmed: file not found: $f" >&2; exit 2; }
@@ -54,12 +69,13 @@ case "$f" in
54
69
  *)
55
70
  # Markdown: rewrite frontmatter — drop existing markers, insert both before
56
71
  # the closing `---`.
57
- awk -v H="$h" '
72
+ awk -v H="$h" -v B="$basis" '
58
73
  NR==1 && $0=="---" { infm=1; print; next }
59
- infm && /^(human-oversight|oversight-hash):/ { next }
74
+ infm && /^(human-oversight|oversight-hash|oversight-basis):/ { next }
60
75
  infm && /^---[[:space:]]*$/ && !done {
61
76
  print "human-oversight: confirmed"
62
77
  print "oversight-hash: " H
78
+ if (B != "") print "oversight-basis: " B
63
79
  print; done=1; infm=0; next
64
80
  }
65
81
  { print }
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env bats
2
+ # Behavioural tests for check-afk-accept-eligible.sh (ADR-101 / P456 / P465).
3
+ # Each case maps onto an ADR-101 Confirmation item. Behavioural, not structural:
4
+ # every test runs the real script against a real fixture tree and asserts on its
5
+ # exit status and stderr, never on its source text (ADR-052 / P081).
6
+ #
7
+ # The POSITIVE case is the load-bearing one. An earlier revision shipped an
8
+ # `awk -v` escape bug that made the checker permanently inert — it fails closed,
9
+ # so no negative test could ever have caught it. Only asserting that a
10
+ # well-formed story IS eligible surfaces that class of bug.
11
+ #
12
+ # @adr ADR-101 ADR-090 ADR-095 ADR-096 ADR-098 ADR-052 @problem P456 P465
13
+
14
+ setup() {
15
+ SCRIPTS="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
16
+ CHECK="$SCRIPTS/check-afk-accept-eligible.sh"
17
+ MARK="$SCRIPTS/mark-story-oversight-confirmed.sh"
18
+ TMP="$(mktemp -d)"; cd "$TMP"
19
+ mkdir -p docs/stories/draft docs/story-maps/draft docs/rfcs docs/jtbd/dev docs/problems/open docs/decisions
20
+ export HOME="$TMP/home"; mkdir -p "$HOME/.claude"
21
+
22
+ # --- confirmed parents -----------------------------------------------------
23
+ printf -- '---\nhuman-oversight: confirmed\n---\n\n# ADR-900\n' > docs/decisions/900-a-confirmed-decision.proposed.md
24
+ printf -- '---\nhuman-oversight: confirmed\n---\n\n# JTBD-900\n' > docs/jtbd/dev/JTBD-900-a-job.proposed.md
25
+ printf -- '---\nhuman-oversight: confirmed\n---\n\n# persona\n' > docs/jtbd/dev/persona.md
26
+ printf -- '---\nadrs: [ADR-900]\n---\n\n# RFC-900\n' > docs/rfcs/RFC-900-a-vehicle.proposed.md
27
+ printf -- '# P900\n' > docs/problems/open/900-a-problem.md
28
+
29
+ # A map that was ratified BEFORE the story's card was added — the common case,
30
+ # since ADR-095 compels the card at capture.
31
+ MAP=docs/story-maps/draft/STORY-MAP-900-a-map.html
32
+ printf '<html><head>\n<a class="slice" data-story-id="STORY-800">old</a>\n</head></html>\n' > "$MAP"
33
+ bash "$MARK" "$MAP" 2>/dev/null
34
+ # Now add STORY-901's card, drifting the map exactly as capture would.
35
+ python3 - "$MAP" <<'PY'
36
+ import sys
37
+ p=sys.argv[1]; s=open(p).read()
38
+ open(p,"w").write(s.replace('</head>',' <a class="slice" data-story-id="STORY-901">new</a>\n</head>'))
39
+ PY
40
+
41
+ export WR_ITIL_AFK_ACCEPT=1
42
+ }
43
+ teardown() { cd /; rm -rf "$TMP"; }
44
+
45
+ # A well-formed pure-decomposition story: 2 criteria, 2 basis entries, all
46
+ # citations traced and confirmed.
47
+ seed_story() {
48
+ cat > docs/stories/draft/STORY-901-a-story.md <<'EOF'
49
+ ---
50
+ status: draft
51
+ problems: [P900]
52
+ jtbd: [JTBD-900]
53
+ rfcs: [RFC-900]
54
+ story-maps: [STORY-MAP-900]
55
+ adrs: [ADR-900]
56
+ afk-accept: pure-decomposition
57
+ ---
58
+
59
+ # STORY-901: A story
60
+
61
+ ## User value
62
+
63
+ In order to land confirmed work, as a developer, I want the loop to proceed.
64
+
65
+ ## Acceptance criteria (accepted-gate, INVEST Testable)
66
+
67
+ - [ ] The first step happens.
68
+ - [ ] The second step happens.
69
+
70
+ ## Decomposition basis
71
+
72
+ - Criterion 1 decomposes ADR-900's rule about the first step.
73
+ - Criterion 2 decomposes ADR-900's rule about the second step.
74
+ EOF
75
+ }
76
+ run_check() { run bash "$CHECK" docs/stories/draft/STORY-901-a-story.md \
77
+ docs/story-maps docs/rfcs docs/jtbd docs/problems docs/decisions; }
78
+
79
+ # --- ADR-101 Confirmation: eligibility ---------------------------------------
80
+
81
+ @test "ELIGIBLE: pure-decomposition story whose parents are all confirmed" {
82
+ seed_story; run_check
83
+ [ "$status" -eq 0 ]
84
+ echo "$output" | grep -q 'ELIGIBLE'
85
+ }
86
+
87
+ @test "NOT eligible: a traced ADR parent is unconfirmed" {
88
+ seed_story
89
+ printf -- '---\nhuman-oversight: unconfirmed\n---\n\n# ADR-900\n' > docs/decisions/900-a-confirmed-decision.proposed.md
90
+ run_check
91
+ [ "$status" -eq 1 ]
92
+ echo "$output" | grep -q 'ADR-900'
93
+ }
94
+
95
+ @test "NOT eligible: a traced JTBD's persona is unconfirmed" {
96
+ seed_story
97
+ printf -- '---\nhuman-oversight: unconfirmed\n---\n\n# persona\n' > docs/jtbd/dev/persona.md
98
+ run_check
99
+ [ "$status" -eq 1 ]
100
+ echo "$output" | grep -qi 'persona'
101
+ }
102
+
103
+ @test "NOT eligible: a traced artefact does not resolve (new substance)" {
104
+ seed_story
105
+ rm docs/decisions/900-a-confirmed-decision.proposed.md
106
+ run_check
107
+ [ "$status" -eq 1 ]
108
+ }
109
+
110
+ @test "NOT eligible: basis entries do not match the criteria count" {
111
+ seed_story
112
+ printf -- '- [ ] A third step nobody accounted for.\n' >> docs/stories/draft/STORY-901-a-story.md
113
+ run_check
114
+ [ "$status" -eq 1 ]
115
+ echo "$output" | grep -q 'Decomposition basis'
116
+ }
117
+
118
+ @test "NOT eligible: basis cites an artefact condition (a) never verified" {
119
+ seed_story
120
+ sed -i.bak 's/ADR-900.s rule about the first step/ADR-999 rule about the first step/' docs/stories/draft/STORY-901-a-story.md
121
+ run_check
122
+ [ "$status" -eq 1 ]
123
+ echo "$output" | grep -q 'ADR-999'
124
+ }
125
+
126
+ @test "NOT eligible: the story does not declare the carve-out" {
127
+ seed_story
128
+ sed -i.bak '/^afk-accept:/d' docs/stories/draft/STORY-901-a-story.md
129
+ run_check
130
+ [ "$status" -eq 1 ]
131
+ echo "$output" | grep -q 'afk-accept'
132
+ }
133
+
134
+ @test "NOT eligible: no acceptance criteria at all (fail-closed on zero)" {
135
+ seed_story
136
+ sed -i.bak 's/^- \[ \] The first step happens.//; s/^- \[ \] The second step happens.//' docs/stories/draft/STORY-901-a-story.md
137
+ run_check
138
+ [ "$status" -eq 1 ]
139
+ }
140
+
141
+ # --- ADR-101 Confirmation: the secondary blacklist ---------------------------
142
+
143
+ @test "NOT eligible: an open-decision marker in ordinary prose" {
144
+ seed_story
145
+ printf -- '\nThe storage locus is still to be decided.\n' >> docs/stories/draft/STORY-901-a-story.md
146
+ run_check
147
+ [ "$status" -eq 1 ]
148
+ }
149
+
150
+ @test "ELIGIBLE: the same vocabulary inside a fenced or backticked span" {
151
+ seed_story
152
+ printf -- '\nThe checker rejects the phrase `to be decided` when it appears in prose:\n\n```\nopen question / TBD / decision needed\n```\n' \
153
+ >> docs/stories/draft/STORY-901-a-story.md
154
+ run_check
155
+ [ "$status" -eq 0 ]
156
+ }
157
+
158
+ # --- ADR-101 Confirmation: the map leg disjunction ---------------------------
159
+
160
+ @test "ELIGIBLE: map re-ratified WITH the story's card already present" {
161
+ seed_story
162
+ bash "$MARK" "$MAP" 2>/dev/null # re-ratify now that STORY-901's card exists
163
+ run_check
164
+ [ "$status" -eq 0 ]
165
+ }
166
+
167
+ @test "NOT eligible: map drifted for a reason OTHER than the story's own card" {
168
+ seed_story
169
+ printf '<p>an unrelated edit</p>\n' >> "$MAP"
170
+ run_check
171
+ [ "$status" -eq 1 ]
172
+ echo "$output" | grep -q 'STORY-MAP-900'
173
+ }
174
+
175
+ @test "NOT eligible: map carries no human-oversight marker at all" {
176
+ seed_story
177
+ grep -v 'oversight' "$MAP" > "$MAP.tmp" && mv "$MAP.tmp" "$MAP"
178
+ run_check
179
+ [ "$status" -eq 1 ]
180
+ }
181
+
182
+ # --- ADR-101 Confirmation: the opt-in split ----------------------------------
183
+
184
+ @test "NOT eligible without opt-in, even when (a) and (b) both hold" {
185
+ seed_story
186
+ unset WR_ITIL_AFK_ACCEPT
187
+ run_check
188
+ [ "$status" -eq 1 ]
189
+ echo "$output" | grep -q 'opted in'
190
+ }
191
+
192
+ @test "project config opts in" {
193
+ seed_story
194
+ unset WR_ITIL_AFK_ACCEPT
195
+ mkdir -p .claude
196
+ printf '{ "afk_accept_pure_decomposition": true }\n' > .claude/itil.config.json
197
+ run_check
198
+ [ "$status" -eq 0 ]
199
+ }
200
+
201
+ @test "a non-boolean config value does NOT opt in" {
202
+ seed_story
203
+ unset WR_ITIL_AFK_ACCEPT
204
+ mkdir -p .claude
205
+ printf '{ "afk_accept_pure_decomposition": "true" }\n' > .claude/itil.config.json
206
+ run_check
207
+ [ "$status" -eq 1 ]
208
+ }
209
+
210
+ @test "an unparseable config does NOT opt in" {
211
+ seed_story
212
+ unset WR_ITIL_AFK_ACCEPT
213
+ mkdir -p .claude
214
+ printf 'not json at all\n' > .claude/itil.config.json
215
+ run_check
216
+ [ "$status" -eq 1 ]
217
+ }
218
+
219
+ @test "ADR-098 per-key fallback: a project file omitting the key falls through to machine" {
220
+ seed_story
221
+ unset WR_ITIL_AFK_ACCEPT
222
+ mkdir -p .claude
223
+ printf '{ "some_other_key": 1 }\n' > .claude/itil.config.json
224
+ printf '{ "afk_accept_pure_decomposition": true }\n' > "$HOME/.claude/itil.config.json"
225
+ run_check
226
+ [ "$status" -eq 0 ]
227
+ }
228
+
229
+ @test "a project file setting the key FALSE wins over a machine TRUE" {
230
+ seed_story
231
+ unset WR_ITIL_AFK_ACCEPT
232
+ mkdir -p .claude
233
+ printf '{ "afk_accept_pure_decomposition": false }\n' > .claude/itil.config.json
234
+ printf '{ "afk_accept_pure_decomposition": true }\n' > "$HOME/.claude/itil.config.json"
235
+ run_check
236
+ [ "$status" -eq 1 ]
237
+ }
238
+
239
+ @test "jq absent resolves to the built-in default: NOT eligible" {
240
+ seed_story
241
+ unset WR_ITIL_AFK_ACCEPT
242
+ mkdir -p .claude bin
243
+ printf '{ "afk_accept_pure_decomposition": true }\n' > .claude/itil.config.json
244
+ run env PATH="/usr/bin:/bin" bash "$CHECK" docs/stories/draft/STORY-901-a-story.md \
245
+ docs/story-maps docs/rfcs docs/jtbd docs/problems docs/decisions
246
+ # Only meaningful where jq is genuinely off PATH; skip otherwise.
247
+ if PATH="/usr/bin:/bin" command -v jq >/dev/null 2>&1; then skip "jq is on the minimal PATH here"; fi
248
+ [ "$status" -eq 1 ]
249
+ }
250
+
251
+ # --- ADR-101 Confirmation: no automated path writes a `true` opt-in ----------
252
+
253
+ @test "no shipped installer or scaffold writes an opt-in value" {
254
+ # The guard that keeps opt-in from becoming opt-out-by-stealth. Any shipped
255
+ # code that WRITES this key would defeat the split; reading it is fine.
256
+ cd "$(dirname "$CHECK")/.."
257
+ # Guard the search roots. Pointed at a directory that does not exist, grep
258
+ # matches nothing and the assertion below passes for the wrong reason — and
259
+ # whether it passes at all then depends on which grep merges its warning into
260
+ # bats' $output.
261
+ [ -d bin ] && [ -d hooks ]
262
+ run grep -rIlE 'afk_accept_pure_decomposition' --include='*.mjs' --include='*.js' bin/ hooks/
263
+ [ -z "$output" ]
264
+ }
265
+
266
+ # --- ADR-101 Confirmation: detector stdout contract --------------------------
267
+
268
+ @test "detect-unratified-stories-maps stdout is byte-identical with and without the flag" {
269
+ seed_story
270
+ a="$(bash "$SCRIPTS/detect-unratified-stories-maps.sh" docs/stories docs/story-maps 2>/dev/null)"
271
+ b="$(bash "$SCRIPTS/detect-unratified-stories-maps.sh" --with-afk-accepted docs/stories docs/story-maps 2>/dev/null)"
272
+ [ "$a" = "$b" ]
273
+ }
274
+
275
+ @test "--with-afk-accepted surfaces the machine-accepted story on stderr" {
276
+ seed_story
277
+ run bash -c "bash '$SCRIPTS/detect-unratified-stories-maps.sh' --with-afk-accepted docs/stories docs/story-maps 2>&1 >/dev/null"
278
+ echo "$output" | grep -q 'STORY-901'
279
+ echo "$output" | grep -q 'AFK-ACCEPTED'
280
+ }
281
+
282
+ @test "the post-hoc drain still finds the story when oversight-basis is stripped" {
283
+ # The union key. `oversight-basis` is excluded from the content hash, so
284
+ # stripping it would otherwise hide a machine-accepted story from the drain;
285
+ # the authored `afk-accept:` declaration is inside the hash and closes that.
286
+ seed_story
287
+ bash "$MARK" --pure-decomposition docs/stories/draft/STORY-901-a-story.md 2>/dev/null
288
+ sed -i.bak '/^oversight-basis:/d' docs/stories/draft/STORY-901-a-story.md
289
+ run bash -c "bash '$SCRIPTS/detect-unratified-stories-maps.sh' --with-afk-accepted docs/stories docs/story-maps 2>&1 >/dev/null"
290
+ echo "$output" | grep -q 'STORY-901'
291
+ }
292
+
293
+ # --- the marker write path ---------------------------------------------------
294
+
295
+ @test "--pure-decomposition records the basis AND leaves the story ratified" {
296
+ seed_story
297
+ bash "$MARK" --pure-decomposition docs/stories/draft/STORY-901-a-story.md 2>/dev/null
298
+ grep -q '^oversight-basis: pure-decomposition' docs/stories/draft/STORY-901-a-story.md
299
+ # The basis line is excluded from the hash, so the story must NOT read as
300
+ # drifted the instant it is marked.
301
+ run bash -c "source '$SCRIPTS/../lib/story-oversight.sh'; is_story_map_ratified docs/stories/draft/STORY-901-a-story.md"
302
+ [ "$status" -eq 0 ]
303
+ }
304
+
305
+ @test "a plain re-ratify CLEARS a stale pure-decomposition basis" {
306
+ seed_story
307
+ bash "$MARK" --pure-decomposition docs/stories/draft/STORY-901-a-story.md 2>/dev/null
308
+ bash "$MARK" docs/stories/draft/STORY-901-a-story.md 2>/dev/null
309
+ ! grep -q '^oversight-basis:' docs/stories/draft/STORY-901-a-story.md
310
+ }
@@ -85,8 +85,8 @@ For each `STORY-<NNN>` in the ordered list, resolve to a file under `docs/storie
85
85
 
86
86
  ## Accepted
87
87
 
88
- | ID | Title | Problems | JTBD | RFCs | Story Maps | Effort |
89
- |----|-------|----------|------|------|------------|--------|
88
+ | ID | Title | Problems | JTBD | RFCs | Story Maps | Effort | Accepted by |
89
+ |----|-------|----------|------|------|------------|--------|-------------|
90
90
  ...
91
91
 
92
92
  ## In Progress
@@ -102,6 +102,8 @@ For each `STORY-<NNN>` in the ordered list, resolve to a file under `docs/storie
102
102
 
103
103
  Omit empty sections rather than rendering empty headers. The Estimated Effort column is omitted from the Draft section because effort is deferred at capture and only required at accepted per I10 INVEST Estimable.
104
104
 
105
+ **The `Accepted by` column (ADR-101, Accepted + In Progress sections only).** `accepted` carries one of two ratification bases and they must not read alike — a report that counts them together overstates how much a human has seen, which is exactly what JTBD-202's auditable-handover outcome depends on. Render `human` when the story's marker is a human ratification event, and `pure-decomposition` when it carries `oversight-basis: pure-decomposition` (machine-accepted by an opted-in project's AFK loop; awaiting post-hoc human ratification). Derive it by reading the story's `oversight-basis:` frontmatter line — its absence means `human`. Never collapse the two into a single "accepted" tick.
106
+
105
107
  **Filtered mode** (`--rfc RFC-<NNN>`) — render a single ordered table:
106
108
 
107
109
  ```markdown
@@ -14,8 +14,8 @@ Per ADR-060 Phase 2 amendment 2026-05-10 lines 200-253:
14
14
 
15
15
  | Status | Filename pattern | Meaning | Entry criteria |
16
16
  |--------|-----------------|---------|----------------|
17
- | **draft** | `docs/stories/draft/STORY-<NNN>-<slug>.md` | Captured (problem + JTBD traces present); skeleton body | I6 + I9 satisfied at capture |
18
- | **accepted** | `docs/stories/accepted/STORY-<NNN>-<slug>.md` | INVEST shape verified; ready for implementation | I7 + I8 + I10 hard-block satisfied |
17
+ | **draft** | `docs/stories/draft/STORY-<NNN>-<slug>.md` | Content-complete (map membership + real user value + ≥1 real acceptance criterion per ADR-095) but not yet accepted — `estimated-effort`/Small unset, I7 RFC trace absent, ratification pending | I6 + I8 + I9 + content floor satisfied at capture |
18
+ | **accepted** | `docs/stories/accepted/STORY-<NNN>-<slug>.md` | INVEST shape verified; ratified; ready for implementation | I7 + I8 + I10 + I12 hard-block satisfied |
19
19
  | **in-progress** | `docs/stories/in-progress/STORY-<NNN>-<slug>.md` | Implementation underway | Auto-transitioned on first non-capture commit carrying `Refs: STORY-<NNN>` |
20
20
  | **done** | `docs/stories/done/STORY-<NNN>-<slug>.md` | All acceptance criteria ticked + linked RFC closed | Auto-transitioned (triggered by RFC close-fire or by manual `manage-story <NNN> done`) |
21
21
  | **archived** | `docs/stories/archived/STORY-<NNN>-<slug>.md` | Closed without completion (scope shifted; superseded) | Manual transition only |
@@ -30,6 +30,7 @@ Per ADR-060 Phase 2 amendment 2026-05-10 lines 200-253:
30
30
  | **I9** trace-to-JTBD | Every story traces to ≥ 1 JTBD | Hard-block at `/wr-itil:capture-story` (also verified at every transition) |
31
31
  | **I10** INVEST shape | At acceptance, INVEST behaviourally: ≥1 acceptance criterion (Testable); user-value statement (Valuable); no Blocked-by-unaccepted refs (Independent); `estimated-effort` field set (Estimable); S/M effort SHOULD; L/XL flagged decomposition-candidate (Small) | Hard-block at `manage-story <NNN> accepted` |
32
32
  | **I11** no-WSJF-leak | Phase 2: stories MUST NOT carry a WSJF field | Behavioural test at this skill (no WSJF field added/read) |
33
+ | **I12** ratification | Every story carries a recorded ratification basis before it is implementable — a human ratification event, OR the ADR-101 pure-decomposition carve-out | Hard-block at `manage-story <NNN> accepted`; re-asserted at the commit locus by `itil-no-implement-draft-gate` |
33
34
 
34
35
  **Bootstrap-exemption marker** (per ADR-060 line 339 + ADR-053 Bootstrapping precedent): the I7/I8/I9/I10 retrofit on bootstrap-migration stories rides a one-time exemption marker `<!-- bootstrap-exempt: STORY-MAP-001 migration per ADR-060 amendment 2026-05-10 -->` inline with the frontmatter. Non-bootstrap captures with the marker fail per the behavioural test.
35
36
 
@@ -53,6 +54,7 @@ Per ADR-060 Phase 2 amendment 2026-05-10 lines 200-253:
53
54
  | Lifecycle transition validation | Mechanical: state machine — draft → accepted → in-progress → done; allow draft → archived; disallow backwards | silent-mechanical |
54
55
  | I7 + I8 hard-block at accepted | Mechanical: frontmatter `rfcs:` and `story-maps:` arrays MUST be non-empty AND each ID must resolve to a file in `docs/rfcs/` and `docs/story-maps/` | silent-mechanical |
55
56
  | I10 INVEST shape check | Mechanical: `## User value` section non-empty; `## Acceptance criteria` has ≥ 1 `- [ ]` line; `estimated-effort` field set to S/M/L/XL; L/XL flagged as decomposition-candidate (advisory, not blocking per ADR-060 line 252 architect-amendment-2026-05-10 nitpick N3) | silent-mechanical |
57
+ | I12 ratification basis at accepted | Mechanical: the story is ratified (`human-oversight: confirmed` + a matching `oversight-hash`), OR `wr-itil-check-afk-accept-eligible` exits 0. Never an `AskUserQuestion` — the eligibility predicate is fully framework-resolved per ADR-101, and a story that fails it is HELD for the ratification drain, not asked about | silent-mechanical |
56
58
  | INVEST shape violation | Halt-with-stderr-directive listing the missing INVEST attributes; user re-invokes after editing the story body | n/a (halt) |
57
59
  | README refresh on every transition | Mechanical: regenerate `docs/stories/README.md` Story Rankings + Done tables from FS truth; stage in same commit | silent-mechanical |
58
60
  | Reverse-trace refresh on driving artefacts | Mechanical: every transition refreshes `## Stories` section on each driving problem + JTBD + RFC + story-map via the Slice 2a/2b helpers | silent-mechanical |
@@ -99,7 +101,14 @@ Use `AskUserQuestion` for direction-setting fields (e.g. `## User value` rewrite
99
101
  For any transition `<from> → <to>`:
100
102
 
101
103
  1. **Verify pre-transition invariants** for `<to>`:
102
- - `accepted`: I7 + I8 + I10 hard-block (see § I-invariant table).
104
+ - `accepted`: I7 + I8 + I10 + **I12** hard-block (see § I-invariant table).
105
+
106
+ **I12 — the ratification gate (ADR-090 / ADR-096, enforced per ADR-101; the hole P465 named).** A story reaches `accepted` only with a recorded ratification basis. Resolve it in this order:
107
+
108
+ 1. Already ratified (`human-oversight: confirmed` with a matching `oversight-hash`)? I12 satisfied; proceed.
109
+ 2. Otherwise run `wr-itil-check-afk-accept-eligible "$story_file"`. **Exit 0** — the story is pure decomposition of confirmed substance in an opted-in project; proceed and mark it with `--pure-decomposition` at step 5 below.
110
+ 3. **Exit 1** — HALT. Do not accept. Interactively, route to the ratify flow (§ 7.5). Under AFK, leave the story in `draft` and queue the ratification to `outstanding_questions`; do NOT ask about it, and never hand-write the marker.
111
+
103
112
  - `in-progress`: linked RFC status is `accepted` or `in-progress` (you can't progress a story under a proposed/closed RFC).
104
113
  - `done`: ALL `- [ ]` checkboxes in `## Acceptance criteria` are ticked (i.e. zero unticked); linked RFC status is `closed` OR the RFC's other stories have closed (transitive closure check deferred to a per-RFC `manage-rfc done-gate` check in a future slice).
105
114
  - `archived`: no invariants (manual close-without-completion).
@@ -113,6 +122,16 @@ For any transition `<from> → <to>`:
113
122
 
114
123
  4. **P057 staging-trap** — after the Edit, re-stage: `git add "docs/stories/${to_state}/STORY-${nnn}-${slug}.md"`.
115
124
 
125
+ 5. **Ratify LAST, then re-stage (ADR-101 — write ordering is load-bearing).** Only when I12 resolved via the eligibility path at step 1. Every step above writes substance — the `estimated-effort` field, the `## User value` and `## Acceptance criteria` fills — and every substance write drifts the content hash. So the ratify write MUST be the LAST write of the transition, and the file MUST be re-staged after it:
126
+
127
+ ```bash
128
+ export CLAUDE_SESSION_ID="${CLAUDE_SESSION_ID:?marker shims silently no-op on an empty SID (P368)}"
129
+ wr-itil-mark-story-oversight-confirmed --pure-decomposition "docs/stories/accepted/STORY-${nnn}-${slug}.md"
130
+ git add "docs/stories/accepted/STORY-${nnn}-${slug}.md"
131
+ ```
132
+
133
+ Get the order wrong and the marker records a hash of a file that no longer exists — the commit-locus gate then denies the very next commit, inside an AFK loop, where the recovery is itself gated. Skip the re-stage and the committed blob carries the story WITHOUT the marker while the worktree has it; the hook reads the worktree, so nothing catches it.
134
+
116
135
  #### Auto-transition triggers (ADR-060 line 292)
117
136
 
118
137
  The auto-transition logic fires in two contexts:
@@ -176,11 +195,13 @@ The helpers are idempotent + lazy-empty per the Slice 2a/2b/Slice 11 contract.
176
195
 
177
196
  ### 7.5. Ratification flow (`ratify`) — ADR-090 / STORY-022
178
197
 
179
- `ratify` confirms human oversight of a single story **orthogonal to the `status:` lifecycle** and drift-invalidated (any later content edit re-opens it via the `oversight-hash` fingerprint). The primary ratification surface is `/wr-itil:manage-story-map <NNN> ratify` (map first, then its stories); this per-story form is for ratifying a story on its own.
198
+ `ratify` confirms human oversight of a single story. Ratification is **not itself a lifecycle state, but it IS a precondition of `accepted`** (I12) and it is drift-invalidated, so any later content edit re-opens it via the `oversight-hash` fingerprint. The primary ratification surface is `/wr-itil:manage-story-map <NNN> ratify` (map first, then its stories); this per-story form is for ratifying a story on its own.
199
+
200
+ > This sentence previously read "orthogonal to the `status:` lifecycle", which was ambiguous between "ratification is not itself a status" (what it meant) and "ratification is not a precondition of any status" (what it read like). An agent acting on the second reading accepted and implemented an unratified story believing it compliant — the P465 failure, 2026-07-26.
180
201
 
181
202
  1. **Born-confirmed discipline (P348):** `export CLAUDE_SESSION_ID` first (the marker shim no-ops on an empty SID). Never write `confirmed` without the same-turn confirm below.
182
203
  2. **Brief + confirm:** present the story's `## User value` + `## Acceptance criteria` — substance BEFORE the ID (P350) — then `AskUserQuestion` with two options: **Ratify** / **(type something)**. On **Ratify**: run `wr-itil-mark-story-oversight-confirmed <story-file>` (writes `confirmed` + fingerprint). On free-text: apply the correction as a story edit and re-present (the edit re-opens ratification).
183
- 3. **AFK (ADR-013 Rule 6):** if `AskUserQuestion` is unavailable, do NOT auto-ratify (hollow marker, P348) — leave it unratified for the `/wr-itil:work-problems` Step 2.4 drain.
204
+ 3. **AFK (ADR-013 Rule 6):** if `AskUserQuestion` is unavailable, do NOT auto-ratify (hollow marker, P348) — leave it unratified for the `/wr-itil:work-problems` Step 2.4 drain. **One carve-out, and only one (ADR-101):** where `wr-itil-check-afk-accept-eligible` exits 0, the loop marks the story with `--pure-decomposition` at the accept transition (§ 7 mechanic 5). That is not a hollow marker — it records a machine-established basis rather than asserting a human confirmed something they never saw, it is opt-in per project and per story, and the story still surfaces for post-hoc human ratification via `wr-itil-detect-unratified-stories-maps --with-afk-accepted`. Everything else stays held.
184
205
  4. **Single commit** per ADR-014.
185
206
 
186
207
  ### 8. List flow (`list`)