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
@@ -5,6 +5,15 @@ import { resolveProjectRoot } from '../project/project-resolver.js';
5
5
  import { loadManifest } from '../runtime/init.js';
6
6
  import { withLock } from '../storage/lock.js';
7
7
  import { cognitionJournalDirectory, locksDirectory } from '../storage/layout.js';
8
+ const eventTypes = new Set([
9
+ 'runtime.enabled', 'runtime.started', 'runtime.stopped', 'host.session.started', 'host.prompt.submitted',
10
+ 'git.state.observed', 'git.branch.changed', 'git.head.changed', 'workspace.paths.changed',
11
+ 'context.checkpoint.imported', 'context.state.recorded', 'note.recorded', 'note.resolved', 'note.absorbed',
12
+ 'knowledge.proposed', 'knowledge.verified', 'knowledge.deprecated', 'knowledge.rejected',
13
+ 'deadend.proposed', 'deadend.verified', 'deadend.obsoleted', 'deadend.rejected', 'record.used',
14
+ 'projection.advanced', 'projection.failed',
15
+ ]);
16
+ const sourceKinds = new Set(['cli', 'claude-hook', 'mcp', 'git-observer', 'fs-observer', 'migration']);
8
17
  const segmentName = '00000001.ndjson';
9
18
  function canonical(value) {
10
19
  if (value === null || typeof value !== 'object')
@@ -33,6 +42,49 @@ async function journalFiles(projectRoot) {
33
42
  throw error;
34
43
  }
35
44
  }
45
+ function isRecord(value) {
46
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
47
+ }
48
+ function validGitAnchor(value) {
49
+ return isRecord(value)
50
+ && (typeof value.worktreeId === 'string' || value.worktreeId === null)
51
+ && (typeof value.branch === 'string' || value.branch === null)
52
+ && (typeof value.ref === 'string' || value.ref === null)
53
+ && (typeof value.head === 'string' || value.head === null);
54
+ }
55
+ function validIsoDate(value) {
56
+ return typeof value === 'string' && Number.isFinite(Date.parse(value));
57
+ }
58
+ function validateEventShape(event, projectId, file, line) {
59
+ if (!isRecord(event)
60
+ || event.schemaVersion !== 2
61
+ || typeof event.eventId !== 'string'
62
+ || event.eventId.length === 0
63
+ || typeof event.type !== 'string'
64
+ || !eventTypes.has(event.type)
65
+ || !validIsoDate(event.occurredAt)
66
+ || !validIsoDate(event.observedAt)
67
+ || event.projectId !== projectId
68
+ || !isRecord(event.source)
69
+ || typeof event.source.kind !== 'string'
70
+ || !sourceKinds.has(event.source.kind)
71
+ || (event.source.instanceId !== undefined && typeof event.source.instanceId !== 'string')
72
+ || (event.correlationId !== undefined && typeof event.correlationId !== 'string')
73
+ || typeof event.idempotencyKey !== 'string'
74
+ || event.idempotencyKey.length === 0
75
+ || !validGitAnchor(event.git)
76
+ || !isRecord(event.privacy)
77
+ || (event.privacy.classification !== 'metadata' && event.privacy.classification !== 'redacted-text')
78
+ || typeof event.privacy.redactionCount !== 'number'
79
+ || !Number.isSafeInteger(event.privacy.redactionCount)
80
+ || event.privacy.redactionCount < 0
81
+ || !isRecord(event.payload)
82
+ || (event.previousHash !== null && typeof event.previousHash !== 'string')
83
+ || typeof event.hash !== 'string'
84
+ || event.hash.length === 0) {
85
+ throw new Error(`invalid journal record schema at ${file}:${line}`);
86
+ }
87
+ }
36
88
  async function recoverPartialTail(projectRoot) {
37
89
  const files = await journalFiles(projectRoot);
38
90
  const file = files.at(-1);
@@ -46,13 +98,15 @@ async function recoverPartialTail(projectRoot) {
46
98
  const tail = content.slice(boundary + 1);
47
99
  try {
48
100
  JSON.parse(tail);
101
+ await appendFile(filePath, '\n', 'utf8');
49
102
  }
50
103
  catch {
51
104
  await truncate(filePath, boundary < 0 ? 0 : boundary + 1);
52
105
  }
53
106
  }
54
- export async function readJournal(startPath) {
107
+ export async function readJournal(startPath, options = {}) {
55
108
  const projectRoot = await resolveProjectRoot(startPath);
109
+ const projectId = (await loadManifest(projectRoot)).projectId;
56
110
  const events = [];
57
111
  let previousHash = null;
58
112
  for (const file of await journalFiles(projectRoot)) {
@@ -67,12 +121,13 @@ export async function readJournal(startPath) {
67
121
  event = JSON.parse(line);
68
122
  }
69
123
  catch (error) {
70
- if (index === lines.length - 1 && !content.endsWith('\n'))
124
+ if (options.allowPartialTail !== false && index === lines.length - 1 && !content.endsWith('\n'))
71
125
  break;
72
126
  throw new Error(`invalid journal record ${file}:${index + 1}: ${error instanceof Error ? error.message : 'invalid JSON'}`);
73
127
  }
128
+ validateEventShape(event, projectId, file, index + 1);
74
129
  const { hash, ...withoutHash } = event;
75
- if (event.schemaVersion !== 2 || !event.eventId || event.previousHash !== previousHash || hash !== eventHash(withoutHash)) {
130
+ if (event.previousHash !== previousHash || hash !== eventHash(withoutHash)) {
76
131
  throw new Error(`invalid journal hash chain at ${file}:${index + 1}`);
77
132
  }
78
133
  previousHash = event.hash;
@@ -81,14 +136,38 @@ export async function readJournal(startPath) {
81
136
  }
82
137
  return events;
83
138
  }
139
+ export async function verifyJournal(startPath) {
140
+ return readJournal(startPath, { allowPartialTail: false });
141
+ }
84
142
  export async function appendEvent(startPath, input) {
85
143
  const projectRoot = await resolveProjectRoot(startPath);
86
144
  return withLock(`${locksDirectory(projectRoot)}/cognition-journal.lock`, async () => {
87
145
  await recoverPartialTail(projectRoot);
88
146
  const existing = await readJournal(projectRoot);
89
147
  const duplicate = existing.find((event) => event.idempotencyKey === input.idempotencyKey);
90
- if (duplicate)
148
+ if (duplicate) {
149
+ const candidate = {
150
+ type: input.type,
151
+ source: input.source ?? { kind: 'cli' },
152
+ correlationId: input.correlationId,
153
+ idempotencyKey: input.idempotencyKey,
154
+ git: input.git ?? { worktreeId: null, branch: null, ref: null, head: null },
155
+ privacy: input.privacy ?? { classification: 'metadata', redactionCount: 0 },
156
+ payload: input.payload,
157
+ };
158
+ const existingSemantic = {
159
+ type: duplicate.type,
160
+ source: duplicate.source,
161
+ correlationId: duplicate.correlationId,
162
+ idempotencyKey: duplicate.idempotencyKey,
163
+ git: duplicate.git,
164
+ privacy: duplicate.privacy,
165
+ payload: duplicate.payload,
166
+ };
167
+ if (canonical(candidate) !== canonical(existingSemantic))
168
+ throw new Error(`idempotency key conflict: ${input.idempotencyKey}`);
91
169
  return { event: duplicate, appended: false };
170
+ }
92
171
  const now = new Date().toISOString();
93
172
  const eventWithoutHash = {
94
173
  eventId: input.eventId ?? eventId(),
@@ -21,6 +21,16 @@ function sha256(value) {
21
21
  function isNotFound(error) {
22
22
  return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
23
23
  }
24
+ function eventType(record, live = false) {
25
+ const status = record.record.status;
26
+ if (record.kind === 'context')
27
+ return live ? 'context.state.recorded' : 'context.checkpoint.imported';
28
+ if (record.kind === 'note')
29
+ return record.record.absorbedBySnapshotId ? 'note.absorbed' : record.record.status === 'resolved' ? 'note.resolved' : 'note.recorded';
30
+ if (record.kind === 'knowledge')
31
+ return status === 'verified' ? 'knowledge.verified' : status === 'deprecated' ? 'knowledge.deprecated' : status === 'rejected' ? 'knowledge.rejected' : 'knowledge.proposed';
32
+ return status === 'verified' ? 'deadend.verified' : status === 'obsolete' ? 'deadend.obsoleted' : status === 'rejected' ? 'deadend.rejected' : 'deadend.proposed';
33
+ }
24
34
  async function readOptional(filePath) {
25
35
  try {
26
36
  return await readFile(filePath, 'utf8');
@@ -95,7 +105,7 @@ async function sourceRecords(projectRoot) {
95
105
  async function writeV2Metadata(projectRoot) {
96
106
  const v1 = await loadManifest(projectRoot);
97
107
  const manifest = { schemaVersion: 2, layoutVersion: COGNITION_LAYOUT_VERSION, projectId: v1.projectId, createdAt: new Date().toISOString(), policyVersion: 1 };
98
- const policy = { schemaVersion: 2, enabled: false, allowHookStartup: false, inactivityTimeoutMs: 300_000, promptIntentMaxChars: 500, filesystemEventsEnabled: false, retentionMaxBytes: 64 * 1024 * 1024 };
108
+ const policy = { schemaVersion: 2, enabled: true, autoStart: true, allowHookStartup: true, inactivityTimeoutMs: 300_000, promptIntentMaxChars: 500, filesystemEventsEnabled: false, retentionMaxBytes: 64 * 1024 * 1024 };
99
109
  await Promise.all([atomicWriteJson(cognitionManifestPath(projectRoot), manifest), atomicWriteJson(cognitionPolicyPath(projectRoot), policy)]);
100
110
  }
101
111
  async function quarantineReadiness(projectRoot, readiness) {
@@ -129,12 +139,8 @@ export async function importV1(startPath) {
129
139
  await writeV2Metadata(projectRoot);
130
140
  const importedEventIds = [];
131
141
  for (const record of records) {
132
- const status = record.record.status;
133
142
  const event = await appendEvent(projectRoot, {
134
- type: record.kind === 'context' ? 'context.checkpoint.imported'
135
- : record.kind === 'note' ? 'note.recorded'
136
- : record.kind === 'knowledge' ? status === 'verified' ? 'knowledge.verified' : 'knowledge.proposed'
137
- : status === 'verified' ? 'deadend.verified' : 'deadend.proposed',
143
+ type: eventType(record),
138
144
  occurredAt: record.occurredAt,
139
145
  source: { kind: 'migration' },
140
146
  idempotencyKey: `v1:${record.sourceHash}:${record.kind}:${record.id}`,
@@ -158,12 +164,8 @@ export async function reconcileV1State(startPath) {
158
164
  return;
159
165
  const { records, sourceHash } = await sourceRecords(projectRoot);
160
166
  for (const record of records) {
161
- const status = record.record.status;
162
167
  await appendEvent(projectRoot, {
163
- type: record.kind === 'context' ? 'context.state.recorded'
164
- : record.kind === 'note' ? record.record.absorbedBySnapshotId ? 'note.absorbed' : record.record.status === 'resolved' ? 'note.resolved' : 'note.recorded'
165
- : record.kind === 'knowledge' ? status === 'verified' ? 'knowledge.verified' : 'knowledge.proposed'
166
- : status === 'verified' ? 'deadend.verified' : 'deadend.proposed',
168
+ type: eventType(record, true),
167
169
  occurredAt: record.occurredAt,
168
170
  source: { kind: 'migration' },
169
171
  idempotencyKey: `v1-live-sync:${sourceHash}:${record.kind}:${record.id}`,
@@ -1,9 +1,13 @@
1
+ import type { CognitionEvent, CognitionRetrievalProjection } from './types.js';
1
2
  export interface V2OrientationPlan {
2
3
  contextId: string;
3
4
  snapshotId: string;
4
5
  knowledgeIds: ReadonlySet<string>;
5
6
  deadendIds: ReadonlySet<string>;
6
7
  noteIds: ReadonlySet<string>;
8
+ retrieval: CognitionRetrievalProjection;
9
+ currentGit: CognitionEvent['git'];
10
+ selection: 'requested-context' | 'exact-worktree-ref' | 'legacy-ref-branch';
7
11
  }
8
12
  export declare function selectV2Orientation(projectRoot: string, contextId?: string): Promise<V2OrientationPlan | null>;
9
13
  export declare function v2Enabled(projectRoot: string): Promise<boolean>;
@@ -1,10 +1,12 @@
1
1
  import { access } from 'node:fs/promises';
2
2
  import { readGitWorktreeState } from '../git/git-adapter.js';
3
- import { cognitionManifestPath, cognitionPolicyPath } from '../storage/layout.js';
3
+ import { cognitionManifestPath } from '../storage/layout.js';
4
4
  import { readJson } from '../storage/json-store.js';
5
5
  import { loadManifest } from '../runtime/init.js';
6
6
  import { readJournal } from './journal.js';
7
7
  import { projectEvents, workspaceKey } from './projections.js';
8
+ import { buildRetrievalProjection } from './retrieval.js';
9
+ import { loadRuntimePolicy } from './policy.js';
8
10
  import { v1Synced } from './v1-bridge.js';
9
11
  function contextReference(value) {
10
12
  const boundary = value.lastIndexOf(':');
@@ -47,7 +49,7 @@ export async function selectV2Orientation(projectRoot, contextId) {
47
49
  const [manifest, cognition, policy] = await Promise.all([
48
50
  loadManifest(projectRoot),
49
51
  readJson(cognitionManifestPath(projectRoot)),
50
- readJson(cognitionPolicyPath(projectRoot)),
52
+ loadRuntimePolicy(projectRoot),
51
53
  ]);
52
54
  if (cognition.schemaVersion !== 2 || cognition.projectId !== manifest.projectId || !policy.enabled || !await v1Synced(projectRoot))
53
55
  return null;
@@ -56,6 +58,7 @@ export async function selectV2Orientation(projectRoot, contextId) {
56
58
  return null;
57
59
  const projection = projectEvents(events);
58
60
  let selected = null;
61
+ let selection = 'requested-context';
59
62
  if (contextId) {
60
63
  const matching = [...Object.keys(projection.contexts)].reverse().find((reference) => contextReference(reference)?.contextId === contextId);
61
64
  selected = matching ? contextReference(matching) : null;
@@ -65,10 +68,17 @@ export async function selectV2Orientation(projectRoot, contextId) {
65
68
  const legacy = Object.values(projection.workspaces).find((workspace) => workspace.worktreeId === null && (workspace.ref === current.ref || workspace.branch === current.branch));
66
69
  const reference = (exact?.activeContextRef ? exact : legacy)?.activeContextRef;
67
70
  selected = reference ? contextReference(reference) : null;
71
+ selection = exact?.activeContextRef ? 'exact-worktree-ref' : 'legacy-ref-branch';
68
72
  }
69
73
  if (!selected || !projection.contexts[`${selected.contextId}:${selected.snapshotId}`])
70
74
  return null;
71
- return { ...selected, ...latestRecords(events, current, selected.contextId) };
75
+ return {
76
+ ...selected,
77
+ ...latestRecords(events, current, selected.contextId),
78
+ retrieval: buildRetrievalProjection(events),
79
+ currentGit: { worktreeId: current.worktreeId, branch: current.branch, ref: current.ref, head: current.commit },
80
+ selection,
81
+ };
72
82
  }
73
83
  catch {
74
84
  return null;
@@ -77,7 +87,7 @@ export async function selectV2Orientation(projectRoot, contextId) {
77
87
  export async function v2Enabled(projectRoot) {
78
88
  try {
79
89
  await access(cognitionManifestPath(projectRoot));
80
- return (await readJson(cognitionPolicyPath(projectRoot))).enabled;
90
+ return (await loadRuntimePolicy(projectRoot)).enabled;
81
91
  }
82
92
  catch {
83
93
  return false;
@@ -0,0 +1,4 @@
1
+ import type { CognitionRuntimePolicy } from './types.js';
2
+ export declare function normalizeRuntimePolicy(value: Partial<CognitionRuntimePolicy>): CognitionRuntimePolicy;
3
+ export declare function loadRuntimePolicy(projectRoot: string): Promise<CognitionRuntimePolicy>;
4
+ export declare function writeRuntimePolicy(projectRoot: string, policy: CognitionRuntimePolicy): Promise<void>;
@@ -0,0 +1,49 @@
1
+ import { atomicWriteJson } from '../storage/atomic-write.js';
2
+ import { cognitionPolicyPath } from '../storage/layout.js';
3
+ import { readJson } from '../storage/json-store.js';
4
+ const defaults = {
5
+ schemaVersion: 2,
6
+ enabled: true,
7
+ autoStart: true,
8
+ allowHookStartup: true,
9
+ inactivityTimeoutMs: 300_000,
10
+ promptIntentMaxChars: 500,
11
+ filesystemEventsEnabled: false,
12
+ retentionMaxBytes: 64 * 1024 * 1024,
13
+ };
14
+ function booleanValue(value, fallback, field) {
15
+ if (value === undefined)
16
+ return fallback;
17
+ if (typeof value !== 'boolean')
18
+ throw new Error(`runtime policy field ${field} must be boolean`);
19
+ return value;
20
+ }
21
+ function nonNegativeInteger(value, fallback, field) {
22
+ if (value === undefined)
23
+ return fallback;
24
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
25
+ throw new Error(`runtime policy field ${field} must be a non-negative integer`);
26
+ return value;
27
+ }
28
+ export function normalizeRuntimePolicy(value) {
29
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
30
+ throw new Error('runtime policy must be an object');
31
+ if (value.schemaVersion !== undefined && value.schemaVersion !== 2)
32
+ throw new Error('unsupported runtime policy schema');
33
+ return {
34
+ ...defaults,
35
+ enabled: booleanValue(value.enabled, defaults.enabled, 'enabled'),
36
+ autoStart: booleanValue(value.autoStart, defaults.autoStart, 'autoStart'),
37
+ allowHookStartup: booleanValue(value.allowHookStartup, defaults.allowHookStartup, 'allowHookStartup'),
38
+ inactivityTimeoutMs: nonNegativeInteger(value.inactivityTimeoutMs, defaults.inactivityTimeoutMs, 'inactivityTimeoutMs'),
39
+ promptIntentMaxChars: nonNegativeInteger(value.promptIntentMaxChars, defaults.promptIntentMaxChars, 'promptIntentMaxChars'),
40
+ filesystemEventsEnabled: booleanValue(value.filesystemEventsEnabled, defaults.filesystemEventsEnabled, 'filesystemEventsEnabled'),
41
+ retentionMaxBytes: nonNegativeInteger(value.retentionMaxBytes, defaults.retentionMaxBytes, 'retentionMaxBytes'),
42
+ };
43
+ }
44
+ export async function loadRuntimePolicy(projectRoot) {
45
+ return normalizeRuntimePolicy(await readJson(cognitionPolicyPath(projectRoot)));
46
+ }
47
+ export async function writeRuntimePolicy(projectRoot, policy) {
48
+ await atomicWriteJson(cognitionPolicyPath(projectRoot), normalizeRuntimePolicy(policy));
49
+ }
Binary file
@@ -0,0 +1,26 @@
1
+ import type { DeadendDiscoveryResult, KnowledgeDiscoveryResult } from '../runtime/types.js';
2
+ import type { CognitionEvent, CognitionRetrievalProjection } from './types.js';
3
+ export declare function normalized(value: string): string;
4
+ export declare function queryTerms(query: string): string[];
5
+ export declare function buildRetrievalProjection(events: CognitionEvent[]): CognitionRetrievalProjection;
6
+ export interface RetrievalExclusions {
7
+ status_not_verified: number;
8
+ applicability_mismatch: number;
9
+ scope_mismatch: number;
10
+ query_no_match: number;
11
+ result_limit: number;
12
+ }
13
+ export interface V2Discovery<T> {
14
+ result: T;
15
+ exclusions: RetrievalExclusions;
16
+ }
17
+ export declare function discoverV2Knowledge(index: CognitionRetrievalProjection, query: string, current: CognitionEvent['git'], options: {
18
+ scope?: string;
19
+ limit: number;
20
+ maxChars: number;
21
+ }): V2Discovery<KnowledgeDiscoveryResult>;
22
+ export declare function discoverV2Deadends(index: CognitionRetrievalProjection, query: string, current: CognitionEvent['git'], options: {
23
+ scope?: string;
24
+ limit: number;
25
+ maxChars: number;
26
+ }): V2Discovery<DeadendDiscoveryResult>;
@@ -0,0 +1,273 @@
1
+ const aliasGroups = [
2
+ ['投保人认证', '投保人身份认证', '身份核验', '实名认证', 'applicant authentication', 'identity verification'],
3
+ ['短信验证码', '验证码', 'sms verification', 'otp'],
4
+ ['证件ocr', '证件识别', 'identity ocr', 'document ocr'],
5
+ ['人脸识别', '人脸认证', 'face recognition', 'facial verification'],
6
+ ];
7
+ export function normalized(value) {
8
+ return value.trim().toLocaleLowerCase();
9
+ }
10
+ export function queryTerms(query) {
11
+ const clean = normalized(query);
12
+ const terms = new Set(clean.split(/\s+/).filter(Boolean));
13
+ for (const group of aliasGroups) {
14
+ if (group.some((alias) => clean.includes(normalized(alias)))) {
15
+ for (const alias of group)
16
+ terms.add(normalized(alias));
17
+ }
18
+ }
19
+ return [...terms].sort();
20
+ }
21
+ function strings(record, entity) {
22
+ return entity === 'knowledge'
23
+ ? {
24
+ statement: typeof record.statement === 'string' ? record.statement : '',
25
+ kind: typeof record.kind === 'string' ? record.kind : '',
26
+ scope: typeof record.scope === 'string' ? record.scope : '',
27
+ }
28
+ : {
29
+ attempt: typeof record.attempt === 'string' ? record.attempt : '',
30
+ reason: typeof record.reason === 'string' ? record.reason : '',
31
+ scope: typeof record.scope === 'string' ? record.scope : '',
32
+ };
33
+ }
34
+ function defaultApplicability(event) {
35
+ if (event.git.ref === null && event.git.branch === null)
36
+ return { project: true, branchMode: 'global' };
37
+ return {
38
+ project: true,
39
+ branchMode: 'exact',
40
+ branches: [event.git.ref ?? event.git.branch ?? 'detached'],
41
+ sourceCommit: event.git.head,
42
+ };
43
+ }
44
+ function applicability(record, event) {
45
+ const value = record.applicability;
46
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
47
+ const candidate = value;
48
+ if (candidate.project === true && (candidate.branchMode === 'global' || candidate.branchMode === 'exact')) {
49
+ return {
50
+ project: true,
51
+ branchMode: candidate.branchMode,
52
+ ...(Array.isArray(candidate.branches) ? { branches: candidate.branches.filter((item) => typeof item === 'string').sort() } : {}),
53
+ ...(Array.isArray(candidate.paths) ? { paths: candidate.paths.filter((item) => typeof item === 'string').sort() } : {}),
54
+ ...(typeof candidate.sourceCommit === 'string' || candidate.sourceCommit === null ? { sourceCommit: candidate.sourceCommit } : {}),
55
+ };
56
+ }
57
+ }
58
+ return defaultApplicability(event);
59
+ }
60
+ function recordPayload(event) {
61
+ const payload = event.payload;
62
+ return payload.record && typeof payload.record === 'object' && !Array.isArray(payload.record)
63
+ ? payload.record
64
+ : null;
65
+ }
66
+ function lifecycleEntity(event, record) {
67
+ if (event.type.startsWith('knowledge.') && typeof record.knowledgeId === 'string')
68
+ return { entity: 'knowledge', entityId: record.knowledgeId };
69
+ if (event.type.startsWith('deadend.') && typeof record.deadendId === 'string')
70
+ return { entity: 'deadend', entityId: record.deadendId };
71
+ return null;
72
+ }
73
+ function lifecycleEvent(event) {
74
+ return [
75
+ 'knowledge.proposed', 'knowledge.verified', 'knowledge.deprecated', 'knowledge.rejected',
76
+ 'deadend.proposed', 'deadend.verified', 'deadend.obsoleted', 'deadend.rejected',
77
+ ].includes(event.type);
78
+ }
79
+ export function buildRetrievalProjection(events) {
80
+ const entries = new Map();
81
+ const feedback = new Map();
82
+ for (const event of events) {
83
+ if (event.type === 'record.used') {
84
+ const payload = event.payload;
85
+ const entity = payload.entity === 'knowledge' || payload.entity === 'deadend' ? payload.entity : null;
86
+ const entityId = typeof payload.entityId === 'string' ? payload.entityId : null;
87
+ if (!entity || !entityId)
88
+ continue;
89
+ const key = `${entity}:${entityId}`;
90
+ const prior = feedback.get(key) ?? { usedCount: 0, latestUsedAt: event.occurredAt };
91
+ feedback.set(key, { usedCount: prior.usedCount + 1, latestUsedAt: event.occurredAt });
92
+ continue;
93
+ }
94
+ if (!lifecycleEvent(event))
95
+ continue;
96
+ const record = recordPayload(event);
97
+ if (!record)
98
+ continue;
99
+ const identity = lifecycleEntity(event, record);
100
+ if (!identity)
101
+ continue;
102
+ const fields = strings(record, identity.entity);
103
+ const normalizedTerms = [...new Set(Object.values(fields).flatMap((value) => {
104
+ const normalizedValue = normalized(value);
105
+ return normalizedValue ? [normalizedValue, ...normalizedValue.split(/\s+/).filter(Boolean)] : [];
106
+ }))].sort();
107
+ const key = `${identity.entity}:${identity.entityId}`;
108
+ entries.set(key, {
109
+ entity: identity.entity,
110
+ entityId: identity.entityId,
111
+ status: typeof record.status === 'string' ? record.status : 'candidate',
112
+ record,
113
+ applicability: applicability(record, event),
114
+ searchable: Object.fromEntries(Object.entries(fields).map(([field, value]) => [field, normalized(value)])),
115
+ normalizedTerms,
116
+ provenance: {
117
+ eventId: event.eventId,
118
+ eventType: event.type,
119
+ occurredAt: event.occurredAt,
120
+ git: event.git,
121
+ ...(typeof event.payload.anchorProvenance === 'string' ? { anchorProvenance: event.payload.anchorProvenance } : {}),
122
+ },
123
+ feedback: { usedCount: 0, latestUsedAt: null },
124
+ });
125
+ }
126
+ for (const [key, entry] of entries) {
127
+ const value = feedback.get(key);
128
+ if (value)
129
+ entry.feedback = value;
130
+ }
131
+ const ordered = Object.fromEntries([...entries.entries()].sort(([left], [right]) => left.localeCompare(right)));
132
+ const terms = new Map();
133
+ for (const [key, entry] of Object.entries(ordered)) {
134
+ for (const term of entry.normalizedTerms)
135
+ terms.set(term, [...(terms.get(term) ?? []), key]);
136
+ }
137
+ return {
138
+ schemaVersion: 2,
139
+ entries: ordered,
140
+ terms: Object.fromEntries([...terms.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([term, ids]) => [term, ids.sort()])),
141
+ };
142
+ }
143
+ function emptyExclusions() {
144
+ return { status_not_verified: 0, applicability_mismatch: 0, scope_mismatch: 0, query_no_match: 0, result_limit: 0 };
145
+ }
146
+ function applicable(entry, current) {
147
+ if (entry.applicability.branchMode === 'global')
148
+ return true;
149
+ const anchor = entry.provenance.git;
150
+ return anchor.ref === current.ref
151
+ && anchor.branch === current.branch
152
+ && (anchor.worktreeId === null || anchor.worktreeId === current.worktreeId);
153
+ }
154
+ function match(entry, terms, entity) {
155
+ const fields = new Set();
156
+ const matched = new Set();
157
+ let score = 0;
158
+ const phrase = terms.join(' ');
159
+ const statementField = entity === 'knowledge' ? 'statement' : 'attempt';
160
+ if (entry.searchable[statementField]?.includes(phrase))
161
+ score += entity === 'knowledge' ? 20 : 0;
162
+ for (const term of terms) {
163
+ for (const [field, value] of Object.entries(entry.searchable)) {
164
+ if (!value.includes(term))
165
+ continue;
166
+ fields.add(field);
167
+ matched.add(term);
168
+ score += field === statementField ? 5 : field === 'scope' ? 4 : 3;
169
+ }
170
+ }
171
+ if (entry.searchable.scope === phrase)
172
+ score += 10;
173
+ return { score, matchedTerms: [...matched].sort(), matchedFields: [...fields].sort() };
174
+ }
175
+ function limitText(value, maxChars) {
176
+ return value.length <= maxChars ? value : `${value.slice(0, Math.max(0, maxChars - 1))}…`;
177
+ }
178
+ export function discoverV2Knowledge(index, query, current, options) {
179
+ const exclusions = emptyExclusions();
180
+ const terms = queryTerms(query);
181
+ const hits = Object.values(index.entries).flatMap((entry) => {
182
+ if (entry.entity !== 'knowledge')
183
+ return [];
184
+ if (entry.status !== 'verified') {
185
+ exclusions.status_not_verified += 1;
186
+ return [];
187
+ }
188
+ if (!applicable(entry, current)) {
189
+ exclusions.applicability_mismatch += 1;
190
+ return [];
191
+ }
192
+ const knowledge = entry.record;
193
+ if (options.scope && normalized(knowledge.scope) !== normalized(options.scope)) {
194
+ exclusions.scope_mismatch += 1;
195
+ return [];
196
+ }
197
+ const matched = match(entry, terms, 'knowledge');
198
+ if (matched.matchedTerms.length === 0) {
199
+ exclusions.query_no_match += 1;
200
+ return [];
201
+ }
202
+ return [{ entry, knowledge, ...matched }];
203
+ }).sort((left, right) => right.score - left.score
204
+ || Math.min(right.entry.feedback.usedCount, 5) - Math.min(left.entry.feedback.usedCount, 5)
205
+ || right.knowledge.confidence - left.knowledge.confidence
206
+ || right.entry.provenance.occurredAt.localeCompare(left.entry.provenance.occurredAt)
207
+ || left.knowledge.knowledgeId.localeCompare(right.knowledge.knowledgeId));
208
+ const selected = hits.slice(0, options.limit);
209
+ exclusions.result_limit = hits.length - selected.length;
210
+ return {
211
+ result: {
212
+ query,
213
+ total: hits.length,
214
+ truncated: hits.length > selected.length,
215
+ hits: selected.map(({ knowledge, score, matchedTerms, matchedFields, entry }) => ({
216
+ knowledge: { ...knowledge, statement: limitText(knowledge.statement, options.maxChars), supportingEvidence: knowledge.supportingEvidence.map((value) => limitText(value, options.maxChars)) },
217
+ score,
218
+ matchedTerms,
219
+ matchedFields,
220
+ why: [...matchedFields.map((field) => `matched ${field}`), ...(entry.feedback.usedCount > 0 ? ['prior recorded use'] : [])],
221
+ })),
222
+ duplicateIds: [],
223
+ possibleConflicts: [],
224
+ },
225
+ exclusions,
226
+ };
227
+ }
228
+ export function discoverV2Deadends(index, query, current, options) {
229
+ const exclusions = emptyExclusions();
230
+ const terms = queryTerms(query);
231
+ const hits = Object.values(index.entries).flatMap((entry) => {
232
+ if (entry.entity !== 'deadend')
233
+ return [];
234
+ if (entry.status !== 'verified') {
235
+ exclusions.status_not_verified += 1;
236
+ return [];
237
+ }
238
+ if (!applicable(entry, current)) {
239
+ exclusions.applicability_mismatch += 1;
240
+ return [];
241
+ }
242
+ const deadend = entry.record;
243
+ if (options.scope && normalized(deadend.scope) !== normalized(options.scope)) {
244
+ exclusions.scope_mismatch += 1;
245
+ return [];
246
+ }
247
+ const matched = match(entry, terms, 'deadend');
248
+ if (matched.matchedTerms.length === 0) {
249
+ exclusions.query_no_match += 1;
250
+ return [];
251
+ }
252
+ return [{ entry, deadend, ...matched }];
253
+ }).sort((left, right) => right.score - left.score
254
+ || Math.min(right.entry.feedback.usedCount, 5) - Math.min(left.entry.feedback.usedCount, 5)
255
+ || right.entry.provenance.occurredAt.localeCompare(left.entry.provenance.occurredAt)
256
+ || left.deadend.deadendId.localeCompare(right.deadend.deadendId));
257
+ const selected = hits.slice(0, options.limit);
258
+ exclusions.result_limit = hits.length - selected.length;
259
+ return {
260
+ result: {
261
+ query,
262
+ total: hits.length,
263
+ truncated: hits.length > selected.length,
264
+ hits: selected.map(({ deadend, score, matchedTerms, matchedFields }) => ({
265
+ deadend: { ...deadend, attempt: limitText(deadend.attempt, options.maxChars), reason: limitText(deadend.reason, options.maxChars), observedEvidence: deadend.observedEvidence.map((value) => limitText(value, options.maxChars)) },
266
+ score,
267
+ matchedTerms,
268
+ matchedFields,
269
+ })),
270
+ },
271
+ exclusions,
272
+ };
273
+ }
@@ -3,10 +3,29 @@ export interface RuntimeServer {
3
3
  lease: RuntimeLease;
4
4
  close(reason?: string): Promise<void>;
5
5
  }
6
- export declare function serveRuntime(startPath?: string): Promise<RuntimeServer>;
6
+ interface RuntimeServeOptions {
7
+ inactivityTimeoutMs?: number;
8
+ heartbeatIntervalMs?: number;
9
+ pollIntervalMs?: number;
10
+ }
11
+ export interface EnsureRuntimeOptions {
12
+ source?: 'claude-hook' | 'mcp' | 'cli';
13
+ timeoutMs?: number;
14
+ }
15
+ export interface EnsureRuntimeResult {
16
+ projectRoot: string;
17
+ started: boolean;
18
+ running: boolean;
19
+ skipped: 'disabled' | 'not-migrated' | 'already-running' | 'startup-in-progress' | 'startup-failed' | 'unavailable' | null;
20
+ }
21
+ export declare function serveRuntime(startPath?: string, options?: RuntimeServeOptions): Promise<RuntimeServer>;
7
22
  export declare function runtimeStatus(startPath?: string): Promise<RuntimeStatus>;
8
23
  export declare function enableRuntime(startPath?: string): Promise<RuntimeStatus>;
9
24
  export declare function disableRuntime(startPath?: string): Promise<RuntimeStatus>;
25
+ export declare function setFilesystemEvents(startPath: string | undefined, enabled: boolean): Promise<RuntimeStatus>;
10
26
  export declare function startRuntime(startPath?: string): Promise<RuntimeStatus>;
27
+ export declare function ensureRuntimeForSession(startPath?: string, options?: EnsureRuntimeOptions): Promise<EnsureRuntimeResult>;
11
28
  export declare function stopRuntime(startPath?: string): Promise<RuntimeStatus>;
12
29
  export declare function observeRuntime(startPath?: string): Promise<boolean>;
30
+ export declare function touchRuntime(startPath?: string): Promise<boolean>;
31
+ export {};