@kisev/skills-opencode 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,776 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { lstat } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, resolve } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { fileURLToPath } from "node:url";
8
+ import { applyTransaction, consumeReceipt, deploymentRoot, destination, digest, LifecycleError, lifecycleRoot, listDirectRegular, readRegular, recoverTransaction, saveReceipt, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
9
+ export const FIXED_AGENT_ROLES = [
10
+ "manager",
11
+ "architect",
12
+ "mapper",
13
+ "worker",
14
+ "review",
15
+ "critic",
16
+ ];
17
+ export class AgentProfileError extends LifecycleError {
18
+ }
19
+ const PACKAGE_NAME = "@kisev/skills-opencode";
20
+ const CONFIG_PATH = ".skills-opencode/agent-profiles.json";
21
+ const MANIFEST_PATH = ".skills-opencode/agent-profiles.manifest.json";
22
+ const AGENTS_DIRECTORY = "agents";
23
+ const NAME_PATTERN = /^(?:manager|architect|mapper|worker|review|critic)$/;
24
+ const CRITIC_PATTERN = /^critic-[a-z0-9]+(?:-[a-z0-9]+)*$/;
25
+ const MODEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*\/[A-Za-z0-9][A-Za-z0-9_./-]*$/;
26
+ const VARIANT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
27
+ const execFileAsync = promisify(execFile);
28
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
29
+ const assetsRoot = resolve(packageRoot, "assets", "agents");
30
+ const V1_AGENT_SHA256 = {
31
+ architect: "3894c4ea5719d8945809157f1611fe5d1ea0ef2461a6092d94c18948baca1bee",
32
+ critic: "17396c401c6680f95394c2a2d004fc8c9409d845fad99bee0b6cc485fd923e74",
33
+ manager: "6b6e2d6a845aebf0b3d5609e461021ece07f80a70618e7ba2d8679c2592e7570",
34
+ mapper: "d98a1c84d718cf3394a9e2274318973784770280b4cbbc53f968819bde3bf7eb",
35
+ review: "ae33393964e9e9faf5c9c87884d8746d8f80d85f4ddf854d442bd0f44f50d95f",
36
+ worker: "eee1163111b59856cc95c8be4ef4bc85a12e23c05bf576ece9f8e6a933b5466d",
37
+ };
38
+ function packageVersion() {
39
+ const value = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8"));
40
+ if (typeof value.version !== "string")
41
+ throw new AgentProfileError("invalid_package", "Package version is unavailable");
42
+ return value.version;
43
+ }
44
+ function emptyConfig() {
45
+ return {
46
+ schema_version: 1,
47
+ fixed: Object.fromEntries(FIXED_AGENT_ROLES.map((role) => [role, {}])),
48
+ additional_critics: {},
49
+ };
50
+ }
51
+ export function validateAgentName(name) {
52
+ if (NAME_PATTERN.test(name) || CRITIC_PATTERN.test(name))
53
+ return name;
54
+ throw new AgentProfileError("invalid_name", "Agent must be a fixed role or critic-<safe-suffix>");
55
+ }
56
+ export function validateModel(model) {
57
+ if (!MODEL_PATTERN.test(model))
58
+ throw new AgentProfileError("invalid_model", "Model must be an exact provider/model value");
59
+ return model;
60
+ }
61
+ export function validateVariant(variant) {
62
+ if (variant === null || variant === undefined || variant === "")
63
+ return undefined;
64
+ if (!VARIANT_PATTERN.test(variant))
65
+ throw new AgentProfileError("invalid_variant", "Variant must be a safe exact value");
66
+ return variant;
67
+ }
68
+ function parseSelection(value, required) {
69
+ if (!value || typeof value !== "object" || Array.isArray(value))
70
+ throw new AgentProfileError("invalid_configuration", "Agent selection must be an object");
71
+ const entry = value;
72
+ const keys = Object.keys(entry).sort();
73
+ if (!entry.model && !required && keys.length === 0)
74
+ return {};
75
+ if (typeof entry.model !== "string")
76
+ throw new AgentProfileError("invalid_configuration", "Agent model is required");
77
+ const selection = { model: validateModel(entry.model) };
78
+ if (entry.variant !== undefined) {
79
+ if (typeof entry.variant !== "string")
80
+ throw new AgentProfileError("invalid_configuration", "Agent variant must be a string");
81
+ selection.variant = validateVariant(entry.variant);
82
+ }
83
+ if (keys.some((key) => key !== "model" && key !== "variant"))
84
+ throw new AgentProfileError("invalid_configuration", "Agent selection contains unknown fields");
85
+ return selection;
86
+ }
87
+ function parseConfig(raw) {
88
+ let value;
89
+ try {
90
+ value = JSON.parse(raw.toString("utf8"));
91
+ }
92
+ catch {
93
+ throw new AgentProfileError("invalid_configuration", "Agent profile configuration is not valid JSON");
94
+ }
95
+ const config = value;
96
+ if (config.schema_version !== 1 ||
97
+ !config.fixed ||
98
+ typeof config.fixed !== "object" ||
99
+ Array.isArray(config.fixed) ||
100
+ !config.additional_critics ||
101
+ typeof config.additional_critics !== "object" ||
102
+ Array.isArray(config.additional_critics)) {
103
+ throw new AgentProfileError("invalid_configuration", "Agent profile configuration has an unsupported schema");
104
+ }
105
+ if (Object.keys(config.fixed).sort().join(",") !== [...FIXED_AGENT_ROLES].sort().join(","))
106
+ throw new AgentProfileError("invalid_configuration", "Configuration must contain every fixed role exactly once");
107
+ const fixed = Object.fromEntries(FIXED_AGENT_ROLES.map((role) => [role, parseSelection(config.fixed[role], false)]));
108
+ const additional = {};
109
+ for (const [name, selection] of Object.entries(config.additional_critics)) {
110
+ if (!CRITIC_PATTERN.test(name))
111
+ throw new AgentProfileError("invalid_configuration", `Unsafe additional critic name: ${name}`);
112
+ additional[name] = parseSelection(selection, true);
113
+ }
114
+ return {
115
+ schema_version: 1,
116
+ fixed,
117
+ additional_critics: Object.fromEntries(Object.entries(additional).sort(([left], [right]) => left.localeCompare(right))),
118
+ };
119
+ }
120
+ function parseManifest(raw, scope) {
121
+ let value;
122
+ try {
123
+ value = JSON.parse(raw.toString("utf8"));
124
+ }
125
+ catch {
126
+ throw new AgentProfileError("invalid_manifest", "Agent deployment manifest is not valid JSON");
127
+ }
128
+ const manifest = value;
129
+ if (manifest.schema_version !== 1 ||
130
+ manifest.package !== PACKAGE_NAME ||
131
+ typeof manifest.package_version !== "string" ||
132
+ manifest.scope !== scope ||
133
+ !Array.isArray(manifest.critic_pool) ||
134
+ !manifest.profiles ||
135
+ typeof manifest.profiles !== "object" ||
136
+ Array.isArray(manifest.profiles)) {
137
+ throw new AgentProfileError("invalid_manifest", "Agent deployment manifest has an unsupported schema");
138
+ }
139
+ const names = Object.keys(manifest.profiles).sort();
140
+ const expectedPool = names.filter((name) => name === "critic" || CRITIC_PATTERN.test(name));
141
+ if (manifest.critic_pool.join(",") !== expectedPool.join(","))
142
+ throw new AgentProfileError("invalid_manifest", "Agent deployment manifest has a non-exact critic pool");
143
+ for (const [name, record] of Object.entries(manifest.profiles)) {
144
+ validateAgentName(name);
145
+ if (!record ||
146
+ typeof record !== "object" ||
147
+ !["fixed", "additional-critic"].includes(record.kind) ||
148
+ !FIXED_AGENT_ROLES.includes(record.template) ||
149
+ ![record.canonical_sha256, record.configuration_sha256, record.rendered_sha256].every((item) => typeof item === "string" && /^[a-f0-9]{64}$/.test(item))) {
150
+ throw new AgentProfileError("invalid_manifest", `Invalid deployment record: ${name}`);
151
+ }
152
+ if ((NAME_PATTERN.test(name) && record.kind !== "fixed") ||
153
+ (CRITIC_PATTERN.test(name) && record.kind !== "additional-critic") ||
154
+ (record.kind === "additional-critic" && record.template !== "critic")) {
155
+ throw new AgentProfileError("invalid_manifest", `Deployment ownership does not match profile name: ${name}`);
156
+ }
157
+ }
158
+ return manifest;
159
+ }
160
+ async function loadState(scope, root) {
161
+ const configPath = destination(root, CONFIG_PATH);
162
+ const manifestPath = destination(root, MANIFEST_PATH);
163
+ const [configRaw, manifestRaw] = await Promise.all([
164
+ readRegular(configPath),
165
+ readRegular(manifestPath),
166
+ ]);
167
+ if (configRaw && (await inspectFileMode(configPath)) !== 0o600)
168
+ throw new AgentProfileError("unsafe_configuration", "Agent profile configuration must use mode 0600");
169
+ if (manifestRaw && (await inspectFileMode(manifestPath)) !== 0o600)
170
+ throw new AgentProfileError("unsafe_manifest", "Agent deployment manifest must use mode 0600");
171
+ return {
172
+ config: configRaw ? parseConfig(configRaw) : emptyConfig(),
173
+ configRaw,
174
+ manifest: manifestRaw ? parseManifest(manifestRaw, scope) : undefined,
175
+ manifestRaw,
176
+ };
177
+ }
178
+ async function canonicalAssets() {
179
+ return Object.fromEntries(await Promise.all(FIXED_AGENT_ROLES.map(async (role) => {
180
+ const path = resolve(assetsRoot, `${role}.md`);
181
+ const content = await readRegular(path);
182
+ if (!content)
183
+ throw new AgentProfileError("asset_error", `Canonical agent asset is missing: ${role}`);
184
+ return [role, content];
185
+ })));
186
+ }
187
+ function withSelection(content, selection) {
188
+ if (!("model" in selection))
189
+ return content;
190
+ const lines = content.split("\n");
191
+ const end = lines.indexOf("---", 1);
192
+ if (lines[0] !== "---" || end < 2)
193
+ throw new AgentProfileError("asset_error", "Canonical agent frontmatter is invalid");
194
+ const filtered = lines.filter((line, index) => index > end || !/^(model|variant):/.test(line));
195
+ const mode = filtered.findIndex((line, index) => index < end && line.startsWith("mode:"));
196
+ if (mode < 0)
197
+ throw new AgentProfileError("asset_error", "Canonical agent has no mode");
198
+ filtered.splice(mode + 1, 0, `model: ${selection.model}`, ...(selection.variant ? [`variant: ${selection.variant}`] : []));
199
+ return filtered.join("\n");
200
+ }
201
+ function replaceTaskAllowlist(content, allowed) {
202
+ const lines = content.split("\n");
203
+ const permission = lines.indexOf("permission:");
204
+ if (permission < 0)
205
+ throw new AgentProfileError("asset_error", "Canonical primary agent has no permission block");
206
+ const task = lines.findIndex((line, index) => index > permission && line === " task:");
207
+ if (task < 0)
208
+ throw new AgentProfileError("asset_error", "Canonical primary agent has no task permission");
209
+ let end = task + 1;
210
+ while (end < lines.length && (lines[end].startsWith(" ") || lines[end] === ""))
211
+ end += 1;
212
+ lines.splice(task, end - task, " task:", ' "*": deny', ...allowed.map((name) => ` ${name}: allow`));
213
+ return lines.join("\n");
214
+ }
215
+ export function renderAgentProfile(name, config, canonical) {
216
+ validateAgentName(name);
217
+ const role = NAME_PATTERN.test(name) ? name : "critic";
218
+ const selection = NAME_PATTERN.test(name) ? config.fixed[role] : config.additional_critics[name];
219
+ if (!selection)
220
+ throw new AgentProfileError("invalid_configuration", `No configuration exists for ${name}`);
221
+ let rendered = withSelection(canonical[role].toString("utf8"), selection);
222
+ const pool = ["critic", ...Object.keys(config.additional_critics)].sort();
223
+ if (name === "manager")
224
+ rendered = replaceTaskAllowlist(rendered, ["architect", "worker", "mapper", ...pool]);
225
+ if (name === "review")
226
+ rendered = replaceTaskAllowlist(rendered, pool);
227
+ return Buffer.from(rendered);
228
+ }
229
+ function selectionFor(config, name) {
230
+ return NAME_PATTERN.test(name)
231
+ ? config.fixed[name]
232
+ : config.additional_critics[name];
233
+ }
234
+ function desiredNames(config) {
235
+ return [...FIXED_AGENT_ROLES, ...Object.keys(config.additional_critics)].sort();
236
+ }
237
+ function desiredManifest(scope, config, rendered, canonical) {
238
+ const profiles = {};
239
+ for (const name of desiredNames(config)) {
240
+ const role = NAME_PATTERN.test(name) ? name : "critic";
241
+ const selection = selectionFor(config, name);
242
+ profiles[name] = {
243
+ kind: NAME_PATTERN.test(name) ? "fixed" : "additional-critic",
244
+ template: role,
245
+ canonical_sha256: sha256(canonical[role]),
246
+ configuration_sha256: digest(selection),
247
+ rendered_sha256: sha256(rendered[name]),
248
+ };
249
+ }
250
+ return {
251
+ schema_version: 1,
252
+ package: PACKAGE_NAME,
253
+ package_version: packageVersion(),
254
+ scope,
255
+ critic_pool: desiredNames(config).filter((name) => name === "critic" || CRITIC_PATTERN.test(name)),
256
+ profiles,
257
+ };
258
+ }
259
+ function changeConfig(current, request) {
260
+ const config = JSON.parse(JSON.stringify(current));
261
+ if (request.action === "model-set") {
262
+ const name = validateAgentName(request.name ?? "");
263
+ if (!NAME_PATTERN.test(name) && !(name in config.additional_critics))
264
+ throw new AgentProfileError("unknown_profile", `Additional critic is not configured: ${name}`);
265
+ const selection = { model: validateModel(request.model ?? "") };
266
+ const variant = validateVariant(request.variant);
267
+ if (variant)
268
+ selection.variant = variant;
269
+ if (NAME_PATTERN.test(name))
270
+ config.fixed[name] = selection;
271
+ else
272
+ config.additional_critics[name] = selection;
273
+ }
274
+ else if (request.action === "critic-add") {
275
+ const name = request.name ?? "";
276
+ if (!CRITIC_PATTERN.test(name))
277
+ throw new AgentProfileError("invalid_name", "Additional critic must match critic-<safe-suffix>");
278
+ if (name in config.additional_critics)
279
+ throw new AgentProfileError("profile_exists", `Additional critic already exists: ${name}`);
280
+ const selection = { model: validateModel(request.model ?? "") };
281
+ const variant = validateVariant(request.variant);
282
+ if (variant)
283
+ selection.variant = variant;
284
+ config.additional_critics[name] = selection;
285
+ }
286
+ else if (request.action === "critic-remove") {
287
+ const name = request.name ?? "";
288
+ if (name === "critic" || NAME_PATTERN.test(name))
289
+ throw new AgentProfileError("immutable_profile", "Standard critic and fixed roles cannot be removed or renamed");
290
+ if (!CRITIC_PATTERN.test(name))
291
+ throw new AgentProfileError("invalid_name", "Additional critic must match critic-<safe-suffix>");
292
+ if (!(name in config.additional_critics))
293
+ throw new AgentProfileError("unknown_profile", `Additional critic is not configured: ${name}`);
294
+ delete config.additional_critics[name];
295
+ }
296
+ config.additional_critics = Object.fromEntries(Object.entries(config.additional_critics).sort(([left], [right]) => left.localeCompare(right)));
297
+ return config;
298
+ }
299
+ function legacyIsExact(legacy, current) {
300
+ if (!legacy ||
301
+ legacy.manifest.schema_version !== 1 ||
302
+ legacy.manifest.package !== PACKAGE_NAME ||
303
+ legacy.manifest.version !== "1.0.0")
304
+ return false;
305
+ return FIXED_AGENT_ROLES.every((role) => {
306
+ const path = `agents/${role}.md`;
307
+ const record = legacy.manifest.files[path];
308
+ const content = current.get(`${role}.md`);
309
+ return (record?.sha256 === V1_AGENT_SHA256[role] &&
310
+ content !== undefined &&
311
+ sha256(content) === record.sha256);
312
+ });
313
+ }
314
+ function planDigestBase(plan, inventoryDigest, request) {
315
+ return digest({ plan, inventory_digest: inventoryDigest, request });
316
+ }
317
+ export async function buildAgentProfilePlan(request, scope, cwd = process.cwd(), home = homedir(), legacy) {
318
+ const root = deploymentRoot(scope, cwd, home);
319
+ const [state, canonical, agentFiles] = await Promise.all([
320
+ loadState(scope, root),
321
+ canonicalAssets(),
322
+ listDirectRegular(destination(root, AGENTS_DIRECTORY)),
323
+ ]);
324
+ const byFile = new Map(agentFiles.filter((item) => item.name.endsWith(".md")).map((item) => [item.name, item.content]));
325
+ const config = changeConfig(state.config, request);
326
+ const names = request.action === "uninstall" ? [] : desiredNames(config);
327
+ const rendered = Object.fromEntries(names.map((name) => [name, renderAgentProfile(name, config, canonical)]));
328
+ const desired = request.action === "uninstall"
329
+ ? undefined
330
+ : desiredManifest(scope, config, rendered, canonical);
331
+ const legacyExact = !state.manifest && legacyIsExact(legacy, byFile);
332
+ const legacyTransferred = legacyExact ? FIXED_AGENT_ROLES.map((role) => `agents/${role}.md`) : [];
333
+ const operations = [];
334
+ const mutations = [];
335
+ const currentManifest = state.manifest;
336
+ const allOwnedNames = new Set([
337
+ ...(currentManifest ? Object.keys(currentManifest.profiles) : []),
338
+ ...names,
339
+ ]);
340
+ for (const name of [...allOwnedNames].sort()) {
341
+ const path = `agents/${name}.md`;
342
+ const content = byFile.get(`${name}.md`);
343
+ const record = currentManifest?.profiles[name];
344
+ const next = rendered[name];
345
+ const adopted = legacyExact && NAME_PATTERN.test(name);
346
+ if (!next) {
347
+ if (!record)
348
+ continue;
349
+ if (!content) {
350
+ operations.push({
351
+ path,
352
+ operation: "unchanged",
353
+ reason: "managed profile is already missing",
354
+ });
355
+ }
356
+ else if (sha256(content) !== record.rendered_sha256) {
357
+ operations.push({
358
+ path,
359
+ operation: "conflict",
360
+ reason: "managed profile drift is preserved",
361
+ sha256: sha256(content),
362
+ });
363
+ }
364
+ else {
365
+ operations.push({
366
+ path,
367
+ operation: "remove",
368
+ reason: "explicit profile uninstall or critic removal",
369
+ });
370
+ mutations.push({ path, operation: "remove", expected: { sha256: record.rendered_sha256 } });
371
+ }
372
+ continue;
373
+ }
374
+ if (!record && !adopted) {
375
+ if (content) {
376
+ operations.push({ path, operation: "conflict", reason: "exact-name user-owned collision" });
377
+ }
378
+ else {
379
+ operations.push({ path, operation: "create", reason: "package profile deployment" });
380
+ mutations.push({
381
+ path,
382
+ operation: "write",
383
+ content: next,
384
+ mode: 0o600,
385
+ expected: { absent: true },
386
+ });
387
+ }
388
+ continue;
389
+ }
390
+ const ownedHash = adopted ? V1_AGENT_SHA256[name] : record.rendered_sha256;
391
+ const drift = !adopted &&
392
+ (!content ||
393
+ sha256(content) !== ownedHash ||
394
+ agentFiles.find((item) => item.name === `${name}.md`)?.mode !== 0o600);
395
+ if (drift && request.action !== "reconcile") {
396
+ operations.push({
397
+ path,
398
+ operation: "conflict",
399
+ reason: "managed profile drift requires explicit reconcile",
400
+ ...(content ? { sha256: sha256(content) } : {}),
401
+ });
402
+ continue;
403
+ }
404
+ if (!content) {
405
+ operations.push({ path, operation: "create", reason: "explicit managed profile reconcile" });
406
+ mutations.push({
407
+ path,
408
+ operation: "write",
409
+ content: next,
410
+ mode: 0o600,
411
+ expected: { absent: true },
412
+ });
413
+ }
414
+ else if (!content.equals(next) ||
415
+ agentFiles.find((item) => item.name === `${name}.md`)?.mode !== 0o600) {
416
+ operations.push({
417
+ path,
418
+ operation: "update",
419
+ reason: drift
420
+ ? "explicit managed profile reconcile"
421
+ : adopted
422
+ ? "v1.0.0 ownership transfer"
423
+ : "model, variant, critic pool, or package update",
424
+ });
425
+ mutations.push({
426
+ path,
427
+ operation: "write",
428
+ content: next,
429
+ mode: 0o600,
430
+ expected: { sha256: sha256(content) },
431
+ });
432
+ }
433
+ else {
434
+ operations.push({ path, operation: "unchanged", reason: "rendered profile is current" });
435
+ }
436
+ }
437
+ const configContent = Buffer.from(`${stable(config)}\n`);
438
+ if (request.action !== "uninstall" &&
439
+ (!state.configRaw || !state.configRaw.equals(configContent))) {
440
+ operations.push({
441
+ path: CONFIG_PATH,
442
+ operation: state.configRaw ? "update" : "create",
443
+ reason: "separate user profile configuration",
444
+ });
445
+ mutations.push({
446
+ path: CONFIG_PATH,
447
+ operation: "write",
448
+ content: configContent,
449
+ mode: 0o600,
450
+ expected: state.configRaw ? { sha256: sha256(state.configRaw) } : { absent: true },
451
+ });
452
+ }
453
+ const remainingDrift = operations
454
+ .filter((item) => item.operation === "conflict" && item.reason.includes("drift"))
455
+ .map((item) => item.path.replace(/^agents\//, "").replace(/\.md$/, ""));
456
+ let finalManifest = desired;
457
+ if (request.action === "uninstall" && currentManifest && remainingDrift.length) {
458
+ finalManifest = {
459
+ ...currentManifest,
460
+ profiles: Object.fromEntries(remainingDrift.map((name) => [name, currentManifest.profiles[name]])),
461
+ critic_pool: remainingDrift.filter((name) => name === "critic" || CRITIC_PATTERN.test(name)),
462
+ };
463
+ }
464
+ const manifestContent = finalManifest ? Buffer.from(`${stable(finalManifest)}\n`) : undefined;
465
+ if (manifestContent && (!state.manifestRaw || !state.manifestRaw.equals(manifestContent))) {
466
+ operations.push({
467
+ path: MANIFEST_PATH,
468
+ operation: state.manifestRaw ? "update" : "create",
469
+ reason: "semantic rendered ownership manifest",
470
+ });
471
+ mutations.push({
472
+ path: MANIFEST_PATH,
473
+ operation: "write",
474
+ content: manifestContent,
475
+ mode: 0o600,
476
+ expected: state.manifestRaw ? { sha256: sha256(state.manifestRaw) } : { absent: true },
477
+ });
478
+ }
479
+ else if (!manifestContent && state.manifestRaw && !remainingDrift.length) {
480
+ operations.push({
481
+ path: MANIFEST_PATH,
482
+ operation: "remove",
483
+ reason: "profile deployment uninstalled",
484
+ });
485
+ mutations.push({
486
+ path: MANIFEST_PATH,
487
+ operation: "remove",
488
+ expected: { sha256: sha256(state.manifestRaw) },
489
+ });
490
+ }
491
+ const inventoryDigest = digest({
492
+ package_version: packageVersion(),
493
+ canonical: Object.fromEntries(FIXED_AGENT_ROLES.map((role) => [role, sha256(canonical[role])])),
494
+ desired_configuration: config,
495
+ desired_manifest: finalManifest ?? null,
496
+ config: state.configRaw?.toString("base64") ?? null,
497
+ manifest: state.manifestRaw?.toString("base64") ?? null,
498
+ agents: agentFiles.map((item) => ({
499
+ name: item.name,
500
+ sha256: sha256(item.content),
501
+ mode: item.mode,
502
+ })),
503
+ });
504
+ const base = {
505
+ schema_version: 1,
506
+ domain: "agent-profiles",
507
+ action: request.action,
508
+ scope,
509
+ root,
510
+ operations: operations.sort((left, right) => left.path.localeCompare(right.path)),
511
+ critic_pool: desired?.critic_pool ?? finalManifest?.critic_pool ?? [],
512
+ requires_restart: mutations.some((item) => item.path.startsWith("agents/")),
513
+ };
514
+ const plan = {
515
+ ...base,
516
+ digest: planDigestBase(base, inventoryDigest, request),
517
+ };
518
+ return {
519
+ plan,
520
+ mutations,
521
+ inventoryDigest,
522
+ config,
523
+ manifest: finalManifest,
524
+ expectedConfig: request.action === "uninstall" ? state.configRaw : configContent,
525
+ expectedManifest: manifestContent,
526
+ legacyTransferred,
527
+ };
528
+ }
529
+ export async function validateBuiltAgentProfilePlan(built) {
530
+ const configPath = destination(built.plan.root, CONFIG_PATH);
531
+ const manifestPath = destination(built.plan.root, MANIFEST_PATH);
532
+ const [config, manifest] = await Promise.all([
533
+ readRegular(configPath),
534
+ readRegular(manifestPath),
535
+ ]);
536
+ if ((built.expectedConfig && (!config || !config.equals(built.expectedConfig))) ||
537
+ (!built.expectedConfig && config))
538
+ throw new AgentProfileError("final_validation_failed", "Agent profile configuration does not match the planned state");
539
+ if ((built.expectedManifest && (!manifest || !manifest.equals(built.expectedManifest))) ||
540
+ (!built.expectedManifest && manifest))
541
+ throw new AgentProfileError("final_validation_failed", "Agent deployment manifest does not match the planned state");
542
+ if (config && (await inspectFileMode(configPath)) !== 0o600)
543
+ throw new AgentProfileError("final_validation_failed", "Agent profile configuration is not private");
544
+ if (manifest && (await inspectFileMode(manifestPath)) !== 0o600)
545
+ throw new AgentProfileError("final_validation_failed", "Agent deployment manifest is not private");
546
+ for (const [name, record] of Object.entries(built.manifest?.profiles ?? {})) {
547
+ const target = destination(built.plan.root, `agents/${name}.md`);
548
+ const content = await readRegular(target);
549
+ const planned = built.plan.operations.find((item) => item.path === `agents/${name}.md`);
550
+ const preservedDrift = built.plan.action === "uninstall" &&
551
+ content &&
552
+ planned?.operation === "conflict" &&
553
+ planned.reason === "managed profile drift is preserved" &&
554
+ planned.sha256 === sha256(content);
555
+ if (!content ||
556
+ (!preservedDrift &&
557
+ (sha256(content) !== record.rendered_sha256 || (await inspectFileMode(target)) !== 0o600)))
558
+ throw new AgentProfileError("final_validation_failed", `Rendered agent does not match the planned manifest: ${name}`);
559
+ }
560
+ }
561
+ export async function listAgentProfiles(scope, cwd = process.cwd(), home = homedir()) {
562
+ const root = deploymentRoot(scope, cwd, home);
563
+ const [state, canonical, files] = await Promise.all([
564
+ loadState(scope, root),
565
+ canonicalAssets(),
566
+ listDirectRegular(destination(root, AGENTS_DIRECTORY)),
567
+ ]);
568
+ const byName = new Map(files.filter((item) => item.name.endsWith(".md")).map((item) => [item.name.slice(0, -3), item]));
569
+ const configured = desiredNames(state.config);
570
+ const desired = Object.fromEntries(configured.map((name) => [name, renderAgentProfile(name, state.config, canonical)]));
571
+ const records = [];
572
+ const collisions = [];
573
+ const drift = [];
574
+ for (const name of configured) {
575
+ const file = byName.get(name);
576
+ const owned = state.manifest?.profiles[name];
577
+ let stateValue;
578
+ if (!owned && file)
579
+ stateValue = "collision";
580
+ else if (!file)
581
+ stateValue = owned ? "drift" : "missing";
582
+ else if (!owned ||
583
+ sha256(file.content) !== owned.rendered_sha256 ||
584
+ !file.content.equals(desired[name]) ||
585
+ file.mode !== 0o600)
586
+ stateValue = owned ? "drift" : "collision";
587
+ else
588
+ stateValue = "current";
589
+ if (stateValue === "collision")
590
+ collisions.push(name);
591
+ if (stateValue === "drift")
592
+ drift.push(name);
593
+ const selection = selectionFor(state.config, name);
594
+ records.push({
595
+ name,
596
+ ownership: NAME_PATTERN.test(name) ? "package-owned" : "managed",
597
+ state: stateValue,
598
+ ...("model" in selection
599
+ ? { model: selection.model, ...(selection.variant ? { variant: selection.variant } : {}) }
600
+ : {}),
601
+ ...(file ? { rendered_sha256: sha256(file.content) } : {}),
602
+ });
603
+ byName.delete(name);
604
+ }
605
+ const userOwned = [...byName.keys()].sort();
606
+ records.push(...userOwned.map((name) => ({
607
+ name,
608
+ ownership: "user-owned",
609
+ state: "current",
610
+ rendered_sha256: sha256(byName.get(name).content),
611
+ })));
612
+ const base = {
613
+ schema_version: 1,
614
+ scope,
615
+ root,
616
+ package_version: packageVersion(),
617
+ critic_pool: state.manifest?.critic_pool ??
618
+ ["critic", ...Object.keys(state.config.additional_critics)].sort(),
619
+ profiles: records.sort((left, right) => left.name.localeCompare(right.name)),
620
+ user_owned: userOwned,
621
+ collisions,
622
+ drift,
623
+ requires_restart: false,
624
+ };
625
+ return { ...base, digest: digest(base) };
626
+ }
627
+ export async function previewAgentProfileChange(request, scope, cwd = process.cwd(), home = homedir()) {
628
+ const stateRoot = lifecycleRoot(scope, cwd, home);
629
+ return withLifecycleLock(stateRoot, async () => {
630
+ if (await recoverTransaction(deploymentRoot(scope, cwd, home), stateRoot))
631
+ throw new AgentProfileError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
632
+ const built = await buildAgentProfilePlan(request, scope, cwd, home);
633
+ const receipt = await saveReceipt(stateRoot, `agent:${request.action}`, scope, built.plan.root, { request, digest: built.plan.digest });
634
+ return { ...built.plan, digest: receipt.digest, receipt_expires_at: receipt.expires_at };
635
+ });
636
+ }
637
+ export async function applyAgentProfileChange(request, scope, confirmationDigest, cwd = process.cwd(), home = homedir(), options = {}) {
638
+ const stateRoot = lifecycleRoot(scope, cwd, home);
639
+ const root = deploymentRoot(scope, cwd, home);
640
+ return withLifecycleLock(stateRoot, async () => {
641
+ if (await recoverTransaction(root, stateRoot))
642
+ throw new AgentProfileError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
643
+ const receipt = (await consumeReceipt(stateRoot, {
644
+ digest: confirmationDigest,
645
+ kind: `agent:${request.action}`,
646
+ scope,
647
+ root,
648
+ }));
649
+ if (stable(receipt.request) !== stable(request))
650
+ throw new AgentProfileError("confirmation_unknown", "Saved confirmation belongs to a different request");
651
+ const built = await buildAgentProfilePlan(request, scope, cwd, home);
652
+ if (built.plan.digest !== receipt.digest)
653
+ throw new AgentProfileError("stale_plan", "Agent inventory changed after preview");
654
+ if (built.plan.operations.some((item) => item.operation === "conflict" && item.reason.includes("collision")))
655
+ throw new AgentProfileError("collision", "Exact-name user-owned collision blocks apply");
656
+ if (request.action !== "uninstall" &&
657
+ request.action !== "reconcile" &&
658
+ built.plan.operations.some((item) => item.operation === "conflict"))
659
+ throw new AgentProfileError("drift", "Managed profile drift requires explicit reconcile");
660
+ await applyTransaction(root, stateRoot, built.mutations, {
661
+ ...options,
662
+ validateFinal: async () => {
663
+ await options.validateFinal?.();
664
+ await validateBuiltAgentProfilePlan(built);
665
+ const final = await listAgentProfiles(scope, cwd, home);
666
+ if (request.action !== "uninstall" &&
667
+ (final.collisions.length ||
668
+ final.drift.length ||
669
+ final.profiles
670
+ .filter((item) => item.ownership !== "user-owned")
671
+ .some((item) => item.state !== "current"))) {
672
+ throw new AgentProfileError("final_validation_failed", "Final agent inventory validation failed");
673
+ }
674
+ },
675
+ });
676
+ return {
677
+ status: "ok",
678
+ applied: true,
679
+ requires_restart: built.plan.requires_restart,
680
+ plan: { ...built.plan, digest: confirmationDigest },
681
+ };
682
+ });
683
+ }
684
+ export async function availableModels() {
685
+ try {
686
+ const { stdout } = await execFileAsync("opencode", ["models"], {
687
+ timeout: 10_000,
688
+ encoding: "utf8",
689
+ });
690
+ const models = [
691
+ ...new Set(stdout
692
+ .split(/\r?\n/)
693
+ .map((line) => line.trim())
694
+ .filter((line) => MODEL_PATTERN.test(line))),
695
+ ].sort();
696
+ if (!models.length)
697
+ throw new Error("empty catalog");
698
+ return models;
699
+ }
700
+ catch (error) {
701
+ throw new AgentProfileError("catalog_unavailable", `Cached OpenCode model catalog is unavailable; provide an explicit provider/model: ${error instanceof Error ? error.message : String(error)}`);
702
+ }
703
+ }
704
+ export async function availableModelVariants(model) {
705
+ const selected = validateModel(model);
706
+ const provider = selected.split("/", 1)[0];
707
+ try {
708
+ const { stdout } = await execFileAsync("opencode", ["models", provider, "--verbose"], {
709
+ timeout: 10_000,
710
+ encoding: "utf8",
711
+ maxBuffer: 10 * 1024 * 1024,
712
+ });
713
+ const lines = stdout.split(/\r?\n/);
714
+ for (let index = 0; index < lines.length; index += 1) {
715
+ if (lines[index].trim() !== selected)
716
+ continue;
717
+ let document = "";
718
+ for (index += 1; index < lines.length; index += 1) {
719
+ document += `${lines[index]}\n`;
720
+ try {
721
+ const metadata = JSON.parse(document);
722
+ if (!metadata.variants ||
723
+ typeof metadata.variants !== "object" ||
724
+ Array.isArray(metadata.variants))
725
+ throw new Error("variants missing");
726
+ const variants = Object.keys(metadata.variants);
727
+ if (!variants.every((variant) => VARIANT_PATTERN.test(variant)))
728
+ throw new Error("unsafe variant");
729
+ return variants;
730
+ }
731
+ catch (error) {
732
+ if (!(error instanceof SyntaxError))
733
+ throw error;
734
+ }
735
+ }
736
+ }
737
+ throw new Error("selected model is absent");
738
+ }
739
+ catch (error) {
740
+ throw new AgentProfileError("catalog_unavailable", `Cached OpenCode model variants are unavailable: ${error instanceof Error ? error.message : String(error)}`);
741
+ }
742
+ }
743
+ export async function inspectFileMode(path) {
744
+ try {
745
+ return (await lstat(path)).mode & 0o777;
746
+ }
747
+ catch (error) {
748
+ if (error.code === "ENOENT")
749
+ return undefined;
750
+ throw error;
751
+ }
752
+ }
753
+ export async function readLegacyManifest(root) {
754
+ const manifestPath = destination(root, ".skills-opencode-manifest.json");
755
+ const raw = await readRegular(manifestPath);
756
+ if (!raw)
757
+ return undefined;
758
+ let value;
759
+ try {
760
+ value = JSON.parse(raw.toString("utf8"));
761
+ }
762
+ catch {
763
+ return undefined;
764
+ }
765
+ const manifest = value;
766
+ if (manifest?.schema_version !== 1 ||
767
+ manifest.package !== PACKAGE_NAME ||
768
+ typeof manifest.version !== "string" ||
769
+ !manifest.files ||
770
+ typeof manifest.files !== "object")
771
+ return undefined;
772
+ return { manifest, manifestPath, manifestSha256: sha256(raw) };
773
+ }
774
+ export async function applyBuiltAgentPlan(built, stateRoot, options = {}) {
775
+ await applyTransaction(built.plan.root, stateRoot, built.mutations, options);
776
+ }