memorix 1.1.12 → 1.2.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 (94) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +5 -2
  3. package/README.zh-CN.md +5 -2
  4. package/dist/cli/index.js +36575 -31127
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/dashboard/static/app.js +50 -30
  7. package/dist/index.js +5348 -674
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +3661 -293
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +28 -0
  13. package/dist/memcode-runtime/package.json +1 -0
  14. package/dist/sdk.d.ts +1 -1
  15. package/dist/sdk.js +5346 -672
  16. package/dist/sdk.js.map +1 -1
  17. package/docs/1.2.0-CLAIM-LEDGER.md +72 -0
  18. package/docs/1.2.0-CODE-STATE.md +61 -0
  19. package/docs/1.2.0-DEVELOPMENT-CHARTER.md +255 -0
  20. package/docs/1.2.0-DYNAMIC-LIFECYCLE.md +71 -0
  21. package/docs/1.2.0-EVALUATION-HARNESS.md +51 -0
  22. package/docs/1.2.0-IMPLEMENTATION-PLAN.md +554 -0
  23. package/docs/1.2.0-KNOWLEDGE-WORKFLOW-RESEARCH.md +205 -0
  24. package/docs/1.2.0-KNOWLEDGE-WORKSPACE.md +68 -0
  25. package/docs/1.2.0-PRODUCT-STORY.md +234 -0
  26. package/docs/1.2.0-PROVIDER-QUALITY.md +189 -0
  27. package/docs/1.2.0-WORKFLOW-INHERITANCE.md +101 -0
  28. package/docs/1.2.0-WORKSET-RETRIEVAL.md +80 -0
  29. package/docs/AGENT_OPERATOR_PLAYBOOK.md +7 -4
  30. package/docs/API_REFERENCE.md +25 -6
  31. package/docs/CONFIGURATION.md +21 -3
  32. package/docs/DEVELOPMENT.md +3 -10
  33. package/docs/INTEGRATIONS.md +1 -1
  34. package/docs/README.md +17 -2
  35. package/docs/SETUP.md +3 -1
  36. package/docs/dev-log/progress.txt +115 -63
  37. package/llms-full.txt +16 -2
  38. package/llms.txt +9 -4
  39. package/package.json +3 -2
  40. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  41. package/src/cli/capability-map.ts +1 -0
  42. package/src/cli/commands/agent-integrations.ts +384 -2
  43. package/src/cli/commands/codegraph.ts +112 -9
  44. package/src/cli/commands/context.ts +2 -0
  45. package/src/cli/commands/doctor.ts +73 -4
  46. package/src/cli/commands/knowledge.ts +282 -0
  47. package/src/cli/commands/serve-http.ts +12 -1
  48. package/src/cli/commands/setup.ts +13 -1
  49. package/src/cli/index.ts +3 -1
  50. package/src/cli/tui/App.tsx +1 -1
  51. package/src/cli/tui/Panels.tsx +8 -8
  52. package/src/cli/tui/theme.ts +1 -1
  53. package/src/cli/tui/views/GraphView.tsx +8 -7
  54. package/src/cli/tui/views/KnowledgeView.tsx +9 -9
  55. package/src/codegraph/auto-context.ts +171 -9
  56. package/src/codegraph/code-state.ts +95 -0
  57. package/src/codegraph/context-pack.ts +82 -1
  58. package/src/codegraph/external-provider.ts +581 -0
  59. package/src/codegraph/lite-provider.ts +64 -19
  60. package/src/codegraph/project-context.ts +9 -1
  61. package/src/codegraph/store.ts +154 -6
  62. package/src/codegraph/types.ts +117 -0
  63. package/src/config/resolved-config.ts +28 -0
  64. package/src/config/toml-loader.ts +3 -0
  65. package/src/config/yaml-loader.ts +6 -0
  66. package/src/dashboard/server.ts +15 -1
  67. package/src/evaluation/workset-evaluation.ts +120 -0
  68. package/src/hooks/handler.ts +48 -6
  69. package/src/knowledge/claim-store.ts +267 -0
  70. package/src/knowledge/claims.ts +537 -0
  71. package/src/knowledge/markdown.ts +129 -0
  72. package/src/knowledge/types.ts +157 -0
  73. package/src/knowledge/wiki.ts +524 -0
  74. package/src/knowledge/workflow-store.ts +168 -0
  75. package/src/knowledge/workflow-types.ts +95 -0
  76. package/src/knowledge/workflows.ts +743 -0
  77. package/src/knowledge/workset.ts +515 -0
  78. package/src/knowledge/workspace-store.ts +220 -0
  79. package/src/knowledge/workspace-types.ts +106 -0
  80. package/src/knowledge/workspace.ts +220 -0
  81. package/src/memory/observations.ts +19 -0
  82. package/src/runtime/control-plane-maintenance.ts +5 -0
  83. package/src/runtime/isolated-maintenance.ts +5 -0
  84. package/src/runtime/lifecycle-status.ts +102 -0
  85. package/src/runtime/lifecycle.ts +107 -0
  86. package/src/runtime/maintenance-jobs.ts +5 -0
  87. package/src/runtime/project-maintenance.ts +190 -0
  88. package/src/server/tool-profile.ts +3 -2
  89. package/src/server.ts +354 -14
  90. package/src/store/file-lock.ts +24 -4
  91. package/src/store/sqlite-db.ts +307 -0
  92. package/src/wiki/generator.ts +4 -2
  93. package/src/wiki/knowledge-graph.ts +7 -4
  94. package/src/wiki/types.ts +16 -4
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from 'node:child_process';
1
2
  import { existsSync } from 'node:fs';
2
3
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
4
  import { basename, dirname, resolve } from 'node:path';
@@ -9,9 +10,12 @@ import {
9
10
  buildMemorixServer,
10
11
  getMcpAdapter,
11
12
  getSetupAgentTargets,
13
+ installPluginPackage,
12
14
  installMcpConfig,
15
+ tryInstallCodexPlugin,
13
16
  type McpConfigAgent,
14
17
  } from './setup.js';
18
+ import { getCliVersion } from '../version.js';
15
19
 
16
20
  export type AgentIntegrationScope = 'local' | 'project' | 'global' | 'all';
17
21
  export type AgentIntegrationStatus = 'ok' | 'missing' | 'repairable' | 'skipped';
@@ -40,6 +44,35 @@ export interface AgentGuidanceCheck {
40
44
  issues: string[];
41
45
  }
42
46
 
47
+ export interface AgentPluginCheck {
48
+ scope: 'global';
49
+ kind: 'bundle' | 'marketplace' | 'runtime' | 'hook-trust';
50
+ path: string;
51
+ exists: boolean;
52
+ status: AgentIntegrationStatus;
53
+ issues: string[];
54
+ version?: string;
55
+ hooks?: {
56
+ declared: string[];
57
+ expected: string[];
58
+ };
59
+ runtime?: {
60
+ installed: boolean;
61
+ enabled: boolean;
62
+ version?: string;
63
+ };
64
+ hookTrust?: {
65
+ trusted: string[];
66
+ expected: string[];
67
+ };
68
+ }
69
+
70
+ export interface AgentPluginStatus {
71
+ status: AgentIntegrationStatus;
72
+ issues: string[];
73
+ checks: AgentPluginCheck[];
74
+ }
75
+
43
76
  export interface AgentIntegrationEntry {
44
77
  agent: AgentName;
45
78
  mcp: {
@@ -52,6 +85,7 @@ export interface AgentIntegrationEntry {
52
85
  issues: string[];
53
86
  checks: AgentGuidanceCheck[];
54
87
  };
88
+ plugin: AgentPluginStatus;
55
89
  }
56
90
 
57
91
  export interface AgentIntegrationReport {
@@ -89,6 +123,12 @@ const GUIDANCE_AGENTS = new Set<AgentName>([
89
123
  'trae',
90
124
  ]);
91
125
 
126
+ const CODEX_PLUGIN_NAME = 'memorix';
127
+ const CODEX_PLUGIN_MARKETPLACE = 'personal';
128
+ const CODEX_PLUGIN_ID = `${CODEX_PLUGIN_NAME}@${CODEX_PLUGIN_MARKETPLACE}`;
129
+ const CODEX_HOOK_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PostToolUse', 'PreCompact', 'Stop'];
130
+ const CODEX_HOOK_STATE_NAMES = ['session_start', 'user_prompt_submit', 'post_tool_use', 'pre_compact', 'stop'];
131
+
92
132
  type ConcreteAgentIntegrationScope = Exclude<AgentIntegrationScope, 'all'>;
93
133
  type JsonRecord = Record<string, unknown>;
94
134
 
@@ -179,6 +219,310 @@ function asRecord(value: unknown): JsonRecord | null {
179
219
  return value && typeof value === 'object' && !Array.isArray(value) ? value as JsonRecord : null;
180
220
  }
181
221
 
222
+ function asRecordArray(value: unknown): JsonRecord[] {
223
+ return Array.isArray(value)
224
+ ? value.map(asRecord).filter((entry): entry is JsonRecord => entry !== null)
225
+ : [];
226
+ }
227
+
228
+ function codexPluginPath(): string {
229
+ return `${homedir()}/.codex/plugins/${CODEX_PLUGIN_NAME}`;
230
+ }
231
+
232
+ function codexMarketplacePath(): string {
233
+ return `${homedir()}/.agents/plugins/marketplace.json`;
234
+ }
235
+
236
+ function codexConfigPath(): string {
237
+ return `${homedir()}/.codex/config.toml`;
238
+ }
239
+
240
+ function normalizeMarketplacePath(value: string): string {
241
+ return value.replace(/\\/g, '/').replace(/^\.\//, '');
242
+ }
243
+
244
+ function isMemorixCodexPlugin(value: JsonRecord): boolean {
245
+ return value.pluginId === CODEX_PLUGIN_ID
246
+ || (value.name === CODEX_PLUGIN_NAME && value.marketplaceName === CODEX_PLUGIN_MARKETPLACE);
247
+ }
248
+
249
+ export function parseCodexPluginList(output: string): AgentPluginCheck['runtime'] | null {
250
+ try {
251
+ const parsed = asRecord(JSON.parse(output));
252
+ if (!parsed) return null;
253
+ const entries = [...asRecordArray(parsed.installed), ...asRecordArray(parsed.available)];
254
+ const plugin = entries.find(isMemorixCodexPlugin);
255
+ if (!plugin) return { installed: false, enabled: false };
256
+ return {
257
+ installed: plugin.installed === true,
258
+ enabled: plugin.enabled === true,
259
+ ...(typeof plugin.version === 'string' ? { version: plugin.version } : {}),
260
+ };
261
+ } catch {
262
+ return null;
263
+ }
264
+ }
265
+
266
+ async function inspectCodexPluginBundle(): Promise<AgentPluginCheck> {
267
+ const pluginPath = codexPluginPath();
268
+ const manifestPath = `${pluginPath}/.codex-plugin/plugin.json`;
269
+ const hooksPath = `${pluginPath}/hooks/hooks.json`;
270
+ if (!existsSync(pluginPath)) {
271
+ return {
272
+ scope: 'global',
273
+ kind: 'bundle',
274
+ path: pluginPath,
275
+ exists: false,
276
+ status: 'missing',
277
+ issues: ['codex-plugin-bundle-missing'],
278
+ };
279
+ }
280
+ if (!existsSync(manifestPath)) {
281
+ return {
282
+ scope: 'global',
283
+ kind: 'bundle',
284
+ path: manifestPath,
285
+ exists: false,
286
+ status: 'repairable',
287
+ issues: ['codex-plugin-manifest-missing'],
288
+ };
289
+ }
290
+
291
+ let manifest: JsonRecord | null = null;
292
+ try {
293
+ manifest = asRecord(JSON.parse(await readFile(manifestPath, 'utf-8')));
294
+ } catch {
295
+ return {
296
+ scope: 'global',
297
+ kind: 'bundle',
298
+ path: manifestPath,
299
+ exists: true,
300
+ status: 'repairable',
301
+ issues: ['codex-plugin-manifest-unreadable'],
302
+ };
303
+ }
304
+ if (!manifest || manifest.name !== CODEX_PLUGIN_NAME) {
305
+ return {
306
+ scope: 'global',
307
+ kind: 'bundle',
308
+ path: manifestPath,
309
+ exists: true,
310
+ status: 'repairable',
311
+ issues: ['codex-plugin-manifest-invalid'],
312
+ };
313
+ }
314
+
315
+ const version = typeof manifest.version === 'string' ? manifest.version : undefined;
316
+ const issues: string[] = [];
317
+ if (version !== getCliVersion()) issues.push('codex-plugin-version-mismatch');
318
+ if (manifest.hooks !== './hooks/hooks.json') issues.push('codex-hook-manifest-missing');
319
+
320
+ let declared: string[] = [];
321
+ if (!existsSync(hooksPath)) {
322
+ issues.push('codex-hook-manifest-missing');
323
+ } else {
324
+ try {
325
+ const hooksConfig = asRecord(JSON.parse(await readFile(hooksPath, 'utf-8')));
326
+ const hooks = asRecord(hooksConfig?.hooks);
327
+ declared = hooks ? Object.keys(hooks) : [];
328
+ if (CODEX_HOOK_EVENTS.some((event) => !declared.includes(event))) {
329
+ issues.push('codex-hook-events-missing');
330
+ }
331
+ } catch {
332
+ issues.push('codex-hook-manifest-unreadable');
333
+ }
334
+ }
335
+
336
+ return {
337
+ scope: 'global',
338
+ kind: 'bundle',
339
+ path: pluginPath,
340
+ exists: true,
341
+ status: issues.length > 0 ? 'repairable' : 'ok',
342
+ issues: unique(issues),
343
+ ...(version ? { version } : {}),
344
+ hooks: { declared, expected: [...CODEX_HOOK_EVENTS] },
345
+ };
346
+ }
347
+
348
+ async function inspectCodexMarketplace(): Promise<AgentPluginCheck> {
349
+ const marketplacePath = codexMarketplacePath();
350
+ if (!existsSync(marketplacePath)) {
351
+ return {
352
+ scope: 'global',
353
+ kind: 'marketplace',
354
+ path: marketplacePath,
355
+ exists: false,
356
+ status: 'missing',
357
+ issues: ['codex-marketplace-missing'],
358
+ };
359
+ }
360
+
361
+ let catalog: JsonRecord | null = null;
362
+ try {
363
+ catalog = asRecord(JSON.parse(await readFile(marketplacePath, 'utf-8')));
364
+ } catch {
365
+ return {
366
+ scope: 'global',
367
+ kind: 'marketplace',
368
+ path: marketplacePath,
369
+ exists: true,
370
+ status: 'repairable',
371
+ issues: ['codex-marketplace-unreadable'],
372
+ };
373
+ }
374
+
375
+ const entry = asRecordArray(catalog?.plugins).find((plugin) => plugin.name === CODEX_PLUGIN_NAME);
376
+ if (!entry) {
377
+ return {
378
+ scope: 'global',
379
+ kind: 'marketplace',
380
+ path: marketplacePath,
381
+ exists: true,
382
+ status: 'repairable',
383
+ issues: ['codex-marketplace-entry-missing'],
384
+ };
385
+ }
386
+
387
+ const source = asRecord(entry.source);
388
+ const sourcePath = typeof source?.path === 'string' ? source.path : '';
389
+ const issues = source?.source === 'local' && normalizeMarketplacePath(sourcePath) === '.codex/plugins/memorix'
390
+ ? []
391
+ : ['codex-marketplace-entry-stale'];
392
+ return {
393
+ scope: 'global',
394
+ kind: 'marketplace',
395
+ path: marketplacePath,
396
+ exists: true,
397
+ status: issues.length > 0 ? 'repairable' : 'ok',
398
+ issues,
399
+ };
400
+ }
401
+
402
+ function inspectCodexPluginRuntime(): AgentPluginCheck {
403
+ const command = 'codex plugin list --marketplace personal --available --json';
404
+ const result = spawnSync('codex', ['plugin', 'list', '--marketplace', CODEX_PLUGIN_MARKETPLACE, '--available', '--json'], {
405
+ encoding: 'utf-8',
406
+ stdio: 'pipe',
407
+ shell: process.platform === 'win32',
408
+ });
409
+ if (result.error || result.status !== 0) {
410
+ return {
411
+ scope: 'global',
412
+ kind: 'runtime',
413
+ path: command,
414
+ exists: false,
415
+ status: 'skipped',
416
+ issues: ['codex-plugin-runtime-unavailable'],
417
+ };
418
+ }
419
+
420
+ const runtime = parseCodexPluginList(String(result.stdout ?? ''));
421
+ if (!runtime) {
422
+ return {
423
+ scope: 'global',
424
+ kind: 'runtime',
425
+ path: command,
426
+ exists: true,
427
+ status: 'skipped',
428
+ issues: ['codex-plugin-runtime-unreadable'],
429
+ };
430
+ }
431
+
432
+ const issues: string[] = [];
433
+ if (!runtime.installed) issues.push('codex-plugin-not-installed');
434
+ else if (!runtime.enabled) issues.push('codex-plugin-disabled');
435
+ return {
436
+ scope: 'global',
437
+ kind: 'runtime',
438
+ path: command,
439
+ exists: true,
440
+ status: issues.length > 0 ? 'repairable' : 'ok',
441
+ issues,
442
+ runtime,
443
+ };
444
+ }
445
+
446
+ async function inspectCodexHookTrust(): Promise<AgentPluginCheck> {
447
+ const configPath = codexConfigPath();
448
+ if (!existsSync(configPath)) {
449
+ return {
450
+ scope: 'global',
451
+ kind: 'hook-trust',
452
+ path: configPath,
453
+ exists: false,
454
+ status: 'skipped',
455
+ issues: ['codex-hook-trust-unavailable'],
456
+ };
457
+ }
458
+
459
+ let config = '';
460
+ try {
461
+ config = await readFile(configPath, 'utf-8');
462
+ } catch {
463
+ return {
464
+ scope: 'global',
465
+ kind: 'hook-trust',
466
+ path: configPath,
467
+ exists: true,
468
+ status: 'skipped',
469
+ issues: ['codex-hook-trust-unreadable'],
470
+ };
471
+ }
472
+
473
+ const trusted = CODEX_HOOK_STATE_NAMES.filter((event) =>
474
+ config.includes(`[hooks.state."${CODEX_PLUGIN_ID}:hooks/hooks.json:${event}:`)
475
+ );
476
+ const issues = trusted.length === CODEX_HOOK_STATE_NAMES.length ? [] : ['codex-hook-trust-pending'];
477
+ return {
478
+ scope: 'global',
479
+ kind: 'hook-trust',
480
+ path: configPath,
481
+ exists: true,
482
+ status: issues.length > 0 ? 'repairable' : 'ok',
483
+ issues,
484
+ hookTrust: { trusted, expected: [...CODEX_HOOK_STATE_NAMES] },
485
+ };
486
+ }
487
+
488
+ function aggregatePluginStatus(checks: AgentPluginCheck[]): AgentIntegrationStatus {
489
+ if (checks.length === 0) return 'skipped';
490
+ return worstStatus(checks.map((check) => check.status));
491
+ }
492
+
493
+ function aggregatePluginIssues(checks: AgentPluginCheck[]): string[] {
494
+ const status = aggregatePluginStatus(checks);
495
+ if (status === 'ok') {
496
+ return unique(checks.filter((check) => check.status === 'skipped').flatMap((check) => check.issues));
497
+ }
498
+ if (status === 'repairable') {
499
+ return unique(checks
500
+ .filter((check) => check.status === 'repairable' || check.status === 'missing')
501
+ .flatMap((check) => check.issues));
502
+ }
503
+ return unique(checks
504
+ .filter((check) => check.status === status)
505
+ .flatMap((check) => check.issues));
506
+ }
507
+
508
+ async function inspectPlugin(agent: AgentName, scope: AgentIntegrationScope): Promise<AgentPluginStatus> {
509
+ if (agent !== 'codex' || scope === 'local' || scope === 'project') {
510
+ return { status: 'skipped', issues: [], checks: [] };
511
+ }
512
+
513
+ const checks = [
514
+ await inspectCodexPluginBundle(),
515
+ await inspectCodexMarketplace(),
516
+ inspectCodexPluginRuntime(),
517
+ await inspectCodexHookTrust(),
518
+ ];
519
+ return {
520
+ status: aggregatePluginStatus(checks),
521
+ issues: aggregatePluginIssues(checks),
522
+ checks,
523
+ };
524
+ }
525
+
182
526
  function looksLikeStaleMemorixCommand(server: MCPServerEntry): boolean {
183
527
  const text = [server.command, ...(server.args ?? [])].join(' ');
184
528
  return /[\\/]\.worktrees[\\/]/i.test(text) || /[\\/]dist[\\/]cli[\\/]index\.js/i.test(text);
@@ -492,10 +836,11 @@ export async function inspectAgentIntegrations(options: {
492
836
  agent,
493
837
  mcp: await inspectMcp(agent, projectRoot, scope),
494
838
  guidance: await inspectGuidance(agent, projectRoot, scope),
839
+ plugin: await inspectPlugin(agent, scope),
495
840
  });
496
841
  }
497
842
 
498
- const entryStatuses = entries.map((entry) => worstStatus([entry.mcp.status, entry.guidance.status]));
843
+ const entryStatuses = entries.map((entry) => worstStatus([entry.mcp.status, entry.guidance.status, entry.plugin.status]));
499
844
  return {
500
845
  projectRoot,
501
846
  scope,
@@ -519,10 +864,11 @@ export function formatAgentIntegrationReport(report: AgentIntegrationReport): st
519
864
  ];
520
865
 
521
866
  for (const entry of report.entries) {
522
- const status = worstStatus([entry.mcp.status, entry.guidance.status]);
867
+ const status = worstStatus([entry.mcp.status, entry.guidance.status, entry.plugin.status]);
523
868
  lines.push(`${entry.agent}: ${status}`);
524
869
  if (entry.mcp.issues.length > 0) lines.push(` MCP: ${entry.mcp.issues.join(', ')}`);
525
870
  if (entry.guidance.issues.length > 0) lines.push(` Guidance: ${entry.guidance.issues.join(', ')}`);
871
+ if (entry.plugin.issues.length > 0) lines.push(` Plugin: ${entry.plugin.issues.join(', ')}`);
526
872
  }
527
873
 
528
874
  lines.push('');
@@ -586,6 +932,42 @@ export async function repairAgentIntegrations(options: {
586
932
  } else {
587
933
  skipped.push(`${entry.agent}:guidance`);
588
934
  }
935
+
936
+ if (entry.agent === 'codex' && entry.plugin.status !== 'ok' && entry.plugin.status !== 'skipped') {
937
+ const bundleOrMarketplaceNeedsRepair = entry.plugin.checks.some((check) =>
938
+ (check.kind === 'bundle' || check.kind === 'marketplace') && check.status !== 'ok'
939
+ );
940
+ const runtime = entry.plugin.checks.find((check) => check.kind === 'runtime');
941
+ const hookTrust = entry.plugin.checks.find((check) => check.kind === 'hook-trust');
942
+ const needsInstall = runtime?.runtime?.installed === false;
943
+ const needsManualEnable = runtime?.runtime?.installed === true && runtime.runtime.enabled === false;
944
+ const needsHookTrust = hookTrust?.status === 'repairable';
945
+ const missingPluginFiles = entry.plugin.checks.some((check) =>
946
+ (check.kind === 'bundle' || check.kind === 'marketplace') && check.status === 'missing'
947
+ );
948
+
949
+ if (needsManualEnable) {
950
+ skipped.push('codex:plugin:global:enable-in-plugin-browser');
951
+ }
952
+ if (needsHookTrust) {
953
+ skipped.push('codex:plugin:global:review-hooks');
954
+ }
955
+
956
+ if (bundleOrMarketplaceNeedsRepair || needsInstall) {
957
+ if ((missingPluginFiles || needsInstall) && !canInstallMissing) {
958
+ skipped.push('codex:plugin:global:missing');
959
+ } else {
960
+ if (!options.dry) {
961
+ await installPluginPackage({ agent: 'codex' });
962
+ if (needsInstall) {
963
+ const install = tryInstallCodexPlugin();
964
+ if (!install.ok) skipped.push('codex:plugin:global:install-pending');
965
+ }
966
+ }
967
+ changed.push('codex:plugin:global');
968
+ }
969
+ }
970
+ }
589
971
  }
590
972
 
591
973
  return {
@@ -1,20 +1,54 @@
1
1
  import { defineCommand } from 'citty';
2
2
  import { CodeGraphStore } from '../../codegraph/store.js';
3
3
  import { refreshProjectLite } from '../../codegraph/lite-provider.js';
4
- import { assembleContextPackForTask, buildContextPackPrompt } from '../../codegraph/context-pack.js';
4
+ import { assembleContextPackForTask, attachTaskWorkset, buildContextPackPrompt } from '../../codegraph/context-pack.js';
5
5
  import { backfillMissingObservationCodeRefs } from '../../codegraph/binder.js';
6
+ import { collectCurrentProjectFacts } from '../../codegraph/current-facts.js';
7
+ import { resolveTaskLens } from '../../codegraph/task-lens.js';
8
+ import { getExternalCodeGraphContext, inspectExternalCodeGraph } from '../../codegraph/external-provider.js';
9
+ import type { CodeGraphProviderQuality } from '../../codegraph/types.js';
6
10
  import { getResolvedConfig } from '../../config/resolved-config.js';
7
11
  import { getAllObservations } from '../../memory/observations.js';
8
12
  import { emitError, emitResult, getCliProjectContext, parsePositiveInt } from './operator-shared.js';
9
13
 
10
- function formatStatus(status: ReturnType<CodeGraphStore['status']>): string {
14
+ function formatSnapshotStatus(status: ReturnType<CodeGraphStore['status']>): string[] {
15
+ const snapshot = status.latestSnapshot;
16
+ if (!snapshot) return ['- Code state: no completed snapshot yet'];
17
+ const revision = snapshot.baseRevision ? snapshot.baseRevision.slice(0, 12) : 'Git unavailable';
18
+ const completeness = snapshot.completeness;
19
+ const scanState = completeness.skippedOversizedFiles > 0
20
+ || (completeness.unreadableFiles ?? 0) > 0
21
+ || completeness.removalScanDeferred
22
+ ? 'incomplete'
23
+ : 'complete';
11
24
  return [
25
+ '- Code state: ' + revision
26
+ + ', ' + snapshot.worktreeState + ' worktree'
27
+ + ', ' + snapshot.changedPathCount + ' changed path(s)'
28
+ + ', epoch ' + snapshot.sourceEpoch,
29
+ '- Scan completeness: ' + scanState
30
+ + ' (' + completeness.scannedFiles + '/' + completeness.maxFiles + ' paths'
31
+ + ', ' + completeness.skippedOversizedFiles + ' oversized skipped'
32
+ + ', ' + (completeness.unreadableFiles ?? 0) + ' unreadable)',
33
+ ];
34
+ }
35
+
36
+ function formatStatus(status: ReturnType<CodeGraphStore['status']>, quality?: CodeGraphProviderQuality): string {
37
+ return [
38
+ ...formatSnapshotStatus(status),
12
39
  `CodeGraph Memory: ${status.provider}`,
13
40
  `- Files: ${status.files}`,
14
41
  `- Symbols: ${status.symbols}`,
15
42
  `- Edges: ${status.edges}`,
16
43
  `- Memory refs: ${status.refs}`,
17
44
  status.indexedAt ? `- Indexed at: ${status.indexedAt}` : '- Indexed at: never',
45
+ ...(quality
46
+ ? [
47
+ `- Persistent provider: ${status.provider} (heuristic local index)`,
48
+ `- External semantic CodeGraph: ${quality.external.state}`
49
+ + (quality.external.reason ? ` (${quality.external.reason})` : ''),
50
+ ]
51
+ : []),
18
52
  ].join('\n');
19
53
  }
20
54
 
@@ -29,6 +63,19 @@ function formatUsageHint(): string {
29
63
  ].join('\n');
30
64
  }
31
65
 
66
+ function compactFacts(project: { rootPath: string }): { facts: string[]; dirty: boolean } {
67
+ const current = collectCurrentProjectFacts({ project, now: new Date() });
68
+ const facts: string[] = [];
69
+ if (current.packageVersion) facts.push('Package version: ' + current.packageVersion);
70
+ if (current.latestChangelog) {
71
+ facts.push('Latest changelog: ' + current.latestChangelog.version
72
+ + (current.latestChangelog.date ? ' (' + current.latestChangelog.date + ')' : ''));
73
+ }
74
+ facts.push('Git: ' + (current.git.branch ? 'branch ' + current.git.branch + ', ' : '')
75
+ + (current.git.dirty ? 'dirty worktree' : 'clean worktree'));
76
+ return { facts, dirty: current.git.dirty };
77
+ }
78
+
32
79
  export default defineCommand({
33
80
  meta: {
34
81
  name: 'codegraph',
@@ -56,10 +103,16 @@ export default defineCommand({
56
103
  switch (action) {
57
104
  case 'status': {
58
105
  const status = store.status(project.id);
106
+ const providerQuality = await inspectExternalCodeGraph({
107
+ projectRoot: project.rootPath,
108
+ mode: codegraphConfig.externalContext,
109
+ command: codegraphConfig.externalCommand,
110
+ timeoutMs: codegraphConfig.externalTimeoutMs,
111
+ });
59
112
  const text = explicitAction || asJson
60
- ? formatStatus(status)
61
- : `${formatStatus(status)}\n\n${formatUsageHint()}`;
62
- emitResult({ project, status }, text, asJson);
113
+ ? formatStatus(status, providerQuality.quality)
114
+ : `${formatStatus(status, providerQuality.quality)}\n\n${formatUsageHint()}`;
115
+ emitResult({ project, status, providerQuality: providerQuality.quality }, text, asJson);
63
116
  return;
64
117
  }
65
118
 
@@ -73,12 +126,25 @@ export default defineCommand({
73
126
  const activeObservations = getAllObservations()
74
127
  .filter(obs => obs.projectId === project.id && (obs.status ?? 'active') === 'active');
75
128
  const backfill = await backfillMissingObservationCodeRefs(store, activeObservations);
129
+ const { enqueueClaimRequalification } = await import('../../runtime/lifecycle.js');
130
+ enqueueClaimRequalification({
131
+ dataDir,
132
+ projectId: project.id,
133
+ source: 'manual-codegraph-refresh',
134
+ snapshotId: refresh.snapshot.id,
135
+ });
76
136
  const status = store.status(project.id);
137
+ const providerQuality = await inspectExternalCodeGraph({
138
+ projectRoot: project.rootPath,
139
+ mode: codegraphConfig.externalContext,
140
+ command: codegraphConfig.externalCommand,
141
+ timeoutMs: codegraphConfig.externalTimeoutMs,
142
+ });
77
143
  emitResult(
78
- { project, status, refresh, backfill },
144
+ { project, status, providerQuality: providerQuality.quality, refresh, backfill },
79
145
  [
80
146
  'CodeGraph Memory refreshed.',
81
- formatStatus(status),
147
+ formatStatus(status, providerQuality.quality),
82
148
  `- Files: ${refresh.changedFiles} changed, ${refresh.unchangedFiles} unchanged, ${refresh.removedFiles} removed`,
83
149
  `- Backfilled memories: ${backfill.observationsBackfilled}`,
84
150
  `- Backfilled refs: ${backfill.refsBackfilled}`,
@@ -98,7 +164,7 @@ export default defineCommand({
98
164
  const observations = getAllObservations()
99
165
  .filter(obs => obs.projectId === project.id && (obs.status ?? 'active') === 'active')
100
166
  .reverse();
101
- const pack = assembleContextPackForTask({
167
+ const basePack = assembleContextPackForTask({
102
168
  store,
103
169
  projectId: project.id,
104
170
  task,
@@ -106,7 +172,44 @@ export default defineCommand({
106
172
  limit,
107
173
  exclude,
108
174
  });
109
- emitResult({ project, pack }, buildContextPackPrompt(pack), asJson);
175
+ const status = store.status(project.id);
176
+ const facts = compactFacts(project);
177
+ const snapshot = status.latestSnapshot;
178
+ const external = await getExternalCodeGraphContext({
179
+ projectRoot: project.rootPath,
180
+ task,
181
+ exclude,
182
+ mode: codegraphConfig.externalContext,
183
+ command: codegraphConfig.externalCommand,
184
+ timeoutMs: codegraphConfig.externalTimeoutMs,
185
+ });
186
+ const pack = await attachTaskWorkset({
187
+ pack: basePack,
188
+ projectId: project.id,
189
+ dataDir,
190
+ lens: resolveTaskLens(task).id,
191
+ worktreeDirty: facts.dirty,
192
+ currentFacts: facts.facts,
193
+ codeState: formatSnapshotStatus(status).join(' '),
194
+ ...(snapshot
195
+ ? {
196
+ snapshot: {
197
+ id: snapshot.id,
198
+ sourceEpoch: snapshot.sourceEpoch,
199
+ worktreeState: snapshot.worktreeState,
200
+ incomplete: snapshot.completeness.skippedOversizedFiles > 0
201
+ || (snapshot.completeness.unreadableFiles ?? 0) > 0
202
+ || snapshot.completeness.removalScanDeferred,
203
+ },
204
+ }
205
+ : {}),
206
+ ...(external.outline ? { semanticCode: external.outline } : {}),
207
+ providerQuality: external.quality,
208
+ ...(external.caution
209
+ ? { runtimeCautions: [{ kind: 'external-codegraph-fallback' as const, message: external.caution }] }
210
+ : {}),
211
+ });
212
+ emitResult({ project, pack, providerQuality: external.quality }, buildContextPackPrompt(pack), asJson);
110
213
  return;
111
214
  }
112
215
 
@@ -45,6 +45,8 @@ export default defineCommand({
45
45
  currentFacts: context.currentFacts,
46
46
  overview: context.overview,
47
47
  refresh: context.refresh,
48
+ providerQuality: context.providerQuality,
49
+ workset: context.workset,
48
50
  ...(context.task ? { task: context.task } : {}),
49
51
  },
50
52
  formatAutoProjectContextPrompt(context),