fluffy-context 0.3.0 → 0.5.0

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 (35) hide show
  1. package/README.md +39 -4
  2. package/dist/src/agent/api.js +20 -9
  3. package/dist/src/cli/main.js +96 -2
  4. package/dist/src/cognition/git-observer.d.ts +13 -0
  5. package/dist/src/cognition/git-observer.js +61 -0
  6. package/dist/src/cognition/journal.d.ts +24 -0
  7. package/dist/src/cognition/journal.js +113 -0
  8. package/dist/src/cognition/migration-v1.d.ts +7 -0
  9. package/dist/src/cognition/migration-v1.js +199 -0
  10. package/dist/src/cognition/orient.d.ts +9 -0
  11. package/dist/src/cognition/orient.js +85 -0
  12. package/dist/src/cognition/projections.d.ts +7 -0
  13. package/dist/src/cognition/projections.js +127 -0
  14. package/dist/src/cognition/readiness.d.ts +2 -0
  15. package/dist/src/cognition/readiness.js +81 -0
  16. package/dist/src/cognition/runtime.d.ts +12 -0
  17. package/dist/src/cognition/runtime.js +295 -0
  18. package/dist/src/cognition/types.d.ts +132 -0
  19. package/dist/src/cognition/types.js +1 -0
  20. package/dist/src/cognition/v1-bridge.d.ts +7 -0
  21. package/dist/src/cognition/v1-bridge.js +74 -0
  22. package/dist/src/git/git-adapter.d.ts +7 -0
  23. package/dist/src/git/git-adapter.js +16 -6
  24. package/dist/src/hooks/claude-code.js +53 -0
  25. package/dist/src/integrations/claude-code.js +36 -13
  26. package/dist/src/runtime/diagnostics.js +6 -0
  27. package/dist/src/runtime/knowledge.js +5 -0
  28. package/dist/src/runtime/notes.js +8 -1
  29. package/dist/src/runtime/runtime.d.ts +1 -0
  30. package/dist/src/runtime/runtime.js +28 -0
  31. package/dist/src/storage/layout.d.ts +18 -0
  32. package/dist/src/storage/layout.js +54 -0
  33. package/dist/src/version.d.ts +1 -1
  34. package/dist/src/version.js +1 -1
  35. package/package.json +1 -1
@@ -0,0 +1,295 @@
1
+ import crypto from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { createConnection, createServer } from 'node:net';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { resolveProjectRoot } from '../project/project-resolver.js';
8
+ import { loadManifest } from '../runtime/init.js';
9
+ import { atomicWriteJson } from '../storage/atomic-write.js';
10
+ import { readJson } from '../storage/json-store.js';
11
+ import { withLock } from '../storage/lock.js';
12
+ import { cognitionManifestPath, cognitionPolicyPath, cognitionRuntimeLeasePath, cognitionRuntimeSecretPath, locksDirectory } from '../storage/layout.js';
13
+ import { appendEvent } from './journal.js';
14
+ import { GitObserver } from './git-observer.js';
15
+ import { reconcileV1State } from './migration-v1.js';
16
+ import { v1Synced } from './v1-bridge.js';
17
+ const host = '127.0.0.1';
18
+ const protocolVersion = 1;
19
+ const maxFrameBytes = 16 * 1024;
20
+ const pollIntervalMs = 2_000;
21
+ const cliPath = fileURLToPath(new URL('../cli/main.js', import.meta.url));
22
+ function createInstanceId() {
23
+ return crypto.randomBytes(12).toString('hex');
24
+ }
25
+ function createToken() {
26
+ return crypto.randomBytes(32).toString('base64url');
27
+ }
28
+ function sameToken(left, right) {
29
+ const expected = Buffer.from(left);
30
+ const actual = Buffer.from(right);
31
+ return expected.length === actual.length && crypto.timingSafeEqual(expected, actual);
32
+ }
33
+ function publicLease(lease) {
34
+ const { projectId: _, ...visible } = lease;
35
+ return visible;
36
+ }
37
+ async function exists(filePath) {
38
+ try {
39
+ await access(filePath);
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ async function loadPolicy(projectRoot) {
47
+ return readJson(cognitionPolicyPath(projectRoot));
48
+ }
49
+ async function loadLease(projectRoot) {
50
+ return readJson(cognitionRuntimeLeasePath(projectRoot)).catch(() => null);
51
+ }
52
+ async function request(projectRoot, lease, method) {
53
+ let secret;
54
+ try {
55
+ secret = (await readFile(cognitionRuntimeSecretPath(projectRoot), 'utf8')).trim();
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ if (!secret || lease.host !== host || !Number.isInteger(lease.port) || lease.port <= 0)
61
+ return null;
62
+ return new Promise((resolve) => {
63
+ const socket = createConnection({ host, port: lease.port });
64
+ let buffer = '';
65
+ const timeout = setTimeout(() => { socket.destroy(); resolve(null); }, 1_000);
66
+ socket.setEncoding('utf8');
67
+ socket.once('error', () => { clearTimeout(timeout); resolve(null); });
68
+ socket.on('data', (chunk) => {
69
+ buffer += chunk;
70
+ const boundary = buffer.indexOf('\n');
71
+ if (boundary < 0)
72
+ return;
73
+ clearTimeout(timeout);
74
+ socket.end();
75
+ try {
76
+ resolve(JSON.parse(buffer.slice(0, boundary)));
77
+ }
78
+ catch {
79
+ resolve(null);
80
+ }
81
+ });
82
+ socket.on('connect', () => socket.write(`${JSON.stringify({ token: secret, method })}\n`));
83
+ });
84
+ }
85
+ async function liveLease(projectRoot, lease) {
86
+ const candidate = lease === undefined ? await loadLease(projectRoot) : lease;
87
+ if (!candidate)
88
+ return null;
89
+ const response = await request(projectRoot, candidate, 'ping');
90
+ return response?.ok ? candidate : null;
91
+ }
92
+ async function writeLease(projectRoot, lease) {
93
+ await atomicWriteJson(cognitionRuntimeLeasePath(projectRoot), lease);
94
+ }
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
+ }
111
+ function attachProtocol(projectRoot, server, observer, secret, lease, touch, close) {
112
+ server.on('connection', (socket) => {
113
+ socket.setEncoding('utf8');
114
+ let buffer = '';
115
+ socket.on('data', async (chunk) => {
116
+ buffer += chunk;
117
+ if (Buffer.byteLength(buffer) > maxFrameBytes) {
118
+ socket.destroy();
119
+ return;
120
+ }
121
+ const boundary = buffer.indexOf('\n');
122
+ if (boundary < 0)
123
+ return;
124
+ const frame = buffer.slice(0, boundary);
125
+ buffer = buffer.slice(boundary + 1);
126
+ let request;
127
+ try {
128
+ request = JSON.parse(frame);
129
+ }
130
+ catch {
131
+ socket.end('{"ok":false,"error":"invalid_request"}\n');
132
+ return;
133
+ }
134
+ if (!request.token || !sameToken(request.token, secret)) {
135
+ socket.end('{"ok":false,"error":"unauthorized"}\n');
136
+ return;
137
+ }
138
+ touch();
139
+ if (request.method === 'ping') {
140
+ socket.end(`${JSON.stringify({ ok: true, result: { instanceId: lease.instanceId } })}\n`);
141
+ return;
142
+ }
143
+ if (request.method === 'observe') {
144
+ try {
145
+ const result = await observer.observeNow();
146
+ socket.end(`${JSON.stringify({ ok: true, result: { changed: result.changed } })}\n`);
147
+ }
148
+ catch {
149
+ socket.end('{"ok":false,"error":"observe_failed"}\n');
150
+ }
151
+ return;
152
+ }
153
+ if (request.method === 'stop') {
154
+ socket.end('{"ok":true}\n');
155
+ void close('ipc');
156
+ return;
157
+ }
158
+ socket.end('{"ok":false,"error":"unknown_method"}\n');
159
+ });
160
+ });
161
+ }
162
+ export async function serveRuntime(startPath) {
163
+ const projectRoot = await resolveProjectRoot(startPath);
164
+ const currentPolicy = await loadPolicy(projectRoot);
165
+ if (!currentPolicy.enabled)
166
+ throw new Error('runtime is disabled; run ctx runtime enable first');
167
+ const existing = await liveLease(projectRoot);
168
+ if (existing)
169
+ throw new Error('runtime is already running');
170
+ const server = createServer();
171
+ await new Promise((resolve, reject) => {
172
+ server.once('error', reject);
173
+ server.listen(0, host, () => resolve());
174
+ });
175
+ const address = server.address();
176
+ if (!address || typeof address === 'string') {
177
+ server.close();
178
+ throw new Error('runtime failed to bind loopback endpoint');
179
+ }
180
+ const manifest = await loadManifest(projectRoot);
181
+ const now = new Date().toISOString();
182
+ const lease = { schemaVersion: 2, protocolVersion, projectId: manifest.projectId, instanceId: createInstanceId(), pid: process.pid, host, port: address.port, startedAt: now, heartbeatAt: now };
183
+ const secret = createToken();
184
+ const observer = new GitObserver(projectRoot);
185
+ let closed = false;
186
+ let heartbeat;
187
+ let poller;
188
+ let inactivity;
189
+ const close = async (reason) => {
190
+ if (closed)
191
+ return;
192
+ closed = true;
193
+ await closeServer(projectRoot, server, observer, heartbeat, poller, inactivity, lease, reason);
194
+ };
195
+ const touch = () => {
196
+ clearTimeout(inactivity);
197
+ inactivity = setTimeout(() => { void close('inactivity'); }, currentPolicy.inactivityTimeoutMs);
198
+ };
199
+ attachProtocol(projectRoot, server, observer, secret, lease, touch, close);
200
+ try {
201
+ await withLock(`${locksDirectory(projectRoot)}/cognition-runtime-start.lock`, async () => {
202
+ const live = await liveLease(projectRoot);
203
+ if (live)
204
+ throw new Error('runtime is already running');
205
+ await mkdir(path.dirname(cognitionRuntimeSecretPath(projectRoot)), { recursive: true });
206
+ await writeFile(cognitionRuntimeSecretPath(projectRoot), secret, { encoding: 'utf8', mode: 0o600 });
207
+ await writeLease(projectRoot, lease);
208
+ });
209
+ }
210
+ catch (error) {
211
+ await new Promise((resolve) => server.close(() => resolve()));
212
+ throw error;
213
+ }
214
+ heartbeat = setInterval(() => {
215
+ lease.heartbeatAt = new Date().toISOString();
216
+ void writeLease(projectRoot, lease);
217
+ }, 5_000);
218
+ poller = setInterval(() => {
219
+ void observer.observeNow();
220
+ void v1Synced(projectRoot).then((synced) => { if (!synced)
221
+ return reconcileV1State(projectRoot); });
222
+ }, pollIntervalMs);
223
+ touch();
224
+ await appendEvent(projectRoot, { type: 'runtime.started', source: { kind: 'cli', instanceId: lease.instanceId }, idempotencyKey: `runtime:started:${lease.instanceId}`, payload: {} });
225
+ await observer.observeNow();
226
+ return { lease, close };
227
+ }
228
+ export async function runtimeStatus(startPath) {
229
+ const projectRoot = await resolveProjectRoot(startPath);
230
+ const migrated = await exists(cognitionManifestPath(projectRoot));
231
+ if (!migrated)
232
+ return { projectRoot, v2Present: false, enabled: false, migrated: false, running: false, lease: null };
233
+ const runtimePolicy = await loadPolicy(projectRoot);
234
+ const lease = await loadLease(projectRoot);
235
+ const live = await liveLease(projectRoot, lease);
236
+ return { projectRoot, v2Present: true, enabled: runtimePolicy.enabled, migrated: true, running: Boolean(live), lease: lease ? publicLease(lease) : null };
237
+ }
238
+ export async function enableRuntime(startPath) {
239
+ const projectRoot = await resolveProjectRoot(startPath);
240
+ if (!await exists(cognitionManifestPath(projectRoot)))
241
+ throw new Error('v2 migration is required before enabling the runtime');
242
+ const current = await loadPolicy(projectRoot);
243
+ if (!current.enabled) {
244
+ await atomicWriteJson(cognitionPolicyPath(projectRoot), { ...current, enabled: true });
245
+ await appendEvent(projectRoot, { type: 'runtime.enabled', source: { kind: 'cli' }, idempotencyKey: 'runtime:enabled', payload: {} });
246
+ }
247
+ return runtimeStatus(projectRoot);
248
+ }
249
+ export async function disableRuntime(startPath) {
250
+ const projectRoot = await resolveProjectRoot(startPath);
251
+ if (!await exists(cognitionManifestPath(projectRoot)))
252
+ throw new Error('v2 migration is required before disabling the runtime');
253
+ await stopRuntime(projectRoot);
254
+ const current = await loadPolicy(projectRoot);
255
+ await atomicWriteJson(cognitionPolicyPath(projectRoot), { ...current, enabled: false });
256
+ return runtimeStatus(projectRoot);
257
+ }
258
+ export async function startRuntime(startPath) {
259
+ const projectRoot = await resolveProjectRoot(startPath);
260
+ const current = await loadPolicy(projectRoot);
261
+ if (!current.enabled)
262
+ throw new Error('runtime is disabled; run ctx runtime enable first');
263
+ 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;
272
+ }
273
+ throw new Error('runtime failed to start');
274
+ }
275
+ export async function stopRuntime(startPath) {
276
+ const projectRoot = await resolveProjectRoot(startPath);
277
+ const lease = await liveLease(projectRoot);
278
+ if (!lease)
279
+ return runtimeStatus(projectRoot);
280
+ await request(projectRoot, lease, 'stop');
281
+ for (let attempt = 0; attempt < 20; attempt += 1) {
282
+ await new Promise((resolve) => setTimeout(resolve, 50));
283
+ const status = await runtimeStatus(projectRoot);
284
+ if (!status.running)
285
+ return status;
286
+ }
287
+ throw new Error('runtime failed to stop');
288
+ }
289
+ export async function observeRuntime(startPath) {
290
+ const projectRoot = await resolveProjectRoot(startPath);
291
+ const lease = await liveLease(projectRoot);
292
+ if (!lease)
293
+ return false;
294
+ return Boolean((await request(projectRoot, lease, 'observe'))?.ok);
295
+ }
@@ -0,0 +1,132 @@
1
+ export declare const COGNITION_LAYOUT_VERSION: "event-journal-v2";
2
+ export type CognitionEventSource = 'cli' | 'claude-hook' | 'mcp' | 'git-observer' | 'fs-observer' | 'migration';
3
+ export type CognitionEventType = 'runtime.enabled' | 'runtime.started' | 'runtime.stopped' | 'host.session.started' | 'host.prompt.submitted' | 'git.state.observed' | 'git.branch.changed' | 'git.head.changed' | 'workspace.paths.changed' | 'context.checkpoint.imported' | 'context.state.recorded' | 'note.recorded' | 'note.resolved' | 'note.absorbed' | 'knowledge.proposed' | 'knowledge.verified' | 'knowledge.deprecated' | 'deadend.proposed' | 'deadend.verified' | 'deadend.obsoleted' | 'projection.advanced' | 'projection.failed';
4
+ export interface CognitionGitAnchor {
5
+ worktreeId: string | null;
6
+ branch: string | null;
7
+ ref: string | null;
8
+ head: string | null;
9
+ }
10
+ export interface CognitionEvent<T = Record<string, unknown>> {
11
+ eventId: string;
12
+ schemaVersion: 2;
13
+ type: CognitionEventType;
14
+ occurredAt: string;
15
+ observedAt: string;
16
+ projectId: string;
17
+ source: {
18
+ kind: CognitionEventSource;
19
+ instanceId?: string;
20
+ };
21
+ correlationId?: string;
22
+ idempotencyKey: string;
23
+ git: CognitionGitAnchor;
24
+ privacy: {
25
+ classification: 'metadata' | 'redacted-text';
26
+ redactionCount: number;
27
+ };
28
+ payload: T;
29
+ previousHash: string | null;
30
+ hash: string;
31
+ }
32
+ export interface CognitionRuntimePolicy {
33
+ schemaVersion: 2;
34
+ enabled: boolean;
35
+ allowHookStartup: boolean;
36
+ inactivityTimeoutMs: number;
37
+ promptIntentMaxChars: number;
38
+ filesystemEventsEnabled: boolean;
39
+ retentionMaxBytes: number;
40
+ }
41
+ export interface CognitionManifest {
42
+ schemaVersion: 2;
43
+ layoutVersion: typeof COGNITION_LAYOUT_VERSION;
44
+ projectId: string;
45
+ createdAt: string;
46
+ policyVersion: 1;
47
+ }
48
+ export interface Applicability {
49
+ project: true;
50
+ branches?: string[];
51
+ branchMode: 'global' | 'exact' | 'ancestor-of-source' | 'descendant-of-source';
52
+ paths?: string[];
53
+ sourceCommit?: string | null;
54
+ dependencyConstraints?: string[];
55
+ }
56
+ export interface BranchWorkspace {
57
+ schemaVersion: 2;
58
+ workspaceKey: string;
59
+ worktreeId: string | null;
60
+ branch: string | null;
61
+ ref: string | null;
62
+ head: string | null;
63
+ activeContextRef: string | null;
64
+ openObservationEventIds: string[];
65
+ pathFocus: string[];
66
+ recentEventIds: string[];
67
+ lastActivatedAt: string;
68
+ }
69
+ export interface ProjectionCursor {
70
+ schemaVersion: 2;
71
+ lastEventId: string | null;
72
+ lastEventHash: string | null;
73
+ updatedAt: string | null;
74
+ }
75
+ export interface MigrationReadiness {
76
+ projectRoot: string;
77
+ eligible: boolean;
78
+ v1Present: boolean;
79
+ v2Present: boolean;
80
+ counts: {
81
+ contexts: number;
82
+ snapshots: number;
83
+ notes: number;
84
+ knowledge: number;
85
+ deadends: number;
86
+ };
87
+ issues: string[];
88
+ }
89
+ export interface V1ImportReceipt {
90
+ schemaVersion: 2;
91
+ sourceHash: string;
92
+ importedAt: string;
93
+ eventIds: string[];
94
+ counts: MigrationReadiness['counts'];
95
+ }
96
+ export interface MigrationReport extends MigrationReadiness {
97
+ applied: boolean;
98
+ importedEventIds: string[];
99
+ }
100
+ export interface CognitionRecordPayload<T extends Record<string, unknown> = Record<string, unknown>> {
101
+ record: T;
102
+ sourcePath?: string;
103
+ sourceHash?: string;
104
+ anchorProvenance?: 'snapshot' | 'live' | 'global';
105
+ }
106
+ export interface CognitionProjection {
107
+ schemaVersion: 2;
108
+ contexts: Record<string, CognitionRecordPayload>;
109
+ notes: Record<string, CognitionRecordPayload>;
110
+ knowledge: Record<string, CognitionRecordPayload>;
111
+ deadends: Record<string, CognitionRecordPayload>;
112
+ workspaces: Record<string, BranchWorkspace>;
113
+ }
114
+ export interface RuntimeLease {
115
+ schemaVersion: 2;
116
+ protocolVersion: 1;
117
+ projectId: string;
118
+ instanceId: string;
119
+ pid: number;
120
+ host: '127.0.0.1';
121
+ port: number;
122
+ startedAt: string;
123
+ heartbeatAt: string;
124
+ }
125
+ export interface RuntimeStatus {
126
+ projectRoot: string;
127
+ v2Present: boolean;
128
+ enabled: boolean;
129
+ migrated: boolean;
130
+ running: boolean;
131
+ lease: Omit<RuntimeLease, 'projectId'> | null;
132
+ }
@@ -0,0 +1 @@
1
+ export const COGNITION_LAYOUT_VERSION = 'event-journal-v2';
@@ -0,0 +1,7 @@
1
+ import type { ContextMetadata, Deadend, Knowledge, Note, Snapshot } from '../runtime/types.js';
2
+ export declare function v1Fingerprint(projectRoot: string): Promise<string>;
3
+ export declare function v1Synced(projectRoot: string): Promise<boolean>;
4
+ export declare function mirrorCheckpoint(projectRoot: string, context: ContextMetadata, snapshot: Snapshot): Promise<void>;
5
+ export declare function mirrorNote(projectRoot: string, note: Note, absorbed?: boolean): Promise<void>;
6
+ export declare function mirrorKnowledge(projectRoot: string, knowledge: Knowledge): Promise<void>;
7
+ export declare function mirrorDeadend(projectRoot: string, deadend: Deadend): Promise<void>;
@@ -0,0 +1,74 @@
1
+ import crypto from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { access, readFile, readdir } from 'node:fs/promises';
4
+ import { readGitWorktreeState } from '../git/git-adapter.js';
5
+ import { cognitionManifestPath, cognitionPolicyPath, cognitionV1SyncPath, contextsRoot, deadendsPath, knowledgePath, notesPath, snapshotsDirectory } from '../storage/layout.js';
6
+ import { readJson } from '../storage/json-store.js';
7
+ import { atomicWriteJson } from '../storage/atomic-write.js';
8
+ import { appendEvent } from './journal.js';
9
+ import { rebuildProjections } from './projections.js';
10
+ function digest(record) {
11
+ return crypto.createHash('sha256').update(JSON.stringify(record)).digest('hex');
12
+ }
13
+ export async function v1Fingerprint(projectRoot) {
14
+ const files = [notesPath(projectRoot), knowledgePath(projectRoot), deadendsPath(projectRoot)];
15
+ const contextIds = await readdir(contextsRoot(projectRoot)).catch(() => []);
16
+ const contextFiles = contextIds.map((contextId) => path.join(contextsRoot(projectRoot), contextId, 'context.json'));
17
+ const snapshots = (await Promise.all(contextIds.map(async (contextId) => {
18
+ const directory = snapshotsDirectory(projectRoot, contextId);
19
+ const files = await readdir(directory).catch(() => []);
20
+ return files.filter((file) => file.endsWith('.json')).map((file) => path.join(directory, file));
21
+ }))).flat();
22
+ const contents = await Promise.all([...files, ...contextFiles, ...snapshots].map(async (file) => readFile(file, 'utf8').catch(() => '')));
23
+ return digest(contents);
24
+ }
25
+ export async function v1Synced(projectRoot) {
26
+ try {
27
+ const [current, cursor] = await Promise.all([v1Fingerprint(projectRoot), readJson(cognitionV1SyncPath(projectRoot))]);
28
+ return current === cursor.fingerprint;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ async function enabled(projectRoot) {
35
+ try {
36
+ await access(cognitionManifestPath(projectRoot));
37
+ return (await readJson(cognitionPolicyPath(projectRoot))).enabled;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ async function mirror(projectRoot, type, record, identity) {
44
+ if (!await enabled(projectRoot))
45
+ return;
46
+ try {
47
+ const state = await readGitWorktreeState(projectRoot);
48
+ const { appended } = await appendEvent(projectRoot, {
49
+ type,
50
+ source: { kind: 'cli' },
51
+ idempotencyKey: `v1-live:${type}:${identity}:${digest(record)}`,
52
+ git: { worktreeId: state.worktreeId, branch: state.branch, ref: state.ref, head: state.commit },
53
+ payload: { record, anchorProvenance: 'live' },
54
+ });
55
+ if (appended)
56
+ await rebuildProjections(projectRoot);
57
+ await atomicWriteJson(cognitionV1SyncPath(projectRoot), { schemaVersion: 2, fingerprint: await v1Fingerprint(projectRoot), updatedAt: new Date().toISOString() });
58
+ }
59
+ catch {
60
+ // v1 remains authoritative when v2 mirroring is unavailable.
61
+ }
62
+ }
63
+ export async function mirrorCheckpoint(projectRoot, context, snapshot) {
64
+ await mirror(projectRoot, 'context.state.recorded', { context, snapshot }, `${context.id}:${snapshot.snapshotId}`);
65
+ }
66
+ export async function mirrorNote(projectRoot, note, absorbed = false) {
67
+ await mirror(projectRoot, absorbed ? 'note.absorbed' : 'note.recorded', note, note.noteId);
68
+ }
69
+ export async function mirrorKnowledge(projectRoot, knowledge) {
70
+ await mirror(projectRoot, knowledge.status === 'verified' ? 'knowledge.verified' : 'knowledge.proposed', knowledge, knowledge.knowledgeId);
71
+ }
72
+ export async function mirrorDeadend(projectRoot, deadend) {
73
+ await mirror(projectRoot, deadend.status === 'verified' ? 'deadend.verified' : 'deadend.proposed', deadend, deadend.deadendId);
74
+ }
@@ -1,2 +1,9 @@
1
1
  import type { GitState } from '../runtime/types.js';
2
+ export interface GitWorktreeState extends GitState {
3
+ ref: string | null;
4
+ detached: boolean;
5
+ worktreeId: string | null;
6
+ fingerprint: string | null;
7
+ }
2
8
  export declare function readGitState(projectRoot: string): Promise<GitState>;
9
+ export declare function readGitWorktreeState(projectRoot: string): Promise<GitWorktreeState>;
@@ -1,3 +1,4 @@
1
+ import crypto from 'node:crypto';
1
2
  import { execFile } from 'node:child_process';
2
3
  import { promisify } from 'node:util';
3
4
  const execFileAsync = promisify(execFile);
@@ -11,13 +12,22 @@ async function git(args, cwd) {
11
12
  }
12
13
  }
13
14
  export async function readGitState(projectRoot) {
15
+ const state = await readGitWorktreeState(projectRoot);
16
+ return { root: state.root, branch: state.branch, commit: state.commit };
17
+ }
18
+ export async function readGitWorktreeState(projectRoot) {
14
19
  const root = await git(['rev-parse', '--show-toplevel'], projectRoot);
15
20
  if (!root) {
16
- return { root: null, branch: null, commit: null };
21
+ return { root: null, branch: null, commit: null, ref: null, detached: false, worktreeId: null, fingerprint: null };
17
22
  }
18
- return {
19
- root,
20
- branch: await git(['branch', '--show-current'], projectRoot),
21
- commit: await git(['rev-parse', 'HEAD'], projectRoot),
22
- };
23
+ const [branch, commit, ref, gitDir] = await Promise.all([
24
+ git(['branch', '--show-current'], projectRoot),
25
+ git(['rev-parse', 'HEAD'], projectRoot),
26
+ git(['symbolic-ref', '--quiet', '--short', 'HEAD'], projectRoot),
27
+ git(['rev-parse', '--git-dir'], projectRoot),
28
+ ]);
29
+ const detached = ref === null;
30
+ const worktreeId = crypto.createHash('sha256').update(`${root}\0${gitDir ?? projectRoot}`).digest('hex').slice(0, 16);
31
+ const fingerprint = crypto.createHash('sha256').update(`${worktreeId}\0${ref ?? 'detached'}\0${commit ?? 'missing'}`).digest('hex').slice(0, 16);
32
+ return { root, branch, commit, ref, detached, worktreeId, fingerprint };
23
33
  }
@@ -1,4 +1,11 @@
1
1
  import { contextOrient } from '../agent/api.js';
2
+ import { appendEvent } from '../cognition/journal.js';
3
+ import { v2Enabled } from '../cognition/orient.js';
4
+ import { startRuntime } from '../cognition/runtime.js';
5
+ import { readGitWorktreeState } from '../git/git-adapter.js';
6
+ import { resolveProjectRoot } from '../project/project-resolver.js';
7
+ import { cognitionPolicyPath } from '../storage/layout.js';
8
+ import { readJson } from '../storage/json-store.js';
2
9
  const HOOK_RESPONSE = '{}\n';
3
10
  function text(value) {
4
11
  return typeof value === 'string' ? value : undefined;
@@ -51,6 +58,49 @@ function promptContext(result) {
51
58
  }
52
59
  return phases.join('\n');
53
60
  }
61
+ function stableId(input, name) {
62
+ return name === 'session' ? text(input.session_id) ?? text(input.sessionId) : text(input.event_id) ?? text(input.eventId);
63
+ }
64
+ function redactIntent(value) {
65
+ let redactionCount = 0;
66
+ const intent = value
67
+ .replace(/(?:api[_-]?key|token|password|secret)\s*[:=]\s*\S+/gi, () => { redactionCount += 1; return '[REDACTED]'; })
68
+ .replace(/\b(?:sk|ghp|gho|github_pat)_[A-Za-z0-9_-]+\b/g, () => { redactionCount += 1; return '[REDACTED]'; });
69
+ return { intent, redactionCount };
70
+ }
71
+ async function within(task, milliseconds) {
72
+ return Promise.race([task, new Promise((_, reject) => setTimeout(() => reject(new Error('hook deadline exceeded')), milliseconds))]);
73
+ }
74
+ async function capture(event, input) {
75
+ const cwd = text(input.cwd);
76
+ const projectRoot = await resolveProjectRoot(cwd);
77
+ if (!await v2Enabled(projectRoot))
78
+ return;
79
+ const policy = await readJson(cognitionPolicyPath(projectRoot));
80
+ const sessionId = stableId(input, 'session');
81
+ const eventId = stableId(input, 'event');
82
+ if (!sessionId || !eventId)
83
+ return;
84
+ const state = await readGitWorktreeState(projectRoot);
85
+ const captured = event === 'user-prompt' ? redactIntent(compact(text(input.prompt) ?? '', policy.promptIntentMaxChars)) : { intent: '', redactionCount: 0 };
86
+ await appendEvent(projectRoot, {
87
+ type: event === 'session-start' ? 'host.session.started' : 'host.prompt.submitted',
88
+ source: { kind: 'claude-hook', instanceId: sessionId },
89
+ correlationId: sessionId,
90
+ idempotencyKey: `claude-hook:${event}:${sessionId}:${eventId}`,
91
+ git: { worktreeId: state.worktreeId, branch: state.branch, ref: state.ref, head: state.commit },
92
+ privacy: { classification: captured.redactionCount > 0 || event === 'user-prompt' ? 'redacted-text' : 'metadata', redactionCount: captured.redactionCount },
93
+ payload: event === 'user-prompt' ? { eventId, intent: captured.intent } : { eventId },
94
+ });
95
+ }
96
+ async function maybeStartRuntime(input) {
97
+ const projectRoot = await resolveProjectRoot(text(input.cwd));
98
+ if (!await v2Enabled(projectRoot))
99
+ return;
100
+ const policy = await readJson(cognitionPolicyPath(projectRoot));
101
+ if (policy.allowHookStartup)
102
+ await within(startRuntime(projectRoot), 1_000);
103
+ }
54
104
  async function parseInput(raw) {
55
105
  try {
56
106
  const value = JSON.parse(raw);
@@ -65,6 +115,9 @@ export async function runClaudeCodeHook(event, raw) {
65
115
  if (!input)
66
116
  return HOOK_RESPONSE;
67
117
  try {
118
+ if (event === 'session-start')
119
+ await maybeStartRuntime(input).catch(() => undefined);
120
+ await capture(event, input).catch(() => undefined);
68
121
  const cwd = text(input.cwd);
69
122
  const result = event === 'session-start'
70
123
  ? await contextOrient(cwd, { maxChars: 1200, noteLimit: 8 })