@windyroad/itil 0.35.6-preview.363 → 0.35.7-preview.365

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -484,5 +484,5 @@
484
484
  }
485
485
  },
486
486
  "name": "wr-itil",
487
- "version": "0.35.6"
487
+ "version": "0.35.7"
488
488
  }
@@ -48,11 +48,22 @@
48
48
  # marker (per-invocation deterministic; mirrors P125 staging-detect.sh
49
49
  # and P141 itil-changeset-discipline.sh precedent).
50
50
  #
51
+ # Command-shape detection delegates to
52
+ # `lib/command-detect.sh::command_invokes_git_commit`, which strips
53
+ # common prefix shapes (leading whitespace, env-var assignments,
54
+ # `cd <path> &&`) and checks whether the residual leading token pair
55
+ # is literally `git commit`. P274: replaced the prior substring match
56
+ # `*"git commit"*` that misfired on non-commit Bash whose argument
57
+ # vectors merely mentioned the phrase (grep / sed / cat-heredoc /
58
+ # echo / `git log --grep`).
59
+ #
51
60
  # References:
52
61
  # ADR-005 — plugin testing strategy (hook bats live under hooks/test/).
53
62
  # ADR-013 Rule 6 — fail-open on missing inputs / parse errors.
54
63
  # ADR-014 — single-commit grain (this hook never auto-fixes via
55
64
  # follow-up commit; advisory-only).
65
+ # ADR-017 — shared-code sync pattern (command-detect.sh canonical at
66
+ # packages/shared/hooks/lib/).
56
67
  # ADR-038 — progressive disclosure / advisory band ≤300 bytes.
57
68
  # ADR-045 — hook injection budget; Pattern 1 silent-on-pass.
58
69
  # ADR-051 — load-bearing-from-the-start (drift detection ships at
@@ -63,6 +74,12 @@
63
74
  # P081 — behavioural tests preferred over structural greps.
64
75
  # P125 — sibling per-invocation no-marker hook precedent.
65
76
  # P141 — sibling commit-time gate hook precedent.
77
+ # P268 — shared `command_invokes_git_commit` helper.
78
+ # P274 — sibling-hook refactor: substring-match → helper here.
79
+
80
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
81
+ # shellcheck source=lib/command-detect.sh
82
+ source "$SCRIPT_DIR/lib/command-detect.sh"
66
83
 
67
84
  INPUT=$(cat)
68
85
 
@@ -89,11 +106,10 @@ except:
89
106
  print('')
90
107
  " 2>/dev/null || echo "")
91
108
 
92
- # Only fire on `git commit` invocations.
93
- case "$COMMAND" in
94
- *"git commit"*) ;;
95
- *) exit 0 ;;
96
- esac
109
+ # Only fire on actual `git commit` invocations. P274: delegates to the
110
+ # shared `command_invokes_git_commit` helper for leading-executable
111
+ # semantics (was substring match prone to grep/sed/echo false positives).
112
+ command_invokes_git_commit "$COMMAND" || exit 0
97
113
 
98
114
  # Bypass via env var.
99
115
  if [ "${BYPASS_RFC_TRAILER_ADVISORY:-}" = "1" ]; then
File without changes
@@ -10,10 +10,19 @@
10
10
  # a recovery path" contract via the mechanical-recovery shape (no
11
11
  # skill wrapper required — re-staging a file is a single command).
12
12
  #
13
+ # Command-shape detection delegates to
14
+ # `lib/command-detect.sh::command_invokes_git_commit`, which strips
15
+ # common prefix shapes (leading whitespace, env-var assignments,
16
+ # `cd <path> &&`) and checks whether the residual leading token pair
17
+ # is literally `git commit`. P273: replaced the prior substring match
18
+ # `*"git commit"*` that misfired on non-commit Bash whose argument
19
+ # vectors merely mentioned the phrase (grep / sed / cat-heredoc /
20
+ # echo / `git log --grep`).
21
+ #
13
22
  # Allow paths (exit 0 without deny):
14
23
  # - tool_name != "Bash" (only Bash invocations are gated)
15
- # - command does not contain `git commit` substring (non-commit
16
- # Bash bypasses entirely)
24
+ # - command is not a `git commit` invocation by leading-executable
25
+ # semantics (helper returns 1)
17
26
  # - working tree clean of trap (helper returns 0)
18
27
  # - outside a git work tree (helper fails-open)
19
28
  # - parse failure on stdin (mirrors create-gate.sh fail-open)
@@ -23,15 +32,23 @@
23
32
  # ADR-009 — gate marker lifecycle (this hook deliberately does NOT
24
33
  # use markers; detection is per-invocation deterministic).
25
34
  # ADR-013 Rule 1 — deny redirects with mechanical recovery.
35
+ # ADR-017 — shared-code sync pattern (command-detect.sh canonical at
36
+ # packages/shared/hooks/lib/; synced into per-package
37
+ # hooks/lib/ via scripts/sync-command-detect.sh).
26
38
  # ADR-038 — progressive disclosure / deny-message terseness budget.
27
39
  # P057 — original staging-trap ticket; this hook is the
28
40
  # enforcement layer the documentation alone didn't provide.
29
41
  # P119 — sibling create-gate hook (PreToolUse:Write + lib/create-gate.sh).
30
42
  # P125 — this hook.
43
+ # P268 — shared `command_invokes_git_commit` helper landed for
44
+ # `itil-readme-refresh-discipline.sh`.
45
+ # P273 — sibling-hook refactor: substring-match → helper here.
31
46
 
32
47
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
33
48
  # shellcheck source=lib/staging-detect.sh
34
49
  source "$SCRIPT_DIR/lib/staging-detect.sh"
50
+ # shellcheck source=lib/command-detect.sh
51
+ source "$SCRIPT_DIR/lib/command-detect.sh"
35
52
 
36
53
  INPUT=$(cat)
37
54
 
@@ -58,13 +75,12 @@ except:
58
75
  print('')
59
76
  " 2>/dev/null || echo "")
60
77
 
61
- # Only fire on `git commit` invocations. Substring match catches
62
- # common shapes (`git commit -m`, `git commit --amend`, leading
63
- # `cd && git commit`, etc.) without over-matching unrelated bash.
64
- case "$COMMAND" in
65
- *"git commit"*) ;;
66
- *) exit 0 ;;
67
- esac
78
+ # Only fire on actual `git commit` invocations. Delegates to
79
+ # `lib/command-detect.sh::command_invokes_git_commit`, which strips
80
+ # common prefix shapes (leading whitespace, env-var assignments,
81
+ # `cd <path> &&`) and checks whether the residual leading token pair
82
+ # is literally `git commit`. P273: replaces the prior substring match.
83
+ command_invokes_git_commit "$COMMAND" || exit 0
68
84
 
69
85
  # Run detection. Helper echoes trap'd path on stdout when detected;
70
86
  # returns 1 in that case. Returns 0 (allow) on no-trap or fail-open
@@ -271,3 +271,105 @@ Refs: RFC-002"
271
271
  # Permit per-line overhead; the advisory should remain readable.
272
272
  [ "${#output}" -le 600 ]
273
273
  }
274
+
275
+ # ── P274 / P268 leading-executable regression cases ─────────────────────────
276
+ #
277
+ # The hook must fire on ACTUAL `git commit` invocations, NOT on Bash that
278
+ # merely MENTIONS the phrase "git commit" in argument vectors or heredoc
279
+ # bodies. Mirrors the P268 regression fixtures in command-detect.bats and
280
+ # the P272 sibling fixtures in itil-changeset-discipline.bats.
281
+ #
282
+ # Setup constructs a drift state (RFC + stale problem) — the hook would
283
+ # emit an advisory on any `git commit` after `Refs: RFC-001` lands in HEAD.
284
+ # These tests confirm non-commit Bash that mentions "git commit" does NOT
285
+ # emit the advisory.
286
+
287
+ @test "P274 allow: grep with literal 'git commit' pattern in drift state → silent" {
288
+ write_rfc "001" "foo" "accepted"
289
+ write_problem_without_rfcs_section "168"
290
+ echo "x" > stub.txt
291
+ git add stub.txt
292
+ git -c commit.gpgsign=false commit --quiet -m "feat: stub" -m "" -m "Refs: RFC-001"
293
+ run run_post_bash_hook "grep -r 'git commit' ."
294
+ [ "$status" -eq 0 ]
295
+ [ -z "$output" ]
296
+ }
297
+
298
+ @test "P274 allow: sed pattern containing 'git commit' in drift state → silent" {
299
+ write_rfc "001" "foo" "accepted"
300
+ write_problem_without_rfcs_section "168"
301
+ echo "x" > stub.txt
302
+ git add stub.txt
303
+ git -c commit.gpgsign=false commit --quiet -m "feat: stub" -m "" -m "Refs: RFC-001"
304
+ run run_post_bash_hook "sed -n 's/git commit/X/p' stub.txt"
305
+ [ "$status" -eq 0 ]
306
+ [ -z "$output" ]
307
+ }
308
+
309
+ @test "P274 allow: echo with literal 'git commit' string in drift state → silent" {
310
+ write_rfc "001" "foo" "accepted"
311
+ write_problem_without_rfcs_section "168"
312
+ echo "x" > stub.txt
313
+ git add stub.txt
314
+ git -c commit.gpgsign=false commit --quiet -m "feat: stub" -m "" -m "Refs: RFC-001"
315
+ run run_post_bash_hook "echo 'run git commit -m foo'"
316
+ [ "$status" -eq 0 ]
317
+ [ -z "$output" ]
318
+ }
319
+
320
+ @test "P274 allow: git log --grep with 'git commit' search term in drift state → silent" {
321
+ write_rfc "001" "foo" "accepted"
322
+ write_problem_without_rfcs_section "168"
323
+ echo "x" > stub.txt
324
+ git add stub.txt
325
+ git -c commit.gpgsign=false commit --quiet -m "feat: stub" -m "" -m "Refs: RFC-001"
326
+ run run_post_bash_hook "git log --grep='git commit'"
327
+ [ "$status" -eq 0 ]
328
+ [ -z "$output" ]
329
+ }
330
+
331
+ @test "P274 allow: git commit-tree plumbing in drift state → silent (boundary)" {
332
+ write_rfc "001" "foo" "accepted"
333
+ write_problem_without_rfcs_section "168"
334
+ echo "x" > stub.txt
335
+ git add stub.txt
336
+ git -c commit.gpgsign=false commit --quiet -m "feat: stub" -m "" -m "Refs: RFC-001"
337
+ run run_post_bash_hook "git commit-tree HEAD^{tree} -m 'msg'"
338
+ [ "$status" -eq 0 ]
339
+ [ -z "$output" ]
340
+ }
341
+
342
+ # ── P274 positive leading-executable cases still emit advisory ──────────────
343
+
344
+ @test "P274 advisory: env-var-prefixed git commit in drift state still emits advisory" {
345
+ write_rfc "001" "foo" "accepted"
346
+ write_problem_without_rfcs_section "168"
347
+ echo "x" > stub.txt
348
+ git add stub.txt
349
+ git -c commit.gpgsign=false commit --quiet -m "feat: stub" -m "" -m "Refs: RFC-001"
350
+ run run_post_bash_hook "GIT_AUTHOR_NAME=foo git commit -m foo"
351
+ [ "$status" -eq 0 ]
352
+ [[ "$output" == *"RFC-001"* ]]
353
+ }
354
+
355
+ @test "P274 advisory: cd-prefixed git commit in drift state still emits advisory" {
356
+ write_rfc "001" "foo" "accepted"
357
+ write_problem_without_rfcs_section "168"
358
+ echo "x" > stub.txt
359
+ git add stub.txt
360
+ git -c commit.gpgsign=false commit --quiet -m "feat: stub" -m "" -m "Refs: RFC-001"
361
+ run run_post_bash_hook "cd . && git commit -m foo"
362
+ [ "$status" -eq 0 ]
363
+ [[ "$output" == *"RFC-001"* ]]
364
+ }
365
+
366
+ @test "P274 advisory: leading-whitespace git commit in drift state still emits advisory" {
367
+ write_rfc "001" "foo" "accepted"
368
+ write_problem_without_rfcs_section "168"
369
+ echo "x" > stub.txt
370
+ git add stub.txt
371
+ git -c commit.gpgsign=false commit --quiet -m "feat: stub" -m "" -m "Refs: RFC-001"
372
+ run run_post_bash_hook " git commit -m foo"
373
+ [ "$status" -eq 0 ]
374
+ [[ "$output" == *"RFC-001"* ]]
375
+ }
@@ -139,3 +139,93 @@ run_bash_hook() {
139
139
  [ "$status" -eq 0 ]
140
140
  [ "${#output}" -lt 400 ]
141
141
  }
142
+
143
+ # --- P273 / P268: leading-executable command detection regression cases ---
144
+ #
145
+ # The hook must fire on ACTUAL `git commit` invocations, NOT on Bash that
146
+ # merely MENTIONS the phrase "git commit" in argument vectors or heredoc
147
+ # bodies. Mirrors the P268 regression fixtures landed in
148
+ # command-detect.bats and the P272 sibling fixtures in
149
+ # itil-changeset-discipline.bats.
150
+
151
+ @test "allow: grep with literal 'git commit' pattern in trap state does NOT deny" {
152
+ # Trap shape IS present in the working tree, but the Bash is a grep
153
+ # invocation whose pattern argument MENTIONS "git commit" — the hook
154
+ # must not fire (leading executable is `grep`, not `git`).
155
+ git mv foo.md bar.md
156
+ echo "modified" > bar.md
157
+ run run_bash_hook "grep -r 'git commit' ."
158
+ [ "$status" -eq 0 ]
159
+ [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
160
+ }
161
+
162
+ @test "allow: sed pattern containing 'git commit' in trap state does NOT deny" {
163
+ git mv foo.md bar.md
164
+ echo "modified" > bar.md
165
+ run run_bash_hook "sed -n 's/git commit/X/p' bar.md"
166
+ [ "$status" -eq 0 ]
167
+ [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
168
+ }
169
+
170
+ @test "allow: echo with literal 'git commit' string in trap state does NOT deny" {
171
+ git mv foo.md bar.md
172
+ echo "modified" > bar.md
173
+ run run_bash_hook "echo 'run git commit -m foo'"
174
+ [ "$status" -eq 0 ]
175
+ [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
176
+ }
177
+
178
+ @test "allow: cat heredoc body containing 'git commit' in trap state does NOT deny" {
179
+ git mv foo.md bar.md
180
+ echo "modified" > bar.md
181
+ run run_bash_hook "cat <<EOF
182
+ prose mentioning git commit invocations
183
+ EOF"
184
+ [ "$status" -eq 0 ]
185
+ [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
186
+ }
187
+
188
+ @test "allow: git log --grep with 'git commit' search term does NOT deny" {
189
+ git mv foo.md bar.md
190
+ echo "modified" > bar.md
191
+ run run_bash_hook "git log --grep='git commit'"
192
+ [ "$status" -eq 0 ]
193
+ [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
194
+ }
195
+
196
+ @test "allow: git commit-tree plumbing in trap state does NOT deny (boundary)" {
197
+ # `git commit-tree` is a different git subcommand — the helper must
198
+ # not match it on the `commit-*` boundary.
199
+ git mv foo.md bar.md
200
+ echo "modified" > bar.md
201
+ run run_bash_hook "git commit-tree HEAD^{tree} -m 'msg'"
202
+ [ "$status" -eq 0 ]
203
+ [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
204
+ }
205
+
206
+ # --- P273 / P268: positive leading-executable cases still deny ---
207
+
208
+ @test "deny: env-var-prefixed git commit in trap state still triggers deny" {
209
+ git mv foo.md bar.md
210
+ echo "modified" > bar.md
211
+ run run_bash_hook "GIT_AUTHOR_NAME=foo git commit -m 'test'"
212
+ [ "$status" -eq 0 ]
213
+ [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
214
+ [[ "$output" == *"P057"* ]]
215
+ }
216
+
217
+ @test "deny: cd-prefixed git commit in trap state still triggers deny" {
218
+ git mv foo.md bar.md
219
+ echo "modified" > bar.md
220
+ run run_bash_hook "cd . && git commit -m 'test'"
221
+ [ "$status" -eq 0 ]
222
+ [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
223
+ }
224
+
225
+ @test "deny: leading-whitespace git commit in trap state still triggers deny" {
226
+ git mv foo.md bar.md
227
+ echo "modified" > bar.md
228
+ run run_bash_hook " git commit -m 'test'"
229
+ [ "$status" -eq 0 ]
230
+ [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
231
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.35.6-preview.363",
3
+ "version": "0.35.7-preview.365",
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"