@windyroad/itil 0.55.4 → 0.55.5-preview.850
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/hooks/hooks.json +4 -0
- package/hooks/itil-deferral-cadence-gate.sh +181 -0
- package/hooks/test/itil-deferral-cadence-gate.bats +187 -0
- package/package.json +1 -1
- package/skills/capture-rfc/SKILL.md +20 -8
- package/skills/manage-problem/SKILL.md +1 -1
- package/skills/work-problems/SKILL.md +1 -1
package/hooks/hooks.json
CHANGED
|
@@ -61,6 +61,10 @@
|
|
|
61
61
|
{
|
|
62
62
|
"matcher": "Write|Edit|MultiEdit",
|
|
63
63
|
"hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-fictional-defer-detect.sh" }]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
67
|
+
"hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-deferral-cadence-gate.sh" }]
|
|
64
68
|
}
|
|
65
69
|
],
|
|
66
70
|
"Stop": [
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# P375 — wr-itil PostToolUse:Write|Edit|MultiEdit hook.
|
|
3
|
+
#
|
|
4
|
+
# The ratified Option-C "authoring-time enforcement gate" (CORE SLICE,
|
|
5
|
+
# advisory rollout). P375's root cause: the repo conflates a "named
|
|
6
|
+
# re-entry point" (a /skill, a lifecycle transition, "next review") with
|
|
7
|
+
# a SELF-FIRING cadence. A deferral that names `/wr-itil:manage-rfc`
|
|
8
|
+
# rots because nothing self-fires manage-rfc on that artefact ("BUT
|
|
9
|
+
# NOTHING TRIGGERS THAT WORK!!!", 2026-06-23). The correct rot test: a
|
|
10
|
+
# deferral is legal only if its trigger chain is reachable from a
|
|
11
|
+
# SELF-FIRING event (a hook, SessionStart, PreToolUse/PostToolUse, a CI
|
|
12
|
+
# workflow, cron, or a work-problems pre-flight). ADR-084's SessionStart
|
|
13
|
+
# census (Option A) only SURFACES accumulated rot; this gate attacks the
|
|
14
|
+
# root cause — it fires at AUTHORING time, the moment a new uncadenced
|
|
15
|
+
# deferral is written.
|
|
16
|
+
#
|
|
17
|
+
# Detection (diff-aware — scans the NEWLY-authored text only, so
|
|
18
|
+
# descriptive prose already on disk never re-triggers):
|
|
19
|
+
# 1. tool_name is Write/Edit/MultiEdit AND file_path is a SHIPPED
|
|
20
|
+
# authoring surface — SKILL.md, docs/decisions/*.md (ADRs),
|
|
21
|
+
# docs/rfcs/*.md (RFCs), or a hook *.sh — and is NOT under
|
|
22
|
+
# docs/problems/ (tickets descriptively narrate deferrals; highest
|
|
23
|
+
# false-positive surface; already covered by the ADR-084 census).
|
|
24
|
+
# 2. The new text (Edit new_string / Write content / MultiEdit joined
|
|
25
|
+
# new_strings) introduces a deferral phrasing (DEFER_RE).
|
|
26
|
+
# 3. Within the +/-5 line window of the match there is NO cadence
|
|
27
|
+
# annotation naming a SELF-FIRING trigger (CADENCE_RE) — an explicit
|
|
28
|
+
# `<!-- cadence: <trigger> -->` comment is the recommended carrier,
|
|
29
|
+
# but a bare prose mention of a self-firing surface also satisfies.
|
|
30
|
+
#
|
|
31
|
+
# THE LOAD-BEARING P375 REFINEMENT vs the P234 sibling
|
|
32
|
+
# (itil-fictional-defer-detect.sh): a bare named on-demand skill
|
|
33
|
+
# (/wr-foo:bar) or a bare ticket ID (Pnnn / RFC-nnn / ADR-nnn) does NOT
|
|
34
|
+
# satisfy the cadence requirement — that bare-naming IS the conflation
|
|
35
|
+
# P375 names illegal. So CADENCE_RE deliberately OMITS skill-invocation
|
|
36
|
+
# and ticket-ID forms (which the P234 hook WRONGLY accepts).
|
|
37
|
+
#
|
|
38
|
+
# CORE-SLICE BOUNDARY (named loudly per the architect review): the gate
|
|
39
|
+
# checks the cadence names a self-firing CLASS — it does NOT validate
|
|
40
|
+
# that the named trigger actually EXISTS or fires on that artefact (the
|
|
41
|
+
# transitive-reachability graph is a DEFERRED later slice; <!-- cadence:
|
|
42
|
+
# retrospective-deferral-census.sh -->). Citing a plausible-but-fictional
|
|
43
|
+
# hook name passes this slice; the graph check closes that gap later.
|
|
44
|
+
#
|
|
45
|
+
# Advisory only — NEVER blocks (exit 0 always). Per ADR-040/045
|
|
46
|
+
# declarative-first + ADR-013 Rule 6 fail-open + the P375 architect
|
|
47
|
+
# review (advisory-first per ADR-057 staged rollout; escalation to a
|
|
48
|
+
# PreToolUse block is a queued user-owned rollout-mode decision once the
|
|
49
|
+
# advisory's false-positive rate is measured). AFK self-suppress:
|
|
50
|
+
# WR_SUPPRESS_DEFERRAL_CADENCE_GATE=1 (mirrors the census's
|
|
51
|
+
# WR_SUPPRESS_DEFERRAL_CENSUS — advisory, valuable, but an AFK loop
|
|
52
|
+
# authoring governance prose should be able to mute it).
|
|
53
|
+
#
|
|
54
|
+
# References:
|
|
55
|
+
# P375 — driver (this gate is the Option-C root-cause fix).
|
|
56
|
+
# P234 — the PostToolUse sibling (retro-only, accepts skills/IDs);
|
|
57
|
+
# converging the two onto a shared vocabulary is a tracked
|
|
58
|
+
# P375 follow-on, NOT done here (ADR-002/003 self-containment
|
|
59
|
+
# — itil keeps its own copy of the deferral vocabulary).
|
|
60
|
+
# ADR-084 — the surfacing-only census (Option A) this gate complements.
|
|
61
|
+
# ADR-040 — declarative-first; advisory over hard block.
|
|
62
|
+
# ADR-045 — hook injection budget; advisory ~600 bytes, ceiling 1000.
|
|
63
|
+
# ADR-013 — Rule 6 fail-open on empty/malformed stdin.
|
|
64
|
+
# ADR-052 — behavioural bats live alongside.
|
|
65
|
+
# ADR-057 — staged advisory-first rollout for cluster-shaped rules.
|
|
66
|
+
|
|
67
|
+
# Self-suppress (AFK). Only the literal "1" suppresses.
|
|
68
|
+
[ "${WR_SUPPRESS_DEFERRAL_CADENCE_GATE:-}" = "1" ] && exit 0
|
|
69
|
+
|
|
70
|
+
# Per-surface configuration (copy-and-retarget extensible, like the
|
|
71
|
+
# P234 sibling: adjust the regex vars below).
|
|
72
|
+
DEFER_RE='deferred to|deferred pending|defer pending|pending review|re-rate at next|\(deferred|: deferred|deferred[ ]*[—.,;:-]|lands in [Ss]lice|future slice|flesh(ed)? out later|next review|when ready'
|
|
73
|
+
# Cadence-satisfying SELF-FIRING citations. NOTE the deliberate absence
|
|
74
|
+
# of skill-invocation (/wr-foo:bar) and ticket-ID (Pnnn/RFC-nnn/ADR-nnn)
|
|
75
|
+
# — those are the P375 conflation and do NOT satisfy the cadence.
|
|
76
|
+
HOOK_PATH_RE='[A-Za-z0-9_./-]+\.sh\b'
|
|
77
|
+
SESSIONSTART_RE='SessionStart'
|
|
78
|
+
TOOLUSE_RE='PreToolUse|PostToolUse'
|
|
79
|
+
CI_WORKFLOW_RE='\.github/workflows/'
|
|
80
|
+
CRON_RE='\bcron\b'
|
|
81
|
+
PREFLIGHT_RE='work-problems Step|pre-flight|Step 0[a-z]'
|
|
82
|
+
|
|
83
|
+
INPUT=$(cat 2>/dev/null || true)
|
|
84
|
+
[ -n "$INPUT" ] || exit 0
|
|
85
|
+
|
|
86
|
+
TOOL_NAME=$(echo "$INPUT" | python3 -c "
|
|
87
|
+
import sys, json
|
|
88
|
+
try:
|
|
89
|
+
print(json.load(sys.stdin).get('tool_name', ''))
|
|
90
|
+
except Exception:
|
|
91
|
+
print('')
|
|
92
|
+
" 2>/dev/null || echo "")
|
|
93
|
+
|
|
94
|
+
case "$TOOL_NAME" in
|
|
95
|
+
Write|Edit|MultiEdit) ;;
|
|
96
|
+
*) exit 0 ;;
|
|
97
|
+
esac
|
|
98
|
+
|
|
99
|
+
FILE_PATH=$(echo "$INPUT" | python3 -c "
|
|
100
|
+
import sys, json
|
|
101
|
+
try:
|
|
102
|
+
print(json.load(sys.stdin).get('tool_input', {}).get('file_path', ''))
|
|
103
|
+
except Exception:
|
|
104
|
+
print('')
|
|
105
|
+
" 2>/dev/null || echo "")
|
|
106
|
+
|
|
107
|
+
[ -n "$FILE_PATH" ] || exit 0
|
|
108
|
+
|
|
109
|
+
# Scope: shipped authoring surfaces only; docs/problems/ excluded.
|
|
110
|
+
case "$FILE_PATH" in
|
|
111
|
+
*/docs/problems/*|docs/problems/*) exit 0 ;;
|
|
112
|
+
esac
|
|
113
|
+
if ! echo "$FILE_PATH" | grep -qE '(^|/)SKILL\.md$|/docs/decisions/.*\.md$|docs/decisions/.*\.md$|/docs/rfcs/.*\.md$|docs/rfcs/.*\.md$|\.sh$'; then
|
|
114
|
+
exit 0
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# Extract the NEWLY-authored text (diff-aware). Write content; Edit
|
|
118
|
+
# new_string; MultiEdit joined new_strings.
|
|
119
|
+
NEW_TEXT=$(echo "$INPUT" | python3 -c "
|
|
120
|
+
import sys, json
|
|
121
|
+
try:
|
|
122
|
+
ti = json.load(sys.stdin).get('tool_input', {})
|
|
123
|
+
if 'content' in ti:
|
|
124
|
+
print(ti.get('content', ''))
|
|
125
|
+
elif 'new_string' in ti:
|
|
126
|
+
print(ti.get('new_string', ''))
|
|
127
|
+
elif 'edits' in ti:
|
|
128
|
+
print('\n'.join(e.get('new_string', '') for e in ti.get('edits', [])))
|
|
129
|
+
else:
|
|
130
|
+
print('')
|
|
131
|
+
except Exception:
|
|
132
|
+
print('')
|
|
133
|
+
" 2>/dev/null || echo "")
|
|
134
|
+
|
|
135
|
+
[ -n "$NEW_TEXT" ] || exit 0
|
|
136
|
+
|
|
137
|
+
# Buffer the new text so we can do the +/-5 line window check.
|
|
138
|
+
SCAN=$(mktemp 2>/dev/null) || exit 0
|
|
139
|
+
printf '%s\n' "$NEW_TEXT" > "$SCAN"
|
|
140
|
+
TOTAL_LINES=$(wc -l < "$SCAN" | tr -d ' ')
|
|
141
|
+
|
|
142
|
+
MATCHES=$(grep -inE "$DEFER_RE" "$SCAN" 2>/dev/null || true)
|
|
143
|
+
if [ -z "$MATCHES" ]; then
|
|
144
|
+
rm -f "$SCAN"
|
|
145
|
+
exit 0
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
FICTIONAL_LINE=""
|
|
149
|
+
FICTIONAL_PHRASE=""
|
|
150
|
+
|
|
151
|
+
while IFS= read -r row; do
|
|
152
|
+
[ -n "$row" ] || continue
|
|
153
|
+
LN="${row%%:*}"
|
|
154
|
+
text="${row#*:}"
|
|
155
|
+
|
|
156
|
+
START=$((LN - 5)); [ "$START" -lt 1 ] && START=1
|
|
157
|
+
END=$((LN + 5)); [ "$END" -gt "$TOTAL_LINES" ] && END="$TOTAL_LINES"
|
|
158
|
+
WINDOW=$(sed -n "${START},${END}p" "$SCAN" 2>/dev/null || true)
|
|
159
|
+
|
|
160
|
+
# Self-firing cadence citation present? → cadenced, skip.
|
|
161
|
+
echo "$WINDOW" | grep -qE "$HOOK_PATH_RE" && continue
|
|
162
|
+
echo "$WINDOW" | grep -qE "$SESSIONSTART_RE" && continue
|
|
163
|
+
echo "$WINDOW" | grep -qE "$TOOLUSE_RE" && continue
|
|
164
|
+
echo "$WINDOW" | grep -qE "$CI_WORKFLOW_RE" && continue
|
|
165
|
+
echo "$WINDOW" | grep -qE "$CRON_RE" && continue
|
|
166
|
+
echo "$WINDOW" | grep -qE "$PREFLIGHT_RE" && continue
|
|
167
|
+
|
|
168
|
+
# No self-firing citation → uncadenced deferral. Record the first.
|
|
169
|
+
FICTIONAL_LINE="$LN"
|
|
170
|
+
FICTIONAL_PHRASE=$(echo "$text" | tr -s ' ' ' ' | sed 's/^[[:space:]]*//' | cut -c1-70)
|
|
171
|
+
break
|
|
172
|
+
done <<< "$MATCHES"
|
|
173
|
+
|
|
174
|
+
rm -f "$SCAN"
|
|
175
|
+
|
|
176
|
+
[ -n "$FICTIONAL_LINE" ] || exit 0
|
|
177
|
+
|
|
178
|
+
# Advisory to stderr. ADR-045 budget (~600 bytes target). exit 0 always.
|
|
179
|
+
echo "P375 ADVISORY: uncadenced deferral authored in ${FILE_PATH} — \"${FICTIONAL_PHRASE}\". A named on-demand re-entry point (/wr-foo:bar, a lifecycle transition, \"next review\") is NOT a cadence — nothing self-fires it, so the work rots. Add a cadence annotation naming a SELF-FIRING trigger within +/-5 lines: <!-- cadence: <hook *.sh | SessionStart | PreToolUse/PostToolUse | .github/workflows/ | cron | work-problems pre-flight> --> OR do the work now. (Core slice checks the trigger CLASS, not that it exists.) See P375." >&2
|
|
180
|
+
|
|
181
|
+
exit 0
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# P375 — itil-deferral-cadence-gate.sh PostToolUse:Write|Edit|MultiEdit
|
|
4
|
+
# advisory hook. The ratified Option-C "authoring-time enforcement gate"
|
|
5
|
+
# (core slice, advisory rollout per ADR-057 staged-rollout + the P375
|
|
6
|
+
# architect review 2026-06-28).
|
|
7
|
+
#
|
|
8
|
+
# The gate fires at AUTHORING time on the NEWLY-authored text (Edit
|
|
9
|
+
# new_string / Write content / MultiEdit concatenated new_strings —
|
|
10
|
+
# diff-aware so descriptive prose already on disk never re-triggers) of
|
|
11
|
+
# a SHIPPED authoring surface (SKILL.md, docs/decisions/*.md ADRs,
|
|
12
|
+
# docs/rfcs/*.md RFCs, hook *.sh). When the new text introduces an
|
|
13
|
+
# uncadenced-deferral phrasing (`deferred to <on-demand re-entry>`,
|
|
14
|
+
# `pending review`, `re-rate at next`, `(deferred …`, `next review`,
|
|
15
|
+
# `when ready`, `lands in Slice N`) WITHOUT a cadence annotation naming
|
|
16
|
+
# a SELF-FIRING trigger within the +/-5 line window, it emits a stderr
|
|
17
|
+
# advisory citing P375 + the remedy.
|
|
18
|
+
#
|
|
19
|
+
# THE LOAD-BEARING P375 REFINEMENT vs the P234 sibling
|
|
20
|
+
# (itil-fictional-defer-detect.sh): a bare named on-demand skill
|
|
21
|
+
# (/wr-foo:bar) or a bare ticket ID (Pnnn / RFC-nnn / ADR-nnn) does NOT
|
|
22
|
+
# satisfy the cadence requirement — naming an on-demand re-entry point
|
|
23
|
+
# and treating it as a cadence IS the exact conflation P375 captures
|
|
24
|
+
# ("BUT NOTHING TRIGGERS THAT WORK!!!"). Only a self-firing-CLASS
|
|
25
|
+
# citation satisfies: a hook *.sh, SessionStart, PreToolUse/PostToolUse,
|
|
26
|
+
# .github/workflows/ (CI), cron, or a work-problems Step-0x / pre-flight
|
|
27
|
+
# reference. The existing P234 hook WRONGLY accepts skills + ticket IDs;
|
|
28
|
+
# this hook does not.
|
|
29
|
+
#
|
|
30
|
+
# Core-slice boundary (architect refinement 1, named loudly): the gate
|
|
31
|
+
# checks the cadence annotation names a self-firing-CLASS trigger — it
|
|
32
|
+
# does NOT validate that the named trigger actually exists / fires (the
|
|
33
|
+
# transitive-reachability graph is a deferred later slice). docs/problems/
|
|
34
|
+
# tickets are EXCLUDED (they descriptively narrate deferrals — highest
|
|
35
|
+
# false-positive surface, already covered by the ADR-084 census).
|
|
36
|
+
#
|
|
37
|
+
# Advisory only — NEVER blocks (exit 0 always), per ADR-040/045
|
|
38
|
+
# declarative-first + ADR-013 Rule 6 fail-open. Behavioural per ADR-052
|
|
39
|
+
# (asserts emitted stderr, never source-greps the hook). Hermetic per
|
|
40
|
+
# P391 (unsets inherited suppress vars in setup).
|
|
41
|
+
|
|
42
|
+
setup() {
|
|
43
|
+
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
|
|
44
|
+
HOOK="$REPO_ROOT/packages/itil/hooks/itil-deferral-cadence-gate.sh"
|
|
45
|
+
# P391 hermeticity: AFK iters export suppress vars that would poison
|
|
46
|
+
# the advisory-fires assertions. Strip them for the test process.
|
|
47
|
+
unset WR_SUPPRESS_DEFERRAL_CADENCE_GATE
|
|
48
|
+
unset WR_SUPPRESS_OVERSIGHT_NUDGE
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Emit a PostToolUse stdin payload for an Edit tool call.
|
|
52
|
+
# $1 = file_path, $2 = new_string (the newly-authored text).
|
|
53
|
+
emit_edit() {
|
|
54
|
+
jq -n --arg p "$1" --arg n "$2" '{
|
|
55
|
+
session_id: "deferral-cadence-test",
|
|
56
|
+
tool_name: "Edit",
|
|
57
|
+
tool_input: { file_path: $p, old_string: "X", new_string: $n },
|
|
58
|
+
tool_response: { success: true }
|
|
59
|
+
}'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Emit a PostToolUse stdin payload for a Write tool call.
|
|
63
|
+
emit_write() {
|
|
64
|
+
jq -n --arg p "$1" --arg c "$2" '{
|
|
65
|
+
session_id: "deferral-cadence-test",
|
|
66
|
+
tool_name: "Write",
|
|
67
|
+
tool_input: { file_path: $p, content: $c },
|
|
68
|
+
tool_response: { success: true }
|
|
69
|
+
}'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fires() { [[ "$output" == *"P375"* ]] || [[ "$stderr" == *"P375"* ]]; }
|
|
73
|
+
silent() { [[ "$output" != *"P375"* ]] && [[ "$stderr" != *"P375"* ]]; }
|
|
74
|
+
|
|
75
|
+
# --- Positive: uncadenced deferral authored → advisory fires ---
|
|
76
|
+
|
|
77
|
+
@test "fire: SKILL.md deferral citing a bare on-demand skill (the P375 conflation) fires" {
|
|
78
|
+
# This is the case the P234 sibling WRONGLY allows — a named on-demand
|
|
79
|
+
# skill is NOT a self-firing cadence.
|
|
80
|
+
run bash "$HOOK" <<<"$(emit_edit packages/foo/skills/bar/SKILL.md \
|
|
81
|
+
'Full scope deferred to /wr-itil:manage-rfc accepted transition.')"
|
|
82
|
+
[ "$status" -eq 0 ]
|
|
83
|
+
fires
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@test "fire: ADR new_string deferral with no citation at all fires" {
|
|
87
|
+
run bash "$HOOK" <<<"$(emit_edit docs/decisions/099-thing.proposed.md \
|
|
88
|
+
'Considered Options and Consequences deferred — flesh out later.')"
|
|
89
|
+
[ "$status" -eq 0 ]
|
|
90
|
+
fires
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@test "fire: RFC deferral citing only a bare ticket ID (not a cadence) fires" {
|
|
94
|
+
# A ticket ID is an audit annotation, not a self-firing trigger.
|
|
95
|
+
run bash "$HOOK" <<<"$(emit_edit docs/rfcs/RFC-099-thing.proposed.md \
|
|
96
|
+
'Scope deferred to next review per P375.')"
|
|
97
|
+
[ "$status" -eq 0 ]
|
|
98
|
+
fires
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@test "fire: Write of a hook .sh whose content carries an uncadenced deferral fires" {
|
|
102
|
+
run bash "$HOOK" <<<"$(emit_write packages/foo/hooks/foo.sh \
|
|
103
|
+
'# TODO pending review when ready; not wired yet.')"
|
|
104
|
+
[ "$status" -eq 0 ]
|
|
105
|
+
fires
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@test "fire: deferral with a cadence comment that names an ON-DEMAND skill still fires" {
|
|
109
|
+
# The annotation carrier is fine but its value must name a self-firing
|
|
110
|
+
# CLASS — naming an on-demand skill inside the comment does not help.
|
|
111
|
+
run bash "$HOOK" <<<"$(emit_edit packages/foo/skills/bar/SKILL.md \
|
|
112
|
+
'Tasks deferred. <!-- cadence: /wr-itil:manage-rfc accepted -->')"
|
|
113
|
+
[ "$status" -eq 0 ]
|
|
114
|
+
fires
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# --- Negative: cadenced deferral or out-of-scope → silent ---
|
|
118
|
+
|
|
119
|
+
@test "allow: deferral citing a self-firing hook .sh in window exits silent" {
|
|
120
|
+
run bash "$HOOK" <<<"$(emit_edit packages/foo/skills/bar/SKILL.md \
|
|
121
|
+
'Re-rate deferred; surfaced every session by retrospective-deferral-census.sh.')"
|
|
122
|
+
[ "$status" -eq 0 ]
|
|
123
|
+
silent
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@test "allow: deferral with an explicit cadence annotation naming PostToolUse exits silent" {
|
|
127
|
+
run bash "$HOOK" <<<"$(emit_edit docs/decisions/099-thing.proposed.md \
|
|
128
|
+
'Section deferred. <!-- cadence: PostToolUse itil-foo-gate.sh fires on author -->')"
|
|
129
|
+
[ "$status" -eq 0 ]
|
|
130
|
+
silent
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@test "allow: deferral citing SessionStart self-firing surface exits silent" {
|
|
134
|
+
run bash "$HOOK" <<<"$(emit_edit docs/rfcs/RFC-099-thing.proposed.md \
|
|
135
|
+
'Drain deferred; the SessionStart nudge re-surfaces it every session.')"
|
|
136
|
+
[ "$status" -eq 0 ]
|
|
137
|
+
silent
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@test "allow: deferral citing a CI workflow exits silent" {
|
|
141
|
+
run bash "$HOOK" <<<"$(emit_edit packages/foo/skills/bar/SKILL.md \
|
|
142
|
+
'Validation deferred to .github/workflows/check.yml on every push.')"
|
|
143
|
+
[ "$status" -eq 0 ]
|
|
144
|
+
silent
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@test "allow: deferral in a docs/problems/ ticket (excluded surface) exits silent" {
|
|
148
|
+
run bash "$HOOK" <<<"$(emit_edit docs/problems/open/099-thing.open.md \
|
|
149
|
+
'Severity deferred to investigation; expand at next review.')"
|
|
150
|
+
[ "$status" -eq 0 ]
|
|
151
|
+
silent
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@test "allow: deferral phrasing in a non-authoring file (.ts) exits silent" {
|
|
155
|
+
run bash "$HOOK" <<<"$(emit_edit packages/foo/src/index.ts \
|
|
156
|
+
'// deferred to next review — TODO')"
|
|
157
|
+
[ "$status" -eq 0 ]
|
|
158
|
+
silent
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@test "allow: authoring-surface edit with NO deferral phrasing exits silent" {
|
|
162
|
+
run bash "$HOOK" <<<"$(emit_edit packages/foo/skills/bar/SKILL.md \
|
|
163
|
+
'This step validates the marker and proceeds to Step 2.')"
|
|
164
|
+
[ "$status" -eq 0 ]
|
|
165
|
+
silent
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# --- Robustness ---
|
|
169
|
+
|
|
170
|
+
@test "fail-open: empty stdin exits 0 silent" {
|
|
171
|
+
run bash "$HOOK" <<<""
|
|
172
|
+
[ "$status" -eq 0 ]
|
|
173
|
+
silent
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@test "fail-open: malformed JSON stdin exits 0 silent" {
|
|
177
|
+
run bash "$HOOK" <<<"{not json"
|
|
178
|
+
[ "$status" -eq 0 ]
|
|
179
|
+
silent
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@test "suppress: WR_SUPPRESS_DEFERRAL_CADENCE_GATE=1 silences the advisory" {
|
|
183
|
+
run env WR_SUPPRESS_DEFERRAL_CADENCE_GATE=1 bash "$HOOK" <<<"$(emit_edit \
|
|
184
|
+
packages/foo/skills/bar/SKILL.md 'deferred to /wr-itil:manage-rfc accepted')"
|
|
185
|
+
[ "$status" -eq 0 ]
|
|
186
|
+
silent
|
|
187
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wr-itil:capture-rfc
|
|
3
|
-
description: Lightweight RFC-capture skill for aside-invocation during foreground work — mandatory problem-trace per ADR-060 I1 invariant, skeleton RFC file, single commit per capture, no inline README refresh. Defers full duplicate analysis and README refresh to /wr-itil:manage-rfc. Use this when the user (or agent) wants to capture an RFC quickly with a clear problem trace. For full lifecycle management, use /wr-itil:manage-rfc.
|
|
3
|
+
description: Lightweight RFC-capture skill for aside-invocation during foreground work — mandatory problem-trace per ADR-060 I1 invariant, skeleton RFC file by default (or a fully-authored Scope + Tasks under the `--fix-time` flag, per ADR-073), single commit per capture, no inline README refresh. Defers full duplicate analysis and README refresh to /wr-itil:manage-rfc. Use this when the user (or agent) wants to capture an RFC quickly with a clear problem trace. For full lifecycle management, use /wr-itil:manage-rfc.
|
|
4
4
|
allowed-tools: Read, Write, Edit, Bash, Grep, Glob
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ Capture a Request for Change (RFC) ticket quickly during foreground work. Lightw
|
|
|
10
10
|
|
|
11
11
|
This skill is one half of the capture-then-manage RFC framework introduced by ADR-060 (Problem-RFC-Story framework with mandatory problem-trace and unified problem ontology, accepted 2026-05-05). The other half is `/wr-itil:manage-rfc` (heavyweight intake + lifecycle management).
|
|
12
12
|
|
|
13
|
-
**Related JTBDs**: JTBD-008 (primary — Decompose a Fix Into Coordinated Changes; this skill IS the capture-time decomposition surface), JTBD-001 (extended scope — change-set-level governance), JTBD-101 (atomic-fix-adopter — every fix goes through an RFC per ADR-071
|
|
13
|
+
**Related JTBDs**: JTBD-008 (primary — Decompose a Fix Into Coordinated Changes; this skill IS the capture-time decomposition surface), JTBD-001 (extended scope — change-set-level governance), JTBD-101 (atomic-fix-adopter — every fix goes through an RFC per ADR-071). capture-rfc is invoked two ways: (1) the **user-aside path** (deliberate, default skeleton — the user fleshes scope out later at `/wr-itil:manage-rfc accepted`); and (2) the **fix-time auto-create path** (`--fix-time`), where the I13 propose-fix gate auto-fires it on an RFC-less Known Error and it **authors the full RFC** from the traced problem. Per the ADR-073 P399 amendment, fix-time RFC scope is **framework-mediated** (derived from the already-pinned problem + ADR-071, not direction-setting), so auto-firing with full authoring is correct — NOT a re-ask of a decided question (P132 / inverse-P078).
|
|
14
14
|
|
|
15
15
|
## Output Formatting
|
|
16
16
|
|
|
@@ -35,6 +35,8 @@ When referencing RFC IDs, problem IDs, ADR IDs, JTBD IDs, or story IDs in prose
|
|
|
35
35
|
|
|
36
36
|
**Optional flag (Phase 2)**: `--stories STORY-<NNN>,STORY-<NNN>,...` — ORDERED execution sequence per ADR-060 line 262. Cardinality 0..N: an RFC whose work is not decomposed into stories OMITS the flag and capture-rfc populates `stories: []` in frontmatter (a structural state, NOT a reduced-ceremony path — every fix goes through an RFC per ADR-071); story-decomposed RFCs supply the ordered list. The flag accepts STORY-IDs that don't yet resolve to files (forward-reference is permitted at capture; the existence check happens at `manage-rfc <NNN> accepted` transition per ADR-060 working-the-problem flow line 304).
|
|
37
37
|
|
|
38
|
+
**Optional flag (fix-time authoring, ADR-073 P399)**: `--fix-time` — switches Step 5 from the deferred-placeholder template to **full authoring**: capture-rfc reads the traced problem ticket(s) and authors a populated `## Scope` (the fix being proposed + chosen implementation approach as prose) and a real `## Tasks` decomposition from the problem's `## Root Cause Analysis` + `## Fix Strategy`, instead of the `(deferred — populate at manage-rfc accepted)` placeholders. Set by the I13 propose-fix gate's auto-create branch (`/wr-itil:manage-problem` + `/wr-itil:work-problems`); the user-aside path omits it. The authored RFC still carries NO Considered-Options block (ADR-070 — chosen-path prose only) and is still born `human-oversight: unconfirmed` (ratified at `manage-rfc accepted`). Composes with `--stories`.
|
|
39
|
+
|
|
38
40
|
```
|
|
39
41
|
/wr-itil:capture-rfc P168 Pipeline consume-catalog and bootstrap-from-reports — multi-commit retrofit
|
|
40
42
|
/wr-itil:capture-rfc P038,P064 Voice-and-tone gates on external comms — coordinated rollout across changeset/PR/release-notes
|
|
@@ -54,6 +56,7 @@ This skill has **one direction-setting AskUserQuestion** (problem-trace, when ar
|
|
|
54
56
|
| RFC ID allocation | Mechanical: `max(local, origin) + 1`, three-digit padded | silent-mechanical |
|
|
55
57
|
| Title kebab-slug | Mechanical: first 8-10 non-stopword tokens of description | silent-mechanical |
|
|
56
58
|
| Title prose / scope summary refinement | Optional `AskUserQuestion`; silent-default to derived form when unavailable | taste |
|
|
59
|
+
| Fix-time Scope/Tasks authoring (`--fix-time`) | Framework-mediated: author `## Scope` + `## Tasks` from the traced problem's RCA + Fix Strategy. NO `AskUserQuestion` — the scope is *derived* from already-pinned ADR-071 direction, not new direction-setting (ADR-073 § ADR-044 reclassification, P399). Born `unconfirmed`; ratified at `manage-rfc accepted` | silent-framework |
|
|
57
60
|
| File write / frontmatter | Mechanical: shape per `docs/rfcs/README.md` § RFC body structure | silent-mechanical |
|
|
58
61
|
| Single commit | Mechanical: `docs(rfcs): capture RFC-<NNN> <title>` | silent-mechanical |
|
|
59
62
|
| Empty arguments | Halt-with-stderr-directive: print "capture-rfc requires `<problem-trace> <description>` — invoke /wr-itil:manage-rfc instead for the full intake flow" and exit. AFK orchestrators MUST NOT invoke capture-rfc with empty arguments. | n/a |
|
|
@@ -85,12 +88,14 @@ The arguments must begin with a problem-trace token (`P<NNN>` or comma-separated
|
|
|
85
88
|
|
|
86
89
|
```bash
|
|
87
90
|
# Tokenise: first non-flag token = problem-trace; rest = description.
|
|
88
|
-
# Optional --stories STORY-NNN,STORY-NNN,...
|
|
91
|
+
# Optional --stories STORY-NNN,STORY-NNN,... and --fix-time flags may appear anywhere.
|
|
89
92
|
stories_trace=""
|
|
93
|
+
fix_time=0
|
|
90
94
|
positional=()
|
|
91
95
|
while [ $# -gt 0 ]; do
|
|
92
96
|
case "$1" in
|
|
93
97
|
--stories) stories_trace="$2"; shift 2 ;;
|
|
98
|
+
--fix-time) fix_time=1; shift ;;
|
|
94
99
|
*) positional+=("$1"); shift ;;
|
|
95
100
|
esac
|
|
96
101
|
done
|
|
@@ -202,11 +207,13 @@ stories: [<from --stories flag — ordered execution sequence; or [] if --storie
|
|
|
202
207
|
|
|
203
208
|
## Scope
|
|
204
209
|
|
|
205
|
-
(deferred — populate at /wr-itil:manage-rfc accepted transition)
|
|
210
|
+
<default (user-aside path): (deferred — populate at /wr-itil:manage-rfc accepted transition)>
|
|
211
|
+
<--fix-time path (ADR-073 P399): AUTHOR this from the traced problem — a real paragraph describing the fix being proposed (the change(s) the Known Error needs) plus the chosen implementation approach as prose. Derive it from the problem's `## Root Cause Analysis` + `## Fix Strategy`. Do NOT enumerate rejected alternatives or add a Considered-Options block (ADR-070 — chosen-path prose only).>
|
|
206
212
|
|
|
207
213
|
## Tasks
|
|
208
214
|
|
|
209
|
-
- [ ] (deferred — populate at /wr-itil:manage-rfc accepted transition)
|
|
215
|
+
<default (user-aside path): - [ ] (deferred — populate at /wr-itil:manage-rfc accepted transition)>
|
|
216
|
+
<--fix-time path (ADR-073 P399): AUTHOR a real ordered task decomposition (one-purpose-per-commit per ADR-014) from the Fix Strategy — the concrete steps to land the fix, as `- [ ]` checkboxes.>
|
|
210
217
|
|
|
211
218
|
## Commits
|
|
212
219
|
|
|
@@ -217,7 +224,10 @@ stories: [<from --stories flag — ordered execution sequence; or [] if --storie
|
|
|
217
224
|
(captured via /wr-itil:capture-rfc; expand at next /wr-itil:manage-rfc invocation)
|
|
218
225
|
```
|
|
219
226
|
|
|
220
|
-
|
|
227
|
+
**Mode branch (`fix_time` from Step 1):**
|
|
228
|
+
|
|
229
|
+
- **`fix_time=0` (user-aside path, default)**: write the deferred placeholders verbatim. The deferred-section pattern matches `capture-problem`'s placeholder approach — the captured RFC is intentionally minimal; full scope and task decomposition land at the manage-rfc accepted-transition step.
|
|
230
|
+
- **`fix_time=1` (fix-time auto-create path, ADR-073 P399)**: BEFORE writing the file, Read each traced problem ticket (`docs/problems/.../<NNN>-*.md`) and author the `## Scope` + `## Tasks` sections from its `## Root Cause Analysis` + `## Fix Strategy` (the fix being proposed + its task decomposition). The RFC is NOT a skeleton on this path — the deferred "flesh out later" step never self-fires (P375), so the scope is authored now while the fix context is in hand. Keep it ADR-070-compliant: chosen-path prose in `## Scope`, NO Considered-Options / alternatives-rejected block (enumerating ≥2 options and choosing among them would be cat-1 direction-setting; deriving the single fix the problem already implies is framework-mediated — ADR-073 § ADR-044 reclassification). `human-oversight` stays `unconfirmed` (Step 7 oversight discipline holds — no per-RFC substance-confirm at fix-time; ratified at `manage-rfc accepted`).
|
|
221
231
|
|
|
222
232
|
### 6. Single commit — `## RFCs` reverse-trace refresh; no rfcs README refresh
|
|
223
233
|
|
|
@@ -278,9 +288,11 @@ After the commit, report:
|
|
|
278
288
|
- The new RFC file path and ID.
|
|
279
289
|
- The traced problems with their lifecycle states (Open / Known Error / Verifying / Closed / Parked).
|
|
280
290
|
- Any advisory warnings (Verifying, Closed, Parked traces).
|
|
281
|
-
- Trailing pointer:
|
|
291
|
+
- Trailing pointer:
|
|
292
|
+
- **user-aside path (default)**: `Run /wr-itil:manage-rfc <RFC-<NNN>> next to populate Scope / Tasks and advance to accepted; refresh docs/rfcs/README.md.`
|
|
293
|
+
- **`--fix-time` path**: `RFC-<NNN> is authored (Scope + Tasks populated from P<NNN>) and born human-oversight: unconfirmed. Run /wr-itil:manage-rfc <RFC-<NNN>> accepted to ratify the authored scope; refresh docs/rfcs/README.md.`
|
|
282
294
|
|
|
283
|
-
The trailing pointer is **not optional** — it is the user-visible signal that the RFC is intentionally skeleton-only and how to advance it.
|
|
295
|
+
The trailing pointer is **not optional** — on the user-aside path it is the user-visible signal that the RFC is intentionally skeleton-only and how to advance it; on the `--fix-time` path it signals the RFC is already authored and points at the ratification drain.
|
|
284
296
|
|
|
285
297
|
**Oversight marker discipline (ADR-066 + ADR-068 amendments 2026-06-02 / P348).** The skeleton frontmatter MUST include `human-oversight: unconfirmed`. capture-rfc is the AFK-friendly aside surface; there is no substance-confirm `AskUserQuestion` pass in this flow (deferred to `/wr-itil:manage-rfc accepted`), so `confirmed` would be a hollow marker (the P348 bug class). The architect-side hook (`architect-oversight-marker-discipline.sh`) does NOT gate `docs/rfcs/`, but downstream paths that promote a captured RFC to `confirmed` MUST do so via a proper substance-confirm AskUserQuestion (e.g. `/wr-itil:manage-rfc accepted` for genuine ratification) — agents authoring RFCs at capture-time MUST NOT write `confirmed`. The drain pattern mirrors the ADR drain: an RFC's `unconfirmed` state surfaces interactively during the `accepted` transition.
|
|
286
298
|
|
|
@@ -191,7 +191,7 @@ wr-itil-check-fix-rfc-trace <problem-file>
|
|
|
191
191
|
- **Empty stdout** (an RFC already traces the problem): proceed to the traversal below.
|
|
192
192
|
- **Non-empty stdout** (directive `no-rfc-trace: P<NNN> …`): the predicate has confirmed only that *no RFC's `problems:` array names this PID* — it has NOT decided that no fix vehicle exists. Distinguish **two sub-cases** before acting (P371; ADR-073 auto-create is intended ONLY when no fix vehicle exists, NOT when an existing vehicle merely lacks the trace edge — architect-confirmed):
|
|
193
193
|
- **(a) Existing-vehicle-untraced — an RFC is already this ticket's fix vehicle but just hasn't wired the trace edge.** Read the ticket's `## Fix Strategy` / `## Resolution` / `## Dependencies` / `## Related` sections for an RFC cited as the **fix vehicle** — i.e. the fix IS that RFC's task set (the recurring shape: a rework / follow-on Known Error whose fix is an existing RFC's B-tasks, so that RFC's `problems:` array names the *original driver* problem, not this ticket). This is a **judgement read of the citation context, NOT a blind "any cited RFC" match**: an RFC named only as context / `composes with` / `**Related**` background is NOT a fix vehicle — wiring its trace edge would pollute its `problems:` array. If a genuine existing fix vehicle is found: **wire** this problem's trace edge into that RFC's frontmatter `problems:` array (Edit the `problems: [...]` line to include `P<NNN>` + a short inline `**Problems**:` body annotation noting the edge was wired because the vehicle already exists), then run `wr-itil-update-problem-rfcs-section <problem-file>` so the ticket's derived `## RFCs` section reflects the wired trace. Re-run the predicate (now empty) and proceed. Do **NOT** auto-create — minting a second RFC here produces a redundant duplicate that fragments the fix across two trace vehicles (the exact P371 defect). Structured-log the wire event (e.g. `I13: wired P<NNN> trace edge into existing fix-vehicle RFC-<NNN>`) to the iter summary `notes` for the JTBD-006 audit trail.
|
|
194
|
-
- **(b) No-vehicle — no existing RFC is this ticket's fix vehicle.** **auto-create** a problem-traced
|
|
194
|
+
- **(b) No-vehicle — no existing RFC is this ticket's fix vehicle.** **auto-create AND fully author** a problem-traced RFC — do NOT block, do NOT skip, do NOT ask (ADR-073 auto-create-everywhere; the create AND its full scope authoring are framework-mediated, not cat-1 direction-setting — the scope is *derived* from the already-traced problem under ADR-071's pinned direction, not new direction *invented*; so NO `AskUserQuestion` consent gate fires here — P132 / inverse-P078; ADR-073 § ADR-044 reclassification, P399). Delegate to `/wr-itil:capture-rfc --fix-time` with a problem trace derived from this Known Error (`P<NNN>` + a fix-scoped title from the problem's `## Fix Strategy` / root cause), which is the canonical ADR-070-compliant vehicle (allocates the next RFC ID, refreshes the RFC README, emits no Considered-Options block — no create logic is duplicated). The `--fix-time` flag makes capture-rfc author a **populated `## Scope`** (the fix being proposed + implementation approach) and a **real `## Tasks`** decomposition from the problem's RCA + Fix Strategy — NOT an empty skeleton placeholder (ADR-073 P399: the deferred "flesh out later" step never self-fires, P375, so author it now while the fix context is in hand). Then re-run the predicate (now empty) and proceed. The authored RFC is the fix-proposal artifact, born `human-oversight: unconfirmed` (ratified at `manage-rfc accepted`). Structured-log the auto-create event to the iter summary `notes` (the ADR-073 reassessment criterion — auto-authored-RFC-mis-scoped signal feeds `/wr-retrospective:run-retro`).
|
|
195
195
|
|
|
196
196
|
The predicate is the load-bearing detection half (committed shell + behavioural bats per ADR-052: `packages/itil/scripts/test/check-fix-rfc-trace.bats`); the create half is orchestrated through `capture-rfc` (the ADR-060 I1 load-bearing-from-the-start standard — deterministic detection in committed shell, create through the canonical capture surface). This gate fires at **every** fix-time surface; the AFK `/wr-itil:work-problems` orchestrator dispatches its fix work *through this same manage-problem traversal*, so the gate covers the AFK surface transitively (ADR-073 "everywhere").
|
|
197
197
|
|
|
@@ -642,7 +642,7 @@ rm -f "$ITER_JSON"
|
|
|
642
642
|
|
|
643
643
|
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.
|
|
644
644
|
2. **Task**: apply the `/wr-itil:manage-problem` workflow for `work highest WSJF problem that can be progressed non-interactively as the user is AFK`. Follow manage-problem SKILL.md verbatim, including architect / jtbd / style-guide / voice-tone gate reviews and the commit gate (manage-problem Step 11). Because this subprocess has the Agent tool in its own surface, the normal review-via-subagent paths work — no inline-verdict fallback needed.
|
|
645
|
-
3. **Constraints**: commit the completed work per ADR-014. Do NOT push, do NOT run `push:watch`, do NOT run `release:watch` — the orchestrator's Step 6.5 owns release cadence. Do NOT invoke `capture-*` background skills mid-iter (AFK carve-out — ADR-032), **EXCEPT** (a) **retro-surfaced observations of recurring class-of-behaviour** — those route to `/wr-itil:capture-problem` per the **P342 mechanical-stage carve-out** (see retro-on-exit constraint #4 below; same trust-boundary as `/wr-retrospective:run-retro` Step 4a verification close-on-evidence — P342); and (b) **the I13 fix-time RFC auto-create** — when the propose-fix gate inside the delegated `/wr-itil:manage-problem` traversal detects an RFC-less Known Error (`wr-itil-check-fix-rfc-trace` emits a `no-rfc-trace:` directive), the iter auto-creates a problem-traced skeleton RFC via `/wr-itil:capture-rfc` then proceeds (ADR-073 auto-create-everywhere) — **UNLESS** an existing RFC cited in the ticket is already its fix vehicle and merely lacks the trace edge, in which case the iter **wires** the trace edge into that RFC's `problems:` array (running `wr-itil-update-problem-rfcs-section`) rather than minting a redundant duplicate RFC that fragments the fix (P371; existing-vehicle-untraced sub-case — ADR-073 auto-create is the *no-vehicle* case only; vehicle-vs-merely-related is a judgement read of citation context, structured-logged as `I13: wired P<NNN> trace edge into existing RFC-<NNN>`; the load-bearing branch prose lives in the delegated `/wr-itil:manage-problem` I13 gate). This is NOT an aside-capture distraction: the auto-created RFC is the **mandatory vehicle for THIS iter's own fix** (ADR-071), not a tangential observation — it is in-scope working of the current ticket, framework-mediated (NOT cat-1 direction-setting → NO `AskUserQuestion`, P132), and the loop is NEVER skipped or blocked for a missing RFC (ADR-073). Structured-log the auto-create event to the iter summary (`notes`) per JTBD-006 audit-trail + the ADR-073 reassessment criterion (auto-created-RFC-under-scoped signal feeds `/wr-retrospective:run-retro`). Do NOT use `ScheduleWakeup` under any circumstance (P083 — iteration workers must not self-reschedule). **NEVER call `AskUserQuestion` mid-loop in AFK** (P135 / ADR-044): direction / deviation-approval / one-time-override / silent-framework observations queue at `ITERATION_SUMMARY.outstanding_questions` for loop-end batched presentation. **This includes the manage-problem substance-confirm-before-build guard (ADR-074 (Confirm a decision's substance before building dependent work)):** when the propose-fix step detects that the fix builds on a born-`proposed` decision whose substance is unconfirmed (via `wr-architect-is-decision-unconfirmed`), the iter does NOT implement on it and does NOT ask mid-loop — it queues a `category: "direction"` entry naming the unconfirmed ADR + its Decision Outcome for loop-end confirmation, and routes the ticket to `action: skipped`, `skip_reason_category: user-answerable`. Building on the unconfirmed substance instead (or guessing the choice) is the P315 failure this guard exists to prevent. The queued substance-confirm is a legitimate cat-1 direction ask — it is NOT counted as lazy in the Step 2d Ask Hygiene Pass (ADR-074 lazy-count exclusion). Per-iter `AskUserQuestion` calls are sub-contracting framework-resolved decisions back to the user (lazy deferral per Step 2d Ask Hygiene Pass classification). Non-interactive defaults apply per ADR-013 Rule 6 + ADR-044's framework-resolution boundary. **Treat the user as transient** (P130): even when observably present at orchestrator dispatch time, the user may answer one question and disappear for hours; presence is not a reliable signal and is not the goal. The iter's job is to progress the ticket and accumulate questions for batched surfacing — not to ask "is it OK to proceed?" at a mechanical-stage boundary. **Do NOT poll `bats` output with a bats-console-summary regex against TAP-format output** (P146 — bash until-loop-deadlock antipattern). The bats-console-summary line `<N> tests, <M> failures` is emitted ONLY by bats's *default* (non-TAP) formatter; `bats --tap` does not emit a console summary, so a polling loop of shape `until [ -f $OUT ] && grep -qE '^[0-9]+ tests?,' $OUT; do sleep 5; done` spins forever after bats completes (silent deadlock — no error, no exit; recovery requires manual SIGTERM with metadata loss per the P146/P147 stuck-before-emit subclass). When you need to wait on a backgrounded bats run, prefer `wait $bg_pid` (Unix idiom — completion signaled by process exit, no regex required) or, for the Bash tool, `run_in_background=true` + `BashOutput` polling on the tool's exit-state field rather than regex-poll on stdout. If you genuinely must regex-poll TAP output, anchor on the TAP plan line `^[0-9]+\.\.[0-9]+` (e.g. `1..1455`) — TAP's plan line is emitted on completion and is format-stable across bats versions; the bats-console-summary line is not. The console-summary vs TAP-format divergence is the load-bearing detail: `bats` and `bats --tap` produce structurally different stdout, and the antipattern assumes the former when iter dispatch typically uses the latter. **Do NOT poll subprocess completion with `pgrep -f '<pattern>'` inside an `until` / `while` loop** (P232 — self-referential pgrep deadlock; sibling variant of P146). `pgrep -f` matches against the FULL command line of every running process, so the polling loop's own `zsh -c` argument (which contains the literal `pgrep -f '<pattern>'` text) matches itself; with multiple concurrent polling loops, each loop matches the others and spins forever. Worked example of the antipattern: `until ! pgrep -f 'bats --recursive' > /dev/null 2>&1; do sleep 5; done` — the 2026-05-16 P232 deadlock witness; 4 concurrent polling loops each matched the others' command lines while no actual bats process ran; 45 min wall-clock + $20-30 wasted before manual SIGTERM. The same self-reference shape applies to `while pgrep -f ...; do sleep; done` and to `until ! pkill -0 -f '<pattern>'` / `while pkill -0 -f '<pattern>'` (signal-0 polling). The structural fix is the same as P146: prefer `wait $bg_pid` (Unix idiom — shell-native completion signal, no regex / no pgrep) or Bash-tool `run_in_background=true` + `BashOutput` polling (harness-tracked completion state). The hook `packages/itil/hooks/itil-bash-polling-antipattern-detect.sh` denies these shapes at PreToolUse:Bash, but the prompt rule belongs here too — structural enforcement + prompt discipline together close the class. **Do NOT leave a backgrounded task unreaped at turn-end** (`run_in_background: true` on an Agent or Bash tool call, or a `&`-detached shell job, whose completion you intend to observe in a *later* turn) inside iter dispatch contexts (P370 — turn-end-mid-background work-loss; sibling-class to P083 / P146 / P232). The iter subprocess is dispatched via `claude -p`, a single-shot CLI invocation with NO auto-resume affordance: its turn boundary IS its process boundary. A background task that outlives the turn never resumes — the iter exits at turn-end with the task incomplete and its own work staged but uncommitted (witnessed: iter 11 of a prior loop — $8.02 / 17 min / 8 staged files / 11 GREEN bats / ZERO commits; recovery required orchestrator main-turn salvage). **The prohibition is on the cross-turn / turn-end-survivor shape, NOT on backgrounding per se:** the P146/P232-sanctioned idiom of launching `run_in_background=true` + `BashOutput`-poll-then-`wait $bg_pid` (or plain `wait $bg_pid` on a `&` job) **within the same turn** is fine — it reaps the task before turn-end. Use foreground-synchronous invocation instead: the Agent tool WITHOUT `run_in_background: true` (the result returns in-turn, so the commit step is reached), or intra-turn background that you `wait` on before the turn closes. The distinction from the P146/P232 polling antipatterns: those forbid *how* you wait (regex / pgrep poll loops); this forbids *deferring a task's completion past the turn boundary*, where `claude -p` has no notification re-entry to bring you back. The interactive Claude Code session masks this hazard (notification-driven re-entry); the AFK iter subprocess does not. **If the fix changes shippable code or package behaviour** (any path under `packages/<plugin>/{src,bin,hooks,skills,scripts,lib,agents}` excluding test paths — `test/`, `hooks/test/`, `scripts/test/` — and excluding `README.md` + `docs/*.md`), **the iter MUST author a `.changeset/*.md` entry in the same single ADR-014-grain commit as the fix** (the changeset names the bumping plugin via the YAML frontmatter `"@windyroad/<plugin>": <patch|minor|major>` per the changesets-action contract). **Doc-only changes** (under `docs/`, `*.md`) **and test-only changes** (under any `test/` path) **that ship no behaviour MAY omit the changeset**. The orchestrator's Step 6.5 release-cadence drain runs `release:watch` only when `.changeset/` is non-empty after push — without an iter-authored changeset, code-shape fixes accumulate without ever shipping to npm (violating JTBD-006's audit-trail expectation + JTBD-007's "Keep Plugins Current" closure dependency). Hook `packages/itil/hooks/itil-changeset-discipline.sh` (P141) provides hook-level enforcement at `git commit` time as defence-in-depth — but plugin hook execution depends on the marketplace cache carrying the current hook version, so the prompt-time constraint here MUST land independently (composes-with the hook; does NOT rely on the hook being installed). Inbound-reported from downstream consumer bbstats as their P195 — see [Related](#related) for `**Origin**: inbound-reported (bbstats#195)` per ADR-076. **`@jtbd JTBD-006`** (load-bearing) **`@jtbd JTBD-007`** (closure-dependent).
|
|
645
|
+
3. **Constraints**: commit the completed work per ADR-014. Do NOT push, do NOT run `push:watch`, do NOT run `release:watch` — the orchestrator's Step 6.5 owns release cadence. Do NOT invoke `capture-*` background skills mid-iter (AFK carve-out — ADR-032), **EXCEPT** (a) **retro-surfaced observations of recurring class-of-behaviour** — those route to `/wr-itil:capture-problem` per the **P342 mechanical-stage carve-out** (see retro-on-exit constraint #4 below; same trust-boundary as `/wr-retrospective:run-retro` Step 4a verification close-on-evidence — P342); and (b) **the I13 fix-time RFC auto-create** — when the propose-fix gate inside the delegated `/wr-itil:manage-problem` traversal detects an RFC-less Known Error (`wr-itil-check-fix-rfc-trace` emits a `no-rfc-trace:` directive), the iter auto-creates AND fully authors a problem-traced RFC via `/wr-itil:capture-rfc --fix-time` (authoring a populated `## Scope` + real `## Tasks` from the problem's RCA + Fix Strategy, NOT an empty skeleton — ADR-073 P399; the deferred "flesh out later" step never self-fires, P375) then proceeds (ADR-073 auto-create-everywhere) — **UNLESS** an existing RFC cited in the ticket is already its fix vehicle and merely lacks the trace edge, in which case the iter **wires** the trace edge into that RFC's `problems:` array (running `wr-itil-update-problem-rfcs-section`) rather than minting a redundant duplicate RFC that fragments the fix (P371; existing-vehicle-untraced sub-case — ADR-073 auto-create is the *no-vehicle* case only; vehicle-vs-merely-related is a judgement read of citation context, structured-logged as `I13: wired P<NNN> trace edge into existing RFC-<NNN>`; the load-bearing branch prose lives in the delegated `/wr-itil:manage-problem` I13 gate). This is NOT an aside-capture distraction: the auto-created RFC is the **mandatory vehicle for THIS iter's own fix** (ADR-071), not a tangential observation — it is in-scope working of the current ticket, framework-mediated (NOT cat-1 direction-setting → NO `AskUserQuestion`, P132), and the loop is NEVER skipped or blocked for a missing RFC (ADR-073). Structured-log the auto-create event to the iter summary (`notes`) per JTBD-006 audit-trail + the ADR-073 reassessment criterion (auto-created-RFC-under-scoped signal feeds `/wr-retrospective:run-retro`). Do NOT use `ScheduleWakeup` under any circumstance (P083 — iteration workers must not self-reschedule). **NEVER call `AskUserQuestion` mid-loop in AFK** (P135 / ADR-044): direction / deviation-approval / one-time-override / silent-framework observations queue at `ITERATION_SUMMARY.outstanding_questions` for loop-end batched presentation. **This includes the manage-problem substance-confirm-before-build guard (ADR-074 (Confirm a decision's substance before building dependent work)):** when the propose-fix step detects that the fix builds on a born-`proposed` decision whose substance is unconfirmed (via `wr-architect-is-decision-unconfirmed`), the iter does NOT implement on it and does NOT ask mid-loop — it queues a `category: "direction"` entry naming the unconfirmed ADR + its Decision Outcome for loop-end confirmation, and routes the ticket to `action: skipped`, `skip_reason_category: user-answerable`. Building on the unconfirmed substance instead (or guessing the choice) is the P315 failure this guard exists to prevent. The queued substance-confirm is a legitimate cat-1 direction ask — it is NOT counted as lazy in the Step 2d Ask Hygiene Pass (ADR-074 lazy-count exclusion). Per-iter `AskUserQuestion` calls are sub-contracting framework-resolved decisions back to the user (lazy deferral per Step 2d Ask Hygiene Pass classification). Non-interactive defaults apply per ADR-013 Rule 6 + ADR-044's framework-resolution boundary. **Treat the user as transient** (P130): even when observably present at orchestrator dispatch time, the user may answer one question and disappear for hours; presence is not a reliable signal and is not the goal. The iter's job is to progress the ticket and accumulate questions for batched surfacing — not to ask "is it OK to proceed?" at a mechanical-stage boundary. **Do NOT poll `bats` output with a bats-console-summary regex against TAP-format output** (P146 — bash until-loop-deadlock antipattern). The bats-console-summary line `<N> tests, <M> failures` is emitted ONLY by bats's *default* (non-TAP) formatter; `bats --tap` does not emit a console summary, so a polling loop of shape `until [ -f $OUT ] && grep -qE '^[0-9]+ tests?,' $OUT; do sleep 5; done` spins forever after bats completes (silent deadlock — no error, no exit; recovery requires manual SIGTERM with metadata loss per the P146/P147 stuck-before-emit subclass). When you need to wait on a backgrounded bats run, prefer `wait $bg_pid` (Unix idiom — completion signaled by process exit, no regex required) or, for the Bash tool, `run_in_background=true` + `BashOutput` polling on the tool's exit-state field rather than regex-poll on stdout. If you genuinely must regex-poll TAP output, anchor on the TAP plan line `^[0-9]+\.\.[0-9]+` (e.g. `1..1455`) — TAP's plan line is emitted on completion and is format-stable across bats versions; the bats-console-summary line is not. The console-summary vs TAP-format divergence is the load-bearing detail: `bats` and `bats --tap` produce structurally different stdout, and the antipattern assumes the former when iter dispatch typically uses the latter. **Do NOT poll subprocess completion with `pgrep -f '<pattern>'` inside an `until` / `while` loop** (P232 — self-referential pgrep deadlock; sibling variant of P146). `pgrep -f` matches against the FULL command line of every running process, so the polling loop's own `zsh -c` argument (which contains the literal `pgrep -f '<pattern>'` text) matches itself; with multiple concurrent polling loops, each loop matches the others and spins forever. Worked example of the antipattern: `until ! pgrep -f 'bats --recursive' > /dev/null 2>&1; do sleep 5; done` — the 2026-05-16 P232 deadlock witness; 4 concurrent polling loops each matched the others' command lines while no actual bats process ran; 45 min wall-clock + $20-30 wasted before manual SIGTERM. The same self-reference shape applies to `while pgrep -f ...; do sleep; done` and to `until ! pkill -0 -f '<pattern>'` / `while pkill -0 -f '<pattern>'` (signal-0 polling). The structural fix is the same as P146: prefer `wait $bg_pid` (Unix idiom — shell-native completion signal, no regex / no pgrep) or Bash-tool `run_in_background=true` + `BashOutput` polling (harness-tracked completion state). The hook `packages/itil/hooks/itil-bash-polling-antipattern-detect.sh` denies these shapes at PreToolUse:Bash, but the prompt rule belongs here too — structural enforcement + prompt discipline together close the class. **Do NOT leave a backgrounded task unreaped at turn-end** (`run_in_background: true` on an Agent or Bash tool call, or a `&`-detached shell job, whose completion you intend to observe in a *later* turn) inside iter dispatch contexts (P370 — turn-end-mid-background work-loss; sibling-class to P083 / P146 / P232). The iter subprocess is dispatched via `claude -p`, a single-shot CLI invocation with NO auto-resume affordance: its turn boundary IS its process boundary. A background task that outlives the turn never resumes — the iter exits at turn-end with the task incomplete and its own work staged but uncommitted (witnessed: iter 11 of a prior loop — $8.02 / 17 min / 8 staged files / 11 GREEN bats / ZERO commits; recovery required orchestrator main-turn salvage). **The prohibition is on the cross-turn / turn-end-survivor shape, NOT on backgrounding per se:** the P146/P232-sanctioned idiom of launching `run_in_background=true` + `BashOutput`-poll-then-`wait $bg_pid` (or plain `wait $bg_pid` on a `&` job) **within the same turn** is fine — it reaps the task before turn-end. Use foreground-synchronous invocation instead: the Agent tool WITHOUT `run_in_background: true` (the result returns in-turn, so the commit step is reached), or intra-turn background that you `wait` on before the turn closes. The distinction from the P146/P232 polling antipatterns: those forbid *how* you wait (regex / pgrep poll loops); this forbids *deferring a task's completion past the turn boundary*, where `claude -p` has no notification re-entry to bring you back. The interactive Claude Code session masks this hazard (notification-driven re-entry); the AFK iter subprocess does not. **If the fix changes shippable code or package behaviour** (any path under `packages/<plugin>/{src,bin,hooks,skills,scripts,lib,agents}` excluding test paths — `test/`, `hooks/test/`, `scripts/test/` — and excluding `README.md` + `docs/*.md`), **the iter MUST author a `.changeset/*.md` entry in the same single ADR-014-grain commit as the fix** (the changeset names the bumping plugin via the YAML frontmatter `"@windyroad/<plugin>": <patch|minor|major>` per the changesets-action contract). **Doc-only changes** (under `docs/`, `*.md`) **and test-only changes** (under any `test/` path) **that ship no behaviour MAY omit the changeset**. The orchestrator's Step 6.5 release-cadence drain runs `release:watch` only when `.changeset/` is non-empty after push — without an iter-authored changeset, code-shape fixes accumulate without ever shipping to npm (violating JTBD-006's audit-trail expectation + JTBD-007's "Keep Plugins Current" closure dependency). Hook `packages/itil/hooks/itil-changeset-discipline.sh` (P141) provides hook-level enforcement at `git commit` time as defence-in-depth — but plugin hook execution depends on the marketplace cache carrying the current hook version, so the prompt-time constraint here MUST land independently (composes-with the hook; does NOT rely on the hook being installed). Inbound-reported from downstream consumer bbstats as their P195 — see [Related](#related) for `**Origin**: inbound-reported (bbstats#195)` per ADR-076. **`@jtbd JTBD-006`** (load-bearing) **`@jtbd JTBD-007`** (closure-dependent).
|
|
646
646
|
4. **Retro-on-exit (P086) + retro-surfaced observation classification (P342) + iter-owned BRIEFING commit (P212)**: before emitting `ITERATION_SUMMARY`, invoke `/wr-retrospective:run-retro`. Retro runs INSIDE this subprocess so its Step 2b pipeline-instability scan has access to the iteration's rich tool-call history (hook misbehaviour, repeat-workaround patterns, subagent-delegation friction, release-path instability). Tickets retro creates ride a separate path: they delegate through `/wr-itil:manage-problem` which IS ADR-014 in-scope and self-commits each ticket per its own Step 11. Those commits land independently and the orchestrator picks them up on the next Step 1 scan.
|
|
647
647
|
|
|
648
648
|
**BRIEFING.md commit responsibility — iter owns, run-retro does not (P212).** run-retro is explicitly out-of-scope for self-commit per ADR-014's Scope section (which lists `packages/retrospective/skills/run-retro/SKILL.md` under "Out of scope for now"). Retro therefore EDITS but DOES NOT COMMIT `docs/BRIEFING.md` / `docs/briefing/*.md`. The iter subprocess (NOT run-retro, NOT the orchestrator main turn) owns the BRIEFING commit. After retro completes, run `git status --porcelain docs/BRIEFING.md docs/briefing/`. If non-empty, the iter:
|