@windyroad/itil 0.61.1 → 0.61.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.
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +7 -2
- package/agents/test/fixtures/proceed-new-genuinely-new.md +0 -43
- package/agents/test/fixtures/proceed-new-subtle-sibling.md +0 -47
- package/agents/test/fixtures/regression-p347-vs-p346.md +0 -54
- package/agents/test/hang-off-check.bats +0 -225
- package/hooks/test/block-list.bats +0 -153
- package/hooks/test/command-detect.bats +0 -181
- package/hooks/test/itil-assistant-output-gate.bats +0 -114
- package/hooks/test/itil-assistant-output-review.bats +0 -224
- package/hooks/test/itil-bash-polling-antipattern-detect.bats +0 -154
- package/hooks/test/itil-changeset-discipline.bats +0 -603
- package/hooks/test/itil-claude-space-protection.bats +0 -294
- package/hooks/test/itil-commit-trailer-transition-advisory.bats +0 -80
- package/hooks/test/itil-correction-detect.bats +0 -189
- package/hooks/test/itil-deferral-cadence-gate.bats +0 -187
- package/hooks/test/itil-fictional-defer-detect.bats +0 -292
- package/hooks/test/itil-fix-title-lifecycle-advisory.bats +0 -129
- package/hooks/test/itil-mid-loop-ask-detect.bats +0 -220
- package/hooks/test/itil-no-implement-draft-gate.bats +0 -210
- package/hooks/test/itil-pending-questions-surface.bats +0 -307
- package/hooks/test/itil-readme-refresh-discipline.bats +0 -642
- package/hooks/test/itil-rfc-oversight-nudge.bats +0 -83
- package/hooks/test/itil-rfc-trailer-advisory.bats +0 -375
- package/hooks/test/itil-story-mirror-migration-nudge.bats +0 -122
- package/hooks/test/manage-problem-enforce-create.bats +0 -424
- package/hooks/test/p057-staging-trap-detect.bats +0 -231
- package/hooks/test/pre-publish-intake-gate.bats +0 -144
- package/hooks/test/runtime-sid-marker.bats +0 -90
- package/hooks/test/session-id.bats +0 -370
- package/scripts/migrate-problems-add-type.sh +0 -116
- package/scripts/test/catchup-scan.bats +0 -274
- package/scripts/test/check-afk-accept-eligible.bats +0 -310
- package/scripts/test/check-fail-soft-skip-discipline.bats +0 -172
- package/scripts/test/check-fix-rfc-trace.bats +0 -164
- package/scripts/test/check-locale-discipline.bats +0 -291
- package/scripts/test/check-problems-readme-budget.bats +0 -192
- package/scripts/test/check-rfc-has-stories.bats +0 -69
- package/scripts/test/check-rfc-rejected-alternatives.bats +0 -108
- package/scripts/test/check-rfc-stories-ratified.bats +0 -82
- package/scripts/test/check-upstream-responses.bats +0 -503
- package/scripts/test/classify-readme-drift.bats +0 -392
- package/scripts/test/derive-first-dispatch.bats +0 -274
- package/scripts/test/derive-release-vehicle.bats +0 -454
- package/scripts/test/detect-unratified-stories-maps.bats +0 -60
- package/scripts/test/dual-tolerant-glob-rfc-002-t2.bats +0 -309
- package/scripts/test/effort-tally.bats +0 -170
- package/scripts/test/evaluate-relevance.bats +0 -773
- package/scripts/test/kind-a-commands.bats +0 -46
- package/scripts/test/mark-create-gate.bats +0 -59
- package/scripts/test/mark-story-oversight-confirmed.bats +0 -53
- package/scripts/test/migrate-problems-add-type.bats +0 -280
- package/scripts/test/migrate-story-status-mirror.bats +0 -167
- package/scripts/test/no-type-regression-guard.bats +0 -74
- package/scripts/test/oversight-mirror-corpus-lint.bats +0 -150
- package/scripts/test/plugin-exercise-index.bats +0 -386
- package/scripts/test/plugin-maturity-doc-lint.bats +0 -470
- package/scripts/test/plugin-maturity-populate.bats +0 -631
- package/scripts/test/plugin-maturity-render.bats +0 -469
- package/scripts/test/plugin-validate-ci-gate.bats +0 -208
- package/scripts/test/reconcile-readme.bats +0 -1007
- package/scripts/test/reconcile-rfcs.bats +0 -496
- package/scripts/test/reconcile-stories.bats +0 -173
- package/scripts/test/reconcile-story-maps.bats +0 -74
- package/scripts/test/release-watch-poll-loop.bats +0 -180
- package/scripts/test/resolve-governance-plugin-dirs.bats +0 -78
- package/scripts/test/rfc-stories-extension.bats +0 -175
- package/scripts/test/skill-invocations.bats +0 -535
- package/scripts/test/skill-md-dual-tolerant-coverage-rfc-002-t3.bats +0 -374
- package/scripts/test/story-oversight-lib.bats +0 -190
- package/scripts/test/update-jtbd-references-related-problems.bats +0 -178
- package/scripts/test/update-problem-references-section.bats +0 -242
- package/scripts/test/update-problem-rfcs-section.bats +0 -242
- package/scripts/test/update-references-section-sibling-helpers.bats +0 -80
- package/scripts/test/update-rfc-commits-section.bats +0 -77
- package/scripts/test/verify-iter-summary.bats +0 -196
- package/scripts/test/working-the-problem-traversal.bats +0 -116
- package/skills/capture-problem/test/capture-problem-step-1-5b-jtbd-trace.bats +0 -398
- package/skills/capture-problem/test/capture-problem.bats +0 -389
- package/skills/capture-story/test/capture-story-behavioural.bats +0 -317
- package/skills/capture-story-map/test/capture-story-map-behavioural.bats +0 -98
- package/skills/close-incident/test/close-incident-contract.bats +0 -146
- package/skills/link-incident/test/link-incident-contract.bats +0 -125
- package/skills/list-incidents/test/list-incidents-contract.bats +0 -115
- package/skills/list-problems/test/list-problems-contract.bats +0 -100
- package/skills/list-stories/test/list-stories-contract.bats +0 -127
- package/skills/list-story-maps/test/list-story-maps-contract.bats +0 -46
- package/skills/manage-incident/test/manage-incident-adr-044-contract.bats +0 -206
- package/skills/manage-incident/test/manage-incident-close-forwarder.bats +0 -67
- package/skills/manage-incident/test/manage-incident-link-forwarder.bats +0 -67
- package/skills/manage-incident/test/manage-incident-list-forwarder.bats +0 -68
- package/skills/manage-incident/test/manage-incident-mitigate-forwarder.bats +0 -66
- package/skills/manage-incident/test/manage-incident-restore-forwarder.bats +0 -66
- package/skills/manage-incident/test/manage-incident.bats +0 -171
- package/skills/manage-problem/test/manage-problem-adr-044-step4-derive-first.bats +0 -151
- package/skills/manage-problem/test/manage-problem-auto-migrate-step.bats +0 -53
- package/skills/manage-problem/test/manage-problem-concern-boundary.bats +0 -64
- package/skills/manage-problem/test/manage-problem-effort-buckets.bats +0 -56
- package/skills/manage-problem/test/manage-problem-external-root-cause-detection.bats +0 -119
- package/skills/manage-problem/test/manage-problem-first-run-intake-prompt.bats +0 -43
- package/skills/manage-problem/test/manage-problem-git-mv-restage-reminder.bats +0 -48
- package/skills/manage-problem/test/manage-problem-id-collision-guard.bats +0 -39
- package/skills/manage-problem/test/manage-problem-list-forwarder.bats +0 -66
- package/skills/manage-problem/test/manage-problem-next-id-origin-lookup.bats +0 -50
- package/skills/manage-problem/test/manage-problem-no-prose-options.bats +0 -74
- package/skills/manage-problem/test/manage-problem-output-formatting.bats +0 -30
- package/skills/manage-problem/test/manage-problem-parked-and-cache.bats +0 -73
- package/skills/manage-problem/test/manage-problem-readme-refresh-on-transition.bats +0 -62
- package/skills/manage-problem/test/manage-problem-readme-tie-break-order.bats +0 -318
- package/skills/manage-problem/test/manage-problem-readme-vq-sort-order.bats +0 -206
- package/skills/manage-problem/test/manage-problem-release-vehicle-seed.bats +0 -83
- package/skills/manage-problem/test/manage-problem-review-forwarder.bats +0 -71
- package/skills/manage-problem/test/manage-problem-step-9d-recovery-path.bats +0 -97
- package/skills/manage-problem/test/manage-problem-transition-forwarder.bats +0 -96
- package/skills/manage-problem/test/manage-problem-transitive-dependencies.bats +0 -365
- package/skills/manage-problem/test/manage-problem-verification-detection.bats +0 -64
- package/skills/manage-problem/test/manage-problem-verification-pending.bats +0 -114
- package/skills/manage-problem/test/manage-problem-work-forwarder.bats +0 -88
- package/skills/manage-story/test/manage-story-contract.bats +0 -174
- package/skills/manage-story-map/test/manage-story-map-contract.bats +0 -63
- package/skills/mitigate-incident/test/mitigate-incident-contract.bats +0 -258
- package/skills/reconcile-readme/test/reconcile-readme-contract.bats +0 -201
- package/skills/report-upstream/test/report-upstream-contract.bats +0 -394
- package/skills/restore-incident/test/restore-incident-contract.bats +0 -170
- package/skills/review-problems/test/inbound-channels-cache-shape.bats +0 -127
- package/skills/review-problems/test/inbound-discovery-contract.bats +0 -383
- package/skills/review-problems/test/jtbd-301-verdict-shape-contract.bats +0 -225
- package/skills/review-problems/test/review-problems-contract.bats +0 -235
- package/skills/review-problems/test/review-problems-likely-verified-cell-shape.bats +0 -229
- package/skills/scaffold-intake/test/scaffold-intake-contract.bats +0 -126
- package/skills/scaffold-intake/test/scaffold-intake-fixture.bats +0 -164
- package/skills/scaffold-intake/test/scaffold-intake-secrets-absent.bats +0 -87
- package/skills/transition-problem/test/transition-problem-contract.bats +0 -285
- package/skills/transition-problems/test/transition-problems-contract.bats +0 -331
- package/skills/update-upstream/test/update-upstream-contract.bats +0 -375
- package/skills/work-problem/test/work-problem-contract.bats +0 -294
- package/skills/work-problems/test/work-problems-above-appetite-remediation.bats +0 -139
- package/skills/work-problems/test/work-problems-adr-013-rule-6-p352-amendment.bats +0 -166
- package/skills/work-problems/test/work-problems-auto-migrate-step.bats +0 -57
- package/skills/work-problems/test/work-problems-cost-logging.bats +0 -109
- package/skills/work-problems/test/work-problems-deviation-candidate-shape.bats +0 -121
- package/skills/work-problems/test/work-problems-first-run-intake-prompt.bats +0 -36
- package/skills/work-problems/test/work-problems-inter-iteration-verify.bats +0 -59
- package/skills/work-problems/test/work-problems-mid-loop-userpromptsubmit-handler.bats +0 -69
- package/skills/work-problems/test/work-problems-no-mid-loop-asking.bats +0 -192
- package/skills/work-problems/test/work-problems-p341-pre-all-done-gate.bats +0 -122
- package/skills/work-problems/test/work-problems-p342-r007-derive-then-pass-flags.bats +0 -170
- package/skills/work-problems/test/work-problems-p342-retro-auto-ticket-carveout.bats +0 -120
- package/skills/work-problems/test/work-problems-preflight-failure-handling.bats +0 -248
- package/skills/work-problems/test/work-problems-preflight-session-continuity.bats +0 -139
- package/skills/work-problems/test/work-problems-preflight.bats +0 -69
- package/skills/work-problems/test/work-problems-release-cadence.bats +0 -63
- package/skills/work-problems/test/work-problems-step-0-iter-error-staleness.bats +0 -93
- package/skills/work-problems/test/work-problems-step-0b-cache-staleness-behavioural.bats +0 -138
- package/skills/work-problems/test/work-problems-step-0c-deferred-placeholder-staleness-behavioural.bats +0 -238
- package/skills/work-problems/test/work-problems-step-0d-outbound-responses-staleness-behavioural.bats +0 -174
- package/skills/work-problems/test/work-problems-step-2-5-routing.bats +0 -80
- package/skills/work-problems/test/work-problems-step-2-5b-cross-halt-routing.bats +0 -149
- package/skills/work-problems/test/work-problems-step-3-5-jtbd-ratification-predicate.bats +0 -191
- package/skills/work-problems/test/work-problems-step-5-bats-polling-discipline.bats +0 -91
- package/skills/work-problems/test/work-problems-step-5-delegation.bats +0 -333
- package/skills/work-problems/test/work-problems-step-5-idle-timeout-sigterm.bats +0 -421
- package/skills/work-problems/test/work-problems-step-5-is-error-transient-halt.bats +0 -278
- package/skills/work-problems/test/work-problems-step-5-iter-changeset-required.bats +0 -95
- package/skills/work-problems/test/work-problems-step-5-prompt-body-re-grounding.bats +0 -128
- package/skills/work-problems/test/work-problems-step-5-stream-timeout-salvage.bats +0 -251
- package/skills/work-problems/test/work-problems-step-6-5-always-drain.bats +0 -203
- package/skills/work-problems/test/work-problems-step-6-5-cache-refresh-chain.bats +0 -174
- package/skills/work-problems/test/work-problems-step-6-5-fix-and-continue.bats +0 -254
- package/skills/work-problems/test/work-problems-step-6-5-postrelease-kv-callback.bats +0 -209
- package/skills/work-problems/test/work-problems-stop-condition-questions.bats +0 -85
|
@@ -1,181 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# P085: itil-assistant-output-gate.sh UserPromptSubmit hook must detect
|
|
4
|
-
# when the user's incoming prompt pins a direction / confirms a prior
|
|
5
|
-
# ask / issues an act-verb, and inject a once-per-session MANDATORY
|
|
6
|
-
# reminder instructing the assistant to act without asking — or use
|
|
7
|
-
# AskUserQuestion for genuine ambiguity — and NEVER prose-ask.
|
|
8
|
-
#
|
|
9
|
-
# Per ADR-038: full block emits once per session; subsequent prompts
|
|
10
|
-
# emit a terse reminder (<250 bytes) that keeps the MANDATORY signal,
|
|
11
|
-
# the gate name, and the AskUserQuestion affordance.
|
|
12
|
-
#
|
|
13
|
-
# Per feedback_behavioural_tests.md (P081): these are behavioural
|
|
14
|
-
# assertions — they simulate the hook's payload on stdin and assert
|
|
15
|
-
# on what the hook emits, not on the source text of the hook file.
|
|
16
|
-
|
|
17
|
-
setup() {
|
|
18
|
-
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
|
|
19
|
-
HOOK="$REPO_ROOT/packages/itil/hooks/itil-assistant-output-gate.sh"
|
|
20
|
-
SID="itil-gate-test-$$-$RANDOM"
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
teardown() {
|
|
24
|
-
rm -f "/tmp/itil-assistant-gate-announced-${SID}"
|
|
25
|
-
rm -f "/tmp/itil-assistant-gate-announced-${SID}-alt"
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
run_hook() {
|
|
29
|
-
local sid="$1"
|
|
30
|
-
local prompt="$2"
|
|
31
|
-
echo "{\"session_id\":\"$sid\",\"prompt\":$(printf '%s' "$prompt" | jq -Rs .)}" | bash "$HOOK"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
@test "gate: emits full MANDATORY block on first direction-pin prompt" {
|
|
35
|
-
run run_hook "$SID" "yes, update P084 and verify the subagent tools thing"
|
|
36
|
-
[ "$status" -eq 0 ]
|
|
37
|
-
[ "${#output}" -gt 400 ]
|
|
38
|
-
[[ "$output" == *"MANDATORY"* ]]
|
|
39
|
-
[[ "$output" == *"AskUserQuestion"* ]]
|
|
40
|
-
[[ "$output" == *"obvious"* ]] || [[ "$output" == *"act"* ]]
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
@test "gate: writes the announcement marker on first emission" {
|
|
44
|
-
run run_hook "$SID" "go ahead and do it"
|
|
45
|
-
[ "$status" -eq 0 ]
|
|
46
|
-
[ -f "/tmp/itil-assistant-gate-announced-${SID}" ]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
@test "gate: second direction-pin prompt in same session emits terse reminder only" {
|
|
50
|
-
run_hook "$SID" "yes, please proceed" >/dev/null
|
|
51
|
-
# Second prompt also pins direction (required for the gate to fire).
|
|
52
|
-
run run_hook "$SID" "yes, go ahead"
|
|
53
|
-
[ "$status" -eq 0 ]
|
|
54
|
-
[ "${#output}" -lt 250 ]
|
|
55
|
-
[[ "$output" == *"AskUserQuestion"* ]]
|
|
56
|
-
# Full block is NOT re-emitted
|
|
57
|
-
[[ "$output" != *"Canonical prose-ask phrasings"* ]]
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
@test "gate: terse reminder preserves MANDATORY / REQUIRED signal word" {
|
|
61
|
-
run_hook "$SID" "yes" >/dev/null
|
|
62
|
-
run run_hook "$SID" "act on this"
|
|
63
|
-
[ "$status" -eq 0 ]
|
|
64
|
-
[[ "$output" == *"MANDATORY"* ]] || [[ "$output" == *"REQUIRED"* ]] || [[ "$output" == *"NON-OPTIONAL"* ]]
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
@test "gate: different session_id re-emits the full block" {
|
|
68
|
-
run_hook "$SID" "yes" >/dev/null
|
|
69
|
-
local SID2="${SID}-alt"
|
|
70
|
-
run run_hook "$SID2" "yes"
|
|
71
|
-
[ "$status" -eq 0 ]
|
|
72
|
-
[ "${#output}" -gt 400 ]
|
|
73
|
-
rm -f "/tmp/itil-assistant-gate-announced-${SID2}"
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
@test "gate: empty session_id emits the full block and writes no marker" {
|
|
77
|
-
run run_hook "" "yes, go"
|
|
78
|
-
[ "$status" -eq 0 ]
|
|
79
|
-
[ "${#output}" -gt 400 ]
|
|
80
|
-
[ ! -f "/tmp/itil-assistant-gate-announced-" ]
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
@test "gate: non-direction-pinning prompt does not emit a block" {
|
|
84
|
-
# A conversational prompt with no direction/act-verb/yes signal
|
|
85
|
-
# should not burn the session-marker budget.
|
|
86
|
-
run run_hook "$SID" "what does ADR-013 say about ambiguous decisions?"
|
|
87
|
-
[ "$status" -eq 0 ]
|
|
88
|
-
# May emit a short neutral note OR nothing, but must not emit the
|
|
89
|
-
# full MANDATORY block on a non-direction prompt.
|
|
90
|
-
[[ "$output" != *"MANDATORY: act"* ]] || [ "${#output}" -lt 250 ]
|
|
91
|
-
# Must not have written the announcement marker either.
|
|
92
|
-
[ ! -f "/tmp/itil-assistant-gate-announced-${SID}" ]
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
@test "gate: direction-pin via 'act now' verb triggers the block" {
|
|
96
|
-
run run_hook "$SID" "act now and close the ticket"
|
|
97
|
-
[ "$status" -eq 0 ]
|
|
98
|
-
[ "${#output}" -gt 400 ]
|
|
99
|
-
[[ "$output" == *"MANDATORY"* ]]
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
@test "gate: direction-pin via 'just do it' triggers the block" {
|
|
103
|
-
run run_hook "$SID" "just do it"
|
|
104
|
-
[ "$status" -eq 0 ]
|
|
105
|
-
[ "${#output}" -gt 400 ]
|
|
106
|
-
[[ "$output" == *"MANDATORY"* ]]
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
@test "gate: terse reminder references AskUserQuestion" {
|
|
110
|
-
run_hook "$SID" "yes" >/dev/null
|
|
111
|
-
run run_hook "$SID" "proceed"
|
|
112
|
-
[ "$status" -eq 0 ]
|
|
113
|
-
[[ "$output" == *"AskUserQuestion"* ]]
|
|
114
|
-
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# P085: itil-assistant-output-review.sh Stop hook reads the last
|
|
4
|
-
# assistant turn from the transcript at `transcript_path` on stdin and
|
|
5
|
-
# scans for canonical prose-ask phrasings ("Want me to", "Should I",
|
|
6
|
-
# "Option A or Option B", etc.). When a prose-ask is detected, the
|
|
7
|
-
# hook emits a stopReason JSON object with a nudge instructing the
|
|
8
|
-
# assistant to re-emit via AskUserQuestion (or to act, if the decision
|
|
9
|
-
# was obvious). Clean turns pass silently (no stopReason field).
|
|
10
|
-
#
|
|
11
|
-
# The Stop hook cannot rewrite the emitted turn — only nudge the next
|
|
12
|
-
# turn — so its job is post-hoc detection + durable signal, per the
|
|
13
|
-
# architect verdict for P085.
|
|
14
|
-
|
|
15
|
-
setup() {
|
|
16
|
-
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
|
|
17
|
-
HOOK="$REPO_ROOT/packages/itil/hooks/itil-assistant-output-review.sh"
|
|
18
|
-
TMPDIR_="$(mktemp -d)"
|
|
19
|
-
TRANSCRIPT="$TMPDIR_/transcript.jsonl"
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
teardown() {
|
|
23
|
-
rm -rf "$TMPDIR_"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
# Writes a JSONL transcript with a user message followed by an
|
|
27
|
-
# assistant message. Claude Code transcript format: each line is a
|
|
28
|
-
# JSON object with `type: user|assistant` and `message.content` being
|
|
29
|
-
# either a string or an array of content blocks (text/tool_use).
|
|
30
|
-
write_transcript() {
|
|
31
|
-
local user_text="$1"
|
|
32
|
-
local assistant_text="$2"
|
|
33
|
-
{
|
|
34
|
-
printf '{"type":"user","message":{"role":"user","content":%s}}\n' "$(printf '%s' "$user_text" | jq -Rs .)"
|
|
35
|
-
printf '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":%s}]}}\n' "$(printf '%s' "$assistant_text" | jq -Rs .)"
|
|
36
|
-
} > "$TRANSCRIPT"
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
run_hook() {
|
|
40
|
-
echo "{\"session_id\":\"stop-test\",\"transcript_path\":$(printf '%s' "$TRANSCRIPT" | jq -Rs .)}" | bash "$HOOK"
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
@test "review: 'Want me to' in assistant text triggers stopReason nudge" {
|
|
44
|
-
write_transcript "update the ticket" "Done. Want me to also commit now or wait?"
|
|
45
|
-
run run_hook
|
|
46
|
-
[ "$status" -eq 0 ]
|
|
47
|
-
[[ "$output" == *"stopReason"* ]]
|
|
48
|
-
[[ "$output" == *"AskUserQuestion"* ]]
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
@test "review: 'Should I' in assistant text triggers stopReason nudge" {
|
|
52
|
-
write_transcript "look at this" "I see the issue. Should I fix it now or wait for review?"
|
|
53
|
-
run run_hook
|
|
54
|
-
[ "$status" -eq 0 ]
|
|
55
|
-
[[ "$output" == *"stopReason"* ]]
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
@test "review: 'Would you like me to' triggers stopReason nudge" {
|
|
59
|
-
write_transcript "ok" "Ticket updated. Would you like me to open a PR next?"
|
|
60
|
-
run run_hook
|
|
61
|
-
[ "$status" -eq 0 ]
|
|
62
|
-
[[ "$output" == *"stopReason"* ]]
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
@test "review: 'Option A or Option B' triggers stopReason nudge" {
|
|
66
|
-
write_transcript "plan this" "Two paths: Option A: do it inline. Or Option B: split into two commits. Which do you prefer?"
|
|
67
|
-
run run_hook
|
|
68
|
-
[ "$status" -eq 0 ]
|
|
69
|
-
[[ "$output" == *"stopReason"* ]]
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
@test "review: '(a) / (b) / (c)?' triggers stopReason nudge" {
|
|
73
|
-
write_transcript "go" "Three choices: (a) ship now, (b) wait for review, or (c) add tests first?"
|
|
74
|
-
run run_hook
|
|
75
|
-
[ "$status" -eq 0 ]
|
|
76
|
-
[[ "$output" == *"stopReason"* ]]
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
@test "review: clean informative turn does not trigger stopReason" {
|
|
80
|
-
write_transcript "what changed" "The last commit added three files: detectors.sh, gate.sh, and review.sh. No new decisions were introduced."
|
|
81
|
-
run run_hook
|
|
82
|
-
[ "$status" -eq 0 ]
|
|
83
|
-
# Clean output: either empty or a JSON object WITHOUT stopReason.
|
|
84
|
-
[[ "$output" != *"stopReason"* ]]
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
@test "review: assistant turn containing AskUserQuestion tool_use does NOT flag" {
|
|
88
|
-
# If the assistant used the AskUserQuestion tool, that's the compliant
|
|
89
|
-
# path — we must not nudge them for using prose inside the tool's
|
|
90
|
-
# rendered question text (the tool surface is structured).
|
|
91
|
-
{
|
|
92
|
-
printf '{"type":"user","message":{"role":"user","content":"plan this"}}\n'
|
|
93
|
-
cat <<'JSON'
|
|
94
|
-
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Here is the choice:"},{"type":"tool_use","name":"AskUserQuestion","input":{"question":"Which option?","options":[{"label":"A"},{"label":"B"}]}}]}}
|
|
95
|
-
JSON
|
|
96
|
-
} > "$TRANSCRIPT"
|
|
97
|
-
run run_hook
|
|
98
|
-
[ "$status" -eq 0 ]
|
|
99
|
-
[[ "$output" != *"stopReason"* ]]
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
@test "review: missing transcript_path exits cleanly without error" {
|
|
103
|
-
run bash -c 'echo "{\"session_id\":\"sid\"}" | bash "$1"' -- "$HOOK"
|
|
104
|
-
[ "$status" -eq 0 ]
|
|
105
|
-
[[ "$output" != *"stopReason"* ]]
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
@test "review: non-existent transcript file exits cleanly without error" {
|
|
109
|
-
run bash -c "echo '{\"session_id\":\"sid\",\"transcript_path\":\"/tmp/does-not-exist-$RANDOM\"}' | bash '$HOOK'"
|
|
110
|
-
[ "$status" -eq 0 ]
|
|
111
|
-
[[ "$output" != *"stopReason"* ]]
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
@test "review: ask-when-obvious pattern triggers stopReason" {
|
|
115
|
-
# Prior user message pins direction ("yes, update the ticket"), next
|
|
116
|
-
# assistant turn ends with a question mark on an obvious-next-step.
|
|
117
|
-
# This is Facet A of P085 — asking when the answer is obvious.
|
|
118
|
-
write_transcript "yes, update the ticket with the findings" "Updated. Want me to commit and close the ticket now, or leave it open for review?"
|
|
119
|
-
run run_hook
|
|
120
|
-
[ "$status" -eq 0 ]
|
|
121
|
-
[[ "$output" == *"stopReason"* ]]
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
@test "review: 'Shall we' triggers stopReason nudge" {
|
|
125
|
-
write_transcript "look at options" "That's one route. Shall we go with it?"
|
|
126
|
-
run run_hook
|
|
127
|
-
[ "$status" -eq 0 ]
|
|
128
|
-
[[ "$output" == *"stopReason"* ]]
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
@test "review: 'Do you want to' triggers stopReason nudge" {
|
|
132
|
-
write_transcript "plan" "I can split into two PRs. Do you want to go that route?"
|
|
133
|
-
run run_hook
|
|
134
|
-
[ "$status" -eq 0 ]
|
|
135
|
-
[[ "$output" == *"stopReason"* ]]
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
# 2026-04-28 regression evidence (P085 reopen, Citation 1).
|
|
139
|
-
# Orchestrator main turn emitted a halt-summary ending with "Awaiting your
|
|
140
|
-
# direction on whether to add it + resume on P123, or end the session."
|
|
141
|
-
# The Stop hook should have caught this binary-choice prose-ask but the
|
|
142
|
-
# pattern list did not match. Detector extension closes the gap.
|
|
143
|
-
@test "review: 'Awaiting your direction on whether ... or ...' (Citation 1 shape) triggers stopReason" {
|
|
144
|
-
write_transcript "ok" "Loop is still halted. Remaining open item: missing changeset for b9da37e. Awaiting your direction on whether to add it + resume on P123, or end the session."
|
|
145
|
-
run run_hook
|
|
146
|
-
[ "$status" -eq 0 ]
|
|
147
|
-
[[ "$output" == *"stopReason"* ]]
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
@test "review: 'Awaiting your input' triggers stopReason nudge" {
|
|
151
|
-
write_transcript "ok" "Plan staged. Awaiting your input on the next step."
|
|
152
|
-
run run_hook
|
|
153
|
-
[ "$status" -eq 0 ]
|
|
154
|
-
[[ "$output" == *"stopReason"* ]]
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
@test "review: 'Pending your decision' triggers stopReason nudge" {
|
|
158
|
-
write_transcript "review" "Refactor scoped to three files. Pending your decision before I continue."
|
|
159
|
-
run run_hook
|
|
160
|
-
[ "$status" -eq 0 ]
|
|
161
|
-
[[ "$output" == *"stopReason"* ]]
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
@test "review: 'Once you confirm' triggers stopReason nudge" {
|
|
165
|
-
write_transcript "look" "Rename ready. Once you confirm, I will proceed with the rename."
|
|
166
|
-
run run_hook
|
|
167
|
-
[ "$status" -eq 0 ]
|
|
168
|
-
[[ "$output" == *"stopReason"* ]]
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
@test "review: 'Awaiting your response' triggers stopReason nudge" {
|
|
172
|
-
write_transcript "go" "Two paths identified. Awaiting your response so I know which to take."
|
|
173
|
-
run run_hook
|
|
174
|
-
[ "$status" -eq 0 ]
|
|
175
|
-
[[ "$output" == *"stopReason"* ]]
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
# P403: mechanical-step-framed-as-user-optional. When a skill contract
|
|
179
|
-
# mandates a mechanical step, the agent must not re-surface it as a user
|
|
180
|
-
# decision (or a budget-caution skip) in end-of-turn prose. The detector
|
|
181
|
-
# fires only when a step-skip signal, a step/mechanical-pass reference,
|
|
182
|
-
# AND an offloaded-justification closer all co-occur (the AND-discriminator).
|
|
183
|
-
|
|
184
|
-
@test "review: P403 evidence 1 — 'Step 7 auto-release skipped — your call' triggers stopReason" {
|
|
185
|
-
write_transcript "review the backlog" "Done. Step 7 auto-release skipped — your call whether to drain via /wr-risk-scorer:assess-release."
|
|
186
|
-
run run_hook
|
|
187
|
-
[ "$status" -eq 0 ]
|
|
188
|
-
[[ "$output" == *"stopReason"* ]]
|
|
189
|
-
[[ "$output" == *"mechanical"* ]]
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
@test "review: P403 evidence 2 — 'Skipped the full re-rank (Step 2) ... to stay within context budget' triggers stopReason" {
|
|
193
|
-
write_transcript "review problems" "Skipped the full 40-ticket re-rank (Step 2), inbound-discovery pipeline (Step 4.5), and relevance-close pass (Step 4.6) to stay within context budget."
|
|
194
|
-
run run_hook
|
|
195
|
-
[ "$status" -eq 0 ]
|
|
196
|
-
[[ "$output" == *"stopReason"* ]]
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
@test "review: AND-discriminator — optional closer WITHOUT a step-skip does not trigger stopReason" {
|
|
200
|
-
# "up to you" is a legitimately user-owned decision with no skipped
|
|
201
|
-
# mechanical step in context — must NOT fire (the inverse-P078 guard).
|
|
202
|
-
write_transcript "which theme" "Both palettes meet contrast AA. It is up to you which one reads better for your brand."
|
|
203
|
-
run run_hook
|
|
204
|
-
[ "$status" -eq 0 ]
|
|
205
|
-
[[ "$output" != *"stopReason"* ]]
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
@test "review: AND-discriminator — skipped step with a real reason (no offload) does not trigger stopReason" {
|
|
209
|
-
# A skipped step reported with a substantive reason (not offloaded to the
|
|
210
|
-
# user, not budget-caution) is a legitimate report, not the P403 defect.
|
|
211
|
-
write_transcript "run it" "Step 2 skipped because its precondition is already satisfied by Step 1. Proceeding to Step 3."
|
|
212
|
-
run run_hook
|
|
213
|
-
[ "$status" -eq 0 ]
|
|
214
|
-
[[ "$output" != *"stopReason"* ]]
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
@test "review: P403 nudge stays within the ADR-045 hook injection budget" {
|
|
218
|
-
write_transcript "review the backlog" "Done. Step 7 auto-release skipped — your call whether to drain."
|
|
219
|
-
run run_hook
|
|
220
|
-
[ "$status" -eq 0 ]
|
|
221
|
-
[[ "$output" == *"stopReason"* ]]
|
|
222
|
-
# ADR-045 advisory band: keep the injected nudge well under 1000 bytes.
|
|
223
|
-
[ "${#output}" -lt 1000 ]
|
|
224
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# P232: itil-bash-polling-antipattern-detect.sh PreToolUse:Bash hook
|
|
4
|
-
# must deny bash polling loops that self-reference via `pgrep -f`
|
|
5
|
-
# (parent class) or `pkill -0` (sibling), advising `wait $bg_pid` or
|
|
6
|
-
# Bash-tool `BashOutput` polling instead.
|
|
7
|
-
#
|
|
8
|
-
# Detection shape: a loop construct (`until` / `while`) combined with a
|
|
9
|
-
# polling mechanism (`pgrep -f` / `pkill -0`). One-shot `pgrep -f` (no
|
|
10
|
-
# surrounding loop) is allowed — the polling shape is the antipattern,
|
|
11
|
-
# not pgrep itself.
|
|
12
|
-
#
|
|
13
|
-
# Per ADR-005 / ADR-052 — bats live under packages/<plugin>/hooks/test/
|
|
14
|
-
# and assert behaviour on emitted JSON, not source-content. Per
|
|
15
|
-
# feedback_behavioural_tests.md (P081) — no source-grep on hook text.
|
|
16
|
-
|
|
17
|
-
setup() {
|
|
18
|
-
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
19
|
-
HOOK="$SCRIPT_DIR/itil-bash-polling-antipattern-detect.sh"
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
# Helper: simulate the PreToolUse:Bash payload on stdin.
|
|
23
|
-
# Uses python to build the JSON so we don't escape-hell with bash.
|
|
24
|
-
run_bash_hook() {
|
|
25
|
-
local cmd="$1"
|
|
26
|
-
python3 -c "
|
|
27
|
-
import json, sys
|
|
28
|
-
print(json.dumps({'tool_name': 'Bash', 'tool_input': {'command': sys.argv[1]}}))
|
|
29
|
-
" "$cmd" | bash "$HOOK"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
# --- Antipattern detection: positive cases (deny) ---
|
|
33
|
-
|
|
34
|
-
@test "deny: until ! pgrep -f loop" {
|
|
35
|
-
run run_bash_hook "until ! pgrep -f 'bats --recursive' > /dev/null 2>&1; do sleep 5; done"
|
|
36
|
-
[ "$status" -eq 0 ]
|
|
37
|
-
[[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
|
|
38
|
-
[[ "$output" == *"P232"* ]]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
@test "deny: while pgrep -f loop (positive form, no negation)" {
|
|
42
|
-
run run_bash_hook "while pgrep -f 'long-running-job'; do sleep 2; done"
|
|
43
|
-
[ "$status" -eq 0 ]
|
|
44
|
-
[[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
|
|
45
|
-
[[ "$output" == *"P232"* ]]
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
@test "deny: until ! pkill -0 signal-0 poll" {
|
|
49
|
-
run run_bash_hook "until ! pkill -0 -f 'worker'; do sleep 3; done"
|
|
50
|
-
[ "$status" -eq 0 ]
|
|
51
|
-
[[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
@test "deny: while pkill -0 signal-0 poll" {
|
|
55
|
-
run run_bash_hook "while pkill -0 12345; do sleep 1; done"
|
|
56
|
-
[ "$status" -eq 0 ]
|
|
57
|
-
[[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
@test "deny: pgrep poll embedded in heredoc body" {
|
|
61
|
-
# Heredoc body lands in the same tool_input.command string.
|
|
62
|
-
run run_bash_hook "bash <<'EOF'
|
|
63
|
-
until ! pgrep -f 'bats'; do sleep 5; done
|
|
64
|
-
EOF"
|
|
65
|
-
[ "$status" -eq 0 ]
|
|
66
|
-
[[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
@test "deny: P232 deadlock witness — multi-line shape with trailing tail" {
|
|
70
|
-
run run_bash_hook "until ! pgrep -f 'bats --recursive' > /dev/null 2>&1; do sleep 5; done; echo done; tail -30 /tmp/bats-out.log"
|
|
71
|
-
[ "$status" -eq 0 ]
|
|
72
|
-
[[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
# --- Allow paths: legitimate non-polling uses ---
|
|
76
|
-
|
|
77
|
-
@test "allow: one-shot pgrep -f without surrounding loop" {
|
|
78
|
-
run run_bash_hook "pgrep -f 'nginx' && echo running"
|
|
79
|
-
[ "$status" -eq 0 ]
|
|
80
|
-
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
@test "allow: one-shot pkill (no -0, real signal — not a poll)" {
|
|
84
|
-
run run_bash_hook "pkill -TERM -f 'stale-worker'"
|
|
85
|
-
[ "$status" -eq 0 ]
|
|
86
|
-
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
@test "allow: wait \$bg_pid (the canonical recovery shape)" {
|
|
90
|
-
run run_bash_hook "bats --recursive packages/itil/hooks/test/ & wait \$!"
|
|
91
|
-
[ "$status" -eq 0 ]
|
|
92
|
-
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
@test "allow: while loop without pgrep/pkill (unrelated)" {
|
|
96
|
-
run run_bash_hook "while read line; do echo \$line; done < input.txt"
|
|
97
|
-
[ "$status" -eq 0 ]
|
|
98
|
-
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
@test "allow: until loop without pgrep/pkill (unrelated)" {
|
|
102
|
-
run run_bash_hook "until [ -f /tmp/sentinel ]; do sleep 1; done"
|
|
103
|
-
[ "$status" -eq 0 ]
|
|
104
|
-
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
@test "allow: commit message text mentioning pgrep does not deny" {
|
|
108
|
-
# The literal pair is in the commit message body, not a poll shape.
|
|
109
|
-
# The hook should not over-match on commit prose.
|
|
110
|
-
run run_bash_hook "git commit -m 'document pgrep antipattern in P232'"
|
|
111
|
-
[ "$status" -eq 0 ]
|
|
112
|
-
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
# --- Tool-name filters ---
|
|
116
|
-
|
|
117
|
-
@test "allow: non-Bash tool exits 0 without deny" {
|
|
118
|
-
run bash -c "echo '{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"foo.md\"}}' | bash $HOOK"
|
|
119
|
-
[ "$status" -eq 0 ]
|
|
120
|
-
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
# --- Parse / fail-open ---
|
|
124
|
-
|
|
125
|
-
@test "allow: empty JSON fails open" {
|
|
126
|
-
run bash -c "echo '{}' | bash $HOOK"
|
|
127
|
-
[ "$status" -eq 0 ]
|
|
128
|
-
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
@test "allow: empty command field fails open" {
|
|
132
|
-
run bash -c "echo '{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"\"}}' | bash $HOOK"
|
|
133
|
-
[ "$status" -eq 0 ]
|
|
134
|
-
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
# --- Deny message contract (ADR-038 progressive disclosure / ADR-045 budget) ---
|
|
138
|
-
|
|
139
|
-
@test "deny message cites P232 + names BOTH recovery alternatives" {
|
|
140
|
-
run run_bash_hook "until ! pgrep -f 'bats'; do sleep 5; done"
|
|
141
|
-
[ "$status" -eq 0 ]
|
|
142
|
-
[[ "$output" == *"P232"* ]]
|
|
143
|
-
[[ "$output" == *"wait"* ]]
|
|
144
|
-
[[ "$output" == *"BashOutput"* ]]
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
@test "deny message stays under ADR-045 deny-path budget (<700 bytes)" {
|
|
148
|
-
# Voice-tone target ~245 bytes (sibling p057-staging-trap-detect.sh
|
|
149
|
-
# precedent). ADR-045 deny-path band hard cap at 700 bytes keeps the
|
|
150
|
-
# message terse — fail loudly if it bloats.
|
|
151
|
-
run run_bash_hook "until ! pgrep -f 'bats'; do sleep 5; done"
|
|
152
|
-
[ "$status" -eq 0 ]
|
|
153
|
-
[ "${#output}" -lt 700 ]
|
|
154
|
-
}
|