@windyroad/itil 0.50.1 → 0.50.2
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/package.json +1 -1
- package/scripts/derive-release-vehicle.sh +72 -4
- package/scripts/test/derive-release-vehicle.bats +99 -2
- package/skills/work-problems/SKILL.md +20 -4
- package/skills/work-problems/test/work-problems-preflight-failure-handling.bats +248 -0
package/package.json
CHANGED
|
@@ -24,11 +24,23 @@
|
|
|
24
24
|
# merge-commit: <SHA>
|
|
25
25
|
# release-date: <YYYY-MM-DD>
|
|
26
26
|
#
|
|
27
|
+
# De-facto-released variant (P361, exit 0) when the changeset is present in
|
|
28
|
+
# .changeset/ but its code already shipped with a sibling release:
|
|
29
|
+
# RELEASE_VEHICLE:
|
|
30
|
+
# changeset: .changeset/<name>.md
|
|
31
|
+
# status: de-facto-released (attribution pending)
|
|
32
|
+
# fix-commit: <SHA>
|
|
33
|
+
# shipped-with-version-packages-commit: <SHA>
|
|
34
|
+
# release-date: <YYYY-MM-DD>
|
|
35
|
+
# note: ...
|
|
36
|
+
#
|
|
27
37
|
# Exit codes:
|
|
28
|
-
# 0 = OK (full citation emitted
|
|
38
|
+
# 0 = OK (full citation emitted, OR de-facto-released graduated-holding
|
|
39
|
+
# changeset whose code already shipped with a sibling release — P361)
|
|
29
40
|
# 1 = ticket file not found
|
|
30
41
|
# 2 = no changeset reference in ticket body
|
|
31
|
-
# 3 = changeset
|
|
42
|
+
# 3 = changeset present in working tree AND not de-facto-released
|
|
43
|
+
# (genuinely unreleased)
|
|
32
44
|
# 4 = deletion commit found but no merge PR / merge commit resolvable
|
|
33
45
|
#
|
|
34
46
|
# @problem P267 — Codify derive-release-vehicle.sh helper for K→V release-
|
|
@@ -36,6 +48,14 @@
|
|
|
36
48
|
# fragile to wrong-release-cited errors when sessions
|
|
37
49
|
# pre-apply transitions across sibling tickets (observed
|
|
38
50
|
# 2026-05-18 P250 K→V cited P247's release refs).
|
|
51
|
+
# @problem P361 — exit-3 "unreleased" false positive on ADR-061 graduated
|
|
52
|
+
# holding changesets; helper now distinguishes "attribution
|
|
53
|
+
# pending" (de-facto-released, exit 0) from "code unreleased"
|
|
54
|
+
# (exit 3) via an add-commit ancestry test against the latest
|
|
55
|
+
# version-packages commit.
|
|
56
|
+
# @adr ADR-061 (dogfood graduation criteria — held changeset reinstated to
|
|
57
|
+
# .changeset/ awaiting attribution after its code shipped; the
|
|
58
|
+
# de-facto-released exit-0 path)
|
|
39
59
|
# @adr ADR-049 (bin/ on PATH shim — adopter-safe script resolution; helper
|
|
40
60
|
# is invoked as `wr-itil-derive-release-vehicle`)
|
|
41
61
|
# @adr ADR-022 (Verifying lifecycle — citation supports the K→V transition's
|
|
@@ -58,10 +78,10 @@ USAGE: derive-release-vehicle.sh <ticket-id> [<problems-dir>]
|
|
|
58
78
|
<problems-dir> — defaults to ./docs/problems
|
|
59
79
|
|
|
60
80
|
Exit codes:
|
|
61
|
-
0 ok (full citation
|
|
81
|
+
0 ok (full citation, OR de-facto-released graduated-holding changeset — P361)
|
|
62
82
|
1 ticket file not found
|
|
63
83
|
2 no changeset reference in ticket body
|
|
64
|
-
3 changeset
|
|
84
|
+
3 changeset present in working tree AND not de-facto-released (unreleased)
|
|
65
85
|
4 deletion commit found but no merge PR / merge commit resolvable
|
|
66
86
|
EOF
|
|
67
87
|
}
|
|
@@ -112,7 +132,55 @@ fi
|
|
|
112
132
|
|
|
113
133
|
# ── Released? Changeset must be ABSENT from working tree (deleted by
|
|
114
134
|
# chore: version packages) AND have a deletion commit in git history. ────
|
|
135
|
+
# Exception (P361 / ADR-061 Rule 5 + P359): a changeset can be reinstated to
|
|
136
|
+
# .changeset/ awaiting changelog attribution AFTER its code already de-facto
|
|
137
|
+
# shipped with a sibling release (held code ships with any sibling release —
|
|
138
|
+
# P359). Present-in-tree therefore does NOT always mean unreleased. Before
|
|
139
|
+
# exiting 3, test whether the commit that originally ADDED this changeset is
|
|
140
|
+
# an ancestor of the latest published "chore: version packages" commit; if so
|
|
141
|
+
# the fix code shipped → emit a de-facto-released citation (exit 0). A
|
|
142
|
+
# genuinely-unreleased fresh changeset has its add-commit NEWER than the last
|
|
143
|
+
# bump, so the is-ancestor test is false and it correctly stays exit 3.
|
|
115
144
|
if [ -f "$CHANGESET_PATH" ]; then
|
|
145
|
+
# Oldest Add of the path = the original fix commit. Robust to the
|
|
146
|
+
# hold→graduate `git mv`, which (without rename detection) records a later
|
|
147
|
+
# Add at the same path; `tail -1` selects the original.
|
|
148
|
+
ADD_SHA="$(
|
|
149
|
+
git log --diff-filter=A --format='%H' -- "$CHANGESET_PATH" 2>/dev/null \
|
|
150
|
+
| tail -1
|
|
151
|
+
)"
|
|
152
|
+
|
|
153
|
+
# Resolve the published-history ref (same ladder used for merge resolution).
|
|
154
|
+
DEFACTO_REF=""
|
|
155
|
+
for ref in origin/main main HEAD; do
|
|
156
|
+
if git rev-parse --verify "$ref" >/dev/null 2>&1; then
|
|
157
|
+
DEFACTO_REF="$ref"
|
|
158
|
+
break
|
|
159
|
+
fi
|
|
160
|
+
done
|
|
161
|
+
|
|
162
|
+
LATEST_VERSION_BUMP=""
|
|
163
|
+
if [ -n "$DEFACTO_REF" ]; then
|
|
164
|
+
LATEST_VERSION_BUMP="$(
|
|
165
|
+
git log --grep='^chore: version packages' --format='%H' -1 "$DEFACTO_REF" 2>/dev/null
|
|
166
|
+
)"
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
if [ -n "$ADD_SHA" ] && [ -n "$LATEST_VERSION_BUMP" ] \
|
|
170
|
+
&& git merge-base --is-ancestor "$ADD_SHA" "$LATEST_VERSION_BUMP" 2>/dev/null; then
|
|
171
|
+
RELEASE_DATE="$(git log -1 --format='%cs' "$LATEST_VERSION_BUMP" 2>/dev/null)"
|
|
172
|
+
cat <<EOF
|
|
173
|
+
RELEASE_VEHICLE:
|
|
174
|
+
changeset: $CHANGESET_PATH
|
|
175
|
+
status: de-facto-released (attribution pending)
|
|
176
|
+
fix-commit: $ADD_SHA
|
|
177
|
+
shipped-with-version-packages-commit: $LATEST_VERSION_BUMP
|
|
178
|
+
release-date: $RELEASE_DATE
|
|
179
|
+
note: changeset present in .changeset/ awaiting changelog attribution (ADR-061 holding-graduation); code already shipped with a sibling release (P359).
|
|
180
|
+
EOF
|
|
181
|
+
exit 0
|
|
182
|
+
fi
|
|
183
|
+
|
|
116
184
|
echo "ERROR: changeset $CHANGESET_PATH still present in working tree (unreleased)" >&2
|
|
117
185
|
exit 3
|
|
118
186
|
fi
|
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
# release-date: <YYYY-MM-DD>
|
|
22
22
|
#
|
|
23
23
|
# Exit codes:
|
|
24
|
-
# 0 = OK (full citation emitted
|
|
24
|
+
# 0 = OK (full citation emitted, OR de-facto-released graduated-holding
|
|
25
|
+
# changeset whose code already shipped with a sibling release — P361)
|
|
25
26
|
# 1 = ticket file not found
|
|
26
27
|
# 2 = no changeset reference in ticket body
|
|
27
|
-
# 3 = changeset not
|
|
28
|
+
# 3 = changeset still present AND not de-facto-released (genuinely unreleased)
|
|
28
29
|
# 4 = deletion commit found but no merge PR / merge commit resolvable
|
|
29
30
|
#
|
|
30
31
|
# @adr ADR-049 (bin/ on PATH shim — adopter-safe script resolution)
|
|
@@ -124,6 +125,102 @@ EOF
|
|
|
124
125
|
echo "$output" | grep -qi "unreleased\|not.*delet"
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
# ── Exit 0: de-facto-released graduated-holding changeset (P361) ─────────────
|
|
129
|
+
|
|
130
|
+
@test "derive-release-vehicle: graduated holding changeset whose code shipped with a sibling release → exit 0 de-facto-released" {
|
|
131
|
+
# P361 / ADR-061 Rule 5 + P359: a changeset can be reinstated to
|
|
132
|
+
# .changeset/ awaiting changelog attribution AFTER its code already shipped
|
|
133
|
+
# with a sibling release. Present-in-tree must NOT read as unreleased.
|
|
134
|
+
cat > .changeset/p211-graduated.md <<'EOF'
|
|
135
|
+
---
|
|
136
|
+
'@windyroad/itil': patch
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
P211 fix — held then graduated.
|
|
140
|
+
EOF
|
|
141
|
+
cat > docs/problems/verifying/211-graduated.md <<'EOF'
|
|
142
|
+
# Problem 211: Graduated
|
|
143
|
+
|
|
144
|
+
**Status**: Known Error
|
|
145
|
+
|
|
146
|
+
## Fix Strategy
|
|
147
|
+
|
|
148
|
+
Ship via `.changeset/p211-graduated.md`.
|
|
149
|
+
EOF
|
|
150
|
+
git add .
|
|
151
|
+
git commit -q -m "feat(itil): P211 fix + changeset" # commit A — the fix
|
|
152
|
+
|
|
153
|
+
# Hold it out of the active release queue (ADR-042 Rule 7).
|
|
154
|
+
mkdir -p docs/changesets-holding
|
|
155
|
+
git mv .changeset/p211-graduated.md docs/changesets-holding/p211-graduated.md
|
|
156
|
+
git commit -q -m "chore(itil): hold P211 changeset (above-appetite)"
|
|
157
|
+
|
|
158
|
+
# A sibling release ships AFTER the fix landed (P359: code ships regardless).
|
|
159
|
+
cat > .changeset/p999-sibling.md <<'EOF'
|
|
160
|
+
---
|
|
161
|
+
'@windyroad/itil': patch
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
Sibling fix.
|
|
165
|
+
EOF
|
|
166
|
+
git add .changeset/p999-sibling.md
|
|
167
|
+
git commit -q -m "feat(itil): sibling fix + changeset"
|
|
168
|
+
git rm -q .changeset/p999-sibling.md
|
|
169
|
+
git commit -q -m "chore: version packages" # the published release bump
|
|
170
|
+
|
|
171
|
+
# Graduate the held changeset back to .changeset/ awaiting attribution.
|
|
172
|
+
mkdir -p .changeset
|
|
173
|
+
git mv docs/changesets-holding/p211-graduated.md .changeset/p211-graduated.md
|
|
174
|
+
git commit -q -m "chore(itil): graduate P211 changeset"
|
|
175
|
+
|
|
176
|
+
run "$SCRIPT" P211 docs/problems
|
|
177
|
+
[ "$status" -eq 0 ]
|
|
178
|
+
echo "$output" | grep -qi "de-facto-released"
|
|
179
|
+
echo "$output" | grep -q "changeset: .changeset/p211-graduated.md"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@test "derive-release-vehicle: fresh changeset added AFTER the last release → still exit 3 (not a false de-facto-released)" {
|
|
183
|
+
# Guard: a prior release exists, but THIS changeset was added afterwards —
|
|
184
|
+
# its code has NOT shipped. Its add-commit is a DESCENDANT of the last
|
|
185
|
+
# version bump, so the is-ancestor discriminator must keep it at exit 3.
|
|
186
|
+
cat > .changeset/p888-old.md <<'EOF'
|
|
187
|
+
---
|
|
188
|
+
'@windyroad/itil': patch
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
Old fix that did ship.
|
|
192
|
+
EOF
|
|
193
|
+
git add .changeset/p888-old.md
|
|
194
|
+
git commit -q -m "feat(itil): old fix + changeset"
|
|
195
|
+
git rm -q .changeset/p888-old.md
|
|
196
|
+
git commit -q -m "chore: version packages" # prior release
|
|
197
|
+
|
|
198
|
+
# Now add a NEW changeset AFTER the release — genuinely unreleased.
|
|
199
|
+
mkdir -p .changeset
|
|
200
|
+
cat > .changeset/p889-fresh.md <<'EOF'
|
|
201
|
+
---
|
|
202
|
+
'@windyroad/itil': patch
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
Fresh fix, not yet released.
|
|
206
|
+
EOF
|
|
207
|
+
cat > docs/problems/known-error/889-fresh.md <<'EOF'
|
|
208
|
+
# Problem 889: Fresh
|
|
209
|
+
|
|
210
|
+
**Status**: Known Error
|
|
211
|
+
|
|
212
|
+
## Fix Strategy
|
|
213
|
+
|
|
214
|
+
Ship via `.changeset/p889-fresh.md`.
|
|
215
|
+
EOF
|
|
216
|
+
git add .
|
|
217
|
+
git commit -q -m "feat(itil): fresh fix + changeset"
|
|
218
|
+
|
|
219
|
+
run "$SCRIPT" P889 docs/problems
|
|
220
|
+
[ "$status" -eq 3 ]
|
|
221
|
+
echo "$output" | grep -qi "unreleased"
|
|
222
|
+
}
|
|
223
|
+
|
|
127
224
|
# ── Exit 0: happy path — full citation emitted ──────────────────────────────
|
|
128
225
|
|
|
129
226
|
@test "derive-release-vehicle: happy path — full citation block on stdout" {
|
|
@@ -176,7 +176,7 @@ The helper returns one of five outcomes (contract documented at `packages/itil/l
|
|
|
176
176
|
| `ttl-expiry age=<N>s ttl=<M>s` | Dispatch `/wr-itil:review-problems` as a pre-flight iter. Cache is stale; review-problems' Step 4.5b's TTL-expiry auto-recheck branch fires inside the dispatched subprocess and refreshes the cache + audit-log + README. |
|
|
177
177
|
| `fresh-within-ttl` | Silent-pass per ADR-013 Rule 5 + P132 mechanical-stage carve-out. Proceed to Step 1. |
|
|
178
178
|
|
|
179
|
-
**Pre-flight dispatch shape**: when promoted, dispatch a single `claude -p --permission-mode bypassPermissions --output-format json` subprocess that invokes `/wr-itil:review-problems` (per P084 + ADR-032 subprocess isolation). The subprocess runs the full Step 4.5 inbound-discovery + assessment pipeline; the cache + `docs/audits/inbound-discovery-log.md` + `docs/problems/README.md` are refreshed in its own commit per ADR-014 (review-problems' Slice E commit grain). After the subprocess completes, the orchestrator reads the freshly-refreshed README at Step 1.
|
|
179
|
+
**Pre-flight dispatch shape**: when promoted, dispatch a single `claude -p --permission-mode bypassPermissions --output-format json` subprocess that invokes `/wr-itil:review-problems` (per P084 + ADR-032 subprocess isolation). The subprocess runs the full Step 4.5 inbound-discovery + assessment pipeline; the cache + `docs/audits/inbound-discovery-log.md` + `docs/problems/README.md` are refreshed in its own commit per ADR-014 (review-problems' Slice E commit grain). After the subprocess completes, the orchestrator reads the freshly-refreshed README at Step 1. **If the pre-flight subprocess exits non-zero OR returns `is_error: true`**, apply the non-blocking revert-and-proceed contract in "Step 0 pre-flight subprocess failure handling (P358)" below — do NOT halt the loop (a failed pre-flight is a non-load-bearing cache-refresh dependency, NOT an iter).
|
|
180
180
|
|
|
181
181
|
**Iter-summary annotation**:
|
|
182
182
|
|
|
@@ -222,7 +222,7 @@ The helper returns one of five outcomes (contract documented at `packages/itil/l
|
|
|
222
222
|
|
|
223
223
|
The intersection is the actual signal: "there is work to do AND the cadence has slipped".
|
|
224
224
|
|
|
225
|
-
**Pre-flight dispatch shape**: when promoted (`no-readme` or `stale-readme`), dispatch a single `claude -p --permission-mode bypassPermissions --output-format json` subprocess that invokes `/wr-itil:review-problems` (per P084 + ADR-032 subprocess isolation). Reuse the Step 5 subprocess wrapper verbatim — same flag set, same idle-timeout SIGTERM poll loop, same retro-on-exit contract. The subprocess runs the full Step 2 + Step 2.5 + Step 4 + Step 5 re-rate + README refresh + commit; the orchestrator reads the freshly-refreshed README at Step 1.
|
|
225
|
+
**Pre-flight dispatch shape**: when promoted (`no-readme` or `stale-readme`), dispatch a single `claude -p --permission-mode bypassPermissions --output-format json` subprocess that invokes `/wr-itil:review-problems` (per P084 + ADR-032 subprocess isolation). Reuse the Step 5 subprocess wrapper verbatim — same flag set, same idle-timeout SIGTERM poll loop, same retro-on-exit contract. The subprocess runs the full Step 2 + Step 2.5 + Step 4 + Step 5 re-rate + README refresh + commit; the orchestrator reads the freshly-refreshed README at Step 1. **If the pre-flight subprocess exits non-zero OR returns `is_error: true`**, apply the non-blocking revert-and-proceed contract in "Step 0 pre-flight subprocess failure handling (P358)" below — do NOT halt the loop (a failed pre-flight is a non-load-bearing cache-refresh dependency, NOT an iter).
|
|
226
226
|
|
|
227
227
|
**ADR-079 composition note**: Step 0c dispatches `/wr-itil:review-problems` which includes Step 4.6 relevance-close per ADR-079 — relevance-close fires as a side-effect of the auto-dispatch. This is desirable: relevance closes accumulate the same way deferred placeholders do, and the AND-trigger reasonably gates both pieces of work.
|
|
228
228
|
|
|
@@ -266,7 +266,7 @@ The helper returns one of five outcomes (contract documented at `packages/itil/l
|
|
|
266
266
|
| `ttl-expiry age=<N>s ttl=<M>s` | Dispatch `/wr-itil:check-upstream-responses` as a pre-flight iter. Cache stale; the skill polls each back-linked upstream URL, diffs against the cache, and emits STATE / NEW / LABEL / NONE / FAIL per back-link ticket. |
|
|
267
267
|
| `fresh-within-ttl` | Silent-pass per ADR-013 Rule 5 + P132 mechanical-stage carve-out. Proceed to Step 1. |
|
|
268
268
|
|
|
269
|
-
**Pre-flight dispatch shape**: when promoted, dispatch a single `claude -p --permission-mode bypassPermissions --output-format json` subprocess that invokes `/wr-itil:check-upstream-responses` (per P084 + ADR-032 subprocess isolation). Reuse the Step 5 subprocess wrapper verbatim — same flag set, same idle-timeout SIGTERM poll loop. The subprocess runs the full check-upstream-responses Step 1 + Step 2 + Step 3 pipeline; the cache file `docs/problems/.outbound-responses-cache.json` + audit-log `docs/audits/outbound-responses-log.md` are refreshed in its own commit per ADR-014 (check-upstream-responses' SKILL.md Step 3 commit grain). After the subprocess completes, the orchestrator proceeds to Step 1.
|
|
269
|
+
**Pre-flight dispatch shape**: when promoted, dispatch a single `claude -p --permission-mode bypassPermissions --output-format json` subprocess that invokes `/wr-itil:check-upstream-responses` (per P084 + ADR-032 subprocess isolation). Reuse the Step 5 subprocess wrapper verbatim — same flag set, same idle-timeout SIGTERM poll loop. The subprocess runs the full check-upstream-responses Step 1 + Step 2 + Step 3 pipeline; the cache file `docs/problems/.outbound-responses-cache.json` + audit-log `docs/audits/outbound-responses-log.md` are refreshed in its own commit per ADR-014 (check-upstream-responses' SKILL.md Step 3 commit grain). After the subprocess completes, the orchestrator proceeds to Step 1. **If the pre-flight subprocess exits non-zero OR returns `is_error: true`**, apply the non-blocking revert-and-proceed contract in "Step 0 pre-flight subprocess failure handling (P358)" below — do NOT halt the loop (a failed pre-flight is a non-load-bearing cache-refresh dependency, NOT an iter).
|
|
270
270
|
|
|
271
271
|
**Iter-summary annotation**:
|
|
272
272
|
|
|
@@ -285,7 +285,23 @@ The annotation pre-empts the "surprise heavy iter" perception JTBD-006 expects a
|
|
|
285
285
|
<!-- @jtbd JTBD-006 (Progress the Backlog While I'm Away — AFK orchestrator pre-flights check-upstream-responses so outbound STATE/NEW deltas surface without manual polling) -->
|
|
286
286
|
<!-- @jtbd JTBD-004 (Connect Agents Across Repos to Collaborate — closes the outbound symmetric feedback loop) -->
|
|
287
287
|
|
|
288
|
-
After Step 0d completes (whether dispatched or silent-passed), proceed to Step 1.
|
|
288
|
+
After Step 0d completes (whether dispatched or silent-passed), proceed to the shared pre-flight failure-handling contract below, then to Step 1.
|
|
289
|
+
|
|
290
|
+
### Step 0 pre-flight subprocess failure handling (P358 — non-blocking revert-and-proceed)
|
|
291
|
+
|
|
292
|
+
Step 0b / Step 0c / Step 0d (and **any future Step 0x pre-flight** that reuses the Step 5 `claude -p` subprocess wrapper) dispatch a `/wr-itil:review-problems` or `/wr-itil:check-upstream-responses` **pre-flight subprocess** "same shape as Step 5". That phrase imports the Step 5 *dispatch mechanism* (the `claude -p --output-format json` wrapper + the idle-timeout SIGTERM poll loop), but the **failure semantics are NOT shared** — and the prior prose left this implicit, which P358 surfaced. Step 5's exit-code semantics HALT the loop on non-zero exit / `is_error: true` because **the iter IS the loop body unit** — its failure is the loop's failure. A **pre-flight is a non-load-bearing cache-refresh dependency**, not an iteration of the loop body: Step 1's backlog scan reads whatever `docs/problems/README.md` already exists (freshly-refreshed or slightly-stale), so a failed pre-flight degrades to "cache not refreshed this pass" — never to "halt the loop".
|
|
293
|
+
|
|
294
|
+
**Contract — a pre-flight subprocess that exits non-zero OR returns `is_error: true` is NON-BLOCKING** (general rule; every Step 0x pre-flight inherits it):
|
|
295
|
+
|
|
296
|
+
1. **Revert any dirty working-tree state the failed pre-flight left.** The dispatched skill commits its own refresh per ADR-014 (review-problems' Slice E grain / check-upstream-responses' Step 3 grain) — it commits end-to-end or not at all. A subprocess that died mid-refresh may leave an **UNSTAGED** partial write across any path the dispatched skill is contractually allowed to touch: the staleness cache (`docs/problems/.upstream-cache.json` for 0b, `docs/problems/.outbound-responses-cache.json` for 0d), the audit log (`docs/audits/inbound-discovery-log.md` for 0b, `docs/audits/outbound-responses-log.md` for 0d), AND `docs/problems/README.md` + re-rated ticket bodies (0c). Revert the whole contractually-touchable set — not just the cache JSON — so a half-written README or audit-log is also restored. Revert each path **independently** (`git checkout -- docs/problems/ 2>/dev/null; git checkout -- docs/audits/ 2>/dev/null`) rather than as a combined `git checkout -- docs/problems/ docs/audits/` pathspec: the combined form errors and reverts NOTHING when `docs/audits/` is absent (a fresh adopter repo that has never run inbound/outbound discovery), whereas the per-path form tolerates the missing directory and still reverts the dirty `docs/problems/` write. Do NOT commit a partial write: a half-refreshed cache/README is worse than a stale-but-coherent one. If the dead pre-flight somehow left **STAGED** residue (it should not — the pre-flight owns its commit end-to-end), `git reset` (unstage) it first, then revert, so the orchestrator's own subsequent Step 1+ gate flow is not contaminated by a dead subprocess's index (mirrors the ADR-009 no-trust-window-extension reasoning — a dead `is_error: true` subprocess MUST NOT seed the parent's commit).
|
|
297
|
+
2. **Log a one-line iter-summary annotation** naming the failed pre-flight + the failure class: `Step 0<b|c|d> pre-flight FAILED (<exit-code | is_error class>) — reverted partial cache write, proceeding to Step 1 with existing README`. Preserves the JTBD-006 audit-trail outcome (the silent degradation becomes observable rather than invisible).
|
|
298
|
+
3. **Proceed to Step 1.** The pre-flight failure does NOT halt the loop and does NOT count against the Step 0 prior-session-state Branch 3 detection (step 1 above restored a clean tree, so the iter dispatches that follow start from a clean state).
|
|
299
|
+
|
|
300
|
+
**`is_error: true` sub-class note (reconciles P358 with the Step 5 taxonomy).** A pre-flight subprocess failure is the SAME `is_error: true` family the Step 5 exit-code semantics taxonomise (P261 SALVAGE / P214 HALT) — **including** the `socket connection was closed unexpectedly` variant (an `is_error: true` shape that routes to the Step 5 catch-all advisory). The load-bearing distinction P358 surfaces is **orthogonal to the SALVAGE-vs-HALT axis**: that axis is scoped to **iters** (the loop body); pre-flights have their own non-blocking failure contract. The Step 5 SALVAGE branch does **NOT** apply to a pre-flight even when the pre-flight left staged work — a pre-flight is not an iteration whose work the orchestrator salvages-and-commits; its job is a cache refresh the dispatched skill owns end-to-end. Pre-flight failure is therefore ALWAYS the revert-and-proceed branch above, never SALVAGE.
|
|
301
|
+
|
|
302
|
+
**AFK authorisation per ADR-013 Rule 6**: revert-and-proceed is a deterministic, non-interactive recovery — no `AskUserQuestion`. Reverting an unstaged partial write is fully reversible (the next loop pass re-attempts the refresh) and policy-authorised (ADR-019 preflight-reconciliation "leave the tree clean" precedent). Mirrors the P121 SIGTERM Rule-6 posture.
|
|
303
|
+
|
|
304
|
+
**Compose-with**: ADR-032 § "Pre-flight subprocess failure handling — non-blocking revert-and-proceed (P358 amendment)" (the architectural record + the iter-vs-pre-flight failure-semantics distinction), Step 5 exit-code semantics (the iter-failure HALT contract this is distinguished from), ADR-019 (preflight-reconciliation clean-tree surface), ADR-009 (no-trust-window-extension — the `git reset` of any staged residue), ADR-013 Rule 6 (non-interactive recovery), P358 (driver ticket). This is a fourth symmetric pre-flight surface alongside the three "Staleness contract drift" clauses (lines for Step 0b/0c/0d) — a future Step 0x pre-flight inherits this failure rule by construction; do NOT re-derive a step-specific copy.
|
|
289
305
|
|
|
290
306
|
### Step 1: Scan the backlog
|
|
291
307
|
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
# tdd-review: structural-permitted (justification: the doc-lint slice below
|
|
3
|
+
# asserts SKILL.md / ADR-032 prose contract — SKILL.md is the contract
|
|
4
|
+
# document per ADR-037 Permitted Exception; these guards catch prose drift
|
|
5
|
+
# away from the behavioural revert-and-proceed contract exercised above. The
|
|
6
|
+
# load-bearing core of this fixture is behavioural per ADR-052. harness-gap P012)
|
|
7
|
+
#
|
|
8
|
+
# Behavioural test: work-problems Step 0 PRE-FLIGHT subprocess failure handling
|
|
9
|
+
# (P358). Step 0b / 0c / 0d dispatch a /wr-itil:review-problems (or
|
|
10
|
+
# check-upstream-responses) pre-flight subprocess "same shape as Step 5". But a
|
|
11
|
+
# pre-flight is a NON-load-bearing cache-refresh dependency, NOT an iter (the
|
|
12
|
+
# loop body). When a pre-flight subprocess exits non-zero OR returns
|
|
13
|
+
# `is_error: true` (e.g. `API Error: The socket connection was closed
|
|
14
|
+
# unexpectedly`), the orchestrator's contract is NON-BLOCKING:
|
|
15
|
+
# (1) revert any dirty (unstaged) partial cache/audit/README write the
|
|
16
|
+
# pre-flight left (`git checkout -- docs/problems/ docs/audits/`);
|
|
17
|
+
# (1b) if a dead pre-flight left STAGED residue, `git reset` then revert
|
|
18
|
+
# (ADR-009 no-trust-window-extension — a dead is_error:true subprocess
|
|
19
|
+
# must not seed the parent's commit);
|
|
20
|
+
# (2) log a one-line annotation;
|
|
21
|
+
# (3) proceed to Step 1 with the existing README.
|
|
22
|
+
# This is ORTHOGONAL to the Step 5 iter SALVAGE-vs-HALT axis (P261 / P214):
|
|
23
|
+
# that axis classifies an *iter's* is_error:true; this fixture classifies the
|
|
24
|
+
# *pre-flight role* — which NEVER salvages and NEVER halts the loop.
|
|
25
|
+
#
|
|
26
|
+
# The fake `claude` shim below re-creates the production shape: it dirties an
|
|
27
|
+
# unstaged cache file in the repo, then emits an is_error:true socket-closed
|
|
28
|
+
# JSON envelope (no commit). The harness re-implements the orchestrator's
|
|
29
|
+
# pre-flight failure contract (faithful to SKILL.md § "Step 0 pre-flight
|
|
30
|
+
# subprocess failure handling (P358)") and asserts the revert-and-proceed
|
|
31
|
+
# outcome across the input shapes. Adopters who copy the SKILL.md pre-flight
|
|
32
|
+
# block into their orchestrator should observe the same outcomes.
|
|
33
|
+
#
|
|
34
|
+
# @problem P358
|
|
35
|
+
# @rfc RFC-024
|
|
36
|
+
# @jtbd JTBD-006
|
|
37
|
+
#
|
|
38
|
+
# Cross-reference:
|
|
39
|
+
# P358 (claude -p subprocess dispatch socket-closed; pre-flight is_error
|
|
40
|
+
# handling gap) — driver ticket
|
|
41
|
+
# RFC-024 (work-problems pre-flight subprocess failure handling) — fix vehicle
|
|
42
|
+
# ADR-032 (governance skill invocation patterns — § "Pre-flight subprocess
|
|
43
|
+
# failure handling — non-blocking revert-and-proceed (P358 amendment)") —
|
|
44
|
+
# the iter-vs-pre-flight failure-semantics distinction this fixture pins
|
|
45
|
+
# ADR-009 (gate-marker / no-trust-window-extension — staged residue git reset)
|
|
46
|
+
# ADR-019 (preflight clean-tree reconciliation surface)
|
|
47
|
+
# P261 / P214 (the iter is_error:true SALVAGE/HALT axis this is orthogonal to)
|
|
48
|
+
# ADR-037 / ADR-052 (skill testing strategy — behavioural default; doc-lint
|
|
49
|
+
# contract assertion is the Permitted Exception, marked above)
|
|
50
|
+
|
|
51
|
+
setup() {
|
|
52
|
+
TEST_TMP="$(mktemp -d)"
|
|
53
|
+
FAKE_BIN="${TEST_TMP}/bin"
|
|
54
|
+
mkdir -p "$FAKE_BIN"
|
|
55
|
+
|
|
56
|
+
# Fake `claude` binary simulating a pre-flight subprocess of the socket-closed
|
|
57
|
+
# class: it leaves a dirty (UNSTAGED by default) partial cache write in the
|
|
58
|
+
# CWD git repo, then emits an is_error:true JSON envelope carrying the
|
|
59
|
+
# socket-closed error string in `.result`. No commit. This matches the
|
|
60
|
+
# 2026-06-10 P358 shape: a /wr-itil:review-problems pre-flight that partially
|
|
61
|
+
# refreshed docs/problems/.upstream-cache.json then died with
|
|
62
|
+
# `API Error: The socket connection was closed unexpectedly`.
|
|
63
|
+
cat > "$FAKE_BIN/claude" <<'FAKE_EOF'
|
|
64
|
+
#!/usr/bin/env bash
|
|
65
|
+
# Test fake for work-problems Step 0 pre-flight failure-handling fixture.
|
|
66
|
+
# Dirties a partial cache write, then emits is_error:true socket-closed JSON.
|
|
67
|
+
mkdir -p docs/problems
|
|
68
|
+
printf 'partial-refresh-mid-die\n' >> docs/problems/.upstream-cache.json
|
|
69
|
+
if [ "${FAKE_STAGE_RESIDUE:-0}" = "1" ]; then
|
|
70
|
+
git add docs/problems/.upstream-cache.json 2>/dev/null || true
|
|
71
|
+
fi
|
|
72
|
+
if [ "${FAKE_EXIT:-0}" != "0" ]; then
|
|
73
|
+
printf '%s\n' '{"is_error":true,"result":"subprocess crashed"}'
|
|
74
|
+
exit "${FAKE_EXIT}"
|
|
75
|
+
fi
|
|
76
|
+
printf '%s\n' '{"is_error":true,"result":"API Error: The socket connection was closed unexpectedly. For more information, pass `verbose: true`","total_cost_usd":0.0,"duration_ms":727000}'
|
|
77
|
+
FAKE_EOF
|
|
78
|
+
chmod +x "$FAKE_BIN/claude"
|
|
79
|
+
export PATH="$FAKE_BIN:$PATH"
|
|
80
|
+
|
|
81
|
+
# A throwaway git repo so dirty-detection + the revert are real. Seed a
|
|
82
|
+
# committed clean cache so the partial write is observably a dirty delta.
|
|
83
|
+
REPO="${TEST_TMP}/repo"
|
|
84
|
+
mkdir -p "$REPO/docs/problems"
|
|
85
|
+
git -C "$REPO" init -q
|
|
86
|
+
git -C "$REPO" config user.email "test@example.com"
|
|
87
|
+
git -C "$REPO" config user.name "Test"
|
|
88
|
+
printf 'clean-cache\n' > "$REPO/docs/problems/.upstream-cache.json"
|
|
89
|
+
git -C "$REPO" add docs/problems/.upstream-cache.json
|
|
90
|
+
git -C "$REPO" commit -q -m "root: clean cache"
|
|
91
|
+
|
|
92
|
+
SKILL_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
93
|
+
SKILL_FILE="${SKILL_DIR}/SKILL.md"
|
|
94
|
+
ADR_FILE="$(cd "${SKILL_DIR}/../../../.." && pwd)/docs/decisions/032-governance-skill-invocation-patterns.proposed.md"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
teardown() {
|
|
98
|
+
if [ -n "${TEST_TMP:-}" ] && [ -d "$TEST_TMP" ]; then
|
|
99
|
+
rm -rf "$TEST_TMP"
|
|
100
|
+
fi
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Faithful re-implementation of SKILL.md § "Step 0 pre-flight subprocess failure
|
|
104
|
+
# handling (P358)". Consumes the pre-flight JSON envelope + the pre-flight exit
|
|
105
|
+
# code + the repo working-tree state, applies the non-blocking revert-and-proceed
|
|
106
|
+
# contract, and returns the orchestrator's decision. KEY PROPERTY: a pre-flight
|
|
107
|
+
# NEVER halts the loop and NEVER salvages — it reverts and proceeds.
|
|
108
|
+
preflight_failure_outcome() {
|
|
109
|
+
local json="$1"
|
|
110
|
+
local exit_code="$2"
|
|
111
|
+
local repo="$3"
|
|
112
|
+
|
|
113
|
+
local is_error
|
|
114
|
+
is_error=$(printf '%s' "$json" | python3 -c 'import json,sys; print(str(json.load(sys.stdin).get("is_error")).lower())' 2>/dev/null || echo unknown)
|
|
115
|
+
|
|
116
|
+
# Pre-flight succeeded (exit 0 AND is_error:false) → cache refreshed; proceed.
|
|
117
|
+
if [ "$exit_code" = "0" ] && [ "$is_error" = "false" ]; then
|
|
118
|
+
printf 'DECISION=PROCEED reason=preflight-ok\n'
|
|
119
|
+
return 0
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# Pre-flight FAILED (non-zero exit OR is_error:true) → NON-BLOCKING contract.
|
|
123
|
+
# Step 1b: if the dead pre-flight left STAGED residue, unstage it first
|
|
124
|
+
# (ADR-009 — a dead is_error:true subprocess must not seed the parent index).
|
|
125
|
+
if [ -n "$(git -C "$repo" diff --cached --name-only)" ]; then
|
|
126
|
+
git -C "$repo" reset -q
|
|
127
|
+
fi
|
|
128
|
+
# Step 1: revert the whole contractually-touchable path set (not just cache).
|
|
129
|
+
# Per-path tolerant — a COMBINED `git checkout -- A B` errors and reverts
|
|
130
|
+
# NOTHING when B is absent (e.g. docs/audits/ on a fresh adopter repo), so
|
|
131
|
+
# revert each path independently.
|
|
132
|
+
git -C "$repo" checkout -- docs/problems/ 2>/dev/null || true
|
|
133
|
+
git -C "$repo" checkout -- docs/audits/ 2>/dev/null || true
|
|
134
|
+
# Step 3: proceed to Step 1 (never halt).
|
|
135
|
+
printf 'DECISION=PROCEED reason=preflight-failed-reverted\n'
|
|
136
|
+
return 0
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Behavioural cases (the load-bearing core per ADR-052).
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
@test "P358: pre-flight is_error:true (socket-closed) + unstaged partial write -> revert + PROCEED (never halt)" {
|
|
144
|
+
export FAKE_STAGE_RESIDUE=0 FAKE_EXIT=0
|
|
145
|
+
local json
|
|
146
|
+
json=$( cd "$REPO" && claude -p --output-format json "PREFLIGHT" < /dev/null )
|
|
147
|
+
# The fake left a dirty partial cache write.
|
|
148
|
+
run git -C "$REPO" status --porcelain docs/problems/.upstream-cache.json
|
|
149
|
+
[ -n "$output" ]
|
|
150
|
+
run preflight_failure_outcome "$json" 0 "$REPO"
|
|
151
|
+
[ "$status" -eq 0 ]
|
|
152
|
+
[[ "$output" == *"DECISION=PROCEED"* ]]
|
|
153
|
+
[[ "$output" == *"preflight-failed-reverted"* ]]
|
|
154
|
+
# The dirty partial write was reverted — tree is clean again.
|
|
155
|
+
run git -C "$REPO" status --porcelain
|
|
156
|
+
[ -z "$output" ]
|
|
157
|
+
# The committed clean cache content survived (revert restored it).
|
|
158
|
+
run cat "$REPO/docs/problems/.upstream-cache.json"
|
|
159
|
+
[[ "$output" == "clean-cache" ]]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@test "P358: pre-flight non-zero exit -> revert + PROCEED (non-blocking; not a loop halt)" {
|
|
163
|
+
export FAKE_STAGE_RESIDUE=0 FAKE_EXIT=1
|
|
164
|
+
local json
|
|
165
|
+
json=$( cd "$REPO" && claude -p --output-format json "PREFLIGHT" < /dev/null || true )
|
|
166
|
+
run preflight_failure_outcome "$json" 1 "$REPO"
|
|
167
|
+
[ "$status" -eq 0 ]
|
|
168
|
+
[[ "$output" == *"DECISION=PROCEED"* ]]
|
|
169
|
+
# Crucially NOT a HALT — a pre-flight failure does not stop the loop.
|
|
170
|
+
[[ "$output" != *"HALT"* ]]
|
|
171
|
+
run git -C "$REPO" status --porcelain
|
|
172
|
+
[ -z "$output" ]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@test "P358: pre-flight left STAGED residue -> git reset then revert (ADR-009 no-seed-parent-index)" {
|
|
176
|
+
export FAKE_STAGE_RESIDUE=1 FAKE_EXIT=0
|
|
177
|
+
local json
|
|
178
|
+
json=$( cd "$REPO" && claude -p --output-format json "PREFLIGHT" < /dev/null )
|
|
179
|
+
# The fake STAGED the partial write.
|
|
180
|
+
run git -C "$REPO" diff --cached --name-only
|
|
181
|
+
[[ "$output" == *".upstream-cache.json"* ]]
|
|
182
|
+
run preflight_failure_outcome "$json" 0 "$REPO"
|
|
183
|
+
[ "$status" -eq 0 ]
|
|
184
|
+
[[ "$output" == *"DECISION=PROCEED"* ]]
|
|
185
|
+
# Staged residue was unstaged AND reverted — index + tree both clean.
|
|
186
|
+
run git -C "$REPO" diff --cached --name-only
|
|
187
|
+
[ -z "$output" ]
|
|
188
|
+
run git -C "$REPO" status --porcelain
|
|
189
|
+
[ -z "$output" ]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@test "P358: pre-flight success (exit 0 + is_error:false) -> PROCEED without revert" {
|
|
193
|
+
# Hand-build a clean success envelope (the fake always fails by design).
|
|
194
|
+
local json='{"is_error":false,"result":"refreshed"}'
|
|
195
|
+
run preflight_failure_outcome "$json" 0 "$REPO"
|
|
196
|
+
[ "$status" -eq 0 ]
|
|
197
|
+
[[ "$output" == *"DECISION=PROCEED"* ]]
|
|
198
|
+
[[ "$output" == *"preflight-ok"* ]]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# Doc-lint contract assertions (Permitted Exception per ADR-037; structural
|
|
203
|
+
# slice marked at top of file per ADR-052 Surface 2). These guard the SKILL.md
|
|
204
|
+
# / ADR-032 prose against drift away from the behavioural contract above.
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
@test "P358: SKILL.md documents the Step 0 pre-flight failure-handling subsection" {
|
|
208
|
+
run grep -niE "pre-flight subprocess failure handling.{0,40}P358|Step 0 pre-flight subprocess failure" "$SKILL_FILE"
|
|
209
|
+
[ "$status" -eq 0 ]
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@test "P358: SKILL.md pre-flight contract names the non-blocking revert-and-proceed rule" {
|
|
213
|
+
run grep -niE "non-?blocking" "$SKILL_FILE"
|
|
214
|
+
[ "$status" -eq 0 ]
|
|
215
|
+
run grep -niE "revert.{0,40}(proceed|partial)|proceed to Step 1 with the existing" "$SKILL_FILE"
|
|
216
|
+
[ "$status" -eq 0 ]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@test "P358: SKILL.md distinguishes pre-flight (cache-refresh dependency) from iter (loop body)" {
|
|
220
|
+
run grep -niE "non-load-bearing cache-refresh dependency" "$SKILL_FILE"
|
|
221
|
+
[ "$status" -eq 0 ]
|
|
222
|
+
run grep -niE "iter IS the loop body|the iter is the loop body" "$SKILL_FILE"
|
|
223
|
+
[ "$status" -eq 0 ]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@test "P358: SKILL.md pre-flight forward-pointer fires in the 0b/0c/0d dispatch paragraphs" {
|
|
227
|
+
# At least three "do NOT halt the loop (a failed pre-flight ...)" pointers.
|
|
228
|
+
run bash -c "grep -ciE 'do NOT halt the loop .a failed pre-flight' '$SKILL_FILE'"
|
|
229
|
+
[ "$status" -eq 0 ]
|
|
230
|
+
[ "$output" -ge 3 ]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@test "P358: SKILL.md pre-flight contract cites P358" {
|
|
234
|
+
run grep -nE "P358" "$SKILL_FILE"
|
|
235
|
+
[ "$status" -eq 0 ]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@test "P358: ADR-032 carries the pre-flight failure-handling P358 amendment" {
|
|
239
|
+
run grep -niE "Pre-flight subprocess failure handling.{0,60}P358 amendment" "$ADR_FILE"
|
|
240
|
+
[ "$status" -eq 0 ]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@test "P358: ADR-032 amendment names the iter-vs-pre-flight failure-semantics axis as orthogonal to SALVAGE/HALT" {
|
|
244
|
+
run grep -niE "orthogonal" "$ADR_FILE"
|
|
245
|
+
[ "$status" -eq 0 ]
|
|
246
|
+
run grep -niE "No SALVAGE for a pre-flight|SALVAGE branch does NOT apply to a pre-flight" "$ADR_FILE"
|
|
247
|
+
[ "$status" -eq 0 ]
|
|
248
|
+
}
|