@windyroad/itil 0.35.6 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/hooks/itil-rfc-trailer-advisory.sh +21 -5
- package/hooks/lib/command-detect.sh +0 -0
- package/hooks/p057-staging-trap-detect.sh +25 -9
- package/hooks/test/itil-rfc-trailer-advisory.bats +102 -0
- package/hooks/test/p057-staging-trap-detect.bats +90 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
16
|
-
#
|
|
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.
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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