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.
- package/CHANGELOG.md +14 -1
- package/README.md +4 -2
- package/README.zh-CN.md +4 -2
- package/dist/cli/index.js +677 -441
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +830 -646
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.js +110 -5
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +14 -1
- package/dist/sdk.js +830 -646
- package/dist/sdk.js.map +1 -1
- package/docs/API_REFERENCE.md +3 -1
- package/docs/DEVELOPMENT.md +4 -0
- package/docs/SETUP.md +7 -1
- package/docs/knowledge/workflows/memorix-release.md +57 -0
- package/package.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/src/cli/commands/codegraph.ts +2 -3
- package/src/cli/commands/knowledge.ts +40 -0
- package/src/cli/commands/serve-http.ts +14 -41
- package/src/cli/commands/setup.ts +9 -3
- package/src/codegraph/auto-context.ts +3 -15
- package/src/codegraph/current-facts.ts +19 -1
- package/src/codegraph/task-lens.ts +49 -5
- package/src/dashboard/server.ts +15 -46
- package/src/knowledge/claims.ts +51 -1
- package/src/knowledge/types.ts +1 -0
- package/src/knowledge/workflows.ts +34 -3
- package/src/memory/auto-relations.ts +21 -0
- package/src/memory/graph-scope.ts +46 -0
- package/src/orchestrate/verify-gate.ts +33 -10
- package/src/server.ts +75 -13
package/src/dashboard/server.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
103
|
-
|
|
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
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
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
|
|
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
|
|
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:
|
|
481
|
-
graphRelations:
|
|
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
|
-
|
|
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:
|
|
731
|
+
graph: { entities: scoped.entities, relations: scoped.relations },
|
|
763
732
|
observations,
|
|
764
733
|
nextId,
|
|
765
734
|
};
|
package/src/knowledge/claims.ts
CHANGED
|
@@ -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
|
});
|
package/src/knowledge/types.ts
CHANGED
|
@@ -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 =>
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
-
//
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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 => ({
|
|
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
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
.map(o => o.entityName),
|
|
2808
|
+
const scoped = scopeKnowledgeGraphToProject(
|
|
2809
|
+
graph,
|
|
2810
|
+
allObs.filter(observation => observation.projectId === project.id),
|
|
2746
2811
|
);
|
|
2747
|
-
|
|
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 */
|