@windyroad/itil 0.61.1 → 0.61.2

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.
Files changed (171) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +7 -2
  3. package/agents/test/fixtures/proceed-new-genuinely-new.md +0 -43
  4. package/agents/test/fixtures/proceed-new-subtle-sibling.md +0 -47
  5. package/agents/test/fixtures/regression-p347-vs-p346.md +0 -54
  6. package/agents/test/hang-off-check.bats +0 -225
  7. package/hooks/test/block-list.bats +0 -153
  8. package/hooks/test/command-detect.bats +0 -181
  9. package/hooks/test/itil-assistant-output-gate.bats +0 -114
  10. package/hooks/test/itil-assistant-output-review.bats +0 -224
  11. package/hooks/test/itil-bash-polling-antipattern-detect.bats +0 -154
  12. package/hooks/test/itil-changeset-discipline.bats +0 -603
  13. package/hooks/test/itil-claude-space-protection.bats +0 -294
  14. package/hooks/test/itil-commit-trailer-transition-advisory.bats +0 -80
  15. package/hooks/test/itil-correction-detect.bats +0 -189
  16. package/hooks/test/itil-deferral-cadence-gate.bats +0 -187
  17. package/hooks/test/itil-fictional-defer-detect.bats +0 -292
  18. package/hooks/test/itil-fix-title-lifecycle-advisory.bats +0 -129
  19. package/hooks/test/itil-mid-loop-ask-detect.bats +0 -220
  20. package/hooks/test/itil-no-implement-draft-gate.bats +0 -210
  21. package/hooks/test/itil-pending-questions-surface.bats +0 -307
  22. package/hooks/test/itil-readme-refresh-discipline.bats +0 -642
  23. package/hooks/test/itil-rfc-oversight-nudge.bats +0 -83
  24. package/hooks/test/itil-rfc-trailer-advisory.bats +0 -375
  25. package/hooks/test/itil-story-mirror-migration-nudge.bats +0 -122
  26. package/hooks/test/manage-problem-enforce-create.bats +0 -424
  27. package/hooks/test/p057-staging-trap-detect.bats +0 -231
  28. package/hooks/test/pre-publish-intake-gate.bats +0 -144
  29. package/hooks/test/runtime-sid-marker.bats +0 -90
  30. package/hooks/test/session-id.bats +0 -370
  31. package/scripts/migrate-problems-add-type.sh +0 -116
  32. package/scripts/test/catchup-scan.bats +0 -274
  33. package/scripts/test/check-afk-accept-eligible.bats +0 -310
  34. package/scripts/test/check-fail-soft-skip-discipline.bats +0 -172
  35. package/scripts/test/check-fix-rfc-trace.bats +0 -164
  36. package/scripts/test/check-locale-discipline.bats +0 -291
  37. package/scripts/test/check-problems-readme-budget.bats +0 -192
  38. package/scripts/test/check-rfc-has-stories.bats +0 -69
  39. package/scripts/test/check-rfc-rejected-alternatives.bats +0 -108
  40. package/scripts/test/check-rfc-stories-ratified.bats +0 -82
  41. package/scripts/test/check-upstream-responses.bats +0 -503
  42. package/scripts/test/classify-readme-drift.bats +0 -392
  43. package/scripts/test/derive-first-dispatch.bats +0 -274
  44. package/scripts/test/derive-release-vehicle.bats +0 -454
  45. package/scripts/test/detect-unratified-stories-maps.bats +0 -60
  46. package/scripts/test/dual-tolerant-glob-rfc-002-t2.bats +0 -309
  47. package/scripts/test/effort-tally.bats +0 -170
  48. package/scripts/test/evaluate-relevance.bats +0 -773
  49. package/scripts/test/kind-a-commands.bats +0 -46
  50. package/scripts/test/mark-create-gate.bats +0 -59
  51. package/scripts/test/mark-story-oversight-confirmed.bats +0 -53
  52. package/scripts/test/migrate-problems-add-type.bats +0 -280
  53. package/scripts/test/migrate-story-status-mirror.bats +0 -167
  54. package/scripts/test/no-type-regression-guard.bats +0 -74
  55. package/scripts/test/oversight-mirror-corpus-lint.bats +0 -150
  56. package/scripts/test/plugin-exercise-index.bats +0 -386
  57. package/scripts/test/plugin-maturity-doc-lint.bats +0 -470
  58. package/scripts/test/plugin-maturity-populate.bats +0 -631
  59. package/scripts/test/plugin-maturity-render.bats +0 -469
  60. package/scripts/test/plugin-validate-ci-gate.bats +0 -208
  61. package/scripts/test/reconcile-readme.bats +0 -1007
  62. package/scripts/test/reconcile-rfcs.bats +0 -496
  63. package/scripts/test/reconcile-stories.bats +0 -173
  64. package/scripts/test/reconcile-story-maps.bats +0 -74
  65. package/scripts/test/release-watch-poll-loop.bats +0 -180
  66. package/scripts/test/resolve-governance-plugin-dirs.bats +0 -78
  67. package/scripts/test/rfc-stories-extension.bats +0 -175
  68. package/scripts/test/skill-invocations.bats +0 -535
  69. package/scripts/test/skill-md-dual-tolerant-coverage-rfc-002-t3.bats +0 -374
  70. package/scripts/test/story-oversight-lib.bats +0 -190
  71. package/scripts/test/update-jtbd-references-related-problems.bats +0 -178
  72. package/scripts/test/update-problem-references-section.bats +0 -242
  73. package/scripts/test/update-problem-rfcs-section.bats +0 -242
  74. package/scripts/test/update-references-section-sibling-helpers.bats +0 -80
  75. package/scripts/test/update-rfc-commits-section.bats +0 -77
  76. package/scripts/test/verify-iter-summary.bats +0 -196
  77. package/scripts/test/working-the-problem-traversal.bats +0 -116
  78. package/skills/capture-problem/test/capture-problem-step-1-5b-jtbd-trace.bats +0 -398
  79. package/skills/capture-problem/test/capture-problem.bats +0 -389
  80. package/skills/capture-story/test/capture-story-behavioural.bats +0 -317
  81. package/skills/capture-story-map/test/capture-story-map-behavioural.bats +0 -98
  82. package/skills/close-incident/test/close-incident-contract.bats +0 -146
  83. package/skills/link-incident/test/link-incident-contract.bats +0 -125
  84. package/skills/list-incidents/test/list-incidents-contract.bats +0 -115
  85. package/skills/list-problems/test/list-problems-contract.bats +0 -100
  86. package/skills/list-stories/test/list-stories-contract.bats +0 -127
  87. package/skills/list-story-maps/test/list-story-maps-contract.bats +0 -46
  88. package/skills/manage-incident/test/manage-incident-adr-044-contract.bats +0 -206
  89. package/skills/manage-incident/test/manage-incident-close-forwarder.bats +0 -67
  90. package/skills/manage-incident/test/manage-incident-link-forwarder.bats +0 -67
  91. package/skills/manage-incident/test/manage-incident-list-forwarder.bats +0 -68
  92. package/skills/manage-incident/test/manage-incident-mitigate-forwarder.bats +0 -66
  93. package/skills/manage-incident/test/manage-incident-restore-forwarder.bats +0 -66
  94. package/skills/manage-incident/test/manage-incident.bats +0 -171
  95. package/skills/manage-problem/test/manage-problem-adr-044-step4-derive-first.bats +0 -151
  96. package/skills/manage-problem/test/manage-problem-auto-migrate-step.bats +0 -53
  97. package/skills/manage-problem/test/manage-problem-concern-boundary.bats +0 -64
  98. package/skills/manage-problem/test/manage-problem-effort-buckets.bats +0 -56
  99. package/skills/manage-problem/test/manage-problem-external-root-cause-detection.bats +0 -119
  100. package/skills/manage-problem/test/manage-problem-first-run-intake-prompt.bats +0 -43
  101. package/skills/manage-problem/test/manage-problem-git-mv-restage-reminder.bats +0 -48
  102. package/skills/manage-problem/test/manage-problem-id-collision-guard.bats +0 -39
  103. package/skills/manage-problem/test/manage-problem-list-forwarder.bats +0 -66
  104. package/skills/manage-problem/test/manage-problem-next-id-origin-lookup.bats +0 -50
  105. package/skills/manage-problem/test/manage-problem-no-prose-options.bats +0 -74
  106. package/skills/manage-problem/test/manage-problem-output-formatting.bats +0 -30
  107. package/skills/manage-problem/test/manage-problem-parked-and-cache.bats +0 -73
  108. package/skills/manage-problem/test/manage-problem-readme-refresh-on-transition.bats +0 -62
  109. package/skills/manage-problem/test/manage-problem-readme-tie-break-order.bats +0 -318
  110. package/skills/manage-problem/test/manage-problem-readme-vq-sort-order.bats +0 -206
  111. package/skills/manage-problem/test/manage-problem-release-vehicle-seed.bats +0 -83
  112. package/skills/manage-problem/test/manage-problem-review-forwarder.bats +0 -71
  113. package/skills/manage-problem/test/manage-problem-step-9d-recovery-path.bats +0 -97
  114. package/skills/manage-problem/test/manage-problem-transition-forwarder.bats +0 -96
  115. package/skills/manage-problem/test/manage-problem-transitive-dependencies.bats +0 -365
  116. package/skills/manage-problem/test/manage-problem-verification-detection.bats +0 -64
  117. package/skills/manage-problem/test/manage-problem-verification-pending.bats +0 -114
  118. package/skills/manage-problem/test/manage-problem-work-forwarder.bats +0 -88
  119. package/skills/manage-story/test/manage-story-contract.bats +0 -174
  120. package/skills/manage-story-map/test/manage-story-map-contract.bats +0 -63
  121. package/skills/mitigate-incident/test/mitigate-incident-contract.bats +0 -258
  122. package/skills/reconcile-readme/test/reconcile-readme-contract.bats +0 -201
  123. package/skills/report-upstream/test/report-upstream-contract.bats +0 -394
  124. package/skills/restore-incident/test/restore-incident-contract.bats +0 -170
  125. package/skills/review-problems/test/inbound-channels-cache-shape.bats +0 -127
  126. package/skills/review-problems/test/inbound-discovery-contract.bats +0 -383
  127. package/skills/review-problems/test/jtbd-301-verdict-shape-contract.bats +0 -225
  128. package/skills/review-problems/test/review-problems-contract.bats +0 -235
  129. package/skills/review-problems/test/review-problems-likely-verified-cell-shape.bats +0 -229
  130. package/skills/scaffold-intake/test/scaffold-intake-contract.bats +0 -126
  131. package/skills/scaffold-intake/test/scaffold-intake-fixture.bats +0 -164
  132. package/skills/scaffold-intake/test/scaffold-intake-secrets-absent.bats +0 -87
  133. package/skills/transition-problem/test/transition-problem-contract.bats +0 -285
  134. package/skills/transition-problems/test/transition-problems-contract.bats +0 -331
  135. package/skills/update-upstream/test/update-upstream-contract.bats +0 -375
  136. package/skills/work-problem/test/work-problem-contract.bats +0 -294
  137. package/skills/work-problems/test/work-problems-above-appetite-remediation.bats +0 -139
  138. package/skills/work-problems/test/work-problems-adr-013-rule-6-p352-amendment.bats +0 -166
  139. package/skills/work-problems/test/work-problems-auto-migrate-step.bats +0 -57
  140. package/skills/work-problems/test/work-problems-cost-logging.bats +0 -109
  141. package/skills/work-problems/test/work-problems-deviation-candidate-shape.bats +0 -121
  142. package/skills/work-problems/test/work-problems-first-run-intake-prompt.bats +0 -36
  143. package/skills/work-problems/test/work-problems-inter-iteration-verify.bats +0 -59
  144. package/skills/work-problems/test/work-problems-mid-loop-userpromptsubmit-handler.bats +0 -69
  145. package/skills/work-problems/test/work-problems-no-mid-loop-asking.bats +0 -192
  146. package/skills/work-problems/test/work-problems-p341-pre-all-done-gate.bats +0 -122
  147. package/skills/work-problems/test/work-problems-p342-r007-derive-then-pass-flags.bats +0 -170
  148. package/skills/work-problems/test/work-problems-p342-retro-auto-ticket-carveout.bats +0 -120
  149. package/skills/work-problems/test/work-problems-preflight-failure-handling.bats +0 -248
  150. package/skills/work-problems/test/work-problems-preflight-session-continuity.bats +0 -139
  151. package/skills/work-problems/test/work-problems-preflight.bats +0 -69
  152. package/skills/work-problems/test/work-problems-release-cadence.bats +0 -63
  153. package/skills/work-problems/test/work-problems-step-0-iter-error-staleness.bats +0 -93
  154. package/skills/work-problems/test/work-problems-step-0b-cache-staleness-behavioural.bats +0 -138
  155. package/skills/work-problems/test/work-problems-step-0c-deferred-placeholder-staleness-behavioural.bats +0 -238
  156. package/skills/work-problems/test/work-problems-step-0d-outbound-responses-staleness-behavioural.bats +0 -174
  157. package/skills/work-problems/test/work-problems-step-2-5-routing.bats +0 -80
  158. package/skills/work-problems/test/work-problems-step-2-5b-cross-halt-routing.bats +0 -149
  159. package/skills/work-problems/test/work-problems-step-3-5-jtbd-ratification-predicate.bats +0 -191
  160. package/skills/work-problems/test/work-problems-step-5-bats-polling-discipline.bats +0 -91
  161. package/skills/work-problems/test/work-problems-step-5-delegation.bats +0 -333
  162. package/skills/work-problems/test/work-problems-step-5-idle-timeout-sigterm.bats +0 -421
  163. package/skills/work-problems/test/work-problems-step-5-is-error-transient-halt.bats +0 -278
  164. package/skills/work-problems/test/work-problems-step-5-iter-changeset-required.bats +0 -95
  165. package/skills/work-problems/test/work-problems-step-5-prompt-body-re-grounding.bats +0 -128
  166. package/skills/work-problems/test/work-problems-step-5-stream-timeout-salvage.bats +0 -251
  167. package/skills/work-problems/test/work-problems-step-6-5-always-drain.bats +0 -203
  168. package/skills/work-problems/test/work-problems-step-6-5-cache-refresh-chain.bats +0 -174
  169. package/skills/work-problems/test/work-problems-step-6-5-fix-and-continue.bats +0 -254
  170. package/skills/work-problems/test/work-problems-step-6-5-postrelease-kv-callback.bats +0 -209
  171. package/skills/work-problems/test/work-problems-stop-condition-questions.bats +0 -85
@@ -1,144 +0,0 @@
1
- #!/usr/bin/env bats
2
-
3
- # P065 / ADR-036: pre-publish-intake-gate.sh PreToolUse:Bash hook must deny
4
- # `npm publish` (and changesets-release `gh pr merge`) when the four intake
5
- # files are missing, unless the project has opted out via the decline
6
- # marker (`.claude/.intake-scaffold-declined`) or the user sets the
7
- # `INTAKE_BYPASS=1` env override.
8
- #
9
- # Required intake files (per ADR-036 Detection step 5):
10
- # .github/ISSUE_TEMPLATE/config.yml
11
- # .github/ISSUE_TEMPLATE/problem-report.yml
12
- # SECURITY.md
13
- # SUPPORT.md
14
- # CONTRIBUTING.md
15
- #
16
- # Per feedback_behavioural_tests.md (P081): behavioural assertions —
17
- # simulate the hook's payload on stdin and assert on emitted JSON
18
- # permissionDecision and exit status. No source-grep on hook content.
19
-
20
- setup() {
21
- SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
22
- HOOK="$SCRIPT_DIR/pre-publish-intake-gate.sh"
23
- ORIG_DIR="$PWD"
24
- TEST_DIR=$(mktemp -d)
25
- cd "$TEST_DIR"
26
- unset INTAKE_BYPASS
27
- }
28
-
29
- teardown() {
30
- cd "$ORIG_DIR"
31
- rm -rf "$TEST_DIR"
32
- unset INTAKE_BYPASS
33
- }
34
-
35
- # Helper: simulate the PreToolUse:Bash payload on stdin.
36
- run_bash_hook() {
37
- local cmd="$1"
38
- local json
39
- json=$(printf '{"tool_name":"Bash","tool_input":{"command":"%s"}}' "$cmd")
40
- echo "$json" | bash "$HOOK"
41
- }
42
-
43
- # Helper: scaffold all five intake files in the test directory.
44
- scaffold_all_intake() {
45
- mkdir -p .github/ISSUE_TEMPLATE
46
- echo "blank_issues_enabled: false" > .github/ISSUE_TEMPLATE/config.yml
47
- echo "name: Report a problem" > .github/ISSUE_TEMPLATE/problem-report.yml
48
- echo "# Security Policy" > SECURITY.md
49
- echo "# Getting Help" > SUPPORT.md
50
- echo "# Contributing" > CONTRIBUTING.md
51
- }
52
-
53
- # Helper: set the decline marker.
54
- decline() {
55
- mkdir -p .claude
56
- : > .claude/.intake-scaffold-declined
57
- }
58
-
59
- # --- Allow paths ---
60
-
61
- @test "allow: npm publish proceeds when all five intake files are present" {
62
- scaffold_all_intake
63
- run run_bash_hook "npm publish"
64
- [ "$status" -eq 0 ]
65
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
66
- }
67
-
68
- @test "allow: missing intake but decline marker present permits publish" {
69
- decline
70
- run run_bash_hook "npm publish"
71
- [ "$status" -eq 0 ]
72
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
73
- }
74
-
75
- @test "allow: missing intake but INTAKE_BYPASS=1 permits publish (checked BEFORE existence)" {
76
- # Architect direction: INTAKE_BYPASS must short-circuit before the
77
- # existence check fires.
78
- INTAKE_BYPASS=1 run run_bash_hook "npm publish"
79
- [ "$status" -eq 0 ]
80
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
81
- }
82
-
83
- @test "allow: non-publish bash commands are out of scope" {
84
- run run_bash_hook "ls -la"
85
- [ "$status" -eq 0 ]
86
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
87
- run run_bash_hook "git status"
88
- [ "$status" -eq 0 ]
89
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
90
- }
91
-
92
- @test "allow: non-Bash tool calls are out of scope" {
93
- json='{"tool_name":"Read","tool_input":{"file_path":"foo"}}'
94
- run bash -c "echo '$json' | bash $HOOK"
95
- [ "$status" -eq 0 ]
96
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
97
- }
98
-
99
- # --- Deny paths ---
100
-
101
- @test "deny: npm publish with no intake files, no marker, no bypass" {
102
- run run_bash_hook "npm publish"
103
- [ "$status" -eq 0 ]
104
- [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
105
- [[ "$output" == *"intake"* ]]
106
- [[ "$output" == *"scaffold-intake"* ]]
107
- }
108
-
109
- @test "deny: npm publish with partial intake (3 of 5) still denies" {
110
- mkdir -p .github/ISSUE_TEMPLATE
111
- echo "blank_issues_enabled: false" > .github/ISSUE_TEMPLATE/config.yml
112
- echo "name: Report a problem" > .github/ISSUE_TEMPLATE/problem-report.yml
113
- echo "# Security Policy" > SECURITY.md
114
- # SUPPORT.md and CONTRIBUTING.md missing
115
- run run_bash_hook "npm publish"
116
- [ "$status" -eq 0 ]
117
- [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
118
- }
119
-
120
- @test "deny message names the recovery affordances (skill, marker, bypass)" {
121
- run run_bash_hook "npm publish"
122
- [ "$status" -eq 0 ]
123
- [[ "$output" == *"scaffold-intake"* ]]
124
- [[ "$output" == *"INTAKE_BYPASS"* ]]
125
- [[ "$output" == *".intake-scaffold-declined"* ]]
126
- }
127
-
128
- # --- gh pr merge on changeset-release/* ---
129
-
130
- @test "deny: gh pr merge of a changeset-release/* PR with missing intake" {
131
- # ADR-036 Trigger 2 also matches gh pr merge against changeset-release/*
132
- # branches (the changesets release-PR pattern).
133
- run run_bash_hook "gh pr merge 42 --repo windyroad/example --branch changeset-release/main"
134
- [ "$status" -eq 0 ]
135
- [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
136
- }
137
-
138
- @test "allow: gh pr merge of a non-changeset PR is out of scope" {
139
- # Only the changeset-release/* branch pattern is in scope; a regular
140
- # feature-branch merge does not flip the publish boundary.
141
- run run_bash_hook "gh pr merge 99 --branch feature/foo"
142
- [ "$status" -eq 0 ]
143
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
144
- }
@@ -1,90 +0,0 @@
1
- #!/usr/bin/env bats
2
-
3
- # P142 / ADR-050: itil-runtime-sid-marker.sh PreToolUse hook.
4
- #
5
- # Behavioural contract:
6
- # 1. Hook receives JSON on stdin with a `session_id` field.
7
- # 2. Hook writes the session_id to the runtime-SID marker path
8
- # (computed by `runtime_sid_path()` in lib/runtime-sid.sh).
9
- # 3. Hook emits 0 bytes on stdout (ADR-045 Pattern 1: side-effect-only,
10
- # silent-on-pass — no context budget burn per tool call).
11
- # 4. Hook always exits 0 (fail-open — never block a tool call on
12
- # marker write).
13
- # 5. Empty session_id -> hook is a no-op (marker not touched).
14
- # 6. Subsequent invocations OVERWRITE the marker (so a subprocess
15
- # tool call replaces the orchestrator's SID with the subprocess's
16
- # SID for the duration of the subprocess; the orchestrator's
17
- # next tool call after subprocess exit overwrites it back).
18
- #
19
- # Per feedback_behavioural_tests.md (P081): tests assert the hook's
20
- # observable effects (marker contents, stdout bytes, exit code) — NOT
21
- # the source content of the hook script.
22
-
23
- setup() {
24
- SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
25
- HOOK="$SCRIPT_DIR/itil-runtime-sid-marker.sh"
26
- SANDBOX_TMP=$(mktemp -d)
27
- export SESSION_MARKER_DIR="$SANDBOX_TMP"
28
- MARKER_PATH="$SANDBOX_TMP/itil-runtime-sid.current"
29
- }
30
-
31
- teardown() {
32
- rm -rf "$SANDBOX_TMP"
33
- unset SESSION_MARKER_DIR
34
- }
35
-
36
- # Helper: invoke the hook with a JSON stdin payload.
37
- fire_hook() {
38
- local json="$1"
39
- echo "$json" | bash "$HOOK"
40
- }
41
-
42
- @test "hook writes session_id to runtime-SID marker" {
43
- expected_uuid="aaaaaaaa-1111-2222-3333-444444444444"
44
- fire_hook "{\"session_id\":\"$expected_uuid\",\"tool_name\":\"Bash\"}"
45
- [ -f "$MARKER_PATH" ]
46
- [ "$(cat "$MARKER_PATH")" = "$expected_uuid" ]
47
- }
48
-
49
- @test "hook is silent on stdout (ADR-045 Pattern 1)" {
50
- expected_uuid="bbbbbbbb-1111-2222-3333-444444444444"
51
- output=$(fire_hook "{\"session_id\":\"$expected_uuid\",\"tool_name\":\"Bash\"}")
52
- [ -z "$output" ]
53
- }
54
-
55
- @test "hook exits 0 on success" {
56
- expected_uuid="cccccccc-1111-2222-3333-444444444444"
57
- echo "{\"session_id\":\"$expected_uuid\",\"tool_name\":\"Bash\"}" | bash "$HOOK"
58
- [ "$?" -eq 0 ]
59
- }
60
-
61
- @test "hook overwrites prior marker on subsequent invocation" {
62
- first_uuid="dddddddd-1111-2222-3333-444444444444"
63
- second_uuid="eeeeeeee-1111-2222-3333-444444444444"
64
- fire_hook "{\"session_id\":\"$first_uuid\",\"tool_name\":\"Bash\"}"
65
- [ "$(cat "$MARKER_PATH")" = "$first_uuid" ]
66
- fire_hook "{\"session_id\":\"$second_uuid\",\"tool_name\":\"Write\"}"
67
- [ "$(cat "$MARKER_PATH")" = "$second_uuid" ]
68
- }
69
-
70
- @test "hook is a no-op when session_id is empty" {
71
- fire_hook "{\"tool_name\":\"Bash\"}"
72
- [ ! -f "$MARKER_PATH" ]
73
- }
74
-
75
- @test "hook is a no-op when stdin is not valid JSON" {
76
- echo "not-json-at-all" | bash "$HOOK"
77
- [ "$?" -eq 0 ]
78
- [ ! -f "$MARKER_PATH" ]
79
- }
80
-
81
- @test "hook fail-open on jq absent (graceful degradation)" {
82
- # Simulate jq absent by making PATH not include any jq binary.
83
- expected_uuid="ffffffff-1111-2222-3333-444444444444"
84
- result=$(echo "{\"session_id\":\"$expected_uuid\",\"tool_name\":\"Bash\"}" | env PATH="/usr/bin:/bin" bash "$HOOK"; echo "EXIT:$?")
85
- # Either jq is in /usr/bin (fine — marker written), or it's absent
86
- # (hook should still exit 0 without crashing). The exit-0 contract
87
- # is the load-bearing assertion; marker presence is a bonus when
88
- # jq is available.
89
- [[ "$result" == *"EXIT:0"* ]]
90
- }
@@ -1,370 +0,0 @@
1
- #!/usr/bin/env bats
2
-
3
- # P124: session-id.sh helper must canonicalise per-session UUID discovery
4
- # for agent-side code paths (e.g. /wr-itil:manage-problem Step 2 substep 7
5
- # marker write) that today fall back to the brittle ${CLAUDE_SESSION_ID:-default}
6
- # pattern when the env var is not exported in the agent's process.
7
- #
8
- # Behavioural contract:
9
- # 1. CLAUDE_SESSION_ID exported -> echo it (env-var fast path).
10
- # 2. CLAUDE_SESSION_ID absent + an /tmp/<system>-announced-<UUID> marker
11
- # present -> echo the UUID parsed from the marker filename.
12
- # 3. CLAUDE_SESSION_ID absent + no markers anywhere -> echo nothing,
13
- # exit non-zero (so callers can detect "could not discover").
14
- # 4. Multiple announce markers across systems -> deterministic selection
15
- # (architect first, then jtbd, then tdd, then itil-assistant-gate,
16
- # then itil-correction-detect, then style-guide, then voice-tone)
17
- # so the discovery is reproducible across invocations.
18
- #
19
- # Per feedback_behavioural_tests.md (P081): tests assert the helper's
20
- # emitted output and exit code, not the source content of the helper.
21
- # The marker-system selection order is asserted by constructing only the
22
- # higher-priority marker and checking the helper picks it.
23
-
24
- setup() {
25
- SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
26
- HELPER="$SCRIPT_DIR/lib/session-id.sh"
27
- # Use a sandbox /tmp so we never leak across real session markers.
28
- SANDBOX_TMP=$(mktemp -d)
29
- # session-id.sh reads from /tmp by default; SESSION_MARKER_DIR
30
- # overrides that for sandboxed bats runs without touching real
31
- # session state in /tmp.
32
- export SESSION_MARKER_DIR="$SANDBOX_TMP"
33
- unset CLAUDE_SESSION_ID
34
- }
35
-
36
- teardown() {
37
- rm -rf "$SANDBOX_TMP"
38
- unset SESSION_MARKER_DIR
39
- unset CLAUDE_SESSION_ID
40
- }
41
-
42
- # Helper: source the helper and emit the discovered SID + exit code.
43
- discover() {
44
- bash -c "source '$HELPER'; get_current_session_id; echo \"EXIT:\$?\""
45
- }
46
-
47
- # Helper: write an announce marker with a known UUID under SESSION_MARKER_DIR.
48
- mark_announced() {
49
- local system="$1"
50
- local uuid="$2"
51
- : > "$SESSION_MARKER_DIR/${system}-announced-${uuid}"
52
- }
53
-
54
- # --- Behavioural contract: env-var fast path ---
55
-
56
- @test "env-var present -> returns the env-var value verbatim" {
57
- expected_uuid="11111111-1111-1111-1111-111111111111"
58
- output=$(CLAUDE_SESSION_ID="$expected_uuid" bash -c "source '$HELPER'; get_current_session_id; echo \"EXIT:\$?\"")
59
- [[ "$output" == *"$expected_uuid"* ]]
60
- [[ "$output" == *"EXIT:0"* ]]
61
- }
62
-
63
- @test "env-var present -> ignores any markers (no scrape)" {
64
- expected_uuid="22222222-2222-2222-2222-222222222222"
65
- decoy_uuid="33333333-3333-3333-3333-333333333333"
66
- mark_announced "architect" "$decoy_uuid"
67
- output=$(CLAUDE_SESSION_ID="$expected_uuid" bash -c "source '$HELPER'; get_current_session_id; echo \"EXIT:\$?\"")
68
- [[ "$output" == *"$expected_uuid"* ]]
69
- [[ "$output" != *"$decoy_uuid"* ]]
70
- }
71
-
72
- # --- Behavioural contract: marker-scrape fallback ---
73
-
74
- @test "env-var absent + architect-announced marker present -> returns marker UUID" {
75
- expected_uuid="44444444-4444-4444-4444-444444444444"
76
- mark_announced "architect" "$expected_uuid"
77
- output=$(discover)
78
- [[ "$output" == *"$expected_uuid"* ]]
79
- [[ "$output" == *"EXIT:0"* ]]
80
- }
81
-
82
- @test "env-var absent + only jtbd-announced marker present -> returns marker UUID (fallback chain works)" {
83
- expected_uuid="55555555-5555-5555-5555-555555555555"
84
- mark_announced "jtbd" "$expected_uuid"
85
- output=$(discover)
86
- [[ "$output" == *"$expected_uuid"* ]]
87
- [[ "$output" == *"EXIT:0"* ]]
88
- }
89
-
90
- @test "env-var absent + no markers anywhere -> empty output, non-zero exit" {
91
- output=$(discover)
92
- # Output should be just "EXIT:<non-zero>" with no UUID before it.
93
- [[ "$output" =~ ^EXIT:[1-9] ]]
94
- }
95
-
96
- @test "deterministic priority: architect-announced beats jtbd-announced when both present" {
97
- architect_uuid="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
98
- jtbd_uuid="bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
99
- mark_announced "jtbd" "$jtbd_uuid"
100
- # Sleep so jtbd marker has older mtime — proves selection is by
101
- # marker-system priority, not by mtime (the architect-flagged
102
- # mtime fragility from session 2026-04-26 review).
103
- sleep 1
104
- mark_announced "architect" "$architect_uuid"
105
- output=$(discover)
106
- [[ "$output" == *"$architect_uuid"* ]]
107
- [[ "$output" != *"$jtbd_uuid"* ]]
108
- }
109
-
110
- # --- Behavioural contract: shell portability (P124 Phase 2) ---
111
- #
112
- # The helper is sourced by `/wr-itil:manage-problem` Step 2 substep 7 from
113
- # whichever shell the agent runs Bash tool calls under. On macOS that is
114
- # zsh by default; the Phase 1 implementation used `shopt -s nullglob` (a
115
- # bash builtin), which under zsh errors with `shopt: command not found`
116
- # and lets the subshell glob expression fall through to a literal
117
- # unmatched-pattern string. The helper then returned a stale or wrong
118
- # UUID — defeating its stated guarantee. Citations: P124 ticket
119
- # "Regression Evidence (2026-04-27)" — main-turn P130 capture, line 119:
120
- # `get_current_session_id:33: command not found: shopt`.
121
- #
122
- # This test asserts the helper works identically under zsh as under bash:
123
- # same UUID returned, exit 0, no `shopt: command not found` error on
124
- # stderr.
125
-
126
- @test "zsh portability: helper returns the same UUID under zsh -c as under bash -c" {
127
- if ! command -v zsh >/dev/null 2>&1; then
128
- skip "zsh not available"
129
- fi
130
- expected_uuid="cccccccc-cccc-cccc-cccc-cccccccccccc"
131
- mark_announced "architect" "$expected_uuid"
132
- zsh_run=$(SESSION_MARKER_DIR="$SANDBOX_TMP" zsh -c "source '$HELPER'; get_current_session_id; echo \"EXIT:\$?\"" 2>&1)
133
- [[ "$zsh_run" == *"$expected_uuid"* ]]
134
- [[ "$zsh_run" == *"EXIT:0"* ]]
135
- [[ "$zsh_run" != *"shopt: command not found"* ]]
136
- [[ "$zsh_run" != *"command not found: shopt"* ]]
137
- }
138
-
139
- # --- Behavioural contract: within-system mtime selection (P124 Phase 3) ---
140
- #
141
- # Phase 2 portability fix (shopt-portable for-loop existence check) shipped
142
- # 2026-04-28 morning, restoring zsh compatibility but introducing a
143
- # regression on multi-session developer machines: glob expansion enumerates
144
- # matches in ASCII-alphabetical order, and the inner "first match wins"
145
- # heuristic returned the lexically-first stale UUID when /tmp had
146
- # accumulated `${system}-announced-*` markers from prior sessions. Ticket
147
- # regression block cites 103 stale architect markers selecting the wrong
148
- # UUID, denying the create-gate (P119) and forcing a brute-force recovery.
149
- #
150
- # Phase 3 selection contract: WITHIN a single system's marker namespace,
151
- # the most-recent-mtime marker wins. ACROSS systems, the fixed priority
152
- # order is preserved (asserted by the existing "deterministic priority"
153
- # test above). `-announced-` markers are write-once-per-session per ADR-038
154
- # (no `touch`-refresh sliding TTL), so mtime IS the announcing session's
155
- # first-prompt timestamp — newest mtime unambiguously identifies the live
156
- # session within that system's marker glob.
157
-
158
- @test "within-system mtime: newest architect-announced marker wins over older same-system markers" {
159
- oldest_uuid="00000000-0000-0000-0000-000000000001" # ASCII-first
160
- middle_uuid="11111111-1111-1111-1111-111111111111"
161
- newest_uuid="22222222-2222-2222-2222-222222222222" # ASCII-last of the three
162
- # Fixture intent: alphabetical-first UUID has the OLDEST mtime; the
163
- # newest-mtime UUID is alphabetically last. Phase 2 first-glob-match
164
- # would pick `oldest_uuid` (alphabetical first); Phase 3 mtime
165
- # selection picks `newest_uuid` (most-recent mtime). The sleep
166
- # boundaries guarantee distinct mtimes across filesystems with
167
- # 1-second mtime resolution (e.g. older HFS+).
168
- mark_announced "architect" "$oldest_uuid"
169
- sleep 1
170
- mark_announced "architect" "$middle_uuid"
171
- sleep 1
172
- mark_announced "architect" "$newest_uuid"
173
- output=$(discover)
174
- [[ "$output" == *"$newest_uuid"* ]]
175
- [[ "$output" != *"$oldest_uuid"* ]]
176
- [[ "$output" != *"$middle_uuid"* ]]
177
- [[ "$output" == *"EXIT:0"* ]]
178
- }
179
-
180
- # --- Behavioural contract: runtime-SID marker (P142 Phase 4) ---
181
- #
182
- # Phase 3 mtime-based within-system selection introduced a regression
183
- # in orchestrator main turns AFTER subprocess dispatch: subprocess
184
- # announce markers have NEWER mtime than the orchestrator's, so the
185
- # helper picked the subprocess SID while the runtime hook stdin still
186
- # contained the orchestrator SID — marker landed under the wrong UUID,
187
- # create-gate (P119) denied. Mirror failure mode would fire in
188
- # subprocess context if the priority list were re-ordered to favour
189
- # orchestrator-only announce systems (no pure-helper algorithm can
190
- # distinguish "running in orchestrator main turn" from "running in
191
- # subprocess" by filesystem state alone).
192
- #
193
- # Phase 4 structural fix: a new PreToolUse hook
194
- # (`itil-runtime-sid-marker.sh`) writes the runtime stdin session_id
195
- # to a per-machine marker on every tool call. The helper reads this
196
- # marker FIRST as the authoritative current-session SID, falling back
197
- # to the existing announce-marker priority logic when the marker is
198
- # absent (cold path — first tool call of a session, before any
199
- # PreToolUse fires).
200
- #
201
- # Sandbox path: when SESSION_MARKER_DIR is set (test override), the
202
- # runtime marker lives at `${SESSION_MARKER_DIR}/itil-runtime-sid.current`
203
- # — a single fixed filename, no per-user/per-project scoping. The
204
- # scoping suffix used in prod (`-${USER}-${proj_hash}`) is irrelevant
205
- # under sandbox because every test gets a fresh SANDBOX_TMP.
206
-
207
- @test "runtime-SID marker present: helper returns marker contents over newer announce markers" {
208
- runtime_uuid="dddddddd-dddd-dddd-dddd-dddddddddddd"
209
- decoy_uuid="eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"
210
- # Decoy: an architect-announced marker with a NEWER mtime than the
211
- # runtime marker. The Phase 3 helper would have picked decoy_uuid;
212
- # the Phase 4 helper picks runtime_uuid because the runtime-SID
213
- # marker is authoritative.
214
- printf '%s' "$runtime_uuid" > "$SANDBOX_TMP/itil-runtime-sid.current"
215
- sleep 1
216
- mark_announced "architect" "$decoy_uuid"
217
- output=$(discover)
218
- [[ "$output" == *"$runtime_uuid"* ]]
219
- [[ "$output" != *"$decoy_uuid"* ]]
220
- [[ "$output" == *"EXIT:0"* ]]
221
- }
222
-
223
- @test "runtime-SID marker empty: helper falls back to announce-marker priority" {
224
- expected_uuid="ffffffff-ffff-ffff-ffff-ffffffffffff"
225
- mark_announced "architect" "$expected_uuid"
226
- # Empty runtime marker (zero-byte file) — helper must NOT return
227
- # the empty contents; it must fall through to the announce-marker
228
- # scrape. Empty marker can occur if the hook ran with empty
229
- # session_id stdin (test harness, hook self-test) and would still
230
- # leave the file at zero bytes per the hook's empty-input fail-open.
231
- : > "$SANDBOX_TMP/itil-runtime-sid.current"
232
- output=$(discover)
233
- [[ "$output" == *"$expected_uuid"* ]]
234
- [[ "$output" == *"EXIT:0"* ]]
235
- }
236
-
237
- @test "runtime-SID marker absent (cold path): helper uses announce-marker priority unchanged" {
238
- expected_uuid="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
239
- mark_announced "architect" "$expected_uuid"
240
- # No runtime marker created — cold path. Helper falls back to
241
- # existing Phase 3 announce-marker priority. This test pins the
242
- # backwards-compat contract: sessions whose first tool call hasn't
243
- # yet fired the PreToolUse hook still discover their SID via the
244
- # announce-marker fallback (the priority list is preserved as-is).
245
- output=$(discover)
246
- [[ "$output" == *"$expected_uuid"* ]]
247
- [[ "$output" == *"EXIT:0"* ]]
248
- }
249
-
250
- @test "env var beats runtime-SID marker (env-var fast path preserved)" {
251
- env_uuid="11111111-aaaa-bbbb-cccc-222222222222"
252
- marker_uuid="22222222-aaaa-bbbb-cccc-333333333333"
253
- printf '%s' "$marker_uuid" > "$SANDBOX_TMP/itil-runtime-sid.current"
254
- output=$(CLAUDE_SESSION_ID="$env_uuid" SESSION_MARKER_DIR="$SANDBOX_TMP" bash -c "source '$HELPER'; get_current_session_id; echo \"EXIT:\$?\"")
255
- [[ "$output" == *"$env_uuid"* ]]
256
- [[ "$output" != *"$marker_uuid"* ]]
257
- [[ "$output" == *"EXIT:0"* ]]
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
- }
352
-
353
- # P380: on macOS /tmp is a symlink to /private/tmp. The `find <symlink>
354
- # -maxdepth 1` enumeration in get_candidate_session_ids in default (-P) mode
355
- # refuses to descend the start-point symlink, so it silently adds zero
356
- # concurrent SIDs; the `-L` flag follows it. (get_current_session_id uses a
357
- # shell glob (`ls`), which traverses the symlink regardless — so this test
358
- # needs a SECOND concurrent marker whose SID is reachable ONLY via the find
359
- # enumeration to isolate the bug.) RED without `-L`, GREEN with it.
360
- @test "candidates: enumerates concurrent markers when SESSION_MARKER_DIR is a symlink (P380 macOS /tmp)" {
361
- primary_uuid="aabbccdd-0000-1111-2222-symlinkprim0" # picked by get_current_session_id glob
362
- concurrent_uuid="eeff0011-0000-1111-2222-symlinkconc0" # reachable only via the find enum
363
- mark_announced "architect" "$primary_uuid"
364
- mark_announced "jtbd" "$concurrent_uuid"
365
- link_tmp="${SANDBOX_TMP}.link"
366
- ln -s "$SANDBOX_TMP" "$link_tmp"
367
- output=$(SESSION_MARKER_DIR="$link_tmp" bash -c "source '$HELPER'; get_candidate_session_ids")
368
- rm -f "$link_tmp"
369
- [[ "$output" == *"$concurrent_uuid"* ]]
370
- }