fluffy-context 0.7.2 → 0.7.6

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.
@@ -1,8 +1,10 @@
1
1
  import type { AgentLoadOptions, AgentLoadResult, AgentSaveResult, CheckpointInput, ContextSearchOptions, ContextSearchResult } from '../runtime/types.js';
2
2
  import type { ContextOrientOptions, ContextOrientResult, ContextUseInput } from './types.js';
3
+ import type { ContextExpandInput, ContextExpandResult } from '../compiler/types.js';
3
4
  export declare function saveContext(startPath: string | undefined, input: CheckpointInput, options?: {
4
5
  minSaveIntervalMs?: number;
5
6
  }): Promise<AgentSaveResult>;
7
+ export declare function contextExpand(startPath: string | undefined, input: ContextExpandInput): Promise<ContextExpandResult>;
6
8
  export declare function loadContext(startPath: string | undefined, options?: AgentLoadOptions): Promise<AgentLoadResult>;
7
9
  export declare function contextOrient(startPath: string | undefined, options?: ContextOrientOptions): Promise<ContextOrientResult>;
8
10
  export declare function recordContextUse(startPath: string | undefined, input: ContextUseInput): Promise<{
@@ -8,6 +8,7 @@ import { selectV2Orientation } from '../cognition/orient.js';
8
8
  import { discoverV2Deadends, discoverV2Knowledge } from '../cognition/retrieval.js';
9
9
  import { discoverDeadends, discoverKnowledge, listDeadends } from '../runtime/knowledge.js';
10
10
  import { listNotes } from '../runtime/notes.js';
11
+ import { expandContext } from '../compiler/expand.js';
11
12
  function normalized(value) {
12
13
  return value.trim().toLocaleLowerCase();
13
14
  }
@@ -138,6 +139,10 @@ export async function saveContext(startPath, input, options = {}) {
138
139
  const projectRoot = await resolveProjectRoot(startPath);
139
140
  return { projectRoot, result: await checkpoint(projectRoot, input, options) };
140
141
  }
142
+ export async function contextExpand(startPath, input) {
143
+ const projectRoot = await resolveProjectRoot(startPath);
144
+ return expandContext(projectRoot, input);
145
+ }
141
146
  export async function loadContext(startPath, options = {}) {
142
147
  const projectRoot = await resolveProjectRoot(startPath);
143
148
  const result = await resume(projectRoot, options.contextId, options.maxChars ?? 4000);
@@ -2,4 +2,5 @@ export * from './api.js';
2
2
  export type { ContextOrientContext, ContextUseInput, ContextOrientExplanation, ContextOrientNoContextResult, ContextOrientOptions, ContextOrientReadyResult, ContextOrientResult, ContextOrientSnapshot, } from './types.js';
3
3
  export type { AgentLoadOptions, AgentLoadResult, AgentSaveResult, ContextSearchHit, ContextSearchOptions, ContextSearchResult, } from '../runtime/types.js';
4
4
  export { compileContext } from '../compiler/compile.js';
5
- export type { ContextActivityRecord, ContextAnchor, ContextBudget, ContextBudgetReport, ContextCandidate, ContextCandidateSection, ContextCompileOptions, ContextExclusion, ContextExclusionReason, ContextIntent, ContextIntentSignal, ContextManifest, ContextProvenance, ContextScenario, ContextSourceDescriptor, ContextTruncation, } from '../compiler/types.js';
5
+ export { canonicalJson, contentHash, estimateTokens, rebuildManifestHash, validateManifestIdentity } from '../compiler/identity.js';
6
+ export type { ContextActivityRecord, ContextAnchor, ContextBudget, ContextBudgetReport, ContextCandidate, ContextCandidateSection, ContextCompileOptions, ContextExpandInput, ContextExpandLevel, ContextExpandResult, ContextExclusion, ContextExclusionReason, ContextIntent, ContextIntentSignal, ContextLevel, ContextManifest, ContextProvenance, ContextScenario, ContextSourceDescriptor, ContextTruncation, } from '../compiler/types.js';
@@ -1,2 +1,3 @@
1
1
  export * from './api.js';
2
2
  export { compileContext } from '../compiler/compile.js';
3
+ export { canonicalJson, contentHash, estimateTokens, rebuildManifestHash, validateManifestIdentity } from '../compiler/identity.js';
@@ -2,6 +2,7 @@
2
2
  import { checkpoint, resume } from '../runtime/runtime.js';
3
3
  import { contextOrient } from '../agent/api.js';
4
4
  import { compileContext } from '../compiler/compile.js';
5
+ import { contextExpand } from '../agent/api.js';
5
6
  import { importV1, verifyV1Import } from '../cognition/migration-v1.js';
6
7
  import { recordUse } from '../cognition/feedback.js';
7
8
  import { verifyJournal } from '../cognition/journal.js';
@@ -131,6 +132,27 @@ Options:
131
132
  --knowledge-limit <number> Maximum knowledge matches (default: 10)
132
133
  --deadend-limit <number> Maximum deadend matches (default: 10)
133
134
  --note-limit <number> Maximum open notes (default: 20)
135
+ --activity-limit <number> Maximum recent activity groups (default: 20)`,
136
+ expand: `usage: ctx expand [options]
137
+
138
+ Expand one selected compiler candidate without writing project state.
139
+
140
+ Options:
141
+ --path <path> Project path
142
+ --manifest-hash <hash> Manifest identity hash from ctx compile
143
+ --candidate-id <id> Selected candidate ID
144
+ --item-hash <hash> Selected item hash
145
+ --level <level> structured|evidence
146
+ --max-chars <number> Maximum expanded characters
147
+ --compile-max-chars <number> Original compile budget (default: 4000)
148
+ --context <id> Context ID
149
+ --query <text> Original compile query
150
+ --scenario <scenario> resume|implementation|debugging|verification|handoff|exploration|unknown
151
+ --scope <scope> Exact consensus scope
152
+ --paths <paths> Comma-separated project-relative paths
153
+ --knowledge-limit <number> Maximum knowledge matches (default: 10)
154
+ --deadend-limit <number> Maximum deadend matches (default: 10)
155
+ --note-limit <number> Maximum open notes (default: 20)
134
156
  --activity-limit <number> Maximum recent activity groups (default: 20)`,
135
157
  agent: `usage: ctx agent serve
136
158
 
@@ -139,7 +161,7 @@ Start an MCP stdio server for Agent integrations.
139
161
  Run "ctx agent serve --help" for details.`,
140
162
  'agent serve': `usage: ctx agent serve
141
163
 
142
- Start an MCP stdio server exposing the context_orient tool.
164
+ Start an MCP stdio server exposing context_orient, context_expand, context_compile, context_resume, context_note_list, context_note_add, and context_checkpoint tools.
143
165
 
144
166
  The server owns standard input/output; do not use it interactively.`,
145
167
  hook: `usage: ctx hook claude-code session-start|user-prompt
@@ -341,7 +363,7 @@ Stop the local runtime through authenticated loopback IPC.`,
341
363
  Internal runtime daemon entry point.`,
342
364
  };
343
365
  function usage() {
344
- return `usage: ctx init|checkpoint|resume|orient|compile|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity|migrate|runtime|filesystem|feedback|journal [options]
366
+ return `usage: ctx init|checkpoint|resume|orient|compile|expand|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity|migrate|runtime|filesystem|feedback|journal [options]
345
367
 
346
368
  Run \"ctx <command> --help\" for command details.`;
347
369
  }
@@ -466,6 +488,39 @@ async function run(args) {
466
488
  }));
467
489
  return;
468
490
  }
491
+ case 'expand': {
492
+ const valueOptions = ['--path', '--manifest-hash', '--candidate-id', '--item-hash', '--level', '--max-chars', '--compile-max-chars', '--context', '--query', '--scenario', '--scope', '--paths', '--knowledge-limit', '--deadend-limit', '--note-limit', '--activity-limit'];
493
+ validateOptions(args.slice(1), valueOptions, valueOptions);
494
+ validatePositionals(positionals(args.slice(1), valueOptions), 0, HELP.expand);
495
+ const level = enumOption(args, '--level', ['structured', 'evidence']);
496
+ if (!level)
497
+ throw new Error('--level is required');
498
+ const manifestHash = option(args, '--manifest-hash');
499
+ if (!manifestHash)
500
+ throw new Error('--manifest-hash is required');
501
+ const compileMaxChars = numericOption(args, '--compile-max-chars', 4000);
502
+ await ensureRuntimeForSession(target, { source: 'cli', timeoutMs: 1_000 }).catch(() => undefined);
503
+ print(await contextExpand(target, {
504
+ manifestHash,
505
+ candidateId: option(args, '--candidate-id'),
506
+ itemHash: option(args, '--item-hash'),
507
+ level: level,
508
+ maxChars: option(args, '--max-chars') === undefined ? undefined : numericOption(args, '--max-chars', 0),
509
+ compile: {
510
+ contextId: option(args, '--context'),
511
+ query: option(args, '--query'),
512
+ scenario: enumOption(args, '--scenario', ['resume', 'implementation', 'debugging', 'verification', 'handoff', 'exploration', 'unknown']),
513
+ scope: option(args, '--scope'),
514
+ paths: listOption(args, '--paths'),
515
+ maxChars: compileMaxChars,
516
+ knowledgeLimit: numericOption(args, '--knowledge-limit', 10),
517
+ deadendLimit: numericOption(args, '--deadend-limit', 10),
518
+ noteLimit: numericOption(args, '--note-limit', 20),
519
+ activityLimit: numericOption(args, '--activity-limit', 20),
520
+ },
521
+ }));
522
+ return;
523
+ }
469
524
  case 'agent':
470
525
  if (args[1] !== 'serve')
471
526
  throw new Error(HELP.agent);
@@ -55,6 +55,22 @@ function validGitAnchor(value) {
55
55
  function validIsoDate(value) {
56
56
  return typeof value === 'string' && Number.isFinite(Date.parse(value));
57
57
  }
58
+ function validatePayloadBoundary(input) {
59
+ const payload = input.payload;
60
+ if (input.type === 'host.prompt.submitted' && (typeof payload.intent !== 'string' || payload.intent.trim().length === 0)) {
61
+ throw new Error('host prompt intent must not be empty');
62
+ }
63
+ if (['note.recorded', 'note.resolved', 'note.absorbed'].includes(input.type)) {
64
+ const record = payload.record;
65
+ if (!isRecord(record) || typeof record.message !== 'string' || record.message.trim().length === 0)
66
+ throw new Error('note message must not be empty');
67
+ }
68
+ if (input.type === 'workspace.paths.changed') {
69
+ const changes = payload.changes;
70
+ if (!Array.isArray(changes) || changes.length === 0)
71
+ throw new Error('workspace activity must contain changes');
72
+ }
73
+ }
58
74
  function validateEventShape(event, projectId, file, line) {
59
75
  if (!isRecord(event)
60
76
  || event.schemaVersion !== 2
@@ -140,6 +156,7 @@ export async function verifyJournal(startPath) {
140
156
  return readJournal(startPath, { allowPartialTail: false });
141
157
  }
142
158
  export async function appendEvent(startPath, input) {
159
+ validatePayloadBoundary(input);
143
160
  const projectRoot = await resolveProjectRoot(startPath);
144
161
  return withLock(`${locksDirectory(projectRoot)}/cognition-journal.lock`, async () => {
145
162
  await recoverPartialTail(projectRoot);
Binary file
@@ -1,4 +1,5 @@
1
1
  import { filterPaths } from '../capture/context-filter.js';
2
+ import { contentHash, estimateTokens, rebuildManifestHash } from './identity.js';
2
3
  import { queryTerms } from '../cognition/retrieval.js';
3
4
  import { listDeadends, listKnowledge } from '../runtime/knowledge.js';
4
5
  import { listNotes } from '../runtime/notes.js';
@@ -84,8 +85,9 @@ function matched(text, terms) {
84
85
  }
85
86
  function makeCandidate(section, source, content, text, terms, matchedFields, discriminator) {
86
87
  const hit = matched(text, terms);
88
+ const candidateId = `${section}:${source.sourceId}:${matchedFields.join('.')}:${discriminator}`;
87
89
  return {
88
- candidateId: `${section}:${source.sourceId}:${matchedFields.join('.')}:${discriminator}`,
90
+ candidateId,
89
91
  section,
90
92
  source,
91
93
  content,
@@ -98,6 +100,10 @@ function makeCandidate(section, source, content, text, terms, matchedFields, dis
98
100
  selectedChars: 0,
99
101
  selected: false,
100
102
  truncated: false,
103
+ level: 'summary',
104
+ itemHash: contentHash({ candidateId, source, content, text }),
105
+ tokenEstimate: estimateTokens(text),
106
+ expandable: true,
101
107
  };
102
108
  }
103
109
  function applicable(candidate, intent) {
@@ -233,7 +239,7 @@ export async function compileContext(startPath, options = {}) {
233
239
  const intent = intentFor(sources, options, normalizedPaths);
234
240
  const orientation = sources.orientation;
235
241
  const maxChars = intent.budget.total;
236
- const notes = orientation.status === 'ready' ? (await listNotes(sources.anchor.projectRoot, { contextId: orientation.context.id, openOnly: true, limit: Number.MAX_SAFE_INTEGER, maxChars: Number.MAX_SAFE_INTEGER })).filter((note) => !orientation || orientation.notes.some((selected) => selected.noteId === note.noteId)) : [];
242
+ const notes = orientation.status === 'ready' ? (await listNotes(sources.anchor.projectRoot, { contextId: orientation.context.id, openOnly: true, limit: options.noteLimit ?? 50, maxChars: Number.MAX_SAFE_INTEGER })).filter((note) => orientation.notes.some((selected) => selected.noteId === note.noteId)) : [];
237
243
  const knowledge = orientation.status === 'ready' && intent.query ? await listKnowledge(sources.anchor.projectRoot, true) : [];
238
244
  const deadends = orientation.status === 'ready' && intent.query ? await listDeadends(sources.anchor.projectRoot, true) : [];
239
245
  const activities = await extractActivity(sources.anchor.projectRoot, sources.events, sources.anchor, sources.workspace, options.activityLimit ?? 20);
@@ -301,9 +307,21 @@ export async function compileContext(startPath, options = {}) {
301
307
  deadends: selected.filter((item) => item.section === 'deadend'),
302
308
  activity: selected.filter((item) => item.section === 'activity'),
303
309
  };
310
+ const tokenEstimate = estimateTokens(manifestText(selected));
311
+ const selectedItemHashes = selected.map((item) => item.itemHash);
312
+ const manifestHash = rebuildManifestHash({
313
+ status: orientation.status,
314
+ projectRoot: sources.anchor.projectRoot,
315
+ mode: orientation.explanation.mode,
316
+ anchor: sources.anchor,
317
+ intent,
318
+ selected,
319
+ budget: { requested: intent.budget, usedChars, remainingChars: Math.max(0, maxChars - usedChars), sections: sectionStats },
320
+ truncation,
321
+ });
304
322
  return {
305
323
  schemaVersion: 1,
306
- compilerVersion: '0.7.2',
324
+ compilerVersion: '0.7.6',
307
325
  rankingVersion: 'compiler-ranking-v2',
308
326
  status: orientation.status,
309
327
  projectRoot: sources.anchor.projectRoot,
@@ -316,6 +334,11 @@ export async function compileContext(startPath, options = {}) {
316
334
  sections,
317
335
  selected,
318
336
  excluded,
337
+ identity: {
338
+ manifestHash,
339
+ selectedItemHashes,
340
+ tokenEstimate: { input: tokenEstimate, output: tokenEstimate, unit: 'estimated-tokens', method: 'chars-div-four' },
341
+ },
319
342
  budget: { requested: intent.budget, usedChars, remainingChars: Math.max(0, maxChars - usedChars), sections: sectionStats },
320
343
  truncation,
321
344
  provenance: {
@@ -0,0 +1,2 @@
1
+ import type { ContextExpandInput, ContextExpandResult } from './types.js';
2
+ export declare function expandContext(startPath: string | undefined, input: ContextExpandInput): Promise<ContextExpandResult>;
@@ -0,0 +1,187 @@
1
+ import { compileContext } from './compile.js';
2
+ import { extractActivity } from './activity.js';
3
+ import { validateManifestIdentity } from './identity.js';
4
+ import { readJournal } from '../cognition/journal.js';
5
+ import { projectEvents, workspaceKey } from '../cognition/projections.js';
6
+ import { rebuildSnapshot } from '../runtime/runtime.js';
7
+ import { listDeadends, listKnowledge } from '../runtime/knowledge.js';
8
+ import { listNotes } from '../runtime/notes.js';
9
+ function truncateText(value, maxChars) {
10
+ if (value.length <= maxChars)
11
+ return { text: value, truncated: false };
12
+ if (maxChars <= 0)
13
+ return { text: '', truncated: true };
14
+ return { text: `${value.slice(0, maxChars - 1)}…`, truncated: true };
15
+ }
16
+ function isExpandLevel(value) {
17
+ return value === 'structured' || value === 'evidence';
18
+ }
19
+ function validateInput(input) {
20
+ if (!input.manifestHash.trim())
21
+ throw new Error('context expand manifest hash must not be empty');
22
+ if (!input.candidateId?.trim() && !input.itemHash?.trim())
23
+ throw new Error('context expand requires candidateId or itemHash');
24
+ if (!isExpandLevel(input.level))
25
+ throw new Error('context expand level must be structured or evidence');
26
+ if (input.maxChars !== undefined && (!Number.isSafeInteger(input.maxChars) || input.maxChars < 0))
27
+ throw new Error('context expand maxChars must be a non-negative integer');
28
+ }
29
+ function selectedCandidate(manifest, input) {
30
+ const byId = input.candidateId ? manifest.selected.find((item) => item.candidateId === input.candidateId) : undefined;
31
+ const byHash = input.itemHash ? manifest.selected.find((item) => item.itemHash === input.itemHash) : undefined;
32
+ if (byId && byHash && byId.candidateId !== byHash.candidateId)
33
+ throw new Error('context expand candidate selectors do not match');
34
+ const candidate = byId ?? byHash;
35
+ if (!candidate)
36
+ throw new Error('context expand candidate not selected');
37
+ if (!candidate.expandable)
38
+ throw new Error('context expand candidate is not expandable');
39
+ if (candidate.level !== 'summary')
40
+ throw new Error('context expand candidate is already expanded');
41
+ return candidate;
42
+ }
43
+ function contextField(content, candidate) {
44
+ const field = candidate.content.field;
45
+ if (typeof field !== 'string')
46
+ throw new Error('context expand current context field is invalid');
47
+ if (field === 'title')
48
+ return candidate.content.value ?? '';
49
+ if (field === 'progressSummary' || field === 'lastError')
50
+ return content[field];
51
+ if (field === 'pendingTasks' || field === 'completed' || field === 'decisions' || field === 'risks' || field === 'relatedFiles') {
52
+ const discriminator = candidate.candidateId.split(':').at(-1) ?? '';
53
+ const index = Number(discriminator);
54
+ const values = content[field];
55
+ if (!Number.isInteger(index) || index < 0 || index >= values.length)
56
+ throw new Error(`context expand current context item is unavailable: ${field}`);
57
+ return values[index];
58
+ }
59
+ throw new Error(`context expand unsupported current context field: ${field}`);
60
+ }
61
+ function currentContextText(value, candidate) {
62
+ const field = candidate.content.field;
63
+ if (typeof field !== 'string')
64
+ throw new Error('context expand current context field is invalid');
65
+ return `${field}: ${Array.isArray(value) ? value.join(', ') : value ?? ''}`;
66
+ }
67
+ function recordText(source, value) {
68
+ if (source.kind === 'note') {
69
+ const note = value;
70
+ return `${note.kind}: ${note.message}`;
71
+ }
72
+ if (source.kind === 'knowledge')
73
+ return value.statement;
74
+ const deadend = value;
75
+ return `${deadend.attempt}: ${deadend.reason}`;
76
+ }
77
+ function evidenceText(source, value, structuredText) {
78
+ return `${structuredText}\nevidence: ${JSON.stringify({
79
+ source: source.provenance,
80
+ ...(source.kind === 'knowledge' ? { supportingEvidence: value.supportingEvidence } : {}),
81
+ ...(source.kind === 'deadend' ? { observedEvidence: value.observedEvidence } : {}),
82
+ ...(source.kind === 'note' ? { relatedFiles: value.relatedFiles } : {}),
83
+ })}`;
84
+ }
85
+ function recordApplicability(value) {
86
+ if (!value || typeof value !== 'object' || Array.isArray(value))
87
+ return undefined;
88
+ const applicability = value.applicability;
89
+ if (!applicability || typeof applicability !== 'object' || Array.isArray(applicability))
90
+ return undefined;
91
+ return applicability;
92
+ }
93
+ function ensureApplicable(source, value, manifest) {
94
+ const applicability = recordApplicability(value);
95
+ if (!applicability)
96
+ return;
97
+ if (applicability.branchMode !== source.applicability.branchMode)
98
+ throw new Error(`context expand source applicability changed: ${source.entityId}`);
99
+ if (applicability.branchMode === 'exact') {
100
+ const branch = manifest.anchor.ref ?? manifest.anchor.branch ?? 'detached';
101
+ const branches = (applicability.branches ?? []).map((item) => item.toLowerCase());
102
+ if (branches.length > 0 && !branches.includes(branch.toLowerCase()) && !branches.includes((manifest.anchor.branch ?? '').toLowerCase()))
103
+ throw new Error(`context expand source is not applicable: ${source.entityId}`);
104
+ if (applicability.sourceCommit && applicability.sourceCommit !== manifest.anchor.head)
105
+ throw new Error(`context expand source commit is not applicable: ${source.entityId}`);
106
+ if (source.applicability.worktreeId && source.applicability.worktreeId !== manifest.anchor.worktreeId)
107
+ throw new Error(`context expand source worktree is not applicable: ${source.entityId}`);
108
+ if (source.applicability.ref && source.applicability.ref !== manifest.anchor.ref)
109
+ throw new Error(`context expand source ref is not applicable: ${source.entityId}`);
110
+ }
111
+ if (source.scope && typeof value.scope === 'string' && source.scope !== value.scope)
112
+ throw new Error(`context expand source scope changed: ${source.entityId}`);
113
+ const paths = applicability.paths ?? [];
114
+ if (source.paths.length > 0 && paths.length > 0 && !source.paths.some((path) => paths.includes(path)))
115
+ throw new Error(`context expand source paths changed: ${source.entityId}`);
116
+ }
117
+ async function resolveCandidate(projectRoot, manifest, candidate, level) {
118
+ const source = candidate.source;
119
+ if (source.kind === 'current-context') {
120
+ if (!source.contextId || !source.snapshotId)
121
+ throw new Error('context expand current context source is incomplete');
122
+ const content = await rebuildSnapshot(projectRoot, source.contextId, source.snapshotId);
123
+ const value = contextField(content, candidate);
124
+ const text = currentContextText(value, candidate);
125
+ return { content: level === 'evidence' ? { field: candidate.content.field, value, source: source.provenance } : { field: candidate.content.field, value }, text: level === 'evidence' ? evidenceText(source, { value }, text) : text };
126
+ }
127
+ if (source.kind === 'note') {
128
+ const note = (await listNotes(projectRoot, { contextId: source.contextId ?? undefined, limit: Number.MAX_SAFE_INTEGER, maxChars: Number.MAX_SAFE_INTEGER })).find((item) => item.noteId === source.entityId);
129
+ if (!note)
130
+ throw new Error(`context expand note not found: ${source.entityId}`);
131
+ const text = recordText(source, note);
132
+ return { content: level === 'evidence' ? { ...note, provenance: source.provenance } : note, text: level === 'evidence' ? evidenceText(source, note, text) : text };
133
+ }
134
+ if (source.kind === 'knowledge') {
135
+ const knowledge = (await listKnowledge(projectRoot, true)).find((item) => item.knowledgeId === source.entityId);
136
+ if (!knowledge || knowledge.status !== 'verified')
137
+ throw new Error(`context expand knowledge is no longer verified: ${source.entityId}`);
138
+ ensureApplicable(source, knowledge, manifest);
139
+ const text = recordText(source, knowledge);
140
+ return { content: level === 'evidence' ? { ...knowledge, provenance: source.provenance } : knowledge, text: level === 'evidence' ? evidenceText(source, knowledge, text) : text };
141
+ }
142
+ if (source.kind === 'deadend') {
143
+ const deadend = (await listDeadends(projectRoot, true)).find((item) => item.deadendId === source.entityId);
144
+ if (!deadend || deadend.status !== 'verified')
145
+ throw new Error(`context expand deadend is no longer verified: ${source.entityId}`);
146
+ ensureApplicable(source, deadend, manifest);
147
+ const text = recordText(source, deadend);
148
+ return { content: level === 'evidence' ? { ...deadend, provenance: source.provenance } : deadend, text: level === 'evidence' ? evidenceText(source, deadend, text) : text };
149
+ }
150
+ if (source.kind === 'journal') {
151
+ const events = manifest.provenance.journalRead ? await readJournal(projectRoot) : [];
152
+ const projection = events.length > 0 ? projectEvents(events) : null;
153
+ const workspace = projection?.workspaces[workspaceKey(manifest.anchor)] ?? null;
154
+ const records = await extractActivity(projectRoot, events, manifest.anchor, workspace, Number.MAX_SAFE_INTEGER);
155
+ const activity = records.find((item) => item.eventId === source.entityId);
156
+ if (!activity)
157
+ throw new Error(`context expand activity not found: ${source.entityId}`);
158
+ return { content: activity, text: `activity: ${activity.changes.map((change) => `${change.path} (${change.kind})`).join(', ')}` };
159
+ }
160
+ throw new Error(`context expand source kind is not supported: ${source.kind}`);
161
+ }
162
+ export async function expandContext(startPath, input) {
163
+ validateInput(input);
164
+ const manifest = await compileContext(startPath, input.compile);
165
+ if (!validateManifestIdentity(manifest) || manifest.identity.manifestHash !== input.manifestHash)
166
+ throw new Error('context expand manifest hash mismatch');
167
+ const candidate = selectedCandidate(manifest, input);
168
+ const resolved = await resolveCandidate(manifest.projectRoot, manifest, candidate, input.level);
169
+ const limited = truncateText(resolved.text, input.maxChars ?? Number.MAX_SAFE_INTEGER);
170
+ const content = limited.truncated ? null : resolved.content;
171
+ const requestedChars = input.maxChars ?? resolved.text.length;
172
+ return {
173
+ status: limited.truncated ? 'partial' : 'complete',
174
+ projectRoot: manifest.projectRoot,
175
+ manifestHash: manifest.identity.manifestHash,
176
+ candidateId: candidate.candidateId,
177
+ itemHash: candidate.itemHash,
178
+ previousLevel: candidate.level,
179
+ requestedLevel: input.level,
180
+ resolvedLevel: input.level,
181
+ source: candidate.source,
182
+ content,
183
+ text: limited.text,
184
+ budget: { requestedChars, usedChars: limited.text.length, remainingChars: Math.max(0, requestedChars - limited.text.length), truncated: limited.truncated },
185
+ provenance: manifest.provenance,
186
+ };
187
+ }
@@ -0,0 +1,6 @@
1
+ import type { ContextManifest } from './types.js';
2
+ export declare function canonicalJson(value: unknown): string;
3
+ export declare function contentHash(value: unknown): string;
4
+ export declare function estimateTokens(value: string): number;
5
+ export declare function rebuildManifestHash(manifest: Pick<ContextManifest, 'status' | 'projectRoot' | 'mode' | 'anchor' | 'intent' | 'selected' | 'budget' | 'truncation'>): string;
6
+ export declare function validateManifestIdentity(manifest: ContextManifest): boolean;
@@ -0,0 +1,36 @@
1
+ import crypto from 'node:crypto';
2
+ export function canonicalJson(value) {
3
+ if (value === null || typeof value !== 'object')
4
+ return JSON.stringify(value) ?? 'null';
5
+ if (Array.isArray(value))
6
+ return `[${value.map(canonicalJson).join(',')}]`;
7
+ const record = value;
8
+ return `{${Object.keys(record).filter((key) => record[key] !== undefined).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
9
+ }
10
+ export function contentHash(value) {
11
+ return crypto.createHash('sha256').update(canonicalJson(value)).digest('hex');
12
+ }
13
+ export function estimateTokens(value) {
14
+ return value.length === 0 ? 0 : Math.ceil(value.length / 4);
15
+ }
16
+ export function rebuildManifestHash(manifest) {
17
+ return contentHash({
18
+ schemaVersion: 1,
19
+ compilerVersion: '0.7.6',
20
+ rankingVersion: 'compiler-ranking-v2',
21
+ status: manifest.status,
22
+ projectRoot: manifest.projectRoot,
23
+ mode: manifest.mode,
24
+ anchor: manifest.anchor,
25
+ intent: manifest.intent,
26
+ selectedItemHashes: manifest.selected.map((item) => item.itemHash),
27
+ budget: { usedChars: manifest.budget.usedChars, maxChars: manifest.budget.requested.total },
28
+ truncation: manifest.truncation,
29
+ });
30
+ }
31
+ export function validateManifestIdentity(manifest) {
32
+ return manifest.identity.manifestHash === rebuildManifestHash(manifest)
33
+ && JSON.stringify(manifest.identity.selectedItemHashes) === JSON.stringify(manifest.selected.map((item) => item.itemHash))
34
+ && manifest.identity.tokenEstimate.input === estimateTokens(manifest.text)
35
+ && manifest.identity.tokenEstimate.output === estimateTokens(manifest.text);
36
+ }
@@ -110,21 +110,17 @@ export async function loadCompilerSources(startPath, options) {
110
110
  return { orientation, anchor, events, workspace, fallbackReason };
111
111
  }
112
112
  export function latestHostIntent(events, anchor) {
113
- return [...events].reverse().find((event) => {
114
- if (event.type !== 'host.prompt.submitted')
113
+ const event = [...events].reverse().find((candidate) => {
114
+ if (candidate.type !== 'host.prompt.submitted')
115
115
  return false;
116
- if (event.git.worktreeId !== null && event.git.worktreeId !== anchor.worktreeId)
116
+ if (candidate.git.worktreeId !== null && candidate.git.worktreeId !== anchor.worktreeId)
117
117
  return false;
118
- if (event.git.ref !== null && event.git.ref !== anchor.ref)
118
+ if (candidate.git.ref !== null && candidate.git.ref !== anchor.ref)
119
119
  return false;
120
- const payload = event.payload;
120
+ const payload = candidate.payload;
121
121
  return typeof payload.intent === 'string' && payload.intent.trim().length > 0;
122
- })?.payload
123
- ? (() => {
124
- const event = [...events].reverse().find((item) => item.type === 'host.prompt.submitted' && item.git.worktreeId === anchor.worktreeId && item.git.ref === anchor.ref && typeof item.payload.intent === 'string');
125
- if (!event)
126
- return null;
127
- return { value: event.payload.intent, eventId: event.eventId };
128
- })()
129
- : null;
122
+ });
123
+ if (!event)
124
+ return null;
125
+ return { value: event.payload.intent, eventId: event.eventId };
130
126
  }
@@ -2,6 +2,8 @@ import type { CognitionGitAnchor, WorkspacePathChange } from '../cognition/types
2
2
  import type { ContextOrientResult } from '../agent/types.js';
3
3
  import type { Deadend, Knowledge, Note, ResumeSummary, StructuredContext } from '../runtime/types.js';
4
4
  export type ContextScenario = 'resume' | 'implementation' | 'debugging' | 'verification' | 'handoff' | 'exploration' | 'unknown';
5
+ export type ContextLevel = 'signal' | 'summary' | 'structured' | 'evidence';
6
+ export type ContextExpandLevel = 'structured' | 'evidence';
5
7
  export type ContextSourceKind = 'current-context' | 'checkpoint' | 'note' | 'journal' | 'knowledge' | 'deadend';
6
8
  export type ContextCandidateSection = 'summary' | 'pending' | 'decision' | 'risk' | 'note' | 'knowledge' | 'deadend' | 'activity';
7
9
  export type ContextSignalOrigin = 'explicit' | 'deterministic';
@@ -41,6 +43,12 @@ export interface ContextBudget {
41
43
  reservedForRenderer: number;
42
44
  sections?: Partial<Record<ContextCandidateSection, number>>;
43
45
  }
46
+ export interface ContextTokenEstimate {
47
+ input: number;
48
+ output: number;
49
+ unit: 'estimated-tokens';
50
+ method: 'chars-div-four';
51
+ }
44
52
  export interface ContextSourceApplicability {
45
53
  project: true;
46
54
  branchMode: 'global' | 'exact' | 'unsupported';
@@ -100,6 +108,10 @@ export interface ContextCandidate<T = unknown> {
100
108
  selectedChars: number;
101
109
  selected: boolean;
102
110
  truncated: boolean;
111
+ level: ContextLevel;
112
+ itemHash: string;
113
+ tokenEstimate: number;
114
+ expandable: boolean;
103
115
  }
104
116
  export type ContextExclusionReason = 'no_active_context' | 'context_mismatch' | 'status_not_verified' | 'not_open' | 'absorbed' | 'applicability_mismatch' | 'scope_mismatch' | 'path_mismatch' | 'query_no_match' | 'result_limit' | 'budget' | 'unsupported_applicability' | 'v2_unavailable';
105
117
  export interface ContextExclusion {
@@ -138,6 +150,11 @@ export interface ContextManifestProvenance {
138
150
  selectedEventIds: string[];
139
151
  fallbackReason: string | null;
140
152
  }
153
+ export interface ContextManifestIdentity {
154
+ manifestHash: string;
155
+ selectedItemHashes: string[];
156
+ tokenEstimate: ContextTokenEstimate;
157
+ }
141
158
  export interface ContextCompileOptions {
142
159
  contextId?: string;
143
160
  query?: string;
@@ -154,9 +171,37 @@ export interface ContextCompileOptions {
154
171
  sessionId?: string;
155
172
  eventId?: string;
156
173
  }
174
+ export interface ContextExpandInput {
175
+ manifestHash: string;
176
+ candidateId?: string;
177
+ itemHash?: string;
178
+ level: ContextExpandLevel;
179
+ maxChars?: number;
180
+ compile: ContextCompileOptions;
181
+ }
182
+ export interface ContextExpandResult {
183
+ status: 'complete' | 'partial';
184
+ projectRoot: string;
185
+ manifestHash: string;
186
+ candidateId: string;
187
+ itemHash: string;
188
+ previousLevel: ContextLevel;
189
+ requestedLevel: ContextExpandLevel;
190
+ resolvedLevel: ContextExpandLevel;
191
+ source: ContextSourceDescriptor;
192
+ content: unknown | null;
193
+ text: string;
194
+ budget: {
195
+ requestedChars: number;
196
+ usedChars: number;
197
+ remainingChars: number;
198
+ truncated: boolean;
199
+ };
200
+ provenance: ContextManifestProvenance;
201
+ }
157
202
  export interface ContextManifest {
158
203
  schemaVersion: 1;
159
- compilerVersion: '0.7.2';
204
+ compilerVersion: '0.7.6';
160
205
  rankingVersion: 'compiler-ranking-v2';
161
206
  status: 'ready' | 'no_context';
162
207
  projectRoot: string;
@@ -178,6 +223,7 @@ export interface ContextManifest {
178
223
  };
179
224
  selected: ContextCandidate[];
180
225
  excluded: ContextExclusion[];
226
+ identity: ContextManifestIdentity;
181
227
  budget: ContextBudgetReport;
182
228
  truncation: ContextTruncation;
183
229
  provenance: ContextManifestProvenance;