fluffy-context 0.7.2 → 0.7.7

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,10 +1,16 @@
1
+ import { type UsageInterface } from '../cognition/usage.js';
2
+ import { type UsageReport, type UsageReportOptions } from '../cognition/usage-report.js';
1
3
  import type { AgentLoadOptions, AgentLoadResult, AgentSaveResult, CheckpointInput, ContextSearchOptions, ContextSearchResult } from '../runtime/types.js';
2
4
  import type { ContextOrientOptions, ContextOrientResult, ContextUseInput } from './types.js';
5
+ import type { ContextExpandInput, ContextExpandResult, ContextCompileOptions, ContextManifest } from '../compiler/types.js';
3
6
  export declare function saveContext(startPath: string | undefined, input: CheckpointInput, options?: {
4
7
  minSaveIntervalMs?: number;
5
8
  }): Promise<AgentSaveResult>;
6
- export declare function loadContext(startPath: string | undefined, options?: AgentLoadOptions): Promise<AgentLoadResult>;
7
- export declare function contextOrient(startPath: string | undefined, options?: ContextOrientOptions): Promise<ContextOrientResult>;
9
+ export declare function contextExpand(startPath: string | undefined, input: ContextExpandInput, interfaceName?: UsageInterface): Promise<ContextExpandResult>;
10
+ export declare function contextCompile(startPath: string | undefined, options?: ContextCompileOptions, interfaceName?: UsageInterface): Promise<ContextManifest>;
11
+ export declare function loadContext(startPath: string | undefined, options?: AgentLoadOptions, interfaceName?: UsageInterface): Promise<AgentLoadResult>;
12
+ export declare function contextOrient(startPath: string | undefined, options?: ContextOrientOptions, interfaceName?: UsageInterface | null): Promise<ContextOrientResult>;
13
+ export declare function contextUsageReport(startPath: string | undefined, options?: UsageReportOptions): Promise<UsageReport>;
8
14
  export declare function recordContextUse(startPath: string | undefined, input: ContextUseInput): Promise<{
9
15
  recorded: boolean;
10
16
  eventId: string;
@@ -8,6 +8,10 @@ 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';
12
+ import { compileContext } from '../compiler/compile.js';
13
+ import { withUsageTelemetry } from '../cognition/usage.js';
14
+ import { buildUsageReport } from '../cognition/usage-report.js';
11
15
  function normalized(value) {
12
16
  return value.trim().toLocaleLowerCase();
13
17
  }
@@ -138,147 +142,163 @@ export async function saveContext(startPath, input, options = {}) {
138
142
  const projectRoot = await resolveProjectRoot(startPath);
139
143
  return { projectRoot, result: await checkpoint(projectRoot, input, options) };
140
144
  }
141
- export async function loadContext(startPath, options = {}) {
142
- const projectRoot = await resolveProjectRoot(startPath);
143
- const result = await resume(projectRoot, options.contextId, options.maxChars ?? 4000);
144
- const knowledge = options.query === undefined ? null : await discoverKnowledge(projectRoot, options.query, { scope: options.scope, maxChars: options.maxChars ?? 4000 });
145
- const verifiedDeadends = await listDeadends(projectRoot);
146
- return {
147
- projectRoot,
148
- context: result.context,
149
- snapshot: result.snapshot,
150
- resumeSummary: result.resumeSummary,
151
- ...(options.includeDetails ? { details: result.details } : {}),
152
- knowledge,
153
- verifiedDeadendIds: verifiedDeadends.map((item) => item.deadendId),
154
- };
145
+ export async function contextExpand(startPath, input, interfaceName = 'agent') {
146
+ return withUsageTelemetry(startPath, 'expand', 'agent', input, () => resolveProjectRoot(startPath).then((projectRoot) => expandContext(projectRoot, input)), interfaceName);
155
147
  }
156
- export async function contextOrient(startPath, options = {}) {
157
- const projectRoot = await resolveProjectRoot(startPath);
158
- const query = options.query === undefined ? null : options.query.trim();
159
- if (query === '')
160
- throw new Error('context orient query must not be empty');
161
- let loaded;
162
- try {
163
- const v2 = await selectV2Orientation(projectRoot, options.contextId);
164
- loaded = v2
165
- ? await resumeSnapshot(projectRoot, v2.contextId, v2.snapshotId, options.maxChars ?? 4000)
166
- : await resume(projectRoot, options.contextId, options.maxChars ?? 4000, { touchLastUsedAt: false });
167
- }
168
- catch (error) {
169
- if (error instanceof Error && error.message === 'no active context found') {
170
- return {
171
- status: 'no_context',
172
- projectRoot,
173
- query,
174
- context: null,
175
- snapshot: null,
176
- resumeSummary: null,
177
- knowledge: emptyKnowledge(query ?? ''),
178
- deadends: emptyDeadends(query ?? ''),
179
- notes: [],
180
- truncated: false,
181
- explanation: {
182
- mode: 'v1-fallback',
183
- selection: 'v1',
184
- retrieval: { knowledge: {}, deadends: {} },
185
- truncation: { summary: false, knowledge: false, deadends: false, notes: false },
186
- },
187
- };
148
+ export async function contextCompile(startPath, options = {}, interfaceName = 'agent') {
149
+ return withUsageTelemetry(startPath, 'compile', 'agent', options, () => compileContext(startPath, options), interfaceName);
150
+ }
151
+ export async function loadContext(startPath, options = {}, interfaceName = 'agent') {
152
+ return withUsageTelemetry(startPath, 'resume', 'agent', options, async () => {
153
+ const projectRoot = await resolveProjectRoot(startPath);
154
+ const result = await resume(projectRoot, options.contextId, options.maxChars ?? 4000);
155
+ const knowledge = options.query === undefined ? null : await discoverKnowledge(projectRoot, options.query, { scope: options.scope, maxChars: options.maxChars ?? 4000 });
156
+ const verifiedDeadends = await listDeadends(projectRoot);
157
+ return {
158
+ projectRoot,
159
+ context: result.context,
160
+ snapshot: result.snapshot,
161
+ resumeSummary: result.resumeSummary,
162
+ ...(options.includeDetails ? { details: result.details } : {}),
163
+ knowledge,
164
+ verifiedDeadendIds: verifiedDeadends.map((item) => item.deadendId),
165
+ };
166
+ }, interfaceName);
167
+ }
168
+ export async function contextOrient(startPath, options = {}, interfaceName = 'agent') {
169
+ const action = async () => {
170
+ const projectRoot = await resolveProjectRoot(startPath);
171
+ const query = options.query === undefined ? null : options.query.trim();
172
+ if (query === '')
173
+ throw new Error('context orient query must not be empty');
174
+ let loaded;
175
+ try {
176
+ const v2 = await selectV2Orientation(projectRoot, options.contextId);
177
+ loaded = v2
178
+ ? await resumeSnapshot(projectRoot, v2.contextId, v2.snapshotId, options.maxChars ?? 4000)
179
+ : await resume(projectRoot, options.contextId, options.maxChars ?? 4000, { touchLastUsedAt: false });
188
180
  }
189
- throw error;
190
- }
191
- const v2 = await selectV2Orientation(projectRoot, options.contextId);
192
- const notes = (await listNotes(projectRoot, {
193
- contextId: loaded.context.id,
194
- openOnly: true,
195
- limit: v2 ? Number.MAX_SAFE_INTEGER : options.noteLimit ?? 20,
196
- maxChars: options.maxChars ?? 4000,
197
- })).filter((note) => !v2 || v2.noteIds.has(note.noteId)).slice(0, options.noteLimit ?? 20);
198
- const v2Knowledge = query === null || !v2 ? null : discoverV2Knowledge(v2.retrieval, query, v2.currentGit, {
199
- scope: options.scope,
200
- limit: options.knowledgeLimit ?? 10,
201
- maxChars: options.maxChars ?? 4000,
202
- });
203
- const v2Deadends = query === null || !v2 ? null : discoverV2Deadends(v2.retrieval, query, v2.currentGit, {
204
- scope: options.scope,
205
- limit: options.deadendLimit ?? 10,
206
- maxChars: options.maxChars ?? 4000,
207
- });
208
- const applicableKnowledge = query === null
209
- ? emptyKnowledge('')
210
- : v2Knowledge?.result ?? await discoverKnowledge(projectRoot, query, {
181
+ catch (error) {
182
+ if (error instanceof Error && error.message === 'no active context found') {
183
+ return {
184
+ status: 'no_context',
185
+ projectRoot,
186
+ query,
187
+ context: null,
188
+ snapshot: null,
189
+ resumeSummary: null,
190
+ knowledge: emptyKnowledge(query ?? ''),
191
+ deadends: emptyDeadends(query ?? ''),
192
+ notes: [],
193
+ truncated: false,
194
+ explanation: {
195
+ mode: 'v1-fallback',
196
+ selection: 'v1',
197
+ retrieval: { knowledge: {}, deadends: {} },
198
+ truncation: { summary: false, knowledge: false, deadends: false, notes: false },
199
+ },
200
+ };
201
+ }
202
+ throw error;
203
+ }
204
+ const v2 = await selectV2Orientation(projectRoot, options.contextId);
205
+ const notes = (await listNotes(projectRoot, {
206
+ contextId: loaded.context.id,
207
+ openOnly: true,
208
+ limit: v2 ? Number.MAX_SAFE_INTEGER : options.noteLimit ?? 20,
209
+ maxChars: options.maxChars ?? 4000,
210
+ })).filter((note) => !v2 || v2.noteIds.has(note.noteId)).slice(0, options.noteLimit ?? 20);
211
+ const v2Knowledge = query === null || !v2 ? null : discoverV2Knowledge(v2.retrieval, query, v2.currentGit, {
211
212
  scope: options.scope,
212
213
  limit: options.knowledgeLimit ?? 10,
213
214
  maxChars: options.maxChars ?? 4000,
214
215
  });
215
- const applicableDeadends = query === null
216
- ? emptyDeadends('')
217
- : v2Deadends?.result ?? await discoverDeadends(projectRoot, query, {
216
+ const v2Deadends = query === null || !v2 ? null : discoverV2Deadends(v2.retrieval, query, v2.currentGit, {
218
217
  scope: options.scope,
219
218
  limit: options.deadendLimit ?? 10,
220
219
  maxChars: options.maxChars ?? 4000,
221
220
  });
222
- const budget = { remaining: Math.max(0, options.maxChars ?? 4000) };
223
- const resumeSummary = {
224
- progressSummary: limited(loaded.resumeSummary.progressSummary, budget),
225
- lastError: loaded.resumeSummary.lastError === null ? null : limited(loaded.resumeSummary.lastError, budget),
226
- completed: limitedList(loaded.resumeSummary.completed, budget),
227
- pendingTasks: limitedList(loaded.resumeSummary.pendingTasks, budget),
228
- decisions: limitedList(loaded.resumeSummary.decisions, budget),
229
- risks: limitedList(loaded.resumeSummary.risks, budget),
230
- relatedFiles: limitedList(loaded.resumeSummary.relatedFiles, budget),
231
- branchOrCommitDrift: loaded.resumeSummary.branchOrCommitDrift,
232
- };
233
- const resumeTruncated = textLength(resumeSummary.progressSummary) < textLength(loaded.resumeSummary.progressSummary)
234
- || textLength(resumeSummary.lastError) < textLength(loaded.resumeSummary.lastError)
235
- || textLength(resumeSummary.completed) < textLength(loaded.resumeSummary.completed)
236
- || textLength(resumeSummary.pendingTasks) < textLength(loaded.resumeSummary.pendingTasks)
237
- || textLength(resumeSummary.decisions) < textLength(loaded.resumeSummary.decisions)
238
- || textLength(resumeSummary.risks) < textLength(loaded.resumeSummary.risks)
239
- || textLength(resumeSummary.relatedFiles) < textLength(loaded.resumeSummary.relatedFiles);
240
- const limitedKnowledgeResult = limitKnowledge(applicableKnowledge, budget);
241
- const limitedDeadendResult = limitDeadends(applicableDeadends, budget);
242
- const limitedNotesResult = limitNotes(notes, budget);
243
- return {
244
- status: 'ready',
245
- projectRoot,
246
- query,
247
- context: {
248
- id: loaded.context.id,
249
- title: loaded.context.title,
250
- status: loaded.context.status,
251
- branch: loaded.context.branch,
252
- commit: loaded.context.commit,
253
- updatedAt: loaded.context.updatedAt,
254
- },
255
- snapshot: {
256
- snapshotId: loaded.snapshot.snapshotId,
257
- mode: loaded.snapshot.mode,
258
- createdAt: loaded.snapshot.createdAt,
259
- branch: loaded.snapshot.branch,
260
- commit: loaded.snapshot.commit,
261
- },
262
- resumeSummary,
263
- knowledge: limitedKnowledgeResult.result,
264
- deadends: limitedDeadendResult.result,
265
- notes: limitedNotesResult.notes,
266
- truncated: resumeTruncated || limitedKnowledgeResult.truncated || limitedDeadendResult.truncated || limitedNotesResult.truncated,
267
- explanation: {
268
- mode: v2 ? 'v2' : 'v1-fallback',
269
- selection: v2?.selection ?? 'v1',
270
- retrieval: {
271
- knowledge: v2Knowledge?.exclusions ?? {},
272
- deadends: v2Deadends?.exclusions ?? {},
221
+ const applicableKnowledge = query === null
222
+ ? emptyKnowledge('')
223
+ : v2Knowledge?.result ?? await discoverKnowledge(projectRoot, query, {
224
+ scope: options.scope,
225
+ limit: options.knowledgeLimit ?? 10,
226
+ maxChars: options.maxChars ?? 4000,
227
+ });
228
+ const applicableDeadends = query === null
229
+ ? emptyDeadends('')
230
+ : v2Deadends?.result ?? await discoverDeadends(projectRoot, query, {
231
+ scope: options.scope,
232
+ limit: options.deadendLimit ?? 10,
233
+ maxChars: options.maxChars ?? 4000,
234
+ });
235
+ const budget = { remaining: Math.max(0, options.maxChars ?? 4000) };
236
+ const resumeSummary = {
237
+ progressSummary: limited(loaded.resumeSummary.progressSummary, budget),
238
+ lastError: loaded.resumeSummary.lastError === null ? null : limited(loaded.resumeSummary.lastError, budget),
239
+ completed: limitedList(loaded.resumeSummary.completed, budget),
240
+ pendingTasks: limitedList(loaded.resumeSummary.pendingTasks, budget),
241
+ decisions: limitedList(loaded.resumeSummary.decisions, budget),
242
+ risks: limitedList(loaded.resumeSummary.risks, budget),
243
+ relatedFiles: limitedList(loaded.resumeSummary.relatedFiles, budget),
244
+ branchOrCommitDrift: loaded.resumeSummary.branchOrCommitDrift,
245
+ };
246
+ const resumeTruncated = textLength(resumeSummary.progressSummary) < textLength(loaded.resumeSummary.progressSummary)
247
+ || textLength(resumeSummary.lastError) < textLength(loaded.resumeSummary.lastError)
248
+ || textLength(resumeSummary.completed) < textLength(loaded.resumeSummary.completed)
249
+ || textLength(resumeSummary.pendingTasks) < textLength(loaded.resumeSummary.pendingTasks)
250
+ || textLength(resumeSummary.decisions) < textLength(loaded.resumeSummary.decisions)
251
+ || textLength(resumeSummary.risks) < textLength(loaded.resumeSummary.risks)
252
+ || textLength(resumeSummary.relatedFiles) < textLength(loaded.resumeSummary.relatedFiles);
253
+ const limitedKnowledgeResult = limitKnowledge(applicableKnowledge, budget);
254
+ const limitedDeadendResult = limitDeadends(applicableDeadends, budget);
255
+ const limitedNotesResult = limitNotes(notes, budget);
256
+ return {
257
+ status: 'ready',
258
+ projectRoot,
259
+ query,
260
+ context: {
261
+ id: loaded.context.id,
262
+ title: loaded.context.title,
263
+ status: loaded.context.status,
264
+ branch: loaded.context.branch,
265
+ commit: loaded.context.commit,
266
+ updatedAt: loaded.context.updatedAt,
267
+ },
268
+ snapshot: {
269
+ snapshotId: loaded.snapshot.snapshotId,
270
+ mode: loaded.snapshot.mode,
271
+ createdAt: loaded.snapshot.createdAt,
272
+ branch: loaded.snapshot.branch,
273
+ commit: loaded.snapshot.commit,
273
274
  },
274
- truncation: {
275
- summary: resumeTruncated,
276
- knowledge: limitedKnowledgeResult.truncated,
277
- deadends: limitedDeadendResult.truncated,
278
- notes: limitedNotesResult.truncated,
275
+ resumeSummary,
276
+ knowledge: limitedKnowledgeResult.result,
277
+ deadends: limitedDeadendResult.result,
278
+ notes: limitedNotesResult.notes,
279
+ truncated: resumeTruncated || limitedKnowledgeResult.truncated || limitedDeadendResult.truncated || limitedNotesResult.truncated,
280
+ explanation: {
281
+ mode: v2 ? 'v2' : 'v1-fallback',
282
+ selection: v2?.selection ?? 'v1',
283
+ retrieval: {
284
+ knowledge: v2Knowledge?.exclusions ?? {},
285
+ deadends: v2Deadends?.exclusions ?? {},
286
+ },
287
+ truncation: {
288
+ summary: resumeTruncated,
289
+ knowledge: limitedKnowledgeResult.truncated,
290
+ deadends: limitedDeadendResult.truncated,
291
+ notes: limitedNotesResult.truncated,
292
+ },
279
293
  },
280
- },
294
+ };
281
295
  };
296
+ return interfaceName === null
297
+ ? action()
298
+ : withUsageTelemetry(startPath, 'orient', 'agent', options, action, interfaceName);
299
+ }
300
+ export async function contextUsageReport(startPath, options = {}) {
301
+ return buildUsageReport(startPath, options);
282
302
  }
283
303
  export async function recordContextUse(startPath, input) {
284
304
  return recordUse(startPath, input);
@@ -1,5 +1,7 @@
1
1
  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
+ export { contextCompile } from './api.js';
4
5
  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';
6
+ export { canonicalJson, contentHash, estimateTokens, rebuildManifestHash, validateManifestIdentity } from '../compiler/identity.js';
7
+ 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,4 @@
1
1
  export * from './api.js';
2
+ export { contextCompile } from './api.js';
2
3
  export { compileContext } from '../compiler/compile.js';
4
+ export { canonicalJson, contentHash, estimateTokens, rebuildManifestHash, validateManifestIdentity } from '../compiler/identity.js';
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { checkpoint, resume } from '../runtime/runtime.js';
3
- import { contextOrient } from '../agent/api.js';
4
- import { compileContext } from '../compiler/compile.js';
2
+ import { checkpoint } from '../runtime/runtime.js';
3
+ import { contextCompile, contextExpand, contextOrient, contextUsageReport, loadContext } from '../agent/api.js';
4
+ import { formatUsageReport } from '../cognition/usage-report.js';
5
5
  import { importV1, verifyV1Import } from '../cognition/migration-v1.js';
6
6
  import { recordUse } from '../cognition/feedback.js';
7
7
  import { verifyJournal } from '../cognition/journal.js';
@@ -81,6 +81,34 @@ Initialize the local .context runtime layout. Re-running init is safe.
81
81
 
82
82
  Options:
83
83
  --path <path> Project path`,
84
+ usage: `usage: ctx usage report [options]
85
+
86
+ Show read-only context asset, reuse, token investment, and operation metrics.
87
+
88
+ Options:
89
+ --path <path> Project path
90
+ --since <ISO> Window start
91
+ --until <ISO> Window end
92
+ --days <number> Rolling window length (default: 30)
93
+ --month <YYYY-MM> Calendar month window
94
+ --granularity <value> day|week|month
95
+ --format <value> json|ascii (default: json)
96
+ --baseline-tokens <number> Explicit comparison token count
97
+ --baseline-source <value> manual|recorded-control`,
98
+ 'usage report': `usage: ctx usage report [options]
99
+
100
+ Show read-only context asset, reuse, token investment, and operation metrics.
101
+
102
+ Options:
103
+ --path <path> Project path
104
+ --since <ISO> Window start
105
+ --until <ISO> Window end
106
+ --days <number> Rolling window length (default: 30)
107
+ --month <YYYY-MM> Calendar month window
108
+ --granularity <value> day|week|month
109
+ --format <value> json|ascii (default: json)
110
+ --baseline-tokens <number> Explicit comparison token count
111
+ --baseline-source <value> manual|recorded-control`,
84
112
  checkpoint: `usage: ctx checkpoint [options]
85
113
 
86
114
  Save structured work state as a baseline or incremental snapshot.
@@ -131,6 +159,27 @@ Options:
131
159
  --knowledge-limit <number> Maximum knowledge matches (default: 10)
132
160
  --deadend-limit <number> Maximum deadend matches (default: 10)
133
161
  --note-limit <number> Maximum open notes (default: 20)
162
+ --activity-limit <number> Maximum recent activity groups (default: 20)`,
163
+ expand: `usage: ctx expand [options]
164
+
165
+ Expand one selected compiler candidate without writing project state.
166
+
167
+ Options:
168
+ --path <path> Project path
169
+ --manifest-hash <hash> Manifest identity hash from ctx compile
170
+ --candidate-id <id> Selected candidate ID
171
+ --item-hash <hash> Selected item hash
172
+ --level <level> structured|evidence
173
+ --max-chars <number> Maximum expanded characters
174
+ --compile-max-chars <number> Original compile budget (default: 4000)
175
+ --context <id> Context ID
176
+ --query <text> Original compile query
177
+ --scenario <scenario> resume|implementation|debugging|verification|handoff|exploration|unknown
178
+ --scope <scope> Exact consensus scope
179
+ --paths <paths> Comma-separated project-relative paths
180
+ --knowledge-limit <number> Maximum knowledge matches (default: 10)
181
+ --deadend-limit <number> Maximum deadend matches (default: 10)
182
+ --note-limit <number> Maximum open notes (default: 20)
134
183
  --activity-limit <number> Maximum recent activity groups (default: 20)`,
135
184
  agent: `usage: ctx agent serve
136
185
 
@@ -139,7 +188,7 @@ Start an MCP stdio server for Agent integrations.
139
188
  Run "ctx agent serve --help" for details.`,
140
189
  'agent serve': `usage: ctx agent serve
141
190
 
142
- Start an MCP stdio server exposing the context_orient tool.
191
+ Start an MCP stdio server exposing context_orient, context_expand, context_compile, context_resume, context_note_list, context_note_add, context_usage_report, and context_checkpoint tools.
143
192
 
144
193
  The server owns standard input/output; do not use it interactively.`,
145
194
  hook: `usage: ctx hook claude-code session-start|user-prompt
@@ -341,7 +390,7 @@ Stop the local runtime through authenticated loopback IPC.`,
341
390
  Internal runtime daemon entry point.`,
342
391
  };
343
392
  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]
393
+ return `usage: ctx init|checkpoint|resume|orient|compile|expand|usage|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity|migrate|runtime|filesystem|feedback|journal [options]
345
394
 
346
395
  Run \"ctx <command> --help\" for command details.`;
347
396
  }
@@ -370,7 +419,7 @@ function printHelp(args) {
370
419
  || (command === 'note' && ['add', 'list'].includes(args[1]))
371
420
  || (command === 'migrate' && ['v1', 'verify'].includes(args[1]))
372
421
  || (command === 'runtime' && ['enable', 'disable', 'start', 'stop', 'status', 'serve'].includes(args[1]))
373
- || (command === 'filesystem' && ['enable', 'disable', 'status'].includes(args[1]));
422
+ || (command === 'usage' && args[1] === 'report');
374
423
  const key = nested ? `${command} ${args[1]}` : command;
375
424
  const nestedKey = key === 'integrate claude' && (args[2] === 'inspect' || args[2] === 'install') ? `${key} ${args[2]}` : key;
376
425
  const help = HELP[nestedKey];
@@ -428,7 +477,7 @@ async function run(args) {
428
477
  case 'resume':
429
478
  validateOptions(args.slice(1), ['--path', '--context', '--max-chars'], ['--path', '--context', '--max-chars']);
430
479
  validatePositionals(positionals(args.slice(1), ['--path', '--context', '--max-chars']), 0, HELP.resume);
431
- print(await resume(target, option(args, '--context'), numericOption(args, '--max-chars', 4000)));
480
+ print(await loadContext(target, { contextId: option(args, '--context'), maxChars: numericOption(args, '--max-chars', 4000) }, 'cli'));
432
481
  return;
433
482
  case 'orient': {
434
483
  const valueOptions = ['--path', '--context', '--scope', '--max-chars', '--knowledge-limit', '--deadend-limit', '--note-limit'];
@@ -444,7 +493,7 @@ async function run(args) {
444
493
  knowledgeLimit: numericOption(args, '--knowledge-limit', 10),
445
494
  deadendLimit: numericOption(args, '--deadend-limit', 10),
446
495
  noteLimit: numericOption(args, '--note-limit', 20),
447
- }));
496
+ }, 'cli'));
448
497
  return;
449
498
  }
450
499
  case 'compile': {
@@ -452,7 +501,7 @@ async function run(args) {
452
501
  validateOptions(args.slice(1), valueOptions, valueOptions);
453
502
  const query = positionals(args.slice(1), valueOptions);
454
503
  validatePositionals(query, 1, HELP.compile);
455
- print(await compileContext(target, {
504
+ print(await contextCompile(target, {
456
505
  contextId: option(args, '--context'),
457
506
  query: query[0],
458
507
  scenario: enumOption(args, '--scenario', ['resume', 'implementation', 'debugging', 'verification', 'handoff', 'exploration', 'unknown']),
@@ -463,9 +512,63 @@ async function run(args) {
463
512
  deadendLimit: numericOption(args, '--deadend-limit', 10),
464
513
  noteLimit: numericOption(args, '--note-limit', 20),
465
514
  activityLimit: numericOption(args, '--activity-limit', 20),
515
+ }, 'cli'));
516
+ return;
517
+ }
518
+ case 'expand': {
519
+ 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'];
520
+ validateOptions(args.slice(1), valueOptions, valueOptions);
521
+ validatePositionals(positionals(args.slice(1), valueOptions), 0, HELP.expand);
522
+ const level = enumOption(args, '--level', ['structured', 'evidence']);
523
+ if (!level)
524
+ throw new Error('--level is required');
525
+ const manifestHash = option(args, '--manifest-hash');
526
+ if (!manifestHash)
527
+ throw new Error('--manifest-hash is required');
528
+ const compileMaxChars = numericOption(args, '--compile-max-chars', 4000);
529
+ await ensureRuntimeForSession(target, { source: 'cli', timeoutMs: 1_000 }).catch(() => undefined);
530
+ print(await contextExpand(target, {
531
+ manifestHash,
532
+ candidateId: option(args, '--candidate-id'),
533
+ itemHash: option(args, '--item-hash'),
534
+ level: level,
535
+ maxChars: option(args, '--max-chars') === undefined ? undefined : numericOption(args, '--max-chars', 0),
536
+ compile: {
537
+ contextId: option(args, '--context'),
538
+ query: option(args, '--query'),
539
+ scenario: enumOption(args, '--scenario', ['resume', 'implementation', 'debugging', 'verification', 'handoff', 'exploration', 'unknown']),
540
+ scope: option(args, '--scope'),
541
+ paths: listOption(args, '--paths'),
542
+ maxChars: compileMaxChars,
543
+ knowledgeLimit: numericOption(args, '--knowledge-limit', 10),
544
+ deadendLimit: numericOption(args, '--deadend-limit', 10),
545
+ noteLimit: numericOption(args, '--note-limit', 20),
546
+ activityLimit: numericOption(args, '--activity-limit', 20),
547
+ },
466
548
  }));
467
549
  return;
468
550
  }
551
+ case 'usage': {
552
+ if (args[1] !== 'report')
553
+ throw new Error(HELP.usage);
554
+ const valueOptions = ['--path', '--since', '--until', '--days', '--month', '--granularity', '--format', '--baseline-tokens', '--baseline-source'];
555
+ validateOptions(args.slice(2), valueOptions, valueOptions);
556
+ validatePositionals(positionals(args.slice(2), valueOptions), 0, HELP['usage report']);
557
+ const report = await contextUsageReport(target, {
558
+ since: option(args, '--since'),
559
+ until: option(args, '--until'),
560
+ days: option(args, '--days') === undefined ? undefined : numericOption(args, '--days', 30),
561
+ month: option(args, '--month'),
562
+ granularity: enumOption(args, '--granularity', ['day', 'week', 'month']),
563
+ baselineTokens: option(args, '--baseline-tokens') === undefined ? undefined : numericOption(args, '--baseline-tokens', 0),
564
+ baselineSource: enumOption(args, '--baseline-source', ['manual', 'recorded-control']),
565
+ });
566
+ if (enumOption(args, '--format', ['json', 'ascii']) === 'ascii')
567
+ process.stdout.write(formatUsageReport(report));
568
+ else
569
+ print(report);
570
+ return;
571
+ }
469
572
  case 'agent':
470
573
  if (args[1] !== 'serve')
471
574
  throw new Error(HELP.agent);
@@ -11,9 +11,10 @@ const eventTypes = new Set([
11
11
  'context.checkpoint.imported', 'context.state.recorded', 'note.recorded', 'note.resolved', 'note.absorbed',
12
12
  'knowledge.proposed', 'knowledge.verified', 'knowledge.deprecated', 'knowledge.rejected',
13
13
  'deadend.proposed', 'deadend.verified', 'deadend.obsoleted', 'deadend.rejected', 'record.used',
14
+ 'usage.operation.completed',
14
15
  'projection.advanced', 'projection.failed',
15
16
  ]);
16
- const sourceKinds = new Set(['cli', 'claude-hook', 'mcp', 'git-observer', 'fs-observer', 'migration']);
17
+ const sourceKinds = new Set(['cli', 'claude-hook', 'mcp', 'agent', 'git-observer', 'fs-observer', 'migration']);
17
18
  const segmentName = '00000001.ndjson';
18
19
  function canonical(value) {
19
20
  if (value === null || typeof value !== 'object')
@@ -55,6 +56,33 @@ function validGitAnchor(value) {
55
56
  function validIsoDate(value) {
56
57
  return typeof value === 'string' && Number.isFinite(Date.parse(value));
57
58
  }
59
+ function validatePayloadBoundary(input) {
60
+ const payload = input.payload;
61
+ if (input.type === 'host.prompt.submitted' && (typeof payload.intent !== 'string' || payload.intent.trim().length === 0)) {
62
+ throw new Error('host prompt intent must not be empty');
63
+ }
64
+ if (['note.recorded', 'note.resolved', 'note.absorbed'].includes(input.type)) {
65
+ const record = payload.record;
66
+ if (!isRecord(record) || typeof record.message !== 'string' || record.message.trim().length === 0)
67
+ throw new Error('note message must not be empty');
68
+ }
69
+ if (input.type === 'usage.operation.completed') {
70
+ const requiredNumbers = ['durationMs', 'inputChars', 'outputChars', 'inputBytes', 'outputBytes', 'estimatedInputTokens', 'estimatedOutputTokens'];
71
+ if (payload.operation !== 'orient' && payload.operation !== 'compile' && payload.operation !== 'expand' && payload.operation !== 'resume')
72
+ throw new Error('invalid usage operation');
73
+ if (payload.interface !== 'cli' && payload.interface !== 'mcp' && payload.interface !== 'agent' && payload.interface !== 'hook')
74
+ throw new Error('invalid usage interface');
75
+ if (typeof payload.success !== 'boolean' || payload.estimatedMethod !== 'chars-div-four' || requiredNumbers.some((key) => typeof payload[key] !== 'number' || !Number.isFinite(payload[key]) || payload[key] < 0))
76
+ throw new Error('invalid usage telemetry payload');
77
+ if (!payload.success && typeof payload.errorClass !== 'string')
78
+ throw new Error('usage errors require an error class');
79
+ }
80
+ if (input.type === 'workspace.paths.changed') {
81
+ const changes = payload.changes;
82
+ if (!Array.isArray(changes) || changes.length === 0)
83
+ throw new Error('workspace activity must contain changes');
84
+ }
85
+ }
58
86
  function validateEventShape(event, projectId, file, line) {
59
87
  if (!isRecord(event)
60
88
  || event.schemaVersion !== 2
@@ -140,6 +168,7 @@ export async function verifyJournal(startPath) {
140
168
  return readJournal(startPath, { allowPartialTail: false });
141
169
  }
142
170
  export async function appendEvent(startPath, input) {
171
+ validatePayloadBoundary(input);
143
172
  const projectRoot = await resolveProjectRoot(startPath);
144
173
  return withLock(`${locksDirectory(projectRoot)}/cognition-journal.lock`, async () => {
145
174
  await recoverPartialTail(projectRoot);
Binary file
@@ -1,6 +1,6 @@
1
1
  export declare const COGNITION_LAYOUT_VERSION: "event-journal-v2";
2
- export type CognitionEventSource = 'cli' | 'claude-hook' | 'mcp' | 'git-observer' | 'fs-observer' | 'migration';
3
- export type CognitionEventType = 'runtime.enabled' | 'runtime.started' | 'runtime.stopped' | 'host.session.started' | 'host.prompt.submitted' | 'git.state.observed' | 'git.branch.changed' | 'git.head.changed' | 'workspace.paths.changed' | 'context.checkpoint.imported' | 'context.state.recorded' | 'note.recorded' | 'note.resolved' | 'note.absorbed' | 'knowledge.proposed' | 'knowledge.verified' | 'knowledge.deprecated' | 'knowledge.rejected' | 'deadend.proposed' | 'deadend.verified' | 'deadend.obsoleted' | 'deadend.rejected' | 'record.used' | 'projection.advanced' | 'projection.failed';
2
+ export type CognitionEventSource = 'cli' | 'claude-hook' | 'mcp' | 'agent' | 'git-observer' | 'fs-observer' | 'migration';
3
+ export type CognitionEventType = 'runtime.enabled' | 'runtime.started' | 'runtime.stopped' | 'host.session.started' | 'host.prompt.submitted' | 'git.state.observed' | 'git.branch.changed' | 'git.head.changed' | 'workspace.paths.changed' | 'context.checkpoint.imported' | 'context.state.recorded' | 'note.recorded' | 'note.resolved' | 'note.absorbed' | 'knowledge.proposed' | 'knowledge.verified' | 'knowledge.deprecated' | 'knowledge.rejected' | 'deadend.proposed' | 'deadend.verified' | 'deadend.obsoleted' | 'deadend.rejected' | 'record.used' | 'usage.operation.completed' | 'projection.advanced' | 'projection.failed';
4
4
  export interface CognitionGitAnchor {
5
5
  worktreeId: string | null;
6
6
  branch: string | null;
@@ -67,6 +67,20 @@ export interface CognitionUseFeedback {
67
67
  entityId: string;
68
68
  outcome: 'used';
69
69
  }
70
+ export interface UsageOperationCompletedPayload {
71
+ operation: 'orient' | 'compile' | 'expand' | 'resume';
72
+ interface: 'cli' | 'mcp' | 'agent' | 'hook';
73
+ success: boolean;
74
+ durationMs: number;
75
+ inputChars: number;
76
+ outputChars: number;
77
+ inputBytes: number;
78
+ outputBytes: number;
79
+ estimatedInputTokens: number;
80
+ estimatedOutputTokens: number;
81
+ estimatedMethod: 'chars-div-four';
82
+ errorClass?: string;
83
+ }
70
84
  export interface CognitionRetrievalEntry {
71
85
  entity: 'knowledge' | 'deadend';
72
86
  entityId: string;