fluffy-context 0.7.7 → 1.0.0-beta.1

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 (53) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +110 -553
  3. package/ROADMAP-1.0.md +70 -0
  4. package/dist/src/agent/api.d.ts +41 -0
  5. package/dist/src/agent/api.js +37 -0
  6. package/dist/src/agent/index.d.ts +9 -1
  7. package/dist/src/agent/index.js +3 -0
  8. package/dist/src/capture/context-filter.js +2 -0
  9. package/dist/src/cli/main.js +293 -15
  10. package/dist/src/cognition/admission.d.ts +8 -0
  11. package/dist/src/cognition/admission.js +24 -0
  12. package/dist/src/cognition/evidence-graph.d.ts +4 -0
  13. package/dist/src/cognition/evidence-graph.js +187 -0
  14. package/dist/src/cognition/evolution.d.ts +36 -0
  15. package/dist/src/cognition/evolution.js +221 -0
  16. package/dist/src/cognition/import-memory.d.ts +18 -0
  17. package/dist/src/cognition/import-memory.js +124 -0
  18. package/dist/src/cognition/journal-segments.d.ts +11 -0
  19. package/dist/src/cognition/journal-segments.js +129 -0
  20. package/dist/src/cognition/journal.d.ts +13 -0
  21. package/dist/src/cognition/journal.js +186 -20
  22. package/dist/src/cognition/migration-v1.js +39 -3
  23. package/dist/src/cognition/projections.js +124 -2
  24. package/dist/src/cognition/recipes.d.ts +25 -0
  25. package/dist/src/cognition/recipes.js +149 -0
  26. package/dist/src/cognition/runtime.js +4 -0
  27. package/dist/src/cognition/skills.d.ts +45 -0
  28. package/dist/src/cognition/skills.js +184 -0
  29. package/dist/src/cognition/types.d.ts +181 -1
  30. package/dist/src/cognition/usage-report.js +30 -15
  31. package/dist/src/compiler/compile.js +101 -31
  32. package/dist/src/compiler/graph.d.ts +7 -0
  33. package/dist/src/compiler/graph.js +117 -0
  34. package/dist/src/compiler/identity.d.ts +1 -1
  35. package/dist/src/compiler/identity.js +2 -0
  36. package/dist/src/compiler/inject.d.ts +23 -0
  37. package/dist/src/compiler/inject.js +27 -0
  38. package/dist/src/compiler/sources.d.ts +3 -1
  39. package/dist/src/compiler/sources.js +10 -1
  40. package/dist/src/compiler/types.d.ts +22 -1
  41. package/dist/src/hooks/claude-code.js +17 -56
  42. package/dist/src/integrations/claude-code.js +30 -8
  43. package/dist/src/mcp/server.js +228 -1
  44. package/dist/src/runtime/knowledge.js +57 -8
  45. package/dist/src/runtime/setup.d.ts +27 -0
  46. package/dist/src/runtime/setup.js +85 -0
  47. package/dist/src/storage/layout.d.ts +5 -0
  48. package/dist/src/storage/layout.js +15 -0
  49. package/dist/src/storage/lock.js +19 -2
  50. package/dist/src/version.d.ts +1 -1
  51. package/dist/src/version.js +1 -1
  52. package/package.json +4 -1
  53. package/skills/fluffy-context/SKILL.md +46 -8
@@ -0,0 +1,187 @@
1
+ import crypto from 'node:crypto';
2
+ export const EVIDENCE_GRAPH_DERIVATION_VERSION = 'evidence-graph-v1';
3
+ function hash(value) {
4
+ return crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
5
+ }
6
+ function record(event) {
7
+ const value = event.payload.record;
8
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
9
+ }
10
+ function normalizePath(value) {
11
+ const normalized = value.replaceAll('\\', '/');
12
+ return normalized.length > 0 && !normalized.startsWith('/') && !normalized.includes('\0') && !normalized.split('/').includes('..') ? normalized : null;
13
+ }
14
+ function entityKind(entity) {
15
+ if (entity === 'compiler-recipe')
16
+ return 'recipe';
17
+ if (entity === 'knowledge' || entity === 'deadend' || entity === 'skill' || entity === 'experience' || entity === 'proposal')
18
+ return entity;
19
+ return null;
20
+ }
21
+ function entityNodeId(entity, entityId) {
22
+ const kind = entityKind(entity);
23
+ return kind && entityId.trim().length > 0 ? `${kind}:${entityId}` : null;
24
+ }
25
+ export function workspaceNodeId(anchor) {
26
+ return `branch-worktree:${hash({ worktreeId: anchor.worktreeId, branch: anchor.branch, ref: anchor.ref }).slice(0, 16)}`;
27
+ }
28
+ function addNode(nodes, nodeId, kind) {
29
+ nodes.set(nodeId, { nodeId, kind });
30
+ }
31
+ function addEntityNode(nodes, entity, entityId) {
32
+ const nodeId = entityNodeId(entity, entityId);
33
+ const kind = entityKind(entity);
34
+ if (nodeId && kind)
35
+ addNode(nodes, nodeId, kind);
36
+ return nodeId;
37
+ }
38
+ function addEdge(edges, input) {
39
+ const sourceEventIds = [...new Set(input.sourceEventIds)].sort();
40
+ const basis = { from: input.from, to: input.to, type: input.type, sourceEventIds, occurredAt: input.occurredAt, confidence: input.confidence };
41
+ const edgeId = `edge:${hash(basis).slice(0, 24)}`;
42
+ edges.set(edgeId, { ...input, edgeId, sourceEventIds, derivationVersion: EVIDENCE_GRAPH_DERIVATION_VERSION });
43
+ }
44
+ function anchorNodes(event, nodes) {
45
+ const workspace = workspaceNodeId(event.git);
46
+ addNode(nodes, workspace, 'branch-worktree');
47
+ if (event.git.head)
48
+ addNode(nodes, `commit:${event.git.head}`, 'commit');
49
+ return workspace;
50
+ }
51
+ function lineageEdge(edges, nodes, from, lineage, event) {
52
+ const target = addEntityNode(nodes, lineage.entity, lineage.entityId);
53
+ if (!target)
54
+ return;
55
+ addEdge(edges, { from, to: target, type: lineage.relation, sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
56
+ }
57
+ function sourceLinks(edges, nodes, from, contextIds, snapshotIds, event) {
58
+ if (Array.isArray(contextIds))
59
+ for (const contextId of contextIds)
60
+ if (typeof contextId === 'string' && contextId.trim()) {
61
+ const target = `context:${contextId}`;
62
+ addNode(nodes, target, 'context');
63
+ addEdge(edges, { from, to: target, type: 'derived_from', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
64
+ }
65
+ if (Array.isArray(snapshotIds))
66
+ for (const snapshotId of snapshotIds)
67
+ if (typeof snapshotId === 'string' && snapshotId.trim()) {
68
+ const target = `snapshot:${snapshotId}`;
69
+ addNode(nodes, target, 'snapshot');
70
+ addEdge(edges, { from, to: target, type: 'derived_from', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
71
+ }
72
+ }
73
+ export function buildEvidenceGraph(events) {
74
+ const nodes = new Map();
75
+ const edges = new Map();
76
+ for (const event of events) {
77
+ const workspace = anchorNodes(event, nodes);
78
+ if (event.git.head)
79
+ addEdge(edges, { from: workspace, to: `commit:${event.git.head}`, type: 'changed_in', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
80
+ const value = record(event);
81
+ if (event.type === 'context.checkpoint.imported' || event.type === 'context.state.recorded') {
82
+ const snapshot = value?.snapshot;
83
+ if (snapshot && typeof snapshot.contextId === 'string' && typeof snapshot.snapshotId === 'string') {
84
+ const context = `context:${snapshot.contextId}`;
85
+ const snap = `snapshot:${snapshot.snapshotId}`;
86
+ addNode(nodes, context, 'context');
87
+ addNode(nodes, snap, 'snapshot');
88
+ addEdge(edges, { from: context, to: snap, type: 'derived_from', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
89
+ }
90
+ }
91
+ if (event.type === 'experience.recorded' && typeof value?.experienceId === 'string') {
92
+ const id = addEntityNode(nodes, 'experience', value.experienceId);
93
+ sourceLinks(edges, nodes, id, value.sourceContextIds, value.sourceSnapshotIds, event);
94
+ if (Array.isArray(value.paths))
95
+ for (const item of value.paths)
96
+ if (typeof item === 'string') {
97
+ const path = normalizePath(item);
98
+ if (!path)
99
+ continue;
100
+ const target = `path:${path}`;
101
+ addNode(nodes, target, 'path');
102
+ addEdge(edges, { from: id, to: target, type: 'observed_in', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
103
+ }
104
+ }
105
+ if (event.type === 'evolution.proposed' && typeof value?.proposalId === 'string') {
106
+ const id = addEntityNode(nodes, 'proposal', value.proposalId);
107
+ if (Array.isArray(value.sourceExperienceIds))
108
+ for (const source of value.sourceExperienceIds)
109
+ if (typeof source === 'string') {
110
+ const target = addEntityNode(nodes, 'experience', source);
111
+ if (target)
112
+ addEdge(edges, { from: id, to: target, type: 'derived_from', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
113
+ }
114
+ if (Array.isArray(value.lineage))
115
+ for (const item of value.lineage)
116
+ if (item && typeof item === 'object')
117
+ lineageEdge(edges, nodes, id, item, event);
118
+ }
119
+ if (['knowledge.proposed', 'knowledge.verified', 'knowledge.deprecated', 'knowledge.rejected'].includes(event.type) && typeof value?.knowledgeId === 'string') {
120
+ const id = addEntityNode(nodes, 'knowledge', value.knowledgeId);
121
+ sourceLinks(edges, nodes, id, value.sourceContextIds, value.sourceSnapshotIds, event);
122
+ }
123
+ if (['deadend.proposed', 'deadend.verified', 'deadend.obsoleted', 'deadend.rejected'].includes(event.type) && typeof value?.deadendId === 'string') {
124
+ const id = addEntityNode(nodes, 'deadend', value.deadendId);
125
+ sourceLinks(edges, nodes, id, value.sourceContextId === null ? [] : [value.sourceContextId], value.sourceSnapshotId === null ? [] : [value.sourceSnapshotId], event);
126
+ }
127
+ if (event.type === 'skill.materialized' && typeof value?.skillId === 'string') {
128
+ const id = addEntityNode(nodes, 'skill', value.skillId);
129
+ const proposal = typeof value.proposalId === 'string' ? addEntityNode(nodes, 'proposal', value.proposalId) : null;
130
+ if (proposal)
131
+ addEdge(edges, { from: id, to: proposal, type: 'derived_from', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
132
+ if (Array.isArray(value.lineage))
133
+ for (const item of value.lineage)
134
+ if (item && typeof item === 'object')
135
+ lineageEdge(edges, nodes, id, item, event);
136
+ }
137
+ if (event.type === 'recipe.materialized' && typeof value?.recipeId === 'string') {
138
+ const id = addEntityNode(nodes, 'compiler-recipe', value.recipeId);
139
+ const proposal = typeof value.proposalId === 'string' ? addEntityNode(nodes, 'proposal', value.proposalId) : null;
140
+ if (proposal)
141
+ addEdge(edges, { from: id, to: proposal, type: 'derived_from', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
142
+ if (Array.isArray(value.lineage))
143
+ for (const item of value.lineage)
144
+ if (item && typeof item === 'object')
145
+ lineageEdge(edges, nodes, id, item, event);
146
+ }
147
+ if (event.type === 'evolution.superseded') {
148
+ const transition = event.payload.transition;
149
+ if (transition && typeof transition === 'object' && !Array.isArray(transition)) {
150
+ const value = transition;
151
+ const from = typeof value.entity === 'string' && typeof value.entityId === 'string' ? addEntityNode(nodes, value.entity, value.entityId) : null;
152
+ const supersedes = value.supersedes;
153
+ if (from && supersedes && typeof supersedes === 'object' && !Array.isArray(supersedes)) {
154
+ const reference = supersedes;
155
+ lineageEdge(edges, nodes, from, reference, event);
156
+ }
157
+ }
158
+ }
159
+ if (event.type === 'workspace.paths.changed') {
160
+ const payload = event.payload;
161
+ if (Array.isArray(payload.paths))
162
+ for (const item of payload.paths)
163
+ if (typeof item === 'string') {
164
+ const path = normalizePath(item);
165
+ if (!path)
166
+ continue;
167
+ const target = `path:${path}`;
168
+ addNode(nodes, target, 'path');
169
+ addEdge(edges, { from: workspace, to: target, type: 'observed_in', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
170
+ }
171
+ }
172
+ if (event.type === 'record.used') {
173
+ const used = event.payload;
174
+ if (typeof used.entity === 'string' && typeof used.entityId === 'string') {
175
+ const from = addEntityNode(nodes, used.entity, used.entityId);
176
+ if (from)
177
+ addEdge(edges, { from, to: workspace, type: 'used_for', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
178
+ }
179
+ }
180
+ }
181
+ return {
182
+ schemaVersion: 1,
183
+ derivationVersion: EVIDENCE_GRAPH_DERIVATION_VERSION,
184
+ nodes: [...nodes.values()].sort((a, b) => a.nodeId.localeCompare(b.nodeId)),
185
+ edges: [...edges.values()].sort((a, b) => a.edgeId.localeCompare(b.edgeId)),
186
+ };
187
+ }
@@ -0,0 +1,36 @@
1
+ import type { EvolutionArtifactKind, EvolutionArtifactStatus, EvolutionLineageReference, EvolutionProposalState } from './types.js';
2
+ type ProposalTargetKind = Extract<EvolutionArtifactKind, 'skill' | 'compiler-recipe'>;
3
+ export interface ProposeEvolutionInput {
4
+ eventIds?: string[];
5
+ recentLimit?: number;
6
+ targetKind?: ProposalTargetKind;
7
+ }
8
+ export interface ProposeEvolutionResult {
9
+ experienceId: string;
10
+ experienceEventId: string;
11
+ proposalId: string | null;
12
+ proposalEventId: string | null;
13
+ recorded: boolean;
14
+ }
15
+ export interface AcceptEvolutionCandidateInput {
16
+ proposalId: string;
17
+ rationale: string;
18
+ }
19
+ export interface TransitionEvolutionProposalInput {
20
+ proposalId: string;
21
+ to: Extract<EvolutionArtifactStatus, 'verified' | 'rejected' | 'deprecated' | 'superseded'>;
22
+ rationale: string;
23
+ supersedes?: EvolutionLineageReference;
24
+ }
25
+ export declare function proposeEvolution(startPath: string | undefined, input?: ProposeEvolutionInput): Promise<ProposeEvolutionResult>;
26
+ export declare function listEvolutionProposals(startPath?: string): Promise<EvolutionProposalState[]>;
27
+ export declare function inspectEvolutionProposal(startPath: string | undefined, proposalId: string): Promise<EvolutionProposalState>;
28
+ export declare function acceptEvolutionCandidate(startPath: string | undefined, input: AcceptEvolutionCandidateInput): Promise<{
29
+ accepted: boolean;
30
+ eventId: string;
31
+ }>;
32
+ export declare function transitionEvolutionProposal(startPath: string | undefined, input: TransitionEvolutionProposalInput): Promise<{
33
+ transitioned: boolean;
34
+ eventId: string;
35
+ }>;
36
+ export {};
@@ -0,0 +1,221 @@
1
+ import crypto from 'node:crypto';
2
+ import { readGitWorktreeState } from '../git/git-adapter.js';
3
+ import { resolveProjectRoot } from '../project/project-resolver.js';
4
+ import { appendEvent, readJournal } from './journal.js';
5
+ import { v2Enabled } from './orient.js';
6
+ import { projectEvents, rebuildProjections } from './projections.js';
7
+ const extractor = { id: 'deterministic-maintainer', version: '1' };
8
+ const maxEvidenceEvents = 32;
9
+ const allowedEvidenceTypes = new Set([
10
+ 'host.prompt.submitted',
11
+ 'context.checkpoint.imported', 'context.state.recorded',
12
+ 'note.recorded', 'note.resolved', 'note.absorbed',
13
+ 'git.state.observed', 'git.branch.changed', 'git.head.changed',
14
+ 'workspace.paths.changed', 'record.used', 'usage.operation.completed',
15
+ ]);
16
+ function canonical(value) {
17
+ if (value === null || typeof value !== 'object')
18
+ return JSON.stringify(value);
19
+ if (Array.isArray(value))
20
+ return `[${value.map(canonical).join(',')}]`;
21
+ const record = value;
22
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`).join(',')}}`;
23
+ }
24
+ function hash(value) {
25
+ return crypto.createHash('sha256').update(canonical(value)).digest('hex');
26
+ }
27
+ function anchor(event) {
28
+ return { worktreeId: event.git.worktreeId, branch: event.git.branch, ref: event.git.ref, head: event.git.head };
29
+ }
30
+ function currentAnchor(state) {
31
+ return { worktreeId: state.worktreeId, branch: state.branch, ref: state.ref, head: state.commit };
32
+ }
33
+ function requireReason(reason) {
34
+ const value = reason.trim();
35
+ if (!value)
36
+ throw new Error('evolution rationale must not be empty');
37
+ return value;
38
+ }
39
+ function selectEvidence(events, input) {
40
+ const selected = input.eventIds
41
+ ? input.eventIds.map((eventId) => {
42
+ if (!eventId.trim())
43
+ throw new Error('evidence event ID must not be empty');
44
+ return eventId;
45
+ })
46
+ : events.filter((event) => allowedEvidenceTypes.has(event.type)).slice(-(input.recentLimit ?? maxEvidenceEvents)).map((event) => event.eventId);
47
+ if (selected.length === 0)
48
+ throw new Error('evolution extraction requires evidence events');
49
+ if (selected.length > maxEvidenceEvents)
50
+ throw new Error(`evolution extraction supports at most ${maxEvidenceEvents} evidence events`);
51
+ if (new Set(selected).size !== selected.length)
52
+ throw new Error('evolution evidence event IDs must be unique');
53
+ const byId = new Map(events.map((event) => [event.eventId, event]));
54
+ const evidence = selected.map((eventId) => byId.get(eventId));
55
+ if (evidence.some((event) => !event))
56
+ throw new Error('evolution evidence event was not found');
57
+ if (evidence.some((event) => !allowedEvidenceTypes.has(event.type)))
58
+ throw new Error('evolution evidence event type is not allowed');
59
+ return evidence;
60
+ }
61
+ function stringArray(value) {
62
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item.trim().length > 0) : [];
63
+ }
64
+ function sourceReferences(events) {
65
+ const contextIds = new Set();
66
+ const snapshotIds = new Set();
67
+ const paths = new Set();
68
+ for (const event of events) {
69
+ const payload = event.payload;
70
+ const record = payload.record;
71
+ if (record && typeof record === 'object' && !Array.isArray(record)) {
72
+ const item = record;
73
+ const snapshot = item.snapshot;
74
+ if (snapshot && typeof snapshot === 'object' && !Array.isArray(snapshot)) {
75
+ const value = snapshot;
76
+ if (typeof value.contextId === 'string')
77
+ contextIds.add(value.contextId);
78
+ if (typeof value.snapshotId === 'string')
79
+ snapshotIds.add(value.snapshotId);
80
+ }
81
+ if (typeof item.contextId === 'string')
82
+ contextIds.add(item.contextId);
83
+ if (typeof item.snapshotId === 'string')
84
+ snapshotIds.add(item.snapshotId);
85
+ }
86
+ for (const value of stringArray(payload.paths))
87
+ paths.add(value.replaceAll('\\', '/'));
88
+ }
89
+ return { contextIds: [...contextIds].sort(), snapshotIds: [...snapshotIds].sort(), paths: [...paths].sort() };
90
+ }
91
+ function privacy(events) {
92
+ const redactionCount = events.reduce((total, event) => total + event.privacy.redactionCount, 0);
93
+ return { classification: events.some((event) => event.privacy.classification === 'redacted-text') ? 'redacted-text' : 'metadata', redactionCount };
94
+ }
95
+ function applicability(event) {
96
+ if (event.git.ref === null && event.git.branch === null)
97
+ return { project: true, branchMode: 'global' };
98
+ return { project: true, branchMode: 'exact', branches: [event.git.ref ?? event.git.branch ?? 'detached'], sourceCommit: event.git.head };
99
+ }
100
+ function candidateEligible(events) {
101
+ const hasContext = events.some((event) => event.type === 'context.checkpoint.imported' || event.type === 'context.state.recorded');
102
+ const hasActivity = events.some((event) => event.type.startsWith('git.') || event.type === 'workspace.paths.changed');
103
+ const hasUse = events.some((event) => event.type === 'record.used');
104
+ return hasContext && hasActivity && hasUse;
105
+ }
106
+ async function enabledProject(startPath) {
107
+ const projectRoot = await resolveProjectRoot(startPath);
108
+ if (!await v2Enabled(projectRoot))
109
+ throw new Error('evolution requires an enabled v2 runtime');
110
+ return projectRoot;
111
+ }
112
+ export async function proposeEvolution(startPath, input = {}) {
113
+ const projectRoot = await enabledProject(startPath);
114
+ const events = await readJournal(projectRoot);
115
+ const evidence = selectEvidence(events, input);
116
+ const references = sourceReferences(evidence);
117
+ const sourceEventIds = evidence.map((event) => event.eventId);
118
+ const last = evidence.at(-1);
119
+ const experienceBasis = { sourceEventIds, sourceContextIds: references.contextIds, sourceSnapshotIds: references.snapshotIds, paths: references.paths, git: anchor(last), privacy: privacy(evidence), extractor, createdAt: last.occurredAt };
120
+ const experience = {
121
+ experienceId: `exp-${hash(experienceBasis).slice(0, 20)}`,
122
+ schemaVersion: 1,
123
+ ...experienceBasis,
124
+ contentHash: hash(experienceBasis),
125
+ summary: `Metadata-only experience from ${sourceEventIds.length} journal events.`,
126
+ };
127
+ const experienceResult = await appendEvent(projectRoot, {
128
+ type: 'experience.recorded', source: { kind: 'agent' }, occurredAt: experience.createdAt,
129
+ idempotencyKey: `experience:${experience.experienceId}`, git: experience.git,
130
+ privacy: experience.privacy, payload: { record: experience },
131
+ });
132
+ let proposalResult = null;
133
+ let proposal = null;
134
+ if (candidateEligible(evidence)) {
135
+ const targetKind = input.targetKind ?? 'skill';
136
+ const proposalBasis = { sourceExperienceIds: [experience.experienceId], evidence: sourceEventIds.map((eventId) => ({ eventId, relation: 'derived_from' })), applicability: applicability(last), targetKind, extractor, createdAt: experience.createdAt };
137
+ const contentHash = hash(proposalBasis);
138
+ proposal = {
139
+ proposalId: `proposal-${contentHash.slice(0, 20)}`,
140
+ schemaVersion: 1,
141
+ targetKind,
142
+ targetId: `${targetKind}-${contentHash.slice(0, 20)}`,
143
+ status: 'candidate',
144
+ sourceExperienceIds: proposalBasis.sourceExperienceIds,
145
+ evidence: proposalBasis.evidence,
146
+ lineage: [],
147
+ applicability: proposalBasis.applicability,
148
+ contentHash,
149
+ summary: `Candidate ${targetKind} derived from metadata-only experience ${experience.experienceId}.`,
150
+ extractor,
151
+ createdAt: experience.createdAt,
152
+ };
153
+ proposalResult = await appendEvent(projectRoot, {
154
+ type: 'evolution.proposed', source: { kind: 'agent' }, occurredAt: proposal.createdAt,
155
+ idempotencyKey: `evolution-proposal:${proposal.proposalId}`, git: experience.git,
156
+ privacy: experience.privacy, payload: { record: proposal },
157
+ });
158
+ }
159
+ if (experienceResult.appended || proposalResult?.appended)
160
+ await rebuildProjections(projectRoot);
161
+ return { experienceId: experience.experienceId, experienceEventId: experienceResult.event.eventId, proposalId: proposal?.proposalId ?? null, proposalEventId: proposalResult?.event.eventId ?? null, recorded: experienceResult.appended || Boolean(proposalResult?.appended) };
162
+ }
163
+ export async function listEvolutionProposals(startPath) {
164
+ const projectRoot = await enabledProject(startPath);
165
+ return Object.values(projectEvents(await readJournal(projectRoot)).proposals).sort((left, right) => left.proposal.proposalId.localeCompare(right.proposal.proposalId));
166
+ }
167
+ export async function inspectEvolutionProposal(startPath, proposalId) {
168
+ const proposal = (await listEvolutionProposals(startPath)).find((item) => item.proposal.proposalId === proposalId);
169
+ if (!proposal)
170
+ throw new Error(`evolution proposal not found: ${proposalId}`);
171
+ return proposal;
172
+ }
173
+ export async function acceptEvolutionCandidate(startPath, input) {
174
+ const projectRoot = await enabledProject(startPath);
175
+ const rationale = requireReason(input.rationale);
176
+ const state = await inspectEvolutionProposal(projectRoot, input.proposalId);
177
+ if (state.proposal.targetKind !== 'skill' && state.proposal.targetKind !== 'compiler-recipe')
178
+ throw new Error('only skill and compiler-recipe proposals can be accepted in Phase 1');
179
+ if (state.status !== 'candidate')
180
+ throw new Error(`candidate acceptance requires a candidate proposal: ${input.proposalId}`);
181
+ if (state.acceptedArtifact)
182
+ throw new Error(`candidate artifact already accepted: ${input.proposalId}`);
183
+ const git = await readGitWorktreeState(projectRoot);
184
+ const artifact = {
185
+ artifactId: state.proposal.targetId,
186
+ schemaVersion: 1,
187
+ proposalId: state.proposal.proposalId,
188
+ kind: state.proposal.targetKind,
189
+ status: 'candidate',
190
+ applicability: state.proposal.applicability,
191
+ contentHash: state.proposal.contentHash,
192
+ lineage: state.proposal.lineage,
193
+ rationale,
194
+ createdAt: new Date().toISOString(),
195
+ };
196
+ const result = await appendEvent(projectRoot, {
197
+ type: 'evolution.candidate.accepted', source: { kind: 'cli' }, idempotencyKey: `evolution-accept:${artifact.artifactId}:${hash(rationale).slice(0, 16)}`,
198
+ git: currentAnchor(git), payload: { record: artifact },
199
+ });
200
+ if (result.appended)
201
+ await rebuildProjections(projectRoot);
202
+ return { accepted: result.appended, eventId: result.event.eventId };
203
+ }
204
+ export async function transitionEvolutionProposal(startPath, input) {
205
+ const projectRoot = await enabledProject(startPath);
206
+ const rationale = requireReason(input.rationale);
207
+ const state = await inspectEvolutionProposal(projectRoot, input.proposalId);
208
+ if (input.to === 'superseded' && !input.supersedes)
209
+ throw new Error('superseding an evolution proposal requires lineage');
210
+ const git = await readGitWorktreeState(projectRoot);
211
+ const result = await appendEvent(projectRoot, {
212
+ type: `evolution.${input.to}`,
213
+ source: { kind: 'cli' },
214
+ idempotencyKey: `evolution-transition:${input.proposalId}:${state.status}:${input.to}:${hash({ rationale, supersedes: input.supersedes }).slice(0, 16)}`,
215
+ git: currentAnchor(git),
216
+ payload: { transition: { entity: 'proposal', entityId: input.proposalId, from: state.status, to: input.to, reason: rationale, ...(input.supersedes ? { supersedes: input.supersedes } : {}) } },
217
+ });
218
+ if (result.appended)
219
+ await rebuildProjections(projectRoot);
220
+ return { transitioned: result.appended, eventId: result.event.eventId };
221
+ }
@@ -0,0 +1,18 @@
1
+ export declare function importMemory(startPath?: string, apply?: boolean): Promise<{
2
+ projectRoot: string;
3
+ applied: boolean;
4
+ scannedBytes: number;
5
+ results: {
6
+ status: string;
7
+ knowledgeId: string | null;
8
+ source: string;
9
+ line: number;
10
+ statement: string;
11
+ fingerprint: string;
12
+ }[];
13
+ skipped: {
14
+ source: string;
15
+ reason: string;
16
+ }[];
17
+ trust: string;
18
+ }>;
@@ -0,0 +1,124 @@
1
+ import { lstat, readdir, readFile, realpath } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { resolveProjectRoot } from '../project/project-resolver.js';
5
+ import { filterPaths } from '../capture/context-filter.js';
6
+ import { learnKnowledge, listKnowledge } from '../runtime/knowledge.js';
7
+ import { loadManifest } from '../runtime/init.js';
8
+ import { withLock } from '../storage/lock.js';
9
+ import { locksDirectory } from '../storage/layout.js';
10
+ const sources = ['memory', '.memory', '.claude', '.zcode', '.cursor', '.curcor', '.codex', 'AGENTS.md', 'agents.md', 'CLAUDE.md', 'MEMORY.md', '.cursorrules'];
11
+ const hash = (text) => crypto.createHash('sha256').update(text).digest('hex');
12
+ const sensitive = /(?:api[_-]?key|token|password|secret)\s*[:=]\s*\S+|\b(?:sk-|sk_|ghp_|github_pat_)[A-Za-z0-9_-]+|-----BEGIN .*PRIVATE KEY-----/i;
13
+ export async function importMemory(startPath, apply = false) {
14
+ const root = await resolveProjectRoot(startPath);
15
+ await loadManifest(root);
16
+ const candidates = [];
17
+ const skipped = [];
18
+ const visited = new Set();
19
+ let bytes = 0;
20
+ let entries = 0;
21
+ const canonicalRoot = await realpath(root);
22
+ async function scan(relative, depth = 0) {
23
+ if (++entries > 2000 || candidates.length >= 500 || depth > 8) {
24
+ skipped.push({ source: relative, reason: 'scan-budget' });
25
+ return;
26
+ }
27
+ const absolute = path.join(root, relative);
28
+ let info;
29
+ try {
30
+ info = await lstat(absolute);
31
+ }
32
+ catch (error) {
33
+ if (error.code === 'ENOENT')
34
+ return;
35
+ throw error;
36
+ }
37
+ if (info.isSymbolicLink()) {
38
+ skipped.push({ source: relative, reason: 'symlink' });
39
+ return;
40
+ }
41
+ const resolved = await realpath(absolute);
42
+ const within = path.relative(canonicalRoot, resolved);
43
+ if (within.startsWith('..') || path.isAbsolute(within)) {
44
+ skipped.push({ source: relative, reason: 'outside-project' });
45
+ return;
46
+ }
47
+ const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
48
+ if (visited.has(key))
49
+ return;
50
+ visited.add(key);
51
+ if ((await filterPaths(root, [relative])).length === 0) {
52
+ skipped.push({ source: relative, reason: 'ignored' });
53
+ return;
54
+ }
55
+ if (info.isDirectory()) {
56
+ for (const entry of (await readdir(absolute)).sort()) {
57
+ if (entries >= 2000 || candidates.length >= 500) {
58
+ skipped.push({ source: relative, reason: 'scan-budget' });
59
+ break;
60
+ }
61
+ await scan(path.join(relative, entry), depth + 1);
62
+ }
63
+ return;
64
+ }
65
+ if (!info.isFile() || !/\.(?:md|mdc|txt)$/i.test(relative) && path.basename(relative) !== '.cursorrules')
66
+ return;
67
+ if (info.size > 64 * 1024 || bytes + info.size > 2 * 1024 * 1024) {
68
+ skipped.push({ source: relative, reason: 'byte-budget' });
69
+ return;
70
+ }
71
+ bytes += info.size;
72
+ const text = await readFile(absolute, 'utf8');
73
+ let managed = false;
74
+ let code = false;
75
+ for (const [index, raw] of text.split(/\r?\n/).entries()) {
76
+ if (raw.includes('<!-- fluffy-context:start -->')) {
77
+ managed = true;
78
+ continue;
79
+ }
80
+ if (raw.includes('<!-- fluffy-context:end -->')) {
81
+ managed = false;
82
+ continue;
83
+ }
84
+ if (managed)
85
+ continue;
86
+ if (/^\s*```/.test(raw)) {
87
+ code = !code;
88
+ continue;
89
+ }
90
+ if (code || /^\s*#/.test(raw))
91
+ continue;
92
+ const statement = raw.replace(/^\s*(?:[-*+] |\d+\. )/, '').trim();
93
+ if (statement.length < 20 || statement.length > 1500)
94
+ continue;
95
+ if (sensitive.test(statement)) {
96
+ skipped.push({ source: `${relative}:${index + 1}`, reason: 'potential-secret' });
97
+ continue;
98
+ }
99
+ if (candidates.length >= 500) {
100
+ skipped.push({ source: relative, reason: 'candidate-budget' });
101
+ break;
102
+ }
103
+ candidates.push({ source: relative.replaceAll('\\', '/'), line: index + 1, statement,
104
+ fingerprint: hash(statement.replace(/\s+/g, ' ').toLowerCase()) });
105
+ }
106
+ }
107
+ for (const source of sources)
108
+ await scan(source);
109
+ const run = async () => {
110
+ const known = new Set((await listKnowledge(root, true)).map((item) => hash(item.statement.replace(/\s+/g, ' ').toLowerCase())));
111
+ const results = [];
112
+ for (const candidate of candidates) {
113
+ const duplicate = known.has(candidate.fingerprint);
114
+ known.add(candidate.fingerprint);
115
+ const record = apply && !duplicate ? await learnKnowledge(root, candidate.statement, {
116
+ kind: 'imported-memory', evidence: [`${candidate.source}:${candidate.line}`, `sha256:${candidate.fingerprint}`],
117
+ }) : null;
118
+ results.push({ ...candidate, status: duplicate ? 'duplicate' : apply ? 'candidate' : 'preview', knowledgeId: record?.knowledgeId ?? null });
119
+ }
120
+ return { projectRoot: root, applied: apply, scannedBytes: bytes, results, skipped,
121
+ trust: 'Imported text is unverified evidence; inspect and verify against the project before injection.' };
122
+ };
123
+ return apply ? withLock(path.join(locksDirectory(root), 'memory-import.lock'), run) : run();
124
+ }
@@ -0,0 +1,11 @@
1
+ export declare const JOURNAL_SEGMENT_BYTES: number;
2
+ export declare function journalFiles(root: string): Promise<string[]>;
3
+ export declare function readSegment(root: string, name: string): Promise<Buffer>;
4
+ export declare function archiveSegment(root: string, name: string): Promise<void>;
5
+ export declare function appendJournalLine(root: string, line: string): Promise<void>;
6
+ export declare function journalStorage(root: string, expand?: boolean): Promise<{
7
+ bytes: number;
8
+ expandedBytes: number | null;
9
+ archives: number;
10
+ segments: number;
11
+ }>;