@windyroad/itil 0.19.4 → 0.19.5
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
|
@@ -162,7 +162,7 @@ If a problem is skipped by this step, add it to a "skipped" list with the reason
|
|
|
162
162
|
- **Agent-tool dispatch to a `general-purpose` subagent** (the P077 amendment) works for context isolation but fails at the governance-gate layer: subagents spawned via the Agent tool do NOT have the Agent tool in their own surface (three-source evidence — ToolSearch probe, Claude Code docs at `code.claude.com/docs/en/subagents.md`, empirical runtime error `"No such tool available: Agent. Agent is not available inside subagents."`). Without Agent, the iteration worker cannot set architect + JTBD PreToolUse edit-gate markers (only settable via Agent-tool PostToolUse hook), cannot satisfy the risk-scorer commit gate, and silently halts on every gate-covered iteration. P084 diagnoses and closes this gap.
|
|
163
163
|
- **`claude -p` subprocess dispatch** (this step, per P084 / ADR-032 amendment): the subprocess is a full main Claude Code session with Agent available in its own surface. Governance review runs at full depth via the normal `wr-architect:agent` / `wr-jtbd:agent` / `wr-risk-scorer:pipeline` delegation path inside the subprocess; PostToolUse marker hooks fire correctly matching the subprocess's own `$CLAUDE_SESSION_ID`; the commit gate unlocks natively. Context isolation preserved by the process boundary (each subprocess is a distinct process with its own session state; orchestrator's main context only sees the stdout). This is the AFK iteration-isolation wrapper — subprocess-boundary variant under ADR-032.
|
|
164
164
|
|
|
165
|
-
**Dispatch command shape (Bash):**
|
|
165
|
+
**Dispatch command shape (Bash, backgrounded with idle-timeout poll loop per P121):**
|
|
166
166
|
|
|
167
167
|
```bash
|
|
168
168
|
ITERATION_PROMPT=$(cat <<'PROMPT_EOF'
|
|
@@ -170,11 +170,44 @@ ITERATION_PROMPT=$(cat <<'PROMPT_EOF'
|
|
|
170
170
|
PROMPT_EOF
|
|
171
171
|
)
|
|
172
172
|
|
|
173
|
+
ITER_JSON=$(mktemp)
|
|
174
|
+
DISPATCH_START_EPOCH=$(date +%s)
|
|
175
|
+
IDLE_TIMEOUT_S="${WORK_PROBLEMS_IDLE_TIMEOUT_S:-3600}"
|
|
176
|
+
|
|
173
177
|
claude -p \
|
|
174
178
|
--permission-mode bypassPermissions \
|
|
175
179
|
--output-format json \
|
|
176
180
|
"$ITERATION_PROMPT" \
|
|
177
|
-
< /dev/null
|
|
181
|
+
< /dev/null \
|
|
182
|
+
> "$ITER_JSON" 2>&1 &
|
|
183
|
+
ITER_PID=$!
|
|
184
|
+
|
|
185
|
+
SIGTERM_SENT=0
|
|
186
|
+
while kill -0 "$ITER_PID" 2>/dev/null; do
|
|
187
|
+
sleep 60
|
|
188
|
+
NOW=$(date +%s)
|
|
189
|
+
LAST_COMMIT_EPOCH=$(git log -1 --format=%at HEAD 2>/dev/null || echo "$DISPATCH_START_EPOCH")
|
|
190
|
+
# LAST_ACTIVITY_MARK = max(DISPATCH_START_EPOCH, last commit timestamp).
|
|
191
|
+
# The dispatch-start floor handles skip-iterations that produce no commit:
|
|
192
|
+
# they are bounded by IDLE_TIMEOUT_S since dispatch start, not by an
|
|
193
|
+
# arbitrarily-stale repo commit. See trade-off paragraph below.
|
|
194
|
+
if (( LAST_COMMIT_EPOCH > DISPATCH_START_EPOCH )); then
|
|
195
|
+
LAST_ACTIVITY_MARK=$LAST_COMMIT_EPOCH
|
|
196
|
+
else
|
|
197
|
+
LAST_ACTIVITY_MARK=$DISPATCH_START_EPOCH
|
|
198
|
+
fi
|
|
199
|
+
IDLE_SECONDS=$(( NOW - LAST_ACTIVITY_MARK ))
|
|
200
|
+
if (( IDLE_SECONDS > IDLE_TIMEOUT_S )) && (( SIGTERM_SENT == 0 )); then
|
|
201
|
+
kill -TERM "$ITER_PID" 2>/dev/null || true
|
|
202
|
+
SIGTERM_SENT=1
|
|
203
|
+
echo "[work-problems] iter idle ${IDLE_SECONDS}s > ${IDLE_TIMEOUT_S}s threshold — SIGTERM sent to PID $ITER_PID" >&2
|
|
204
|
+
fi
|
|
205
|
+
done
|
|
206
|
+
|
|
207
|
+
wait "$ITER_PID" 2>/dev/null
|
|
208
|
+
ITER_EXIT=$?
|
|
209
|
+
SUBPROCESS_OUTPUT=$(<"$ITER_JSON")
|
|
210
|
+
rm -f "$ITER_JSON"
|
|
178
211
|
```
|
|
179
212
|
|
|
180
213
|
**Flag rationale:**
|
|
@@ -185,6 +218,10 @@ claude -p \
|
|
|
185
218
|
|
|
186
219
|
**No per-iteration budget cap.** The dispatch deliberately omits `--max-budget-usd`. Per user direction 2026-04-21: the natural stop condition for an AFK loop is quota exhaustion, not an arbitrary per-iteration dollar cap. A cap would halt iterations before quota is actually exhausted, wasting remaining budget. Runaway-iteration risk is bounded by quota + the orchestrator's Step 6.75 halt on unexpected dirty state + exit-code handling below.
|
|
187
220
|
|
|
221
|
+
**Idle-timeout SIGTERM (P121).** The poll loop above is the orchestrator-side guard against stuck iteration subprocesses — iters that complete their semantic work (commits land, retro runs, `ITERATION_SUMMARY` is emitted into the agent output stream) but then sit waiting on a hook timeout, a backgrounded subagent that never resolved, or some other CLI-level idle behaviour before exiting. Without the guard the orchestrator polls indefinitely; the JSON file stays 0 bytes (the CLI only flushes on exit) and wall-clock burns for ~$8/hour of subprocess overhead with no API turns. The 2026-04-25 P118 iter 5 evidence: 121 min wall-clock; final commit at ~100 min; manual SIGTERM at 121 min produced a clean 5649-byte JSON response with `is_error: false`, full `## Session Retrospective` section, parseable `ITERATION_SUMMARY` block, and `duration_ms: 2992935` (49.9 min — the real-work portion). SIGTERM is therefore a safe recovery primitive for this stuck-state class — empirically a clean exit-flush, not a destructive interrupt. Behavioural confirmation lives in `test/work-problems-step-5-idle-timeout-sigterm.bats` (P121 ships with this fixture as the second-source the production observation needed). The default `IDLE_TIMEOUT_S=3600` (60 min) leaves headroom for genuinely long architectural iters; the `WORK_PROBLEMS_IDLE_TIMEOUT_S` env-var overrides per-environment for adopters who run very long iters or want a tighter guard. The orchestrator's Step 6 progress line SHOULD annotate `(SIGTERM_SENT)` when the branch fires so the user can distinguish a SIGTERM-recovered iter from a normal completion (per JTBD-006 audit-trail expectation).
|
|
222
|
+
|
|
223
|
+
**LAST_ACTIVITY_MARK signal trade-off.** The mark is `max(DISPATCH_START_EPOCH, last commit timestamp)`. The dispatch-start floor is intentional: skip-iterations that produce no commit (Step 4 routes a ticket to `action: skipped`) are bounded by `IDLE_TIMEOUT_S` since dispatch start, not by an arbitrarily-stale prior-commit timestamp. This protects against false-positive SIGTERM at iter T=0 when the most recent commit happens to be hours old. The trade-off is the inverse: a skip-iter that runs for `IDLE_TIMEOUT_S` (60 min default) will SIGTERM even though it never had a chance to commit. The 60-min default is well past the typical skip-iter wall-clock (a normal skip completes in seconds), so the trade-off rarely fires in practice; adopters who run unusually long skip-evaluation iters (e.g. deep architect-design probes) should raise `WORK_PROBLEMS_IDLE_TIMEOUT_S` accordingly. Alternative signals considered and rejected: `stat -f%m "$ITER_JSON"` (binary — file mtime only changes on subprocess exit, useless during the idle gap); subprocess RSS-change tracking (noisy; spikes during Agent-tool expansions confound the signal). The git-log signal is the cheapest reliable progress indicator the orchestrator already has.
|
|
224
|
+
|
|
188
225
|
**Iteration prompt body (self-contained — the subprocess has no prior conversation context):**
|
|
189
226
|
|
|
190
227
|
1. **Context**: this is one iteration of the AFK work-problems loop. The user is AFK. The orchestrator selected `P<NNN> (<title>)` as the highest-WSJF actionable ticket.
|
|
@@ -460,6 +497,7 @@ When every skipped ticket is in the `upstream-blocked` category (stop-condition
|
|
|
460
497
|
|
|
461
498
|
## Related
|
|
462
499
|
|
|
500
|
+
- **P121** (`docs/problems/121-afk-orchestrator-should-sigterm-stuck-subprocesses-after-idle-timeout.verifying.md`) — driver for Step 5's backgrounded-poll-loop dispatch shape (replacing the prior foreground-synchronous form) and the idle-timeout SIGTERM branch. The 2026-04-25 P118 iter 5 evidence: an iteration subprocess sat idle ~70 min after its final commit, then SIGTERM produced a clean JSON exit-flush. Fix: orchestrator backgrounds the subprocess, polls every 60s, computes `LAST_ACTIVITY_MARK = max(DISPATCH_START_EPOCH, git log -1 --format=%at HEAD)`, and sends SIGTERM when `now - LAST_ACTIVITY_MARK > WORK_PROBLEMS_IDLE_TIMEOUT_S` (default 3600s = 60 min). Behavioural second-source: `test/work-problems-step-5-idle-timeout-sigterm.bats` exercises a fake `claude -p` shim that sleeps past the threshold and asserts SIGTERM, JSON exit-flush, env-var override, and within-threshold no-fire. Step 6's per-iter progress line SHOULD annotate `(SIGTERM_SENT)` when the branch fires so users can distinguish recovered iters from natural completions. ADR-032's subprocess-boundary variant amended 2026-04-26 with the backgrounded-poll-loop refinement.
|
|
463
501
|
- **P089** (`docs/problems/089-work-problems-step-5-dispatch-robustness-stdin-warning-and-cost-metadata-edge-case.verifying.md`) — driver for Step 5's `< /dev/null` dispatch redirect and the Per-iteration cost metadata "Authority hierarchy" paragraph. Gap 1: stdin warning contaminated stderr-merged JSON captures; closed by adding `< /dev/null` to the canonical dispatch command. Gap 2: `.usage.*` undercounts when subprocess exits via a background-task completion ack while `.total_cost_usd` stays cumulative-authoritative; closed by documenting the authority hierarchy in Step 5 and the Session Cost output section so adopters trust cost and label token totals best-effort.
|
|
464
502
|
- **P086** (`docs/problems/086-afk-iteration-subprocess-does-not-run-retro-before-returning.verifying.md`) — driver for Step 5's retro-on-exit clause. Iteration subprocesses exit without running retro, so per-iteration friction (hook misbehaviour, repeat-workaround patterns, pipeline instability) evaporates on exit. Fix: iteration prompt body names `/wr-retrospective:run-retro` as a closing step before `ITERATION_SUMMARY` emission; retro runs inside the subprocess so Step 2b pipeline-instability scan has the full tool-call history; run-retro commits its own work per ADR-014; orchestrator picks up retro-created tickets on the next Step 1 scan.
|
|
465
503
|
- **P084** (`docs/problems/084-work-problems-iteration-worker-has-no-agent-tool-so-architect-jtbd-gates-block.open.md`) — driver for Step 5's subprocess-boundary dispatch. Supersedes P077's Agent-tool dispatch on the same Step 5 surface because Agent-tool-spawned subagents cannot themselves invoke Agent (platform restriction), which prevents governance gate markers from being set inside the iteration worker.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
# Behavioural test: work-problems Step 5 backgrounded-poll-loop dispatch fires
|
|
3
|
+
# SIGTERM on the iteration subprocess when LAST_ACTIVITY_MARK has been stale
|
|
4
|
+
# longer than WORK_PROBLEMS_IDLE_TIMEOUT_S (default 3600s = 60 min). The SIGTERM
|
|
5
|
+
# empirically produces a clean JSON exit-flush per the 2026-04-25 P118 iter 5
|
|
6
|
+
# evidence captured in P121 (and in docs/briefing/afk-subprocess.md).
|
|
7
|
+
#
|
|
8
|
+
# This is the second-source the architect addendum required: the SIGTERM-flushes-
|
|
9
|
+
# JSON evidence is otherwise single-source from one production observation. The
|
|
10
|
+
# fake-claude shim here re-creates the stuck-subprocess shape (emit JSON to stdout,
|
|
11
|
+
# then sleep past the threshold while remaining killable by SIGTERM) and asserts
|
|
12
|
+
# the orchestrator-shape harness's poll-and-sigterm behaviour matches the
|
|
13
|
+
# contract documented in SKILL.md Step 5.
|
|
14
|
+
#
|
|
15
|
+
# @problem P121
|
|
16
|
+
# @jtbd JTBD-006
|
|
17
|
+
# @jtbd JTBD-001
|
|
18
|
+
#
|
|
19
|
+
# Cross-reference:
|
|
20
|
+
# P121 (orchestrator should SIGTERM stuck claude -p subprocesses after idle-
|
|
21
|
+
# timeout — and SIGTERM appears to flush a clean JSON) — driver ticket
|
|
22
|
+
# ADR-032 (governance skill invocation patterns — subprocess-boundary variant
|
|
23
|
+
# amended 2026-04-26 with the backgrounded-poll-loop refinement)
|
|
24
|
+
# ADR-037 (skill testing strategy — behavioural is the default; doc-lint
|
|
25
|
+
# contract assertions are the Permitted Exception)
|
|
26
|
+
# docs/briefing/afk-subprocess.md (P121 entry — the cross-session knowledge
|
|
27
|
+
# index entry this fixture provides empirical second-source for)
|
|
28
|
+
|
|
29
|
+
setup() {
|
|
30
|
+
TEST_TMP="$(mktemp -d)"
|
|
31
|
+
FAKE_BIN="${TEST_TMP}/bin"
|
|
32
|
+
mkdir -p "$FAKE_BIN"
|
|
33
|
+
|
|
34
|
+
# Fake `claude` binary that simulates a stuck iteration subprocess: emits a
|
|
35
|
+
# valid `claude -p --output-format json` envelope to stdout, then sleeps for
|
|
36
|
+
# FAKE_SLEEP_AFTER seconds (default 30s) while trapping SIGTERM. This matches
|
|
37
|
+
# the 2026-04-25 P118 iter 5 shape: subprocess completes its semantic work,
|
|
38
|
+
# then sits in an idle-wait state until SIGTERM unblocks it. The trap exits 0
|
|
39
|
+
# with the JSON already flushed to stdout — same observable as the production
|
|
40
|
+
# CLI behaviour that motivated P121.
|
|
41
|
+
cat > "$FAKE_BIN/claude" <<'FAKE_EOF'
|
|
42
|
+
#!/usr/bin/env bash
|
|
43
|
+
# Test fake for work-problems Step 5 idle-timeout SIGTERM bats fixture.
|
|
44
|
+
# Emits a JSON envelope then sleeps; SIGTERM exits cleanly (JSON already flushed).
|
|
45
|
+
trap 'exit 0' TERM
|
|
46
|
+
printf '%s\n' '{"is_error":false,"result":"ITERATION_SUMMARY\nticket_id: P000\nticket_title: fake\naction: worked\noutcome: investigated\ncommitted: false\nreason: test fixture\nremaining_backlog_count: 0\nnotes: stuck-subprocess simulation","total_cost_usd":0.01,"duration_ms":100,"usage":{"input_tokens":10,"output_tokens":20,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}'
|
|
47
|
+
sleep "${FAKE_SLEEP_AFTER:-30}"
|
|
48
|
+
FAKE_EOF
|
|
49
|
+
chmod +x "$FAKE_BIN/claude"
|
|
50
|
+
export PATH="$FAKE_BIN:$PATH"
|
|
51
|
+
|
|
52
|
+
SKILL_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
53
|
+
SKILL_FILE="${SKILL_DIR}/SKILL.md"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
teardown() {
|
|
57
|
+
if [ -n "${TEST_TMP:-}" ] && [ -d "$TEST_TMP" ]; then
|
|
58
|
+
rm -rf "$TEST_TMP"
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Faithful re-implementation of SKILL.md Step 5's backgrounded-poll-loop dispatch.
|
|
63
|
+
# Adopters who copy-paste the SKILL.md Step 5 block into their orchestrator
|
|
64
|
+
# should observe the same outcomes this harness does — that's the contract this
|
|
65
|
+
# fixture pins. The harness uses sleep 1 instead of the SKILL.md's sleep 60 so
|
|
66
|
+
# the test wall-clock stays bounded; the LAST_ACTIVITY_MARK math, SIGTERM action,
|
|
67
|
+
# and JSON-after-SIGTERM read are otherwise identical.
|
|
68
|
+
dispatch_with_poll() {
|
|
69
|
+
local json_file="${TEST_TMP}/iter.json"
|
|
70
|
+
local idle_timeout_s="${WORK_PROBLEMS_IDLE_TIMEOUT_S:-3600}"
|
|
71
|
+
local dispatch_start_epoch
|
|
72
|
+
dispatch_start_epoch=$(date +%s)
|
|
73
|
+
local sigterm_sent=0
|
|
74
|
+
|
|
75
|
+
: > "$json_file"
|
|
76
|
+
claude -p --permission-mode bypassPermissions --output-format json "TEST" \
|
|
77
|
+
< /dev/null > "$json_file" 2>&1 &
|
|
78
|
+
local iter_pid=$!
|
|
79
|
+
|
|
80
|
+
while kill -0 "$iter_pid" 2>/dev/null; do
|
|
81
|
+
sleep 1
|
|
82
|
+
local now
|
|
83
|
+
now=$(date +%s)
|
|
84
|
+
# LAST_ACTIVITY_MARK = max(DISPATCH_START, last commit timestamp).
|
|
85
|
+
# In this test there is no git repo and no commits, so the max is just
|
|
86
|
+
# DISPATCH_START — same shape as a real skip-iteration that produces no
|
|
87
|
+
# commit during its run.
|
|
88
|
+
local last_activity_mark=$dispatch_start_epoch
|
|
89
|
+
local idle_seconds=$(( now - last_activity_mark ))
|
|
90
|
+
if (( idle_seconds > idle_timeout_s )) && (( sigterm_sent == 0 )); then
|
|
91
|
+
kill -TERM "$iter_pid" 2>/dev/null || true
|
|
92
|
+
sigterm_sent=1
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
|
|
96
|
+
wait "$iter_pid" 2>/dev/null || true
|
|
97
|
+
|
|
98
|
+
printf 'SIGTERM_SENT=%d\n' "$sigterm_sent"
|
|
99
|
+
printf '%s\n' '---JSON---'
|
|
100
|
+
cat "$json_file"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# (a) SIGTERM was sent within the threshold.
|
|
104
|
+
@test "P121: SIGTERM fires when subprocess idle exceeds WORK_PROBLEMS_IDLE_TIMEOUT_S" {
|
|
105
|
+
export FAKE_SLEEP_AFTER=10
|
|
106
|
+
export WORK_PROBLEMS_IDLE_TIMEOUT_S=2
|
|
107
|
+
run dispatch_with_poll
|
|
108
|
+
[ "$status" -eq 0 ]
|
|
109
|
+
[[ "$output" == *"SIGTERM_SENT=1"* ]]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# (b) JSON arrives after SIGTERM (clean exit-flush).
|
|
113
|
+
@test "P121: JSON arrives after SIGTERM and parses cleanly (clean exit-flush per 2026-04-25 P118 iter 5)" {
|
|
114
|
+
export FAKE_SLEEP_AFTER=10
|
|
115
|
+
export WORK_PROBLEMS_IDLE_TIMEOUT_S=2
|
|
116
|
+
run dispatch_with_poll
|
|
117
|
+
[ "$status" -eq 0 ]
|
|
118
|
+
[[ "$output" == *"SIGTERM_SENT=1"* ]]
|
|
119
|
+
# Extract the JSON portion after the ---JSON--- marker and validate.
|
|
120
|
+
json_payload=$(printf '%s\n' "$output" | sed -n '/^---JSON---$/,$p' | tail -n +2)
|
|
121
|
+
printf '%s' "$json_payload" | python3 -c '
|
|
122
|
+
import json, sys
|
|
123
|
+
j = json.loads(sys.stdin.read().strip())
|
|
124
|
+
assert not j.get("is_error"), "is_error should be false"
|
|
125
|
+
assert "ITERATION_SUMMARY" in j["result"], "result must carry ITERATION_SUMMARY"
|
|
126
|
+
assert "total_cost_usd" in j, "cost metadata must survive SIGTERM exit-flush"
|
|
127
|
+
'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# (c) Env-var override is honoured (default 3600s; override to 2s).
|
|
131
|
+
@test "P121: WORK_PROBLEMS_IDLE_TIMEOUT_S env-var override is honoured" {
|
|
132
|
+
# Without an override, the default 3600s would never fire in test wall-clock,
|
|
133
|
+
# so no SIGTERM. With WORK_PROBLEMS_IDLE_TIMEOUT_S=2, SIGTERM fires within
|
|
134
|
+
# seconds. Confirms the override is consulted by the harness, matching the
|
|
135
|
+
# SKILL.md contract that adopters can tune the threshold per-environment.
|
|
136
|
+
export FAKE_SLEEP_AFTER=10
|
|
137
|
+
export WORK_PROBLEMS_IDLE_TIMEOUT_S=2
|
|
138
|
+
run dispatch_with_poll
|
|
139
|
+
[ "$status" -eq 0 ]
|
|
140
|
+
[[ "$output" == *"SIGTERM_SENT=1"* ]]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# (d) Within-threshold runs are NOT SIGTERMed (negative case).
|
|
144
|
+
@test "P121: within-threshold runs are NOT SIGTERMed (subprocess exits before idle threshold)" {
|
|
145
|
+
# Subprocess exits naturally in 1 second; idle timeout is 60s. Loop must
|
|
146
|
+
# observe the natural exit and NOT send SIGTERM. Guards against an over-eager
|
|
147
|
+
# poll loop that would interrupt every iteration regardless of state.
|
|
148
|
+
export FAKE_SLEEP_AFTER=1
|
|
149
|
+
export WORK_PROBLEMS_IDLE_TIMEOUT_S=60
|
|
150
|
+
run dispatch_with_poll
|
|
151
|
+
[ "$status" -eq 0 ]
|
|
152
|
+
[[ "$output" == *"SIGTERM_SENT=0"* ]]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Doc-lint contract assertions — pin SKILL.md prose to the contract this fixture
|
|
156
|
+
# exercises behaviourally. Permitted Exception under ADR-037 (the SKILL.md is
|
|
157
|
+
# the contract document; these assertions guard against silent prose drift away
|
|
158
|
+
# from the behavioural expectation above).
|
|
159
|
+
|
|
160
|
+
@test "P121: SKILL.md Step 5 names WORK_PROBLEMS_IDLE_TIMEOUT_S env var" {
|
|
161
|
+
run grep -nE "WORK_PROBLEMS_IDLE_TIMEOUT_S" "$SKILL_FILE"
|
|
162
|
+
[ "$status" -eq 0 ]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@test "P121: SKILL.md Step 5 documents SIGTERM-on-idle action" {
|
|
166
|
+
run grep -niE "SIGTERM.{0,80}idle|idle.{0,80}SIGTERM|kill[[:space:]]+-TERM" "$SKILL_FILE"
|
|
167
|
+
[ "$status" -eq 0 ]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@test "P121: SKILL.md Step 5 names LAST_ACTIVITY_MARK signal" {
|
|
171
|
+
run grep -nE "LAST_ACTIVITY_MARK|last activity mark" "$SKILL_FILE"
|
|
172
|
+
[ "$status" -eq 0 ]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@test "P121: SKILL.md Step 5 cites P121 (idle-timeout SIGTERM driver)" {
|
|
176
|
+
run grep -nE "P121" "$SKILL_FILE"
|
|
177
|
+
[ "$status" -eq 0 ]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@test "P121: SKILL.md Step 5 documents the LAST_ACTIVITY signal trade-off (skip-iteration case)" {
|
|
181
|
+
# Per architect amendment 3: signal trade-off must be explicit so future
|
|
182
|
+
# contributors don't silently re-rate it. The SKILL.md prose names the
|
|
183
|
+
# max(dispatch_start, last commit) shape so adopters know skip iterations
|
|
184
|
+
# are bounded by IDLE_TIMEOUT_S since dispatch start, not a stale commit
|
|
185
|
+
# timestamp.
|
|
186
|
+
run grep -niE "max.{0,40}(dispatch.?start|DISPATCH_START).{0,80}(commit|git log)|skip.?iteration.{0,80}(timeout|threshold|bounded)|dispatch.?start.{0,80}upper.?bound" "$SKILL_FILE"
|
|
187
|
+
[ "$status" -eq 0 ]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@test "P121: SKILL.md Step 5 dispatch backgrounds the subprocess (PID capture for poll)" {
|
|
191
|
+
# The dispatch command shape must show the backgrounded form (& + $!) so the
|
|
192
|
+
# poll loop has a PID to kill -0 / kill -TERM. Foreground synchronous
|
|
193
|
+
# dispatch (current pre-P121 shape) cannot support idle-timeout SIGTERM.
|
|
194
|
+
run grep -nE 'ITER_PID=\$!|& *\n*ITER_PID|claude -p.{0,200}&[[:space:]]*$' "$SKILL_FILE"
|
|
195
|
+
[ "$status" -eq 0 ]
|
|
196
|
+
}
|