fluffy-context 0.6.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +99 -8
  2. package/dist/src/agent/index.d.ts +2 -0
  3. package/dist/src/agent/index.js +1 -0
  4. package/dist/src/cli/main.js +69 -5
  5. package/dist/src/cognition/filesystem-observer.d.ts +41 -0
  6. package/dist/src/cognition/filesystem-observer.js +164 -0
  7. package/dist/src/cognition/git-observer.d.ts +1 -0
  8. package/dist/src/cognition/git-observer.js +3 -2
  9. package/dist/src/cognition/journal.d.ts +4 -1
  10. package/dist/src/cognition/journal.js +83 -4
  11. package/dist/src/cognition/migration-v1.js +1 -1
  12. package/dist/src/cognition/orient.js +4 -3
  13. package/dist/src/cognition/policy.d.ts +4 -0
  14. package/dist/src/cognition/policy.js +49 -0
  15. package/dist/src/cognition/projections.js +0 -0
  16. package/dist/src/cognition/runtime.d.ts +20 -1
  17. package/dist/src/cognition/runtime.js +229 -53
  18. package/dist/src/cognition/types.d.ts +16 -0
  19. package/dist/src/cognition/v1-bridge.js +3 -2
  20. package/dist/src/compiler/activity.d.ts +3 -0
  21. package/dist/src/compiler/activity.js +79 -0
  22. package/dist/src/compiler/compile.d.ts +2 -0
  23. package/dist/src/compiler/compile.js +330 -0
  24. package/dist/src/compiler/sources.d.ts +19 -0
  25. package/dist/src/compiler/sources.js +130 -0
  26. package/dist/src/compiler/types.d.ts +187 -0
  27. package/dist/src/compiler/types.js +1 -0
  28. package/dist/src/hooks/claude-code.js +7 -15
  29. package/dist/src/mcp/main.js +2 -0
  30. package/dist/src/mcp/server.js +2 -0
  31. package/dist/src/storage/layout.d.ts +1 -0
  32. package/dist/src/storage/layout.js +3 -0
  33. package/dist/src/version.d.ts +1 -1
  34. package/dist/src/version.js +1 -1
  35. package/package.json +1 -1
  36. package/skills/fluffy-context/SKILL.md +2 -1
@@ -32,6 +32,7 @@ export interface CognitionEvent<T = Record<string, unknown>> {
32
32
  export interface CognitionRuntimePolicy {
33
33
  schemaVersion: 2;
34
34
  enabled: boolean;
35
+ autoStart: boolean;
35
36
  allowHookStartup: boolean;
36
37
  inactivityTimeoutMs: number;
37
38
  promptIntentMaxChars: number;
@@ -141,6 +142,21 @@ export interface CognitionRecordPayload<T extends Record<string, unknown> = Reco
141
142
  sourceHash?: string;
142
143
  anchorProvenance?: 'snapshot' | 'live' | 'global';
143
144
  }
145
+ export interface WorkspacePathChange {
146
+ path: string;
147
+ kind: 'created' | 'modified' | 'deleted' | 'renamed' | 'unknown';
148
+ fileType: 'file';
149
+ sizeBytes?: number;
150
+ mtimeMs?: number;
151
+ }
152
+ export interface WorkspacePathsChangedPayload {
153
+ schemaVersion: 1;
154
+ paths: string[];
155
+ changes: WorkspacePathChange[];
156
+ pathCount: number;
157
+ coalesced: boolean;
158
+ truncated: boolean;
159
+ }
144
160
  export interface CognitionProjection {
145
161
  schemaVersion: 2;
146
162
  contexts: Record<string, CognitionRecordPayload>;
@@ -2,11 +2,12 @@ import crypto from 'node:crypto';
2
2
  import path from 'node:path';
3
3
  import { access, readFile, readdir } from 'node:fs/promises';
4
4
  import { readGitWorktreeState } from '../git/git-adapter.js';
5
- import { cognitionManifestPath, cognitionPolicyPath, cognitionV1SyncPath, contextsRoot, deadendsPath, knowledgePath, notesPath, snapshotsDirectory } from '../storage/layout.js';
5
+ import { cognitionManifestPath, cognitionV1SyncPath, contextsRoot, deadendsPath, knowledgePath, notesPath, snapshotsDirectory } from '../storage/layout.js';
6
6
  import { readJson } from '../storage/json-store.js';
7
7
  import { atomicWriteJson } from '../storage/atomic-write.js';
8
8
  import { appendEvent } from './journal.js';
9
9
  import { rebuildProjections } from './projections.js';
10
+ import { loadRuntimePolicy } from './policy.js';
10
11
  function digest(record) {
11
12
  return crypto.createHash('sha256').update(JSON.stringify(record)).digest('hex');
12
13
  }
@@ -34,7 +35,7 @@ export async function v1Synced(projectRoot) {
34
35
  async function enabled(projectRoot) {
35
36
  try {
36
37
  await access(cognitionManifestPath(projectRoot));
37
- return (await readJson(cognitionPolicyPath(projectRoot))).enabled;
38
+ return (await loadRuntimePolicy(projectRoot)).enabled;
38
39
  }
39
40
  catch {
40
41
  return false;
@@ -0,0 +1,3 @@
1
+ import type { BranchWorkspace, CognitionEvent } from '../cognition/types.js';
2
+ import type { ContextActivityRecord, ContextAnchor } from './types.js';
3
+ export declare function extractActivity(projectRoot: string, events: CognitionEvent[], anchor: ContextAnchor, workspace: BranchWorkspace | null, limit: number): Promise<ContextActivityRecord[]>;
@@ -0,0 +1,79 @@
1
+ import { filterPaths } from '../capture/context-filter.js';
2
+ import { workspaceKey } from '../cognition/projections.js';
3
+ const changeKinds = new Set(['created', 'modified', 'deleted', 'renamed', 'unknown']);
4
+ function normalizedPath(value) {
5
+ if (typeof value !== 'string')
6
+ return null;
7
+ const normalized = value.replaceAll('\\', '/');
8
+ return normalized.length > 0 ? normalized : null;
9
+ }
10
+ function stableCompare(left, right) {
11
+ return left < right ? -1 : left > right ? 1 : 0;
12
+ }
13
+ function metadataNumber(value) {
14
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
15
+ }
16
+ function parsePayload(event) {
17
+ const payload = event.payload;
18
+ return payload.schemaVersion === 1 && typeof payload === 'object' && payload !== null ? payload : null;
19
+ }
20
+ async function activityForEvent(projectRoot, event, focus) {
21
+ const payload = parsePayload(event);
22
+ if (!payload)
23
+ return null;
24
+ const rawPaths = Array.isArray(payload.paths) ? payload.paths.map(normalizedPath).filter((value) => value !== null) : [];
25
+ const rawChanges = Array.isArray(payload.changes) ? payload.changes : [];
26
+ const changePaths = rawChanges.map((change) => normalizedPath(change?.path)).filter((value) => value !== null);
27
+ const allowed = await filterPaths(projectRoot, [...new Set([...rawPaths, ...changePaths])]);
28
+ if (allowed.length === 0)
29
+ return null;
30
+ const allowedSet = new Set(allowed);
31
+ const changes = [];
32
+ for (const change of rawChanges) {
33
+ const path = normalizedPath(change?.path);
34
+ if (!path || !allowedSet.has(path))
35
+ continue;
36
+ changes.push({
37
+ path,
38
+ kind: changeKinds.has(change?.kind) ? change.kind : 'unknown',
39
+ fileType: 'file',
40
+ ...(metadataNumber(change?.sizeBytes) !== undefined ? { sizeBytes: metadataNumber(change?.sizeBytes) } : {}),
41
+ ...(metadataNumber(change?.mtimeMs) !== undefined ? { mtimeMs: metadataNumber(change?.mtimeMs) } : {}),
42
+ });
43
+ }
44
+ const fallbackChanges = changes.length > 0 ? changes : allowed.map((path) => ({ path, kind: 'unknown', fileType: 'file' }));
45
+ const paths = [...new Set(fallbackChanges.map((change) => change.path))].sort(stableCompare);
46
+ return {
47
+ eventId: event.eventId,
48
+ occurredAt: event.occurredAt,
49
+ anchor: {
50
+ projectRoot,
51
+ worktreeId: event.git.worktreeId,
52
+ branch: event.git.branch,
53
+ ref: event.git.ref,
54
+ head: event.git.head,
55
+ },
56
+ paths,
57
+ changes: fallbackChanges.sort((left, right) => stableCompare(left.path, right.path)),
58
+ pathCount: typeof payload.pathCount === 'number' && Number.isSafeInteger(payload.pathCount) && payload.pathCount >= paths.length ? payload.pathCount : paths.length,
59
+ coalesced: payload.coalesced === true,
60
+ truncated: payload.truncated === true,
61
+ focusMatch: paths.some((path) => focus.includes(path)),
62
+ };
63
+ }
64
+ export async function extractActivity(projectRoot, events, anchor, workspace, limit) {
65
+ if (limit <= 0 || !workspace)
66
+ return [];
67
+ const currentWorkspace = workspaceKey(anchor);
68
+ const matching = events
69
+ .filter((event) => event.type === 'workspace.paths.changed' && workspaceKey(event.git) === currentWorkspace)
70
+ .sort((left, right) => right.occurredAt.localeCompare(left.occurredAt) || right.eventId.localeCompare(left.eventId))
71
+ .slice(0, limit);
72
+ const records = [];
73
+ for (const event of matching) {
74
+ const record = await activityForEvent(projectRoot, event, workspace.pathFocus);
75
+ if (record)
76
+ records.push(record);
77
+ }
78
+ return records;
79
+ }
@@ -0,0 +1,2 @@
1
+ import type { ContextCompileOptions, ContextManifest } from './types.js';
2
+ export declare function compileContext(startPath?: string, options?: ContextCompileOptions): Promise<ContextManifest>;
@@ -0,0 +1,330 @@
1
+ import { filterPaths } from '../capture/context-filter.js';
2
+ import { queryTerms } from '../cognition/retrieval.js';
3
+ import { listDeadends, listKnowledge } from '../runtime/knowledge.js';
4
+ import { listNotes } from '../runtime/notes.js';
5
+ import { extractActivity } from './activity.js';
6
+ import { latestHostIntent, loadCompilerSources, sourceDescriptor } from './sources.js';
7
+ const scenarioKeywords = [
8
+ { scenario: 'debugging', words: ['bug', 'error', 'fail', 'failure', 'exception', 'regression', 'debug', '失败', '报错', '异常', '修复'] },
9
+ { scenario: 'verification', words: ['test', 'verify', 'check', 'build', '测试', '验证', '检查', '构建'] },
10
+ { scenario: 'handoff', words: ['handoff', 'summary', '交接', '总结', '移交'] },
11
+ { scenario: 'implementation', words: ['implement', 'add', 'change', 'feature', '实现', '新增', '修改', '功能'] },
12
+ { scenario: 'exploration', words: ['search', 'investigate', 'find', 'explore', '查找', '调查', '分析', '探索'] },
13
+ { scenario: 'resume', words: ['resume', 'continue', 'restore', '继续', '恢复'] },
14
+ ];
15
+ const priorities = {
16
+ summary: 100,
17
+ pending: 95,
18
+ risk: 94,
19
+ decision: 92,
20
+ note: 85,
21
+ knowledge: 75,
22
+ deadend: 73,
23
+ activity: 70,
24
+ };
25
+ function clean(value) {
26
+ return value.normalize('NFC').trim().toLowerCase();
27
+ }
28
+ function stableCompare(left, right) {
29
+ return left < right ? -1 : left > right ? 1 : 0;
30
+ }
31
+ function anchorFromSources(sources) {
32
+ return sources.anchor;
33
+ }
34
+ function signal(source, value, origin, confidence) {
35
+ return { source, value, origin, confidence };
36
+ }
37
+ function inferScenario(query, explicit) {
38
+ if (explicit)
39
+ return { scenario: explicit, signals: [signal('explicit-scenario', explicit, 'explicit', 'high')] };
40
+ if (!query)
41
+ return { scenario: 'resume', signals: [] };
42
+ const normalized = clean(query);
43
+ const matches = scenarioKeywords.flatMap(({ scenario, words }) => words.filter((word) => normalized.includes(clean(word))).map((word) => ({ scenario, word })));
44
+ if (matches.length === 0)
45
+ return { scenario: 'unknown', signals: [] };
46
+ const selected = matches[0].scenario;
47
+ return {
48
+ scenario: selected,
49
+ signals: matches.map(({ scenario, word }) => signal('context-state', `${scenario}:${word}`, 'deterministic', 'medium')),
50
+ };
51
+ }
52
+ function sourcePaths(record) {
53
+ const applicability = record.applicability;
54
+ if (!applicability || typeof applicability !== 'object' || Array.isArray(applicability))
55
+ return [];
56
+ const paths = applicability.paths;
57
+ return Array.isArray(paths) ? paths.filter((value) => typeof value === 'string') : [];
58
+ }
59
+ function sourceApplicability(record, descriptor) {
60
+ const applicability = record.applicability;
61
+ if (!applicability || typeof applicability !== 'object' || Array.isArray(applicability))
62
+ return descriptor;
63
+ const value = applicability;
64
+ const branchMode = value.branchMode === 'global' || value.branchMode === 'exact' ? value.branchMode : descriptor.applicability.branchMode;
65
+ const branches = Array.isArray(value.branches) ? value.branches.filter((item) => typeof item === 'string').map(clean).sort(stableCompare) : descriptor.applicability.branches;
66
+ const paths = sourcePaths(record).map((item) => item.replaceAll('\\', '/')).sort(stableCompare);
67
+ return {
68
+ ...descriptor,
69
+ scope: typeof record.scope === 'string' ? record.scope : descriptor.scope,
70
+ paths,
71
+ applicability: {
72
+ ...descriptor.applicability,
73
+ branchMode,
74
+ branches,
75
+ paths,
76
+ ref: descriptor.applicability.ref,
77
+ },
78
+ };
79
+ }
80
+ function matched(text, terms) {
81
+ const normalized = clean(text);
82
+ const matchedTerms = terms.filter((term) => normalized.includes(term));
83
+ return { score: matchedTerms.length, matchedTerms };
84
+ }
85
+ function makeCandidate(section, source, content, text, terms, matchedFields, discriminator) {
86
+ const hit = matched(text, terms);
87
+ return {
88
+ candidateId: `${section}:${source.sourceId}:${matchedFields.join('.')}:${discriminator}`,
89
+ section,
90
+ source,
91
+ content,
92
+ text,
93
+ priority: priorities[section],
94
+ score: hit.score,
95
+ matchedTerms: hit.matchedTerms,
96
+ matchedFields,
97
+ originalChars: text.length,
98
+ selectedChars: 0,
99
+ selected: false,
100
+ truncated: false,
101
+ };
102
+ }
103
+ function applicable(candidate, intent) {
104
+ const source = candidate.source;
105
+ if (source.privacy.classification === 'unknown')
106
+ return { ok: false, reason: 'v2_unavailable' };
107
+ if (source.status && ['candidate', 'deprecated', 'rejected', 'obsolete'].includes(source.status))
108
+ return { ok: false, reason: 'status_not_verified' };
109
+ if (source.applicability.branchMode === 'unsupported')
110
+ return { ok: false, reason: 'unsupported_applicability' };
111
+ if (source.applicability.branchMode === 'exact') {
112
+ const branch = intent.anchor.ref ?? intent.anchor.branch ?? 'detached';
113
+ if (source.applicability.branches.length > 0 && !source.applicability.branches.includes(branch) && !source.applicability.branches.includes(intent.anchor.branch ?? ''))
114
+ return { ok: false, reason: 'applicability_mismatch' };
115
+ if (source.applicability.worktreeId && source.applicability.worktreeId !== intent.anchor.worktreeId)
116
+ return { ok: false, reason: 'applicability_mismatch' };
117
+ if (source.applicability.ref && source.applicability.ref !== intent.anchor.ref)
118
+ return { ok: false, reason: 'applicability_mismatch' };
119
+ }
120
+ if (intent.scope && source.scope && clean(intent.scope) !== clean(source.scope))
121
+ return { ok: false, reason: 'scope_mismatch' };
122
+ if (intent.paths.length > 0 && source.paths.length > 0 && !source.paths.some((path) => intent.paths.includes(path)))
123
+ return { ok: false, reason: 'path_mismatch' };
124
+ if (intent.query && ['knowledge', 'deadend'].includes(candidate.section) && candidate.matchedTerms.length === 0)
125
+ return { ok: false, reason: 'query_no_match' };
126
+ return { ok: true };
127
+ }
128
+ function sortCandidates(left, right) {
129
+ return right.priority - left.priority
130
+ || right.score - left.score
131
+ || right.matchedTerms.length - left.matchedTerms.length
132
+ || stableCompare(left.candidateId, right.candidateId);
133
+ }
134
+ function truncateText(value, maxChars) {
135
+ if (value.length <= maxChars)
136
+ return { text: value, truncated: false };
137
+ if (maxChars <= 0)
138
+ return { text: '', truncated: true };
139
+ return { text: `${value.slice(0, maxChars - 1)}…`, truncated: true };
140
+ }
141
+ function textForSummary(label, value) {
142
+ return value ? `${label}: ${value}` : '';
143
+ }
144
+ function activityText(record) {
145
+ const changes = record.changes.map((change) => `${change.path} (${change.kind})`).join(', ');
146
+ return `activity: ${changes}`;
147
+ }
148
+ function createCandidates(sources, intent, notes, knowledge, deadends, activities) {
149
+ if (sources.orientation.status !== 'ready')
150
+ return [];
151
+ const { context, snapshot, resumeSummary } = sources.orientation;
152
+ const terms = intent.normalizedTerms;
153
+ const candidates = [];
154
+ const contextSource = sourceDescriptor(sources.anchor.projectRoot, sources.anchor, sources.events, 'current-context', context.id, context.id, snapshot.snapshotId, context.status, null, resumeSummary.relatedFiles);
155
+ const summaryFields = [
156
+ ['title', context.title, 'summary'],
157
+ ['progressSummary', resumeSummary.progressSummary, 'summary'],
158
+ ['lastError', resumeSummary.lastError, 'risk'],
159
+ ];
160
+ for (const [field, value, section] of summaryFields) {
161
+ const text = textForSummary(field, value);
162
+ if (text)
163
+ candidates.push(makeCandidate(section, contextSource, { field, value }, text, terms, [field], field));
164
+ }
165
+ const listFields = [
166
+ ['pendingTasks', resumeSummary.pendingTasks, 'pending'],
167
+ ['completed', resumeSummary.completed, 'summary'],
168
+ ['decisions', resumeSummary.decisions, 'decision'],
169
+ ['risks', resumeSummary.risks, 'risk'],
170
+ ['relatedFiles', resumeSummary.relatedFiles, 'summary'],
171
+ ];
172
+ for (const [field, values, section] of listFields) {
173
+ for (const [index, value] of values.entries()) {
174
+ candidates.push(makeCandidate(section, contextSource, { field, value }, `${field}: ${value}`, terms, [field], `${field}:${index}`));
175
+ }
176
+ }
177
+ for (const note of notes) {
178
+ const source = sourceDescriptor(sources.anchor.projectRoot, sources.anchor, sources.events, 'note', note.noteId, note.contextId, note.snapshotId, note.status, null, note.relatedFiles);
179
+ candidates.push(makeCandidate('note', source, note, `${note.kind}: ${note.message}`, terms, ['kind', 'message'], note.noteId));
180
+ }
181
+ for (const item of knowledge) {
182
+ const source = sourceApplicability(item, sourceDescriptor(sources.anchor.projectRoot, sources.anchor, sources.events, 'knowledge', item.knowledgeId, item.sourceContextIds[0] ?? null, item.sourceSnapshotIds[0] ?? null, item.status, item.scope, sourcePaths(item)));
183
+ candidates.push(makeCandidate('knowledge', source, item, item.statement, terms, ['statement', 'kind', 'scope'], item.knowledgeId));
184
+ }
185
+ for (const item of deadends) {
186
+ const source = sourceApplicability(item, sourceDescriptor(sources.anchor.projectRoot, sources.anchor, sources.events, 'deadend', item.deadendId, item.sourceContextId, item.sourceSnapshotId, item.status, item.scope, sourcePaths(item)));
187
+ candidates.push(makeCandidate('deadend', source, item, `${item.attempt}: ${item.reason}`, terms, ['attempt', 'reason', 'scope'], item.deadendId));
188
+ }
189
+ for (const item of activities) {
190
+ const source = sourceDescriptor(sources.anchor.projectRoot, sources.anchor, sources.events, 'journal', item.eventId, null, null, null, null, item.paths);
191
+ candidates.push(makeCandidate('activity', source, item, activityText(item), terms, ['paths'], item.eventId));
192
+ }
193
+ return candidates;
194
+ }
195
+ function emptyTruncation() {
196
+ return { truncated: false, summary: false, pending: false, decisions: false, risks: false, notes: false, knowledge: false, deadends: false, activity: false, candidatesOmittedByBudget: 0, candidatesPartiallyTruncated: 0 };
197
+ }
198
+ function manifestText(selected) {
199
+ return selected.map((candidate) => candidate.text).join('\n');
200
+ }
201
+ function intentFor(sources, options, normalizedPaths) {
202
+ const orientation = sources.orientation;
203
+ const host = latestHostIntent(sources.events, sources.anchor);
204
+ const query = options.query?.trim() || host?.value.trim() || (orientation.status === 'ready' ? orientation.context.title : null);
205
+ const inferred = inferScenario(query, options.scenario);
206
+ const signals = [
207
+ ...(options.query ? [signal('explicit-query', options.query.trim(), 'explicit', 'high')] : []),
208
+ ...(host && !options.query ? [signal('host-prompt', host.value, 'deterministic', 'medium')] : []),
209
+ ...(normalizedPaths.map((value) => signal('path', value, 'explicit', 'high'))),
210
+ ...(inferred.signals),
211
+ ];
212
+ return {
213
+ schemaVersion: 1,
214
+ trigger: options.trigger ?? 'manual',
215
+ query,
216
+ normalizedTerms: query ? queryTerms(query).map(clean).sort(stableCompare) : [],
217
+ scenario: inferred.scenario,
218
+ scope: options.scope ?? null,
219
+ paths: normalizedPaths,
220
+ contextId: options.contextId ?? (orientation.status === 'ready' ? orientation.context.id : null),
221
+ anchor: anchorFromSources(sources),
222
+ signals,
223
+ requestedSections: ['summary', 'pending', 'decision', 'risk', 'note', 'knowledge', 'deadend', 'activity'],
224
+ budget: { unit: 'chars', total: Math.max(0, options.maxChars ?? 4000), reservedForRenderer: 0 },
225
+ adapter: options.adapter ?? 'generic',
226
+ sessionId: options.sessionId ?? null,
227
+ eventId: options.eventId ?? null,
228
+ };
229
+ }
230
+ export async function compileContext(startPath, options = {}) {
231
+ const sources = await loadCompilerSources(startPath, options);
232
+ const normalizedPaths = options.paths ? await filterPaths(sources.anchor.projectRoot, options.paths) : [];
233
+ const intent = intentFor(sources, options, normalizedPaths);
234
+ const orientation = sources.orientation;
235
+ 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)) : [];
237
+ const knowledge = orientation.status === 'ready' && intent.query ? await listKnowledge(sources.anchor.projectRoot, true) : [];
238
+ const deadends = orientation.status === 'ready' && intent.query ? await listDeadends(sources.anchor.projectRoot, true) : [];
239
+ const activities = await extractActivity(sources.anchor.projectRoot, sources.events, sources.anchor, sources.workspace, options.activityLimit ?? 20);
240
+ const all = createCandidates(sources, intent, notes, knowledge, deadends, activities);
241
+ const excluded = [];
242
+ const eligible = all.filter((candidate) => {
243
+ const check = applicable(candidate, intent);
244
+ if (!check.ok)
245
+ excluded.push({ candidateId: candidate.candidateId, source: candidate.source, reason: check.reason ?? 'query_no_match' });
246
+ return check.ok;
247
+ }).sort(sortCandidates);
248
+ const selected = [];
249
+ const truncation = emptyTruncation();
250
+ const sectionStats = {};
251
+ let usedChars = 0;
252
+ for (const candidate of eligible) {
253
+ const separator = selected.length > 0 ? 1 : 0;
254
+ const available = maxChars - usedChars - separator;
255
+ if (available <= 0) {
256
+ excluded.push({ candidateId: candidate.candidateId, source: candidate.source, reason: 'budget' });
257
+ truncation.candidatesOmittedByBudget += 1;
258
+ truncation.truncated = true;
259
+ continue;
260
+ }
261
+ const limited = truncateText(candidate.text, available);
262
+ if (!limited.text) {
263
+ excluded.push({ candidateId: candidate.candidateId, source: candidate.source, reason: 'budget' });
264
+ truncation.candidatesOmittedByBudget += 1;
265
+ truncation.truncated = true;
266
+ continue;
267
+ }
268
+ const item = { ...candidate, text: limited.text, selectedChars: limited.text.length, selected: true, truncated: limited.truncated };
269
+ selected.push(item);
270
+ usedChars += separator + item.selectedChars;
271
+ const prior = sectionStats[item.section] ?? { allocated: maxChars, consumed: 0, truncated: false };
272
+ sectionStats[item.section] = { ...prior, consumed: prior.consumed + item.selectedChars + separator, truncated: prior.truncated || limited.truncated };
273
+ if (limited.truncated) {
274
+ truncation.candidatesPartiallyTruncated += 1;
275
+ truncation.truncated = true;
276
+ if (item.section === 'summary')
277
+ truncation.summary = true;
278
+ if (item.section === 'pending')
279
+ truncation.pending = true;
280
+ if (item.section === 'decision')
281
+ truncation.decisions = true;
282
+ if (item.section === 'risk')
283
+ truncation.risks = true;
284
+ if (item.section === 'note')
285
+ truncation.notes = true;
286
+ if (item.section === 'knowledge')
287
+ truncation.knowledge = true;
288
+ if (item.section === 'deadend')
289
+ truncation.deadends = true;
290
+ if (item.section === 'activity')
291
+ truncation.activity = true;
292
+ }
293
+ }
294
+ const sections = {
295
+ summary: selected.filter((item) => item.section === 'summary'),
296
+ pending: selected.filter((item) => item.section === 'pending'),
297
+ decision: selected.filter((item) => item.section === 'decision'),
298
+ risk: selected.filter((item) => item.section === 'risk'),
299
+ notes: selected.filter((item) => item.section === 'note'),
300
+ knowledge: selected.filter((item) => item.section === 'knowledge'),
301
+ deadends: selected.filter((item) => item.section === 'deadend'),
302
+ activity: selected.filter((item) => item.section === 'activity'),
303
+ };
304
+ return {
305
+ schemaVersion: 1,
306
+ compilerVersion: '0.7.2',
307
+ rankingVersion: 'compiler-ranking-v2',
308
+ status: orientation.status,
309
+ projectRoot: sources.anchor.projectRoot,
310
+ mode: orientation.explanation.mode,
311
+ anchor: sources.anchor,
312
+ intent,
313
+ context: orientation.context,
314
+ snapshot: orientation.snapshot,
315
+ resumeSummary: orientation.resumeSummary,
316
+ sections,
317
+ selected,
318
+ excluded,
319
+ budget: { requested: intent.budget, usedChars, remainingChars: Math.max(0, maxChars - usedChars), sections: sectionStats },
320
+ truncation,
321
+ provenance: {
322
+ journalRead: sources.events.length > 0,
323
+ journalEventCount: sources.events.length,
324
+ selectedSourceIds: [...new Set(selected.map((item) => item.source.sourceId))].sort(stableCompare),
325
+ selectedEventIds: [...new Set(selected.flatMap((item) => item.source.provenance.eventId ? [item.source.provenance.eventId] : []))].sort(stableCompare),
326
+ fallbackReason: sources.fallbackReason,
327
+ },
328
+ text: manifestText(selected),
329
+ };
330
+ }
@@ -0,0 +1,19 @@
1
+ import type { BranchWorkspace, CognitionEvent } from '../cognition/types.js';
2
+ import type { ContextOrientResult } from '../agent/types.js';
3
+ import type { ContextAnchor, ContextProvenance, ContextSourceDescriptor, ContextSourceApplicability } from './types.js';
4
+ import type { ContextCompileOptions } from './types.js';
5
+ export interface CompilerSources {
6
+ orientation: ContextOrientResult;
7
+ anchor: ContextAnchor;
8
+ events: CognitionEvent[];
9
+ workspace: BranchWorkspace | null;
10
+ fallbackReason: string | null;
11
+ }
12
+ export declare function sourceApplicability(event: CognitionEvent | null, anchor: ContextAnchor): ContextSourceApplicability;
13
+ export declare function provenanceFor(events: CognitionEvent[], entityId: string, anchor: ContextAnchor): ContextProvenance;
14
+ export declare function sourceDescriptor(projectRoot: string, anchor: ContextAnchor, events: CognitionEvent[], kind: ContextSourceDescriptor['kind'], entityId: string, contextId: string | null, snapshotId: string | null, status: string | null, scope: string | null, paths: string[]): ContextSourceDescriptor;
15
+ export declare function loadCompilerSources(startPath: string | undefined, options: ContextCompileOptions): Promise<CompilerSources>;
16
+ export declare function latestHostIntent(events: CognitionEvent[], anchor: ContextAnchor): {
17
+ value: string;
18
+ eventId: string;
19
+ } | null;
@@ -0,0 +1,130 @@
1
+ import { readGitWorktreeState } from '../git/git-adapter.js';
2
+ import { contextOrient } from '../agent/api.js';
3
+ import { readJournal } from '../cognition/journal.js';
4
+ import { projectEvents, workspaceKey } from '../cognition/projections.js';
5
+ function anchorFromGit(git, projectRoot) {
6
+ return {
7
+ projectRoot,
8
+ worktreeId: git.worktreeId,
9
+ branch: git.branch,
10
+ ref: git.ref,
11
+ head: git.commit,
12
+ };
13
+ }
14
+ function payloadRecord(event) {
15
+ const payload = event.payload;
16
+ return payload.record && typeof payload.record === 'object' && !Array.isArray(payload.record)
17
+ ? payload.record
18
+ : null;
19
+ }
20
+ function matchesRecord(event, entityId) {
21
+ if (event.eventId === entityId)
22
+ return true;
23
+ const record = payloadRecord(event);
24
+ return record?.contextId === entityId
25
+ || record?.knowledgeId === entityId
26
+ || record?.deadendId === entityId
27
+ || record?.noteId === entityId
28
+ || record?.snapshotId === entityId;
29
+ }
30
+ export function sourceApplicability(event, anchor) {
31
+ if (!event || (event.git.branch === null && event.git.ref === null)) {
32
+ return { project: true, branchMode: 'global', branches: [], paths: [], worktreeId: null, ref: null };
33
+ }
34
+ return {
35
+ project: true,
36
+ branchMode: 'exact',
37
+ branches: [event.git.ref ?? event.git.branch ?? 'detached'],
38
+ paths: [],
39
+ worktreeId: event.git.worktreeId,
40
+ ref: event.git.ref,
41
+ };
42
+ }
43
+ export function provenanceFor(events, entityId, anchor) {
44
+ const event = [...events].reverse().find((item) => matchesRecord(item, entityId));
45
+ if (!event) {
46
+ return {
47
+ origin: 'v1-file',
48
+ eventId: null,
49
+ eventType: null,
50
+ occurredAt: null,
51
+ sourcePath: null,
52
+ sourceHash: null,
53
+ anchorProvenance: null,
54
+ };
55
+ }
56
+ const payload = event.payload;
57
+ return {
58
+ origin: 'v2-journal',
59
+ eventId: event.eventId,
60
+ eventType: event.type,
61
+ occurredAt: event.occurredAt,
62
+ sourcePath: typeof payload.sourcePath === 'string' ? payload.sourcePath : null,
63
+ sourceHash: typeof payload.sourceHash === 'string' ? payload.sourceHash : null,
64
+ anchorProvenance: payload.anchorProvenance === 'snapshot' || payload.anchorProvenance === 'live' || payload.anchorProvenance === 'global'
65
+ ? payload.anchorProvenance
66
+ : null,
67
+ };
68
+ }
69
+ export function sourceDescriptor(projectRoot, anchor, events, kind, entityId, contextId, snapshotId, status, scope, paths) {
70
+ const provenance = provenanceFor(events, entityId, anchor);
71
+ const event = provenance.eventId ? events.find((item) => item.eventId === provenance.eventId) ?? null : null;
72
+ return {
73
+ sourceId: `${kind}:${entityId}`,
74
+ kind,
75
+ entityId,
76
+ contextId,
77
+ snapshotId,
78
+ status,
79
+ scope,
80
+ paths: [...new Set(paths)].sort(),
81
+ anchor,
82
+ applicability: sourceApplicability(event, anchor),
83
+ privacy: { classification: provenance.origin === 'v2-journal' ? (event?.privacy.classification ?? 'metadata') : 'metadata', redactionCount: event?.privacy.redactionCount ?? 0 },
84
+ provenance,
85
+ };
86
+ }
87
+ export async function loadCompilerSources(startPath, options) {
88
+ const orientation = await contextOrient(startPath, {
89
+ contextId: options.contextId,
90
+ query: options.query,
91
+ scope: options.scope,
92
+ maxChars: Number.MAX_SAFE_INTEGER,
93
+ knowledgeLimit: options.knowledgeLimit ?? 10,
94
+ deadendLimit: options.deadendLimit ?? 10,
95
+ noteLimit: options.noteLimit ?? 20,
96
+ });
97
+ const projectRoot = orientation.projectRoot;
98
+ const git = await readGitWorktreeState(projectRoot);
99
+ let events = [];
100
+ let fallbackReason = null;
101
+ try {
102
+ events = await readJournal(projectRoot);
103
+ }
104
+ catch (error) {
105
+ fallbackReason = error instanceof Error ? error.message : 'journal-unavailable';
106
+ }
107
+ const anchor = anchorFromGit(git, projectRoot);
108
+ const projection = events.length > 0 ? projectEvents(events) : null;
109
+ const workspace = projection?.workspaces[workspaceKey(anchor)] ?? null;
110
+ return { orientation, anchor, events, workspace, fallbackReason };
111
+ }
112
+ export function latestHostIntent(events, anchor) {
113
+ return [...events].reverse().find((event) => {
114
+ if (event.type !== 'host.prompt.submitted')
115
+ return false;
116
+ if (event.git.worktreeId !== null && event.git.worktreeId !== anchor.worktreeId)
117
+ return false;
118
+ if (event.git.ref !== null && event.git.ref !== anchor.ref)
119
+ return false;
120
+ const payload = event.payload;
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;
130
+ }