memorix 1.8.4 → 1.8.6

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 (88) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/CLAUDE.md +3 -0
  3. package/README.md +8 -6
  4. package/README.zh-CN.md +5 -3
  5. package/assets/star-history-dark.svg +5 -5
  6. package/assets/star-history-light.svg +5 -5
  7. package/assets/star-history.json +35 -5
  8. package/dist/cli/index.js +1848 -754
  9. package/dist/dashboard/static/app.js +29 -885
  10. package/dist/dashboard/static/index.html +1 -5
  11. package/dist/dashboard/static/style.css +36 -503
  12. package/dist/index.d.ts +4 -4
  13. package/dist/index.js +286 -79
  14. package/dist/memcode/index.js +28035 -169
  15. package/dist/memcode-runtime/CHANGELOG.md +52 -0
  16. package/dist/sdk.js +103 -21
  17. package/dist/types.d.ts +1 -1
  18. package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +22 -16
  19. package/docs/AGENT_OPERATOR_PLAYBOOK.md +2 -2
  20. package/docs/API_REFERENCE.md +10 -8
  21. package/docs/DASHBOARD.md +23 -0
  22. package/docs/DEVELOPMENT.md +2 -2
  23. package/docs/DOCKER.md +6 -3
  24. package/docs/INTEGRATIONS.md +2 -1
  25. package/docs/MODULES.md +46 -20
  26. package/docs/PERFORMANCE.md +1 -1
  27. package/docs/README.md +8 -7
  28. package/docs/SETUP.md +4 -3
  29. package/package.json +11 -11
  30. package/plugins/antigravity/memorix/plugin.json +1 -1
  31. package/plugins/antigravity/memorix/rules/memorix.md +5 -0
  32. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  33. package/plugins/claude/memorix/skills/memorix-memory/SKILL.md +3 -0
  34. package/plugins/claude/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
  35. package/plugins/claude/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
  36. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  37. package/plugins/codebuddy/memorix/skills/memorix-memory/SKILL.md +3 -0
  38. package/plugins/codebuddy/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
  39. package/plugins/codebuddy/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
  40. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  41. package/plugins/codex/memorix/skills/memorix-memory/SKILL.md +3 -0
  42. package/plugins/codex/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
  43. package/plugins/codex/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
  44. package/plugins/copilot/memorix/plugin.json +1 -1
  45. package/plugins/copilot/memorix/skills/memorix-memory/SKILL.md +3 -0
  46. package/plugins/copilot/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
  47. package/plugins/copilot/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
  48. package/plugins/cursor/memorix/skills/memorix-memory/SKILL.md +3 -0
  49. package/plugins/cursor/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
  50. package/plugins/cursor/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
  51. package/plugins/gemini/memorix/GEMINI.md +5 -0
  52. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  53. package/plugins/hermes/memorix/plugin.yaml +7 -1
  54. package/plugins/omp/memorix/package.json +1 -1
  55. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  56. package/plugins/pi/memorix/package.json +1 -1
  57. package/plugins/pi/memorix/skills/memorix-memory/SKILL.md +3 -0
  58. package/plugins/pi/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
  59. package/plugins/pi/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
  60. package/server.json +2 -2
  61. package/src/cli/command-guide.ts +52 -1
  62. package/src/cli/commands/agent-integrations.ts +1 -0
  63. package/src/cli/commands/background.ts +166 -6
  64. package/src/cli/commands/hooks-install.ts +9 -2
  65. package/src/cli/commands/hooks-preview.ts +5 -0
  66. package/src/cli/commands/integrate.ts +2 -0
  67. package/src/cli/commands/serve-http.ts +73 -80
  68. package/src/cli/commands/serve.ts +95 -93
  69. package/src/cli/commands/setup.ts +11 -1
  70. package/src/cli/commands/uninstall.ts +24 -2
  71. package/src/cli/index.ts +1 -1
  72. package/src/codegraph/task-lens.ts +72 -7
  73. package/src/dashboard/static/index.html +1 -5
  74. package/src/dashboard/static/style.css +36 -503
  75. package/src/hooks/installers/index.ts +58 -0
  76. package/src/hooks/official-skills.ts +13 -6
  77. package/src/hooks/types.ts +2 -0
  78. package/src/integrations/registry.ts +9 -0
  79. package/src/memory/graph.ts +2 -2
  80. package/src/server/mcp-diagnostics.ts +9 -7
  81. package/src/server/modern-mcp-bridge.ts +108 -0
  82. package/src/server/stdio-startup-gate.ts +41 -2
  83. package/src/server/tool-profile.ts +7 -6
  84. package/src/server.ts +1 -1
  85. package/src/store/orama-store.ts +2 -2
  86. package/src/types.ts +1 -1
  87. package/src/wiki/types.ts +2 -2
  88. package/src/workspace/mcp-adapters/workbuddy.ts +71 -0
@@ -1,5 +1,10 @@
1
1
  /**
2
- * memorix serve — Start MCP Server on stdio
2
+ * memorix serve - Start MCP Server on stdio transport.
3
+ *
4
+ * The official MCP v2 stdio entry owns modern discovery, per-request metadata,
5
+ * legacy compatibility, and protocol lifecycle. Memorix keeps its startup
6
+ * gate in front of that entry so early claim-less discovery remains compatible
7
+ * with clients shipped before the 2026 protocol revision.
3
8
  */
4
9
 
5
10
  import { defineCommand } from 'citty';
@@ -7,6 +12,7 @@ import { resolveToolProfile } from '../../server/tool-profile.js';
7
12
  import { getMcpServerInfo } from '../../server/mcp-discovery.js';
8
13
  import { StdioStartupGate } from '../../server/stdio-startup-gate.js';
9
14
  import { mcpFileUriToPath } from '../mcp-root-path.js';
15
+ import type { ModernMcpBridge } from '../../server/modern-mcp-bridge.js';
10
16
 
11
17
  export default defineCommand({
12
18
  meta: {
@@ -31,40 +37,42 @@ export default defineCommand({
31
37
  },
32
38
  },
33
39
  run: async ({ args }) => {
34
- // Take ownership of stdin before project/config/runtime initialization. The
35
- // gate answers handshake-free discovery and queues every other JSON-RPC line
36
- // until the SDK transport is connected, so cold starts cannot lose requests.
40
+ const { serveStdio, StdioServerTransport } = await import('@modelcontextprotocol/server/stdio');
41
+ const { createModernMcpBridge } = await import('../../server/modern-mcp-bridge.js');
42
+ const { detectProject, findGitInSubdirs, isSystemDirectory } = await import('../../project/detector.js');
43
+ const { homedir } = await import('node:os');
44
+ const { writeLastProjectRoot } = await import('../../project/launch-root.js');
45
+ const { resolveServeProject } = await import('./serve-shared.js');
46
+
47
+ // Take ownership of stdin before project/config/runtime initialization.
48
+ // The gate answers the old claim-less discover probe and queues every
49
+ // other line until the official v2 transport is listening.
50
+ let stdioHandle: { close: () => Promise<void> } | undefined;
51
+ let stdinEndedBeforeHandle = false;
37
52
  const startupGate = new StdioStartupGate({
38
53
  stdin: process.stdin,
39
54
  stdout: process.stdout,
40
55
  serverInfo: getMcpServerInfo(),
41
56
  onError: (error) => {
42
57
  console.error(`[memorix] stdio startup gate error: ${error.message}`);
43
- // The stream is no longer safe to replay after an input-limit or I/O
44
- // failure. Exit instead of leaving a half-connected MCP process alive.
45
58
  process.exit(1);
46
59
  },
47
60
  onEnd: () => {
48
- console.error('[memorix] stdin closed — exiting');
49
- // The gate has already ended the transport input when ready. Set the
50
- // exit code and let pending JSON-RPC stdout writes drain naturally.
51
- process.exitCode = 0;
61
+ console.error('[memorix] stdin closed - exiting');
62
+ if (stdioHandle) {
63
+ void stdioHandle.close().catch((error) => {
64
+ console.error(`[memorix] stdio close error: ${error instanceof Error ? error.message : String(error)}`);
65
+ });
66
+ } else {
67
+ stdinEndedBeforeHandle = true;
68
+ }
52
69
  },
53
70
  });
54
71
  startupGate.start();
55
72
 
56
- const { StdioServerTransport } = await import(
57
- '@modelcontextprotocol/sdk/server/stdio.js'
58
- );
59
- const { createMemorixServer } = await import('../../server.js');
60
- const { detectProject, findGitInSubdirs, isSystemDirectory } = await import('../../project/detector.js');
61
- const { homedir } = await import('node:os');
62
- const { resolveServeProject } = await import('./serve-shared.js');
63
-
64
- // Priority: explicit --cwd arg > MEMORIX_PROJECT_ROOT env > INIT_CWD (npm lifecycle) > process.cwd()
73
+ // Priority: explicit --cwd arg > MEMORIX_PROJECT_ROOT > INIT_CWD > process.cwd().
65
74
  let safeCwd: string;
66
75
  try { safeCwd = process.cwd(); } catch { safeCwd = homedir(); }
67
-
68
76
  const resolution = resolveServeProject(
69
77
  {
70
78
  cwdArg: args.cwd,
@@ -75,101 +83,95 @@ export default defineCommand({
75
83
  },
76
84
  { detectProject, findGitInSubdirs, isSystemDirectory },
77
85
  );
78
-
79
- for (const message of resolution.messages) {
80
- console.error(message);
81
- }
86
+ for (const message of resolution.messages) console.error(message);
82
87
 
83
88
  if (!resolution.detectedProject) {
84
89
  console.error(`[memorix] [WARN] ${resolution.error}`);
85
- console.error(`[memorix] Starting in deferred-binding mode — project will bind via MCP roots or memorix_session_start.`);
86
- console.error(`[memorix] For non-git directories, use --allow-untracked to enable untracked/ fallback.`);
87
- // Don't exit — allow deferred binding via session_start or MCP roots (fixes Cursor stdio #75)
90
+ console.error('[memorix] Starting in deferred-binding mode - bind with memorix_session_start(projectRoot=...) when needed.');
91
+ console.error('[memorix] For non-git directories, use --allow-untracked to enable untracked/ fallback.');
88
92
  }
89
93
 
90
94
  const detected = resolution.detectedProject;
91
95
  const projectRoot = resolution.projectRoot;
92
- if (detected) {
93
- const { writeLastProjectRoot } = await import('../../project/launch-root.js');
94
- writeLastProjectRoot(detected.rootPath);
95
- }
96
+ if (detected) writeLastProjectRoot(detected.rootPath);
96
97
 
97
- // Always register ALL tools BEFORE connecting transport.
98
- // This ensures tools/list returns the full tool set immediately on connect.
99
- // When no project detected, use deferred binding (allowUntrackedFallback=false, deferProjectInitUntilBound=true)
100
98
  const allowUntracked = args['allow-untracked'] ?? false;
101
- const toolProfile = resolveToolProfile({ explicit: args.mode, envValue: process.env.MEMORIX_MODE, fallback: 'micro' });
102
- const serverOptions = detected
103
- ? { toolProfile, deferProjectRuntimeInit: true }
104
- : { allowUntrackedFallback: allowUntracked, deferProjectInitUntilBound: !allowUntracked, deferProjectRuntimeInit: true, toolProfile };
105
- const { server, projectId, deferredInit, switchProject } = await createMemorixServer(projectRoot, undefined, undefined, serverOptions);
106
- const transport = new StdioServerTransport(startupGate.input, process.stdout);
107
- await server.connect(transport);
108
- startupGate.markReady();
109
-
110
- console.error(`[memorix] MCP Server running on stdio (project: ${projectId})`);
111
- console.error(`[memorix] Project root: ${detected?.rootPath ?? projectRoot}`);
99
+ const toolProfile = resolveToolProfile({
100
+ explicit: args.mode,
101
+ envValue: process.env.MEMORIX_MODE,
102
+ fallback: 'micro',
103
+ });
112
104
 
113
- // ── MCP Roots Protocol ──────────────────────────────────────────
114
- // After connect, request workspace roots from the client (IDE).
115
- // This is the proper way to discover the user's workspace —
116
- // no --cwd needed if the IDE supports roots capability.
117
- const tryRootsSwitch = async () => {
118
- try {
119
- const { roots } = await server.server.listRoots();
120
- if (!roots || roots.length === 0) return;
105
+ const transport = new StdioServerTransport(startupGate.input, process.stdout);
106
+ const createBridge = async (): Promise<ModernMcpBridge> => {
107
+ const bridge = await createModernMcpBridge({
108
+ projectRoot,
109
+ allowUntrackedFallback: allowUntracked,
110
+ deferProjectInitUntilBound: !allowUntracked,
111
+ deferProjectRuntimeInit: true,
112
+ toolProfile,
113
+ });
121
114
 
122
- for (const root of roots) {
123
- if (!root.uri.startsWith('file://')) continue;
124
- const rootPath = mcpFileUriToPath(root.uri);
125
- if (!rootPath) continue;
115
+ const tryRootsSwitch = async (): Promise<void> => {
116
+ try {
117
+ if (bridge.memorix.isExplicitlyBound()) return;
118
+ const { roots } = await bridge.server.listRoots();
119
+ for (const root of roots ?? []) {
120
+ if (!root.uri.startsWith('file://')) continue;
121
+ const rootPath = mcpFileUriToPath(root.uri);
122
+ if (!rootPath) continue;
126
123
 
127
- const rootDetected = detectProject(rootPath);
128
- if (rootDetected) {
129
- const switched = await switchProject(rootPath);
124
+ const switched = await bridge.memorix.switchProject(rootPath, 'mcp-roots');
130
125
  if (switched) {
131
- console.error(`[memorix] [UPDATED] Project updated via MCP roots: ${rootDetected.id}`);
126
+ console.error(`[memorix] [UPDATED] Project updated via MCP roots: ${rootPath}`);
127
+ return;
132
128
  }
133
- return; // use first valid root
134
- }
135
- // Root itself has no .git — try its subdirs
136
- const subGit = findGitInSubdirs(rootPath);
137
- if (subGit) {
138
- const switched = await switchProject(subGit);
139
- if (switched) {
129
+ const subGit = findGitInSubdirs(rootPath);
130
+ if (subGit && await bridge.memorix.switchProject(subGit, 'mcp-roots')) {
140
131
  console.error(`[memorix] [UPDATED] Project updated via MCP roots (subdir): ${subGit}`);
132
+ return;
141
133
  }
142
- return;
143
134
  }
135
+ } catch (error) {
136
+ console.error(`[memorix] MCP roots not available (${error instanceof Error ? error.message : 'unsupported'})`);
144
137
  }
145
- } catch (err) {
146
- // Client doesn't support roots — that's OK, fall back to existing detection
147
- console.error(`[memorix] MCP roots not available (${(err as Error).message ?? 'unsupported'})`);
148
- }
149
- };
150
-
151
- // Do NOT proactively call listRoots() after connect — this violates MCP SEP-2260
152
- // which requires server-initiated requests to be associated with a client request.
153
- // Some clients (e.g. Codex) treat standalone roots/list as unexpected and may
154
- // fail to inject MCP tools. Instead, rely on:
155
- // 1. RootsListChangedNotification (client-initiated, then we respond)
156
- // 2. memorix_session_start({ projectRoot }) for explicit binding
157
- // 3. cwd-based detection as fallback (already done in deferred-binding)
138
+ };
158
139
 
159
- // Listen for roots changes (user switches workspace)
160
- try {
161
- const { RootsListChangedNotificationSchema } = await import('@modelcontextprotocol/sdk/types.js');
162
- server.server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
163
- console.error(`[memorix] Roots changed — re-detecting project...`);
140
+ // roots/list is a legacy-era request. The v2 SDK rejects it on the
141
+ // modern era, while retaining this handler keeps existing IDE clients.
142
+ bridge.server.setNotificationHandler('notifications/roots/list_changed', async () => {
164
143
  await tryRootsSwitch();
165
144
  });
166
- } catch { /* notification handler setup is optional */ }
167
145
 
168
- const deferredInitTimer = setTimeout(() => {
169
- deferredInit().catch(e => console.error(`[memorix] Deferred init error:`, e));
170
- }, 5_000);
171
- deferredInitTimer.unref?.();
172
- // Fire-and-forget: background update check. Default is notify-only.
146
+ const deferredInitTimer = setTimeout(() => {
147
+ bridge.memorix.deferredInit().catch((error) => {
148
+ console.error(`[memorix] Deferred init error: ${error instanceof Error ? error.message : String(error)}`);
149
+ });
150
+ }, 5_000);
151
+ deferredInitTimer.unref?.();
152
+
153
+ const originalClose = bridge.close.bind(bridge);
154
+ bridge.close = async () => {
155
+ clearTimeout(deferredInitTimer);
156
+ await originalClose();
157
+ };
158
+ return bridge;
159
+ };
160
+
161
+ stdioHandle = serveStdio(createBridge, {
162
+ transport,
163
+ legacy: 'serve',
164
+ onerror: (error) => console.error(`[memorix] stdio MCP error: ${error.message}`),
165
+ });
166
+ startupGate.markReady();
167
+ if (stdinEndedBeforeHandle) {
168
+ void stdioHandle.close().catch((error) => {
169
+ console.error(`[memorix] stdio close error: ${error instanceof Error ? error.message : String(error)}`);
170
+ });
171
+ }
172
+
173
+ console.error(`[memorix] MCP Server running on stdio (profile: ${toolProfile}, project: ${detected?.id ?? 'deferred'})`);
174
+ console.error(`[memorix] Project root: ${detected?.rootPath ?? projectRoot}`);
173
175
  import('../update-checker.js').then(m => m.checkForUpdates()).catch(() => {});
174
176
  },
175
177
  });
@@ -31,6 +31,7 @@ import { KiroMCPAdapter } from '../../workspace/mcp-adapters/kiro.js';
31
31
  import { OpenCodeMCPAdapter } from '../../workspace/mcp-adapters/opencode.js';
32
32
  import { TraeMCPAdapter } from '../../workspace/mcp-adapters/trae.js';
33
33
  import { DshMCPAdapter } from '../../workspace/mcp-adapters/dsh.js';
34
+ import { WorkbuddyMCPAdapter } from '../../workspace/mcp-adapters/workbuddy.js';
34
35
  import { CodeBuddyMCPAdapter } from '../../workspace/mcp-adapters/codebuddy.js';
35
36
  import { getSetupIntegrationRows as readSetupIntegrationRows } from '../../integrations/registry.js';
36
37
  import { OFFICIAL_MEMORIX_SKILLS } from '../../hooks/official-skills.js';
@@ -192,6 +193,7 @@ const SUPPORTED_SETUP_AGENTS: AgentName[] = [
192
193
  'kiro',
193
194
  'trae',
194
195
  'dsh',
196
+ 'workbuddy',
195
197
  'pi',
196
198
  ];
197
199
 
@@ -405,6 +407,7 @@ export function getMcpAdapter(agent: McpConfigAgent): MCPConfigAdapter {
405
407
  opencode: new OpenCodeMCPAdapter(),
406
408
  trae: new TraeMCPAdapter(),
407
409
  dsh: new DshMCPAdapter(),
410
+ workbuddy: new WorkbuddyMCPAdapter(),
408
411
  };
409
412
  return adapters[agent];
410
413
  }
@@ -1611,7 +1614,14 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1611
1614
  }
1612
1615
  } else if (plan.actions.includes('hooks')) {
1613
1616
  const result = await installHooks(agent, targetRoot, global);
1614
- p.log.success(`${agent}: integration files -> ${result.configPath}`);
1617
+ const note = (result.generated as Record<string, unknown>)?.note;
1618
+ if (result.configPath) {
1619
+ p.log.success(`${agent}: integration files -> ${result.configPath}`);
1620
+ } else if (note) {
1621
+ p.log.info(`${agent}: ${note}`);
1622
+ } else {
1623
+ p.log.info(`${agent}: no hooks or guidance are available at this scope`);
1624
+ }
1615
1625
  } else if (plan.actions.includes('project-guidance')) {
1616
1626
  const rulesPath = await installAgentGuidance(agent, targetRoot, global);
1617
1627
  p.log.success(`${agent}: guidance -> ${rulesPath}`);
@@ -24,7 +24,7 @@ import * as os from 'node:os';
24
24
 
25
25
  // ── MCP config paths (documented, not modified) ────────────────────
26
26
 
27
- interface MCPConfigEntry {
27
+ export interface MCPConfigEntry {
28
28
  agent: string;
29
29
  path: string;
30
30
  kind: 'project' | 'global';
@@ -66,7 +66,7 @@ function checkDshPatch(p: string): boolean {
66
66
  }
67
67
 
68
68
  /** Detects memorix MCP config entries in both global and project-level paths. */
69
- function getMCPConfigEntries(home: string, cwd: string): MCPConfigEntry[] {
69
+ export function getMCPConfigEntries(home: string, cwd: string): MCPConfigEntry[] {
70
70
  const entries: MCPConfigEntry[] = [];
71
71
 
72
72
  // ── Global paths ──
@@ -236,6 +236,28 @@ function getMCPConfigEntries(home: string, cwd: string): MCPConfigEntry[] {
236
236
  note: 'Remove the memory-memorix insert row from this patch file.',
237
237
  });
238
238
 
239
+ // WorkBuddy — global
240
+ const workbuddyCfg = path.join(home, '.workbuddy', 'mcp.json');
241
+ entries.push({
242
+ agent: 'WorkBuddy',
243
+ path: workbuddyCfg,
244
+ kind: 'global',
245
+ format: 'json',
246
+ detected: checkJSON(workbuddyCfg),
247
+ note: 'Remove the "memorix" key from mcpServers in this file.',
248
+ });
249
+
250
+ // WorkBuddy — project level (project-only installs must be discoverable too)
251
+ const workbuddyProjectCfg = path.join(cwd, '.workbuddy', 'mcp.json');
252
+ entries.push({
253
+ agent: 'WorkBuddy',
254
+ path: workbuddyProjectCfg,
255
+ kind: 'project',
256
+ format: 'json',
257
+ detected: checkJSON(workbuddyProjectCfg),
258
+ note: 'Remove the "memorix" key from mcpServers in this file.',
259
+ });
260
+
239
261
  return entries;
240
262
  }
241
263
 
package/src/cli/index.ts CHANGED
@@ -389,7 +389,7 @@ const main = defineCommand({
389
389
  console.error(' integrate Install one IDE integration into the current repo');
390
390
  console.error(' status Show project info + stats');
391
391
  console.error(' dashboard Open standalone dashboard (read-mostly)');
392
- console.error(' hooks Open legacy hook installer menu');
392
+ console.error(' hooks Install, inspect, or remove agent hooks');
393
393
  console.error(' cleanup Remove old memories');
394
394
  console.error(' purge Retire all memories (project by default, --all for everything)');
395
395
  console.error(' sync Rules/workspace sync plus interactive wizard');
@@ -143,19 +143,73 @@ const KEYWORDS: Record<Exclude<TaskLensId, 'general'>, string[]> = {
143
143
  // Continuation is a delivery intent, not a task lens. A request can both resume
144
144
  // prior work and be a bugfix, feature, or release task, so callers keep the
145
145
  // normal lens and add a bounded prior-work projection separately.
146
- const CONTINUATION_KEYWORDS = [
146
+ //
147
+ // Matching triggers buildHookProjectContext() and a full prior-work brief,
148
+ // which measurably costs 2-3x an ordinary prompt (~3s → ~7.5s locally, 15s+
149
+ // under load) — enough to blow the 10s UserPromptSubmit hook timeout, after
150
+ // which Claude Code discards the injection entirely.
151
+ //
152
+ // The split below is about *evidence of intent*, not about the words alone:
153
+ //
154
+ // EXPLICIT — handoff vocabulary is deliberate on its own. "交接" and
155
+ // "handoff" are not said casually, so a bare mention counts.
156
+ // AMBIGUOUS — "继续" / "continue" / "恢复" are everyday verbs. Bare, they
157
+ // are filler meaning "go on" (and "恢复" usually means "restore
158
+ // the service" in ops work). Carrying a real task they are a
159
+ // genuine resume request — `memorix resume "继续处理发布阻塞问题"`
160
+ // is documented usage — so they require substantive content.
161
+ const EXPLICIT_HANDOFF_KEYWORDS = [
162
+ 'handoff',
163
+ 'hand off',
164
+ 'hand over',
165
+ 'previous session',
166
+ 'last session',
167
+ 'pick up where',
168
+ '交接', // 覆盖「任务交接」「工作交接」
169
+ '接手',
170
+ '上次会话',
171
+ '上次的会话',
172
+ ];
173
+
174
+ const AMBIGUOUS_CONTINUATION_KEYWORDS = [
147
175
  'continue',
148
176
  'resume',
149
- 'pick up',
150
177
  'carry on',
151
- 'previous session',
178
+ 'pick up',
152
179
  '继续',
153
- '接手',
154
180
  '恢复',
155
181
  '延续',
156
- '上次会话',
157
182
  ];
158
183
 
184
+ /**
185
+ * Characters that carry no task meaning on their own: punctuation, whitespace,
186
+ * Chinese modal particles, and the generic scaffolding words users wrap around
187
+ * a bare "继续" ("任务继续", "继续一下").
188
+ */
189
+ const TASK_FILLER_WORDS = /\b(?:please|task|work|the|a|an)\b/giu;
190
+ const TASK_FILLER_PHRASES = /(?:吧|了|呢|啊|吗|嘛|呀|哦|噢|一下|一起|接着|请|帮我)/gu;
191
+
192
+ /** Minimum residual code points that count as a real task description. */
193
+ const MIN_TASK_SUBSTANCE = 2;
194
+
195
+ function hasSubstantiveTask(text: string, keyword: string): boolean {
196
+ const withoutKeyword = text.replace(
197
+ new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'),
198
+ ' ',
199
+ );
200
+ const residual = withoutKeyword
201
+ // English fillers are removed as whole words, so `a` in `API` and `work`
202
+ // in `workflow` remain meaningful task text.
203
+ .replace(TASK_FILLER_WORDS, ' ')
204
+ .replace(TASK_FILLER_PHRASES, ' ')
205
+ .replace(/[\s\p{P}]/gu, '');
206
+
207
+ // These are scaffolding-only phrases, not tasks. Keep `工作流` intact by
208
+ // checking the complete residual instead of removing `工作` as a prefix.
209
+ if (/^(?:任务|工作|一下|一起|接着)$/u.test(residual)) return false;
210
+ return Array.from(residual).length >= MIN_TASK_SUBSTANCE;
211
+ }
212
+
159
213
  const LENS_PRIORITY: Exclude<TaskLensId, 'general'>[] = [
160
214
  'bugfix',
161
215
  'release',
@@ -250,11 +304,22 @@ export function resolveTaskLens(task?: string): TaskLens {
250
304
  * Detect when the caller is asking to continue existing work. This stays
251
305
  * separate from task-lens routing so "continue fixing the timeout" remains a
252
306
  * bugfix, while still receiving a compact prior-work brief.
307
+ *
308
+ * Explicit handoff vocabulary counts on its own. Everyday continuation verbs
309
+ * additionally require a substantive task, so a bare "继续" / "continue" — the
310
+ * single most common thing a user types to mean "go on" — stays on the cheap
311
+ * path instead of paying for a full brief on every turn.
253
312
  */
254
313
  export function isContinuationTask(task?: string): boolean {
255
314
  const normalized = (task ?? '').trim().toLowerCase();
256
- return normalized.length > 0 && CONTINUATION_KEYWORDS.some((keyword) =>
257
- containsTaskKeyword(normalized, keyword),
315
+ if (normalized.length === 0) return false;
316
+
317
+ if (EXPLICIT_HANDOFF_KEYWORDS.some((keyword) => containsTaskKeyword(normalized, keyword))) {
318
+ return true;
319
+ }
320
+
321
+ return AMBIGUOUS_CONTINUATION_KEYWORDS.some((keyword) =>
322
+ containsTaskKeyword(normalized, keyword) && hasSubstantiveTask(normalized, keyword),
258
323
  );
259
324
  }
260
325
 
@@ -142,7 +142,7 @@
142
142
  <path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
143
143
  <path d="M16 3.13a4 4 0 0 1 0 7.75"/>
144
144
  </svg>
145
- <span class="nav-label">Team</span>
145
+ <span class="nav-label">Coordination</span>
146
146
  </button>
147
147
  </div>
148
148
  <div class="sidebar-spacer"></div>
@@ -211,10 +211,6 @@
211
211
 
212
212
  <!-- Apache ECharts 5 — Knowledge Graph renderer -->
213
213
  <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
214
- <!-- Cytoscape.js + Dagre — legacy entity-graph fallback -->
215
- <script src="https://unpkg.com/cytoscape@3.30.4/dist/cytoscape.min.js"></script>
216
- <script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
217
- <script src="https://unpkg.com/cytoscape-dagre@2.5.0/cytoscape-dagre.js"></script>
218
214
  <script>
219
215
  // Cache-bust app.js by appending mtime-based version so edits always hit the browser.
220
216
  (function () {