fluffy-context 0.1.0 → 0.3.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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -136
  3. package/dist/src/agent/api.d.ts +8 -0
  4. package/dist/src/agent/api.js +285 -0
  5. package/dist/src/agent/index.d.ts +3 -0
  6. package/dist/src/agent/index.js +1 -0
  7. package/dist/src/agent/types.d.ts +50 -0
  8. package/dist/src/agent/types.js +1 -0
  9. package/dist/src/capture/context-filter.d.ts +7 -0
  10. package/dist/src/cli/main.d.ts +2 -0
  11. package/dist/src/cli/main.js +382 -30
  12. package/dist/src/git/git-adapter.d.ts +2 -0
  13. package/dist/src/hooks/claude-code.d.ts +1 -0
  14. package/dist/src/hooks/claude-code.js +84 -0
  15. package/dist/src/integrations/claude-code.d.ts +19 -0
  16. package/dist/src/integrations/claude-code.js +122 -0
  17. package/dist/src/mcp/main.d.ts +1 -0
  18. package/dist/src/mcp/main.js +5 -0
  19. package/dist/src/mcp/server.d.ts +2 -0
  20. package/dist/src/mcp/server.js +40 -0
  21. package/dist/src/project/project-resolver.d.ts +1 -0
  22. package/dist/src/runtime/diagnostics.d.ts +6 -0
  23. package/dist/src/runtime/diagnostics.js +34 -5
  24. package/dist/src/runtime/init.d.ts +7 -0
  25. package/dist/src/runtime/knowledge.d.ts +9 -0
  26. package/dist/src/runtime/knowledge.js +179 -12
  27. package/dist/src/runtime/notes.d.ts +6 -0
  28. package/dist/src/runtime/notes.js +173 -0
  29. package/dist/src/runtime/runtime.d.ts +11 -0
  30. package/dist/src/runtime/runtime.js +48 -18
  31. package/dist/src/runtime/types.d.ts +294 -0
  32. package/dist/src/storage/atomic-write.d.ts +1 -0
  33. package/dist/src/storage/json-store.d.ts +6 -0
  34. package/dist/src/storage/layout.d.ts +13 -0
  35. package/dist/src/storage/layout.js +3 -0
  36. package/dist/src/storage/lock.d.ts +1 -0
  37. package/dist/src/version.d.ts +1 -0
  38. package/dist/src/version.js +1 -0
  39. package/package.json +48 -11
  40. package/skills/fluffy-context/SKILL.md +345 -0
@@ -0,0 +1,173 @@
1
+ import crypto from 'node:crypto';
2
+ import { access, readdir } from 'node:fs/promises';
3
+ import { resolveProjectRoot } from '../project/project-resolver.js';
4
+ import { filterPaths } from '../capture/context-filter.js';
5
+ import { atomicWriteJson } from '../storage/atomic-write.js';
6
+ import { isRecord, readJson } from '../storage/json-store.js';
7
+ import { contextMetadataPath, contextsRoot, locksDirectory, notesPath, snapshotPath, snapshotsDirectory } from '../storage/layout.js';
8
+ import { withLock } from '../storage/lock.js';
9
+ function id() {
10
+ return `note-${Date.now().toString(36)}-${crypto.randomBytes(4).toString('hex')}`;
11
+ }
12
+ function isNotFound(error) {
13
+ return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
14
+ }
15
+ function isNote(value) {
16
+ return isRecord(value)
17
+ && value.schemaVersion === 1
18
+ && typeof value.noteId === 'string'
19
+ && ['problem', 'action', 'observation', 'decision'].includes(value.kind)
20
+ && typeof value.message === 'string'
21
+ && value.message.trim().length > 0
22
+ && ['agent', 'human'].includes(value.actor)
23
+ && (typeof value.contextId === 'string' || value.contextId === null)
24
+ && (typeof value.snapshotId === 'string' || value.snapshotId === null)
25
+ && Array.isArray(value.relatedFiles) && value.relatedFiles.every((item) => typeof item === 'string')
26
+ && ['open', 'resolved'].includes(value.status)
27
+ && (typeof value.absorbedBySnapshotId === 'string' || value.absorbedBySnapshotId === null)
28
+ && typeof value.createdAt === 'string'
29
+ && typeof value.updatedAt === 'string';
30
+ }
31
+ function isNoteFile(value) {
32
+ return isRecord(value) && value.schemaVersion === 1 && Array.isArray(value.items) && value.items.every(isNote);
33
+ }
34
+ async function loadNotes(projectRoot) {
35
+ try {
36
+ return await readJson(notesPath(projectRoot), isNoteFile);
37
+ }
38
+ catch (error) {
39
+ if (isNotFound(error))
40
+ return { schemaVersion: 1, items: [] };
41
+ throw error;
42
+ }
43
+ }
44
+ async function validateSource(projectRoot, contextId, snapshotId) {
45
+ if (!contextId && snapshotId)
46
+ throw new Error('source snapshot requires source context');
47
+ if (!contextId)
48
+ return;
49
+ await access(contextMetadataPath(projectRoot, contextId));
50
+ if (snapshotId)
51
+ await access(snapshotPath(projectRoot, contextId, snapshotId));
52
+ }
53
+ function limited(value, budget) {
54
+ return value.length <= budget ? value : value.slice(0, Math.max(0, budget - 1)) + '…';
55
+ }
56
+ export async function addNote(startPath, message, input = {}) {
57
+ const projectRoot = await resolveProjectRoot(startPath);
58
+ const normalizedMessage = message.trim();
59
+ if (!normalizedMessage)
60
+ throw new Error('note message must not be empty');
61
+ await validateSource(projectRoot, input.contextId, input.snapshotId);
62
+ const relatedFiles = await filterPaths(projectRoot, (input.relatedFiles ?? []).map((item) => item.trim()).filter(Boolean));
63
+ return withLock(`${locksDirectory(projectRoot)}/notes.lock`, async () => {
64
+ const file = await loadNotes(projectRoot);
65
+ const now = new Date().toISOString();
66
+ const note = {
67
+ noteId: id(),
68
+ schemaVersion: 1,
69
+ kind: input.kind ?? 'observation',
70
+ message: normalizedMessage,
71
+ actor: input.actor ?? 'agent',
72
+ contextId: input.contextId ?? null,
73
+ snapshotId: input.snapshotId ?? null,
74
+ relatedFiles,
75
+ status: input.status ?? 'open',
76
+ absorbedBySnapshotId: null,
77
+ createdAt: now,
78
+ updatedAt: now,
79
+ };
80
+ await atomicWriteJson(notesPath(projectRoot), { schemaVersion: 1, items: [...file.items, note] });
81
+ return note;
82
+ });
83
+ }
84
+ export async function listNotes(startPath, query = {}) {
85
+ const projectRoot = await resolveProjectRoot(startPath);
86
+ const file = await loadNotes(projectRoot);
87
+ const limit = query.limit ?? 50;
88
+ const maxChars = query.maxChars ?? 500;
89
+ return file.items
90
+ .filter((note) => !query.contextId || note.contextId === query.contextId)
91
+ .filter((note) => !query.openOnly || (note.status === 'open' && note.absorbedBySnapshotId === null))
92
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))
93
+ .slice(0, limit)
94
+ .map((note) => ({ ...note, message: limited(note.message, maxChars) }));
95
+ }
96
+ function noteActivity(note) {
97
+ if (!note.contextId)
98
+ return null;
99
+ return {
100
+ type: 'note',
101
+ occurredAt: note.createdAt,
102
+ contextId: note.contextId,
103
+ snapshotId: note.snapshotId ?? undefined,
104
+ noteId: note.noteId,
105
+ kind: note.kind,
106
+ message: note.message,
107
+ actor: note.actor,
108
+ status: note.status,
109
+ absorbedBySnapshotId: note.absorbedBySnapshotId,
110
+ };
111
+ }
112
+ async function checkpointActivities(projectRoot, contextId) {
113
+ const items = [];
114
+ let contextIds;
115
+ try {
116
+ contextIds = await readdir(contextsRoot(projectRoot));
117
+ }
118
+ catch {
119
+ contextIds = [];
120
+ }
121
+ for (const id of contextIds) {
122
+ if (contextId && id !== contextId)
123
+ continue;
124
+ try {
125
+ const context = await readJson(contextMetadataPath(projectRoot, id), (value) => isRecord(value) && typeof value.id === 'string' && typeof value.title === 'string' && (typeof value.currentSnapshotId === 'string' || value.currentSnapshotId === null));
126
+ if (!context.currentSnapshotId)
127
+ continue;
128
+ const snapshots = await readdir(snapshotsDirectory(projectRoot, id));
129
+ for (const fileName of snapshots.filter((name) => name.endsWith('.json'))) {
130
+ const snapshot = await readJson(snapshotPath(projectRoot, id, fileName.slice(0, -5)), (value) => isRecord(value) && value.schemaVersion === 1 && typeof value.snapshotId === 'string' && typeof value.createdAt === 'string' && (value.mode === 'baseline' || value.mode === 'patch'));
131
+ items.push({ type: 'checkpoint', occurredAt: snapshot.createdAt, contextId: id, snapshotId: snapshot.snapshotId, title: context.title });
132
+ }
133
+ }
134
+ catch {
135
+ // Doctor reports malformed activity sources.
136
+ }
137
+ }
138
+ return items;
139
+ }
140
+ export async function listActivity(startPath, query = {}) {
141
+ const projectRoot = await resolveProjectRoot(startPath);
142
+ const notes = await loadNotes(projectRoot);
143
+ const noteItems = notes.items.map(noteActivity).filter((item) => item !== null);
144
+ const checkpointItems = await checkpointActivities(projectRoot, query.contextId);
145
+ const maxChars = query.maxChars ?? 500;
146
+ const items = [...noteItems, ...checkpointItems]
147
+ .filter((item) => !query.contextId || item.contextId === query.contextId)
148
+ .filter((item) => !query.since || item.occurredAt >= query.since)
149
+ .filter((item) => !query.openOnly || (item.type === 'note' && item.status === 'open' && item.absorbedBySnapshotId === null))
150
+ .sort((left, right) => right.occurredAt.localeCompare(left.occurredAt))
151
+ .slice(0, query.limit ?? 50)
152
+ .map((item) => item.message ? { ...item, message: limited(item.message, maxChars) } : item);
153
+ return { projectRoot, items };
154
+ }
155
+ export async function absorbNotes(startPath, contextId, snapshotId) {
156
+ const projectRoot = await resolveProjectRoot(startPath);
157
+ return withLock(`${locksDirectory(projectRoot)}/notes.lock`, async () => {
158
+ const file = await loadNotes(projectRoot);
159
+ let absorbed = 0;
160
+ const items = file.items.map((note) => {
161
+ if (note.contextId !== contextId || note.absorbedBySnapshotId !== null)
162
+ return note;
163
+ absorbed += 1;
164
+ return { ...note, absorbedBySnapshotId: snapshotId, updatedAt: new Date().toISOString() };
165
+ });
166
+ if (absorbed > 0)
167
+ await atomicWriteJson(notesPath(projectRoot), { schemaVersion: 1, items });
168
+ return absorbed;
169
+ });
170
+ }
171
+ export function isValidNote(value) {
172
+ return isNote(value);
173
+ }
@@ -0,0 +1,11 @@
1
+ import type { CheckpointInput, CheckpointResult, IndexFile, ResumeResult, StructuredContext } from './types.js';
2
+ export interface CheckpointOptions {
3
+ minSaveIntervalMs?: number;
4
+ }
5
+ export declare function rebuildSnapshot(projectRoot: string, contextId: string, snapshotId: string, seen?: Set<string>): Promise<StructuredContext>;
6
+ export declare function rebuildIndex(projectRoot: string, persist?: boolean): Promise<IndexFile>;
7
+ export declare function checkpoint(startPath: string | undefined, input: CheckpointInput, options?: CheckpointOptions): Promise<CheckpointResult>;
8
+ export interface ResumeOptions {
9
+ touchLastUsedAt?: boolean;
10
+ }
11
+ export declare function resume(startPath?: string, contextId?: string, maxChars?: number, options?: ResumeOptions): Promise<ResumeResult>;
@@ -8,6 +8,7 @@ import { withLock } from '../storage/lock.js';
8
8
  import { contextDirectory, contextMetadataPath, contextsRoot, indexPath, locksDirectory, snapshotPath, snapshotsDirectory, } from '../storage/layout.js';
9
9
  import { loadManifest } from './init.js';
10
10
  import { filterPaths } from '../capture/context-filter.js';
11
+ import { absorbNotes } from './notes.js';
11
12
  const emptyContext = () => ({
12
13
  progressSummary: '',
13
14
  lastError: null,
@@ -46,6 +47,29 @@ function limitedList(values, budget) {
46
47
  function applyPatch(base, patch) {
47
48
  return { ...base, ...patch };
48
49
  }
50
+ function isContextPatch(value) {
51
+ if (!isRecord(value))
52
+ return false;
53
+ const fields = ['progressSummary', 'lastError', 'completed', 'pendingTasks', 'decisions', 'risks', 'relatedFiles'];
54
+ if (Object.keys(value).some((key) => !fields.includes(key)))
55
+ return false;
56
+ return fields.every((field) => {
57
+ if (!(field in value))
58
+ return true;
59
+ const fieldValue = value[field];
60
+ if (field === 'lastError')
61
+ return typeof fieldValue === 'string' || fieldValue === null;
62
+ if (field === 'progressSummary')
63
+ return typeof fieldValue === 'string';
64
+ return isStringArray(fieldValue);
65
+ });
66
+ }
67
+ function normalizeText(value) {
68
+ return value.trim();
69
+ }
70
+ function normalizeList(values) {
71
+ return values.map(normalizeText).filter(Boolean);
72
+ }
49
73
  function diffContext(previous, current) {
50
74
  const patch = {};
51
75
  if (previous.progressSummary !== current.progressSummary)
@@ -65,16 +89,17 @@ function diffContext(previous, current) {
65
89
  return patch;
66
90
  }
67
91
  async function contextFromInput(projectRoot, input, previous) {
68
- const relatedFiles = await filterPaths(projectRoot, input.relatedFiles ?? previous?.relatedFiles ?? []);
69
- return {
70
- progressSummary: input.progressSummary ?? previous?.progressSummary ?? '',
71
- lastError: input.lastError === undefined ? previous?.lastError ?? null : input.lastError,
72
- completed: input.completed ?? previous?.completed ?? [],
73
- pendingTasks: input.pendingTasks ?? previous?.pendingTasks ?? [],
74
- decisions: input.decisions ?? previous?.decisions ?? [],
75
- risks: input.risks ?? previous?.risks ?? [],
76
- relatedFiles,
77
- };
92
+ const progressSummary = input.progressSummary === undefined ? previous?.progressSummary ?? '' : normalizeText(input.progressSummary);
93
+ const lastError = input.lastError === undefined
94
+ ? previous?.lastError ?? null
95
+ : normalizeText(input.lastError ?? '') || null;
96
+ const completed = input.completed === undefined ? previous?.completed ?? [] : normalizeList(input.completed);
97
+ const pendingTasks = input.pendingTasks === undefined ? previous?.pendingTasks ?? [] : normalizeList(input.pendingTasks);
98
+ const decisions = input.decisions === undefined ? previous?.decisions ?? [] : normalizeList(input.decisions);
99
+ const risks = input.risks === undefined ? previous?.risks ?? [] : normalizeList(input.risks);
100
+ const relatedFilesInput = input.relatedFiles === undefined ? previous?.relatedFiles ?? [] : normalizeList(input.relatedFiles);
101
+ const relatedFiles = await filterPaths(projectRoot, relatedFilesInput);
102
+ return { progressSummary, lastError, completed, pendingTasks, decisions, risks, relatedFiles };
78
103
  }
79
104
  function isStructuredContext(value) {
80
105
  return isRecord(value)
@@ -126,7 +151,7 @@ async function loadSnapshot(projectRoot, contextId, snapshotId) {
126
151
  }
127
152
  return snapshot;
128
153
  }
129
- async function rebuildSnapshot(projectRoot, contextId, snapshotId, seen = new Set()) {
154
+ export async function rebuildSnapshot(projectRoot, contextId, snapshotId, seen = new Set()) {
130
155
  if (seen.has(snapshotId))
131
156
  throw new Error(`snapshot cycle detected at ${snapshotId}`);
132
157
  seen.add(snapshotId);
@@ -139,7 +164,7 @@ async function rebuildSnapshot(projectRoot, contextId, snapshotId, seen = new Se
139
164
  content = snapshot.content;
140
165
  }
141
166
  else {
142
- if (!snapshot.parentSnapshotId || !snapshot.patch || !isRecord(snapshot.patch))
167
+ if (!snapshot.parentSnapshotId || !snapshot.patch || !isContextPatch(snapshot.patch))
143
168
  throw new Error(`incomplete patch snapshot ${snapshotId}`);
144
169
  const parentSnapshot = await loadSnapshot(projectRoot, contextId, snapshot.parentSnapshotId);
145
170
  if (parentSnapshot.baseSnapshotId !== snapshot.baseSnapshotId)
@@ -151,7 +176,7 @@ async function rebuildSnapshot(projectRoot, contextId, snapshotId, seen = new Se
151
176
  }
152
177
  return content;
153
178
  }
154
- export async function rebuildIndex(projectRoot) {
179
+ export async function rebuildIndex(projectRoot, persist = true) {
155
180
  const contexts = [];
156
181
  let entries = [];
157
182
  try {
@@ -178,7 +203,8 @@ export async function rebuildIndex(projectRoot) {
178
203
  }
179
204
  }
180
205
  const index = { schemaVersion: 1, rebuiltAt: new Date().toISOString(), contexts };
181
- await atomicWriteJson(indexPath(projectRoot), index);
206
+ if (persist)
207
+ await atomicWriteJson(indexPath(projectRoot), index);
182
208
  return index;
183
209
  }
184
210
  async function updateIndex(projectRoot, entry) {
@@ -279,14 +305,16 @@ export async function checkpoint(startPath, input, options = {}) {
279
305
  currentSnapshotId: snapshotId,
280
306
  updatedAt: now,
281
307
  });
308
+ if (input.absorbNotes)
309
+ await absorbNotes(projectRoot, contextId, snapshotId);
282
310
  return { status: 'saved', context: updatedContext, snapshot };
283
311
  });
284
312
  }
285
- export async function resume(startPath, contextId, maxChars = 4000) {
313
+ export async function resume(startPath, contextId, maxChars = 4000, options = {}) {
286
314
  const projectRoot = await resolveProjectRoot(startPath);
287
315
  await loadManifest(projectRoot);
288
316
  const git = await readGitState(projectRoot);
289
- const index = await rebuildIndex(projectRoot);
317
+ const index = await rebuildIndex(projectRoot, options.touchLastUsedAt !== false);
290
318
  const candidates = index.contexts
291
319
  .filter((entry) => !contextId || entry.contextId === contextId)
292
320
  .filter((entry) => ['active', 'stable'].includes(entry.status))
@@ -300,8 +328,10 @@ export async function resume(startPath, contextId, maxChars = 4000) {
300
328
  const snapshot = await loadSnapshot(projectRoot, candidate.contextId, context.currentSnapshotId);
301
329
  const content = await rebuildSnapshot(projectRoot, candidate.contextId, snapshot.snapshotId);
302
330
  const drift = context.branch !== git.branch || context.commit !== git.commit;
303
- context.lastUsedAt = new Date().toISOString();
304
- await atomicWriteJson(contextMetadataPath(projectRoot, context.id), context);
331
+ if (options.touchLastUsedAt !== false) {
332
+ context.lastUsedAt = new Date().toISOString();
333
+ await atomicWriteJson(contextMetadataPath(projectRoot, context.id), context);
334
+ }
305
335
  const budget = { remaining: Math.max(0, maxChars) };
306
336
  const resumeSummary = {
307
337
  progressSummary: limited(content.progressSummary, budget.remaining),
@@ -0,0 +1,294 @@
1
+ export type ContextStatus = 'draft' | 'active' | 'stable' | 'archived' | 'deleted';
2
+ export type SnapshotMode = 'baseline' | 'patch';
3
+ export interface GitState {
4
+ root: string | null;
5
+ branch: string | null;
6
+ commit: string | null;
7
+ }
8
+ export interface ContextMetadata {
9
+ id: string;
10
+ schemaVersion: 1;
11
+ title: string;
12
+ projectRoot: string;
13
+ branch: string | null;
14
+ commit: string | null;
15
+ createdAt: string;
16
+ updatedAt: string;
17
+ lastUsedAt: string | null;
18
+ usageCount: number;
19
+ status: ContextStatus;
20
+ currentSnapshotId: string | null;
21
+ tags: string[];
22
+ dependencies: string[];
23
+ }
24
+ export interface StructuredContext {
25
+ progressSummary: string;
26
+ lastError: string | null;
27
+ completed: string[];
28
+ pendingTasks: string[];
29
+ decisions: string[];
30
+ risks: string[];
31
+ relatedFiles: string[];
32
+ }
33
+ export type ContextPatch = Partial<StructuredContext>;
34
+ export interface Snapshot {
35
+ snapshotId: string;
36
+ schemaVersion: 1;
37
+ contextId: string;
38
+ parentSnapshotId: string | null;
39
+ baseSnapshotId: string;
40
+ mode: SnapshotMode;
41
+ createdAt: string;
42
+ branch: string | null;
43
+ commit: string | null;
44
+ contentHash: string;
45
+ rebuildHash: string;
46
+ content?: StructuredContext;
47
+ patch?: ContextPatch;
48
+ }
49
+ export interface Manifest {
50
+ schemaVersion: 1;
51
+ layoutVersion: 'compact-json-v1';
52
+ projectId: string;
53
+ projectRoot: string;
54
+ initializedAt: string;
55
+ }
56
+ export interface IndexEntry {
57
+ contextId: string;
58
+ title: string;
59
+ status: ContextStatus;
60
+ branch: string | null;
61
+ commit: string | null;
62
+ currentSnapshotId: string | null;
63
+ updatedAt: string;
64
+ }
65
+ export interface IndexFile {
66
+ schemaVersion: 1;
67
+ rebuiltAt: string;
68
+ contexts: IndexEntry[];
69
+ }
70
+ export interface CheckpointInput {
71
+ contextId?: string;
72
+ absorbNotes?: boolean;
73
+ title?: string;
74
+ progressSummary?: string;
75
+ lastError?: string | null;
76
+ completed?: string[];
77
+ pendingTasks?: string[];
78
+ decisions?: string[];
79
+ risks?: string[];
80
+ relatedFiles?: string[];
81
+ }
82
+ export type NoteKind = 'problem' | 'action' | 'observation' | 'decision';
83
+ export type NoteStatus = 'open' | 'resolved';
84
+ export type NoteActor = 'agent' | 'human';
85
+ export interface Note {
86
+ noteId: string;
87
+ schemaVersion: 1;
88
+ kind: NoteKind;
89
+ message: string;
90
+ actor: NoteActor;
91
+ contextId: string | null;
92
+ snapshotId: string | null;
93
+ relatedFiles: string[];
94
+ status: NoteStatus;
95
+ absorbedBySnapshotId: string | null;
96
+ createdAt: string;
97
+ updatedAt: string;
98
+ }
99
+ export interface NoteInput {
100
+ kind?: NoteKind;
101
+ actor?: NoteActor;
102
+ contextId?: string;
103
+ snapshotId?: string;
104
+ relatedFiles?: string[];
105
+ status?: NoteStatus;
106
+ }
107
+ export interface NoteFile {
108
+ schemaVersion: 1;
109
+ items: Note[];
110
+ }
111
+ export interface NoteQuery {
112
+ contextId?: string;
113
+ openOnly?: boolean;
114
+ limit?: number;
115
+ maxChars?: number;
116
+ }
117
+ export interface ActivityItem {
118
+ type: 'note' | 'checkpoint';
119
+ occurredAt: string;
120
+ contextId: string;
121
+ snapshotId?: string;
122
+ noteId?: string;
123
+ kind?: NoteKind;
124
+ message?: string;
125
+ actor?: NoteActor;
126
+ status?: NoteStatus;
127
+ absorbedBySnapshotId?: string | null;
128
+ title?: string;
129
+ }
130
+ export interface ActivityQuery extends NoteQuery {
131
+ since?: string;
132
+ }
133
+ export interface ActivityResult {
134
+ projectRoot: string;
135
+ items: ActivityItem[];
136
+ }
137
+ export type KnowledgeStatus = 'candidate' | 'verified' | 'deprecated' | 'rejected';
138
+ export type DeadendStatus = 'candidate' | 'verified' | 'obsolete' | 'rejected';
139
+ export interface Knowledge {
140
+ knowledgeId: string;
141
+ kind: string;
142
+ statement: string;
143
+ scope: string;
144
+ confidence: number;
145
+ status: KnowledgeStatus;
146
+ sourceContextIds: string[];
147
+ sourceSnapshotIds: string[];
148
+ supportingEvidence: string[];
149
+ createdAt: string;
150
+ updatedAt: string;
151
+ }
152
+ export interface KnowledgeInput {
153
+ kind?: string;
154
+ scope?: string;
155
+ sourceContextId?: string;
156
+ sourceSnapshotId?: string;
157
+ evidence?: string[];
158
+ }
159
+ export interface KnowledgeDiscoverOptions {
160
+ scope?: string;
161
+ statuses?: KnowledgeStatus[];
162
+ limit?: number;
163
+ maxChars?: number;
164
+ }
165
+ export interface KnowledgeDiscoveryHit {
166
+ knowledge: Knowledge;
167
+ score: number;
168
+ matchedTerms: string[];
169
+ matchedFields: string[];
170
+ why: string[];
171
+ }
172
+ export interface KnowledgeConflictGroup {
173
+ scope: string;
174
+ knowledgeIds: string[];
175
+ statements: string[];
176
+ }
177
+ export interface KnowledgeDiscoveryResult {
178
+ query: string;
179
+ total: number;
180
+ truncated: boolean;
181
+ hits: KnowledgeDiscoveryHit[];
182
+ duplicateIds: string[][];
183
+ possibleConflicts: KnowledgeConflictGroup[];
184
+ }
185
+ export interface DeadendDiscoverOptions {
186
+ scope?: string;
187
+ statuses?: DeadendStatus[];
188
+ limit?: number;
189
+ maxChars?: number;
190
+ }
191
+ export interface DeadendDiscoveryHit {
192
+ deadend: Deadend;
193
+ score: number;
194
+ matchedTerms: string[];
195
+ matchedFields: string[];
196
+ }
197
+ export interface DeadendDiscoveryResult {
198
+ query: string;
199
+ total: number;
200
+ truncated: boolean;
201
+ hits: DeadendDiscoveryHit[];
202
+ }
203
+ export interface DeadendInput {
204
+ scope?: string;
205
+ sourceContextId?: string;
206
+ sourceSnapshotId?: string;
207
+ evidence?: string[];
208
+ }
209
+ export interface Deadend {
210
+ deadendId: string;
211
+ attempt: string;
212
+ reason: string;
213
+ observedEvidence: string[];
214
+ scope: string;
215
+ status: DeadendStatus;
216
+ sourceContextId: string | null;
217
+ sourceSnapshotId: string | null;
218
+ createdAt: string;
219
+ updatedAt: string;
220
+ }
221
+ export interface KnowledgeFile {
222
+ schemaVersion: 1;
223
+ items: Knowledge[];
224
+ }
225
+ export interface DeadendFile {
226
+ schemaVersion: 1;
227
+ items: Deadend[];
228
+ }
229
+ export interface ResumeSummary extends StructuredContext {
230
+ branchOrCommitDrift: boolean;
231
+ }
232
+ export interface ResumeResult {
233
+ context: ContextMetadata;
234
+ snapshot: Snapshot;
235
+ resumeSummary: ResumeSummary;
236
+ details: StructuredContext;
237
+ }
238
+ export interface AgentLoadOptions {
239
+ contextId?: string;
240
+ maxChars?: number;
241
+ includeDetails?: boolean;
242
+ query?: string;
243
+ scope?: string;
244
+ }
245
+ export interface AgentLoadResult {
246
+ projectRoot: string;
247
+ context: ContextMetadata;
248
+ snapshot: Snapshot;
249
+ resumeSummary: ResumeSummary;
250
+ details?: StructuredContext;
251
+ knowledge: KnowledgeDiscoveryResult | null;
252
+ verifiedDeadendIds: string[];
253
+ }
254
+ export interface AgentSaveResult {
255
+ projectRoot: string;
256
+ result: CheckpointResult;
257
+ }
258
+ export interface ContextSearchOptions {
259
+ contextId?: string;
260
+ scope?: string;
261
+ limit?: number;
262
+ maxChars?: number;
263
+ }
264
+ export interface ContextSearchHit {
265
+ context: ContextMetadata;
266
+ snapshotId: string;
267
+ score: number;
268
+ matchedTerms: string[];
269
+ matchedFields: string[];
270
+ summary: ResumeSummary;
271
+ }
272
+ export interface ContextSearchResult {
273
+ projectRoot: string;
274
+ query: string;
275
+ total: number;
276
+ truncated: boolean;
277
+ hits: ContextSearchHit[];
278
+ knowledge: KnowledgeDiscoveryResult;
279
+ verifiedDeadendIds: string[];
280
+ }
281
+ export type CheckpointResult = {
282
+ status: 'saved';
283
+ context: ContextMetadata;
284
+ snapshot: Snapshot;
285
+ } | {
286
+ status: 'no_change';
287
+ context: ContextMetadata;
288
+ snapshot: Snapshot;
289
+ } | {
290
+ status: 'rate_limited';
291
+ context: ContextMetadata;
292
+ snapshot: Snapshot;
293
+ retryAt: string;
294
+ };
@@ -0,0 +1 @@
1
+ export declare function atomicWriteJson(filePath: string, value: unknown): Promise<void>;
@@ -0,0 +1,6 @@
1
+ export declare class JsonStoreError extends Error {
2
+ readonly filePath: string;
3
+ constructor(filePath: string, message: string);
4
+ }
5
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
6
+ export declare function readJson<T>(filePath: string, validate?: (value: unknown) => value is T): Promise<T>;
@@ -0,0 +1,13 @@
1
+ export declare function contextRoot(projectRoot: string): string;
2
+ export declare function manifestPath(projectRoot: string): string;
3
+ export declare function indexPath(projectRoot: string): string;
4
+ export declare function ignoredPath(projectRoot: string): string;
5
+ export declare function contextsRoot(projectRoot: string): string;
6
+ export declare function contextDirectory(projectRoot: string, contextId: string): string;
7
+ export declare function contextMetadataPath(projectRoot: string, contextId: string): string;
8
+ export declare function snapshotsDirectory(projectRoot: string, contextId: string): string;
9
+ export declare function snapshotPath(projectRoot: string, contextId: string, snapshotId: string): string;
10
+ export declare function locksDirectory(projectRoot: string): string;
11
+ export declare function knowledgePath(projectRoot: string): string;
12
+ export declare function deadendsPath(projectRoot: string): string;
13
+ export declare function notesPath(projectRoot: string): string;
@@ -35,3 +35,6 @@ export function knowledgePath(projectRoot) {
35
35
  export function deadendsPath(projectRoot) {
36
36
  return path.join(contextRoot(projectRoot), 'deadends.json');
37
37
  }
38
+ export function notesPath(projectRoot) {
39
+ return path.join(contextRoot(projectRoot), 'notes.json');
40
+ }
@@ -0,0 +1 @@
1
+ export declare function withLock<T>(lockPath: string, operation: () => Promise<T>): Promise<T>;
@@ -0,0 +1 @@
1
+ export declare const VERSION = "0.3.0";
@@ -0,0 +1 @@
1
+ export const VERSION = '0.3.0';