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
@@ -553,8 +553,8 @@ export function WikiView({ knowledge, loading }: WikiViewProps): React.ReactElem
553
553
  if (loading) {
554
554
  return (
555
555
  <Box flexDirection="column" paddingX={1}>
556
- <Text color={COLORS.brand} bold>Knowledge Base</Text>
557
- <Text color={COLORS.muted}> LLM Wiki</Text>
556
+ <Text color={COLORS.brand} bold>Memory Overview</Text>
557
+ <Text color={COLORS.muted}> Generated from durable memory</Text>
558
558
  <Text color={COLORS.border}>{separator()}</Text>
559
559
  <Text color={COLORS.muted}>Loading...</Text>
560
560
  </Box>
@@ -564,19 +564,19 @@ export function WikiView({ knowledge, loading }: WikiViewProps): React.ReactElem
564
564
  if (!knowledge) {
565
565
  return (
566
566
  <Box flexDirection="column" paddingX={1}>
567
- <Text color={COLORS.brand} bold>Knowledge Base</Text>
568
- <Text color={COLORS.muted}> LLM Wiki</Text>
567
+ <Text color={COLORS.brand} bold>Memory Overview</Text>
568
+ <Text color={COLORS.muted}> Generated from durable memory</Text>
569
569
  <Text color={COLORS.border}>{separator()}</Text>
570
- <Text color={COLORS.warning}>No knowledge available for this project.</Text>
571
- <Text color={COLORS.textDim}>Store memories with /remember or /chat to build your Knowledge Base.</Text>
570
+ <Text color={COLORS.warning}>No memory overview is available for this project.</Text>
571
+ <Text color={COLORS.textDim}>Store durable memories with /remember or /chat to build your overview.</Text>
572
572
  </Box>
573
573
  );
574
574
  }
575
575
 
576
576
  return (
577
577
  <Box flexDirection="column" paddingX={1}>
578
- <Text color={COLORS.brand} bold>Knowledge Base</Text>
579
- <Text color={COLORS.muted}> LLM Wiki</Text>
578
+ <Text color={COLORS.brand} bold>Memory Overview</Text>
579
+ <Text color={COLORS.muted}> Generated from durable memory</Text>
580
580
  <Box>
581
581
  <Text color={COLORS.textDim}> Project: {knowledge.projectId}</Text>
582
582
  <Text color={COLORS.muted}> │ {knowledge.stats.observationsUsed} obs, {knowledge.stats.miniSkillsUsed} skills</Text>
@@ -31,7 +31,7 @@ export const SLASH_COMMANDS: SlashCommand[] = [
31
31
  { name: '/integrate', description: 'Set up an IDE', alias: '/setup' },
32
32
  { name: '/cleanup', description: 'Cleanup and purge' },
33
33
  { name: '/ingest', description: 'Git to Memory' },
34
- { name: '/wiki', description: 'Knowledge Base', alias: '/knowledge' },
34
+ { name: '/wiki', description: 'Memory overview', alias: '/knowledge' },
35
35
  { name: '/exit', description: 'Exit workbench', alias: '/q' },
36
36
  ];
37
37
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * GraphView — Knowledge graph text browser.
2
+ * GraphView — deterministic memory-map text browser.
3
3
  *
4
4
  * Modes: browse (clusters + nodes), detail (single node with edge summary).
5
5
  * Keyboard: f filter, up/down navigate, enter detail, k -> Knowledge, esc back.
@@ -168,7 +168,7 @@ export function GraphView({ projectId, inputFocused, onNavigateKnowledge }: Grap
168
168
  return (
169
169
  <Box flexDirection="column" paddingX={1}>
170
170
  <Box>
171
- <Text color={COLORS.brand} bold>Graph Node Detail</Text>
171
+ <Text color={COLORS.brand} bold>Memory Map Item</Text>
172
172
  </Box>
173
173
  <Text color={COLORS.border}>{separator()}</Text>
174
174
  <Box marginY={1} flexDirection="column">
@@ -190,7 +190,7 @@ export function GraphView({ projectId, inputFocused, onNavigateKnowledge }: Grap
190
190
  )}
191
191
  {/* Edge summary */}
192
192
  <Box marginY={1} flexDirection="column">
193
- <Text color={COLORS.brandDim} bold>Edges</Text>
193
+ <Text color={COLORS.brandDim} bold>Links</Text>
194
194
  <Text color={COLORS.muted}>Outgoing ({outgoing.length}): </Text>
195
195
  {outgoing.slice(0, 6).map((e) => {
196
196
  const targetNode = graph?.nodes.find((n) => n.id === e.target);
@@ -213,7 +213,7 @@ export function GraphView({ projectId, inputFocused, onNavigateKnowledge }: Grap
213
213
  {incoming.length > 6 && <Text color={COLORS.muted}> ... +{incoming.length - 6} more</Text>}
214
214
  </Box>
215
215
  <Box marginTop={1}>
216
- <Text color={COLORS.muted}>esc back | k {'>'} Knowledge</Text>
216
+ <Text color={COLORS.muted}>esc back | k {'>'} Memory Overview</Text>
217
217
  </Box>
218
218
  </Box>
219
219
  );
@@ -230,11 +230,12 @@ export function GraphView({ projectId, inputFocused, onNavigateKnowledge }: Grap
230
230
  <Box flexDirection="column" paddingX={1}>
231
231
  <Box flexDirection="column">
232
232
  <Box>
233
- <Text color={COLORS.brand} bold>Knowledge Graph</Text>
233
+ <Text color={COLORS.brand} bold>Memory Map</Text>
234
234
  {!loading && graph && (
235
235
  <Text color={COLORS.muted}> | {graph.stats.totalNodes} nodes, {graph.stats.totalEdges} edges, {graph.stats.clusterCount} clusters</Text>
236
236
  )}
237
237
  </Box>
238
+ <Text color={COLORS.textDim}>Deterministic links from shared entities, skill sources, and explicit relations</Text>
238
239
  {/* Filter bar */}
239
240
  {!loading && graph && (
240
241
  <Box marginTop={0}>
@@ -248,7 +249,7 @@ export function GraphView({ projectId, inputFocused, onNavigateKnowledge }: Grap
248
249
  {loading ? (
249
250
  <Text color={COLORS.muted}>Loading...</Text>
250
251
  ) : !graph ? (
251
- <Text color={COLORS.muted}>No graph data available. Store observations to build the knowledge graph.</Text>
252
+ <Text color={COLORS.muted}>No memory map data is available. Store durable observations to build deterministic links.</Text>
252
253
  ) : flatItems.length === 0 ? (
253
254
  <Text color={COLORS.muted}>
254
255
  {filterMode !== 'all' ? 'No nodes match this filter.' : 'No nodes in graph.'}
@@ -290,7 +291,7 @@ export function GraphView({ projectId, inputFocused, onNavigateKnowledge }: Grap
290
291
 
291
292
  <Box marginTop={1} flexDirection="column">
292
293
  <Text color={COLORS.textDim}>
293
- f filter | up/down navigate | enter detail | k {'>'} Knowledge
294
+ f filter | up/down navigate | enter detail | k {'>'} Memory Overview
294
295
  </Text>
295
296
  </Box>
296
297
  </Box>
@@ -1,5 +1,5 @@
1
1
  /**
2
- * KnowledgeView — Wiki browser for the shared knowledge layer.
2
+ * KnowledgeView — read-only memory overview browser.
3
3
  *
4
4
  * Displays ProjectKnowledgeOverview sections and items.
5
5
  * Each item shows its provenance refs; pressing 'm' on an item
@@ -36,8 +36,8 @@ export function KnowledgeView({ knowledge, loading, selectedItemIdx, itemCount }
36
36
  if (loading) {
37
37
  return (
38
38
  <Box flexDirection="column" paddingX={1}>
39
- <Text color={COLORS.brand} bold>Knowledge Base</Text>
40
- <Text color={COLORS.muted}> LLM Wiki</Text>
39
+ <Text color={COLORS.brand} bold>Memory Overview</Text>
40
+ <Text color={COLORS.muted}> Generated from durable memory</Text>
41
41
  <Text color={COLORS.border}>{separator()}</Text>
42
42
  <Text color={COLORS.muted}>Loading…</Text>
43
43
  </Box>
@@ -47,11 +47,11 @@ export function KnowledgeView({ knowledge, loading, selectedItemIdx, itemCount }
47
47
  if (!knowledge) {
48
48
  return (
49
49
  <Box flexDirection="column" paddingX={1}>
50
- <Text color={COLORS.brand} bold>Knowledge Base</Text>
51
- <Text color={COLORS.muted}> LLM Wiki</Text>
50
+ <Text color={COLORS.brand} bold>Memory Overview</Text>
51
+ <Text color={COLORS.muted}> Generated from durable memory</Text>
52
52
  <Text color={COLORS.border}>{separator()}</Text>
53
- <Text color={COLORS.warning}>No knowledge available for this project.</Text>
54
- <Text color={COLORS.textDim}>Store memories with /remember or use Workbench to build context.</Text>
53
+ <Text color={COLORS.warning}>No memory overview is available for this project.</Text>
54
+ <Text color={COLORS.textDim}>Store durable memories with /remember or use Workbench to build context.</Text>
55
55
  </Box>
56
56
  );
57
57
  }
@@ -68,8 +68,8 @@ export function KnowledgeView({ knowledge, loading, selectedItemIdx, itemCount }
68
68
  return (
69
69
  <Box flexDirection="column" paddingX={1}>
70
70
  <Box>
71
- <Text color={COLORS.brand} bold>Knowledge Base</Text>
72
- <Text color={COLORS.muted}> LLM Wiki</Text>
71
+ <Text color={COLORS.brand} bold>Memory Overview</Text>
72
+ <Text color={COLORS.muted}> Generated from durable memory</Text>
73
73
  </Box>
74
74
  <Box>
75
75
  <Text color={COLORS.textDim}> {knowledge.projectId}</Text>
@@ -1,10 +1,17 @@
1
1
  import { existsSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { getResolvedConfig } from '../config/resolved-config.js';
4
+ import { buildTaskWorkset, type TaskWorkset, type WorksetCaution } from '../knowledge/workset.js';
4
5
  import type { ProjectInfo } from '../types.js';
5
6
  import { backfillMissingObservationCodeRefs, type CodeRefBackfillResult } from './binder.js';
6
- import { collectCurrentProjectFacts, type CurrentProjectFacts } from './current-facts.js';
7
+ import { collectCurrentProjectFacts, formatGitFact, type CurrentProjectFacts } from './current-facts.js';
7
8
  import { refreshProjectLite } from './lite-provider.js';
9
+ import {
10
+ getExternalCodeGraphContext,
11
+ inspectExternalCodeGraph,
12
+ type ExternalCodeGraphRunner,
13
+ } from './external-provider.js';
14
+ import type { CodeGraphProviderQuality, ExternalCodeGraphOutline } from './types.js';
8
15
  import {
9
16
  buildProjectContextExplain,
10
17
  type ProjectContextExplain,
@@ -40,6 +47,8 @@ export interface AutoProjectContext {
40
47
  overview: ProjectContextOverview;
41
48
  explain: ProjectContextExplain;
42
49
  refresh: AutoContextRefreshResult;
50
+ providerQuality: CodeGraphProviderQuality;
51
+ workset: TaskWorkset;
43
52
  }
44
53
 
45
54
  export interface AutoProjectBrief {
@@ -102,6 +111,8 @@ export async function buildAutoProjectContext(input: {
102
111
  now?: Date;
103
112
  exclude?: string[];
104
113
  maxFileBytes?: number;
114
+ /** Test-only injection point; production uses the bounded local runner. */
115
+ externalRunner?: ExternalCodeGraphRunner;
105
116
  /**
106
117
  * When supplied, a needed refresh is queued instead of running in this
107
118
  * request. MCP and hook callers use this to keep their response path fast.
@@ -161,6 +172,17 @@ export async function buildAutoProjectContext(input: {
161
172
  store,
162
173
  activeProjectObservations(input.observations, input.project.id) as any,
163
174
  );
175
+ try {
176
+ const { enqueueClaimRequalification } = await import('../runtime/lifecycle.js');
177
+ enqueueClaimRequalification({
178
+ dataDir: input.dataDir,
179
+ projectId: input.project.id,
180
+ source: 'foreground-refresh',
181
+ snapshotId: store.latestSnapshot(input.project.id)?.id,
182
+ });
183
+ } catch {
184
+ // The completed scan remains useful even if its later maintenance cannot queue.
185
+ }
164
186
  refresh = { ...refresh, backfill };
165
187
  } catch (error) {
166
188
  refresh = {
@@ -180,15 +202,116 @@ export async function buildAutoProjectContext(input: {
180
202
  exclude,
181
203
  });
182
204
  const overview = explain.overview;
205
+ const currentFacts = collectCurrentProjectFacts({ project: input.project, now });
206
+ const latestSnapshot = overview.code.latestSnapshot;
207
+ const sourceSets = lensSourceSets({ task, lens, explain });
208
+ let externalOutline: ExternalCodeGraphOutline | undefined;
209
+ let externalCaution: string | undefined;
210
+ let providerQuality: CodeGraphProviderQuality;
211
+ if (task) {
212
+ const external = await getExternalCodeGraphContext({
213
+ projectRoot: input.project.rootPath,
214
+ task,
215
+ exclude,
216
+ mode: codegraphConfig.externalContext,
217
+ command: codegraphConfig.externalCommand,
218
+ timeoutMs: codegraphConfig.externalTimeoutMs,
219
+ ...(input.externalRunner ? { runner: input.externalRunner } : {}),
220
+ });
221
+ externalOutline = external.outline;
222
+ externalCaution = external.caution;
223
+ providerQuality = external.quality;
224
+ } else {
225
+ const external = await inspectExternalCodeGraph({
226
+ projectRoot: input.project.rootPath,
227
+ mode: codegraphConfig.externalContext,
228
+ command: codegraphConfig.externalCommand,
229
+ timeoutMs: codegraphConfig.externalTimeoutMs,
230
+ ...(input.externalRunner ? { runner: input.externalRunner } : {}),
231
+ });
232
+ providerQuality = external.quality;
233
+ }
234
+ const externalStartHere = externalOutline
235
+ ? [...externalOutline.relatedFiles, ...externalOutline.entryPoints.map(entry => entry.path)]
236
+ : [];
237
+ const startHere = [...new Set([
238
+ ...externalStartHere,
239
+ ...rankLensPaths([
240
+ ...existingLensCandidates(input.project.rootPath, lens),
241
+ ...overview.suggestedReads,
242
+ ], lens, task),
243
+ ])].slice(0, 5);
244
+ const runtimeCautions: WorksetCaution[] = [];
245
+ if (refresh.reason === 'queued') {
246
+ runtimeCautions.push({ kind: 'codegraph-refresh-queued', message: refresh.message });
247
+ } else if (refresh.reason === 'failed') {
248
+ runtimeCautions.push({ kind: 'codegraph-refresh-failed', message: refresh.message });
249
+ }
250
+ if (externalCaution) {
251
+ runtimeCautions.push({ kind: 'external-codegraph-fallback', message: externalCaution });
252
+ }
253
+ const workset = await buildTaskWorkset({
254
+ projectId: input.project.id,
255
+ dataDir: input.dataDir,
256
+ ...(task ? { task } : {}),
257
+ lens: lens.id,
258
+ startHere,
259
+ ...(externalOutline ? { semanticCode: externalOutline } : {}),
260
+ providerQuality,
261
+ currentFacts: worksetFactLines(currentFacts),
262
+ codeState: codeStateLine(overview),
263
+ reliableMemory: sourceSets.reliableSources
264
+ .slice(0, lens.sourceLimit)
265
+ .map(source => ({
266
+ id: source.observationId,
267
+ title: source.title,
268
+ type: source.type,
269
+ status: source.status,
270
+ ...(source.path ? { path: source.path } : {}),
271
+ ...(source.symbol ? { symbol: source.symbol } : {}),
272
+ })),
273
+ cautionMemory: sourceSets.cautionSources
274
+ .slice(0, lens.cautionLimit)
275
+ .map(source => ({
276
+ id: source.observationId,
277
+ title: source.title,
278
+ type: source.type,
279
+ status: source.status,
280
+ ...(source.path ? { path: source.path } : {}),
281
+ ...(source.symbol ? { symbol: source.symbol } : {}),
282
+ })),
283
+ hiddenCautionMemoryCount: sourceSets.hiddenCautionCount,
284
+ verificationHints: lensVerificationHints(lens),
285
+ worktreeDirty: currentFacts.git.dirty,
286
+ ...(latestSnapshot
287
+ ? {
288
+ snapshot: {
289
+ id: latestSnapshot.id,
290
+ sourceEpoch: latestSnapshot.sourceEpoch,
291
+ worktreeState: latestSnapshot.worktreeState,
292
+ incomplete: latestSnapshot.completeness.skippedOversizedFiles > 0
293
+ || (latestSnapshot.completeness.unreadableFiles ?? 0) > 0
294
+ || latestSnapshot.completeness.removalScanDeferred,
295
+ },
296
+ }
297
+ : {}),
298
+ freshness: {
299
+ suspect: overview.freshness.suspect,
300
+ stale: overview.freshness.stale,
301
+ },
302
+ runtimeCautions,
303
+ });
183
304
 
184
305
  return {
185
306
  project: input.project,
186
307
  ...(task ? { task } : {}),
187
308
  lens,
188
- currentFacts: collectCurrentProjectFacts({ project: input.project, now }),
309
+ currentFacts,
189
310
  overview,
190
311
  explain,
191
312
  refresh,
313
+ providerQuality,
314
+ workset,
192
315
  };
193
316
  }
194
317
 
@@ -198,6 +321,22 @@ function formatLanguages(overview: ProjectContextOverview): string {
198
321
  : 'none indexed yet';
199
322
  }
200
323
 
324
+ function codeStateLine(overview: ProjectContextOverview): string {
325
+ const snapshot = overview.code.latestSnapshot;
326
+ if (!snapshot) return '- Code state: no completed snapshot yet';
327
+ const revision = snapshot.baseRevision ? snapshot.baseRevision.slice(0, 12) : 'Git unavailable';
328
+ const scanState = snapshot.completeness.skippedOversizedFiles > 0
329
+ || (snapshot.completeness.unreadableFiles ?? 0) > 0
330
+ || snapshot.completeness.removalScanDeferred
331
+ ? 'incomplete scan'
332
+ : 'complete scan';
333
+ return '- Code state: ' + revision
334
+ + ', ' + snapshot.worktreeState + ' worktree'
335
+ + ', ' + snapshot.changedPathCount + ' changed path(s)'
336
+ + ', epoch ' + snapshot.sourceEpoch
337
+ + ', ' + scanState;
338
+ }
339
+
201
340
  function dedupeSourcesByObservation(
202
341
  sources: ProjectContextExplain['sources'],
203
342
  ): ProjectContextExplain['sources'] {
@@ -228,18 +367,14 @@ function existingLensCandidates(rootPath: string, lens: TaskLens): string[] {
228
367
  }
229
368
 
230
369
  function rankedStartHere(context: AutoProjectContext, limit = 8): string[] {
231
- const candidates = [
232
- ...existingLensCandidates(context.project.rootPath, context.lens),
233
- ...context.overview.suggestedReads,
234
- ];
235
- return rankLensPaths(candidates, context.lens, context.task).slice(0, limit);
370
+ return context.workset.startHere.slice(0, limit);
236
371
  }
237
372
 
238
373
  function lensLine(context: AutoProjectContext): string {
239
374
  return `Task lens: ${context.lens.id} - ${context.lens.description}`;
240
375
  }
241
376
 
242
- function lensSourceSets(context: AutoProjectContext): {
377
+ function lensSourceSets(context: Pick<AutoProjectContext, 'task' | 'lens' | 'explain'>): {
243
378
  reliableSources: ProjectContextExplain['sources'];
244
379
  cautionSources: ProjectContextExplain['sources'];
245
380
  hiddenReliableCount: number;
@@ -275,7 +410,7 @@ export function buildAutoProjectBrief(context: AutoProjectContext): AutoProjectB
275
410
  return {
276
411
  lens: context.lens.id,
277
412
  lensDescription: context.lens.description,
278
- startHere: rankedStartHere(context),
413
+ startHere: context.workset.startHere,
279
414
  reliableMemoryIds: reliableSources
280
415
  .slice(0, context.lens.sourceLimit)
281
416
  .map(source => source.observationId),
@@ -284,7 +419,7 @@ export function buildAutoProjectBrief(context: AutoProjectContext): AutoProjectB
284
419
  .map(source => source.observationId),
285
420
  hiddenReliableCount,
286
421
  hiddenCautionCount,
287
- suggestedVerification: lensVerificationHints(context.lens),
422
+ suggestedVerification: context.workset.verification,
288
423
  };
289
424
  }
290
425
 
@@ -295,15 +430,7 @@ function formatCurrentFactsLines(facts: CurrentProjectFacts): string[] {
295
430
  lines.push(`- Latest changelog: ${facts.latestChangelog.version}${facts.latestChangelog.date ? ` (${facts.latestChangelog.date})` : ''}`);
296
431
  }
297
432
 
298
- const gitParts: string[] = [];
299
- if (facts.git.detached) {
300
- gitParts.push('detached HEAD');
301
- } else if (facts.git.branch) {
302
- gitParts.push(`branch ${facts.git.branch}`);
303
- }
304
- if (facts.git.commit) gitParts.push(`commit ${facts.git.commit}`);
305
- gitParts.push(facts.git.dirty ? 'dirty worktree' : 'clean worktree');
306
- lines.push(`- Git: ${gitParts.join(', ')}`);
433
+ lines.push('- ' + formatGitFact(facts.git));
307
434
  if (facts.git.latestCommit) lines.push(`- Latest commit: ${facts.git.latestCommit}`);
308
435
  lines.push('- Current facts above outrank progress/dev-log files when they conflict.');
309
436
 
@@ -322,6 +449,23 @@ function formatCurrentFactsLines(facts: CurrentProjectFacts): string[] {
322
449
  return lines;
323
450
  }
324
451
 
452
+ function worksetFactLines(facts: CurrentProjectFacts): string[] {
453
+ const lines: string[] = [];
454
+ if (facts.packageVersion) lines.push('Package version: ' + facts.packageVersion);
455
+ if (facts.latestChangelog) {
456
+ lines.push('Latest changelog: ' + facts.latestChangelog.version
457
+ + (facts.latestChangelog.date ? ' (' + facts.latestChangelog.date + ')' : ''));
458
+ }
459
+ lines.push(formatGitFact(facts.git));
460
+ for (const note of facts.staleNotes.slice(0, 1)) {
461
+ lines.push(
462
+ 'Historical note: ' + note.path
463
+ + (note.branchHint ? ' (branch hint ' + note.branchHint + '; ' + note.reason + ')' : ' (' + note.reason + ')'),
464
+ );
465
+ }
466
+ return lines;
467
+ }
468
+
325
469
  export function formatAutoProjectContextSummary(context: AutoProjectContext): string {
326
470
  const reliableSources = rankLensSources(
327
471
  dedupeSourcesByObservation(context.explain.sources.filter(source => source.status === 'current')),
@@ -337,6 +481,7 @@ export function formatAutoProjectContextSummary(context: AutoProjectContext): st
337
481
  ...formatCurrentFactsLines(context.currentFacts),
338
482
  '',
339
483
  `- Code memory: ${context.overview.code.files} files / ${context.overview.code.symbols} symbols / ${context.overview.code.refs} memory links`,
484
+ `- Code provider: ${context.providerQuality.selected} (${context.providerQuality.selectedQuality})`,
340
485
  `- Languages: ${formatLanguages(context.overview)}`,
341
486
  `- Memories: ${context.overview.memory.active} active / ${context.overview.memory.total} total`,
342
487
  `- Freshness: ${context.overview.freshness.current} current, ${context.overview.freshness.suspect} suspect, ${context.overview.freshness.stale} stale`,
@@ -363,6 +508,10 @@ export function formatAutoProjectContextSummary(context: AutoProjectContext): st
363
508
  }
364
509
 
365
510
  export function formatAutoProjectContextPrompt(context: AutoProjectContext): string {
511
+ return context.workset.prompt;
512
+ }
513
+
514
+ export function formatLegacyAutoProjectContextPrompt(context: AutoProjectContext): string {
366
515
  const lines = [
367
516
  `Memorix Autopilot Brief for ${context.project.name}`,
368
517
  context.task ? `Task: ${context.task}` : '',
@@ -371,6 +520,7 @@ export function formatAutoProjectContextPrompt(context: AutoProjectContext): str
371
520
  ...formatCurrentFactsLines(context.currentFacts),
372
521
  '',
373
522
  'Project state',
523
+ codeStateLine(context.overview),
374
524
  `- Code memory: ${context.overview.code.files} files, ${context.overview.code.symbols} symbols, ${context.overview.code.refs} memory links`,
375
525
  `- Languages: ${formatLanguages(context.overview)}`,
376
526
  `- Memories: ${context.overview.memory.active} active / ${context.overview.memory.total} total`,
@@ -0,0 +1,95 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { promisify } from 'node:util';
4
+ import type {
5
+ CodeGraphProviderKind,
6
+ CodeStateScanCompleteness,
7
+ CodeStateSnapshotInput,
8
+ CodeStateWorktreeState,
9
+ } from './types.js';
10
+
11
+ const execFileAsync = promisify(execFile);
12
+ const GIT_TIMEOUT_MS = 3_000;
13
+ const GIT_MAX_BUFFER_BYTES = 256 * 1024;
14
+
15
+ interface GitResult {
16
+ ok: boolean;
17
+ output: Buffer;
18
+ }
19
+
20
+ async function runGit(rootPath: string, args: string[]): Promise<GitResult> {
21
+ try {
22
+ const result = await execFileAsync('git', ['-C', rootPath, ...args], {
23
+ encoding: 'buffer',
24
+ windowsHide: true,
25
+ timeout: GIT_TIMEOUT_MS,
26
+ maxBuffer: GIT_MAX_BUFFER_BYTES,
27
+ });
28
+ const output = Buffer.isBuffer(result.stdout)
29
+ ? result.stdout
30
+ : Buffer.from(result.stdout ?? '');
31
+ return { ok: true, output };
32
+ } catch {
33
+ return { ok: false, output: Buffer.alloc(0) };
34
+ }
35
+ }
36
+
37
+ function changedPathCount(status: Buffer): number {
38
+ const entries = status.toString('utf8').split('\0');
39
+ let count = 0;
40
+ for (let index = 0; index < entries.length; index++) {
41
+ const entry = entries[index];
42
+ if (!entry) continue;
43
+ count++;
44
+ const code = entry.slice(0, 2);
45
+ if (code.includes('R') || code.includes('C')) index++;
46
+ }
47
+ return count;
48
+ }
49
+
50
+ function fingerprint(baseRevision: string | undefined, status: Buffer): string {
51
+ return createHash('sha256')
52
+ .update(baseRevision ?? '')
53
+ .update('\0')
54
+ .update(status)
55
+ .digest('hex');
56
+ }
57
+
58
+ export interface CollectCodeStateInput {
59
+ projectId: string;
60
+ projectRoot: string;
61
+ provider: CodeGraphProviderKind;
62
+ indexedAt: string;
63
+ completeness: CodeStateScanCompleteness;
64
+ }
65
+
66
+ /**
67
+ * Capture bounded Git metadata for a completed scan. No source file contents
68
+ * are copied into the snapshot, and an unavailable Git executable is explicit
69
+ * instead of being treated as a clean worktree.
70
+ */
71
+ export async function collectCodeStateSnapshot(
72
+ input: CollectCodeStateInput,
73
+ ): Promise<CodeStateSnapshotInput> {
74
+ const [revision, status] = await Promise.all([
75
+ runGit(input.projectRoot, ['rev-parse', 'HEAD']),
76
+ runGit(input.projectRoot, ['status', '--porcelain=v1', '-z', '--untracked-files=normal']),
77
+ ]);
78
+ const baseRevision = revision.ok
79
+ ? revision.output.toString('utf8').trim() || undefined
80
+ : undefined;
81
+ const worktreeState: CodeStateWorktreeState = status.ok
82
+ ? (status.output.length > 0 ? 'dirty' : 'clean')
83
+ : 'unavailable';
84
+
85
+ return {
86
+ projectId: input.projectId,
87
+ provider: input.provider,
88
+ ...(baseRevision ? { baseRevision } : {}),
89
+ worktreeFingerprint: fingerprint(baseRevision, status.ok ? status.output : Buffer.alloc(0)),
90
+ worktreeState,
91
+ changedPathCount: status.ok ? changedPathCount(status.output) : 0,
92
+ indexedAt: input.indexedAt,
93
+ completeness: input.completeness,
94
+ };
95
+ }
@@ -1,5 +1,13 @@
1
1
  import { evaluateCodeRefFreshness } from './freshness.js';
2
- import type { CodeFile, CodeRefStatus, CodeSymbol, ObservationCodeRef } from './types.js';
2
+ import { buildTaskWorkset, type TaskWorkset, type WorksetCaution } from '../knowledge/workset.js';
3
+ import type {
4
+ CodeFile,
5
+ CodeGraphProviderQuality,
6
+ CodeRefStatus,
7
+ CodeSymbol,
8
+ ExternalCodeGraphOutline,
9
+ ObservationCodeRef,
10
+ } from './types.js';
3
11
  import type { CodeGraphStore } from './store.js';
4
12
  import { isCodeGraphExcludedPath } from './exclude.js';
5
13
 
@@ -9,6 +17,8 @@ export interface ContextPackMemory {
9
17
  type: string;
10
18
  status: CodeRefStatus | 'unbound';
11
19
  reason: string;
20
+ path?: string;
21
+ symbol?: string;
12
22
  }
13
23
 
14
24
  export interface ContextPackCodeFact {
@@ -32,6 +42,8 @@ export interface ContextPack {
32
42
  warnings: ContextPackWarning[];
33
43
  suggestedReads: string[];
34
44
  suggestedVerification: string[];
45
+ /** 1.2 bounded task selection, added without removing legacy pack fields. */
46
+ workset?: TaskWorkset;
35
47
  }
36
48
 
37
49
  export interface ContextPackObservation {
@@ -181,6 +193,8 @@ export function assembleContextPack(input: AssembleContextPackInput): ContextPac
181
193
  type: observation.type,
182
194
  status: freshness.status,
183
195
  reason: freshness.reason,
196
+ ...(file ? { path: file.path } : {}),
197
+ ...(symbol ? { symbol: symbol.name } : {}),
184
198
  });
185
199
  }
186
200
  if (file) suggestedReads.push(file.path);
@@ -240,7 +254,74 @@ export function assembleContextPack(input: AssembleContextPackInput): ContextPac
240
254
  };
241
255
  }
242
256
 
257
+ /**
258
+ * Context Pack remains compatible with its detailed legacy fields, but the
259
+ * agent-facing prompt now uses the same bounded Workset selector as Project
260
+ * Context. No repository scan or page compilation happens here.
261
+ */
262
+ export async function attachTaskWorkset(input: {
263
+ pack: ContextPack;
264
+ projectId: string;
265
+ dataDir: string;
266
+ lens: string;
267
+ worktreeDirty: boolean;
268
+ currentFacts?: string[];
269
+ codeState?: string;
270
+ snapshot?: {
271
+ id?: string;
272
+ sourceEpoch?: number;
273
+ worktreeState?: 'clean' | 'dirty' | 'unavailable';
274
+ incomplete?: boolean;
275
+ };
276
+ semanticCode?: ExternalCodeGraphOutline;
277
+ providerQuality?: CodeGraphProviderQuality;
278
+ runtimeCautions?: WorksetCaution[];
279
+ }): Promise<ContextPack> {
280
+ const semanticStartHere = input.semanticCode
281
+ ? [...input.semanticCode.relatedFiles, ...input.semanticCode.entryPoints.map(entry => entry.path)]
282
+ : [];
283
+ const workset = await buildTaskWorkset({
284
+ projectId: input.projectId,
285
+ dataDir: input.dataDir,
286
+ task: input.pack.task,
287
+ lens: input.lens,
288
+ currentFacts: input.currentFacts ?? [],
289
+ ...(input.codeState ? { codeState: input.codeState } : {}),
290
+ startHere: uniq([...semanticStartHere, ...input.pack.suggestedReads]),
291
+ ...(input.semanticCode ? { semanticCode: input.semanticCode } : {}),
292
+ ...(input.providerQuality ? { providerQuality: input.providerQuality } : {}),
293
+ reliableMemory: input.pack.memories
294
+ .filter(memory => memory.status === 'current')
295
+ .map(memory => ({
296
+ id: memory.id,
297
+ title: memory.title,
298
+ type: memory.type,
299
+ status: memory.status,
300
+ ...(memory.path ? { path: memory.path } : {}),
301
+ ...(memory.symbol ? { symbol: memory.symbol } : {}),
302
+ ...(memory.reason ? { reason: memory.reason } : {}),
303
+ })),
304
+ cautionMemory: input.pack.warnings.map(warning => ({
305
+ id: warning.id,
306
+ title: warning.title,
307
+ type: 'memory',
308
+ status: warning.status,
309
+ reason: warning.reason,
310
+ })),
311
+ verificationHints: input.pack.suggestedVerification,
312
+ worktreeDirty: input.worktreeDirty,
313
+ ...(input.snapshot ? { snapshot: input.snapshot } : {}),
314
+ freshness: {
315
+ suspect: input.pack.warnings.filter(warning => warning.status === 'suspect').length,
316
+ stale: input.pack.warnings.filter(warning => warning.status === 'stale').length,
317
+ },
318
+ runtimeCautions: input.runtimeCautions,
319
+ });
320
+ return { ...input.pack, workset };
321
+ }
322
+
243
323
  export function buildContextPackPrompt(pack: ContextPack): string {
324
+ if (pack.workset) return pack.workset.prompt;
244
325
  const reliableMemories = pack.memories.filter(memory => memory.status === 'current');
245
326
  const unboundMemories = pack.memories.filter(memory => memory.status === 'unbound');
246
327
  const lines: string[] = ['## Task', pack.task, '', '## Reliable Memories'];