@windyroad/itil 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -484,5 +484,5 @@
484
484
  }
485
485
  },
486
486
  "name": "wr-itil",
487
- "version": "0.36.0"
487
+ "version": "0.37.0"
488
488
  }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/../scripts/derive-release-vehicle.sh" "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "ITIL-aligned IT service management for Claude Code (problem, and future incident/change skills)",
5
5
  "bin": {
6
6
  "windyroad-itil": "./bin/install.mjs"
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env bash
2
+ # packages/itil/scripts/derive-release-vehicle.sh
3
+ #
4
+ # Derive the release-vehicle citation for a problem ticket by walking git
5
+ # history. Input: ticket ID (e.g. `P267` or `267`). The script reads the
6
+ # ticket body for a `.changeset/<name>.md` filename reference, finds the
7
+ # `chore: version packages` deletion commit via `git log --diff-filter=D`,
8
+ # resolves the merge PR via the ancestry-path merge commit (or `gh pr list`
9
+ # fallback when available), and emits a structured citation block on stdout.
10
+ #
11
+ # Usage:
12
+ # derive-release-vehicle.sh <ticket-id> [<problems-dir>]
13
+ #
14
+ # <ticket-id>: `P<NNN>` or bare `<NNN>`. Case-insensitive.
15
+ # <problems-dir>: defaults to ./docs/problems. Dual-tolerant lookup spans
16
+ # flat layout (`<problems-dir>/<NNN>-*.md`) AND per-state
17
+ # subdir layout (`<problems-dir>/*/<NNN>-*.md`) per ADR-031.
18
+ #
19
+ # Output (stdout, multi-line key:value block):
20
+ # RELEASE_VEHICLE:
21
+ # changeset: .changeset/<name>.md
22
+ # version-packages-commit: <SHA>
23
+ # pr: #<N>
24
+ # merge-commit: <SHA>
25
+ # release-date: <YYYY-MM-DD>
26
+ #
27
+ # Exit codes:
28
+ # 0 = OK (full citation emitted)
29
+ # 1 = ticket file not found
30
+ # 2 = no changeset reference in ticket body
31
+ # 3 = changeset still present in working tree (unreleased)
32
+ # 4 = deletion commit found but no merge PR / merge commit resolvable
33
+ #
34
+ # @problem P267 — Codify derive-release-vehicle.sh helper for K→V release-
35
+ # cycle citation. K→V transitions composed by hand are
36
+ # fragile to wrong-release-cited errors when sessions
37
+ # pre-apply transitions across sibling tickets (observed
38
+ # 2026-05-18 P250 K→V cited P247's release refs).
39
+ # @adr ADR-049 (bin/ on PATH shim — adopter-safe script resolution; helper
40
+ # is invoked as `wr-itil-derive-release-vehicle`)
41
+ # @adr ADR-022 (Verifying lifecycle — citation supports the K→V transition's
42
+ # `## Fix Released` section)
43
+ # @adr ADR-014 (single-commit grain — helper is read-only, no commit impact)
44
+ # @adr ADR-038 (progressive disclosure — short structured stdout block)
45
+ # @adr ADR-005 (Plugin testing strategy — behavioural bats per P081)
46
+ # @jtbd JTBD-001 (Enforce Governance Without Slowing Down — deterministic
47
+ # citation prevents cross-cite errors)
48
+ # @jtbd JTBD-006 (Progress the Backlog While I'm Away — orchestrator per-iter
49
+ # K→V audit trail is trustworthy)
50
+ # @jtbd JTBD-101 (Extend the Plugin Suite — sibling shim naming grammar)
51
+
52
+ set -uo pipefail
53
+
54
+ usage() {
55
+ cat >&2 <<'EOF'
56
+ USAGE: derive-release-vehicle.sh <ticket-id> [<problems-dir>]
57
+ <ticket-id> — P<NNN> or bare <NNN> (e.g. P267 or 267)
58
+ <problems-dir> — defaults to ./docs/problems
59
+
60
+ Exit codes:
61
+ 0 ok (full citation emitted)
62
+ 1 ticket file not found
63
+ 2 no changeset reference in ticket body
64
+ 3 changeset still present in working tree (unreleased)
65
+ 4 deletion commit found but no merge PR / merge commit resolvable
66
+ EOF
67
+ }
68
+
69
+ RAW_ID="${1:-}"
70
+ PROBLEMS_DIR="${2:-docs/problems}"
71
+
72
+ if [ -z "$RAW_ID" ]; then
73
+ usage
74
+ exit 2
75
+ fi
76
+
77
+ # Normalise to three-digit numeric portion.
78
+ NNN="$(printf '%s\n' "$RAW_ID" | grep -oE '[0-9]+' | head -1)"
79
+ if [ -z "$NNN" ]; then
80
+ echo "ERROR: ticket id must contain digits (got: $RAW_ID)" >&2
81
+ exit 1
82
+ fi
83
+ NNN="$(printf '%03d' "$((10#$NNN))")"
84
+
85
+ # ── Locate ticket file (dual-tolerant per ADR-031) ──────────────────────────
86
+ TICKET_FILE=""
87
+ for candidate in \
88
+ "$PROBLEMS_DIR/$NNN-"*.md \
89
+ "$PROBLEMS_DIR"/*/"$NNN-"*.md; do
90
+ if [ -f "$candidate" ]; then
91
+ TICKET_FILE="$candidate"
92
+ break
93
+ fi
94
+ done
95
+
96
+ if [ -z "$TICKET_FILE" ]; then
97
+ echo "ERROR: ticket P$NNN not found under $PROBLEMS_DIR" >&2
98
+ exit 1
99
+ fi
100
+
101
+ # ── Extract changeset filename reference from ticket body ───────────────────
102
+ # Match `.changeset/<name>.md` — kebab + alphanumeric.
103
+ CHANGESET_PATH="$(
104
+ grep -oE '\.changeset/[a-z0-9._-]+\.md' "$TICKET_FILE" 2>/dev/null \
105
+ | head -1
106
+ )"
107
+
108
+ if [ -z "$CHANGESET_PATH" ]; then
109
+ echo "ERROR: no .changeset/<name>.md reference in $TICKET_FILE" >&2
110
+ exit 2
111
+ fi
112
+
113
+ # ── Released? Changeset must be ABSENT from working tree (deleted by
114
+ # chore: version packages) AND have a deletion commit in git history. ────
115
+ if [ -f "$CHANGESET_PATH" ]; then
116
+ echo "ERROR: changeset $CHANGESET_PATH still present in working tree (unreleased)" >&2
117
+ exit 3
118
+ fi
119
+
120
+ # ── Find the deletion commit (chore: version packages) ─────────────────────
121
+ # `--diff-filter=D` filters to the commit that deleted the file. `--all`
122
+ # searches across branches. First match (oldest deletion) is canonical.
123
+ DELETE_SHA="$(
124
+ git log --all --diff-filter=D --format='%H' -- "$CHANGESET_PATH" 2>/dev/null \
125
+ | tail -1
126
+ )"
127
+
128
+ if [ -z "$DELETE_SHA" ]; then
129
+ echo "ERROR: changeset $CHANGESET_PATH has no deletion commit in git history (unreleased)" >&2
130
+ exit 3
131
+ fi
132
+
133
+ # ── Resolve merge PR + merge commit ────────────────────────────────────────
134
+ # Strategy 1: walk first-parent merges from DELETE_SHA forward toward main,
135
+ # match the merge commit that introduced DELETE_SHA into main. The
136
+ # `git log --merges --first-parent --ancestry-path DELETE_SHA..HEAD` form
137
+ # enumerates merge commits on main whose history descends from DELETE_SHA.
138
+ MERGE_SHA=""
139
+ PR_NUMBER=""
140
+
141
+ # Determine the target ref — prefer origin/main, fall back to local main,
142
+ # fall back to HEAD's branch.
143
+ TARGET_REF=""
144
+ for ref in origin/main main HEAD; do
145
+ if git rev-parse --verify "$ref" >/dev/null 2>&1; then
146
+ TARGET_REF="$ref"
147
+ break
148
+ fi
149
+ done
150
+
151
+ if [ -n "$TARGET_REF" ]; then
152
+ # First merge commit on the first-parent path from DELETE_SHA..TARGET_REF.
153
+ # `tail -1` picks the closest ancestor (oldest merge that brought
154
+ # DELETE_SHA into the target ref).
155
+ MERGE_SHA="$(
156
+ git log --merges --first-parent --ancestry-path \
157
+ --format='%H' "$DELETE_SHA^..$TARGET_REF" 2>/dev/null \
158
+ | tail -1
159
+ )"
160
+ fi
161
+
162
+ if [ -n "$MERGE_SHA" ]; then
163
+ # Extract `#<N>` from the merge commit subject (canonical PR-merge shape
164
+ # `Merge pull request #<N> from ...`).
165
+ MERGE_SUBJECT="$(git log -1 --format='%s' "$MERGE_SHA" 2>/dev/null)"
166
+ PR_NUMBER="$(printf '%s\n' "$MERGE_SUBJECT" | grep -oE '#[0-9]+' | head -1)"
167
+ fi
168
+
169
+ # Strategy 2: gh pr list fallback when git-history path didn't resolve a
170
+ # PR number and `gh` is installed + authenticated.
171
+ if [ -z "$PR_NUMBER" ] && command -v gh >/dev/null 2>&1; then
172
+ PR_NUMBER="$(
173
+ gh pr list --state merged --search "$DELETE_SHA" \
174
+ --json number --jq '.[0].number' 2>/dev/null \
175
+ | sed 's/^/#/'
176
+ )"
177
+ [ "$PR_NUMBER" = "#" ] && PR_NUMBER=""
178
+ fi
179
+
180
+ if [ -z "$PR_NUMBER" ] || [ -z "$MERGE_SHA" ]; then
181
+ echo "ERROR: deletion commit $DELETE_SHA found but no merge PR resolvable" >&2
182
+ exit 4
183
+ fi
184
+
185
+ # ── Release date from the merge commit's committer date ────────────────────
186
+ RELEASE_DATE="$(git log -1 --format='%cs' "$MERGE_SHA" 2>/dev/null)"
187
+
188
+ # ── Emit the structured citation block ─────────────────────────────────────
189
+ cat <<EOF
190
+ RELEASE_VEHICLE:
191
+ changeset: $CHANGESET_PATH
192
+ version-packages-commit: $DELETE_SHA
193
+ pr: $PR_NUMBER
194
+ merge-commit: $MERGE_SHA
195
+ release-date: $RELEASE_DATE
196
+ EOF
197
+ exit 0
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # @problem P267 — Codify derive-release-vehicle.sh helper for K→V release-cycle
4
+ # citation. K→V transitions composed by hand from inline
5
+ # pre-flight evidence are fragile to wrong-release-cited errors
6
+ # (observed 2026-05-18 session 7 iter 1 — P250's K→V cited
7
+ # P247's release refs). Helper makes citation deterministic.
8
+ #
9
+ # Contract: `derive-release-vehicle.sh <ticket-id> [<problems-dir>]` reads the
10
+ # ticket file for a changeset filename reference, walks `git log
11
+ # --diff-filter=D` for the deletion commit (chore: version packages), then
12
+ # resolves the merge PR + merge commit. Emits a structured citation block on
13
+ # stdout.
14
+ #
15
+ # Output (stdout, multi-line key:value block):
16
+ # RELEASE_VEHICLE:
17
+ # changeset: .changeset/<name>.md
18
+ # version-packages-commit: <SHA>
19
+ # pr: #<N>
20
+ # merge-commit: <SHA>
21
+ # release-date: <YYYY-MM-DD>
22
+ #
23
+ # Exit codes:
24
+ # 0 = OK (full citation emitted)
25
+ # 1 = ticket file not found
26
+ # 2 = no changeset reference in ticket body
27
+ # 3 = changeset not yet deleted (unreleased)
28
+ # 4 = deletion commit found but no merge PR / merge commit resolvable
29
+ #
30
+ # @adr ADR-049 (bin/ on PATH shim — adopter-safe script resolution)
31
+ # @adr ADR-022 (Verifying lifecycle — citation supports K→V transition)
32
+ # @adr ADR-014 (single-commit grain — helper is read-only, no commit impact)
33
+ # @adr ADR-005 (Plugin testing strategy — behavioural bats per P081)
34
+ # @jtbd JTBD-001 (Enforce Governance — deterministic citation prevents
35
+ # cross-cite errors)
36
+ # @jtbd JTBD-006 (Progress Backlog AFK — orchestrator per-iter K→V audit
37
+ # trail trustworthy)
38
+ # @jtbd JTBD-101 (Extend the Suite — sibling shim naming grammar)
39
+
40
+ setup() {
41
+ SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
42
+ SCRIPT="$SCRIPTS_DIR/derive-release-vehicle.sh"
43
+ FIXTURE_DIR="$(mktemp -d)"
44
+ cd "$FIXTURE_DIR"
45
+ git init -q -b main
46
+ git config user.email test@example.com
47
+ git config user.name "Test"
48
+ mkdir -p docs/problems/known-error docs/problems/verifying .changeset
49
+ }
50
+
51
+ teardown() {
52
+ cd /
53
+ rm -rf "$FIXTURE_DIR"
54
+ }
55
+
56
+ # ── Existence + executable ──────────────────────────────────────────────────
57
+
58
+ @test "derive-release-vehicle: script exists" {
59
+ [ -f "$SCRIPT" ]
60
+ }
61
+
62
+ @test "derive-release-vehicle: script is executable" {
63
+ [ -x "$SCRIPT" ]
64
+ }
65
+
66
+ # ── Usage / argument errors ─────────────────────────────────────────────────
67
+
68
+ @test "derive-release-vehicle: no arguments → exit non-zero with usage" {
69
+ run "$SCRIPT"
70
+ [ "$status" -ne 0 ]
71
+ echo "$output" | grep -qi "USAGE"
72
+ }
73
+
74
+ @test "derive-release-vehicle: ticket file not found → exit 1" {
75
+ run "$SCRIPT" P999 docs/problems
76
+ [ "$status" -eq 1 ]
77
+ echo "$output" | grep -qi "not found"
78
+ }
79
+
80
+ # ── Exit 2: no changeset reference in body ──────────────────────────────────
81
+
82
+ @test "derive-release-vehicle: ticket body has no changeset reference → exit 2" {
83
+ cat > docs/problems/known-error/100-no-changeset.md <<'EOF'
84
+ # Problem 100: No Changeset
85
+
86
+ **Status**: Known Error
87
+
88
+ ## Description
89
+
90
+ This ticket body does NOT reference any .changeset filename.
91
+ EOF
92
+ git add docs/problems/known-error/100-no-changeset.md
93
+ git commit -q -m "init"
94
+
95
+ run "$SCRIPT" P100 docs/problems
96
+ [ "$status" -eq 2 ]
97
+ echo "$output" | grep -qiE "no .?changeset|changeset.*reference"
98
+ }
99
+
100
+ # ── Exit 3: changeset still present (unreleased) ────────────────────────────
101
+
102
+ @test "derive-release-vehicle: changeset still in working tree (unreleased) → exit 3" {
103
+ cat > .changeset/p101-unreleased.md <<'EOF'
104
+ ---
105
+ '@windyroad/itil': patch
106
+ ---
107
+
108
+ Stub for P101.
109
+ EOF
110
+ cat > docs/problems/known-error/101-unreleased.md <<'EOF'
111
+ # Problem 101: Unreleased
112
+
113
+ **Status**: Known Error
114
+
115
+ ## Fix Strategy
116
+
117
+ Ship via `.changeset/p101-unreleased.md`.
118
+ EOF
119
+ git add .
120
+ git commit -q -m "init"
121
+
122
+ run "$SCRIPT" P101 docs/problems
123
+ [ "$status" -eq 3 ]
124
+ echo "$output" | grep -qi "unreleased\|not.*delet"
125
+ }
126
+
127
+ # ── Exit 0: happy path — full citation emitted ──────────────────────────────
128
+
129
+ @test "derive-release-vehicle: happy path — full citation block on stdout" {
130
+ # Set up the canonical release-cycle shape:
131
+ # commit A: ticket + changeset added
132
+ # commit B (on PR branch): chore: version packages — deletes changeset
133
+ # commit C (on main): merge commit referencing PR #42
134
+ cat > .changeset/p102-happy.md <<'EOF'
135
+ ---
136
+ '@windyroad/itil': patch
137
+ ---
138
+
139
+ P102 happy-path fix.
140
+ EOF
141
+ cat > docs/problems/known-error/102-happy.md <<'EOF'
142
+ # Problem 102: Happy
143
+
144
+ **Status**: Known Error
145
+
146
+ ## Fix Strategy
147
+
148
+ Ship via `.changeset/p102-happy.md`.
149
+ EOF
150
+ git add .
151
+ git commit -q -m "feat(itil): P102 fix + changeset"
152
+
153
+ # Simulate the changeset-release/main PR branch: version-packages commit
154
+ # deletes the changeset; merge commit lands on main with a "#<N>" reference.
155
+ git checkout -q -b changeset-release/main
156
+ git rm -q .changeset/p102-happy.md
157
+ git commit -q -m "chore: version packages"
158
+ git checkout -q main
159
+ git merge --no-ff -q changeset-release/main -m "Merge pull request #42 from windyroad/changeset-release/main"
160
+
161
+ run "$SCRIPT" P102 docs/problems
162
+ [ "$status" -eq 0 ]
163
+ echo "$output" | grep -q "RELEASE_VEHICLE:"
164
+ echo "$output" | grep -q "changeset: .changeset/p102-happy.md"
165
+ echo "$output" | grep -qE "version-packages-commit: [0-9a-f]{7,40}"
166
+ echo "$output" | grep -qE "pr: #42"
167
+ echo "$output" | grep -qE "merge-commit: [0-9a-f]{7,40}"
168
+ echo "$output" | grep -qE "release-date: [0-9]{4}-[0-9]{2}-[0-9]{2}"
169
+ }
170
+
171
+ @test "derive-release-vehicle: per-state subdir layout (verifying/) is reachable" {
172
+ # ADR-031 per-state subdir — ticket may live in docs/problems/verifying/
173
+ # post-K→V. Helper must dual-tolerantly find it.
174
+ cat > .changeset/p103-subdir.md <<'EOF'
175
+ ---
176
+ '@windyroad/itil': patch
177
+ ---
178
+
179
+ P103.
180
+ EOF
181
+ cat > docs/problems/verifying/103-subdir.md <<'EOF'
182
+ # Problem 103: Subdir
183
+
184
+ **Status**: Verification Pending
185
+
186
+ ## Fix Strategy
187
+
188
+ Ship via `.changeset/p103-subdir.md`.
189
+ EOF
190
+ git add .
191
+ git commit -q -m "init"
192
+
193
+ git checkout -q -b changeset-release/main
194
+ git rm -q .changeset/p103-subdir.md
195
+ git commit -q -m "chore: version packages"
196
+ git checkout -q main
197
+ git merge --no-ff -q changeset-release/main -m "Merge pull request #43 from windyroad/changeset-release/main"
198
+
199
+ run "$SCRIPT" P103 docs/problems
200
+ [ "$status" -eq 0 ]
201
+ echo "$output" | grep -q "changeset: .changeset/p103-subdir.md"
202
+ }
203
+
204
+ @test "derive-release-vehicle: accepts bare numeric ID (no P prefix)" {
205
+ cat > .changeset/p104-bare.md <<'EOF'
206
+ ---
207
+ '@windyroad/itil': patch
208
+ ---
209
+
210
+ P104.
211
+ EOF
212
+ cat > docs/problems/known-error/104-bare.md <<'EOF'
213
+ # Problem 104: Bare
214
+
215
+ **Status**: Known Error
216
+
217
+ ## Fix Strategy
218
+
219
+ Ship via `.changeset/p104-bare.md`.
220
+ EOF
221
+ git add .
222
+ git commit -q -m "init"
223
+
224
+ git checkout -q -b changeset-release/main
225
+ git rm -q .changeset/p104-bare.md
226
+ git commit -q -m "chore: version packages"
227
+ git checkout -q main
228
+ git merge --no-ff -q changeset-release/main -m "Merge pull request #44 from windyroad/changeset-release/main"
229
+
230
+ # Plain "104" — no P prefix.
231
+ run "$SCRIPT" 104 docs/problems
232
+ [ "$status" -eq 0 ]
233
+ echo "$output" | grep -q "RELEASE_VEHICLE:"
234
+ }
235
+
236
+ # ── Exit 4: deletion commit found but no merge PR resolvable ────────────────
237
+
238
+ @test "derive-release-vehicle: deletion on main branch, no merge PR → exit 4" {
239
+ # Direct commit to main (no PR merge) — helper finds the deletion commit
240
+ # but cannot resolve a #<N> merge PR. Exit 4.
241
+ cat > .changeset/p105-no-pr.md <<'EOF'
242
+ ---
243
+ '@windyroad/itil': patch
244
+ ---
245
+
246
+ P105.
247
+ EOF
248
+ cat > docs/problems/known-error/105-no-pr.md <<'EOF'
249
+ # Problem 105: No PR
250
+
251
+ **Status**: Known Error
252
+
253
+ ## Fix Strategy
254
+
255
+ Ship via `.changeset/p105-no-pr.md`.
256
+ EOF
257
+ git add .
258
+ git commit -q -m "init"
259
+
260
+ # Delete on main directly (no merge commit, no PR reference).
261
+ git rm -q .changeset/p105-no-pr.md
262
+ git commit -q -m "chore: version packages (direct)"
263
+
264
+ run "$SCRIPT" P105 docs/problems
265
+ [ "$status" -eq 4 ]
266
+ echo "$output" | grep -qiE "no.*(merge|pr)|cannot resolve"
267
+ }
268
+
269
+ # ── Default problems-dir resolution ─────────────────────────────────────────
270
+
271
+ @test "derive-release-vehicle: defaults to ./docs/problems when problems-dir omitted" {
272
+ cat > .changeset/p106-default.md <<'EOF'
273
+ ---
274
+ '@windyroad/itil': patch
275
+ ---
276
+
277
+ P106.
278
+ EOF
279
+ cat > docs/problems/known-error/106-default.md <<'EOF'
280
+ # Problem 106: Default
281
+
282
+ **Status**: Known Error
283
+
284
+ ## Fix Strategy
285
+
286
+ Ship via `.changeset/p106-default.md`.
287
+ EOF
288
+ git add .
289
+ git commit -q -m "init"
290
+
291
+ git checkout -q -b changeset-release/main
292
+ git rm -q .changeset/p106-default.md
293
+ git commit -q -m "chore: version packages"
294
+ git checkout -q main
295
+ git merge --no-ff -q changeset-release/main -m "Merge pull request #46 from windyroad/changeset-release/main"
296
+
297
+ # Omit problems-dir.
298
+ run "$SCRIPT" P106
299
+ [ "$status" -eq 0 ]
300
+ echo "$output" | grep -q "changeset: .changeset/p106-default.md"
301
+ }
302
+
303
+ # ── Bin shim parity ─────────────────────────────────────────────────────────
304
+
305
+ @test "derive-release-vehicle: bin shim wr-itil-derive-release-vehicle exists and is executable" {
306
+ BIN="$(cd "$SCRIPTS_DIR/../bin" && pwd)/wr-itil-derive-release-vehicle"
307
+ [ -f "$BIN" ]
308
+ [ -x "$BIN" ]
309
+ }
310
+
311
+ @test "derive-release-vehicle: bin shim dispatches to the canonical script" {
312
+ BIN="$(cd "$SCRIPTS_DIR/../bin" && pwd)/wr-itil-derive-release-vehicle"
313
+ cat > .changeset/p107-shim.md <<'EOF'
314
+ ---
315
+ '@windyroad/itil': patch
316
+ ---
317
+
318
+ P107.
319
+ EOF
320
+ cat > docs/problems/known-error/107-shim.md <<'EOF'
321
+ # Problem 107: Shim
322
+
323
+ **Status**: Known Error
324
+
325
+ ## Fix Strategy
326
+
327
+ Ship via `.changeset/p107-shim.md`.
328
+ EOF
329
+ git add .
330
+ git commit -q -m "init"
331
+
332
+ git checkout -q -b changeset-release/main
333
+ git rm -q .changeset/p107-shim.md
334
+ git commit -q -m "chore: version packages"
335
+ git checkout -q main
336
+ git merge --no-ff -q changeset-release/main -m "Merge pull request #47 from windyroad/changeset-release/main"
337
+
338
+ run "$BIN" P107 docs/problems
339
+ [ "$status" -eq 0 ]
340
+ echo "$output" | grep -q "RELEASE_VEHICLE:"
341
+ echo "$output" | grep -q "pr: #47"
342
+ }
@@ -157,6 +157,33 @@ The `## Fix Released` section contains: release marker (version, commit SHA, or
157
157
 
158
158
  When this transition is folded into a `fix(<scope>): ... (closes P<NNN>)` commit (the common case), the `git mv` + `Edit` + re-stage + README refresh all join that single commit — never split across commits.
159
159
 
160
+ **Release-vehicle citation (P267)**: derive the release marker deterministically — never hand-type it from `git log` browse output. Hand-typed citations are fragile to wrong-release-cited errors when a session pre-applies transitions for multiple sibling tickets before working any of them (origin: 2026-05-18 session 7 iter 1 — P250's K→V cited P247's release refs). Invoke the helper:
161
+
162
+ ```bash
163
+ wr-itil-derive-release-vehicle <NNN>
164
+ ```
165
+
166
+ `wr-itil-derive-release-vehicle` is the ADR-049 `$PATH` shim (adopter-safe — resolves the canonical `derive-release-vehicle.sh` relative to the script, NOT cwd; P317/RFC-009) that reads the ticket body for the `.changeset/<name>.md` reference, walks `git log --diff-filter=D` for the deletion commit (`chore: version packages`), resolves the merge PR via the first-parent ancestry-path merge commit (or `gh pr list` fallback when available), and emits a structured citation block on stdout:
167
+
168
+ ```
169
+ RELEASE_VEHICLE:
170
+ changeset: .changeset/<name>.md
171
+ version-packages-commit: <SHA>
172
+ pr: #<N>
173
+ merge-commit: <SHA>
174
+ release-date: <YYYY-MM-DD>
175
+ ```
176
+
177
+ Use the structured values verbatim when authoring the `## Fix Released` section's release marker (e.g. `Released in @windyroad/itil@<version> (merge commit <merge-commit>, PR #<N>, released <release-date>)`). The helper is **mechanical** per ADR-044 framework-resolution — no `AskUserQuestion` per transition.
178
+
179
+ **Helper exit-code routing**:
180
+
181
+ - Exit 0 (full citation emitted): use the values in the `## Fix Released` section.
182
+ - Exit 1 (ticket file not found): the upstream ticket-discovery step (Step 2) should have caught this; if it fires here, halt and report.
183
+ - Exit 2 (no `.changeset/<name>.md` reference in ticket body): add the changeset reference to the ticket's Fix Strategy section, OR cite the release marker manually with explicit `<!-- no-changeset-reference -->` comment so a future review can audit the gap.
184
+ - Exit 3 (changeset still in working tree — unreleased): the fix has not yet been released to npm. Halt the transition — `.known-error.md` → `.verifying.md` requires a shipped release per ADR-022. The orchestrator's Step 6.5 drain should fire first.
185
+ - Exit 4 (deletion commit found but no merge PR resolvable): direct-to-main commit (no PR), or `gh pr list` unavailable + first-parent walk did not match. Cite the deletion commit SHA manually in the `## Fix Released` section with an explicit `<!-- no-pr -->` comment.
186
+
160
187
  **Verification Pending → Closed**:
161
188
 
162
189
  ```bash