@devrik-tools/claude-gates 0.7.2 → 0.8.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 +2 -2
- package/README.es.md +69 -9
- package/README.md +68 -7
- package/cli/doctor.mjs +132 -0
- package/cli/evidence.mjs +78 -0
- package/cli/hooks-manifest.mjs +89 -0
- package/cli/index.mjs +124 -6
- package/cli/init.mjs +97 -3
- package/cli/log.mjs +70 -0
- package/cli/materialize.mjs +36 -2
- package/cli/registry.mjs +11 -1
- package/cli/selection.mjs +18 -0
- package/cli/smoke-fixtures.json +114 -22
- package/cli/task.mjs +36 -8
- package/cli/toggle.mjs +125 -0
- package/package.json +1 -1
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/doctor.mjs +47 -1
- package/plugins/gates/hooks/gates/atomic-commit/index.mjs +92 -119
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +101 -66
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +8 -8
- package/plugins/gates/hooks/gates/autonomous-mode/stop.mjs +13 -64
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +186 -163
- package/plugins/gates/hooks/gates/block-remote-publish/index.mjs +100 -101
- package/plugins/gates/hooks/gates/brief-approved/index.mjs +71 -140
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +56 -137
- package/plugins/gates/hooks/gates/capability-map/index.mjs +280 -506
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +98 -212
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +127 -46
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +81 -19
- package/plugins/gates/hooks/gates/engram-first/index.mjs +41 -0
- package/plugins/gates/hooks/gates/engram-first/session-start.mjs +64 -0
- package/plugins/gates/hooks/gates/engram-first/shared.mjs +90 -0
- package/plugins/gates/hooks/gates/engram-first/stop.mjs +23 -0
- package/plugins/gates/hooks/gates/engram-first/track.mjs +97 -0
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +156 -50
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +51 -92
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +51 -83
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +40 -80
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +23 -149
- package/plugins/gates/hooks/gates/library-docs/index.mjs +262 -0
- package/plugins/gates/hooks/gates/library-docs/track.mjs +95 -0
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +88 -97
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +48 -95
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +69 -31
- package/plugins/gates/hooks/gates/never-assume/index.mjs +23 -18
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +127 -86
- package/plugins/gates/hooks/gates/no-coauthor/index.mjs +108 -88
- package/plugins/gates/hooks/gates/no-explanatory-comments/index.mjs +281 -0
- package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +170 -110
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +46 -77
- package/plugins/gates/hooks/gates/no-reconfirm/index.mjs +85 -47
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +137 -90
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +95 -51
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +72 -216
- package/plugins/gates/hooks/gates/risk-level/index.mjs +51 -204
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +34 -17
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +160 -95
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +196 -97
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +90 -143
- package/plugins/gates/hooks/gates/staged-lint/index.mjs +98 -106
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +47 -148
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +78 -68
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +53 -88
- package/plugins/gates/hooks/gates/tool-map/index.mjs +80 -110
- package/plugins/gates/hooks/hooks.json +112 -42
- package/plugins/gates/hooks/lib/config.mjs +6 -3
- package/plugins/gates/hooks/lib/delegation.mjs +352 -0
- package/plugins/gates/hooks/lib/gate-log.mjs +141 -0
- package/plugins/gates/hooks/lib/git.mjs +262 -0
- package/plugins/gates/hooks/lib/hook-io.mjs +448 -83
- package/plugins/gates/hooks/lib/session-state.mjs +150 -0
- package/plugins/gates/hooks/lib/signals.mjs +18 -13
- package/plugins/gates/hooks/lib/testing.mjs +137 -0
- package/plugins/gates/hooks/lib/tools.mjs +144 -0
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
- package/plugins/tasks/hooks/lib/task-store.mjs +6 -6
- package/registry.json +204 -36
|
@@ -1,44 +1,20 @@
|
|
|
1
|
-
// no-lint-suppression — denies a write that SILENCES
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// not a prose reminder. A legitimate suppression (a documented false positive) is allowed by
|
|
14
|
-
// putting the escape hatch on the SAME line as the directive, which also forces a reason to
|
|
15
|
-
// live next to it in the diff.
|
|
16
|
-
//
|
|
17
|
-
// ── What this catches ────────────────────────────────────────────────────────────────
|
|
18
|
-
// inline directives in any source file:
|
|
19
|
-
// // eslint-disable, /* eslint-disable */, // eslint-disable-next-line,
|
|
20
|
-
// // @ts-ignore, // @ts-nocheck, # type: ignore, # noqa, // prettier-ignore,
|
|
21
|
-
// // biome-ignore, // stylelint-disable, // NOSONAR
|
|
22
|
-
// config edits that weaken the ruleset, in eslint/tsconfig/prettier/biome/stylelint config:
|
|
23
|
-
// a rule set to "off" / 0, "@ts-nocheck", disabling strict, or "ignore" additions.
|
|
24
|
-
//
|
|
25
|
-
// ── What a project can configure (params) ───────────────────────────────────────────
|
|
26
|
-
// suppressionPatterns regex sources (case-insensitive) that mark a line as a
|
|
27
|
-
// suppression. Replaces the built-in list wholesale.
|
|
28
|
-
// escapeHatch substring that, present on the same line as a suppression, allows
|
|
29
|
-
// it — so a real false positive is annotated, not smuggled. Default
|
|
30
|
-
// 'lint-ok:' (write e.g. `// eslint-disable-next-line ... lint-ok: <reason>`).
|
|
31
|
-
// watchedConfigFiles basenames whose edits are also scanned for rule-weakening. Replaces
|
|
32
|
-
// the built-in list.
|
|
33
|
-
//
|
|
34
|
-
// ── Fail-safe shape ──────────────────────────────────────────────────────────────────
|
|
35
|
-
// A non-write tool, or a write with no suppression line: allow (silent). A write that adds a
|
|
36
|
-
// suppression line WITHOUT the escape hatch on that same line: deny, naming the line.
|
|
37
|
-
|
|
38
|
-
import { extname } from 'node:path';
|
|
1
|
+
// no-lint-suppression — denies a write that SILENCES a linter/type-checker instead of fixing
|
|
2
|
+
// the code: a NEW inline disable directive in a source file, or a rule turned off/downgraded
|
|
3
|
+
// in a watched config file. A line already present in the file on disk is not a new
|
|
4
|
+
// suppression (rewriting a file must not re-litigate old ones). A legitimate suppression is
|
|
5
|
+
// allowed by putting the escape hatch on the SAME line, so the reason lives next to it in the
|
|
6
|
+
// diff. Deliberate limits: `@ts-expect-error` WITH a description is the recommended form and
|
|
7
|
+
// passes; in config files only a rule-like key (`a/b`, `no-x`, or one under a rules/overrides
|
|
8
|
+
// block) counts, so `"printWidth": 0` is not a weakening; shell writes are out of scope.
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { basename, extname, resolve } from 'node:path';
|
|
12
|
+
import { projectRootOf } from '../../lib/config.mjs';
|
|
39
13
|
import {
|
|
40
|
-
|
|
14
|
+
compileRegexList,
|
|
41
15
|
deny,
|
|
16
|
+
escapeRegExp,
|
|
17
|
+
runGate,
|
|
42
18
|
toolInGroups,
|
|
43
19
|
writtenContentOf,
|
|
44
20
|
writtenPathOf,
|
|
@@ -47,28 +23,36 @@ import {
|
|
|
47
23
|
const GATE_ID = 'no-lint-suppression';
|
|
48
24
|
const CONFIG_KEY = 'blockLintSuppression';
|
|
49
25
|
|
|
50
|
-
// Inline suppression directives across the common linters/type-checkers. Sources, compiled
|
|
51
|
-
// case-insensitively and matched one line at a time.
|
|
52
26
|
const DEFAULT_SUPPRESSION_PATTERNS = [
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
27
|
+
'eslint-disable',
|
|
28
|
+
'@ts-ignore',
|
|
29
|
+
'@ts-nocheck',
|
|
30
|
+
'@ts-expect-error\\s*[:-]?\\s*$',
|
|
31
|
+
'prettier-ignore',
|
|
32
|
+
'biome-ignore',
|
|
33
|
+
'stylelint-disable',
|
|
34
|
+
'nosonar',
|
|
35
|
+
'#\\s*noqa',
|
|
36
|
+
'#\\s*type:\\s*ignore',
|
|
37
|
+
'//\\s*@flow-ignore',
|
|
38
|
+
'istanbul ignore',
|
|
39
|
+
'c8 ignore',
|
|
40
|
+
'pylint:\\s*disable',
|
|
41
|
+
'pyright:\\s*ignore',
|
|
42
|
+
'//\\s*nolint',
|
|
43
|
+
'#\\[allow\\(',
|
|
44
|
+
'#pragma\\s+warning\\s+disable',
|
|
45
|
+
'@SuppressWarnings',
|
|
65
46
|
];
|
|
66
47
|
|
|
67
|
-
//
|
|
48
|
+
// Basenames; `*` matches within one name.
|
|
68
49
|
const DEFAULT_WATCHED_CONFIG_FILES = [
|
|
69
50
|
'eslint.config.mjs',
|
|
70
51
|
'eslint.config.js',
|
|
71
52
|
'eslint.config.cjs',
|
|
53
|
+
'eslint.config.ts',
|
|
54
|
+
'eslint.config.mts',
|
|
55
|
+
'eslint.config.cts',
|
|
72
56
|
'.eslintrc',
|
|
73
57
|
'.eslintrc.js',
|
|
74
58
|
'.eslintrc.cjs',
|
|
@@ -76,60 +60,138 @@ const DEFAULT_WATCHED_CONFIG_FILES = [
|
|
|
76
60
|
'.eslintrc.yml',
|
|
77
61
|
'.eslintrc.yaml',
|
|
78
62
|
'tsconfig.json',
|
|
63
|
+
'tsconfig.*.json',
|
|
64
|
+
'jsconfig.json',
|
|
79
65
|
'.prettierrc',
|
|
66
|
+
'.prettierrc.json',
|
|
80
67
|
'biome.json',
|
|
81
68
|
'.stylelintrc',
|
|
82
69
|
'.stylelintrc.json',
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Inside a watched config file, these mark a rule being turned off or a check being weakened.
|
|
86
|
-
const CONFIG_WEAKENING_PATTERNS = [
|
|
87
|
-
// a rule mapped to "off" or 0: "no-unused-vars": "off" 'rule': 0
|
|
88
|
-
// (no trailing \b: the value can end in a quote, which is a non-word char, so \b would
|
|
89
|
-
// never match after "off" and silently miss every quoted "off")
|
|
90
|
-
String.raw`["'][^"']+["']\s*:\s*(?:["']off["']|0)(?:\s|,|}|$)`,
|
|
91
|
-
// strict / type-checking disabled in tsconfig
|
|
92
|
-
String.raw`"(?:strict|noImplicitAny|strictNullChecks|checkJs)"\s*:\s*false`,
|
|
93
|
-
String.raw`@ts-nocheck`,
|
|
70
|
+
'package.json',
|
|
94
71
|
];
|
|
95
72
|
|
|
96
73
|
const DEFAULT_ESCAPE_HATCH = 'lint-ok:';
|
|
97
74
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
75
|
+
const NON_SOURCE_EXTENSIONS = new Set(['.md', '.mdx', '.txt', '.lock', '.log']);
|
|
76
|
+
|
|
77
|
+
// ── Config weakening ────────────────────────────────────────────────────────────────
|
|
78
|
+
const RULES_SECTION_PATTERN = /(?:^|[\s{,])["']?(?:rules|overrides)["']?\s*:/;
|
|
79
|
+
const LINTER_SECTION_PATTERN = /["']linter["']\s*:/;
|
|
80
|
+
const STRICTNESS_OFF_PATTERN =
|
|
81
|
+
/["'](?:strict|noImplicitAny|strictNullChecks|checkJs)["']\s*:\s*false/;
|
|
82
|
+
const LINTER_DISABLED_PATTERN = /["']enabled["']\s*:\s*false/;
|
|
83
|
+
const TS_NOCHECK_PATTERN = /@ts-nocheck/i;
|
|
84
|
+
const DISABLING_TOKEN_PATTERN = /^["']?(?:off|warn|0)["']?(?=\s*(?:[,}\]]|$))/i;
|
|
85
|
+
const RULE_LIKE_KEY = /[/-]/;
|
|
86
|
+
const KEY_CHARACTER = /[\w@./-]/;
|
|
87
|
+
|
|
88
|
+
function hasSectionBefore(lines, index, sectionPattern) {
|
|
89
|
+
for (let cursor = index; cursor >= 0; cursor -= 1)
|
|
90
|
+
if (sectionPattern.test(lines[cursor])) return true;
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isDisablingValue(rest) {
|
|
95
|
+
const value = rest.trimStart().replace(/^\[\s*/, '');
|
|
96
|
+
return DISABLING_TOKEN_PATTERN.test(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Each `key: value` on the line, located by walking back from every colon (a regex over
|
|
100
|
+
// the key run is quadratic on colon-less lines).
|
|
101
|
+
function assignmentsOf(line) {
|
|
102
|
+
const found = [];
|
|
103
|
+
for (
|
|
104
|
+
let colon = line.indexOf(':');
|
|
105
|
+
colon !== -1;
|
|
106
|
+
colon = line.indexOf(':', colon + 1)
|
|
107
|
+
) {
|
|
108
|
+
let end = colon;
|
|
109
|
+
while (end > 0 && /\s/.test(line[end - 1])) end -= 1;
|
|
110
|
+
if (end > 0 && (line[end - 1] === '"' || line[end - 1] === "'")) end -= 1;
|
|
111
|
+
let start = end;
|
|
112
|
+
while (start > 0 && KEY_CHARACTER.test(line[start - 1])) start -= 1;
|
|
113
|
+
if (start < end)
|
|
114
|
+
found.push({ key: line.slice(start, end), rest: line.slice(colon + 1) });
|
|
115
|
+
}
|
|
116
|
+
return found;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function assignsDisablingValue(line, index, lines) {
|
|
120
|
+
for (const { key, rest } of assignmentsOf(line)) {
|
|
121
|
+
if (!isDisablingValue(rest)) continue;
|
|
122
|
+
if (RULE_LIKE_KEY.test(key)) return true;
|
|
123
|
+
if (hasSectionBefore(lines, index, RULES_SECTION_PATTERN)) return true;
|
|
106
124
|
}
|
|
107
|
-
return
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function weakensConfig(lines, index) {
|
|
129
|
+
const line = lines[index];
|
|
130
|
+
if (STRICTNESS_OFF_PATTERN.test(line) || TS_NOCHECK_PATTERN.test(line))
|
|
131
|
+
return true;
|
|
132
|
+
if (
|
|
133
|
+
LINTER_DISABLED_PATTERN.test(line) &&
|
|
134
|
+
hasSectionBefore(lines, index, LINTER_SECTION_PATTERN)
|
|
135
|
+
)
|
|
136
|
+
return true;
|
|
137
|
+
return assignsDisablingValue(line, index, lines);
|
|
108
138
|
}
|
|
109
139
|
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
140
|
+
function watchedFilePatterns(names) {
|
|
141
|
+
return compileRegexList(
|
|
142
|
+
names.map((name) => `^${escapeRegExp(name).replace(/\\\*/g, '[^/]*')}$`),
|
|
143
|
+
).patterns;
|
|
113
144
|
}
|
|
114
145
|
|
|
115
|
-
function isWatchedConfig(filePath, watchedConfigFiles) {
|
|
116
|
-
|
|
146
|
+
function isWatchedConfig(filePath, content, watchedConfigFiles) {
|
|
147
|
+
const name = basename(filePath.replace(/\\/g, '/'));
|
|
148
|
+
if (name === 'package.json' && !content.includes('eslintConfig'))
|
|
149
|
+
return false;
|
|
150
|
+
return watchedFilePatterns(watchedConfigFiles).some((pattern) =>
|
|
151
|
+
pattern.test(name),
|
|
152
|
+
);
|
|
117
153
|
}
|
|
118
154
|
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
155
|
+
// ── Lines already on disk are not new suppressions ──────────────────────────────────
|
|
156
|
+
// Counted, not just collected: a directive the file already has once does not license a
|
|
157
|
+
// second identical one.
|
|
158
|
+
function existingLineCounts(filePath, root) {
|
|
159
|
+
const counts = new Map();
|
|
160
|
+
const resolved = resolve(root, filePath);
|
|
161
|
+
try {
|
|
162
|
+
if (!existsSync(resolved)) return counts;
|
|
163
|
+
for (const line of readFileSync(resolved, 'utf8').split('\n')) {
|
|
164
|
+
const trimmed = line.trim();
|
|
165
|
+
counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
counts.clear();
|
|
169
|
+
}
|
|
170
|
+
return counts;
|
|
171
|
+
}
|
|
123
172
|
|
|
124
|
-
function
|
|
125
|
-
|
|
173
|
+
function consumeExisting(existing, trimmed) {
|
|
174
|
+
const remaining = existing.get(trimmed) ?? 0;
|
|
175
|
+
if (remaining === 0) return false;
|
|
176
|
+
existing.set(trimmed, remaining - 1);
|
|
177
|
+
return true;
|
|
126
178
|
}
|
|
127
179
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
180
|
+
function firstOffendingLine({
|
|
181
|
+
content,
|
|
182
|
+
inlinePatterns,
|
|
183
|
+
isConfig,
|
|
184
|
+
escapeHatch,
|
|
185
|
+
existing,
|
|
186
|
+
}) {
|
|
187
|
+
const lines = content.split('\n');
|
|
188
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
189
|
+
const line = lines[index];
|
|
190
|
+
const trimmed = line.trim();
|
|
191
|
+
if (!trimmed || consumeExisting(existing, trimmed)) continue;
|
|
131
192
|
if (escapeHatch && line.includes(escapeHatch)) continue;
|
|
132
|
-
if (
|
|
193
|
+
if (inlinePatterns.some((pattern) => pattern.test(line))) return trimmed;
|
|
194
|
+
if (isConfig && weakensConfig(lines, index)) return trimmed;
|
|
133
195
|
}
|
|
134
196
|
return null;
|
|
135
197
|
}
|
|
@@ -145,31 +207,29 @@ runGate(
|
|
|
145
207
|
escapeHatch: DEFAULT_ESCAPE_HATCH,
|
|
146
208
|
},
|
|
147
209
|
},
|
|
148
|
-
({ toolName, toolInput, parameters }) => {
|
|
210
|
+
({ toolName, toolInput, parameters, cwd }) => {
|
|
149
211
|
if (!toolInGroups(toolName, ['write'])) return;
|
|
150
212
|
|
|
151
213
|
const filePath = writtenPathOf(toolInput);
|
|
152
|
-
if (!filePath) return;
|
|
153
|
-
|
|
154
214
|
const content = writtenContentOf(toolInput);
|
|
155
|
-
if (!content) return;
|
|
156
|
-
|
|
157
|
-
const escapeHatch = parameters.escapeHatch ?? DEFAULT_ESCAPE_HATCH;
|
|
158
|
-
|
|
159
|
-
// Config files: scan for rule-weakening. Any source file: scan for inline suppressions.
|
|
160
|
-
const isConfig = isWatchedConfig(filePath, parameters.watchedConfigFiles);
|
|
161
|
-
const inlinePatterns = compile(parameters.suppressionPatterns);
|
|
162
|
-
const configPatterns = compile(CONFIG_WEAKENING_PATTERNS);
|
|
215
|
+
if (!filePath || !content) return;
|
|
163
216
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
217
|
+
const isConfig = isWatchedConfig(
|
|
218
|
+
filePath,
|
|
219
|
+
content,
|
|
220
|
+
parameters.watchedConfigFiles,
|
|
221
|
+
);
|
|
222
|
+
if (!isConfig && NON_SOURCE_EXTENSIONS.has(extname(filePath).toLowerCase()))
|
|
223
|
+
return;
|
|
224
|
+
|
|
225
|
+
const escapeHatch = String(parameters.escapeHatch ?? '');
|
|
226
|
+
const offender = firstOffendingLine({
|
|
227
|
+
content,
|
|
228
|
+
inlinePatterns: compileRegexList(parameters.suppressionPatterns).patterns,
|
|
229
|
+
isConfig,
|
|
230
|
+
escapeHatch,
|
|
231
|
+
existing: existingLineCounts(filePath, projectRootOf(cwd) ?? cwd),
|
|
232
|
+
});
|
|
173
233
|
if (!offender) return;
|
|
174
234
|
|
|
175
235
|
deny(
|
|
@@ -177,7 +237,7 @@ runGate(
|
|
|
177
237
|
`This write silences the linter/type-checker instead of fixing the code: "${offender}". ` +
|
|
178
238
|
'Fix the underlying issue rather than turning the check off. If this is a genuine, ' +
|
|
179
239
|
`documented false positive, put "${escapeHatch} <reason>" on the same line so the ` +
|
|
180
|
-
|
|
240
|
+
`reason lives next to the suppression, or set ${CONFIG_KEY} in .ai/config.json.`,
|
|
181
241
|
);
|
|
182
242
|
},
|
|
183
243
|
);
|
|
@@ -1,33 +1,29 @@
|
|
|
1
|
-
// no-memory-dependency — denies
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// matters, it must travel IN the prompt, in a file the subagent reads, or in an
|
|
5
|
-
// already-persisted decision. The dependency on unavailable memory is a complete, objective
|
|
6
|
-
// fact once stage 2 has ruled out a real persistence instruction — so this blocks.
|
|
1
|
+
// no-memory-dependency — denies a delegation whose prompt refers the subagent to something
|
|
2
|
+
// said earlier in the conversation ("como te dije", "as we discussed") instead of carrying
|
|
3
|
+
// the data itself: a fresh subagent has none of the delegator's history.
|
|
7
4
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// genuinely depends on a file, not on model memory. A persistence noun/verb that merely
|
|
14
|
-
// co-occurs in the window without forming an instruction TO persist THIS remembered
|
|
15
|
-
// thing (e.g. an unrelated file named elsewhere in the same sentence) does not suppress
|
|
16
|
-
// the deny.
|
|
17
|
-
//
|
|
18
|
-
// ── Escape hatch ─────────────────────────────────────────────────────────────────────
|
|
19
|
-
// A memory phrase can be a false positive: "no te olvides de cerrar el server al final"
|
|
20
|
-
// directs the SUBAGENT'S own future action, not data it must recall from the delegator. For
|
|
21
|
-
// that case the author adds the escapeHatch marker to the prompt to state, explicitly, that
|
|
22
|
-
// no cross-conversation memory is actually required.
|
|
5
|
+
// Decisions: the default phrases are references to PRIOR CONVERSATION only — "don't forget
|
|
6
|
+
// to X", "keep in mind that <fact>", "remember to run the tests" carry their own content and
|
|
7
|
+
// are allowed. A reference next to a real persistence instruction ("como quedamos, guardá la
|
|
8
|
+
// decisión en .ai/decision.md") depends on a file, not on memory, and is allowed. The escape
|
|
9
|
+
// hatch marker states that no recalled data is actually needed.
|
|
23
10
|
|
|
11
|
+
import {
|
|
12
|
+
allMatches,
|
|
13
|
+
promptExcerpt,
|
|
14
|
+
stripQuoted,
|
|
15
|
+
} from '../../lib/delegation.mjs';
|
|
24
16
|
import {
|
|
25
17
|
runGate,
|
|
26
18
|
deny,
|
|
19
|
+
compileRegexList,
|
|
27
20
|
toolInGroups,
|
|
28
21
|
delegationPromptOf,
|
|
29
22
|
} from '../../lib/hook-io.mjs';
|
|
30
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
PERSISTENCE_VERB,
|
|
25
|
+
withUnicodeWordBoundary,
|
|
26
|
+
} from '../../lib/signals.mjs';
|
|
31
27
|
|
|
32
28
|
const GATE_ID = 'no-memory-dependency';
|
|
33
29
|
const CONFIG_KEY = 'warnMemoryDependencyInBrief';
|
|
@@ -35,64 +31,39 @@ const CONFIG_KEY = 'warnMemoryDependencyInBrief';
|
|
|
35
31
|
const DEFAULT_ESCAPE_HATCH = 'memory-not-needed';
|
|
36
32
|
|
|
37
33
|
const DEFAULT_MEMORY_DEPENDENCY_PATTERNS = [
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
|
|
34
|
+
'recuerda que|acu[eé]rdate de (?:lo )?que|acordate de (?:lo )?que|como te dije',
|
|
35
|
+
'como (vimos|hablamos|dijimos|quedamos)|lo de antes',
|
|
36
|
+
'lo que te (dije|comenté|pasé)|ya sabes cu[aá]l',
|
|
37
|
+
'el (archivo|cambio|tema) de antes',
|
|
38
|
+
'remember that we|as (i|we) (said|discussed|mentioned|agreed)|as discussed',
|
|
39
|
+
'as mentioned (earlier|before|above)|the one from (before|earlier)',
|
|
40
|
+
'what we (discussed|talked about|decided)|you know which',
|
|
41
|
+
'like (before|last time)|the previous (one|file|change) i mentioned',
|
|
43
42
|
];
|
|
44
43
|
|
|
45
|
-
// A real persistence INSTRUCTION requires an imperative persistence VERB (an actual
|
|
46
|
-
// directive to save/persist/write THIS remembered thing), not merely a persistence-
|
|
47
|
-
// related NOUN mentioned somewhere nearby (a stray "incident.md" in the same sentence
|
|
48
|
-
// names a file without instructing anything be saved to it). Centralized in
|
|
49
|
-
// lib/signals.mjs (ES+EN) so this class of signal is not duplicated per gate.
|
|
50
|
-
const PERSISTENCE_VERB_PATTERN = PERSISTENCE_VERB;
|
|
51
|
-
|
|
52
44
|
const PERSISTENCE_WINDOW = 80;
|
|
53
45
|
|
|
54
|
-
function withUnicodeWordBoundary(alternatives) {
|
|
55
|
-
return new RegExp(
|
|
56
|
-
`(?<![\\p{L}\\p{N}_])(${alternatives})(?![\\p{L}\\p{N}_])`,
|
|
57
|
-
'iu',
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function stripQuoted(text) {
|
|
62
|
-
return text
|
|
63
|
-
.replace(/```[\s\S]*?```/g, ' ')
|
|
64
|
-
.replace(/"[^"\n]{0,300}"/g, ' ')
|
|
65
|
-
.replace(/'[^'\n]{0,300}'/g, ' ');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function allMatches(pattern, text) {
|
|
69
|
-
const flags = pattern.flags.includes('g')
|
|
70
|
-
? pattern.flags
|
|
71
|
-
: `${pattern.flags}g`;
|
|
72
|
-
return [...text.matchAll(new RegExp(pattern.source, flags))];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** True only when the window around a memory phrase carries a real persistence
|
|
76
|
-
* INSTRUCTION — an imperative persistence verb — not merely a persistence-related noun
|
|
77
|
-
* mentioned in passing. A noun alone ("el reporte esta en incident.md") names a file
|
|
78
|
-
* without instructing anything be saved to it, so it must NOT suppress the warning. */
|
|
79
46
|
function hasPersistenceInstructionNearby(text, match) {
|
|
80
47
|
const from = Math.max(0, match.index - PERSISTENCE_WINDOW);
|
|
81
48
|
const to = Math.min(
|
|
82
49
|
text.length,
|
|
83
50
|
match.index + match[0].length + PERSISTENCE_WINDOW,
|
|
84
51
|
);
|
|
85
|
-
|
|
86
|
-
return PERSISTENCE_VERB_PATTERN.test(window);
|
|
52
|
+
return PERSISTENCE_VERB.test(text.slice(from, to));
|
|
87
53
|
}
|
|
88
54
|
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
55
|
+
function memoryPatternFrom(sources) {
|
|
56
|
+
const { patterns } = compileRegexList(sources, 'iu');
|
|
57
|
+
if (patterns.length === 0) return null;
|
|
58
|
+
return withUnicodeWordBoundary(
|
|
59
|
+
patterns.map((pattern) => `(?:${pattern.source})`).join('|'),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
94
62
|
|
|
95
|
-
|
|
63
|
+
function unresolvedMemoryPhrases(prompt, memoryPattern) {
|
|
64
|
+
if (!memoryPattern.test(prompt)) return [];
|
|
65
|
+
const text = stripQuoted(prompt);
|
|
66
|
+
return allMatches(memoryPattern, text)
|
|
96
67
|
.filter((match) => !hasPersistenceInstructionNearby(text, match))
|
|
97
68
|
.map((match) => match[0].trim());
|
|
98
69
|
}
|
|
@@ -113,15 +84,13 @@ runGate(
|
|
|
113
84
|
const prompt = delegationPromptOf(toolInput);
|
|
114
85
|
if (!prompt.trim()) return;
|
|
115
86
|
|
|
116
|
-
|
|
117
|
-
const escapeHatch = (
|
|
118
|
-
parameters.escapeHatch ?? DEFAULT_ESCAPE_HATCH
|
|
119
|
-
).toLowerCase();
|
|
87
|
+
const escapeHatch = String(parameters.escapeHatch ?? '').toLowerCase();
|
|
120
88
|
if (escapeHatch && prompt.toLowerCase().includes(escapeHatch)) return;
|
|
121
89
|
|
|
122
|
-
const memoryPattern =
|
|
123
|
-
|
|
90
|
+
const memoryPattern = memoryPatternFrom(
|
|
91
|
+
parameters.memoryDependencyPatterns,
|
|
124
92
|
);
|
|
93
|
+
if (!memoryPattern) return;
|
|
125
94
|
|
|
126
95
|
const phrases = unresolvedMemoryPhrases(prompt, memoryPattern);
|
|
127
96
|
if (phrases.length === 0) return;
|
|
@@ -129,10 +98,10 @@ runGate(
|
|
|
129
98
|
const quotedPhrases = phrases.map((phrase) => `"${phrase}"`).join(', ');
|
|
130
99
|
deny(
|
|
131
100
|
CONFIG_KEY,
|
|
132
|
-
`This delegation depends on the subagent remembering something
|
|
133
|
-
|
|
134
|
-
'reads, a flag, or an already-persisted decision. If the phrase
|
|
135
|
-
`
|
|
101
|
+
`This delegation ("${promptExcerpt(prompt)}") depends on the subagent remembering something ` +
|
|
102
|
+
`(${quotedPhrases}), but a fresh subagent has none of this conversation. Put the data IN the ` +
|
|
103
|
+
'prompt, in a file it reads, a flag, or an already-persisted decision. If the phrase needs no ' +
|
|
104
|
+
`recalled data, add the marker "${escapeHatch || DEFAULT_ESCAPE_HATCH}" to the prompt.`,
|
|
136
105
|
);
|
|
137
106
|
},
|
|
138
107
|
);
|