@windyroad/retrospective 0.27.1 → 0.27.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 (33) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +5 -2
  3. package/hooks/test/retrospective-deferral-census.bats +0 -112
  4. package/hooks/test/retrospective-readme-jtbd-currency.bats +0 -312
  5. package/scripts/test/check-ask-hygiene.bats +0 -262
  6. package/scripts/test/check-autocreate-rfc-scope.bats +0 -250
  7. package/scripts/test/check-briefing-budgets.bats +0 -349
  8. package/scripts/test/check-internal-id-leaks.bats +0 -286
  9. package/scripts/test/check-plugin-maturity-drift.bats +0 -269
  10. package/scripts/test/check-readme-jtbd-currency.bats +0 -165
  11. package/scripts/test/check-skill-md-budgets.bats +0 -336
  12. package/scripts/test/check-tarball-shipped-shims.bats +0 -284
  13. package/scripts/test/check-tickets-deferred-cause.bats +0 -330
  14. package/scripts/test/list-plugin-attribution.bats +0 -149
  15. package/scripts/test/measure-context-budget.bats +0 -298
  16. package/skills/analyze-context/test/analyze-context-skill-contract.bats +0 -142
  17. package/skills/migrate-briefing/test/migrate-briefing-contract.bats +0 -105
  18. package/skills/migrate-briefing/test/migrate-briefing-fixture.bats +0 -242
  19. package/skills/run-retro/test/run-retro-anti-pattern-clause.bats +0 -87
  20. package/skills/run-retro/test/run-retro-codification-candidates.bats +0 -168
  21. package/skills/run-retro/test/run-retro-context-usage-step-2c.bats +0 -156
  22. package/skills/run-retro/test/run-retro-pipeline-instability-scan.bats +0 -122
  23. package/skills/run-retro/test/run-retro-signal-vs-noise.bats +0 -137
  24. package/skills/run-retro/test/run-retro-skill-candidates.bats +0 -116
  25. package/skills/run-retro/test/run-retro-stage-1-fallback-gating.bats +0 -65
  26. package/skills/run-retro/test/run-retro-step-2d-r6-auto-flag.bats +0 -82
  27. package/skills/run-retro/test/run-retro-step-3-tier-3-branch-b-evidence-based.bats +0 -189
  28. package/skills/run-retro/test/run-retro-step-4a-cross-plugin-dispatch.bats +0 -114
  29. package/skills/run-retro/test/run-retro-step-4a-prior-session-evidence-drain.bats +0 -168
  30. package/skills/run-retro/test/run-retro-step-4a-recovery-path.bats +0 -84
  31. package/skills/run-retro/test/run-retro-step-4b-retro-auto-ticket-carveout.bats +0 -115
  32. package/skills/run-retro/test/run-retro-ticket-first-flow.bats +0 -161
  33. package/skills/run-retro/test/run-retro-verification-close-housekeeping.bats +0 -132
@@ -78,5 +78,5 @@
78
78
  }
79
79
  },
80
80
  "name": "wr-retrospective",
81
- "version": "0.27.1"
81
+ "version": "0.27.2"
82
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/retrospective",
3
- "version": "0.27.1",
3
+ "version": "0.27.2",
4
4
  "description": "Session retrospectives that update briefings and create problem tickets",
5
5
  "bin": {
6
6
  "windyroad-retrospective": "./bin/install.mjs"
@@ -26,6 +26,9 @@
26
26
  "scripts/",
27
27
  ".claude-plugin/",
28
28
  "lib/",
29
- "!skills/*/eval/"
29
+ "!skills/*/eval/",
30
+ "!hooks/test/",
31
+ "!scripts/test/",
32
+ "!skills/*/test/"
30
33
  ]
31
34
  }
@@ -1,112 +0,0 @@
1
- #!/usr/bin/env bats
2
-
3
- # ADR-040 / P375: retrospective-deferral-census.sh (SessionStart) emits a one-line
4
- # census + bounded top-offenders list when deferred-work markers exist that lack a
5
- # self-firing trigger, is silent at zero, self-suppresses under the AFK guard
6
- # (WR_SUPPRESS_DEFERRAL_CENSUS=1), respects the ADR-040 Tier-1 ≤2KB budget, scans
7
- # BOTH docs/ and packages/ (.md only — code-comment false-positive guard), and is
8
- # fail-open (never aborts session startup). Behavioural — exercises the hook against
9
- # fixture trees. Marker vocabulary is the single source of truth in
10
- # lib/deferral-markers.sh (P375 "shared vocabulary").
11
-
12
- setup() {
13
- REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
14
- HOOK="$REPO_ROOT/packages/retrospective/hooks/retrospective-deferral-census.sh"
15
- PLUGIN_ROOT="$REPO_ROOT/packages/retrospective"
16
- DIR="$(mktemp -d)"
17
- mkdir -p "$DIR/docs/problems" "$DIR/packages/itil/skills/foo"
18
- }
19
-
20
- teardown() { rm -rf "$DIR"; }
21
-
22
- mk() { mkdir -p "$(dirname "$DIR/$1")"; printf '%s\n' "$2" > "$DIR/$1"; }
23
-
24
- @test "emits a census line with the total marker count" {
25
- mk "docs/problems/001-x.md" "Priority: 3 (deferred — re-rate at next /wr-itil:review-problems)"
26
- mk "docs/problems/002-y.md" "**Severity**: (deferred to investigation)"
27
- run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
28
- [ "$status" -eq 0 ]
29
- [[ "$output" == *"deferred-work marker"* ]]
30
- [[ "$output" == *"[wr-retrospective]"* ]]
31
- }
32
-
33
- @test "lists the worst-offender files" {
34
- mk "docs/problems/001-x.md" "$(printf '(deferred to A)\n(deferred to B)\n(deferred to C)')"
35
- mk "docs/problems/002-y.md" "(deferred to D)"
36
- run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
37
- [[ "$output" == *"001-x.md"* ]]
38
- }
39
-
40
- @test "scans BOTH docs/ and packages/ .md files" {
41
- mk "packages/itil/skills/foo/SKILL.md" "section deferred to /wr-itil:manage-rfc accepted transition"
42
- run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
43
- [ "$status" -eq 0 ]
44
- [[ "$output" == *"SKILL.md"* ]]
45
- }
46
-
47
- @test "silent when there are no deferred-work markers" {
48
- mk "docs/problems/001-x.md" "Everything here is rated and tracked."
49
- run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
50
- [ "$status" -eq 0 ]
51
- [ -z "$output" ]
52
- }
53
-
54
- @test "fail-open: silent + exit 0 when neither docs/ nor packages/ exists" {
55
- EMPTY="$(mktemp -d)"
56
- run env CLAUDE_PROJECT_DIR="$EMPTY" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
57
- rm -rf "$EMPTY"
58
- [ "$status" -eq 0 ]
59
- [ -z "$output" ]
60
- }
61
-
62
- @test "fail-open: marker lib unreadable → exit 0, no output, no abort" {
63
- mk "docs/problems/001-x.md" "(deferred to investigation)"
64
- run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="/nonexistent/plugin/root" bash "$HOOK"
65
- [ "$status" -eq 0 ]
66
- }
67
-
68
- @test "AFK guard WR_SUPPRESS_DEFERRAL_CENSUS=1 suppresses entirely" {
69
- mk "docs/problems/001-x.md" "(deferred to investigation)"
70
- run env WR_SUPPRESS_DEFERRAL_CENSUS=1 CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
71
- [ "$status" -eq 0 ]
72
- [ -z "$output" ]
73
- }
74
-
75
- @test "guard value other than 1 does not suppress" {
76
- mk "docs/problems/001-x.md" "(deferred to investigation)"
77
- run env WR_SUPPRESS_DEFERRAL_CENSUS=yes CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
78
- [[ "$output" == *"deferred-work marker"* ]]
79
- }
80
-
81
- @test "output respects the ADR-040 Tier-1 budget (<= 2048 bytes) even with many offenders" {
82
- for i in $(seq 1 50); do
83
- mk "docs/problems/$(printf '%03d' "$i")-ticket.md" "$(printf '(deferred to X)\n(deferred to Y)\n(deferred to Z)')"
84
- done
85
- run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
86
- [ "$status" -eq 0 ]
87
- bytes=$(printf '%s' "$output" | wc -c | tr -d ' ')
88
- [ "$bytes" -le 2048 ]
89
- }
90
-
91
- @test "excludes archival records (CHANGELOG.md, *-history.md) — they are not rotting work" {
92
- mk "packages/itil/CHANGELOG.md" "$(printf '(deferred to A)\n(deferred to B)')"
93
- mk "docs/problems/README-history.md" "(deferred to investigation)"
94
- run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
95
- [ "$status" -eq 0 ]
96
- [ -z "$output" ]
97
- }
98
-
99
- @test "catches slice/phase-deferral phrasing (P378 fold) — 'lands in Slice N' etc." {
100
- mk "packages/itil/skills/foo/SKILL.md" "the executor lands in Slice 3 task B5.T9"
101
- mk "packages/itil/skills/bar/SKILL.md" "deferred to a hook-source slice"
102
- run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
103
- [ "$status" -eq 0 ]
104
- [[ "$output" == *"deferred-work marker"* ]]
105
- [[ "$output" == *"SKILL.md"* ]]
106
- }
107
-
108
- @test "points at a drain path (run-retro / backlog) and cites P375" {
109
- mk "docs/problems/001-x.md" "(deferred to investigation)"
110
- run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
111
- [[ "$output" == *"P375"* ]]
112
- }
@@ -1,312 +0,0 @@
1
- #!/usr/bin/env bats
2
-
3
- # ADR-069 (P294): retrospective-readme-jtbd-currency.sh PreToolUse:Bash hook
4
- # must deny `git commit` invocations whose post-commit working tree exhibits
5
- # skill-inventory drift (a directory under packages/<plugin>/skills/ is not
6
- # named in that plugin's README). Hook-level enforcement at commit time is
7
- # retained per the carried-forward load-bearing-from-the-start-for-drift-class
8
- # driver: the most-common drift class (contributor adds a skill and forgets
9
- # the README) ships in a commit that doesn't touch README.md.
10
- #
11
- # HISTORY: under superseded ADR-051 (P159) this hook also gated on
12
- # JTBD-ID-citation drift. ADR-069 superseded that rule — READMEs market the
13
- # persona's problem derived FROM the JTBD but MUST NOT cite JTBD IDs. The
14
- # JTBD-ID anchor + its docs/jtbd/ resolution (and the docs/jtbd/ activation
15
- # guard) are removed; only skill-inventory-drift remains.
16
- #
17
- # Detection delegates to
18
- # `packages/retrospective/scripts/check-readme-jtbd-currency.sh`, invoked
19
- # against the project's working tree (`./packages/`). The hook reads the
20
- # detector's `TOTAL packages=<N> drift_instances=<K>` summary and denies
21
- # when `drift_instances > 0`.
22
- #
23
- # Per ADR-005 / ADR-052 / P081 — behavioural; assert on emitted JSON, no
24
- # source greps. Per ADR-045 Pattern 1 — allow paths emit 0 bytes; deny-band
25
- # ≤300 bytes. Per ADR-013 Rule 1 — deny redirects to mechanical recovery
26
- # (here: "name the skill in the README"). Per ADR-013 Rule 6 — fail-open
27
- # outside a git work tree, on parse errors, or in projects without
28
- # `./packages/`.
29
-
30
- setup() {
31
- SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
32
- HOOK="$SCRIPT_DIR/retrospective-readme-jtbd-currency.sh"
33
- ORIG_DIR="$PWD"
34
- TEST_DIR=$(mktemp -d)
35
- cd "$TEST_DIR"
36
- git init --quiet -b main
37
- git config user.email "test@example.com"
38
- git config user.name "Test"
39
- echo "seed" > seed.txt
40
- git add seed.txt
41
- git -c commit.gpgsign=false commit --quiet -m "initial"
42
- unset BYPASS_JTBD_CURRENCY
43
- }
44
-
45
- teardown() {
46
- cd "$ORIG_DIR"
47
- rm -rf "$TEST_DIR"
48
- unset BYPASS_JTBD_CURRENCY
49
- }
50
-
51
- run_bash_hook() {
52
- local cmd="$1"
53
- local json
54
- json=$(printf '{"tool_name":"Bash","tool_input":{"command":"%s"}}' "$cmd")
55
- echo "$json" | bash "$HOOK"
56
- }
57
-
58
- # Helper: stub project with skill-inventory drift —
59
- # packages/stub/skills/orphan/ exists but the README doesn't name "orphan".
60
- make_drifted_project() {
61
- mkdir -p packages/stub/skills/orphan
62
- printf '%s\n' "# @windyroad/stub" "Markets a problem in prose; names no skills." > packages/stub/README.md
63
- }
64
-
65
- # Helper: clean stub project — every skill directory is named in the README.
66
- make_clean_project() {
67
- mkdir -p packages/stub/skills/do-thing
68
- printf '%s\n' "# @windyroad/stub" "Run /wr-stub:do-thing to solve the problem." > packages/stub/README.md
69
- }
70
-
71
- # ── Trap detection: deny when drift detected ───────────────────────────────
72
-
73
- @test "deny: skill-inventory drift on git commit triggers deny" {
74
- make_drifted_project
75
- run run_bash_hook "git commit -m 'feat'"
76
- [ "$status" -eq 0 ]
77
- [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
78
- [[ "$output" == *"P294"* ]]
79
- }
80
-
81
- @test "deny message names the offending plugin slug" {
82
- make_drifted_project
83
- run run_bash_hook "git commit -m 'feat'"
84
- [ "$status" -eq 0 ]
85
- [[ "$output" == *"stub"* ]]
86
- }
87
-
88
- @test "deny message names the mechanical recovery (name the skill in the README)" {
89
- make_drifted_project
90
- run run_bash_hook "git commit -m 'feat'"
91
- [ "$status" -eq 0 ]
92
- [[ "$output" == *"name the skill in the README"* ]]
93
- }
94
-
95
- @test "deny message does NOT instruct citing a JTBD ID (ADR-069 regression guard)" {
96
- make_drifted_project
97
- run run_bash_hook "git commit -m 'feat'"
98
- [ "$status" -eq 0 ]
99
- [[ "$output" != *"JTBD-NNN"* ]]
100
- [[ "$output" != *"wr-jtbd"* ]]
101
- }
102
-
103
- @test "deny message stays under ADR-045 deny-band (<300 bytes)" {
104
- make_drifted_project
105
- run run_bash_hook "git commit -m 'feat'"
106
- [ "$status" -eq 0 ]
107
- [ "${#output}" -lt 300 ]
108
- }
109
-
110
- # P173: the deny must NOT advertise the env bypass as an in-flight escape.
111
- # BYPASS_JTBD_CURRENCY only takes effect when set in Claude Code's process env
112
- # before the session started; a mid-session Bash export never reaches the hook.
113
- # The deny clarifies the bypass is pre-session.
114
- @test "P173 deny message clarifies the env bypass is pre-session (not a mid-session action)" {
115
- make_drifted_project
116
- run run_bash_hook "git commit -m 'feat'"
117
- [ "$status" -eq 0 ]
118
- [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
119
- [[ "$output" == *"pre-session"* ]]
120
- }
121
-
122
- @test "deny: canonical release commit shape (chore: version packages) is subject to the gate" {
123
- make_drifted_project
124
- # Not a `git commit` invocation — the bare message must NOT deny.
125
- run run_bash_hook "chore: version packages"
126
- [ "$status" -eq 0 ]
127
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
128
- # The canonical release commit shape IS gated:
129
- run run_bash_hook "git commit -m 'chore: version packages'"
130
- [ "$status" -eq 0 ]
131
- [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
132
- }
133
-
134
- @test "deny: git commit --amend on drifted tree also triggers deny" {
135
- make_drifted_project
136
- run run_bash_hook "git commit --amend --no-edit"
137
- [ "$status" -eq 0 ]
138
- [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
139
- }
140
-
141
- @test "deny: skill drift with NO docs/jtbd present still denies (ADR-069 guard removed)" {
142
- make_drifted_project
143
- # deliberately no docs/jtbd/ — under ADR-051 the hook was a no-op here;
144
- # ADR-069 removed that guard, so inventory drift is now evaluated.
145
- run run_bash_hook "git commit -m 'feat'"
146
- [ "$status" -eq 0 ]
147
- [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
148
- }
149
-
150
- # ── Allow paths: each non-trap shape must NOT deny ─────────────────────────
151
-
152
- @test "allow: clean README (all skills named) on git commit allows the commit" {
153
- make_clean_project
154
- run run_bash_hook "git commit -m 'feat'"
155
- [ "$status" -eq 0 ]
156
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
157
- }
158
-
159
- @test "allow: BYPASS_JTBD_CURRENCY=1 env var allows drifted commit" {
160
- make_drifted_project
161
- BYPASS_JTBD_CURRENCY=1 run run_bash_hook "git commit -m 'feat'"
162
- [ "$status" -eq 0 ]
163
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
164
- }
165
-
166
- @test "allow: non-Bash tool exits 0 without deny" {
167
- make_drifted_project
168
- run bash -c "echo '{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"foo.md\"}}' | bash $HOOK"
169
- [ "$status" -eq 0 ]
170
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
171
- }
172
-
173
- @test "allow: Bash command that is NOT git commit (e.g., git status) bypasses detection" {
174
- make_drifted_project
175
- run run_bash_hook "git status"
176
- [ "$status" -eq 0 ]
177
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
178
- }
179
-
180
- # ── Fail-open contracts (ADR-013 Rule 6) ───────────────────────────────────
181
-
182
- @test "allow: outside a git work tree exits 0 without deny (fail-open)" {
183
- cd "$ORIG_DIR"
184
- TEMP_NONGIT=$(mktemp -d)
185
- cd "$TEMP_NONGIT"
186
- run run_bash_hook "git commit -m 'feat'"
187
- cd "$TEST_DIR"
188
- rm -rf "$TEMP_NONGIT"
189
- [ "$status" -eq 0 ]
190
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
191
- }
192
-
193
- @test "allow: project without packages/ dir exits 0 without deny (fail-open)" {
194
- # No packages/ — adopter project shape.
195
- run run_bash_hook "git commit -m 'feat'"
196
- [ "$status" -eq 0 ]
197
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
198
- }
199
-
200
- @test "allow: project with packages/ and no docs/jtbd/, clean README, allows (docs/jtbd no longer required)" {
201
- make_clean_project
202
- run run_bash_hook "git commit -m 'feat'"
203
- [ "$status" -eq 0 ]
204
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
205
- }
206
-
207
- @test "allow: empty JSON exits 0 without deny (fail-open on parse-incomplete)" {
208
- make_drifted_project
209
- run bash -c "echo '{}' | bash $HOOK"
210
- [ "$status" -eq 0 ]
211
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
212
- }
213
-
214
- @test "allow: malformed JSON exits 0 without deny (fail-open on parse error)" {
215
- make_drifted_project
216
- run bash -c "echo 'not-json' | bash $HOOK"
217
- [ "$status" -eq 0 ]
218
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
219
- }
220
-
221
- # ── Allow path silence (ADR-045 Pattern 1) ─────────────────────────────────
222
-
223
- @test "allow path on clean tree emits 0 bytes (ADR-045 Pattern 1 silent-on-pass)" {
224
- make_clean_project
225
- run run_bash_hook "git commit -m 'feat'"
226
- [ "$status" -eq 0 ]
227
- [ "${#output}" -eq 0 ]
228
- }
229
-
230
- @test "allow path on non-Bash tool emits 0 bytes (silent-on-pass)" {
231
- make_drifted_project
232
- run bash -c "echo '{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"foo.md\"}}' | bash $HOOK"
233
- [ "$status" -eq 0 ]
234
- [ "${#output}" -eq 0 ]
235
- }
236
-
237
- @test "allow path on non-commit Bash emits 0 bytes (silent-on-pass)" {
238
- make_drifted_project
239
- run run_bash_hook "git status"
240
- [ "$status" -eq 0 ]
241
- [ "${#output}" -eq 0 ]
242
- }
243
-
244
- # ── P275 / P268 leading-executable regression cases ─────────────────────────
245
- #
246
- # The hook must fire on ACTUAL `git commit` invocations, NOT on Bash that
247
- # merely MENTIONS the phrase "git commit" in argument vectors or heredoc
248
- # bodies. Mirrors P268 regression fixtures in command-detect.bats.
249
-
250
- @test "P275 allow: grep with literal 'git commit' pattern on drifted project does NOT deny" {
251
- make_drifted_project
252
- run run_bash_hook "grep -r 'git commit' ."
253
- [ "$status" -eq 0 ]
254
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
255
- [ "${#output}" -eq 0 ]
256
- }
257
-
258
- @test "P275 allow: sed pattern containing 'git commit' on drifted project does NOT deny" {
259
- make_drifted_project
260
- run run_bash_hook "sed -n 's/git commit/X/p' packages/stub/README.md"
261
- [ "$status" -eq 0 ]
262
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
263
- [ "${#output}" -eq 0 ]
264
- }
265
-
266
- @test "P275 allow: echo with literal 'git commit' string on drifted project does NOT deny" {
267
- make_drifted_project
268
- run run_bash_hook "echo 'run git commit -m foo'"
269
- [ "$status" -eq 0 ]
270
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
271
- [ "${#output}" -eq 0 ]
272
- }
273
-
274
- @test "P275 allow: git log --grep with 'git commit' search term on drifted project does NOT deny" {
275
- make_drifted_project
276
- run run_bash_hook "git log --grep='git commit'"
277
- [ "$status" -eq 0 ]
278
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
279
- [ "${#output}" -eq 0 ]
280
- }
281
-
282
- @test "P275 allow: git commit-tree plumbing on drifted project does NOT deny (boundary)" {
283
- make_drifted_project
284
- run run_bash_hook "git commit-tree HEAD^{tree} -m 'msg'"
285
- [ "$status" -eq 0 ]
286
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
287
- [ "${#output}" -eq 0 ]
288
- }
289
-
290
- # ── P275 positive leading-executable cases still deny ──────────────────────
291
-
292
- @test "P275 deny: env-var-prefixed git commit on drifted project still triggers deny" {
293
- make_drifted_project
294
- run run_bash_hook "GIT_AUTHOR_NAME=foo git commit -m 'feat'"
295
- [ "$status" -eq 0 ]
296
- [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
297
- [[ "$output" == *"P294"* ]]
298
- }
299
-
300
- @test "P275 deny: cd-prefixed git commit on drifted project still triggers deny" {
301
- make_drifted_project
302
- run run_bash_hook "cd . && git commit -m 'feat'"
303
- [ "$status" -eq 0 ]
304
- [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
305
- }
306
-
307
- @test "P275 deny: leading-whitespace git commit on drifted project still triggers deny" {
308
- make_drifted_project
309
- run run_bash_hook " git commit -m 'feat'"
310
- [ "$status" -eq 0 ]
311
- [[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
312
- }