@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,120 +1,123 @@
|
|
|
1
|
-
// no-memory-dependency — warns (never denies) when a delegation prompt leans on the
|
|
2
|
-
// subagent "remembering" something said earlier in conversation instead of carrying
|
|
3
|
-
// the data itself. A fresh subagent has no access to the delegator's conversation
|
|
4
|
-
// history: if the data matters, it must travel IN the prompt, in a file the subagent
|
|
5
|
-
// reads, or in an already-persisted decision.
|
|
6
|
-
//
|
|
7
|
-
// ── Two stages, same pattern as intent-flow ─────────────────────────────────────────
|
|
8
|
-
// Stage 1 (cheap): does any memory-dependency phrase appear anywhere? If not, allow.
|
|
9
|
-
// Stage 2 (confirmation, only on candidates): strip quoted text, then discard a phrase
|
|
10
|
-
// that has a deterministic persistence
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
return
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
1
|
+
// no-memory-dependency — warns (never denies) when a delegation prompt leans on the
|
|
2
|
+
// subagent "remembering" something said earlier in conversation instead of carrying
|
|
3
|
+
// the data itself. A fresh subagent has no access to the delegator's conversation
|
|
4
|
+
// history: if the data matters, it must travel IN the prompt, in a file the subagent
|
|
5
|
+
// reads, or in an already-persisted decision.
|
|
6
|
+
//
|
|
7
|
+
// ── Two stages, same pattern as intent-flow ─────────────────────────────────────────
|
|
8
|
+
// Stage 1 (cheap): does any memory-dependency phrase appear anywhere? If not, allow.
|
|
9
|
+
// Stage 2 (confirmation, only on candidates): strip quoted text, then discard a phrase
|
|
10
|
+
// that has a REAL deterministic persistence INSTRUCTION nearby (not just a noun/verb
|
|
11
|
+
// mentioned in passing) — "no te olvides de guardar la decision en .ai/decision.md"
|
|
12
|
+
// genuinely depends on a file, not on model memory. A persistence noun/verb that merely
|
|
13
|
+
// co-occurs in the window without forming an instruction TO persist THIS remembered
|
|
14
|
+
// thing (e.g. an unrelated file named elsewhere in the same sentence) does not suppress
|
|
15
|
+
// the warning. This stays a soft signal (warn), never a hard block: whether the brief
|
|
16
|
+
// truly needs the data or is just referencing prior agreement needs judgment this gate
|
|
17
|
+
// can't supply on its own.
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
runGate,
|
|
21
|
+
warn,
|
|
22
|
+
toolInGroups,
|
|
23
|
+
delegationPromptOf,
|
|
24
|
+
} from '../../lib/hook-io.mjs';
|
|
25
|
+
import { PERSISTENCE_VERB } from '../../lib/signals.mjs';
|
|
26
|
+
|
|
27
|
+
const GATE_ID = 'no-memory-dependency';
|
|
28
|
+
const CONFIG_KEY = 'warnMemoryDependencyInBrief';
|
|
29
|
+
|
|
30
|
+
const DEFAULT_MEMORY_DEPENDENCY_PATTERNS = [
|
|
31
|
+
'acordate de|acu[eé]rdate de',
|
|
32
|
+
'no olvides|no te olvides',
|
|
33
|
+
'record[aá] que|ten[eé] en cuenta que',
|
|
34
|
+
'como te dije|como ya te dije|ya te dije|te lo dije antes|ya te ped[ií]',
|
|
35
|
+
"remember to|don'?t forget|keep in mind",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// A real persistence INSTRUCTION requires an imperative persistence VERB (an actual
|
|
39
|
+
// directive to save/persist/write THIS remembered thing), not merely a persistence-
|
|
40
|
+
// related NOUN mentioned somewhere nearby (a stray "incident.md" in the same sentence
|
|
41
|
+
// names a file without instructing anything be saved to it). Centralized in
|
|
42
|
+
// lib/signals.mjs (ES+EN) so this class of signal is not duplicated per gate.
|
|
43
|
+
const PERSISTENCE_VERB_PATTERN = PERSISTENCE_VERB;
|
|
44
|
+
|
|
45
|
+
const PERSISTENCE_WINDOW = 80;
|
|
46
|
+
|
|
47
|
+
function withUnicodeWordBoundary(alternatives) {
|
|
48
|
+
return new RegExp(
|
|
49
|
+
`(?<![\\p{L}\\p{N}_])(${alternatives})(?![\\p{L}\\p{N}_])`,
|
|
50
|
+
'iu',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stripQuoted(text) {
|
|
55
|
+
return text
|
|
56
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
57
|
+
.replace(/"[^"\n]{0,300}"/g, ' ')
|
|
58
|
+
.replace(/'[^'\n]{0,300}'/g, ' ');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function allMatches(pattern, text) {
|
|
62
|
+
const flags = pattern.flags.includes('g')
|
|
63
|
+
? pattern.flags
|
|
64
|
+
: `${pattern.flags}g`;
|
|
65
|
+
return [...text.matchAll(new RegExp(pattern.source, flags))];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** True only when the window around a memory phrase carries a real persistence
|
|
69
|
+
* INSTRUCTION — an imperative persistence verb — not merely a persistence-related noun
|
|
70
|
+
* mentioned in passing. A noun alone ("el reporte esta en incident.md") names a file
|
|
71
|
+
* without instructing anything be saved to it, so it must NOT suppress the warning. */
|
|
72
|
+
function hasPersistenceInstructionNearby(text, match) {
|
|
73
|
+
const from = Math.max(0, match.index - PERSISTENCE_WINDOW);
|
|
74
|
+
const to = Math.min(
|
|
75
|
+
text.length,
|
|
76
|
+
match.index + match[0].length + PERSISTENCE_WINDOW,
|
|
77
|
+
);
|
|
78
|
+
const window = text.slice(from, to);
|
|
79
|
+
return PERSISTENCE_VERB_PATTERN.test(window);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function unresolvedMemoryPhrases(prompt, memoryPattern) {
|
|
83
|
+
if (!memoryPattern.test(prompt)) return []; // stage 1: no candidate, allow cheap
|
|
84
|
+
|
|
85
|
+
const text = stripQuoted(prompt); // stage 2(a)
|
|
86
|
+
const matches = allMatches(memoryPattern, text);
|
|
87
|
+
|
|
88
|
+
return matches
|
|
89
|
+
.filter((match) => !hasPersistenceInstructionNearby(text, match))
|
|
90
|
+
.map((match) => match[0].trim());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
runGate(
|
|
94
|
+
{
|
|
95
|
+
id: GATE_ID,
|
|
96
|
+
configKey: CONFIG_KEY,
|
|
97
|
+
enabledByDefault: false,
|
|
98
|
+
defaultParams: {
|
|
99
|
+
memoryDependencyPatterns: DEFAULT_MEMORY_DEPENDENCY_PATTERNS,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
({ toolName, toolInput, parameters }) => {
|
|
103
|
+
if (!toolInGroups(toolName, ['delegation'])) return;
|
|
104
|
+
|
|
105
|
+
const prompt = delegationPromptOf(toolInput);
|
|
106
|
+
if (!prompt.trim()) return;
|
|
107
|
+
|
|
108
|
+
const memoryPattern = withUnicodeWordBoundary(
|
|
109
|
+
(parameters.memoryDependencyPatterns ?? []).join('|'),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const phrases = unresolvedMemoryPhrases(prompt, memoryPattern);
|
|
113
|
+
if (phrases.length === 0) return;
|
|
114
|
+
|
|
115
|
+
const quotedPhrases = phrases.map((phrase) => `"${phrase}"`).join(', ');
|
|
116
|
+
warn(
|
|
117
|
+
GATE_ID,
|
|
118
|
+
`This delegation depends on the model remembering something (${quotedPhrases}). ` +
|
|
119
|
+
'If it matters, it should live in a file, a flag, an environment variable, or a deterministic gate — ' +
|
|
120
|
+
"not in the model's memory.",
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Edge-case audit for no-memory-dependency. This gate only warns, never denies, so a
|
|
2
|
+
// "bypass" here means the warning silently fails to fire for a prompt that really does
|
|
3
|
+
// depend on the model's memory, not that anything unsafe was blocked.
|
|
4
|
+
// Run: node --test no-memory-dependency.edge.test.mjs
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
14
|
+
|
|
15
|
+
function runGate(payload, { config } = {}) {
|
|
16
|
+
const project = mkdtempSync(join(tmpdir(), 'no-memory-dependency-edge-'));
|
|
17
|
+
mkdirSync(join(project, '.git'));
|
|
18
|
+
if (config) {
|
|
19
|
+
mkdirSync(join(project, '.ai'));
|
|
20
|
+
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
21
|
+
}
|
|
22
|
+
const out = execFileSync(process.execPath, [GATE], {
|
|
23
|
+
input: JSON.stringify(payload),
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
cwd: project,
|
|
26
|
+
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
27
|
+
});
|
|
28
|
+
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isWarn(result) {
|
|
32
|
+
return typeof result?.hookSpecificOutput?.additionalContext === 'string';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ENABLED = { config: { gates: { warnMemoryDependencyInBrief: true } } };
|
|
36
|
+
const MEMORY_PROMPT = 'Acordate de lo que hablamos antes y aplica el mismo criterio.';
|
|
37
|
+
|
|
38
|
+
test('FIXED: a delegation tool name outside the native list is now caught via toolInGroups (MCP delegation signal)', () => {
|
|
39
|
+
const result = runGate(
|
|
40
|
+
{ tool_name: 'mcp__orchestrator__spawn_agent', tool_input: { prompt: MEMORY_PROMPT } },
|
|
41
|
+
ENABLED,
|
|
42
|
+
);
|
|
43
|
+
assert.ok(isWarn(result), 'gate now warns for the memory-dependency prompt under the MCP tool name too');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('FIXED: prompt carried in a field other than prompt/description/task is now read via delegationPromptOf', () => {
|
|
47
|
+
const result = runGate(
|
|
48
|
+
{ tool_name: 'Agent', tool_input: { instructions: MEMORY_PROMPT } },
|
|
49
|
+
ENABLED,
|
|
50
|
+
);
|
|
51
|
+
assert.ok(isWarn(result), 'gate now sees the prompt under "instructions" and warns');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('FIXED: a persistence noun with no verb no longer suppresses the warning', () => {
|
|
55
|
+
// hasPersistenceInstructionNearby now requires an imperative persistence VERB nearby,
|
|
56
|
+
// not merely a noun. A prompt that mentions an unrelated file's name within the window
|
|
57
|
+
// (with no verb instructing anything be saved) no longer suppresses the warning.
|
|
58
|
+
const prompt =
|
|
59
|
+
'Acordate de lo que dijimos del login (el reporte esta en incident.md) y aplica el ' +
|
|
60
|
+
'mismo criterio de siempre.';
|
|
61
|
+
const result = runGate({ tool_name: 'Agent', tool_input: { prompt } }, ENABLED);
|
|
62
|
+
assert.ok(
|
|
63
|
+
isWarn(result),
|
|
64
|
+
'gate now warns: "incident.md" nearby with no persistence verb is not a real persistence instruction',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('OK: a genuine memory-dependency phrase via Agent is warned', () => {
|
|
69
|
+
assert.ok(isWarn(runGate({ tool_name: 'Agent', tool_input: { prompt: MEMORY_PROMPT } }, ENABLED)));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('OK: a memory phrase paired with a real persistence verb still suppresses the warning', () => {
|
|
73
|
+
const result = runGate(
|
|
74
|
+
{
|
|
75
|
+
tool_name: 'Agent',
|
|
76
|
+
tool_input: {
|
|
77
|
+
prompt: 'No te olvides de guardar la decision en .ai/decision.md antes de continuar.',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
ENABLED,
|
|
81
|
+
);
|
|
82
|
+
assert.equal(result, null);
|
|
83
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { test } from 'node:test';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
+
|
|
11
|
+
function newProject({ config } = {}) {
|
|
12
|
+
const project = mkdtempSync(join(tmpdir(), 'no-reconfirm-edge-'));
|
|
13
|
+
mkdirSync(join(project, '.git'));
|
|
14
|
+
if (config) {
|
|
15
|
+
mkdirSync(join(project, '.ai'));
|
|
16
|
+
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
17
|
+
}
|
|
18
|
+
return project;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeTranscript(project, events) {
|
|
22
|
+
const transcriptPath = join(project, 'transcript.jsonl');
|
|
23
|
+
writeFileSync(transcriptPath, events.map((e) => JSON.stringify(e)).join('\n'));
|
|
24
|
+
return transcriptPath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function humanTurn(text) {
|
|
28
|
+
return { type: 'user', userType: 'external', message: { content: text } };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function askQuestion(transcriptPath, question) {
|
|
32
|
+
return {
|
|
33
|
+
tool_name: 'AskUserQuestion',
|
|
34
|
+
transcript_path: transcriptPath,
|
|
35
|
+
tool_input: { questions: [question] },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function runGateIn(project, payload) {
|
|
40
|
+
const out = execFileSync(process.execPath, [GATE], {
|
|
41
|
+
input: JSON.stringify(payload),
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
cwd: project,
|
|
44
|
+
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
45
|
+
});
|
|
46
|
+
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isWarn(result) {
|
|
50
|
+
return result?.hookSpecificOutput?.additionalContext !== undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// EDGE CASE (BUG, confirmed via manual calculation before writing this test): the overlap
|
|
54
|
+
// check has no notion of WHICH entity the action targets — it only counts shared
|
|
55
|
+
// significant (>=5 char) words between the approving turn and the new question's topic
|
|
56
|
+
// text (header+question+option labels). Approving "migrate the STAGING database schema"
|
|
57
|
+
// shares 3 of 4 topic words (migrate/database/schema) with an unrelated question about
|
|
58
|
+
// migrating the BILLING database schema — a materially different target — and clears both
|
|
59
|
+
// MIN_SHARED_WORDS (2) and the default overlapThreshold (0.34): shared=3, fraction=0.75.
|
|
60
|
+
test('BUG: approving one migration is treated as approval for an unrelated migration of a different entity (staging vs billing)', () => {
|
|
61
|
+
const project = newProject();
|
|
62
|
+
const transcriptPath = writeTranscript(project, [
|
|
63
|
+
humanTurn('go ahead, migrate the staging database schema now'),
|
|
64
|
+
]);
|
|
65
|
+
const question = {
|
|
66
|
+
header: '',
|
|
67
|
+
question: 'Should I migrate the billing database schema?',
|
|
68
|
+
options: [],
|
|
69
|
+
};
|
|
70
|
+
const result = runGateIn(project, askQuestion(transcriptPath, question));
|
|
71
|
+
// Documents the false positive: the gate claims this was "already approved" even though
|
|
72
|
+
// the user approved a migration of a DIFFERENT database (staging, not billing).
|
|
73
|
+
assert.ok(isWarn(result), 'expected the (buggy) false-positive warn to fire: no entity/target check, only generic word overlap');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// EDGE CASE (BUG candidate): false negative via negation. The APPROVAL_PATTERN matches
|
|
77
|
+
// literal phrases like "go ahead" / "do it" without checking for a preceding negation.
|
|
78
|
+
// "don't go ahead" or "no, don't do it yet" still matches the raw pattern.
|
|
79
|
+
test('BUG: a NEGATED approval phrase ("do not go ahead") is still treated as approval', () => {
|
|
80
|
+
const project = newProject();
|
|
81
|
+
const transcriptPath = writeTranscript(project, [
|
|
82
|
+
humanTurn('do not go ahead with migrating the authentication database schema'),
|
|
83
|
+
]);
|
|
84
|
+
const question = {
|
|
85
|
+
header: 'Migration',
|
|
86
|
+
question: 'Should I migrate the authentication database schema?',
|
|
87
|
+
options: [],
|
|
88
|
+
};
|
|
89
|
+
const result = runGateIn(project, askQuestion(transcriptPath, question));
|
|
90
|
+
assert.ok(isWarn(result), 'expected the (buggy) warn to fire even though the user said NOT to go ahead — APPROVAL_PATTERN has no negation check');
|
|
91
|
+
});
|