@windyroad/retrospective 0.20.3 → 0.20.4
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.
|
@@ -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
|
+
}
|
|
@@ -81,6 +81,9 @@
|
|
|
81
81
|
# retro-time advisory consumption as primary).
|
|
82
82
|
# ADR-052 — behavioural-tests default (bats fixture asserts on
|
|
83
83
|
# emitted JSON, not source content).
|
|
84
|
+
# ADR-017 — shared-code sync pattern (command-detect.sh canonical at
|
|
85
|
+
# packages/shared/hooks/lib/; synced into per-package
|
|
86
|
+
# hooks/lib/ via scripts/sync-command-detect.sh).
|
|
84
87
|
# P081 — behavioural tests preferred over structural greps.
|
|
85
88
|
# P125 — sibling staging-trap helper (per-invocation no-marker).
|
|
86
89
|
# P141 — sibling changeset-discipline gate on `git commit` (same
|
|
@@ -88,9 +91,15 @@
|
|
|
88
91
|
# P158 — retro Step 2b wiring (backup advisory; survives this
|
|
89
92
|
# hook's primary-surface migration).
|
|
90
93
|
# P159 — this hook.
|
|
94
|
+
# P268 — shared `command_invokes_git_commit` helper landed in
|
|
95
|
+
# packages/shared/hooks/lib/command-detect.sh.
|
|
96
|
+
# P275 — sibling-hook refactor: substring-match → helper here
|
|
97
|
+
# (first cross-package consumer of the shared helper).
|
|
91
98
|
|
|
92
99
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
93
100
|
DETECTOR="$SCRIPT_DIR/../scripts/check-readme-jtbd-currency.sh"
|
|
101
|
+
# shellcheck source=lib/command-detect.sh
|
|
102
|
+
source "$SCRIPT_DIR/lib/command-detect.sh"
|
|
94
103
|
|
|
95
104
|
INPUT=$(cat)
|
|
96
105
|
|
|
@@ -117,15 +126,10 @@ except:
|
|
|
117
126
|
print('')
|
|
118
127
|
" 2>/dev/null || echo "")
|
|
119
128
|
|
|
120
|
-
# Only fire on `git commit` invocations.
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
# over-matching unrelated bash.
|
|
125
|
-
case "$COMMAND" in
|
|
126
|
-
*"git commit"*) ;;
|
|
127
|
-
*) exit 0 ;;
|
|
128
|
-
esac
|
|
129
|
+
# Only fire on actual `git commit` invocations. P275: delegates to the
|
|
130
|
+
# shared `command_invokes_git_commit` helper for leading-executable
|
|
131
|
+
# semantics (was substring match prone to grep/sed/echo false positives).
|
|
132
|
+
command_invokes_git_commit "$COMMAND" || exit 0
|
|
129
133
|
|
|
130
134
|
# Bypass via env var — single most-common legitimate escape.
|
|
131
135
|
if [ "${BYPASS_JTBD_CURRENCY:-}" = "1" ]; then
|
|
@@ -261,3 +261,74 @@ EOF
|
|
|
261
261
|
[ "$status" -eq 0 ]
|
|
262
262
|
[ "${#output}" -eq 0 ]
|
|
263
263
|
}
|
|
264
|
+
|
|
265
|
+
# ── P275 / P268 leading-executable regression cases ─────────────────────────
|
|
266
|
+
#
|
|
267
|
+
# The hook must fire on ACTUAL `git commit` invocations, NOT on Bash that
|
|
268
|
+
# merely MENTIONS the phrase "git commit" in argument vectors or heredoc
|
|
269
|
+
# bodies. Mirrors P268 regression fixtures in command-detect.bats and
|
|
270
|
+
# P272 sibling fixtures in itil-changeset-discipline.bats.
|
|
271
|
+
|
|
272
|
+
@test "P275 allow: grep with literal 'git commit' pattern on drifted project does NOT deny" {
|
|
273
|
+
make_drifted_project
|
|
274
|
+
run run_bash_hook "grep -r 'git commit' ."
|
|
275
|
+
[ "$status" -eq 0 ]
|
|
276
|
+
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
277
|
+
[ "${#output}" -eq 0 ]
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@test "P275 allow: sed pattern containing 'git commit' on drifted project does NOT deny" {
|
|
281
|
+
make_drifted_project
|
|
282
|
+
run run_bash_hook "sed -n 's/git commit/X/p' packages/stub/README.md"
|
|
283
|
+
[ "$status" -eq 0 ]
|
|
284
|
+
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
285
|
+
[ "${#output}" -eq 0 ]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@test "P275 allow: echo with literal 'git commit' string on drifted project does NOT deny" {
|
|
289
|
+
make_drifted_project
|
|
290
|
+
run run_bash_hook "echo 'run git commit -m foo'"
|
|
291
|
+
[ "$status" -eq 0 ]
|
|
292
|
+
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
293
|
+
[ "${#output}" -eq 0 ]
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@test "P275 allow: git log --grep with 'git commit' search term on drifted project does NOT deny" {
|
|
297
|
+
make_drifted_project
|
|
298
|
+
run run_bash_hook "git log --grep='git commit'"
|
|
299
|
+
[ "$status" -eq 0 ]
|
|
300
|
+
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
301
|
+
[ "${#output}" -eq 0 ]
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
@test "P275 allow: git commit-tree plumbing on drifted project does NOT deny (boundary)" {
|
|
305
|
+
make_drifted_project
|
|
306
|
+
run run_bash_hook "git commit-tree HEAD^{tree} -m 'msg'"
|
|
307
|
+
[ "$status" -eq 0 ]
|
|
308
|
+
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
309
|
+
[ "${#output}" -eq 0 ]
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# ── P275 positive leading-executable cases still deny ──────────────────────
|
|
313
|
+
|
|
314
|
+
@test "P275 deny: env-var-prefixed git commit on drifted project still triggers deny" {
|
|
315
|
+
make_drifted_project
|
|
316
|
+
run run_bash_hook "GIT_AUTHOR_NAME=foo git commit -m 'feat'"
|
|
317
|
+
[ "$status" -eq 0 ]
|
|
318
|
+
[[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
|
|
319
|
+
[[ "$output" == *"P159"* ]]
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
@test "P275 deny: cd-prefixed git commit on drifted project still triggers deny" {
|
|
323
|
+
make_drifted_project
|
|
324
|
+
run run_bash_hook "cd . && git commit -m 'feat'"
|
|
325
|
+
[ "$status" -eq 0 ]
|
|
326
|
+
[[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
@test "P275 deny: leading-whitespace git commit on drifted project still triggers deny" {
|
|
330
|
+
make_drifted_project
|
|
331
|
+
run run_bash_hook " git commit -m 'feat'"
|
|
332
|
+
[ "$status" -eq 0 ]
|
|
333
|
+
[[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
|
|
334
|
+
}
|