memorix 1.1.13 → 1.2.0

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 (88) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +3 -2
  3. package/README.zh-CN.md +3 -2
  4. package/dist/cli/index.js +36313 -31189
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/dashboard/static/app.js +50 -30
  7. package/dist/index.js +5348 -674
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +3661 -293
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +19 -0
  13. package/dist/sdk.d.ts +1 -1
  14. package/dist/sdk.js +5346 -672
  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 +25 -6
  30. package/docs/CONFIGURATION.md +21 -3
  31. package/docs/README.md +17 -2
  32. package/docs/dev-log/progress.txt +120 -40
  33. package/llms-full.txt +16 -2
  34. package/llms.txt +9 -4
  35. package/package.json +3 -2
  36. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  37. package/src/cli/capability-map.ts +1 -0
  38. package/src/cli/commands/codegraph.ts +112 -9
  39. package/src/cli/commands/context.ts +2 -0
  40. package/src/cli/commands/doctor.ts +73 -4
  41. package/src/cli/commands/knowledge.ts +282 -0
  42. package/src/cli/commands/serve-http.ts +12 -1
  43. package/src/cli/index.ts +3 -1
  44. package/src/cli/tui/App.tsx +1 -1
  45. package/src/cli/tui/Panels.tsx +8 -8
  46. package/src/cli/tui/theme.ts +1 -1
  47. package/src/cli/tui/views/GraphView.tsx +8 -7
  48. package/src/cli/tui/views/KnowledgeView.tsx +9 -9
  49. package/src/codegraph/auto-context.ts +171 -9
  50. package/src/codegraph/code-state.ts +95 -0
  51. package/src/codegraph/context-pack.ts +82 -1
  52. package/src/codegraph/external-provider.ts +581 -0
  53. package/src/codegraph/lite-provider.ts +64 -19
  54. package/src/codegraph/project-context.ts +9 -1
  55. package/src/codegraph/store.ts +154 -6
  56. package/src/codegraph/types.ts +117 -0
  57. package/src/config/resolved-config.ts +28 -0
  58. package/src/config/toml-loader.ts +3 -0
  59. package/src/config/yaml-loader.ts +6 -0
  60. package/src/dashboard/server.ts +15 -1
  61. package/src/evaluation/workset-evaluation.ts +120 -0
  62. package/src/hooks/handler.ts +48 -6
  63. package/src/knowledge/claim-store.ts +267 -0
  64. package/src/knowledge/claims.ts +537 -0
  65. package/src/knowledge/markdown.ts +129 -0
  66. package/src/knowledge/types.ts +157 -0
  67. package/src/knowledge/wiki.ts +524 -0
  68. package/src/knowledge/workflow-store.ts +168 -0
  69. package/src/knowledge/workflow-types.ts +95 -0
  70. package/src/knowledge/workflows.ts +743 -0
  71. package/src/knowledge/workset.ts +515 -0
  72. package/src/knowledge/workspace-store.ts +220 -0
  73. package/src/knowledge/workspace-types.ts +106 -0
  74. package/src/knowledge/workspace.ts +220 -0
  75. package/src/memory/observations.ts +19 -0
  76. package/src/runtime/control-plane-maintenance.ts +5 -0
  77. package/src/runtime/isolated-maintenance.ts +5 -0
  78. package/src/runtime/lifecycle-status.ts +102 -0
  79. package/src/runtime/lifecycle.ts +107 -0
  80. package/src/runtime/maintenance-jobs.ts +5 -0
  81. package/src/runtime/project-maintenance.ts +190 -0
  82. package/src/server/tool-profile.ts +3 -2
  83. package/src/server.ts +354 -14
  84. package/src/store/file-lock.ts +24 -4
  85. package/src/store/sqlite-db.ts +307 -0
  86. package/src/wiki/generator.ts +4 -2
  87. package/src/wiki/knowledge-graph.ts +7 -4
  88. package/src/wiki/types.ts +16 -4
@@ -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
7
  import { collectCurrentProjectFacts, 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
 
@@ -322,6 +457,27 @@ function formatCurrentFactsLines(facts: CurrentProjectFacts): string[] {
322
457
  return lines;
323
458
  }
324
459
 
460
+ function worksetFactLines(facts: CurrentProjectFacts): string[] {
461
+ const lines: string[] = [];
462
+ if (facts.packageVersion) lines.push('Package version: ' + facts.packageVersion);
463
+ if (facts.latestChangelog) {
464
+ lines.push('Latest changelog: ' + facts.latestChangelog.version
465
+ + (facts.latestChangelog.date ? ' (' + facts.latestChangelog.date + ')' : ''));
466
+ }
467
+ const gitParts: string[] = [];
468
+ if (facts.git.branch) gitParts.push('branch ' + facts.git.branch);
469
+ if (facts.git.commit) gitParts.push('commit ' + facts.git.commit);
470
+ gitParts.push(facts.git.dirty ? 'dirty worktree' : 'clean worktree');
471
+ lines.push('Git: ' + gitParts.join(', '));
472
+ for (const note of facts.staleNotes.slice(0, 1)) {
473
+ lines.push(
474
+ 'Historical note: ' + note.path
475
+ + (note.branchHint ? ' (branch hint ' + note.branchHint + '; ' + note.reason + ')' : ' (' + note.reason + ')'),
476
+ );
477
+ }
478
+ return lines;
479
+ }
480
+
325
481
  export function formatAutoProjectContextSummary(context: AutoProjectContext): string {
326
482
  const reliableSources = rankLensSources(
327
483
  dedupeSourcesByObservation(context.explain.sources.filter(source => source.status === 'current')),
@@ -337,6 +493,7 @@ export function formatAutoProjectContextSummary(context: AutoProjectContext): st
337
493
  ...formatCurrentFactsLines(context.currentFacts),
338
494
  '',
339
495
  `- Code memory: ${context.overview.code.files} files / ${context.overview.code.symbols} symbols / ${context.overview.code.refs} memory links`,
496
+ `- Code provider: ${context.providerQuality.selected} (${context.providerQuality.selectedQuality})`,
340
497
  `- Languages: ${formatLanguages(context.overview)}`,
341
498
  `- Memories: ${context.overview.memory.active} active / ${context.overview.memory.total} total`,
342
499
  `- Freshness: ${context.overview.freshness.current} current, ${context.overview.freshness.suspect} suspect, ${context.overview.freshness.stale} stale`,
@@ -363,6 +520,10 @@ export function formatAutoProjectContextSummary(context: AutoProjectContext): st
363
520
  }
364
521
 
365
522
  export function formatAutoProjectContextPrompt(context: AutoProjectContext): string {
523
+ return context.workset.prompt;
524
+ }
525
+
526
+ export function formatLegacyAutoProjectContextPrompt(context: AutoProjectContext): string {
366
527
  const lines = [
367
528
  `Memorix Autopilot Brief for ${context.project.name}`,
368
529
  context.task ? `Task: ${context.task}` : '',
@@ -371,6 +532,7 @@ export function formatAutoProjectContextPrompt(context: AutoProjectContext): str
371
532
  ...formatCurrentFactsLines(context.currentFacts),
372
533
  '',
373
534
  'Project state',
535
+ codeStateLine(context.overview),
374
536
  `- Code memory: ${context.overview.code.files} files, ${context.overview.code.symbols} symbols, ${context.overview.code.refs} memory links`,
375
537
  `- Languages: ${formatLanguages(context.overview)}`,
376
538
  `- 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'];