memorix 1.1.13 → 1.2.1

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 (97) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +6 -3
  3. package/README.zh-CN.md +6 -3
  4. package/dist/cli/index.js +36639 -31279
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/dashboard/static/app.js +50 -30
  7. package/dist/index.js +6636 -1778
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +3775 -302
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +33 -1
  13. package/dist/sdk.d.ts +1 -1
  14. package/dist/sdk.js +6634 -1776
  15. package/dist/sdk.js.map +1 -1
  16. package/docs/1.2.0-CLAIM-LEDGER.md +72 -0
  17. package/docs/1.2.0-CODE-STATE.md +61 -0
  18. package/docs/1.2.0-DEVELOPMENT-CHARTER.md +255 -0
  19. package/docs/1.2.0-DYNAMIC-LIFECYCLE.md +71 -0
  20. package/docs/1.2.0-EVALUATION-HARNESS.md +51 -0
  21. package/docs/1.2.0-IMPLEMENTATION-PLAN.md +554 -0
  22. package/docs/1.2.0-KNOWLEDGE-WORKFLOW-RESEARCH.md +205 -0
  23. package/docs/1.2.0-KNOWLEDGE-WORKSPACE.md +68 -0
  24. package/docs/1.2.0-PRODUCT-STORY.md +234 -0
  25. package/docs/1.2.0-PROVIDER-QUALITY.md +189 -0
  26. package/docs/1.2.0-WORKFLOW-INHERITANCE.md +101 -0
  27. package/docs/1.2.0-WORKSET-RETRIEVAL.md +80 -0
  28. package/docs/AGENT_OPERATOR_PLAYBOOK.md +6 -3
  29. package/docs/API_REFERENCE.md +27 -6
  30. package/docs/CONFIGURATION.md +21 -3
  31. package/docs/DEVELOPMENT.md +4 -0
  32. package/docs/README.md +17 -2
  33. package/docs/SETUP.md +7 -1
  34. package/docs/dev-log/progress.txt +120 -40
  35. package/docs/knowledge/workflows/memorix-release.md +57 -0
  36. package/llms-full.txt +16 -2
  37. package/llms.txt +9 -4
  38. package/package.json +3 -2
  39. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  40. package/src/cli/capability-map.ts +1 -0
  41. package/src/cli/commands/codegraph.ts +111 -9
  42. package/src/cli/commands/context.ts +2 -0
  43. package/src/cli/commands/doctor.ts +73 -4
  44. package/src/cli/commands/knowledge.ts +322 -0
  45. package/src/cli/commands/serve-http.ts +26 -42
  46. package/src/cli/commands/setup.ts +9 -3
  47. package/src/cli/index.ts +3 -1
  48. package/src/cli/tui/App.tsx +1 -1
  49. package/src/cli/tui/Panels.tsx +8 -8
  50. package/src/cli/tui/theme.ts +1 -1
  51. package/src/cli/tui/views/GraphView.tsx +8 -7
  52. package/src/cli/tui/views/KnowledgeView.tsx +9 -9
  53. package/src/codegraph/auto-context.ts +169 -19
  54. package/src/codegraph/code-state.ts +95 -0
  55. package/src/codegraph/context-pack.ts +82 -1
  56. package/src/codegraph/current-facts.ts +19 -1
  57. package/src/codegraph/external-provider.ts +581 -0
  58. package/src/codegraph/lite-provider.ts +64 -19
  59. package/src/codegraph/project-context.ts +9 -1
  60. package/src/codegraph/store.ts +154 -6
  61. package/src/codegraph/task-lens.ts +49 -5
  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 +30 -47
  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 +587 -0
  71. package/src/knowledge/markdown.ts +129 -0
  72. package/src/knowledge/types.ts +158 -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 +774 -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/auto-relations.ts +21 -0
  82. package/src/memory/graph-scope.ts +46 -0
  83. package/src/memory/observations.ts +19 -0
  84. package/src/orchestrate/verify-gate.ts +33 -10
  85. package/src/runtime/control-plane-maintenance.ts +5 -0
  86. package/src/runtime/isolated-maintenance.ts +5 -0
  87. package/src/runtime/lifecycle-status.ts +102 -0
  88. package/src/runtime/lifecycle.ts +107 -0
  89. package/src/runtime/maintenance-jobs.ts +5 -0
  90. package/src/runtime/project-maintenance.ts +190 -0
  91. package/src/server/tool-profile.ts +3 -2
  92. package/src/server.ts +424 -22
  93. package/src/store/file-lock.ts +24 -4
  94. package/src/store/sqlite-db.ts +307 -0
  95. package/src/wiki/generator.ts +4 -2
  96. package/src/wiki/knowledge-graph.ts +7 -4
  97. package/src/wiki/types.ts +16 -4
package/src/server.ts CHANGED
@@ -28,6 +28,7 @@ import { initSessionStore } from './store/session-store.js';
28
28
  import { checkProjectAttribution, auditProjectObservations } from './memory/attribution-guard.js';
29
29
  import { createAutoRelations } from './memory/auto-relations.js';
30
30
  import { extractEntities } from './memory/entity-extractor.js';
31
+ import { scopeKnowledgeGraphToProject } from './memory/graph-scope.js';
31
32
  import { compactSearch, compactTimeline, compactDetail } from './compact/engine.js';
32
33
  import { buildGraphContextPacket, formatGraphContextPrompt } from './memory/graph-context.js';
33
34
  import { detectProject } from './project/detector.js';
@@ -469,10 +470,11 @@ export async function createMemorixServer(
469
470
  let maintenanceWorker: { stop(): void } | null = null;
470
471
  const startProjectMaintenanceWorker = async (autoCleanup = false): Promise<void> => {
471
472
  maintenanceWorker?.stop();
472
- const [{ MaintenanceJobStore, MaintenanceJobWorker }, maintenance, observations] = await Promise.all([
473
+ const [{ MaintenanceJobStore, MaintenanceJobWorker }, maintenance, observations, lifecycle] = await Promise.all([
473
474
  import('./runtime/maintenance-jobs.js'),
474
475
  import('./runtime/project-maintenance.js'),
475
476
  import('./memory/observations.js'),
477
+ import('./runtime/lifecycle.js'),
476
478
  ]);
477
479
  const queue = new MaintenanceJobStore(projectDir);
478
480
  const vectorStatus = observations.getVectorStatus(project.id);
@@ -509,11 +511,12 @@ export async function createMemorixServer(
509
511
  || !Number.isFinite(indexedAt)
510
512
  || Date.now() - indexedAt > 10 * 60_000;
511
513
  if (needsRefresh) {
512
- queue.enqueue({
514
+ lifecycle.enqueueCodegraphRefresh({
515
+ dataDir: projectDir,
513
516
  projectId: project.id,
514
- kind: 'codegraph-refresh',
515
- dedupeKey: 'startup-codegraph-refresh',
516
- payload: { maxFiles: 5_000 },
517
+ source: 'startup',
518
+ maxFiles: 5_000,
519
+ queue,
517
520
  });
518
521
  }
519
522
  } catch {
@@ -1329,10 +1332,12 @@ export async function createMemorixServer(
1329
1332
  },
1330
1333
  { getObservationStore },
1331
1334
  { MaintenanceJobStore },
1335
+ { enqueueCodegraphRefresh },
1332
1336
  ] = await Promise.all([
1333
1337
  import('./codegraph/auto-context.js'),
1334
1338
  import('./store/obs-store.js'),
1335
1339
  import('./runtime/maintenance-jobs.js'),
1340
+ import('./runtime/lifecycle.js'),
1336
1341
  ]);
1337
1342
  const observations = await getObservationStore().loadByProject(project.id, { status: 'active' });
1338
1343
  const context = await buildAutoProjectContext({
@@ -1342,11 +1347,12 @@ export async function createMemorixServer(
1342
1347
  task,
1343
1348
  refresh: refresh ?? 'auto',
1344
1349
  enqueueRefresh: () => {
1345
- new MaintenanceJobStore(projectDir).enqueue({
1350
+ enqueueCodegraphRefresh({
1351
+ dataDir: projectDir,
1346
1352
  projectId: project.id,
1347
- kind: 'codegraph-refresh',
1348
- dedupeKey: 'context-codegraph-refresh',
1349
- payload: { maxFiles: 5_000 },
1353
+ source: 'project-context',
1354
+ maxFiles: 5_000,
1355
+ queue: new MaintenanceJobStore(projectDir),
1350
1356
  });
1351
1357
  },
1352
1358
  });
@@ -1373,13 +1379,24 @@ export async function createMemorixServer(
1373
1379
  const unresolved = requireResolvedProject('show CodeGraph Memory status for the current project');
1374
1380
  if (unresolved) return unresolved;
1375
1381
 
1376
- const { CodeGraphStore } = await import('./codegraph/store.js');
1382
+ const [{ CodeGraphStore }, { getResolvedConfig }, { inspectExternalCodeGraph }] = await Promise.all([
1383
+ import('./codegraph/store.js'),
1384
+ import('./config/resolved-config.js'),
1385
+ import('./codegraph/external-provider.js'),
1386
+ ]);
1377
1387
  const store = new CodeGraphStore();
1378
1388
  await store.init(projectDir);
1379
1389
  const status = store.status(project.id);
1390
+ const codegraphConfig = getResolvedConfig({ projectRoot: project.rootPath }).codegraph;
1391
+ const providerQuality = await inspectExternalCodeGraph({
1392
+ projectRoot: project.rootPath,
1393
+ mode: codegraphConfig.externalContext,
1394
+ command: codegraphConfig.externalCommand,
1395
+ timeoutMs: codegraphConfig.externalTimeoutMs,
1396
+ });
1380
1397
 
1381
1398
  return {
1382
- content: [{ type: 'text' as const, text: JSON.stringify(status, null, 2) }],
1399
+ content: [{ type: 'text' as const, text: JSON.stringify({ ...status, providerQuality: providerQuality.quality }, null, 2) }],
1383
1400
  };
1384
1401
  },
1385
1402
  );
@@ -1405,21 +1422,28 @@ export async function createMemorixServer(
1405
1422
 
1406
1423
  const [
1407
1424
  { CodeGraphStore },
1408
- { assembleContextPackForTask, buildContextPackPrompt },
1425
+ { assembleContextPackForTask, attachTaskWorkset, buildContextPackPrompt },
1409
1426
  { getResolvedConfig },
1427
+ { getExternalCodeGraphContext },
1410
1428
  { getObservationStore },
1429
+ { collectCurrentProjectFacts, formatGitFact },
1430
+ { resolveTaskLens },
1411
1431
  ] = await Promise.all([
1412
1432
  import('./codegraph/store.js'),
1413
1433
  import('./codegraph/context-pack.js'),
1414
1434
  import('./config/resolved-config.js'),
1435
+ import('./codegraph/external-provider.js'),
1415
1436
  import('./store/obs-store.js'),
1437
+ import('./codegraph/current-facts.js'),
1438
+ import('./codegraph/task-lens.js'),
1416
1439
  ]);
1417
1440
  const store = new CodeGraphStore();
1418
1441
  await store.init(projectDir);
1419
- const exclude = getResolvedConfig({ projectRoot: project.rootPath }).codegraph.excludePatterns;
1442
+ const codegraphConfig = getResolvedConfig({ projectRoot: project.rootPath }).codegraph;
1443
+ const exclude = codegraphConfig.excludePatterns;
1420
1444
  const observations = await getObservationStore().loadByProject(project.id, { status: 'active' });
1421
1445
  observations.reverse();
1422
- const pack = assembleContextPackForTask({
1446
+ const basePack = assembleContextPackForTask({
1423
1447
  store,
1424
1448
  projectId: project.id,
1425
1449
  task,
@@ -1427,6 +1451,55 @@ export async function createMemorixServer(
1427
1451
  limit: typeof limit === 'number' ? limit : 20,
1428
1452
  exclude,
1429
1453
  });
1454
+ const status = store.status(project.id);
1455
+ const currentFacts = collectCurrentProjectFacts({ project, now: new Date() });
1456
+ const snapshot = status.latestSnapshot;
1457
+ const external = await getExternalCodeGraphContext({
1458
+ projectRoot: project.rootPath,
1459
+ task,
1460
+ exclude,
1461
+ mode: codegraphConfig.externalContext,
1462
+ command: codegraphConfig.externalCommand,
1463
+ timeoutMs: codegraphConfig.externalTimeoutMs,
1464
+ });
1465
+ const worksetFacts: string[] = [];
1466
+ if (currentFacts.packageVersion) worksetFacts.push('Package version: ' + currentFacts.packageVersion);
1467
+ if (currentFacts.latestChangelog) {
1468
+ worksetFacts.push('Latest changelog: ' + currentFacts.latestChangelog.version
1469
+ + (currentFacts.latestChangelog.date ? ' (' + currentFacts.latestChangelog.date + ')' : ''));
1470
+ }
1471
+ worksetFacts.push(formatGitFact(currentFacts.git));
1472
+ const codeState = snapshot
1473
+ ? '- Code state: ' + (snapshot.baseRevision ? snapshot.baseRevision.slice(0, 12) : 'Git unavailable')
1474
+ + ', ' + snapshot.worktreeState + ' worktree'
1475
+ + ', epoch ' + snapshot.sourceEpoch
1476
+ : '- Code state: no completed snapshot yet';
1477
+ const pack = await attachTaskWorkset({
1478
+ pack: basePack,
1479
+ projectId: project.id,
1480
+ dataDir: projectDir,
1481
+ lens: resolveTaskLens(task).id,
1482
+ worktreeDirty: currentFacts.git.dirty,
1483
+ currentFacts: worksetFacts,
1484
+ codeState,
1485
+ ...(snapshot
1486
+ ? {
1487
+ snapshot: {
1488
+ id: snapshot.id,
1489
+ sourceEpoch: snapshot.sourceEpoch,
1490
+ worktreeState: snapshot.worktreeState,
1491
+ incomplete: snapshot.completeness.skippedOversizedFiles > 0
1492
+ || (snapshot.completeness.unreadableFiles ?? 0) > 0
1493
+ || snapshot.completeness.removalScanDeferred,
1494
+ },
1495
+ }
1496
+ : {}),
1497
+ ...(external.outline ? { semanticCode: external.outline } : {}),
1498
+ providerQuality: external.quality,
1499
+ ...(external.caution
1500
+ ? { runtimeCautions: [{ kind: 'external-codegraph-fallback' as const, message: external.caution }] }
1501
+ : {}),
1502
+ });
1430
1503
  const text = buildContextPackPrompt(pack);
1431
1504
 
1432
1505
  return {
@@ -1435,6 +1508,339 @@ export async function createMemorixServer(
1435
1508
  },
1436
1509
  );
1437
1510
 
1511
+ server.registerTool(
1512
+ 'memorix_knowledge',
1513
+ {
1514
+ title: 'Knowledge Workspace',
1515
+ description:
1516
+ 'Manage the reviewable project Knowledge Workspace. Use it only for deliberate knowledge operations: initialize a local or versioned workspace, review source-backed claims, compile proposals, lint, apply a reviewed proposal, or manage canonical workflows. It is intentionally absent from the default micro/lite profiles.',
1517
+ inputSchema: {
1518
+ action: z.enum([
1519
+ 'workspace_init',
1520
+ 'status',
1521
+ 'claim_list',
1522
+ 'claim_review',
1523
+ 'compile',
1524
+ 'lint',
1525
+ 'proposal_apply',
1526
+ 'workflow_import',
1527
+ 'workflow_list',
1528
+ 'workflow_select',
1529
+ 'workflow_preview',
1530
+ 'workflow_apply',
1531
+ 'workflow_run',
1532
+ ]).describe('Knowledge operation to perform'),
1533
+ mode: z.enum(['local', 'versioned']).optional().default('local').describe('Workspace mode; versioned writes require an explicit project path during workspace_init'),
1534
+ path: z.string().optional().describe('Explicit versioned workspace path, used only by workspace_init'),
1535
+ proposalId: z.string().optional().describe('Pending proposal id for proposal_apply'),
1536
+ allowManualOverwrite: z.boolean().optional().default(false).describe('Explicitly allow proposal_apply to replace a manually edited page'),
1537
+ claimId: z.string().optional().describe('Source-backed claim id for claim_review'),
1538
+ claimReviewState: z.enum(['approved', 'rejected']).optional().describe('Deliberate review verdict for claim_review'),
1539
+ reviewDetail: z.string().max(2_000).optional().describe('Evidence check performed before approving or rejecting a claim'),
1540
+ workflowId: z.string().optional().describe('Canonical workflow id for workflow preview, apply, or run'),
1541
+ agent: z.string().optional().describe('Target agent for a workflow adapter'),
1542
+ task: z.string().optional().describe('Task text for workflow selection or a workflow run'),
1543
+ outcome: z.enum(['passed', 'failed', 'cancelled', 'in-progress']).optional().describe('Workflow run outcome'),
1544
+ verificationVerdict: z.enum(['passed', 'failed', 'not-run']).optional().describe('Workflow run verification verdict'),
1545
+ failureReason: z.string().optional().describe('Sanitized workflow failure reason'),
1546
+ startingSnapshotId: z.string().optional().describe('Code snapshot id present when a workflow run began'),
1547
+ evidenceIds: z.array(z.string()).max(24).optional().describe('Selected evidence ids for a workflow run'),
1548
+ },
1549
+ },
1550
+ async ({
1551
+ action,
1552
+ mode,
1553
+ path: workspacePath,
1554
+ proposalId,
1555
+ allowManualOverwrite,
1556
+ claimId,
1557
+ claimReviewState,
1558
+ reviewDetail,
1559
+ workflowId,
1560
+ agent,
1561
+ task,
1562
+ outcome,
1563
+ verificationVerdict,
1564
+ failureReason,
1565
+ startingSnapshotId,
1566
+ evidenceIds,
1567
+ }) => {
1568
+ const unresolved = requireResolvedProject('manage the Knowledge Workspace for the current project');
1569
+ if (unresolved) return unresolved;
1570
+
1571
+ const text = (value: unknown, isError = false) => ({
1572
+ content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }],
1573
+ ...(isError ? { isError: true as const } : {}),
1574
+ });
1575
+ const modeValue = mode ?? 'local';
1576
+ const requireText = (value: string | undefined, field: string): string | undefined => {
1577
+ const normalized = value?.trim();
1578
+ return normalized ? normalized : undefined;
1579
+ };
1580
+
1581
+ const [
1582
+ { ClaimStore },
1583
+ { reviewClaim },
1584
+ { CodeGraphStore },
1585
+ { initializeKnowledgeWorkspace, loadKnowledgeWorkspace },
1586
+ { KnowledgeWorkspaceStore },
1587
+ { applyKnowledgeProposal, compileKnowledgeWorkspace, lintKnowledgeWorkspace },
1588
+ { WorkflowStore },
1589
+ {
1590
+ applyWorkflowAdapter,
1591
+ importWindsurfWorkflows,
1592
+ previewWorkflowAdapter,
1593
+ recordWorkflowRun,
1594
+ selectWorkspaceWorkflows,
1595
+ syncCanonicalWorkflows,
1596
+ },
1597
+ ] = await Promise.all([
1598
+ import('./knowledge/claim-store.js'),
1599
+ import('./knowledge/claims.js'),
1600
+ import('./codegraph/store.js'),
1601
+ import('./knowledge/workspace.js'),
1602
+ import('./knowledge/workspace-store.js'),
1603
+ import('./knowledge/wiki.js'),
1604
+ import('./knowledge/workflow-store.js'),
1605
+ import('./knowledge/workflows.js'),
1606
+ ]);
1607
+
1608
+ if (action === 'workspace_init') {
1609
+ if (modeValue === 'versioned' && !requireText(workspacePath, 'path')) {
1610
+ return text({ error: 'path is required to initialize a versioned Knowledge Workspace.' }, true);
1611
+ }
1612
+ const workspace = await initializeKnowledgeWorkspace({
1613
+ projectId: project.id,
1614
+ dataDir: projectDir,
1615
+ mode: modeValue,
1616
+ ...(modeValue === 'versioned'
1617
+ ? { projectRoot: project.rootPath, rootPath: requireText(workspacePath, 'path')! }
1618
+ : {}),
1619
+ });
1620
+ return text({
1621
+ workspace: {
1622
+ id: workspace.id,
1623
+ mode: workspace.mode,
1624
+ rootPath: workspace.rootPath,
1625
+ status: workspace.status,
1626
+ },
1627
+ next: 'Compile creates reviewable proposals; it does not silently publish pages.',
1628
+ });
1629
+ }
1630
+
1631
+ const workspace = await loadKnowledgeWorkspace({ projectId: project.id, dataDir: projectDir, mode: modeValue });
1632
+ if (!workspace) {
1633
+ return text({ error: 'Knowledge Workspace is not initialized. Run workspace_init first.' }, true);
1634
+ }
1635
+ const claims = new ClaimStore();
1636
+ const workspaceStore = new KnowledgeWorkspaceStore();
1637
+ await Promise.all([claims.init(projectDir), workspaceStore.init(projectDir)]);
1638
+
1639
+ if (action === 'status') {
1640
+ const pages = workspaceStore.listPages(workspace.id);
1641
+ const pending = workspaceStore.listProposals(workspace.id, 'pending');
1642
+ return text({
1643
+ workspace: {
1644
+ id: workspace.id,
1645
+ mode: workspace.mode,
1646
+ rootPath: workspace.rootPath,
1647
+ status: workspace.status,
1648
+ publishedPages: pages.filter(page => page.status === 'active').length,
1649
+ pendingProposals: pending.map(proposal => ({
1650
+ id: proposal.id,
1651
+ targetPath: proposal.targetPath,
1652
+ reason: proposal.reason,
1653
+ createdAt: proposal.createdAt,
1654
+ })),
1655
+ reviewableClaims: claims.listClaims(project.id, { limit: 100 })
1656
+ .filter(claim => claim.reviewState === 'needs-review')
1657
+ .map(claim => ({ id: claim.id, subject: claim.subject, predicate: claim.predicate, objectValue: claim.objectValue })),
1658
+ },
1659
+ });
1660
+ }
1661
+
1662
+ if (action === 'claim_list') {
1663
+ return text({
1664
+ claims: claims.listClaims(project.id, { limit: 100 }).map(claim => ({
1665
+ id: claim.id,
1666
+ subject: claim.subject,
1667
+ predicate: claim.predicate,
1668
+ objectValue: claim.objectValue,
1669
+ status: claim.status,
1670
+ reviewState: claim.reviewState,
1671
+ origin: claim.origin,
1672
+ confidence: claim.confidence,
1673
+ evidenceCount: claims.listEvidence(claim.id).length,
1674
+ })),
1675
+ next: 'Approve only after checking the linked evidence. Rejected claims stay out of retrieval and publication.',
1676
+ });
1677
+ }
1678
+
1679
+ if (action === 'claim_review') {
1680
+ const requestedClaimId = requireText(claimId, 'claimId');
1681
+ const detail = requireText(reviewDetail, 'reviewDetail');
1682
+ if (!requestedClaimId || !claimReviewState || !detail) {
1683
+ return text({ error: 'claimId, claimReviewState, and reviewDetail are required for claim_review.' }, true);
1684
+ }
1685
+ const existing = claims.getClaim(requestedClaimId);
1686
+ if (!existing || existing.projectId !== project.id) {
1687
+ return text({ error: 'Claim was not found for the current project.' }, true);
1688
+ }
1689
+ const claim = reviewClaim(claims, {
1690
+ claimId: requestedClaimId,
1691
+ reviewState: claimReviewState,
1692
+ detail,
1693
+ });
1694
+ return text({
1695
+ claim: {
1696
+ id: claim.id,
1697
+ status: claim.status,
1698
+ reviewState: claim.reviewState,
1699
+ updatedAt: claim.updatedAt,
1700
+ },
1701
+ next: claim.reviewState === 'approved'
1702
+ ? 'This approved source-backed claim can now be considered by knowledge compilation.'
1703
+ : 'This rejected claim is excluded from retrieval and knowledge compilation.',
1704
+ });
1705
+ }
1706
+
1707
+ if (action === 'compile') {
1708
+ const result = await compileKnowledgeWorkspace({ workspace, claims });
1709
+ return text({
1710
+ proposals: result.proposals.map(proposal => ({
1711
+ id: proposal.id,
1712
+ targetPath: proposal.targetPath,
1713
+ proposalPath: proposal.proposalPath,
1714
+ reason: proposal.reason,
1715
+ })),
1716
+ unchangedPublishedPages: result.published.map(page => page.relativePath),
1717
+ next: result.proposals.length ? 'Review a proposal, then use proposal_apply deliberately.' : undefined,
1718
+ });
1719
+ }
1720
+
1721
+ if (action === 'lint') {
1722
+ const codeStore = new CodeGraphStore();
1723
+ await codeStore.init(projectDir);
1724
+ const result = await lintKnowledgeWorkspace({ workspace, claims, codeStore });
1725
+ return text(result);
1726
+ }
1727
+
1728
+ if (action === 'proposal_apply') {
1729
+ const proposal = requireText(proposalId, 'proposalId');
1730
+ if (!proposal) return text({ error: 'proposalId is required for proposal_apply.' }, true);
1731
+ const result = await applyKnowledgeProposal({
1732
+ workspace,
1733
+ proposalId: proposal,
1734
+ allowManualOverwrite: !!allowManualOverwrite,
1735
+ });
1736
+ return text({
1737
+ proposal: { id: result.proposal.id, status: result.proposal.status },
1738
+ targetPath: result.targetPath,
1739
+ });
1740
+ }
1741
+
1742
+ const workflowStore = new WorkflowStore();
1743
+ await workflowStore.init(projectDir);
1744
+ const projectRoot = workspace.projectRoot ?? project.rootPath;
1745
+
1746
+ if (action === 'workflow_import') {
1747
+ const result = await importWindsurfWorkflows({ workspace, projectRoot });
1748
+ return text({
1749
+ imported: result.imported.map(workflow => ({
1750
+ id: workflow.id,
1751
+ title: workflow.title,
1752
+ sourcePath: workflow.sourcePath,
1753
+ importedFrom: workflow.importedFrom,
1754
+ verificationGates: workflow.verificationGates,
1755
+ })),
1756
+ skipped: result.skipped,
1757
+ });
1758
+ }
1759
+
1760
+ const synced = await syncCanonicalWorkflows(workspace);
1761
+ if (action === 'workflow_list') {
1762
+ return text({
1763
+ workflows: workflowStore.listWorkflows(workspace.id).map(workflow => ({
1764
+ id: workflow.id,
1765
+ title: workflow.title,
1766
+ status: workflow.status,
1767
+ taskLenses: workflow.taskLenses,
1768
+ sourcePath: workflow.sourcePath,
1769
+ importedFrom: workflow.importedFrom,
1770
+ verificationGates: workflow.verificationGates,
1771
+ })),
1772
+ parseErrors: synced.errors,
1773
+ });
1774
+ }
1775
+
1776
+ if (action === 'workflow_select') {
1777
+ const selectedTask = requireText(task, 'task');
1778
+ if (!selectedTask) return text({ error: 'task is required for workflow_select.' }, true);
1779
+ const result = await selectWorkspaceWorkflows({ workspace, task: selectedTask });
1780
+ return text({
1781
+ selections: result.selections.map(selection => ({
1782
+ id: selection.workflow.id,
1783
+ title: selection.workflow.title,
1784
+ reasons: selection.reasons,
1785
+ firstPhase: selection.firstPhase.title,
1786
+ cautions: selection.cautions,
1787
+ })),
1788
+ parseErrors: result.errors,
1789
+ });
1790
+ }
1791
+
1792
+ const requestedWorkflowId = requireText(workflowId, 'workflowId');
1793
+ if (!requestedWorkflowId) return text({ error: 'workflowId is required for this workflow action.' }, true);
1794
+ const workflow = workflowStore.getWorkflow(requestedWorkflowId);
1795
+ if (!workflow || workflow.workspaceId !== workspace.id) {
1796
+ return text({ error: 'Workflow was not found for this Knowledge Workspace.' }, true);
1797
+ }
1798
+
1799
+ if (action === 'workflow_preview' || action === 'workflow_apply') {
1800
+ const targetAgent = requireText(agent, 'agent');
1801
+ if (!targetAgent) return text({ error: 'agent is required for a workflow adapter action.' }, true);
1802
+ const result = action === 'workflow_preview'
1803
+ ? await previewWorkflowAdapter({ workflow, projectRoot, agent: targetAgent as any })
1804
+ : await applyWorkflowAdapter({ workflow, projectRoot, agent: targetAgent as any });
1805
+ return text({
1806
+ workflowId: workflow.id,
1807
+ agent: targetAgent,
1808
+ status: result.status,
1809
+ targetPath: result.targetPath,
1810
+ reason: result.reason,
1811
+ ...(action === 'workflow_preview' && result.content ? { content: result.content } : {}),
1812
+ });
1813
+ }
1814
+
1815
+ if (action === 'workflow_run') {
1816
+ const runTask = requireText(task, 'task');
1817
+ if (!runTask) return text({ error: 'task is required for workflow_run.' }, true);
1818
+ if (!outcome) return text({ error: 'outcome is required for workflow_run.' }, true);
1819
+ const result = await recordWorkflowRun({
1820
+ workspace,
1821
+ run: {
1822
+ workflowId: workflow.id,
1823
+ projectId: project.id,
1824
+ task: runTask,
1825
+ outcome,
1826
+ ...(verificationVerdict ? { verificationVerdict } : {}),
1827
+ ...(requireText(failureReason, 'failureReason') ? { failureReason: requireText(failureReason, 'failureReason') } : {}),
1828
+ ...(requireText(startingSnapshotId, 'startingSnapshotId') ? { startingSnapshotId: requireText(startingSnapshotId, 'startingSnapshotId') } : {}),
1829
+ selectedEvidence: [...new Set((evidenceIds ?? []).map(item => item.trim()).filter(Boolean))],
1830
+ },
1831
+ });
1832
+ return text({
1833
+ id: result.id,
1834
+ workflowId: result.workflowId,
1835
+ outcome: result.outcome,
1836
+ verificationVerdict: result.verificationVerdict,
1837
+ });
1838
+ }
1839
+
1840
+ return text({ error: 'Unsupported knowledge action.' }, true);
1841
+ },
1842
+ );
1843
+
1438
1844
  /**
1439
1845
  * memorix_resolve — Mark memories as resolved/completed
1440
1846
  *
@@ -2399,15 +2805,11 @@ export async function createMemorixServer(
2399
2805
  async function scopeGraphToProject(graph: { entities: any[]; relations: any[] }) {
2400
2806
  const { getAllObservations } = await import('./memory/observations.js');
2401
2807
  const allObs = await withFreshIndex(() => getAllObservations());
2402
- const projectEntityNames = new Set(
2403
- allObs
2404
- .filter(o => o.projectId === project.id && (o.status ?? 'active') === 'active' && o.entityName)
2405
- .map(o => o.entityName),
2808
+ const scoped = scopeKnowledgeGraphToProject(
2809
+ graph,
2810
+ allObs.filter(observation => observation.projectId === project.id),
2406
2811
  );
2407
- const entities = graph.entities.filter((e: any) => projectEntityNames.has(e.name));
2408
- const entityNameSet = new Set(entities.map((e: any) => e.name));
2409
- const relations = graph.relations.filter((r: any) => entityNameSet.has(r.from) && entityNameSet.has(r.to));
2410
- return { entities, relations };
2812
+ return { entities: scoped.entities, relations: scoped.relations };
2411
2813
  }
2412
2814
 
2413
2815
  /** read_graph — MCP Official compatible */
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { promises as fs } from 'node:fs';
12
+ import { randomUUID } from 'node:crypto';
12
13
  import path from 'node:path';
13
14
 
14
15
  /** Lock is considered stale after 10 seconds (process crash recovery) */
@@ -17,6 +18,7 @@ const LOCK_STALE_MS = 10_000;
17
18
  const RETRY_INTERVAL_MS = 50;
18
19
  /** Maximum retries before giving up (50ms × 60 = 3 seconds) */
19
20
  const MAX_RETRIES = 60;
21
+ const atomicWriteTails = new Map<string, Promise<void>>();
20
22
 
21
23
  /**
22
24
  * Acquire a lock file atomically.
@@ -93,8 +95,26 @@ export async function withFileLock<T>(projectDir: string, fn: () => Promise<T>):
93
95
  * On most filesystems, rename() is atomic within the same directory,
94
96
  * so readers always see either the old complete file or the new complete file.
95
97
  */
96
- export async function atomicWriteFile(filePath: string, data: string): Promise<void> {
97
- const tmpPath = filePath + `.tmp.${process.pid}`;
98
- await fs.writeFile(tmpPath, data, 'utf-8');
99
- await fs.rename(tmpPath, filePath);
98
+ async function atomicWriteFileOnce(filePath: string, data: string): Promise<void> {
99
+ const tmpPath = filePath + `.tmp.${process.pid}.${randomUUID()}`;
100
+ try {
101
+ await fs.writeFile(tmpPath, data, 'utf-8');
102
+ await fs.rename(tmpPath, filePath);
103
+ } catch (error) {
104
+ await fs.unlink(tmpPath).catch(() => {});
105
+ throw error;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Serialize same-process writes to one target. Windows refuses concurrent
111
+ * replacements of the same path even when each writer has its own temp file.
112
+ */
113
+ export function atomicWriteFile(filePath: string, data: string): Promise<void> {
114
+ const previous = atomicWriteTails.get(filePath) ?? Promise.resolve();
115
+ const next = previous.catch(() => undefined).then(() => atomicWriteFileOnce(filePath, data));
116
+ atomicWriteTails.set(filePath, next);
117
+ return next.finally(() => {
118
+ if (atomicWriteTails.get(filePath) === next) atomicWriteTails.delete(filePath);
119
+ });
100
120
  }