@windyroad/itil 0.35.13-preview.412 → 0.35.14-preview.415
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/skills/capture-problem/SKILL.md +3 -3
- package/skills/manage-problem/SKILL.md +7 -5
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windyroad/itil",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.14-preview.415",
|
|
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"
|
|
@@ -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
|
|
|
@@ -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
|
|