fluffy-context 0.5.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 (47) hide show
  1. package/README.md +121 -9
  2. package/dist/src/agent/api.d.ts +5 -1
  3. package/dist/src/agent/api.js +41 -12
  4. package/dist/src/agent/index.d.ts +3 -1
  5. package/dist/src/agent/index.js +1 -0
  6. package/dist/src/agent/types.d.ts +21 -0
  7. package/dist/src/cli/main.js +150 -12
  8. package/dist/src/cognition/feedback.d.ts +9 -0
  9. package/dist/src/cognition/feedback.js +30 -0
  10. package/dist/src/cognition/filesystem-observer.d.ts +41 -0
  11. package/dist/src/cognition/filesystem-observer.js +164 -0
  12. package/dist/src/cognition/git-observer.d.ts +1 -0
  13. package/dist/src/cognition/git-observer.js +3 -2
  14. package/dist/src/cognition/journal.d.ts +4 -1
  15. package/dist/src/cognition/journal.js +83 -4
  16. package/dist/src/cognition/migration-v1.js +13 -11
  17. package/dist/src/cognition/orient.d.ts +4 -0
  18. package/dist/src/cognition/orient.js +14 -4
  19. package/dist/src/cognition/policy.d.ts +4 -0
  20. package/dist/src/cognition/policy.js +49 -0
  21. package/dist/src/cognition/projections.js +0 -0
  22. package/dist/src/cognition/retrieval.d.ts +26 -0
  23. package/dist/src/cognition/retrieval.js +273 -0
  24. package/dist/src/cognition/runtime.d.ts +20 -1
  25. package/dist/src/cognition/runtime.js +229 -53
  26. package/dist/src/cognition/types.d.ts +55 -1
  27. package/dist/src/cognition/v1-bridge.js +13 -4
  28. package/dist/src/compiler/activity.d.ts +3 -0
  29. package/dist/src/compiler/activity.js +79 -0
  30. package/dist/src/compiler/compile.d.ts +2 -0
  31. package/dist/src/compiler/compile.js +330 -0
  32. package/dist/src/compiler/sources.d.ts +19 -0
  33. package/dist/src/compiler/sources.js +130 -0
  34. package/dist/src/compiler/types.d.ts +187 -0
  35. package/dist/src/compiler/types.js +1 -0
  36. package/dist/src/hooks/claude-code.js +7 -15
  37. package/dist/src/mcp/main.js +2 -0
  38. package/dist/src/mcp/server.js +2 -0
  39. package/dist/src/runtime/knowledge.d.ts +2 -0
  40. package/dist/src/runtime/knowledge.js +63 -0
  41. package/dist/src/runtime/types.d.ts +19 -0
  42. package/dist/src/storage/layout.d.ts +2 -0
  43. package/dist/src/storage/layout.js +6 -0
  44. package/dist/src/version.d.ts +1 -1
  45. package/dist/src/version.js +1 -1
  46. package/package.json +2 -2
  47. package/skills/fluffy-context/SKILL.md +2 -1
@@ -9,8 +9,11 @@ import { loadManifest } from '../runtime/init.js';
9
9
  import { atomicWriteJson } from '../storage/atomic-write.js';
10
10
  import { readJson } from '../storage/json-store.js';
11
11
  import { withLock } from '../storage/lock.js';
12
- import { cognitionManifestPath, cognitionPolicyPath, cognitionRuntimeLeasePath, cognitionRuntimeSecretPath, locksDirectory } from '../storage/layout.js';
12
+ import { cognitionManifestPath, cognitionRuntimeLeasePath, cognitionRuntimeSecretPath, cognitionRuntimeStartupPath, locksDirectory } from '../storage/layout.js';
13
13
  import { appendEvent } from './journal.js';
14
+ import { rebuildProjections } from './projections.js';
15
+ import { loadRuntimePolicy, writeRuntimePolicy } from './policy.js';
16
+ import { FilesystemObserver } from './filesystem-observer.js';
14
17
  import { GitObserver } from './git-observer.js';
15
18
  import { reconcileV1State } from './migration-v1.js';
16
19
  import { v1Synced } from './v1-bridge.js';
@@ -19,6 +22,8 @@ const protocolVersion = 1;
19
22
  const maxFrameBytes = 16 * 1024;
20
23
  const pollIntervalMs = 2_000;
21
24
  const cliPath = fileURLToPath(new URL('../cli/main.js', import.meta.url));
25
+ const startupPromises = new Map();
26
+ const startupMarkerTimeoutMs = 10_000;
22
27
  function createInstanceId() {
23
28
  return crypto.randomBytes(12).toString('hex');
24
29
  }
@@ -44,11 +49,21 @@ async function exists(filePath) {
44
49
  }
45
50
  }
46
51
  async function loadPolicy(projectRoot) {
47
- return readJson(cognitionPolicyPath(projectRoot));
52
+ return loadRuntimePolicy(projectRoot);
48
53
  }
49
54
  async function loadLease(projectRoot) {
50
55
  return readJson(cognitionRuntimeLeasePath(projectRoot)).catch(() => null);
51
56
  }
57
+ async function loadStartupMarker(projectRoot) {
58
+ return readJson(cognitionRuntimeStartupPath(projectRoot)).catch(() => null);
59
+ }
60
+ async function removeOwnedRuntimeFiles(projectRoot, instanceId) {
61
+ const current = await loadLease(projectRoot);
62
+ if (current?.instanceId !== instanceId)
63
+ return;
64
+ await rm(cognitionRuntimeLeasePath(projectRoot), { force: true });
65
+ await rm(cognitionRuntimeSecretPath(projectRoot), { force: true });
66
+ }
52
67
  async function request(projectRoot, lease, method) {
53
68
  let secret;
54
69
  try {
@@ -92,23 +107,52 @@ async function liveLease(projectRoot, lease) {
92
107
  async function writeLease(projectRoot, lease) {
93
108
  await atomicWriteJson(cognitionRuntimeLeasePath(projectRoot), lease);
94
109
  }
95
- async function closeServer(projectRoot, server, observer, heartbeat, poller, inactivity, lease, reason) {
96
- clearInterval(heartbeat);
97
- clearInterval(poller);
98
- clearTimeout(inactivity);
99
- await observer.drain();
100
- await new Promise((resolve) => server.close(() => resolve()));
101
- await new Promise((resolve) => setImmediate(resolve));
102
- await rm(cognitionRuntimeLeasePath(projectRoot), { force: true });
103
- await rm(cognitionRuntimeSecretPath(projectRoot), { force: true });
104
- await appendEvent(projectRoot, {
105
- type: 'runtime.stopped',
106
- source: { kind: 'cli', instanceId: lease.instanceId },
107
- idempotencyKey: `runtime:stopped:${lease.instanceId}`,
108
- payload: { reason },
109
- });
110
+ async function closeServer(projectRoot, server, observer, filesystem, heartbeat, poller, inactivity, background, leaseWrite, lease, reason) {
111
+ if (heartbeat)
112
+ clearInterval(heartbeat);
113
+ if (poller)
114
+ clearInterval(poller);
115
+ if (inactivity)
116
+ clearTimeout(inactivity);
117
+ let failure;
118
+ try {
119
+ filesystem.stop();
120
+ await filesystem.flush();
121
+ await observer.drain();
122
+ await filesystem.drain();
123
+ await background;
124
+ await rebuildProjections(projectRoot);
125
+ await leaseWrite;
126
+ }
127
+ catch (error) {
128
+ failure = error;
129
+ }
130
+ finally {
131
+ await new Promise((resolve) => {
132
+ if (!server.listening) {
133
+ resolve();
134
+ return;
135
+ }
136
+ server.close(() => resolve());
137
+ });
138
+ await new Promise((resolve) => setImmediate(resolve));
139
+ await removeOwnedRuntimeFiles(projectRoot, lease.instanceId);
140
+ }
141
+ try {
142
+ await appendEvent(projectRoot, {
143
+ type: 'runtime.stopped',
144
+ source: { kind: 'cli', instanceId: lease.instanceId },
145
+ idempotencyKey: `runtime:stopped:${lease.instanceId}`,
146
+ payload: { reason },
147
+ });
148
+ }
149
+ catch (error) {
150
+ failure ??= error;
151
+ }
152
+ if (failure)
153
+ throw failure;
110
154
  }
111
- function attachProtocol(projectRoot, server, observer, secret, lease, touch, close) {
155
+ function attachProtocol(server, secret, lease, touch, observe, close) {
112
156
  server.on('connection', (socket) => {
113
157
  socket.setEncoding('utf8');
114
158
  let buffer = '';
@@ -142,7 +186,7 @@ function attachProtocol(projectRoot, server, observer, secret, lease, touch, clo
142
186
  }
143
187
  if (request.method === 'observe') {
144
188
  try {
145
- const result = await observer.observeNow();
189
+ const result = await observe();
146
190
  socket.end(`${JSON.stringify({ ok: true, result: { changed: result.changed } })}\n`);
147
191
  }
148
192
  catch {
@@ -159,11 +203,14 @@ function attachProtocol(projectRoot, server, observer, secret, lease, touch, clo
159
203
  });
160
204
  });
161
205
  }
162
- export async function serveRuntime(startPath) {
206
+ export async function serveRuntime(startPath, options = {}) {
163
207
  const projectRoot = await resolveProjectRoot(startPath);
164
208
  const currentPolicy = await loadPolicy(projectRoot);
165
209
  if (!currentPolicy.enabled)
166
210
  throw new Error('runtime is disabled; run ctx runtime enable first');
211
+ const inactivityTimeoutMs = options.inactivityTimeoutMs ?? currentPolicy.inactivityTimeoutMs;
212
+ const heartbeatIntervalMs = options.heartbeatIntervalMs ?? 5_000;
213
+ const runtimePollIntervalMs = options.pollIntervalMs ?? pollIntervalMs;
167
214
  const existing = await liveLease(projectRoot);
168
215
  if (existing)
169
216
  throw new Error('runtime is already running');
@@ -182,21 +229,55 @@ export async function serveRuntime(startPath) {
182
229
  const lease = { schemaVersion: 2, protocolVersion, projectId: manifest.projectId, instanceId: createInstanceId(), pid: process.pid, host, port: address.port, startedAt: now, heartbeatAt: now };
183
230
  const secret = createToken();
184
231
  const observer = new GitObserver(projectRoot);
185
- let closed = false;
186
- let heartbeat;
187
- let poller;
188
- let inactivity;
232
+ let filesystem;
233
+ let heartbeat = null;
234
+ let poller = null;
235
+ let inactivity = null;
236
+ let background = Promise.resolve();
237
+ let leaseWrite = Promise.resolve();
238
+ let filesystemEventPending = false;
239
+ let closing = null;
240
+ const enqueueObservation = (operation) => {
241
+ const next = background.then(operation, operation);
242
+ background = next.then(() => undefined, () => undefined);
243
+ return next;
244
+ };
189
245
  const close = async (reason) => {
190
- if (closed)
191
- return;
192
- closed = true;
193
- await closeServer(projectRoot, server, observer, heartbeat, poller, inactivity, lease, reason);
246
+ if (closing)
247
+ return closing;
248
+ closing = closeServer(projectRoot, server, observer, filesystem, heartbeat, poller, inactivity, background, leaseWrite, lease, reason);
249
+ return closing;
194
250
  };
195
251
  const touch = () => {
196
- clearTimeout(inactivity);
197
- inactivity = setTimeout(() => { void close('inactivity'); }, currentPolicy.inactivityTimeoutMs);
252
+ if (inactivity)
253
+ clearTimeout(inactivity);
254
+ inactivity = setTimeout(() => { void close('inactivity').catch(() => undefined); }, inactivityTimeoutMs);
198
255
  };
199
- attachProtocol(projectRoot, server, observer, secret, lease, touch, close);
256
+ const observe = () => enqueueObservation(async () => {
257
+ const [gitResult, filesystemResult] = await Promise.all([
258
+ observer.observeNow(),
259
+ filesystem.observeNow(),
260
+ ]);
261
+ const synced = await v1Synced(projectRoot);
262
+ if (!synced)
263
+ await reconcileV1State(projectRoot);
264
+ if (gitResult.changed || filesystemResult.changed || filesystemEventPending || !synced) {
265
+ await rebuildProjections(projectRoot);
266
+ filesystemEventPending = false;
267
+ }
268
+ const changed = gitResult.changed || filesystemResult.changed || filesystemEventPending || !synced;
269
+ if (changed)
270
+ touch();
271
+ return { changed };
272
+ });
273
+ filesystem = new FilesystemObserver(projectRoot, {
274
+ anchor: () => ({ worktreeId: observer.current?.worktreeId ?? null, branch: observer.current?.branch ?? null, ref: observer.current?.ref ?? null, head: observer.current?.commit ?? null }),
275
+ onChanged: () => {
276
+ filesystemEventPending = true;
277
+ touch();
278
+ },
279
+ });
280
+ attachProtocol(server, secret, lease, touch, observe, close);
200
281
  try {
201
282
  await withLock(`${locksDirectory(projectRoot)}/cognition-runtime-start.lock`, async () => {
202
283
  const live = await liveLease(projectRoot);
@@ -213,16 +294,18 @@ export async function serveRuntime(startPath) {
213
294
  }
214
295
  heartbeat = setInterval(() => {
215
296
  lease.heartbeatAt = new Date().toISOString();
216
- void writeLease(projectRoot, lease);
217
- }, 5_000);
297
+ leaseWrite = leaseWrite.then(() => writeLease(projectRoot, lease)).catch(() => undefined);
298
+ }, heartbeatIntervalMs);
218
299
  poller = setInterval(() => {
219
- void observer.observeNow();
220
- void v1Synced(projectRoot).then((synced) => { if (!synced)
221
- return reconcileV1State(projectRoot); });
222
- }, pollIntervalMs);
223
- touch();
300
+ void observe().catch(() => undefined);
301
+ }, runtimePollIntervalMs);
224
302
  await appendEvent(projectRoot, { type: 'runtime.started', source: { kind: 'cli', instanceId: lease.instanceId }, idempotencyKey: `runtime:started:${lease.instanceId}`, payload: {} });
225
- await observer.observeNow();
303
+ if (currentPolicy.filesystemEventsEnabled)
304
+ filesystem.start();
305
+ await observe();
306
+ await writeLease(projectRoot, lease);
307
+ await rm(cognitionRuntimeStartupPath(projectRoot), { force: true });
308
+ touch();
226
309
  return { lease, close };
227
310
  }
228
311
  export async function runtimeStatus(startPath) {
@@ -240,8 +323,8 @@ export async function enableRuntime(startPath) {
240
323
  if (!await exists(cognitionManifestPath(projectRoot)))
241
324
  throw new Error('v2 migration is required before enabling the runtime');
242
325
  const current = await loadPolicy(projectRoot);
243
- if (!current.enabled) {
244
- await atomicWriteJson(cognitionPolicyPath(projectRoot), { ...current, enabled: true });
326
+ if (!current.enabled || !current.autoStart) {
327
+ await writeRuntimePolicy(projectRoot, { ...current, enabled: true, autoStart: true });
245
328
  await appendEvent(projectRoot, { type: 'runtime.enabled', source: { kind: 'cli' }, idempotencyKey: 'runtime:enabled', payload: {} });
246
329
  }
247
330
  return runtimeStatus(projectRoot);
@@ -252,25 +335,106 @@ export async function disableRuntime(startPath) {
252
335
  throw new Error('v2 migration is required before disabling the runtime');
253
336
  await stopRuntime(projectRoot);
254
337
  const current = await loadPolicy(projectRoot);
255
- await atomicWriteJson(cognitionPolicyPath(projectRoot), { ...current, enabled: false });
338
+ await writeRuntimePolicy(projectRoot, { ...current, enabled: false, autoStart: false });
256
339
  return runtimeStatus(projectRoot);
257
340
  }
341
+ export async function setFilesystemEvents(startPath, enabled) {
342
+ const projectRoot = await resolveProjectRoot(startPath);
343
+ if (!await exists(cognitionManifestPath(projectRoot)))
344
+ throw new Error('v2 migration is required before configuring filesystem events');
345
+ const wasRunning = (await runtimeStatus(projectRoot)).running;
346
+ if (wasRunning)
347
+ await stopRuntime(projectRoot);
348
+ const current = await loadPolicy(projectRoot);
349
+ await writeRuntimePolicy(projectRoot, { ...current, filesystemEventsEnabled: enabled });
350
+ if (wasRunning)
351
+ return startRuntime(projectRoot);
352
+ return runtimeStatus(projectRoot);
353
+ }
354
+ async function waitForRuntime(projectRoot, timeoutMs) {
355
+ const deadline = Date.now() + timeoutMs;
356
+ while (Date.now() < deadline) {
357
+ const status = await runtimeStatus(projectRoot);
358
+ if (status.running && !await loadStartupMarker(projectRoot))
359
+ return status;
360
+ await new Promise((resolve) => setTimeout(resolve, 50));
361
+ }
362
+ return null;
363
+ }
364
+ async function startRuntimeCoordinated(projectRoot, timeoutMs) {
365
+ const existing = await liveLease(projectRoot);
366
+ if (existing)
367
+ return runtimeStatus(projectRoot);
368
+ const attemptId = createInstanceId();
369
+ let owner = false;
370
+ try {
371
+ await withLock(`${locksDirectory(projectRoot)}/cognition-runtime-start.lock`, async () => {
372
+ const live = await liveLease(projectRoot);
373
+ if (live)
374
+ return;
375
+ const marker = await loadStartupMarker(projectRoot);
376
+ if (marker && Date.now() - Date.parse(marker.createdAt) <= startupMarkerTimeoutMs)
377
+ return;
378
+ await atomicWriteJson(cognitionRuntimeStartupPath(projectRoot), { attemptId, pid: process.pid, createdAt: new Date().toISOString() });
379
+ owner = true;
380
+ });
381
+ }
382
+ catch {
383
+ // Another process owns the startup lock; it will publish the lease or the marker will expire.
384
+ }
385
+ if (owner) {
386
+ const child = spawn(process.execPath, [cliPath, 'runtime', 'serve', '--path', projectRoot], { detached: true, stdio: 'ignore', windowsHide: true });
387
+ child.unref();
388
+ }
389
+ const status = await waitForRuntime(projectRoot, timeoutMs);
390
+ const marker = await loadStartupMarker(projectRoot);
391
+ if (marker?.attemptId === attemptId)
392
+ await rm(cognitionRuntimeStartupPath(projectRoot), { force: true });
393
+ if (status)
394
+ return status;
395
+ throw new Error('runtime failed to start');
396
+ }
258
397
  export async function startRuntime(startPath) {
259
398
  const projectRoot = await resolveProjectRoot(startPath);
260
399
  const current = await loadPolicy(projectRoot);
261
400
  if (!current.enabled)
262
401
  throw new Error('runtime is disabled; run ctx runtime enable first');
402
+ const pending = startupPromises.get(projectRoot);
403
+ if (pending)
404
+ return pending;
405
+ const operation = startRuntimeCoordinated(projectRoot, 1_500).finally(() => startupPromises.delete(projectRoot));
406
+ startupPromises.set(projectRoot, operation);
407
+ return operation;
408
+ }
409
+ export async function ensureRuntimeForSession(startPath, options = {}) {
410
+ const projectRoot = await resolveProjectRoot(startPath);
411
+ if (!await exists(cognitionManifestPath(projectRoot)))
412
+ return { projectRoot, started: false, running: false, skipped: 'not-migrated' };
413
+ const current = await loadPolicy(projectRoot);
414
+ if (!current.enabled || !current.autoStart || (options.source === 'claude-hook' && !current.allowHookStartup))
415
+ return { projectRoot, started: false, running: false, skipped: 'disabled' };
263
416
  if (await liveLease(projectRoot))
264
- return runtimeStatus(projectRoot);
265
- const child = spawn(process.execPath, [cliPath, 'runtime', 'serve', '--path', projectRoot], { detached: true, stdio: 'ignore', windowsHide: true });
266
- child.unref();
267
- for (let attempt = 0; attempt < 20; attempt += 1) {
268
- await new Promise((resolve) => setTimeout(resolve, 50));
269
- const status = await runtimeStatus(projectRoot);
270
- if (status.running)
271
- return status;
417
+ return { projectRoot, started: false, running: true, skipped: 'already-running' };
418
+ const pending = startupPromises.get(projectRoot);
419
+ if (pending) {
420
+ try {
421
+ const status = await pending;
422
+ return { projectRoot, started: false, running: status.running, skipped: 'startup-in-progress' };
423
+ }
424
+ catch {
425
+ return { projectRoot, started: false, running: false, skipped: 'unavailable' };
426
+ }
427
+ }
428
+ const operation = startRuntimeCoordinated(projectRoot, options.timeoutMs ?? 1_000)
429
+ .finally(() => startupPromises.delete(projectRoot));
430
+ startupPromises.set(projectRoot, operation);
431
+ try {
432
+ const status = await operation;
433
+ return { projectRoot, started: true, running: status.running, skipped: null };
434
+ }
435
+ catch {
436
+ return { projectRoot, started: false, running: false, skipped: 'unavailable' };
272
437
  }
273
- throw new Error('runtime failed to start');
274
438
  }
275
439
  export async function stopRuntime(startPath) {
276
440
  const projectRoot = await resolveProjectRoot(startPath);
@@ -278,10 +442,10 @@ export async function stopRuntime(startPath) {
278
442
  if (!lease)
279
443
  return runtimeStatus(projectRoot);
280
444
  await request(projectRoot, lease, 'stop');
281
- for (let attempt = 0; attempt < 20; attempt += 1) {
445
+ for (let attempt = 0; attempt < 40; attempt += 1) {
282
446
  await new Promise((resolve) => setTimeout(resolve, 50));
283
447
  const status = await runtimeStatus(projectRoot);
284
- if (!status.running)
448
+ if (!status.running && !status.lease)
285
449
  return status;
286
450
  }
287
451
  throw new Error('runtime failed to stop');
@@ -293,3 +457,15 @@ export async function observeRuntime(startPath) {
293
457
  return false;
294
458
  return Boolean((await request(projectRoot, lease, 'observe'))?.ok);
295
459
  }
460
+ export async function touchRuntime(startPath) {
461
+ try {
462
+ const projectRoot = await resolveProjectRoot(startPath);
463
+ const lease = await loadLease(projectRoot);
464
+ if (!lease)
465
+ return false;
466
+ return Boolean((await request(projectRoot, lease, 'ping'))?.ok);
467
+ }
468
+ catch {
469
+ return false;
470
+ }
471
+ }
@@ -1,6 +1,6 @@
1
1
  export declare const COGNITION_LAYOUT_VERSION: "event-journal-v2";
2
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' | 'deadend.proposed' | 'deadend.verified' | 'deadend.obsoleted' | 'projection.advanced' | 'projection.failed';
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';
4
4
  export interface CognitionGitAnchor {
5
5
  worktreeId: string | null;
6
6
  branch: string | null;
@@ -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;
@@ -53,6 +54,44 @@ export interface Applicability {
53
54
  sourceCommit?: string | null;
54
55
  dependencyConstraints?: string[];
55
56
  }
57
+ export interface CognitionLifecycleTransition {
58
+ entity: 'knowledge' | 'deadend';
59
+ entityId: string;
60
+ from: 'candidate' | 'verified';
61
+ to: 'verified' | 'deprecated' | 'rejected' | 'obsolete';
62
+ reason?: string;
63
+ }
64
+ export interface CognitionUseFeedback {
65
+ feedbackId: string;
66
+ entity: 'knowledge' | 'deadend';
67
+ entityId: string;
68
+ outcome: 'used';
69
+ }
70
+ export interface CognitionRetrievalEntry {
71
+ entity: 'knowledge' | 'deadend';
72
+ entityId: string;
73
+ status: string;
74
+ record: Record<string, unknown>;
75
+ applicability: Applicability;
76
+ searchable: Record<string, string>;
77
+ normalizedTerms: string[];
78
+ provenance: {
79
+ eventId: string;
80
+ eventType: CognitionEventType;
81
+ occurredAt: string;
82
+ git: CognitionGitAnchor;
83
+ anchorProvenance?: CognitionRecordPayload['anchorProvenance'];
84
+ };
85
+ feedback: {
86
+ usedCount: number;
87
+ latestUsedAt: string | null;
88
+ };
89
+ }
90
+ export interface CognitionRetrievalProjection {
91
+ schemaVersion: 2;
92
+ entries: Record<string, CognitionRetrievalEntry>;
93
+ terms: Record<string, string[]>;
94
+ }
56
95
  export interface BranchWorkspace {
57
96
  schemaVersion: 2;
58
97
  workspaceKey: string;
@@ -103,6 +142,21 @@ export interface CognitionRecordPayload<T extends Record<string, unknown> = Reco
103
142
  sourceHash?: string;
104
143
  anchorProvenance?: 'snapshot' | 'live' | 'global';
105
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
+ }
106
160
  export interface CognitionProjection {
107
161
  schemaVersion: 2;
108
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;
@@ -67,8 +68,16 @@ export async function mirrorNote(projectRoot, note, absorbed = false) {
67
68
  await mirror(projectRoot, absorbed ? 'note.absorbed' : 'note.recorded', note, note.noteId);
68
69
  }
69
70
  export async function mirrorKnowledge(projectRoot, knowledge) {
70
- await mirror(projectRoot, knowledge.status === 'verified' ? 'knowledge.verified' : 'knowledge.proposed', knowledge, knowledge.knowledgeId);
71
+ const type = knowledge.status === 'verified' ? 'knowledge.verified'
72
+ : knowledge.status === 'deprecated' ? 'knowledge.deprecated'
73
+ : knowledge.status === 'rejected' ? 'knowledge.rejected'
74
+ : 'knowledge.proposed';
75
+ await mirror(projectRoot, type, knowledge, knowledge.knowledgeId);
71
76
  }
72
77
  export async function mirrorDeadend(projectRoot, deadend) {
73
- await mirror(projectRoot, deadend.status === 'verified' ? 'deadend.verified' : 'deadend.proposed', deadend, deadend.deadendId);
78
+ const type = deadend.status === 'verified' ? 'deadend.verified'
79
+ : deadend.status === 'obsolete' ? 'deadend.obsoleted'
80
+ : deadend.status === 'rejected' ? 'deadend.rejected'
81
+ : 'deadend.proposed';
82
+ await mirror(projectRoot, type, deadend, deadend.deadendId);
74
83
  }
@@ -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>;