@windyroad/itil 0.56.0-preview.879 → 0.56.0-preview.881

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.
@@ -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/mark-story-oversight-confirmed.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/mark-story-oversight-confirmed.sh" "$@"
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # story-oversight.sh — ADR-090 lazy-fingerprint ratification helpers.
3
+ #
4
+ # Shared by detect-unratified-stories-maps.sh, check-rfc-stories-ratified.sh, and
5
+ # mark-story-oversight-confirmed.sh so all three agree on ONE hash definition
6
+ # (if they diverged, a freshly-ratified artefact would read as drifted forever).
7
+ #
8
+ # A story/map is RATIFIED when it carries a `confirmed` human-oversight marker
9
+ # AND a stored oversight-hash that matches a fresh hash of its content-minus-
10
+ # marker. Any content edit changes the hash → the artefact reads as drifted /
11
+ # unratified until re-ratified. This is ADR-090's drift-invalidation (ADR-009
12
+ # drift lineage, NOT ADR-066 write-once) — the same hash-the-artefact pattern
13
+ # the external-comms gate uses.
14
+ #
15
+ # Sourced, not executed. Requires `shasum` (BSD + coreutils both ship it).
16
+
17
+ # Stable hash of the artefact's content EXCLUDING the two marker lines, so
18
+ # writing/updating the marker is idempotent w.r.t. the hash. Covers both
19
+ # encodings in one filter:
20
+ # - markdown: `human-oversight:` / `oversight-hash:` frontmatter lines
21
+ # - HTML: <meta name="human-oversight" ...> / <meta name="oversight-hash" ...>
22
+ oversight_content_hash() {
23
+ # Exclude the marker + lifecycle-`status` lines, and normalize lifecycle-PROGRESS
24
+ # state — acceptance-criterion checkbox ticks and slice `data-status` — so that
25
+ # ONLY a SUBSTANCE change re-opens ratification. Ticking a criterion or advancing
26
+ # status/slice-progress is progress, not a change to what the user ratified; the
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" \
29
+ | sed -E 's/- \[[ xX]\]/- [ ]/g; s/data-status="[^"]*"/data-status=""/g' \
30
+ | shasum -a 256 | awk '{print $1}'
31
+ }
32
+
33
+ # Echo the stored oversight-hash (md frontmatter OR HTML meta), empty if none.
34
+ oversight_stored_hash() {
35
+ local h
36
+ h="$(grep -oE '^oversight-hash:[[:space:]]*[a-f0-9]{64}' "$1" 2>/dev/null | grep -oE '[a-f0-9]{64}' | head -1)"
37
+ [ -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)"
38
+ printf '%s' "$h"
39
+ }
40
+
41
+ # True (0) if the file carries a `confirmed` human-oversight marker (md or HTML).
42
+ oversight_is_confirmed() {
43
+ grep -qiE '^human-oversight:[[:space:]]*confirmed([[:space:]]|$)' "$1" && return 0
44
+ grep -qiE '<meta[^>]*name="human-oversight"[^>]*content="confirmed"' "$1" && return 0
45
+ return 1
46
+ }
47
+
48
+ # True (0) if RATIFIED: confirmed AND a stored hash that matches current content.
49
+ # A confirmed marker with NO stored hash (legacy hand-ratified) is NOT ratified —
50
+ # it must be re-ratified once to gain its fingerprint.
51
+ is_story_map_ratified() {
52
+ local f="$1" stored
53
+ oversight_is_confirmed "$f" || return 1
54
+ stored="$(oversight_stored_hash "$f")"
55
+ [ -z "$stored" ] && return 1
56
+ [ "$stored" = "$(oversight_content_hash "$f")" ]
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.56.0-preview.879",
3
+ "version": "0.56.0-preview.881",
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"
@@ -17,6 +17,12 @@
17
17
  # Authority: ADR-090. Driver: P404 Phase 2. Test: check-rfc-stories-ratified.bats.
18
18
  set -euo pipefail
19
19
 
20
+ # Adopter-safe: source the shared lazy-fingerprint lib RELATIVE TO THIS SCRIPT (P317).
21
+ LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)" || {
22
+ echo "check-rfc-stories-ratified: cannot locate lib dir" >&2; exit 2; }
23
+ # shellcheck source=/dev/null
24
+ source "$LIB/story-oversight.sh"
25
+
20
26
  rfc="${1:-}"
21
27
  stories_root="${2:-docs/stories}"
22
28
  if [ -z "$rfc" ]; then
@@ -45,7 +51,7 @@ for id in $ids; do
45
51
  unratified="$unratified $id(missing)"
46
52
  continue
47
53
  fi
48
- if ! grep -qE '^human-oversight:[[:space:]]*confirmed([[:space:]]|$)' "$f"; then
54
+ if ! is_story_map_ratified "$f"; then
49
55
  unratified="$unratified $id(unratified)"
50
56
  fi
51
57
  done
@@ -1,42 +1,43 @@
1
1
  #!/usr/bin/env bash
2
2
  # detect-unratified-stories-maps.sh — ADR-090 detector.
3
3
  #
4
- # Token-cheap detection of story maps + stories lacking the ratified
5
- # `human-oversight: confirmed` marker. Mirrors wr-architect detect-unoversighted.sh
6
- # but spans two artefact types with different marker encodings:
7
- # - stories: markdown YAML frontmatter `human-oversight: confirmed`
8
- # - story-maps: HTML meta tag `<meta name="human-oversight" content="confirmed">`
9
- #
10
- # ADR-090's marker is DRIFT-INVALIDATED (unlike ADR-066 write-once): an edit
11
- # re-opens it to `unconfirmed`, so this detector surfaces both the never-ratified
12
- # (no marker) and the drift-reopened (`unconfirmed`) cases as the drain queue.
4
+ # Token-cheap detection of story maps + stories that are NOT ratified. Mirrors
5
+ # wr-architect detect-unoversighted.sh but spans two artefact types and is
6
+ # DRIFT-AWARE via the shared lazy-fingerprint helper: an artefact is ratified
7
+ # only when it carries a `confirmed` human-oversight marker AND a stored
8
+ # oversight-hash matching its current content. This surfaces three cases as the
9
+ # drain queue:
10
+ # - never ratified (no marker)
11
+ # - drift-reopened (marker says confirmed, but content changed hash mismatch)
12
+ # - legacy confirmed (confirmed but no fingerprint yet needs one re-ratify)
13
13
  #
14
14
  # Usage: detect-unratified-stories-maps.sh [STORIES_DIR=docs/stories] [MAPS_DIR=docs/story-maps]
15
- # Output: one unratified artefact path per line, sorted. Empty = all confirmed.
15
+ # Output: one unratified artefact path per line, sorted. Empty = all ratified.
16
16
  # Always exits 0 (detector, not a gate). Consumed by the work-problems Step 2.4
17
- # oversight-unconfirmed drain (mirror of the architect/jtbd detectors).
17
+ # oversight-unconfirmed drain.
18
18
  set -euo pipefail
19
19
 
20
+ # Adopter-safe: source the shared hash lib RELATIVE TO THIS SCRIPT (P317).
21
+ LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)" || {
22
+ echo "detect-unratified-stories-maps: cannot locate lib dir" >&2; exit 0; }
23
+ # shellcheck source=/dev/null
24
+ source "$LIB/story-oversight.sh"
25
+
20
26
  STORIES_DIR="${1:-docs/stories}"
21
27
  MAPS_DIR="${2:-docs/story-maps}"
22
28
 
23
29
  shopt -s nullglob
24
30
 
25
31
  {
26
- # Stories — markdown YAML frontmatter.
27
32
  if [ -d "$STORIES_DIR" ]; then
28
33
  for f in "$STORIES_DIR"/*.md "$STORIES_DIR"/*/*.md; do
29
34
  [ "$(basename "$f")" = "README.md" ] && continue
30
- fm="$(awk 'NR==1 && $0 != "---" { exit } NR==1 { next } /^---[[:space:]]*$/ { exit } { print }' "$f")"
31
- printf '%s\n' "$fm" | grep -qiE '^human-oversight:[[:space:]]*confirmed[[:space:]]*$' && continue
32
- echo "$f"
35
+ is_story_map_ratified "$f" || echo "$f"
33
36
  done
34
37
  fi
35
- # Story maps — HTML meta tag.
36
38
  if [ -d "$MAPS_DIR" ]; then
37
39
  for f in "$MAPS_DIR"/*.html "$MAPS_DIR"/*/*.html; do
38
- grep -qiE '<meta[^>]*name="human-oversight"[^>]*content="confirmed"' "$f" && continue
39
- echo "$f"
40
+ is_story_map_ratified "$f" || echo "$f"
40
41
  done
41
42
  fi
42
43
  } | sort
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # mark-story-oversight-confirmed.sh — ADR-090 ratify write-path.
3
+ #
4
+ # Writes `human-oversight: confirmed` + an `oversight-hash` fingerprint of the
5
+ # current content into a story (markdown frontmatter) or story-map (HTML meta).
6
+ # Idempotent: re-running recomputes the hash and replaces any existing marker
7
+ # (never duplicates). A later content edit changes the content hash, so the
8
+ # stored fingerprint no longer matches → the artefact reads as drifted /
9
+ # unratified until re-ratified (ADR-090 drift-invalidation).
10
+ #
11
+ # Usage: mark-story-oversight-confirmed.sh <story-or-map-file>
12
+ # Exit: 0 = ratified; 2 = usage / file error.
13
+ #
14
+ # Authority: ADR-090. Driver: P404 Phase 2. Test: mark-story-oversight-confirmed.bats.
15
+ set -euo pipefail
16
+
17
+ # Adopter-safe: source the shared hash lib RELATIVE TO THIS SCRIPT (P317), never
18
+ # repo-relative.
19
+ LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)" || {
20
+ echo "mark-story-oversight-confirmed: cannot locate lib dir" >&2; exit 2; }
21
+ # shellcheck source=/dev/null
22
+ source "$LIB/story-oversight.sh"
23
+
24
+ f="${1:-}"
25
+ if [ -z "$f" ]; then
26
+ echo "mark-story-oversight-confirmed: usage: mark-story-oversight-confirmed.sh <file>" >&2
27
+ exit 2
28
+ fi
29
+ [ -f "$f" ] || { echo "mark-story-oversight-confirmed: file not found: $f" >&2; exit 2; }
30
+
31
+ h="$(oversight_content_hash "$f")"
32
+ tmp="$(mktemp)"
33
+
34
+ case "$f" in
35
+ *.html)
36
+ if grep -qi '<head' "$f"; then
37
+ # Insert the two metas after the <head> tag; drop any existing marker metas.
38
+ awk -v H="$h" '
39
+ /<meta[^>]*name="(human-oversight|oversight-hash)"/ { next }
40
+ { print }
41
+ !done && tolower($0) ~ /<head[ >]/ {
42
+ print " <meta name=\"human-oversight\" content=\"confirmed\">"
43
+ print " <meta name=\"oversight-hash\" content=\"" H "\">"
44
+ done=1
45
+ }
46
+ ' "$f" > "$tmp"
47
+ else
48
+ # No <head> — prepend the metas (position is irrelevant to the hash).
49
+ { printf '<meta name="human-oversight" content="confirmed">\n'
50
+ printf '<meta name="oversight-hash" content="%s">\n' "$h"
51
+ grep -vE '<meta[^>]*name="(human-oversight|oversight-hash)"' "$f"; } > "$tmp"
52
+ fi
53
+ ;;
54
+ *)
55
+ # Markdown: rewrite frontmatter — drop existing markers, insert both before
56
+ # the closing `---`.
57
+ awk -v H="$h" '
58
+ NR==1 && $0=="---" { infm=1; print; next }
59
+ infm && /^(human-oversight|oversight-hash):/ { next }
60
+ infm && /^---[[:space:]]*$/ && !done {
61
+ print "human-oversight: confirmed"
62
+ print "oversight-hash: " H
63
+ print; done=1; infm=0; next
64
+ }
65
+ { print }
66
+ ' "$f" > "$tmp"
67
+ ;;
68
+ esac
69
+
70
+ mv "$tmp" "$f"
71
+ echo "mark-story-oversight-confirmed: ratified $f (oversight-hash ${h:0:12}…)" >&2
72
+ exit 0
@@ -1,26 +1,29 @@
1
1
  #!/usr/bin/env bats
2
2
  # Behavioural test for check-rfc-stories-ratified.sh (ADR-090 — an RFC may
3
- # reference only RATIFIED stories). For each STORY-NNN in the RFC's stories:
4
- # frontmatter, the predicate resolves the story file and verifies
5
- # human-oversight: confirmed. Composes with check-rfc-has-stories (ADR-089):
6
- # has-stories checks >=1 exists; this checks each listed one is ratified.
3
+ # reference only RATIFIED stories). DRIFT-AWARE via the shared lazy-fingerprint
4
+ # lib: a listed story counts only when it is `confirmed` AND its stored
5
+ # oversight-hash matches current content. Composes with check-rfc-has-stories
6
+ # (ADR-089): has-stories checks >=1 exists; this checks each listed one is ratified.
7
7
  #
8
- # @adr ADR-090 (story maps and stories carry a drift-invalidated human-oversight marker)
8
+ # @adr ADR-090
9
9
  # @problem P404 (implement ADR-089 + ADR-090) — Phase 2
10
10
 
11
11
  setup() {
12
12
  REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
13
13
  SCRIPT="${REPO_ROOT}/packages/itil/scripts/check-rfc-stories-ratified.sh"
14
+ MARK="${REPO_ROOT}/packages/itil/scripts/mark-story-oversight-confirmed.sh"
14
15
  TMPD="$(mktemp -d)"
15
16
  mkdir -p "$TMPD/stories/accepted"
16
17
  }
17
18
  teardown() { rm -rf "$TMPD"; }
18
19
 
19
- mkstory() { # $1=id $2=oversight-value(or "none")
20
+ mkstory() { # $1=id $2=confirmed|unconfirmed|none
20
21
  local f="$TMPD/stories/accepted/$1-x.md"
21
- { echo "---"; echo "status: accepted"; echo "story-id: x"
22
- [ "$2" != "none" ] && echo "human-oversight: $2"
23
- echo "---"; echo "# $1"; } > "$f"
22
+ case "$2" in
23
+ confirmed) printf -- '---\nstatus: accepted\n---\n# %s\n' "$1" > "$f"; bash "$MARK" "$f" ;;
24
+ unconfirmed) printf -- '---\nstatus: accepted\nhuman-oversight: unconfirmed\n---\n# %s\n' "$1" > "$f" ;;
25
+ none) printf -- '---\nstatus: accepted\n---\n# %s\n' "$1" > "$f" ;;
26
+ esac
24
27
  }
25
28
  mkrfc() { printf -- '---\nstatus: proposed\nstories: [%s]\n---\n# rfc\n' "$1" > "$TMPD/RFC.proposed.md"; }
26
29
 
@@ -30,14 +33,14 @@ mkrfc() { printf -- '---\nstatus: proposed\nstories: [%s]\n---\n# rfc\n' "$1" >
30
33
  [ "$status" -eq 0 ]
31
34
  }
32
35
 
33
- @test "check-rfc-stories-ratified: a confirmed story is ACCEPTED (exit 0)" {
36
+ @test "check-rfc-stories-ratified: a ratified (confirmed+hash) story is ACCEPTED (exit 0)" {
34
37
  mkstory STORY-020 confirmed
35
38
  mkrfc STORY-020
36
39
  run bash "$SCRIPT" "$TMPD/RFC.proposed.md" "$TMPD/stories"
37
40
  [ "$status" -eq 0 ]
38
41
  }
39
42
 
40
- @test "check-rfc-stories-ratified: an unconfirmed story is REJECTED (exit non-zero)" {
43
+ @test "check-rfc-stories-ratified: an unconfirmed story is REJECTED" {
41
44
  mkstory STORY-020 unconfirmed
42
45
  mkrfc STORY-020
43
46
  run bash "$SCRIPT" "$TMPD/RFC.proposed.md" "$TMPD/stories"
@@ -53,6 +56,15 @@ mkrfc() { printf -- '---\nstatus: proposed\nstories: [%s]\n---\n# rfc\n' "$1" >
53
56
  [[ "$output" == *"STORY-020"* ]]
54
57
  }
55
58
 
59
+ @test "check-rfc-stories-ratified: a confirmed-but-DRIFTED story is REJECTED" {
60
+ mkstory STORY-020 confirmed
61
+ printf '\nedit after ratify\n' >> "$TMPD/stories/accepted/STORY-020-x.md"
62
+ mkrfc STORY-020
63
+ run bash "$SCRIPT" "$TMPD/RFC.proposed.md" "$TMPD/stories"
64
+ [ "$status" -ne 0 ]
65
+ [[ "$output" == *"STORY-020"* ]]
66
+ }
67
+
56
68
  @test "check-rfc-stories-ratified: a missing story file is REJECTED" {
57
69
  mkrfc STORY-099
58
70
  run bash "$SCRIPT" "$TMPD/RFC.proposed.md" "$TMPD/stories"
@@ -60,7 +72,7 @@ mkrfc() { printf -- '---\nstatus: proposed\nstories: [%s]\n---\n# rfc\n' "$1" >
60
72
  [[ "$output" == *"STORY-099"* ]]
61
73
  }
62
74
 
63
- @test "check-rfc-stories-ratified: mixed — one confirmed one unconfirmed is REJECTED" {
75
+ @test "check-rfc-stories-ratified: mixed — one ratified one unconfirmed is REJECTED" {
64
76
  mkstory STORY-020 confirmed
65
77
  mkstory STORY-021 unconfirmed
66
78
  mkrfc "STORY-020, STORY-021"
@@ -1,44 +1,54 @@
1
1
  #!/usr/bin/env bats
2
2
  # Behavioural test for detect-unratified-stories-maps.sh (ADR-090 detector).
3
- # Mirrors wr-architect detect-unoversighted.sh, but spans TWO artefact types
4
- # with DIFFERENT marker encodings:
5
- # - stories: markdown YAML frontmatter `human-oversight: confirmed`
6
- # - story-maps: HTML `<meta name="human-oversight" content="confirmed">`
7
- # Lists every artefact whose marker is missing or not `confirmed`. Always exit 0.
3
+ # DRIFT-AWARE via the shared lazy-fingerprint lib: an artefact counts as ratified
4
+ # only when it is `confirmed` AND its stored oversight-hash matches current
5
+ # content. Surfaces never-ratified, unconfirmed, and drift-reopened (confirmed
6
+ # then edited) cases; omits genuinely-ratified ones. Always exit 0.
8
7
  #
9
- # @adr ADR-090 (drift-invalidated story-map/story human-oversight marker)
10
- # @problem P404 (implement ADR-089 + ADR-090) — Phase 2
8
+ # @adr ADR-090
9
+ # @problem P404 (Phase 2)
11
10
 
12
11
  setup() {
13
12
  REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
14
13
  SCRIPT="${REPO_ROOT}/packages/itil/scripts/detect-unratified-stories-maps.sh"
14
+ MARK="${REPO_ROOT}/packages/itil/scripts/mark-story-oversight-confirmed.sh"
15
15
  TMPD="$(mktemp -d)"
16
16
  mkdir -p "$TMPD/stories/accepted" "$TMPD/story-maps/draft"
17
- # stories (markdown frontmatter)
18
- printf -- '---\nstatus: accepted\nhuman-oversight: confirmed\n---\n# ok\n' > "$TMPD/stories/accepted/STORY-1-ok.md"
17
+
18
+ # Genuinely ratified (via the mark write-path) should NOT be listed.
19
+ printf -- '---\nstatus: accepted\n---\n# ok\n' > "$TMPD/stories/accepted/STORY-1-ok.md"
20
+ bash "$MARK" "$TMPD/stories/accepted/STORY-1-ok.md"
21
+ printf -- '<head></head>\n<h1>ok map</h1>\n' > "$TMPD/story-maps/draft/STORY-MAP-1-ok.html"
22
+ bash "$MARK" "$TMPD/story-maps/draft/STORY-MAP-1-ok.html"
23
+
24
+ # Unconfirmed + unmarked — listed.
19
25
  printf -- '---\nstatus: accepted\nhuman-oversight: unconfirmed\n---\n# no\n' > "$TMPD/stories/accepted/STORY-2-unconf.md"
20
26
  printf -- '---\nstatus: accepted\n---\n# nomarker\n' > "$TMPD/stories/accepted/STORY-3-none.md"
21
- # story-maps (HTML meta)
22
- printf -- '<meta name="human-oversight" content="confirmed">\n' > "$TMPD/story-maps/draft/STORY-MAP-1-ok.html"
23
- printf -- '<meta name="human-oversight" content="unconfirmed">\n' > "$TMPD/story-maps/draft/STORY-MAP-2-unconf.html"
24
- printf -- '<h1>no meta</h1>\n' > "$TMPD/story-maps/draft/STORY-MAP-3-none.html"
27
+
28
+ # Drifted: ratified, then content edited afterward — listed.
29
+ printf -- '---\nstatus: accepted\n---\n# body\n' > "$TMPD/stories/accepted/STORY-4-drift.md"
30
+ bash "$MARK" "$TMPD/stories/accepted/STORY-4-drift.md"
31
+ printf '\nAn edit made after ratification.\n' >> "$TMPD/stories/accepted/STORY-4-drift.md"
32
+
33
+ # HTML map with no marker — listed.
34
+ printf -- '<h1>no meta</h1>\n' > "$TMPD/story-maps/draft/STORY-MAP-3-none.html"
25
35
  }
26
36
  teardown() { rm -rf "$TMPD"; }
27
37
 
28
- @test "detect: lists unconfirmed + unmarked, omits confirmed; exits 0" {
38
+ @test "detect: lists unconfirmed + unmarked + DRIFTED, omits genuinely-ratified; exit 0" {
29
39
  run bash "$SCRIPT" "$TMPD/stories" "$TMPD/story-maps"
30
40
  [ "$status" -eq 0 ]
31
41
  [[ "$output" == *"STORY-2-unconf.md"* ]]
32
42
  [[ "$output" == *"STORY-3-none.md"* ]]
33
- [[ "$output" == *"STORY-MAP-2-unconf.html"* ]]
43
+ [[ "$output" == *"STORY-4-drift.md"* ]]
34
44
  [[ "$output" == *"STORY-MAP-3-none.html"* ]]
35
45
  [[ "$output" != *"STORY-1-ok.md"* ]]
36
46
  [[ "$output" != *"STORY-MAP-1-ok.html"* ]]
37
47
  }
38
48
 
39
- @test "detect: all-confirmed set yields empty output, exit 0" {
49
+ @test "detect: all-ratified set yields empty output, exit 0" {
40
50
  rm "$TMPD/stories/accepted/STORY-2-unconf.md" "$TMPD/stories/accepted/STORY-3-none.md" \
41
- "$TMPD/story-maps/draft/STORY-MAP-2-unconf.html" "$TMPD/story-maps/draft/STORY-MAP-3-none.html"
51
+ "$TMPD/stories/accepted/STORY-4-drift.md" "$TMPD/story-maps/draft/STORY-MAP-3-none.html"
42
52
  run bash "$SCRIPT" "$TMPD/stories" "$TMPD/story-maps"
43
53
  [ "$status" -eq 0 ]
44
54
  [ -z "$output" ]
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bats
2
+ # Behavioural test for mark-story-oversight-confirmed.sh (ADR-090 ratify write-path).
3
+ # Writes `confirmed` + the content fingerprint into a story (md frontmatter) or a
4
+ # story-map (HTML meta). After marking, is_story_map_ratified is true; a later
5
+ # content edit drifts it back to not-ratified.
6
+ #
7
+ # @adr ADR-090
8
+ # @problem P404 (Phase 2)
9
+
10
+ setup() {
11
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
12
+ SCRIPT="${REPO_ROOT}/packages/itil/scripts/mark-story-oversight-confirmed.sh"
13
+ # shellcheck source=/dev/null
14
+ source "${REPO_ROOT}/packages/itil/lib/story-oversight.sh"
15
+ TMPD="$(mktemp -d)"
16
+ }
17
+ teardown() { rm -rf "$TMPD"; }
18
+
19
+ @test "mark: md story with no marker → ratified after mark" {
20
+ printf -- '---\nstatus: accepted\n---\n# body\n' > "$TMPD/s.md"
21
+ run bash "$SCRIPT" "$TMPD/s.md"; [ "$status" -eq 0 ]
22
+ run is_story_map_ratified "$TMPD/s.md"; [ "$status" -eq 0 ]
23
+ }
24
+
25
+ @test "mark: md story with existing unconfirmed marker → confirmed + ratified" {
26
+ printf -- '---\nstatus: accepted\nhuman-oversight: unconfirmed\n---\n# body\n' > "$TMPD/s.md"
27
+ bash "$SCRIPT" "$TMPD/s.md"
28
+ grep -qE '^human-oversight:[[:space:]]*confirmed' "$TMPD/s.md"
29
+ run is_story_map_ratified "$TMPD/s.md"; [ "$status" -eq 0 ]
30
+ # no duplicate marker line left behind
31
+ [ "$(grep -cE '^human-oversight:' "$TMPD/s.md")" -eq 1 ]
32
+ }
33
+
34
+ @test "mark: idempotent — marking twice stays ratified, single marker" {
35
+ printf -- '---\nstatus: accepted\n---\n# body\n' > "$TMPD/s.md"
36
+ bash "$SCRIPT" "$TMPD/s.md"; bash "$SCRIPT" "$TMPD/s.md"
37
+ run is_story_map_ratified "$TMPD/s.md"; [ "$status" -eq 0 ]
38
+ [ "$(grep -cE '^oversight-hash:' "$TMPD/s.md")" -eq 1 ]
39
+ }
40
+
41
+ @test "mark: drift — mark then edit body → not ratified" {
42
+ printf -- '---\nstatus: accepted\n---\n# body\n' > "$TMPD/s.md"
43
+ bash "$SCRIPT" "$TMPD/s.md"
44
+ printf -- '---\nstatus: accepted\nhuman-oversight: confirmed\n---\n# body EDITED\n' > "$TMPD/s.md"
45
+ run is_story_map_ratified "$TMPD/s.md"; [ "$status" -ne 0 ]
46
+ }
47
+
48
+ @test "mark: HTML map → ratified after mark" {
49
+ printf -- '<!doctype html>\n<head><title>map</title></head>\n<body><h1>map</h1></body>\n' > "$TMPD/m.html"
50
+ run bash "$SCRIPT" "$TMPD/m.html"; [ "$status" -eq 0 ]
51
+ grep -qE '<meta[^>]*name="human-oversight"[^>]*content="confirmed"' "$TMPD/m.html"
52
+ run is_story_map_ratified "$TMPD/m.html"; [ "$status" -eq 0 ]
53
+ }
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env bats
2
+ # Behavioural test for lib/story-oversight.sh — ADR-090 lazy-fingerprint helpers.
3
+ # The hash EXCLUDES the marker lines (so writing the marker is idempotent), and
4
+ # "ratified" = confirmed marker AND stored hash matches current content. Any
5
+ # content edit drifts the hash → not ratified (ADR-090 drift-invalidation).
6
+ #
7
+ # @adr ADR-090
8
+ # @problem P404 (Phase 2)
9
+
10
+ setup() {
11
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
12
+ LIB="${REPO_ROOT}/packages/itil/lib/story-oversight.sh"
13
+ TMPD="$(mktemp -d)"
14
+ # shellcheck source=/dev/null
15
+ source "$LIB"
16
+ }
17
+ teardown() { rm -rf "$TMPD"; }
18
+
19
+ @test "oversight_content_hash: identical content → identical hash" {
20
+ printf '# body\nline two\n' > "$TMPD/a"; printf '# body\nline two\n' > "$TMPD/b"
21
+ [ "$(oversight_content_hash "$TMPD/a")" = "$(oversight_content_hash "$TMPD/b")" ]
22
+ }
23
+
24
+ @test "oversight_content_hash: adding/changing the marker lines does NOT change the hash" {
25
+ printf -- '---\nstatus: accepted\n---\n# body\n' > "$TMPD/f"
26
+ before="$(oversight_content_hash "$TMPD/f")"
27
+ printf -- '---\nstatus: accepted\nhuman-oversight: confirmed\noversight-hash: %064d\n---\n# body\n' 0 > "$TMPD/f"
28
+ [ "$before" = "$(oversight_content_hash "$TMPD/f")" ]
29
+ }
30
+
31
+ @test "oversight_content_hash: a real content edit DOES change the hash" {
32
+ printf '# body\n' > "$TMPD/f"; before="$(oversight_content_hash "$TMPD/f")"
33
+ printf '# body EDITED\n' > "$TMPD/f"
34
+ [ "$before" != "$(oversight_content_hash "$TMPD/f")" ]
35
+ }
36
+
37
+ @test "oversight_content_hash: ticking an acceptance-criterion checkbox does NOT drift (lifecycle progress)" {
38
+ printf -- '---\nstatus: accepted\n---\n- [ ] a criterion\n' > "$TMPD/f"; before="$(oversight_content_hash "$TMPD/f")"
39
+ printf -- '---\nstatus: accepted\n---\n- [x] a criterion\n' > "$TMPD/f"
40
+ [ "$before" = "$(oversight_content_hash "$TMPD/f")" ]
41
+ }
42
+
43
+ @test "oversight_content_hash: advancing frontmatter status: does NOT drift (lifecycle)" {
44
+ printf -- '---\nstatus: accepted\n---\n# body\n' > "$TMPD/f"; before="$(oversight_content_hash "$TMPD/f")"
45
+ printf -- '---\nstatus: done\n---\n# body\n' > "$TMPD/f"
46
+ [ "$before" = "$(oversight_content_hash "$TMPD/f")" ]
47
+ }
48
+
49
+ @test "oversight_content_hash: changing criterion TEXT DOES drift (substance)" {
50
+ printf -- '---\nstatus: accepted\n---\n- [ ] original\n' > "$TMPD/f"; before="$(oversight_content_hash "$TMPD/f")"
51
+ printf -- '---\nstatus: accepted\n---\n- [ ] a DIFFERENT criterion\n' > "$TMPD/f"
52
+ [ "$before" != "$(oversight_content_hash "$TMPD/f")" ]
53
+ }
54
+
55
+ @test "oversight_content_hash: advancing slice data-status does NOT drift (HTML map lifecycle)" {
56
+ printf '<a class="slice" data-story-id="STORY-1" data-status="draft">x</a>\n' > "$TMPD/m.html"; before="$(oversight_content_hash "$TMPD/m.html")"
57
+ printf '<a class="slice" data-story-id="STORY-1" data-status="done">x</a>\n' > "$TMPD/m.html"
58
+ [ "$before" = "$(oversight_content_hash "$TMPD/m.html")" ]
59
+ }
60
+
61
+ @test "is_story_map_ratified: confirmed + matching hash → ratified (md)" {
62
+ printf -- '---\nstatus: accepted\n---\n# body\n' > "$TMPD/f"
63
+ h="$(oversight_content_hash "$TMPD/f")"
64
+ printf -- '---\nstatus: accepted\nhuman-oversight: confirmed\noversight-hash: %s\n---\n# body\n' "$h" > "$TMPD/f"
65
+ run is_story_map_ratified "$TMPD/f"; [ "$status" -eq 0 ]
66
+ }
67
+
68
+ @test "is_story_map_ratified: confirmed but STALE hash (drifted) → not ratified" {
69
+ printf -- '---\nhuman-oversight: confirmed\noversight-hash: %064d\n---\n# body EDITED AFTER RATIFY\n' 1 > "$TMPD/f"
70
+ run is_story_map_ratified "$TMPD/f"; [ "$status" -ne 0 ]
71
+ }
72
+
73
+ @test "is_story_map_ratified: confirmed with NO hash (legacy hand-ratified) → not ratified" {
74
+ printf -- '---\nhuman-oversight: confirmed\n---\n# body\n' > "$TMPD/f"
75
+ run is_story_map_ratified "$TMPD/f"; [ "$status" -ne 0 ]
76
+ }
77
+
78
+ @test "is_story_map_ratified: unconfirmed → not ratified" {
79
+ printf -- '---\nhuman-oversight: unconfirmed\n---\n# body\n' > "$TMPD/f"
80
+ run is_story_map_ratified "$TMPD/f"; [ "$status" -ne 0 ]
81
+ }
82
+
83
+ @test "is_story_map_ratified: HTML map confirmed + matching hash → ratified" {
84
+ printf '<h1>map</h1>\n' > "$TMPD/m.html"
85
+ h="$(oversight_content_hash "$TMPD/m.html")"
86
+ printf '<meta name="human-oversight" content="confirmed">\n<meta name="oversight-hash" content="%s">\n<h1>map</h1>\n' "$h" > "$TMPD/m.html"
87
+ run is_story_map_ratified "$TMPD/m.html"; [ "$status" -eq 0 ]
88
+ }
@@ -202,6 +202,7 @@ jtbd: [JTBD-<NNN>, JTBD-<NNN>, ...]
202
202
  rfcs: [<RFC-<NNN>, ...> or empty]
203
203
  story-maps: [<STORY-MAP-<NNN>, ...> or empty]
204
204
  estimated-effort: <S|M|L|XL — derived at capture per ADR-067 (real best-effort value, no deferral marker)>
205
+ human-oversight: unconfirmed
205
206
  ---
206
207
 
207
208
  # STORY-<NNN>: <Title>
@@ -244,6 +245,8 @@ estimated-effort: <S|M|L|XL — derived at capture per ADR-067 (real best-effort
244
245
  (captured via /wr-itil:capture-story; expand at next /wr-itil:manage-story invocation)
245
246
  ```
246
247
 
248
+ **Born `human-oversight: unconfirmed` (ADR-090).** A new story is created with `human-oversight: unconfirmed` in its frontmatter — orthogonal to the `status:` lifecycle. It is NOT ratified until a human confirms it via `/wr-itil:manage-story <NNN> ratify` (which writes `confirmed` + an `oversight-hash` fingerprint via `wr-itil-mark-story-oversight-confirmed`). Until then `wr-itil-detect-unratified-stories-maps` surfaces it and an RFC may not reference it (`wr-itil-check-rfc-stories-ratified`). Adding a story also drifts its parent map's fingerprint, re-opening the map's ratification (lazy-fingerprint, ADR-009 lineage). Do NOT hand-write `confirmed` — born-unconfirmed is the load-bearing default.
249
+
247
250
  The deferred-section pattern matches `capture-rfc`'s placeholder approach — the captured story is intentionally minimal; full INVEST shape lands at the manage-story accepted-transition step.
248
251
 
249
252
  ### 6. Single commit — `## Stories` reverse-trace refresh; no stories README refresh
@@ -127,6 +127,7 @@ Same shape as capture-story Step 4 — silent-default when unavailable.
127
127
  <meta name="adrs" content="">
128
128
  <meta name="reported" content="<YYYY-MM-DD>">
129
129
  <meta name="decision-makers" content="<git config user.name>">
130
+ <meta name="human-oversight" content="unconfirmed">
130
131
  <style>
131
132
  body { font-family: system-ui, sans-serif; max-width: 1200px; margin: 1rem auto; padding: 0 1rem; }
132
133
  h1 { font-size: 1.5rem; }
@@ -161,6 +162,8 @@ Same shape as capture-story Step 4 — silent-default when unavailable.
161
162
 
162
163
  Per `docs/STYLE-GUIDE.md`: NO inline `style=""` on `<a class="slice">` or `<h2 data-rib>` data-bearing elements; embedded `<style>` block in `<head>` is the only permitted styling source; `--cols` custom-property on `.backbone` is the layout-container exception.
163
164
 
165
+ **Born `human-oversight: unconfirmed` (ADR-090).** A new map is created with `<meta name="human-oversight" content="unconfirmed">` — orthogonal to the `status:` lifecycle. It is NOT ratified until a human confirms it via `/wr-itil:manage-story-map <NNN> ratify` (which writes `confirmed` + an `oversight-hash` fingerprint via `wr-itil-mark-story-oversight-confirmed`). Until then `wr-itil-detect-unratified-stories-maps` surfaces it and an RFC may not reference its stories (`wr-itil-check-rfc-stories-ratified`). Any later content edit drifts the fingerprint and silently re-opens ratification (lazy-fingerprint, ADR-009 lineage). Do NOT hand-write `confirmed` here — born-unconfirmed is the load-bearing default.
166
+
164
167
  ### 6. Single commit — `## Story Maps` reverse-trace refresh
165
168
 
166
169
  **Stage list**: new HTML file PLUS driving problem files (refresh `## Story Maps` section via `update-problem-references-section.sh <file> "Story Maps"`) PLUS driving JTBD files (refresh `## Story Maps` section via `update-jtbd-references-section.sh <file> "Story Maps"`). Do NOT stage `docs/story-maps/README.md` (deferred).
@@ -41,6 +41,7 @@ Per ADR-060 Phase 2 amendment 2026-05-10 lines 200-253:
41
41
  /wr-itil:manage-story <STORY-NNN> in-progress # Manual transition (auto-fires on first non-capture commit)
42
42
  /wr-itil:manage-story <STORY-NNN> done # Transition in-progress → done (gates all-criteria-ticked + RFC closed)
43
43
  /wr-itil:manage-story <STORY-NNN> archived # Close without completion
44
+ /wr-itil:manage-story <STORY-NNN> ratify # ADR-090: confirm this story (usually via manage-story-map ratify)
44
45
  /wr-itil:manage-story review # Re-rank all stories + refresh README
45
46
  ```
46
47
 
@@ -173,6 +174,15 @@ done
173
174
 
174
175
  The helpers are idempotent + lazy-empty per the Slice 2a/2b/Slice 11 contract.
175
176
 
177
+ ### 7.5. Ratification flow (`ratify`) — ADR-090 / STORY-022
178
+
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.
180
+
181
+ 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
+ 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.
184
+ 4. **Single commit** per ADR-014.
185
+
176
186
  ### 8. List flow (`list`)
177
187
 
178
188
  Read all `docs/stories/*/STORY-*.md` files. Extract ID, title, status, traced problems / RFCs / story-maps / JTBDs. Sort by lifecycle state (accepted > in-progress > draft > done > archived) then by `Reported` ASC. Display as markdown tables grouped by state.
@@ -38,6 +38,7 @@ Per ADR-060 amendment 2026-05-10 lines 145-189 + encoding amendment 2026-05-12:
38
38
  /wr-itil:manage-story-map <STORY-MAP-NNN> in-progress # Manual transition
39
39
  /wr-itil:manage-story-map <STORY-MAP-NNN> completed # Transition in-progress → completed
40
40
  /wr-itil:manage-story-map <STORY-MAP-NNN> archived # Close without completion
41
+ /wr-itil:manage-story-map <STORY-MAP-NNN> ratify # ADR-090: confirm the map, then its stories, one at a time
41
42
  /wr-itil:manage-story-map review # Re-validate all maps + refresh README
42
43
  ```
43
44
 
@@ -113,6 +114,32 @@ done
113
114
 
114
115
  Per architect amend finding 2 on Slice 7: story-map HTML files do NOT carry an auto-maintained markdown reverse-trace section themselves (the `<a class="slice">` data-attribute traces are authored manually during backbone design). No reverse-trace refresh on the map itself; reverse-trace only flows OUT to problem + JTBD parents.
115
116
 
117
+ ### 7.5. Ratification flow (`ratify`) — ADR-090 / STORY-022
118
+
119
+ `ratify` is **orthogonal to the status lifecycle** — a map can be ratified at any status. It confirms human oversight of the map + its stories after they are authored or edited. Ratification is **drift-invalidated** (ADR-009 lineage, NOT ADR-066 write-once): any later content edit silently re-opens it (the `oversight-hash` fingerprint stops matching). This is the STORY-022 surface.
120
+
121
+ **Born-confirmed discipline (P348).** Before any marker write, `export CLAUDE_SESSION_ID` from the transcript path — the marker shim silently no-ops on an empty SID. Every `confirmed` marker MUST be backed by a same-turn human confirm event; never write `confirmed` without the `AskUserQuestion` below (a hollow marker is the P348 bug).
122
+
123
+ **Map first, then stories — one at a time (STORY-022 UX):**
124
+
125
+ 1. **Ratify the map.** Present the map's path/URL + a self-contained briefing of what it is — the JTBD it serves, its backbone activities, its release slices — briefing the substance BEFORE any ID (P350; the user may be on a device with no repo access). Then `AskUserQuestion` with exactly two options:
126
+ - **Ratify** — the map is correct as-is.
127
+ - **(type something)** — free-text; treat the response as a change request, apply it as a map edit, and re-present. Do NOT ratify a map the user just amended — the edit drifts the fingerprint; loop back to re-brief.
128
+
129
+ On **Ratify**: run `wr-itil-mark-story-oversight-confirmed <map-file>` (writes `confirmed` + the fingerprint). The map is now ratified.
130
+
131
+ 2. **Ratify each story, one at a time.** ONLY after the map is ratified, walk the map's `data-story-id` references in order. For each story that is not already ratified (test with `wr-itil-detect-unratified-stories-maps` or the `is_story_map_ratified` lib helper): brief its `## User value` + `## Acceptance criteria` (substance before ID), then the SAME two-option `AskUserQuestion` (**Ratify** / type-something). On **Ratify**: `wr-itil-mark-story-oversight-confirmed <story-file>`. On free-text: apply the correction as a story edit and re-present (the edit re-opens that story only).
132
+
133
+ 3. **AFK / non-interactive (ADR-013 Rule 6).** When `AskUserQuestion` is unavailable, do NOT auto-ratify — that would forge a hollow marker (P348). Leave the artefacts unratified; they surface in the `/wr-itil:work-problems` Step 2.4 drain for the next interactive session.
134
+
135
+ 4. **Single commit** — stage the map + every newly-ratified story + the README refresh; commit per ADR-014.
136
+
137
+ **Why map-first:** an RFC may reference only ratified stories (`wr-itil-check-rfc-stories-ratified`), and a story is only meaningful inside its ratified map — ratifying stories under an unratified map would invert the dependency STORY-022 encodes.
138
+
139
+ **Reuse offers ratified stories only (STORY-024).** When decomposing a fix, existing map stories the fix touches are offered for **reuse** — referenced by an additional slice card / `data-story-id`, NOT duplicated as a new file; the reused story's `rfcs:` reverse-trace picks up the new RFC. Only **ratified** stories (test with `is_story_map_ratified`) are offered for reuse; an unratified story must be ratified (§ 7.5) before an RFC can reference it.
140
+
141
+ **Re-slicing re-opens ratification (STORY-025).** Grouping stories into ordered release slices (Release 1 walking skeleton → Release 2 …) is authored as `data-status` / slice-card edits on the map; deferred phases stay first-class cards (visible, competing for priority), never buried. Because slicing edits the map's content, it drifts the `oversight-hash` and silently re-opens the map's ratification (§ 7.5) — no explicit marker reset needed; the fingerprint handles it.
142
+
116
143
  ### 8. List flow (`list`)
117
144
 
118
145
  Forward-points to `/wr-itil:list-story-maps` (read-only sibling).
@@ -350,7 +350,7 @@ Before the orchestrator emits the final `ALL_DONE` sentinel for the AFK loop, it
350
350
 
351
351
  1. *Outstanding-questions surface.* Read `.afk-run-state/outstanding-questions.jsonl`. If non-empty, invoke Step 2.5b's surfacing routine to present the accumulated queue (via `AskUserQuestion`-when-available-else-table per ADR-013 Rule 1 / Rule 6). On completion, truncate the queue file. If the queue is empty, this sub-surface returns immediately. The surfacing routine is the existing Step 2.5b — Step 2.4 does NOT re-implement; it sequences.
352
352
 
353
- 2. *Oversight-unconfirmed drain.* Run `wr-architect-detect-unoversighted` and `wr-jtbd-detect-unoversighted` (both ADR-049 PATH shims, both always exit 0; output is the list of unoversighted artefact paths). If either lists ≥ 1 artefact whose frontmatter carries `human-oversight: unconfirmed` (the AFK-explicit-deferred state, distinct from the implicit-absent state pre-existing ADR/JTBD files carry), surface a one-line nudge: *"N iter-deferred decision(s)/job(s) carry `human-oversight: unconfirmed`. Run `/wr-architect:review-decisions` and `/wr-jtbd:confirm-jobs-and-personas` to drain."* If `AskUserQuestion` is available (`/wr-itil:work-problems` was invoked interactively before the AFK loop started), surface a 2-option choice — `Drain now` (invokes the appropriate drain skill before `ALL_DONE`) / `Defer to next session` (proceeds to gate (b) with the nudge in the final summary). If `AskUserQuestion` is unavailable, the nudge prints in the final summary table and gate (b) proceeds. The drain is NOT a halt — `unconfirmed` markers are explicit-by-design AFK signals (the iter wrote them KNOWING the user would need to confirm), and the drain is the documented path. Detector difference matters: ADRs/JTBDs that pre-date the ADR-066/ADR-068 marker contract carry NO `human-oversight:` line at all; they fall through to the existing review-decisions/confirm-jobs-and-personas backlog drain (no new surfacing here). The new surfacing fires ONLY on the explicit `unconfirmed` value — the AFK-iter-deferred class P348 introduces.
353
+ 2. *Oversight-unconfirmed drain.* Run `wr-architect-detect-unoversighted`, `wr-jtbd-detect-unoversighted`, and `wr-itil-detect-unratified-stories-maps` (all ADR-049 PATH shims, all always exit 0; output is the list of unoversighted/unratified artefact paths). The story/map detector (**ADR-090**) is **drift-aware**: it lists story maps + stories that are never-ratified, explicitly `unconfirmed`, OR **drift-reopened** — a `confirmed` marker whose `oversight-hash` fingerprint no longer matches the edited content. Re-ratify each via `/wr-itil:manage-story` / `/wr-itil:manage-story-map` (or `wr-itil-mark-story-oversight-confirmed`); the nudge + 2-option `Drain now` / `Defer` surface below applies to this list identically. If either lists ≥ 1 artefact whose frontmatter carries `human-oversight: unconfirmed` (the AFK-explicit-deferred state, distinct from the implicit-absent state pre-existing ADR/JTBD files carry), surface a one-line nudge: *"N iter-deferred decision(s)/job(s) carry `human-oversight: unconfirmed`. Run `/wr-architect:review-decisions` and `/wr-jtbd:confirm-jobs-and-personas` to drain."* If `AskUserQuestion` is available (`/wr-itil:work-problems` was invoked interactively before the AFK loop started), surface a 2-option choice — `Drain now` (invokes the appropriate drain skill before `ALL_DONE`) / `Defer to next session` (proceeds to gate (b) with the nudge in the final summary). If `AskUserQuestion` is unavailable, the nudge prints in the final summary table and gate (b) proceeds. The drain is NOT a halt — `unconfirmed` markers are explicit-by-design AFK signals (the iter wrote them KNOWING the user would need to confirm), and the drain is the documented path. Detector difference matters: ADRs/JTBDs that pre-date the ADR-066/ADR-068 marker contract carry NO `human-oversight:` line at all; they fall through to the existing review-decisions/confirm-jobs-and-personas backlog drain (no new surfacing here). The new surfacing fires ONLY on the explicit `unconfirmed` value — the AFK-iter-deferred class P348 introduces.
354
354
 
355
355
  **Gate (b) — Session-level retro.** Invoke `/wr-retrospective:run-retro` via the Skill tool. This is the **orchestrator-main-turn session-level retro**, distinct from the per-iter retro fired inside each iter subprocess (per P086 / Step 5 retro-on-exit clause). The session-level retro covers cross-iter patterns, friction observations, framework-improvement candidates, and the AFK loop's overall trajectory — surface visible only after multiple iters have completed. Retro commits its own work per ADR-014; any tickets retro creates ride retro's own commit, and the orchestrator picks them up on the *next* invocation of `/wr-itil:work-problems` rather than re-entering the loop here.
356
356