@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +17 -1
- package/bin/wr-itil-check-afk-accept-eligible +51 -0
- package/hooks/itil-no-implement-draft-gate.sh +46 -1
- package/hooks/test/itil-no-implement-draft-gate.bats +131 -3
- package/lib/story-oversight.sh +54 -1
- package/package.json +1 -1
- package/scripts/check-afk-accept-eligible.sh +230 -0
- package/scripts/detect-unratified-stories-maps.sh +25 -1
- package/scripts/mark-story-oversight-confirmed.sh +21 -5
- package/scripts/test/check-afk-accept-eligible.bats +310 -0
- package/skills/list-stories/SKILL.md +4 -2
- package/skills/manage-story/SKILL.md +26 -5
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
|
-
#
|
|
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
|
-
|
|
29
|
-
|
|
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"'
|
package/lib/story-oversight.sh
CHANGED
|
@@ -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
|
@@ -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
|
-
#
|
|
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
|
-
#
|
|
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` |
|
|
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
|
|
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`)
|