memtrace 0.6.0 → 0.6.11

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 (30) hide show
  1. package/hooks/userprompt-claude.sh +12 -6
  2. package/install.js +0 -19
  3. package/installer/dist/commands/rail-install.d.ts +7 -0
  4. package/installer/dist/commands/rail-install.js +37 -0
  5. package/installer/dist/index.js +20 -1
  6. package/installer/dist/rail-install.d.ts +20 -0
  7. package/installer/dist/rail-install.js +183 -0
  8. package/installer/dist/transformers/claude.d.ts +21 -0
  9. package/installer/dist/transformers/claude.js +118 -3
  10. package/installer/dist/transformers/codex.js +17 -0
  11. package/installer/dist/transformers/cursor.js +23 -1
  12. package/installer/dist/transformers/gemini.d.ts +3 -0
  13. package/installer/dist/transformers/gemini.js +78 -0
  14. package/installer/dist/transformers/index.d.ts +2 -1
  15. package/installer/dist/transformers/index.js +3 -1
  16. package/installer/dist/transformers/opencode.js +31 -0
  17. package/installer/dist/transformers/rail-hooks.d.ts +56 -0
  18. package/installer/dist/transformers/rail-hooks.js +303 -0
  19. package/installer/dist/transformers/shared.js +5 -6
  20. package/installer/dist/transformers/types.d.ts +1 -1
  21. package/installer/skills/commands/memtrace-fleet-publish-intent.md +51 -0
  22. package/installer/skills/commands/memtrace-fleet-record-episode.md +48 -0
  23. package/installer/skills/commands/memtrace-fleet-resolve.md +59 -0
  24. package/installer/skills/workflows/memtrace-code-review.md +6 -0
  25. package/installer/skills/workflows/memtrace-first.md +2 -0
  26. package/installer/skills/workflows/memtrace-fleet-coordination.md +87 -0
  27. package/installer/skills/workflows/memtrace-fleet-first.md +132 -0
  28. package/installer/skills/workflows/memtrace-style-fingerprint.md +111 -0
  29. package/lib/claude-integration.js +6 -0
  30. package/package.json +6 -6
@@ -0,0 +1,78 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import { registerMcpServersJsonAt, removeMcpServersJsonAt, removeMemtraceSkills, writeSkills, } from './shared.js';
4
+ import { registerSettingsHook, removeSettingsHook, GEMINI_MATCHER, } from './rail-hooks.js';
5
+ /**
6
+ * Gemini CLI transformer.
7
+ *
8
+ * Gemini CLI (v0.26+) supports:
9
+ * - MCP servers in `.gemini/settings.json` under `mcpServers` — the SAME
10
+ * shape as the generic JSON registration (`command`/`args`/`env`).
11
+ * - Lifecycle hooks under `hooks.BeforeTool` (matcher against the tool name,
12
+ * e.g. `run_shell_command`) — where Memtrace Rail wires the discovery hook.
13
+ *
14
+ * Both live in the one `settings.json`; we register MCP first, then the Rail
15
+ * hook (each is a read-modify-write that preserves the other's keys).
16
+ */
17
+ function geminiBase(ctx) {
18
+ return ctx.scope === 'global' ? path.join(os.homedir(), '.gemini') : path.join(ctx.cwd, '.gemini');
19
+ }
20
+ function skillsRoot(ctx) {
21
+ return path.join(geminiBase(ctx), 'skills');
22
+ }
23
+ export function geminiSettingsPath(ctx) {
24
+ return path.join(geminiBase(ctx), 'settings.json');
25
+ }
26
+ export const geminiTransformer = {
27
+ name: 'gemini',
28
+ async install(skills, ctx) {
29
+ const warnings = [];
30
+ const rootDir = skillsRoot(ctx);
31
+ // Skills as Markdown context files (Gemini reads MCP-server instructions
32
+ // for tool guidance; the skill files document the Memtrace-first workflow).
33
+ const skillsWritten = writeSkills(skills, rootDir);
34
+ const settingsPath = geminiSettingsPath(ctx);
35
+ let mcpRegistered = false;
36
+ if (!ctx.skipMcp) {
37
+ const result = registerMcpServersJsonAt(settingsPath, ctx.memtraceBinary);
38
+ mcpRegistered = result.registered;
39
+ if (!mcpRegistered && result.backupPath) {
40
+ warnings.push(`Gemini settings.json was malformed; backed up to ${result.backupPath}.`);
41
+ }
42
+ }
43
+ // ─── memtrace-rail ─── Wire the BeforeTool discovery hook (default
44
+ // observe). Matches Gemini's shell/read/glob/grep tool names. Gemini gates
45
+ // its hook system behind `enableHooks` + `enableMessageBusIntegration`
46
+ // (PR #14460) — without them a registered BeforeTool hook NEVER fires, so we
47
+ // enable both here (only if the user hasn't already set them).
48
+ try {
49
+ registerSettingsHook(settingsPath, ctx.memtraceBinary, 'gemini', 'BeforeTool', GEMINI_MATCHER, {
50
+ enableHooks: true,
51
+ enableMessageBusIntegration: true,
52
+ });
53
+ }
54
+ catch (e) {
55
+ warnings.push(`failed to register Gemini Rail hook: ${e.message}`);
56
+ }
57
+ return {
58
+ agent: 'gemini',
59
+ skillsWritten,
60
+ skillsDir: rootDir,
61
+ mcpConfigPath: settingsPath,
62
+ mcpRegistered,
63
+ warnings,
64
+ };
65
+ },
66
+ async uninstall(ctx) {
67
+ removeMemtraceSkills(skillsRoot(ctx));
68
+ const settingsPath = geminiSettingsPath(ctx);
69
+ try {
70
+ removeSettingsHook(settingsPath, 'BeforeTool');
71
+ }
72
+ catch { /* best effort */ }
73
+ try {
74
+ removeMcpServersJsonAt(settingsPath);
75
+ }
76
+ catch { /* best effort */ }
77
+ },
78
+ };
@@ -2,6 +2,7 @@ import { Transformer } from './types.js';
2
2
  import { claudeTransformer } from './claude.js';
3
3
  import { cursorTransformer } from './cursor.js';
4
4
  import { codexTransformer } from './codex.js';
5
+ import { geminiTransformer } from './gemini.js';
5
6
  import { windsurfTransformer } from './windsurf.js';
6
7
  import { vscodeTransformer } from './vscode.js';
7
8
  import { hermesTransformer } from './hermes.js';
@@ -9,5 +10,5 @@ import { opencodeTransformer } from './opencode.js';
9
10
  import { kiroTransformer } from './kiro.js';
10
11
  export declare const ALL_TRANSFORMERS: Transformer[];
11
12
  export declare function findTransformer(name: string): Transformer | undefined;
12
- export { claudeTransformer, cursorTransformer, codexTransformer, windsurfTransformer, vscodeTransformer, hermesTransformer, opencodeTransformer, kiroTransformer, };
13
+ export { claudeTransformer, cursorTransformer, codexTransformer, geminiTransformer, windsurfTransformer, vscodeTransformer, hermesTransformer, opencodeTransformer, kiroTransformer, };
13
14
  export type { Transformer, InstallContext, InstallResult, TransformResult } from './types.js';
@@ -1,6 +1,7 @@
1
1
  import { claudeTransformer } from './claude.js';
2
2
  import { cursorTransformer } from './cursor.js';
3
3
  import { codexTransformer } from './codex.js';
4
+ import { geminiTransformer } from './gemini.js';
4
5
  import { windsurfTransformer } from './windsurf.js';
5
6
  import { vscodeTransformer } from './vscode.js';
6
7
  import { hermesTransformer } from './hermes.js';
@@ -10,6 +11,7 @@ export const ALL_TRANSFORMERS = [
10
11
  claudeTransformer,
11
12
  cursorTransformer,
12
13
  codexTransformer,
14
+ geminiTransformer,
13
15
  windsurfTransformer,
14
16
  vscodeTransformer,
15
17
  hermesTransformer,
@@ -19,4 +21,4 @@ export const ALL_TRANSFORMERS = [
19
21
  export function findTransformer(name) {
20
22
  return ALL_TRANSFORMERS.find(t => t.name === name);
21
23
  }
22
- export { claudeTransformer, cursorTransformer, codexTransformer, windsurfTransformer, vscodeTransformer, hermesTransformer, opencodeTransformer, kiroTransformer, };
24
+ export { claudeTransformer, cursorTransformer, codexTransformer, geminiTransformer, windsurfTransformer, vscodeTransformer, hermesTransformer, opencodeTransformer, kiroTransformer, };
@@ -1,6 +1,8 @@
1
+ import fs from 'fs';
1
2
  import os from 'os';
2
3
  import path from 'path';
3
4
  import { registerOpenCodeMcpAt, removeMemtraceSkills, removeOpenCodeMcpAt, writeSkills, } from './shared.js';
5
+ import { openCodePluginSource } from './rail-hooks.js';
4
6
  function opencodeConfigDir() {
5
7
  const xdg = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), '.config');
6
8
  return path.join(xdg, 'opencode');
@@ -15,6 +17,17 @@ function mcpPath(ctx) {
15
17
  ? path.join(opencodeConfigDir(), 'opencode.json')
16
18
  : path.join(ctx.cwd, 'opencode.json');
17
19
  }
20
+ // OpenCode's plugin directory name changed across versions: current official
21
+ // docs use `plugins/` (plural) while older/community builds load `plugin/`
22
+ // (singular). We write the plugin to BOTH so it loads regardless of version;
23
+ // uninstall removes both. (Identical content — whichever the runtime reads.)
24
+ function railPluginPaths(ctx) {
25
+ const base = ctx.scope === 'global' ? opencodeConfigDir() : path.join(ctx.cwd, '.opencode');
26
+ return [
27
+ path.join(base, 'plugin', 'memtrace-rail.js'),
28
+ path.join(base, 'plugins', 'memtrace-rail.js'),
29
+ ];
30
+ }
18
31
  export const opencodeTransformer = {
19
32
  name: 'opencode',
20
33
  async install(skills, ctx) {
@@ -30,6 +43,18 @@ export const opencodeTransformer = {
30
43
  warnings.push(`OpenCode config was malformed; backed up to ${result.backupPath}.`);
31
44
  }
32
45
  }
46
+ // ─── memtrace-rail ─── Write the in-process discovery plugin (OpenCode
47
+ // has no stdio hook). Defaults to observe; fail-open. Best effort.
48
+ try {
49
+ const src = openCodePluginSource(ctx.memtraceBinary);
50
+ for (const pluginPath of railPluginPaths(ctx)) {
51
+ fs.mkdirSync(path.dirname(pluginPath), { recursive: true });
52
+ fs.writeFileSync(pluginPath, src);
53
+ }
54
+ }
55
+ catch (e) {
56
+ warnings.push(`failed to write OpenCode Rail plugin: ${e.message}`);
57
+ }
33
58
  return {
34
59
  agent: 'opencode',
35
60
  skillsWritten: count,
@@ -42,5 +67,11 @@ export const opencodeTransformer = {
42
67
  async uninstall(ctx) {
43
68
  removeMemtraceSkills(skillsRoot(ctx));
44
69
  removeOpenCodeMcpAt(mcpPath(ctx));
70
+ for (const p of railPluginPaths(ctx)) {
71
+ try {
72
+ fs.rmSync(p, { force: true });
73
+ }
74
+ catch { /* best effort */ }
75
+ }
45
76
  },
46
77
  };
@@ -0,0 +1,56 @@
1
+ /** Substring identifying a memtrace-owned Rail hook entry. */
2
+ export declare const RAIL_HOOK_MARKER = "route --hook";
3
+ /** The discovery tools each host surfaces. */
4
+ export declare const CLAUDE_MATCHER = "Grep|Glob|Bash";
5
+ export declare const CODEX_MATCHER = "Bash";
6
+ export declare const GEMINI_MATCHER = "run_shell_command|read_file|read_many_files|glob|search_file_content";
7
+ export declare function railCommand(binary: string, host: string): string;
8
+ export declare function isRailHookEntry(entry: unknown): boolean;
9
+ export interface HookResult {
10
+ registered: boolean;
11
+ backupPath?: string;
12
+ }
13
+ export interface CleanupResult {
14
+ changed: boolean;
15
+ backupPath?: string;
16
+ }
17
+ /**
18
+ * Register a Claude-style settings.json hook (Codex `PreToolUse`, Gemini
19
+ * `BeforeTool`). Idempotent; preserves unrelated hooks; never overwrites a
20
+ * malformed file.
21
+ */
22
+ export declare function registerSettingsHook(settingsPath: string, binary: string, host: string, eventName: string, matcher: string, extraSettings?: Record<string, unknown>): HookResult;
23
+ /** Remove a Rail hook from a Claude-style settings.json event array. */
24
+ export declare function removeSettingsHook(settingsPath: string, eventName: string): CleanupResult;
25
+ /**
26
+ * Register the Cursor Rail hook in `.cursor/hooks.json`. Cursor uses a distinct
27
+ * schema: `{version, hooks: {beforeShellExecution: [{command}]}}`. We intercept
28
+ * both shell execution (where rg/grep/find run) and file reads.
29
+ */
30
+ export declare function registerCursorRailHook(hooksPath: string, binary: string): HookResult;
31
+ /** Remove the Cursor Rail hook entries. */
32
+ /** Windsurf Cascade hooks path (user-level). */
33
+ export declare function windsurfHooksPath(): string;
34
+ /**
35
+ * Register Windsurf `pre_run_command` Rail hook. Uses exit codes (not JSON stdout);
36
+ * `show_output: true` so nudge context on stdout is visible in Cascade.
37
+ */
38
+ export declare function registerWindsurfRailHook(hooksPath: string, binary: string): HookResult;
39
+ export declare function removeWindsurfRailHook(hooksPath: string): CleanupResult;
40
+ /** VS Code / Copilot user hooks file (global). */
41
+ export declare function vscodeCopilotRailHookPath(): string;
42
+ /**
43
+ * Register VS Code agent `PreToolUse` hook (Claude-compatible JSON on stdio).
44
+ * Writes a dedicated file under `~/.copilot/hooks/` (loaded by default in recent VS Code).
45
+ */
46
+ export declare function registerVsCodeRailHook(hookFilePath: string, binary: string): HookResult;
47
+ export declare function removeVsCodeRailHook(hookFilePath: string): CleanupResult;
48
+ export declare function removeCursorRailHook(hooksPath: string): CleanupResult;
49
+ /**
50
+ * The OpenCode in-process plugin source. OpenCode has no stdio hook — plugins
51
+ * are TS/JS modules with a `tool.execute.before` async hook. This plugin shells
52
+ * out to the SAME router via `--host generic` (a ToolAttempt in, a
53
+ * DecisionEnvelope out) and, on suppression, throws to block the raw tool with
54
+ * the Memtrace result as the error the agent sees.
55
+ */
56
+ export declare function openCodePluginSource(binary: string): string;
@@ -0,0 +1,303 @@
1
+ /**
2
+ * memtrace-rail — per-host installer wiring for the Memtrace-first discovery
3
+ * hook. Each supported agent intercepts tool calls differently, so each gets a
4
+ * thin host-specific registration that points at the SAME router
5
+ * (`memtrace route --hook --host <h>`). All entries are idempotent and remove
6
+ * cleanly. Hooks default to observe mode (zero behavior change); the user opts
7
+ * into more via `MEMTRACE_RAIL=nudge|rail|strict`.
8
+ *
9
+ * Host families:
10
+ * - Claude Code → `.claude/settings.json` hooks.PreToolUse (in claude.ts)
11
+ * - Codex → `.codex/hooks.json` hooks.PreToolUse
12
+ * - Gemini CLI → `.gemini/settings.json` hooks.BeforeTool
13
+ * - Cursor → `.cursor/hooks.json` beforeShellExecution/beforeReadFile
14
+ * - OpenCode → `.opencode/plugin/memtrace-rail.js` (in-process plugin)
15
+ * - Windsurf → `~/.codeium/windsurf/hooks.json` pre_run_command (exit-code contract)
16
+ * - VS Code → `~/.copilot/hooks/memtrace-rail.json` PreToolUse (Claude-shaped)
17
+ */
18
+ import fs from 'fs';
19
+ import os from 'os';
20
+ import path from 'path';
21
+ import { safeReadJson, writeJsonAtomic } from '../fs-safe.js';
22
+ /** Substring identifying a memtrace-owned Rail hook entry. */
23
+ export const RAIL_HOOK_MARKER = 'route --hook';
24
+ /** The discovery tools each host surfaces. */
25
+ export const CLAUDE_MATCHER = 'Grep|Glob|Bash';
26
+ export const CODEX_MATCHER = 'Bash';
27
+ export const GEMINI_MATCHER = 'run_shell_command|read_file|read_many_files|glob|search_file_content';
28
+ export function railCommand(binary, host) {
29
+ return `${binary} route --hook --host ${host}`;
30
+ }
31
+ function isRecord(v) {
32
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
33
+ }
34
+ export function isRailHookEntry(entry) {
35
+ if (!isRecord(entry) || !Array.isArray(entry.hooks))
36
+ return false;
37
+ return entry.hooks.some((h) => isRecord(h) && typeof h.command === 'string' && h.command.includes(RAIL_HOOK_MARKER));
38
+ }
39
+ /**
40
+ * Register a Claude-style settings.json hook (Codex `PreToolUse`, Gemini
41
+ * `BeforeTool`). Idempotent; preserves unrelated hooks; never overwrites a
42
+ * malformed file.
43
+ */
44
+ export function registerSettingsHook(settingsPath, binary, host, eventName, matcher, extraSettings) {
45
+ const { value, corrupted, backupPath } = safeReadJson(settingsPath);
46
+ if (corrupted) {
47
+ console.warn(`memtrace: ${settingsPath} is malformed; backed up to ${backupPath}. Skipped Rail hook.`);
48
+ return { registered: false, backupPath };
49
+ }
50
+ const settings = (value ?? {});
51
+ // Some hosts gate the hook system behind top-level feature flags (e.g. Gemini
52
+ // requires `enableHooks` + `enableMessageBusIntegration` or BeforeTool hooks
53
+ // never fire). Merge them in without clobbering a value the user already set.
54
+ if (extraSettings) {
55
+ for (const [k, v] of Object.entries(extraSettings)) {
56
+ if (!(k in settings))
57
+ settings[k] = v;
58
+ }
59
+ }
60
+ settings.hooks = settings.hooks ?? {};
61
+ const existing = Array.isArray(settings.hooks[eventName])
62
+ ? settings.hooks[eventName]
63
+ : [];
64
+ const preserved = existing.filter((e) => !isRailHookEntry(e));
65
+ preserved.push({
66
+ matcher,
67
+ hooks: [{ type: 'command', command: railCommand(binary, host) }],
68
+ });
69
+ settings.hooks[eventName] = preserved;
70
+ writeJsonAtomic(settingsPath, settings);
71
+ return { registered: true };
72
+ }
73
+ /** Remove a Rail hook from a Claude-style settings.json event array. */
74
+ export function removeSettingsHook(settingsPath, eventName) {
75
+ const { value, corrupted, backupPath } = safeReadJson(settingsPath);
76
+ if (corrupted)
77
+ return { changed: false, backupPath };
78
+ if (!value)
79
+ return { changed: false };
80
+ const settings = value;
81
+ if (!isRecord(settings.hooks) || !Array.isArray(settings.hooks[eventName])) {
82
+ return { changed: false };
83
+ }
84
+ const before = settings.hooks[eventName];
85
+ const after = before.filter((e) => !isRailHookEntry(e));
86
+ if (after.length === before.length)
87
+ return { changed: false };
88
+ if (after.length === 0)
89
+ delete settings.hooks[eventName];
90
+ else
91
+ settings.hooks[eventName] = after;
92
+ if (Object.keys(settings.hooks).length === 0)
93
+ delete settings.hooks;
94
+ writeJsonAtomic(settingsPath, settings);
95
+ return { changed: true };
96
+ }
97
+ /**
98
+ * Register the Cursor Rail hook in `.cursor/hooks.json`. Cursor uses a distinct
99
+ * schema: `{version, hooks: {beforeShellExecution: [{command}]}}`. We intercept
100
+ * both shell execution (where rg/grep/find run) and file reads.
101
+ */
102
+ export function registerCursorRailHook(hooksPath, binary) {
103
+ const { value, corrupted, backupPath } = safeReadJson(hooksPath);
104
+ if (corrupted) {
105
+ console.warn(`memtrace: ${hooksPath} is malformed; backed up to ${backupPath}. Skipped Cursor Rail hook.`);
106
+ return { registered: false, backupPath };
107
+ }
108
+ const cfg = (value ?? {});
109
+ cfg.version = cfg.version ?? 1;
110
+ cfg.hooks = cfg.hooks ?? {};
111
+ const cmd = railCommand(binary, 'cursor');
112
+ // Only `beforeShellExecution` — that's where rg/grep/find run. We do NOT wire
113
+ // `beforeReadFile`: a file Read always classifies non-discovery (→ allow), so
114
+ // hooking it would spawn a router subprocess on every file read for no benefit.
115
+ const event = 'beforeShellExecution';
116
+ const existing = Array.isArray(cfg.hooks[event]) ? cfg.hooks[event] : [];
117
+ const preserved = existing.filter((e) => !(isRecord(e) && typeof e.command === 'string' && e.command.includes(RAIL_HOOK_MARKER)));
118
+ preserved.push({ command: cmd });
119
+ cfg.hooks[event] = preserved;
120
+ writeJsonAtomic(hooksPath, cfg);
121
+ return { registered: true };
122
+ }
123
+ /** Remove the Cursor Rail hook entries. */
124
+ /** Windsurf Cascade hooks path (user-level). */
125
+ export function windsurfHooksPath() {
126
+ return path.join(os.homedir(), '.codeium', 'windsurf', 'hooks.json');
127
+ }
128
+ /**
129
+ * Register Windsurf `pre_run_command` Rail hook. Uses exit codes (not JSON stdout);
130
+ * `show_output: true` so nudge context on stdout is visible in Cascade.
131
+ */
132
+ export function registerWindsurfRailHook(hooksPath, binary) {
133
+ const { value, corrupted, backupPath } = safeReadJson(hooksPath);
134
+ if (corrupted) {
135
+ console.warn(`memtrace: ${hooksPath} is malformed; backed up to ${backupPath}. Skipped Windsurf Rail hook.`);
136
+ return { registered: false, backupPath };
137
+ }
138
+ const cfg = (value ?? {});
139
+ cfg.hooks = cfg.hooks ?? {};
140
+ const event = 'pre_run_command';
141
+ const cmd = railCommand(binary, 'windsurf');
142
+ const existing = Array.isArray(cfg.hooks[event]) ? cfg.hooks[event] : [];
143
+ const preserved = existing.filter((e) => !(isRecord(e) &&
144
+ typeof e.command === 'string' &&
145
+ e.command.includes(RAIL_HOOK_MARKER)));
146
+ preserved.push({ command: cmd, show_output: true });
147
+ cfg.hooks[event] = preserved;
148
+ writeJsonAtomic(hooksPath, cfg);
149
+ return { registered: true };
150
+ }
151
+ export function removeWindsurfRailHook(hooksPath) {
152
+ const { value, corrupted, backupPath } = safeReadJson(hooksPath);
153
+ if (corrupted)
154
+ return { changed: false, backupPath };
155
+ if (!value)
156
+ return { changed: false };
157
+ const cfg = value;
158
+ if (!isRecord(cfg.hooks) || !Array.isArray(cfg.hooks.pre_run_command)) {
159
+ return { changed: false };
160
+ }
161
+ const before = cfg.hooks.pre_run_command;
162
+ const after = before.filter((e) => !(isRecord(e) &&
163
+ typeof e.command === 'string' &&
164
+ e.command.includes(RAIL_HOOK_MARKER)));
165
+ if (after.length === before.length)
166
+ return { changed: false };
167
+ if (after.length === 0)
168
+ delete cfg.hooks.pre_run_command;
169
+ else
170
+ cfg.hooks.pre_run_command = after;
171
+ if (Object.keys(cfg.hooks).length === 0)
172
+ delete cfg.hooks;
173
+ writeJsonAtomic(hooksPath, cfg);
174
+ return { changed: true };
175
+ }
176
+ /** VS Code / Copilot user hooks file (global). */
177
+ export function vscodeCopilotRailHookPath() {
178
+ return path.join(os.homedir(), '.copilot', 'hooks', 'memtrace-rail.json');
179
+ }
180
+ /**
181
+ * Register VS Code agent `PreToolUse` hook (Claude-compatible JSON on stdio).
182
+ * Writes a dedicated file under `~/.copilot/hooks/` (loaded by default in recent VS Code).
183
+ */
184
+ export function registerVsCodeRailHook(hookFilePath, binary) {
185
+ const { value, corrupted, backupPath } = safeReadJson(hookFilePath);
186
+ if (corrupted) {
187
+ console.warn(`memtrace: ${hookFilePath} is malformed; backed up to ${backupPath}. Skipped VS Code Rail hook.`);
188
+ return { registered: false, backupPath };
189
+ }
190
+ const cmd = railCommand(binary, 'vscode');
191
+ const existing = (value ?? {});
192
+ const pre = Array.isArray(existing.hooks?.PreToolUse) ? existing.hooks.PreToolUse : [];
193
+ const preserved = pre.filter((e) => !(isRecord(e) &&
194
+ typeof e.command === 'string' &&
195
+ e.command.includes(RAIL_HOOK_MARKER)));
196
+ preserved.push({ type: 'command', command: cmd, timeout: 30 });
197
+ writeJsonAtomic(hookFilePath, {
198
+ hooks: {
199
+ PreToolUse: preserved,
200
+ },
201
+ });
202
+ return { registered: true };
203
+ }
204
+ export function removeVsCodeRailHook(hookFilePath) {
205
+ const { value, corrupted, backupPath } = safeReadJson(hookFilePath);
206
+ if (corrupted)
207
+ return { changed: false, backupPath };
208
+ if (!value)
209
+ return { changed: false };
210
+ const cfg = value;
211
+ if (!Array.isArray(cfg.hooks?.PreToolUse))
212
+ return { changed: false };
213
+ const before = cfg.hooks.PreToolUse;
214
+ const after = before.filter((e) => !(isRecord(e) &&
215
+ typeof e.command === 'string' &&
216
+ e.command.includes(RAIL_HOOK_MARKER)));
217
+ if (after.length === before.length)
218
+ return { changed: false };
219
+ if (after.length === 0) {
220
+ try {
221
+ fs.rmSync(hookFilePath, { force: true });
222
+ }
223
+ catch {
224
+ writeJsonAtomic(hookFilePath, { hooks: {} });
225
+ }
226
+ return { changed: true };
227
+ }
228
+ writeJsonAtomic(hookFilePath, { hooks: { PreToolUse: after } });
229
+ return { changed: true };
230
+ }
231
+ export function removeCursorRailHook(hooksPath) {
232
+ const { value, corrupted, backupPath } = safeReadJson(hooksPath);
233
+ if (corrupted)
234
+ return { changed: false, backupPath };
235
+ if (!value)
236
+ return { changed: false };
237
+ const cfg = value;
238
+ if (!isRecord(cfg.hooks))
239
+ return { changed: false };
240
+ let changed = false;
241
+ for (const event of ['beforeShellExecution', 'beforeReadFile']) {
242
+ if (!Array.isArray(cfg.hooks[event]))
243
+ continue;
244
+ const before = cfg.hooks[event];
245
+ const after = before.filter((e) => !(isRecord(e) && typeof e.command === 'string' && e.command.includes(RAIL_HOOK_MARKER)));
246
+ if (after.length !== before.length) {
247
+ changed = true;
248
+ if (after.length === 0)
249
+ delete cfg.hooks[event];
250
+ else
251
+ cfg.hooks[event] = after;
252
+ }
253
+ }
254
+ if (changed) {
255
+ if (isRecord(cfg.hooks) && Object.keys(cfg.hooks).length === 0)
256
+ delete cfg.hooks;
257
+ writeJsonAtomic(hooksPath, cfg);
258
+ }
259
+ return { changed };
260
+ }
261
+ /**
262
+ * The OpenCode in-process plugin source. OpenCode has no stdio hook — plugins
263
+ * are TS/JS modules with a `tool.execute.before` async hook. This plugin shells
264
+ * out to the SAME router via `--host generic` (a ToolAttempt in, a
265
+ * DecisionEnvelope out) and, on suppression, throws to block the raw tool with
266
+ * the Memtrace result as the error the agent sees.
267
+ */
268
+ export function openCodePluginSource(binary) {
269
+ return `// AUTO-GENERATED by memtrace install — Memtrace Rail discovery hook.
270
+ // Routes OpenCode tool calls through \`${binary} route\`. Defaults to observe
271
+ // (no-op) unless MEMTRACE_RAIL=nudge|rail|strict. Remove via 'memtrace uninstall'.
272
+ import { execFileSync } from 'node:child_process';
273
+
274
+ const BIN = ${JSON.stringify(binary)};
275
+
276
+ export const MemtraceRail = async ({ project, client, $ }) => ({
277
+ 'tool.execute.before': async (input, output) => {
278
+ try {
279
+ const tool = input?.tool;
280
+ if (tool !== 'bash' && tool !== 'grep' && tool !== 'glob') return;
281
+ const attempt = {
282
+ host: 'open-code',
283
+ cwd: output?.args?.cwd || process.cwd(),
284
+ tool: tool === 'bash' ? 'Bash' : tool === 'grep' ? 'Grep' : 'Glob',
285
+ input: output?.args || {},
286
+ session_id: input?.sessionID || 'opencode',
287
+ };
288
+ const out = execFileSync(BIN, ['route', '--json', '-'], {
289
+ input: JSON.stringify(attempt), timeout: 1500, encoding: 'utf8',
290
+ });
291
+ const decision = JSON.parse(out);
292
+ if (decision.action === 'suppress_raw_with_result') {
293
+ throw new Error('[Memtrace Rail] ' + (decision.message || 'Use Memtrace for code discovery.') +
294
+ (decision.result ? '\\n' + JSON.stringify(decision.result, null, 2) : ''));
295
+ }
296
+ } catch (e) {
297
+ // Fail open on any router/exec error — never block the agent.
298
+ if (e && String(e.message || '').startsWith('[Memtrace Rail]')) throw e;
299
+ }
300
+ },
301
+ });
302
+ `;
303
+ }
@@ -2,12 +2,11 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { safeReadJson, writeJsonAtomic } from '../fs-safe.js';
4
4
  export const MCP_SERVER_NAME = 'memtrace';
5
- // Phase 3 (ArcadeDB removal): the binary uses an embedded MemDB by default and
6
- // reads MEMTRACE_MEMDB_MODE / workspace anchors directly. No env vars are
7
- // required at MCP-registration time. Preserved as an empty constant so any
8
- // future env-var plumbing (e.g. RUST_LOG, MEMTRACE_DATA_DIR overrides) has
9
- // one place to land. The corresponding upgrade migration in install.js strips
10
- // any legacy MEMTRACE_ARCADEDB_* keys from existing MCP entries on upgrade.
5
+ // The binary reads its configuration (workspace anchors, mode) directly, so no
6
+ // env vars are required at MCP-registration time. Kept as an empty constant so
7
+ // any future env-var plumbing (e.g. RUST_LOG, MEMTRACE_DATA_DIR overrides) has
8
+ // one place to land. The upgrade migration in install.js also prunes stale env
9
+ // keys from pre-existing MCP entries.
11
10
  export const MEMTRACE_MCP_ENV = {};
12
11
  export function skillName(skill) {
13
12
  return skill.filename.replace(/\.md$/, '');
@@ -29,7 +29,7 @@ export interface InstallResult {
29
29
  mcpRegistered: boolean;
30
30
  warnings: string[];
31
31
  }
32
- export type AgentName = 'claude' | 'cursor' | 'codex' | 'windsurf' | 'vscode' | 'hermes' | 'opencode' | 'kiro';
32
+ export type AgentName = 'claude' | 'cursor' | 'codex' | 'gemini' | 'windsurf' | 'vscode' | 'hermes' | 'opencode' | 'kiro';
33
33
  /**
34
34
  * One transformer per supported AI coding agent.
35
35
  */
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: memtrace-fleet-publish-intent
3
+ description: "Use to declare a structural intent BEFORE editing in a fleet — what symbols you'll touch and why — so other agents on your branch coordinate around you. Triggered by: 'I'm about to edit/rename/refactor X', starting any non-trivial edit while other agents share your repo+branch. Returns the graph blast radius, overlapping live intents on your branch, and a shift-left coordination/partition hint. Do not start editing shared symbols without publishing first."
4
+ allowed-tools:
5
+ - mcp__memtrace__fleet_publish_intent
6
+ - mcp__memtrace__fleet_preflight
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ `fleet_publish_intent` is step 1 of the fleet protocol: announce what you're about
12
+ to touch so other agents on the **same `(repo, branch)`** coordinate around you.
13
+ It's a typed, ~20-token declaration — not prose.
14
+
15
+ ## Call it
16
+
17
+ ```jsonc
18
+ fleet_publish_intent({
19
+ repo_id: "myrepo",
20
+ branch: "session/auth-revamp", // your fleet's session branch
21
+ agent_id:"agent-a",
22
+ touched: ["auth::verify_token"], // qualified symbol identities
23
+ intent: {"refactor": {"pattern": "change_signature"}},
24
+ assignment: "widen verify_token signature for pagination" // the alignment anchor
25
+ })
26
+ ```
27
+
28
+ You get back:
29
+ - `impact_preview` — the real graph blast radius of the touched symbols.
30
+ - `active_conflicts` — overlapping live intents **on your branch** (none from other
31
+ branches; coordination is branch-scoped).
32
+ - `coordination` — a shift-left hint: `would_escalate`, a `suggested_partition`
33
+ (who should own a contested symbol), and advice to realign *before* you edit.
34
+
35
+ ## Intent kinds (JSON, snake_case, externally-tagged)
36
+
37
+ - `{"refactor":{"pattern":"rename_symbol"|"change_signature"|"move_symbol"|"extract_function"|…}}`
38
+ - `{"feature_add":{"surface":"new_symbol"|"new_endpoint"|…}}`
39
+ - `{"bug_fix":{"defect":"logic_error"|"null_handling"|…}}`
40
+ - `{"cleanup":{"kind":"dead_code"|"unused_import"|…}}`
41
+ - `{"performance":{"axis":"latency"|…}}`, `{"security_fix":{"severity":"high"}}`,
42
+ `{"test_add":{"covers":[…]}}`, `"docs_only"`, `"exploratory"`
43
+
44
+ **Destructive** kinds — `change_signature`, `move_symbol`, `cleanup/dead_code` —
45
+ are what raise a Class C decision when they overlap another agent's work.
46
+
47
+ ## Rules
48
+
49
+ - Always pass `branch` (your session branch) and `assignment` (your task).
50
+ - Read-only check first? Use `fleet_preflight` (same inputs, no registration).
51
+ - An empty `active_conflicts` is good — proceed and `fleet_record_episode` when done.
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: memtrace-fleet-record-episode
3
+ description: "Use AFTER an edit in a fleet to record it and get its conflict class (A/B/C) against agents on your branch. Triggered by: finishing an edit, 'I just changed X', completing a refactor step while other agents share your repo+branch. Returns conflict_class + replan_hint; a Class C returns an escalation_id and mediation_request that starts the decision loop. Do not finish a coordinated edit without recording it."
4
+ allowed-tools:
5
+ - mcp__memtrace__fleet_record_episode
6
+ - mcp__memtrace__fleet_get_escalation
7
+ - mcp__memtrace__fleet_query_episodes
8
+ ---
9
+
10
+ ## Overview
11
+
12
+ `fleet_record_episode` is step 3 of the fleet protocol: record the edit you just
13
+ made and learn whether it collided with another agent on your branch.
14
+
15
+ ## Call it
16
+
17
+ ```jsonc
18
+ fleet_record_episode({
19
+ repo_id: "myrepo",
20
+ branch: "session/auth-revamp",
21
+ agent_id:"agent-a",
22
+ touched: ["auth::verify_token"],
23
+ intent: {"refactor": {"pattern": "change_signature"}}
24
+ })
25
+ ```
26
+
27
+ ## The result: conflict_class
28
+
29
+ - **A → proceed.** Additive, order-independent.
30
+ - **B → re-read, then proceed.** Non-destructive overlap; re-read the shared
31
+ symbols so you build on current state.
32
+ - **C → a decision is needed.** A destructive change overlaps another agent's
33
+ work. The response includes:
34
+ - `escalation_id` — the decision's id.
35
+ - `mediation_request` — the judging task (every agent's `assignment` + the
36
+ contested symbols). If you're asked to judge, call `fleet_submit_verdict`.
37
+ - `next_action` — poll `fleet_get_escalation({escalation_id, agent_id})` until
38
+ `your_directive` ≠ `wait`, then `proceed` / `defer` / `review`.
39
+
40
+ Class is computed **only against agents on your branch**. Agents on other branches
41
+ never make your edit a Class C.
42
+
43
+ ## After recording
44
+
45
+ - Class A/B → continue.
46
+ - Class C → enter the decision loop (see `memtrace-fleet-coordination`). Don't keep
47
+ editing the contested symbols until your directive is `proceed`.
48
+ - Review history with `fleet_query_episodes({repo_id, node?, conflict_class?})`.