@windyroad/itil 0.35.13 → 0.35.14-preview.416
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/lib/create-gate.sh +33 -0
- package/hooks/lib/session-id.sh +73 -0
- package/hooks/test/manage-problem-enforce-create.bats +77 -0
- package/hooks/test/session-id.bats +93 -0
- package/package.json +1 -1
- package/scripts/check-rfc-rejected-alternatives.sh +74 -0
- package/scripts/test/check-rfc-rejected-alternatives.bats +108 -0
- package/skills/capture-problem/SKILL.md +3 -3
- package/skills/capture-rfc/SKILL.md +4 -4
- package/skills/manage-problem/SKILL.md +7 -5
- package/skills/manage-rfc/SKILL.md +6 -4
package/hooks/lib/create-gate.sh
CHANGED
|
@@ -52,6 +52,39 @@ mark_step2_complete() {
|
|
|
52
52
|
: > "/tmp/manage-problem-grep-${SESSION_ID}"
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
# P260 / ADR-050 Option C: write the Step 2 grep marker under EVERY
|
|
56
|
+
# candidate SID read from stdin (one UUID per line, as emitted by
|
|
57
|
+
# `get_candidate_session_ids` in lib/session-id.sh).
|
|
58
|
+
#
|
|
59
|
+
# Under concurrent orchestrator+subprocess sessions in the same project,
|
|
60
|
+
# the per-machine runtime-sid marker is last-writer-wins, so agent-side
|
|
61
|
+
# code cannot reliably PREDICT which single SID the create-gate hook will
|
|
62
|
+
# read from the Write's stdin (ADR-050 §Context). Marking under every recent
|
|
63
|
+
# candidate guarantees a matching marker exists whichever SID the hook reads,
|
|
64
|
+
# eliminating the P260 marker-mismatch deny without a process-topology
|
|
65
|
+
# assumption. The candidate set is bounded to recent announce markers + the
|
|
66
|
+
# runtime-sid value by `get_candidate_session_ids` (NOT a global fail-open —
|
|
67
|
+
# the P119 audit invariant holds: each marker still records that THIS session
|
|
68
|
+
# ran the duplicate-check grep).
|
|
69
|
+
#
|
|
70
|
+
# Reuses the unchanged single-SID `mark_step2_complete` per candidate (same
|
|
71
|
+
# idempotent `/tmp/manage-problem-grep-${SID}` marker class — no new
|
|
72
|
+
# convention). Blank/whitespace lines are skipped. Returns 0 if at least one
|
|
73
|
+
# marker was written, 1 if no candidate SIDs were supplied (fail-closed
|
|
74
|
+
# parity with the empty-SID single-write no-op — the subsequent Write would
|
|
75
|
+
# be denied and the agent recovers by re-running Step 2).
|
|
76
|
+
#
|
|
77
|
+
# Usage: get_candidate_session_ids | mark_step2_complete_candidates
|
|
78
|
+
mark_step2_complete_candidates() {
|
|
79
|
+
local sid count=0
|
|
80
|
+
while IFS= read -r sid; do
|
|
81
|
+
[ -n "$sid" ] || continue
|
|
82
|
+
mark_step2_complete "$sid"
|
|
83
|
+
count=$((count + 1))
|
|
84
|
+
done
|
|
85
|
+
[ "$count" -gt 0 ]
|
|
86
|
+
}
|
|
87
|
+
|
|
55
88
|
# Returns 0 if the RFC capture-step marker exists for SESSION_ID; 1 otherwise.
|
|
56
89
|
# Empty SESSION_ID => returns 1 (no marker).
|
|
57
90
|
#
|
package/hooks/lib/session-id.sh
CHANGED
|
@@ -153,3 +153,76 @@ get_current_session_id() {
|
|
|
153
153
|
|
|
154
154
|
return 1
|
|
155
155
|
}
|
|
156
|
+
|
|
157
|
+
# P260 / ADR-050 Option C: bounded multi-UUID candidate enumeration.
|
|
158
|
+
#
|
|
159
|
+
# Echoes EVERY candidate session UUID — one per line, deduplicated — that
|
|
160
|
+
# the create-gate hook (manage-problem-enforce-create.sh) might read from
|
|
161
|
+
# the Write's stdin `session_id`. The create-gate marker-write
|
|
162
|
+
# (`mark_step2_complete_candidates`, lib/create-gate.sh) writes the marker
|
|
163
|
+
# under each, so whichever SID the hook reads, a matching marker provably
|
|
164
|
+
# exists.
|
|
165
|
+
#
|
|
166
|
+
# Why enumerate instead of picking one (get_current_session_id):
|
|
167
|
+
# `/wr-itil:work-problems` Step 5 BACKGROUNDS the iter subprocess and runs
|
|
168
|
+
# the orchestrator's poll loop in the main turn, so the orchestrator's
|
|
169
|
+
# PreToolUse hooks fire CONCURRENTLY with the subprocess. Both sessions
|
|
170
|
+
# write the same per-machine runtime-sid marker (same project => same
|
|
171
|
+
# proj_hash), last-writer-wins. When the orchestrator captures a ticket
|
|
172
|
+
# while the subprocess holds the runtime-sid, `get_current_session_id`
|
|
173
|
+
# returns the SUBPROCESS SID, but the orchestrator's Write carries the
|
|
174
|
+
# ORCHESTRATOR SID on its stdin — marker mismatch, create-gate deny (P260).
|
|
175
|
+
# ADR-050 §Context establishes that no agent-side algorithm can PREDICT
|
|
176
|
+
# the right single SID from filesystem state alone. Option C stops
|
|
177
|
+
# predicting and writes under every recent candidate instead.
|
|
178
|
+
#
|
|
179
|
+
# Candidate set (each line is one UUID):
|
|
180
|
+
# 1. `get_current_session_id`'s pick (env-var > runtime-sid > announce-
|
|
181
|
+
# marker priority). Emitting this FIRST guarantees the candidate set is
|
|
182
|
+
# never a strict subset of the prior single-SID behaviour — Option C
|
|
183
|
+
# only ADDS the concurrent-session SIDs.
|
|
184
|
+
# 2. Every announce-marker UUID across ALL systems whose marker mtime is
|
|
185
|
+
# within the window (the concurrently-active sessions: orchestrator +
|
|
186
|
+
# its running subprocess(es)). Announce markers are write-once-per-
|
|
187
|
+
# session (ADR-038, no touch-refresh), so mtime is the announcing
|
|
188
|
+
# session's first-prompt timestamp — a stable bound.
|
|
189
|
+
#
|
|
190
|
+
# Bounding (NOT a global fail-open):
|
|
191
|
+
# The mtime window (SESSION_CANDIDATE_WINDOW_MINS, default 1440 = 24h)
|
|
192
|
+
# bounds the enumeration against the P124 stale-marker pathology (103
|
|
193
|
+
# accumulated UUIDs selecting the wrong SID). 24h comfortably covers any
|
|
194
|
+
# realistic single AFK loop while excluding multi-day marker accumulation.
|
|
195
|
+
# Extra markers under recently-stale UUIDs are HARMLESS — empty files; the
|
|
196
|
+
# hook only matches the marker equal to the Write's stdin SID. The P119
|
|
197
|
+
# audit invariant holds: every marker still records that THIS session ran
|
|
198
|
+
# the duplicate-check grep (the marker is only written because Step 2's
|
|
199
|
+
# grep provably ran this turn; widening WHICH SID files receive that proof
|
|
200
|
+
# does not weaken the proof). A loop running >24h degrades gracefully to
|
|
201
|
+
# the recoverable create-gate deny (status quo), not silent corruption.
|
|
202
|
+
#
|
|
203
|
+
# Test overrides: SESSION_MARKER_DIR (marker dir, default /tmp) +
|
|
204
|
+
# SESSION_CANDIDATE_WINDOW_MINS (window minutes, default 1440).
|
|
205
|
+
#
|
|
206
|
+
# Usage:
|
|
207
|
+
# source packages/itil/hooks/lib/session-id.sh
|
|
208
|
+
# source packages/itil/hooks/lib/create-gate.sh
|
|
209
|
+
# get_candidate_session_ids | mark_step2_complete_candidates
|
|
210
|
+
get_candidate_session_ids() {
|
|
211
|
+
local marker_dir="${SESSION_MARKER_DIR:-/tmp}"
|
|
212
|
+
local window_mins="${SESSION_CANDIDATE_WINDOW_MINS:-1440}"
|
|
213
|
+
{
|
|
214
|
+
# Guaranteed member: the single-SID discovery's pick. Suppress its
|
|
215
|
+
# non-zero exit (no-SID cold path) so the pipeline still emits the
|
|
216
|
+
# enumerated candidates.
|
|
217
|
+
get_current_session_id 2>/dev/null || true
|
|
218
|
+
|
|
219
|
+
# Concurrent-session SIDs: every recent announce marker across all
|
|
220
|
+
# systems, within the mtime window. `*-announced-*` is system-agnostic
|
|
221
|
+
# (picks up any present or future announcing plugin). `-maxdepth 1` and
|
|
222
|
+
# `-mmin -N` are portable across BSD (macOS) and GNU find. The sed strips
|
|
223
|
+
# the leading path then the `<system>-announced-` prefix, leaving the
|
|
224
|
+
# trailing UUID (UUIDs never contain the literal "-announced-").
|
|
225
|
+
find "$marker_dir" -maxdepth 1 -name '*-announced-*' -mmin "-${window_mins}" 2>/dev/null \
|
|
226
|
+
| sed 's|.*/||; s/.*-announced-//'
|
|
227
|
+
} | awk 'NF && !seen[$0]++'
|
|
228
|
+
}
|
|
@@ -345,3 +345,80 @@ teardown_other_sid_marker() {
|
|
|
345
345
|
[ "$status" -eq 0 ]
|
|
346
346
|
[[ "$output" != *"BLOCKED"* ]]
|
|
347
347
|
}
|
|
348
|
+
|
|
349
|
+
# --- P260: concurrent orchestrator+subprocess create-gate marker race ---
|
|
350
|
+
#
|
|
351
|
+
# /wr-itil:work-problems Step 5 BACKGROUNDS the iter subprocess
|
|
352
|
+
# (`claude -p ... &`) and runs an idle-timeout poll loop in the
|
|
353
|
+
# orchestrator's main turn, so the orchestrator fires PreToolUse hooks
|
|
354
|
+
# CONCURRENTLY with the running subprocess. Both sessions write the same
|
|
355
|
+
# per-machine runtime-sid marker (same project ⇒ same proj_hash),
|
|
356
|
+
# last-writer-wins. When the orchestrator captures a ticket while the
|
|
357
|
+
# subprocess holds the runtime-sid, the single-SID Step-2 marker-write
|
|
358
|
+
# (`sid=$(get_current_session_id) && mark_step2_complete "$sid"`) lands the
|
|
359
|
+
# marker under the SUBPROCESS SID, but the orchestrator's Write fires the
|
|
360
|
+
# PreToolUse:Write hook whose stdin session_id is the ORCHESTRATOR SID ⇒
|
|
361
|
+
# marker mismatch ⇒ deny. This was P260 (the 2026-05-18 P254/P255 foreground
|
|
362
|
+
# captures). ADR-050 Option C (architect-resolved + human-confirmed
|
|
363
|
+
# 2026-05-26) is the fix: the candidate-set marker-write
|
|
364
|
+
# (`get_candidate_session_ids | mark_step2_complete_candidates`) writes the
|
|
365
|
+
# marker under EVERY recent candidate SID, so whichever SID the hook reads,
|
|
366
|
+
# a matching marker exists.
|
|
367
|
+
#
|
|
368
|
+
# These tests are end-to-end: they run the real agent-side marker-write
|
|
369
|
+
# helpers under the concurrent fixture, then fire the real hook with the
|
|
370
|
+
# orchestrator's stdin SID and assert the observable permit/deny outcome.
|
|
371
|
+
|
|
372
|
+
# Build the concurrent fixture in a sandbox marker dir: orchestrator
|
|
373
|
+
# announced first (older mtime), subprocess announced later (newer mtime),
|
|
374
|
+
# and the subprocess clobbered the runtime-sid marker last.
|
|
375
|
+
p260_setup_concurrent_fixture() {
|
|
376
|
+
P260_SANDBOX=$(mktemp -d)
|
|
377
|
+
P260_ORCH_SID="p260-orch-$$-$RANDOM"
|
|
378
|
+
P260_SUB_SID="p260-sub-$$-$RANDOM"
|
|
379
|
+
: > "$P260_SANDBOX/architect-announced-${P260_ORCH_SID}"
|
|
380
|
+
sleep 1
|
|
381
|
+
: > "$P260_SANDBOX/jtbd-announced-${P260_SUB_SID}"
|
|
382
|
+
printf '%s' "$P260_SUB_SID" > "$P260_SANDBOX/itil-runtime-sid.current"
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
p260_teardown_concurrent_fixture() {
|
|
386
|
+
rm -f "/tmp/manage-problem-grep-${P260_ORCH_SID}" \
|
|
387
|
+
"/tmp/manage-problem-grep-${P260_SUB_SID}"
|
|
388
|
+
rm -rf "$P260_SANDBOX"
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
@test "P260: candidate-set marker-write PERMITS the orchestrator Write despite runtime-sid clobbered to the subprocess SID" {
|
|
392
|
+
p260_setup_concurrent_fixture
|
|
393
|
+
# Agent-side Step 2 marker write under the FULL candidate set (Option C).
|
|
394
|
+
SESSION_MARKER_DIR="$P260_SANDBOX" bash -c "
|
|
395
|
+
source '$SCRIPT_DIR/lib/session-id.sh'
|
|
396
|
+
source '$SCRIPT_DIR/lib/create-gate.sh'
|
|
397
|
+
get_candidate_session_ids | mark_step2_complete_candidates
|
|
398
|
+
"
|
|
399
|
+
# The orchestrator's Write carries the orchestrator SID on its stdin.
|
|
400
|
+
run run_write_hook "$PWD/docs/problems/997-concurrent-capture.open.md" "$P260_ORCH_SID"
|
|
401
|
+
p260_teardown_concurrent_fixture
|
|
402
|
+
[ "$status" -eq 0 ]
|
|
403
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
404
|
+
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
@test "P260 negative control: single-SID marker-write (pre-Option-C behaviour) DENIES the orchestrator Write under the same race" {
|
|
408
|
+
# Pins WHY Option C is needed: the old single-SID write lands the marker
|
|
409
|
+
# under the subprocess SID (the clobbered runtime value), so the
|
|
410
|
+
# orchestrator's Write — stdin = orchestrator SID — is denied. This is
|
|
411
|
+
# the reproduced P260 failure; the candidate-set write above is what
|
|
412
|
+
# fixes it.
|
|
413
|
+
p260_setup_concurrent_fixture
|
|
414
|
+
SESSION_MARKER_DIR="$P260_SANDBOX" bash -c "
|
|
415
|
+
source '$SCRIPT_DIR/lib/session-id.sh'
|
|
416
|
+
source '$SCRIPT_DIR/lib/create-gate.sh'
|
|
417
|
+
sid=\$(get_current_session_id) && mark_step2_complete \"\$sid\"
|
|
418
|
+
"
|
|
419
|
+
run run_write_hook "$PWD/docs/problems/996-concurrent-capture.open.md" "$P260_ORCH_SID"
|
|
420
|
+
p260_teardown_concurrent_fixture
|
|
421
|
+
[ "$status" -eq 0 ]
|
|
422
|
+
[[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
|
|
423
|
+
[[ "$output" == *"BLOCKED"* ]]
|
|
424
|
+
}
|
|
@@ -256,3 +256,96 @@ mark_announced() {
|
|
|
256
256
|
[[ "$output" != *"$marker_uuid"* ]]
|
|
257
257
|
[[ "$output" == *"EXIT:0"* ]]
|
|
258
258
|
}
|
|
259
|
+
|
|
260
|
+
# --- Behavioural contract: candidate enumeration (Option C, P260) ---
|
|
261
|
+
#
|
|
262
|
+
# Under concurrent orchestrator+subprocess sessions in the SAME project,
|
|
263
|
+
# the per-machine runtime-sid marker is last-writer-wins, so the single-SID
|
|
264
|
+
# get_current_session_id cannot reliably predict which SID the create-gate
|
|
265
|
+
# hook will read from the Write's stdin (the orchestrator's Write carries
|
|
266
|
+
# the orchestrator SID; the subprocess may have just clobbered the runtime
|
|
267
|
+
# marker with its own SID). No agent-side algorithm can PREDICT the right
|
|
268
|
+
# single SID from filesystem state alone (ADR-050 §Context). Option C stops
|
|
269
|
+
# predicting: get_candidate_session_ids returns EVERY recent candidate SID
|
|
270
|
+
# — the get_current_session_id pick PLUS every recent announce-marker UUID
|
|
271
|
+
# across all systems within the mtime window, deduplicated — so the marker
|
|
272
|
+
# can be written under all of them and a match provably exists whichever
|
|
273
|
+
# SID the hook reads. Bounded by SESSION_CANDIDATE_WINDOW_MINS (24h default)
|
|
274
|
+
# to avoid the P124 stale-marker pathology (103 accumulated UUIDs); NOT a
|
|
275
|
+
# global fail-open (the P119 audit invariant holds: every marker still
|
|
276
|
+
# records that THIS session ran the duplicate-check grep).
|
|
277
|
+
#
|
|
278
|
+
# Per feedback_behavioural_tests.md (P081): tests assert the emitted
|
|
279
|
+
# candidate set, not the source content of the helper.
|
|
280
|
+
|
|
281
|
+
# Helper: source the helper and emit the candidate SID set (one per line).
|
|
282
|
+
candidates() {
|
|
283
|
+
bash -c "source '$HELPER'; get_candidate_session_ids"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@test "candidates: returns BOTH the orchestrator and subprocess announce-marker UUIDs" {
|
|
287
|
+
orch_uuid="aaaaaaaa-0000-1111-2222-orchestrator0"
|
|
288
|
+
sub_uuid="bbbbbbbb-0000-1111-2222-subprocess00"
|
|
289
|
+
# Orchestrator announced first (loop start); subprocess announced later
|
|
290
|
+
# (dispatched mid-loop) — the subprocess marker has the NEWER mtime,
|
|
291
|
+
# exactly the condition that made the single-SID helper pick the wrong
|
|
292
|
+
# SID and deny the orchestrator's Write (the P260 race). Candidate
|
|
293
|
+
# enumeration must surface BOTH so neither is left out of the marker set.
|
|
294
|
+
mark_announced "architect" "$orch_uuid"
|
|
295
|
+
sleep 1
|
|
296
|
+
mark_announced "jtbd" "$sub_uuid"
|
|
297
|
+
output=$(candidates)
|
|
298
|
+
[[ "$output" == *"$orch_uuid"* ]]
|
|
299
|
+
[[ "$output" == *"$sub_uuid"* ]]
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@test "candidates: includes the runtime-sid value even when no announce marker carries it" {
|
|
303
|
+
rt_uuid="cccccccc-0000-1111-2222-runtimeonly0"
|
|
304
|
+
printf '%s' "$rt_uuid" > "$SANDBOX_TMP/itil-runtime-sid.current"
|
|
305
|
+
output=$(candidates)
|
|
306
|
+
[[ "$output" == *"$rt_uuid"* ]]
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@test "candidates: deduplicates a SID present in both the runtime-sid marker and an announce marker" {
|
|
310
|
+
dup_uuid="dddddddd-0000-1111-2222-duplicated00"
|
|
311
|
+
printf '%s' "$dup_uuid" > "$SANDBOX_TMP/itil-runtime-sid.current"
|
|
312
|
+
mark_announced "architect" "$dup_uuid"
|
|
313
|
+
output=$(candidates)
|
|
314
|
+
# The SID must appear exactly once — not once per source.
|
|
315
|
+
count=$(printf '%s\n' "$output" | grep -c "$dup_uuid")
|
|
316
|
+
[ "$count" -eq 1 ]
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
@test "candidates: announce-marker enumeration excludes markers older than the mtime window" {
|
|
320
|
+
rt_uuid="eeeeeeee-0000-1111-2222-runtimepick0"
|
|
321
|
+
fresh_uuid="ffffffff-0000-1111-2222-freshmarker0"
|
|
322
|
+
stale_uuid="99999999-0000-1111-2222-stalemarker0"
|
|
323
|
+
# runtime-sid wins get_current_session_id (so its single pick is the
|
|
324
|
+
# runtime value, not an announce marker) — this isolates the window
|
|
325
|
+
# behaviour to the announce-marker ENUMERATION step.
|
|
326
|
+
printf '%s' "$rt_uuid" > "$SANDBOX_TMP/itil-runtime-sid.current"
|
|
327
|
+
mark_announced "jtbd" "$fresh_uuid"
|
|
328
|
+
mark_announced "voice-tone" "$stale_uuid"
|
|
329
|
+
# Backdate the stale marker well beyond the window. touch -t is POSIX
|
|
330
|
+
# and portable across BSD (macOS) and GNU find/touch.
|
|
331
|
+
touch -t 202001010000 "$SANDBOX_TMP/voice-tone-announced-${stale_uuid}"
|
|
332
|
+
output=$(SESSION_CANDIDATE_WINDOW_MINS=60 bash -c "source '$HELPER'; get_candidate_session_ids")
|
|
333
|
+
[[ "$output" == *"$rt_uuid"* ]]
|
|
334
|
+
[[ "$output" == *"$fresh_uuid"* ]]
|
|
335
|
+
[[ "$output" != *"$stale_uuid"* ]]
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
@test "candidates: empty output when no markers, no runtime-sid, and no env var" {
|
|
339
|
+
output=$(candidates)
|
|
340
|
+
[ -z "$output" ]
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
@test "candidates: env-var SID is included as a candidate" {
|
|
344
|
+
env_uuid="12121212-0000-1111-2222-envvarsid000"
|
|
345
|
+
other_uuid="34343434-0000-1111-2222-announced000"
|
|
346
|
+
mark_announced "architect" "$other_uuid"
|
|
347
|
+
output=$(CLAUDE_SESSION_ID="$env_uuid" SESSION_MARKER_DIR="$SANDBOX_TMP" bash -c "source '$HELPER'; get_candidate_session_ids")
|
|
348
|
+
[[ "$output" == *"$env_uuid"* ]]
|
|
349
|
+
# The concurrent announce marker is still enumerated alongside the env SID.
|
|
350
|
+
[[ "$output" == *"$other_uuid"* ]]
|
|
351
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-rfc-rejected-alternatives.sh — ADR-052 behavioural lint enforcing
|
|
3
|
+
# ADR-070 (RFCs hold no independent decisions).
|
|
4
|
+
#
|
|
5
|
+
# Invariant (ADR-070 § Confirmation): no RFC body in the RFC directory
|
|
6
|
+
# contains a "Considered Options / Alternatives Rejected" block WITHOUT a
|
|
7
|
+
# matching `adrs:` frontmatter reference. ADR-070 line 44 names the
|
|
8
|
+
# machine-detectable tell: "an RFC body containing a rejected-alternatives
|
|
9
|
+
# block with no matching `adrs:` reference is a decision masquerading as
|
|
10
|
+
# scope." Contested choices belong in an ADR (referenced via `adrs:`),
|
|
11
|
+
# never re-argued in the RFC body.
|
|
12
|
+
#
|
|
13
|
+
# This is an ARTEFACT-STATE behavioural check (ADR-052): given an RFC
|
|
14
|
+
# corpus directory, it inspects the on-disk RFC bodies + frontmatter and
|
|
15
|
+
# reports violations. It is NOT a structural grep of any SKILL.md / agent
|
|
16
|
+
# prose. Detection targets a markdown HEADING block (`## ... Considered
|
|
17
|
+
# Options ...` / `## ... Alternatives Rejected ...`), never a prose mention
|
|
18
|
+
# of the phrase (e.g. a retrofit note explaining the section was removed).
|
|
19
|
+
#
|
|
20
|
+
# Usage: check-rfc-rejected-alternatives.sh [rfcs-dir] (default docs/rfcs)
|
|
21
|
+
# Exit: 0 = clean (no violations); 1 = ≥1 violation; 2 = usage/dir error.
|
|
22
|
+
#
|
|
23
|
+
# Scope: docs/rfcs/ only. ADRs (docs/decisions/) legitimately carry
|
|
24
|
+
# "Considered Options" headings — they ARE the decision ledger; this lint
|
|
25
|
+
# never scans them.
|
|
26
|
+
#
|
|
27
|
+
# @adr ADR-070 (RFCs hold no independent decisions — the invariant)
|
|
28
|
+
# @adr ADR-052 (behavioural-tests-default — artefact-state assertion)
|
|
29
|
+
# @adr ADR-049 (plugin-bundled scripts; adopters run this over their docs/rfcs)
|
|
30
|
+
# @problem P310 (RFC decisions invisible to the ADR-066 oversight net)
|
|
31
|
+
|
|
32
|
+
set -euo pipefail
|
|
33
|
+
|
|
34
|
+
rfcs_dir="${1:-docs/rfcs}"
|
|
35
|
+
|
|
36
|
+
if [ ! -d "$rfcs_dir" ]; then
|
|
37
|
+
echo "check-rfc-rejected-alternatives: not a directory: $rfcs_dir" >&2
|
|
38
|
+
exit 2
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Heading-block detector: a markdown ATX heading (1-6 '#') whose text
|
|
42
|
+
# contains "Considered Options" or "Alternatives Rejected" (case-insensitive).
|
|
43
|
+
# Anchored to '^#' so a prose/blockquote mention of the phrase never matches.
|
|
44
|
+
heading_re='^#{1,6}[[:space:]]+.*([Cc]onsidered [Oo]ptions|[Aa]lternatives [Rr]ejected)'
|
|
45
|
+
|
|
46
|
+
# adrs: frontmatter is non-empty when its line carries ≥1 ADR-<NNN> token.
|
|
47
|
+
# (`adrs:` only appears as a line-leading key in the YAML frontmatter; an
|
|
48
|
+
# RFC body never starts a line with `adrs:`.)
|
|
49
|
+
adrs_re='^adrs:.*ADR-[0-9]'
|
|
50
|
+
|
|
51
|
+
violations=0
|
|
52
|
+
scanned=0
|
|
53
|
+
|
|
54
|
+
# Iterate RFC files (RFC-*.md) in the directory. Sorted for stable output.
|
|
55
|
+
shopt -s nullglob
|
|
56
|
+
for f in "$rfcs_dir"/RFC-*.md; do
|
|
57
|
+
scanned=$((scanned + 1))
|
|
58
|
+
if grep -qE "$heading_re" "$f"; then
|
|
59
|
+
if ! grep -qE "$adrs_re" "$f"; then
|
|
60
|
+
line=$(grep -nE "$heading_re" "$f" | head -1 | cut -d: -f1)
|
|
61
|
+
echo "VIOLATION $f:${line} rejected-alternatives block with empty/absent adrs: frontmatter (ADR-070)"
|
|
62
|
+
violations=$((violations + 1))
|
|
63
|
+
fi
|
|
64
|
+
fi
|
|
65
|
+
done
|
|
66
|
+
shopt -u nullglob
|
|
67
|
+
|
|
68
|
+
if [ "$violations" -gt 0 ]; then
|
|
69
|
+
echo "check-rfc-rejected-alternatives: $violations violation(s) across $scanned RFC(s) — an RFC carrying a rejected-alternatives block must reference its governing ADR(s) in adrs: (ADR-070)." >&2
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
echo "check-rfc-rejected-alternatives: clean ($scanned RFC(s) scanned; no rejected-alternatives block without adrs: reference)."
|
|
74
|
+
exit 0
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# @problem P310 — RFCs carry independent decisions invisible to the ADR-066
|
|
4
|
+
# human-oversight net. ADR-070 closes the blind spot; this lint enforces it.
|
|
5
|
+
# @adr ADR-070 (RFCs hold no independent decisions — the invariant under test)
|
|
6
|
+
# @adr ADR-052 (behavioural-tests-default — this is an artefact-state behavioural
|
|
7
|
+
# assertion: it RUNS the checker against fixture RFC corpora + the real corpus
|
|
8
|
+
# and asserts the verdict (exit code + output). It does NOT structurally grep
|
|
9
|
+
# the checker's own source, the SKILL.md, or any agent prose — that would be
|
|
10
|
+
# the P081 structural-test-disguised-as-behavioural anti-pattern.)
|
|
11
|
+
# @adr ADR-071 (every fix via RFC — composes; the lint guards the RFC corpus)
|
|
12
|
+
#
|
|
13
|
+
# Contract under test (ADR-070 § Confirmation): no RFC body in docs/rfcs/
|
|
14
|
+
# contains a "Considered Options / Alternatives Rejected" HEADING block
|
|
15
|
+
# without a matching `adrs:` frontmatter reference. The detector targets a
|
|
16
|
+
# markdown heading, never a prose mention of the phrase.
|
|
17
|
+
|
|
18
|
+
setup() {
|
|
19
|
+
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
|
|
20
|
+
SCRIPT="$REPO_ROOT/packages/itil/scripts/check-rfc-rejected-alternatives.sh"
|
|
21
|
+
FIXTURE_DIR="$(mktemp -d)"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
teardown() {
|
|
25
|
+
rm -rf "$FIXTURE_DIR"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# ── Fixture helper: write an RFC file with given adrs-line + body ────────────
|
|
29
|
+
write_rfc() {
|
|
30
|
+
local name="$1" adrs_line="$2" body="$3"
|
|
31
|
+
cat > "$FIXTURE_DIR/$name" <<EOF
|
|
32
|
+
---
|
|
33
|
+
status: accepted
|
|
34
|
+
rfc-id: ${name%.md}
|
|
35
|
+
problems: [P999]
|
|
36
|
+
$adrs_line
|
|
37
|
+
stories: []
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
# ${name%.md}: fixture
|
|
41
|
+
|
|
42
|
+
## Summary
|
|
43
|
+
|
|
44
|
+
A fixture RFC.
|
|
45
|
+
|
|
46
|
+
$body
|
|
47
|
+
EOF
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@test "violation: rejected-alternatives heading block WITH empty adrs: -> exit 1 + VIOLATION" {
|
|
51
|
+
write_rfc "RFC-901-bad.md" "adrs: []" $'## Considered Options / Alternatives Rejected\n\n- F1 alternative rejected: foo.'
|
|
52
|
+
run bash "$SCRIPT" "$FIXTURE_DIR"
|
|
53
|
+
[ "$status" -eq 1 ]
|
|
54
|
+
[[ "$output" == *"VIOLATION"* ]]
|
|
55
|
+
[[ "$output" == *"RFC-901-bad.md"* ]]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@test "allowed: rejected-alternatives heading block WITH a matching adrs: reference -> exit 0 (clean)" {
|
|
59
|
+
write_rfc "RFC-902-homed.md" "adrs: [ADR-072, ADR-073]" $'## Considered Options / Alternatives Rejected\n\n- The contested choice is recorded in ADR-072; this block references it.'
|
|
60
|
+
run bash "$SCRIPT" "$FIXTURE_DIR"
|
|
61
|
+
[ "$status" -eq 0 ]
|
|
62
|
+
[[ "$output" == *"clean"* ]]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@test "clean: no rejected-alternatives block, empty adrs: -> exit 0" {
|
|
66
|
+
write_rfc "RFC-903-scope.md" "adrs: []" $'## Scope\n\nPure scope + decomposition + traces. No decisions here.'
|
|
67
|
+
run bash "$SCRIPT" "$FIXTURE_DIR"
|
|
68
|
+
[ "$status" -eq 0 ]
|
|
69
|
+
[[ "$output" == *"clean"* ]]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@test "prose mention (not a heading) of the phrase does NOT trigger -> exit 0" {
|
|
73
|
+
# Guards the RFC-005 retrofit-banner false positive: a blockquote/prose line
|
|
74
|
+
# mentioning the struck section must not be flagged.
|
|
75
|
+
write_rfc "RFC-904-retrofit.md" "adrs: []" $'> Retrofitted: this RFC originally carried a "Considered Options / Alternatives Rejected" section, now struck per ADR-070.'
|
|
76
|
+
run bash "$SCRIPT" "$FIXTURE_DIR"
|
|
77
|
+
[ "$status" -eq 0 ]
|
|
78
|
+
[[ "$output" == *"clean"* ]]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@test "variant heading 'Alternatives Rejected' WITH empty adrs: -> exit 1" {
|
|
82
|
+
write_rfc "RFC-905-variant.md" "adrs: []" $'### Alternatives Rejected\n\n- rejected: bar.'
|
|
83
|
+
run bash "$SCRIPT" "$FIXTURE_DIR"
|
|
84
|
+
[ "$status" -eq 1 ]
|
|
85
|
+
[[ "$output" == *"RFC-905-variant.md"* ]]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@test "mixed corpus: one violation among clean RFCs -> exit 1, names only the offender" {
|
|
89
|
+
write_rfc "RFC-906-ok.md" "adrs: [ADR-001]" $'## Considered Options\n\n- references ADR-001.'
|
|
90
|
+
write_rfc "RFC-907-bad.md" "adrs: []" $'## Considered Options\n\n- no adr.'
|
|
91
|
+
write_rfc "RFC-908-scope.md" "adrs: []" $'## Scope\n\nclean.'
|
|
92
|
+
run bash "$SCRIPT" "$FIXTURE_DIR"
|
|
93
|
+
[ "$status" -eq 1 ]
|
|
94
|
+
[[ "$output" == *"RFC-907-bad.md"* ]]
|
|
95
|
+
[[ "$output" != *"RFC-906-ok.md"* ]]
|
|
96
|
+
[[ "$output" != *"RFC-908-scope.md"* ]]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@test "non-existent directory -> exit 2 (usage error)" {
|
|
100
|
+
run bash "$SCRIPT" "$FIXTURE_DIR/does-not-exist"
|
|
101
|
+
[ "$status" -eq 2 ]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@test "dogfood: the real docs/rfcs/ corpus is clean -> exit 0" {
|
|
105
|
+
run bash "$SCRIPT" "$REPO_ROOT/docs/rfcs"
|
|
106
|
+
[ "$status" -eq 0 ]
|
|
107
|
+
[[ "$output" == *"clean"* ]]
|
|
108
|
+
}
|
|
@@ -153,15 +153,15 @@ The **3-keyword cap** is a hard-coded constant. Do NOT make it env-overridable
|
|
|
153
153
|
|
|
154
154
|
If matches are found: list them in the final report. **Do NOT halt or branch.** Capture proceeds. The user can resolve duplicates at the next `/wr-itil:review-problems` invocation (or invoke `/wr-itil:manage-problem` directly if the duplicate-check shape needs a structured branch).
|
|
155
155
|
|
|
156
|
-
**After the grep completes**, write the per-session create-gate marker so the `PreToolUse:Write` hook (P119) permits the subsequent Write of the new `.open.md` file:
|
|
156
|
+
**After the grep completes**, write the per-session create-gate marker so the `PreToolUse:Write` hook (P119) permits the subsequent Write of the new `.open.md` file. Per **P260 / ADR-050 Option C**, write it under EVERY recent candidate session SID (not just one) so a concurrent orchestrator+subprocess race cannot land the marker under the wrong UUID:
|
|
157
157
|
|
|
158
158
|
```bash
|
|
159
159
|
source packages/itil/hooks/lib/session-id.sh
|
|
160
160
|
source packages/itil/hooks/lib/create-gate.sh
|
|
161
|
-
|
|
161
|
+
get_candidate_session_ids | mark_step2_complete_candidates
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
-
The marker is shared between `manage-problem` and `capture-problem` per ADR-032 amendment — same `/tmp/manage-problem-grep-${SESSION_ID}` path, idempotent across cross-skill ordering.
|
|
164
|
+
The marker is shared between `manage-problem` and `capture-problem` per ADR-032 amendment — same `/tmp/manage-problem-grep-${SESSION_ID}` path, idempotent across cross-skill ordering. `get_candidate_session_ids` enumerates the `get_current_session_id` pick (P124) plus every recent `/tmp/<system>-announced-<UUID>` UUID within a 24h mtime window, and `mark_step2_complete_candidates` writes the marker under each — so whichever SID the hook reads from the Write's stdin, a matching marker exists. This closes the P260 create-gate race that fires when the orchestrator main turn captures a ticket while a backgrounded iter subprocess holds the per-machine runtime-sid marker (last-writer-wins). The candidate set is bounded to recent same-machine markers — not a global fail-open (the P119 audit invariant holds: each marker still records that THIS session ran the duplicate-check grep). See `/wr-itil:manage-problem` Step 2 substep 7 for the full mechanism.
|
|
165
165
|
|
|
166
166
|
### 3. Compute the next ID
|
|
167
167
|
|
|
@@ -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
|
|
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 deliberately, not auto-fired, because RFC scope is direction-setting per ADR-073 — NOT because atomic fixes skip ceremony).
|
|
14
14
|
|
|
15
15
|
## When to invoke
|
|
16
16
|
|
|
@@ -27,7 +27,7 @@ This skill is one half of the capture-then-manage RFC framework introduced by AD
|
|
|
27
27
|
|
|
28
28
|
**Positional**: `<problem-trace> <description>` where `<problem-trace>` is `P<NNN>` or `P<NNN>,P<NNN>,...` (no spaces inside the trace; multiple problems comma-separated).
|
|
29
29
|
|
|
30
|
-
**Optional flag (Phase 2)**: `--stories STORY-<NNN>,STORY-<NNN>,...` — ORDERED execution sequence per ADR-060 line 262. Cardinality 0..N:
|
|
30
|
+
**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).
|
|
31
31
|
|
|
32
32
|
```
|
|
33
33
|
/wr-itil:capture-rfc P168 Pipeline consume-catalog and bootstrap-from-reports — multi-commit retrofit
|
|
@@ -225,7 +225,7 @@ done
|
|
|
225
225
|
|
|
226
226
|
The helper (`packages/itil/scripts/update-problem-rfcs-section.sh`) is idempotent: running over a current section is a no-op. Lazy-empty discipline applies (zero traced RFCs → section absent) — capture-rfc invocations always have ≥ 1 trace at this step, so this surface always emits a populated section. The `git add` is conditional on the helper actually modifying the file — `cmp -s` no-op-on-current is the helper's idempotency contract; `git add` of an unchanged file is also a no-op.
|
|
227
227
|
|
|
228
|
-
**Phase 2 — render `## Stories` body section on the new RFC** (when `--stories` was provided): the just-written RFC file carries `stories: [STORY-NNN, ...]` in frontmatter; the helper `update-rfc-references-section.sh <rfc-file> "Stories"` renders the forward-trace `## Stories` body section from that frontmatter array in execution order per ADR-060 line 270. Lazy-empty discipline applies — when `stories: []` (
|
|
228
|
+
**Phase 2 — render `## Stories` body section on the new RFC** (when `--stories` was provided): the just-written RFC file carries `stories: [STORY-NNN, ...]` in frontmatter; the helper `update-rfc-references-section.sh <rfc-file> "Stories"` renders the forward-trace `## Stories` body section from that frontmatter array in execution order per ADR-060 line 270. Lazy-empty discipline applies — when `stories: []` (an RFC not decomposed into stories), the helper omits the section entirely:
|
|
229
229
|
|
|
230
230
|
```bash
|
|
231
231
|
if [ -n "$stories_trace" ]; then
|
|
@@ -288,7 +288,7 @@ The two skills share the `/tmp/wr-itil-rfc-capture-grep-${SESSION_ID}` create-ga
|
|
|
288
288
|
- **`docs/plans/170-rfc-framework-story-map.md`** — Slice 2 task B5.T3 lands this skill.
|
|
289
289
|
- **JTBD-008** — Decompose a Fix Into Coordinated Changes. Primary persona-anchor.
|
|
290
290
|
- **JTBD-001** (extended scope) — change-set-level governance composition.
|
|
291
|
-
- **JTBD-101** (atomic-fix-adopter
|
|
291
|
+
- **JTBD-101** (atomic-fix-adopter) — every fix goes through an RFC (ADR-071); capture-rfc is a deliberate aside-invocation, not auto-fired (RFC scope is direction-setting per ADR-073).
|
|
292
292
|
- **`docs/rfcs/README.md`** — RFC tier lifecycle index + frontmatter shape spec (Slice 2 tasks B5.T1 + B5.T2 — committed `adc53c8`).
|
|
293
293
|
- **ADR-014** — governance skills commit their own work. Single-commit grain per capture.
|
|
294
294
|
- **ADR-022** — problem lifecycle conventions; RFC lifecycle mirrors.
|
|
@@ -321,19 +321,21 @@ Before creating, search existing problems for similar issues. The user may not k
|
|
|
321
321
|
- "I found existing problems that may be related: P011 (stuck saving, CLOSED), P023 (foul drawn garbled, OPEN). Would you like to: (a) Update an existing problem, (b) Create a new problem anyway, (c) Cancel?"
|
|
322
322
|
5. If the user chooses to update, switch to the update flow for that problem ID
|
|
323
323
|
6. If no matches found, proceed to create
|
|
324
|
-
7. **After the grep completes** (whether duplicates were found or not), write the per-session create-gate marker so the `PreToolUse:Write` hook (`packages/itil/hooks/manage-problem-enforce-create.sh`, P119) allows the subsequent Write of the new `.open.md` file. The marker is `/tmp/manage-problem-grep-${SESSION_ID}
|
|
324
|
+
7. **After the grep completes** (whether duplicates were found or not), write the per-session create-gate marker so the `PreToolUse:Write` hook (`packages/itil/hooks/manage-problem-enforce-create.sh`, P119) allows the subsequent Write of the new `.open.md` file. The marker is `/tmp/manage-problem-grep-${SESSION_ID}`. Per **P260 / ADR-050 Option C**, the agent writes it under EVERY recent candidate session SID — not just one — by sourcing the discovery helpers (P124) and piping the candidate set into `mark_step2_complete_candidates`:
|
|
325
325
|
|
|
326
326
|
```bash
|
|
327
327
|
source packages/itil/hooks/lib/session-id.sh
|
|
328
328
|
source packages/itil/hooks/lib/create-gate.sh
|
|
329
|
-
|
|
329
|
+
get_candidate_session_ids | mark_step2_complete_candidates
|
|
330
330
|
```
|
|
331
331
|
|
|
332
|
-
`
|
|
332
|
+
**Why every candidate, not one (P260 / ADR-050 Option C)**: under `/wr-itil:work-problems` the orchestrator main turn fires PreToolUse hooks concurrently with its backgrounded iter subprocess (Step 5). Both sessions write the same per-machine runtime-sid marker (last-writer-wins), so the single-SID `get_current_session_id` can return the subprocess SID while the orchestrator's Write carries the orchestrator SID on its stdin — marker mismatch, create-gate deny. No agent-side algorithm can predict the right single SID from filesystem state alone (ADR-050 §Context). `get_candidate_session_ids` instead enumerates EVERY candidate SID the hook might read — the `get_current_session_id` pick (env-var > runtime-sid > announce-marker priority, P124) PLUS every recent `/tmp/<system>-announced-<UUID>` UUID within a 24h mtime window (ADR-038 announce markers, set on prompt 1 of every session by architect / jtbd / tdd / style-guide / voice-tone / itil-assistant-gate / itil-correction-detect hooks) — and `mark_step2_complete_candidates` writes the marker under each. Whichever SID the hook reads from the Write's stdin, a matching marker provably exists. The candidate set is **bounded** to recent same-machine announce markers + the runtime-sid value — NOT a global fail-open: the P119 audit invariant holds (every marker still records that THIS session ran the duplicate-check grep). The marker is per-session, so a single write covers all new tickets for the rest of this session, enabling Step 4b multi-concern splits and same-session unrelated-ticket creation without re-running the grep.
|
|
333
333
|
|
|
334
|
-
**Why
|
|
334
|
+
**Why helpers instead of inline `${CLAUDE_SESSION_ID:-default}`**: the agent's process does NOT export `CLAUDE_SESSION_ID` today; the hook side reads `session_id` from its stdin JSON payload (per the Claude Code PreToolUse contract). The prior fallback wrote the marker under `default` while the hook checked the real UUID — mismatch caused the Write deny on every first ticket of a session until the agent ad-hoc scraped a UUID-bearing marker. The helpers canonicalise that scrape so every agent context discovers candidate SIDs the same way. P124.
|
|
335
335
|
|
|
336
|
-
**Phase 4 (P142 / ADR-050)** — the helper
|
|
336
|
+
**Phase 4 (P142 / ADR-050)** — the helper reads the runtime stdin `session_id` from a per-machine marker written by the `itil-runtime-sid-marker.sh` PreToolUse hook on every tool call. Because every Bash call that sources the helper is itself a PreToolUse:Bash event, the marker the helper reads was written moments earlier with the same `session_id` the runtime Write hook will see — so SID-mismatch denial is structurally impossible **in non-concurrent flow**. The Phase 3 announce-marker priority logic is preserved as cold-path fallback (first tool call of a session, before any PreToolUse fires).
|
|
337
|
+
|
|
338
|
+
**Phase 5 (P260 / ADR-050 Option C)** — the "structurally impossible" guarantee above holds ONLY when no second session writes the runtime-sid marker concurrently. Under `/wr-itil:work-problems`, the orchestrator main turn and its backgrounded subprocess write the per-machine runtime-sid marker concurrently (last-writer-wins), re-introducing the mismatch (this was P260, surfaced by the 2026-05-18 P254/P255 foreground captures). The candidate-set marker-write above is the mitigation: it does not depend on the runtime marker carrying the right SID at Write-time, because it marks under every recent candidate SID.
|
|
337
339
|
|
|
338
340
|
**Search strategy**: Search problem filenames AND file content. A match on the filename (kebab-case title) or the Description/Symptoms sections counts. Cast a wide net — false positives are cheap (user chooses), but false negatives mean duplicate problems.
|
|
339
341
|
|
|
@@ -8,7 +8,7 @@ allowed-tools: Read, Write, Edit, Bash, Grep, Glob, Task
|
|
|
8
8
|
|
|
9
9
|
Create, update, or transition RFC tickets following the Problem-RFC-Story framework introduced by ADR-060 (accepted 2026-05-05). This skill is the heavyweight counterpart to `/wr-itil:capture-rfc` — it owns the full intake flow, lifecycle transitions, batch review, and README refresh.
|
|
10
10
|
|
|
11
|
-
**Related JTBDs**: JTBD-008 (primary — Decompose a Fix Into Coordinated Changes; this skill governs the lifecycle of decomposed work), JTBD-001 (extended scope — change-set-level governance), JTBD-101 (atomic-fix-adopter
|
|
11
|
+
**Related JTBDs**: JTBD-008 (primary — Decompose a Fix Into Coordinated Changes; this skill governs the lifecycle of decomposed work), JTBD-001 (extended scope — change-set-level governance), JTBD-101 (atomic-fix-adopter — every fix goes through an RFC per ADR-071; the RFC skills are invoked deliberately, not auto-fired, because RFC scope is direction-setting per ADR-073 — NOT because atomic fixes skip ceremony).
|
|
12
12
|
|
|
13
13
|
## RFC Lifecycle
|
|
14
14
|
|
|
@@ -96,6 +96,8 @@ Apply the update — typical edits:
|
|
|
96
96
|
- Adding `## Related` entries.
|
|
97
97
|
- Updating `decision-makers` or `adrs` frontmatter when ADRs are referenced mid-RFC execution.
|
|
98
98
|
|
|
99
|
+
**Do NOT add a "Considered Options / Alternatives Rejected" section to the RFC body.** Per ADR-070 (RFCs hold no independent decisions), every contested choice among ≥ 2 viable options is recorded as an ADR (inheriting the ADR-064 confirm gate + ADR-066 oversight marker) and referenced in the RFC's `adrs:` frontmatter — never re-argued in the RFC body. An RFC carries only scope, decomposition (sequencing/breakdown of already-decided work), and traces. The ADR-052 behavioural lint hard-fails any RFC body that contains a rejected-alternatives block without a matching `adrs:` reference.
|
|
100
|
+
|
|
99
101
|
#### README refresh on conditional update (P094 mirror)
|
|
100
102
|
|
|
101
103
|
If the update changed any ranking-bearing field (Status, Severity-via-problems, Effort, WSJF), regenerate `docs/rfcs/README.md` in-place reflecting the new ranking and stage it in the same commit. If the edit touched only `## Summary`, `## Scope`, `## Tasks`, `## Related`, or other non-ranking sections, skip the refresh.
|
|
@@ -150,7 +152,7 @@ for pid_token in $(awk '/^problems:/{gsub(/[][]/,"");gsub(/,/,"\n");for(i=2;i<=N
|
|
|
150
152
|
done
|
|
151
153
|
```
|
|
152
154
|
|
|
153
|
-
The helper (`packages/itil/scripts/update-problem-rfcs-section.sh`) is idempotent and applies lazy-empty discipline (zero traced RFCs → section absent —
|
|
155
|
+
The helper (`packages/itil/scripts/update-problem-rfcs-section.sh`) is idempotent and applies lazy-empty discipline (zero traced RFCs → section absent — a structural rendering rule, not a ceremony exemption; a problem traces ≥ 1 RFC once it reaches fix-time per ADR-071 / I13). After the transition, the helper:
|
|
154
156
|
- Updates the row's `Status` column to the new lifecycle status.
|
|
155
157
|
- Removes the row when this transition de-traces a problem (frontmatter `problems:` edit removed the entry).
|
|
156
158
|
- No-op when the table is already current (idempotent contract).
|
|
@@ -159,7 +161,7 @@ The trailer hook (`itil-rfc-trailer-advisory.sh`) sits on top of this skill-side
|
|
|
159
161
|
|
|
160
162
|
#### Forward trace — `## Stories` body section (Phase 2)
|
|
161
163
|
|
|
162
|
-
Per ADR-060 line 270 + line 296: every transition that touches the RFC body refreshes the RFC's own `## Stories` body section from its frontmatter `stories:` array. The forward-trace surface renders the ordered execution sequence as inline links to the story files, lazy-empty when `stories: []` (
|
|
164
|
+
Per ADR-060 line 270 + line 296: every transition that touches the RFC body refreshes the RFC's own `## Stories` body section from its frontmatter `stories:` array. The forward-trace surface renders the ordered execution sequence as inline links to the story files, lazy-empty when `stories: []` (an RFC not decomposed into stories). The helper is the Slice 2b sibling `update-rfc-references-section.sh`:
|
|
163
165
|
|
|
164
166
|
```bash
|
|
165
167
|
bash "$(wr-itil-script-path 2>/dev/null || echo packages/itil/scripts)/update-rfc-references-section.sh" "$rfc_file" "Stories"
|
|
@@ -251,7 +253,7 @@ The two skills share the `/tmp/wr-itil-rfc-capture-grep-${SESSION_ID}` create-ga
|
|
|
251
253
|
- **ADR-060** — Problem-RFC-Story framework with mandatory problem-trace and unified problem ontology.
|
|
252
254
|
- **P170** — driver problem ticket.
|
|
253
255
|
- **`docs/plans/170-rfc-framework-story-map.md`** — Slice 2 task B5.T4 lands this skill.
|
|
254
|
-
- **JTBD-008** (primary), JTBD-001 (extended scope), JTBD-101 (atomic-fix-adopter
|
|
256
|
+
- **JTBD-008** (primary), JTBD-001 (extended scope), JTBD-101 (atomic-fix-adopter — every fix via RFC per ADR-071).
|
|
255
257
|
- **`docs/rfcs/README.md`** — lifecycle index + frontmatter shape (Slice 2 B5.T1 + B5.T2 — `adc53c8`).
|
|
256
258
|
- **`packages/itil/skills/capture-rfc/SKILL.md`** — sibling lightweight capture skill.
|
|
257
259
|
- **`packages/itil/skills/manage-problem/SKILL.md`** — heavyweight counterpart at the problem tier; structural template for this skill.
|