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.
- package/README.md +121 -9
- package/dist/src/agent/api.d.ts +5 -1
- package/dist/src/agent/api.js +41 -12
- package/dist/src/agent/index.d.ts +3 -1
- package/dist/src/agent/index.js +1 -0
- package/dist/src/agent/types.d.ts +21 -0
- package/dist/src/cli/main.js +150 -12
- package/dist/src/cognition/feedback.d.ts +9 -0
- package/dist/src/cognition/feedback.js +30 -0
- package/dist/src/cognition/filesystem-observer.d.ts +41 -0
- package/dist/src/cognition/filesystem-observer.js +164 -0
- package/dist/src/cognition/git-observer.d.ts +1 -0
- package/dist/src/cognition/git-observer.js +3 -2
- package/dist/src/cognition/journal.d.ts +4 -1
- package/dist/src/cognition/journal.js +83 -4
- package/dist/src/cognition/migration-v1.js +13 -11
- package/dist/src/cognition/orient.d.ts +4 -0
- package/dist/src/cognition/orient.js +14 -4
- package/dist/src/cognition/policy.d.ts +4 -0
- package/dist/src/cognition/policy.js +49 -0
- package/dist/src/cognition/projections.js +0 -0
- package/dist/src/cognition/retrieval.d.ts +26 -0
- package/dist/src/cognition/retrieval.js +273 -0
- package/dist/src/cognition/runtime.d.ts +20 -1
- package/dist/src/cognition/runtime.js +229 -53
- package/dist/src/cognition/types.d.ts +55 -1
- package/dist/src/cognition/v1-bridge.js +13 -4
- package/dist/src/compiler/activity.d.ts +3 -0
- package/dist/src/compiler/activity.js +79 -0
- package/dist/src/compiler/compile.d.ts +2 -0
- package/dist/src/compiler/compile.js +330 -0
- package/dist/src/compiler/sources.d.ts +19 -0
- package/dist/src/compiler/sources.js +130 -0
- package/dist/src/compiler/types.d.ts +187 -0
- package/dist/src/compiler/types.js +1 -0
- package/dist/src/hooks/claude-code.js +7 -15
- package/dist/src/mcp/main.js +2 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/runtime/knowledge.d.ts +2 -0
- package/dist/src/runtime/knowledge.js +63 -0
- package/dist/src/runtime/types.d.ts +19 -0
- package/dist/src/storage/layout.d.ts +2 -0
- package/dist/src/storage/layout.js +6 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +2 -2
- 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,
|
|
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
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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(
|
|
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
|
|
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
|
|
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 (
|
|
191
|
-
return;
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
197
|
-
|
|
252
|
+
if (inactivity)
|
|
253
|
+
clearTimeout(inactivity);
|
|
254
|
+
inactivity = setTimeout(() => { void close('inactivity').catch(() => undefined); }, inactivityTimeoutMs);
|
|
198
255
|
};
|
|
199
|
-
|
|
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
|
-
|
|
217
|
-
},
|
|
297
|
+
leaseWrite = leaseWrite.then(() => writeLease(projectRoot, lease)).catch(() => undefined);
|
|
298
|
+
}, heartbeatIntervalMs);
|
|
218
299
|
poller = setInterval(() => {
|
|
219
|
-
void
|
|
220
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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 <
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|