agentwheel 0.19.10 → 0.20.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,3163 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ atomicCopy,
4
+ hashPath,
5
+ pathExists,
6
+ writeJsonAtomic
7
+ } from "./chunk-7VPI5J5Y.js";
8
+
9
+ // src/install/transaction.ts
10
+ import { createHash as createHash5 } from "crypto";
11
+ import { cp, mkdir as mkdir3, readFile as readFile7, rm as rm4, stat as stat2 } from "fs/promises";
12
+ import { dirname as dirname5, join as join6, resolve as resolve9 } from "path";
13
+
14
+ // src/transport/local.ts
15
+ import { execFile } from "child_process";
16
+ import { mkdir, readdir, readFile, rename, rm, writeFile } from "fs/promises";
17
+ import { dirname } from "path";
18
+ import { promisify } from "util";
19
+ var execFileAsync = promisify(execFile);
20
+ var localTransport = {
21
+ kind: "local",
22
+ description: "local filesystem",
23
+ pathExists,
24
+ async mkdirExclusive(path) {
25
+ await mkdir(dirname(path), { recursive: true });
26
+ await mkdir(path);
27
+ },
28
+ hashPath,
29
+ readFile: (path) => readFile(path, "utf8"),
30
+ async listDir(path) {
31
+ try {
32
+ return await readdir(path);
33
+ } catch (error) {
34
+ if (error.code === "ENOENT") return [];
35
+ throw error;
36
+ }
37
+ },
38
+ async writeFileAtomic(path, content) {
39
+ await mkdir(dirname(path), { recursive: true });
40
+ const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
41
+ await writeFile(temp, content, "utf8");
42
+ await rename(temp, path);
43
+ },
44
+ writeJsonAtomic,
45
+ atomicCopy,
46
+ rm: (path) => rm(path, { recursive: true, force: true }),
47
+ async execFile(command, args, options = {}) {
48
+ await execFileAsync(command, args, { cwd: options.cwd });
49
+ }
50
+ };
51
+
52
+ // src/transport/ssh.ts
53
+ import { execFile as execFile2, spawn } from "child_process";
54
+ import { basename, dirname as dirname2 } from "path";
55
+ import { dirname as posixDirname } from "path/posix";
56
+ import { promisify as promisify2 } from "util";
57
+ var execFileAsync2 = promisify2(execFile2);
58
+ function createSshTransport(config) {
59
+ const endpoint = config.user ? `${config.user}@${config.host}` : config.host;
60
+ const args = baseSshArgs(config, endpoint);
61
+ async function run(command) {
62
+ const { stdout } = await execFileAsync2("ssh", [...args, command], { maxBuffer: 20 * 1024 * 1024 });
63
+ return stdout;
64
+ }
65
+ async function runWithInput(command, input) {
66
+ await spawnWithInput("ssh", [...args, command], input);
67
+ }
68
+ return {
69
+ kind: "ssh",
70
+ description: `ssh://${endpoint}`,
71
+ async pathExists(path) {
72
+ try {
73
+ await run(`test -e ${quoteSh(path)}`);
74
+ return true;
75
+ } catch {
76
+ return false;
77
+ }
78
+ },
79
+ async mkdirExclusive(path) {
80
+ const dir = posixDirname(path);
81
+ try {
82
+ await run(`mkdir -p ${quoteSh(dir)} && mkdir ${quoteSh(path)}`);
83
+ } catch (error) {
84
+ if (isFileExistsError(error)) throw asAlreadyExists(error);
85
+ throw error;
86
+ }
87
+ },
88
+ async hashPath(path) {
89
+ return (await run(`node -e ${quoteSh(remoteHashScript)} -- ${quoteSh(path)}`)).trim();
90
+ },
91
+ readFile(path) {
92
+ return run(`cat -- ${quoteSh(path)}`);
93
+ },
94
+ async listDir(path) {
95
+ const output = await run(`ls -A -- ${quoteSh(path)} 2>/dev/null || true`);
96
+ return output.split("\n").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
97
+ },
98
+ writeFileAtomic(path, content) {
99
+ const dir = posixDirname(path);
100
+ const temp = `${path}.tmp-agentwheel-${process.pid}-${Date.now()}`;
101
+ return runWithInput(`mkdir -p ${quoteSh(dir)} && cat > ${quoteSh(temp)} && mv ${quoteSh(temp)} ${quoteSh(path)}`, content);
102
+ },
103
+ writeJsonAtomic(path, data) {
104
+ return this.writeFileAtomic(path, `${JSON.stringify(data, null, 2)}
105
+ `);
106
+ },
107
+ atomicCopy(source, dest) {
108
+ return copyViaTar(source, dest, args);
109
+ },
110
+ rm(path) {
111
+ return run(`rm -rf -- ${quoteSh(path)}`).then(() => void 0);
112
+ },
113
+ execFile(command, commandArgs, options = {}) {
114
+ const quoted = [command, ...commandArgs].map(quoteSh).join(" ");
115
+ const remoteCommand = options.cwd ? `cd ${quoteSh(options.cwd)} && ${quoted}` : quoted;
116
+ return run(remoteCommand).then(() => void 0);
117
+ }
118
+ };
119
+ }
120
+ function baseSshArgs(config, endpoint) {
121
+ const args = ["-o", "BatchMode=yes"];
122
+ if (config.port) args.push("-p", String(config.port));
123
+ if (config.identityFile) args.push("-i", config.identityFile);
124
+ args.push(endpoint);
125
+ return args;
126
+ }
127
+ async function copyViaTar(source, dest, sshArgs) {
128
+ const sourceParent = dirname2(source);
129
+ const sourceBase = basename(source);
130
+ const destParent = posixDirname(dest);
131
+ const temp = `${dest}.tmp-agentwheel-${process.pid}-${Date.now()}`;
132
+ const remoteCommand = [
133
+ `rm -rf -- ${quoteSh(temp)}`,
134
+ `mkdir -p ${quoteSh(temp)} ${quoteSh(destParent)}`,
135
+ `tar -xf - -C ${quoteSh(temp)}`,
136
+ `rm -rf -- ${quoteSh(dest)}`,
137
+ `mv ${quoteSh(`${temp}/${sourceBase}`)} ${quoteSh(dest)}`,
138
+ `rmdir ${quoteSh(temp)}`
139
+ ].join(" && ");
140
+ const tar = spawn("tar", ["-cf", "-", "-C", sourceParent, sourceBase], { stdio: ["ignore", "pipe", "pipe"] });
141
+ const ssh = spawn("ssh", [...sshArgs, remoteCommand], { stdio: ["pipe", "pipe", "pipe"] });
142
+ tar.stdout.pipe(ssh.stdin);
143
+ const [tarResult, sshResult] = await Promise.all([waitForProcess(tar, "tar"), waitForProcess(ssh, "ssh")]);
144
+ if (tarResult.stderr) throw new Error(tarResult.stderr);
145
+ if (sshResult.stderr) throw new Error(sshResult.stderr);
146
+ }
147
+ async function spawnWithInput(command, args, input) {
148
+ const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
149
+ child.stdin.end(input);
150
+ const result = await waitForProcess(child, command);
151
+ if (result.stderr) throw new Error(result.stderr);
152
+ }
153
+ function waitForProcess(child, label) {
154
+ return new Promise((resolve10, reject) => {
155
+ const stderr = [];
156
+ child.stderr?.on("data", (chunk) => stderr.push(chunk));
157
+ child.on("error", reject);
158
+ child.on("close", (code) => {
159
+ const message = Buffer.concat(stderr).toString("utf8");
160
+ if (code === 0) resolve10({ stderr: "" });
161
+ else reject(new Error(`${label} exited ${code}${message ? `: ${message}` : ""}`));
162
+ });
163
+ });
164
+ }
165
+ function quoteSh(value) {
166
+ return `'${value.replaceAll("'", "'\\''")}'`;
167
+ }
168
+ function isFileExistsError(error) {
169
+ const text = typeof error === "object" && error !== null ? `${"message" in error ? String(error.message) : ""}
170
+ ${"stderr" in error ? String(error.stderr) : ""}` : String(error);
171
+ return text.includes("File exists");
172
+ }
173
+ function asAlreadyExists(error) {
174
+ const out = error instanceof Error ? error : new Error(String(error));
175
+ out.code = "EEXIST";
176
+ return out;
177
+ }
178
+ var remoteHashScript = String.raw`
179
+ const { createHash } = require("node:crypto");
180
+ const { readdirSync, readFileSync, statSync } = require("node:fs");
181
+ const { join, relative } = require("node:path");
182
+ const target = process.argv[1];
183
+ const ignoredNames = new Set([".git", "node_modules", "__pycache__", ".DS_Store"]);
184
+ const ignoredSuffixes = [".pyc", ".pyo"];
185
+ function isIgnoredGeneratedEntry(name) {
186
+ return ignoredNames.has(name) || ignoredSuffixes.some((suffix) => name.endsWith(suffix));
187
+ }
188
+ function hashPath(path) {
189
+ const stats = statSync(path);
190
+ if (stats.isFile()) {
191
+ return createHash("sha256").update("file\0").update(readFileSync(path)).digest("hex");
192
+ }
193
+ if (!stats.isDirectory()) throw new Error("Unsupported path kind: " + path);
194
+ const hash = createHash("sha256").update("dir\0");
195
+ for (const file of listFiles(path)) {
196
+ hash.update(relative(path, file).replaceAll("\\", "/")).update("\0");
197
+ hash.update(hashPath(file)).update("\0");
198
+ }
199
+ return hash.digest("hex");
200
+ }
201
+ function listFiles(root) {
202
+ const out = [];
203
+ function walk(dir) {
204
+ for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
205
+ if (isIgnoredGeneratedEntry(entry.name)) continue;
206
+ const full = join(dir, entry.name);
207
+ if (entry.isDirectory()) walk(full);
208
+ else if (entry.isFile()) out.push(full);
209
+ }
210
+ }
211
+ walk(root);
212
+ return out;
213
+ }
214
+ process.stdout.write(hashPath(target));
215
+ `;
216
+
217
+ // src/transport/index.ts
218
+ function transportForTarget(target) {
219
+ if (target.transport === "local") return localTransport;
220
+ if (!target.ssh) throw new Error(`SSH target ${target.agentName ?? target.targetRoot} is missing SSH connection details.`);
221
+ return createSshTransport(target.ssh);
222
+ }
223
+
224
+ // src/install/paths.ts
225
+ import { join } from "path";
226
+
227
+ // src/model/adapter.ts
228
+ import { readFile as readFile2 } from "fs/promises";
229
+ import { homedir } from "os";
230
+ import { parse, printParseErrorCode } from "jsonc-parser";
231
+ import { z as z2 } from "zod";
232
+
233
+ // src/model/artifact.ts
234
+ import { z } from "zod";
235
+ var artifactTypeSchema = z.enum([
236
+ "instructions",
237
+ "rules",
238
+ "skills",
239
+ "commands",
240
+ "subagents",
241
+ "mcp",
242
+ "hooks",
243
+ "settings",
244
+ "plugins",
245
+ "fragments"
246
+ ]);
247
+ var fileKindSchema = z.enum(["file", "dir"]);
248
+ var artifactFormatSchema = z.string().min(1).regex(/^[a-z0-9][a-z0-9._-]*$/i, "artifact format must be a stable identifier");
249
+ var packageAssetSchema = z.object({
250
+ from: z.string().min(1),
251
+ into: z.string().min(1),
252
+ include: z.array(z.string().min(1)).optional(),
253
+ mode: z.enum(["preserve", "copy"]).default("preserve")
254
+ });
255
+ var packageComposeEntrySchema = z.object({
256
+ include: z.string().min(1),
257
+ markers: z.boolean().optional(),
258
+ optional: z.boolean().optional()
259
+ });
260
+ var packageCompositionRuleSchema = z.object({
261
+ target: z.string().min(1),
262
+ include: z.string().min(1),
263
+ exclude: z.array(z.string().min(1)).optional(),
264
+ runtimes: z.array(z.string().min(1)).optional(),
265
+ markers: z.boolean().optional()
266
+ });
267
+ var packageSupersedesEntrySchema = z.object({
268
+ package: z.string().min(1),
269
+ selector: z.string().min(1),
270
+ reason: z.string().min(1)
271
+ });
272
+ var packageItemRequireObjectSchema = z.object({
273
+ selector: z.string().min(1),
274
+ optional: z.boolean().optional(),
275
+ runtimes: z.array(z.string().min(1)).optional()
276
+ }).passthrough();
277
+ var packageItemRequireSchema = z.union([
278
+ z.string().min(1),
279
+ packageItemRequireObjectSchema
280
+ ]);
281
+ var packageItemSuggestObjectSchema = z.object({
282
+ alias: z.string().min(1),
283
+ select: z.array(z.string().min(1)).optional(),
284
+ optional: z.boolean().optional(),
285
+ runtimes: z.array(z.string().min(1)).optional(),
286
+ reason: z.string().min(1).optional(),
287
+ when: z.string().min(1).optional()
288
+ }).passthrough();
289
+ var packageItemSuggestSchema = z.union([
290
+ z.string().min(1),
291
+ packageItemSuggestObjectSchema
292
+ ]);
293
+ var composedFromEntrySchema = z.object({
294
+ selector: z.string().min(1),
295
+ hash: z.string().min(16)
296
+ });
297
+ var artifactSchema = z.object({
298
+ type: artifactTypeSchema,
299
+ name: z.string().min(1),
300
+ sourcePath: z.string().min(1),
301
+ stagedPath: z.string().min(1).optional(),
302
+ relativePath: z.string().min(1),
303
+ kind: fileKindSchema,
304
+ hash: z.string().min(16),
305
+ format: artifactFormatSchema.optional(),
306
+ packageName: z.string().min(1).optional(),
307
+ channel: z.enum(["managed", "overlay", "addition", "override", "ejected"]).default("managed"),
308
+ assets: z.array(packageAssetSchema).optional(),
309
+ required: z.boolean().optional(),
310
+ requires: z.array(packageItemRequireSchema).optional(),
311
+ suggests: z.array(packageItemSuggestSchema).optional(),
312
+ compose: z.array(packageComposeEntrySchema).optional(),
313
+ supersedes: z.array(packageSupersedesEntrySchema).optional(),
314
+ runtimes: z.array(z.string().min(1)).optional(),
315
+ composedFrom: z.array(composedFromEntrySchema).optional()
316
+ });
317
+
318
+ // src/model/adapter.ts
319
+ var defaultInstallationType = "local";
320
+ var installationTypeSchema = z2.string().min(1).regex(/^[a-z0-9][a-z0-9._-]*$/i, "installation type must be a stable path-safe identifier");
321
+ var targetMappingSchema = z2.object({
322
+ dest: z2.string().min(1),
323
+ enabled: z2.boolean().default(true),
324
+ root: z2.enum(["target", "home"]).optional(),
325
+ formats: z2.array(z2.string().min(1)).optional(),
326
+ semantic: z2.enum([
327
+ "openclaw-plugin",
328
+ "claude-plugin",
329
+ "codex-plugin",
330
+ "hermes-plugin",
331
+ "copilot-plugin",
332
+ "openclaw-subagent",
333
+ "codex-subagent",
334
+ "copilot-instruction",
335
+ "copilot-prompt",
336
+ "copilot-agent"
337
+ ]).optional(),
338
+ merge: z2.enum(["json-deep", "openclaw-json-deep", "yaml-deep", "codex-toml-mcp"]).optional(),
339
+ mode: z2.enum(["managed-block"]).optional()
340
+ });
341
+ var targetRegistrySchema = z2.record(installationTypeSchema, targetMappingSchema);
342
+ var targetRegistryInputSchema = z2.union([
343
+ targetMappingSchema.transform((mapping) => ({ [defaultInstallationType]: mapping })),
344
+ targetRegistrySchema
345
+ ]);
346
+ var adapterSchema = z2.object({
347
+ name: z2.string().regex(/^[a-z0-9][a-z0-9._-]*$/i, "adapter name must be a canonical path-safe identifier"),
348
+ displayName: z2.string().min(1).optional(),
349
+ targets: z2.partialRecord(
350
+ artifactTypeSchema,
351
+ targetRegistryInputSchema
352
+ ).default({})
353
+ });
354
+ function supportedInstallationTypes(adapter, artifactType) {
355
+ const registries = artifactType ? [adapter.targets[artifactType]] : Object.entries(adapter.targets).filter(([type]) => type !== "fragments").map(([, registry]) => registry);
356
+ const types = /* @__PURE__ */ new Set();
357
+ for (const registry of registries) {
358
+ for (const [installationType, target] of Object.entries(registry ?? {})) {
359
+ if (target.enabled) types.add(installationType);
360
+ }
361
+ }
362
+ return [...types].sort((a, b) => a.localeCompare(b));
363
+ }
364
+ function adapterTargetSupport(adapter, artifactType, installationType) {
365
+ if (artifactType === "fragments") return { ok: true };
366
+ const registry = adapter.targets[artifactType];
367
+ const supported = supportedInstallationTypes(adapter, artifactType);
368
+ if (!registry) {
369
+ return { ok: false, reason: "adapter-target-unsupported", supportedInstallationTypes: supported };
370
+ }
371
+ const target = registry[installationType];
372
+ if (target?.enabled) return { ok: true };
373
+ if (target && !target.enabled) {
374
+ return { ok: false, reason: "adapter-target-disabled", supportedInstallationTypes: supported };
375
+ }
376
+ return { ok: false, reason: "adapter-target-unsupported", supportedInstallationTypes: supported };
377
+ }
378
+ function resolveInstallationTypeForArtifacts(adapter, artifactTypes, requested) {
379
+ const installableTypes = [...new Set(artifactTypes.filter((type) => type !== "fragments"))];
380
+ if (installableTypes.length === 0) {
381
+ return requested ?? resolveInstallationTypeForAdapter(adapter, requested);
382
+ }
383
+ for (const type of installableTypes) {
384
+ const supported = supportedInstallationTypes(adapter, type);
385
+ if (supported.length === 0) {
386
+ throw new Error(`Adapter ${adapter.name} does not support ${type} artifacts for any installation type.`);
387
+ }
388
+ if (requested && !supported.includes(requested)) {
389
+ throw new Error(`Adapter ${adapter.name} does not support ${type} artifacts for installation type '${requested}'. Supported: ${supported.join(", ")}`);
390
+ }
391
+ }
392
+ if (requested) return requested;
393
+ const [firstType, ...restTypes] = installableTypes;
394
+ let candidates = new Set(supportedInstallationTypes(adapter, firstType));
395
+ for (const type of restTypes) {
396
+ const supported = new Set(supportedInstallationTypes(adapter, type));
397
+ candidates = new Set([...candidates].filter((candidate) => supported.has(candidate)));
398
+ }
399
+ const available = [...candidates].sort((a, b) => a.localeCompare(b));
400
+ if (available.length === 1) return available[0];
401
+ if (available.length === 0) {
402
+ throw new Error(`Adapter ${adapter.name} has no common installation type for: ${installableTypes.join(", ")}`);
403
+ }
404
+ throw new Error(`Installation type required for ${adapter.name}; supported for selected artifacts: ${available.join(", ")}. Pass --installation-type <type>.`);
405
+ }
406
+ function resolveInstallationTypeForAdapter(adapter, requested) {
407
+ const supported = supportedInstallationTypes(adapter);
408
+ if (requested) {
409
+ if (!supported.includes(requested)) {
410
+ throw new Error(`Adapter ${adapter.name} does not support installation type '${requested}'. Supported: ${supported.join(", ") || "<none>"}`);
411
+ }
412
+ return requested;
413
+ }
414
+ if (supported.length === 1) return supported[0];
415
+ if (supported.length === 0) return defaultInstallationType;
416
+ throw new Error(`Installation type required for ${adapter.name}; supported: ${supported.join(", ")}. Pass --installation-type <type>.`);
417
+ }
418
+ function targetMappingForArtifact(adapter, artifactType, installationType) {
419
+ return adapter.targets[artifactType]?.[installationType];
420
+ }
421
+ function installRootForArtifacts(adapter, targetRoot, installationType, artifactTypes, isSsh = false) {
422
+ const roots = new Set(
423
+ [...new Set(artifactTypes.filter((type) => type !== "fragments"))].map((type) => targetMappingForArtifact(adapter, type, installationType)?.root ?? "target")
424
+ );
425
+ if (roots.has("home") && roots.has("target")) {
426
+ throw new Error(`Installation type '${installationType}' for ${adapter.name} mixes home-rooted and target-rooted artifacts.`);
427
+ }
428
+ return roots.has("home") && !isSsh ? userHomeRoot() : targetRoot;
429
+ }
430
+ function installRootForAdapterInstallationType(adapter, targetRoot, installationType, isSsh = false) {
431
+ const roots = /* @__PURE__ */ new Set();
432
+ for (const registry of Object.values(adapter.targets)) {
433
+ const target = registry?.[installationType];
434
+ if (target?.enabled) roots.add(target.root ?? "target");
435
+ }
436
+ if (roots.has("home") && roots.has("target")) {
437
+ throw new Error(`Installation type '${installationType}' for ${adapter.name} mixes home-rooted and target-rooted artifacts.`);
438
+ }
439
+ return roots.has("home") && !isSsh ? userHomeRoot() : targetRoot;
440
+ }
441
+ function userHomeRoot() {
442
+ return process.env.AGENTWHEEL_TEST_HOME || process.env.HOME || homedir();
443
+ }
444
+ async function loadAdapterConfig(path) {
445
+ const content = await readFile2(path, "utf8");
446
+ const errors = [];
447
+ const parsed = parse(content, errors, {
448
+ allowTrailingComma: true,
449
+ disallowComments: false
450
+ });
451
+ if (errors.length > 0) {
452
+ const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
453
+ throw new Error(`Invalid adapter config ${path}: ${details}`);
454
+ }
455
+ return adapterSchema.parse(parsed);
456
+ }
457
+
458
+ // src/install/paths.ts
459
+ function metadataDir(targetRoot) {
460
+ return join(targetRoot, ".agentwheel");
461
+ }
462
+ function stateKeyFor(adapter, scope = {}) {
463
+ assertPathSafeAdapterName(adapter);
464
+ if (scope.stateKey) {
465
+ const explicit = sanitizeStateKey(scope.stateKey);
466
+ const adapterScoped = scope.fleetId && explicit !== adapter && !explicit.startsWith(`${adapter}.`) ? `${adapter}.${explicit}` : explicit;
467
+ const fleetSuffix = scope.fleetId ? `.fleet-${sanitizeStateKey(scope.fleetId)}${scope.targetFingerprint ? `.${scope.targetFingerprint}` : ""}` : "";
468
+ return sanitizeStateKey(`${adapterScoped}${fleetSuffix}`);
469
+ }
470
+ const installationType = scope.installationType ?? defaultInstallationType;
471
+ const fingerprint = scope.targetFingerprint ? `.${scope.targetFingerprint}` : "";
472
+ return sanitizeStateKey(`${adapter}.${installationType}${fingerprint}`);
473
+ }
474
+ function assertPathSafeAdapterName(adapter) {
475
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(adapter)) {
476
+ throw new Error(`Adapter name '${adapter}' is not a canonical path-safe identifier.`);
477
+ }
478
+ }
479
+ function installManifestPath(targetRoot, adapter, scope = {}) {
480
+ return join(metadataDir(targetRoot), `${stateKeyFor(adapter, scope)}.install-manifest.json`);
481
+ }
482
+ function sourceLockPath(targetRoot, adapter, scope = {}) {
483
+ return join(metadataDir(targetRoot), `${stateKeyFor(adapter, scope)}.source-lock.json`);
484
+ }
485
+ function sanitizeStateKey(value) {
486
+ return value.replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "") || "default";
487
+ }
488
+
489
+ // src/mutation/coordinator.ts
490
+ import { randomUUID as randomUUID2 } from "crypto";
491
+ import { homedir as homedir4 } from "os";
492
+ import { resolve as resolve8 } from "path";
493
+
494
+ // src/model/mutation.ts
495
+ import { z as z3 } from "zod";
496
+ import { isAbsolute, resolve } from "path";
497
+ var REVISION_PROVIDER_PROTOCOL_VERSION = 1;
498
+ var providerIdSchema = z3.string().min(1).max(80).regex(/^[a-z0-9][a-z0-9._-]*$/i);
499
+ var sha256Schema = z3.string().regex(/^[a-f0-9]{64}$/);
500
+ var gitRevisionProviderSchema = z3.object({
501
+ kind: z3.literal("git"),
502
+ id: providerIdSchema.default("git"),
503
+ protocolVersion: z3.literal(REVISION_PROVIDER_PROTOCOL_VERSION)
504
+ }).strict();
505
+ var commandRevisionProviderSchema = z3.object({
506
+ kind: z3.literal("command"),
507
+ id: providerIdSchema,
508
+ command: z3.array(z3.string().min(1)).min(1).superRefine((command, ctx) => {
509
+ const executable = command[0];
510
+ if (!isAbsolute(executable) || resolve(executable) !== executable || /[\r\n\0]/u.test(executable)) {
511
+ ctx.addIssue({
512
+ code: "custom",
513
+ path: [0],
514
+ message: "Command provider executables must use an absolute normalized path"
515
+ });
516
+ }
517
+ }),
518
+ executableSha256: sha256Schema,
519
+ trustBoundary: z3.literal("entrypoint"),
520
+ timeoutMs: z3.number().int().min(100).max(3e5).default(3e4),
521
+ protocolVersion: z3.literal(REVISION_PROVIDER_PROTOCOL_VERSION)
522
+ }).strict();
523
+ var revisionProviderConfigSchema = z3.discriminatedUnion("kind", [
524
+ gitRevisionProviderSchema,
525
+ commandRevisionProviderSchema
526
+ ]);
527
+ var revisioningOffSchema = z3.object({
528
+ mode: z3.literal("off")
529
+ }).strict();
530
+ var commitAfterVerifySchema = z3.object({
531
+ mode: z3.literal("commit-after-verify"),
532
+ allowNoCommitOverride: z3.boolean().default(false),
533
+ reasonInCommit: z3.literal("full"),
534
+ provider: revisionProviderConfigSchema
535
+ }).strict();
536
+ var mutationPolicySchema = z3.object({
537
+ reason: z3.enum(["optional", "required"]),
538
+ journal: z3.enum(["off", "required"]),
539
+ revisioning: z3.discriminatedUnion("mode", [revisioningOffSchema, commitAfterVerifySchema])
540
+ }).strict().superRefine((policy, ctx) => {
541
+ if (policy.revisioning.mode === "commit-after-verify" && policy.journal !== "required") {
542
+ ctx.addIssue({
543
+ code: "custom",
544
+ path: ["journal"],
545
+ message: "commit-after-verify requires durable mutation journaling"
546
+ });
547
+ }
548
+ });
549
+
550
+ // src/model/workspace.ts
551
+ import { readFile as readFile4 } from "fs/promises";
552
+ import { homedir as homedir3 } from "os";
553
+ import { dirname as dirname4, join as join4, resolve as resolve5 } from "path";
554
+ import { z as z6 } from "zod";
555
+
556
+ // src/resolve/semver.ts
557
+ var semverPattern = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
558
+ function parseSemver(value) {
559
+ const match = semverPattern.exec(value.trim());
560
+ if (!match) return void 0;
561
+ return {
562
+ major: Number(match[1]),
563
+ minor: Number(match[2]),
564
+ patch: Number(match[3]),
565
+ prerelease: match[4]
566
+ };
567
+ }
568
+ function satisfiesVersionRange(version, range) {
569
+ if (!range || range.trim() === "" || range.trim() === "*") return true;
570
+ const parsedVersion = parseSemver(version);
571
+ const trimmed = range.trim();
572
+ if (!parsedVersion) {
573
+ return trimmed === "*" || trimmed === version;
574
+ }
575
+ const comparators = parseRange(trimmed);
576
+ if (!comparators) return trimmed === version;
577
+ return comparators.every((comparator) => compareWith(parsedVersion, comparator));
578
+ }
579
+ function semverMajorOrVersion(version) {
580
+ const parsed = parseSemver(version);
581
+ return parsed ? String(parsed.major) : version;
582
+ }
583
+ function compareSemverStrings(a, b) {
584
+ const parsedA = parseSemver(a);
585
+ const parsedB = parseSemver(b);
586
+ if (!parsedA || !parsedB) return a.localeCompare(b);
587
+ return compareSemver(parsedA, parsedB);
588
+ }
589
+ function isSupportedVersionRange(range) {
590
+ const trimmed = range.trim();
591
+ return trimmed === "*" || parseRange(trimmed) !== void 0;
592
+ }
593
+ function parseRange(range) {
594
+ if (range === "*") return [];
595
+ if (range.startsWith("^")) {
596
+ const base = parseSemver(range.slice(1));
597
+ if (!base) return void 0;
598
+ return [
599
+ { op: ">=", version: base },
600
+ { op: "<", version: caretUpperBound(base) }
601
+ ];
602
+ }
603
+ if (range.startsWith("~")) {
604
+ const base = parseSemver(range.slice(1));
605
+ if (!base) return void 0;
606
+ return [
607
+ { op: ">=", version: base },
608
+ { op: "<", version: { major: base.major, minor: base.minor + 1, patch: 0 } }
609
+ ];
610
+ }
611
+ const parts = range.split(/\s+/).filter(Boolean);
612
+ const comparators = [];
613
+ for (const part of parts) {
614
+ const match = /^(>=|<=|>|<|=)?(.+)$/.exec(part);
615
+ if (!match) return void 0;
616
+ const version = parseSemver(match[2] ?? "");
617
+ if (!version) return void 0;
618
+ comparators.push({ op: match[1] ?? "=", version });
619
+ }
620
+ return comparators;
621
+ }
622
+ function caretUpperBound(version) {
623
+ if (version.major > 0) return { major: version.major + 1, minor: 0, patch: 0 };
624
+ if (version.minor > 0) return { major: 0, minor: version.minor + 1, patch: 0 };
625
+ return { major: 0, minor: 0, patch: version.patch + 1 };
626
+ }
627
+ function compareWith(version, comparator) {
628
+ const order = compareSemver(version, comparator.version);
629
+ switch (comparator.op) {
630
+ case "=":
631
+ return order === 0;
632
+ case ">":
633
+ return order > 0;
634
+ case ">=":
635
+ return order >= 0;
636
+ case "<":
637
+ return order < 0;
638
+ case "<=":
639
+ return order <= 0;
640
+ }
641
+ }
642
+ function compareSemver(a, b) {
643
+ for (const key of ["major", "minor", "patch"]) {
644
+ if (a[key] !== b[key]) return a[key] > b[key] ? 1 : -1;
645
+ }
646
+ if (a.prerelease === b.prerelease) return 0;
647
+ if (!a.prerelease) return 1;
648
+ if (!b.prerelease) return -1;
649
+ return a.prerelease.localeCompare(b.prerelease);
650
+ }
651
+
652
+ // src/mutation/declarations.ts
653
+ import { chmodSync, closeSync, fsyncSync, mkdirSync, openSync, readFileSync, writeSync } from "fs";
654
+ import { isAbsolute as isAbsolute3, join as join3, relative, resolve as resolve4, sep } from "path";
655
+
656
+ // src/mutation/protocol.ts
657
+ import { createHash } from "crypto";
658
+ import { isAbsolute as isAbsolute2, posix, resolve as resolve2 } from "path";
659
+ import { z as z4 } from "zod";
660
+ var sha256Schema2 = z4.string().regex(/^[a-f0-9]{64}$/);
661
+ var nullableSha256Schema = sha256Schema2.nullable();
662
+ var gitCommitShaSchema = z4.string().regex(/^[a-f0-9]{40}$/);
663
+ var nullableGitCommitShaSchema = gitCommitShaSchema.nullable();
664
+ var mutationOperationIdSchema = z4.string().min(1).max(63).regex(/^[a-z0-9][a-z0-9_-]*$/i);
665
+ var mutationReasonSchema = z4.string().min(1).max(4096).refine((value) => !/[\u0000\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(value), {
666
+ message: "Mutation reasons may not contain control characters"
667
+ });
668
+ var revisionPathSchema = z4.object({
669
+ path: z4.string().min(1).max(4096).refine(isSafeRepoRelativePath, {
670
+ message: "Revision paths must be normalized repository-relative paths"
671
+ }),
672
+ beforeSha256: nullableSha256Schema,
673
+ afterSha256: nullableSha256Schema
674
+ }).strict().superRefine((entry, ctx) => {
675
+ if (entry.beforeSha256 === entry.afterSha256) {
676
+ ctx.addIssue({ code: "custom", message: "Revision path hashes must describe a change" });
677
+ }
678
+ });
679
+ var revisionActionSchema = z4.enum(["check", "preflight", "finalize", "recover", "release"]);
680
+ var requestBase = z4.object({
681
+ protocolVersion: z4.literal(REVISION_PROVIDER_PROTOCOL_VERSION),
682
+ action: revisionActionSchema,
683
+ operationId: mutationOperationIdSchema,
684
+ repositoryRoot: z4.string().min(1).max(4096).refine(isCanonicalAbsolutePath, {
685
+ message: "repositoryRoot must be an absolute normalized path"
686
+ }),
687
+ expectedHead: gitCommitShaSchema,
688
+ expectedManifestDigest: sha256Schema2.optional(),
689
+ commandName: z4.string().min(1).max(256).refine((value) => !/[\r\n\u0000-\u001f\u007f]/u.test(value), {
690
+ message: "commandName must be a single printable line"
691
+ }),
692
+ reason: mutationReasonSchema,
693
+ noCommit: z4.boolean(),
694
+ paths: z4.array(revisionPathSchema)
695
+ }).strict();
696
+ var revisionProviderRequestUnion = z4.discriminatedUnion("action", [
697
+ requestBase.extend({ action: z4.literal("check") }).strict(),
698
+ requestBase.extend({ action: z4.literal("preflight") }).strict(),
699
+ requestBase.extend({ action: z4.literal("finalize") }).strict(),
700
+ requestBase.extend({ action: z4.literal("recover") }).strict(),
701
+ requestBase.extend({ action: z4.literal("release") }).strict()
702
+ ]);
703
+ var revisionProviderRequestSchema = revisionProviderRequestUnion.superRefine((request, ctx) => {
704
+ const seen = /* @__PURE__ */ new Set();
705
+ for (const [index, entry] of request.paths.entries()) {
706
+ if (seen.has(entry.path)) {
707
+ ctx.addIssue({ code: "custom", path: ["paths", index, "path"], message: `Duplicate revision path: ${entry.path}` });
708
+ }
709
+ seen.add(entry.path);
710
+ }
711
+ });
712
+ var responseBase = z4.object({
713
+ protocolVersion: z4.literal(REVISION_PROVIDER_PROTOCOL_VERSION),
714
+ providerId: z4.string().min(1).max(80).regex(/^[a-z0-9][a-z0-9._-]*$/i),
715
+ action: revisionActionSchema,
716
+ operationId: mutationOperationIdSchema,
717
+ ok: z4.literal(true),
718
+ status: z4.string().min(1).max(80).regex(/^[a-z0-9][a-z0-9._-]*$/i)
719
+ }).strict();
720
+ var terminalResponseFields = {
721
+ expectedHead: gitCommitShaSchema,
722
+ resultingHead: gitCommitShaSchema,
723
+ productCommitSha: nullableGitCommitShaSchema,
724
+ draftStackId: z4.string().min(1).max(256).nullable(),
725
+ draftBranch: z4.string().min(1).max(1024).refine(isSafeGitBranch, { message: "draftBranch must be a valid branch name" }).nullable(),
726
+ draftTipSha: nullableGitCommitShaSchema,
727
+ controlCommitSha: nullableGitCommitShaSchema,
728
+ manifestDigest: sha256Schema2.nullable(),
729
+ unmappedIntegrationCommits: z4.array(gitCommitShaSchema).superRefine(rejectDuplicates),
730
+ published: z4.literal(false)
731
+ };
732
+ var terminalErrorResponseFields = {
733
+ expectedHead: nullableGitCommitShaSchema,
734
+ resultingHead: nullableGitCommitShaSchema,
735
+ productCommitSha: nullableGitCommitShaSchema,
736
+ draftStackId: z4.string().min(1).max(256).nullable(),
737
+ draftBranch: z4.string().min(1).max(1024).refine(isSafeGitBranch, { message: "draftBranch must be a valid branch name" }).nullable(),
738
+ draftTipSha: nullableGitCommitShaSchema,
739
+ controlCommitSha: nullableGitCommitShaSchema,
740
+ manifestDigest: sha256Schema2.nullable(),
741
+ unmappedIntegrationCommits: z4.array(gitCommitShaSchema).superRefine(rejectDuplicates),
742
+ published: z4.literal(false)
743
+ };
744
+ var revisionProviderResponseSchema = z4.discriminatedUnion("action", [
745
+ responseBase.extend({ action: z4.literal("check") }).strict(),
746
+ responseBase.extend({ action: z4.literal("preflight") }).strict(),
747
+ responseBase.extend({ action: z4.literal("release") }).strict(),
748
+ responseBase.extend({ action: z4.literal("finalize"), ...terminalResponseFields }).strict(),
749
+ responseBase.extend({ action: z4.literal("recover"), ...terminalResponseFields }).strict()
750
+ ]);
751
+ var errorResponseBase = z4.object({
752
+ protocolVersion: z4.literal(REVISION_PROVIDER_PROTOCOL_VERSION),
753
+ providerId: z4.string().min(1).max(80).regex(/^[a-z0-9][a-z0-9._-]*$/i),
754
+ operationId: z4.string().min(1).max(128),
755
+ ok: z4.literal(false),
756
+ status: z4.string().min(1).max(80).regex(/^[a-z0-9][a-z0-9._-]*$/i),
757
+ error: z4.string().min(1).max(4096)
758
+ });
759
+ var unknownErrorActionSchema = z4.string().min(1).max(80).refine(
760
+ (value) => !revisionActionSchema.options.includes(value),
761
+ { message: "Known provider actions must use their action-specific error schema" }
762
+ );
763
+ var revisionProviderErrorResponseSchema = z4.union([
764
+ errorResponseBase.extend({ action: z4.literal("check") }).strict(),
765
+ errorResponseBase.extend({ action: z4.literal("preflight") }).strict(),
766
+ errorResponseBase.extend({ action: z4.literal("release") }).strict(),
767
+ errorResponseBase.extend({ action: z4.literal("finalize"), ...terminalErrorResponseFields }).strict(),
768
+ errorResponseBase.extend({ action: z4.literal("recover"), ...terminalErrorResponseFields }).strict(),
769
+ errorResponseBase.extend({ action: unknownErrorActionSchema }).strict()
770
+ ]);
771
+ var revisionProviderResultSchema = z4.union([
772
+ revisionProviderResponseSchema,
773
+ revisionProviderErrorResponseSchema
774
+ ]);
775
+ function revisionRequestDigest(request) {
776
+ return createHash("sha256").update(canonicalJson(request)).digest("hex");
777
+ }
778
+ function isSafeRepoRelativePath(value) {
779
+ if (isAbsolute2(value) || value.includes("\\") || value.includes("\0")) return false;
780
+ const normalized = posix.normalize(value);
781
+ const parts = value.split("/");
782
+ if (normalized !== value || parts.some((part) => part === "" || part === "." || part === "..")) return false;
783
+ const first = value.split("/", 1)[0];
784
+ if (first === ".git") return false;
785
+ if (value === ".syncwheel/manifest.json" || value === ".syncwheel/ledger" || value.startsWith(".syncwheel/ledger/")) return false;
786
+ return true;
787
+ }
788
+ function isCanonicalAbsolutePath(value) {
789
+ return isAbsolute2(value) && !value.includes("\0") && !/[\r\n]/u.test(value) && resolve2(value) === value;
790
+ }
791
+ function isSafeGitBranch(value) {
792
+ if (value.startsWith("/") || value.endsWith("/") || value.endsWith(".") || value.includes("@{")) return false;
793
+ if (/[\u0000-\u0020\u007f~^:?*\\[]/u.test(value) || value.includes("..") || value.includes("//")) return false;
794
+ return value.split("/").every((part) => part.length > 0 && part !== "." && part !== ".." && !part.endsWith(".lock"));
795
+ }
796
+ function rejectDuplicates(values, ctx) {
797
+ const seen = /* @__PURE__ */ new Set();
798
+ for (const [index, value] of values.entries()) {
799
+ if (seen.has(value)) ctx.addIssue({ code: "custom", path: [index], message: `Duplicate value: ${value}` });
800
+ seen.add(value);
801
+ }
802
+ }
803
+ function canonicalJson(value) {
804
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
805
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
806
+ const record = value;
807
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
808
+ }
809
+
810
+ // src/mutation/receipts.ts
811
+ import { createHash as createHash2, randomUUID } from "crypto";
812
+ import { chmod, link, mkdir as mkdir2, open, readFile as readFile3, readdir as readdir2, rename as rename2, rm as rm2 } from "fs/promises";
813
+ import { homedir as homedir2 } from "os";
814
+ import { dirname as dirname3, join as join2, resolve as resolve3 } from "path";
815
+ import { z as z5 } from "zod";
816
+ var preexistingPathSchema = z5.object({
817
+ path: z5.string().min(1).max(4096),
818
+ sha256: z5.string().regex(/^[a-f0-9]{64}$/).nullable()
819
+ }).strict();
820
+ var receiptStatusSchema = z5.enum([
821
+ "prepared",
822
+ "handler-succeeded",
823
+ "mutation-applied",
824
+ "succeeded",
825
+ "revisioning-skipped",
826
+ "no-repository-delta",
827
+ "commit-pending",
828
+ "precheck-failed",
829
+ "partial",
830
+ "postcheck-failed",
831
+ "failed"
832
+ ]);
833
+ var runtimeJournalSchema = z5.object({
834
+ path: z5.string().min(1).max(4096),
835
+ status: z5.enum(["reserved", "pending", "resolving", "resolved"]),
836
+ transport: z5.enum(["local", "ssh"]),
837
+ transportDescription: z5.string().min(1).max(1024),
838
+ journalDigest: z5.string().regex(/^[a-f0-9]{64}$/)
839
+ }).strict();
840
+ var mutationReceiptBaseSchema = z5.object({
841
+ version: z5.literal(1),
842
+ revision: z5.number().int().min(1),
843
+ receiptDigest: z5.string().regex(/^[a-f0-9]{64}$/),
844
+ operationId: mutationOperationIdSchema,
845
+ commandName: z5.string().min(1).max(256),
846
+ reason: z5.string().min(1).max(4096),
847
+ noCommit: z5.boolean(),
848
+ workspaceRoot: z5.string().min(1),
849
+ repositoryRoot: z5.string().min(1).nullable(),
850
+ expectedHead: z5.string().min(1).max(256).nullable(),
851
+ expectedManifestDigest: z5.string().regex(/^[a-f0-9]{64}$/).nullable(),
852
+ revisionMode: z5.enum(["off", "commit-after-verify"]),
853
+ provider: revisionProviderConfigSchema.nullable(),
854
+ preexistingPaths: z5.array(preexistingPathSchema),
855
+ paths: z5.array(revisionPathSchema),
856
+ runtimeJournals: z5.array(runtimeJournalSchema).default([]),
857
+ status: receiptStatusSchema,
858
+ createdAt: z5.string().datetime(),
859
+ updatedAt: z5.string().datetime(),
860
+ providerResponse: revisionProviderResultSchema.optional(),
861
+ error: z5.string().min(1).max(4096).optional()
862
+ }).strict();
863
+ var mutationReceiptSchema = mutationReceiptBaseSchema.superRefine((receipt, ctx) => {
864
+ const expected = mutationReceiptDigest(receipt);
865
+ if (receipt.receiptDigest !== expected) {
866
+ ctx.addIssue({
867
+ code: "custom",
868
+ path: ["receiptDigest"],
869
+ message: `Mutation receipt digest mismatch: expected ${expected}`
870
+ });
871
+ }
872
+ });
873
+ function mutationStateRoot() {
874
+ return resolve3(process.env.AGENTWHEEL_MUTATION_STATE_ROOT ?? join2(homedir2(), ".agentwheel", "mutations"));
875
+ }
876
+ async function createMutationReceipt(receipt) {
877
+ const path = receiptPath(receipt.operationId);
878
+ await ensureStateDirectory();
879
+ const reservation = `${path}.reserve`;
880
+ try {
881
+ await open(reservation, "wx", 384).then(async (handle) => {
882
+ await handle.sync();
883
+ await handle.close();
884
+ });
885
+ await fsyncDirectory(dirname3(path));
886
+ } catch (error) {
887
+ if (isAlreadyExists(error)) throw new Error(`Mutation operation '${receipt.operationId}' already has a durable receipt.`);
888
+ throw error;
889
+ }
890
+ try {
891
+ if (await fileExists(path)) throw new Error(`Mutation operation '${receipt.operationId}' already has a durable receipt.`);
892
+ const now = (/* @__PURE__ */ new Date()).toISOString();
893
+ const parsed = sealMutationReceipt({
894
+ ...receipt,
895
+ version: 1,
896
+ revision: 1,
897
+ receiptDigest: "0".repeat(64),
898
+ createdAt: now,
899
+ updatedAt: now
900
+ });
901
+ await writeReceipt(path, parsed);
902
+ return parsed;
903
+ } finally {
904
+ await rm2(reservation, { force: true });
905
+ await fsyncDirectory(dirname3(path));
906
+ }
907
+ }
908
+ async function updateMutationReceipt(receipt, patch) {
909
+ const path = receiptPath(receipt.operationId);
910
+ const release = await acquireReceiptUpdateLock(path, receipt.operationId);
911
+ try {
912
+ const current = await readMutationReceipt(receipt.operationId);
913
+ if (current.revision !== receipt.revision || current.receiptDigest !== receipt.receiptDigest) {
914
+ throw new Error(
915
+ `Mutation receipt '${receipt.operationId}' changed concurrently: expected revision ${receipt.revision}/${receipt.receiptDigest}, found ${current.revision}/${current.receiptDigest}.`
916
+ );
917
+ }
918
+ const next = sealMutationReceipt({
919
+ ...current,
920
+ ...patch,
921
+ revision: current.revision + 1,
922
+ receiptDigest: "0".repeat(64),
923
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
924
+ });
925
+ await writeReceipt(path, next);
926
+ return next;
927
+ } finally {
928
+ await release();
929
+ }
930
+ }
931
+ async function readMutationReceipt(operationId) {
932
+ const id = mutationOperationIdSchema.parse(operationId);
933
+ const path = receiptPath(id);
934
+ try {
935
+ return mutationReceiptSchema.parse(JSON.parse(await readFile3(path, "utf8")));
936
+ } catch (error) {
937
+ if (isMissing(error)) throw new Error(`Unknown mutation operation '${id}'.`);
938
+ throw error;
939
+ }
940
+ }
941
+ async function listMutationReceipts() {
942
+ const root = join2(mutationStateRoot(), "receipts");
943
+ let names;
944
+ try {
945
+ names = await readdir2(root);
946
+ } catch (error) {
947
+ if (isMissing(error)) return [];
948
+ throw error;
949
+ }
950
+ const receipts = [];
951
+ for (const name of names.filter((entry) => entry.endsWith(".json")).sort()) {
952
+ receipts.push(mutationReceiptSchema.parse(JSON.parse(await readFile3(join2(root, name), "utf8"))));
953
+ }
954
+ return receipts.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
955
+ }
956
+ async function acquireMutationLock(repositoryRoot, operationId) {
957
+ const root = mutationStateRoot();
958
+ const digest = createHash2("sha256").update(resolve3(repositoryRoot)).digest("hex");
959
+ const lockPath = join2(root, "locks", `${digest}.lock`);
960
+ await mkdir2(join2(root, "locks"), { recursive: true, mode: 448 });
961
+ await chmod(join2(root, "locks"), 448);
962
+ try {
963
+ await mkdir2(lockPath, { mode: 448 });
964
+ } catch (error) {
965
+ if (!isAlreadyExists(error)) throw error;
966
+ if (!await reapStaleLock(lockPath)) {
967
+ throw new Error(`Another Agentwheel mutation owns the repository lock at ${lockPath}.`);
968
+ }
969
+ await mkdir2(lockPath, { mode: 448 });
970
+ }
971
+ const ownerPath = join2(lockPath, "owner.json");
972
+ await writeSecureJson(ownerPath, {
973
+ version: 1,
974
+ operationId,
975
+ pid: process.pid,
976
+ repositoryRoot: resolve3(repositoryRoot),
977
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
978
+ });
979
+ let released = false;
980
+ return {
981
+ path: lockPath,
982
+ async release() {
983
+ if (released) return;
984
+ released = true;
985
+ await rm2(lockPath, { recursive: true, force: true });
986
+ }
987
+ };
988
+ }
989
+ function receiptPath(operationId) {
990
+ return join2(mutationStateRoot(), "receipts", `${mutationOperationIdSchema.parse(operationId)}.json`);
991
+ }
992
+ async function ensureStateDirectory() {
993
+ const root = mutationStateRoot();
994
+ await mkdir2(join2(root, "receipts"), { recursive: true, mode: 448 });
995
+ await chmod(root, 448);
996
+ await chmod(join2(root, "receipts"), 448);
997
+ }
998
+ async function writeReceipt(path, receipt) {
999
+ await writeSecureJson(path, receipt);
1000
+ }
1001
+ function sealMutationReceipt(value) {
1002
+ const parsed = mutationReceiptBaseSchema.parse(value);
1003
+ return mutationReceiptSchema.parse({ ...parsed, receiptDigest: mutationReceiptDigest(parsed) });
1004
+ }
1005
+ function mutationReceiptDigest(receipt) {
1006
+ const { receiptDigest: _ignored, ...payload } = receipt;
1007
+ return createHash2("sha256").update(canonicalJson2(payload)).digest("hex");
1008
+ }
1009
+ function canonicalJson2(value) {
1010
+ if (Array.isArray(value)) return `[${value.map((entry) => canonicalJson2(entry === void 0 ? null : entry)).join(",")}]`;
1011
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
1012
+ const record = value;
1013
+ return `{${Object.keys(record).filter((key) => record[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson2(record[key])}`).join(",")}}`;
1014
+ }
1015
+ async function acquireReceiptUpdateLock(path, operationId) {
1016
+ const lockPath = `${path}.cas-lock`;
1017
+ const candidatePath = `${lockPath}.candidate-${process.pid}-${randomUUID()}`;
1018
+ const candidate = await open(candidatePath, "wx", 384);
1019
+ try {
1020
+ await candidate.writeFile(`${JSON.stringify({
1021
+ version: 1,
1022
+ operationId,
1023
+ pid: process.pid,
1024
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1025
+ })}
1026
+ `, "utf8");
1027
+ await candidate.sync();
1028
+ } finally {
1029
+ await candidate.close();
1030
+ }
1031
+ let acquired = false;
1032
+ try {
1033
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1034
+ try {
1035
+ await link(candidatePath, lockPath);
1036
+ await fsyncDirectory(dirname3(lockPath));
1037
+ acquired = true;
1038
+ break;
1039
+ } catch (error) {
1040
+ if (!isAlreadyExists(error)) throw error;
1041
+ if (!await reapStaleReceiptUpdateLock(lockPath)) {
1042
+ throw new Error(`Mutation receipt '${operationId}' has another active compare-and-swap writer.`);
1043
+ }
1044
+ }
1045
+ }
1046
+ } finally {
1047
+ await rm2(candidatePath, { force: true });
1048
+ }
1049
+ if (!acquired) throw new Error(`Mutation receipt '${operationId}' compare-and-swap lock could not be acquired.`);
1050
+ let released = false;
1051
+ return async () => {
1052
+ if (released) return;
1053
+ released = true;
1054
+ await rm2(lockPath, { force: true });
1055
+ await fsyncDirectory(dirname3(lockPath));
1056
+ };
1057
+ }
1058
+ async function reapStaleReceiptUpdateLock(lockPath) {
1059
+ try {
1060
+ const owner = JSON.parse(await readFile3(lockPath, "utf8"));
1061
+ if (typeof owner.pid !== "number" || processIsAlive(owner.pid)) return false;
1062
+ } catch {
1063
+ return false;
1064
+ }
1065
+ const archive = `${lockPath}.stale-${process.pid}-${Date.now()}`;
1066
+ try {
1067
+ await rename2(lockPath, archive);
1068
+ await rm2(archive, { force: true });
1069
+ await fsyncDirectory(dirname3(lockPath));
1070
+ return true;
1071
+ } catch {
1072
+ return false;
1073
+ }
1074
+ }
1075
+ async function writeSecureJson(path, value) {
1076
+ const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
1077
+ const handle = await open(temp, "w", 384);
1078
+ try {
1079
+ await handle.writeFile(`${JSON.stringify(value, null, 2)}
1080
+ `, "utf8");
1081
+ await handle.sync();
1082
+ } finally {
1083
+ await handle.close();
1084
+ }
1085
+ await rename2(temp, path);
1086
+ await chmod(path, 384);
1087
+ await fsyncDirectory(dirname3(path));
1088
+ }
1089
+ async function fsyncDirectory(path) {
1090
+ const handle = await open(path, "r");
1091
+ try {
1092
+ await handle.sync();
1093
+ } finally {
1094
+ await handle.close();
1095
+ }
1096
+ }
1097
+ async function fileExists(path) {
1098
+ try {
1099
+ await readFile3(path);
1100
+ return true;
1101
+ } catch (error) {
1102
+ if (isMissing(error)) return false;
1103
+ throw error;
1104
+ }
1105
+ }
1106
+ async function reapStaleLock(lockPath) {
1107
+ try {
1108
+ const owner = JSON.parse(await readFile3(join2(lockPath, "owner.json"), "utf8"));
1109
+ if (typeof owner.pid !== "number" || processIsAlive(owner.pid)) return false;
1110
+ } catch (error) {
1111
+ return false;
1112
+ }
1113
+ const archive = `${lockPath}.stale-${Date.now()}`;
1114
+ try {
1115
+ await rename2(lockPath, archive);
1116
+ await rm2(archive, { recursive: true, force: true });
1117
+ return true;
1118
+ } catch {
1119
+ return false;
1120
+ }
1121
+ }
1122
+ function processIsAlive(pid) {
1123
+ try {
1124
+ process.kill(pid, 0);
1125
+ return true;
1126
+ } catch (error) {
1127
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EPERM";
1128
+ }
1129
+ }
1130
+ function isAlreadyExists(error) {
1131
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
1132
+ }
1133
+ function isMissing(error) {
1134
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1135
+ }
1136
+
1137
+ // src/mutation/declarations.ts
1138
+ var active;
1139
+ function beginMutationPathDeclarations(repositoryRoot, operationIdInput, blockedPaths = []) {
1140
+ if (active) throw new Error("A mutation path declaration scope is already active.");
1141
+ const operationId = mutationOperationIdSchema.parse(operationIdInput);
1142
+ const root = join3(mutationStateRoot(), "declarations");
1143
+ mkdirSync(root, { recursive: true, mode: 448 });
1144
+ chmodSync(root, 448);
1145
+ const journalPath = join3(root, `${operationId}.jsonl`);
1146
+ const journal = openSync(journalPath, "wx", 384);
1147
+ try {
1148
+ fsyncSync(journal);
1149
+ } finally {
1150
+ closeSync(journal);
1151
+ }
1152
+ const directory = openSync(root, "r");
1153
+ try {
1154
+ fsyncSync(directory);
1155
+ } finally {
1156
+ closeSync(directory);
1157
+ }
1158
+ active = {
1159
+ repositoryRoot: resolve4(repositoryRoot),
1160
+ operationId,
1161
+ journalPath,
1162
+ paths: /* @__PURE__ */ new Set(),
1163
+ blockedPaths: new Set(blockedPaths)
1164
+ };
1165
+ }
1166
+ function resumeMutationPathDeclarations(repositoryRoot, operationIdInput, blockedPaths = []) {
1167
+ if (active) throw new Error("A mutation path declaration scope is already active.");
1168
+ const operationId = mutationOperationIdSchema.parse(operationIdInput);
1169
+ const journalPath = join3(mutationStateRoot(), "declarations", `${operationId}.jsonl`);
1170
+ const paths = readMutationPathDeclarations(operationId);
1171
+ if (paths.length === 0) {
1172
+ const journal = openSync(journalPath, "r");
1173
+ closeSync(journal);
1174
+ }
1175
+ active = {
1176
+ repositoryRoot: resolve4(repositoryRoot),
1177
+ operationId,
1178
+ journalPath,
1179
+ paths: new Set(paths),
1180
+ blockedPaths: new Set(blockedPaths)
1181
+ };
1182
+ }
1183
+ function declareMutationPath(path) {
1184
+ if (!active) return;
1185
+ const absolute = isAbsolute3(path) ? resolve4(path) : resolve4(process.cwd(), path);
1186
+ const relativePath = relative(active.repositoryRoot, absolute);
1187
+ if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute3(relativePath)) return;
1188
+ const normalized = relativePath.split(sep).join("/");
1189
+ if (active.blockedPaths.has(normalized)) {
1190
+ throw new Error(`Mutation intended path is already dirty and cannot be claimed: ${normalized}.`);
1191
+ }
1192
+ if (active.paths.has(normalized)) return;
1193
+ active.paths.add(normalized);
1194
+ const fd = openSync(active.journalPath, "a", 384);
1195
+ try {
1196
+ writeSync(fd, `${JSON.stringify({ path: normalized })}
1197
+ `, void 0, "utf8");
1198
+ fsyncSync(fd);
1199
+ } finally {
1200
+ closeSync(fd);
1201
+ }
1202
+ }
1203
+ function declaredMutationPaths() {
1204
+ return active ? [...active.paths].sort((a, b) => a.localeCompare(b)) : [];
1205
+ }
1206
+ function endMutationPathDeclarations() {
1207
+ active = void 0;
1208
+ }
1209
+ function readMutationPathDeclarations(operationIdInput) {
1210
+ const operationId = mutationOperationIdSchema.parse(operationIdInput);
1211
+ const path = join3(mutationStateRoot(), "declarations", `${operationId}.jsonl`);
1212
+ let content;
1213
+ try {
1214
+ content = readFileSync(path, "utf8");
1215
+ } catch (error) {
1216
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return [];
1217
+ throw error;
1218
+ }
1219
+ const paths = /* @__PURE__ */ new Set();
1220
+ for (const line of content.split(/\r?\n/u).filter(Boolean)) {
1221
+ const record = JSON.parse(line);
1222
+ if (typeof record.path !== "string" || !record.path || record.path.startsWith("../") || record.path.includes("\\")) {
1223
+ throw new Error(`Invalid mutation declaration journal for '${operationId}'.`);
1224
+ }
1225
+ paths.add(record.path);
1226
+ }
1227
+ return [...paths].sort((a, b) => a.localeCompare(b));
1228
+ }
1229
+
1230
+ // src/model/workspace.ts
1231
+ var artifactSelectorListSchema = z6.array(z6.string().min(1));
1232
+ var CURRENT_WORKSPACE_SCHEMA_VERSION = 4;
1233
+ var workspaceSelectionImportSchema = z6.object({
1234
+ export: z6.string().min(1),
1235
+ add: artifactSelectorListSchema.optional(),
1236
+ exclude: artifactSelectorListSchema.optional()
1237
+ }).strict();
1238
+ var workspaceSelectionExportSchema = z6.object({
1239
+ extends: z6.string().min(1).optional(),
1240
+ select: artifactSelectorListSchema.optional(),
1241
+ add: artifactSelectorListSchema.optional(),
1242
+ exclude: artifactSelectorListSchema.optional()
1243
+ }).strict().superRefine((selection, ctx) => {
1244
+ if (selection.extends && selection.select) {
1245
+ ctx.addIssue({
1246
+ code: "custom",
1247
+ path: ["select"],
1248
+ message: "Selection exports may use either select or extends, not both."
1249
+ });
1250
+ }
1251
+ if (!selection.extends && !selection.select) {
1252
+ ctx.addIssue({
1253
+ code: "custom",
1254
+ path: ["select"],
1255
+ message: "Selection exports without extends require select."
1256
+ });
1257
+ }
1258
+ });
1259
+ var workspaceExportsSchema = z6.object({
1260
+ selections: z6.record(z6.string().min(1), workspaceSelectionExportSchema).default({})
1261
+ }).strict();
1262
+ var workspacePackageBaseSchema = z6.object({
1263
+ name: z6.string().min(1),
1264
+ source: z6.string().min(1),
1265
+ driver: z6.enum(["local", "git", "skillkit", "vercel-skills", "mcp-registry", "clawhub"]).default("local"),
1266
+ adapter: z6.string().min(1).default("openclaw"),
1267
+ adapterConfig: z6.string().min(1).optional(),
1268
+ adapterModule: z6.string().min(1).optional(),
1269
+ adapterCodeHash: z6.string().min(16).optional(),
1270
+ installationType: installationTypeSchema.optional(),
1271
+ mode: z6.enum(["pinned", "tracking"]).default("pinned"),
1272
+ version: z6.string().min(1).refine(isSupportedVersionRange, {
1273
+ message: "Version policy must be an exact semver, ~range, ^range, comparator range, or *"
1274
+ }).optional(),
1275
+ requestedRef: z6.string().min(1).optional(),
1276
+ select: z6.array(z6.string().min(1)).optional(),
1277
+ skills: z6.array(z6.string().min(1)).optional(),
1278
+ withSuggestions: z6.boolean().optional(),
1279
+ suggestions: z6.array(z6.string().min(1)).optional(),
1280
+ aliases: z6.record(z6.string(), z6.string().min(1)).optional(),
1281
+ overrides: z6.array(z6.string().min(1)).optional()
1282
+ });
1283
+ var workspacePackageSchema = workspacePackageBaseSchema.extend({
1284
+ selection: workspaceSelectionImportSchema.optional()
1285
+ }).superRefine((pkg, ctx) => {
1286
+ if (pkg.selection && (pkg.select !== void 0 || pkg.skills !== void 0)) {
1287
+ ctx.addIssue({
1288
+ code: "custom",
1289
+ path: ["selection"],
1290
+ message: "Packages may use either selection or select/skills, not both."
1291
+ });
1292
+ }
1293
+ });
1294
+ var workspacePackageV1Schema = workspacePackageBaseSchema.extend({
1295
+ selection: z6.never().optional()
1296
+ });
1297
+ var commandSchema = z6.array(z6.string().min(1)).min(1);
1298
+ var commandListSchema = z6.array(commandSchema).min(1).optional();
1299
+ var installStateKeySchema = z6.string().min(1).regex(/^[a-z0-9][a-z0-9._-]*$/i);
1300
+ var fleetIdSchema = z6.string().min(1).regex(/^[a-z0-9][a-z0-9._-]*$/i);
1301
+ var registeredFleetSchema = z6.object({
1302
+ root: z6.string().min(1),
1303
+ requiredPackages: z6.array(z6.string().min(1)).min(1)
1304
+ }).strict().superRefine((fleet, ctx) => {
1305
+ if (new Set(fleet.requiredPackages).size !== fleet.requiredPackages.length) {
1306
+ ctx.addIssue({ code: "custom", path: ["requiredPackages"], message: "Required fleet packages must be unique." });
1307
+ }
1308
+ });
1309
+ var workspaceProfileRuntimeSchema = z6.object({
1310
+ agent: z6.string().min(1).optional(),
1311
+ adapter: z6.string().min(1).default("openclaw"),
1312
+ adapterConfig: z6.string().min(1).optional(),
1313
+ adapterModule: z6.string().min(1).optional(),
1314
+ installationType: installationTypeSchema.optional(),
1315
+ stateKey: installStateKeySchema.optional(),
1316
+ targetRoot: z6.string().min(1).optional(),
1317
+ executePlugins: z6.boolean().optional(),
1318
+ reloadRuntimes: z6.boolean().optional(),
1319
+ reloadCommands: commandListSchema
1320
+ });
1321
+ var workspaceProfileMemberSchema = z6.object({
1322
+ id: z6.string().min(1).regex(/^[a-z0-9][a-z0-9._-]*$/i),
1323
+ workspace: z6.string().min(1),
1324
+ profile: z6.string().min(1),
1325
+ transport: z6.enum(["local", "ssh"]).default("local"),
1326
+ host: z6.string().min(1).optional(),
1327
+ user: z6.string().min(1).optional(),
1328
+ port: z6.number().int().positive().optional(),
1329
+ identityFile: z6.string().min(1).optional(),
1330
+ refreshTtlSeconds: z6.number().int().positive().optional()
1331
+ }).strict().superRefine((member, ctx) => {
1332
+ if (member.transport === "ssh" && !member.host) {
1333
+ ctx.addIssue({ code: "custom", path: ["host"], message: "SSH profile members require host" });
1334
+ }
1335
+ if (member.transport === "ssh" && !member.workspace.startsWith("/")) {
1336
+ ctx.addIssue({ code: "custom", path: ["workspace"], message: "SSH profile member workspaces must be absolute" });
1337
+ }
1338
+ });
1339
+ var workspaceLeafProfileSchema = z6.object({
1340
+ runtimes: z6.array(workspaceProfileRuntimeSchema).min(1),
1341
+ members: z6.never().optional()
1342
+ }).strict();
1343
+ var workspaceCompositeProfileSchema = z6.object({
1344
+ members: z6.array(workspaceProfileMemberSchema).min(1),
1345
+ runtimes: z6.never().optional(),
1346
+ refreshTtlSeconds: z6.number().int().positive().default(86400)
1347
+ }).strict().superRefine((profile, ctx) => {
1348
+ const seen = /* @__PURE__ */ new Set();
1349
+ for (const [index, member] of profile.members.entries()) {
1350
+ if (seen.has(member.id)) {
1351
+ ctx.addIssue({ code: "custom", path: ["members", index, "id"], message: "Composite profile member ids must be unique" });
1352
+ }
1353
+ seen.add(member.id);
1354
+ }
1355
+ });
1356
+ var workspaceProfileSchema = z6.union([
1357
+ workspaceLeafProfileSchema,
1358
+ workspaceCompositeProfileSchema
1359
+ ]);
1360
+ var workspaceRegistrySchema = z6.object({
1361
+ sources: z6.array(z6.string().min(1)).optional(),
1362
+ ttlSeconds: z6.number().int().positive().optional()
1363
+ }).default({});
1364
+ var workspaceTrustSchema = z6.object({
1365
+ allow: z6.array(z6.string().min(1)).optional(),
1366
+ acceptedSources: z6.array(z6.string().min(1)).optional(),
1367
+ denyArtifactTypes: z6.array(artifactTypeSchema).optional(),
1368
+ requireReviewForTransitive: z6.boolean().optional()
1369
+ }).default({});
1370
+ var workspaceAgentSchema = z6.object({
1371
+ adapter: z6.string().min(1),
1372
+ adapterConfig: z6.string().min(1).optional(),
1373
+ adapterModule: z6.string().min(1).optional(),
1374
+ root: z6.string().min(1),
1375
+ installationType: installationTypeSchema.optional(),
1376
+ stateKey: installStateKeySchema.optional(),
1377
+ transport: z6.enum(["local", "ssh"]).default("local"),
1378
+ host: z6.string().min(1).optional(),
1379
+ user: z6.string().min(1).optional(),
1380
+ port: z6.number().int().positive().optional(),
1381
+ identityFile: z6.string().min(1).optional(),
1382
+ reloadCommands: commandListSchema
1383
+ }).superRefine((agent, ctx) => {
1384
+ if (agent.transport !== "ssh") return;
1385
+ if (!agent.host) {
1386
+ ctx.addIssue({
1387
+ code: "custom",
1388
+ path: ["host"],
1389
+ message: "SSH agents require host"
1390
+ });
1391
+ }
1392
+ });
1393
+ var workspaceConfigBaseSchema = z6.object({
1394
+ bootstrapSkills: z6.boolean().optional(),
1395
+ registry: workspaceRegistrySchema,
1396
+ trust: workspaceTrustSchema,
1397
+ profiles: z6.record(z6.string(), workspaceProfileSchema).default({}),
1398
+ agents: z6.record(z6.string(), workspaceAgentSchema).default({})
1399
+ });
1400
+ var workspaceConfigV1Schema = workspaceConfigBaseSchema.extend({
1401
+ schemaVersion: z6.literal(1),
1402
+ packages: z6.array(workspacePackageV1Schema).default([]),
1403
+ exports: z6.never().optional()
1404
+ }).strict();
1405
+ var workspaceConfigV2Schema = workspaceConfigBaseSchema.extend({
1406
+ schemaVersion: z6.literal(2),
1407
+ packages: z6.array(workspacePackageSchema).default([]),
1408
+ exports: workspaceExportsSchema.default({ selections: {} })
1409
+ }).strict();
1410
+ var workspaceConfigV3Schema = workspaceConfigBaseSchema.extend({
1411
+ schemaVersion: z6.literal(3),
1412
+ packages: z6.array(workspacePackageSchema).default([]),
1413
+ exports: workspaceExportsSchema.default({ selections: {} }),
1414
+ fleetId: fleetIdSchema.optional(),
1415
+ fleets: z6.record(fleetIdSchema, registeredFleetSchema).default({})
1416
+ }).strict();
1417
+ var workspaceConfigV4Schema = workspaceConfigBaseSchema.extend({
1418
+ schemaVersion: z6.literal(CURRENT_WORKSPACE_SCHEMA_VERSION),
1419
+ packages: z6.array(workspacePackageSchema).default([]),
1420
+ exports: workspaceExportsSchema.default({ selections: {} }),
1421
+ fleetId: fleetIdSchema.optional(),
1422
+ fleets: z6.record(fleetIdSchema, registeredFleetSchema).default({}),
1423
+ mutationPolicy: mutationPolicySchema.optional()
1424
+ }).strict();
1425
+ var workspaceConfigSchema = z6.discriminatedUnion("schemaVersion", [
1426
+ workspaceConfigV1Schema,
1427
+ workspaceConfigV2Schema,
1428
+ workspaceConfigV3Schema,
1429
+ workspaceConfigV4Schema
1430
+ ]);
1431
+ function supportsFleetConfig(config) {
1432
+ return config.schemaVersion === 3 || config.schemaVersion === 4;
1433
+ }
1434
+ function workspaceConfigPath(workspaceRoot) {
1435
+ return join4(workspaceRoot, ".agentwheel", "config.json");
1436
+ }
1437
+ async function readWorkspaceConfig(workspaceRoot) {
1438
+ const path = workspaceConfigPath(workspaceRoot);
1439
+ if (!await pathExists(path)) return emptyWorkspaceConfig();
1440
+ return workspaceConfigSchema.parse(JSON.parse(await readFile4(path, "utf8")));
1441
+ }
1442
+ async function writeWorkspaceConfig(workspaceRoot, config) {
1443
+ const path = workspaceConfigPath(workspaceRoot);
1444
+ declareMutationPath(path);
1445
+ await writeJsonAtomic(path, workspaceConfigSchema.parse(config));
1446
+ }
1447
+ function upsertPackage(config, entry) {
1448
+ const parsed = workspaceConfigSchema.parse(config);
1449
+ const packages = parsed.packages.filter((candidate) => candidate.name !== entry.name);
1450
+ packages.push(entry);
1451
+ packages.sort((a, b) => a.name.localeCompare(b.name));
1452
+ return workspaceConfigSchema.parse({ ...parsed, packages });
1453
+ }
1454
+ function globalWorkspaceConfigPath(globalRoot = homedir3()) {
1455
+ return join4(globalRoot, ".agentwheel", "config.json");
1456
+ }
1457
+ async function findWorkspaceRoot(start = process.cwd()) {
1458
+ let current = resolve5(start);
1459
+ while (true) {
1460
+ if (await pathExists(workspaceConfigPath(current))) return current;
1461
+ const parent = dirname4(current);
1462
+ if (parent === current) return resolve5(start);
1463
+ current = parent;
1464
+ }
1465
+ }
1466
+ async function findExistingWorkspaceRoot(start = process.cwd()) {
1467
+ let current = resolve5(start);
1468
+ while (true) {
1469
+ if (await pathExists(workspaceConfigPath(current))) return current;
1470
+ const parent = dirname4(current);
1471
+ if (parent === current) return void 0;
1472
+ current = parent;
1473
+ }
1474
+ }
1475
+ async function readMergedWorkspaceConfig(projectRoot, options = {}) {
1476
+ const global = await readConfigPath(globalWorkspaceConfigPath(options.globalRoot));
1477
+ const project = await readWorkspaceConfig(projectRoot);
1478
+ return mergeWorkspaceConfig(global, project);
1479
+ }
1480
+ function mergeWorkspaceConfig(global, project) {
1481
+ const schemaVersion = project.schemaVersion;
1482
+ const exports = project.schemaVersion >= 2 ? project.exports : void 0;
1483
+ return workspaceConfigSchema.parse({
1484
+ schemaVersion,
1485
+ packages: project.packages,
1486
+ bootstrapSkills: project.bootstrapSkills,
1487
+ registry: {
1488
+ ...global.registry,
1489
+ ...project.registry,
1490
+ sources: project.registry.sources ?? global.registry.sources,
1491
+ ttlSeconds: project.registry.ttlSeconds ?? global.registry.ttlSeconds
1492
+ },
1493
+ trust: mergeWorkspaceTrust(global.trust, project.trust),
1494
+ profiles: project.profiles,
1495
+ agents: project.agents,
1496
+ ...exports ? { exports } : {},
1497
+ ...supportsFleetConfig(project) ? { fleetId: project.fleetId, fleets: project.fleets } : {},
1498
+ ...project.schemaVersion === 4 ? { mutationPolicy: project.mutationPolicy } : {}
1499
+ });
1500
+ }
1501
+ function resolveConfigPath(path, baseRoot) {
1502
+ if (path.startsWith("~/")) return resolve5(homedir3(), path.slice(2));
1503
+ if (path === "~") return homedir3();
1504
+ return path.startsWith("/") ? resolve5(path) : resolve5(baseRoot, path);
1505
+ }
1506
+ function emptyWorkspaceConfig() {
1507
+ return { schemaVersion: 1, packages: [], registry: {}, trust: {}, profiles: {}, agents: {} };
1508
+ }
1509
+ function isCompositeWorkspaceProfile(profile) {
1510
+ return "members" in profile && Array.isArray(profile.members);
1511
+ }
1512
+ async function readConfigPath(path) {
1513
+ if (!await pathExists(path)) return emptyWorkspaceConfig();
1514
+ return workspaceConfigSchema.parse(JSON.parse(await readFile4(path, "utf8")));
1515
+ }
1516
+ function mergeWorkspaceTrust(global, project) {
1517
+ return {
1518
+ allow: sortedUnique([...global?.allow ?? [], ...project?.allow ?? []]),
1519
+ acceptedSources: sortedUnique([...global?.acceptedSources ?? [], ...project?.acceptedSources ?? []]),
1520
+ denyArtifactTypes: sortedUnique([...global?.denyArtifactTypes ?? [], ...project?.denyArtifactTypes ?? []]),
1521
+ requireReviewForTransitive: project?.requireReviewForTransitive ?? global?.requireReviewForTransitive
1522
+ };
1523
+ }
1524
+ function sortedUnique(values) {
1525
+ return [...new Set(values)].sort((a, b) => a.localeCompare(b));
1526
+ }
1527
+
1528
+ // src/mutation/providers.ts
1529
+ import { createHash as createHash4 } from "crypto";
1530
+ import { chmod as chmod2, mkdtemp, open as open2, readFile as readFile6, rename as rename3, rm as rm3, rmdir, stat, writeFile as writeFile2 } from "fs/promises";
1531
+ import { tmpdir } from "os";
1532
+ import { isAbsolute as isAbsolute4, join as join5, resolve as resolve7 } from "path";
1533
+
1534
+ // src/mutation/repository.ts
1535
+ import { createHash as createHash3 } from "crypto";
1536
+ import { lstat, readFile as readFile5, readlink, readdir as readdir3 } from "fs/promises";
1537
+ import { posix as posix2, relative as relative2, resolve as resolve6, sep as sep2 } from "path";
1538
+
1539
+ // src/mutation/process.ts
1540
+ import { spawn as spawn2 } from "child_process";
1541
+ async function runProcess(command, args, options = {}) {
1542
+ return new Promise((resolve10, reject) => {
1543
+ const useProcessGroup = options.killProcessGroup === true && process.platform !== "win32";
1544
+ const child = spawn2(command, args, {
1545
+ cwd: options.cwd,
1546
+ env: options.env,
1547
+ stdio: options.inheritedFileDescriptor === void 0 ? ["pipe", "pipe", "pipe"] : ["pipe", "pipe", "pipe", options.inheritedFileDescriptor],
1548
+ detached: useProcessGroup,
1549
+ windowsHide: true
1550
+ });
1551
+ const stdout = [];
1552
+ const stderr = [];
1553
+ let stdoutBytes = 0;
1554
+ let stderrBytes = 0;
1555
+ let outputExceeded = false;
1556
+ let timedOut = false;
1557
+ const terminate = () => {
1558
+ if (useProcessGroup && child.pid !== void 0) {
1559
+ try {
1560
+ process.kill(-child.pid, "SIGKILL");
1561
+ return;
1562
+ } catch {
1563
+ }
1564
+ }
1565
+ child.kill("SIGKILL");
1566
+ };
1567
+ const timeout = options.timeoutMs === void 0 ? void 0 : setTimeout(() => {
1568
+ timedOut = true;
1569
+ terminate();
1570
+ }, options.timeoutMs);
1571
+ const collect = (target, stream, chunk) => {
1572
+ if (outputExceeded) return;
1573
+ if (stream === "stdout") stdoutBytes += chunk.length;
1574
+ else stderrBytes += chunk.length;
1575
+ if (options.maxOutputBytes !== void 0 && (stdoutBytes > options.maxOutputBytes || stderrBytes > options.maxOutputBytes)) {
1576
+ outputExceeded = true;
1577
+ terminate();
1578
+ return;
1579
+ }
1580
+ target.push(chunk);
1581
+ };
1582
+ child.stdout.on("data", (chunk) => collect(stdout, "stdout", chunk));
1583
+ child.stderr.on("data", (chunk) => collect(stderr, "stderr", chunk));
1584
+ child.on("error", reject);
1585
+ child.on("close", (code) => {
1586
+ if (timeout) clearTimeout(timeout);
1587
+ const exitCode = code ?? 1;
1588
+ const result = { stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr), exitCode };
1589
+ if (timedOut) {
1590
+ reject(new Error(`${command} exceeded the ${options.timeoutMs}ms execution timeout.`));
1591
+ return;
1592
+ }
1593
+ if (outputExceeded) {
1594
+ reject(new Error(`${command} exceeded the provider output limit.`));
1595
+ return;
1596
+ }
1597
+ if (!(options.allowExitCodes ?? [0]).includes(exitCode)) {
1598
+ const detail = options.includeFailureOutput === false ? "" : result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim();
1599
+ reject(new Error(`${command} exited ${exitCode}${detail ? `: ${detail}` : ""}`));
1600
+ return;
1601
+ }
1602
+ resolve10(result);
1603
+ });
1604
+ if (options.input !== void 0) child.stdin.end(options.input);
1605
+ else child.stdin.end();
1606
+ });
1607
+ }
1608
+
1609
+ // src/mutation/repository.ts
1610
+ async function discoverGitRepository(start) {
1611
+ const probe = await runProcess("git", ["-C", start, "rev-parse", "--show-toplevel"], { allowExitCodes: [0, 128] });
1612
+ if (probe.exitCode !== 0) return void 0;
1613
+ const root = probe.stdout.toString("utf8").trim();
1614
+ const head = (await runProcess("git", ["-C", root, "rev-parse", "HEAD"])).stdout.toString("utf8").trim();
1615
+ const branchProbe = await runProcess("git", ["-C", root, "symbolic-ref", "--quiet", "--short", "HEAD"], { allowExitCodes: [0, 1] });
1616
+ if (branchProbe.exitCode !== 0) throw new Error(`Revisioning requires an attached Git branch at ${root}.`);
1617
+ return { root, head, branch: branchProbe.stdout.toString("utf8").trim() };
1618
+ }
1619
+ async function assertGitPreflight(repository) {
1620
+ const current = (await runProcess("git", ["-C", repository.root, "rev-parse", "HEAD"])).stdout.toString("utf8").trim();
1621
+ if (current !== repository.head) throw new Error(`Git HEAD changed during mutation preflight: expected ${repository.head}, found ${current}.`);
1622
+ const staged = await runProcess("git", ["-C", repository.root, "diff", "--cached", "--quiet"], { allowExitCodes: [0, 1] });
1623
+ if (staged.exitCode !== 0) throw new Error("Revisioning requires a clean Git index; existing staged changes are not owned by this operation.");
1624
+ const conflicts = (await runProcess("git", ["-C", repository.root, "diff", "--name-only", "--diff-filter=U", "-z"])).stdout;
1625
+ if (conflicts.length > 0) throw new Error("Revisioning refuses a repository with unresolved Git conflicts.");
1626
+ const gitDir = (await runProcess("git", ["-C", repository.root, "rev-parse", "--absolute-git-dir"])).stdout.toString("utf8").trim();
1627
+ for (const marker of ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"]) {
1628
+ const result = await runProcess("git", ["-C", repository.root, "rev-parse", "--verify", "--quiet", marker], { allowExitCodes: [0, 1, 128] });
1629
+ if (result.exitCode === 0) throw new Error(`Revisioning refuses an in-progress Git operation (${marker}) in ${gitDir}.`);
1630
+ }
1631
+ for (const marker of ["rebase-merge", "rebase-apply"]) {
1632
+ if (await exists(resolve6(gitDir, marker))) throw new Error(`Revisioning refuses an in-progress Git operation (${marker}) in ${gitDir}.`);
1633
+ }
1634
+ }
1635
+ async function snapshotRepository(repositoryRoot) {
1636
+ const paths = await changedRepositoryPaths(repositoryRoot);
1637
+ const changed = /* @__PURE__ */ new Map();
1638
+ for (const path of paths) changed.set(path, await hashWorkingPath(repositoryRoot, path));
1639
+ return { changed };
1640
+ }
1641
+ async function collectIntroducedPaths(repositoryRoot, expectedHead, before, declaredPaths) {
1642
+ const after = await snapshotRepository(repositoryRoot);
1643
+ const touchedPreexisting = /* @__PURE__ */ new Set();
1644
+ for (const [path, hash] of before.changed) {
1645
+ if (!after.changed.has(path) || after.changed.get(path) !== hash) touchedPreexisting.add(path);
1646
+ }
1647
+ if (touchedPreexisting.size > 0) {
1648
+ throw new Error(`Mutation touched pre-existing dirty paths: ${[...touchedPreexisting].sort().join(", ")}.`);
1649
+ }
1650
+ const introduced = [...after.changed.keys()].filter((path) => !before.changed.has(path)).sort((a, b) => a.localeCompare(b));
1651
+ const allowed = new Set(declaredPaths);
1652
+ const unexpected = introduced.filter((path) => !allowed.has(path));
1653
+ if (unexpected.length > 0) {
1654
+ throw new Error(`Mutation produced undeclared repository paths: ${unexpected.join(", ")}.`);
1655
+ }
1656
+ const result = [];
1657
+ for (const path of introduced) {
1658
+ result.push({
1659
+ path,
1660
+ beforeSha256: await hashHeadPath(repositoryRoot, expectedHead, path),
1661
+ afterSha256: after.changed.get(path) ?? null
1662
+ });
1663
+ }
1664
+ return result;
1665
+ }
1666
+ async function verifyRevisionPaths(repositoryRoot, expectedHead, paths) {
1667
+ for (const entry of paths) {
1668
+ const before = await hashHeadPath(repositoryRoot, expectedHead, entry.path);
1669
+ const after = await hashWorkingPath(repositoryRoot, entry.path);
1670
+ if (before !== entry.beforeSha256 || after !== entry.afterSha256) {
1671
+ throw new Error(`Revision path lease changed for ${entry.path}.`);
1672
+ }
1673
+ }
1674
+ }
1675
+ async function currentHead(repositoryRoot) {
1676
+ return (await runProcess("git", ["-C", repositoryRoot, "rev-parse", "HEAD"])).stdout.toString("utf8").trim();
1677
+ }
1678
+ async function changedRepositoryPaths(repositoryRoot) {
1679
+ const result = await runProcess("git", [
1680
+ "-C",
1681
+ repositoryRoot,
1682
+ "ls-files",
1683
+ "-m",
1684
+ "-d",
1685
+ "-o",
1686
+ "--exclude-standard",
1687
+ "-z"
1688
+ ]);
1689
+ return [...new Set(splitNull(result.stdout).map(normalizeRepoPath))].sort((a, b) => a.localeCompare(b));
1690
+ }
1691
+ async function hashHeadPath(repositoryRoot, head, path) {
1692
+ const result = await runProcess("git", ["-C", repositoryRoot, "show", `${head}:${path}`], { allowExitCodes: [0, 128] });
1693
+ if (result.exitCode !== 0) return null;
1694
+ return sha256(result.stdout);
1695
+ }
1696
+ async function hashWorkingPath(repositoryRoot, path) {
1697
+ const absolute = resolve6(repositoryRoot, ...path.split("/"));
1698
+ let stats;
1699
+ try {
1700
+ stats = await lstat(absolute);
1701
+ } catch (error) {
1702
+ if (isMissing2(error)) return null;
1703
+ throw error;
1704
+ }
1705
+ if (stats.isSymbolicLink()) return sha256(Buffer.from(await readlink(absolute), "utf8"));
1706
+ if (stats.isFile()) return sha256(await readFile5(absolute));
1707
+ if (stats.isDirectory()) return hashDirectory(absolute);
1708
+ throw new Error(`Unsupported repository path kind: ${path}`);
1709
+ }
1710
+ async function hashDirectory(root) {
1711
+ const hash = createHash3("sha256");
1712
+ async function walk(dir) {
1713
+ for (const entry of (await readdir3(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
1714
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
1715
+ const absolute = resolve6(dir, entry.name);
1716
+ const relativePath = normalizeRepoPath(relative2(root, absolute));
1717
+ if (entry.isDirectory()) await walk(absolute);
1718
+ else if (entry.isSymbolicLink()) hash.update(relativePath).update("\0").update(await readlink(absolute)).update("\0");
1719
+ else if (entry.isFile()) hash.update(relativePath).update("\0").update(await readFile5(absolute)).update("\0");
1720
+ }
1721
+ }
1722
+ await walk(root);
1723
+ return hash.digest("hex");
1724
+ }
1725
+ function splitNull(value) {
1726
+ return value.toString("utf8").split("\0").filter(Boolean);
1727
+ }
1728
+ function normalizeRepoPath(path) {
1729
+ return sep2 === "/" ? path : path.split(sep2).join(posix2.sep);
1730
+ }
1731
+ function sha256(value) {
1732
+ return createHash3("sha256").update(value).digest("hex");
1733
+ }
1734
+ function isMissing2(error) {
1735
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1736
+ }
1737
+ async function exists(path) {
1738
+ try {
1739
+ await lstat(path);
1740
+ return true;
1741
+ } catch (error) {
1742
+ if (isMissing2(error)) return false;
1743
+ throw error;
1744
+ }
1745
+ }
1746
+
1747
+ // src/mutation/providers.ts
1748
+ var RevisionProviderRejectedError = class extends Error {
1749
+ constructor(response) {
1750
+ super(`Revision provider '${response.providerId}' rejected ${response.action}: ${response.error}`);
1751
+ this.response = response;
1752
+ this.name = "RevisionProviderRejectedError";
1753
+ }
1754
+ response;
1755
+ };
1756
+ function revisionProviderRejection(error) {
1757
+ return error instanceof RevisionProviderRejectedError ? error.response : void 0;
1758
+ }
1759
+ async function invokeRevisionProvider(configInput, input, options) {
1760
+ const config = revisionProviderConfigSchema.parse(configInput);
1761
+ const request = revisionProviderRequestSchema.parse(input);
1762
+ const raw = config.kind === "git" ? await invokeGitProvider(config.id, request) : await invokeCommandProvider(
1763
+ config.command,
1764
+ config.executableSha256,
1765
+ config.timeoutMs,
1766
+ request,
1767
+ options.afterCommandExecutablePinned
1768
+ );
1769
+ const failure = revisionProviderErrorResponseSchema.safeParse(raw);
1770
+ if (failure.success) {
1771
+ assertProviderCorrelation(config.id, request, failure.data);
1772
+ throw new RevisionProviderRejectedError(failure.data);
1773
+ }
1774
+ const response = revisionProviderResponseSchema.parse(raw);
1775
+ assertProviderCorrelation(config.id, request, response);
1776
+ assertProviderStatus(response);
1777
+ await assertProviderSemantics(request, response);
1778
+ return response;
1779
+ }
1780
+ function assertProviderCorrelation(providerId, request, response) {
1781
+ if (response.providerId !== providerId) {
1782
+ throw new Error(`Revision provider id mismatch: expected ${providerId}, found ${response.providerId}.`);
1783
+ }
1784
+ if (response.action !== request.action) {
1785
+ throw new Error(`Revision provider action mismatch: expected ${request.action}, found ${response.action}.`);
1786
+ }
1787
+ if (response.operationId !== request.operationId) {
1788
+ throw new Error(`Revision provider operation mismatch: expected ${request.operationId}, found ${response.operationId}.`);
1789
+ }
1790
+ }
1791
+ function assertProviderStatus(response) {
1792
+ const allowed = {
1793
+ check: ["ready"],
1794
+ preflight: ["prepared", "product_committed", "stack_owned", "control_committed", "verified"],
1795
+ release: ["released"],
1796
+ finalize: ["verified", "already-verified", "revisioning-skipped", "no-repository-delta"],
1797
+ recover: ["verified", "already-verified", "revisioning-skipped", "no-repository-delta"]
1798
+ };
1799
+ if (!allowed[response.action].includes(response.status)) {
1800
+ throw new Error(`Revision provider returned invalid ${response.action} status '${response.status}'.`);
1801
+ }
1802
+ }
1803
+ async function invokeCommandProvider(command, executableSha256, timeoutMs, request, afterExecutablePinned) {
1804
+ const [executable, ...args] = command;
1805
+ const descriptorPath = immutableDescriptorExecutablePath();
1806
+ const executableBytes = await readVerifiedExecutable(executable, executableSha256);
1807
+ const executableHandle = await openImmutableExecutableSnapshot(executableBytes);
1808
+ let result;
1809
+ try {
1810
+ await afterExecutablePinned?.(executable);
1811
+ result = await runProcess(descriptorPath, args, {
1812
+ cwd: request.repositoryRoot,
1813
+ input: `${JSON.stringify(request)}
1814
+ `,
1815
+ allowExitCodes: [0, 2],
1816
+ maxOutputBytes: 65536,
1817
+ includeFailureOutput: false,
1818
+ timeoutMs,
1819
+ inheritedFileDescriptor: executableHandle.fd,
1820
+ killProcessGroup: true
1821
+ });
1822
+ } finally {
1823
+ await executableHandle.close();
1824
+ }
1825
+ const output = result.stdout.toString("utf8").trim();
1826
+ if (!output) throw new Error(`Revision provider command '${executable}' returned no JSON response.`);
1827
+ let parsed;
1828
+ try {
1829
+ parsed = JSON.parse(output);
1830
+ } catch {
1831
+ throw new Error(`Revision provider command '${executable}' returned invalid JSON.`);
1832
+ }
1833
+ if (result.exitCode === 2 && parsed.ok !== false) {
1834
+ throw new Error(`Revision provider command '${executable}' exited 2 without a protocol rejection.`);
1835
+ }
1836
+ if (result.exitCode === 0 && parsed.ok === false) {
1837
+ throw new Error(`Revision provider command '${executable}' returned a rejection with exit 0.`);
1838
+ }
1839
+ return parsed;
1840
+ }
1841
+ async function readVerifiedExecutable(executable, expectedSha256) {
1842
+ const handle = await open2(executable, "r");
1843
+ try {
1844
+ const info = await handle.stat();
1845
+ if (!info.isFile()) throw new Error(`Revision provider executable must be a regular file: ${executable}.`);
1846
+ const bytes = await handle.readFile();
1847
+ const actualSha256 = createHash4("sha256").update(bytes).digest("hex");
1848
+ if (actualSha256 !== expectedSha256) {
1849
+ throw new Error(
1850
+ `Revision provider executable hash mismatch for ${executable}: expected ${expectedSha256}, found ${actualSha256}.`
1851
+ );
1852
+ }
1853
+ return bytes;
1854
+ } finally {
1855
+ await handle.close();
1856
+ }
1857
+ }
1858
+ async function openImmutableExecutableSnapshot(bytes) {
1859
+ if (process.platform !== "linux") {
1860
+ throw new Error(
1861
+ `Command revision providers require an unlinked immutable executable snapshot, which is unsupported on ${process.platform}.`
1862
+ );
1863
+ }
1864
+ const root = await mkdtemp(join5(tmpdir(), "agentwheel-revision-command-"));
1865
+ const path = join5(root, "entrypoint");
1866
+ let snapshot;
1867
+ try {
1868
+ await chmod2(root, 448);
1869
+ const writer = await open2(path, "wx", 320);
1870
+ try {
1871
+ await writer.writeFile(bytes);
1872
+ await writer.sync();
1873
+ } finally {
1874
+ await writer.close();
1875
+ }
1876
+ snapshot = await open2(path, "r");
1877
+ await rm3(path);
1878
+ await rmdir(root);
1879
+ return snapshot;
1880
+ } catch (error) {
1881
+ await snapshot?.close().catch(() => void 0);
1882
+ await rm3(root, { recursive: true, force: true }).catch(() => void 0);
1883
+ throw error;
1884
+ }
1885
+ }
1886
+ function immutableDescriptorExecutablePath() {
1887
+ if (process.platform === "linux") return "/proc/self/fd/3";
1888
+ throw new Error(
1889
+ `Command revision providers require an unlinked immutable executable snapshot, which is unsupported on ${process.platform}.`
1890
+ );
1891
+ }
1892
+ async function assertProviderSemantics(request, response) {
1893
+ if (response.action !== "finalize" && response.action !== "recover") return;
1894
+ if (response.expectedHead !== request.expectedHead) {
1895
+ throw new Error(`Revision provider expectedHead mismatch: expected ${request.expectedHead}, found ${response.expectedHead}.`);
1896
+ }
1897
+ if (response.unmappedIntegrationCommits.length > 0) {
1898
+ throw new Error(`Revision provider cannot report terminal success with unmapped integration commits: ${response.unmappedIntegrationCommits.join(", ")}.`);
1899
+ }
1900
+ const ownership = [response.draftStackId, response.draftBranch, response.draftTipSha, response.controlCommitSha];
1901
+ const ownsDraft = ownership.some((value) => value !== null);
1902
+ if (ownsDraft && ownership.some((value) => value === null)) {
1903
+ throw new Error("Revision provider returned incomplete draft ownership fields.");
1904
+ }
1905
+ if (ownsDraft && response.manifestDigest === null) {
1906
+ throw new Error("Revision provider returned draft ownership without a manifest digest.");
1907
+ }
1908
+ if (request.noCommit) {
1909
+ if (response.status !== "revisioning-skipped") {
1910
+ throw new Error(`noCommit requires revisioning-skipped, found ${response.status}.`);
1911
+ }
1912
+ if (response.productCommitSha || ownsDraft || response.resultingHead !== request.expectedHead) {
1913
+ throw new Error("A noCommit response may not create product/control commits or draft ownership.");
1914
+ }
1915
+ } else if (request.paths.length === 0) {
1916
+ if (response.status !== "no-repository-delta") {
1917
+ throw new Error(`An empty path set requires no-repository-delta, found ${response.status}.`);
1918
+ }
1919
+ if (response.productCommitSha || ownsDraft || response.resultingHead !== request.expectedHead) {
1920
+ throw new Error("A no-repository-delta response may not create product/control commits or draft ownership.");
1921
+ }
1922
+ } else {
1923
+ if (!(/* @__PURE__ */ new Set(["verified", "already-verified"])).has(response.status) || !response.productCommitSha) {
1924
+ throw new Error("A non-empty committed mutation requires a verified product commit.");
1925
+ }
1926
+ await verifyOperationCommit(
1927
+ request,
1928
+ response.productCommitSha
1929
+ );
1930
+ const terminalCommit = response.controlCommitSha ?? response.productCommitSha;
1931
+ if (response.resultingHead !== terminalCommit) {
1932
+ throw new Error(`Revision provider resultingHead ${response.resultingHead} does not match terminal commit ${terminalCommit}.`);
1933
+ }
1934
+ if (response.controlCommitSha) {
1935
+ const parent = (await runProcess("git", ["-C", request.repositoryRoot, "rev-parse", `${response.controlCommitSha}^`])).stdout.toString("utf8").trim();
1936
+ if (parent !== response.productCommitSha) {
1937
+ throw new Error(`Revision provider control commit ${response.controlCommitSha} does not descend directly from the product commit.`);
1938
+ }
1939
+ }
1940
+ if (response.draftBranch) {
1941
+ await runProcess("git", ["-C", request.repositoryRoot, "check-ref-format", "--branch", response.draftBranch]);
1942
+ const branchHead = (await runProcess(
1943
+ "git",
1944
+ ["-C", request.repositoryRoot, "rev-parse", "--verify", `refs/heads/${response.draftBranch}`]
1945
+ )).stdout.toString("utf8").trim();
1946
+ if (branchHead !== response.draftTipSha) {
1947
+ throw new Error(
1948
+ `Revision provider draft branch ${response.draftBranch} points to ${branchHead}, not draft tip ${response.draftTipSha}.`
1949
+ );
1950
+ }
1951
+ }
1952
+ }
1953
+ const head = await currentHead(request.repositoryRoot);
1954
+ if (head !== response.resultingHead) {
1955
+ throw new Error(`Revision provider resultingHead ${response.resultingHead} does not match repository HEAD ${head}.`);
1956
+ }
1957
+ }
1958
+ async function invokeGitProvider(providerId, request) {
1959
+ const repository = await discoverGitRepository(request.repositoryRoot);
1960
+ if (!repository || repository.root !== resolve7(request.repositoryRoot)) {
1961
+ throw new Error(`Git revision provider requires the repository root itself: ${request.repositoryRoot}.`);
1962
+ }
1963
+ if (request.action === "check" || request.action === "preflight") {
1964
+ if (repository.head !== request.expectedHead) {
1965
+ if (request.action === "check") {
1966
+ throw new Error(`Git HEAD lease mismatch: expected ${request.expectedHead}, found ${repository.head}.`);
1967
+ }
1968
+ const planDigest = providerPlanDigest(request);
1969
+ const existing = await findOperationCommit(request.repositoryRoot, request.operationId, planDigest);
1970
+ if (!existing) throw new Error(`Git HEAD lease mismatch: expected ${request.expectedHead}, found ${repository.head}.`);
1971
+ await verifyOperationCommit(asTerminalRequest(request, "recover"), existing);
1972
+ return revisionProviderResponseSchema.parse(baseResponse(providerId, request, "prepared"));
1973
+ }
1974
+ await assertGitPreflight(repository);
1975
+ await assertGitProviderCompatible(request.repositoryRoot);
1976
+ await assertGitIdentity(request.repositoryRoot);
1977
+ return revisionProviderResponseSchema.parse(baseResponse(providerId, request, request.action === "check" ? "ready" : "prepared"));
1978
+ }
1979
+ if (request.action === "release") return revisionProviderResponseSchema.parse(baseResponse(providerId, request, "released"));
1980
+ return finalizeGit(providerId, request);
1981
+ }
1982
+ async function finalizeGit(providerId, request) {
1983
+ const planDigest = providerPlanDigest(request);
1984
+ const existing = await findOperationCommit(request.repositoryRoot, request.operationId, planDigest);
1985
+ if (existing) {
1986
+ await verifyOperationCommit(request, existing);
1987
+ await realignIndexAfterRecoveredCommit(request.repositoryRoot, request.expectedHead, existing);
1988
+ return terminalResponse(providerId, request, "already-verified", existing, await currentHead(request.repositoryRoot));
1989
+ }
1990
+ const repository = await discoverGitRepository(request.repositoryRoot);
1991
+ if (!repository) throw new Error(`No Git repository at ${request.repositoryRoot}.`);
1992
+ if (repository.head !== request.expectedHead) {
1993
+ throw new Error(`Git HEAD lease mismatch: expected ${request.expectedHead}, found ${repository.head}.`);
1994
+ }
1995
+ await assertGitPreflight(repository);
1996
+ await assertGitProviderCompatible(request.repositoryRoot);
1997
+ await assertGitIdentity(request.repositoryRoot);
1998
+ await verifyRevisionPaths(request.repositoryRoot, request.expectedHead, request.paths);
1999
+ if (request.noCommit || request.paths.length === 0) {
2000
+ return terminalResponse(providerId, request, request.noCommit ? "revisioning-skipped" : "no-repository-delta", null, repository.head);
2001
+ }
2002
+ const indexLease = await captureIndexLease(request.repositoryRoot);
2003
+ await assertIndexTree(request.repositoryRoot, request.expectedHead);
2004
+ const hookBaseline = await snapshotRepository(request.repositoryRoot);
2005
+ const tempRoot = await mkdtemp(join5(tmpdir(), "agentwheel-git-index-"));
2006
+ const indexPath = join5(tempRoot, "index");
2007
+ const env = { ...process.env, GIT_INDEX_FILE: indexPath };
2008
+ let commitSha;
2009
+ try {
2010
+ await runProcess("git", ["-C", request.repositoryRoot, "read-tree", request.expectedHead], { env });
2011
+ await runProcess("git", ["-C", request.repositoryRoot, "add", "--", ...request.paths.map((entry) => entry.path)], { env });
2012
+ const staged = splitNull2((await runProcess("git", ["-C", request.repositoryRoot, "diff", "--cached", "--name-only", "-z"], { env })).stdout);
2013
+ assertExactSet(staged, request.paths.map((entry) => entry.path), "temporary Git index");
2014
+ await verifyTemporaryIndexPaths(request, env);
2015
+ await runProcess("git", ["-C", request.repositoryRoot, "diff", "--cached", "--check"], { env });
2016
+ const tree = (await runProcess("git", ["-C", request.repositoryRoot, "write-tree"], { env })).stdout.toString("utf8").trim();
2017
+ const messagePath = join5(tempRoot, "COMMIT_EDITMSG");
2018
+ await writeFile2(messagePath, commitMessage(request, planDigest), { encoding: "utf8", mode: 384 });
2019
+ await runCommitHooks(request, env, messagePath);
2020
+ assertRepositorySnapshotEqual(hookBaseline, await snapshotRepository(request.repositoryRoot), "Git commit hooks");
2021
+ const stagedAfterHooks = splitNull2((await runProcess("git", ["-C", request.repositoryRoot, "diff", "--cached", "--name-only", "-z"], { env })).stdout);
2022
+ assertExactSet(stagedAfterHooks, request.paths.map((entry) => entry.path), "temporary Git index after hooks");
2023
+ await verifyTemporaryIndexPaths(request, env);
2024
+ const message = await readFile6(messagePath, "utf8");
2025
+ assertCommitMessageContract(message, request, planDigest);
2026
+ commitSha = (await runProcess("git", ["-C", request.repositoryRoot, "commit-tree", tree, "-p", request.expectedHead], {
2027
+ env,
2028
+ input: message
2029
+ })).stdout.toString("utf8").trim();
2030
+ } finally {
2031
+ await rm3(tempRoot, { recursive: true, force: true });
2032
+ }
2033
+ const branchRef = (await runProcess("git", ["-C", request.repositoryRoot, "symbolic-ref", "HEAD"])).stdout.toString("utf8").trim();
2034
+ await runProcess("git", [
2035
+ "-C",
2036
+ request.repositoryRoot,
2037
+ "update-ref",
2038
+ "-m",
2039
+ `agentwheel ${request.operationId}`,
2040
+ branchRef,
2041
+ commitSha,
2042
+ request.expectedHead
2043
+ ]);
2044
+ await replaceIndexCas(request.repositoryRoot, indexLease, commitSha);
2045
+ await verifyOperationCommit(request, commitSha);
2046
+ return terminalResponse(providerId, request, "verified", commitSha, commitSha);
2047
+ }
2048
+ async function runCommitHooks(request, env, messagePath) {
2049
+ for (const [name, args] of [
2050
+ ["pre-commit", []],
2051
+ ["prepare-commit-msg", [messagePath, "message"]],
2052
+ ["commit-msg", [messagePath]]
2053
+ ]) {
2054
+ const rawPath = (await runProcess("git", ["-C", request.repositoryRoot, "rev-parse", "--git-path", `hooks/${name}`])).stdout.toString("utf8").trim();
2055
+ const hookPath = isAbsolute4(rawPath) ? rawPath : resolve7(request.repositoryRoot, rawPath);
2056
+ let executable = false;
2057
+ try {
2058
+ executable = ((await stat(hookPath)).mode & 73) !== 0;
2059
+ } catch (error) {
2060
+ if (!isMissing3(error)) throw error;
2061
+ }
2062
+ if (!executable) continue;
2063
+ try {
2064
+ await runProcess(hookPath, [...args], {
2065
+ cwd: request.repositoryRoot,
2066
+ env: {
2067
+ ...env,
2068
+ AGENTWHEEL_OPERATION_ID: request.operationId,
2069
+ AGENTWHEEL_COMMAND_NAME: request.commandName
2070
+ },
2071
+ maxOutputBytes: 65536,
2072
+ includeFailureOutput: false
2073
+ });
2074
+ } catch {
2075
+ throw new Error(`Git ${name} hook rejected Agentwheel mutation ${request.operationId}.`);
2076
+ }
2077
+ }
2078
+ }
2079
+ function assertCommitMessageContract(message, request, planDigest) {
2080
+ if (!message.includes(request.reason.trim())) {
2081
+ throw new Error("Git commit hooks removed or changed the full Agentwheel mutation reason.");
2082
+ }
2083
+ for (const trailer of [
2084
+ `Agentwheel-Operation: ${request.operationId}`,
2085
+ `Agentwheel-Plan: ${planDigest}`
2086
+ ]) {
2087
+ if (!message.split(/\r?\n/u).includes(trailer)) {
2088
+ throw new Error(`Git commit hooks removed required trailer: ${trailer}.`);
2089
+ }
2090
+ }
2091
+ }
2092
+ function assertRepositorySnapshotEqual(before, after, label) {
2093
+ const entries = (snapshot) => [...snapshot.changed.entries()].sort(([a], [b]) => a.localeCompare(b));
2094
+ if (JSON.stringify(entries(before)) !== JSON.stringify(entries(after))) {
2095
+ throw new Error(`${label} changed repository working state outside the governed commit contract.`);
2096
+ }
2097
+ }
2098
+ async function findOperationCommit(repositoryRoot, operationId, planDigest) {
2099
+ const output = (await runProcess("git", ["-C", repositoryRoot, "log", "--format=%H%x1f%B%x1e", "--max-count=500", "HEAD"])).stdout.toString("utf8");
2100
+ for (const record of output.split("")) {
2101
+ const separator = record.indexOf("");
2102
+ if (separator < 0) continue;
2103
+ const sha = record.slice(0, separator).trim();
2104
+ const message = record.slice(separator + 1);
2105
+ if (message.split(/\r?\n/u).includes(`Agentwheel-Operation: ${operationId}`) && message.split(/\r?\n/u).includes(`Agentwheel-Plan: ${planDigest}`)) return sha;
2106
+ }
2107
+ return void 0;
2108
+ }
2109
+ async function verifyOperationCommit(request, commitSha) {
2110
+ const parent = (await runProcess("git", ["-C", request.repositoryRoot, "rev-parse", `${commitSha}^`])).stdout.toString("utf8").trim();
2111
+ if (parent !== request.expectedHead) throw new Error(`Recovered operation commit ${commitSha} has unexpected parent ${parent}.`);
2112
+ const committedPaths = splitNull2((await runProcess("git", [
2113
+ "-C",
2114
+ request.repositoryRoot,
2115
+ "diff-tree",
2116
+ "--no-commit-id",
2117
+ "--name-only",
2118
+ "-r",
2119
+ "-z",
2120
+ commitSha
2121
+ ])).stdout);
2122
+ assertExactSet(committedPaths, request.paths.map((entry) => entry.path), `operation commit ${commitSha}`);
2123
+ for (const entry of request.paths) {
2124
+ const after = await hashGitObject(request.repositoryRoot, `${commitSha}:${entry.path}`);
2125
+ if (after !== entry.afterSha256) {
2126
+ throw new Error(`Operation commit ${commitSha} content mismatch for ${entry.path}.`);
2127
+ }
2128
+ }
2129
+ }
2130
+ async function realignIndexAfterRecoveredCommit(repositoryRoot, expectedHead, commitSha) {
2131
+ if (await currentHead(repositoryRoot) !== commitSha) return;
2132
+ const staged = await runProcess("git", ["-C", repositoryRoot, "diff", "--cached", "--quiet"], { allowExitCodes: [0, 1] });
2133
+ if (staged.exitCode === 0) return;
2134
+ await assertIndexTree(repositoryRoot, expectedHead);
2135
+ await replaceIndexCas(repositoryRoot, await captureIndexLease(repositoryRoot), commitSha);
2136
+ }
2137
+ async function captureIndexLease(repositoryRoot) {
2138
+ const rawPath = (await runProcess("git", ["-C", repositoryRoot, "rev-parse", "--git-path", "index"])).stdout.toString("utf8").trim();
2139
+ const path = isAbsolute4(rawPath) ? rawPath : resolve7(repositoryRoot, rawPath);
2140
+ try {
2141
+ const [content, info] = await Promise.all([readFile6(path), stat(path)]);
2142
+ return { path, sha256: sha2562(content), mode: info.mode & 511 };
2143
+ } catch (error) {
2144
+ if (isMissing3(error)) return { path, sha256: null, mode: 384 };
2145
+ throw error;
2146
+ }
2147
+ }
2148
+ async function replaceIndexCas(repositoryRoot, lease, commitSha) {
2149
+ const tempRoot = await mkdtemp(join5(tmpdir(), "agentwheel-index-replacement-"));
2150
+ const replacementPath = join5(tempRoot, "index");
2151
+ const lockPath = `${lease.path}.lock`;
2152
+ let handle;
2153
+ try {
2154
+ await runProcess("git", ["-C", repositoryRoot, "read-tree", commitSha], {
2155
+ env: { ...process.env, GIT_INDEX_FILE: replacementPath }
2156
+ });
2157
+ const replacement = await readFile6(replacementPath);
2158
+ handle = await open2(lockPath, "wx", lease.mode);
2159
+ const current = await readOptionalFile(lease.path);
2160
+ const currentSha256 = current ? sha2562(current) : null;
2161
+ if (currentSha256 !== lease.sha256) {
2162
+ throw new Error("Git index changed concurrently; the operation commit was preserved but index alignment was refused.");
2163
+ }
2164
+ await handle.writeFile(replacement);
2165
+ await handle.sync();
2166
+ await handle.close();
2167
+ handle = void 0;
2168
+ await chmod2(lockPath, lease.mode);
2169
+ await rename3(lockPath, lease.path);
2170
+ } finally {
2171
+ await handle?.close().catch(() => void 0);
2172
+ await rm3(lockPath, { force: true }).catch(() => void 0);
2173
+ await rm3(tempRoot, { recursive: true, force: true });
2174
+ }
2175
+ }
2176
+ async function assertIndexTree(repositoryRoot, expectedHead) {
2177
+ const indexTree = (await runProcess("git", ["-C", repositoryRoot, "write-tree"])).stdout.toString("utf8").trim();
2178
+ const expectedTree = (await runProcess("git", ["-C", repositoryRoot, "rev-parse", `${expectedHead}^{tree}`])).stdout.toString("utf8").trim();
2179
+ if (indexTree !== expectedTree) {
2180
+ throw new Error("Revisioning requires the real Git index to match the expected HEAD tree exactly.");
2181
+ }
2182
+ }
2183
+ async function verifyTemporaryIndexPaths(request, env) {
2184
+ for (const entry of request.paths) {
2185
+ const result = await runProcess("git", ["-C", request.repositoryRoot, "show", `:${entry.path}`], {
2186
+ env,
2187
+ allowExitCodes: [0, 128]
2188
+ });
2189
+ const actual = result.exitCode === 0 ? sha2562(result.stdout) : null;
2190
+ if (actual !== entry.afterSha256) throw new Error(`Temporary Git index content mismatch for ${entry.path}.`);
2191
+ }
2192
+ }
2193
+ async function hashGitObject(repositoryRoot, spec) {
2194
+ const result = await runProcess("git", ["-C", repositoryRoot, "show", spec], { allowExitCodes: [0, 128] });
2195
+ return result.exitCode === 0 ? sha2562(result.stdout) : null;
2196
+ }
2197
+ async function readOptionalFile(path) {
2198
+ try {
2199
+ return await readFile6(path);
2200
+ } catch (error) {
2201
+ if (isMissing3(error)) return null;
2202
+ throw error;
2203
+ }
2204
+ }
2205
+ function sha2562(value) {
2206
+ return createHash4("sha256").update(value).digest("hex");
2207
+ }
2208
+ function isMissing3(error) {
2209
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2210
+ }
2211
+ async function assertGitProviderCompatible(repositoryRoot) {
2212
+ const trackedSyncwheelManifest = await runProcess("git", [
2213
+ "-C",
2214
+ repositoryRoot,
2215
+ "ls-files",
2216
+ "--error-unmatch",
2217
+ ".syncwheel/manifest.json"
2218
+ ], { allowExitCodes: [0, 1] });
2219
+ if (trackedSyncwheelManifest.exitCode === 0) {
2220
+ throw new Error("The builtin Git revision provider refuses a tracked .syncwheel/manifest.json; configure an external coordination provider.");
2221
+ }
2222
+ }
2223
+ async function assertGitIdentity(repositoryRoot) {
2224
+ for (const identity of ["GIT_AUTHOR_IDENT", "GIT_COMMITTER_IDENT"]) {
2225
+ const result = await runProcess("git", ["-C", repositoryRoot, "var", identity], { allowExitCodes: [0, 128] });
2226
+ if (result.exitCode !== 0 || !result.stdout.toString("utf8").trim()) {
2227
+ throw new Error(`Git revisioning requires a configured ${identity === "GIT_AUTHOR_IDENT" ? "author" : "committer"} identity.`);
2228
+ }
2229
+ }
2230
+ }
2231
+ function terminalResponse(providerId, request, status, productCommitSha, resultingHead) {
2232
+ return revisionProviderResponseSchema.parse({
2233
+ ...baseResponse(providerId, request, status),
2234
+ expectedHead: request.expectedHead,
2235
+ resultingHead,
2236
+ productCommitSha,
2237
+ draftStackId: null,
2238
+ draftBranch: null,
2239
+ draftTipSha: null,
2240
+ controlCommitSha: null,
2241
+ manifestDigest: request.expectedManifestDigest ?? null,
2242
+ unmappedIntegrationCommits: [],
2243
+ published: false
2244
+ });
2245
+ }
2246
+ function baseResponse(providerId, request, status) {
2247
+ return {
2248
+ protocolVersion: request.protocolVersion,
2249
+ providerId,
2250
+ action: request.action,
2251
+ operationId: request.operationId,
2252
+ ok: true,
2253
+ status
2254
+ };
2255
+ }
2256
+ function providerPlanDigest(request) {
2257
+ return revisionRequestDigest(revisionProviderRequestSchema.parse({ ...request, action: "finalize" }));
2258
+ }
2259
+ function asTerminalRequest(request, action) {
2260
+ return revisionProviderRequestSchema.parse({ ...request, action });
2261
+ }
2262
+ function commitMessage(request, planDigest) {
2263
+ const subjectCommand = request.commandName.replace(/\s+/gu, " ").trim().slice(0, 120);
2264
+ return [
2265
+ `chore(agentwheel): ${subjectCommand}`,
2266
+ "",
2267
+ request.reason.trim(),
2268
+ "",
2269
+ `Agentwheel-Operation: ${request.operationId}`,
2270
+ `Agentwheel-Plan: ${planDigest}`,
2271
+ ""
2272
+ ].join("\n");
2273
+ }
2274
+ function assertExactSet(actualInput, expectedInput, label) {
2275
+ const actual = [...new Set(actualInput)].sort((a, b) => a.localeCompare(b));
2276
+ const expected = [...new Set(expectedInput)].sort((a, b) => a.localeCompare(b));
2277
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
2278
+ throw new Error(`Exact path mismatch in ${label}: expected [${expected.join(", ")}], found [${actual.join(", ")}].`);
2279
+ }
2280
+ }
2281
+ function splitNull2(value) {
2282
+ return value.toString("utf8").split("\0").filter(Boolean);
2283
+ }
2284
+
2285
+ // src/mutation/coordinator.ts
2286
+ var GovernedMutation = class _GovernedMutation {
2287
+ constructor(policy, receipt, baseline, lock) {
2288
+ this.policy = policy;
2289
+ this.receipt = receipt;
2290
+ this.baseline = baseline;
2291
+ this.lock = lock;
2292
+ this.operationId = receipt.operationId;
2293
+ }
2294
+ policy;
2295
+ receipt;
2296
+ baseline;
2297
+ lock;
2298
+ operationId;
2299
+ closed = false;
2300
+ static async begin(options) {
2301
+ const policy = await mutationPolicyForWorkspace(options.workspaceRoot, options.globalRoot);
2302
+ if (!policy) {
2303
+ if (options.noCommit) throw new Error("--no-commit requires an effective commit-after-verify mutation policy.");
2304
+ return void 0;
2305
+ }
2306
+ const reason = normalizeReason(options.reason, options.commandName, policy);
2307
+ if (policy.revisioning.mode === "off") {
2308
+ if (options.noCommit) throw new Error("--no-commit is only valid when revisioning.mode is commit-after-verify.");
2309
+ if (policy.journal === "off") return void 0;
2310
+ } else if (options.noCommit && !policy.revisioning.allowNoCommitOverride) {
2311
+ throw new Error("This workspace does not allow the audited --no-commit override.");
2312
+ }
2313
+ const operationId = mutationOperationIdSchema.parse(options.operationId ?? randomUUID2());
2314
+ const repository = await discoverGitRepository(options.workspaceRoot);
2315
+ if (policy.revisioning.mode === "commit-after-verify" && !repository) {
2316
+ throw new Error(`commit-after-verify requires a Git repository containing ${options.workspaceRoot}.`);
2317
+ }
2318
+ if (policy.revisioning.mode === "commit-after-verify" && options.requiresDeclarativeRepositoryDelta) {
2319
+ throw new Error(
2320
+ `${options.commandName} is a runtime-only ownership transition with no durable declarative repository representation; commit-after-verify refuses it before writes.`
2321
+ );
2322
+ }
2323
+ if (policy.revisioning.mode === "commit-after-verify" && repository) {
2324
+ for (const additionalRoot of options.additionalWorkspaceRoots ?? []) {
2325
+ const additionalRepository = await discoverGitRepository(additionalRoot);
2326
+ if (!additionalRepository || additionalRepository.root !== repository.root) {
2327
+ throw new Error(
2328
+ "commit-after-verify refuses a mutation spanning multiple repositories or non-repository state; split it into governed single-repository operations."
2329
+ );
2330
+ }
2331
+ }
2332
+ }
2333
+ const lock = await acquireMutationLock(repository?.root ?? options.workspaceRoot, operationId);
2334
+ let baseline;
2335
+ let receipt;
2336
+ try {
2337
+ baseline = repository ? await snapshotRepository(repository.root) : void 0;
2338
+ receipt = await createMutationReceipt({
2339
+ operationId,
2340
+ commandName: options.commandName,
2341
+ reason,
2342
+ noCommit: options.noCommit === true,
2343
+ workspaceRoot: options.workspaceRoot,
2344
+ repositoryRoot: repository?.root ?? null,
2345
+ expectedHead: repository?.head ?? null,
2346
+ expectedManifestDigest: null,
2347
+ revisionMode: policy.revisioning.mode,
2348
+ provider: policy.revisioning.mode === "commit-after-verify" ? policy.revisioning.provider : null,
2349
+ preexistingPaths: snapshotEntries(baseline),
2350
+ paths: [],
2351
+ runtimeJournals: [],
2352
+ status: "prepared"
2353
+ });
2354
+ if (policy.revisioning.mode === "commit-after-verify" && options.requireCleanWorkingTree && baseline && baseline.changed.size > 0) {
2355
+ throw new Error(
2356
+ `This governed command computes declarative paths during planning and requires a clean working tree before runtime mutation; found: ${[...baseline.changed.keys()].sort().join(", ")}.`
2357
+ );
2358
+ }
2359
+ beginMutationPathDeclarations(
2360
+ repository?.root ?? options.workspaceRoot,
2361
+ operationId,
2362
+ baseline?.changed.keys()
2363
+ );
2364
+ for (const path of options.anticipatedPaths ?? []) declareMutationPath(path);
2365
+ if (policy.revisioning.mode === "commit-after-verify" && repository) {
2366
+ await invokeRevisionProvider(policy.revisioning.provider, providerRequest(receipt, "check", []), {
2367
+ workspaceRoot: options.workspaceRoot
2368
+ });
2369
+ }
2370
+ const mutation = new _GovernedMutation(policy, receipt, baseline, lock);
2371
+ activeMutation = mutation;
2372
+ return mutation;
2373
+ } catch (error) {
2374
+ if (receipt) {
2375
+ await updateMutationReceipt(receipt, { status: "precheck-failed", error: sanitizeError(error) }).catch(() => void 0);
2376
+ }
2377
+ endMutationPathDeclarations();
2378
+ await lock.release();
2379
+ throw error;
2380
+ }
2381
+ }
2382
+ async complete(action = "finalize") {
2383
+ if (this.closed) return this.receipt;
2384
+ let paths;
2385
+ try {
2386
+ const pendingRuntimeJournals = this.receipt.runtimeJournals.filter((entry) => entry.status !== "resolved");
2387
+ if (pendingRuntimeJournals.length > 0) {
2388
+ throw new Error(
2389
+ `Runtime apply verification is incomplete: ${pendingRuntimeJournals.map((entry) => entry.path).join(", ")}.`
2390
+ );
2391
+ }
2392
+ this.receipt = await updateMutationReceipt(this.receipt, { status: "handler-succeeded" });
2393
+ paths = await this.collectPaths();
2394
+ this.receipt = await updateMutationReceipt(this.receipt, { paths, status: "mutation-applied" });
2395
+ } catch (error) {
2396
+ this.receipt = await updateMutationReceipt(this.receipt, {
2397
+ status: "postcheck-failed",
2398
+ error: sanitizeError(error)
2399
+ });
2400
+ await this.close();
2401
+ throw new Error(`Agentwheel mutation ${this.operationId} failed its repository postcheck: ${sanitizeError(error)}`);
2402
+ }
2403
+ try {
2404
+ if (this.policy.revisioning.mode === "off") {
2405
+ this.receipt = await updateMutationReceipt(this.receipt, {
2406
+ status: paths.length === 0 ? "no-repository-delta" : "succeeded"
2407
+ });
2408
+ return this.receipt;
2409
+ }
2410
+ const provider = this.policy.revisioning.provider;
2411
+ await invokeRevisionProvider(provider, providerRequest(this.receipt, "preflight", paths), {
2412
+ workspaceRoot: this.receipt.workspaceRoot
2413
+ });
2414
+ const response = await invokeRevisionProvider(provider, providerRequest(this.receipt, action, paths), {
2415
+ workspaceRoot: this.receipt.workspaceRoot
2416
+ });
2417
+ this.receipt = await updateMutationReceipt(this.receipt, {
2418
+ status: receiptStatusForProvider(response.status),
2419
+ providerResponse: response
2420
+ });
2421
+ return this.receipt;
2422
+ } catch (error) {
2423
+ this.receipt = await updateMutationReceipt(this.receipt, {
2424
+ status: "commit-pending",
2425
+ ...revisionProviderRejection(error) ? { providerResponse: revisionProviderRejection(error) } : {},
2426
+ error: sanitizeError(error)
2427
+ });
2428
+ throw new Error(`Agentwheel mutation ${this.operationId} is commit-pending: ${sanitizeError(error)}`);
2429
+ } finally {
2430
+ await this.close();
2431
+ }
2432
+ }
2433
+ async fail(error) {
2434
+ if (this.closed) return;
2435
+ try {
2436
+ let paths = this.receipt.paths;
2437
+ try {
2438
+ paths = await this.collectPaths();
2439
+ } catch {
2440
+ }
2441
+ this.receipt = await updateMutationReceipt(this.receipt, {
2442
+ paths,
2443
+ status: "partial",
2444
+ error: sanitizeError(error)
2445
+ });
2446
+ if (this.policy.revisioning.mode === "commit-after-verify") {
2447
+ await invokeRevisionProvider(
2448
+ this.policy.revisioning.provider,
2449
+ providerRequest(this.receipt, "release", paths),
2450
+ { workspaceRoot: this.receipt.workspaceRoot }
2451
+ ).catch(() => void 0);
2452
+ }
2453
+ } finally {
2454
+ await this.close();
2455
+ }
2456
+ }
2457
+ metadata() {
2458
+ return {
2459
+ operationId: this.receipt.operationId,
2460
+ reason: this.receipt.reason,
2461
+ noCommit: this.receipt.noCommit,
2462
+ revisionMode: this.receipt.revisionMode
2463
+ };
2464
+ }
2465
+ async transitionRuntimeJournal(link2, status) {
2466
+ const runtimeJournals = upsertRuntimeJournal(this.receipt.runtimeJournals, link2, status);
2467
+ this.receipt = await updateMutationReceipt(this.receipt, { runtimeJournals });
2468
+ }
2469
+ async transitionExistingRuntimeJournal(path, status) {
2470
+ const existing = this.receipt.runtimeJournals.find((entry) => entry.path === path);
2471
+ if (!existing) throw new Error(`Mutation '${this.operationId}' has no runtime journal link for ${path}.`);
2472
+ const runtimeJournals = upsertRuntimeJournal(this.receipt.runtimeJournals, existing, status);
2473
+ this.receipt = await updateMutationReceipt(this.receipt, { runtimeJournals });
2474
+ }
2475
+ static activateExisting(receipt, baseline, lock) {
2476
+ if (receipt.revisionMode === "commit-after-verify" && !receipt.provider) {
2477
+ throw new Error(`Mutation '${receipt.operationId}' is missing its revision provider recovery policy.`);
2478
+ }
2479
+ const policy = mutationPolicySchema.parse(receipt.revisionMode === "commit-after-verify" ? {
2480
+ reason: "required",
2481
+ journal: "required",
2482
+ revisioning: {
2483
+ mode: "commit-after-verify",
2484
+ allowNoCommitOverride: true,
2485
+ reasonInCommit: "full",
2486
+ provider: receipt.provider
2487
+ }
2488
+ } : {
2489
+ reason: "required",
2490
+ journal: "required",
2491
+ revisioning: { mode: "off" }
2492
+ });
2493
+ const mutation = new _GovernedMutation(policy, receipt, baseline, lock);
2494
+ activeMutation = mutation;
2495
+ return mutation;
2496
+ }
2497
+ async collectPaths() {
2498
+ if (!this.receipt.repositoryRoot || !this.receipt.expectedHead || !this.baseline) return [];
2499
+ const declarations = [.../* @__PURE__ */ new Set([
2500
+ ...declaredMutationPaths(),
2501
+ ...readMutationPathDeclarations(this.receipt.operationId)
2502
+ ])].sort((a, b) => a.localeCompare(b));
2503
+ return collectIntroducedPaths(this.receipt.repositoryRoot, this.receipt.expectedHead, this.baseline, declarations);
2504
+ }
2505
+ async close() {
2506
+ if (this.closed) return;
2507
+ this.closed = true;
2508
+ if (activeMutation === this) activeMutation = void 0;
2509
+ endMutationPathDeclarations();
2510
+ await this.lock.release();
2511
+ }
2512
+ };
2513
+ var activeMutation;
2514
+ function activeMutationMetadata() {
2515
+ return activeMutation?.metadata();
2516
+ }
2517
+ async function reserveActiveRuntimeJournal(link2) {
2518
+ await activeMutation?.transitionRuntimeJournal(link2, "reserved");
2519
+ }
2520
+ async function activateActiveRuntimeJournal(link2) {
2521
+ await activeMutation?.transitionRuntimeJournal(link2, "pending");
2522
+ }
2523
+ async function beginResolveMutationRuntimeJournal(operationId, path) {
2524
+ await transitionMutationRuntimeJournal(operationId, path, "resolving");
2525
+ }
2526
+ async function resolveMutationRuntimeJournal(operationId, path) {
2527
+ await transitionMutationRuntimeJournal(operationId, path, "resolved");
2528
+ }
2529
+ async function transitionMutationRuntimeJournal(operationId, path, status) {
2530
+ if (activeMutation?.operationId === operationId) {
2531
+ await activeMutation.transitionExistingRuntimeJournal(path, status);
2532
+ return;
2533
+ }
2534
+ const receipt = await readMutationReceipt(operationId);
2535
+ const existing = receipt.runtimeJournals.find((entry) => entry.path === path);
2536
+ if (!existing) throw new Error(`Mutation '${operationId}' has no runtime journal link for ${path}.`);
2537
+ await updateMutationReceipt(receipt, {
2538
+ runtimeJournals: upsertRuntimeJournal(receipt.runtimeJournals, existing, status)
2539
+ });
2540
+ }
2541
+ async function resumeMutation(operationId, action) {
2542
+ let receipt = await readMutationReceipt(operationId);
2543
+ if (["succeeded", "revisioning-skipped", "no-repository-delta"].includes(receipt.status)) return receipt;
2544
+ if (["prepared", "precheck-failed", "partial", "postcheck-failed", "failed"].includes(receipt.status)) {
2545
+ throw new Error(
2546
+ `Mutation '${receipt.operationId}' is ${receipt.status}; finalize/recover is refused because handler success and runtime journal resolution were not verified.`
2547
+ );
2548
+ }
2549
+ const pendingRuntimeJournals = receipt.runtimeJournals.filter((entry) => entry.status !== "resolved");
2550
+ if (pendingRuntimeJournals.length > 0) {
2551
+ throw new Error(
2552
+ `Mutation '${receipt.operationId}' still has unresolved runtime apply journals: ${pendingRuntimeJournals.map((entry) => entry.path).join(", ")}.`
2553
+ );
2554
+ }
2555
+ if (!receipt.provider || !receipt.repositoryRoot || !receipt.expectedHead || receipt.revisionMode !== "commit-after-verify") {
2556
+ throw new Error(`Mutation '${receipt.operationId}' has no revision provider recovery contract.`);
2557
+ }
2558
+ const provider = receipt.provider;
2559
+ const repositoryRoot = receipt.repositoryRoot;
2560
+ const expectedHead = receipt.expectedHead;
2561
+ const lock = await acquireMutationLock(repositoryRoot, receipt.operationId);
2562
+ try {
2563
+ let paths = receipt.paths;
2564
+ const head = await currentHead(repositoryRoot);
2565
+ if (paths.length === 0 && head === expectedHead) {
2566
+ paths = await collectIntroducedPaths(
2567
+ repositoryRoot,
2568
+ expectedHead,
2569
+ snapshotFromReceipt(receipt),
2570
+ readMutationPathDeclarations(receipt.operationId)
2571
+ );
2572
+ receipt = await updateMutationReceipt(receipt, { paths, status: "mutation-applied" });
2573
+ }
2574
+ await invokeRevisionProvider(provider, providerRequest(receipt, "preflight", paths), {
2575
+ workspaceRoot: receipt.workspaceRoot
2576
+ });
2577
+ const response = await invokeRevisionProvider(provider, providerRequest(receipt, action, paths), {
2578
+ workspaceRoot: receipt.workspaceRoot
2579
+ });
2580
+ return updateMutationReceipt(receipt, {
2581
+ status: receiptStatusForProvider(response.status),
2582
+ providerResponse: response,
2583
+ error: void 0
2584
+ });
2585
+ } catch (error) {
2586
+ receipt = await updateMutationReceipt(receipt, {
2587
+ status: "commit-pending",
2588
+ ...revisionProviderRejection(error) ? { providerResponse: revisionProviderRejection(error) } : {},
2589
+ error: sanitizeError(error)
2590
+ });
2591
+ throw error;
2592
+ } finally {
2593
+ await lock.release();
2594
+ }
2595
+ }
2596
+ async function recoverMutationRuntime(operationId, options = {}) {
2597
+ let receipt = await readMutationReceipt(operationId);
2598
+ const lockRoot = receipt.repositoryRoot ?? receipt.workspaceRoot;
2599
+ const lock = await acquireMutationLock(lockRoot, receipt.operationId);
2600
+ try {
2601
+ await options.afterLockAcquired?.();
2602
+ receipt = await readMutationReceipt(operationId);
2603
+ assertRuntimeRecoveryReceipt(receipt);
2604
+ } catch (error) {
2605
+ await lock.release();
2606
+ throw error;
2607
+ }
2608
+ const pending = receipt.runtimeJournals.filter((entry) => entry.status !== "resolved");
2609
+ const baseline = receipt.repositoryRoot ? snapshotFromReceipt(receipt) : void 0;
2610
+ const declarationRoot = receipt.repositoryRoot ?? receipt.workspaceRoot;
2611
+ let mutation;
2612
+ resumeMutationPathDeclarations(declarationRoot, receipt.operationId, baseline?.changed.keys());
2613
+ try {
2614
+ if (receipt.repositoryRoot && receipt.expectedHead && baseline) {
2615
+ const declarations = readMutationPathDeclarations(receipt.operationId);
2616
+ const repository = await discoverGitRepository(receipt.repositoryRoot);
2617
+ if (!repository || repository.root !== receipt.repositoryRoot || repository.head !== receipt.expectedHead) {
2618
+ throw new Error(
2619
+ `Mutation '${receipt.operationId}' repository HEAD lease changed before runtime recovery.`
2620
+ );
2621
+ }
2622
+ await assertGitPreflight(repository);
2623
+ await collectIntroducedPaths(receipt.repositoryRoot, receipt.expectedHead, baseline, declarations);
2624
+ }
2625
+ mutation = GovernedMutation.activateExisting(receipt, baseline, lock);
2626
+ const [{ recoverPendingApply }, { readLinkedLocalApplyJournal: readLinkedLocalApplyJournal2, removeApplyJournal: removeApplyJournal2, localPathExists: localPathExists2 }] = await Promise.all([
2627
+ import("./apply-R5VHCIUN.js"),
2628
+ import("./transaction-ITXC7FFV.js")
2629
+ ]);
2630
+ let missingReservation = false;
2631
+ for (const entry of pending) {
2632
+ if (entry.transport !== "local") {
2633
+ throw new Error(`Runtime recovery for ${entry.transportDescription} is unsupported until a durable remote recovery protocol exists.`);
2634
+ }
2635
+ const exists2 = await localPathExists2(entry.path);
2636
+ if (!exists2) {
2637
+ if (entry.status === "reserved") {
2638
+ await resolveMutationRuntimeJournal(receipt.operationId, entry.path);
2639
+ missingReservation = true;
2640
+ continue;
2641
+ }
2642
+ if (entry.status === "resolving") {
2643
+ await resolveMutationRuntimeJournal(receipt.operationId, entry.path);
2644
+ continue;
2645
+ }
2646
+ throw new Error(`Runtime apply journal ${entry.path} disappeared while its receipt was ${entry.status}.`);
2647
+ }
2648
+ const journal = await readLinkedLocalApplyJournal2(
2649
+ entry.path,
2650
+ receipt.operationId,
2651
+ entry.journalDigest,
2652
+ entry.transportDescription
2653
+ );
2654
+ if (journal.mutation?.reason !== receipt.reason || journal.mutation.noCommit !== receipt.noCommit) {
2655
+ throw new Error(`Runtime apply journal ${entry.path} mutation metadata does not match its durable receipt.`);
2656
+ }
2657
+ if (entry.status === "resolving") {
2658
+ await removeApplyJournal2(journal.targetRoot, journal.adapter, void 0, {
2659
+ installationType: journal.installationType,
2660
+ stateKey: journal.stateKey
2661
+ });
2662
+ continue;
2663
+ }
2664
+ const recovered = await recoverPendingApply(journal.targetRoot, journal.adapter, void 0, {
2665
+ installationType: journal.installationType,
2666
+ stateKey: journal.stateKey
2667
+ });
2668
+ if (!recovered) throw new Error(`Runtime apply journal ${entry.path} could not be recovered deterministically.`);
2669
+ }
2670
+ if (missingReservation) {
2671
+ throw new Error("A reserved runtime journal was never created, so the interrupted handler must be rerun as a new governed operation.");
2672
+ }
2673
+ return await mutation.complete("recover");
2674
+ } catch (error) {
2675
+ if (mutation) {
2676
+ await mutation.fail(error).catch(() => void 0);
2677
+ } else {
2678
+ receipt = await updateMutationReceipt(receipt, { status: "partial", error: sanitizeError(error) });
2679
+ endMutationPathDeclarations();
2680
+ await lock.release();
2681
+ }
2682
+ throw new Error(`Agentwheel mutation ${receipt.operationId} runtime recovery failed: ${sanitizeError(error)}`);
2683
+ }
2684
+ }
2685
+ function assertRuntimeRecoveryReceipt(receipt) {
2686
+ const pending = receipt.runtimeJournals.filter((entry) => entry.status !== "resolved");
2687
+ if (pending.length === 0) {
2688
+ throw new Error(`Mutation '${receipt.operationId}' has no pending linked runtime apply journals.`);
2689
+ }
2690
+ if (!["prepared", "partial", "postcheck-failed", "handler-succeeded", "mutation-applied", "commit-pending"].includes(receipt.status)) {
2691
+ throw new Error(`Mutation '${receipt.operationId}' is ${receipt.status}; runtime recovery is not allowed.`);
2692
+ }
2693
+ if (receipt.revisionMode === "commit-after-verify" && (!receipt.provider || !receipt.repositoryRoot || !receipt.expectedHead)) {
2694
+ throw new Error(`Mutation '${receipt.operationId}' has no revision provider recovery contract.`);
2695
+ }
2696
+ }
2697
+ async function checkMutationProvider(workspaceRoot, globalRoot) {
2698
+ const policy = await mutationPolicyForWorkspace(workspaceRoot, globalRoot);
2699
+ if (!policy || policy.revisioning.mode === "off") return void 0;
2700
+ const repository = await discoverGitRepository(workspaceRoot);
2701
+ if (!repository) throw new Error(`No Git repository contains ${workspaceRoot}.`);
2702
+ const operationId = randomUUID2();
2703
+ const request = revisionProviderRequestSchema.parse({
2704
+ protocolVersion: policy.revisioning.provider.protocolVersion,
2705
+ action: "check",
2706
+ operationId,
2707
+ repositoryRoot: repository.root,
2708
+ expectedHead: repository.head,
2709
+ commandName: "mutation check",
2710
+ reason: "Check the configured Agentwheel revision provider",
2711
+ noCommit: false,
2712
+ paths: []
2713
+ });
2714
+ return invokeRevisionProvider(policy.revisioning.provider, request, { workspaceRoot });
2715
+ }
2716
+ async function mutationPolicyForWorkspace(workspaceRoot, globalRoot = homedir4()) {
2717
+ const scopedConfig = await readWorkspaceConfig(workspaceRoot);
2718
+ const scoped = scopedConfig.schemaVersion === 4 ? scopedConfig.mutationPolicy : void 0;
2719
+ if (resolve8(workspaceRoot) === resolve8(globalRoot)) return scoped;
2720
+ const globalConfig = await readWorkspaceConfig(globalRoot);
2721
+ const global = globalConfig.schemaVersion === 4 ? globalConfig.mutationPolicy : void 0;
2722
+ return mergeMutationPolicies(global, scoped);
2723
+ }
2724
+ function mergeMutationPolicies(global, scoped) {
2725
+ if (!global) return scoped;
2726
+ if (!scoped) return global;
2727
+ const globalCommit = global.revisioning.mode === "commit-after-verify" ? global.revisioning : void 0;
2728
+ const scopedCommit = scoped.revisioning.mode === "commit-after-verify" ? scoped.revisioning : void 0;
2729
+ const chosen = scopedCommit ?? globalCommit;
2730
+ const revisioning = chosen ? {
2731
+ ...chosen,
2732
+ allowNoCommitOverride: globalCommit && scopedCommit ? globalCommit.allowNoCommitOverride && scopedCommit.allowNoCommitOverride : chosen.allowNoCommitOverride
2733
+ } : { mode: "off" };
2734
+ return mutationPolicySchema.parse({
2735
+ reason: global.reason === "required" || scoped.reason === "required" ? "required" : "optional",
2736
+ journal: global.journal === "required" || scoped.journal === "required" || chosen ? "required" : "off",
2737
+ revisioning
2738
+ });
2739
+ }
2740
+ function providerRequest(receipt, action, paths) {
2741
+ if (!receipt.repositoryRoot || !receipt.expectedHead || !receipt.provider) {
2742
+ throw new Error(`Mutation '${receipt.operationId}' is missing revision provider request state.`);
2743
+ }
2744
+ return revisionProviderRequestSchema.parse({
2745
+ protocolVersion: receipt.provider.protocolVersion,
2746
+ action,
2747
+ operationId: receipt.operationId,
2748
+ repositoryRoot: receipt.repositoryRoot,
2749
+ expectedHead: receipt.expectedHead,
2750
+ ...receipt.expectedManifestDigest ? { expectedManifestDigest: receipt.expectedManifestDigest } : {},
2751
+ commandName: receipt.commandName,
2752
+ reason: receipt.reason,
2753
+ noCommit: receipt.noCommit,
2754
+ paths
2755
+ });
2756
+ }
2757
+ function normalizeReason(input, commandName, policy) {
2758
+ const normalized = input?.replace(/\r\n?/gu, "\n").trim();
2759
+ if (!normalized && policy.reason === "required") throw new Error(`Mutation reason required: pass --reason <why> for '${commandName}'.`);
2760
+ return mutationReasonSchema.parse(normalized || `Run Agentwheel ${commandName}`);
2761
+ }
2762
+ function snapshotEntries(snapshot) {
2763
+ if (!snapshot) return [];
2764
+ return [...snapshot.changed.entries()].map(([path, sha2563]) => ({ path, sha256: sha2563 })).sort((a, b) => a.path.localeCompare(b.path));
2765
+ }
2766
+ function snapshotFromReceipt(receipt) {
2767
+ return { changed: new Map(receipt.preexistingPaths.map((entry) => [entry.path, entry.sha256])) };
2768
+ }
2769
+ function sanitizeError(error) {
2770
+ const value = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, " ").trim();
2771
+ return (value || "Unknown mutation failure").slice(0, 4096);
2772
+ }
2773
+ function receiptStatusForProvider(status) {
2774
+ if (status === "revisioning-skipped") return "revisioning-skipped";
2775
+ if (status === "no-repository-delta") return "no-repository-delta";
2776
+ return "succeeded";
2777
+ }
2778
+ function upsertRuntimeJournal(entries, link2, status) {
2779
+ const existing = entries.find((entry) => entry.path === link2.path);
2780
+ if (existing && (existing.transport !== link2.transport || existing.transportDescription !== link2.transportDescription || existing.journalDigest !== link2.journalDigest)) {
2781
+ throw new Error(`Runtime journal link collision for ${link2.path}.`);
2782
+ }
2783
+ return [
2784
+ ...entries.filter((entry) => entry.path !== link2.path),
2785
+ { ...link2, status }
2786
+ ].sort((a, b) => a.path.localeCompare(b.path));
2787
+ }
2788
+
2789
+ // src/install/transaction.ts
2790
+ function applyLockPath(targetRoot, adapter, scope = {}) {
2791
+ const installationType = scope.installationType ?? "local";
2792
+ return join6(metadataDir(targetRoot), `${stateKeyFor(adapter, { installationType })}.runtime-apply-lock`);
2793
+ }
2794
+ function applyJournalPath(targetRoot, adapter, scope = {}) {
2795
+ return join6(metadataDir(targetRoot), `${stateKeyFor(adapter, scope)}.apply-journal.json`);
2796
+ }
2797
+ function applyBackupDir(targetRoot, adapter, scope = {}) {
2798
+ return join6(metadataDir(targetRoot), `${stateKeyFor(adapter, scope)}.apply-backups`);
2799
+ }
2800
+ async function acquireApplyLock(targetRoot, adapter, transport = localTransport, options = {}, scope = {}) {
2801
+ const lockPath = applyLockPath(targetRoot, adapter, scope);
2802
+ const ownerPath = join6(lockPath, "owner.json");
2803
+ const metadata = {
2804
+ pid: process.pid,
2805
+ adapter,
2806
+ installationType: scope.installationType,
2807
+ stateKey: scope.stateKey,
2808
+ targetRoot,
2809
+ transport: transport.description,
2810
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2811
+ };
2812
+ try {
2813
+ await transport.mkdirExclusive(lockPath);
2814
+ } catch (error) {
2815
+ if (!isAlreadyExists2(error)) throw error;
2816
+ await handleExistingLock(lockPath, ownerPath, transport, options);
2817
+ await transport.mkdirExclusive(lockPath);
2818
+ }
2819
+ await transport.writeJsonAtomic(ownerPath, metadata);
2820
+ return {
2821
+ path: lockPath,
2822
+ release: () => transport.rm(lockPath)
2823
+ };
2824
+ }
2825
+ async function writeApplyJournal(journal, transport = localTransport) {
2826
+ const path = applyJournalPath(journal.targetRoot, journal.adapter, {
2827
+ installationType: journal.installationType,
2828
+ stateKey: journal.stateKey
2829
+ });
2830
+ let link2;
2831
+ if (journal.mutation) {
2832
+ const digest = applyJournalLinkDigest(journal, transport);
2833
+ if (journal.mutation.journalDigest && journal.mutation.journalDigest !== digest) {
2834
+ throw new Error(`Runtime apply journal identity changed for ${path}.`);
2835
+ }
2836
+ if (journal.mutation.transport && (journal.mutation.transport.kind !== transport.kind || journal.mutation.transport.description !== transport.description)) {
2837
+ throw new Error(`Runtime apply journal transport changed for ${path}.`);
2838
+ }
2839
+ journal.mutation.journalDigest = digest;
2840
+ journal.mutation.transport = { kind: transport.kind, description: transport.description };
2841
+ link2 = {
2842
+ path,
2843
+ transport: transport.kind,
2844
+ transportDescription: transport.description,
2845
+ journalDigest: digest
2846
+ };
2847
+ await reserveActiveRuntimeJournal(link2);
2848
+ }
2849
+ await transport.writeJsonAtomic(path, {
2850
+ ...journal,
2851
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2852
+ });
2853
+ if (link2) await activateActiveRuntimeJournal(link2);
2854
+ }
2855
+ async function readApplyJournal(targetRoot, adapter, transport = localTransport, scope = {}) {
2856
+ const path = applyJournalPath(targetRoot, adapter, scope);
2857
+ if (!await transport.pathExists(path)) return void 0;
2858
+ return parseApplyJournal(JSON.parse(await transport.readFile(path)));
2859
+ }
2860
+ async function listApplyJournals(targetRoot, adapter, transport = localTransport, scope = {}) {
2861
+ const installationType = scope.installationType ?? "local";
2862
+ const suffix = ".apply-journal.json";
2863
+ const found = [];
2864
+ for (const fileName of await transport.listDir(metadataDir(targetRoot))) {
2865
+ if (!fileName.endsWith(suffix)) continue;
2866
+ const path = join6(metadataDir(targetRoot), fileName);
2867
+ const journal = parseApplyJournal(JSON.parse(await transport.readFile(path)));
2868
+ if (journal.adapter !== adapter || (journal.installationType ?? "local") !== installationType) continue;
2869
+ found.push({ path, journal });
2870
+ }
2871
+ return found.sort((a, b) => a.path.localeCompare(b.path));
2872
+ }
2873
+ async function readLinkedLocalApplyJournal(path, operationId, expectedDigest, expectedTransportDescription) {
2874
+ const absolutePath = resolve9(path);
2875
+ const journal = parseApplyJournal(JSON.parse(await readFile7(absolutePath, "utf8")));
2876
+ if (journal.version !== 2 || !journal.mutation) {
2877
+ throw new Error(`Runtime apply journal ${absolutePath} is not linked to a governed mutation.`);
2878
+ }
2879
+ if (journal.mutation.operationId !== mutationOperationIdSchema.parse(operationId)) {
2880
+ throw new Error(
2881
+ `Runtime apply journal ${absolutePath} belongs to mutation ${journal.mutation.operationId}, not ${operationId}.`
2882
+ );
2883
+ }
2884
+ if (journal.mutation.transport?.kind !== "local" || journal.mutation.transport.description !== expectedTransportDescription) {
2885
+ throw new Error(`Runtime apply journal ${absolutePath} transport metadata does not match its durable receipt.`);
2886
+ }
2887
+ if (journal.mutation.journalDigest !== expectedDigest || applyJournalLinkDigest(journal, {
2888
+ kind: "local",
2889
+ description: expectedTransportDescription
2890
+ }) !== expectedDigest) {
2891
+ throw new Error(`Runtime apply journal ${absolutePath} digest does not match its durable receipt.`);
2892
+ }
2893
+ const expectedPath = resolve9(applyJournalPath(journal.targetRoot, journal.adapter, {
2894
+ installationType: journal.installationType,
2895
+ stateKey: journal.stateKey
2896
+ }));
2897
+ if (absolutePath !== expectedPath) {
2898
+ throw new Error(`Runtime apply journal path mismatch: expected ${expectedPath}, found ${absolutePath}.`);
2899
+ }
2900
+ return journal;
2901
+ }
2902
+ async function removeApplyJournal(targetRoot, adapter, transport = localTransport, scope = {}) {
2903
+ const existing = await readApplyJournal(targetRoot, adapter, transport, scope);
2904
+ const path = applyJournalPath(targetRoot, adapter, scope);
2905
+ if (existing?.mutation) await beginResolveMutationRuntimeJournal(existing.mutation.operationId, path);
2906
+ await transport.rm(applyJournalPath(targetRoot, adapter, scope));
2907
+ await transport.rm(applyBackupDir(targetRoot, adapter, scope));
2908
+ if (existing?.mutation) await resolveMutationRuntimeJournal(existing.mutation.operationId, path);
2909
+ }
2910
+ async function abortApplyJournal(targetRoot, adapter, transport = localTransport, scope = {}) {
2911
+ assertGovernedRuntimeTransportSupported(transport);
2912
+ const lock = await acquireApplyLock(targetRoot, adapter, transport, {}, scope);
2913
+ try {
2914
+ const journalPath = applyJournalPath(targetRoot, adapter, scope);
2915
+ if (!await transport.pathExists(journalPath)) return void 0;
2916
+ const stateKey = stateKeyFor(adapter, scope);
2917
+ const archivePath = join6(metadataDir(targetRoot), "archive", `${stateKey}.apply-journal.failed-${journalTimestamp(/* @__PURE__ */ new Date())}.json`);
2918
+ const content = await transport.readFile(journalPath);
2919
+ const journal = parseApplyJournal(JSON.parse(content));
2920
+ await transport.writeFileAtomic(archivePath, content.endsWith("\n") ? content : `${content}
2921
+ `);
2922
+ if (journal.mutation) await beginResolveMutationRuntimeJournal(journal.mutation.operationId, journalPath);
2923
+ await transport.rm(journalPath);
2924
+ await transport.rm(applyBackupDir(targetRoot, adapter, scope));
2925
+ if (journal.mutation) await resolveMutationRuntimeJournal(journal.mutation.operationId, journalPath);
2926
+ return { journalPath, archivePath };
2927
+ } finally {
2928
+ await lock.release();
2929
+ }
2930
+ }
2931
+ function mutationMetadataForApplyJournal() {
2932
+ const metadata = activeMutationMetadata();
2933
+ return metadata ? {
2934
+ operationId: mutationOperationIdSchema.parse(metadata.operationId),
2935
+ reason: mutationReasonSchema.parse(metadata.reason),
2936
+ noCommit: metadata.noCommit
2937
+ } : void 0;
2938
+ }
2939
+ function assertGovernedRuntimeTransportSupported(transport) {
2940
+ const active2 = activeMutationMetadata();
2941
+ if (active2 && transport.kind !== "local") {
2942
+ throw new Error(
2943
+ `Governed runtime apply refuses ${transport.description} before writes; durable remote journal recovery is not implemented.`
2944
+ );
2945
+ }
2946
+ }
2947
+ function applyJournalLinkDigest(journal, transport) {
2948
+ if (!journal.mutation) throw new Error("Cannot digest an unlinked runtime apply journal.");
2949
+ return createHash5("sha256").update(canonicalJson3({
2950
+ version: journal.version,
2951
+ mutation: {
2952
+ operationId: journal.mutation.operationId,
2953
+ reason: journal.mutation.reason,
2954
+ noCommit: journal.mutation.noCommit
2955
+ },
2956
+ transport,
2957
+ mode: journal.mode ?? "apply",
2958
+ adapter: journal.adapter,
2959
+ installationType: journal.installationType ?? null,
2960
+ stateKey: journal.stateKey ?? null,
2961
+ targetRoot: resolve9(journal.targetRoot),
2962
+ baseRevision: journal.baseRevision,
2963
+ graphLockDigest: journal.graphLockDigest ?? null,
2964
+ graphLockPath: journal.graphLockPath ?? null,
2965
+ graphLockRemovePath: journal.graphLockRemovePath ?? null,
2966
+ workspaceConfigPath: journal.workspaceConfigPath ?? null,
2967
+ operations: journal.operations
2968
+ })).digest("hex");
2969
+ }
2970
+ function assertApplyJournalRecoveryAllowed(journal) {
2971
+ const active2 = activeMutationMetadata();
2972
+ if (!active2) return;
2973
+ if (journal.version === 1) {
2974
+ throw new Error(
2975
+ "A governed mutation refuses automatic recovery of a legacy v1 apply journal; inspect it and use an explicit journal recovery/abort workflow first."
2976
+ );
2977
+ }
2978
+ if (!journal.mutation || journal.mutation.operationId !== active2.operationId) {
2979
+ throw new Error(
2980
+ `Pending apply journal belongs to mutation ${journal.mutation?.operationId ?? "unknown"}; current mutation ${active2.operationId} may not adopt it.`
2981
+ );
2982
+ }
2983
+ }
2984
+ function parseApplyJournal(value) {
2985
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid Agentwheel apply journal.");
2986
+ const journal = value;
2987
+ if (journal.version !== 1 && journal.version !== 2) throw new Error(`Unsupported Agentwheel apply journal version: ${String(journal.version)}.`);
2988
+ if (journal.version === 1 && journal.mutation) throw new Error("Legacy Agentwheel apply journals may not contain mutation metadata.");
2989
+ if (journal.version === 2 && journal.mutation) {
2990
+ journal.mutation = {
2991
+ operationId: mutationOperationIdSchema.parse(journal.mutation.operationId),
2992
+ reason: mutationReasonSchema.parse(journal.mutation.reason),
2993
+ noCommit: journal.mutation.noCommit === true,
2994
+ transport: parseMutationTransport(journal.mutation.transport),
2995
+ journalDigest: parseJournalDigest(journal.mutation.journalDigest)
2996
+ };
2997
+ }
2998
+ return journal;
2999
+ }
3000
+ function parseMutationTransport(value) {
3001
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3002
+ throw new Error("Governed runtime apply journal is missing transport metadata.");
3003
+ }
3004
+ const record = value;
3005
+ if (record.kind !== "local" && record.kind !== "ssh" || typeof record.description !== "string" || record.description.length === 0 || record.description.length > 1024) {
3006
+ throw new Error("Governed runtime apply journal has invalid transport metadata.");
3007
+ }
3008
+ return { kind: record.kind, description: record.description };
3009
+ }
3010
+ function parseJournalDigest(value) {
3011
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) {
3012
+ throw new Error("Governed runtime apply journal is missing its durable link digest.");
3013
+ }
3014
+ return value;
3015
+ }
3016
+ function canonicalJson3(value) {
3017
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson3(item === void 0 ? null : item)).join(",")}]`;
3018
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
3019
+ const record = value;
3020
+ return `{${Object.keys(record).filter((key) => record[key] !== void 0 && typeof record[key] !== "function").sort().map((key) => `${JSON.stringify(key)}:${canonicalJson3(record[key])}`).join(",")}}`;
3021
+ }
3022
+ async function recordBackup(operation, index, targetRoot, adapter, transport = localTransport, scope = {}) {
3023
+ const hadExisting = await transport.pathExists(operation.destPath);
3024
+ if (!hadExisting || transport.kind !== "local" || operation.action !== "update" && operation.action !== "remove" && operation.action !== "create") {
3025
+ return {
3026
+ index,
3027
+ destPath: operation.destPath,
3028
+ kind: operation.kind,
3029
+ hadExisting
3030
+ };
3031
+ }
3032
+ const backupPath = join6(applyBackupDir(targetRoot, adapter, scope), String(index));
3033
+ await rm4(backupPath, { recursive: true, force: true });
3034
+ await mkdir3(dirname5(backupPath), { recursive: true });
3035
+ await cp(operation.destPath, backupPath, { recursive: operation.kind === "dir", dereference: true });
3036
+ return {
3037
+ index,
3038
+ destPath: operation.destPath,
3039
+ kind: operation.kind,
3040
+ hadExisting,
3041
+ backupPath
3042
+ };
3043
+ }
3044
+ async function rollbackCompletedOperations(completed, transport = localTransport) {
3045
+ for (const item of [...completed].sort((a, b) => b.index - a.index)) {
3046
+ if (item.hadExisting && item.backupPath) {
3047
+ await transport.atomicCopy(item.backupPath, item.destPath, item.kind);
3048
+ } else if (!item.hadExisting) {
3049
+ await transport.rm(item.destPath);
3050
+ } else {
3051
+ throw new Error(`Cannot roll back ${item.destPath}: no backup was recorded for ${transport.description}`);
3052
+ }
3053
+ }
3054
+ }
3055
+ async function handleExistingLock(lockPath, ownerPath, transport, options) {
3056
+ const owner = await readLockOwner(ownerPath, transport);
3057
+ if (options.staleAfterMs !== void 0 && owner?.createdAt) {
3058
+ const ageMs = Date.now() - Date.parse(owner.createdAt);
3059
+ if (Number.isFinite(ageMs) && ageMs > options.staleAfterMs) {
3060
+ await transport.rm(lockPath);
3061
+ return;
3062
+ }
3063
+ }
3064
+ const ownerDetails = owner ? ` created by pid ${owner.pid} at ${owner.createdAt}` : "";
3065
+ throw new Error(`Apply lock already exists at ${lockPath}${ownerDetails}. Run recovery or remove the lock after verifying no sync is running.`);
3066
+ }
3067
+ async function readLockOwner(ownerPath, transport) {
3068
+ try {
3069
+ if (!await transport.pathExists(ownerPath)) return void 0;
3070
+ return JSON.parse(await transport.readFile(ownerPath));
3071
+ } catch {
3072
+ return void 0;
3073
+ }
3074
+ }
3075
+ function isAlreadyExists2(error) {
3076
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
3077
+ }
3078
+ function journalTimestamp(date) {
3079
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
3080
+ }
3081
+ async function localPathExists(path) {
3082
+ try {
3083
+ await stat2(path);
3084
+ return true;
3085
+ } catch {
3086
+ return false;
3087
+ }
3088
+ }
3089
+
3090
+ export {
3091
+ artifactTypeSchema,
3092
+ fileKindSchema,
3093
+ artifactFormatSchema,
3094
+ packageAssetSchema,
3095
+ packageComposeEntrySchema,
3096
+ packageCompositionRuleSchema,
3097
+ packageSupersedesEntrySchema,
3098
+ packageItemRequireSchema,
3099
+ packageItemSuggestSchema,
3100
+ composedFromEntrySchema,
3101
+ defaultInstallationType,
3102
+ installationTypeSchema,
3103
+ adapterSchema,
3104
+ adapterTargetSupport,
3105
+ resolveInstallationTypeForArtifacts,
3106
+ resolveInstallationTypeForAdapter,
3107
+ targetMappingForArtifact,
3108
+ installRootForArtifacts,
3109
+ installRootForAdapterInstallationType,
3110
+ loadAdapterConfig,
3111
+ readMutationReceipt,
3112
+ listMutationReceipts,
3113
+ declareMutationPath,
3114
+ localTransport,
3115
+ transportForTarget,
3116
+ metadataDir,
3117
+ stateKeyFor,
3118
+ installManifestPath,
3119
+ sourceLockPath,
3120
+ parseSemver,
3121
+ satisfiesVersionRange,
3122
+ semverMajorOrVersion,
3123
+ compareSemverStrings,
3124
+ CURRENT_WORKSPACE_SCHEMA_VERSION,
3125
+ workspaceSelectionImportSchema,
3126
+ workspaceExportsSchema,
3127
+ fleetIdSchema,
3128
+ registeredFleetSchema,
3129
+ workspaceConfigSchema,
3130
+ supportsFleetConfig,
3131
+ workspaceConfigPath,
3132
+ readWorkspaceConfig,
3133
+ writeWorkspaceConfig,
3134
+ upsertPackage,
3135
+ globalWorkspaceConfigPath,
3136
+ findWorkspaceRoot,
3137
+ findExistingWorkspaceRoot,
3138
+ readMergedWorkspaceConfig,
3139
+ resolveConfigPath,
3140
+ isCompositeWorkspaceProfile,
3141
+ GovernedMutation,
3142
+ resumeMutation,
3143
+ recoverMutationRuntime,
3144
+ checkMutationProvider,
3145
+ mutationPolicyForWorkspace,
3146
+ applyLockPath,
3147
+ applyJournalPath,
3148
+ applyBackupDir,
3149
+ acquireApplyLock,
3150
+ writeApplyJournal,
3151
+ readApplyJournal,
3152
+ listApplyJournals,
3153
+ readLinkedLocalApplyJournal,
3154
+ removeApplyJournal,
3155
+ abortApplyJournal,
3156
+ mutationMetadataForApplyJournal,
3157
+ assertGovernedRuntimeTransportSupported,
3158
+ applyJournalLinkDigest,
3159
+ assertApplyJournalRecoveryAllowed,
3160
+ recordBackup,
3161
+ rollbackCompletedOperations,
3162
+ localPathExists
3163
+ };