@cairnkeep/cli 2.16.0 → 2.17.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 (59) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -0
  3. package/docs/README.md +8 -0
  4. package/docs/agents.md +20 -0
  5. package/docs/context-intelligence.md +143 -0
  6. package/docs/context-packs.md +33 -0
  7. package/docs/domain-knowledge.md +29 -0
  8. package/docs/learning/CURRICULUM-MAP.md +3 -1
  9. package/docs/learning/FEATURE-GUIDE.md +22 -3
  10. package/docs/learning/README.md +1 -0
  11. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  12. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  13. package/docs/learning/lessons/L02-installation.md +2 -2
  14. package/docs/learning/lessons/L03-first-project.md +1 -1
  15. package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
  16. package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
  17. package/docs/learning/lessons/L21-context-packs.md +1 -1
  18. package/docs/learning/lessons/L22-native-windows.md +2 -2
  19. package/docs/learning/lessons/L23-guided-setup.md +1 -1
  20. package/docs/learning/lessons/L24-okf-exchange.md +1 -1
  21. package/docs/learning/lessons/L25-playbooks.md +1 -1
  22. package/docs/learning/lessons/L26-context-intelligence.md +180 -0
  23. package/docs/learning/tracks/evidence-and-evaluation.md +1 -0
  24. package/docs/learning/tracks/operator.md +1 -0
  25. package/docs/learning/tracks/practitioner.md +1 -0
  26. package/docs/learning/video-scripts/V26-context-intelligence.md +144 -0
  27. package/docs/operating.md +44 -0
  28. package/docs/privacy-and-data-flow.md +22 -0
  29. package/docs/storage.md +21 -0
  30. package/docs/work-evidence.md +14 -0
  31. package/examples/eval/bundled-fake.json +1 -1
  32. package/mcp-memory-server/dist/context-pack-cli.js +3 -1
  33. package/mcp-memory-server/dist/context-pack-retrieval.js +413 -0
  34. package/mcp-memory-server/dist/context-pack.js +171 -3
  35. package/mcp-memory-server/dist/domain-retrieval-provider.js +257 -0
  36. package/mcp-memory-server/dist/index.js +73 -387
  37. package/mcp-memory-server/dist/mcp-tool-catalog.js +2 -0
  38. package/mcp-memory-server/dist/memory-extraction.js +87 -0
  39. package/mcp-memory-server/dist/memory-proposal-cli.js +110 -0
  40. package/mcp-memory-server/dist/memory-proposal-schema.js +78 -0
  41. package/mcp-memory-server/dist/memory-proposal-store.js +273 -0
  42. package/mcp-memory-server/dist/platform-security.js +32 -26
  43. package/mcp-memory-server/dist/retrieval-benchmark.js +199 -0
  44. package/mcp-memory-server/dist/reviewed-memory-store.js +281 -0
  45. package/mcp-memory-server/dist/work-evidence-schema.js +17 -0
  46. package/mcp-memory-server/dist/work-evidence-store.js +57 -1
  47. package/mcp-memory-server/package.json +8 -1
  48. package/package.json +1 -1
  49. package/schemas/context-usage-receipt.schema.json +41 -0
  50. package/schemas/memory-proposal.schema.json +65 -0
  51. package/schemas/retrieval-benchmark.schema.json +64 -0
  52. package/scripts/cairn-cli.mjs +2 -0
  53. package/scripts/completion.sh +17 -6
  54. package/scripts/doctor.sh +4 -2
  55. package/scripts/test-completion.sh +8 -1
  56. package/scripts/test-doctor.sh +21 -0
  57. package/scripts/test-release-sbom.sh +3 -0
  58. package/scripts/test-windows-native.mjs +30 -0
  59. package/scripts/windows-platform.mjs +8 -5
@@ -0,0 +1,199 @@
1
+ import { createHash } from "node:crypto";
2
+ import { performance } from "node:perf_hooks";
3
+ import { z } from "zod";
4
+ const benchmarkFileSchema = z.object({
5
+ path: z.string().min(1).regex(/^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$)).+$/),
6
+ kind: z.enum(["document", "skill"]),
7
+ title: z.string().min(1),
8
+ description: z.string(),
9
+ keywords: z.array(z.string()),
10
+ content: z.string(),
11
+ }).strict();
12
+ const benchmarkPackSchema = z.object({
13
+ id: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
14
+ version: z.string().regex(/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/),
15
+ title: z.string().min(1),
16
+ enabled: z.boolean(),
17
+ files: z.array(benchmarkFileSchema).min(1).max(1_024),
18
+ }).strict();
19
+ const benchmarkCaseSchema = z.object({
20
+ id: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
21
+ query: z.string().min(1),
22
+ relevant: z.array(z.string().min(1)),
23
+ forbidden: z.array(z.string().min(1)),
24
+ limit: z.number().int().min(1).max(100),
25
+ }).strict();
26
+ const retrievalBenchmarkSuiteSchema = z.object({
27
+ schema_version: z.literal(1),
28
+ id: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
29
+ description: z.string().min(1),
30
+ packs: z.array(benchmarkPackSchema).min(1).max(32),
31
+ cases: z.array(benchmarkCaseSchema).min(1).max(1_024),
32
+ }).strict();
33
+ function assertUnique(values, label) {
34
+ if (new Set(values).size !== values.length)
35
+ throw new Error(`Duplicate ${label}`);
36
+ }
37
+ function resultKey(row) {
38
+ return `${row.pack_id}@${row.version}:${row.path}`;
39
+ }
40
+ function resultIdentity(pack, path) {
41
+ return `${pack.id}@${pack.version}:${path}`;
42
+ }
43
+ function suiteVisibilityPolicy(suite) {
44
+ const declared = new Set();
45
+ const forbidden = new Set();
46
+ for (const pack of suite.packs) {
47
+ for (const file of pack.files) {
48
+ const identity = resultIdentity(pack, file.path);
49
+ declared.add(identity);
50
+ if (!pack.enabled || file.kind === "skill")
51
+ forbidden.add(identity);
52
+ }
53
+ }
54
+ return { declared, forbidden };
55
+ }
56
+ function round(value) {
57
+ return Number(value.toFixed(6));
58
+ }
59
+ function quantile(values, percentile) {
60
+ if (values.length === 0)
61
+ return 0;
62
+ const ordered = [...values].sort((a, b) => a - b);
63
+ return ordered[Math.ceil(percentile * ordered.length) - 1] ?? ordered[ordered.length - 1];
64
+ }
65
+ function stable(value) {
66
+ if (Array.isArray(value))
67
+ return value.map(stable);
68
+ if (value && typeof value === "object") {
69
+ return Object.fromEntries(Object.entries(value)
70
+ .sort(([left], [right]) => left.localeCompare(right, "en"))
71
+ .map(([key, item]) => [key, stable(item)]));
72
+ }
73
+ return value;
74
+ }
75
+ function digest(value) {
76
+ return createHash("sha256").update(JSON.stringify(stable(value))).digest("hex");
77
+ }
78
+ export function parseRetrievalBenchmarkSuite(value) {
79
+ const suite = retrievalBenchmarkSuiteSchema.parse(value);
80
+ assertUnique(suite.packs.map(({ id, version }) => `${id}@${version}`), "benchmark pack id and version");
81
+ assertUnique(suite.cases.map(({ id }) => id), "benchmark case id");
82
+ for (const pack of suite.packs) {
83
+ assertUnique(pack.files.map(({ path }) => path), `file path in ${pack.id}@${pack.version}`);
84
+ }
85
+ const { declared, forbidden: suiteForbidden } = suiteVisibilityPolicy(suite);
86
+ for (const benchmarkCase of suite.cases) {
87
+ assertUnique(benchmarkCase.relevant, `relevant result in ${benchmarkCase.id}`);
88
+ assertUnique(benchmarkCase.forbidden, `forbidden result in ${benchmarkCase.id}`);
89
+ if (benchmarkCase.relevant.some((key) => benchmarkCase.forbidden.includes(key))) {
90
+ throw new Error(`Benchmark case ${benchmarkCase.id} marks a result both relevant and forbidden`);
91
+ }
92
+ const forbiddenRelevant = benchmarkCase.relevant.find((key) => suiteForbidden.has(key));
93
+ if (forbiddenRelevant) {
94
+ throw new Error(`Benchmark case ${benchmarkCase.id} marks suite-forbidden result ${forbiddenRelevant} as relevant`);
95
+ }
96
+ for (const key of [...benchmarkCase.relevant, ...benchmarkCase.forbidden]) {
97
+ if (!declared.has(key))
98
+ throw new Error(`Benchmark case ${benchmarkCase.id} references undeclared result ${key}`);
99
+ }
100
+ }
101
+ return suite;
102
+ }
103
+ export function canonicalBenchmarkReport(report) {
104
+ const { generated_at: _generatedAt, report_digest: _reportDigest, ...rest } = report;
105
+ return stable({
106
+ ...rest,
107
+ cases: rest.cases.map(({ latency_ms: _latency, ...benchmarkCase }) => benchmarkCase),
108
+ metrics: {
109
+ ...rest.metrics,
110
+ median_latency_ms: 0,
111
+ p95_latency_ms: 0,
112
+ },
113
+ });
114
+ }
115
+ export async function evaluateRetrievalBenchmark(suiteValue, adapter) {
116
+ const suite = parseRetrievalBenchmarkSuite(suiteValue);
117
+ const clock = adapter.now ?? (() => performance.now());
118
+ const networkBefore = adapter.networkRequests?.() ?? 0;
119
+ const mutationsBefore = adapter.filesystemMutations?.() ?? 0;
120
+ const cases = [];
121
+ const { declared, forbidden: suiteForbidden } = suiteVisibilityPolicy(suite);
122
+ for (const benchmarkCase of suite.cases) {
123
+ const started = clock();
124
+ const rows = (await adapter.retrieve(benchmarkCase)).slice(0, benchmarkCase.limit);
125
+ const latency = Math.max(0, clock() - started);
126
+ const relevant = new Set(benchmarkCase.relevant);
127
+ const forbidden = new Set([...suiteForbidden, ...benchmarkCase.forbidden]);
128
+ const returned = rows.map(resultKey);
129
+ const leakedResults = returned.filter((key) => forbidden.has(key) || !declared.has(key));
130
+ const undeclaredResults = returned.filter((key) => !declared.has(key)).length;
131
+ const uniqueTopFive = new Set(returned.slice(0, 5).filter((key) => relevant.has(key)));
132
+ const firstRelevant = returned.findIndex((key) => relevant.has(key));
133
+ let relevantBytes = 0;
134
+ let irrelevantBytes = 0;
135
+ for (const [index, row] of rows.entries()) {
136
+ const bytes = Buffer.byteLength(row.text, "utf8");
137
+ if (relevant.has(returned[index]))
138
+ relevantBytes += bytes;
139
+ else
140
+ irrelevantBytes += bytes;
141
+ }
142
+ cases.push({
143
+ id: benchmarkCase.id,
144
+ query: benchmarkCase.query,
145
+ relevant: [...benchmarkCase.relevant],
146
+ forbidden: [...forbidden].sort((left, right) => left.localeCompare(right, "en")),
147
+ returned,
148
+ leaked_results: leakedResults,
149
+ isolation_case: relevant.size === 0,
150
+ isolation_pass: leakedResults.length === 0,
151
+ hit_at_1: returned[0] && relevant.has(returned[0]) ? 1 : 0,
152
+ recall_at_5: relevant.size ? round(uniqueTopFive.size / relevant.size) : 0,
153
+ reciprocal_rank: firstRelevant < 0 ? 0 : round(1 / (firstRelevant + 1)),
154
+ relevant_bytes: relevantBytes,
155
+ irrelevant_bytes: irrelevantBytes,
156
+ estimated_context_tokens: Math.ceil((relevantBytes + irrelevantBytes) / 4),
157
+ forbidden_results: leakedResults.length,
158
+ undeclared_results: undeclaredResults,
159
+ latency_ms: round(latency),
160
+ });
161
+ }
162
+ const count = cases.length;
163
+ const sum = (field) => cases.reduce((total, item) => total + Number(item[field]), 0);
164
+ const positiveCases = cases.filter(({ isolation_case }) => !isolation_case);
165
+ const negativeCases = cases.filter(({ isolation_case }) => isolation_case);
166
+ const sumPositive = (field) => positiveCases.reduce((total, item) => total + item[field], 0);
167
+ const negativeCasesPassed = negativeCases.filter(({ isolation_pass }) => isolation_pass).length;
168
+ const negativeCaseLeaks = negativeCases.reduce((total, item) => total + item.forbidden_results, 0);
169
+ const report = {
170
+ schema_version: 1,
171
+ benchmark_id: suite.id,
172
+ mode: adapter.mode,
173
+ generated_at: new Date().toISOString(),
174
+ cases,
175
+ metrics: {
176
+ cases: count,
177
+ positive_cases: positiveCases.length,
178
+ negative_cases: negativeCases.length,
179
+ negative_cases_passed: negativeCasesPassed,
180
+ negative_case_pass_rate: negativeCases.length ? round(negativeCasesPassed / negativeCases.length) : 1,
181
+ negative_case_leaks: negativeCaseLeaks,
182
+ hit_at_1: positiveCases.length ? round(sumPositive("hit_at_1") / positiveCases.length) : 0,
183
+ recall_at_5: positiveCases.length ? round(sumPositive("recall_at_5") / positiveCases.length) : 0,
184
+ mean_reciprocal_rank: positiveCases.length ? round(sumPositive("reciprocal_rank") / positiveCases.length) : 0,
185
+ relevant_bytes: sum("relevant_bytes"),
186
+ irrelevant_bytes: sum("irrelevant_bytes"),
187
+ estimated_context_tokens: sum("estimated_context_tokens"),
188
+ median_latency_ms: round(quantile(cases.map(({ latency_ms }) => latency_ms), 0.5)),
189
+ p95_latency_ms: round(quantile(cases.map(({ latency_ms }) => latency_ms), 0.95)),
190
+ network_requests: Math.max(0, (adapter.networkRequests?.() ?? networkBefore) - networkBefore),
191
+ filesystem_mutations: Math.max(0, (adapter.filesystemMutations?.() ?? mutationsBefore) - mutationsBefore),
192
+ forbidden_results: sum("forbidden_results"),
193
+ undeclared_results: sum("undeclared_results"),
194
+ },
195
+ report_digest: "",
196
+ };
197
+ report.report_digest = digest(canonicalBenchmarkReport(report));
198
+ return report;
199
+ }
@@ -0,0 +1,281 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { AgentFS } from "agentfs-sdk";
6
+ import { z } from "zod";
7
+ import { hashText } from "./embeddings.js";
8
+ import { applyReviewedTypedNode, invalidateReviewedTypedNode } from "./node-store.js";
9
+ import { assertMemoryProposalCandidateConsistency, digestText, expectedMemoryProposalOperation, } from "./memory-proposal-schema.js";
10
+ export const HISTORY_NAMESPACE = "__history__";
11
+ export const REVIEWED_NAMESPACE = "__reviewed__";
12
+ export const REVIEW_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
13
+ const SCOPE_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
14
+ const reviewedRecordSchema = z.object({
15
+ schema_version: z.literal(1),
16
+ review_id: z.string(),
17
+ key: z.string(),
18
+ value_hash: z.string(),
19
+ state: z.enum(["active", "superseded", "invalidated"]),
20
+ applied_at: z.string(),
21
+ superseded_at: z.string().optional(),
22
+ superseded_by: z.string().optional(),
23
+ invalidated_at: z.string().optional(),
24
+ invalidation_reason: z.string().optional(),
25
+ }).strict();
26
+ function expandHome(value) {
27
+ if (value === "~")
28
+ return homedir();
29
+ return value.startsWith("~/") ? join(homedir(), value.slice(2)) : value;
30
+ }
31
+ function baseDir() {
32
+ return resolve(expandHome(process.env.CAIRN_AGENTFS_BASE_DIR ?? "~/.cairnkeep"));
33
+ }
34
+ export function resolveMemoryScopePath(scope, options = {}) {
35
+ if (scope === "project") {
36
+ return options.projectId
37
+ ? resolve(baseDir(), "projects", `${options.projectId}.db`)
38
+ : resolve(options.cwd ?? process.cwd(), ".agentfs", "project.db");
39
+ }
40
+ if (scope === "all")
41
+ throw new Error('Scope "all" is read-only; choose one concrete scope.');
42
+ if (!SCOPE_PATTERN.test(scope))
43
+ throw new Error(`Invalid scope "${scope}": must be a concrete kebab-case scope or "project".`);
44
+ const root = baseDir();
45
+ const dbPath = resolve(root, `${scope}.db`);
46
+ const rel = relative(root, dbPath);
47
+ if (!rel || rel.startsWith("..") || isAbsolute(rel))
48
+ throw new Error(`Invalid scope "${scope}": resolves outside the base directory.`);
49
+ return dbPath;
50
+ }
51
+ async function openMemoryScope(scope, create, options = {}) {
52
+ const path = resolveMemoryScopePath(scope, options);
53
+ if (!create && !existsSync(path))
54
+ return null;
55
+ if (create)
56
+ mkdirSync(dirname(path), { recursive: true });
57
+ return AgentFS.open({ id: scope, path });
58
+ }
59
+ function normalizeValue(value) {
60
+ if (typeof value === "string")
61
+ return value;
62
+ if (value === undefined || value === null)
63
+ return "";
64
+ return JSON.stringify(value);
65
+ }
66
+ export function assertWritableMemoryKey(key) {
67
+ if (key === HISTORY_NAMESPACE || key.startsWith(`${HISTORY_NAMESPACE}/`)
68
+ || key === REVIEWED_NAMESPACE || key.startsWith(`${REVIEWED_NAMESPACE}/`)) {
69
+ throw new Error(`Keys under ${HISTORY_NAMESPACE}/ and ${REVIEWED_NAMESPACE}/ are reserved.`);
70
+ }
71
+ }
72
+ function historySnapshotKey(key, timestamp) {
73
+ return `${HISTORY_NAMESPACE}/${key}/${timestamp}-${randomUUID()}`;
74
+ }
75
+ function recordKey(reviewId) {
76
+ return `${REVIEWED_NAMESPACE}/${reviewId}`;
77
+ }
78
+ function parseRecord(value) {
79
+ const parsed = reviewedRecordSchema.safeParse(value);
80
+ if (!parsed.success)
81
+ throw new Error("Stored reviewed-memory provenance is invalid; refusing to continue.");
82
+ return parsed.data;
83
+ }
84
+ async function immediate(agent, operation) {
85
+ const transaction = agent.getDatabase().transaction(operation);
86
+ const run = transaction.immediate;
87
+ return run();
88
+ }
89
+ async function supersedeActiveRecord(agent, key, reviewId, at) {
90
+ for (const entry of await agent.kv.list(`${REVIEWED_NAMESPACE}/`)) {
91
+ const prior = parseRecord(entry.value);
92
+ if (prior.state === "active" && prior.key === key && prior.review_id !== reviewId) {
93
+ await agent.kv.set(entry.key, { ...prior, state: "superseded", superseded_at: at, superseded_by: reviewId });
94
+ }
95
+ }
96
+ }
97
+ async function writeReviewed(agent, scope, reviewId, key, value, typed, appliedAt) {
98
+ await supersedeActiveRecord(agent, key, reviewId, appliedAt);
99
+ let snapshotKey = null;
100
+ if (typed) {
101
+ const changed = await applyReviewedTypedNode({
102
+ agent, scope, review_id: reviewId, key, value, node_type: "memory", tags: [], in_transaction: true,
103
+ });
104
+ snapshotKey = changed.snapshot_key;
105
+ }
106
+ else {
107
+ const previous = await agent.kv.get(key);
108
+ const previousValue = previous === undefined ? undefined : normalizeValue(previous);
109
+ if (previousValue !== undefined && previousValue !== value) {
110
+ snapshotKey = historySnapshotKey(key, appliedAt);
111
+ await agent.kv.set(snapshotKey, {
112
+ value: previousValue,
113
+ superseded_at: appliedAt,
114
+ superseded_reason: `reviewed memory ${reviewId}`,
115
+ });
116
+ }
117
+ await agent.kv.set(key, value);
118
+ }
119
+ await agent.kv.set(recordKey(reviewId), {
120
+ schema_version: 1,
121
+ review_id: reviewId,
122
+ key,
123
+ value_hash: hashText(value),
124
+ state: "active",
125
+ applied_at: appliedAt,
126
+ });
127
+ return snapshotKey;
128
+ }
129
+ export async function applyReviewedMemory(scope, reviewId, key, value, options = {}) {
130
+ assertWritableMemoryKey(key);
131
+ if (!REVIEW_ID_PATTERN.test(reviewId))
132
+ throw new Error("Invalid review id.");
133
+ const agent = await openMemoryScope(scope, true, options);
134
+ if (!agent)
135
+ throw new Error(`Unable to open scope ${scope}.`);
136
+ try {
137
+ return await immediate(agent, async () => {
138
+ const stored = await agent.kv.get(recordKey(reviewId));
139
+ const valueHash = hashText(value);
140
+ if (stored !== undefined) {
141
+ const existing = parseRecord(stored);
142
+ if (existing.key !== key || existing.value_hash !== valueHash)
143
+ throw new Error(`Review id ${reviewId} was already used with different content.`);
144
+ if (existing.state !== "active")
145
+ throw new Error(`Review id ${reviewId} is ${existing.state} and cannot be reapplied.`);
146
+ return { ok: true, scope, review_id: reviewId, key, applied: false, idempotent: true, snapshot_key: null };
147
+ }
148
+ const snapshotKey = await writeReviewed(agent, scope, reviewId, key, value, options.typed ?? false, new Date().toISOString());
149
+ return { ok: true, scope, review_id: reviewId, key, applied: true, idempotent: false, snapshot_key: snapshotKey };
150
+ });
151
+ }
152
+ finally {
153
+ await agent.close();
154
+ }
155
+ }
156
+ export async function invalidateReviewedMemory(scope, reviewId, key, reason, options = {}) {
157
+ assertWritableMemoryKey(key);
158
+ const agent = await openMemoryScope(scope, true, options);
159
+ if (!agent)
160
+ throw new Error(`Unable to open scope ${scope}.`);
161
+ try {
162
+ return await immediate(agent, async () => {
163
+ const keyForRecord = recordKey(reviewId);
164
+ const stored = await agent.kv.get(keyForRecord);
165
+ if (stored === undefined) {
166
+ const at = new Date().toISOString();
167
+ await agent.kv.set(keyForRecord, { schema_version: 1, review_id: reviewId, key, value_hash: "", state: "invalidated", applied_at: at, invalidated_at: at, ...(reason ? { invalidation_reason: reason } : {}) });
168
+ return { ok: true, scope, review_id: reviewId, key, invalidated: true, idempotent: false, missing: true, removed: false, current_changed: false, snapshot_key: null };
169
+ }
170
+ const record = parseRecord(stored);
171
+ if (record.key !== key)
172
+ throw new Error(`Review id ${reviewId} belongs to a different memory key.`);
173
+ if (record.state === "invalidated")
174
+ return { ok: true, scope, review_id: reviewId, key: record.key, invalidated: false, idempotent: true, missing: record.value_hash === "", removed: false, current_changed: false, snapshot_key: null };
175
+ const at = new Date().toISOString();
176
+ let currentChanged = false;
177
+ let removed = false;
178
+ let snapshotKey = null;
179
+ if (record.state === "active") {
180
+ const current = await agent.kv.get(record.key);
181
+ if (current !== undefined) {
182
+ const currentValue = normalizeValue(current);
183
+ if (hashText(currentValue) === record.value_hash) {
184
+ if (options.typed) {
185
+ const changed = await invalidateReviewedTypedNode({ agent, scope, review_id: reviewId, key: record.key, reason, in_transaction: true });
186
+ snapshotKey = changed.snapshot_key;
187
+ }
188
+ else {
189
+ snapshotKey = historySnapshotKey(record.key, at);
190
+ await agent.kv.set(snapshotKey, { value: currentValue, superseded_at: at, superseded_reason: reason ?? `reviewed memory ${reviewId} invalidated` });
191
+ await agent.kv.delete(record.key);
192
+ }
193
+ removed = true;
194
+ }
195
+ else
196
+ currentChanged = true;
197
+ }
198
+ }
199
+ await agent.kv.set(keyForRecord, { ...record, state: "invalidated", invalidated_at: at, ...(reason ? { invalidation_reason: reason } : {}) });
200
+ return { ok: true, scope, review_id: reviewId, key: record.key, invalidated: true, idempotent: false, missing: false, removed, current_changed: currentChanged, snapshot_key: snapshotKey };
201
+ });
202
+ }
203
+ finally {
204
+ await agent.close();
205
+ }
206
+ }
207
+ export async function readMemoryBaseHashes(scope, keys, options = {}) {
208
+ const agent = await openMemoryScope(scope, false, options);
209
+ const result = new Map();
210
+ try {
211
+ for (const key of keys) {
212
+ assertWritableMemoryKey(key);
213
+ const raw = agent ? await agent.kv.get(key) : undefined;
214
+ const value = raw === undefined ? null : normalizeValue(raw);
215
+ result.set(key, { value, hash: value === null ? null : digestText(value) });
216
+ }
217
+ return result;
218
+ }
219
+ finally {
220
+ if (agent)
221
+ await agent.close();
222
+ }
223
+ }
224
+ function proposalReviewId(digest, index) {
225
+ return `mp:${digest}:${index}`;
226
+ }
227
+ export async function applyProposalCandidates(scope, proposalDigest, candidates, options = {}) {
228
+ for (const candidate of candidates)
229
+ assertMemoryProposalCandidateConsistency(candidate);
230
+ const agent = await openMemoryScope(scope, true, options);
231
+ if (!agent)
232
+ throw new Error(`Unable to open scope ${scope}.`);
233
+ try {
234
+ return await immediate(agent, async () => {
235
+ const existing = [];
236
+ for (let index = 0; index < candidates.length; index += 1) {
237
+ const candidate = candidates[index];
238
+ assertWritableMemoryKey(candidate.key);
239
+ const stored = await agent.kv.get(recordKey(proposalReviewId(proposalDigest, index)));
240
+ existing.push(stored === undefined ? null : parseRecord(stored));
241
+ }
242
+ if (existing.every((record) => record !== null)) {
243
+ for (let index = 0; index < candidates.length; index += 1) {
244
+ const record = existing[index];
245
+ const candidate = candidates[index];
246
+ const current = await agent.kv.get(candidate.key);
247
+ if (record.state !== "active" || record.key !== candidate.key || record.value_hash !== hashText(candidate.value)
248
+ || current === undefined || digestText(normalizeValue(current)) !== candidate.value_hash) {
249
+ throw new Error("Proposal replay does not match current reviewed memory state.");
250
+ }
251
+ }
252
+ return { ok: true, scope, proposal_digest: proposalDigest, applied: false, idempotent: true, count: candidates.length, results: [] };
253
+ }
254
+ if (existing.some((record) => record !== null))
255
+ throw new Error("Proposal has partial reviewed provenance; refusing a non-atomic replay.");
256
+ for (const candidate of candidates) {
257
+ const current = await agent.kv.get(candidate.key);
258
+ const currentValue = current === undefined ? null : normalizeValue(current);
259
+ const currentHash = currentValue === null ? null : digestText(currentValue);
260
+ if (currentHash !== candidate.base_hash)
261
+ throw new Error(`Memory proposal is stale at key "${candidate.key}".`);
262
+ const expectedOperation = expectedMemoryProposalOperation(currentHash, candidate.value_hash);
263
+ if (candidate.operation !== expectedOperation) {
264
+ throw new Error(`Memory proposal operation is inconsistent at key "${candidate.key}".`);
265
+ }
266
+ }
267
+ const at = new Date().toISOString();
268
+ const results = [];
269
+ for (let index = 0; index < candidates.length; index += 1) {
270
+ const candidate = candidates[index];
271
+ const reviewId = proposalReviewId(proposalDigest, index);
272
+ const snapshotKey = await writeReviewed(agent, scope, reviewId, candidate.key, candidate.value, options.typed ?? false, at);
273
+ results.push({ key: candidate.key, operation: candidate.operation, review_id: reviewId, snapshot_key: snapshotKey });
274
+ }
275
+ return { ok: true, scope, proposal_digest: proposalDigest, applied: true, idempotent: false, count: candidates.length, results };
276
+ });
277
+ }
278
+ finally {
279
+ await agent.close();
280
+ }
281
+ }
@@ -63,6 +63,12 @@ export const completeWorkEvidenceSchema = z.object({
63
63
  change_digest: digestSchema,
64
64
  patch: workEvidencePatchSchema,
65
65
  }).strict();
66
+ export const CONTEXT_USAGE_OUTCOMES = ["used", "unused", "unknown"];
67
+ export const contextUsageReceiptInputSchema = z.object({
68
+ task_digest: digestSchema,
69
+ result_digest: digestSchema,
70
+ outcome: z.enum(CONTEXT_USAGE_OUTCOMES),
71
+ }).strict();
66
72
  export const workEvidenceLinkSchema = z.discriminatedUnion("kind", [
67
73
  z.object({
68
74
  schema_version: z.literal(WORK_EVIDENCE_SCHEMA_VERSION),
@@ -90,6 +96,17 @@ export const workEvidenceLinkSchema = z.discriminatedUnion("kind", [
90
96
  review_id: safeRefSchema,
91
97
  key: safeRefSchema,
92
98
  }).strict(),
99
+ z.object({
100
+ schema_version: z.literal(WORK_EVIDENCE_SCHEMA_VERSION),
101
+ link_id: digestSchema,
102
+ evidence_id: workEvidenceIdSchema,
103
+ kind: z.literal("context_usage"),
104
+ created_at: z.iso.datetime(),
105
+ receipt_id: digestSchema,
106
+ task_digest: digestSchema,
107
+ result_digest: digestSchema,
108
+ outcome: z.enum(CONTEXT_USAGE_OUTCOMES),
109
+ }).strict(),
93
110
  ]);
94
111
  export const storedWorkEvidenceSchema = z.discriminatedUnion("status", [pendingWorkEvidenceSchema, completeWorkEvidenceSchema]);
95
112
  function truthy(value) {
@@ -5,7 +5,7 @@ import { basename, dirname, join, resolve } from "node:path";
5
5
  import { getArtifactLimits, isArtifactStoreEnabled } from "./artifact-schema.js";
6
6
  import { putArtifact } from "./artifact-store.js";
7
7
  import { hardenPrivatePath, privatePathIsSafe } from "./platform-security.js";
8
- import { WORK_EVIDENCE_SCHEMA_VERSION, completeWorkEvidenceSchema, getWorkEvidenceLimits, isWorkEvidenceEnabled, isWorkEvidencePatchEnabled, pendingWorkEvidenceSchema, storedWorkEvidenceSchema, workEvidenceIdSchema, workEvidenceLinkSchema, } from "./work-evidence-schema.js";
8
+ import { WORK_EVIDENCE_SCHEMA_VERSION, completeWorkEvidenceSchema, contextUsageReceiptInputSchema, getWorkEvidenceLimits, isWorkEvidenceEnabled, isWorkEvidencePatchEnabled, pendingWorkEvidenceSchema, storedWorkEvidenceSchema, workEvidenceIdSchema, workEvidenceLinkSchema, } from "./work-evidence-schema.js";
9
9
  const MAX_RECORD_BYTES = 1024 * 1024;
10
10
  const STALE_PENDING_MS = 24 * 60 * 60 * 1000;
11
11
  const EMPTY_DIGEST = createHash("sha256").update("").digest("hex");
@@ -22,6 +22,13 @@ function canonical(value) {
22
22
  }
23
23
  return JSON.stringify(value);
24
24
  }
25
+ function contextUsageReceiptId(evidenceId, value) {
26
+ return sha256(canonical({
27
+ evidence_id: evidenceId,
28
+ task_digest: value.task_digest,
29
+ result_digest: value.result_digest,
30
+ }));
31
+ }
25
32
  function storeRoot(projectRoot) {
26
33
  return resolve(projectRoot, ".agentfs", "work-evidence", "v1");
27
34
  }
@@ -350,6 +357,48 @@ export async function linkActiveWorkEvidence(projectRoot, value) {
350
357
  return null;
351
358
  }
352
359
  }
360
+ export async function appendContextUsageReceipt(projectRoot, evidenceId, value, now = new Date()) {
361
+ if (!isWorkEvidenceEnabled())
362
+ return null;
363
+ const id = workEvidenceIdSchema.parse(evidenceId);
364
+ const input = contextUsageReceiptInputSchema.parse(value);
365
+ const root = resolve(projectRoot);
366
+ if (!ensureStoreHierarchy(root, false) || !existsSync(recordPath(root, id))) {
367
+ throw new Error(`Work evidence "${id}" not found.`);
368
+ }
369
+ readStored(id, root);
370
+ const receiptId = contextUsageReceiptId(id, input);
371
+ const link = workEvidenceLinkSchema.parse({
372
+ schema_version: WORK_EVIDENCE_SCHEMA_VERSION,
373
+ link_id: receiptId,
374
+ evidence_id: id,
375
+ kind: "context_usage",
376
+ created_at: now.toISOString(),
377
+ receipt_id: receiptId,
378
+ ...input,
379
+ });
380
+ const path = join(linksRoot(root, id), `${receiptId}.json`);
381
+ if (existsSync(path)) {
382
+ const stored = workEvidenceLinkSchema.parse(readJson(path));
383
+ if (stored.kind !== "context_usage"
384
+ || stored.receipt_id !== receiptId
385
+ || stored.task_digest !== input.task_digest
386
+ || stored.result_digest !== input.result_digest
387
+ || stored.outcome !== input.outcome) {
388
+ throw new Error(`Conflicting context usage outcome for receipt "${receiptId}".`);
389
+ }
390
+ return stored;
391
+ }
392
+ atomicJson(path, link);
393
+ return link;
394
+ }
395
+ export async function linkActiveContextUsageReceipt(projectRoot, value) {
396
+ const evidenceId = process.env.CAIRN_WORK_EVIDENCE_ID?.trim();
397
+ if (!evidenceId || !isWorkEvidenceEnabled())
398
+ return null;
399
+ const root = process.env.CAIRN_WORK_EVIDENCE_ROOT?.trim() || projectRoot;
400
+ return appendContextUsageReceipt(resolve(root), evidenceId, value);
401
+ }
353
402
  function storedWorkEvidenceRows(projectRoot, status) {
354
403
  if (!ensureStoreHierarchy(projectRoot, false))
355
404
  return [];
@@ -483,6 +532,13 @@ export function doctorWorkEvidence(projectRoot = process.cwd(), repair = false)
483
532
  const link = workEvidenceLinkSchema.parse(readJson(path));
484
533
  if (link.evidence_id !== evidenceId || `${link.link_id}.json` !== name)
485
534
  throw new Error("mismatch");
535
+ if (link.kind === "context_usage") {
536
+ const expected = contextUsageReceiptId(evidenceId, link);
537
+ if (!existsSync(recordPath(projectRoot, evidenceId))
538
+ || link.receipt_id !== expected
539
+ || link.link_id !== expected)
540
+ throw new Error("invalid context usage receipt identity");
541
+ }
486
542
  links += 1;
487
543
  }
488
544
  catch {
@@ -23,6 +23,7 @@
23
23
  "check:explore-crossref": "node scripts/smoke-explore-crossref.mjs",
24
24
  "check:route-guard": "node scripts/smoke-route-guard.mjs",
25
25
  "check:reviewed-memory": "node scripts/smoke-reviewed-memory.mjs",
26
+ "check:memory-proposals": "node scripts/smoke-memory-proposals.mjs",
26
27
  "check:trajectory-roundtrip": "node scripts/smoke-trajectory-roundtrip.mjs",
27
28
  "check:trajectory-redaction": "node scripts/smoke-trajectory-redaction.mjs",
28
29
  "check:trajectory-retention": "node scripts/smoke-trajectory-retention.mjs",
@@ -49,12 +50,18 @@
49
50
  "check:mcp-trust": "node scripts/smoke-mcp-trust.mjs",
50
51
  "check:pi-mcp-bridge": "node scripts/smoke-pi-mcp-bridge.mjs",
51
52
  "check:context-pack": "node scripts/smoke-context-pack.mjs",
53
+ "check:context-pack-retrieval": "node scripts/smoke-context-pack-retrieval.mjs",
54
+ "check:context-pack-v216-compat": "node scripts/smoke-context-pack-v216-compat.mjs",
55
+ "check:context-usage": "node scripts/smoke-context-usage.mjs",
56
+ "check:domain-retrieval": "node scripts/smoke-domain-retrieval.mjs",
57
+ "check:retrieval-benchmark": "node scripts/smoke-retrieval-benchmark.mjs && node scripts/run-retrieval-benchmark.mjs --check",
58
+ "benchmark:retrieval": "npm run build && node scripts/run-retrieval-benchmark.mjs",
52
59
  "check:work-evidence": "node scripts/smoke-work-evidence.mjs",
53
60
  "check:okf": "node scripts/smoke-okf.mjs",
54
61
  "check:playbook": "node scripts/smoke-playbook.mjs",
55
62
  "check:notes": "npm run build && npm run check:note-signatures && npm run check:note-distillation && npm run check:note-enrichment",
56
63
  "test": "npm run test:smoke",
57
- "test:smoke": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:scope-guard && npm run check:http-guard && npm run check:remote-context && npm run check:explore-guard && npm run check:explore-cache && npm run check:explore-crossref && npm run check:route-guard && npm run check:reviewed-memory && npm run check:trajectory-roundtrip && npm run check:trajectory-redaction && npm run check:trajectory-retention && npm run check:note-signatures && npm run check:note-distillation && npm run check:note-enrichment && npm run check:node-compat && npm run check:typed-nodes && npm run check:memory-import && npm run check:note-mcp && npm run check:node-doctor && npm run check:artifact-store && npm run check:artifact-mcp && npm run check:compaction-capture && npm run check:capability-contract && npm run check:capability-logging && npm run check:capability-harness && npm run check:capability-mcp && npm run check:eval-schema && npm run check:eval-process && npm run check:eval-statistics && npm run check:skill-lifecycle && npm run check:graph-cli && npm run check:mcp-trust && npm run check:pi-mcp-bridge && npm run check:context-pack && npm run check:work-evidence && npm run check:okf && npm run check:playbook",
64
+ "test:smoke": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:scope-guard && npm run check:http-guard && npm run check:remote-context && npm run check:explore-guard && npm run check:explore-cache && npm run check:explore-crossref && npm run check:route-guard && npm run check:reviewed-memory && npm run check:memory-proposals && npm run check:trajectory-roundtrip && npm run check:trajectory-redaction && npm run check:trajectory-retention && npm run check:note-signatures && npm run check:note-distillation && npm run check:note-enrichment && npm run check:node-compat && npm run check:typed-nodes && npm run check:memory-import && npm run check:note-mcp && npm run check:node-doctor && npm run check:artifact-store && npm run check:artifact-mcp && npm run check:compaction-capture && npm run check:capability-contract && npm run check:capability-logging && npm run check:capability-harness && npm run check:capability-mcp && npm run check:eval-schema && npm run check:eval-process && npm run check:eval-statistics && npm run check:skill-lifecycle && npm run check:graph-cli && npm run check:mcp-trust && npm run check:pi-mcp-bridge && npm run check:context-pack && npm run check:context-pack-retrieval && npm run check:context-pack-v216-compat && npm run check:context-usage && npm run check:domain-retrieval && npm run check:retrieval-benchmark && npm run check:work-evidence && npm run check:okf && npm run check:playbook",
58
65
  "test:search": "npm run build && npm run check:search",
59
66
  "test:memory": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:search",
60
67
  "test:all": "npm run test:memory"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cairnkeep/cli",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "description": "A durable, harness-agnostic memory + context layer for coding agents and MCP clients.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",