@theaileverage/marionette 0.2.0 → 0.2.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.
package/dist/cli.js CHANGED
@@ -5346,10 +5346,10 @@ var require_raw_body = __commonJS({
5346
5346
  if (done) {
5347
5347
  return readStream(stream, encoding, length, limit, wrap(done));
5348
5348
  }
5349
- return new Promise(function executor(resolve12, reject) {
5349
+ return new Promise(function executor(resolve13, reject) {
5350
5350
  readStream(stream, encoding, length, limit, function onRead(err, buf) {
5351
5351
  if (err) return reject(err);
5352
- resolve12(buf);
5352
+ resolve13(buf);
5353
5353
  });
5354
5354
  });
5355
5355
  }
@@ -18931,11 +18931,11 @@ var require_view = __commonJS({
18931
18931
  var debug = require_src()("express:view");
18932
18932
  var path = __require("node:path");
18933
18933
  var fs = __require("node:fs");
18934
- var dirname10 = path.dirname;
18934
+ var dirname11 = path.dirname;
18935
18935
  var basename2 = path.basename;
18936
18936
  var extname = path.extname;
18937
18937
  var join = path.join;
18938
- var resolve12 = path.resolve;
18938
+ var resolve13 = path.resolve;
18939
18939
  module.exports = View;
18940
18940
  function View(name, options) {
18941
18941
  var opts = options || {};
@@ -18969,8 +18969,8 @@ var require_view = __commonJS({
18969
18969
  debug('lookup "%s"', name);
18970
18970
  for (var i = 0; i < roots.length && !path2; i++) {
18971
18971
  var root = roots[i];
18972
- var loc = resolve12(root, name);
18973
- var dir = dirname10(loc);
18972
+ var loc = resolve13(root, name);
18973
+ var dir = dirname11(loc);
18974
18974
  var file = basename2(loc);
18975
18975
  path2 = this.resolve(dir, file);
18976
18976
  }
@@ -18994,7 +18994,7 @@ var require_view = __commonJS({
18994
18994
  });
18995
18995
  sync = false;
18996
18996
  };
18997
- View.prototype.resolve = function resolve13(dir, file) {
18997
+ View.prototype.resolve = function resolve14(dir, file) {
18998
18998
  var ext = this.ext;
18999
18999
  var path2 = join(dir, file);
19000
19000
  var stat = tryStat(path2);
@@ -21253,7 +21253,7 @@ var require_application = __commonJS({
21253
21253
  var compileETag = require_utils3().compileETag;
21254
21254
  var compileQueryParser = require_utils3().compileQueryParser;
21255
21255
  var compileTrust = require_utils3().compileTrust;
21256
- var resolve12 = __require("node:path").resolve;
21256
+ var resolve13 = __require("node:path").resolve;
21257
21257
  var once = require_once();
21258
21258
  var Router = require_router();
21259
21259
  var slice = Array.prototype.slice;
@@ -21307,7 +21307,7 @@ var require_application = __commonJS({
21307
21307
  this.mountpath = "/";
21308
21308
  this.locals.settings = this.settings;
21309
21309
  this.set("view", View);
21310
- this.set("views", resolve12("views"));
21310
+ this.set("views", resolve13("views"));
21311
21311
  this.set("jsonp callback name", "callback");
21312
21312
  if (env === "production") {
21313
21313
  this.enable("view cache");
@@ -22887,7 +22887,7 @@ var require_send = __commonJS({
22887
22887
  var extname = path.extname;
22888
22888
  var join = path.join;
22889
22889
  var normalize = path.normalize;
22890
- var resolve12 = path.resolve;
22890
+ var resolve13 = path.resolve;
22891
22891
  var sep = path.sep;
22892
22892
  var BYTES_RANGE_REGEXP = /^ *bytes=/;
22893
22893
  var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1e3;
@@ -22916,7 +22916,7 @@ var require_send = __commonJS({
22916
22916
  this._maxage = opts.maxAge || opts.maxage;
22917
22917
  this._maxage = typeof this._maxage === "string" ? ms(this._maxage) : Number(this._maxage);
22918
22918
  this._maxage = !isNaN(this._maxage) ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE) : 0;
22919
- this._root = opts.root ? resolve12(opts.root) : null;
22919
+ this._root = opts.root ? resolve13(opts.root) : null;
22920
22920
  }
22921
22921
  util2.inherits(SendStream, Stream);
22922
22922
  SendStream.prototype.error = function error(status, err) {
@@ -23065,7 +23065,7 @@ var require_send = __commonJS({
23065
23065
  return res;
23066
23066
  }
23067
23067
  parts = normalize(path2).split(sep);
23068
- path2 = resolve12(path2);
23068
+ path2 = resolve13(path2);
23069
23069
  }
23070
23070
  if (containsDotFile(parts)) {
23071
23071
  debug('%s dotfile "%s"', this._dotfiles, path2);
@@ -23443,7 +23443,7 @@ var require_response = __commonJS({
23443
23443
  var cookie = require_cookie();
23444
23444
  var send = require_send();
23445
23445
  var extname = path.extname;
23446
- var resolve12 = path.resolve;
23446
+ var resolve13 = path.resolve;
23447
23447
  var vary = require_vary();
23448
23448
  var { Buffer: Buffer2 } = __require("node:buffer");
23449
23449
  var res = Object.create(http.ServerResponse.prototype);
@@ -23649,7 +23649,7 @@ var require_response = __commonJS({
23649
23649
  }
23650
23650
  opts = Object.create(opts);
23651
23651
  opts.headers = headers;
23652
- var fullPath = !opts.root ? resolve12(path2) : path2;
23652
+ var fullPath = !opts.root ? resolve13(path2) : path2;
23653
23653
  return this.sendFile(fullPath, opts, done);
23654
23654
  };
23655
23655
  res.contentType = res.type = function contentType(type) {
@@ -23898,7 +23898,7 @@ var require_serve_static = __commonJS({
23898
23898
  var encodeUrl = require_encodeurl();
23899
23899
  var escapeHtml = require_escape_html();
23900
23900
  var parseUrl = require_parseurl();
23901
- var resolve12 = __require("path").resolve;
23901
+ var resolve13 = __require("path").resolve;
23902
23902
  var send = require_send();
23903
23903
  var url = __require("url");
23904
23904
  module.exports = serveStatic;
@@ -23917,7 +23917,7 @@ var require_serve_static = __commonJS({
23917
23917
  throw new TypeError("option setHeaders must be function");
23918
23918
  }
23919
23919
  opts.maxage = opts.maxage || opts.maxAge || 0;
23920
- opts.root = resolve12(root);
23920
+ opts.root = resolve13(root);
23921
23921
  var onDirectory = redirect ? createRedirectDirectoryListener() : createNotFoundDirectoryListener();
23922
23922
  return function serveStatic2(req, res, next) {
23923
23923
  if (req.method !== "GET" && req.method !== "HEAD") {
@@ -24044,8 +24044,8 @@ var require_express2 = __commonJS({
24044
24044
  });
24045
24045
 
24046
24046
  // src/cli.ts
24047
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, openSync as openSync3, closeSync as closeSync3, existsSync as existsSync8 } from "node:fs";
24048
- import { resolve as resolve11 } from "node:path";
24047
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, openSync as openSync4, closeSync as closeSync4, existsSync as existsSync9 } from "node:fs";
24048
+ import { resolve as resolve12 } from "node:path";
24049
24049
  import { spawn as spawn4 } from "node:child_process";
24050
24050
 
24051
24051
  // src/config.ts
@@ -24365,8 +24365,8 @@ var ZodError = class _ZodError extends Error {
24365
24365
  let i = 0;
24366
24366
  while (i < issue.path.length) {
24367
24367
  const el = issue.path[i];
24368
- const terminal2 = i === issue.path.length - 1;
24369
- if (!terminal2) {
24368
+ const terminal3 = i === issue.path.length - 1;
24369
+ if (!terminal3) {
24370
24370
  curr[el] = curr[el] || { _errors: [] };
24371
24371
  } else {
24372
24372
  curr[el] = curr[el] || { _errors: [] };
@@ -28138,7 +28138,9 @@ async function call(home2, action, input = {}) {
28138
28138
  method: "POST",
28139
28139
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${c.token}` },
28140
28140
  body: JSON.stringify({ action, input }),
28141
- signal: AbortSignal.timeout(action === "profile.validate" ? 135e3 : 45e3)
28141
+ signal: AbortSignal.timeout(
28142
+ action.startsWith("cleanup.") ? 3e5 : action === "profile.validate" ? 135e3 : 45e3
28143
+ )
28142
28144
  });
28143
28145
  const body = await response.json();
28144
28146
  if (!response.ok)
@@ -28150,9 +28152,9 @@ async function call(home2, action, input = {}) {
28150
28152
 
28151
28153
  // src/server.ts
28152
28154
  var import_express = __toESM(require_express2(), 1);
28153
- import { timingSafeEqual, randomUUID as randomUUID8 } from "node:crypto";
28154
- import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2 } from "node:fs";
28155
- import { resolve as resolve8, dirname as dirname7 } from "node:path";
28155
+ import { timingSafeEqual, randomUUID as randomUUID9 } from "node:crypto";
28156
+ import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "node:fs";
28157
+ import { resolve as resolve9, dirname as dirname8 } from "node:path";
28156
28158
  import { fileURLToPath } from "node:url";
28157
28159
 
28158
28160
  // src/store.ts
@@ -28285,8 +28287,8 @@ var Store = class {
28285
28287
  };
28286
28288
 
28287
28289
  // src/service.ts
28288
- import { randomUUID as randomUUID5, randomBytes as randomBytes2 } from "node:crypto";
28289
- import { realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
28290
+ import { randomUUID as randomUUID6, randomBytes as randomBytes2 } from "node:crypto";
28291
+ import { realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
28290
28292
  import { isAbsolute as isAbsolute2 } from "node:path";
28291
28293
 
28292
28294
  // src/continuation.ts
@@ -28341,7 +28343,7 @@ function digest(path) {
28341
28343
  }
28342
28344
  }
28343
28345
  function command(command2, args2, cwd, timeoutMs = 3e4) {
28344
- return new Promise((resolve12, reject) => {
28346
+ return new Promise((resolve13, reject) => {
28345
28347
  const env = { ...process.env };
28346
28348
  for (const k of Object.keys(env))
28347
28349
  if (k.startsWith("MARIONETTE_") || k.startsWith("HERDR_")) delete env[k];
@@ -28372,7 +28374,7 @@ function command(command2, args2, cwd, timeoutMs = 3e4) {
28372
28374
  });
28373
28375
  child.on("close", (code) => {
28374
28376
  clearTimeout(timer);
28375
- resolve12({ code, output, timedOut });
28377
+ resolve13({ code, output, timedOut });
28376
28378
  });
28377
28379
  });
28378
28380
  }
@@ -28590,7 +28592,7 @@ var Continuation = class {
28590
28592
  }
28591
28593
  }
28592
28594
  async stop() {
28593
- while (this.busy.size) await new Promise((resolve12) => setTimeout(resolve12, 25));
28595
+ while (this.busy.size) await new Promise((resolve13) => setTimeout(resolve13, 25));
28594
28596
  }
28595
28597
  async process(w) {
28596
28598
  if (!["waiting", "ready"].includes(w.state)) {
@@ -28913,6 +28915,776 @@ ${JSON.stringify(summary)}`;
28913
28915
  }
28914
28916
  };
28915
28917
 
28918
+ // src/cleanup.ts
28919
+ import { randomUUID as randomUUID3, createHash as createHash2 } from "node:crypto";
28920
+ import {
28921
+ existsSync as existsSync3,
28922
+ mkdirSync as mkdirSync4,
28923
+ readFileSync as readFileSync4,
28924
+ writeFileSync as writeFileSync2,
28925
+ realpathSync as realpathSync3,
28926
+ openSync,
28927
+ fsyncSync,
28928
+ closeSync,
28929
+ createReadStream,
28930
+ statSync as statSync3
28931
+ } from "node:fs";
28932
+ import { dirname as dirname4, resolve as resolve4 } from "node:path";
28933
+
28934
+ // src/worktrees.ts
28935
+ import { execFile } from "node:child_process";
28936
+ import { promisify } from "node:util";
28937
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
28938
+ import { dirname as dirname3, relative as relative2, resolve as resolve3 } from "node:path";
28939
+ var exec = promisify(execFile);
28940
+ async function git(cwd, args2) {
28941
+ const env = { ...process.env };
28942
+ for (const key of Object.keys(env)) if (key.startsWith("GIT_")) delete env[key];
28943
+ env.GIT_TERMINAL_PROMPT = "0";
28944
+ try {
28945
+ const result = await exec("git", ["-c", "core.hooksPath=/dev/null", "-C", cwd, ...args2], {
28946
+ env,
28947
+ timeout: 12e4,
28948
+ maxBuffer: 4 * 1024 * 1024
28949
+ });
28950
+ return result.stdout.trimEnd();
28951
+ } catch (error) {
28952
+ const e = error;
28953
+ throw new AppError(
28954
+ "worktree_git",
28955
+ `Git worktree operation failed: ${e.stderr?.trim() || e.message}`
28956
+ );
28957
+ }
28958
+ }
28959
+ async function planWorktree(task, stateRoot) {
28960
+ const repositoryRoot = realpathSync2(await git(task.cwd, ["rev-parse", "--show-toplevel"]));
28961
+ const commonDir = realpathSync2(
28962
+ await git(task.cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"])
28963
+ );
28964
+ const baseRef = task.execution?.mode === "worktree" ? task.execution.baseRef ?? "HEAD" : "HEAD";
28965
+ const baseCommit = await git(task.cwd, [
28966
+ "rev-parse",
28967
+ "--verify",
28968
+ "--end-of-options",
28969
+ `${baseRef}^{commit}`
28970
+ ]);
28971
+ if (!inside(repositoryRoot, task.cwd))
28972
+ throw new AppError("worktree_scope", "Task directory is outside its Git repository");
28973
+ const path = safePath(stateRoot, `worktrees/${task.projectId}/${task.id}`);
28974
+ return {
28975
+ state: "planned",
28976
+ repositoryRoot,
28977
+ commonDir,
28978
+ sourceCwd: task.cwd,
28979
+ path,
28980
+ cwd: resolve3(path, relative2(repositoryRoot, task.cwd)),
28981
+ branch: `marionette/${task.id}`,
28982
+ baseCommit
28983
+ };
28984
+ }
28985
+ async function validateWorktree(w, requireClean = false) {
28986
+ const listing = await git(w.repositoryRoot, ["worktree", "list", "--porcelain", "-z"]);
28987
+ const records = listing.split("\0\0").map((record2) => record2.split("\0"));
28988
+ const record = records.find((fields) => fields.includes(`worktree ${w.path}`));
28989
+ if (!record || !record.includes(`branch refs/heads/${w.branch}`) || record.some((field) => /^(locked|prunable)( |$)/.test(field)) || !existsSync2(w.path) || realpathSync2(w.path) !== w.path)
28990
+ throw new AppError(
28991
+ "worktree_identity",
28992
+ "Managed worktree is missing, incomplete, or has changed identity. Inspect it before retrying; existing work is preserved."
28993
+ );
28994
+ const commonDir = realpathSync2(
28995
+ await git(w.path, ["rev-parse", "--path-format=absolute", "--git-common-dir"])
28996
+ );
28997
+ const root = realpathSync2(await git(w.path, ["rev-parse", "--show-toplevel"]));
28998
+ const branch = await git(w.path, ["symbolic-ref", "HEAD"]);
28999
+ if (commonDir !== w.commonDir || root !== w.path || branch !== `refs/heads/${w.branch}`)
29000
+ throw new AppError(
29001
+ "worktree_identity",
29002
+ "Managed worktree repository or branch changed; refusing to reuse it."
29003
+ );
29004
+ if (requireClean) {
29005
+ const head = await git(w.path, ["rev-parse", "HEAD"]);
29006
+ const status = await git(w.path, ["status", "--porcelain", "--untracked-files=all"]);
29007
+ if (head !== w.baseCommit || status)
29008
+ throw new AppError(
29009
+ "worktree_incomplete",
29010
+ "Worktree creation was interrupted and its checkout is not clean at the pinned base. Inspect it before retrying; no files were reset."
29011
+ );
29012
+ }
29013
+ const cwd = safePath(w.path, relative2(w.path, w.cwd), true);
29014
+ if (!statSync2(cwd).isDirectory())
29015
+ throw new AppError(
29016
+ "worktree_cwd",
29017
+ "Task working directory does not exist in the selected Git revision"
29018
+ );
29019
+ return cwd;
29020
+ }
29021
+ async function createWorktree(w) {
29022
+ mkdirSync3(dirname3(w.path), { recursive: true, mode: 448 });
29023
+ await git(w.repositoryRoot, ["worktree", "add", "-b", w.branch, "--", w.path, w.baseCommit]);
29024
+ return validateWorktree(w, true);
29025
+ }
29026
+
29027
+ // src/cleanup.ts
29028
+ var cleanupPolicySchema = external_exports.object({
29029
+ autoRelease: external_exports.boolean().default(true),
29030
+ collectAfterHours: external_exports.number().min(0).max(87600).nullable().default(null),
29031
+ deleteMergedBranches: external_exports.boolean().default(false)
29032
+ }).strict();
29033
+ var terminal2 = (t) => ["completed", "failed", "cancelled"].includes(t.status);
29034
+ function refuse(message) {
29035
+ throw new AppError("cleanup_blocked", message, 409);
29036
+ }
29037
+ function flush(path) {
29038
+ const fd = openSync(path, "r");
29039
+ try {
29040
+ fsyncSync(fd);
29041
+ } finally {
29042
+ closeSync(fd);
29043
+ }
29044
+ }
29045
+ async function fileHash(path) {
29046
+ const value = createHash2("sha256");
29047
+ for await (const chunk of createReadStream(path)) value.update(chunk);
29048
+ return value.digest("hex");
29049
+ }
29050
+ var Cleanup = class {
29051
+ constructor(s) {
29052
+ this.s = s;
29053
+ }
29054
+ s;
29055
+ busy = /* @__PURE__ */ new Set();
29056
+ stopped = false;
29057
+ lastAutomatic = 0;
29058
+ policy(projectId) {
29059
+ return this.s.store.get("cleanup-policy", projectId) ?? cleanupPolicySchema.parse({});
29060
+ }
29061
+ archive(t) {
29062
+ return t.archiveId ? this.s.store.get("archive", t.archiveId) : void 0;
29063
+ }
29064
+ group(t) {
29065
+ return t.worktree ? this.s.store.all("task").filter((x) => x.worktree?.path === t.worktree.path || inside(t.worktree.path, x.cwd)) : [t];
29066
+ }
29067
+ assertMutable(t) {
29068
+ if (t.archiveId || this.busy.has(t.id))
29069
+ refuse(
29070
+ "Task is archived or cleanup is in progress. Create a new assignment for further work."
29071
+ );
29072
+ const affected = /* @__PURE__ */ new Set([t.id]);
29073
+ const tasks = this.s.store.all("task");
29074
+ for (const id of affected) {
29075
+ const parentId = tasks.find((child) => child.id === id)?.parentId;
29076
+ for (const other of tasks)
29077
+ if (other.dependencies.includes(id) || other.id === parentId) affected.add(other.id);
29078
+ }
29079
+ if ([...affected].some((id) => this.busy.has(id)))
29080
+ refuse("Cleanup is inspecting a consumer of this task; retry after it finishes");
29081
+ const r = t.runId ? this.s.store.get("run", t.runId) : void 0;
29082
+ if (r?.cleanup && ["closing", "uncertain"].includes(r.cleanup.state))
29083
+ refuse("Reconcile terminal cleanup before changing this task.");
29084
+ }
29085
+ active(taskId) {
29086
+ return this.busy.has(taskId);
29087
+ }
29088
+ assertOutcomeMutable(outcomeId) {
29089
+ if (this.s.store.all("task").some((t) => t.outcomeId === outcomeId && this.busy.has(t.id)))
29090
+ refuse("Cleanup is inspecting this outcome; retry after it finishes");
29091
+ }
29092
+ consumers(tasks) {
29093
+ const ids = new Set(tasks.map((t) => t.id));
29094
+ const roots = tasks.map((t) => t.worktree?.path ?? t.cwd);
29095
+ const reasons = [];
29096
+ for (const t of this.s.store.all("task")) {
29097
+ if (ids.has(t.id)) continue;
29098
+ if (!terminal2(t) && (ids.has(t.parentId ?? "") || t.dependencies.some((id) => ids.has(id)) || (t.waitForChildren ?? []).some((id) => ids.has(id))))
29099
+ reasons.push(`Task ${t.id} still consumes these results`);
29100
+ }
29101
+ for (const p of this.s.store.all("project")) {
29102
+ if (roots.some((root) => tasks.some((t) => t.worktree?.path === root) && inside(root, p.root)))
29103
+ reasons.push(`Registered project ${p.id} uses this checkout`);
29104
+ }
29105
+ return reasons;
29106
+ }
29107
+ runReasons(t, r, automatic) {
29108
+ const reasons = [];
29109
+ if (!terminal2(t) || r.phase !== "stopped")
29110
+ reasons.push("Task and native run must be finished and settled");
29111
+ if (t.resumePending) reasons.push("A parent continuation is pending");
29112
+ if (this.s.store.all("operation").some((o) => o.taskId === t.id && !["done", "failed"].includes(o.phase)))
29113
+ reasons.push("A task control is unresolved");
29114
+ if (this.s.store.all("lead-wait").some(
29115
+ (w) => w.adapter.type === "herdr" && w.adapter.paneId === r.paneId && !["invalidated", "acknowledged"].includes(w.state)
29116
+ ))
29117
+ reasons.push("This pane is pinned by a lead wait");
29118
+ reasons.push(...this.consumers([t]));
29119
+ if (automatic && (!this.policy(t.projectId).autoRelease || t.status !== "completed" || !t.outcomeId || this.s.orchestration.outcome(t.outcomeId).status !== "completed"))
29120
+ reasons.push(
29121
+ "Automatic release waits for the completed integrated outcome and enabled policy; failures require inspection"
29122
+ );
29123
+ return reasons;
29124
+ }
29125
+ groupReasons(t) {
29126
+ const tasks = this.group(t), reasons = this.consumers(tasks);
29127
+ if (!t.worktree || t.worktree.state !== "ready")
29128
+ reasons.push("Only ready Marionette-managed worktrees can be collected");
29129
+ for (const item of tasks) {
29130
+ if (!terminal2(item)) reasons.push(`Task ${item.id} is ${item.status}`);
29131
+ for (const r of this.s.store.all("run").filter((r2) => r2.taskId === item.id))
29132
+ if (r.paneId && r.cleanup?.state !== "closed")
29133
+ reasons.push(`Run ${r.id} must release its terminal first`);
29134
+ if (item.status === "completed" && item.outcomeId && this.s.orchestration.outcome(item.outcomeId).status !== "completed")
29135
+ reasons.push(`Outcome ${item.outcomeId} still needs integration`);
29136
+ else if (item.status === "completed" && item.outcomeId && this.s.orchestration.unmet(this.s.orchestration.outcome(item.outcomeId)).length)
29137
+ reasons.push(`Outcome ${item.outcomeId} evidence is no longer current`);
29138
+ }
29139
+ for (const o of this.s.store.all("outcome")) {
29140
+ if (o.status === "completed") continue;
29141
+ for (const ref of [
29142
+ ...o.assessments.flatMap((a) => a.references),
29143
+ ...o.integrated?.evidence ?? []
29144
+ ]) {
29145
+ if (tasks.some((t2) => ref.path.startsWith(`task:${t2.id}:`)))
29146
+ reasons.push(`Outcome ${o.id} still references this checkout`);
29147
+ }
29148
+ }
29149
+ return [...new Set(reasons)];
29150
+ }
29151
+ async preview(taskId) {
29152
+ const t = this.s.task(taskId), runs = this.s.store.all("run").filter((r) => r.taskId === taskId), archive = this.archive(t);
29153
+ let gitReason;
29154
+ if (t.worktree && !archive)
29155
+ try {
29156
+ await this.clean(t.worktree);
29157
+ } catch (e) {
29158
+ gitReason = String(e);
29159
+ }
29160
+ return {
29161
+ taskId,
29162
+ policy: this.policy(t.projectId),
29163
+ runs: runs.map((r) => ({
29164
+ runId: r.id,
29165
+ tabId: r.tabId,
29166
+ cleanup: r.cleanup,
29167
+ reasons: this.runReasons(t, r, true)
29168
+ })),
29169
+ delivery: this.s.store.get("delivery", taskId),
29170
+ archive,
29171
+ collectionReasons: [
29172
+ ...this.groupReasons(t),
29173
+ ...gitReason ? [gitReason] : [],
29174
+ ...!this.s.store.get("delivery", taskId) && !archive ? ["Record delivery or explicit abandonment first"] : []
29175
+ ],
29176
+ boundary: "Completion retains branches and worktrees. Collection never removes sessions or workspaces."
29177
+ };
29178
+ }
29179
+ async locked(tasks, fn) {
29180
+ if (this.stopped || tasks.some((t) => this.busy.has(t.id)))
29181
+ refuse("Cleanup is stopping or already in progress");
29182
+ tasks.forEach((t) => this.busy.add(t.id));
29183
+ try {
29184
+ return await fn();
29185
+ } finally {
29186
+ tasks.forEach((t) => this.busy.delete(t.id));
29187
+ }
29188
+ }
29189
+ recover() {
29190
+ this.stopped = false;
29191
+ for (const r of this.s.store.all("run"))
29192
+ if (r.cleanup?.state === "closing")
29193
+ this.saveRun(r, {
29194
+ ...r.cleanup,
29195
+ state: "uncertain",
29196
+ error: "Restart during tab closure; inspect and reconcile. No automatic replay."
29197
+ });
29198
+ }
29199
+ async stop() {
29200
+ this.stopped = true;
29201
+ while (this.busy.size) await new Promise((r) => setTimeout(r, 25));
29202
+ }
29203
+ tick() {
29204
+ if (this.stopped || Date.now() - this.lastAutomatic < 1e4) return;
29205
+ this.lastAutomatic = Date.now();
29206
+ for (const t of this.s.store.all("task")) {
29207
+ if (this.busy.has(t.id)) continue;
29208
+ const p = this.policy(t.projectId);
29209
+ const r = t.runId ? this.s.store.get("run", t.runId) : void 0;
29210
+ if (p.autoRelease && r && !r.cleanup && !this.runReasons(t, r, true).length) {
29211
+ void this.locked([t], () => this.release(t, r, "Integrated outcome completed", true)).catch(
29212
+ (e) => this.s.store.event(t.projectId, "cleanup.error", String(e), t.id)
29213
+ );
29214
+ continue;
29215
+ }
29216
+ const delivery = this.s.store.get("delivery", t.id), a = this.archive(t);
29217
+ if (p.collectAfterHours === null || !delivery || delivery.disposition === "abandoned" || a && a.taskId !== t.id || this.group(t).some((x) => x.status !== "completed"))
29218
+ continue;
29219
+ if (a?.phase === "collected" && (!p.deleteMergedBranches || a.branchDeleted || delivery.disposition !== "merged"))
29220
+ continue;
29221
+ if (Date.now() - Date.parse(delivery.createdAt) < p.collectAfterHours * 36e5 || this.groupReasons(t).length)
29222
+ continue;
29223
+ if (a?.error || a?.phase === "removing") continue;
29224
+ void this.locked(this.group(t), async () => {
29225
+ const guard = () => {
29226
+ if (JSON.stringify(this.policy(t.projectId)) !== JSON.stringify(p))
29227
+ refuse("Cleanup policy changed during inspection");
29228
+ };
29229
+ const archived = a ?? await this.createArchive(t, guard);
29230
+ await this.collect(
29231
+ archived,
29232
+ p.deleteMergedBranches && delivery.disposition === "merged",
29233
+ guard
29234
+ );
29235
+ }).catch((e) => this.s.store.event(t.projectId, "cleanup.error", String(e), t.id));
29236
+ }
29237
+ }
29238
+ saveRun(r, cleanup) {
29239
+ const latest = this.s.store.get("run", r.id);
29240
+ this.s.store.put("run", r.id, { ...latest, cleanup: { ...cleanup, updatedAt: now() } });
29241
+ }
29242
+ async pinned(t, r) {
29243
+ const p = this.s.project(t.projectId), h = this.s.port(p);
29244
+ if (!r.tabId || !r.paneId || !r.terminalId || !r.nativeSession)
29245
+ refuse("A full saved worker and native session identity is required");
29246
+ const tab = (await h.call("tab.get", { tab_id: r.tabId })).tab;
29247
+ const panes = (await h.call("pane.list", { workspace_id: p.workspaceId })).panes;
29248
+ const members = panes?.filter((pane) => pane.tab_id === r.tabId);
29249
+ if (tab?.workspace_id !== p.workspaceId || tab.tab_id !== r.tabId || tab.pane_count !== 1 || members?.length !== 1 || members[0].pane_id !== r.paneId || members[0].terminal_id !== r.terminalId)
29250
+ refuse("Tab contents changed; refusing to close another pane");
29251
+ const a = (await h.call("agent.get", { target: r.paneId })).agent;
29252
+ if (a?.workspace_id !== p.workspaceId || a.pane_id !== r.paneId || a.terminal_id !== r.terminalId || a.name !== r.agentName || a.agent !== r.kind || a.agent_session?.value !== r.nativeSession || !["idle", "done"].includes(a.agent_status) || a.launch_pending || a.interactive_ready === false)
29253
+ refuse("Native worker identity or readiness changed");
29254
+ return h;
29255
+ }
29256
+ async absent(t, r) {
29257
+ const h = this.s.port(this.s.project(t.projectId));
29258
+ try {
29259
+ await h.call("tab.get", { tab_id: r.tabId });
29260
+ return false;
29261
+ } catch (e) {
29262
+ if (e.code !== "tab_not_found") throw e;
29263
+ }
29264
+ const panes = (await h.call("pane.list")).panes;
29265
+ if (!Array.isArray(panes) || panes.some((p) => p.pane_id === r.paneId || p.terminal_id === r.terminalId))
29266
+ refuse("Original terminal may have moved to another tab; inspect it before cleanup");
29267
+ return true;
29268
+ }
29269
+ async checkReleased(tasks) {
29270
+ for (const t of tasks) {
29271
+ for (const r of this.s.store.all("run").filter((r2) => r2.taskId === t.id && r2.paneId))
29272
+ if (r.cleanup?.state !== "closed" || !await this.absent(t, r))
29273
+ refuse(
29274
+ "A recorded worker terminal is still present; inspect it before collecting its checkout"
29275
+ );
29276
+ }
29277
+ }
29278
+ async release(t, r, reason, automatic, guard = () => {
29279
+ }) {
29280
+ if (r.cleanup?.state === "closed") return r.cleanup;
29281
+ if (r.cleanup && ["closing", "uncertain"].includes(r.cleanup.state))
29282
+ refuse("Reconcile ambiguous tab closure first");
29283
+ try {
29284
+ const reasons = this.runReasons(this.s.task(t.id), r, automatic);
29285
+ if (reasons.length) refuse(reasons.join("; "));
29286
+ if (await this.absent(t, r)) {
29287
+ guard();
29288
+ this.saveRun(r, { state: "closed", reason, updatedAt: now(), output: t.output });
29289
+ return this.s.store.get("run", r.id).cleanup;
29290
+ }
29291
+ const h = await this.pinned(t, r);
29292
+ const output = (await h.call("pane.read", {
29293
+ pane_id: r.paneId,
29294
+ source: "recent_unwrapped",
29295
+ lines: 500,
29296
+ format: "text"
29297
+ })).read?.text ?? t.output;
29298
+ await this.pinned(t, r);
29299
+ guard();
29300
+ const current = this.s.task(t.id);
29301
+ if (current.revision !== t.revision || this.runReasons(current, this.s.store.get("run", r.id), automatic).length)
29302
+ refuse("Task changed during cleanup inspection");
29303
+ this.saveRun(r, {
29304
+ state: "closing",
29305
+ reason,
29306
+ updatedAt: now(),
29307
+ output: String(output).slice(-1e5)
29308
+ });
29309
+ await h.call("tab.close", { tab_id: r.tabId });
29310
+ this.saveRun(r, { ...this.s.store.get("run", r.id).cleanup, state: "closed" });
29311
+ this.s.store.event(
29312
+ t.projectId,
29313
+ "cleanup.released",
29314
+ "Saved worker diagnostics and closed its settled tab",
29315
+ t.id,
29316
+ { runId: r.id }
29317
+ );
29318
+ return this.s.store.get("run", r.id).cleanup;
29319
+ } catch (e) {
29320
+ const current = this.s.store.get("run", r.id).cleanup;
29321
+ this.saveRun(r, {
29322
+ ...current,
29323
+ state: current?.state === "closing" ? "uncertain" : "retained",
29324
+ reason,
29325
+ updatedAt: now(),
29326
+ error: String(e)
29327
+ });
29328
+ throw e;
29329
+ }
29330
+ }
29331
+ async clean(w) {
29332
+ await validateWorktree(w);
29333
+ const status = await git(w.path, [
29334
+ "status",
29335
+ "--porcelain",
29336
+ "--untracked-files=all",
29337
+ "--ignored"
29338
+ ]);
29339
+ if (status)
29340
+ refuse(
29341
+ "Worktree has modified, untracked, or ignored files; preserve or remove them explicitly before collection"
29342
+ );
29343
+ return git(w.path, ["rev-parse", "HEAD"]);
29344
+ }
29345
+ async checkDelivery(w, d) {
29346
+ if (await this.clean(w) !== d.head) refuse("Worktree HEAD changed after delivery");
29347
+ if (d.disposition !== "abandoned") {
29348
+ if (!d.targetRef || !d.targetCommit) refuse("Delivery target is missing");
29349
+ const current = await git(w.repositoryRoot, [
29350
+ "rev-parse",
29351
+ "--verify",
29352
+ "--end-of-options",
29353
+ `${d.targetRef}^{commit}`
29354
+ ]);
29355
+ await git(w.repositoryRoot, ["merge-base", "--is-ancestor", d.head, current]);
29356
+ }
29357
+ }
29358
+ archiveRoot(id) {
29359
+ return safePath(dirname4(this.s.store.path), `archives/${id}`);
29360
+ }
29361
+ /** Live files remain authoritative until collection actually removes the checkout. */
29362
+ evidencePath(t, input) {
29363
+ const a = this.archive(t);
29364
+ if (!a || a.phase === "archived" || existsSync3(a.worktree.path))
29365
+ return safePath(t.cwd, input, true);
29366
+ const source = resolve4(t.cwd, input);
29367
+ if (!inside(t.cwd, source)) refuse("Evidence escapes task directory");
29368
+ const f = a.files.find((f2) => f2.source === source);
29369
+ if (!f) refuse("Evidence was not preserved in this archive");
29370
+ const path = safePath(this.archiveRoot(a.id), `files/${f.digest}`, true);
29371
+ if (digest(path) !== f.digest) refuse("Archived evidence digest changed");
29372
+ return path;
29373
+ }
29374
+ async createArchive(t, guard) {
29375
+ const prior = this.archive(t);
29376
+ if (prior) return prior;
29377
+ const reasons = this.groupReasons(t);
29378
+ if (reasons.length) refuse(reasons.join("; "));
29379
+ const w = t.worktree, d = this.s.store.get("delivery", t.id);
29380
+ if (!d) refuse("Record delivery or abandonment before archiving");
29381
+ await this.checkDelivery(w, d);
29382
+ const tasks = this.group(t), taskIds = new Set(tasks.map((t2) => t2.id));
29383
+ await this.checkReleased(tasks);
29384
+ const id = randomUUID3(), root = this.archiveRoot(id);
29385
+ mkdirSync4(resolve4(root, "files"), { recursive: true, mode: 448 });
29386
+ const files = /* @__PURE__ */ new Map();
29387
+ const copy = (source, expected, alias = source) => {
29388
+ const actual = digest(source);
29389
+ if (!actual || expected && expected !== actual)
29390
+ refuse("Evidence is missing or changed before archival");
29391
+ const bytes = readFileSync4(source);
29392
+ if (hash(bytes) !== actual) refuse("Evidence changed during archival");
29393
+ writeFileSync2(resolve4(root, "files", actual), bytes, { mode: 384, flush: true });
29394
+ files.set(source, actual);
29395
+ files.set(alias, actual);
29396
+ };
29397
+ for (const item of tasks) {
29398
+ for (const v of item.verification ?? [])
29399
+ if (v.passed && v.check.type === "file")
29400
+ copy(safePath(item.cwd, v.check.path, true), v.digest, resolve4(item.cwd, v.check.path));
29401
+ for (const path of item.receipt?.artifacts ?? []) {
29402
+ const source = safePath(item.cwd, path, true);
29403
+ if (!item.ownership.some((owned) => inside(safePath(item.cwd, owned), source)))
29404
+ refuse("Reported artifact no longer belongs to the task");
29405
+ if (statSync3(source).isDirectory()) continue;
29406
+ copy(source, void 0, resolve4(item.cwd, path));
29407
+ }
29408
+ }
29409
+ for (const o of this.s.store.all("outcome")) {
29410
+ for (const ref of [
29411
+ ...o.assessments.flatMap((a2) => a2.references),
29412
+ ...o.integrated?.evidence ?? []
29413
+ ]) {
29414
+ const match = /^task:([^:]+):(.+)$/.exec(ref.path);
29415
+ if (match && taskIds.has(match[1]))
29416
+ copy(
29417
+ safePath(this.s.task(match[1]).cwd, match[2], true),
29418
+ ref.digest,
29419
+ resolve4(this.s.task(match[1]).cwd, match[2])
29420
+ );
29421
+ else if (!match) {
29422
+ const path = resolve4(this.s.project(o.projectId).root, ref.path);
29423
+ if (inside(w.path, path))
29424
+ refuse(
29425
+ "Project-relative evidence still uses this checkout; replace it with a task reference"
29426
+ );
29427
+ }
29428
+ }
29429
+ }
29430
+ await git(w.repositoryRoot, [
29431
+ "bundle",
29432
+ "create",
29433
+ resolve4(root, "commits.bundle"),
29434
+ `refs/heads/${w.branch}`
29435
+ ]);
29436
+ await git(w.repositoryRoot, ["bundle", "verify", resolve4(root, "commits.bundle")]);
29437
+ flush(resolve4(root, "commits.bundle"));
29438
+ const bundleDigest = await fileHash(resolve4(root, "commits.bundle"));
29439
+ const manifest = JSON.stringify(
29440
+ {
29441
+ tasks,
29442
+ runs: this.s.store.all("run").filter((r) => taskIds.has(r.taskId)).map(({ tokenHash, ...r }) => r),
29443
+ outcomes: this.s.store.all("outcome").filter((o) => tasks.some((t2) => t2.outcomeId === o.id)),
29444
+ delivery: d,
29445
+ files: [...files],
29446
+ archivedAt: now()
29447
+ },
29448
+ null,
29449
+ 2
29450
+ );
29451
+ writeFileSync2(resolve4(root, "manifest.json"), manifest, { mode: 384, flush: true });
29452
+ flush(resolve4(root, "files"));
29453
+ flush(root);
29454
+ flush(dirname4(root));
29455
+ flush(dirname4(dirname4(root)));
29456
+ await this.checkDelivery(w, d);
29457
+ guard();
29458
+ if (this.groupReasons(t).length || this.group(t).some((x) => !tasks.some((y) => x.id === y.id && x.revision === y.revision)))
29459
+ refuse("Checkout consumers changed during archival");
29460
+ for (const [path, expected] of files)
29461
+ if (digest(path) !== expected) refuse("Evidence changed during archival");
29462
+ const a = {
29463
+ id,
29464
+ taskId: t.id,
29465
+ projectId: t.projectId,
29466
+ taskIds: [...taskIds],
29467
+ phase: "archived",
29468
+ worktree: w,
29469
+ delivery: d,
29470
+ files: [...files].map(([source, digest2]) => ({ source, digest: digest2 })),
29471
+ manifestDigest: hash(manifest),
29472
+ bundleDigest,
29473
+ createdAt: now(),
29474
+ updatedAt: now()
29475
+ };
29476
+ this.s.store.transaction(() => {
29477
+ this.s.store.put("archive", id, a);
29478
+ for (const item of tasks) this.s.updateTask(item, { archiveId: id });
29479
+ this.s.store.event(
29480
+ t.projectId,
29481
+ "cleanup.archived",
29482
+ "Preserved evidence, diagnostics, and committed history; tasks are sealed",
29483
+ t.id,
29484
+ { archiveId: id }
29485
+ );
29486
+ });
29487
+ return a;
29488
+ }
29489
+ async verifyArchive(a) {
29490
+ const root = this.archiveRoot(a.id);
29491
+ if (hash(readFileSync4(safePath(root, "manifest.json", true))) !== a.manifestDigest || await fileHash(safePath(root, "commits.bundle", true)) !== a.bundleDigest)
29492
+ refuse("Archive integrity check failed");
29493
+ for (const f of a.files)
29494
+ if (digest(safePath(root, `files/${f.digest}`, true)) !== f.digest)
29495
+ refuse("Archived evidence integrity check failed");
29496
+ }
29497
+ saveArchive(a, patch) {
29498
+ const next = { ...this.s.store.get("archive", a.id), ...patch, updatedAt: now() };
29499
+ this.s.store.put("archive", a.id, next);
29500
+ return next;
29501
+ }
29502
+ async collect(a, deleteBranch, guard) {
29503
+ if (a.phase === "collected" && (!deleteBranch || a.branchDeleted)) return a;
29504
+ if (deleteBranch && a.delivery.disposition !== "merged" && a.delivery.disposition !== "abandoned")
29505
+ refuse("Published PR branches are retained until merged or explicitly abandoned");
29506
+ try {
29507
+ await this.verifyArchive(a);
29508
+ const t = this.s.task(a.taskId), reasons = this.groupReasons(t);
29509
+ if (reasons.length) refuse(reasons.join("; "));
29510
+ await this.checkReleased(this.group(t));
29511
+ const w = a.worktree;
29512
+ const common = realpathSync3(
29513
+ await git(w.repositoryRoot, ["rev-parse", "--path-format=absolute", "--git-common-dir"])
29514
+ );
29515
+ if (common !== w.commonDir) refuse("Source repository identity changed");
29516
+ if (existsSync3(w.path)) {
29517
+ await this.checkDelivery(w, a.delivery);
29518
+ for (const f of a.files)
29519
+ if (digest(f.source) !== f.digest) refuse("Live evidence changed after archival");
29520
+ guard();
29521
+ if (this.groupReasons(t).length) refuse("Checkout consumers changed before collection");
29522
+ a = this.saveArchive(a, { phase: "removing", error: void 0 });
29523
+ await git(w.repositoryRoot, ["worktree", "remove", "--", w.path]);
29524
+ }
29525
+ const listing = await git(w.repositoryRoot, ["worktree", "list", "--porcelain", "-z"]);
29526
+ if (common !== w.commonDir || listing.split("\0").includes(`worktree ${w.path}`) || existsSync3(w.path))
29527
+ refuse("Worktree removal is incomplete; inspect its registration");
29528
+ a = this.saveArchive(a, { phase: "worktree-removed", error: void 0 });
29529
+ if (deleteBranch) {
29530
+ const refs = await git(w.repositoryRoot, [
29531
+ "for-each-ref",
29532
+ "--format=%(refname) %(objectname)",
29533
+ `refs/heads/${w.branch}`
29534
+ ]);
29535
+ if (refs) {
29536
+ if (refs !== `refs/heads/${w.branch} ${a.delivery.head}`)
29537
+ refuse("Branch moved after archival; preserving it");
29538
+ if (listing.split("\0").includes(`branch refs/heads/${w.branch}`))
29539
+ refuse("Another checkout still uses the branch");
29540
+ if (a.delivery.disposition === "abandoned") {
29541
+ guard();
29542
+ await git(w.repositoryRoot, [
29543
+ "update-ref",
29544
+ "-d",
29545
+ `refs/heads/${w.branch}`,
29546
+ a.delivery.head
29547
+ ]);
29548
+ } else {
29549
+ const target = await git(w.repositoryRoot, [
29550
+ "rev-parse",
29551
+ "--verify",
29552
+ "--end-of-options",
29553
+ `${a.delivery.targetRef}^{commit}`
29554
+ ]);
29555
+ await git(w.repositoryRoot, ["merge-base", "--is-ancestor", a.delivery.head, target]);
29556
+ guard();
29557
+ await git(w.repositoryRoot, [
29558
+ "update-ref",
29559
+ "-d",
29560
+ `refs/heads/${w.branch}`,
29561
+ a.delivery.head
29562
+ ]);
29563
+ }
29564
+ }
29565
+ }
29566
+ a = this.saveArchive(a, {
29567
+ phase: "collected",
29568
+ branchDeleted: deleteBranch,
29569
+ error: void 0
29570
+ });
29571
+ this.s.store.event(
29572
+ a.projectId,
29573
+ "cleanup.collected",
29574
+ "Collected delivered worktree; archive retained",
29575
+ a.taskId,
29576
+ { archiveId: a.id, branchDeleted: deleteBranch }
29577
+ );
29578
+ return a;
29579
+ } catch (e) {
29580
+ this.saveArchive(a, { error: String(e) });
29581
+ throw e;
29582
+ }
29583
+ }
29584
+ async invoke(action, raw) {
29585
+ if (action === "cleanup.preview") return this.preview(external_exports.string().parse(raw.taskId));
29586
+ const c = this.s.guard(raw.lease), reason = external_exports.string().min(1).max(4e3).parse(raw.reason);
29587
+ const guard = () => {
29588
+ this.s.guard(raw.lease);
29589
+ };
29590
+ if (action === "cleanup.configure") {
29591
+ const p = cleanupPolicySchema.parse(raw.policy);
29592
+ this.s.store.put("cleanup-policy", c.projectId, p);
29593
+ this.s.store.event(c.projectId, "cleanup.policy", `${c.owner}: ${reason}`, void 0, p);
29594
+ return p;
29595
+ }
29596
+ const t = this.s.task(external_exports.string().parse(raw.taskId));
29597
+ if (t.projectId !== c.projectId) refuse("Task and lead belong to different projects");
29598
+ if (action === "cleanup.release" || action === "cleanup.reconcile") {
29599
+ const r = this.s.store.get("run", external_exports.string().parse(raw.runId ?? t.runId));
29600
+ if (!r || r.taskId !== t.id) refuse("Run does not belong to this task");
29601
+ return this.locked([t], async () => {
29602
+ if (action === "cleanup.release") return this.release(t, r, reason, false, guard);
29603
+ if (r.cleanup?.state !== "uncertain")
29604
+ refuse("Only uncertain cleanup needs reconciliation");
29605
+ const resolution = external_exports.enum(["closed", "not-closed"]).parse(raw.resolution);
29606
+ if (resolution === "closed") {
29607
+ if (await this.absent(t, r)) {
29608
+ guard();
29609
+ this.saveRun(r, { ...r.cleanup, state: "closed", reason, error: void 0 });
29610
+ return this.s.store.get("run", r.id).cleanup;
29611
+ }
29612
+ refuse("Original tab is still present");
29613
+ }
29614
+ await this.pinned(t, r);
29615
+ guard();
29616
+ this.saveRun(r, { ...r.cleanup, state: "retained", reason, error: void 0 });
29617
+ return this.s.store.get("run", r.id).cleanup;
29618
+ });
29619
+ }
29620
+ if (!t.worktree) refuse("Task has no managed worktree");
29621
+ return this.locked(this.group(t), async () => {
29622
+ if (action === "cleanup.deliver") {
29623
+ const archived = this.archive(t);
29624
+ const disposition = external_exports.enum(["merged", "published", "abandoned"]).parse(raw.disposition);
29625
+ if (archived && archived.taskId !== t.id)
29626
+ refuse("Update delivery on the task that owns the archive");
29627
+ const head = archived && !existsSync3(t.worktree.path) ? await git(t.worktree.repositoryRoot, [
29628
+ "rev-parse",
29629
+ "--verify",
29630
+ "--end-of-options",
29631
+ `refs/heads/${t.worktree.branch}^{commit}`
29632
+ ]) : await this.clean(t.worktree);
29633
+ if (archived && (head !== archived.delivery.head || archived.branchDeleted))
29634
+ refuse("Archived branch changed or was already deleted");
29635
+ let targetRef, targetCommit;
29636
+ if (disposition !== "abandoned") {
29637
+ targetRef = external_exports.string().min(1).parse(raw.targetRef);
29638
+ if (!/^refs\/(heads|remotes)\//.test(targetRef) || targetRef === `refs/heads/${t.worktree.branch}` || disposition === "published" && !targetRef.startsWith("refs/remotes/"))
29639
+ refuse(
29640
+ "Use an explicit target branch ref; published delivery needs a refreshed remote-tracking ref"
29641
+ );
29642
+ await git(t.worktree.repositoryRoot, ["check-ref-format", targetRef]);
29643
+ targetCommit = await git(t.worktree.repositoryRoot, [
29644
+ "rev-parse",
29645
+ "--verify",
29646
+ "--end-of-options",
29647
+ `${targetRef}^{commit}`
29648
+ ]);
29649
+ await git(t.worktree.repositoryRoot, [
29650
+ "merge-base",
29651
+ "--is-ancestor",
29652
+ head,
29653
+ targetCommit
29654
+ ]);
29655
+ }
29656
+ guard();
29657
+ const d = {
29658
+ taskId: t.id,
29659
+ projectId: t.projectId,
29660
+ disposition,
29661
+ head,
29662
+ targetRef,
29663
+ targetCommit,
29664
+ reason,
29665
+ owner: c.owner,
29666
+ createdAt: now()
29667
+ };
29668
+ const previous = this.s.store.get("delivery", t.id);
29669
+ if (previous && JSON.stringify({ ...previous, createdAt: "" }) === JSON.stringify({ ...d, createdAt: "" }))
29670
+ return previous;
29671
+ this.s.store.put("delivery", t.id, d);
29672
+ if (archived) this.saveArchive(archived, { delivery: d, error: void 0 });
29673
+ this.s.store.event(t.projectId, "cleanup.delivered", `${disposition}: ${reason}`, t.id, d);
29674
+ return d;
29675
+ }
29676
+ if (action === "cleanup.archive") return this.createArchive(t, guard);
29677
+ if (action === "cleanup.collect") {
29678
+ const a = this.archive(t);
29679
+ if (!a || a.id !== raw.archiveId)
29680
+ refuse("Preview and supply the current archiveId before collection");
29681
+ return this.collect(a, external_exports.boolean().parse(raw.deleteBranch ?? false), guard);
29682
+ }
29683
+ throw new AppError("unknown_action", `Unknown action: ${action}`, 404);
29684
+ });
29685
+ }
29686
+ };
29687
+
28916
29688
  // src/model-catalog.ts
28917
29689
  import { spawn as spawn2 } from "node:child_process";
28918
29690
 
@@ -28973,7 +29745,7 @@ var strategySchema = external_exports.object({
28973
29745
 
28974
29746
  // src/model-catalog.ts
28975
29747
  async function metadata(binary, args2, cwd, kind) {
28976
- return new Promise((resolve12, reject) => {
29748
+ return new Promise((resolve13, reject) => {
28977
29749
  const child = spawn2(binary, args2, { cwd, stdio: ["pipe", "pipe", "pipe"] });
28978
29750
  let buffer = "", bytes = 0, finished = false, nextId = 2;
28979
29751
  const rows = [];
@@ -28983,7 +29755,7 @@ async function metadata(binary, args2, cwd, kind) {
28983
29755
  clearTimeout(timer);
28984
29756
  child.stdin.end();
28985
29757
  child.kill("SIGTERM");
28986
- error ? reject(error) : resolve12(rows);
29758
+ error ? reject(error) : resolve13(rows);
28987
29759
  };
28988
29760
  const timer = setTimeout(
28989
29761
  () => finish(new AppError("catalog_timeout", `${binary} model metadata timed out`)),
@@ -29569,9 +30341,9 @@ async function probeProfile(profile, cwd) {
29569
30341
  }
29570
30342
 
29571
30343
  // src/orchestration.ts
29572
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "node:fs";
29573
- import { dirname as dirname3, resolve as resolve3, relative as relative2 } from "node:path";
29574
- import { randomUUID as randomUUID3 } from "node:crypto";
30344
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "node:fs";
30345
+ import { dirname as dirname5, resolve as resolve5, relative as relative3 } from "node:path";
30346
+ import { randomUUID as randomUUID4 } from "node:crypto";
29575
30347
  var fail = (code, message) => {
29576
30348
  throw new AppError(code, message, 409);
29577
30349
  };
@@ -29590,7 +30362,7 @@ var Orchestration = class {
29590
30362
  id: `legacy-${t.id}`,
29591
30363
  projectId: t.projectId,
29592
30364
  objective: t.prompt,
29593
- scope: t.ownership.map((path) => resolve3(t.worktree?.sourceCwd ?? t.cwd, path)),
30365
+ scope: t.ownership.map((path) => resolve5(t.worktree?.sourceCwd ?? t.cwd, path)),
29594
30366
  category: "software",
29595
30367
  criteria: t.checks.map((check, index) => ({
29596
30368
  id: `check-${index + 1}`,
@@ -29660,7 +30432,7 @@ var Orchestration = class {
29660
30432
  if (!v.passed) return false;
29661
30433
  if (v.check.type !== "file") return true;
29662
30434
  try {
29663
- return !!v.digest && digest(safePath(t.cwd, v.check.path, true)) === v.digest;
30435
+ return !!v.digest && digest(this.s.cleanup.evidencePath(t, v.check.path)) === v.digest;
29664
30436
  } catch {
29665
30437
  return false;
29666
30438
  }
@@ -29701,8 +30473,15 @@ var Orchestration = class {
29701
30473
  const t = this.s.task(taskReference[1]);
29702
30474
  if (t.projectId !== projectId)
29703
30475
  fail("evidence_scope", "Evidence task belongs to another project");
29704
- const path = safePath(t.cwd, taskReference[2], true);
29705
- if (!t.ownership.some((owned) => inside(safePath(t.cwd, owned), path)))
30476
+ const original = resolve5(t.cwd, taskReference[2]);
30477
+ const path = this.s.cleanup.evidencePath(t, taskReference[2]);
30478
+ const archived = !inside(t.cwd, path);
30479
+ if (!t.ownership.some(
30480
+ (owned) => inside(
30481
+ archived ? resolve5(t.cwd, owned) : safePath(t.cwd, owned),
30482
+ archived ? original : path
30483
+ )
30484
+ ))
29706
30485
  fail("evidence_scope", "Task evidence must be inside its ownership");
29707
30486
  return path;
29708
30487
  }
@@ -29737,7 +30516,7 @@ var Orchestration = class {
29737
30516
  }
29738
30517
  revision(o, reason, owner, before, after, taskId, evidence = []) {
29739
30518
  const r = {
29740
- id: randomUUID3(),
30519
+ id: randomUUID4(),
29741
30520
  projectId: o.projectId,
29742
30521
  outcomeId: o.id,
29743
30522
  revision: o.revision,
@@ -29842,6 +30621,9 @@ var Orchestration = class {
29842
30621
  }
29843
30622
  references(projectId, paths) {
29844
30623
  return external_exports.array(text).min(1).parse(paths).map((path) => {
30624
+ const taskReference = /^task:([^:]+):/.exec(path);
30625
+ if (taskReference && this.s.cleanup.active(taskReference[1]))
30626
+ fail("cleanup_busy", "Evidence is being archived; retry after cleanup finishes");
29845
30627
  const value = digest(this.evidencePath(projectId, path));
29846
30628
  if (!value)
29847
30629
  fail(
@@ -29922,6 +30704,7 @@ var Orchestration = class {
29922
30704
  fail("depth_limit", "Outcome delegation depth reached");
29923
30705
  }
29924
30706
  if (task.outcomeId) {
30707
+ this.s.cleanup.assertOutcomeMutable(task.outcomeId);
29925
30708
  const o = this.outcome(task.outcomeId);
29926
30709
  if (o.projectId !== task.projectId)
29927
30710
  fail("outcome_scope", "Outcome belongs to another project");
@@ -29932,7 +30715,7 @@ var Orchestration = class {
29932
30715
  safePath(this.s.project(task.projectId).root, scope),
29933
30716
  task.worktree ? safePath(
29934
30717
  task.worktree.sourceCwd,
29935
- relative2(task.worktree.cwd, safePath(task.cwd, owned))
30718
+ relative3(task.worktree.cwd, safePath(task.cwd, owned))
29936
30719
  ) : safePath(task.cwd, owned)
29937
30720
  )
29938
30721
  ))
@@ -29945,7 +30728,7 @@ var Orchestration = class {
29945
30728
  );
29946
30729
  } else {
29947
30730
  const o = {
29948
- id: randomUUID3(),
30731
+ id: randomUUID4(),
29949
30732
  projectId: task.projectId,
29950
30733
  objective: task.prompt,
29951
30734
  scope: task.ownership.map((p) => safePath(task.cwd, p)),
@@ -30055,6 +30838,7 @@ var Orchestration = class {
30055
30838
  }
30056
30839
  reviseTask(raw, c) {
30057
30840
  const t = this.s.task(text.parse(raw.taskId));
30841
+ this.s.cleanup.assertMutable(t);
30058
30842
  if (t.projectId !== c.projectId || !t.outcomeId)
30059
30843
  fail("project_mismatch", "Task must belong to this lead and an outcome");
30060
30844
  const o = this.outcome(t.outcomeId);
@@ -30062,6 +30846,9 @@ var Orchestration = class {
30062
30846
  if (t.revision !== external_exports.number().int().parse(raw.expectedRevision))
30063
30847
  fail("stale_revision", "Task changed; refresh before revising");
30064
30848
  const patch = planPatchSchema.parse(raw.patch), reason = text.parse(raw.reason);
30849
+ for (const id of patch.dependencies ?? [])
30850
+ if (this.s.cleanup.active(id))
30851
+ fail("cleanup_busy", "Dependency is being cleaned up; retry after it finishes");
30065
30852
  if (t.runId && this.s.store.get("run", t.runId)?.phase !== "stopped" && !["paused", "waiting"].includes(t.status))
30066
30853
  fail("task_active", "Pause and settle the task before revising it");
30067
30854
  if (patch.supersededBy) {
@@ -30070,13 +30857,15 @@ var Orchestration = class {
30070
30857
  fail("supersede_scope", "Replacement must be required work in the same outcome");
30071
30858
  }
30072
30859
  for (const check of patch.checks ?? []) if (check.type === "file") safePath(t.cwd, check.path);
30860
+ const released = t.runId && this.s.store.get("run", t.runId)?.cleanup?.state === "closed";
30073
30861
  const updated = {
30074
30862
  ...t,
30075
30863
  ...patch,
30076
30864
  revision: t.revision + 1,
30077
30865
  receipt: void 0,
30078
30866
  verification: void 0,
30079
- status: patch.supersededBy ? "cancelled" : t.runId ? "paused" : "queued",
30867
+ runId: released ? void 0 : t.runId,
30868
+ status: patch.supersededBy ? "cancelled" : t.runId && !released ? "paused" : "queued",
30080
30869
  updatedAt: now()
30081
30870
  };
30082
30871
  this.validateGraph(
@@ -30121,7 +30910,7 @@ var Orchestration = class {
30121
30910
  }
30122
30911
  if (raw.action === "finding") {
30123
30912
  const finding = {
30124
- id: randomUUID3(),
30913
+ id: randomUUID4(),
30125
30914
  projectId: t.projectId,
30126
30915
  outcomeId: t.outcomeId,
30127
30916
  taskId: t.id,
@@ -30217,9 +31006,9 @@ var Orchestration = class {
30217
31006
  "profile_changed",
30218
31007
  "Profile changed during validation; validate the current configuration"
30219
31008
  );
30220
- const path = resolve3(dirname3(this.s.store.path), "profile-evidence", randomUUID3() + ".json");
30221
- mkdirSync3(dirname3(path), { recursive: true, mode: 448 });
30222
- writeFileSync2(path, result.output, { mode: 384 });
31009
+ const path = resolve5(dirname5(this.s.store.path), "profile-evidence", randomUUID4() + ".json");
31010
+ mkdirSync5(dirname5(path), { recursive: true, mode: 448 });
31011
+ writeFileSync3(path, result.output, { mode: 384 });
30223
31012
  const updated = {
30224
31013
  ...profile,
30225
31014
  availability,
@@ -30244,7 +31033,7 @@ var Orchestration = class {
30244
31033
  const { key, ...fields } = i;
30245
31034
  const o2 = {
30246
31035
  ...fields,
30247
- id: randomUUID3(),
31036
+ id: randomUUID4(),
30248
31037
  leadOwner: c.owner,
30249
31038
  revision: 1,
30250
31039
  status: "open",
@@ -30304,6 +31093,7 @@ var Orchestration = class {
30304
31093
  }
30305
31094
  if (action.startsWith("strategy.")) return this.strategy(action, raw, c);
30306
31095
  const o = this.outcome(text.parse(raw.outcomeId));
31096
+ this.s.cleanup.assertOutcomeMutable(o.id);
30307
31097
  if (o.projectId !== c.projectId) fail("project_mismatch", "Outcome and lead differ");
30308
31098
  this.checkRevision(o, raw.expectedRevision);
30309
31099
  if (action === "outcome.revise") {
@@ -30372,6 +31162,8 @@ var Orchestration = class {
30372
31162
  });
30373
31163
  }
30374
31164
  strategy(action, raw, c) {
31165
+ const cleanupOutcome = raw.strategy?.outcomeId ?? raw.outcomeId ?? (raw.strategyId ? this.s.store.get("strategy", raw.strategyId)?.outcomeId : void 0);
31166
+ if (cleanupOutcome) this.s.cleanup.assertOutcomeMutable(cleanupOutcome);
30375
31167
  if (action === "strategy.create") {
30376
31168
  const i = strategySchema.parse(raw.strategy), o = this.outcome(i.outcomeId);
30377
31169
  if (o.projectId !== c.projectId) fail("project_mismatch", "Strategy and lead differ");
@@ -30382,7 +31174,7 @@ var Orchestration = class {
30382
31174
  fail("quorum", "Quorum exceeds participant count");
30383
31175
  const strategy2 = {
30384
31176
  ...i,
30385
- id: randomUUID3(),
31177
+ id: randomUUID4(),
30386
31178
  projectId: c.projectId,
30387
31179
  revision: 1,
30388
31180
  round: 1,
@@ -30477,15 +31269,15 @@ var Orchestration = class {
30477
31269
 
30478
31270
  // src/herdr.ts
30479
31271
  import net from "node:net";
30480
- import { randomUUID as randomUUID4 } from "node:crypto";
31272
+ import { randomUUID as randomUUID5 } from "node:crypto";
30481
31273
  var Herdr = class {
30482
31274
  constructor(socketPath) {
30483
31275
  this.socketPath = socketPath;
30484
31276
  }
30485
31277
  socketPath;
30486
31278
  call(method, params = {}, timeoutMs = 1e4) {
30487
- return new Promise((resolve12, reject) => {
30488
- const id = randomUUID4();
31279
+ return new Promise((resolve13, reject) => {
31280
+ const id = randomUUID5();
30489
31281
  let buffer = "";
30490
31282
  let settled2 = false;
30491
31283
  const socket = net.createConnection(this.socketPath);
@@ -30494,7 +31286,7 @@ var Herdr = class {
30494
31286
  settled2 = true;
30495
31287
  clearTimeout(timer);
30496
31288
  socket.destroy();
30497
- error ? reject(error) : resolve12(result);
31289
+ error ? reject(error) : resolve13(result);
30498
31290
  };
30499
31291
  const timer = setTimeout(
30500
31292
  () => finish(
@@ -30551,6 +31343,7 @@ var Service = class {
30551
31343
  port;
30552
31344
  orchestration = new Orchestration(this);
30553
31345
  continuation = new Continuation(this);
31346
+ cleanup = new Cleanup(this);
30554
31347
  project(id) {
30555
31348
  const p = this.store.get("project", id);
30556
31349
  if (!p) throw new AppError("not_found", "Project not found", 404);
@@ -30591,6 +31384,7 @@ var Service = class {
30591
31384
  );
30592
31385
  return {
30593
31386
  project,
31387
+ cleanupPolicy: this.cleanup.policy(projectId),
30594
31388
  lead: this.publicLead(projectId),
30595
31389
  ...this.orchestration.board(projectId),
30596
31390
  tasks,
@@ -30613,7 +31407,7 @@ var Service = class {
30613
31407
  const existing = this.store.all("question").find((q2) => q2.taskId === task.id && !q2.answeredAt && q2.text === text2);
30614
31408
  if (existing) return existing;
30615
31409
  const q = {
30616
- id: randomUUID5(),
31410
+ id: randomUUID6(),
30617
31411
  projectId: task.projectId,
30618
31412
  taskId: task.id,
30619
31413
  text: text2,
@@ -30641,6 +31435,7 @@ var Service = class {
30641
31435
  return result;
30642
31436
  }
30643
31437
  async invoke(action, raw = {}) {
31438
+ if (action.startsWith("cleanup.")) return this.cleanup.invoke(action, raw);
30644
31439
  if (/^(lead\.wait|checkpoint\.|usage\.|adapter\.)/.test(action))
30645
31440
  return this.continuation.invoke(action, raw);
30646
31441
  if (/^(outcome\.|plan\.|profile\.|limits\.|strategy\.|board\.)/.test(action))
@@ -30661,8 +31456,8 @@ var Service = class {
30661
31456
  }).parse(raw);
30662
31457
  if (!isAbsolute2(input.root) || !isAbsolute2(input.socketPath))
30663
31458
  throw new AppError("absolute_paths", "root and socketPath must be absolute");
30664
- const root = realpathSync2(input.root);
30665
- if (!statSync2(root).isDirectory())
31459
+ const root = realpathSync4(input.root);
31460
+ if (!statSync4(root).isDirectory())
30666
31461
  throw new AppError("root", "Project root is not a directory");
30667
31462
  const existing = this.store.all("project").find((p2) => p2.socketPath === input.socketPath && p2.workspaceId === input.workspaceId);
30668
31463
  if (existing) {
@@ -30673,11 +31468,13 @@ var Service = class {
30673
31468
  409
30674
31469
  );
30675
31470
  }
30676
- const p = { ...input, root, id: randomUUID5(), createdAt: now() };
31471
+ const p = { ...input, root, id: randomUUID6(), createdAt: now() };
30677
31472
  const h = this.port(p);
30678
31473
  await h.call("ping");
30679
31474
  await h.call("workspace.get", { workspace_id: p.workspaceId });
30680
31475
  return this.store.transaction(() => {
31476
+ for (const task of this.store.all("task"))
31477
+ if (task.worktree && inside(task.worktree.path, root)) this.cleanup.assertMutable(task);
30681
31478
  const concurrent = this.store.all("project").find((v) => v.socketPath === p.socketPath && v.workspaceId === p.workspaceId);
30682
31479
  if (concurrent) {
30683
31480
  if (concurrent.root === root) return concurrent;
@@ -30820,6 +31617,7 @@ var Service = class {
30820
31617
  const c = this.guard(raw.lease), t = this.task(external_exports.string().parse(raw.taskId)), key = external_exports.string().min(1).parse(raw.key);
30821
31618
  if (c.projectId !== t.projectId)
30822
31619
  throw new AppError("project_mismatch", "Task and lease differ");
31620
+ this.cleanup.assertMutable(t);
30823
31621
  const cached = this.store.get("idempotency", t.projectId + ":retry:" + key);
30824
31622
  if (cached)
30825
31623
  return this.store.transaction(
@@ -30855,6 +31653,7 @@ var Service = class {
30855
31653
  }
30856
31654
  this.guard(raw.lease);
30857
31655
  const current = this.task(t.id);
31656
+ this.cleanup.assertMutable(current);
30858
31657
  if (current.runId !== t.runId || !["failed", "cancelled"].includes(current.status))
30859
31658
  throw new AppError(
30860
31659
  "retry_state",
@@ -30968,7 +31767,7 @@ var Service = class {
30968
31767
  case "decision.record": {
30969
31768
  const c = this.guard(raw.lease), text2 = external_exports.string().min(1).parse(raw.text), rationale = external_exports.string().default("").parse(raw.rationale);
30970
31769
  const d = {
30971
- id: randomUUID5(),
31770
+ id: randomUUID6(),
30972
31771
  projectId: c.projectId,
30973
31772
  text: text2,
30974
31773
  rationale,
@@ -31022,12 +31821,15 @@ var Service = class {
31022
31821
  checks: external_exports.array(checkSchema).min(1).optional()
31023
31822
  }).parse(raw);
31024
31823
  return this.store.transaction(() => {
31025
- const t = this.task(i.taskId);
31824
+ let t = this.task(i.taskId);
31825
+ this.cleanup.assertMutable(t);
31026
31826
  if (t.projectId !== c.projectId)
31027
31827
  throw new AppError("project_mismatch", "Task and lease differ");
31028
31828
  return this.idempotent(c.projectId, "control:" + i.key, i, () => {
31029
31829
  if (terminalStates.has(t.status))
31030
31830
  throw new AppError("task_finished", "Task is already finished", 409);
31831
+ if (t.runId && this.store.get("run", t.runId)?.cleanup?.state === "closed")
31832
+ t = this.updateTask(t, { runId: void 0 });
31031
31833
  if (["redirect", "reply"].includes(i.type) && !i.text?.trim())
31032
31834
  throw new AppError("text_required", "Provide the new instructions or answer");
31033
31835
  if (i.type === "keys" && !i.keys)
@@ -31048,7 +31850,7 @@ var Service = class {
31048
31850
  for (const check of i.checks) if (check.type === "file") safePath(t.cwd, check.path);
31049
31851
  }
31050
31852
  const op = {
31051
- id: randomUUID5(),
31853
+ id: randomUUID6(),
31052
31854
  projectId: t.projectId,
31053
31855
  taskId: t.id,
31054
31856
  type: i.type,
@@ -31105,8 +31907,11 @@ var Service = class {
31105
31907
  const a = assignmentSchema.parse(rawAssignment);
31106
31908
  if (c.projectId !== a.projectId)
31107
31909
  throw new AppError("project_mismatch", "Lease does not match assignment");
31108
- const p = this.project(a.projectId), cwd = realpathSync2(a.cwd ?? p.root);
31910
+ const p = this.project(a.projectId), cwd = realpathSync4(a.cwd ?? p.root);
31109
31911
  const parent = a.parentId ? this.task(a.parentId) : void 0;
31912
+ if (parent) this.cleanup.assertMutable(parent);
31913
+ for (const task of this.store.all("task"))
31914
+ if (task.worktree && inside(task.worktree.path, cwd)) this.cleanup.assertMutable(task);
31110
31915
  const inheritedWorktree = parent?.projectId === p.id && parent.worktree?.state === "ready" && parent.cwd === cwd;
31111
31916
  if (!inside(p.root, cwd) && !inheritedWorktree)
31112
31917
  throw new AppError(
@@ -31137,6 +31942,12 @@ var Service = class {
31137
31942
  }
31138
31943
  for (const id of a.dependencies) {
31139
31944
  const d = this.task(id);
31945
+ if (this.cleanup.active(d.id))
31946
+ throw new AppError(
31947
+ "cleanup_busy",
31948
+ "Dependency is being cleaned up; retry after it finishes",
31949
+ 409
31950
+ );
31140
31951
  if (d.projectId !== a.projectId)
31141
31952
  throw new AppError("dependency_scope", "Dependencies must belong to the same project");
31142
31953
  }
@@ -31146,7 +31957,7 @@ var Service = class {
31146
31957
  const task = {
31147
31958
  ...fields,
31148
31959
  cwd,
31149
- id: randomUUID5(),
31960
+ id: randomUUID6(),
31150
31961
  status: a.deferStart ? "paused" : "queued",
31151
31962
  revision: 1,
31152
31963
  attempt: 0,
@@ -31168,6 +31979,7 @@ var Service = class {
31168
31979
  }
31169
31980
  workerGuard(taskId, token, revision) {
31170
31981
  const t = this.task(taskId), r = t.runId ? this.store.get("run", t.runId) : void 0;
31982
+ this.cleanup.assertMutable(t);
31171
31983
  if (!r || hash(token) !== r.tokenHash)
31172
31984
  throw new AppError("worker_auth", "Invalid or obsolete worker token", 401);
31173
31985
  if (terminalStates.has(t.status) || revision !== t.revision || r.phase === "stopped")
@@ -31185,6 +31997,7 @@ var Service = class {
31185
31997
  }).parse(raw);
31186
31998
  return this.store.transaction(() => {
31187
31999
  const t = this.task(taskId), r = t.runId ? this.store.get("run", t.runId) : void 0;
32000
+ this.cleanup.assertMutable(t);
31188
32001
  if (!r || hash(token) !== r.tokenHash)
31189
32002
  throw new AppError("worker_auth", "Invalid or obsolete worker token", 401);
31190
32003
  if (terminalStates.has(t.status) || i.revision !== t.revision)
@@ -31272,39 +32085,39 @@ var strategyInstructions = {
31272
32085
 
31273
32086
  // src/agy-trust.ts
31274
32087
  import {
31275
- existsSync as existsSync2,
31276
- mkdirSync as mkdirSync4,
31277
- openSync,
31278
- closeSync,
31279
- readFileSync as readFileSync4,
31280
- writeFileSync as writeFileSync3,
32088
+ existsSync as existsSync4,
32089
+ mkdirSync as mkdirSync6,
32090
+ openSync as openSync2,
32091
+ closeSync as closeSync2,
32092
+ readFileSync as readFileSync5,
32093
+ writeFileSync as writeFileSync4,
31281
32094
  renameSync,
31282
32095
  unlinkSync,
31283
- statSync as statSync3,
31284
- realpathSync as realpathSync3
32096
+ statSync as statSync5,
32097
+ realpathSync as realpathSync5
31285
32098
  } from "node:fs";
31286
- import { dirname as dirname4, resolve as resolve5 } from "node:path";
32099
+ import { dirname as dirname6, resolve as resolve7 } from "node:path";
31287
32100
  import { homedir as homedir2 } from "node:os";
31288
- import { randomUUID as randomUUID6 } from "node:crypto";
32101
+ import { randomUUID as randomUUID7 } from "node:crypto";
31289
32102
  function agySettingsPath() {
31290
- return resolve5(
31291
- process.env.MARIONETTE_AGY_SETTINGS ?? resolve5(homedir2(), ".gemini/antigravity-cli/settings.json")
32103
+ return resolve7(
32104
+ process.env.MARIONETTE_AGY_SETTINGS ?? resolve7(homedir2(), ".gemini/antigravity-cli/settings.json")
31292
32105
  );
31293
32106
  }
31294
32107
  function trustAgyWorkspace(root, settingsPath = agySettingsPath()) {
31295
- root = realpathSync3(root);
31296
- if (!statSync3(root).isDirectory()) throw new Error("AGY trust requires a directory");
31297
- mkdirSync4(dirname4(settingsPath), { recursive: true, mode: 448 });
32108
+ root = realpathSync5(root);
32109
+ if (!statSync5(root).isDirectory()) throw new Error("AGY trust requires a directory");
32110
+ mkdirSync6(dirname6(settingsPath), { recursive: true, mode: 448 });
31298
32111
  const lock = settingsPath + ".marionette.lock";
31299
32112
  let fd;
31300
32113
  try {
31301
- fd = openSync(lock, "wx", 384);
32114
+ fd = openSync2(lock, "wx", 384);
31302
32115
  } catch {
31303
32116
  throw new Error(`AGY settings are being edited. Retry after checking ${lock}`);
31304
32117
  }
31305
- const temp = settingsPath + "." + randomUUID6() + ".tmp";
32118
+ const temp = settingsPath + "." + randomUUID7() + ".tmp";
31306
32119
  try {
31307
- const before = existsSync2(settingsPath) ? readFileSync4(settingsPath, "utf8") : null;
32120
+ const before = existsSync4(settingsPath) ? readFileSync5(settingsPath, "utf8") : null;
31308
32121
  const settings = before === null ? {} : JSON.parse(before);
31309
32122
  if (!settings || typeof settings !== "object" || Array.isArray(settings))
31310
32123
  throw new Error("AGY settings must be a JSON object");
@@ -31313,118 +32126,23 @@ function trustAgyWorkspace(root, settingsPath = agySettingsPath()) {
31313
32126
  throw new Error("AGY trustedWorkspaces must be a string array");
31314
32127
  if (trusted.includes(root)) return { changed: false, root, settingsPath };
31315
32128
  settings.trustedWorkspaces = [...trusted, root];
31316
- const mode = before === null ? 384 : statSync3(settingsPath).mode & 511;
31317
- writeFileSync3(temp, JSON.stringify(settings, null, 2) + "\n", { flag: "wx", mode });
31318
- const current = existsSync2(settingsPath) ? readFileSync4(settingsPath, "utf8") : null;
32129
+ const mode = before === null ? 384 : statSync5(settingsPath).mode & 511;
32130
+ writeFileSync4(temp, JSON.stringify(settings, null, 2) + "\n", { flag: "wx", mode });
32131
+ const current = existsSync4(settingsPath) ? readFileSync5(settingsPath, "utf8") : null;
31319
32132
  if (current !== before) throw new Error("AGY settings changed during registration; retry");
31320
32133
  renameSync(temp, settingsPath);
31321
32134
  return { changed: true, root, settingsPath };
31322
32135
  } finally {
31323
- if (existsSync2(temp)) unlinkSync(temp);
31324
- closeSync(fd);
32136
+ if (existsSync4(temp)) unlinkSync(temp);
32137
+ closeSync2(fd);
31325
32138
  unlinkSync(lock);
31326
32139
  }
31327
32140
  }
31328
32141
 
31329
32142
  // src/supervisor.ts
31330
- import { randomBytes as randomBytes3, randomUUID as randomUUID7 } from "node:crypto";
31331
- import { existsSync as existsSync4, readFileSync as readFileSync5, realpathSync as realpathSync5 } from "node:fs";
31332
- import { dirname as dirname6, resolve as resolve7 } from "node:path";
31333
-
31334
- // src/worktrees.ts
31335
- import { execFile } from "node:child_process";
31336
- import { promisify } from "node:util";
31337
- import { existsSync as existsSync3, mkdirSync as mkdirSync5, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
31338
- import { dirname as dirname5, relative as relative3, resolve as resolve6 } from "node:path";
31339
- var exec = promisify(execFile);
31340
- async function git(cwd, args2) {
31341
- const env = { ...process.env };
31342
- for (const key of Object.keys(env)) if (key.startsWith("GIT_")) delete env[key];
31343
- env.GIT_TERMINAL_PROMPT = "0";
31344
- try {
31345
- const result = await exec("git", ["-c", "core.hooksPath=/dev/null", "-C", cwd, ...args2], {
31346
- env,
31347
- timeout: 12e4,
31348
- maxBuffer: 4 * 1024 * 1024
31349
- });
31350
- return result.stdout.trimEnd();
31351
- } catch (error) {
31352
- const e = error;
31353
- throw new AppError(
31354
- "worktree_git",
31355
- `Git worktree operation failed: ${e.stderr?.trim() || e.message}`
31356
- );
31357
- }
31358
- }
31359
- async function planWorktree(task, stateRoot) {
31360
- const repositoryRoot = realpathSync4(await git(task.cwd, ["rev-parse", "--show-toplevel"]));
31361
- const commonDir = realpathSync4(
31362
- await git(task.cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"])
31363
- );
31364
- const baseRef = task.execution?.mode === "worktree" ? task.execution.baseRef ?? "HEAD" : "HEAD";
31365
- const baseCommit = await git(task.cwd, [
31366
- "rev-parse",
31367
- "--verify",
31368
- "--end-of-options",
31369
- `${baseRef}^{commit}`
31370
- ]);
31371
- if (!inside(repositoryRoot, task.cwd))
31372
- throw new AppError("worktree_scope", "Task directory is outside its Git repository");
31373
- const path = safePath(stateRoot, `worktrees/${task.projectId}/${task.id}`);
31374
- return {
31375
- state: "planned",
31376
- repositoryRoot,
31377
- commonDir,
31378
- sourceCwd: task.cwd,
31379
- path,
31380
- cwd: resolve6(path, relative3(repositoryRoot, task.cwd)),
31381
- branch: `marionette/${task.id}`,
31382
- baseCommit
31383
- };
31384
- }
31385
- async function validateWorktree(w, requireClean = false) {
31386
- const listing = await git(w.repositoryRoot, ["worktree", "list", "--porcelain", "-z"]);
31387
- const records = listing.split("\0\0").map((record2) => record2.split("\0"));
31388
- const record = records.find((fields) => fields.includes(`worktree ${w.path}`));
31389
- if (!record || !record.includes(`branch refs/heads/${w.branch}`) || record.some((field) => /^(locked|prunable)( |$)/.test(field)) || !existsSync3(w.path) || realpathSync4(w.path) !== w.path)
31390
- throw new AppError(
31391
- "worktree_identity",
31392
- "Managed worktree is missing, incomplete, or has changed identity. Inspect it before retrying; existing work is preserved."
31393
- );
31394
- const commonDir = realpathSync4(
31395
- await git(w.path, ["rev-parse", "--path-format=absolute", "--git-common-dir"])
31396
- );
31397
- const root = realpathSync4(await git(w.path, ["rev-parse", "--show-toplevel"]));
31398
- const branch = await git(w.path, ["symbolic-ref", "HEAD"]);
31399
- if (commonDir !== w.commonDir || root !== w.path || branch !== `refs/heads/${w.branch}`)
31400
- throw new AppError(
31401
- "worktree_identity",
31402
- "Managed worktree repository or branch changed; refusing to reuse it."
31403
- );
31404
- if (requireClean) {
31405
- const head = await git(w.path, ["rev-parse", "HEAD"]);
31406
- const status = await git(w.path, ["status", "--porcelain", "--untracked-files=all"]);
31407
- if (head !== w.baseCommit || status)
31408
- throw new AppError(
31409
- "worktree_incomplete",
31410
- "Worktree creation was interrupted and its checkout is not clean at the pinned base. Inspect it before retrying; no files were reset."
31411
- );
31412
- }
31413
- const cwd = safePath(w.path, relative3(w.path, w.cwd), true);
31414
- if (!statSync4(cwd).isDirectory())
31415
- throw new AppError(
31416
- "worktree_cwd",
31417
- "Task working directory does not exist in the selected Git revision"
31418
- );
31419
- return cwd;
31420
- }
31421
- async function createWorktree(w) {
31422
- mkdirSync5(dirname5(w.path), { recursive: true, mode: 448 });
31423
- await git(w.repositoryRoot, ["worktree", "add", "-b", w.branch, "--", w.path, w.baseCommit]);
31424
- return validateWorktree(w, true);
31425
- }
31426
-
31427
- // src/supervisor.ts
32143
+ import { randomBytes as randomBytes3, randomUUID as randomUUID8 } from "node:crypto";
32144
+ import { existsSync as existsSync5, readFileSync as readFileSync6, realpathSync as realpathSync6 } from "node:fs";
32145
+ import { dirname as dirname7, resolve as resolve8 } from "node:path";
31428
32146
  var settled = (status) => status === "idle" || status === "done";
31429
32147
  var quote = (s) => "'" + s.replaceAll("'", "'\\''") + "'";
31430
32148
  function inputScreen(text2) {
@@ -31450,6 +32168,7 @@ var Supervisor = class {
31450
32168
  start() {
31451
32169
  this.recover();
31452
32170
  this.service.continuation.recover();
32171
+ this.service.cleanup.recover();
31453
32172
  this.timer = setInterval(() => this.tick(), this.pollMs);
31454
32173
  this.tick();
31455
32174
  }
@@ -31457,6 +32176,7 @@ var Supervisor = class {
31457
32176
  this.stopped = true;
31458
32177
  if (this.timer) clearInterval(this.timer);
31459
32178
  await this.service.continuation.stop();
32179
+ await this.service.cleanup.stop();
31460
32180
  while (this.busy.size) await new Promise((r) => setTimeout(r, 50));
31461
32181
  }
31462
32182
  recover() {
@@ -31532,9 +32252,10 @@ var Supervisor = class {
31532
32252
  if (this.stopped) return;
31533
32253
  this.service.orchestration.refreshEvidence();
31534
32254
  this.service.continuation.tick();
32255
+ this.service.cleanup.tick();
31535
32256
  const s = this.service, all = s.store.all("task");
31536
32257
  for (const t of all)
31537
- if (t.runId && t.status !== "queued" && !this.busy.has(t.id) && (!terminalStates.has(t.status) || this.run(t).phase !== "stopped"))
32258
+ if (t.runId && !this.service.cleanup.active(t.id) && !["closing", "closed", "uncertain"].includes(this.run(t).cleanup?.state ?? "") && t.status !== "queued" && !this.busy.has(t.id) && (!terminalStates.has(t.status) || this.run(t).phase !== "stopped"))
31538
32259
  this.launch(t.id, () => this.monitor(t.id));
31539
32260
  for (const t of s.store.all("task").filter(
31540
32261
  (t2) => t2.status === "waiting" || t2.resumePending && t2.status === "blocked" && t2.blockKind === "missing-report"
@@ -31551,7 +32272,9 @@ var Supervisor = class {
31551
32272
  }
31552
32273
  for (const project of s.store.all("project")) {
31553
32274
  const tasks = s.tasks(project.id);
31554
- for (const task of tasks.filter((t) => t.status === "queued" && !this.busy.has(t.id))) {
32275
+ for (const task of tasks.filter(
32276
+ (t) => t.status === "queued" && !t.archiveId && !this.service.cleanup.active(t.id) && !this.busy.has(t.id)
32277
+ )) {
31555
32278
  const dependencies = task.dependencies.map((id) => s.task(id));
31556
32279
  let waitReason = dependencies.some((d) => d.status !== "completed") ? "Waiting for dependencies to complete" : void 0;
31557
32280
  if (!waitReason) waitReason = s.orchestration.capacity(task, (t) => !!this.holds(t));
@@ -31575,10 +32298,10 @@ var Supervisor = class {
31575
32298
  return !terminalStates.has(t.status) || t.runId && this.run(t)?.phase !== "stopped";
31576
32299
  }
31577
32300
  overlap(a, b) {
31578
- const root = (t) => t.worktree?.cwd ?? (t.execution?.mode === "worktree" ? resolve7(dirname6(this.service.store.path), "worktrees", t.projectId, t.id) : t.cwd);
32301
+ const root = (t) => t.worktree?.cwd ?? (t.execution?.mode === "worktree" ? resolve8(dirname7(this.service.store.path), "worktrees", t.projectId, t.id) : t.cwd);
31579
32302
  const path = (t, owned) => {
31580
32303
  const cwd = root(t);
31581
- return t.execution?.mode === "worktree" && !existsSync4(cwd) ? resolve7(cwd, owned) : safePath(cwd, owned);
32304
+ return t.execution?.mode === "worktree" && !existsSync5(cwd) ? resolve8(cwd, owned) : safePath(cwd, owned);
31582
32305
  };
31583
32306
  return a.ownership.some(
31584
32307
  (x) => b.ownership.some((y) => {
@@ -31684,7 +32407,7 @@ Report format: {"revision":${t.revision},"type":"complete","summary":"What chang
31684
32407
  try {
31685
32408
  if (t.execution?.mode === "worktree") {
31686
32409
  if (!t.worktree) {
31687
- const plan = await planWorktree(t, realpathSync5(dirname6(s.store.path)));
32410
+ const plan = await planWorktree(t, realpathSync6(dirname7(s.store.path)));
31688
32411
  if (s.task(id).status !== "preparing") return;
31689
32412
  t = s.updateTask(t, { worktree: plan }, "Planned isolated Git worktree");
31690
32413
  }
@@ -31748,7 +32471,7 @@ ${JSON.stringify(children.map((child) => ({ id: child.id, title: child.title, st
31748
32471
  }
31749
32472
  const token = randomBytes3(32).toString("hex");
31750
32473
  const run = {
31751
- id: randomUUID7(),
32474
+ id: randomUUID8(),
31752
32475
  taskId: id,
31753
32476
  attempt: t.attempt + 1,
31754
32477
  revision: t.revision,
@@ -31808,7 +32531,7 @@ ${JSON.stringify(children.map((child) => ({ id: child.id, title: child.title, st
31808
32531
  break;
31809
32532
  } catch (error) {
31810
32533
  if (/not an available shell/.test(String(error)) && attempt < 10) {
31811
- await new Promise((resolve12) => setTimeout(resolve12, 500));
32534
+ await new Promise((resolve13) => setTimeout(resolve13, 500));
31812
32535
  continue;
31813
32536
  }
31814
32537
  if (["agent_not_ready", "agent_not_found"].includes(error.code)) break;
@@ -31822,7 +32545,7 @@ ${JSON.stringify(children.map((child) => ({ id: child.id, title: child.title, st
31822
32545
  } catch (error) {
31823
32546
  if (!["agent_not_ready", "agent_not_found"].includes(error.code))
31824
32547
  throw error;
31825
- await new Promise((resolve12) => setTimeout(resolve12, 500));
32548
+ await new Promise((resolve13) => setTimeout(resolve13, 500));
31826
32549
  continue;
31827
32550
  }
31828
32551
  const read = await h.call("pane.read", {
@@ -31838,7 +32561,7 @@ ${JSON.stringify(children.map((child) => ({ id: child.id, title: child.title, st
31838
32561
  if (a.agent === run.kind && settled(a.agent_status) && !a.launch_pending && a.interactive_ready !== false)
31839
32562
  break;
31840
32563
  a = void 0;
31841
- await new Promise((resolve12) => setTimeout(resolve12, 500));
32564
+ await new Promise((resolve13) => setTimeout(resolve13, 500));
31842
32565
  }
31843
32566
  if (!a) throw new AppError("startup_timeout", "Agent did not become ready within 30 seconds");
31844
32567
  run.nativeSession = a.agent_session?.value;
@@ -32171,7 +32894,7 @@ Ownership remains ${t.ownership.join(", ")}. Current acceptance checks: ${JSON.s
32171
32894
  if (!fileDigest) throw new Error("Artifact is not a regular file");
32172
32895
  if (!check.allowUnchanged && fileDigest === r.baseline[check.path])
32173
32896
  throw new Error("Artifact is unchanged from before dispatch");
32174
- if (check.contains !== void 0 && !readFileSync5(path, "utf8").includes(check.contains))
32897
+ if (check.contains !== void 0 && !readFileSync6(path, "utf8").includes(check.contains))
32175
32898
  throw new Error("Artifact does not contain the expected content");
32176
32899
  if (check.sha256 && check.sha256 !== fileDigest)
32177
32900
  throw new Error("Artifact SHA-256 does not match");
@@ -32211,14 +32934,14 @@ ${result.output}`;
32211
32934
  };
32212
32935
 
32213
32936
  // src/version.ts
32214
- var VERSION = "0.2.0";
32937
+ var VERSION = "0.2.1";
32215
32938
  var SETUP_VERSION = 2;
32216
32939
 
32217
32940
  // src/server.ts
32218
32941
  async function serve(home2, port) {
32219
- const config = initConfig(home2, port), lock = resolve8(home2, "supervisor.lock"), lockId = randomUUID8();
32220
- if (existsSync5(lock)) {
32221
- const old = JSON.parse(readFileSync6(lock, "utf8"));
32942
+ const config = initConfig(home2, port), lock = resolve9(home2, "supervisor.lock"), lockId = randomUUID9();
32943
+ if (existsSync6(lock)) {
32944
+ const old = JSON.parse(readFileSync7(lock, "utf8"));
32222
32945
  try {
32223
32946
  process.kill(old.pid, 0);
32224
32947
  throw new AppError(
@@ -32231,13 +32954,13 @@ async function serve(home2, port) {
32231
32954
  unlinkSync2(lock);
32232
32955
  }
32233
32956
  }
32234
- writeFileSync4(lock, JSON.stringify({ pid: process.pid, id: lockId }), {
32957
+ writeFileSync5(lock, JSON.stringify({ pid: process.pid, id: lockId }), {
32235
32958
  flag: "wx",
32236
32959
  mode: 384
32237
32960
  });
32238
- const store = new Store(resolve8(home2, "state.sqlite")), service = new Service(store);
32239
- const projectRoot = resolve8(dirname7(fileURLToPath(import.meta.url)), "..");
32240
- const cliPath = resolve8(projectRoot, "dist/cli.js");
32961
+ const store = new Store(resolve9(home2, "state.sqlite")), service = new Service(store);
32962
+ const projectRoot = resolve9(dirname8(fileURLToPath(import.meta.url)), "..");
32963
+ const cliPath = resolve9(projectRoot, "dist/cli.js");
32241
32964
  const url = `http://127.0.0.1:${config.port}`;
32242
32965
  const supervisor = new Supervisor(service, url, cliPath);
32243
32966
  const app = (0, import_express.default)();
@@ -32312,7 +33035,7 @@ async function serve(home2, port) {
32312
33035
  res.json({ stopping: true });
32313
33036
  setTimeout(() => void shutdown(), 20);
32314
33037
  });
32315
- app.use(import_express.default.static(resolve8(projectRoot, "public"), { index: "index.html", maxAge: 0 }));
33038
+ app.use(import_express.default.static(resolve9(projectRoot, "public"), { index: "index.html", maxAge: 0 }));
32316
33039
  app.use(
32317
33040
  (error, req, res, _next) => {
32318
33041
  const status = error instanceof AppError ? error.status : error instanceof ZodError ? 400 : 500;
@@ -32342,7 +33065,7 @@ async function serve(home2, port) {
32342
33065
  server.closeIdleConnections();
32343
33066
  });
32344
33067
  store.close();
32345
- if (existsSync5(lock) && JSON.parse(readFileSync6(lock, "utf8")).id === lockId) unlinkSync2(lock);
33068
+ if (existsSync6(lock) && JSON.parse(readFileSync7(lock, "utf8")).id === lockId) unlinkSync2(lock);
32346
33069
  }
32347
33070
  process.once("SIGTERM", () => void shutdown());
32348
33071
  process.once("SIGINT", () => void shutdown());
@@ -32354,51 +33077,51 @@ async function serve(home2, port) {
32354
33077
  }
32355
33078
 
32356
33079
  // src/runtime.ts
32357
- import { createHash as createHash2, randomUUID as randomUUID9 } from "node:crypto";
33080
+ import { createHash as createHash3, randomUUID as randomUUID10 } from "node:crypto";
32358
33081
  import {
32359
33082
  cpSync,
32360
- existsSync as existsSync6,
32361
- mkdirSync as mkdirSync6,
32362
- readFileSync as readFileSync7,
33083
+ existsSync as existsSync7,
33084
+ mkdirSync as mkdirSync7,
33085
+ readFileSync as readFileSync8,
32363
33086
  readdirSync,
32364
33087
  renameSync as renameSync2,
32365
33088
  rmSync,
32366
- writeFileSync as writeFileSync5
33089
+ writeFileSync as writeFileSync6
32367
33090
  } from "node:fs";
32368
- import { dirname as dirname8, resolve as resolve9, relative as relative4 } from "node:path";
33091
+ import { dirname as dirname9, resolve as resolve10, relative as relative4 } from "node:path";
32369
33092
  import { fileURLToPath as fileURLToPath2 } from "node:url";
32370
- var packageRoot = resolve9(dirname8(fileURLToPath2(import.meta.url)), "..");
33093
+ var packageRoot = resolve10(dirname9(fileURLToPath2(import.meta.url)), "..");
32371
33094
  function installRuntime(home2, source = packageRoot) {
32372
33095
  const files = ["dist/cli.js", "dist/mcp.js", "package.json"];
32373
- if (existsSync6(resolve9(source, "THIRD_PARTY_NOTICES.md"))) files.push("THIRD_PARTY_NOTICES.md");
33096
+ if (existsSync7(resolve10(source, "THIRD_PARTY_NOTICES.md"))) files.push("THIRD_PARTY_NOTICES.md");
32374
33097
  function walk(dir) {
32375
- for (const entry of readdirSync(resolve9(source, dir), { withFileTypes: true })) {
33098
+ for (const entry of readdirSync(resolve10(source, dir), { withFileTypes: true })) {
32376
33099
  const path = dir + "/" + entry.name;
32377
33100
  entry.isDirectory() ? walk(path) : files.push(path);
32378
33101
  }
32379
33102
  }
32380
- if (!existsSync6(resolve9(source, "dist/cli.js")) || !existsSync6(resolve9(source, "public/index.html")))
33103
+ if (!existsSync7(resolve10(source, "dist/cli.js")) || !existsSync7(resolve10(source, "public/index.html")))
32381
33104
  throw new Error("Build Marionette first with npm run build");
32382
33105
  walk("public");
32383
- const digest2 = createHash2("sha256");
32384
- for (const file of files.sort()) digest2.update(file).update(readFileSync7(resolve9(source, file)));
32385
- const version = JSON.parse(readFileSync7(resolve9(source, "package.json"), "utf8")).version;
32386
- const target = resolve9(home2, "runtimes", version + "-" + digest2.digest("hex").slice(0, 16));
33106
+ const digest2 = createHash3("sha256");
33107
+ for (const file of files.sort()) digest2.update(file).update(readFileSync8(resolve10(source, file)));
33108
+ const version = JSON.parse(readFileSync8(resolve10(source, "package.json"), "utf8")).version;
33109
+ const target = resolve10(home2, "runtimes", version + "-" + digest2.digest("hex").slice(0, 16));
32387
33110
  if (relative4(source, target) === "") return target;
32388
- if (existsSync6(resolve9(target, ".complete"))) return target;
32389
- mkdirSync6(dirname8(target), { recursive: true, mode: 448 });
32390
- const temp = target + "." + randomUUID9();
32391
- mkdirSync6(temp, { mode: 448 });
33111
+ if (existsSync7(resolve10(target, ".complete"))) return target;
33112
+ mkdirSync7(dirname9(target), { recursive: true, mode: 448 });
33113
+ const temp = target + "." + randomUUID10();
33114
+ mkdirSync7(temp, { mode: 448 });
32392
33115
  try {
32393
33116
  for (const file of files) {
32394
- mkdirSync6(dirname8(resolve9(temp, file)), { recursive: true });
32395
- cpSync(resolve9(source, file), resolve9(temp, file));
33117
+ mkdirSync7(dirname9(resolve10(temp, file)), { recursive: true });
33118
+ cpSync(resolve10(source, file), resolve10(temp, file));
32396
33119
  }
32397
- writeFileSync5(resolve9(temp, ".complete"), "1\n");
33120
+ writeFileSync6(resolve10(temp, ".complete"), "1\n");
32398
33121
  try {
32399
33122
  renameSync2(temp, target);
32400
33123
  } catch (e) {
32401
- if (!existsSync6(resolve9(target, ".complete"))) throw e;
33124
+ if (!existsSync7(resolve10(target, ".complete"))) throw e;
32402
33125
  }
32403
33126
  } finally {
32404
33127
  rmSync(temp, { recursive: true, force: true });
@@ -32408,20 +33131,20 @@ function installRuntime(home2, source = packageRoot) {
32408
33131
 
32409
33132
  // src/setup.ts
32410
33133
  import {
32411
- existsSync as existsSync7,
32412
- mkdirSync as mkdirSync7,
32413
- readFileSync as readFileSync8,
32414
- writeFileSync as writeFileSync6,
33134
+ existsSync as existsSync8,
33135
+ mkdirSync as mkdirSync8,
33136
+ readFileSync as readFileSync9,
33137
+ writeFileSync as writeFileSync7,
32415
33138
  renameSync as renameSync3,
32416
33139
  unlinkSync as unlinkSync3,
32417
- openSync as openSync2,
32418
- closeSync as closeSync2,
32419
- realpathSync as realpathSync6,
32420
- statSync as statSync5
33140
+ openSync as openSync3,
33141
+ closeSync as closeSync3,
33142
+ realpathSync as realpathSync7,
33143
+ statSync as statSync6
32421
33144
  } from "node:fs";
32422
- import { basename, dirname as dirname9, resolve as resolve10 } from "node:path";
33145
+ import { basename, dirname as dirname10, resolve as resolve11 } from "node:path";
32423
33146
  import { homedir as homedir3 } from "node:os";
32424
- import { createHash as createHash3, randomUUID as randomUUID10 } from "node:crypto";
33147
+ import { createHash as createHash4, randomUUID as randomUUID11 } from "node:crypto";
32425
33148
  import { createInterface } from "node:readline/promises";
32426
33149
  import { execFile as execFile2, spawn as spawn3 } from "node:child_process";
32427
33150
  import { promisify as promisify2 } from "node:util";
@@ -32445,22 +33168,22 @@ var setupSchema = external_exports.object({
32445
33168
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
32446
33169
  var quote2 = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
32447
33170
  function privateJson(path, data) {
32448
- mkdirSync7(dirname9(path), { recursive: true, mode: 448 });
32449
- const temp = path + "." + randomUUID10() + ".tmp";
33171
+ mkdirSync8(dirname10(path), { recursive: true, mode: 448 });
33172
+ const temp = path + "." + randomUUID11() + ".tmp";
32450
33173
  try {
32451
- writeFileSync6(temp, JSON.stringify(data, null, 2) + "\n", { mode: 384, flag: "wx" });
33174
+ writeFileSync7(temp, JSON.stringify(data, null, 2) + "\n", { mode: 384, flag: "wx" });
32452
33175
  renameSync3(temp, path);
32453
33176
  } finally {
32454
- if (existsSync7(temp)) unlinkSync3(temp);
33177
+ if (existsSync8(temp)) unlinkSync3(temp);
32455
33178
  }
32456
33179
  }
32457
33180
  function setupPlan(input) {
32458
33181
  const supplied = setupSchema.partial().parse(input);
32459
- const root = realpathSync6(resolve10(supplied.project ?? process.cwd()));
32460
- if (!statSync5(root).isDirectory()) throw new Error("Project must be an existing directory");
32461
- const hash2 = createHash3("sha256").update(root).digest("hex").slice(0, 10);
32462
- const bindingPath = resolve10(root, ".marionette/project.json");
32463
- const binding = existsSync7(bindingPath) ? JSON.parse(readFileSync8(bindingPath, "utf8")) : null;
33182
+ const root = realpathSync7(resolve11(supplied.project ?? process.cwd()));
33183
+ if (!statSync6(root).isDirectory()) throw new Error("Project must be an existing directory");
33184
+ const hash2 = createHash4("sha256").update(root).digest("hex").slice(0, 10);
33185
+ const bindingPath = resolve11(root, ".marionette/project.json");
33186
+ const binding = existsSync8(bindingPath) ? JSON.parse(readFileSync9(bindingPath, "utf8")) : null;
32464
33187
  const options = setupSchema.parse({
32465
33188
  lead: binding?.lead,
32466
33189
  leadName: binding?.leadName,
@@ -32469,13 +33192,13 @@ function setupPlan(input) {
32469
33192
  mcp: binding?.mcp,
32470
33193
  ...supplied
32471
33194
  });
32472
- const legacy = resolve10(root, ".marionette");
32473
- const home2 = resolve10(
32474
- options.home ?? process.env.MARIONETTE_HOME ?? binding?.home ?? (existsSync7(resolve10(legacy, "config.json")) ? legacy : resolve10(process.env.XDG_DATA_HOME ?? resolve10(homedir3(), ".local/share"), "marionette"))
33195
+ const legacy = resolve11(root, ".marionette");
33196
+ const home2 = resolve11(
33197
+ options.home ?? process.env.MARIONETTE_HOME ?? binding?.home ?? (existsSync8(resolve11(legacy, "config.json")) ? legacy : resolve11(process.env.XDG_DATA_HOME ?? resolve11(homedir3(), ".local/share"), "marionette"))
32475
33198
  );
32476
33199
  const session = options.session ?? binding?.session ?? `marionette-${hash2}`;
32477
- const socket = resolve10(
32478
- options.socket ?? binding?.socket ?? resolve10(homedir3(), ".config/herdr/sessions", session, "herdr.sock")
33200
+ const socket = resolve11(
33201
+ options.socket ?? binding?.socket ?? resolve11(homedir3(), ".config/herdr/sessions", session, "herdr.sock")
32479
33202
  );
32480
33203
  return {
32481
33204
  ...options,
@@ -32546,11 +33269,11 @@ async function ensureHerdr(plan) {
32546
33269
  return h;
32547
33270
  } catch {
32548
33271
  }
32549
- if (plan.socket !== resolve10(homedir3(), ".config/herdr/sessions", plan.session, "herdr.sock"))
33272
+ if (plan.socket !== resolve11(homedir3(), ".config/herdr/sessions", plan.session, "herdr.sock"))
32550
33273
  throw new Error(
32551
33274
  `Cannot connect to supplied socket ${plan.socket}; start its Herdr session first`
32552
33275
  );
32553
- const log = openSync2(resolve10(plan.home, "herdr.log"), "a", 384);
33276
+ const log = openSync3(resolve11(plan.home, "herdr.log"), "a", 384);
32554
33277
  const child = spawn3("herdr", ["--session", plan.session, "server"], {
32555
33278
  detached: true,
32556
33279
  stdio: ["ignore", log, log]
@@ -32559,7 +33282,7 @@ async function ensureHerdr(plan) {
32559
33282
  child.on("error", (e) => {
32560
33283
  error = e;
32561
33284
  });
32562
- closeSync2(log);
33285
+ closeSync3(log);
32563
33286
  child.unref();
32564
33287
  for (let n = 0; n < 100; n++) {
32565
33288
  if (error) throw error;
@@ -32570,14 +33293,14 @@ async function ensureHerdr(plan) {
32570
33293
  }
32571
33294
  await sleep(100);
32572
33295
  }
32573
- throw new Error(`Herdr session did not start; inspect ${resolve10(plan.home, "herdr.log")}`);
33296
+ throw new Error(`Herdr session did not start; inspect ${resolve11(plan.home, "herdr.log")}`);
32574
33297
  }
32575
33298
  function mcpCommand(agent, name, runtime, home2) {
32576
33299
  const binary = agent === "codex-desktop" ? "codex" : agent;
32577
33300
  const serverArgs = [
32578
33301
  process.execPath,
32579
33302
  "--no-warnings",
32580
- resolve10(runtime, "dist/mcp.js"),
33303
+ resolve11(runtime, "dist/mcp.js"),
32581
33304
  "--home",
32582
33305
  home2
32583
33306
  ];
@@ -32590,7 +33313,7 @@ function mcpCommand(agent, name, runtime, home2) {
32590
33313
  };
32591
33314
  }
32592
33315
  async function installMcp(command2, name, home2) {
32593
- const receipt = resolve10(home2, "clients", command2.binary + ".json");
33316
+ const receipt = resolve11(home2, "clients", command2.binary + ".json");
32594
33317
  let found;
32595
33318
  try {
32596
33319
  const output = (await exec2(
@@ -32605,14 +33328,14 @@ async function installMcp(command2, name, home2) {
32605
33328
  }
32606
33329
  if (found) {
32607
33330
  if (found.includes(command2.server.args[1]) && found.includes(home2)) return "already-configured";
32608
- const old = existsSync7(receipt) ? JSON.parse(readFileSync8(receipt, "utf8")) : null;
33331
+ const old = existsSync8(receipt) ? JSON.parse(readFileSync9(receipt, "utf8")) : null;
32609
33332
  if (!old || old.name !== name || !found.includes(old.runtime) || !found.includes(home2))
32610
33333
  throw new Error(
32611
33334
  `MCP server ${name} already has another configuration. Use --mcp print to review it.`
32612
33335
  );
32613
33336
  }
32614
33337
  await exec2(command2.binary, command2.args, { timeout: 3e4 });
32615
- privateJson(receipt, { name, runtime: dirname9(dirname9(command2.server.args[1])) });
33338
+ privateJson(receipt, { name, runtime: dirname10(dirname10(command2.server.args[1])) });
32616
33339
  return "installed";
32617
33340
  }
32618
33341
  function leadPrompt(projectId, leadName, leasePath) {
@@ -32631,18 +33354,18 @@ async function runSetup(input) {
32631
33354
  }).catch(() => {
32632
33355
  throw new Error(`Install the selected lead CLI first, or use --mcp print.`);
32633
33356
  });
32634
- mkdirSync7(p.home, { recursive: true, mode: 448 });
32635
- const lockPath = resolve10(p.home, "setup.lock");
33357
+ mkdirSync8(p.home, { recursive: true, mode: 448 });
33358
+ const lockPath = resolve11(p.home, "setup.lock");
32636
33359
  let lock;
32637
33360
  try {
32638
- lock = openSync2(lockPath, "wx", 384);
32639
- writeFileSync6(lock, JSON.stringify({ pid: process.pid }));
33361
+ lock = openSync3(lockPath, "wx", 384);
33362
+ writeFileSync7(lock, JSON.stringify({ pid: process.pid }));
32640
33363
  } catch {
32641
33364
  throw new Error(`Another setup may be running. Check ${lockPath} before retrying.`);
32642
33365
  }
32643
33366
  try {
32644
33367
  const runtime = installRuntime(p.home);
32645
- if (!existsSync7(resolve10(p.home, "config.json")))
33368
+ if (!existsSync8(resolve11(p.home, "config.json")))
32646
33369
  initConfig(p.home, await availablePort(p.port));
32647
33370
  const config = loadConfig(p.home);
32648
33371
  if (p.port && config.port !== p.port)
@@ -32651,7 +33374,7 @@ async function runSetup(input) {
32651
33374
  );
32652
33375
  await exec2(
32653
33376
  process.execPath,
32654
- ["--no-warnings", resolve10(runtime, "dist/cli.js"), "start", "--home", p.home],
33377
+ ["--no-warnings", resolve11(runtime, "dist/cli.js"), "start", "--home", p.home],
32655
33378
  { timeout: 15e3 }
32656
33379
  );
32657
33380
  const health = await (await fetch(`http://127.0.0.1:${config.port}/health`, { signal: AbortSignal.timeout(3e3) })).json();
@@ -32672,9 +33395,9 @@ async function runSetup(input) {
32672
33395
  }
32673
33396
  await h.call("workspace.get", { workspace_id: workspaceId });
32674
33397
  privateJson(
32675
- resolve10(
33398
+ resolve11(
32676
33399
  p.home,
32677
- "setup-project-" + createHash3("sha256").update(p.root).digest("hex").slice(0, 10) + ".json"
33400
+ "setup-project-" + createHash4("sha256").update(p.root).digest("hex").slice(0, 10) + ".json"
32678
33401
  ),
32679
33402
  { root: p.root, session: p.session, socket: p.socket, workspace: workspaceId }
32680
33403
  );
@@ -32686,8 +33409,8 @@ async function runSetup(input) {
32686
33409
  workspaceId,
32687
33410
  trustAgyWorkspaces: p.trustAgy
32688
33411
  });
32689
- const leasePath = resolve10(p.home, "leads", project.id + ".json");
32690
- let lease = existsSync7(leasePath) ? credentialsSchema.parse(JSON.parse(readFileSync8(leasePath, "utf8"))) : null;
33412
+ const leasePath = resolve11(p.home, "leads", project.id + ".json");
33413
+ let lease = existsSync8(leasePath) ? credentialsSchema.parse(JSON.parse(readFileSync9(leasePath, "utf8"))) : null;
32691
33414
  const briefing = await call(p.home, "project.briefing", { projectId: project.id });
32692
33415
  const same = briefing.lead?.owner === p.leadName && briefing.lead?.agent === p.lead;
32693
33416
  if (briefing.lead && !(same && lease && lease.epoch === briefing.lead.epoch && lease.owner === briefing.lead.owner) && !p.takeover)
@@ -32724,15 +33447,15 @@ async function runSetup(input) {
32724
33447
  mcp: p.mcp
32725
33448
  };
32726
33449
  privateJson(p.bindingPath, binding);
32727
- const ignore = resolve10(dirname9(p.bindingPath), ".gitignore");
32728
- if (!existsSync7(ignore)) writeFileSync6(ignore, "*\n", { flag: "wx" });
33450
+ const ignore = resolve11(dirname10(p.bindingPath), ".gitignore");
33451
+ if (!existsSync8(ignore)) writeFileSync7(ignore, "*\n", { flag: "wx" });
32729
33452
  const trust = p.trustAgy ? trustAgyWorkspace(p.root) : { changed: false, disabled: true };
32730
33453
  const mcpName = "marionette-" + config.id.slice(0, 8);
32731
33454
  const command2 = mcpCommand(p.lead, mcpName, runtime, p.home);
32732
33455
  const mcpStatus = p.mcp === "install" ? await installMcp(command2, mcpName, p.home) : p.mcp;
32733
33456
  const prompt = leadPrompt(project.id, p.leadName, leasePath);
32734
- const promptPath = resolve10(p.home, "leads", project.id + ".md");
32735
- writeFileSync6(promptPath, prompt + "\n", { mode: 384 });
33457
+ const promptPath = resolve11(p.home, "leads", project.id + ".md");
33458
+ writeFileSync7(promptPath, prompt + "\n", { mode: 384 });
32736
33459
  return {
32737
33460
  ok: true,
32738
33461
  home: p.home,
@@ -32749,19 +33472,19 @@ async function runSetup(input) {
32749
33472
  prompt
32750
33473
  };
32751
33474
  } finally {
32752
- closeSync2(lock);
33475
+ closeSync3(lock);
32753
33476
  unlinkSync3(lockPath);
32754
33477
  }
32755
33478
  }
32756
33479
  async function launchLead(project, printOnly, profileId) {
32757
- while (!existsSync7(resolve10(project, ".marionette/project.json"))) {
32758
- if (dirname9(project) === project)
33480
+ while (!existsSync8(resolve11(project, ".marionette/project.json"))) {
33481
+ if (dirname10(project) === project)
32759
33482
  throw new Error("No configured Marionette project found. Run marionette setup first.");
32760
- project = dirname9(project);
33483
+ project = dirname10(project);
32761
33484
  }
32762
- const binding = JSON.parse(readFileSync8(resolve10(project, ".marionette/project.json"), "utf8"));
33485
+ const binding = JSON.parse(readFileSync9(resolve11(project, ".marionette/project.json"), "utf8"));
32763
33486
  const brief = await call(binding.home, "project.briefing", { projectId: binding.projectId });
32764
- const lease = credentialsSchema.parse(JSON.parse(readFileSync8(binding.leasePath, "utf8")));
33487
+ const lease = credentialsSchema.parse(JSON.parse(readFileSync9(binding.leasePath, "utf8")));
32765
33488
  if (!brief.lead || brief.lead.owner !== lease.owner || brief.lead.epoch !== lease.epoch)
32766
33489
  throw new Error(
32767
33490
  "The saved lead no longer controls this project. Refresh setup with an explicit handover or takeover."
@@ -32810,7 +33533,7 @@ var print = (v) => console.log(JSON.stringify(v, null, 2));
32810
33533
  async function main() {
32811
33534
  const cmd = args[0] ?? "help";
32812
33535
  if (cmd === "--version" || cmd === "version") {
32813
- console.log(JSON.parse(readFileSync9(resolve11(packageRoot, "package.json"), "utf8")).version);
33536
+ console.log(JSON.parse(readFileSync10(resolve12(packageRoot, "package.json"), "utf8")).version);
32814
33537
  return;
32815
33538
  }
32816
33539
  if (cmd === "setup" || cmd === "init") {
@@ -32868,7 +33591,7 @@ async function main() {
32868
33591
  n++;
32869
33592
  }
32870
33593
  }
32871
- let input = flag("config") ? JSON.parse(readFileSync9(flag("config"), "utf8")) : {};
33594
+ let input = flag("config") ? JSON.parse(readFileSync10(flag("config"), "utf8")) : {};
32872
33595
  for (const key of ["project", "home", "name", "session", "socket", "workspace", "lead", "mcp"])
32873
33596
  if (flag(key)) input[key] = flag(key);
32874
33597
  if (flag("lead-name")) input.leadName = flag("lead-name");
@@ -32907,7 +33630,7 @@ ${result.mcp.status === "print" ? "\nMCP install command:\n" + result.mcp.shell
32907
33630
  }
32908
33631
  if (cmd === "lead") {
32909
33632
  await launchLead(
32910
- resolve11(flag("project") ?? process.cwd()),
33633
+ resolve12(flag("project") ?? process.cwd()),
32911
33634
  args.includes("--print"),
32912
33635
  flag("profile")
32913
33636
  );
@@ -32921,7 +33644,7 @@ ${result.mcp.status === "print" ? "\nMCP install command:\n" + result.mcp.shell
32921
33644
  } = process.env;
32922
33645
  if (!url || !taskId || !token)
32923
33646
  throw new Error("worker-report must run in a Marionette-created worker pane");
32924
- const input = JSON.parse(readFileSync9(flag("file") ?? 0, "utf8"));
33647
+ const input = JSON.parse(readFileSync10(flag("file") ?? 0, "utf8"));
32925
33648
  const res = await fetch(
32926
33649
  `${url}/api/worker/${encodeURIComponent(taskId)}${cmd === "worker-call" ? "/call" : ""}`,
32927
33650
  {
@@ -32955,16 +33678,16 @@ ${result.mcp.status === "print" ? "\nMCP install command:\n" + result.mcp.shell
32955
33678
  } catch (e) {
32956
33679
  if (!String(e).includes("fetch failed") && !String(e).includes("TimeoutError")) throw e;
32957
33680
  }
32958
- const cli = resolve11(installRuntime(home), "dist/cli.js");
32959
- if (!existsSync8(cli))
33681
+ const cli = resolve12(installRuntime(home), "dist/cli.js");
33682
+ if (!existsSync9(cli))
32960
33683
  throw new Error("Run npm run build before starting the background supervisor");
32961
- const log = openSync3(resolve11(home, "supervisor.log"), "a", 384);
33684
+ const log = openSync4(resolve12(home, "supervisor.log"), "a", 384);
32962
33685
  const child = spawn4(process.execPath, ["--no-warnings", cli, "serve", "--home", home], {
32963
33686
  detached: true,
32964
33687
  stdio: ["ignore", log, log],
32965
33688
  env: process.env
32966
33689
  });
32967
- closeSync3(log);
33690
+ closeSync4(log);
32968
33691
  child.unref();
32969
33692
  for (let i = 0; i < 50; i++) {
32970
33693
  await new Promise((r) => setTimeout(r, 100));
@@ -32977,7 +33700,7 @@ ${result.mcp.status === "print" ? "\nMCP install command:\n" + result.mcp.shell
32977
33700
  } catch {
32978
33701
  }
32979
33702
  }
32980
- throw new Error(`Supervisor did not start. Read ${resolve11(home, "supervisor.log")}`);
33703
+ throw new Error(`Supervisor did not start. Read ${resolve12(home, "supervisor.log")}`);
32981
33704
  }
32982
33705
  if (cmd === "stop") {
32983
33706
  const c = loadConfig(home), url = `http://127.0.0.1:${c.port}`;
@@ -33011,7 +33734,7 @@ ${result.mcp.status === "print" ? "\nMCP install command:\n" + result.mcp.shell
33011
33734
  return;
33012
33735
  }
33013
33736
  if (cmd === "mcp-config") {
33014
- const server = resolve11(installRuntime(home), "dist/mcp.js");
33737
+ const server = resolve12(installRuntime(home), "dist/mcp.js");
33015
33738
  console.log(
33016
33739
  `[mcp_servers.marionette]
33017
33740
  command = ${JSON.stringify(process.execPath)}
@@ -33023,11 +33746,11 @@ args = ["--no-warnings", ${JSON.stringify(server)}, "--home", ${JSON.stringify(h
33023
33746
  if (cmd === "call") {
33024
33747
  const action = args[1];
33025
33748
  if (!action) throw new Error("Provide an action; see help");
33026
- const input = flag("file") ? JSON.parse(readFileSync9(flag("file"), "utf8")) : flag("json") ? JSON.parse(flag("json")) : {};
33027
- if (flag("lease")) input.lease = JSON.parse(readFileSync9(flag("lease"), "utf8"));
33749
+ const input = flag("file") ? JSON.parse(readFileSync10(flag("file"), "utf8")) : flag("json") ? JSON.parse(flag("json")) : {};
33750
+ if (flag("lease")) input.lease = JSON.parse(readFileSync10(flag("lease"), "utf8"));
33028
33751
  const result = await call(home, action, input);
33029
33752
  if (flag("save-lease") && result.lease) {
33030
- writeFileSync7(flag("save-lease"), JSON.stringify(result.lease, null, 2) + "\n", {
33753
+ writeFileSync8(flag("save-lease"), JSON.stringify(result.lease, null, 2) + "\n", {
33031
33754
  mode: 384
33032
33755
  });
33033
33756
  const { lease, ...rest } = result;