@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.
Files changed (78) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.es.md +69 -9
  3. package/README.md +68 -7
  4. package/cli/doctor.mjs +132 -0
  5. package/cli/evidence.mjs +78 -0
  6. package/cli/hooks-manifest.mjs +89 -0
  7. package/cli/index.mjs +124 -6
  8. package/cli/init.mjs +97 -3
  9. package/cli/log.mjs +70 -0
  10. package/cli/materialize.mjs +36 -2
  11. package/cli/registry.mjs +11 -1
  12. package/cli/selection.mjs +18 -0
  13. package/cli/smoke-fixtures.json +114 -22
  14. package/cli/task.mjs +36 -8
  15. package/cli/toggle.mjs +125 -0
  16. package/package.json +1 -1
  17. package/plugins/gates/.claude-plugin/plugin.json +1 -1
  18. package/plugins/gates/hooks/doctor.mjs +47 -1
  19. package/plugins/gates/hooks/gates/atomic-commit/index.mjs +92 -119
  20. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +101 -66
  21. package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +8 -8
  22. package/plugins/gates/hooks/gates/autonomous-mode/stop.mjs +13 -64
  23. package/plugins/gates/hooks/gates/bash-commands/index.mjs +186 -163
  24. package/plugins/gates/hooks/gates/block-remote-publish/index.mjs +100 -101
  25. package/plugins/gates/hooks/gates/brief-approved/index.mjs +71 -140
  26. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +56 -137
  27. package/plugins/gates/hooks/gates/capability-map/index.mjs +280 -506
  28. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +98 -212
  29. package/plugins/gates/hooks/gates/dependency-skills/index.mjs +127 -46
  30. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +81 -19
  31. package/plugins/gates/hooks/gates/engram-first/index.mjs +41 -0
  32. package/plugins/gates/hooks/gates/engram-first/session-start.mjs +64 -0
  33. package/plugins/gates/hooks/gates/engram-first/shared.mjs +90 -0
  34. package/plugins/gates/hooks/gates/engram-first/stop.mjs +23 -0
  35. package/plugins/gates/hooks/gates/engram-first/track.mjs +97 -0
  36. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +156 -50
  37. package/plugins/gates/hooks/gates/force-parallel/index.mjs +51 -92
  38. package/plugins/gates/hooks/gates/forge-flow/index.mjs +51 -83
  39. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +40 -80
  40. package/plugins/gates/hooks/gates/intent-flow/index.mjs +23 -149
  41. package/plugins/gates/hooks/gates/library-docs/index.mjs +262 -0
  42. package/plugins/gates/hooks/gates/library-docs/track.mjs +95 -0
  43. package/plugins/gates/hooks/gates/lint-commit/index.mjs +88 -97
  44. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +48 -95
  45. package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +69 -31
  46. package/plugins/gates/hooks/gates/never-assume/index.mjs +23 -18
  47. package/plugins/gates/hooks/gates/no-blocking/index.mjs +127 -86
  48. package/plugins/gates/hooks/gates/no-coauthor/index.mjs +108 -88
  49. package/plugins/gates/hooks/gates/no-explanatory-comments/index.mjs +281 -0
  50. package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +170 -110
  51. package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +46 -77
  52. package/plugins/gates/hooks/gates/no-reconfirm/index.mjs +85 -47
  53. package/plugins/gates/hooks/gates/protected-paths/index.mjs +137 -90
  54. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +95 -51
  55. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +72 -216
  56. package/plugins/gates/hooks/gates/risk-level/index.mjs +51 -204
  57. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +34 -17
  58. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +160 -95
  59. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +196 -97
  60. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +90 -143
  61. package/plugins/gates/hooks/gates/staged-lint/index.mjs +98 -106
  62. package/plugins/gates/hooks/gates/stop-pending/index.mjs +47 -148
  63. package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +78 -68
  64. package/plugins/gates/hooks/gates/test-matrix/index.mjs +53 -88
  65. package/plugins/gates/hooks/gates/tool-map/index.mjs +80 -110
  66. package/plugins/gates/hooks/hooks.json +112 -42
  67. package/plugins/gates/hooks/lib/config.mjs +6 -3
  68. package/plugins/gates/hooks/lib/delegation.mjs +352 -0
  69. package/plugins/gates/hooks/lib/gate-log.mjs +141 -0
  70. package/plugins/gates/hooks/lib/git.mjs +262 -0
  71. package/plugins/gates/hooks/lib/hook-io.mjs +448 -83
  72. package/plugins/gates/hooks/lib/session-state.mjs +150 -0
  73. package/plugins/gates/hooks/lib/signals.mjs +18 -13
  74. package/plugins/gates/hooks/lib/testing.mjs +137 -0
  75. package/plugins/gates/hooks/lib/tools.mjs +144 -0
  76. package/plugins/tasks/.claude-plugin/plugin.json +1 -1
  77. package/plugins/tasks/hooks/lib/task-store.mjs +6 -6
  78. package/registry.json +204 -36
@@ -1,20 +1,12 @@
1
- // tool-map — maintains the per-project tool map so a discovery is never repeated. It is
2
- // the write half of the discovery pair (reuse-before-build is the read half). When a new
3
- // tool is created WITH its audit declared, this gate records what that tool covers into
4
- // .ai/tool-map.json, so next time reuse-before-build finds it and does not block.
5
- //
6
- // It never reaches the network and takes no judgment: it only appends a deterministic
7
- // record (path + the audit line the author already wrote) to a JSON file. The cloud search
8
- // that decides "does something already exist?" is the assistant's / the CLI's job. This
9
- // gate never denies it observes a legitimate build and remembers it. It warns once when
10
- // it could not record, so a silent write failure does not go unnoticed.
11
- //
12
- // ── What a project can configure (params) ───────────────────────────────────────────
13
- // toolMapFile path to the per-project tool map, relative to the project root.
14
- // Default .ai/tool-map.json.
15
-
16
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
17
- import { dirname, extname, join } from 'node:path';
1
+ // tool-map — the write half of the discovery pair (reuse-before-build reads). When a tool
2
+ // is created WITH its audit declared, the tool and that audit line are recorded in the
3
+ // project's tool map so the discovery is never repeated. Never denies; warns once when it
4
+ // cannot record. What counts as a tool and as an audit line is the same lib definition
5
+ // reuse-before-build uses, so the map records exactly what the read half accepts.
6
+
7
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
8
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
9
+ import { projectRootOf, readJsonOrNull } from '../../lib/config.mjs';
18
10
  import {
19
11
  runGate,
20
12
  warn,
@@ -22,6 +14,13 @@ import {
22
14
  writtenContentOf,
23
15
  writtenPathOf,
24
16
  } from '../../lib/hook-io.mjs';
17
+ import {
18
+ DEFAULT_TOOL_EXTENSIONS,
19
+ DEFAULT_TOOL_FOLDERS,
20
+ DEFAULT_TOOL_NAME_PATTERNS,
21
+ hasAuditEvidence,
22
+ isToolPath,
23
+ } from '../../lib/tools.mjs';
25
24
 
26
25
  const GATE_ID = 'tool-map';
27
26
  const CONFIG_KEY = 'maintainToolMap';
@@ -30,107 +29,73 @@ const DEFAULT_TOOL_MAP_FILE = join('.ai', 'tool-map.json');
30
29
  const JSON_INDENT = 2;
31
30
  const MAX_AUDIT_LENGTH = 300;
32
31
 
33
- // Kept in sync with reuse-before-build's scope (its read half): a file this gate would NOT
34
- // record is a file reuse-before-build would still flag, and the pair must agree on what
35
- // counts as a tool/helper. Extensions and folders are broad because a reusable helper lives
36
- // in many places (lib/utils/composables/components), not only the four original tool dirs.
37
- const EXECUTABLE_EXTENSIONS = new Set([
38
- '.js',
39
- '.mjs',
40
- '.cjs',
41
- '.ts',
42
- '.tsx',
43
- '.jsx',
44
- '.vue',
45
- '.py',
46
- '.sh',
47
- '.ps1',
48
- '.rb',
49
- '.go',
50
- ]);
51
- const TOOL_FOLDERS = [
52
- 'scripts/',
53
- 'hooks/',
54
- 'tools/',
55
- 'gates/',
56
- 'lib/',
57
- 'libs/',
58
- 'utils/',
59
- 'util/',
60
- 'helpers/',
61
- 'helper/',
62
- 'composables/',
63
- 'components/',
64
- 'services/',
65
- 'shared/',
66
- 'common/',
67
- ];
68
-
69
- // Phrases that mark a line as the author's audit statement. Matched against one line at a
70
- // time (no wrapping `.*`, which backtracks): the matching line is captured whole.
71
- const AUDIT_PHRASE_PATTERN =
72
- /(already exists|no existing tool|there is no plugin|audited|nothing does this|justification:)/i;
32
+ // Same rule as reuse-before-build: a map path outside the project root is ignored.
33
+ function toolMapPathFor(root, toolMapFile) {
34
+ const resolved = resolve(root, String(toolMapFile || DEFAULT_TOOL_MAP_FILE));
35
+ const relativePath = relative(root, resolved);
36
+ const inside =
37
+ relativePath !== '' &&
38
+ !relativePath.startsWith('..') &&
39
+ !isAbsolute(relativePath);
40
+ return inside ? resolved : join(root, DEFAULT_TOOL_MAP_FILE);
41
+ }
73
42
 
74
- /** The first line of `content` that states an audit, trimmed — or null when none does. */
75
43
  function auditLineOf(content) {
76
- for (const line of content.split('\n')) {
77
- if (AUDIT_PHRASE_PATTERN.test(line)) return line.trim();
78
- }
79
- return null;
44
+ return content
45
+ .split('\n')
46
+ .map((line) => line.trim())
47
+ .find((line) => hasAuditEvidence(line));
80
48
  }
81
49
 
82
- function projectRootOf(startDirectory) {
83
- let current = startDirectory;
84
- while (true) {
85
- if (existsSync(join(current, '.git'))) return current;
86
- const parent = dirname(current);
87
- if (parent === current) return null;
88
- current = parent;
89
- }
50
+ function normalizePath(path) {
51
+ return String(path ?? '')
52
+ .replace(/\\/g, '/')
53
+ .replace(/^\.\//, '');
90
54
  }
91
55
 
92
- function isExecutableToolPath(filePath) {
93
- const normalized = filePath.replace(/\\/g, '/');
94
- const inToolFolder = TOOL_FOLDERS.some((folder) =>
95
- normalized.includes(folder),
96
- );
97
- return (
98
- inToolFolder && EXECUTABLE_EXTENSIONS.has(extname(filePath).toLowerCase())
99
- );
56
+ function rootRelativePath(root, rawPath) {
57
+ const absolute = isAbsolute(rawPath) ? rawPath : join(root, rawPath);
58
+ const relativePath = normalizePath(relative(root, absolute));
59
+ return relativePath.startsWith('..') ? normalizePath(rawPath) : relativePath;
100
60
  }
101
61
 
102
62
  function readMap(path) {
103
- if (!existsSync(path)) return { tools: [] };
104
- try {
105
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
106
- return Array.isArray(parsed.tools) ? parsed : { tools: [] };
107
- } catch {
108
- return { tools: [] };
109
- }
63
+ const parsed = readJsonOrNull(path);
64
+ return Array.isArray(parsed?.tools) ? parsed : { tools: [] };
110
65
  }
111
66
 
112
- /** Appends the tool's record to the map file, warning (never blocking) if it cannot. */
113
- function recordTool(mapPath, normalizedPath, auditLine, toolMapFile) {
67
+ // The file being recorded does not exist yet at PreToolUse time, so it is exempt from
68
+ // the prune; every other entry must still point at a real file.
69
+ function recordTool(root, mapPath, toolPath, auditLine) {
114
70
  const map = readMap(mapPath);
115
- if (map.tools.some((tool) => tool.path === normalizedPath)) return; // already recorded
116
-
117
- map.tools.push({
118
- path: normalizedPath,
119
- audit: auditLine.slice(0, MAX_AUDIT_LENGTH),
120
- recordedAt: new Date().toISOString(),
71
+ const kept = map.tools.filter((tool) => {
72
+ const recorded = normalizePath(tool?.path);
73
+ return recorded !== toolPath && existsSync(join(root, recorded));
121
74
  });
122
-
75
+ const unchanged =
76
+ kept.length === map.tools.length &&
77
+ map.tools.some((tool) => normalizePath(tool?.path) === toolPath);
78
+ if (unchanged) return;
79
+
80
+ const tools = [
81
+ ...kept,
82
+ {
83
+ path: toolPath,
84
+ audit: auditLine.slice(0, MAX_AUDIT_LENGTH),
85
+ recordedAt: new Date().toISOString(),
86
+ },
87
+ ];
123
88
  try {
124
89
  mkdirSync(dirname(mapPath), { recursive: true });
125
90
  writeFileSync(
126
91
  mapPath,
127
- `${JSON.stringify(map, null, JSON_INDENT)}\n`,
92
+ `${JSON.stringify({ ...map, tools }, null, JSON_INDENT)}\n`,
128
93
  'utf8',
129
94
  );
130
95
  } catch (error) {
131
96
  warn(
132
97
  CONFIG_KEY,
133
- `Could not record this tool in ${toolMapFile}: ${error?.message ?? error}. ` +
98
+ `Could not record this tool in ${mapPath}: ${error?.message ?? error}. ` +
134
99
  'The build proceeds, but the discovery was not remembered.',
135
100
  );
136
101
  }
@@ -141,28 +106,33 @@ runGate(
141
106
  id: GATE_ID,
142
107
  configKey: CONFIG_KEY,
143
108
  enabledByDefault: false,
144
- defaultParams: { toolMapFile: DEFAULT_TOOL_MAP_FILE },
109
+ defaultParams: {
110
+ toolMapFile: DEFAULT_TOOL_MAP_FILE,
111
+ toolFolders: DEFAULT_TOOL_FOLDERS,
112
+ toolExtensions: DEFAULT_TOOL_EXTENSIONS,
113
+ toolNamePatterns: DEFAULT_TOOL_NAME_PATTERNS,
114
+ },
145
115
  },
146
- ({ toolName, toolInput, parameters }) => {
116
+ ({ toolName, toolInput, parameters, cwd }) => {
147
117
  if (!toolInGroups(toolName, ['write'])) return;
148
118
 
149
- const filePath = writtenPathOf(toolInput);
150
- if (!isExecutableToolPath(filePath)) return;
119
+ const rawPath = writtenPathOf(toolInput);
120
+ const isTool = isToolPath(rawPath, {
121
+ folders: parameters.toolFolders,
122
+ extensions: parameters.toolExtensions,
123
+ namePatterns: parameters.toolNamePatterns,
124
+ });
125
+ if (!isTool) return;
151
126
 
152
- // Only a build that declared its audit is worth recording: that line IS the reason
153
- // this tool exists and what it was checked against.
154
- const content = writtenContentOf(toolInput);
155
- const auditLine = auditLineOf(content);
127
+ const auditLine = auditLineOf(writtenContentOf(toolInput));
156
128
  if (!auditLine) return;
157
129
 
158
- const root = projectRootOf(process.cwd());
159
- if (!root) return;
160
- const toolMapFile = parameters.toolMapFile ?? DEFAULT_TOOL_MAP_FILE;
130
+ const root = projectRootOf(cwd) ?? cwd;
161
131
  recordTool(
162
- join(root, toolMapFile),
163
- filePath.replace(/\\/g, '/'),
132
+ root,
133
+ toolMapPathFor(root, parameters.toolMapFile),
134
+ rootRelativePath(root, rawPath),
164
135
  auditLine,
165
- toolMapFile,
166
136
  );
167
137
  },
168
138
  );
@@ -2,7 +2,7 @@
2
2
  "hooks": {
3
3
  "PreToolUse": [
4
4
  {
5
- "matcher": "Bash|run_command|Agent|Task|invoke_subagent|mcp__.*",
5
+ "matcher": "Bash|run_command|PowerShell|Agent|Task|invoke_subagent|mcp__.*",
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
@@ -12,7 +12,7 @@
12
12
  ]
13
13
  },
14
14
  {
15
- "matcher": "Bash|run_command|Agent|Task|invoke_subagent|mcp__.*",
15
+ "matcher": "Bash|run_command|PowerShell|Agent|Task|invoke_subagent|mcp__.*",
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
@@ -22,7 +22,7 @@
22
22
  ]
23
23
  },
24
24
  {
25
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|mcp__.*",
25
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|PowerShell|mcp__.*",
26
26
  "hooks": [
27
27
  {
28
28
  "type": "command",
@@ -32,7 +32,7 @@
32
32
  ]
33
33
  },
34
34
  {
35
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|mcp__.*",
35
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|PowerShell|mcp__.*",
36
36
  "hooks": [
37
37
  {
38
38
  "type": "command",
@@ -42,7 +42,7 @@
42
42
  ]
43
43
  },
44
44
  {
45
- "matcher": "Bash|run_command|Agent|Task|invoke_subagent|mcp__.*",
45
+ "matcher": "Bash|run_command|PowerShell|Agent|Task|invoke_subagent|mcp__.*",
46
46
  "hooks": [
47
47
  {
48
48
  "type": "command",
@@ -102,7 +102,17 @@
102
102
  ]
103
103
  },
104
104
  {
105
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
105
+ "matcher": "Agent|Task|invoke_subagent|mcp__.*",
106
+ "hooks": [
107
+ {
108
+ "type": "command",
109
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/force-parallel/index.mjs\"",
110
+ "timeout": 30
111
+ }
112
+ ]
113
+ },
114
+ {
115
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|PowerShell|mcp__.*",
106
116
  "hooks": [
107
117
  {
108
118
  "type": "command",
@@ -112,7 +122,7 @@
112
122
  ]
113
123
  },
114
124
  {
115
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
125
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
116
126
  "hooks": [
117
127
  {
118
128
  "type": "command",
@@ -162,7 +172,7 @@
162
172
  ]
163
173
  },
164
174
  {
165
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
175
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|PowerShell|mcp__.*",
166
176
  "hooks": [
167
177
  {
168
178
  "type": "command",
@@ -172,7 +182,7 @@
172
182
  ]
173
183
  },
174
184
  {
175
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
185
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
176
186
  "hooks": [
177
187
  {
178
188
  "type": "command",
@@ -182,7 +192,7 @@
182
192
  ]
183
193
  },
184
194
  {
185
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
195
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
186
196
  "hooks": [
187
197
  {
188
198
  "type": "command",
@@ -192,7 +202,7 @@
192
202
  ]
193
203
  },
194
204
  {
195
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
205
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
196
206
  "hooks": [
197
207
  {
198
208
  "type": "command",
@@ -202,7 +212,7 @@
202
212
  ]
203
213
  },
204
214
  {
205
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|mcp__ide__executeCode|Agent|Task|invoke_subagent|mcp__.*",
215
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|PowerShell|mcp__ide__executeCode|Agent|Task|invoke_subagent|mcp__.*",
206
216
  "hooks": [
207
217
  {
208
218
  "type": "command",
@@ -212,7 +222,7 @@
212
222
  ]
213
223
  },
214
224
  {
215
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|mcp__ide__executeCode|Agent|Task|invoke_subagent|mcp__.*",
225
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|PowerShell|mcp__ide__executeCode|Agent|Task|invoke_subagent|mcp__.*",
216
226
  "hooks": [
217
227
  {
218
228
  "type": "command",
@@ -222,7 +232,7 @@
222
232
  ]
223
233
  },
224
234
  {
225
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
235
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
226
236
  "hooks": [
227
237
  {
228
238
  "type": "command",
@@ -242,7 +252,7 @@
242
252
  ]
243
253
  },
244
254
  {
245
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
255
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
246
256
  "hooks": [
247
257
  {
248
258
  "type": "command",
@@ -252,7 +262,7 @@
252
262
  ]
253
263
  },
254
264
  {
255
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
265
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
256
266
  "hooks": [
257
267
  {
258
268
  "type": "command",
@@ -262,113 +272,144 @@
262
272
  ]
263
273
  },
264
274
  {
265
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
275
+ "matcher": "Bash|run_command|PowerShell|mcp__.*",
266
276
  "hooks": [
267
277
  {
268
278
  "type": "command",
269
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/reuse-before-build/index.mjs\"",
279
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/lint-commit/index.mjs\"",
280
+ "timeout": 65
281
+ }
282
+ ]
283
+ },
284
+ {
285
+ "matcher": "Bash|run_command|PowerShell|mcp__.*",
286
+ "hooks": [
287
+ {
288
+ "type": "command",
289
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/staged-lint/index.mjs\"",
290
+ "timeout": 65
291
+ }
292
+ ]
293
+ },
294
+ {
295
+ "matcher": "Bash|run_command|PowerShell|mcp__.*",
296
+ "hooks": [
297
+ {
298
+ "type": "command",
299
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/atomic-commit/index.mjs\"",
270
300
  "timeout": 30
271
301
  }
272
302
  ]
273
303
  },
274
304
  {
275
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
305
+ "matcher": "Bash|run_command|PowerShell|Agent|Task|invoke_subagent|mcp__.*",
276
306
  "hooks": [
277
307
  {
278
308
  "type": "command",
279
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/tool-map/index.mjs\"",
309
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/no-coauthor/index.mjs\"",
280
310
  "timeout": 30
281
311
  }
282
312
  ]
283
313
  },
284
314
  {
285
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|mcp__.*",
315
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
286
316
  "hooks": [
287
317
  {
288
318
  "type": "command",
289
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/forge-flow/index.mjs\"",
319
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/no-lint-suppression/index.mjs\"",
290
320
  "timeout": 30
291
321
  }
292
322
  ]
293
323
  },
294
324
  {
295
- "matcher": "AskUserQuestion|mcp__.*",
325
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
296
326
  "hooks": [
297
327
  {
298
328
  "type": "command",
299
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/autonomous-mode/index.mjs\"",
329
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/no-explanatory-comments/index.mjs\"",
300
330
  "timeout": 30
301
331
  }
302
332
  ]
303
333
  },
304
334
  {
305
- "matcher": "Bash|run_command|mcp__.*",
335
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
306
336
  "hooks": [
307
337
  {
308
338
  "type": "command",
309
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/lint-commit/index.mjs\"",
310
- "timeout": 65
339
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/reuse-before-build/index.mjs\"",
340
+ "timeout": 30
311
341
  }
312
342
  ]
313
343
  },
314
344
  {
315
- "matcher": "Bash|run_command|mcp__.*",
345
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
316
346
  "hooks": [
317
347
  {
318
348
  "type": "command",
319
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/staged-lint/index.mjs\"",
320
- "timeout": 65
349
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/tool-map/index.mjs\"",
350
+ "timeout": 30
321
351
  }
322
352
  ]
323
353
  },
324
354
  {
325
- "matcher": "Agent|Task|invoke_subagent|mcp__.*",
355
+ "matcher": "WebSearch|WebFetch|mcp__.*",
326
356
  "hooks": [
327
357
  {
328
358
  "type": "command",
329
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/force-parallel/index.mjs\"",
359
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/engram-first/index.mjs\"",
330
360
  "timeout": 30
331
361
  }
332
362
  ]
333
363
  },
334
364
  {
335
- "matcher": "Bash|run_command|Agent|Task|invoke_subagent|mcp__.*",
365
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
336
366
  "hooks": [
337
367
  {
338
368
  "type": "command",
339
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/no-coauthor/index.mjs\"",
369
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/library-docs/index.mjs\"",
340
370
  "timeout": 30
341
371
  }
342
372
  ]
343
373
  },
344
374
  {
345
- "matcher": "Bash|run_command|mcp__.*",
375
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|PowerShell|mcp__.*",
346
376
  "hooks": [
347
377
  {
348
378
  "type": "command",
349
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/atomic-commit/index.mjs\"",
379
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/forge-flow/index.mjs\"",
350
380
  "timeout": 30
351
381
  }
352
382
  ]
353
383
  },
354
384
  {
355
- "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
385
+ "matcher": "AskUserQuestion|mcp__.*",
356
386
  "hooks": [
357
387
  {
358
388
  "type": "command",
359
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/no-lint-suppression/index.mjs\"",
389
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/autonomous-mode/index.mjs\"",
360
390
  "timeout": 30
361
391
  }
362
392
  ]
363
393
  }
364
394
  ],
365
- "UserPromptSubmit": [
395
+ "PostToolUse": [
366
396
  {
397
+ "matcher": "WebSearch|WebFetch|mcp__.*",
367
398
  "hooks": [
368
399
  {
369
400
  "type": "command",
370
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/capability-map/index.mjs\"",
371
- "timeout": 10
401
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/engram-first/track.mjs\"",
402
+ "timeout": 30
403
+ }
404
+ ]
405
+ },
406
+ {
407
+ "matcher": "WebSearch|WebFetch|mcp__.*",
408
+ "hooks": [
409
+ {
410
+ "type": "command",
411
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/library-docs/track.mjs\"",
412
+ "timeout": 30
372
413
  }
373
414
  ]
374
415
  }
@@ -378,7 +419,7 @@
378
419
  "hooks": [
379
420
  {
380
421
  "type": "command",
381
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/stop-pending/index.mjs\"",
422
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/engram-first/stop.mjs\"",
382
423
  "timeout": 30
383
424
  }
384
425
  ]
@@ -391,9 +432,27 @@
391
432
  "timeout": 30
392
433
  }
393
434
  ]
435
+ },
436
+ {
437
+ "hooks": [
438
+ {
439
+ "type": "command",
440
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/stop-pending/index.mjs\"",
441
+ "timeout": 30
442
+ }
443
+ ]
394
444
  }
395
445
  ],
396
446
  "SessionStart": [
447
+ {
448
+ "hooks": [
449
+ {
450
+ "type": "command",
451
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/engram-first/session-start.mjs\"",
452
+ "timeout": 30
453
+ }
454
+ ]
455
+ },
397
456
  {
398
457
  "hooks": [
399
458
  {
@@ -421,6 +480,17 @@
421
480
  }
422
481
  ]
423
482
  }
483
+ ],
484
+ "UserPromptSubmit": [
485
+ {
486
+ "hooks": [
487
+ {
488
+ "type": "command",
489
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/capability-map/index.mjs\"",
490
+ "timeout": 10
491
+ }
492
+ ]
493
+ }
424
494
  ]
425
495
  }
426
496
  }
@@ -33,6 +33,9 @@ const CONFIG_FILE = 'config.json';
33
33
  // read. Anchoring on `.git` alone silently dropped the project layer in such repos.
34
34
  const PROJECT_ROOT_MARKERS = ['.git', PROJECT_STATE_DIRECTORY];
35
35
 
36
+ export const PROJECT_ROOT_MARKERS_LIST = PROJECT_ROOT_MARKERS;
37
+ export { PROJECT_STATE_DIRECTORY, CONFIG_FILE };
38
+
36
39
  const BOM_CODE_POINT = 0xfeff;
37
40
 
38
41
  // Strip a leading UTF-8 BOM (U+FEFF) before parsing. `readFileSync(path,'utf8')` does NOT
@@ -43,11 +46,11 @@ const BOM_CODE_POINT = 0xfeff;
43
46
  // symmetric case) means a project can't be trusted to have been read at all. Removing the
44
47
  // BOM makes the common Windows round-trip parse correctly. Compared by char code (not a regex
45
48
  // literal) so the BOM never appears as literal irregular whitespace in the source.
46
- function stripBom(text) {
49
+ export function stripBom(text) {
47
50
  return text.charCodeAt(0) === BOM_CODE_POINT ? text.slice(1) : text;
48
51
  }
49
52
 
50
- function readJsonOrNull(path) {
53
+ export function readJsonOrNull(path) {
51
54
  if (!existsSync(path)) return null;
52
55
  try {
53
56
  return JSON.parse(stripBom(readFileSync(path, 'utf8')));
@@ -59,7 +62,7 @@ function readJsonOrNull(path) {
59
62
  }
60
63
 
61
64
  /** Climbs to the nearest project root (a dir holding `.git` or `.ai/`); null when none. */
62
- function projectRootOf(startDirectory) {
65
+ export function projectRootOf(startDirectory) {
63
66
  let current = startDirectory;
64
67
  while (true) {
65
68
  if (