@windyroad/itil 0.35.9-preview.396 → 0.35.10-preview.401
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windyroad/itil",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.10-preview.401",
|
|
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"
|
|
@@ -471,6 +471,13 @@ Aggregation rule: sum `.total_cost_usd` into the session total and trust it; sum
|
|
|
471
471
|
- Exit 0 → parse `ITERATION_SUMMARY` from `.result` field; proceed to Step 6.
|
|
472
472
|
- Non-zero exit → halt the loop; report the exit code, stderr, and any partial `.result` in the final summary. Do NOT spawn the next iteration. The user returns to a stopped loop with a clear failure reason (e.g. "quota exhausted — resume when quota resets").
|
|
473
473
|
|
|
474
|
+
**`is_error: true` stream-timeout salvage carve-out (P261).** Orthogonal to the process exit code, the `claude -p --output-format json` envelope carries an `is_error` field. An iter that returns `is_error: true` with `API Error: Stream idle timeout - partial response received` in `.result` AFTER staging coherent work but BEFORE `git commit` leaves that work intact in the working tree (the staged files survive; the JSON metadata is preserved — unlike the P147 stuck-before-emit class). This is a NEW recovery branch, not a replacement for the halt rule above. Deterministic SALVAGE-vs-HALT decision contract:
|
|
475
|
+
|
|
476
|
+
- **IF** `is_error: true` AND staged files exist in the working tree (`git diff --cached --name-only` non-empty) AND any iter-authored bats fixtures pass → the orchestrator MAY apply the documented **4-step salvage path**: (1) run the iter's bats as a structural sanity check; (2) inspect the changeset + diffs for quality; (3) commit the staged work from the orchestrator main turn with explicit iter-attribution in the message (e.g. "iter hit API stream timeout before commit — committed staged work from orchestrator main turn"); (4) **the commit gate fires fresh** on the salvage commit, so architect / JTBD / risk-scorer validate the work cleanly on the orchestrator's own SESSION_ID (never reusing the dead subprocess's gate markers, per ADR-009 line 89). The salvage commit IS the iteration's one commit per ADR-014 (amend-folding is inapplicable — no iter commit exists to amend).
|
|
477
|
+
- **ELSE** (staged work incoherent / bats fail / nothing staged) → halt per the existing exit-code contract above.
|
|
478
|
+
|
|
479
|
+
The decision is deterministic and non-interactive — no `AskUserQuestion` (Rule 6, mirroring the P121 SIGTERM precedent at line 154 of ADR-032). **Distinct class** from: P121 (SIGTERM idle-timeout — `is_error: false` clean exit-flush; subprocess HAD committed before going idle), P147 (SIGTERM stuck-before-emit — exit 143 + 0-byte JSON, metadata lost), and P146 (bash-polling antipattern — the deadlock mechanism behind P147). Here the iter exits on its own with `is_error: true`; no SIGTERM involved; metadata AND staged files survive. Full contract: ADR-032 § "is_error:true stream-timeout salvage (P261 amendment)". Behavioural fixture: `test/work-problems-step-5-stream-timeout-salvage.bats`.
|
|
480
|
+
|
|
474
481
|
**Quota as the natural stop.** The AFK loop runs until quota is exhausted or a stop-condition from Step 2 fires. There is no per-iteration dollar cap; running iterations until quota is actually exhausted maximises backlog progress per quota cycle. Quota-exhaust on a `claude -p` invocation surfaces as a non-zero exit and the orchestrator halts cleanly per the rule above.
|
|
475
482
|
|
|
476
483
|
**Hook session-id isolation.** Each `claude -p` subprocess has its own `$CLAUDE_SESSION_ID`. Gate markers at `/tmp/architect-reviewed-<ID>`, `/tmp/jtbd-reviewed-<ID>`, `/tmp/risk-scorer-*-<ID>` are scoped to the subprocess's own hook interactions and never shared with the orchestrator's main-turn SESSION_ID. This is the correct behaviour — the orchestrator's main turn runs its own gate flow if it edits gated paths; the subprocess's gate flow is independent. Implementations MUST NOT wire cross-process marker sharing.
|
|
@@ -483,7 +490,7 @@ The manage-problem skill (running inside the iteration subprocess) will:
|
|
|
483
490
|
- Select and work the highest-WSJF problem.
|
|
484
491
|
- Use its built-in non-interactive fallbacks (auto-split multi-concern problems, auto-commit when risk is within appetite).
|
|
485
492
|
- Delegate architect / JTBD / risk-scorer reviews via the Agent tool (available in the subprocess's surface) at the depth defined in each review skill's SKILL.md.
|
|
486
|
-
- Commit completed work per ADR-014 (the iteration subprocess's commit inside its own session — the orchestrator does NOT commit from its main turn).
|
|
493
|
+
- Commit completed work per ADR-014 (the iteration subprocess's commit inside its own session — the orchestrator does NOT commit from its main turn, EXCEPT the one bounded `is_error: true` stream-timeout salvage carve-out per the Step 5 exit-code semantics above + ADR-032 P261 amendment, where the orchestrator main turn commits an iter's staged-but-uncommitted work after a fresh commit-gate validation).
|
|
487
494
|
|
|
488
495
|
### Step 6: Report progress
|
|
489
496
|
|
|
@@ -0,0 +1,251 @@
|
|
|
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 SALVAGE/HALT 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 5 exit-code semantics — the is_error:true
|
|
9
|
+
# stream-timeout SALVAGE carve-out (P261). When an iter subprocess returns
|
|
10
|
+
# `is_error: true` (e.g. `API Error: Stream idle timeout - partial response
|
|
11
|
+
# received`) AFTER staging coherent work but BEFORE `git commit`, the staged
|
|
12
|
+
# work survives in the working tree. The orchestrator's salvage decision logic
|
|
13
|
+
# is: IF is_error:true AND staged files exist AND iter-authored bats pass →
|
|
14
|
+
# SALVAGE (commit the staged work from the main turn with iter-attribution; the
|
|
15
|
+
# commit gate fires fresh). ELSE → HALT per the existing exit-code contract.
|
|
16
|
+
#
|
|
17
|
+
# This is a NEW recovery branch, distinct from:
|
|
18
|
+
# - P121 (SIGTERM idle-timeout — is_error:false clean exit-flush; subprocess
|
|
19
|
+
# HAD committed before going idle)
|
|
20
|
+
# - P147 (SIGTERM stuck-before-emit — exit 143 + 0-byte JSON; metadata lost)
|
|
21
|
+
# - P146 (bash-polling antipattern — the deadlock mechanism behind P147)
|
|
22
|
+
# The stream-timeout class preserves metadata in the JSON envelope AND the
|
|
23
|
+
# staged files; the iter exits on its own with is_error:true (no SIGTERM).
|
|
24
|
+
#
|
|
25
|
+
# The fake-stuck-shim below re-creates the production shape: it stages coherent
|
|
26
|
+
# work in the repo, then emits an is_error:true stream-timeout JSON envelope
|
|
27
|
+
# (no ITERATION_SUMMARY, no commit). The harness re-implements the orchestrator's
|
|
28
|
+
# SALVAGE-vs-HALT decision contract (faithful to SKILL.md Step 5) and asserts the
|
|
29
|
+
# branch outcome across the four input shapes. Adopters who copy the SKILL.md
|
|
30
|
+
# Step 5 exit-code block into their orchestrator should observe the same outcomes.
|
|
31
|
+
#
|
|
32
|
+
# @problem P261
|
|
33
|
+
# @jtbd JTBD-006
|
|
34
|
+
# @jtbd JTBD-001
|
|
35
|
+
#
|
|
36
|
+
# Cross-reference:
|
|
37
|
+
# P261 (iter subprocess API stream-timeout salvage path) — driver ticket
|
|
38
|
+
# ADR-032 (governance skill invocation patterns — is_error:true stream-timeout
|
|
39
|
+
# salvage sub-variant, P261 amendment) — the carved-out commit-authorship
|
|
40
|
+
# contract this fixture pins
|
|
41
|
+
# ADR-009 (gate-marker-lifecycle — is_error:true subprocess MUST NOT extend
|
|
42
|
+
# parent trust window; salvage commit fires the gate fresh on the
|
|
43
|
+
# orchestrator's own SESSION_ID)
|
|
44
|
+
# ADR-014 (single-commit grain — the salvage commit IS the iteration's one
|
|
45
|
+
# commit; amend-folding is inapplicable because no iter commit exists)
|
|
46
|
+
# ADR-037 / ADR-052 (skill testing strategy — behavioural default; doc-lint
|
|
47
|
+
# contract assertion is the Permitted Exception, marked above)
|
|
48
|
+
|
|
49
|
+
setup() {
|
|
50
|
+
TEST_TMP="$(mktemp -d)"
|
|
51
|
+
FAKE_BIN="${TEST_TMP}/bin"
|
|
52
|
+
mkdir -p "$FAKE_BIN"
|
|
53
|
+
|
|
54
|
+
# Fake `claude` binary simulating a stuck iteration subprocess of the
|
|
55
|
+
# stream-timeout class: it stages coherent work in the CWD git repo, then
|
|
56
|
+
# emits an is_error:true JSON envelope carrying the stream-timeout error
|
|
57
|
+
# string in `.result`. No ITERATION_SUMMARY; no commit. This matches the
|
|
58
|
+
# 2026-05-18 session-6 iter-4 shape captured in P261: 7 files staged, then
|
|
59
|
+
# `API Error: Stream idle timeout - partial response received`.
|
|
60
|
+
cat > "$FAKE_BIN/claude" <<'FAKE_EOF'
|
|
61
|
+
#!/usr/bin/env bash
|
|
62
|
+
# Test fake for work-problems Step 5 P261 stream-timeout salvage fixture.
|
|
63
|
+
# Stages coherent work, then emits is_error:true stream-timeout JSON.
|
|
64
|
+
if [ "${FAKE_STAGE_WORK:-1}" = "1" ]; then
|
|
65
|
+
printf 'salvaged SKILL amendment + bats fixture\n' > staged-iter-work.txt
|
|
66
|
+
git add staged-iter-work.txt 2>/dev/null || true
|
|
67
|
+
fi
|
|
68
|
+
if [ "${FAKE_IS_ERROR:-true}" = "true" ]; then
|
|
69
|
+
printf '%s\n' '{"is_error":true,"result":"API Error: Stream idle timeout - partial response received","total_cost_usd":12.91,"duration_ms":300000,"usage":{"input_tokens":1000,"output_tokens":2000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}'
|
|
70
|
+
else
|
|
71
|
+
printf '%s\n' '{"is_error":false,"result":"ITERATION_SUMMARY\nticket_id: P000\naction: worked\noutcome: investigated\ncommitted: true\nremaining_backlog_count: 0\nnotes: normal exit","total_cost_usd":0.5,"duration_ms":1000,"usage":{"input_tokens":10,"output_tokens":20,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}'
|
|
72
|
+
fi
|
|
73
|
+
FAKE_EOF
|
|
74
|
+
chmod +x "$FAKE_BIN/claude"
|
|
75
|
+
|
|
76
|
+
# Fake `iter_bats` stub — stands in for running the iter-authored bats
|
|
77
|
+
# fixtures as the salvage path's step-1 structural sanity check. Its exit
|
|
78
|
+
# code is controlled by FAKE_BATS_EXIT (0 = green, 1 = fail) so the harness
|
|
79
|
+
# can exercise both the bats-green and bats-fail branches behaviourally.
|
|
80
|
+
cat > "$FAKE_BIN/iter_bats" <<'FAKE_EOF'
|
|
81
|
+
#!/usr/bin/env bash
|
|
82
|
+
exit "${FAKE_BATS_EXIT:-0}"
|
|
83
|
+
FAKE_EOF
|
|
84
|
+
chmod +x "$FAKE_BIN/iter_bats"
|
|
85
|
+
export PATH="$FAKE_BIN:$PATH"
|
|
86
|
+
|
|
87
|
+
# A throwaway git repo so staged-work detection + the salvage commit are real.
|
|
88
|
+
REPO="${TEST_TMP}/repo"
|
|
89
|
+
mkdir -p "$REPO"
|
|
90
|
+
git -C "$REPO" init -q
|
|
91
|
+
git -C "$REPO" config user.email "test@example.com"
|
|
92
|
+
git -C "$REPO" config user.name "Test"
|
|
93
|
+
git -C "$REPO" commit -q --allow-empty -m "root"
|
|
94
|
+
|
|
95
|
+
SKILL_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
96
|
+
SKILL_FILE="${SKILL_DIR}/SKILL.md"
|
|
97
|
+
ADR_FILE="$(cd "${SKILL_DIR}/../../../.." && pwd)/docs/decisions/032-governance-skill-invocation-patterns.proposed.md"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
teardown() {
|
|
101
|
+
if [ -n "${TEST_TMP:-}" ] && [ -d "$TEST_TMP" ]; then
|
|
102
|
+
rm -rf "$TEST_TMP"
|
|
103
|
+
fi
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Faithful re-implementation of SKILL.md Step 5's is_error:true salvage decision
|
|
107
|
+
# contract. Consumes the iter JSON envelope + the repo working-tree state and
|
|
108
|
+
# returns the orchestrator's branch decision. The SALVAGE branch performs the
|
|
109
|
+
# real 4-step path's commit (step 3) so the commit is observable; the bats
|
|
110
|
+
# sanity check (step 1) is exercised via the FAKE_BATS_EXIT-controlled stub.
|
|
111
|
+
salvage_decision() {
|
|
112
|
+
local json="$1"
|
|
113
|
+
local repo="$2"
|
|
114
|
+
|
|
115
|
+
local is_error
|
|
116
|
+
is_error=$(printf '%s' "$json" | python3 -c 'import json,sys; print(str(json.load(sys.stdin).get("is_error")).lower())')
|
|
117
|
+
|
|
118
|
+
# is_error:false → normal exit-code path; not the salvage branch.
|
|
119
|
+
if [ "$is_error" != "true" ]; then
|
|
120
|
+
printf 'DECISION=PARSE_SUMMARY\n'
|
|
121
|
+
return 0
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# is_error:true with no staged work → halt per the existing exit-code contract.
|
|
125
|
+
local staged
|
|
126
|
+
staged=$(git -C "$repo" diff --cached --name-only)
|
|
127
|
+
if [ -z "$staged" ]; then
|
|
128
|
+
printf 'DECISION=HALT reason=no-staged-work\n'
|
|
129
|
+
return 0
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# is_error:true with staged work → run the iter-authored bats as the
|
|
133
|
+
# structural sanity check (step 1). Green → SALVAGE; fail → HALT.
|
|
134
|
+
if iter_bats; then
|
|
135
|
+
# Step 3: commit the staged work from the orchestrator main turn with
|
|
136
|
+
# explicit iter-attribution. (Step 4 — fresh commit gate — is the runtime
|
|
137
|
+
# orchestrator's concern, asserted via the doc-lint slice below.)
|
|
138
|
+
git -C "$repo" commit -q -m "salvage(P000): iter hit API stream timeout before commit — committed staged work from orchestrator main turn; iter-authored bats green"
|
|
139
|
+
printf 'DECISION=SALVAGE\n'
|
|
140
|
+
else
|
|
141
|
+
printf 'DECISION=HALT reason=bats-fail\n'
|
|
142
|
+
fi
|
|
143
|
+
return 0
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# Behavioural cases (the load-bearing core per ADR-052).
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
@test "P261: is_error:true + staged work + bats green -> SALVAGE (commit from main turn)" {
|
|
151
|
+
export FAKE_IS_ERROR=true FAKE_STAGE_WORK=1 FAKE_BATS_EXIT=0
|
|
152
|
+
local json
|
|
153
|
+
json=$( cd "$REPO" && claude -p --output-format json "TEST" < /dev/null )
|
|
154
|
+
run salvage_decision "$json" "$REPO"
|
|
155
|
+
[ "$status" -eq 0 ]
|
|
156
|
+
[[ "$output" == *"DECISION=SALVAGE"* ]]
|
|
157
|
+
# The salvage commit must have landed, carrying the staged work + attribution.
|
|
158
|
+
run git -C "$REPO" log -1 --format=%s
|
|
159
|
+
[[ "$output" == *"salvage"* ]]
|
|
160
|
+
[[ "$output" == *"orchestrator main turn"* ]]
|
|
161
|
+
run git -C "$REPO" show --stat HEAD
|
|
162
|
+
[[ "$output" == *"staged-iter-work.txt"* ]]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@test "P261: is_error:true + staged work + bats FAIL -> HALT (incoherent work not salvaged)" {
|
|
166
|
+
export FAKE_IS_ERROR=true FAKE_STAGE_WORK=1 FAKE_BATS_EXIT=1
|
|
167
|
+
local json
|
|
168
|
+
json=$( cd "$REPO" && claude -p --output-format json "TEST" < /dev/null )
|
|
169
|
+
run salvage_decision "$json" "$REPO"
|
|
170
|
+
[ "$status" -eq 0 ]
|
|
171
|
+
[[ "$output" == *"DECISION=HALT"* ]]
|
|
172
|
+
[[ "$output" == *"reason=bats-fail"* ]]
|
|
173
|
+
# No salvage commit landed — HEAD is still the root commit.
|
|
174
|
+
run git -C "$REPO" log --oneline
|
|
175
|
+
[ "$(printf '%s\n' "$output" | grep -c .)" -eq 1 ]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@test "P261: is_error:true + NO staged work -> HALT per existing exit-code contract" {
|
|
179
|
+
export FAKE_IS_ERROR=true FAKE_STAGE_WORK=0 FAKE_BATS_EXIT=0
|
|
180
|
+
local json
|
|
181
|
+
json=$( cd "$REPO" && claude -p --output-format json "TEST" < /dev/null )
|
|
182
|
+
run salvage_decision "$json" "$REPO"
|
|
183
|
+
[ "$status" -eq 0 ]
|
|
184
|
+
[[ "$output" == *"DECISION=HALT"* ]]
|
|
185
|
+
[[ "$output" == *"reason=no-staged-work"* ]]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@test "P261: is_error:false (normal exit) -> PARSE_SUMMARY, NOT the salvage branch" {
|
|
189
|
+
export FAKE_IS_ERROR=false FAKE_STAGE_WORK=0
|
|
190
|
+
local json
|
|
191
|
+
json=$( cd "$REPO" && claude -p --output-format json "TEST" < /dev/null )
|
|
192
|
+
run salvage_decision "$json" "$REPO"
|
|
193
|
+
[ "$status" -eq 0 ]
|
|
194
|
+
[[ "$output" == *"DECISION=PARSE_SUMMARY"* ]]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
# Doc-lint contract assertions (Permitted Exception per ADR-037; structural
|
|
199
|
+
# slice marked at top of file per ADR-052 Surface 2). These guard the SKILL.md
|
|
200
|
+
# / ADR-032 prose against drift away from the behavioural contract above.
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
@test "P261: SKILL.md Step 5 exit-code semantics documents the is_error:true salvage carve-out" {
|
|
204
|
+
run grep -niE "is_error.{0,30}salvage|salvage.{0,40}is_error|stream.?idle.?timeout.{0,80}salvage|salvage.{0,80}stream.?(idle.?)?timeout" "$SKILL_FILE"
|
|
205
|
+
[ "$status" -eq 0 ]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@test "P261: SKILL.md salvage carve-out names the staged-work + bats-pass gate condition" {
|
|
209
|
+
run grep -niE "staged.{0,40}(file|work).{0,80}bats|bats.{0,80}(pass|green).{0,80}salvage|salvage.{0,120}staged" "$SKILL_FILE"
|
|
210
|
+
[ "$status" -eq 0 ]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@test "P261: SKILL.md salvage carve-out documents the commit-from-main-turn + fresh-gate steps" {
|
|
214
|
+
run grep -niE "(orchestrator|main turn).{0,80}commit.{0,120}(attribut|fresh)|commit gate fires fresh|fresh.{0,30}commit gate" "$SKILL_FILE"
|
|
215
|
+
[ "$status" -eq 0 ]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@test "P261: SKILL.md salvage carve-out distinguishes the class from P147 / P121 / P146" {
|
|
219
|
+
run grep -niE "P147" "$SKILL_FILE"
|
|
220
|
+
[ "$status" -eq 0 ]
|
|
221
|
+
run grep -niE "P146" "$SKILL_FILE"
|
|
222
|
+
[ "$status" -eq 0 ]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@test "P261: SKILL.md Step 5 cites P261 (salvage-carve-out driver)" {
|
|
226
|
+
run grep -nE "P261" "$SKILL_FILE"
|
|
227
|
+
[ "$status" -eq 0 ]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@test "P261: SKILL.md line ~486 orchestrator-commit rule carries the salvage exception cross-reference" {
|
|
231
|
+
# The 'orchestrator does NOT commit from its main turn' rule must no longer
|
|
232
|
+
# read as unqualified — it must name the salvage carve-out as the one
|
|
233
|
+
# bounded exception so SKILL.md and ADR-032 stay in agreement.
|
|
234
|
+
run grep -niE "orchestrator does NOT commit from its main turn.{0,200}(except|salvage)|salvage.{0,80}(except|one case).{0,120}main turn" "$SKILL_FILE"
|
|
235
|
+
[ "$status" -eq 0 ]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@test "P261: ADR-032 carries the is_error:true stream-timeout salvage sub-variant amendment" {
|
|
239
|
+
run grep -niE "is_error.{0,30}(stream.?timeout)?.{0,30}salvage|stream.?timeout salvage|P261 amendment" "$ADR_FILE"
|
|
240
|
+
[ "$status" -eq 0 ]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@test "P261: ADR-032 salvage amendment preserves one-commit-per-iteration grain (amend-folding inapplicable)" {
|
|
244
|
+
run grep -niE "amend.?(based)?.?folding.{0,80}(inapplicable|no iter commit|not apply)|salvage commit IS the iteration|no iter commit.{0,40}amend" "$ADR_FILE"
|
|
245
|
+
[ "$status" -eq 0 ]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@test "P261: ADR-032 salvage amendment confirms fresh-gate-marker behaviour per ADR-009" {
|
|
249
|
+
run grep -niE "ADR-009|fresh.{0,30}(gate|commit gate)|own SESSION_ID|trust window" "$ADR_FILE"
|
|
250
|
+
[ "$status" -eq 0 ]
|
|
251
|
+
}
|