@cairnkeep/cli 2.11.0 → 2.13.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.
- package/CHANGELOG.md +33 -1
- package/README.md +51 -7
- package/docs/building-an-overlay.md +1 -1
- package/docs/harness-compatibility.md +40 -1
- package/docs/learning/CURRICULUM-MAP.md +10 -7
- package/docs/learning/FEATURE-GUIDE.md +14 -5
- package/docs/learning/README.md +4 -3
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +16 -9
- package/docs/learning/lessons/L03-first-project.md +34 -15
- package/docs/learning/lessons/L13-session-evidence.md +48 -2
- package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
- package/docs/learning/lessons/L21-context-packs.md +1 -1
- package/docs/learning/lessons/L22-native-windows.md +9 -9
- package/docs/learning/lessons/L23-guided-setup.md +6 -2
- package/docs/learning/tracks/evidence-and-evaluation.md +2 -1
- package/docs/learning/tracks/quickstart.md +5 -4
- package/docs/native-windows.md +23 -0
- package/docs/operating.md +69 -5
- package/docs/privacy-and-data-flow.md +44 -1
- package/docs/quickstart.md +91 -0
- package/docs/storage.md +26 -0
- package/docs/work-evidence.md +80 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/artifact-cli.js +6 -2
- package/mcp-memory-server/dist/index.js +27 -0
- package/mcp-memory-server/dist/mcp-tool-catalog.js +2 -0
- package/mcp-memory-server/dist/trajectory-cli.js +9 -3
- package/mcp-memory-server/dist/work-evidence-cli.js +121 -0
- package/mcp-memory-server/dist/work-evidence-schema.js +121 -0
- package/mcp-memory-server/dist/work-evidence-store.js +516 -0
- package/mcp-memory-server/package.json +2 -1
- package/package.json +2 -1
- package/schemas/cairnkeep-setup-policy.schema.json +3 -3
- package/schemas/cairnkeep-setup.schema.json +2 -2
- package/scripts/bootstrap.sh +7 -15
- package/scripts/cairn-cli.mjs +2 -0
- package/scripts/completion.sh +19 -9
- package/scripts/doctor.sh +35 -5
- package/scripts/harness-registry.mjs +126 -0
- package/scripts/setup-core.mjs +4 -2
- package/scripts/setup-reconcile.mjs +3 -2
- package/scripts/setup.mjs +41 -5
- package/scripts/test-bootstrap-untracked.sh +2 -1
- package/scripts/test-cli-dispatch.sh +3 -1
- package/scripts/test-completion.sh +6 -0
- package/scripts/test-harness-registry.sh +94 -0
- package/scripts/test-package-install.sh +9 -0
- package/scripts/test-phase19-eval-lifecycle.sh +12 -2
- package/scripts/test-setup-compatibility.mjs +5 -4
- package/scripts/test-uninstall.sh +10 -0
- package/scripts/test-windows-native.mjs +27 -1
- package/scripts/test-work-evidence-cli.sh +89 -0
- package/scripts/uninstall.sh +7 -3
- package/scripts/verify-docs-parity.sh +36 -0
- package/scripts/windows-platform.mjs +28 -7
- package/templates/codex-config.toml.template +5 -0
- package/templates/env.example.template +10 -0
- package/templates/start-claude.sh.template +18 -0
- package/templates/start-codex.sh.template +48 -0
- package/templates/start-kimi.sh.template +17 -0
- package/templates/start-opencode.sh.template +17 -0
- package/templates/start-pi.sh.template +17 -0
- package/templates/start-qwen.sh.template +17 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"identifier":"cairn-offline-fake-v1","package_version":"2.
|
|
1
|
+
{"identifier":"cairn-offline-fake-v1","package_version":"2.13.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
|
|
@@ -82,7 +82,9 @@ async function captureClaude(projectRoot, args) {
|
|
|
82
82
|
});
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
|
-
await putArtifact(projectRoot, artifactInput(normalized));
|
|
85
|
+
const result = await putArtifact(projectRoot, artifactInput(normalized));
|
|
86
|
+
const { linkActiveWorkEvidence } = await import("./work-evidence-store.js");
|
|
87
|
+
await linkActiveWorkEvidence(projectRoot, { kind: "artifact", artifact_id: result.artifact.artifact_id });
|
|
86
88
|
}
|
|
87
89
|
async function captureOpenCode(projectRoot) {
|
|
88
90
|
const raw = JSON.parse(await readStdin());
|
|
@@ -95,7 +97,9 @@ async function captureOpenCode(projectRoot) {
|
|
|
95
97
|
await recordUnsupportedCompactionAdapter(projectRoot, { harness: "opencode", harness_version: version, reason: "unsupported_version" });
|
|
96
98
|
return;
|
|
97
99
|
}
|
|
98
|
-
await putArtifact(projectRoot, artifactInput(normalized));
|
|
100
|
+
const result = await putArtifact(projectRoot, artifactInput(normalized));
|
|
101
|
+
const { linkActiveWorkEvidence } = await import("./work-evidence-store.js");
|
|
102
|
+
await linkActiveWorkEvidence(projectRoot, { kind: "artifact", artifact_id: result.artifact.artifact_id });
|
|
99
103
|
}
|
|
100
104
|
async function recover(projectRoot, args) {
|
|
101
105
|
const listed = await listArtifacts(projectRoot, { kind: "compaction_summary" });
|
|
@@ -20,6 +20,7 @@ import { attachNodeMetadata, applyReviewedTypedNode, commitMemoryImport, createT
|
|
|
20
20
|
import { contextPackHttpEnabled, contextPacksEnabled, listVisibleContext, readVisibleContext, searchVisibleContext } from "./context-pack.js";
|
|
21
21
|
import { metadataForTool } from "./mcp-tool-catalog.js";
|
|
22
22
|
import { profileAllowsTool, resolveMcpToolProfile } from "./mcp-tool-profile.js";
|
|
23
|
+
import { isWorkEvidenceEnabled } from "./work-evidence-schema.js";
|
|
23
24
|
const MINIMUM_NODE_MAJOR = 22;
|
|
24
25
|
const nodeMajor = Number.parseInt(process.versions.node, 10);
|
|
25
26
|
if (!Number.isInteger(nodeMajor) || nodeMajor < MINIMUM_NODE_MAJOR) {
|
|
@@ -871,6 +872,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProf
|
|
|
871
872
|
const typedNodesEnabled = isTypedMemoryNodesEnabled();
|
|
872
873
|
const artifactToolsEnabled = isArtifactStoreEnabled() && (!context.remote || isArtifactHttpEnabled());
|
|
873
874
|
const contextPackToolsEnabled = contextPacksEnabled() && (!context.remote || contextPackHttpEnabled());
|
|
875
|
+
const workEvidenceToolsEnabled = isWorkEvidenceEnabled() && !context.remote;
|
|
874
876
|
if (artifactToolsEnabled && context.remote && !context.projectId) {
|
|
875
877
|
throw new ClientContextError("Remote artifact access requires X-Cairn-Project.");
|
|
876
878
|
}
|
|
@@ -936,6 +938,8 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProf
|
|
|
936
938
|
const { putArtifact } = await import("./artifact-store.js");
|
|
937
939
|
const result = await putArtifact(projectRoot, { ...input, session_ref: sessionRef });
|
|
938
940
|
const artifact = result.artifact;
|
|
941
|
+
const { linkActiveWorkEvidence } = await import("./work-evidence-store.js");
|
|
942
|
+
await linkActiveWorkEvidence(projectRoot, { kind: "artifact", artifact_id: artifact.artifact_id });
|
|
939
943
|
const payload = {
|
|
940
944
|
schema_version: artifact.schema_version,
|
|
941
945
|
artifact_id: artifact.artifact_id,
|
|
@@ -1506,6 +1510,8 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProf
|
|
|
1506
1510
|
snapshot_key: snapshotKey,
|
|
1507
1511
|
};
|
|
1508
1512
|
});
|
|
1513
|
+
const { linkActiveWorkEvidence } = await import("./work-evidence-store.js");
|
|
1514
|
+
await linkActiveWorkEvidence(process.cwd(), { kind: "reviewed_memory", scope, review_id, key });
|
|
1509
1515
|
return {
|
|
1510
1516
|
content: [{ type: "text", text: asToolText(payload) }],
|
|
1511
1517
|
structuredContent: payload,
|
|
@@ -1929,6 +1935,27 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProf
|
|
|
1929
1935
|
return { content: [{ type: "text", text: asToolText(payload) }], structuredContent: payload };
|
|
1930
1936
|
});
|
|
1931
1937
|
}
|
|
1938
|
+
if (workEvidenceToolsEnabled) {
|
|
1939
|
+
registerTool("work_evidence_list", {
|
|
1940
|
+
description: "List bounded local Git-linked work-evidence records for this project.",
|
|
1941
|
+
inputSchema: z.object({
|
|
1942
|
+
status: z.enum(["pending", "complete"]).optional(),
|
|
1943
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
1944
|
+
}).strict(),
|
|
1945
|
+
}, async ({ status, limit }) => {
|
|
1946
|
+
const { listWorkEvidence } = await import("./work-evidence-store.js");
|
|
1947
|
+
const payload = listWorkEvidence(process.cwd(), { status, limit });
|
|
1948
|
+
return { content: [{ type: "text", text: asToolText(payload) }], structuredContent: payload };
|
|
1949
|
+
});
|
|
1950
|
+
registerTool("work_evidence_read", {
|
|
1951
|
+
description: "Read one local Git-linked work-evidence record by exact ID or unambiguous prefix.",
|
|
1952
|
+
inputSchema: z.object({ evidence_id: z.string().min(4).max(40).regex(/^wev_[0-9a-f-]*$/i) }).strict(),
|
|
1953
|
+
}, async ({ evidence_id }) => {
|
|
1954
|
+
const { readWorkEvidence } = await import("./work-evidence-store.js");
|
|
1955
|
+
const payload = readWorkEvidence(evidence_id, process.cwd());
|
|
1956
|
+
return { content: [{ type: "text", text: asToolText(payload) }], structuredContent: payload };
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1932
1959
|
return server;
|
|
1933
1960
|
}
|
|
1934
1961
|
function createMemoryServer(context = {}) {
|
|
@@ -35,6 +35,8 @@ export const MCP_TOOL_CATALOG = {
|
|
|
35
35
|
context_pack_list: observation("List context packs"),
|
|
36
36
|
context_pack_search: observation("Search context packs"),
|
|
37
37
|
context_pack_read: observation("Read context pack file"),
|
|
38
|
+
work_evidence_list: observation("List work evidence"),
|
|
39
|
+
work_evidence_read: observation("Read work evidence"),
|
|
38
40
|
};
|
|
39
41
|
export const MCP_TOOL_NAMES = Object.freeze(Object.keys(MCP_TOOL_CATALOG));
|
|
40
42
|
export function isCairnToolName(value) {
|
|
@@ -62,7 +62,9 @@ async function captureClaude(args) {
|
|
|
62
62
|
const normalized = await normalizeClaudeTranscript(transcriptPath, projectRoot);
|
|
63
63
|
const redacted = redactTrajectory(normalized, projectRoot);
|
|
64
64
|
const fitted = fitTrajectoryToBytes(redacted, limits.sessionMaxBytes);
|
|
65
|
-
await putTrajectory(projectRoot, fitted, limits);
|
|
65
|
+
const stored = await putTrajectory(projectRoot, fitted, limits);
|
|
66
|
+
const { linkActiveWorkEvidence } = await import("./work-evidence-store.js");
|
|
67
|
+
await linkActiveWorkEvidence(projectRoot, { kind: "trajectory", trajectory_id: stored.session_id });
|
|
66
68
|
}
|
|
67
69
|
async function captureOpenCode(args) {
|
|
68
70
|
const [projectRoot] = args;
|
|
@@ -72,7 +74,9 @@ async function captureOpenCode(args) {
|
|
|
72
74
|
const normalized = normalizeOpenCodeSession(JSON.parse(await readStdin()), projectRoot);
|
|
73
75
|
const redacted = redactTrajectory(normalized, projectRoot);
|
|
74
76
|
const fitted = fitTrajectoryToBytes(redacted, limits.sessionMaxBytes);
|
|
75
|
-
await putTrajectory(projectRoot, fitted, limits);
|
|
77
|
+
const stored = await putTrajectory(projectRoot, fitted, limits);
|
|
78
|
+
const { linkActiveWorkEvidence } = await import("./work-evidence-store.js");
|
|
79
|
+
await linkActiveWorkEvidence(projectRoot, { kind: "trajectory", trajectory_id: stored.session_id });
|
|
76
80
|
}
|
|
77
81
|
async function capturePi(args) {
|
|
78
82
|
const [projectRoot] = args;
|
|
@@ -82,7 +86,9 @@ async function capturePi(args) {
|
|
|
82
86
|
const normalized = normalizePiSession(JSON.parse(await readStdin()), projectRoot);
|
|
83
87
|
const redacted = redactTrajectory(normalized, projectRoot);
|
|
84
88
|
const fitted = fitTrajectoryToBytes(redacted, limits.sessionMaxBytes);
|
|
85
|
-
await putTrajectory(projectRoot, fitted, limits);
|
|
89
|
+
const stored = await putTrajectory(projectRoot, fitted, limits);
|
|
90
|
+
const { linkActiveWorkEvidence } = await import("./work-evidence-store.js");
|
|
91
|
+
await linkActiveWorkEvidence(projectRoot, { kind: "trajectory", trajectory_id: stored.session_id });
|
|
86
92
|
}
|
|
87
93
|
async function main() {
|
|
88
94
|
const [command = "help", ...args] = process.argv.slice(2);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { constants as osConstants } from "node:os";
|
|
4
|
+
import { workEvidenceHarnessSchema } from "./work-evidence-schema.js";
|
|
5
|
+
import { activeEvidenceEnvironment, deleteWorkEvidence, doctorWorkEvidence, finishWorkEvidence, listWorkEvidence, pruneWorkEvidence, readWorkEvidence, startWorkEvidence, } from "./work-evidence-store.js";
|
|
6
|
+
import { getWorkEvidenceLimits, isWorkEvidenceEnabled } from "./work-evidence-schema.js";
|
|
7
|
+
process.stdout.on("error", (error) => {
|
|
8
|
+
if (error.code === "EPIPE")
|
|
9
|
+
process.exit(0);
|
|
10
|
+
throw error;
|
|
11
|
+
});
|
|
12
|
+
function usage() {
|
|
13
|
+
return `cairn evidence — opt-in local Git-linked work evidence
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
cairn evidence list [--status pending|complete] [--json]
|
|
17
|
+
cairn evidence show <evidence-id-or-prefix> [--json]
|
|
18
|
+
cairn evidence delete <evidence-id-or-prefix> [--dry-run] [--json]
|
|
19
|
+
cairn evidence prune [--dry-run] [--json]
|
|
20
|
+
cairn evidence doctor [--repair] [--json]
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
function hasFlag(args, flag) {
|
|
24
|
+
return args.includes(flag);
|
|
25
|
+
}
|
|
26
|
+
function option(args, name) {
|
|
27
|
+
const index = args.indexOf(name);
|
|
28
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
29
|
+
}
|
|
30
|
+
function humanList(value) {
|
|
31
|
+
if (value.evidence.length === 0)
|
|
32
|
+
return "No local work evidence found.";
|
|
33
|
+
return value.evidence.map((row) => [row.evidence_id, row.status, row.harness, row.started_at, `${row.links.length} links`].join("\t")).join("\n");
|
|
34
|
+
}
|
|
35
|
+
async function runHarness(args) {
|
|
36
|
+
const separator = args.indexOf("--");
|
|
37
|
+
const harness = workEvidenceHarnessSchema.parse(option(args.slice(0, separator < 0 ? args.length : separator), "--harness"));
|
|
38
|
+
const command = separator >= 0 ? args[separator + 1] : undefined;
|
|
39
|
+
const commandArgs = separator >= 0 ? args.slice(separator + 2) : [];
|
|
40
|
+
if (!command)
|
|
41
|
+
throw new Error("run requires --harness NAME -- COMMAND [ARG...].");
|
|
42
|
+
let evidence;
|
|
43
|
+
if (isWorkEvidenceEnabled()) {
|
|
44
|
+
try {
|
|
45
|
+
evidence = startWorkEvidence(process.cwd(), harness);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
process.stderr.write("cairn evidence: capture unavailable; launching without work evidence\n");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const env = evidence ? { ...process.env, ...activeEvidenceEnvironment(evidence, process.cwd()) } : process.env;
|
|
52
|
+
const result = spawnSync(command, commandArgs, { cwd: process.cwd(), env, stdio: "inherit", shell: false, windowsHide: false });
|
|
53
|
+
const signalNumber = result.signal ? osConstants.signals[result.signal] : undefined;
|
|
54
|
+
const status = result.status ?? (signalNumber ? 128 + signalNumber : 1);
|
|
55
|
+
if (evidence) {
|
|
56
|
+
try {
|
|
57
|
+
await finishWorkEvidence(process.cwd(), evidence.evidence_id, status);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
process.stderr.write("cairn evidence: final capture failed; the evidence remains pending\n");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (result.error)
|
|
64
|
+
throw result.error;
|
|
65
|
+
process.exitCode = status;
|
|
66
|
+
}
|
|
67
|
+
async function main() {
|
|
68
|
+
const [command = "help", ...args] = process.argv.slice(2);
|
|
69
|
+
try {
|
|
70
|
+
if (command === "run") {
|
|
71
|
+
await runHarness(args);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (command === "list") {
|
|
75
|
+
const status = option(args, "--status");
|
|
76
|
+
if (status && !["pending", "complete"].includes(status))
|
|
77
|
+
throw new Error("--status must be pending or complete.");
|
|
78
|
+
const value = listWorkEvidence(process.cwd(), { status });
|
|
79
|
+
process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(value) : humanList(value)}\n`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (command === "show") {
|
|
83
|
+
const identifier = args.find((arg) => !arg.startsWith("-"));
|
|
84
|
+
if (!identifier)
|
|
85
|
+
throw new Error("show requires an evidence ID or unambiguous prefix.");
|
|
86
|
+
const value = readWorkEvidence(identifier, process.cwd());
|
|
87
|
+
process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(value) : JSON.stringify(value, null, 2)}\n`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (command === "delete") {
|
|
91
|
+
const identifier = args.find((arg) => !arg.startsWith("-"));
|
|
92
|
+
if (!identifier)
|
|
93
|
+
throw new Error("delete requires an evidence ID or unambiguous prefix.");
|
|
94
|
+
const value = deleteWorkEvidence(identifier, process.cwd(), hasFlag(args, "--dry-run"));
|
|
95
|
+
process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(value) : `${value.dry_run ? "Would delete" : "Deleted"} ${value.evidence_id}.`}\n`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (command === "prune") {
|
|
99
|
+
const value = pruneWorkEvidence(process.cwd(), getWorkEvidenceLimits(), { dryRun: hasFlag(args, "--dry-run") });
|
|
100
|
+
process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(value) : `${value.dry_run ? "Would remove" : "Removed"} ${value.removed.length} evidence record(s).`}\n`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (command === "doctor") {
|
|
104
|
+
const value = doctorWorkEvidence(process.cwd(), hasFlag(args, "--repair"));
|
|
105
|
+
process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(value) : JSON.stringify(value, null, 2)}\n`);
|
|
106
|
+
if (!value.ok)
|
|
107
|
+
process.exitCode = 2;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (["help", "--help", "-h"].includes(command)) {
|
|
111
|
+
process.stdout.write(usage());
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`unknown evidence command "${command}".`);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
process.stderr.write(`cairn evidence: ${error instanceof Error ? error.message : "unknown error"}\n`);
|
|
118
|
+
process.exitCode = 2;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
await main();
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const WORK_EVIDENCE_SCHEMA_VERSION = 1;
|
|
3
|
+
export const WORK_EVIDENCE_HARNESSES = ["claude", "opencode", "pi", "kimi", "qwen", "codex"];
|
|
4
|
+
export const WORK_EVIDENCE_DEFAULT_RETENTION_DAYS = 30;
|
|
5
|
+
export const WORK_EVIDENCE_DEFAULT_STORE_MAX_BYTES = 64 * 1024 * 1024;
|
|
6
|
+
export const WORK_EVIDENCE_DEFAULT_MAX_TOUCHED_PATHS = 4096;
|
|
7
|
+
export const WORK_EVIDENCE_DEFAULT_PATCH_MAX_BYTES = 1024 * 1024;
|
|
8
|
+
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
9
|
+
const commitSchema = z.string().regex(/^[a-f0-9]{40,64}$/);
|
|
10
|
+
const safeRefSchema = z.string().min(1).max(512).refine((value) => !/[\u0000-\u001f\u007f]/.test(value));
|
|
11
|
+
const pathLabelSchema = z.string().min(1).max(4096).refine((value) => (!value.startsWith("/")
|
|
12
|
+
&& !value.includes("\\")
|
|
13
|
+
&& !value.split("/").some((segment) => segment === "" || segment === "." || segment === "..")
|
|
14
|
+
&& !/[\u0000-\u001f\u007f]/.test(value)), "Touched paths must be safe repository-relative labels.");
|
|
15
|
+
export const workEvidenceIdSchema = z.string().regex(/^wev_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
|
|
16
|
+
export const workEvidenceHarnessSchema = z.enum(WORK_EVIDENCE_HARNESSES);
|
|
17
|
+
export const gitEvidenceSnapshotSchema = z.object({
|
|
18
|
+
head_commit: commitSchema.nullable(),
|
|
19
|
+
branch: safeRefSchema.nullable(),
|
|
20
|
+
detached: z.boolean(),
|
|
21
|
+
unborn: z.boolean(),
|
|
22
|
+
dirty: z.boolean(),
|
|
23
|
+
status_digest: digestSchema,
|
|
24
|
+
workspace_diff_digest: digestSchema,
|
|
25
|
+
}).strict();
|
|
26
|
+
const pathFingerprintSchema = z.object({
|
|
27
|
+
path: pathLabelSchema,
|
|
28
|
+
state_digest: digestSchema,
|
|
29
|
+
}).strict();
|
|
30
|
+
export const pendingWorkEvidenceSchema = z.object({
|
|
31
|
+
schema_version: z.literal(WORK_EVIDENCE_SCHEMA_VERSION),
|
|
32
|
+
evidence_id: workEvidenceIdSchema,
|
|
33
|
+
status: z.literal("pending"),
|
|
34
|
+
harness: workEvidenceHarnessSchema,
|
|
35
|
+
started_at: z.iso.datetime(),
|
|
36
|
+
start: gitEvidenceSnapshotSchema,
|
|
37
|
+
path_fingerprints: z.array(pathFingerprintSchema).max(WORK_EVIDENCE_DEFAULT_MAX_TOUCHED_PATHS),
|
|
38
|
+
}).strict();
|
|
39
|
+
export const workEvidencePatchSchema = z.object({
|
|
40
|
+
requested: z.boolean(),
|
|
41
|
+
scope: z.literal("end-worktree-vs-start-commit").nullable(),
|
|
42
|
+
artifact_id: z.string().regex(/^art_[0-9a-f-]{36}$/i).nullable(),
|
|
43
|
+
unavailable_reason: z.enum([
|
|
44
|
+
"not-requested",
|
|
45
|
+
"artifact-store-disabled",
|
|
46
|
+
"unborn-start",
|
|
47
|
+
"unchanged",
|
|
48
|
+
"capture-failed",
|
|
49
|
+
]).nullable(),
|
|
50
|
+
}).strict();
|
|
51
|
+
export const completeWorkEvidenceSchema = z.object({
|
|
52
|
+
schema_version: z.literal(WORK_EVIDENCE_SCHEMA_VERSION),
|
|
53
|
+
evidence_id: workEvidenceIdSchema,
|
|
54
|
+
status: z.literal("complete"),
|
|
55
|
+
harness: workEvidenceHarnessSchema,
|
|
56
|
+
started_at: z.iso.datetime(),
|
|
57
|
+
ended_at: z.iso.datetime(),
|
|
58
|
+
exit_status: z.number().int().min(0).max(255),
|
|
59
|
+
start: gitEvidenceSnapshotSchema,
|
|
60
|
+
end: gitEvidenceSnapshotSchema,
|
|
61
|
+
touched_paths: z.array(pathLabelSchema).max(WORK_EVIDENCE_DEFAULT_MAX_TOUCHED_PATHS),
|
|
62
|
+
omitted_touched_paths: z.number().int().nonnegative(),
|
|
63
|
+
change_digest: digestSchema,
|
|
64
|
+
patch: workEvidencePatchSchema,
|
|
65
|
+
}).strict();
|
|
66
|
+
export const workEvidenceLinkSchema = z.discriminatedUnion("kind", [
|
|
67
|
+
z.object({
|
|
68
|
+
schema_version: z.literal(WORK_EVIDENCE_SCHEMA_VERSION),
|
|
69
|
+
link_id: digestSchema,
|
|
70
|
+
evidence_id: workEvidenceIdSchema,
|
|
71
|
+
kind: z.literal("trajectory"),
|
|
72
|
+
created_at: z.iso.datetime(),
|
|
73
|
+
trajectory_id: safeRefSchema,
|
|
74
|
+
}).strict(),
|
|
75
|
+
z.object({
|
|
76
|
+
schema_version: z.literal(WORK_EVIDENCE_SCHEMA_VERSION),
|
|
77
|
+
link_id: digestSchema,
|
|
78
|
+
evidence_id: workEvidenceIdSchema,
|
|
79
|
+
kind: z.literal("artifact"),
|
|
80
|
+
created_at: z.iso.datetime(),
|
|
81
|
+
artifact_id: z.string().regex(/^art_[0-9a-f-]{36}$/i),
|
|
82
|
+
}).strict(),
|
|
83
|
+
z.object({
|
|
84
|
+
schema_version: z.literal(WORK_EVIDENCE_SCHEMA_VERSION),
|
|
85
|
+
link_id: digestSchema,
|
|
86
|
+
evidence_id: workEvidenceIdSchema,
|
|
87
|
+
kind: z.literal("reviewed_memory"),
|
|
88
|
+
created_at: z.iso.datetime(),
|
|
89
|
+
scope: safeRefSchema,
|
|
90
|
+
review_id: safeRefSchema,
|
|
91
|
+
key: safeRefSchema,
|
|
92
|
+
}).strict(),
|
|
93
|
+
]);
|
|
94
|
+
export const storedWorkEvidenceSchema = z.discriminatedUnion("status", [pendingWorkEvidenceSchema, completeWorkEvidenceSchema]);
|
|
95
|
+
function truthy(value) {
|
|
96
|
+
return /^(?:1|true|yes|on)$/i.test(value?.trim() ?? "");
|
|
97
|
+
}
|
|
98
|
+
function integerSetting(name, fallback, minimum) {
|
|
99
|
+
const raw = process.env[name]?.trim();
|
|
100
|
+
if (!raw)
|
|
101
|
+
return fallback;
|
|
102
|
+
const value = Number(raw);
|
|
103
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
104
|
+
throw new Error(`${name} must be an integer greater than or equal to ${minimum}.`);
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
export function isWorkEvidenceEnabled(value = process.env.CAIRN_WORK_EVIDENCE) {
|
|
109
|
+
return truthy(value);
|
|
110
|
+
}
|
|
111
|
+
export function isWorkEvidencePatchEnabled(value = process.env.CAIRN_WORK_EVIDENCE_PATCH) {
|
|
112
|
+
return truthy(value);
|
|
113
|
+
}
|
|
114
|
+
export function getWorkEvidenceLimits() {
|
|
115
|
+
return {
|
|
116
|
+
retentionDays: integerSetting("CAIRN_WORK_EVIDENCE_RETENTION_DAYS", WORK_EVIDENCE_DEFAULT_RETENTION_DAYS, 0),
|
|
117
|
+
storeMaxBytes: integerSetting("CAIRN_WORK_EVIDENCE_STORE_MAX_BYTES", WORK_EVIDENCE_DEFAULT_STORE_MAX_BYTES, 1024),
|
|
118
|
+
maxTouchedPaths: Math.min(integerSetting("CAIRN_WORK_EVIDENCE_MAX_TOUCHED_PATHS", WORK_EVIDENCE_DEFAULT_MAX_TOUCHED_PATHS, 1), WORK_EVIDENCE_DEFAULT_MAX_TOUCHED_PATHS),
|
|
119
|
+
patchMaxBytes: integerSetting("CAIRN_WORK_EVIDENCE_PATCH_MAX_BYTES", WORK_EVIDENCE_DEFAULT_PATCH_MAX_BYTES, 1024),
|
|
120
|
+
};
|
|
121
|
+
}
|