@cairnkeep/cli 2.3.1 → 2.5.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 +85 -0
- package/README.md +232 -6
- package/bin/cairn +43 -2
- package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
- package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
- package/claude/hooks/compaction-capture.sh +34 -0
- package/claude/hooks/memory-capture.sh +47 -5
- package/claude/hooks/memory-wakeup.sh +55 -2
- package/docs/building-an-overlay.md +4 -3
- package/docs/design/meta-agent.md +439 -0
- package/docs/harness-compatibility.md +117 -0
- package/docs/learning/CURRICULUM-MAP.md +91 -0
- package/docs/learning/FEATURE-GUIDE.md +86 -0
- package/docs/learning/PRODUCTION-PLAN.md +45 -14
- package/docs/learning/README.md +20 -1
- package/docs/learning/instructor-guide.md +6 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +20 -8
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +23 -5
- package/docs/learning/lessons/L03-first-project.md +7 -2
- package/docs/learning/lessons/L04-memory-fundamentals.md +4 -0
- package/docs/learning/lessons/L05-daily-workflow.md +4 -0
- package/docs/learning/lessons/L07-privacy-and-storage.md +3 -0
- package/docs/learning/lessons/L10-context-exploration.md +4 -2
- package/docs/learning/lessons/L12-managed-overlays.md +2 -0
- package/docs/learning/lessons/L13-session-evidence.md +71 -0
- package/docs/learning/lessons/L14-typed-memory.md +51 -0
- package/docs/learning/lessons/L15-capability-governance.md +56 -0
- package/docs/learning/lessons/L16-evaluation.md +67 -0
- package/docs/learning/lessons/L17-agent-boundary.md +55 -0
- package/docs/learning/templates/lesson-template.md +4 -1
- package/docs/learning/tracks/evidence-and-evaluation.md +20 -0
- package/docs/learning/tracks/operator.md +8 -2
- package/docs/learning/tracks/practitioner.md +6 -1
- package/docs/learning/tracks/quickstart.md +3 -0
- package/docs/learning/video-scripts/L00-why-cairnkeep.md +21 -17
- package/docs/learning/video-scripts/L02-installation.md +9 -2
- package/docs/learning/video-scripts/L03-first-project.md +3 -0
- package/docs/operating.md +567 -7
- package/docs/privacy-and-data-flow.md +417 -0
- package/docs/storage.md +252 -0
- package/examples/eval/README.md +42 -0
- package/examples/eval/adapter.json +12 -0
- package/examples/eval/bundled-fake.json +1 -0
- package/examples/eval/task-set.json +1 -0
- package/mcp-memory-server/dist/artifact-cli.js +184 -0
- package/mcp-memory-server/dist/artifact-schema.js +218 -0
- package/mcp-memory-server/dist/artifact-store.js +812 -0
- package/mcp-memory-server/dist/capability-adapter.js +286 -0
- package/mcp-memory-server/dist/capability-cli.js +361 -0
- package/mcp-memory-server/dist/capability-config.js +310 -0
- package/mcp-memory-server/dist/capability-harness.js +496 -0
- package/mcp-memory-server/dist/capability-registry.js +123 -0
- package/mcp-memory-server/dist/capability-schema.js +65 -0
- package/mcp-memory-server/dist/capability-store.js +431 -0
- package/mcp-memory-server/dist/compaction-normalize.js +267 -0
- package/mcp-memory-server/dist/eval-cli.js +523 -0
- package/mcp-memory-server/dist/eval-plan.js +346 -0
- package/mcp-memory-server/dist/eval-process.js +273 -0
- package/mcp-memory-server/dist/eval-report.js +609 -0
- package/mcp-memory-server/dist/eval-runner.js +827 -0
- package/mcp-memory-server/dist/eval-schema.js +368 -0
- package/mcp-memory-server/dist/eval-statistics.js +161 -0
- package/mcp-memory-server/dist/eval-workspace.js +313 -0
- package/mcp-memory-server/dist/failure-signature.js +141 -0
- package/mcp-memory-server/dist/index.js +600 -55
- package/mcp-memory-server/dist/node-cli.js +84 -0
- package/mcp-memory-server/dist/node-schema.js +170 -0
- package/mcp-memory-server/dist/node-store.js +455 -0
- package/mcp-memory-server/dist/note-cli.js +235 -0
- package/mcp-memory-server/dist/note-distiller.js +177 -0
- package/mcp-memory-server/dist/note-enrichment.js +93 -0
- package/mcp-memory-server/dist/note-schema.js +4 -0
- package/mcp-memory-server/dist/note-store.js +985 -0
- package/mcp-memory-server/dist/trajectory-cli.js +141 -0
- package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
- package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
- package/mcp-memory-server/dist/trajectory-schema.js +73 -0
- package/mcp-memory-server/dist/trajectory-store.js +339 -0
- package/mcp-memory-server/package.json +24 -2
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
- package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
- package/opencode/capability-contract/plugins/capability-command.ts +319 -0
- package/opencode/command/graphify.md +25 -14
- package/opencode/plugins/memory-capture.ts +76 -10
- package/opencode/plugins/memory-wakeup.ts +58 -6
- package/package.json +8 -4
- package/pi/extensions/cairnkeep-trajectory.ts +55 -0
- package/schemas/artifact.schema.json +133 -0
- package/schemas/capability-callback.schema.json +68 -0
- package/schemas/capability-contract.schema.json +126 -0
- package/schemas/eval-adapter.schema.json +43 -0
- package/schemas/eval-protocol.schema.json +114 -0
- package/schemas/eval-report.schema.json +423 -0
- package/schemas/eval-task-set.schema.json +114 -0
- package/schemas/memory-node.schema.json +90 -0
- package/schemas/note.schema.json +112 -0
- package/schemas/trajectory-redaction.schema.json +25 -0
- package/schemas/trajectory.schema.json +82 -0
- package/scripts/bootstrap.sh +6 -0
- package/scripts/completion.sh +119 -5
- package/scripts/doctor.sh +284 -0
- package/scripts/fake-eval-adapter.mjs +120 -0
- package/scripts/fixtures/capability-harness-contracts.json +408 -0
- package/scripts/install-audit-timer.sh +3 -1
- package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
- package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
- package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
- package/scripts/memory-wiki-audit.sh +49 -0
- package/scripts/smoke-bash32.sh +9 -0
- package/scripts/sync-claude-assets.sh +99 -6
- package/scripts/sync-opencode-graphify-assets.sh +24 -3
- package/scripts/sync-opencode-plugin-assets.sh +31 -5
- package/scripts/sync-opencode-security-assets.sh +24 -3
- package/scripts/sync-opencode-wiki-assets.sh +24 -3
- package/scripts/sync-pi-assets.sh +72 -0
- package/scripts/test-audit-timer.sh +98 -0
- package/scripts/test-bootstrap-untracked.sh +49 -0
- package/scripts/test-cli-dispatch.sh +41 -2
- package/scripts/test-compaction-hooks.sh +495 -0
- package/scripts/test-completion.sh +77 -0
- package/scripts/test-doctor.sh +348 -1
- package/scripts/test-eval-cli.sh +175 -0
- package/scripts/test-launcher-hooks.sh +630 -3
- package/scripts/test-learning-docs.sh +20 -4
- package/scripts/test-notes-cli.sh +114 -0
- package/scripts/test-package-install.sh +254 -0
- package/scripts/test-phase16-node-lifecycle.sh +50 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
- package/scripts/test-phase18-capability-lifecycle.sh +504 -0
- package/scripts/test-phase18-harness-boundary.sh +1060 -0
- package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
- package/scripts/test-trajectory-cli.sh +87 -0
- package/scripts/test-trajectory-hooks.sh +142 -0
- package/scripts/test-uninstall.sh +231 -5
- package/scripts/uninstall.sh +38 -5
- package/scripts/verify-dependency-security.mjs +121 -0
- package/scripts/verify-docs-parity.sh +527 -1
- package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
- package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
- package/templates/capabilities.json.template +7 -0
- package/templates/env.example.template +51 -0
- package/templates/memory-wiki-audit.service.template +1 -1
- package/templates/start-claude.sh.template +36 -0
- package/templates/start-kimi.sh.template +39 -0
- package/templates/start-opencode.sh.template +47 -5
- package/templates/start-pi.sh.template +30 -0
- package/templates/trajectory-redaction.json.template +4 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { chmod, lstat, mkdir, open, readdir, realpath, rename, rm, stat, } from "node:fs/promises";
|
|
5
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { isCapabilityContractEnabled, resolveCapabilityStatus, } from "./capability-config.js";
|
|
8
|
+
import { operatingCapabilityHandleSchema, } from "./capability-adapter.js";
|
|
9
|
+
import { CAPABILITY_CALLBACK_SCHEMA_VERSION, isOperatingCapabilitySettled, issueOperatingCapability, settleOperatingCapability, } from "./capability-store.js";
|
|
10
|
+
import { isTrajectoryCaptureEnabled } from "./trajectory-schema.js";
|
|
11
|
+
const LEASE_DIRECTORY_NAME = "capability-leases-v1";
|
|
12
|
+
const LEASE_SCHEMA_VERSION = 1;
|
|
13
|
+
const MAX_LEASE_BYTES = 4_096;
|
|
14
|
+
const MAX_LEASES = 10_000;
|
|
15
|
+
const LEASE_TIMEOUT_MS = 30 * 60 * 1_000;
|
|
16
|
+
const harnessSchema = z.enum(["claude-code", "opencode"]);
|
|
17
|
+
const sessionIdSchema = z.string()
|
|
18
|
+
.min(1)
|
|
19
|
+
.max(256)
|
|
20
|
+
.regex(/^(?:cairn:)?[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/)
|
|
21
|
+
.refine((value) => value !== "unknown");
|
|
22
|
+
const projectRootSchema = z.string().min(1).max(4_096);
|
|
23
|
+
const commandSchema = z.enum([
|
|
24
|
+
"wiki-ingest",
|
|
25
|
+
"wiki-query",
|
|
26
|
+
"wiki-lint",
|
|
27
|
+
"graphify",
|
|
28
|
+
"security-audit",
|
|
29
|
+
]);
|
|
30
|
+
export const harnessCapabilityBeforeInputSchema = z.strictObject({
|
|
31
|
+
schema_version: z.literal(LEASE_SCHEMA_VERSION),
|
|
32
|
+
harness: harnessSchema,
|
|
33
|
+
command: commandSchema,
|
|
34
|
+
session_id: sessionIdSchema,
|
|
35
|
+
project_root: projectRootSchema,
|
|
36
|
+
});
|
|
37
|
+
export const harnessCapabilityTerminalInputSchema = z.strictObject({
|
|
38
|
+
schema_version: z.literal(LEASE_SCHEMA_VERSION),
|
|
39
|
+
harness: harnessSchema,
|
|
40
|
+
session_id: sessionIdSchema,
|
|
41
|
+
outcome: z.enum(["success", "error", "timeout", "abandoned"]),
|
|
42
|
+
});
|
|
43
|
+
const harnessCapabilityAbandonInputSchema = z.strictObject({
|
|
44
|
+
schema_version: z.literal(LEASE_SCHEMA_VERSION),
|
|
45
|
+
harness: harnessSchema,
|
|
46
|
+
session_id: sessionIdSchema,
|
|
47
|
+
});
|
|
48
|
+
const harnessCapabilityCwdChangedInputSchema = z.strictObject({
|
|
49
|
+
schema_version: z.literal(LEASE_SCHEMA_VERSION),
|
|
50
|
+
harness: harnessSchema,
|
|
51
|
+
session_id: sessionIdSchema,
|
|
52
|
+
old_cwd: projectRootSchema,
|
|
53
|
+
new_cwd: projectRootSchema,
|
|
54
|
+
});
|
|
55
|
+
const harnessCapabilityLeaseSchema = z.strictObject({
|
|
56
|
+
schema_version: z.literal(LEASE_SCHEMA_VERSION),
|
|
57
|
+
harness: harnessSchema,
|
|
58
|
+
session_id: sessionIdSchema,
|
|
59
|
+
command: commandSchema,
|
|
60
|
+
project_root: projectRootSchema,
|
|
61
|
+
project_identity: z.string().regex(/^[a-f0-9]{64}$/),
|
|
62
|
+
expires_at: z.iso.datetime(),
|
|
63
|
+
phase: z.enum(["active", "terminal"]),
|
|
64
|
+
outcome: z.enum(["success", "error", "timeout", "disabled"]).optional(),
|
|
65
|
+
handle: operatingCapabilityHandleSchema,
|
|
66
|
+
}).superRefine((lease, context) => {
|
|
67
|
+
if ((lease.phase === "terminal") !== (lease.outcome !== undefined)) {
|
|
68
|
+
context.addIssue({ code: "custom", path: ["outcome"], message: "Terminal leases require one outcome." });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const ALLOW = { schema_version: 1, decision: "allow" };
|
|
72
|
+
const BLOCK = {
|
|
73
|
+
schema_version: 1,
|
|
74
|
+
decision: "block",
|
|
75
|
+
reason: "capability-disabled",
|
|
76
|
+
};
|
|
77
|
+
export function commandCapability(command) {
|
|
78
|
+
switch (command) {
|
|
79
|
+
case "wiki-ingest":
|
|
80
|
+
case "wiki-query":
|
|
81
|
+
case "wiki-lint":
|
|
82
|
+
return "wiki";
|
|
83
|
+
case "graphify":
|
|
84
|
+
return "graph";
|
|
85
|
+
case "security-audit":
|
|
86
|
+
return "security.audit";
|
|
87
|
+
default:
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function stateRoot(options = {}) {
|
|
92
|
+
const configured = options.state_root ?? process.env.CAIRN_HARNESS_STATE_DIR;
|
|
93
|
+
const root = configured ?? join(process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "cairn", "harness");
|
|
94
|
+
if (!isAbsolute(root) && configured !== undefined) {
|
|
95
|
+
throw new Error("Harness state root must be absolute.");
|
|
96
|
+
}
|
|
97
|
+
return resolve(root);
|
|
98
|
+
}
|
|
99
|
+
export function getHarnessCapabilityLeaseDirectory(options = {}) {
|
|
100
|
+
const root = stateRoot(options);
|
|
101
|
+
const directory = resolve(root, LEASE_DIRECTORY_NAME);
|
|
102
|
+
const child = relative(root, directory);
|
|
103
|
+
if (child.startsWith("..") || isAbsolute(child)) {
|
|
104
|
+
throw new Error("Harness lease directory is outside local state.");
|
|
105
|
+
}
|
|
106
|
+
return directory;
|
|
107
|
+
}
|
|
108
|
+
function leaseName(harness, sessionId, invocationId) {
|
|
109
|
+
return `${createHash("sha256").update(`${harness}\0${sessionId}\0${invocationId}`, "utf8").digest("hex")}.json`;
|
|
110
|
+
}
|
|
111
|
+
function legacyLeaseName(harness, sessionId) {
|
|
112
|
+
return `${createHash("sha256").update(`${harness}\0${sessionId}`, "utf8").digest("hex")}.json`;
|
|
113
|
+
}
|
|
114
|
+
function leaseNameMatches(lease, name) {
|
|
115
|
+
return name === leaseName(lease.harness, lease.session_id, lease.handle.invocation_id)
|
|
116
|
+
|| name === legacyLeaseName(lease.harness, lease.session_id);
|
|
117
|
+
}
|
|
118
|
+
function leasePath(lease, options = {}) {
|
|
119
|
+
return join(getHarnessCapabilityLeaseDirectory(options), leaseName(lease.harness, lease.session_id, lease.handle.invocation_id));
|
|
120
|
+
}
|
|
121
|
+
async function ensureLeaseDirectory(options = {}) {
|
|
122
|
+
const root = stateRoot(options);
|
|
123
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
124
|
+
const rootInfo = await lstat(root);
|
|
125
|
+
if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink())
|
|
126
|
+
throw new Error("Harness state root is unsafe.");
|
|
127
|
+
const directory = getHarnessCapabilityLeaseDirectory(options);
|
|
128
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
129
|
+
const info = await lstat(directory);
|
|
130
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
131
|
+
throw new Error("Harness lease directory is unsafe.");
|
|
132
|
+
await chmod(directory, 0o700);
|
|
133
|
+
return directory;
|
|
134
|
+
}
|
|
135
|
+
async function canonicalProjectBinding(rawRoot) {
|
|
136
|
+
const projectRoot = await realpath(rawRoot);
|
|
137
|
+
const info = await stat(projectRoot);
|
|
138
|
+
if (!info.isDirectory())
|
|
139
|
+
throw new Error("Harness project root is not a directory.");
|
|
140
|
+
const projectIdentity = createHash("sha256")
|
|
141
|
+
.update(`cairn:harness-project:v1\0${projectRoot}\0${String(info.dev)}\0${String(info.ino)}`, "utf8")
|
|
142
|
+
.digest("hex");
|
|
143
|
+
return { project_root: projectRoot, project_identity: projectIdentity };
|
|
144
|
+
}
|
|
145
|
+
async function bindingMatches(lease) {
|
|
146
|
+
try {
|
|
147
|
+
const binding = await canonicalProjectBinding(lease.project_root);
|
|
148
|
+
return binding.project_root === lease.project_root
|
|
149
|
+
&& binding.project_identity === lease.project_identity;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function writeLease(lease, options = {}) {
|
|
156
|
+
const parsed = harnessCapabilityLeaseSchema.parse(lease);
|
|
157
|
+
const bytes = Buffer.from(`${JSON.stringify(parsed)}\n`, "utf8");
|
|
158
|
+
if (bytes.byteLength > MAX_LEASE_BYTES)
|
|
159
|
+
throw new Error("Harness capability lease is too large.");
|
|
160
|
+
const directory = await ensureLeaseDirectory(options);
|
|
161
|
+
const name = leaseName(parsed.harness, parsed.session_id, parsed.handle.invocation_id);
|
|
162
|
+
const path = join(directory, name);
|
|
163
|
+
let replacing = false;
|
|
164
|
+
try {
|
|
165
|
+
const existing = await lstat(path);
|
|
166
|
+
if (!existing.isFile() || existing.isSymbolicLink())
|
|
167
|
+
throw new Error("Harness capability lease path is unsafe.");
|
|
168
|
+
replacing = true;
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (error.code !== "ENOENT")
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
if (!replacing) {
|
|
175
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
176
|
+
if (entries.filter((entry) => entry.isFile() && /^[a-f0-9]{64}\.json$/.test(entry.name)).length >= MAX_LEASES) {
|
|
177
|
+
throw new Error("Harness capability lease limit reached.");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const temporary = join(directory, `.${name}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
181
|
+
let handle;
|
|
182
|
+
try {
|
|
183
|
+
handle = await open(temporary, "wx", 0o600);
|
|
184
|
+
await handle.writeFile(bytes);
|
|
185
|
+
await handle.sync();
|
|
186
|
+
await handle.close();
|
|
187
|
+
handle = undefined;
|
|
188
|
+
await chmod(temporary, 0o600);
|
|
189
|
+
await rename(temporary, path);
|
|
190
|
+
await chmod(path, 0o600);
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
if (handle)
|
|
194
|
+
await handle.close().catch(() => undefined);
|
|
195
|
+
await rm(temporary, { force: true });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function readLease(path) {
|
|
199
|
+
let handle;
|
|
200
|
+
try {
|
|
201
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
202
|
+
const info = await handle.stat();
|
|
203
|
+
if (!info.isFile() || info.size > MAX_LEASE_BYTES || (info.mode & 0o077) !== 0)
|
|
204
|
+
return undefined;
|
|
205
|
+
const bytes = await handle.readFile();
|
|
206
|
+
if (bytes.byteLength > MAX_LEASE_BYTES)
|
|
207
|
+
return undefined;
|
|
208
|
+
return harnessCapabilityLeaseSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
if (handle)
|
|
215
|
+
await handle.close().catch(() => undefined);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function removeLease(path) {
|
|
219
|
+
await rm(path, { force: true });
|
|
220
|
+
}
|
|
221
|
+
function capabilityState(snapshot, id) {
|
|
222
|
+
const state = snapshot.capabilities.find((row) => row.id === id);
|
|
223
|
+
if (!state)
|
|
224
|
+
throw new Error("Harness capability state is unavailable.");
|
|
225
|
+
return state;
|
|
226
|
+
}
|
|
227
|
+
function makeHandle(input, snapshot, capabilityId, startedAt) {
|
|
228
|
+
const state = capabilityState(snapshot, capabilityId);
|
|
229
|
+
return operatingCapabilityHandleSchema.parse({
|
|
230
|
+
schema_version: CAPABILITY_CALLBACK_SCHEMA_VERSION,
|
|
231
|
+
capability_id: capabilityId,
|
|
232
|
+
invocation_id: `cap:${randomUUID()}`,
|
|
233
|
+
correlation_id: input.session_id,
|
|
234
|
+
harness: input.harness,
|
|
235
|
+
source: "operating-command",
|
|
236
|
+
transport: "harness-command",
|
|
237
|
+
started_at: startedAt,
|
|
238
|
+
state_source: state.source,
|
|
239
|
+
configuration_digest: snapshot.configuration_digest,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function finalRecord(handle, outcome) {
|
|
243
|
+
const finishedAt = new Date().toISOString();
|
|
244
|
+
const errorCode = outcome === "success"
|
|
245
|
+
? undefined
|
|
246
|
+
: outcome === "disabled"
|
|
247
|
+
? "capability-disabled"
|
|
248
|
+
: outcome === "timeout"
|
|
249
|
+
? "callback-timeout"
|
|
250
|
+
: "callback-error";
|
|
251
|
+
return {
|
|
252
|
+
...handle,
|
|
253
|
+
finished_at: finishedAt,
|
|
254
|
+
duration_ms: Math.max(0, Date.parse(finishedAt) - Date.parse(handle.started_at)),
|
|
255
|
+
outcome,
|
|
256
|
+
...(errorCode === undefined ? {} : { error_code: errorCode }),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function leaseFor(input, binding, handle, outcome) {
|
|
260
|
+
return harnessCapabilityLeaseSchema.parse({
|
|
261
|
+
schema_version: LEASE_SCHEMA_VERSION,
|
|
262
|
+
harness: input.harness,
|
|
263
|
+
session_id: input.session_id,
|
|
264
|
+
command: input.command,
|
|
265
|
+
...binding,
|
|
266
|
+
expires_at: new Date(Date.parse(handle.started_at) + LEASE_TIMEOUT_MS).toISOString(),
|
|
267
|
+
phase: outcome === undefined ? "active" : "terminal",
|
|
268
|
+
...(outcome === undefined ? {} : { outcome }),
|
|
269
|
+
handle,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async function settleLease(lease) {
|
|
273
|
+
if (lease.phase !== "terminal" || lease.outcome === undefined)
|
|
274
|
+
return false;
|
|
275
|
+
const settled = await settleOperatingCapability(lease.project_root, lease.handle, finalRecord(lease.handle, lease.outcome));
|
|
276
|
+
return settled || isOperatingCapabilitySettled(lease.project_root, lease.handle);
|
|
277
|
+
}
|
|
278
|
+
async function matchingLeases(input, options = {}) {
|
|
279
|
+
const directory = getHarnessCapabilityLeaseDirectory(options);
|
|
280
|
+
let entries;
|
|
281
|
+
try {
|
|
282
|
+
const info = await lstat(directory);
|
|
283
|
+
if (!info.isDirectory() || info.isSymbolicLink() || (info.mode & 0o077) !== 0)
|
|
284
|
+
return [];
|
|
285
|
+
entries = (await readdir(directory, { withFileTypes: true }))
|
|
286
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
287
|
+
.slice(0, MAX_LEASES);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
const matches = [];
|
|
293
|
+
for (const entry of entries) {
|
|
294
|
+
const path = join(directory, entry.name);
|
|
295
|
+
if (!entry.isFile()) {
|
|
296
|
+
if (entry.isSymbolicLink())
|
|
297
|
+
await removeLease(path);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (!/^[a-f0-9]{64}\.json$/.test(entry.name)) {
|
|
301
|
+
await removeLease(path);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const lease = await readLease(path);
|
|
305
|
+
if (!lease
|
|
306
|
+
|| !leaseNameMatches(lease, entry.name)
|
|
307
|
+
|| !await bindingMatches(lease)) {
|
|
308
|
+
await removeLease(path);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (lease.harness === input.harness && lease.session_id === input.session_id) {
|
|
312
|
+
matches.push({ path, lease });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return matches;
|
|
316
|
+
}
|
|
317
|
+
export async function beginHarnessCapability(rawInput) {
|
|
318
|
+
const input = harnessCapabilityBeforeInputSchema.parse(rawInput);
|
|
319
|
+
if (!isCapabilityContractEnabled())
|
|
320
|
+
return ALLOW;
|
|
321
|
+
const capabilityId = commandCapability(input.command);
|
|
322
|
+
if (!capabilityId)
|
|
323
|
+
return ALLOW;
|
|
324
|
+
let binding;
|
|
325
|
+
try {
|
|
326
|
+
binding = await canonicalProjectBinding(input.project_root);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return ALLOW;
|
|
330
|
+
}
|
|
331
|
+
const snapshot = await resolveCapabilityStatus({ projectRoot: binding.project_root });
|
|
332
|
+
const state = capabilityState(snapshot, capabilityId);
|
|
333
|
+
const measured = snapshot.logging.enabled && isTrajectoryCaptureEnabled();
|
|
334
|
+
if (!state.enabled) {
|
|
335
|
+
if (!measured)
|
|
336
|
+
return BLOCK;
|
|
337
|
+
const handle = makeHandle(input, snapshot, capabilityId, new Date().toISOString());
|
|
338
|
+
const lease = leaseFor(input, binding, handle, "disabled");
|
|
339
|
+
const path = leasePath(lease);
|
|
340
|
+
try {
|
|
341
|
+
await writeLease(lease);
|
|
342
|
+
const issued = await issueOperatingCapability(binding.project_root, handle);
|
|
343
|
+
if (!issued || await settleLease(lease))
|
|
344
|
+
await removeLease(path);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
await removeLease(path).catch(() => undefined);
|
|
348
|
+
}
|
|
349
|
+
return BLOCK;
|
|
350
|
+
}
|
|
351
|
+
if (!measured)
|
|
352
|
+
return ALLOW;
|
|
353
|
+
const handle = makeHandle(input, snapshot, capabilityId, new Date().toISOString());
|
|
354
|
+
const lease = leaseFor(input, binding, handle);
|
|
355
|
+
try {
|
|
356
|
+
await writeLease(lease);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return ALLOW;
|
|
360
|
+
}
|
|
361
|
+
const issued = await issueOperatingCapability(binding.project_root, handle);
|
|
362
|
+
if (!issued)
|
|
363
|
+
await removeLease(leasePath(lease)).catch(() => undefined);
|
|
364
|
+
return ALLOW;
|
|
365
|
+
}
|
|
366
|
+
export async function finishHarnessCapability(rawInput, options = {}) {
|
|
367
|
+
const input = harnessCapabilityTerminalInputSchema.parse(rawInput);
|
|
368
|
+
if (input.outcome === "abandoned") {
|
|
369
|
+
return abandonHarnessCapability({
|
|
370
|
+
schema_version: input.schema_version,
|
|
371
|
+
harness: input.harness,
|
|
372
|
+
session_id: input.session_id,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
const priors = await matchingLeases(input);
|
|
376
|
+
if (priors.length === 0)
|
|
377
|
+
return { schema_version: 1, finalized: false };
|
|
378
|
+
const terminals = priors.map(({ path, lease }) => ({
|
|
379
|
+
path,
|
|
380
|
+
lease: lease.phase === "terminal" ? lease : harnessCapabilityLeaseSchema.parse({
|
|
381
|
+
...lease,
|
|
382
|
+
phase: "terminal",
|
|
383
|
+
outcome: input.outcome,
|
|
384
|
+
}),
|
|
385
|
+
}));
|
|
386
|
+
for (const terminal of terminals)
|
|
387
|
+
await writeLease(terminal.lease);
|
|
388
|
+
if (options.testCrashAt === "before-claim")
|
|
389
|
+
throw crashInjection();
|
|
390
|
+
for (const terminal of terminals)
|
|
391
|
+
await writeLease(terminal.lease);
|
|
392
|
+
if (options.testCrashAt === "after-claim")
|
|
393
|
+
throw crashInjection();
|
|
394
|
+
let finalized = false;
|
|
395
|
+
for (const terminal of terminals) {
|
|
396
|
+
const settled = await settleLease(terminal.lease);
|
|
397
|
+
if (settled) {
|
|
398
|
+
await removeLease(terminal.path);
|
|
399
|
+
finalized = true;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (options.testCrashAt === "after-settlement")
|
|
403
|
+
throw crashInjection();
|
|
404
|
+
return { schema_version: 1, finalized };
|
|
405
|
+
}
|
|
406
|
+
function crashInjection() {
|
|
407
|
+
const error = new Error("Injected harness lifecycle crash.");
|
|
408
|
+
error.name = "HarnessCrashInjection";
|
|
409
|
+
return error;
|
|
410
|
+
}
|
|
411
|
+
export async function abandonHarnessCapability(rawInput) {
|
|
412
|
+
const input = harnessCapabilityAbandonInputSchema.parse(rawInput);
|
|
413
|
+
const priors = await matchingLeases(input);
|
|
414
|
+
let finalized = false;
|
|
415
|
+
for (const prior of priors) {
|
|
416
|
+
if (prior.lease.phase === "terminal") {
|
|
417
|
+
finalized = await settleLease(prior.lease) || finalized;
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
await settleOperatingCapability(prior.lease.project_root, prior.lease.handle);
|
|
421
|
+
}
|
|
422
|
+
await removeLease(prior.path);
|
|
423
|
+
}
|
|
424
|
+
return { schema_version: 1, finalized };
|
|
425
|
+
}
|
|
426
|
+
export async function observeHarnessCwdChanged(rawInput) {
|
|
427
|
+
const input = harnessCapabilityCwdChangedInputSchema.parse(rawInput);
|
|
428
|
+
const priors = await matchingLeases(input);
|
|
429
|
+
return { schema_version: 1, observed: priors.length > 0 };
|
|
430
|
+
}
|
|
431
|
+
export async function recoverHarnessCapabilities(options = {}) {
|
|
432
|
+
let directory;
|
|
433
|
+
try {
|
|
434
|
+
directory = await ensureLeaseDirectory(options);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return { schema_version: 1, recovered: 0, pruned: 0, pending: 0 };
|
|
438
|
+
}
|
|
439
|
+
const entries = (await readdir(directory, { withFileTypes: true }))
|
|
440
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
441
|
+
let recovered = 0;
|
|
442
|
+
let pruned = 0;
|
|
443
|
+
let pending = 0;
|
|
444
|
+
for (const entry of entries.slice(0, MAX_LEASES)) {
|
|
445
|
+
let path = join(directory, entry.name);
|
|
446
|
+
if (!entry.isFile()) {
|
|
447
|
+
if (entry.isSymbolicLink()) {
|
|
448
|
+
await removeLease(path);
|
|
449
|
+
pruned += 1;
|
|
450
|
+
}
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (!/^[a-f0-9]{64}\.json$/.test(entry.name)) {
|
|
454
|
+
await removeLease(path);
|
|
455
|
+
pruned += 1;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const lease = await readLease(path);
|
|
459
|
+
if (!lease
|
|
460
|
+
|| !leaseNameMatches(lease, entry.name)
|
|
461
|
+
|| !await bindingMatches(lease)) {
|
|
462
|
+
await removeLease(path);
|
|
463
|
+
pruned += 1;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (entry.name === legacyLeaseName(lease.harness, lease.session_id)) {
|
|
467
|
+
await writeLease(lease, options);
|
|
468
|
+
await removeLease(path);
|
|
469
|
+
path = leasePath(lease, options);
|
|
470
|
+
}
|
|
471
|
+
let terminal = lease;
|
|
472
|
+
if (lease.phase === "active" && Date.parse(lease.expires_at) <= Date.now()) {
|
|
473
|
+
terminal = harnessCapabilityLeaseSchema.parse({ ...lease, phase: "terminal", outcome: "timeout" });
|
|
474
|
+
await writeLease(terminal, options);
|
|
475
|
+
}
|
|
476
|
+
if (terminal.phase === "terminal") {
|
|
477
|
+
if (await settleLease(terminal)) {
|
|
478
|
+
await removeLease(path);
|
|
479
|
+
recovered += 1;
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
await removeLease(path);
|
|
483
|
+
pruned += 1;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
else
|
|
487
|
+
pending += 1;
|
|
488
|
+
}
|
|
489
|
+
for (const entry of entries.slice(MAX_LEASES)) {
|
|
490
|
+
if (entry.isFile() || entry.isSymbolicLink()) {
|
|
491
|
+
await removeLease(join(directory, entry.name));
|
|
492
|
+
pruned += 1;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return { schema_version: 1, recovered, pruned, pending };
|
|
496
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isNoteDistillationEnabled } from "./note-schema.js";
|
|
4
|
+
import { CAPABILITY_IDS, } from "./capability-schema.js";
|
|
5
|
+
const MAX_COMPATIBILITY_CONFIG_BYTES = 64 * 1024;
|
|
6
|
+
function environmentName(id) {
|
|
7
|
+
return `CAIRN_CAPABILITY_${id.toUpperCase().replaceAll(".", "_")}`;
|
|
8
|
+
}
|
|
9
|
+
async function readGraphCompatibility(projectRoot) {
|
|
10
|
+
const planningDirectory = join(projectRoot, ".planning");
|
|
11
|
+
const configPath = join(planningDirectory, "config.json");
|
|
12
|
+
try {
|
|
13
|
+
const [directoryStat, fileStat] = await Promise.all([
|
|
14
|
+
lstat(planningDirectory),
|
|
15
|
+
lstat(configPath),
|
|
16
|
+
]);
|
|
17
|
+
if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory())
|
|
18
|
+
return false;
|
|
19
|
+
if (fileStat.isSymbolicLink() || !fileStat.isFile())
|
|
20
|
+
return false;
|
|
21
|
+
if (fileStat.size > MAX_COMPATIBILITY_CONFIG_BYTES)
|
|
22
|
+
return false;
|
|
23
|
+
const bytes = await readFile(configPath);
|
|
24
|
+
if (bytes.byteLength > MAX_COMPATIBILITY_CONFIG_BYTES)
|
|
25
|
+
return false;
|
|
26
|
+
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
27
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
28
|
+
return false;
|
|
29
|
+
const graphify = parsed.graphify;
|
|
30
|
+
return Boolean(graphify && typeof graphify === "object" && !Array.isArray(graphify)
|
|
31
|
+
&& graphify.enabled === true);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const alwaysEnabled = () => true;
|
|
38
|
+
export const CAPABILITY_REGISTRY = [
|
|
39
|
+
{
|
|
40
|
+
id: "memory.write",
|
|
41
|
+
kind: "mcp-tool",
|
|
42
|
+
owner: "mcp-memory-server:index#memory_write",
|
|
43
|
+
environment: environmentName("memory.write"),
|
|
44
|
+
compatibility_default: true,
|
|
45
|
+
restart_required: true,
|
|
46
|
+
logging_policy: "final-callback",
|
|
47
|
+
resolveCompatibility: alwaysEnabled,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "memory.search",
|
|
51
|
+
kind: "mcp-tool",
|
|
52
|
+
owner: "mcp-memory-server:index#memory_search",
|
|
53
|
+
environment: environmentName("memory.search"),
|
|
54
|
+
compatibility_default: true,
|
|
55
|
+
restart_required: true,
|
|
56
|
+
logging_policy: "final-callback",
|
|
57
|
+
resolveCompatibility: alwaysEnabled,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "notes.distill",
|
|
61
|
+
kind: "offline-job",
|
|
62
|
+
owner: "mcp-memory-server:note-cli#distill",
|
|
63
|
+
environment: environmentName("notes.distill"),
|
|
64
|
+
compatibility_default: false,
|
|
65
|
+
restart_required: false,
|
|
66
|
+
logging_policy: "final-callback",
|
|
67
|
+
resolveCompatibility: ({ env }) => isNoteDistillationEnabled(env.CAIRN_NOTE_DISTILLATION),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "wiki",
|
|
71
|
+
kind: "operating-workflow",
|
|
72
|
+
owner: "operating-layer:wiki",
|
|
73
|
+
environment: environmentName("wiki"),
|
|
74
|
+
compatibility_default: true,
|
|
75
|
+
restart_required: false,
|
|
76
|
+
logging_policy: "final-callback",
|
|
77
|
+
resolveCompatibility: alwaysEnabled,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "graph",
|
|
81
|
+
kind: "operating-workflow",
|
|
82
|
+
owner: "operating-layer:graph",
|
|
83
|
+
environment: environmentName("graph"),
|
|
84
|
+
compatibility_default: false,
|
|
85
|
+
restart_required: false,
|
|
86
|
+
logging_policy: "final-callback",
|
|
87
|
+
resolveCompatibility: ({ projectRoot, graphifyEnabled }) => graphifyEnabled ?? readGraphCompatibility(projectRoot),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "security.audit",
|
|
91
|
+
kind: "operating-workflow",
|
|
92
|
+
owner: "operating-layer:security-audit",
|
|
93
|
+
environment: environmentName("security.audit"),
|
|
94
|
+
compatibility_default: true,
|
|
95
|
+
restart_required: false,
|
|
96
|
+
logging_policy: "final-callback",
|
|
97
|
+
resolveCompatibility: alwaysEnabled,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "route.check",
|
|
101
|
+
kind: "mcp-tool",
|
|
102
|
+
owner: "mcp-memory-server:index#route_check",
|
|
103
|
+
environment: environmentName("route.check"),
|
|
104
|
+
compatibility_default: true,
|
|
105
|
+
restart_required: true,
|
|
106
|
+
logging_policy: "final-callback",
|
|
107
|
+
resolveCompatibility: alwaysEnabled,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "context.explore",
|
|
111
|
+
kind: "mcp-tool",
|
|
112
|
+
owner: "mcp-memory-server:index#context_explore",
|
|
113
|
+
environment: environmentName("context.explore"),
|
|
114
|
+
compatibility_default: true,
|
|
115
|
+
restart_required: true,
|
|
116
|
+
logging_policy: "final-callback",
|
|
117
|
+
resolveCompatibility: alwaysEnabled,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
if (CAPABILITY_REGISTRY.length !== CAPABILITY_IDS.length
|
|
121
|
+
|| CAPABILITY_REGISTRY.some((row, index) => row.id !== CAPABILITY_IDS[index])) {
|
|
122
|
+
throw new Error("Capability registry order does not match the canonical capability IDs.");
|
|
123
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const CAPABILITY_SCHEMA_VERSION = 1;
|
|
3
|
+
export const CAPABILITY_IDS = [
|
|
4
|
+
"memory.write",
|
|
5
|
+
"memory.search",
|
|
6
|
+
"notes.distill",
|
|
7
|
+
"wiki",
|
|
8
|
+
"graph",
|
|
9
|
+
"security.audit",
|
|
10
|
+
"route.check",
|
|
11
|
+
"context.explore",
|
|
12
|
+
];
|
|
13
|
+
export const CAPABILITY_KINDS = [
|
|
14
|
+
"mcp-tool",
|
|
15
|
+
"offline-job",
|
|
16
|
+
"operating-workflow",
|
|
17
|
+
];
|
|
18
|
+
export const CAPABILITY_SOURCES = [
|
|
19
|
+
"environment",
|
|
20
|
+
"project",
|
|
21
|
+
"compatibility",
|
|
22
|
+
];
|
|
23
|
+
export const CAPABILITY_ISSUE_CODES = [
|
|
24
|
+
"invalid-config",
|
|
25
|
+
"unknown-capability",
|
|
26
|
+
"invalid-capability-value",
|
|
27
|
+
"invalid-logging-value",
|
|
28
|
+
];
|
|
29
|
+
export const capabilityIdSchema = z.enum(CAPABILITY_IDS);
|
|
30
|
+
export const capabilityKindSchema = z.enum(CAPABILITY_KINDS);
|
|
31
|
+
export const capabilitySourceSchema = z.enum(CAPABILITY_SOURCES);
|
|
32
|
+
export const capabilityIssueCodeSchema = z.enum(CAPABILITY_ISSUE_CODES);
|
|
33
|
+
export const capabilityOverridesSchema = z.partialRecord(capabilityIdSchema.or(z.never()), z.boolean());
|
|
34
|
+
export const capabilityLoggingConfigSchema = z.strictObject({
|
|
35
|
+
callbacks: z.boolean().optional(),
|
|
36
|
+
});
|
|
37
|
+
export const capabilityManagedConfigSchema = z.strictObject({
|
|
38
|
+
schema_version: z.literal(CAPABILITY_SCHEMA_VERSION),
|
|
39
|
+
capabilities: capabilityOverridesSchema,
|
|
40
|
+
logging: capabilityLoggingConfigSchema,
|
|
41
|
+
});
|
|
42
|
+
export const capabilityIssueSchema = z.strictObject({
|
|
43
|
+
code: capabilityIssueCodeSchema,
|
|
44
|
+
capability_id: capabilityIdSchema.optional(),
|
|
45
|
+
setting: z.literal("logging.callbacks").optional(),
|
|
46
|
+
});
|
|
47
|
+
export const capabilityStatusRowSchema = z.strictObject({
|
|
48
|
+
id: capabilityIdSchema,
|
|
49
|
+
kind: capabilityKindSchema,
|
|
50
|
+
enabled: z.boolean(),
|
|
51
|
+
source: capabilitySourceSchema,
|
|
52
|
+
restart_required: z.boolean(),
|
|
53
|
+
});
|
|
54
|
+
export const capabilityLoggingStatusSchema = z.strictObject({
|
|
55
|
+
enabled: z.boolean(),
|
|
56
|
+
source: capabilitySourceSchema,
|
|
57
|
+
});
|
|
58
|
+
export const capabilityStatusSchema = z.strictObject({
|
|
59
|
+
schema_version: z.literal(CAPABILITY_SCHEMA_VERSION),
|
|
60
|
+
contract_enabled: z.boolean(),
|
|
61
|
+
logging: capabilityLoggingStatusSchema,
|
|
62
|
+
configuration_digest: z.string().regex(/^[a-f0-9]{64}$/),
|
|
63
|
+
capabilities: z.array(capabilityStatusRowSchema).length(CAPABILITY_IDS.length),
|
|
64
|
+
issues: z.array(capabilityIssueSchema),
|
|
65
|
+
});
|