@kisev/skills-opencode 1.0.0 → 1.1.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.
@@ -0,0 +1,632 @@
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { chmod, lstat, mkdir, open, readFile, readdir, rename, rm, rmdir, stat, unlink, } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { basename, dirname, isAbsolute, join, normalize, parse, relative, resolve, sep, } from "node:path";
6
+ export const RECEIPT_TTL_MS = 10 * 60 * 1000;
7
+ export class LifecycleError extends Error {
8
+ code;
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.code = code;
12
+ }
13
+ }
14
+ export function stable(value) {
15
+ if (value === null || typeof value !== "object")
16
+ return JSON.stringify(value);
17
+ if (Array.isArray(value))
18
+ return `[${value.map(stable).join(",")}]`;
19
+ const object = value;
20
+ return `{${Object.keys(object)
21
+ .sort()
22
+ .map((key) => `${JSON.stringify(key)}:${stable(object[key])}`)
23
+ .join(",")}}`;
24
+ }
25
+ export function sha256(value) {
26
+ return createHash("sha256").update(value).digest("hex");
27
+ }
28
+ export function digest(value) {
29
+ return sha256(stable(value));
30
+ }
31
+ export function assertSafeRelative(value) {
32
+ if (!value ||
33
+ isAbsolute(value) ||
34
+ normalize(value) !== value ||
35
+ value.split(/[\\/]/).some((part) => !part || part === "." || part === "..")) {
36
+ throw new LifecycleError("unsafe_path", `Unsafe relative path: ${value}`);
37
+ }
38
+ }
39
+ function inside(root, target) {
40
+ const difference = relative(root, target);
41
+ return (difference === "" ||
42
+ (!difference.startsWith(`..${sep}`) && difference !== ".." && !isAbsolute(difference)));
43
+ }
44
+ export function destination(root, relativePath) {
45
+ assertSafeRelative(relativePath);
46
+ const target = resolve(root, relativePath);
47
+ if (!inside(resolve(root), target))
48
+ throw new LifecycleError("unsafe_path", `Path escapes destination root: ${relativePath}`);
49
+ return target;
50
+ }
51
+ async function lstatSafe(path) {
52
+ try {
53
+ return await lstat(path);
54
+ }
55
+ catch (error) {
56
+ if (error.code === "ENOENT")
57
+ return undefined;
58
+ throw error;
59
+ }
60
+ }
61
+ export async function assertSafePath(path, options = {}) {
62
+ const target = resolve(path);
63
+ const parsed = parse(target);
64
+ let current = parsed.root;
65
+ const pieces = target.slice(parsed.root.length).split(sep).filter(Boolean);
66
+ for (let index = 0; index < pieces.length; index += 1) {
67
+ current = join(current, pieces[index]);
68
+ const metadata = await lstatSafe(current);
69
+ if (!metadata) {
70
+ if (options.allowMissing !== false)
71
+ return;
72
+ throw new LifecycleError("unsafe_path", `Required path is missing: ${current}`);
73
+ }
74
+ if (metadata.isSymbolicLink())
75
+ throw new LifecycleError("unsafe_path", `Symlink is not allowed: ${current}`);
76
+ const final = index === pieces.length - 1;
77
+ if (!final && !metadata.isDirectory())
78
+ throw new LifecycleError("unsafe_path", `Path parent is not a directory: ${current}`);
79
+ if (final && options.target === "file" && !metadata.isFile())
80
+ throw new LifecycleError("unsafe_path", `Target is not a regular file: ${current}`);
81
+ if (final && options.target === "directory" && !metadata.isDirectory())
82
+ throw new LifecycleError("unsafe_path", `Target is not a directory: ${current}`);
83
+ }
84
+ }
85
+ export async function readRegular(path) {
86
+ await assertSafePath(path);
87
+ const metadata = await lstatSafe(path);
88
+ if (!metadata)
89
+ return undefined;
90
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1) {
91
+ throw new LifecycleError("unsafe_path", `Target is not a single-link regular file: ${path}`);
92
+ }
93
+ return readFile(path);
94
+ }
95
+ async function ensureDirectory(path, mode) {
96
+ const target = resolve(path);
97
+ const parsed = parse(target);
98
+ let current = parsed.root;
99
+ const created = [];
100
+ for (const piece of target.slice(parsed.root.length).split(sep).filter(Boolean)) {
101
+ current = join(current, piece);
102
+ const metadata = await lstatSafe(current);
103
+ if (metadata) {
104
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
105
+ throw new LifecycleError("unsafe_path", `Unsafe directory: ${current}`);
106
+ continue;
107
+ }
108
+ await mkdir(current, { mode });
109
+ created.push(current);
110
+ }
111
+ return created;
112
+ }
113
+ export async function writeAtomic(path, content, mode) {
114
+ await ensureDirectory(dirname(path), 0o700);
115
+ await assertSafePath(path);
116
+ const current = await lstatSafe(path);
117
+ if (current && (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1)) {
118
+ throw new LifecycleError("unsafe_path", `Target is not a single-link regular file: ${path}`);
119
+ }
120
+ const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
121
+ const handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, mode);
122
+ try {
123
+ await handle.writeFile(content);
124
+ await handle.sync();
125
+ }
126
+ finally {
127
+ await handle.close();
128
+ }
129
+ try {
130
+ await chmod(temporary, mode);
131
+ await rename(temporary, path);
132
+ const directory = await open(dirname(path), constants.O_RDONLY | constants.O_DIRECTORY);
133
+ try {
134
+ await directory.sync();
135
+ }
136
+ finally {
137
+ await directory.close();
138
+ }
139
+ }
140
+ finally {
141
+ await rm(temporary, { force: true });
142
+ }
143
+ }
144
+ export function deploymentRoot(scope, cwd = process.cwd(), home = homedir()) {
145
+ return scope === "global" ? resolve(home, ".config", "opencode") : resolve(cwd, ".opencode");
146
+ }
147
+ export function lifecycleRoot(scope, cwd = process.cwd(), home = homedir()) {
148
+ const base = process.env.XDG_STATE_HOME && home === homedir()
149
+ ? resolve(process.env.XDG_STATE_HOME)
150
+ : resolve(home, ".local", "state");
151
+ const suffix = scope === "global" ? "global" : join("project", sha256(resolve(cwd)));
152
+ return join(base, "opencode", "skills-opencode", suffix);
153
+ }
154
+ async function processAlive(pid) {
155
+ if (!Number.isSafeInteger(pid) || pid <= 0)
156
+ return false;
157
+ try {
158
+ process.kill(pid, 0);
159
+ return true;
160
+ }
161
+ catch (error) {
162
+ return error.code === "EPERM";
163
+ }
164
+ }
165
+ async function reclaimStaleLifecycleLock(stateRoot, lock) {
166
+ const cleanup = join(stateRoot, "lifecycle-lock-cleanup");
167
+ try {
168
+ await mkdir(cleanup, { mode: 0o700 });
169
+ }
170
+ catch (error) {
171
+ if (error.code === "EEXIST")
172
+ throw new LifecycleError("lifecycle_locked", "Another process is checking a stale lock");
173
+ throw error;
174
+ }
175
+ try {
176
+ const lockInfo = await lstatSafe(lock);
177
+ if (!lockInfo)
178
+ return;
179
+ if (!lockInfo.isDirectory() || lockInfo.isSymbolicLink() || (lockInfo.mode & 0o077) !== 0)
180
+ throw new LifecycleError("unsafe_path", "Lifecycle lock is not a private directory");
181
+ const ownerRaw = await readRegular(join(lock, "owner.json")).catch(() => undefined);
182
+ if (!ownerRaw) {
183
+ if (Date.now() - lockInfo.mtimeMs < 10_000)
184
+ throw new LifecycleError("lifecycle_locked", "Lifecycle lock is being initialized");
185
+ await rm(lock, { recursive: true, force: true });
186
+ return;
187
+ }
188
+ let ownerPid = 0;
189
+ let released = false;
190
+ try {
191
+ const owner = JSON.parse(ownerRaw.toString("utf8"));
192
+ ownerPid = Number(owner.pid);
193
+ released = owner.released === true;
194
+ }
195
+ catch {
196
+ ownerPid = 0;
197
+ }
198
+ if (!released && (await processAlive(ownerPid)))
199
+ throw new LifecycleError("lifecycle_locked", `Lifecycle is locked by process ${ownerPid}`);
200
+ await rm(lock, { recursive: true, force: true });
201
+ }
202
+ finally {
203
+ await rm(cleanup, { recursive: true, force: true }).catch(() => undefined);
204
+ }
205
+ }
206
+ export async function withLifecycleLock(stateRoot, callback) {
207
+ await ensureDirectory(stateRoot, 0o700);
208
+ const stateInfo = await lstat(stateRoot);
209
+ if (!stateInfo.isDirectory() || stateInfo.isSymbolicLink() || (stateInfo.mode & 0o077) !== 0) {
210
+ throw new LifecycleError("unsafe_path", "Lifecycle state root must be a private directory");
211
+ }
212
+ const lock = join(stateRoot, "lifecycle.lock");
213
+ for (let attempt = 0; attempt < 2; attempt += 1) {
214
+ try {
215
+ await mkdir(lock, { mode: 0o700 });
216
+ }
217
+ catch (error) {
218
+ if (error.code !== "EEXIST")
219
+ throw error;
220
+ await reclaimStaleLifecycleLock(stateRoot, lock);
221
+ continue;
222
+ }
223
+ try {
224
+ await writeAtomic(join(lock, "owner.json"), Buffer.from(`${JSON.stringify({ pid: process.pid, created_at: new Date().toISOString(), released: false })}\n`), 0o600);
225
+ return await callback();
226
+ }
227
+ finally {
228
+ await writeAtomic(join(lock, "owner.json"), Buffer.from(`${JSON.stringify({ pid: process.pid, released_at: new Date().toISOString(), released: true })}\n`), 0o600).catch(() => undefined);
229
+ await rm(lock, { recursive: true, force: true }).catch(() => undefined);
230
+ }
231
+ }
232
+ throw new LifecycleError("lifecycle_locked", "Cannot acquire lifecycle lock");
233
+ }
234
+ function receiptPath(stateRoot) {
235
+ return join(stateRoot, "receipt.json");
236
+ }
237
+ export async function saveReceipt(stateRoot, kind, scope, root, payload, now = Date.now()) {
238
+ const confirmationDigest = digest({ schema_version: 1, kind, scope, root, payload });
239
+ const existingRaw = await readRegular(receiptPath(stateRoot));
240
+ if (existingRaw) {
241
+ const existing = parseReceipt(existingRaw);
242
+ if (!existing.consumed &&
243
+ Date.parse(existing.expires_at) >= now &&
244
+ existing.digest !== confirmationDigest) {
245
+ throw new LifecycleError("active_receipt", "An unconsumed agent or installer plan is still active");
246
+ }
247
+ if (!existing.consumed &&
248
+ Date.parse(existing.expires_at) >= now &&
249
+ existing.digest === confirmationDigest) {
250
+ return { digest: existing.digest, expires_at: existing.expires_at };
251
+ }
252
+ }
253
+ const receipt = {
254
+ schema_version: 1,
255
+ digest: confirmationDigest,
256
+ nonce: randomBytes(32).toString("base64url"),
257
+ expires_at: new Date(now + RECEIPT_TTL_MS).toISOString(),
258
+ consumed: false,
259
+ kind,
260
+ scope,
261
+ root,
262
+ payload,
263
+ integrity: "",
264
+ };
265
+ receipt.integrity = receiptIntegrity(receipt);
266
+ await writeAtomic(receiptPath(stateRoot), Buffer.from(`${stable(receipt)}\n`), 0o600);
267
+ return { digest: receipt.digest, expires_at: receipt.expires_at };
268
+ }
269
+ function parseReceipt(raw) {
270
+ let value;
271
+ try {
272
+ value = JSON.parse(raw.toString("utf8"));
273
+ }
274
+ catch {
275
+ throw new LifecycleError("invalid_receipt", "Receipt is not valid JSON");
276
+ }
277
+ const receipt = value;
278
+ if (!receipt ||
279
+ receipt.schema_version !== 1 ||
280
+ typeof receipt.digest !== "string" ||
281
+ !/^[a-f0-9]{64}$/.test(receipt.digest) ||
282
+ typeof receipt.nonce !== "string" ||
283
+ typeof receipt.expires_at !== "string" ||
284
+ typeof receipt.consumed !== "boolean" ||
285
+ typeof receipt.kind !== "string" ||
286
+ (receipt.scope !== "global" && receipt.scope !== "project") ||
287
+ typeof receipt.root !== "string" ||
288
+ typeof receipt.integrity !== "string") {
289
+ throw new LifecycleError("invalid_receipt", "Receipt has an unsupported schema");
290
+ }
291
+ if (receipt.digest !==
292
+ digest({
293
+ schema_version: 1,
294
+ kind: receipt.kind,
295
+ scope: receipt.scope,
296
+ root: receipt.root,
297
+ payload: receipt.payload,
298
+ })) {
299
+ throw new LifecycleError("invalid_receipt", "Receipt digest does not match its payload");
300
+ }
301
+ if (receipt.integrity !== receiptIntegrity(receipt))
302
+ throw new LifecycleError("invalid_receipt", "Receipt integrity check failed");
303
+ return receipt;
304
+ }
305
+ function receiptIntegrity(receipt) {
306
+ const { integrity: _integrity, ...document } = receipt;
307
+ return digest(document);
308
+ }
309
+ export async function consumeReceipt(stateRoot, expected, now = Date.now()) {
310
+ if (!/^[a-f0-9]{64}$/.test(expected.digest))
311
+ throw new LifecycleError("invalid_digest", "Confirmation digest must be a SHA-256 value");
312
+ const raw = await readRegular(receiptPath(stateRoot));
313
+ if (!raw)
314
+ throw new LifecycleError("confirmation_unknown", "Confirmation receipt is missing");
315
+ const receipt = parseReceipt(raw);
316
+ if (receipt.digest !== expected.digest ||
317
+ receipt.kind !== expected.kind ||
318
+ receipt.scope !== expected.scope ||
319
+ receipt.root !== expected.root) {
320
+ throw new LifecycleError("confirmation_unknown", "Confirmation does not match the saved plan");
321
+ }
322
+ if (receipt.consumed)
323
+ throw new LifecycleError("confirmation_consumed", "Confirmation receipt was already consumed");
324
+ if (Date.parse(receipt.expires_at) < now)
325
+ throw new LifecycleError("confirmation_expired", "Confirmation receipt expired; request a fresh plan");
326
+ const consumed = { ...receipt, consumed: true, integrity: "" };
327
+ consumed.integrity = receiptIntegrity(consumed);
328
+ await writeAtomic(receiptPath(stateRoot), Buffer.from(`${stable(consumed)}\n`), 0o600);
329
+ return receipt.payload;
330
+ }
331
+ function journalPath(stateRoot) {
332
+ return join(stateRoot, "transaction-journal.json");
333
+ }
334
+ async function snapshot(root, mutation) {
335
+ const target = destination(root, mutation.path);
336
+ const content = await readRegular(target);
337
+ if (!content)
338
+ return { path: mutation.path, content: null, mode: null };
339
+ const metadata = await stat(target);
340
+ return { path: mutation.path, content: content.toString("base64"), mode: metadata.mode & 0o777 };
341
+ }
342
+ async function validateExpectation(root, mutation) {
343
+ const current = await readRegular(destination(root, mutation.path));
344
+ if (mutation.expected.absent) {
345
+ if (current)
346
+ throw new LifecycleError("stale_plan", `Expected an absent target: ${mutation.path}`);
347
+ return;
348
+ }
349
+ if (!mutation.expected.sha256 || !current || sha256(current) !== mutation.expected.sha256) {
350
+ throw new LifecycleError("stale_plan", `Destination changed after preview: ${mutation.path}`);
351
+ }
352
+ }
353
+ async function writeJournal(stateRoot, journal) {
354
+ await writeAtomic(journalPath(stateRoot), Buffer.from(`${stable(journal)}\n`), 0o600);
355
+ }
356
+ async function restore(root, journal) {
357
+ const conflicts = [];
358
+ for (let index = journal.snapshots.length - 1; index >= 0; index -= 1) {
359
+ const item = journal.snapshots[index];
360
+ const operation = journal.operations[index];
361
+ const target = destination(root, item.path);
362
+ const current = await readRegular(target);
363
+ const currentHash = current ? sha256(current) : undefined;
364
+ const currentMode = current ? (await lstat(target)).mode & 0o777 : undefined;
365
+ const beforeHash = item.content === null ? undefined : sha256(Buffer.from(item.content, "base64"));
366
+ const beforeMode = item.mode ?? undefined;
367
+ const intendedHash = operation.operation === "write" && operation.content
368
+ ? sha256(Buffer.from(operation.content, "base64"))
369
+ : undefined;
370
+ const intendedMode = operation.operation === "write" ? operation.mode : undefined;
371
+ const touched = index < journal.published || index === journal.applying;
372
+ if (!touched) {
373
+ if (currentHash !== beforeHash || currentMode !== beforeMode)
374
+ conflicts.push(item.path);
375
+ continue;
376
+ }
377
+ if (currentHash === beforeHash && currentMode === beforeMode)
378
+ continue;
379
+ if (currentHash !== intendedHash || currentMode !== intendedMode) {
380
+ conflicts.push(item.path);
381
+ continue;
382
+ }
383
+ if (item.content === null) {
384
+ if (current)
385
+ await unlink(target);
386
+ }
387
+ else {
388
+ await writeAtomic(target, Buffer.from(item.content, "base64"), item.mode ?? 0o600);
389
+ }
390
+ }
391
+ for (const directory of [...journal.created_directories].reverse()) {
392
+ try {
393
+ const metadata = await lstat(directory.path);
394
+ if (!metadata.isDirectory() ||
395
+ metadata.isSymbolicLink() ||
396
+ metadata.dev !== directory.device ||
397
+ metadata.ino !== directory.inode) {
398
+ conflicts.push(directory.path);
399
+ continue;
400
+ }
401
+ await rmdir(directory.path);
402
+ }
403
+ catch (error) {
404
+ const code = error.code;
405
+ if (code !== "ENOENT" && code !== "ENOTEMPTY")
406
+ throw error;
407
+ }
408
+ }
409
+ if (conflicts.length)
410
+ throw new LifecycleError("recovery_conflict", `Transaction targets changed outside the lifecycle lock: ${conflicts.join(", ")}`);
411
+ }
412
+ function parseJournal(raw, expectedRoot) {
413
+ let value;
414
+ try {
415
+ value = JSON.parse(raw.toString("utf8"));
416
+ }
417
+ catch {
418
+ throw new LifecycleError("invalid_journal", "Transaction journal is not valid JSON");
419
+ }
420
+ const journal = value;
421
+ if (journal.schema_version !== 1 ||
422
+ journal.root !== expectedRoot ||
423
+ !Array.isArray(journal.operations) ||
424
+ !Array.isArray(journal.snapshots) ||
425
+ journal.operations.length !== journal.snapshots.length ||
426
+ !Array.isArray(journal.created_directories) ||
427
+ typeof journal.published !== "number" ||
428
+ !Number.isSafeInteger(journal.published) ||
429
+ journal.published < 0 ||
430
+ journal.published > journal.operations.length ||
431
+ (journal.applying !== null &&
432
+ (typeof journal.applying !== "number" ||
433
+ !Number.isSafeInteger(journal.applying) ||
434
+ journal.applying !== journal.published ||
435
+ journal.applying < 0 ||
436
+ journal.applying >= journal.operations.length))) {
437
+ throw new LifecycleError("invalid_journal", "Transaction journal has an unsupported schema");
438
+ }
439
+ for (const item of journal.snapshots)
440
+ assertSafeRelative(item.path);
441
+ for (const item of journal.snapshots) {
442
+ if ((item.content !== null &&
443
+ (typeof item.content !== "string" ||
444
+ !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(item.content))) ||
445
+ (item.mode !== null &&
446
+ (!Number.isSafeInteger(item.mode) || item.mode < 0 || item.mode > 0o777))) {
447
+ throw new LifecycleError("invalid_journal", "Transaction journal contains an invalid snapshot");
448
+ }
449
+ }
450
+ for (const item of journal.operations) {
451
+ assertSafeRelative(item.path);
452
+ if (item.operation !== "write" && item.operation !== "remove")
453
+ throw new LifecycleError("invalid_journal", "Transaction journal contains an invalid operation");
454
+ if (item.operation === "write" &&
455
+ (typeof item.content !== "string" ||
456
+ !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(item.content) ||
457
+ typeof item.mode !== "number" ||
458
+ !Number.isSafeInteger(item.mode) ||
459
+ item.mode < 0 ||
460
+ item.mode > 0o777))
461
+ throw new LifecycleError("invalid_journal", "Transaction journal contains invalid write content");
462
+ }
463
+ if (!journal.created_directories.every((item) => item &&
464
+ typeof item.path === "string" &&
465
+ inside(resolve(expectedRoot), resolve(item.path)) &&
466
+ Number.isSafeInteger(item.device) &&
467
+ item.device >= 0 &&
468
+ Number.isSafeInteger(item.inode) &&
469
+ item.inode > 0)) {
470
+ throw new LifecycleError("invalid_journal", "Transaction journal contains an unsafe created directory");
471
+ }
472
+ return journal;
473
+ }
474
+ export async function recoverTransaction(root, stateRoot) {
475
+ const raw = await readRegular(journalPath(stateRoot));
476
+ if (!raw)
477
+ return false;
478
+ const journal = parseJournal(raw, resolve(root));
479
+ await restore(root, journal);
480
+ await unlink(journalPath(stateRoot));
481
+ return true;
482
+ }
483
+ async function ensureTransactionDirectories(root, mutation, stateRoot, journal) {
484
+ await ensureDirectory(dirname(resolve(root)), 0o700);
485
+ const relativeParent = relative(resolve(root), dirname(destination(root, mutation.path)));
486
+ const parts = relativeParent.split(sep).filter(Boolean);
487
+ const directories = [
488
+ resolve(root),
489
+ ...parts.map((_, index) => resolve(root, ...parts.slice(0, index + 1))),
490
+ ];
491
+ for (const directory of directories) {
492
+ const existing = await lstatSafe(directory);
493
+ if (existing) {
494
+ if (!existing.isDirectory() || existing.isSymbolicLink())
495
+ throw new LifecycleError("unsafe_path", `Transaction parent is unsafe: ${directory}`);
496
+ continue;
497
+ }
498
+ try {
499
+ await mkdir(directory, { mode: 0o700 });
500
+ }
501
+ catch (error) {
502
+ if (error.code !== "EEXIST")
503
+ throw error;
504
+ const raced = await lstat(directory);
505
+ if (!raced.isDirectory() || raced.isSymbolicLink())
506
+ throw new LifecycleError("unsafe_path", `Transaction parent is unsafe: ${directory}`);
507
+ continue;
508
+ }
509
+ const metadata = await lstat(directory);
510
+ journal.created_directories.push({
511
+ path: directory,
512
+ device: metadata.dev,
513
+ inode: metadata.ino,
514
+ });
515
+ await writeJournal(stateRoot, journal);
516
+ }
517
+ }
518
+ export async function applyTransaction(root, stateRoot, mutations, options = {}) {
519
+ if (await readRegular(journalPath(stateRoot)))
520
+ throw new LifecycleError("recovery_required", "An interrupted transaction must be recovered before apply");
521
+ const unique = new Set();
522
+ for (const mutation of mutations) {
523
+ assertSafeRelative(mutation.path);
524
+ if (unique.has(mutation.path))
525
+ throw new LifecycleError("invalid_plan", `Duplicate transaction target: ${mutation.path}`);
526
+ unique.add(mutation.path);
527
+ await validateExpectation(root, mutation);
528
+ }
529
+ const snapshots = await Promise.all(mutations.map((mutation) => snapshot(root, mutation)));
530
+ const journal = {
531
+ schema_version: 1,
532
+ root: resolve(root),
533
+ operations: mutations.map((mutation) => mutation.operation === "write"
534
+ ? {
535
+ path: mutation.path,
536
+ operation: mutation.operation,
537
+ content: mutation.content.toString("base64"),
538
+ mode: mutation.mode,
539
+ expected: mutation.expected,
540
+ }
541
+ : { path: mutation.path, operation: mutation.operation, expected: mutation.expected }),
542
+ snapshots,
543
+ created_directories: [],
544
+ published: 0,
545
+ applying: null,
546
+ };
547
+ await writeJournal(stateRoot, journal);
548
+ try {
549
+ for (const mutation of mutations) {
550
+ journal.applying = journal.published;
551
+ await writeJournal(stateRoot, journal);
552
+ options.beforePublish?.(journal.published);
553
+ if (mutation.operation === "write")
554
+ await ensureTransactionDirectories(root, mutation, stateRoot, journal);
555
+ await validateExpectation(root, mutation);
556
+ const target = destination(root, mutation.path);
557
+ if (mutation.operation === "write")
558
+ await writeAtomic(target, mutation.content, mutation.mode);
559
+ else
560
+ await unlink(target);
561
+ journal.published += 1;
562
+ journal.applying = null;
563
+ await writeJournal(stateRoot, journal);
564
+ const injected = options.afterPublish?.(journal.published);
565
+ if (injected === "interrupt")
566
+ throw new LifecycleError("test_interruption", "Injected transaction interruption");
567
+ if (injected === "fail")
568
+ throw new LifecycleError("test_failure", "Injected transaction failure");
569
+ }
570
+ for (const mutation of mutations) {
571
+ const target = destination(root, mutation.path);
572
+ const current = await readRegular(target);
573
+ if ((mutation.operation === "remove" && current) ||
574
+ (mutation.operation === "write" &&
575
+ (!current ||
576
+ !current.equals(mutation.content) ||
577
+ ((await stat(target)).mode & 0o777) !== mutation.mode))) {
578
+ throw new LifecycleError("final_validation_failed", `Transaction target failed final validation: ${mutation.path}`);
579
+ }
580
+ }
581
+ await options.validateFinal?.();
582
+ await unlink(journalPath(stateRoot));
583
+ }
584
+ catch (error) {
585
+ if (error instanceof LifecycleError && error.code === "test_interruption")
586
+ throw error;
587
+ try {
588
+ await restore(root, journal);
589
+ await unlink(journalPath(stateRoot));
590
+ }
591
+ catch (rollbackError) {
592
+ throw new LifecycleError("rollback_failed", `Transaction failed and rollback failed: ${String(rollbackError)}`);
593
+ }
594
+ throw new LifecycleError("rolled_back", `Transaction failed and was rolled back: ${error instanceof Error ? error.message : String(error)}`);
595
+ }
596
+ }
597
+ export async function appendPrivate(path, value) {
598
+ await ensureDirectory(dirname(path), 0o700);
599
+ await assertSafePath(path);
600
+ const existing = await lstatSafe(path);
601
+ if (existing && (!existing.isFile() || existing.isSymbolicLink() || existing.nlink !== 1))
602
+ throw new LifecycleError("unsafe_path", `Append target is unsafe: ${path}`);
603
+ const handle = await open(path, constants.O_APPEND | constants.O_CREAT | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
604
+ try {
605
+ const metadata = await handle.stat();
606
+ if (!metadata.isFile() || metadata.nlink !== 1 || (metadata.mode & 0o777) !== 0o600)
607
+ throw new LifecycleError("unsafe_path", `Append target is unsafe: ${path}`);
608
+ await handle.writeFile(`${stable(value)}\n`);
609
+ await handle.sync();
610
+ }
611
+ finally {
612
+ await handle.close();
613
+ }
614
+ }
615
+ export async function listDirectRegular(directory) {
616
+ const metadata = await lstatSafe(directory);
617
+ if (!metadata)
618
+ return [];
619
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
620
+ throw new LifecycleError("unsafe_path", `Directory is unsafe: ${directory}`);
621
+ const values = [];
622
+ for (const entry of (await readdir(directory, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name))) {
623
+ if (!entry.isFile() || entry.isSymbolicLink())
624
+ throw new LifecycleError("unsafe_path", `Directory entry is not a regular file: ${entry.name}`);
625
+ const path = join(directory, entry.name);
626
+ const info = await lstat(path);
627
+ if (info.nlink !== 1)
628
+ throw new LifecycleError("unsafe_path", `Directory entry has multiple hard links: ${entry.name}`);
629
+ values.push({ name: entry.name, content: await readFile(path), mode: info.mode & 0o777 });
630
+ }
631
+ return values;
632
+ }
@@ -35,10 +35,10 @@ export declare function backgroundAttempts({ client, directory, cwd }: {
35
35
  description: string;
36
36
  args: {
37
37
  action: import("zod").ZodEnum<{
38
- cancel: "cancel";
38
+ status: "status";
39
39
  list: "list";
40
+ cancel: "cancel";
40
41
  result: "result";
41
- status: "status";
42
42
  start: "start";
43
43
  retry: "retry";
44
44
  }>;
@@ -56,7 +56,7 @@ export declare function backgroundAttempts({ client, directory, cwd }: {
56
56
  }>>;
57
57
  };
58
58
  execute(args: {
59
- action: "cancel" | "list" | "result" | "status" | "start" | "retry";
59
+ action: "status" | "list" | "cancel" | "result" | "start" | "retry";
60
60
  task?: string | undefined;
61
61
  category?: string | undefined;
62
62
  decision?: any;
@@ -1,7 +1,8 @@
1
1
  export type CommandRegistration = {
2
2
  name: string;
3
3
  skill?: string;
4
- packageTool?: "capabilities" | "route" | "doctor";
4
+ packageTool?: "capabilities" | "route" | "doctor" | "agent_profiles";
5
+ packageAction?: "list" | "model_set" | "critic_add" | "critic_remove";
5
6
  description: string;
6
7
  mode?: string;
7
8
  };