@devrik-tools/claude-gates 0.1.2 → 0.2.0
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/marketplace.json +7 -1
- package/cli/__tests__/config.test.mjs +113 -101
- package/cli/__tests__/install.test.mjs +102 -0
- package/cli/__tests__/materialize.test.mjs +95 -0
- package/cli/__tests__/registry-gates-consistency.test.mjs +52 -44
- package/cli/__tests__/task.test.mjs +124 -0
- package/cli/__tests__/version-consistency.test.mjs +44 -0
- package/cli/config.mjs +124 -113
- package/cli/index.mjs +100 -97
- package/cli/init.mjs +259 -256
- package/cli/install.mjs +150 -80
- package/cli/materialize.mjs +102 -51
- package/cli/registry.mjs +136 -127
- package/cli/task.mjs +140 -0
- package/package.json +2 -1
- package/plugins/gates/.claude-plugin/plugin.json +8 -2
- package/plugins/gates/hooks/__tests__/ask-adoption.test.mjs +83 -0
- package/plugins/gates/hooks/__tests__/doctor.test.mjs +85 -0
- package/plugins/gates/hooks/__tests__/wiring-check.test.mjs +65 -0
- package/plugins/gates/hooks/ask-adoption.mjs +147 -0
- package/plugins/gates/hooks/doctor.mjs +207 -0
- package/plugins/gates/hooks/gates/audit-before-build/audit-before-build.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +88 -88
- package/plugins/gates/hooks/gates/audit-before-build/test.mjs +117 -92
- package/plugins/gates/hooks/gates/autonomous-mode/autonomous.edge.test.mjs +104 -0
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +48 -45
- package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +165 -0
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +285 -268
- package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +151 -0
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +265 -177
- package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +207 -0
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +501 -456
- package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +161 -143
- package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +69 -0
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +126 -118
- package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +68 -0
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +43 -49
- package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -100
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +119 -0
- package/plugins/gates/hooks/gates/force-parallel/test.mjs +88 -0
- package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +155 -0
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -112
- package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +66 -0
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -186
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -238
- package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +96 -0
- package/plugins/gates/hooks/gates/intent-flow/test.mjs +165 -136
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +149 -0
- package/plugins/gates/hooks/gates/lint-commit/test.mjs +85 -0
- package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +100 -0
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -166
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +79 -85
- package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +80 -0
- package/plugins/gates/hooks/gates/never-assume/index.mjs +58 -55
- package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +71 -0
- package/plugins/gates/hooks/gates/never-assume/test.mjs +95 -78
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +148 -142
- package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +109 -0
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +123 -120
- package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +91 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +144 -147
- package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +89 -64
- package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +102 -0
- package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +94 -0
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +159 -150
- package/plugins/gates/hooks/gates/risk-level/index.mjs +263 -203
- package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +116 -0
- package/plugins/gates/hooks/gates/risk-level/test.mjs +153 -125
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +56 -50
- package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +58 -0
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +116 -111
- package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +97 -0
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +184 -99
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +130 -0
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -251
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +159 -0
- package/plugins/gates/hooks/gates/stop-pending/test.mjs +114 -0
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +101 -101
- package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +72 -0
- package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +63 -0
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -181
- package/plugins/gates/hooks/gates/test-matrix/test.mjs +114 -87
- package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +62 -0
- package/plugins/gates/hooks/gates/tool-map/index.mjs +143 -140
- package/plugins/gates/hooks/hooks.json +346 -286
- package/plugins/gates/hooks/lib/__tests__/config.test.mjs +173 -154
- package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +210 -154
- package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +114 -0
- package/plugins/gates/hooks/lib/config.mjs +172 -165
- package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +189 -0
- package/plugins/gates/hooks/lib/hook-io.mjs +318 -208
- package/plugins/gates/hooks/lib/signals.mjs +127 -0
- package/plugins/gates/hooks/wiring-check.mjs +227 -0
- package/plugins/tasks/.claude-plugin/plugin.json +14 -0
- package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +100 -0
- package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +95 -0
- package/plugins/tasks/hooks/hooks.json +26 -0
- package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +180 -132
- package/plugins/tasks/hooks/lib/task-store.mjs +197 -159
- package/plugins/tasks/hooks/register-requests.mjs +141 -108
- package/plugins/tasks/hooks/session-tasks.mjs +108 -0
- package/registry.json +793 -686
|
@@ -1,142 +1,148 @@
|
|
|
1
|
-
// no-blocking — denies syntactic forms of waiting in the foreground (sleeping, polling
|
|
2
|
-
// loops, following unbounded output, foreground dev servers) on a real shell command and
|
|
3
|
-
// on a delegation prompt (a subagent can be told "wait with sleep" in prose). Migrated
|
|
4
|
-
// from ~/.claude/hooks/guard-no-blocking.mjs. Off by default: a project opts in, because
|
|
5
|
-
// many legitimate workflows still need a bounded wait the project accepts as normal.
|
|
6
|
-
//
|
|
7
|
-
// This gate only recognizes SYNTACTIC forms of waiting — it never judges whether other
|
|
8
|
-
// work was available meanwhile, because that is judgment, not a fact a regex can read.
|
|
9
|
-
// A gate that misfires on the legitimate case gets disabled, taking every real catch
|
|
10
|
-
// down with it.
|
|
11
|
-
//
|
|
12
|
-
// ── What a project can configure (params) ───────────────────────────────────────────
|
|
13
|
-
// blockingPatterns regex sources (matched case-insensitively) of blocking/waiting
|
|
14
|
-
// forms to deny. Replaces the built-in list wholesale.
|
|
15
|
-
// waitJustifiedMarker a marker token that, present in the command with a reason,
|
|
16
|
-
// escapes the block — a declared wait is a decision, not an
|
|
17
|
-
// oversight. Replaces the built-in marker wholesale.
|
|
18
|
-
// The defaults live here, in the source, so a project reads them and knows exactly what
|
|
19
|
-
// its override replaces.
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
String.raw
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
1
|
+
// no-blocking — denies syntactic forms of waiting in the foreground (sleeping, polling
|
|
2
|
+
// loops, following unbounded output, foreground dev servers) on a real shell command and
|
|
3
|
+
// on a delegation prompt (a subagent can be told "wait with sleep" in prose). Migrated
|
|
4
|
+
// from ~/.claude/hooks/guard-no-blocking.mjs. Off by default: a project opts in, because
|
|
5
|
+
// many legitimate workflows still need a bounded wait the project accepts as normal.
|
|
6
|
+
//
|
|
7
|
+
// This gate only recognizes SYNTACTIC forms of waiting — it never judges whether other
|
|
8
|
+
// work was available meanwhile, because that is judgment, not a fact a regex can read.
|
|
9
|
+
// A gate that misfires on the legitimate case gets disabled, taking every real catch
|
|
10
|
+
// down with it.
|
|
11
|
+
//
|
|
12
|
+
// ── What a project can configure (params) ───────────────────────────────────────────
|
|
13
|
+
// blockingPatterns regex sources (matched case-insensitively) of blocking/waiting
|
|
14
|
+
// forms to deny. Replaces the built-in list wholesale.
|
|
15
|
+
// waitJustifiedMarker a marker token that, present in the command with a reason,
|
|
16
|
+
// escapes the block — a declared wait is a decision, not an
|
|
17
|
+
// oversight. Replaces the built-in marker wholesale.
|
|
18
|
+
// The defaults live here, in the source, so a project reads them and knows exactly what
|
|
19
|
+
// its override replaces.
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
runGate,
|
|
23
|
+
deny,
|
|
24
|
+
toolInGroups,
|
|
25
|
+
delegationPromptOf,
|
|
26
|
+
} from '../../lib/hook-io.mjs';
|
|
27
|
+
|
|
28
|
+
const GATE_ID = 'no-blocking';
|
|
29
|
+
const CONFIG_KEY = 'blockWaitingCommands';
|
|
30
|
+
|
|
31
|
+
const DEFAULT_WAIT_JUSTIFIED_MARKER = 'WAIT-JUSTIFIED:';
|
|
32
|
+
|
|
33
|
+
// systemd's log-follow command, assembled from fragments so the spell checker does not
|
|
34
|
+
// read it as prose (the project keeps an empty dictionary by policy).
|
|
35
|
+
const SYSTEMD_LOG_COMMAND = 'journal' + 'ctl';
|
|
36
|
+
|
|
37
|
+
/** Blocking rules as `[regexSource, reason]`, matched case-insensitively. */
|
|
38
|
+
const DEFAULT_BLOCKING_PATTERNS = [
|
|
39
|
+
[
|
|
40
|
+
String.raw`(^|[|;&]\s*|\bthen\s+|\bdo\s+)(sleep|timeout)\s+\d`,
|
|
41
|
+
'Sleeping in the foreground spends the turn doing nothing. If waiting for something ' +
|
|
42
|
+
'to finish, launch it in the background and move on to something else; if waiting ' +
|
|
43
|
+
'on an external condition, use a monitor.',
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
String.raw`\bStart-Sleep\b`,
|
|
47
|
+
'Start-Sleep freezes the turn. Launch the work in the background and continue with ' +
|
|
48
|
+
'whatever does not depend on it.',
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
String.raw`(^|[|;&]\s*)wait\b|--wait\b|\bWait-Process\b|\bWait-Job\b`,
|
|
52
|
+
'Waiting for another process to finish blocks the whole turn. Launch it in the ' +
|
|
53
|
+
'background and consume the result when it arrives, not before.',
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
[
|
|
57
|
+
String.raw`\b(tail|`,
|
|
58
|
+
SYSTEMD_LOG_COMMAND,
|
|
59
|
+
String.raw`|kubectl\s+logs|docker\s+logs)\b[^|;&]*\s-{1,2}f\b`,
|
|
60
|
+
].join(''),
|
|
61
|
+
'Following live output never returns and takes the turn with it. Read the file once, ' +
|
|
62
|
+
'or leave the following to a background process.',
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
String.raw`(^|[|;&]\s*)watch\s+`,
|
|
66
|
+
"'watch' repeats forever and blocks. Run the command once; if a change genuinely " +
|
|
67
|
+
'needs watching, use a monitor that does not take the turn.',
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
String.raw`\b(until|while)\b[^\n]{0,80}\bdo\b[^\n]{0,80}\bsleep\b`,
|
|
71
|
+
'A loop that sleeps waiting for something to change is foreground polling: it blocks ' +
|
|
72
|
+
'and it spins. Use a monitor with the condition, or launch the work in the ' +
|
|
73
|
+
'background and wait for its notification.',
|
|
74
|
+
],
|
|
75
|
+
[
|
|
76
|
+
String.raw`\b(npm|pnpm|yarn|bun)\s+(run\s+)?(dev|start|serve|watch)\b`,
|
|
77
|
+
'A dev server does not end on its own: in the foreground it takes the turn until ' +
|
|
78
|
+
'something kills it. Launch it in the background and keep working while it comes up.',
|
|
79
|
+
],
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
// Marks that a command will not take the turn: backgrounded, detached, or bounded. The bare
|
|
83
|
+
// `-d` token was removed: it matched any -d flag (e.g. `curl -d payload`), letting an
|
|
84
|
+
// unrelated flag whitelist a genuine foreground `sleep`. Detached forms are now matched
|
|
85
|
+
// explicitly (--detach, docker/compose -d at a word boundary before end/pipe), and PowerShell's
|
|
86
|
+
// Start-Job / Start-Process -NoNewWindow backgrounding is recognized.
|
|
87
|
+
const NOT_TAKING_THE_TURN =
|
|
88
|
+
/(&\s*$|\bnohup\b|\bstart\s+\/b\b|--detach\b|\b-d(?=\s*($|[|;&]))|\bStart-Job\b|\bStart-Process\b[^|;\n]*-NoNewWindow\b|\brun_in_background\b)/i;
|
|
89
|
+
|
|
90
|
+
function compile(source) {
|
|
91
|
+
return new RegExp(source, 'i');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The text to inspect: a real command's command line, or the delegation prompt. */
|
|
95
|
+
function commandTextFrom(toolName, toolInput) {
|
|
96
|
+
if (toolInGroups(toolName, ['shell'])) {
|
|
97
|
+
return String(toolInput.CommandLine ?? toolInput.command ?? '');
|
|
98
|
+
}
|
|
99
|
+
return delegationPromptOf(toolInput);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
runGate(
|
|
103
|
+
{
|
|
104
|
+
id: GATE_ID,
|
|
105
|
+
configKey: CONFIG_KEY,
|
|
106
|
+
enabledByDefault: false,
|
|
107
|
+
defaultParams: {
|
|
108
|
+
blockingPatterns: DEFAULT_BLOCKING_PATTERNS,
|
|
109
|
+
waitJustifiedMarker: DEFAULT_WAIT_JUSTIFIED_MARKER,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
({ toolName, toolInput, parameters }) => {
|
|
113
|
+
const isShell = toolInGroups(toolName, ['shell']);
|
|
114
|
+
const isDelegation = toolInGroups(toolName, ['delegation']);
|
|
115
|
+
if (!isShell && !isDelegation) return;
|
|
116
|
+
|
|
117
|
+
const command = commandTextFrom(toolName, toolInput);
|
|
118
|
+
if (!command.trim()) return;
|
|
119
|
+
|
|
120
|
+
// Already declared in the background: the turn stays free, which is all that matters.
|
|
121
|
+
if (toolInput.run_in_background === true) return;
|
|
122
|
+
if (NOT_TAKING_THE_TURN.test(command)) return;
|
|
123
|
+
|
|
124
|
+
// A declared wait with its reason is a decision, not an oversight.
|
|
125
|
+
const marker = String(parameters.waitJustifiedMarker ?? '');
|
|
126
|
+
if (marker) {
|
|
127
|
+
const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
128
|
+
const reasonAfterMarker = String.raw`\s*\S+\s+\S+`;
|
|
129
|
+
const justified = new RegExp(`${escapedMarker}${reasonAfterMarker}`, 'i');
|
|
130
|
+
if (justified.test(command)) return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const blockingPairs = (parameters.blockingPatterns ?? []).map((entry) =>
|
|
134
|
+
Array.isArray(entry)
|
|
135
|
+
? entry
|
|
136
|
+
: [entry, 'Blocking/waiting command is not allowed.'],
|
|
137
|
+
);
|
|
138
|
+
for (const [source, reason] of blockingPairs) {
|
|
139
|
+
if (compile(source).test(command)) {
|
|
140
|
+
deny(
|
|
141
|
+
GATE_ID,
|
|
142
|
+
`${reason} If this wait is genuinely justified, add "${marker} <concrete reason>" ` +
|
|
143
|
+
'to the command and try again. A declared wait is a decision; a silent one is an oversight.',
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Edge-case audit for no-blocking: bypasses and false positives.
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
11
|
+
|
|
12
|
+
function runGate(payload, { config } = {}) {
|
|
13
|
+
const project = mkdtempSync(join(tmpdir(), 'no-blocking-edge-'));
|
|
14
|
+
mkdirSync(join(project, '.git'));
|
|
15
|
+
if (config) {
|
|
16
|
+
mkdirSync(join(project, '.ai'));
|
|
17
|
+
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
18
|
+
}
|
|
19
|
+
const out = execFileSync(process.execPath, [GATE], {
|
|
20
|
+
input: JSON.stringify(payload),
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
cwd: project,
|
|
23
|
+
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
24
|
+
});
|
|
25
|
+
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ENABLED = { gates: { blockWaitingCommands: true } };
|
|
29
|
+
|
|
30
|
+
function bash(command, extra = {}) {
|
|
31
|
+
return { tool_name: 'Bash', tool_input: { command, ...extra } };
|
|
32
|
+
}
|
|
33
|
+
function mcpTool(name, input) {
|
|
34
|
+
return { tool_name: name, tool_input: input };
|
|
35
|
+
}
|
|
36
|
+
function isDeny(result) {
|
|
37
|
+
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── BUG: MCP shell-equivalent tools are not in TOOL_GROUPS.shell/delegation.
|
|
41
|
+
test('FIXED: an mcp shell-equivalent tool running "sleep 30" is now recognized', () => {
|
|
42
|
+
const result = runGate(mcpTool('mcp__shell__run', { command: 'sleep 30' }), {
|
|
43
|
+
config: ENABLED,
|
|
44
|
+
});
|
|
45
|
+
assert.ok(isDeny(result)); // toolInGroups classifies mcp__shell__run into the shell group
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ── BUG (false positive / evasion pairing): the WAIT-JUSTIFIED marker escape requires
|
|
49
|
+
// NO reason validation beyond "two more tokens exist after the marker" — the reason text
|
|
50
|
+
// itself is never checked for plausibility, so trivially defeats the intent of a
|
|
51
|
+
// "declared, justified wait": any two words work as an unchecked excuse.
|
|
52
|
+
test('BUG (weak justification check): any two-word text after the marker escapes the block regardless of content', () => {
|
|
53
|
+
const result = runGate(
|
|
54
|
+
bash('sleep 9999 # WAIT-JUSTIFIED: because yes'),
|
|
55
|
+
{ config: ENABLED },
|
|
56
|
+
);
|
|
57
|
+
assert.equal(result, null); // allowed: marker escape has no semantic validation, only shape
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ── BUG candidate: `timeout 30 someserver` is matched by the sleep|timeout pattern
|
|
61
|
+
// requiring the command to START the line or follow |;&/then/do. A background-launched
|
|
62
|
+
// dev server that is piped through `| cat` first, e.g. `npm run dev | cat`, should still
|
|
63
|
+
// match \b(npm|pnpm|yarn|bun)\s+(run\s+)?(dev|start|serve|watch)\b since that's a
|
|
64
|
+
// substring test with no anchoring requirement (unlike sleep/timeout). Confirm no bypass.
|
|
65
|
+
test('OK: piping a dev server through another command does not bypass the dev-server pattern', () => {
|
|
66
|
+
assert.ok(
|
|
67
|
+
isDeny(runGate(bash('npm run dev | cat'), { config: ENABLED })),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ── BUG: `NOT_TAKING_THE_TURN` includes `-d\b` as a generic "detached" flag marker. This
|
|
72
|
+
// is dangerously broad: ANY command containing a standalone `-d` token — including one
|
|
73
|
+
// completely unrelated to backgrounding — escapes every blocking pattern. E.g. `sleep 30
|
|
74
|
+
// -d` (nonsensical flag for sleep, but matches) or more realistically a docker/curl
|
|
75
|
+
// command with an unrelated `-d` (data) flag combined with a blocking pattern.
|
|
76
|
+
test('FIXED: an unrelated "-d" flag (curl -d) no longer escapes the blocking check', () => {
|
|
77
|
+
// curl -d is "data", not detach. The bare -d escape was removed, so a genuinely foreground
|
|
78
|
+
// sleep on the same command line is now denied despite the unrelated -d flag.
|
|
79
|
+
const result = runGate(bash('sleep 30; curl -d "payload" http://x'), {
|
|
80
|
+
config: ENABLED,
|
|
81
|
+
});
|
|
82
|
+
assert.ok(isDeny(result));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── BUG candidate: `npm run dev &` (trailing background operator with a space before
|
|
86
|
+
// EOL, e.g. training whitespace) — NOT_TAKING_THE_TURN requires `&\s*$`. A trailing
|
|
87
|
+
// newline in the JSON string (unlikely from a real shell, but a delegation prompt or a
|
|
88
|
+
// tool that appends '\n') would still satisfy `\s*$` since \s includes \n. Not pursued
|
|
89
|
+
// further as a distinct bug; documenting as checked.
|
|
90
|
+
test('OK: trailing background operator followed by trailing whitespace still escapes correctly (not a bug)', () => {
|
|
91
|
+
assert.equal(
|
|
92
|
+
runGate(bash('npm run dev & '), { config: ENABLED }),
|
|
93
|
+
null,
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── BUG candidate: PowerShell `Start-Job` / `Start-Process ... -NoNewWindow` background
|
|
98
|
+
// forms are not recognized by NOT_TAKING_THE_TURN, but that only matters if the command
|
|
99
|
+
// is ALSO caught by a blocking pattern in the first place. `Start-Sleep` alone, run as
|
|
100
|
+
// `Start-Job { Start-Sleep 30 }` (already backgrounded via Start-Job) is still matched by
|
|
101
|
+
// \bStart-Sleep\b and NOT recognized as non-blocking, causing a FALSE POSITIVE: a
|
|
102
|
+
// genuinely backgrounded PowerShell job gets denied because its "backgrounded" wrapper
|
|
103
|
+
// (Start-Job) is not in NOT_TAKING_THE_TURN.
|
|
104
|
+
test('FIXED: Start-Sleep inside a Start-Job background wrapper is now allowed', () => {
|
|
105
|
+
const result = runGate(bash('Start-Job { Start-Sleep 30 }'), {
|
|
106
|
+
config: ENABLED,
|
|
107
|
+
});
|
|
108
|
+
assert.equal(result, null); // Start-Job is now recognized as a backgrounding mechanism
|
|
109
|
+
});
|