@devrik-tools/claude-gates 0.7.2 → 0.9.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 +3 -3
- package/README.es.md +129 -11
- package/README.md +120 -10
- 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 +12 -1
- package/cli/selection.mjs +18 -0
- package/cli/smoke-fixtures.json +159 -25
- package/cli/task.mjs +104 -11
- package/cli/toggle.mjs +125 -0
- package/package.json +4 -3
- 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 +94 -215
- package/plugins/gates/hooks/gates/circuit-breaker/track.mjs +285 -0
- 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 +53 -95
- 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 +338 -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/no-trivial-scripts/index.mjs +114 -0
- 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/require-monitor/index.mjs +126 -0
- package/plugins/gates/hooks/gates/require-task-split/index.mjs +88 -0
- 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 +152 -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 +451 -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 +148 -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 +12 -6
- package/plugins/tasks/hooks/register-requests.mjs +37 -10
- package/registry.json +249 -38
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// require-task-split — denies writes and execution when the active task is too large
|
|
2
|
+
// to implement without sub-tasks. Deterministic: reads active.json, checks size and
|
|
3
|
+
// children count, denies unconditionally when the criteria are not met.
|
|
4
|
+
//
|
|
5
|
+
// Inspired by the Depth Tree method (unlazy): each sub-task must own specific files
|
|
6
|
+
// (OWNS), carry its own verification gate, and be independently completable. If the
|
|
7
|
+
// task's scope is ambiguous or the description is too vague to split confidently, the
|
|
8
|
+
// model must ask the user to clarify before registering sub-tasks.
|
|
9
|
+
//
|
|
10
|
+
// Sizes that require splitting: medium, large, unspecified (anything that is not
|
|
11
|
+
// explicitly small or trivial). A task that already has sub-tasks (children with
|
|
12
|
+
// matching parentId) passes. A project with no tasks also passes (nothing to enforce).
|
|
13
|
+
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { projectRootOf, readJsonOrNull } from '../../lib/config.mjs';
|
|
16
|
+
import { deny, runGate, toolInGroups } from '../../lib/hook-io.mjs';
|
|
17
|
+
|
|
18
|
+
const GATE_ID = 'require-task-split';
|
|
19
|
+
const CONFIG_KEY = 'requireTaskSplitBeforeImplementing';
|
|
20
|
+
|
|
21
|
+
const ACTIVE_TASKS_FILE = join('.ai', 'tasks', 'active.json');
|
|
22
|
+
const SIZES_EXEMPT_FROM_SPLIT = new Set(['small', 'trivial']);
|
|
23
|
+
const IMPLEMENTATION_STATUSES = new Set(['open', 'in_forge']);
|
|
24
|
+
|
|
25
|
+
function readActiveTasks(root) {
|
|
26
|
+
const parsed = readJsonOrNull(join(root, ACTIVE_TASKS_FILE));
|
|
27
|
+
const tasks = Array.isArray(parsed?.tasks) ? parsed.tasks : [];
|
|
28
|
+
return tasks.filter((task) => task && typeof task === 'object');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hasChildren(tasks, parentId) {
|
|
32
|
+
return tasks.some((task) => task.parentId === parentId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function unsplitLargeTasks(tasks) {
|
|
36
|
+
return tasks.filter((task) => {
|
|
37
|
+
if (!IMPLEMENTATION_STATUSES.has(task.status)) return false;
|
|
38
|
+
if (task.parentId) return false;
|
|
39
|
+
if (SIZES_EXEMPT_FROM_SPLIT.has(task.size)) return false;
|
|
40
|
+
return !hasChildren(tasks, task.id);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DENY_MESSAGE_TEMPLATE = (count, lines) =>
|
|
45
|
+
`${count} task(s) need splitting before implementation:\n${lines}\n\n` +
|
|
46
|
+
'BEFORE SPLITTING — check scope clarity:\n' +
|
|
47
|
+
' If the task description is vague or you are unsure what files/modules are affected,\n' +
|
|
48
|
+
' ASK THE USER to clarify the scope before registering sub-tasks. Do not guess.\n\n' +
|
|
49
|
+
'SPLITTING RULES (Depth Tree method):\n' +
|
|
50
|
+
' 1. Each sub-task must be small and independently verifiable\n' +
|
|
51
|
+
' 2. Each sub-task should OWN specific files — no two sub-tasks modify the same file\n' +
|
|
52
|
+
' 3. Each sub-task carries its own verification gate (--verify-command or --verify-path)\n' +
|
|
53
|
+
' 4. Split at natural boundaries: one module, one function, one test file\n' +
|
|
54
|
+
' 5. If the task has unclear scope, ask the user — do not split blindly\n\n' +
|
|
55
|
+
'Register sub-tasks:\n' +
|
|
56
|
+
' claude-gates task add "<what this sub-task delivers>" --parent <parent-id> --size small \\\n' +
|
|
57
|
+
' --verify-command "<check>" [--verify-expect <text>] \\\n' +
|
|
58
|
+
' --description "OWNS: <file1>, <file2>"\n\n' +
|
|
59
|
+
'Only after sub-tasks are registered can you proceed with writes and execution.';
|
|
60
|
+
|
|
61
|
+
runGate(
|
|
62
|
+
{
|
|
63
|
+
id: GATE_ID,
|
|
64
|
+
configKey: CONFIG_KEY,
|
|
65
|
+
enabledByDefault: true,
|
|
66
|
+
defaultParams: {},
|
|
67
|
+
},
|
|
68
|
+
({ toolName, cwd }) => {
|
|
69
|
+
const isWrite = toolInGroups(toolName, ['write']);
|
|
70
|
+
const isExecution = toolInGroups(toolName, ['execution']);
|
|
71
|
+
if (!isWrite && !isExecution) return;
|
|
72
|
+
|
|
73
|
+
const root = projectRootOf(cwd) ?? cwd;
|
|
74
|
+
const tasks = readActiveTasks(root);
|
|
75
|
+
if (tasks.length === 0) return;
|
|
76
|
+
|
|
77
|
+
const unsplit = unsplitLargeTasks(tasks);
|
|
78
|
+
if (unsplit.length === 0) return;
|
|
79
|
+
|
|
80
|
+
const lines = unsplit
|
|
81
|
+
.map(
|
|
82
|
+
(task) =>
|
|
83
|
+
` - [${task.status}] ${task.id}: ${task.title} (size: ${task.size ?? 'unspecified'})`,
|
|
84
|
+
)
|
|
85
|
+
.join('\n');
|
|
86
|
+
deny(CONFIG_KEY, DENY_MESSAGE_TEMPLATE(unsplit.length, lines));
|
|
87
|
+
},
|
|
88
|
+
);
|
|
@@ -1,41 +1,13 @@
|
|
|
1
|
-
// reuse-before-build — do not reinvent the wheel.
|
|
2
|
-
//
|
|
3
|
-
// the
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// Default .ai/tool-map.json.
|
|
12
|
-
// toolFolders folder names that mark a file as a "tool/helper". Replaces the built-in
|
|
13
|
-
// list wholesale.
|
|
14
|
-
// toolNamePatterns regex sources; a new file whose BASENAME matches any is treated as a
|
|
15
|
-
// generic helper regardless of its folder (useFoo, fooHelper, fooUtil...).
|
|
16
|
-
// toolExtensions file extensions considered code that can reinvent a wheel.
|
|
17
|
-
//
|
|
18
|
-
// ── How it decides (deterministic, no judgment) ─────────────────────────────────────
|
|
19
|
-
// A write that creates a tool/helper (by folder, by name pattern, or by a helper-ish
|
|
20
|
-
// extension), or a delegation prompt that asks to build one, must carry evidence the wheel
|
|
21
|
-
// was checked: (a) an audit phrase in the text, OR (b) the tool's own name already recorded
|
|
22
|
-
// in the tool map, OR (c) an installed dependency whose name matches the tool's name (there
|
|
23
|
-
// is already a package that plausibly covers it — reuse it or justify not to). Absent all
|
|
24
|
-
// three, the gate blocks with an actionable message.
|
|
25
|
-
//
|
|
26
|
-
// ── What changed vs the first version (closed holes) ────────────────────────────────
|
|
27
|
-
// · Map match was a lax "any 4+-letter word of the target appears anywhere in the JSON",
|
|
28
|
-
// which produced false "already covered" (a `parser.mjs` cleared by any unrelated entry
|
|
29
|
-
// mentioning "parser"). Now it matches the tool's own basename as a whole token against
|
|
30
|
-
// the recorded tool paths — structural, not substring-of-prose.
|
|
31
|
-
// · Scope was only scripts/hooks/tools/gates + executable extensions. Now it also covers
|
|
32
|
-
// lib/utils/helpers/composables/components/services AND name patterns (useX, XHelper,
|
|
33
|
-
// XUtil, XService) anywhere, which is where a wheel is most often reinvented.
|
|
34
|
-
// · Build-intent detection was English-only prose; now it uses the bilingual BUILD_INTENT.
|
|
35
|
-
// · Adds an installed-dependency check: a matching package.json dep also clears the build.
|
|
36
|
-
|
|
37
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
38
|
-
import { basename, dirname, extname, join } from 'node:path';
|
|
1
|
+
// reuse-before-build — do not reinvent the wheel. A write that creates a tool/helper, or a
|
|
2
|
+
// delegation asking to build one, must carry evidence the wheel was checked: an audit
|
|
3
|
+
// phrase, the tool's basename already recorded in the project's tool map, or an installed
|
|
4
|
+
// dependency of the same name. What counts as a tool and as evidence is shared with
|
|
5
|
+
// tool-map (lib/tools.mjs) so the read and write halves of the pair cannot drift apart.
|
|
6
|
+
// The gate never reaches the network: the cloud search is the assistant's job.
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
10
|
+
import { projectRootOf, readJsonOrNull } from '../../lib/config.mjs';
|
|
39
11
|
import {
|
|
40
12
|
runGate,
|
|
41
13
|
deny,
|
|
@@ -45,167 +17,57 @@ import {
|
|
|
45
17
|
delegationPromptOf,
|
|
46
18
|
} from '../../lib/hook-io.mjs';
|
|
47
19
|
import { isBuildIntent } from '../../lib/signals.mjs';
|
|
20
|
+
import {
|
|
21
|
+
AUDIT_DONE_PATTERN,
|
|
22
|
+
DEFAULT_TOOL_EXTENSIONS,
|
|
23
|
+
DEFAULT_TOOL_FOLDERS,
|
|
24
|
+
DEFAULT_TOOL_NAME_PATTERNS,
|
|
25
|
+
hasAuditEvidence,
|
|
26
|
+
isToolPath,
|
|
27
|
+
} from '../../lib/tools.mjs';
|
|
48
28
|
|
|
49
29
|
const GATE_ID = 'reuse-before-build';
|
|
50
30
|
const CONFIG_KEY = 'requireReuseCheckBeforeBuilding';
|
|
51
31
|
|
|
52
32
|
const DEFAULT_TOOL_MAP_FILE = join('.ai', 'tool-map.json');
|
|
33
|
+
const AUDIT_DONE_EXAMPLE_PHRASE = 'no existing tool covers this';
|
|
53
34
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
'.sh',
|
|
65
|
-
'.ps1',
|
|
66
|
-
'.rb',
|
|
67
|
-
'.go',
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
// Folders whose files are "tools/helpers" worth a reuse check. Beyond the original four,
|
|
71
|
-
// this now includes the places generic reusable code actually lives.
|
|
72
|
-
const DEFAULT_TOOL_FOLDERS = [
|
|
73
|
-
'scripts',
|
|
74
|
-
'hooks',
|
|
75
|
-
'tools',
|
|
76
|
-
'gates',
|
|
77
|
-
'lib',
|
|
78
|
-
'libs',
|
|
79
|
-
'utils',
|
|
80
|
-
'util',
|
|
81
|
-
'helpers',
|
|
82
|
-
'helper',
|
|
83
|
-
'composables',
|
|
84
|
-
'components',
|
|
85
|
-
'services',
|
|
86
|
-
'shared',
|
|
87
|
-
'common',
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
// A file whose BASENAME matches one of these is a generic helper wherever it lives —
|
|
91
|
-
// useDebounce.ts in features/x/ is still a candidate for "does this already exist?".
|
|
92
|
-
const DEFAULT_TOOL_NAME_PATTERNS = [
|
|
93
|
-
String.raw`^use[A-Z]`, // React/Vue hook/composable: useFoo
|
|
94
|
-
String.raw`[-.]?helpers?\.`, // foo.helper.ts, foo-helpers.js
|
|
95
|
-
String.raw`[-.]?utils?\.`, // foo.util.ts, string-utils.js
|
|
96
|
-
String.raw`[-.]?service\.`, // foo.service.ts
|
|
97
|
-
String.raw`[-.]?wrapper\.`, // foo.wrapper.ts
|
|
98
|
-
];
|
|
99
|
-
|
|
100
|
-
// Evidence the wheel was already checked: any phrase stating the audit result. Bilingual.
|
|
101
|
-
const AUDIT_DONE_PATTERN =
|
|
102
|
-
/(already exists|no existing tool|there is no plugin|audited|checked whether|nothing does this|justification:|ya existe|no existe (?:una |la )?herramienta|no hay plugin|audit[eé]|verifiqu[eé] que no)/i;
|
|
103
|
-
|
|
104
|
-
function projectRootOf(startDirectory) {
|
|
105
|
-
let current = startDirectory;
|
|
106
|
-
while (true) {
|
|
107
|
-
if (existsSync(join(current, '.git'))) return current;
|
|
108
|
-
const parent = dirname(current);
|
|
109
|
-
if (parent === current) return null;
|
|
110
|
-
current = parent;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Whether the tool map already records THIS tool, matched structurally: the new file's
|
|
116
|
-
* basename (without extension) is compared as a whole `-`/`.`-delimited token against the
|
|
117
|
-
* basenames of the recorded tool paths. This replaces the old "any 4+-letter word appears
|
|
118
|
-
* anywhere in the JSON blob", which cleared unrelated builds by coincidental word overlap.
|
|
119
|
-
*/
|
|
120
|
-
function toolAlreadyInMap(startDirectory, toolMapFile, toolBaseName) {
|
|
121
|
-
const root = projectRootOf(startDirectory);
|
|
122
|
-
if (!root) return false;
|
|
123
|
-
const path = join(root, toolMapFile);
|
|
124
|
-
if (!existsSync(path)) return false;
|
|
125
|
-
let parsed;
|
|
126
|
-
try {
|
|
127
|
-
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
128
|
-
} catch {
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
if (!Array.isArray(parsed.tools)) return false;
|
|
132
|
-
const target = toolBaseName.toLowerCase();
|
|
133
|
-
return parsed.tools.some((tool) => {
|
|
134
|
-
const recorded = basename(String(tool.path ?? ''))
|
|
135
|
-
.replace(/\.[^.]+$/, '')
|
|
136
|
-
.toLowerCase();
|
|
137
|
-
return recorded === target;
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** Whether an installed dependency's name matches the tool's name (a package likely covers
|
|
142
|
-
* it). Normalizes scoped names (@scope/foo → foo) and matches whole, so `parser` is cleared
|
|
143
|
-
* by a `parser`/`@x/parser` dep but not by an unrelated `body-parser`. */
|
|
144
|
-
function installedDependencyCovers(startDirectory, toolBaseName) {
|
|
145
|
-
const root = projectRootOf(startDirectory);
|
|
146
|
-
if (!root) return false;
|
|
147
|
-
const path = join(root, 'package.json');
|
|
148
|
-
if (!existsSync(path)) return false;
|
|
149
|
-
let parsed;
|
|
150
|
-
try {
|
|
151
|
-
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
152
|
-
} catch {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
const names = [
|
|
156
|
-
...Object.keys(parsed.dependencies ?? {}),
|
|
157
|
-
...Object.keys(parsed.devDependencies ?? {}),
|
|
158
|
-
];
|
|
159
|
-
const target = toolBaseName.toLowerCase();
|
|
160
|
-
return names.some((name) => {
|
|
161
|
-
const bare = name.replace(/^@[^/]+\//, '').toLowerCase();
|
|
162
|
-
return bare === target;
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function hasToolExtension(filePath, toolExtensions) {
|
|
167
|
-
return toolExtensions.includes(extname(filePath).toLowerCase());
|
|
35
|
+
// A configured map path that escapes the project root is ignored in favor of the default:
|
|
36
|
+
// the map is project state, never a file elsewhere on the machine.
|
|
37
|
+
function toolMapPathFor(root, toolMapFile) {
|
|
38
|
+
const resolved = resolve(root, String(toolMapFile || DEFAULT_TOOL_MAP_FILE));
|
|
39
|
+
const relativePath = relative(root, resolved);
|
|
40
|
+
const inside =
|
|
41
|
+
relativePath !== '' &&
|
|
42
|
+
!relativePath.startsWith('..') &&
|
|
43
|
+
!isAbsolute(relativePath);
|
|
44
|
+
return inside ? resolved : join(root, DEFAULT_TOOL_MAP_FILE);
|
|
168
45
|
}
|
|
169
46
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return segments.some((segment) =>
|
|
175
|
-
toolFolders.includes(segment.toLowerCase()),
|
|
176
|
-
);
|
|
47
|
+
function stemOf(path) {
|
|
48
|
+
return basename(String(path ?? ''))
|
|
49
|
+
.replace(/\.[^.]+$/, '')
|
|
50
|
+
.toLowerCase();
|
|
177
51
|
}
|
|
178
52
|
|
|
179
|
-
function
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
} catch {
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
});
|
|
53
|
+
function toolAlreadyInMap(root, toolMapFile, toolBaseName) {
|
|
54
|
+
const parsed = readJsonOrNull(toolMapPathFor(root, toolMapFile));
|
|
55
|
+
if (!Array.isArray(parsed?.tools)) return false;
|
|
56
|
+
return parsed.tools.some((tool) => stemOf(tool?.path) === toolBaseName);
|
|
187
57
|
}
|
|
188
58
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
59
|
+
function installedDependencyCovers(root, toolBaseName) {
|
|
60
|
+
const parsed = readJsonOrNull(join(root, 'package.json'));
|
|
61
|
+
const names = [];
|
|
62
|
+
for (const field of ['dependencies', 'devDependencies']) {
|
|
63
|
+
const block = parsed?.[field];
|
|
64
|
+
if (block && typeof block === 'object') names.push(...Object.keys(block));
|
|
65
|
+
}
|
|
66
|
+
return names.some(
|
|
67
|
+
(name) => name.replace(/^@[^/]+\//, '').toLowerCase() === toolBaseName,
|
|
196
68
|
);
|
|
197
69
|
}
|
|
198
70
|
|
|
199
|
-
// The exact phrase AUDIT_DONE_PATTERN matches — quoted verbatim in the deny message so the
|
|
200
|
-
// model can copy it instead of guessing a paraphrase the regex might not recognize (a vague
|
|
201
|
-
// "I checked" or "no lo encontré" does NOT match AUDIT_DONE_PATTERN and the gate fires again).
|
|
202
|
-
const AUDIT_DONE_EXAMPLE_PHRASE = 'no existing tool covers this';
|
|
203
|
-
|
|
204
|
-
/** The concrete deny message for a blocked WRITE: names the exact file and gives the exact
|
|
205
|
-
* text to add (copy-pasteable, matches AUDIT_DONE_PATTERN verbatim) — no exploration needed
|
|
206
|
-
* to find any of this, it is the gate's own job to hand it over. Adding the phrase also
|
|
207
|
-
* satisfies the tool-map gate (maintainToolMap), which records the file + this same line
|
|
208
|
-
* into .ai/tool-map.json automatically — no separate command to run. */
|
|
209
71
|
function writeDenyMessage(filePath) {
|
|
210
72
|
return (
|
|
211
73
|
`Blocked: '${filePath}' looks like a new tool/helper, and nothing shows the wheel was ` +
|
|
@@ -214,16 +76,14 @@ function writeDenyMessage(filePath) {
|
|
|
214
76
|
'reuse/import the existing one instead.\n' +
|
|
215
77
|
' 2. It does not exist anywhere you checked (repo, installed deps, npm/marketplaces) — ' +
|
|
216
78
|
"add ONE line to the file's content stating that, e.g.: " +
|
|
217
|
-
`"// ${AUDIT_DONE_EXAMPLE_PHRASE}" (any phrase matching /${AUDIT_DONE_PATTERN.source}/
|
|
79
|
+
`"// ${AUDIT_DONE_EXAMPLE_PHRASE}" (any phrase matching /${AUDIT_DONE_PATTERN.source}/iu ` +
|
|
218
80
|
'works, this one is guaranteed to). That line also gets this file auto-recorded into ' +
|
|
219
|
-
'
|
|
81
|
+
'the tool map — nothing further to run.\n' +
|
|
220
82
|
'No filesystem exploration is required to satisfy this gate — the audit is a sentence ' +
|
|
221
83
|
'in the file or a matching package.json dependency, nothing else.'
|
|
222
84
|
);
|
|
223
85
|
}
|
|
224
86
|
|
|
225
|
-
/** The concrete deny message for a blocked DELEGATION prompt: same two options, but no file
|
|
226
|
-
* name exists yet (only a prompt), so both talk about the prompt text, not a file. */
|
|
227
87
|
function delegationDenyMessage() {
|
|
228
88
|
return (
|
|
229
89
|
"Blocked: this delegation's prompt asks to build a new tool/helper, and nothing in " +
|
|
@@ -233,46 +93,42 @@ function delegationDenyMessage() {
|
|
|
233
93
|
'reference the existing one in the prompt instead.\n' +
|
|
234
94
|
' 2. It does not exist anywhere you checked — add ONE line to the prompt stating ' +
|
|
235
95
|
`that, e.g.: "${AUDIT_DONE_EXAMPLE_PHRASE}" (any phrase matching ` +
|
|
236
|
-
`/${AUDIT_DONE_PATTERN.source}/
|
|
96
|
+
`/${AUDIT_DONE_PATTERN.source}/iu works, this one is guaranteed to). Once the delegate ` +
|
|
237
97
|
'actually writes the file, include the same phrase in its content so it also gets ' +
|
|
238
|
-
'auto-recorded into
|
|
98
|
+
'auto-recorded into the tool map.\n' +
|
|
239
99
|
'No filesystem exploration is required to satisfy this gate — the audit is a sentence ' +
|
|
240
100
|
'in the prompt, or a matching package.json dependency, nothing else.'
|
|
241
101
|
);
|
|
242
102
|
}
|
|
243
103
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
toolBaseName
|
|
250
|
-
|
|
251
|
-
)
|
|
252
|
-
return true;
|
|
253
|
-
if (toolBaseName && installedDependencyCovers(cwd, toolBaseName)) return true;
|
|
254
|
-
return false;
|
|
104
|
+
function buildIsCleared(text, root, parameters, toolBaseName) {
|
|
105
|
+
if (hasAuditEvidence(text)) return true;
|
|
106
|
+
if (!toolBaseName) return false;
|
|
107
|
+
return (
|
|
108
|
+
toolAlreadyInMap(root, parameters.toolMapFile, toolBaseName) ||
|
|
109
|
+
installedDependencyCovers(root, toolBaseName)
|
|
110
|
+
);
|
|
255
111
|
}
|
|
256
112
|
|
|
257
|
-
|
|
258
|
-
* prompt, so only the audit-phrase evidence applies there. */
|
|
259
|
-
function checkDelegation(toolInput, cwd, parameters) {
|
|
113
|
+
function checkDelegation(toolInput, root, parameters) {
|
|
260
114
|
const prompt = delegationPromptOf(toolInput);
|
|
261
115
|
if (!isBuildIntent(prompt)) return;
|
|
262
|
-
if (buildIsCleared(prompt,
|
|
116
|
+
if (buildIsCleared(prompt, root, parameters, null)) return;
|
|
263
117
|
deny(CONFIG_KEY, delegationDenyMessage());
|
|
264
118
|
}
|
|
265
119
|
|
|
266
|
-
|
|
267
|
-
function checkWrite(toolInput, cwd, parameters) {
|
|
120
|
+
function checkWrite(toolInput, root, parameters) {
|
|
268
121
|
const rawPath = writtenPathOf(toolInput);
|
|
269
122
|
const filePath = rawPath.replace(/\\/g, '/');
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
123
|
+
const isTool = isToolPath(filePath, {
|
|
124
|
+
folders: parameters.toolFolders,
|
|
125
|
+
extensions: parameters.toolExtensions,
|
|
126
|
+
namePatterns: parameters.toolNamePatterns,
|
|
127
|
+
});
|
|
128
|
+
if (!isTool) return;
|
|
129
|
+
if (existsSync(isAbsolute(rawPath) ? rawPath : join(root, rawPath))) return;
|
|
273
130
|
const content = writtenContentOf(toolInput);
|
|
274
|
-
|
|
275
|
-
if (buildIsCleared(content, cwd, parameters, toolBaseName)) return;
|
|
131
|
+
if (buildIsCleared(content, root, parameters, stemOf(filePath))) return;
|
|
276
132
|
deny(CONFIG_KEY, writeDenyMessage(filePath));
|
|
277
133
|
}
|
|
278
134
|
|
|
@@ -288,12 +144,12 @@ runGate(
|
|
|
288
144
|
toolExtensions: DEFAULT_TOOL_EXTENSIONS,
|
|
289
145
|
},
|
|
290
146
|
},
|
|
291
|
-
({ toolName, toolInput, parameters }) => {
|
|
292
|
-
const
|
|
147
|
+
({ toolName, toolInput, parameters, cwd }) => {
|
|
148
|
+
const root = projectRootOf(cwd) ?? cwd;
|
|
293
149
|
if (toolInGroups(toolName, ['delegation'])) {
|
|
294
|
-
checkDelegation(toolInput,
|
|
150
|
+
checkDelegation(toolInput, root, parameters);
|
|
295
151
|
} else if (toolInGroups(toolName, ['write'])) {
|
|
296
|
-
checkWrite(toolInput,
|
|
152
|
+
checkWrite(toolInput, root, parameters);
|
|
297
153
|
}
|
|
298
154
|
},
|
|
299
155
|
);
|