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
@@ -319,9 +319,11 @@ export default defineCommand({
319
319
  const { CodeGraphStore } = await import('../../codegraph/store.js');
320
320
  const { buildProjectContextOverview } = await import('../../codegraph/project-context.js');
321
321
  const { getResolvedConfig } = await import('../../config/resolved-config.js');
322
+ const { inspectExternalCodeGraph } = await import('../../codegraph/external-provider.js');
322
323
  const codeStore = new CodeGraphStore();
323
324
  await codeStore.init(dataDir);
324
- const exclude = getResolvedConfig({ projectRoot: projectRoot || process.cwd() }).codegraph.excludePatterns;
325
+ const codegraphConfig = getResolvedConfig({ projectRoot: projectRoot || process.cwd() }).codegraph;
326
+ const exclude = codegraphConfig.excludePatterns;
325
327
  const overview = buildProjectContextOverview({
326
328
  project: {
327
329
  id: projectId,
@@ -335,6 +337,12 @@ export default defineCommand({
335
337
  const languageText = overview.code.languages.length > 0
336
338
  ? overview.code.languages.map((item: any) => `${item.language} ${item.files}`).join(', ')
337
339
  : 'none indexed yet';
340
+ const providerQuality = await inspectExternalCodeGraph({
341
+ projectRoot: projectRoot || process.cwd(),
342
+ mode: codegraphConfig.externalContext,
343
+ command: codegraphConfig.externalCommand,
344
+ timeoutMs: codegraphConfig.externalTimeoutMs,
345
+ });
338
346
 
339
347
  if (overview.code.files > 0) {
340
348
  lines.push(ok(`Code memory: ${overview.code.files} files, ${overview.code.symbols} symbols, ${overview.code.refs} memory links`));
@@ -344,6 +352,15 @@ export default defineCommand({
344
352
  tips.push('Run `memorix codegraph refresh` once to seed project context.');
345
353
  }
346
354
 
355
+ lines.push(info(
356
+ `Code provider: Lite durable index; external semantic graph ${providerQuality.health.state}`,
357
+ ));
358
+ if (providerQuality.health.state !== 'ready'
359
+ && providerQuality.health.state !== 'not-detected'
360
+ && providerQuality.health.state !== 'disabled') {
361
+ lines.push(warn(`External CodeGraph: ${providerQuality.health.reason ?? providerQuality.health.state}`));
362
+ }
363
+
347
364
  if (overview.freshness.stale > 0 || overview.freshness.suspect > 0) {
348
365
  lines.push(warn(`Freshness: ${overview.freshness.current} current, ${overview.freshness.suspect} suspect, ${overview.freshness.stale} stale`));
349
366
  } else {
@@ -362,6 +379,7 @@ export default defineCommand({
362
379
  refs: overview.code.refs,
363
380
  languages: overview.code.languages,
364
381
  freshness: overview.freshness,
382
+ providerQuality: providerQuality.quality,
365
383
  ...(overview.code.indexedAt ? { indexedAt: overview.code.indexedAt } : {}),
366
384
  };
367
385
  } catch (e) {
@@ -373,7 +391,58 @@ export default defineCommand({
373
391
  lines.push(info('Project context unavailable without a detected project.'));
374
392
  }
375
393
 
376
- // ── 6. Conflict Check ────────────────────────────────────────
394
+ // ── 6. Knowledge Lifecycle ──────────────────────────────────
395
+ lines.push('');
396
+ lines.push('┌─ Knowledge Lifecycle ─────────────────────────────');
397
+
398
+ if (projectId && dataDir) {
399
+ try {
400
+ const { collectLifecycleDiagnostics } = await import('../../runtime/lifecycle-status.js');
401
+ const lifecycle = await collectLifecycleDiagnostics({ dataDir, projectId });
402
+ report.lifecycle = lifecycle;
403
+
404
+ const queued = lifecycle.maintenance.summary.pending + lifecycle.maintenance.summary.retrying;
405
+ const running = lifecycle.maintenance.summary.running;
406
+ if (lifecycle.maintenance.summary.failed > 0) {
407
+ lines.push(warn(`Maintenance: ${lifecycle.maintenance.summary.failed} failed job(s)`));
408
+ for (const job of lifecycle.maintenance.failedJobs) {
409
+ lines.push(info(`Failed: ${job.kind} after ${job.attempts} attempt(s)`));
410
+ }
411
+ issues.push('Knowledge maintenance has failed work. Run `memorix doctor --json` to inspect the affected job kind.');
412
+ } else if (queued > 0 || running > 0) {
413
+ lines.push(info(`Maintenance: ${queued} queued, ${running} running`));
414
+ } else {
415
+ lines.push(ok('Maintenance: idle'));
416
+ }
417
+
418
+ lines.push(info(
419
+ `Claims: ${lifecycle.claims.active} active, ${lifecycle.claims.needsReview} need review, ${lifecycle.claims.unknown} unknown`,
420
+ ));
421
+ if (lifecycle.claims.conflicts > 0) {
422
+ lines.push(warn(`Claim conflicts: ${lifecycle.claims.conflicts}`));
423
+ issues.push('Source-backed claims conflict and need review before they guide work.');
424
+ }
425
+
426
+ if (lifecycle.workspaces.length === 0) {
427
+ lines.push(info('Knowledge workspace: not initialized'));
428
+ } else {
429
+ for (const workspace of lifecycle.workspaces) {
430
+ lines.push(info(
431
+ `${workspace.mode} workspace: ${workspace.pages} pages, ${workspace.pendingProposals} pending proposal(s), ${workspace.status}`,
432
+ ));
433
+ }
434
+ }
435
+ lines.push(info(`Workflows: ${lifecycle.workflows.active}/${lifecycle.workflows.total} active, ${lifecycle.workflows.failedRuns} failed run(s)`));
436
+ } catch (e) {
437
+ const message = e instanceof Error ? e.message : String(e);
438
+ lines.push(warn(`Knowledge lifecycle unavailable: ${message}`));
439
+ report.lifecycle = { error: message };
440
+ }
441
+ } else {
442
+ lines.push(info('Knowledge lifecycle unavailable without a detected project.'));
443
+ }
444
+
445
+ // ── 7. Conflict Check ────────────────────────────────────────
377
446
  lines.push('');
378
447
  lines.push('┌─ Conflict Check ──────────────────────────────────');
379
448
 
@@ -403,7 +472,7 @@ export default defineCommand({
403
472
  }
404
473
  report.conflicts = { found: conflictsFound };
405
474
 
406
- // ── 7. LLM Status ────────────────────────────────────────────
475
+ // ── 8. LLM Status ────────────────────────────────────────────
407
476
  lines.push('');
408
477
  lines.push('┌─ LLM Enhanced Mode ───────────────────────────────');
409
478
 
@@ -427,7 +496,7 @@ export default defineCommand({
427
496
  report.llm = { enabled: false };
428
497
  }
429
498
 
430
- // ── 8. Auto-Update Status ──────────────────────────────────
499
+ // ── 9. Auto-Update Status ──────────────────────────────────
431
500
  lines.push('');
432
501
  lines.push('┌─ Auto-Update ─────────────────────────────────────');
433
502
 
@@ -0,0 +1,322 @@
1
+ import { defineCommand } from 'citty';
2
+ import { CodeGraphStore } from '../../codegraph/store.js';
3
+ import { ClaimStore } from '../../knowledge/claim-store.js';
4
+ import { reviewClaim } from '../../knowledge/claims.js';
5
+ import { applyKnowledgeProposal, compileKnowledgeWorkspace, lintKnowledgeWorkspace } from '../../knowledge/wiki.js';
6
+ import { initializeKnowledgeWorkspace, loadKnowledgeWorkspace } from '../../knowledge/workspace.js';
7
+ import { KnowledgeWorkspaceStore } from '../../knowledge/workspace-store.js';
8
+ import { WorkflowStore } from '../../knowledge/workflow-store.js';
9
+ import {
10
+ applyWorkflowAdapter,
11
+ importWindsurfWorkflows,
12
+ previewWorkflowAdapter,
13
+ recordWorkflowRun,
14
+ selectWorkspaceWorkflows,
15
+ syncCanonicalWorkflows,
16
+ } from '../../knowledge/workflows.js';
17
+ import { emitError, emitResult, getCliProjectContext } from './operator-shared.js';
18
+
19
+ function modeFrom(value: unknown): 'local' | 'versioned' {
20
+ if (!value || value === 'local') return 'local';
21
+ if (value === 'versioned') return 'versioned';
22
+ throw new Error('knowledge mode must be local or versioned');
23
+ }
24
+
25
+ function usage(): string {
26
+ return [
27
+ 'Memorix Knowledge Commands',
28
+ '',
29
+ ' memorix knowledge init [--mode local]',
30
+ ' memorix knowledge init --mode versioned --path C:\\project\\docs\\knowledge',
31
+ ' memorix knowledge status',
32
+ ' memorix knowledge claims',
33
+ ' memorix knowledge review --id <claim-id> --review approved --detail "checked current source evidence"',
34
+ ' memorix knowledge compile',
35
+ ' memorix knowledge lint',
36
+ ' memorix knowledge apply --proposal <id> [--force]',
37
+ ' memorix knowledge workflow import',
38
+ ' memorix knowledge workflow list',
39
+ ' memorix knowledge workflow select --task "prepare a release"',
40
+ ' memorix knowledge workflow preview --id <workflow-id> --agent codex',
41
+ ' memorix knowledge workflow apply --id <workflow-id> --agent codex',
42
+ ' memorix knowledge workflow run --id <workflow-id> --task "..." --outcome passed',
43
+ ].join('\n');
44
+ }
45
+
46
+ function requiredText(value: unknown, field: string): string {
47
+ const text = typeof value === 'string' ? value.trim() : '';
48
+ if (!text) throw new Error(field + ' is required');
49
+ return text;
50
+ }
51
+
52
+ function workflowOutcome(value: unknown): 'passed' | 'failed' | 'cancelled' | 'in-progress' {
53
+ if (value === 'passed' || value === 'failed' || value === 'cancelled' || value === 'in-progress') return value;
54
+ throw new Error('workflow outcome must be passed, failed, cancelled, or in-progress');
55
+ }
56
+
57
+ function workflowVerdict(value: unknown): 'passed' | 'failed' | 'not-run' | undefined {
58
+ if (value === undefined || value === null || value === '') return undefined;
59
+ if (value === 'passed' || value === 'failed' || value === 'not-run') return value;
60
+ throw new Error('workflow verdict must be passed, failed, or not-run');
61
+ }
62
+
63
+ function claimReviewState(value: unknown): 'approved' | 'rejected' {
64
+ if (value === 'approved' || value === 'rejected') return value;
65
+ throw new Error('claim review must be approved or rejected');
66
+ }
67
+
68
+ function evidenceList(value: unknown): string[] {
69
+ if (typeof value !== 'string' || !value.trim()) return [];
70
+ return [...new Set(value.split(',').map(item => item.trim()).filter(Boolean))];
71
+ }
72
+
73
+ export default defineCommand({
74
+ meta: {
75
+ name: 'knowledge',
76
+ description: 'Initialize, review, and lint the Memorix Knowledge Workspace',
77
+ },
78
+ args: {
79
+ action: { type: 'string', description: 'Action: init, status, compile, lint, or apply' },
80
+ mode: { type: 'string', description: 'Workspace mode: local or versioned' },
81
+ path: { type: 'string', description: 'Explicit versioned workspace path for init' },
82
+ proposal: { type: 'string', description: 'Proposal id for apply' },
83
+ force: { type: 'boolean', description: 'Explicitly allow an approved proposal to replace manual page edits' },
84
+ id: { type: 'string', description: 'Canonical workflow id' },
85
+ agent: { type: 'string', description: 'Target agent for a workflow adapter' },
86
+ task: { type: 'string', description: 'Task wording for workflow selection or a workflow run' },
87
+ outcome: { type: 'string', description: 'Workflow run outcome: passed, failed, cancelled, or in-progress' },
88
+ verdict: { type: 'string', description: 'Workflow verification verdict: passed, failed, or not-run' },
89
+ failure: { type: 'string', description: 'Sanitized failure reason for a workflow run' },
90
+ snapshot: { type: 'string', description: 'Starting code snapshot id for a workflow run' },
91
+ evidence: { type: 'string', description: 'Comma-separated evidence ids selected for a workflow run' },
92
+ review: { type: 'string', description: 'Claim review verdict: approved or rejected' },
93
+ detail: { type: 'string', description: 'Evidence check performed for a claim review' },
94
+ json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
95
+ },
96
+ run: async ({ args }) => {
97
+ const positional = (args._ as string[]) ?? [];
98
+ const action = (positional[0] || (args.action as string | undefined) || 'status').toLowerCase();
99
+ const asJson = !!args.json;
100
+ try {
101
+ const { project, dataDir } = await getCliProjectContext();
102
+ const mode = modeFrom(args.mode);
103
+ if (action === 'init') {
104
+ const workspace = await initializeKnowledgeWorkspace({
105
+ projectId: project.id,
106
+ dataDir,
107
+ mode,
108
+ ...(mode === 'versioned'
109
+ ? {
110
+ projectRoot: project.rootPath,
111
+ rootPath: (args.path as string | undefined) ?? '',
112
+ }
113
+ : {}),
114
+ });
115
+ emitResult(
116
+ { project, workspace },
117
+ 'Knowledge workspace initialized at ' + workspace.rootPath,
118
+ asJson,
119
+ );
120
+ return;
121
+ }
122
+
123
+ const workspace = await loadKnowledgeWorkspace({ projectId: project.id, dataDir, mode });
124
+ if (!workspace) {
125
+ emitError('Knowledge workspace is not initialized. Run "memorix knowledge init" first.', asJson);
126
+ return;
127
+ }
128
+ const claimStore = new ClaimStore();
129
+ await claimStore.init(dataDir);
130
+ const workspaceStore = new KnowledgeWorkspaceStore();
131
+ await workspaceStore.init(dataDir);
132
+
133
+ if (action === 'claims') {
134
+ const claims = claimStore.listClaims(project.id, { limit: 100 });
135
+ emitResult(
136
+ { project, workspace, claims },
137
+ 'Claims: ' + claims.length + ' total, ' + claims.filter(claim => claim.reviewState === 'needs-review').length + ' awaiting review.',
138
+ asJson,
139
+ );
140
+ return;
141
+ }
142
+
143
+ if (action === 'review') {
144
+ const claimId = requiredText(args.id, 'claim id');
145
+ const existing = claimStore.getClaim(claimId);
146
+ if (!existing || existing.projectId !== project.id) {
147
+ emitError('Claim was not found for this knowledge workspace.', asJson);
148
+ return;
149
+ }
150
+ const claim = reviewClaim(claimStore, {
151
+ claimId,
152
+ reviewState: claimReviewState(args.review),
153
+ detail: requiredText(args.detail, 'review detail'),
154
+ });
155
+ emitResult(
156
+ { project, workspace, claim },
157
+ 'Claim ' + claim.id + ' marked ' + claim.reviewState + '.',
158
+ asJson,
159
+ );
160
+ return;
161
+ }
162
+
163
+ if (action === 'workflow') {
164
+ const workflowAction = (positional[1] || 'list').toLowerCase();
165
+ const workflowStore = new WorkflowStore();
166
+ await workflowStore.init(dataDir);
167
+ const projectRoot = workspace.projectRoot ?? project.rootPath;
168
+
169
+ if (workflowAction === 'import') {
170
+ const result = await importWindsurfWorkflows({ workspace, projectRoot });
171
+ emitResult(
172
+ { project, workspace, result },
173
+ 'Imported ' + result.imported.length + ' Windsurf workflow(s); preserved ' + result.skipped.length + ' existing source or canonical file(s).',
174
+ asJson,
175
+ );
176
+ return;
177
+ }
178
+
179
+ const synced = await syncCanonicalWorkflows(workspace);
180
+ if (workflowAction === 'list') {
181
+ emitResult(
182
+ {
183
+ project,
184
+ workspace,
185
+ workflows: workflowStore.listWorkflows(workspace.id),
186
+ parseErrors: synced.errors,
187
+ },
188
+ [
189
+ 'Canonical workflows: ' + workflowStore.listWorkflows(workspace.id).length,
190
+ 'Parse errors: ' + synced.errors.length,
191
+ ].join('\n'),
192
+ asJson,
193
+ );
194
+ return;
195
+ }
196
+
197
+ if (workflowAction === 'select') {
198
+ const task = requiredText(args.task, 'task');
199
+ const result = await selectWorkspaceWorkflows({ workspace, task });
200
+ emitResult(
201
+ { project, workspace, task, ...result },
202
+ result.selections.length
203
+ ? 'Selected ' + result.selections.length + ' workflow(s) for this task.'
204
+ : 'No project workflow matches this task.',
205
+ asJson,
206
+ );
207
+ return;
208
+ }
209
+
210
+ const workflowId = requiredText(args.id, 'workflow id');
211
+ const workflow = workflowStore.getWorkflow(workflowId);
212
+ if (!workflow || workflow.workspaceId !== workspace.id) {
213
+ emitError('Workflow was not found for this knowledge workspace.', asJson);
214
+ return;
215
+ }
216
+
217
+ if (workflowAction === 'preview' || workflowAction === 'apply') {
218
+ const agent = requiredText(args.agent, 'agent');
219
+ const result = workflowAction === 'preview'
220
+ ? await previewWorkflowAdapter({ workflow, projectRoot, agent: agent as any })
221
+ : await applyWorkflowAdapter({ workflow, projectRoot, agent: agent as any });
222
+ emitResult(
223
+ { project, workspace, result },
224
+ 'Workflow adapter ' + result.status + ': ' + result.reason,
225
+ asJson,
226
+ );
227
+ return;
228
+ }
229
+
230
+ if (workflowAction === 'run') {
231
+ const task = requiredText(args.task, 'task');
232
+ const outcome = workflowOutcome(args.outcome);
233
+ const result = await recordWorkflowRun({
234
+ workspace,
235
+ run: {
236
+ workflowId,
237
+ projectId: project.id,
238
+ task,
239
+ outcome,
240
+ ...(workflowVerdict(args.verdict) ? { verificationVerdict: workflowVerdict(args.verdict) } : {}),
241
+ ...(typeof args.failure === 'string' && args.failure.trim() ? { failureReason: args.failure.trim() } : {}),
242
+ ...(typeof args.snapshot === 'string' && args.snapshot.trim() ? { startingSnapshotId: args.snapshot.trim() } : {}),
243
+ selectedEvidence: evidenceList(args.evidence),
244
+ },
245
+ });
246
+ emitResult(
247
+ { project, workspace, result },
248
+ 'Recorded workflow run ' + result.id + ' (' + result.outcome + ').',
249
+ asJson,
250
+ );
251
+ return;
252
+ }
253
+
254
+ emitResult({ usage: usage() }, usage(), asJson);
255
+ return;
256
+ }
257
+
258
+ switch (action) {
259
+ case 'status': {
260
+ const pages = workspaceStore.listPages(workspace.id);
261
+ const pending = workspaceStore.listProposals(workspace.id, 'pending');
262
+ emitResult(
263
+ { project, workspace, pages, pendingProposals: pending },
264
+ [
265
+ 'Knowledge workspace: ' + workspace.rootPath,
266
+ '- Mode: ' + workspace.mode,
267
+ '- Published pages: ' + pages.filter(page => page.status === 'active').length,
268
+ '- Pending proposals: ' + pending.length,
269
+ '- Status: ' + workspace.status,
270
+ ].join('\n'),
271
+ asJson,
272
+ );
273
+ return;
274
+ }
275
+ case 'compile': {
276
+ const result = await compileKnowledgeWorkspace({ workspace, claims: claimStore });
277
+ emitResult(
278
+ { project, workspace, result },
279
+ 'Knowledge compilation completed: ' + result.proposals.length + ' proposal(s), ' + result.published.length + ' unchanged published page(s).',
280
+ asJson,
281
+ );
282
+ return;
283
+ }
284
+ case 'lint': {
285
+ const codeStore = new CodeGraphStore();
286
+ await codeStore.init(dataDir);
287
+ const result = await lintKnowledgeWorkspace({ workspace, claims: claimStore, codeStore });
288
+ emitResult(
289
+ { project, workspace, result },
290
+ result.valid
291
+ ? 'Knowledge workspace lint passed.'
292
+ : 'Knowledge workspace lint found ' + result.issues.length + ' issue(s).',
293
+ asJson,
294
+ );
295
+ return;
296
+ }
297
+ case 'apply': {
298
+ const proposalId = (args.proposal as string | undefined)?.trim();
299
+ if (!proposalId) {
300
+ emitError('proposal is required for "memorix knowledge apply"', asJson);
301
+ return;
302
+ }
303
+ const result = await applyKnowledgeProposal({
304
+ workspace,
305
+ proposalId,
306
+ allowManualOverwrite: !!args.force,
307
+ });
308
+ emitResult(
309
+ { project, workspace, result },
310
+ 'Knowledge proposal applied to ' + result.targetPath,
311
+ asJson,
312
+ );
313
+ return;
314
+ }
315
+ default:
316
+ emitResult({ usage: usage() }, usage(), asJson);
317
+ }
318
+ } catch (error) {
319
+ emitError(error instanceof Error ? error.message : String(error), asJson);
320
+ }
321
+ },
322
+ });
@@ -23,6 +23,7 @@ import { defineCommand } from 'citty';
23
23
  import type { IncomingMessage, ServerResponse } from 'node:http';
24
24
  import type { ObservationStore } from '../../store/obs-store.js';
25
25
  import { resolveToolProfile } from '../../server/tool-profile.js';
26
+ import { scopeKnowledgeGraphToProject } from '../../memory/graph-scope.js';
26
27
 
27
28
  export const DEFAULT_SESSION_TIMEOUT_MS = 30 * 60 * 1000;
28
29
 
@@ -764,6 +765,7 @@ export default defineCommand({
764
765
  source?: string;
765
766
  commitHash?: string;
766
767
  filesModified?: string[];
768
+ relatedEntities?: string[];
767
769
  status?: string;
768
770
  importance?: number;
769
771
  accessCount?: number;
@@ -860,22 +862,20 @@ export default defineCommand({
860
862
  } catch { /* best effort */ }
861
863
 
862
864
  // Project-scoped graph counts (must match /graph and /export)
863
- const projectEntityNames = new Set(
864
- observations.filter(o => o.entityName).map(o => o.entityName!)
865
- );
866
- const projectEntities = graph.entities.filter((e: any) => projectEntityNames.has(e.name));
867
- const projectEntitySet = new Set(projectEntities.map((e: any) => e.name));
868
- const projectRelations = graph.relations.filter((r: any) => projectEntitySet.has(r.from) && projectEntitySet.has(r.to));
865
+ const projectGraph = scopeKnowledgeGraphToProject(graph, observations);
869
866
 
870
867
  let maintenance = { total: 0, pending: 0, running: 0, retrying: 0, completed: 0, failed: 0 };
868
+ let lifecycle: unknown;
871
869
  try {
872
870
  const { MaintenanceJobStore } = await import('../../runtime/maintenance-jobs.js');
873
871
  maintenance = new MaintenanceJobStore(statsDataDir).summary(statsProjectId);
872
+ const { collectLifecycleDiagnostics } = await import('../../runtime/lifecycle-status.js');
873
+ lifecycle = await collectLifecycleDiagnostics({ dataDir: statsDataDir, projectId: statsProjectId });
874
874
  } catch { /* optional maintenance diagnostics */ }
875
875
 
876
876
  sendJson({
877
- entities: projectEntities.length,
878
- relations: projectRelations.length,
877
+ entities: projectGraph.entities.length,
878
+ relations: projectGraph.relations.length,
879
879
  observations: observations.length,
880
880
  nextId,
881
881
  typeCounts,
@@ -892,17 +892,25 @@ export default defineCommand({
892
892
  },
893
893
  retentionSummary,
894
894
  maintenance,
895
+ ...(lifecycle ? { lifecycle } : {}),
895
896
  });
896
897
  return;
897
898
  }
898
899
 
899
900
  if (apiPath === '/maintenance') {
900
901
  const { projectId: maintenanceProjectId, dataDir: maintenanceDataDir } = await resolveRequestProject(url);
901
- const { MaintenanceJobStore } = await import('../../runtime/maintenance-jobs.js');
902
+ const [{ MaintenanceJobStore }, { collectLifecycleDiagnostics }] = await Promise.all([
903
+ import('../../runtime/maintenance-jobs.js'),
904
+ import('../../runtime/lifecycle-status.js'),
905
+ ]);
902
906
  const queue = new MaintenanceJobStore(maintenanceDataDir);
903
907
  sendJson({
904
908
  summary: queue.summary(maintenanceProjectId),
905
909
  jobs: queue.list({ projectId: maintenanceProjectId, limit: 50 }),
910
+ lifecycle: await collectLifecycleDiagnostics({
911
+ dataDir: maintenanceDataDir,
912
+ projectId: maintenanceProjectId,
913
+ }),
906
914
  });
907
915
  return;
908
916
  }
@@ -919,19 +927,9 @@ export default defineCommand({
919
927
  await initGraphStore(graphDataDir);
920
928
  const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
921
929
 
922
- // Project-scope the graph: only include entities that have observations in this project
923
- const allObs = await loadDashboardProjectObservations(graphDataDir, graphProjectId, 'active') as Array<{ entityName?: string }>;
924
- const projectEntityNames = new Set(
925
- allObs
926
- .filter(o => o.entityName)
927
- .map(o => o.entityName!)
928
- );
929
-
930
- const entities = fullGraph.entities.filter(e => projectEntityNames.has(e.name));
931
- const entityNameSet = new Set(entities.map(e => e.name));
932
- const relations = fullGraph.relations.filter(r => entityNameSet.has(r.from) && entityNameSet.has(r.to));
933
-
934
- sendJson({ entities, relations });
930
+ const allObs = await loadDashboardProjectObservations(graphDataDir, graphProjectId, 'active');
931
+ const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
932
+ sendJson({ entities: scoped.entities, relations: scoped.relations });
935
933
  return;
936
934
  }
937
935
 
@@ -1001,24 +999,15 @@ export default defineCommand({
1001
999
  const allObs = await loadDashboardProjectObservations(kgDataDir, kgProjectId, 'active');
1002
1000
  const skills = await getMiniSkillStore().loadByProject(kgProjectId);
1003
1001
 
1004
- // Project-scope graph store (same logic as /api/graph)
1005
1002
  const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
1006
- const graphObs = await loadDashboardProjectObservations(kgDataDir, kgProjectId, 'active') as Array<{ entityName?: string }>;
1007
- const projectEntityNames = new Set(
1008
- graphObs
1009
- .filter(o => o.entityName)
1010
- .map(o => o.entityName!)
1011
- );
1012
- const scopedEntities = fullGraph.entities.filter(e => projectEntityNames.has(e.name));
1013
- const scopedEntityNameSet = new Set(scopedEntities.map(e => e.name));
1014
- const scopedRelations = fullGraph.relations.filter(r => scopedEntityNameSet.has(r.from) && scopedEntityNameSet.has(r.to));
1003
+ const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
1015
1004
 
1016
1005
  const graph = generateKnowledgeGraph({
1017
1006
  projectId: kgProjectId,
1018
1007
  observations: allObs,
1019
1008
  miniSkills: skills,
1020
- graphEntities: scopedEntities,
1021
- graphRelations: scopedRelations,
1009
+ graphEntities: scoped.entities,
1010
+ graphRelations: scoped.relations,
1022
1011
  });
1023
1012
 
1024
1013
  sendJson(graph);
@@ -1358,19 +1347,14 @@ export default defineCommand({
1358
1347
  const { initGraphStore, getGraphStore } = await import('../../store/graph-store.js');
1359
1348
  await initGraphStore(expDataDir);
1360
1349
  const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
1361
- const observations = await loadDashboardProjectObservations(expDataDir, expProjectId, 'active') as Array<{ entityName?: string }>;
1350
+ const observations = await loadDashboardProjectObservations(expDataDir, expProjectId, 'active');
1362
1351
  const expStore = await getDashboardObservationStore(expDataDir);
1363
1352
  const nextId = await expStore.loadIdCounter();
1364
- const exportEntityNames = new Set(
1365
- observations.filter(o => o.entityName).map(o => o.entityName!),
1366
- );
1367
- const exportEntities = fullGraph.entities.filter((e: any) => exportEntityNames.has(e.name));
1368
- const exportEntitySet = new Set(exportEntities.map((e: any) => e.name));
1369
- const exportRelations = fullGraph.relations.filter((r: any) => exportEntitySet.has(r.from) && exportEntitySet.has(r.to));
1353
+ const scoped = scopeKnowledgeGraphToProject(fullGraph, observations);
1370
1354
  const exportData = {
1371
1355
  project: { id: expProjectId, name: expProjectName },
1372
1356
  exportedAt: new Date().toISOString(),
1373
- graph: { entities: exportEntities, relations: exportRelations },
1357
+ graph: { entities: scoped.entities, relations: scoped.relations },
1374
1358
  observations,
1375
1359
  nextId,
1376
1360
  };
@@ -1062,7 +1062,7 @@ function tryInstallOmpPackage(packagePath: string): { ok: boolean; message: stri
1062
1062
  };
1063
1063
  }
1064
1064
 
1065
- async function installAgentSetup(agent: AgentName, plan: SetupPlan, global: boolean): Promise<void> {
1065
+ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, global: boolean): Promise<void> {
1066
1066
  const { installAgentGuidance, installHooks } = await import('../../hooks/installers/index.js');
1067
1067
  const targetRoot = global ? homedir() : process.cwd();
1068
1068
  const hasPluginPackage = plan.actions.includes('plugin-package') && PLUGIN_PACKAGE_AGENTS.has(agent);
@@ -1200,13 +1200,19 @@ async function installAgentSetup(agent: AgentName, plan: SetupPlan, global: bool
1200
1200
  } else if (hasOmpPackage) {
1201
1201
  p.log.info(`${agent}: extension command, hook events, and skills are bundled in the Oh-my-Pi package`);
1202
1202
  } else if (hasExtensionPackage) {
1203
- if (plan.actions.includes('hooks') || plan.actions.includes('project-guidance')) {
1203
+ if (plan.actions.includes('hooks')) {
1204
1204
  const result = await installHooks(agent, targetRoot, global);
1205
1205
  p.log.success(`${agent}: integration files -> ${result.configPath}`);
1206
+ } else if (plan.actions.includes('project-guidance')) {
1207
+ const rulesPath = await installAgentGuidance(agent, targetRoot, global);
1208
+ p.log.success(`${agent}: guidance -> ${rulesPath}`);
1206
1209
  }
1207
- } else if (agent === 'opencode' || plan.actions.includes('hooks') || plan.actions.includes('project-guidance')) {
1210
+ } else if (plan.actions.includes('hooks')) {
1208
1211
  const result = await installHooks(agent, targetRoot, global);
1209
1212
  p.log.success(`${agent}: integration files -> ${result.configPath}`);
1213
+ } else if (plan.actions.includes('project-guidance')) {
1214
+ const rulesPath = await installAgentGuidance(agent, targetRoot, global);
1215
+ p.log.success(`${agent}: guidance -> ${rulesPath}`);
1210
1216
  }
1211
1217
 
1212
1218
  if (agent === 'pi') {
package/src/cli/index.ts CHANGED
@@ -989,6 +989,7 @@ const main = defineCommand({
989
989
  context: () => import('./commands/context.js').then(m => m.default),
990
990
  explain: () => import('./commands/explain.js').then(m => m.default),
991
991
  codegraph: () => import('./commands/codegraph.js').then(m => m.default),
992
+ knowledge: () => import('./commands/knowledge.js').then(m => m.default),
992
993
  reasoning: () => import('./commands/reasoning.js').then(m => m.default),
993
994
  retention: () => import('./commands/retention.js').then(m => m.default),
994
995
  formation: () => import('./commands/formation.js').then(m => m.default),
@@ -1048,7 +1049,7 @@ const main = defineCommand({
1048
1049
  // Detect by checking if the first CLI arg matches a registered subcommand name.
1049
1050
  const firstArg = process.argv[2];
1050
1051
  const knownSubs = ['ask', 'search', 'remember', 'recent', 'memcode', 'config',
1051
- 'init', 'setup', 'integrate', 'memory', 'context', 'explain', 'codegraph', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills',
1052
+ 'init', 'setup', 'integrate', 'memory', 'context', 'explain', 'codegraph', 'knowledge', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills',
1052
1053
  'session', 'team', 'task', 'message', 'lock', 'handoff', 'poll',
1053
1054
  'receipt',
1054
1055
  'serve', 'serve-http', 'status', 'sync',
@@ -1088,6 +1089,7 @@ const main = defineCommand({
1088
1089
  console.error(' context Show the Memory Autopilot brief for this project');
1089
1090
  console.error(' explain Explain where Memorix project context comes from');
1090
1091
  console.error(' codegraph Refresh/status/context-pack for CodeGraph Memory');
1092
+ console.error(' knowledge Review source-backed knowledge pages and project workflows');
1091
1093
  console.error(' reasoning Store/search decision rationale');
1092
1094
  console.error(' retention Inspect stale/archive status');
1093
1095
  console.error(' formation Inspect Memory Formation metrics');
@@ -598,7 +598,7 @@ export function WorkbenchApp({ version, onExitForInteractive }: AppProps): React
598
598
  setKnowledgeSelectedIdx(0);
599
599
  setLoading(false);
600
600
  navigateTo('knowledge');
601
- setStatusMsg({ text: kb ? `Knowledge Base: ${kb.stats.observationsUsed} obs, ${kb.stats.miniSkillsUsed} skills` : 'No knowledge available', type: 'info' });
601
+ setStatusMsg({ text: kb ? `Memory Overview: ${kb.stats.observationsUsed} obs, ${kb.stats.miniSkillsUsed} skills` : 'No memory overview available', type: 'info' });
602
602
  break;
603
603
  }
604
604
  case 'memory': {