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.
- package/README.md +463 -175
- package/dist/src/agent/api.d.ts +2 -0
- package/dist/src/agent/api.js +5 -0
- package/dist/src/agent/index.d.ts +2 -1
- package/dist/src/agent/index.js +1 -0
- package/dist/src/cli/main.js +57 -2
- package/dist/src/cognition/journal.js +17 -0
- package/dist/src/cognition/projections.js +0 -0
- package/dist/src/compiler/compile.js +26 -3
- package/dist/src/compiler/expand.d.ts +2 -0
- package/dist/src/compiler/expand.js +187 -0
- package/dist/src/compiler/identity.d.ts +6 -0
- package/dist/src/compiler/identity.js +36 -0
- package/dist/src/compiler/sources.js +9 -13
- package/dist/src/compiler/types.d.ts +47 -1
- package/dist/src/mcp/server.js +165 -11
- package/dist/src/runtime/runtime.js +10 -0
- package/dist/src/runtime/types.d.ts +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/skills/fluffy-context/SKILL.md +8 -2
package/dist/src/agent/api.d.ts
CHANGED
|
@@ -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<{
|
package/dist/src/agent/api.js
CHANGED
|
@@ -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
|
|
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';
|
package/dist/src/agent/index.js
CHANGED
package/dist/src/cli/main.js
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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.
|
|
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,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
|
-
|
|
114
|
-
if (
|
|
113
|
+
const event = [...events].reverse().find((candidate) => {
|
|
114
|
+
if (candidate.type !== 'host.prompt.submitted')
|
|
115
115
|
return false;
|
|
116
|
-
if (
|
|
116
|
+
if (candidate.git.worktreeId !== null && candidate.git.worktreeId !== anchor.worktreeId)
|
|
117
117
|
return false;
|
|
118
|
-
if (
|
|
118
|
+
if (candidate.git.ref !== null && candidate.git.ref !== anchor.ref)
|
|
119
119
|
return false;
|
|
120
|
-
const payload =
|
|
120
|
+
const payload = candidate.payload;
|
|
121
121
|
return typeof payload.intent === 'string' && payload.intent.trim().length > 0;
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
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;
|