memorix 1.2.0 → 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.
@@ -22,6 +22,7 @@ import { loadDotenv } from '../config/dotenv-loader.js';
22
22
  import { resetDotenv } from '../config/dotenv-loader.js';
23
23
  import { initProjectRoot } from '../config/yaml-loader.js';
24
24
  import { clearProjectRoot } from '../config/yaml-loader.js';
25
+ import { scopeKnowledgeGraphToProject } from '../memory/graph-scope.js';
25
26
 
26
27
  // MIME types for static file serving
27
28
  const MIME_TYPES: Record<string, string> = {
@@ -92,22 +93,15 @@ export function prepareDashboardConfig(projectRoot: string | null): void {
92
93
 
93
94
  /**
94
95
  * Compute project-scoped graph counts from observations.
95
- * Only entities referenced by this project's active observations are counted.
96
+ * Active observation entities and their explicit cross-references are counted.
96
97
  */
97
98
  function computeProjectGraphCounts(
98
99
  allEntities: Array<{ name: string }>,
99
100
  allRelations: Array<{ from: string; to: string }>,
100
- projectObs: Array<{ entityName?: string; status?: string }>,
101
+ projectObs: Array<{ entityName?: string; relatedEntities?: string[]; status?: string }>,
101
102
  ): { entities: number; relations: number; entityNames: Set<string> } {
102
- const entityNames = new Set(
103
- projectObs
104
- .filter(o => (o.status ?? 'active') === 'active' && o.entityName)
105
- .map(o => o.entityName!),
106
- );
107
- const entities = allEntities.filter(e => entityNames.has(e.name));
108
- const entityNameSet = new Set(entities.map(e => e.name));
109
- const relations = allRelations.filter(r => entityNameSet.has(r.from) && entityNameSet.has(r.to));
110
- return { entities: entities.length, relations: relations.length, entityNames };
103
+ const scoped = scopeKnowledgeGraphToProject({ entities: allEntities, relations: allRelations }, projectObs);
104
+ return { entities: scoped.entities.length, relations: scoped.relations.length, entityNames: scoped.entityNames };
111
105
  }
112
106
 
113
107
  /**
@@ -210,17 +204,9 @@ async function handleApi(
210
204
  await initGraphStore(effectiveDataDir);
211
205
  const gStore = getGraphStore();
212
206
  const graph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
213
- // Project-scope the graph: only include entities that have observations in this project
214
- const graphObs = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }) as Array<{ entityName?: string }>;
215
- const projectEntityNames = new Set(
216
- graphObs
217
- .filter(o => o.entityName)
218
- .map(o => o.entityName!),
219
- );
220
- const entities = graph.entities.filter((e: any) => projectEntityNames.has(e.name));
221
- const entityNameSet = new Set(entities.map((e: any) => e.name));
222
- const relations = graph.relations.filter((r: any) => entityNameSet.has(r.from) && entityNameSet.has(r.to));
223
- sendJson(res, { entities, relations });
207
+ const graphObs = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' });
208
+ const scoped = scopeKnowledgeGraphToProject(graph, graphObs);
209
+ sendJson(res, { entities: scoped.entities, relations: scoped.relations });
224
210
  break;
225
211
  }
226
212
 
@@ -240,11 +226,11 @@ async function handleApi(
240
226
  case '/stats': {
241
227
  await initGraphStore(effectiveDataDir);
242
228
  const graph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
243
- const observations = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }) as Array<{ type?: string; id?: number; createdAt?: string; title?: string; entityName?: string; status?: string }>;
229
+ const observations = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }) as Array<{ type?: string; id?: number; createdAt?: string; title?: string; entityName?: string; relatedEntities?: string[]; status?: string }>;
244
230
  const nextId = await getObservationStore().loadIdCounter();
245
231
 
246
232
  // Project-scoped graph counts (must match /api/graph and /api/export)
247
- const projectGraphCounts = computeProjectGraphCounts(graph.entities, graph.relations, observations as Array<{ entityName?: string; status?: string }>);
233
+ const projectGraphCounts = computeProjectGraphCounts(graph.entities, graph.relations, observations);
248
234
 
249
235
  // Type counts (exclude probe -- operational heartbeats, not durable knowledge)
250
236
  const typeCounts: Record<string, number> = {};
@@ -461,24 +447,15 @@ async function handleApi(
461
447
  const allObs = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' });
462
448
  const skills = await getMiniSkillStore().loadByProject(effectiveProjectId);
463
449
 
464
- // Project-scope graph store (same logic as /api/graph)
465
450
  const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
466
- const graphObs = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }) as Array<{ entityName?: string }>;
467
- const projectEntityNames = new Set(
468
- graphObs
469
- .filter(o => o.entityName)
470
- .map(o => o.entityName!),
471
- );
472
- const scopedEntities = fullGraph.entities.filter((e: any) => projectEntityNames.has(e.name));
473
- const scopedEntityNameSet = new Set(scopedEntities.map((e: any) => e.name));
474
- const scopedRelations = fullGraph.relations.filter((r: any) => scopedEntityNameSet.has(r.from) && scopedEntityNameSet.has(r.to));
451
+ const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
475
452
 
476
453
  const graph = generateKnowledgeGraph({
477
454
  projectId: effectiveProjectId,
478
455
  observations: allObs,
479
456
  miniSkills: skills,
480
- graphEntities: scopedEntities,
481
- graphRelations: scopedRelations,
457
+ graphEntities: scoped.entities,
458
+ graphRelations: scoped.relations,
482
459
  });
483
460
 
484
461
  sendJson(res, graph);
@@ -747,19 +724,11 @@ async function handleApi(
747
724
  const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
748
725
  const observations = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' });
749
726
  const nextId = await getObservationStore().loadIdCounter();
750
- // Project-scope the graph: only entities referenced by this project's observations
751
- const exportEntityNames = new Set(
752
- observations
753
- .filter(o => (o.status ?? 'active') === 'active' && o.entityName)
754
- .map(o => o.entityName!),
755
- );
756
- const exportEntities = fullGraph.entities.filter((e: any) => exportEntityNames.has(e.name));
757
- const exportEntitySet = new Set(exportEntities.map((e: any) => e.name));
758
- const exportRelations = fullGraph.relations.filter((r: any) => exportEntitySet.has(r.from) && exportEntitySet.has(r.to));
727
+ const scoped = scopeKnowledgeGraphToProject(fullGraph, observations);
759
728
  const exportData = {
760
729
  project: { id: effectiveProjectId, name: effectiveProjectName },
761
730
  exportedAt: new Date().toISOString(),
762
- graph: { entities: exportEntities, relations: exportRelations },
731
+ graph: { entities: scoped.entities, relations: scoped.relations },
763
732
  observations,
764
733
  nextId,
765
734
  };
@@ -242,6 +242,56 @@ export function writeClaim(store: ClaimStore, input: KnowledgeClaimInput): Claim
242
242
  });
243
243
  }
244
244
 
245
+ /**
246
+ * Deliberately approve or reject a source-backed claim after its evidence has
247
+ * been checked. Derived agent observations never cross this boundary by
248
+ * themselves.
249
+ */
250
+ export function reviewClaim(
251
+ store: ClaimStore,
252
+ input: {
253
+ claimId: string;
254
+ reviewState: Extract<KnowledgeClaimReviewState, 'approved' | 'rejected'>;
255
+ detail: string;
256
+ },
257
+ ): KnowledgeClaim {
258
+ if (input.reviewState !== 'approved' && input.reviewState !== 'rejected') {
259
+ throw new Error('A claim review must approve or reject the claim');
260
+ }
261
+ const detail = compactText(input.detail, 'review detail');
262
+ return store.transaction(() => {
263
+ const claim = store.getClaim(input.claimId);
264
+ if (!claim) throw new Error('Claim was not found');
265
+ if (input.reviewState === 'approved' && claim.status === 'superseded') {
266
+ throw new Error('A superseded claim cannot be approved');
267
+ }
268
+ const now = new Date().toISOString();
269
+ const nextStatus: KnowledgeClaimStatus = input.reviewState === 'rejected'
270
+ ? 'unknown'
271
+ : claim.status === 'unknown' && claim.reviewState === 'rejected'
272
+ ? 'active'
273
+ : claim.status;
274
+ const updated: KnowledgeClaim = {
275
+ ...claim,
276
+ status: nextStatus,
277
+ reviewState: input.reviewState,
278
+ updatedAt: now,
279
+ };
280
+ store.updateClaim(updated);
281
+ store.recordEvent({
282
+ projectId: claim.projectId,
283
+ claimId: claim.id,
284
+ kind: 'reviewed',
285
+ fromStatus: claim.status,
286
+ toStatus: updated.status,
287
+ detail: 'Review: ' + claim.reviewState + ' -> ' + input.reviewState + '. ' + detail,
288
+ createdAt: now,
289
+ });
290
+ reconcileConflicts(store, claim.projectId, claim.conflictKey, now);
291
+ return store.getClaim(claim.id) ?? updated;
292
+ });
293
+ }
294
+
245
295
  export function supersedeClaim(
246
296
  store: ClaimStore,
247
297
  input: {
@@ -353,7 +403,7 @@ export function deriveLowRiskClaimsFromObservation(
353
403
  scope: 'project',
354
404
  confidence,
355
405
  observedAt: observation.updatedAt ?? observation.createdAt,
356
- reviewState: 'approved',
406
+ reviewState: isGit ? 'approved' : 'needs-review',
357
407
  origin: isGit ? 'git' : 'derived',
358
408
  evidence,
359
409
  });
@@ -59,6 +59,7 @@ export type KnowledgeClaimOrigin = 'explicit' | 'git' | 'derived' | 'model';
59
59
  export const CLAIM_EVENT_KINDS = [
60
60
  'created',
61
61
  'evidence-added',
62
+ 'reviewed',
62
63
  'conflicted',
63
64
  'superseded',
64
65
  'requalified',
@@ -7,6 +7,7 @@ import { atomicWriteFile, withFileLock } from '../store/file-lock.js';
7
7
  import { WorkflowSyncer } from '../workspace/workflow-sync.js';
8
8
  import { getKnowledgeWorkspacePaths, resolveKnowledgeWorkspaceFile } from './workspace.js';
9
9
  import { WorkflowStore } from './workflow-store.js';
10
+ import { containsTaskKeyword } from '../codegraph/task-lens.js';
10
11
  import type {
11
12
  WorkflowAdapterPreview,
12
13
  WorkflowAdapterTarget,
@@ -414,6 +415,9 @@ const LENS_TERMS: Record<string, string[]> = {
414
415
  test: ['test', 'verify', 'smoke', '测试', '验证'],
415
416
  };
416
417
 
418
+ const GENERIC_WORKFLOW_LENSES = new Set(['review', 'test']);
419
+ const GENERIC_WORKFLOW_TERMS = new Set(['review', 'audit', 'test', 'verify', 'smoke']);
420
+
417
421
  function inferTaskLenses(value: string): string[] {
418
422
  const text = value.toLowerCase();
419
423
  return Object.entries(LENS_TERMS)
@@ -425,20 +429,28 @@ function taskScore(workflow: WorkflowSpec, task: string): { score: number; reaso
425
429
  const text = task.toLowerCase();
426
430
  let score = 0;
427
431
  const reasons: string[] = [];
432
+ const matchedLenses = new Set<string>();
433
+ const matchedTriggers = new Set<string>();
428
434
  for (const lens of workflow.taskLenses) {
429
435
  const terms = LENS_TERMS[lens.toLowerCase()] ?? [lens.toLowerCase()];
430
- if (terms.some(term => text.includes(term))) {
436
+ if (terms.some(term => containsTaskKeyword(task, term))) {
431
437
  score += 20;
432
438
  reasons.push('matches ' + lens + ' workflow');
439
+ matchedLenses.add(lens.toLowerCase());
433
440
  }
434
441
  }
435
442
  for (const trigger of workflow.triggers) {
436
443
  const term = trigger.toLowerCase().trim();
437
- if (term.length >= 2 && text.includes(term)) {
444
+ if (term.length >= 2 && containsTaskKeyword(task, term)) {
438
445
  score += 8;
439
446
  reasons.push('matches trigger "' + trigger + '"');
447
+ matchedTriggers.add(term);
440
448
  }
441
449
  }
450
+ const hasSpecificLens = workflow.taskLenses.some(lens => !GENERIC_WORKFLOW_LENSES.has(lens.toLowerCase()));
451
+ const hasSpecificMatch = [...matchedLenses].some(lens => !GENERIC_WORKFLOW_LENSES.has(lens))
452
+ || [...matchedTriggers].some(trigger => !GENERIC_WORKFLOW_TERMS.has(trigger));
453
+ if (hasSpecificLens && !hasSpecificMatch) return { score: 0, reasons: [] };
442
454
  return { score, reasons: [...new Set(reasons)] };
443
455
  }
444
456
 
@@ -475,9 +487,28 @@ function importedWorkflowSpec(input: {
475
487
  sourcePath: string;
476
488
  raw: string;
477
489
  }): WorkflowSpec {
490
+ let sourceMatter: matter.GrayMatterFile<string>;
491
+ try {
492
+ sourceMatter = matter(input.raw);
493
+ } catch (error) {
494
+ throw new Error('Malformed Windsurf workflow Markdown: ' + (error instanceof Error ? error.message : String(error)));
495
+ }
496
+ if (sourceMatter.data.id !== undefined) {
497
+ const id = requiredText(sourceMatter.data as Record<string, unknown>, 'id');
498
+ const sourcePath = 'workflows/' + slug(id) + '.md';
499
+ const canonical = parsedWorkflow(sourceMatter.data as Record<string, unknown>, sourceMatter.content, {
500
+ workspaceId: input.workspace.id,
501
+ sourcePath,
502
+ contentHash: hash(input.raw),
503
+ });
504
+ return materializeWorkflow({
505
+ ...canonical,
506
+ importedFrom: input.sourcePath.replace(/\\/g, '/'),
507
+ });
508
+ }
478
509
  const entry = new WorkflowSyncer().parseWindsurfWorkflow(input.sourceName, input.raw);
479
510
  const content = entry.content.trim() || '## Execute\n\nFollow the imported workflow.';
480
- const lenses = inferTaskLenses(entry.name + '\n' + entry.description + '\n' + content);
511
+ const lenses = inferTaskLenses(entry.name + '\n' + entry.description);
481
512
  const createdAt = now();
482
513
  const sourcePath = 'workflows/' + slug(entry.name) + '.md';
483
514
  const spec: WorkflowSpec = {
@@ -56,6 +56,27 @@ export async function createAutoRelations(
56
56
  // Skip self-references
57
57
  const selfName = obs.entityName.toLowerCase();
58
58
 
59
+ const explicitRelated = [...new Set((obs.relatedEntities ?? [])
60
+ .map(name => name.trim())
61
+ .filter(name => name && name.toLowerCase() !== selfName))];
62
+ if (explicitRelated.length > 0) {
63
+ await graphManager.createEntities(explicitRelated.map(name => ({
64
+ name,
65
+ entityType: 'related',
66
+ observations: [],
67
+ })));
68
+ for (const name of explicitRelated) {
69
+ const matchedEntity = graphManager.findEntityByName(name);
70
+ if (matchedEntity) {
71
+ relations.push({
72
+ from: obs.entityName,
73
+ to: matchedEntity.name,
74
+ relationType: 'related_entity',
75
+ });
76
+ }
77
+ }
78
+ }
79
+
59
80
  // Check extracted identifiers against existing entities (O(1) lookups via index)
60
81
  const candidates = [
61
82
  ...extracted.identifiers,
@@ -0,0 +1,46 @@
1
+ export interface GraphScopeObservation {
2
+ entityName?: string;
3
+ relatedEntities?: string[];
4
+ status?: string;
5
+ }
6
+
7
+ export interface ScopedKnowledgeGraph<Entity extends { name: string }, Relation extends { from: string; to: string }> {
8
+ entities: Entity[];
9
+ relations: Relation[];
10
+ entityNames: Set<string>;
11
+ }
12
+
13
+ /** Return the active project entities, including explicit cross-references. */
14
+ export function projectGraphEntityNames(observations: readonly GraphScopeObservation[]): Set<string> {
15
+ const entityNames = new Set<string>();
16
+ for (const observation of observations) {
17
+ if ((observation.status ?? 'active') !== 'active') continue;
18
+ const entityName = observation.entityName?.trim();
19
+ if (entityName) entityNames.add(entityName);
20
+ for (const relatedEntity of observation.relatedEntities ?? []) {
21
+ const name = relatedEntity.trim();
22
+ if (name) entityNames.add(name);
23
+ }
24
+ }
25
+ return entityNames;
26
+ }
27
+
28
+ /**
29
+ * Give every graph surface the same project projection: active entities plus
30
+ * explicit related entities, with only edges whose endpoints remain visible.
31
+ */
32
+ export function scopeKnowledgeGraphToProject<
33
+ Entity extends { name: string },
34
+ Relation extends { from: string; to: string },
35
+ >(
36
+ graph: { entities: readonly Entity[]; relations: readonly Relation[] },
37
+ observations: readonly GraphScopeObservation[],
38
+ ): ScopedKnowledgeGraph<Entity, Relation> {
39
+ const entityNames = projectGraphEntityNames(observations);
40
+ const entities = graph.entities.filter(entity => entityNames.has(entity.name));
41
+ const visibleNames = new Set(entities.map(entity => entity.name));
42
+ const relations = graph.relations.filter(relation =>
43
+ visibleNames.has(relation.from) && visibleNames.has(relation.to),
44
+ );
45
+ return { entities, relations, entityNames };
46
+ }
@@ -9,7 +9,7 @@
9
9
  * is skipped and the task completes as before (backward compatible).
10
10
  */
11
11
 
12
- import { spawn, execSync } from 'node:child_process';
12
+ import { spawn } from 'node:child_process';
13
13
 
14
14
  // ── Types ──────────────────────────────────────────────────────────
15
15
 
@@ -63,6 +63,15 @@ export function runGate(
63
63
  return new Promise<GateResult>((resolve) => {
64
64
  const chunks: Buffer[] = [];
65
65
  let totalBytes = 0;
66
+ let settled = false;
67
+ let timer: ReturnType<typeof setTimeout> | undefined;
68
+
69
+ const finish = (result: GateResult) => {
70
+ if (settled) return;
71
+ settled = true;
72
+ if (timer) clearTimeout(timer);
73
+ resolve(result);
74
+ };
66
75
 
67
76
  const proc = spawn(command, {
68
77
  cwd,
@@ -86,9 +95,14 @@ export function runGate(
86
95
  let killed = false;
87
96
  const killTree = () => {
88
97
  try {
89
- if (process.platform === 'win32') {
90
- // Windows: kill entire process tree via taskkill
91
- execSync(`taskkill /F /T /PID ${proc.pid}`, { stdio: 'ignore' });
98
+ if (process.platform === 'win32' && proc.pid) {
99
+ // `shell: true` gives Windows a cmd.exe parent. Kill its whole tree
100
+ // without making the timeout path wait for a stubborn descendant.
101
+ const killer = spawn('taskkill.exe', ['/F', '/T', '/PID', String(proc.pid)], {
102
+ stdio: 'ignore',
103
+ windowsHide: true,
104
+ });
105
+ killer.unref();
92
106
  } else {
93
107
  // Unix: kill process group
94
108
  process.kill(-proc.pid!, 'SIGKILL');
@@ -97,20 +111,30 @@ export function runGate(
97
111
  // Fallback: kill the process directly
98
112
  try { proc.kill('SIGKILL'); } catch { /* already dead */ }
99
113
  }
114
+ try { proc.stdout?.destroy(); } catch { /* already closed */ }
115
+ try { proc.stderr?.destroy(); } catch { /* already closed */ }
116
+ proc.unref();
100
117
  };
101
118
 
102
- const timer = setTimeout(() => {
119
+ timer = setTimeout(() => {
103
120
  killed = true;
104
121
  killTree();
122
+ const output = Buffer.concat(chunks).toString('utf-8');
123
+ finish({
124
+ gate,
125
+ passed: false,
126
+ output: output + `\n[TIMEOUT] Gate "${gate}" killed after ${timeoutMs}ms`,
127
+ durationMs: Date.now() - start,
128
+ command,
129
+ });
105
130
  }, timeoutMs);
106
131
 
107
132
  proc.on('close', (code) => {
108
- clearTimeout(timer);
109
133
  const output = Buffer.concat(chunks).toString('utf-8');
110
134
  const durationMs = Date.now() - start;
111
135
 
112
136
  if (killed) {
113
- resolve({
137
+ finish({
114
138
  gate,
115
139
  passed: false,
116
140
  output: output + `\n[TIMEOUT] Gate "${gate}" killed after ${timeoutMs}ms`,
@@ -120,7 +144,7 @@ export function runGate(
120
144
  return;
121
145
  }
122
146
 
123
- resolve({
147
+ finish({
124
148
  gate,
125
149
  passed: code === 0,
126
150
  output,
@@ -130,8 +154,7 @@ export function runGate(
130
154
  });
131
155
 
132
156
  proc.on('error', (err) => {
133
- clearTimeout(timer);
134
- resolve({
157
+ finish({
135
158
  gate,
136
159
  passed: false,
137
160
  output: `[ERROR] Failed to spawn gate command: ${err.message}`,
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';
@@ -1425,7 +1426,7 @@ export async function createMemorixServer(
1425
1426
  { getResolvedConfig },
1426
1427
  { getExternalCodeGraphContext },
1427
1428
  { getObservationStore },
1428
- { collectCurrentProjectFacts },
1429
+ { collectCurrentProjectFacts, formatGitFact },
1429
1430
  { resolveTaskLens },
1430
1431
  ] = await Promise.all([
1431
1432
  import('./codegraph/store.js'),
@@ -1467,8 +1468,7 @@ export async function createMemorixServer(
1467
1468
  worksetFacts.push('Latest changelog: ' + currentFacts.latestChangelog.version
1468
1469
  + (currentFacts.latestChangelog.date ? ' (' + currentFacts.latestChangelog.date + ')' : ''));
1469
1470
  }
1470
- worksetFacts.push('Git: ' + (currentFacts.git.branch ? 'branch ' + currentFacts.git.branch + ', ' : '')
1471
- + (currentFacts.git.dirty ? 'dirty worktree' : 'clean worktree'));
1471
+ worksetFacts.push(formatGitFact(currentFacts.git));
1472
1472
  const codeState = snapshot
1473
1473
  ? '- Code state: ' + (snapshot.baseRevision ? snapshot.baseRevision.slice(0, 12) : 'Git unavailable')
1474
1474
  + ', ' + snapshot.worktreeState + ' worktree'
@@ -1513,11 +1513,13 @@ export async function createMemorixServer(
1513
1513
  {
1514
1514
  title: 'Knowledge Workspace',
1515
1515
  description:
1516
- 'Manage the reviewable project Knowledge Workspace. Use it only for deliberate knowledge operations: initialize a local or versioned workspace, inspect status, compile source-backed proposals, lint, apply a reviewed proposal, or manage canonical workflows. It is intentionally absent from the default micro/lite profiles.',
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
1517
  inputSchema: {
1518
1518
  action: z.enum([
1519
1519
  'workspace_init',
1520
1520
  'status',
1521
+ 'claim_list',
1522
+ 'claim_review',
1521
1523
  'compile',
1522
1524
  'lint',
1523
1525
  'proposal_apply',
@@ -1532,6 +1534,9 @@ export async function createMemorixServer(
1532
1534
  path: z.string().optional().describe('Explicit versioned workspace path, used only by workspace_init'),
1533
1535
  proposalId: z.string().optional().describe('Pending proposal id for proposal_apply'),
1534
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'),
1535
1540
  workflowId: z.string().optional().describe('Canonical workflow id for workflow preview, apply, or run'),
1536
1541
  agent: z.string().optional().describe('Target agent for a workflow adapter'),
1537
1542
  task: z.string().optional().describe('Task text for workflow selection or a workflow run'),
@@ -1548,6 +1553,9 @@ export async function createMemorixServer(
1548
1553
  path: workspacePath,
1549
1554
  proposalId,
1550
1555
  allowManualOverwrite,
1556
+ claimId,
1557
+ claimReviewState,
1558
+ reviewDetail,
1551
1559
  workflowId,
1552
1560
  agent,
1553
1561
  task,
@@ -1572,6 +1580,7 @@ export async function createMemorixServer(
1572
1580
 
1573
1581
  const [
1574
1582
  { ClaimStore },
1583
+ { reviewClaim },
1575
1584
  { CodeGraphStore },
1576
1585
  { initializeKnowledgeWorkspace, loadKnowledgeWorkspace },
1577
1586
  { KnowledgeWorkspaceStore },
@@ -1587,6 +1596,7 @@ export async function createMemorixServer(
1587
1596
  },
1588
1597
  ] = await Promise.all([
1589
1598
  import('./knowledge/claim-store.js'),
1599
+ import('./knowledge/claims.js'),
1590
1600
  import('./codegraph/store.js'),
1591
1601
  import('./knowledge/workspace.js'),
1592
1602
  import('./knowledge/workspace-store.js'),
@@ -1642,10 +1652,58 @@ export async function createMemorixServer(
1642
1652
  reason: proposal.reason,
1643
1653
  createdAt: proposal.createdAt,
1644
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 })),
1645
1658
  },
1646
1659
  });
1647
1660
  }
1648
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
+
1649
1707
  if (action === 'compile') {
1650
1708
  const result = await compileKnowledgeWorkspace({ workspace, claims });
1651
1709
  return text({
@@ -1688,7 +1746,13 @@ export async function createMemorixServer(
1688
1746
  if (action === 'workflow_import') {
1689
1747
  const result = await importWindsurfWorkflows({ workspace, projectRoot });
1690
1748
  return text({
1691
- imported: result.imported.map(workflow => ({ id: workflow.id, title: workflow.title, sourcePath: workflow.sourcePath })),
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
+ })),
1692
1756
  skipped: result.skipped,
1693
1757
  });
1694
1758
  }
@@ -1702,6 +1766,8 @@ export async function createMemorixServer(
1702
1766
  status: workflow.status,
1703
1767
  taskLenses: workflow.taskLenses,
1704
1768
  sourcePath: workflow.sourcePath,
1769
+ importedFrom: workflow.importedFrom,
1770
+ verificationGates: workflow.verificationGates,
1705
1771
  })),
1706
1772
  parseErrors: synced.errors,
1707
1773
  });
@@ -2739,15 +2805,11 @@ export async function createMemorixServer(
2739
2805
  async function scopeGraphToProject(graph: { entities: any[]; relations: any[] }) {
2740
2806
  const { getAllObservations } = await import('./memory/observations.js');
2741
2807
  const allObs = await withFreshIndex(() => getAllObservations());
2742
- const projectEntityNames = new Set(
2743
- allObs
2744
- .filter(o => o.projectId === project.id && (o.status ?? 'active') === 'active' && o.entityName)
2745
- .map(o => o.entityName),
2808
+ const scoped = scopeKnowledgeGraphToProject(
2809
+ graph,
2810
+ allObs.filter(observation => observation.projectId === project.id),
2746
2811
  );
2747
- const entities = graph.entities.filter((e: any) => projectEntityNames.has(e.name));
2748
- const entityNameSet = new Set(entities.map((e: any) => e.name));
2749
- const relations = graph.relations.filter((r: any) => entityNameSet.has(r.from) && entityNameSet.has(r.to));
2750
- return { entities, relations };
2812
+ return { entities: scoped.entities, relations: scoped.relations };
2751
2813
  }
2752
2814
 
2753
2815
  /** read_graph — MCP Official compatible */