@windyroad/itil 0.35.4-preview.357 → 0.35.5

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.4"
487
+ "version": "0.35.5"
488
488
  }
@@ -19,8 +19,12 @@
19
19
  #
20
20
  # Allow paths (exit 0 silently per ADR-045 Pattern 1):
21
21
  # - tool_name != "Bash" (only Bash invocations are gated)
22
- # - command does not contain `git commit` substring (non-commit
23
- # Bash bypasses entirely)
22
+ # - command does not invoke (P268: leading-executable check
23
+ # `git commit` as its leading- via `lib/command-detect.sh`
24
+ # effective command — replaces prior substring match
25
+ # that misfired on grep/sed/cat
26
+ # whose arguments contained the
27
+ # literal phrase)
24
28
  # - staged set is README-discipline- (helper returns 0)
25
29
  # clean
26
30
  # - BYPASS_README_REFRESH_GATE=1 env (helper returns 0 first)
@@ -44,10 +48,13 @@
44
48
  # P125 — sibling staging-trap hook (same enforcement-layer shape).
45
49
  # P141 — sibling changeset-discipline hook (same shape).
46
50
  # P165 — this hook.
51
+ # P268 — leading-executable-token command-detect helper.
47
52
 
48
53
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
49
54
  # shellcheck source=lib/readme-refresh-detect.sh
50
55
  source "$SCRIPT_DIR/lib/readme-refresh-detect.sh"
56
+ # shellcheck source=lib/command-detect.sh
57
+ source "$SCRIPT_DIR/lib/command-detect.sh"
51
58
 
52
59
  INPUT=$(cat)
53
60
 
@@ -74,13 +81,14 @@ except:
74
81
  print('')
75
82
  " 2>/dev/null || echo "")
76
83
 
77
- # Only fire on `git commit` invocations. Substring match catches common
78
- # shapes (`git commit -m`, `git commit --amend`, leading `cd && git
79
- # commit`, etc.) without over-matching unrelated bash.
80
- case "$COMMAND" in
81
- *"git commit"*) ;;
82
- *) exit 0 ;;
83
- esac
84
+ # Only fire on actual `git commit` invocations. Delegates to
85
+ # `lib/command-detect.sh::command_invokes_git_commit`, which strips
86
+ # common prefix shapes (leading whitespace, env-var assignments,
87
+ # `cd <path> &&`) and checks whether the residual leading token pair
88
+ # is literally `git commit`. P268: replaced the prior substring match
89
+ # `*"git commit"*` that misfired on non-commit Bash whose argument
90
+ # vectors merely mentioned the phrase (grep/sed/cat-heredoc/echo).
91
+ command_invokes_git_commit "$COMMAND" || exit 0
84
92
 
85
93
  # Run detection. Helper echoes offending ticket path on stdout when
86
94
  # detected; returns 1 in that case. Returns 0 (allow) on no-trap,
@@ -0,0 +1,126 @@
1
+ #!/bin/bash
2
+ # P268: shared command-detection helper for PreToolUse:Bash hooks
3
+ # that need to distinguish ACTUAL `git commit` invocations from Bash
4
+ # commands that merely MENTION the literal phrase "git commit" in
5
+ # argument vectors or heredoc bodies (grep patterns, sed patterns,
6
+ # echo strings, cat heredocs, `git log --grep` queries, etc.).
7
+ #
8
+ # Replaces the case-statement substring match
9
+ #
10
+ # case "$COMMAND" in
11
+ # *"git commit"*) ;;
12
+ # *) exit 0 ;;
13
+ # esac
14
+ #
15
+ # that 5 sibling PreToolUse:Bash hooks previously used to gate on
16
+ # `git commit`. The substring match misfired on legitimate non-commit
17
+ # Bash whose arguments contained the phrase (P268 ticket Description
18
+ # — observed iter-1 retro write and orchestrator grep, ≥3 events per
19
+ # session).
20
+ #
21
+ # Strategy (Fix shape B per P268 ticket):
22
+ # 1. Strip leading whitespace from the candidate command string.
23
+ # 2. Iteratively strip env-var-assignment prefixes (`VAR=value `,
24
+ # `VAR="..." `, `VAR='...' `) and a `cd <path> &&` prefix until
25
+ # stable (order-independent — both shapes can interleave).
26
+ # 3. Check whether the residual leading token pair is literally
27
+ # `git[whitespace]commit` followed by whitespace or end-of-
28
+ # string. The end-of-token boundary check stops `git commit-tree`
29
+ # and similar `git commit-*` plumbing commands from matching.
30
+ #
31
+ # Scope deliberately narrow (per P268 ticket Description recommended
32
+ # Fix shape B): handles only the prefix shapes the orchestrator and
33
+ # capture/manage/work skills actually emit — direct `git commit`,
34
+ # `cd && git commit`, `VAR=value git commit`. Does NOT split on
35
+ # `&&`/`||`/`;`/`|` mid-chain — so `git add foo && git commit` reads
36
+ # as `git add` leading and the helper returns 1 (the gate silently
37
+ # passes). That is a documented and acceptable false-negative: a
38
+ # stand-alone re-run of `git commit` re-triggers detection. False
39
+ # positives — the case this fix exists to close — were causing the
40
+ # orchestrator to need manual workaround (stage README first, then
41
+ # run the offending non-commit command) 3+ times per session.
42
+ #
43
+ # Pure exit-code contract — helper never writes to stdout or stderr.
44
+ # Callers that need to name the trigger surface in deny messages
45
+ # should do so from their own delegated-detect helpers (e.g.
46
+ # `lib/readme-refresh-detect.sh` echoes the offending ticket path).
47
+ #
48
+ # References:
49
+ # ADR-005 — plugin testing strategy (this helper's bats live at
50
+ # `packages/itil/hooks/test/command-detect.bats` per
51
+ # behavioural-test discipline).
52
+ # ADR-009 — gate marker lifecycle (helper is per-invocation, no
53
+ # marker state — same precedent as `lib/staging-detect.sh`,
54
+ # `lib/changeset-detect.sh`, `lib/readme-refresh-detect.sh`).
55
+ # ADR-045 — hook injection budget (silent-on-pass / silent-on-fail
56
+ # — pure exit-code contract).
57
+ # P125 — sibling staging-trap hook (candidate for follow-up
58
+ # refactor to use this helper).
59
+ # P141 — sibling changeset-discipline hook (candidate).
60
+ # P165 — sibling README-refresh-discipline hook (first consumer
61
+ # under P268).
62
+ # P268 — this helper's surfacing ticket.
63
+
64
+ # Returns 0 if $1 is a Bash command that invokes `git commit`.
65
+ # Returns 1 otherwise (including commands that merely mention the
66
+ # phrase in their argument vectors or heredoc bodies, and commands
67
+ # whose leading-effective token is some other git subcommand).
68
+ command_invokes_git_commit() {
69
+ local cmd="$1"
70
+
71
+ # Strip leading whitespace (spaces, tabs, newlines).
72
+ if [[ "$cmd" =~ ^[[:space:]]+ ]]; then
73
+ cmd="${cmd#"${BASH_REMATCH[0]}"}"
74
+ fi
75
+
76
+ # Iteratively strip env-var-assignment prefixes and `cd <path> &&`
77
+ # prefixes until stable. The order is unconstrained: both shapes
78
+ # can appear in either sequence (`VAR=1 cd /tmp && git commit` or
79
+ # `cd /tmp && VAR=1 git commit`).
80
+ local prev=""
81
+ while [ "$cmd" != "$prev" ]; do
82
+ prev="$cmd"
83
+
84
+ # Env-var assignment with double-quoted value: VAR="..." then ws.
85
+ if [[ "$cmd" =~ ^[A-Za-z_][A-Za-z0-9_]*=\"[^\"]*\"[[:space:]]+ ]]; then
86
+ cmd="${cmd#"${BASH_REMATCH[0]}"}"
87
+ continue
88
+ fi
89
+
90
+ # Env-var assignment with single-quoted value: VAR='...' then ws.
91
+ if [[ "$cmd" =~ ^[A-Za-z_][A-Za-z0-9_]*=\'[^\']*\'[[:space:]]+ ]]; then
92
+ cmd="${cmd#"${BASH_REMATCH[0]}"}"
93
+ continue
94
+ fi
95
+
96
+ # Env-var assignment with unquoted value: VAR=word then ws.
97
+ # `word` here is any sequence of non-whitespace characters.
98
+ if [[ "$cmd" =~ ^[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+ ]]; then
99
+ cmd="${cmd#"${BASH_REMATCH[0]}"}"
100
+ continue
101
+ fi
102
+
103
+ # `cd <path> &&` prefix. Path token is double-quoted, single-
104
+ # quoted, or unquoted (in which case it cannot itself contain
105
+ # whitespace or `&`).
106
+ if [[ "$cmd" =~ ^cd[[:space:]]+\"[^\"]*\"[[:space:]]*\&\&[[:space:]]+ ]]; then
107
+ cmd="${cmd#"${BASH_REMATCH[0]}"}"
108
+ continue
109
+ fi
110
+
111
+ if [[ "$cmd" =~ ^cd[[:space:]]+\'[^\']*\'[[:space:]]*\&\&[[:space:]]+ ]]; then
112
+ cmd="${cmd#"${BASH_REMATCH[0]}"}"
113
+ continue
114
+ fi
115
+
116
+ if [[ "$cmd" =~ ^cd[[:space:]]+[^[:space:]\&]+[[:space:]]*\&\&[[:space:]]+ ]]; then
117
+ cmd="${cmd#"${BASH_REMATCH[0]}"}"
118
+ continue
119
+ fi
120
+ done
121
+
122
+ # After prefix-strip, the leading two tokens must be literally
123
+ # `git` and `commit`, with whitespace or end-of-string after
124
+ # `commit` (so `git commit-tree` and similar do not match).
125
+ [[ "$cmd" =~ ^git[[:space:]]+commit([[:space:]]|$) ]]
126
+ }
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # P268: lib/command-detect.sh — `command_invokes_git_commit` helper.
4
+ #
5
+ # Behavioural contract:
6
+ # `command_invokes_git_commit "$cmd"` returns 0 iff `$cmd`, when
7
+ # executed by bash, would actually invoke `git commit` as its
8
+ # leading-effective command (after stripping common prefix shapes:
9
+ # leading whitespace, env-var assignments, `cd <path> &&` prefix).
10
+ # Returns 1 for any other command, including commands whose
11
+ # argument vectors or heredoc bodies merely mention the literal
12
+ # string "git commit".
13
+ #
14
+ # Replaces the substring-match `case "$COMMAND" in *"git commit"*) ;;`
15
+ # pattern that 5 sibling PreToolUse:Bash hooks previously shared. The
16
+ # substring match misfired on `grep -n 'git commit' file.md`,
17
+ # `cat >> file <<EOF ... git commit ... EOF`, `echo "git commit ..."`,
18
+ # `sed -i 's/git commit/.../' file`, `git log --grep 'git commit'`,
19
+ # and similar non-commit Bash invocations whose arguments contained
20
+ # the literal phrase (P268 ticket Description).
21
+ #
22
+ # Per P081 (behavioural tests): tests assert helper return code against
23
+ # realistic command strings — NOT grep-against-source-content.
24
+
25
+ setup() {
26
+ SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
27
+ HELPER="$SCRIPT_DIR/lib/command-detect.sh"
28
+ # shellcheck source=../lib/command-detect.sh
29
+ source "$HELPER"
30
+ }
31
+
32
+ # --- Positive cases (helper returns 0 — command IS a git commit) ---
33
+
34
+ @test "positive: bare git commit -m" {
35
+ run command_invokes_git_commit "git commit -m 'feat'"
36
+ [ "$status" -eq 0 ]
37
+ }
38
+
39
+ @test "positive: git commit --amend" {
40
+ run command_invokes_git_commit "git commit --amend"
41
+ [ "$status" -eq 0 ]
42
+ }
43
+
44
+ @test "positive: git commit with no args" {
45
+ run command_invokes_git_commit "git commit"
46
+ [ "$status" -eq 0 ]
47
+ }
48
+
49
+ @test "positive: leading whitespace before git commit" {
50
+ run command_invokes_git_commit " git commit -m 'feat'"
51
+ [ "$status" -eq 0 ]
52
+ }
53
+
54
+ @test "positive: env-var prefix BYPASS_X=1 git commit" {
55
+ run command_invokes_git_commit "BYPASS_README_REFRESH_GATE=1 git commit -m 'feat'"
56
+ [ "$status" -eq 0 ]
57
+ }
58
+
59
+ @test "positive: multiple env-var prefixes git commit" {
60
+ run command_invokes_git_commit "FOO=1 BAR=baz git commit -m 'feat'"
61
+ [ "$status" -eq 0 ]
62
+ }
63
+
64
+ @test "positive: env-var with double-quoted value git commit" {
65
+ run command_invokes_git_commit "GIT_AUTHOR_NAME=\"Test User\" git commit -m 'feat'"
66
+ [ "$status" -eq 0 ]
67
+ }
68
+
69
+ @test "positive: env-var with single-quoted value git commit" {
70
+ run command_invokes_git_commit "GIT_AUTHOR_NAME='Test User' git commit -m 'feat'"
71
+ [ "$status" -eq 0 ]
72
+ }
73
+
74
+ @test "positive: cd <path> && git commit" {
75
+ run command_invokes_git_commit "cd /tmp && git commit -m 'feat'"
76
+ [ "$status" -eq 0 ]
77
+ }
78
+
79
+ @test "positive: cd <quoted path> && git commit" {
80
+ run command_invokes_git_commit "cd \"/tmp/with spaces\" && git commit -m 'feat'"
81
+ [ "$status" -eq 0 ]
82
+ }
83
+
84
+ @test "positive: env-var then cd then git commit (combined prefixes)" {
85
+ run command_invokes_git_commit "BYPASS=1 cd /tmp && git commit -m 'feat'"
86
+ [ "$status" -eq 0 ]
87
+ }
88
+
89
+ @test "positive: cd then env-var then git commit (reversed prefix order)" {
90
+ run command_invokes_git_commit "cd /tmp && BYPASS=1 git commit -m 'feat'"
91
+ [ "$status" -eq 0 ]
92
+ }
93
+
94
+ @test "positive: tab-indented git commit" {
95
+ run command_invokes_git_commit $'\tgit commit -m feat'
96
+ [ "$status" -eq 0 ]
97
+ }
98
+
99
+ # --- Negative cases (helper returns 1 — command is NOT a git commit) ---
100
+
101
+ @test "negative: grep with 'git commit' as pattern argument" {
102
+ run command_invokes_git_commit "grep -n 'git commit' file.md"
103
+ [ "$status" -eq 1 ]
104
+ }
105
+
106
+ @test "negative: grep -rn 'git commit' (the recurring orchestrator surface)" {
107
+ run command_invokes_git_commit "grep -rn 'git commit' packages/"
108
+ [ "$status" -eq 1 ]
109
+ }
110
+
111
+ @test "negative: cat heredoc whose body contains the phrase git commit" {
112
+ run command_invokes_git_commit $'cat >> docs/problems/README-history.md <<EOF\nWe added a git commit gate.\nEOF'
113
+ [ "$status" -eq 1 ]
114
+ }
115
+
116
+ @test "negative: echo string containing git commit" {
117
+ run command_invokes_git_commit "echo 'the git commit gate fires here'"
118
+ [ "$status" -eq 1 ]
119
+ }
120
+
121
+ @test "negative: sed substitution mentioning git commit" {
122
+ run command_invokes_git_commit "sed -i 's/git commit/git push/' file.md"
123
+ [ "$status" -eq 1 ]
124
+ }
125
+
126
+ @test "negative: git log --grep 'git commit'" {
127
+ run command_invokes_git_commit "git log --grep 'git commit'"
128
+ [ "$status" -eq 1 ]
129
+ }
130
+
131
+ @test "negative: git status (other git subcommand)" {
132
+ run command_invokes_git_commit "git status"
133
+ [ "$status" -eq 1 ]
134
+ }
135
+
136
+ @test "negative: git push (other git subcommand)" {
137
+ run command_invokes_git_commit "git push origin main"
138
+ [ "$status" -eq 1 ]
139
+ }
140
+
141
+ @test "negative: git commit-tree (plumbing — boundary check)" {
142
+ run command_invokes_git_commit "git commit-tree HEAD^{tree}"
143
+ [ "$status" -eq 1 ]
144
+ }
145
+
146
+ @test "negative: empty command" {
147
+ run command_invokes_git_commit ""
148
+ [ "$status" -eq 1 ]
149
+ }
150
+
151
+ @test "negative: whitespace-only command" {
152
+ run command_invokes_git_commit " "
153
+ [ "$status" -eq 1 ]
154
+ }
155
+
156
+ @test "negative: cat with single-quoted body containing git commit phrase" {
157
+ run command_invokes_git_commit "printf '%s\n' 'the git commit gate is here' > /tmp/out"
158
+ [ "$status" -eq 1 ]
159
+ }
160
+
161
+ @test "negative: a different command chained before git commit (e.g. git add foo && git commit) — leading is git add, helper passes" {
162
+ # Per Fix shape B narrow scope: helper only checks the leading-
163
+ # effective command after prefix-strip. Mid-chain `git commit`
164
+ # after a non-prefix-shape leading command (here `git add`) is a
165
+ # documented false negative — standalone re-commit would re-trigger
166
+ # detection. Acceptable per P268 ticket Description.
167
+ run command_invokes_git_commit "git add foo && git commit -m 'feat'"
168
+ [ "$status" -eq 1 ]
169
+ }
170
+
171
+ # --- Silence contract (ADR-045 Pattern 1) ---
172
+
173
+ @test "helper emits zero bytes on stdout (pure exit-code contract)" {
174
+ run command_invokes_git_commit "git commit -m 'feat'"
175
+ [ "${#output}" -eq 0 ]
176
+ }
177
+
178
+ @test "helper emits zero bytes on stdout for negative case too" {
179
+ run command_invokes_git_commit "grep -n 'git commit' file.md"
180
+ [ "${#output}" -eq 0 ]
181
+ }
@@ -419,3 +419,110 @@ EOF
419
419
  [[ "$output" == *".claude/settings.json"* ]]
420
420
  [[ "$output" == *"P173"* ]]
421
421
  }
422
+
423
+ # --- P268: substring-vs-invocation regression coverage ---
424
+ #
425
+ # Prior to P268, the hook used `case "$COMMAND" in *"git commit"*) ;;`
426
+ # which fired on ANY Bash command whose text contained the literal
427
+ # phrase "git commit" — including grep patterns, sed substitutions,
428
+ # cat heredoc bodies, echo strings, and `git log --grep` queries.
429
+ # Workaround was stage-README-first, observed ≥3 times per session.
430
+ # P268 replaces that match with a leading-executable-token check via
431
+ # `lib/command-detect.sh::command_invokes_git_commit`. The tests
432
+ # below stage a ticket file (which would trigger deny if the gate
433
+ # fired) and run various non-commit Bash commands whose argument
434
+ # vectors mention `git commit`. The hook MUST pass silently.
435
+
436
+ @test "P268 allow: grep with 'git commit' pattern does NOT trigger gate" {
437
+ echo "# Problem 999" > docs/problems/open/999-x.md
438
+ git add docs/problems/open/999-x.md
439
+ run run_bash_hook "grep -n 'git commit' file.md"
440
+ [ "$status" -eq 0 ]
441
+ [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
442
+ # Silent pass per ADR-045 Pattern 1.
443
+ [ "${#output}" -eq 0 ]
444
+ }
445
+
446
+ @test "P268 allow: grep -rn 'git commit' packages/ (the recurring orchestrator surface)" {
447
+ echo "# Problem 999" > docs/problems/open/999-x.md
448
+ git add docs/problems/open/999-x.md
449
+ run run_bash_hook "grep -rn 'git commit' packages/"
450
+ [ "$status" -eq 0 ]
451
+ [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
452
+ [ "${#output}" -eq 0 ]
453
+ }
454
+
455
+ @test "P268 allow: sed -i 's/git commit/.../' substitution does NOT trigger gate" {
456
+ echo "# Problem 999" > docs/problems/open/999-x.md
457
+ git add docs/problems/open/999-x.md
458
+ run run_bash_hook "sed -i 's/git commit/git push/' file.md"
459
+ [ "$status" -eq 0 ]
460
+ [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
461
+ [ "${#output}" -eq 0 ]
462
+ }
463
+
464
+ @test "P268 allow: echo with 'git commit' inside string does NOT trigger gate" {
465
+ echo "# Problem 999" > docs/problems/open/999-x.md
466
+ git add docs/problems/open/999-x.md
467
+ run run_bash_hook "echo 'the git commit gate fires here'"
468
+ [ "$status" -eq 0 ]
469
+ [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
470
+ [ "${#output}" -eq 0 ]
471
+ }
472
+
473
+ @test "P268 allow: git log --grep 'git commit' does NOT trigger gate (git log is leading)" {
474
+ echo "# Problem 999" > docs/problems/open/999-x.md
475
+ git add docs/problems/open/999-x.md
476
+ run run_bash_hook "git log --grep 'git commit'"
477
+ [ "$status" -eq 0 ]
478
+ [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
479
+ [ "${#output}" -eq 0 ]
480
+ }
481
+
482
+ @test "P268 allow: cat heredoc whose body contains 'git commit' does NOT trigger gate (iter-1 retro write surface)" {
483
+ echo "# Problem 999" > docs/problems/open/999-x.md
484
+ git add docs/problems/open/999-x.md
485
+ # Inline-build JSON with embedded newlines via printf to mimic the
486
+ # Bash tool's multi-line command payload (the canonical retro-write
487
+ # surface that misfired in P268).
488
+ local payload
489
+ payload=$(python3 -c "import json,sys; print(json.dumps({'tool_name':'Bash','tool_input':{'command':'cat >> docs/problems/README-history.md <<EOF\nFlow note: the git commit gate fires here.\nEOF'}}))")
490
+ run bash -c "echo '$payload' | bash $HOOK"
491
+ [ "$status" -eq 0 ]
492
+ [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
493
+ [ "${#output}" -eq 0 ]
494
+ }
495
+
496
+ @test "P268 deny: actual git commit invocation with staged ticket still triggers gate (positive regression)" {
497
+ echo "# Problem 999" > docs/problems/open/999-x.md
498
+ git add docs/problems/open/999-x.md
499
+ run run_bash_hook "git commit -m 'feat'"
500
+ [ "$status" -eq 0 ]
501
+ [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
502
+ [[ "$output" == *"P165"* ]]
503
+ }
504
+
505
+ @test "P268 deny: cd <path> && git commit (prefix-strip path) still triggers gate" {
506
+ echo "# Problem 999" > docs/problems/open/999-x.md
507
+ git add docs/problems/open/999-x.md
508
+ run run_bash_hook "cd . && git commit -m 'feat'"
509
+ [ "$status" -eq 0 ]
510
+ [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
511
+ }
512
+
513
+ @test "P268 deny: GIT_AUTHOR_NAME=Test git commit (env-prefix path) still triggers gate" {
514
+ echo "# Problem 999" > docs/problems/open/999-x.md
515
+ git add docs/problems/open/999-x.md
516
+ run run_bash_hook "GIT_AUTHOR_NAME=Test git commit -m 'feat'"
517
+ [ "$status" -eq 0 ]
518
+ [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
519
+ }
520
+
521
+ @test "P268 allow: git commit-tree (boundary check — commit-tree is a different plumbing command)" {
522
+ echo "# Problem 999" > docs/problems/open/999-x.md
523
+ git add docs/problems/open/999-x.md
524
+ run run_bash_hook "git commit-tree HEAD^{tree}"
525
+ [ "$status" -eq 0 ]
526
+ [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
527
+ [ "${#output}" -eq 0 ]
528
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.35.4-preview.357",
3
+ "version": "0.35.5",
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"