@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,535 +0,0 @@
1
- #!/usr/bin/env bats
2
-
3
- # @problem P087 — Phase 2a transcript-axis script behavioural confirmation.
4
- #
5
- # Contract under test: `packages/itil/scripts/skill-invocations.sh` reads
6
- # `~/.claude/projects/**/*.jsonl` (recursive), tallies tool_use invocations
7
- # by `Skill` / `Agent` / `Bash` (per ADR-058 §Script contracts), and emits
8
- # one NDJSON record per surface to stdout. Exit code 0 always per ADR-013
9
- # Rule 6 fail-safe (inaccessible root, opt-out marker, no data all hit the
10
- # zero-records/stderr-comment path).
11
- #
12
- # Confirmation criteria 1-5 from ADR-058 §Confirmation are the load-bearing
13
- # behavioural assertions in this file. Phase 2b's git-axis script ships
14
- # criteria 6-8 in a sibling bats file.
15
- #
16
- # @adr ADR-058 (Plugin maturity measurement mechanism)
17
- # @adr ADR-049 (Shim grammar — `wr-itil-skill-invocations` on $PATH)
18
- # @adr ADR-035 (Privacy posture — opt-out marker, no network primitive,
19
- # content sanitisation, path-hashing)
20
- # @adr ADR-052 (Behavioural tests default — NDJSON-output-driven against
21
- # fixture transcripts, not source-greps on script body; the no-network
22
- # negative-grep at Confirmation #3 is the documented carve-out)
23
- # @adr ADR-013 Rule 6 (non-interactive fail-safe — exit 0 always)
24
- # @jtbd JTBD-101 (Extend the Suite — hardening-prioritisation outcome)
25
- # @jtbd JTBD-201 (Restore Service Fast — audit-trail composition)
26
-
27
- setup() {
28
- SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
29
- SCRIPT="$SCRIPTS_DIR/skill-invocations.sh"
30
- FIXTURE_DIR="$(mktemp -d)"
31
- TRANSCRIPT_ROOT="$FIXTURE_DIR/transcripts"
32
- PROJECT_ROOT="$FIXTURE_DIR/project"
33
- mkdir -p "$TRANSCRIPT_ROOT" "$PROJECT_ROOT"
34
- }
35
-
36
- teardown() {
37
- rm -rf "$FIXTURE_DIR"
38
- }
39
-
40
- # Helper: write a synthetic JSONL transcript under the fixture root.
41
- # Each call appends one assistant message carrying a single tool_use entry.
42
- # Layout mirrors the live `~/.claude/projects/<encoded-path>/<UUID>.jsonl`
43
- # shape (recursive rglob discovers it).
44
- write_skill_invocation() {
45
- local file="$1"; local skill_name="$2"; local ts="$3"
46
- mkdir -p "$(dirname "$file")"
47
- python3 - "$file" "$skill_name" "$ts" <<'PYEOF'
48
- import json, sys
49
- file, skill, ts = sys.argv[1], sys.argv[2], sys.argv[3]
50
- rec = {
51
- "type": "assistant",
52
- "timestamp": ts,
53
- "message": {
54
- "role": "assistant",
55
- "content": [
56
- {"type": "tool_use", "name": "Skill", "input": {"skill": skill}}
57
- ]
58
- }
59
- }
60
- with open(file, "a") as fh:
61
- fh.write(json.dumps(rec) + "\n")
62
- PYEOF
63
- }
64
-
65
- write_agent_invocation() {
66
- local file="$1"; local agent_kind="$2"; local ts="$3"
67
- mkdir -p "$(dirname "$file")"
68
- python3 - "$file" "$agent_kind" "$ts" <<'PYEOF'
69
- import json, sys
70
- file, kind, ts = sys.argv[1], sys.argv[2], sys.argv[3]
71
- rec = {
72
- "type": "assistant",
73
- "timestamp": ts,
74
- "message": {
75
- "role": "assistant",
76
- "content": [
77
- {"type": "tool_use", "name": "Agent", "input": {"subagent_type": kind}}
78
- ]
79
- }
80
- }
81
- with open(file, "a") as fh:
82
- fh.write(json.dumps(rec) + "\n")
83
- PYEOF
84
- }
85
-
86
- write_bash_invocation() {
87
- local file="$1"; local cmd="$2"; local ts="$3"
88
- mkdir -p "$(dirname "$file")"
89
- python3 - "$file" "$cmd" "$ts" <<'PYEOF'
90
- import json, sys
91
- file, cmd, ts = sys.argv[1], sys.argv[2], sys.argv[3]
92
- rec = {
93
- "type": "assistant",
94
- "timestamp": ts,
95
- "message": {
96
- "role": "assistant",
97
- "content": [
98
- {"type": "tool_use", "name": "Bash", "input": {"command": cmd}}
99
- ]
100
- }
101
- }
102
- with open(file, "a") as fh:
103
- fh.write(json.dumps(rec) + "\n")
104
- PYEOF
105
- }
106
-
107
- # Helper: produce an ISO 8601 timestamp N hours before "now" (UTC).
108
- recent_iso() {
109
- python3 -c "import sys, datetime; print((datetime.datetime.utcnow() - datetime.timedelta(hours=int(sys.argv[1]))).strftime('%Y-%m-%dT%H:%M:%SZ'))" "$1"
110
- }
111
-
112
- # ── Existence / executable ──────────────────────────────────────────────────
113
-
114
- @test "skill-invocations: canonical script exists" {
115
- [ -f "$SCRIPT" ]
116
- }
117
-
118
- @test "skill-invocations: canonical script is executable" {
119
- [ -x "$SCRIPT" ]
120
- }
121
-
122
- @test "skill-invocations: shim file exists with ADR-049 grammar" {
123
- local shim="$SCRIPTS_DIR/../bin/wr-itil-skill-invocations"
124
- [ -f "$shim" ]
125
- [ -x "$shim" ]
126
- grep -q 'exec.*scripts/skill-invocations.sh' "$shim"
127
- }
128
-
129
- # ── Confirmation #1: NDJSON-shape fixture ───────────────────────────────────
130
- # Seed three Skill invocations of `wr-itil:manage-problem` within the
131
- # default 30-day window. Assert one NDJSON record on stdout with
132
- # axis="skill-invocations", surface="wr-itil:manage-problem", kind="skill",
133
- # plugin="itil", invocations=3. Each line is valid JSON with the expected
134
- # schema keys.
135
-
136
- @test "Confirmation #1: NDJSON record for 3 wr-itil:manage-problem invocations" {
137
- local sess="$TRANSCRIPT_ROOT/proj/aaa.jsonl"
138
- local ts=$(recent_iso 1)
139
- write_skill_invocation "$sess" "wr-itil:manage-problem" "$ts"
140
- write_skill_invocation "$sess" "wr-itil:manage-problem" "$ts"
141
- write_skill_invocation "$sess" "wr-itil:manage-problem" "$ts"
142
-
143
- run "$SCRIPT" --window-days=30 --root="$TRANSCRIPT_ROOT" --project-root="$PROJECT_ROOT"
144
- [ "$status" -eq 0 ]
145
- # Exactly one record line, no extras, no leading/trailing junk on stdout
146
- local line_count
147
- line_count="$(printf '%s' "$output" | grep -c .)"
148
- [ "$line_count" -eq 1 ]
149
- # Validate schema fields
150
- echo "$output" | python3 -c "
151
- import json, sys
152
- rec = json.loads(sys.stdin.read().strip())
153
- assert rec['schema_version'] == '1.0', rec
154
- assert rec['axis'] == 'skill-invocations', rec
155
- assert rec['surface'] == 'wr-itil:manage-problem', rec
156
- assert rec['kind'] == 'skill', rec
157
- assert rec['plugin'] == 'itil', rec
158
- assert rec['window_days'] == 30, rec
159
- assert rec['invocations'] == 3, rec
160
- assert rec.get('first_invocation_iso') is not None, rec
161
- assert rec.get('last_invocation_iso') is not None, rec
162
- "
163
- }
164
-
165
- @test "Confirmation #1: aggregates Skill + Agent + Bash kinds into distinct records" {
166
- local sess="$TRANSCRIPT_ROOT/proj/mix.jsonl"
167
- local ts=$(recent_iso 1)
168
- write_skill_invocation "$sess" "wr-itil:manage-problem" "$ts"
169
- write_agent_invocation "$sess" "wr-architect:agent" "$ts"
170
- write_bash_invocation "$sess" "wr-itil-reconcile-readme docs/problems" "$ts"
171
-
172
- run "$SCRIPT" --window-days=30 --root="$TRANSCRIPT_ROOT" --project-root="$PROJECT_ROOT"
173
- [ "$status" -eq 0 ]
174
- # Three distinct NDJSON lines, one per kind
175
- local line_count
176
- line_count="$(printf '%s' "$output" | grep -c .)"
177
- [ "$line_count" -eq 3 ]
178
- echo "$output" | grep -q '"kind":"skill"'
179
- echo "$output" | grep -q '"kind":"agent"'
180
- echo "$output" | grep -q '"kind":"bash-attributed"'
181
- echo "$output" | grep -q '"plugin":"itil"'
182
- echo "$output" | grep -q '"plugin":"architect"'
183
- }
184
-
185
- @test "Confirmation #1: filters unknown short-form skill names from plugin attribution" {
186
- local sess="$TRANSCRIPT_ROOT/proj/short.jsonl"
187
- local ts=$(recent_iso 1)
188
- # Bare names without `wr-<plugin>:` prefix should be excluded from
189
- # per-plugin attribution per ADR-058 §Script contracts.
190
- write_skill_invocation "$sess" "commit" "$ts"
191
- write_skill_invocation "$sess" "loop" "$ts"
192
-
193
- run "$SCRIPT" --window-days=30 --root="$TRANSCRIPT_ROOT" --project-root="$PROJECT_ROOT"
194
- [ "$status" -eq 0 ]
195
- # No records emitted — both invocations are short-form and unattributable
196
- [ -z "$output" ]
197
- }
198
-
199
- # ── Confirmation #2: opt-out marker fixture ─────────────────────────────────
200
-
201
- @test "Confirmation #2: opt-out marker disables reads, stderr comment, exit 0" {
202
- # Seed a transcript that WOULD produce records if read.
203
- local sess="$TRANSCRIPT_ROOT/proj/optout.jsonl"
204
- local ts=$(recent_iso 1)
205
- write_skill_invocation "$sess" "wr-itil:manage-problem" "$ts"
206
- # Plant the opt-out marker in the project root.
207
- mkdir -p "$PROJECT_ROOT/.claude"
208
- touch "$PROJECT_ROOT/.claude/.skill-metrics-opt-out"
209
-
210
- # Capture stdout and stderr separately so we can assert: zero records
211
- # on stdout, opt-out comment on stderr, exit 0.
212
- local stdout_file="$FIXTURE_DIR/optout.out"
213
- local stderr_file="$FIXTURE_DIR/optout.err"
214
- "$SCRIPT" --window-days=30 --root="$TRANSCRIPT_ROOT" --project-root="$PROJECT_ROOT" >"$stdout_file" 2>"$stderr_file"
215
- local rc=$?
216
- [ "$rc" -eq 0 ]
217
- [ ! -s "$stdout_file" ]
218
- grep -q "opt-out marker present at" "$stderr_file"
219
- grep -q "$PROJECT_ROOT/.claude/.skill-metrics-opt-out" "$stderr_file"
220
- }
221
-
222
- # ── Confirmation #3: no-network-primitive (negative grep carve-out) ─────────
223
- # ADR-052 carves out this case from "behavioural unless declarative-only".
224
- # Asserts the canonical script body invokes none of the standard exfiltration
225
- # primitives. Negative-grep is the closest behavioural approximation of
226
- # "this script is incapable of network egress".
227
-
228
- @test "Confirmation #3: canonical body contains no network primitives" {
229
- # Grep for curl, wget, raw `nc ` (space-bounded to avoid the `since`,
230
- # `synchronise`, etc. false-positives), fetch, http.client, urllib.
231
- run grep -nE '\bcurl\b|\bwget\b|\bnc[[:space:]]|\bfetch\b|http\.client|\burllib\b' "$SCRIPT"
232
- # `run` captures exit; grep exits 1 on no matches — that is the pass.
233
- [ "$status" -eq 1 ]
234
- [ -z "$output" ]
235
- }
236
-
237
- # ── Confirmation #4: path-hashing / no-content-leak fixture ─────────────────
238
-
239
- @test "Confirmation #4: literal secret-shaped token never appears in NDJSON" {
240
- local sess="$TRANSCRIPT_ROOT/proj/secret.jsonl"
241
- local ts=$(recent_iso 1)
242
- # Bash command carries an absolute-path-shaped string containing a
243
- # synthetic secret token. The plugin attribution extracts the wr-itil-*
244
- # shim name only; the surrounding path and secret are NOT in the
245
- # emitted NDJSON.
246
- write_bash_invocation "$sess" "/private/users/foo/password-XXXX-secret/bin/wr-itil-reconcile-readme docs/problems" "$ts"
247
-
248
- run "$SCRIPT" --window-days=30 --root="$TRANSCRIPT_ROOT" --project-root="$PROJECT_ROOT"
249
- [ "$status" -eq 0 ]
250
- # NDJSON contains the surface attribution
251
- echo "$output" | grep -q '"plugin":"itil"'
252
- echo "$output" | grep -q '"kind":"bash-attributed"'
253
- # NDJSON does NOT contain the secret token or the raw absolute path
254
- ! echo "$output" | grep -q "password-XXXX-secret"
255
- ! echo "$output" | grep -q "/private/users/foo"
256
- }
257
-
258
- @test "Confirmation #4: any surface containing a project path is sha256-12hex hashed" {
259
- # Structural assertion on the schema: when path-bearing fields are added
260
- # in future schema versions, they MUST be 12-hex-char sha256 prefixes.
261
- # The v1.0 schema does not include path-bearing fields; this fixture
262
- # asserts the structural invariant by verifying no unhashed path shape
263
- # surfaces. If a future schema bump introduces a `project_path_hash`
264
- # field, the assertion regex below catches non-hex shapes.
265
- local sess="$TRANSCRIPT_ROOT/proj/hash.jsonl"
266
- local ts=$(recent_iso 1)
267
- write_bash_invocation "$sess" "wr-itil-reconcile-readme docs/problems" "$ts"
268
-
269
- run "$SCRIPT" --window-days=30 --root="$TRANSCRIPT_ROOT" --project-root="$PROJECT_ROOT"
270
- [ "$status" -eq 0 ]
271
- # Any value matching a `path_hash` field (current or future schema)
272
- # MUST be exactly 12 lowercase hex chars. Negative regex: no field with
273
- # value matching a raw path shape.
274
- ! echo "$output" | grep -qE '"[a-z_]+":"/[^"]*"'
275
- }
276
-
277
- # ── Confirmation #5: inaccessible-directory fixture ─────────────────────────
278
-
279
- @test "Confirmation #5: inaccessible --root emits zero records, stderr comment, exit 0" {
280
- local stdout_file="$FIXTURE_DIR/inacc.out"
281
- local stderr_file="$FIXTURE_DIR/inacc.err"
282
- "$SCRIPT" --window-days=30 --root="/nonexistent/path/that/cannot/exist" --project-root="$PROJECT_ROOT" >"$stdout_file" 2>"$stderr_file"
283
- local rc=$?
284
- [ "$rc" -eq 0 ]
285
- [ ! -s "$stdout_file" ]
286
- grep -q "transcript root inaccessible" "$stderr_file"
287
- grep -q "/nonexistent/path/that/cannot/exist" "$stderr_file"
288
- }
289
-
290
- # ── Forward-extension flag: --category-overrides ────────────────────────────
291
- # ADR-058 §"Per-category override hook" — ships unused in Phase 2; the flag
292
- # is accepted-and-validated, no functional effect yet.
293
-
294
- @test "category-overrides flag is accepted without functional effect" {
295
- local sess="$TRANSCRIPT_ROOT/proj/cat.jsonl"
296
- local ts=$(recent_iso 1)
297
- write_skill_invocation "$sess" "wr-itil:manage-problem" "$ts"
298
- # Empty JSON file is a valid (no-op) overrides file.
299
- echo '{}' > "$FIXTURE_DIR/overrides.json"
300
-
301
- run "$SCRIPT" --window-days=30 --root="$TRANSCRIPT_ROOT" --project-root="$PROJECT_ROOT" --category-overrides="$FIXTURE_DIR/overrides.json"
302
- [ "$status" -eq 0 ]
303
- echo "$output" | grep -q '"invocations":1'
304
- }
305
-
306
- # ── Window-days filtering ───────────────────────────────────────────────────
307
-
308
- @test "window-days filter excludes invocations older than the window" {
309
- local sess="$TRANSCRIPT_ROOT/proj/old.jsonl"
310
- # Hours-back exceeds the 1-day window (24h).
311
- local old_ts=$(recent_iso 48)
312
- local recent_ts=$(recent_iso 1)
313
- write_skill_invocation "$sess" "wr-itil:manage-problem" "$old_ts"
314
- write_skill_invocation "$sess" "wr-itil:manage-problem" "$recent_ts"
315
-
316
- run "$SCRIPT" --window-days=1 --root="$TRANSCRIPT_ROOT" --project-root="$PROJECT_ROOT"
317
- [ "$status" -eq 0 ]
318
- # Only the in-window invocation counts; old one drops.
319
- echo "$output" | grep -q '"invocations":1'
320
- }
321
-
322
- # ── Phase 2d: substring-prefilter false-positive fall-through ───────────────
323
- # Iter 6 (2026-05-17) adds a cheap substring guard before json.loads() to skip
324
- # lines that cannot possibly contribute counts. The filter checks for the
325
- # literal substrings `"type":"assistant"` and `"tool_use"` in each line; lines
326
- # missing either are skipped without parsing. Correctness invariant: any line
327
- # whose body content (a `type=text` block, a tool_result, a user message
328
- # rendered into the transcript verbatim) happens to contain those substrings
329
- # MUST fall through to full JSON parse and the existing not-a-real-tool_use
330
- # check MUST exclude it from counts. This fixture seeds exactly that scenario:
331
- # an assistant message carrying a single `type=text` content block whose body
332
- # literally contains both trigger substrings. The legitimate tool_use line in
333
- # the same fixture establishes the expected count = 1. Without the existing
334
- # `c.get("type") == "tool_use"` guard, the false-positive line would inflate
335
- # counts; the assertion below catches any future regression on the
336
- # fall-through path.
337
-
338
- @test "Phase 2d: false-positive substring fall-through does not inflate counts" {
339
- local sess="$TRANSCRIPT_ROOT/proj/falsepos.jsonl"
340
- local ts=$(recent_iso 1)
341
- # One legitimate Skill invocation (counts as 1).
342
- write_skill_invocation "$sess" "wr-itil:manage-problem" "$ts"
343
- # One adversarial assistant message: text body contains both trigger
344
- # substrings but no real tool_use entry. Must NOT add to counts.
345
- python3 - "$sess" "$ts" <<'PYEOF'
346
- import json, sys
347
- file, ts = sys.argv[1], sys.argv[2]
348
- rec = {
349
- "type": "assistant",
350
- "timestamp": ts,
351
- "message": {
352
- "role": "assistant",
353
- "content": [
354
- {"type": "text", "text": 'discussing "type":"assistant" and "tool_use" tokens in prose'}
355
- ]
356
- }
357
- }
358
- with open(file, "a") as fh:
359
- fh.write(json.dumps(rec) + "\n")
360
- PYEOF
361
-
362
- run "$SCRIPT" --window-days=30 --root="$TRANSCRIPT_ROOT" --project-root="$PROJECT_ROOT"
363
- [ "$status" -eq 0 ]
364
- # Exactly one record (the legitimate Skill invocation); count = 1.
365
- local line_count
366
- line_count="$(printf '%s' "$output" | grep -c .)"
367
- [ "$line_count" -eq 1 ]
368
- echo "$output" | grep -q '"invocations":1'
369
- echo "$output" | grep -q '"surface":"wr-itil:manage-problem"'
370
- }
371
-
372
- # ── Phase 2e: binary-search-to-first-in-window byte-seek ────────────────────
373
- # Iter 7 (2026-05-17) adds a binary-search byte-seek before the line iterator
374
- # for files above a size threshold. JSONL is append-only — older lines appear
375
- # earlier in the file by author-timestamp monotonicity. The bisect locates the
376
- # first byte offset whose line carries a timestamp >= cutoff, then scans
377
- # forward. Files below the threshold linear-scan from byte 0 (bisect overhead
378
- # is not worth it for small files). Correctness invariants pinned below.
379
-
380
- # Helper: write a large jsonl that straddles the window cutoff. The first
381
- # `old_count` lines carry timestamps `old_iso` (out-of-window); the next
382
- # `new_count` lines carry timestamps `new_iso` (in-window). Pads each record
383
- # with a `_pad` field so the file is comfortably above the bisect threshold
384
- # even with modest line counts. Sets mtime to "fresh" so the file-level
385
- # mtime filter does not skip the file before the bisect runs.
386
- write_straddle_file() {
387
- local file="$1"; local old_count="$2"; local new_count="$3"
388
- local old_iso="$4"; local new_iso="$5"
389
- mkdir -p "$(dirname "$file")"
390
- python3 - "$file" "$old_count" "$new_count" "$old_iso" "$new_iso" <<'PYEOF'
391
- import json, sys
392
- file, old_count, new_count, old_iso, new_iso = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4], sys.argv[5]
393
- # Pad each record so the file is comfortably > 256 KB even at modest line counts.
394
- pad = "x" * 2048
395
- def rec(ts, skill):
396
- return {
397
- "type": "assistant",
398
- "timestamp": ts,
399
- "_pad": pad,
400
- "message": {
401
- "role": "assistant",
402
- "content": [{"type": "tool_use", "name": "Skill", "input": {"skill": skill}}],
403
- },
404
- }
405
- with open(file, "w") as fh:
406
- for _ in range(old_count):
407
- fh.write(json.dumps(rec(old_iso, "wr-itil:manage-problem")) + "\n")
408
- for _ in range(new_count):
409
- fh.write(json.dumps(rec(new_iso, "wr-itil:manage-problem")) + "\n")
410
- PYEOF
411
- }
412
-
413
- @test "Phase 2e: byte-seek straddle file counts only in-window lines" {
414
- local sess="$TRANSCRIPT_ROOT/proj/straddle.jsonl"
415
- local old_iso=$(recent_iso 1440) # 60 days ago — out-of-window for 30d
416
- local new_iso=$(recent_iso 1) # 1 hour ago — in-window
417
- # 200 old + 50 new = 250 lines × ~2.2KB padded = ~550KB → bisect path.
418
- write_straddle_file "$sess" 200 50 "$old_iso" "$new_iso"
419
- # Ensure file size is above the 256KB bisect threshold (sanity check).
420
- local size
421
- size=$(python3 -c 'import os,sys; print(os.path.getsize(sys.argv[1]))' "$sess")
422
- [ "$size" -gt 262144 ]
423
-
424
- run "$SCRIPT" --window-days=30 --root="$TRANSCRIPT_ROOT" --project-root="$PROJECT_ROOT"
425
- [ "$status" -eq 0 ]
426
- # Only the 50 in-window invocations count; the 200 historical lines are
427
- # excluded by the message-level timestamp filter (already correct under
428
- # linear scan; the bisect must preserve this invariant).
429
- echo "$output" | grep -q '"invocations":50'
430
- echo "$output" | grep -q '"surface":"wr-itil:manage-problem"'
431
- }
432
-
433
- @test "Phase 2e: byte-seek all-in-window file counts every line (no fallthrough loss)" {
434
- local sess="$TRANSCRIPT_ROOT/proj/allnew.jsonl"
435
- local new_iso=$(recent_iso 1)
436
- # 250 lines × ~2.2KB = ~550KB → bisect path. Bisect finds offset 0 (every
437
- # line already in-window) and the linear scan from there counts all 250.
438
- write_straddle_file "$sess" 0 250 "$new_iso" "$new_iso"
439
- local size
440
- size=$(python3 -c 'import os,sys; print(os.path.getsize(sys.argv[1]))' "$sess")
441
- [ "$size" -gt 262144 ]
442
-
443
- run "$SCRIPT" --window-days=30 --root="$TRANSCRIPT_ROOT" --project-root="$PROJECT_ROOT"
444
- [ "$status" -eq 0 ]
445
- echo "$output" | grep -q '"invocations":250'
446
- }
447
-
448
- @test "Phase 2e: small file under threshold takes linear-scan path correctly" {
449
- local sess="$TRANSCRIPT_ROOT/proj/small.jsonl"
450
- local old_iso=$(recent_iso 1440)
451
- local new_iso=$(recent_iso 1)
452
- # Three lines without padding — well under 256KB → linear-scan path.
453
- write_skill_invocation "$sess" "wr-itil:manage-problem" "$old_iso"
454
- write_skill_invocation "$sess" "wr-itil:manage-problem" "$new_iso"
455
- write_skill_invocation "$sess" "wr-itil:manage-problem" "$new_iso"
456
- local size
457
- size=$(python3 -c 'import os,sys; print(os.path.getsize(sys.argv[1]))' "$sess")
458
- [ "$size" -lt 262144 ]
459
-
460
- run "$SCRIPT" --window-days=30 --root="$TRANSCRIPT_ROOT" --project-root="$PROJECT_ROOT"
461
- [ "$status" -eq 0 ]
462
- # 2 in-window (1 old, 2 new); message-timestamp filter excludes the old.
463
- echo "$output" | grep -q '"invocations":2'
464
- }
465
-
466
- @test "Phase 2e: empty large file emits zero records and exits 0" {
467
- local sess="$TRANSCRIPT_ROOT/proj/empty.jsonl"
468
- mkdir -p "$(dirname "$sess")"
469
- # Create empty file with fresh mtime.
470
- : > "$sess"
471
-
472
- run "$SCRIPT" --window-days=30 --root="$TRANSCRIPT_ROOT" --project-root="$PROJECT_ROOT"
473
- [ "$status" -eq 0 ]
474
- # No records.
475
- [ -z "$output" ]
476
- }
477
-
478
- @test "Phase 2e: non-monotonic timestamps — graceful degradation, no crash, NDJSON well-formed" {
479
- # Architect advisory (P087 iter-7 review 2026-05-17): pin behaviour under
480
- # clock-skew / replay where in-window lines appear BEFORE out-of-window
481
- # lines within the same file. Real Claude Code session jsonl files are
482
- # append-only by a single process with a monotonic-ish wall clock; this
483
- # fixture documents the contract under synthetic violation. Bisect MAY
484
- # under-count under non-monotonic input (it locates the first in-window
485
- # line by byte position, not by content scan); the contract is that the
486
- # script exits 0 and emits structurally well-formed NDJSON. ADR-058
487
- # §Performance amendment pins monotonicity as the input invariant.
488
- local sess="$TRANSCRIPT_ROOT/proj/nonmono.jsonl"
489
- local old_iso=$(recent_iso 1440)
490
- local new_iso=$(recent_iso 1)
491
- mkdir -p "$(dirname "$sess")"
492
- # Interleave new / old / new / old ... pattern; padded so file is over
493
- # threshold and bisect path activates.
494
- python3 - "$sess" "$old_iso" "$new_iso" <<'PYEOF'
495
- import json, sys
496
- file, old_iso, new_iso = sys.argv[1], sys.argv[2], sys.argv[3]
497
- pad = "y" * 2048
498
- def rec(ts):
499
- return {
500
- "type": "assistant",
501
- "timestamp": ts,
502
- "_pad": pad,
503
- "message": {
504
- "role": "assistant",
505
- "content": [{"type": "tool_use", "name": "Skill", "input": {"skill": "wr-itil:manage-problem"}}],
506
- },
507
- }
508
- with open(file, "w") as fh:
509
- # 200 lines interleaved old/new — non-monotonic on purpose.
510
- for i in range(200):
511
- ts = new_iso if i % 2 == 0 else old_iso
512
- fh.write(json.dumps(rec(ts)) + "\n")
513
- PYEOF
514
- local size
515
- size=$(python3 -c 'import os,sys; print(os.path.getsize(sys.argv[1]))' "$sess")
516
- [ "$size" -gt 262144 ]
517
-
518
- run "$SCRIPT" --window-days=30 --root="$TRANSCRIPT_ROOT" --project-root="$PROJECT_ROOT"
519
- [ "$status" -eq 0 ]
520
- # Structurally well-formed: zero-or-one record, each line valid JSON, no
521
- # crash. Exact count is NOT pinned — bisect under-count under non-monotonic
522
- # input is documented graceful degradation per ADR-058 amendment.
523
- if [ -n "$output" ]; then
524
- echo "$output" | python3 -c "
525
- import json, sys
526
- for line in sys.stdin.read().splitlines():
527
- if not line: continue
528
- rec = json.loads(line)
529
- assert rec['schema_version'] == '1.0', rec
530
- assert rec['axis'] == 'skill-invocations', rec
531
- assert isinstance(rec.get('invocations'), int), rec
532
- assert rec['invocations'] >= 0, rec
533
- "
534
- fi
535
- }