@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.
@@ -484,5 +484,5 @@
484
484
  }
485
485
  },
486
486
  "name": "wr-itil",
487
- "version": "0.35.13"
487
+ "version": "0.35.14"
488
488
  }
@@ -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
  #
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.35.13",
3
+ "version": "0.35.14-preview.416",
4
4
  "description": "ITIL-aligned IT service management for Claude Code (problem, and future incident/change skills)",
5
5
  "bin": {
6
6
  "windyroad-itil": "./bin/install.mjs"
@@ -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
- sid=$(get_current_session_id) && mark_step2_complete "$sid"
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 friction guard capture-rfc remains opt-in, never auto-fires on atomic captures).
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: atomic RFCs OMIT the flag and capture-rfc populates `stories: []` in frontmatter (JTBD-101 friction guard atomic RFCs are first-class); 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).
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 pathevery 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: []` (atomic RFC, JTBD-101 friction guard), the helper omits the section entirely:
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 friction guard) capture-rfc remains opt-in aside-invocation.
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}` and the agent writes it via Bash by sourcing the session-id discovery helper (P124) and calling the existing `mark_step2_complete` helper:
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
- sid=$(get_current_session_id) && mark_step2_complete "$sid"
329
+ get_candidate_session_ids | mark_step2_complete_candidates
330
330
  ```
331
331
 
332
- `get_current_session_id` (P124) returns the canonical session UUID by reading `CLAUDE_SESSION_ID` if exported, else by scraping the most-reliable per-session announce marker (`/tmp/<system>-announced-<UUID>`, set on prompt 1 of every session per ADR-038 by architect / jtbd / tdd / style-guide / voice-tone / itil-assistant-gate / itil-correction-detect hooks). It exits non-zero if no session can be discovered the `&&` short-circuits the marker write so the agent never lands `/tmp/manage-problem-grep-` (an empty UUID would never match the hook's stdin-JSON `session_id` and would silently fail later). `mark_step2_complete` (existing helper from `create-gate.sh`) writes the marker file under the canonical path; the marker is per-session (single marker 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.
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 a helper 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 helper canonicalises that scrape so every agent context discovers the SID the same way. P124.
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 now 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 the routine flow. The Phase 3 announce-marker priority logic is preserved as cold-path fallback (first tool call of a session, before any PreToolUse fires).
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 friction guard RFC ceremony only fires on opt-in invocations).
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 — protects atomic-fix-adopter friction guard per JTBD-101). After the transition, the helper:
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: []` (atomic RFC JTBD-101 friction guard). The helper is the Slice 2b sibling `update-rfc-references-section.sh`:
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 friction guard).
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.