@theaileverage/marionette 0.2.0 → 0.2.2

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
@@ -886,15 +886,15 @@ var require_depd = __commonJS({
886
886
  return relative5(basePath, callSite[0]) + ":" + callSite[1] + ":" + callSite[2];
887
887
  }
888
888
  function getStack() {
889
- var limit = Error.stackTraceLimit;
889
+ var limit2 = Error.stackTraceLimit;
890
890
  var obj = {};
891
891
  var prep = Error.prepareStackTrace;
892
892
  Error.prepareStackTrace = prepareObjectStackTrace;
893
- Error.stackTraceLimit = Math.max(10, limit);
893
+ Error.stackTraceLimit = Math.max(10, limit2);
894
894
  Error.captureStackTrace(obj);
895
895
  var stack = obj.stack.slice(1);
896
896
  Error.prepareStackTrace = prep;
897
- Error.stackTraceLimit = limit;
897
+ Error.stackTraceLimit = limit2;
898
898
  return stack;
899
899
  }
900
900
  function prepareObjectStackTrace(obj, stack) {
@@ -5341,15 +5341,15 @@ var require_raw_body = __commonJS({
5341
5341
  throw new TypeError("argument callback is required");
5342
5342
  }
5343
5343
  var encoding = opts.encoding !== true ? opts.encoding : "utf-8";
5344
- var limit = bytes.parse(opts.limit);
5344
+ var limit2 = bytes.parse(opts.limit);
5345
5345
  var length = opts.length != null && !isNaN(opts.length) ? parseInt(opts.length, 10) : null;
5346
5346
  if (done) {
5347
- return readStream(stream, encoding, length, limit, wrap(done));
5347
+ return readStream(stream, encoding, length, limit2, wrap(done));
5348
5348
  }
5349
- return new Promise(function executor(resolve12, reject) {
5350
- readStream(stream, encoding, length, limit, function onRead(err, buf) {
5349
+ return new Promise(function executor(resolve13, reject) {
5350
+ readStream(stream, encoding, length, limit2, function onRead(err, buf) {
5351
5351
  if (err) return reject(err);
5352
- resolve12(buf);
5352
+ resolve13(buf);
5353
5353
  });
5354
5354
  });
5355
5355
  }
@@ -5359,14 +5359,14 @@ var require_raw_body = __commonJS({
5359
5359
  stream.pause();
5360
5360
  }
5361
5361
  }
5362
- function readStream(stream, encoding, length, limit, callback) {
5362
+ function readStream(stream, encoding, length, limit2, callback) {
5363
5363
  var complete = false;
5364
5364
  var sync = true;
5365
- if (limit !== null && length !== null && length > limit) {
5365
+ if (limit2 !== null && length !== null && length > limit2) {
5366
5366
  return done(createError(413, "request entity too large", {
5367
5367
  expected: length,
5368
5368
  length,
5369
- limit,
5369
+ limit: limit2,
5370
5370
  type: "entity.too.large"
5371
5371
  }));
5372
5372
  }
@@ -5427,9 +5427,9 @@ var require_raw_body = __commonJS({
5427
5427
  function onData(chunk) {
5428
5428
  if (complete) return;
5429
5429
  received += chunk.length;
5430
- if (limit !== null && received > limit) {
5430
+ if (limit2 !== null && received > limit2) {
5431
5431
  done(createError(413, "request entity too large", {
5432
- limit,
5432
+ limit: limit2,
5433
5433
  received,
5434
5434
  type: "entity.too.large"
5435
5435
  }));
@@ -15608,11 +15608,11 @@ var require_utils = __commonJS({
15608
15608
  throw new TypeError("defaultType must be provided");
15609
15609
  }
15610
15610
  const inflate = options?.inflate !== false;
15611
- const limit = typeof options?.limit === "undefined" || options?.limit === null ? 102400 : bytes.parse(options.limit);
15611
+ const limit2 = typeof options?.limit === "undefined" || options?.limit === null ? 102400 : bytes.parse(options.limit);
15612
15612
  const type = options?.type || defaultType;
15613
15613
  const verify = options?.verify || false;
15614
15614
  const defaultCharset = options?.defaultCharset || "utf-8";
15615
- if (limit === null) {
15615
+ if (limit2 === null) {
15616
15616
  throw new TypeError(`option limit "${String(options.limit)}" is invalid`);
15617
15617
  }
15618
15618
  if (verify !== false && typeof verify !== "function") {
@@ -15621,7 +15621,7 @@ var require_utils = __commonJS({
15621
15621
  const shouldParse = typeof type !== "function" ? typeChecker(type) : type;
15622
15622
  return {
15623
15623
  inflate,
15624
- limit,
15624
+ limit: limit2,
15625
15625
  verify,
15626
15626
  defaultCharset,
15627
15627
  shouldParse
@@ -17756,7 +17756,7 @@ var require_utils2 = __commonJS({
17756
17756
  return strWithoutPlus;
17757
17757
  }
17758
17758
  };
17759
- var limit = 1024;
17759
+ var limit2 = 1024;
17760
17760
  var encode = function encode2(str, defaultEncoder, charset, kind, format) {
17761
17761
  if (str.length === 0) {
17762
17762
  return str;
@@ -17773,9 +17773,9 @@ var require_utils2 = __commonJS({
17773
17773
  });
17774
17774
  }
17775
17775
  var out = "";
17776
- for (var j = 0; j < string.length; j += limit) {
17777
- var segment = string.length >= limit ? string.slice(j, j + limit) : string;
17778
- if (j + limit < string.length) {
17776
+ for (var j = 0; j < string.length; j += limit2) {
17777
+ var segment = string.length >= limit2 ? string.slice(j, j + limit2) : string;
17778
+ if (j + limit2 < string.length) {
17779
17779
  var last = segment.charCodeAt(segment.length - 1);
17780
17780
  if (last >= 55296 && last <= 56319) {
17781
17781
  segment = segment.slice(0, -1);
@@ -18246,13 +18246,13 @@ var require_parse = __commonJS({
18246
18246
  var obj = { __proto__: null };
18247
18247
  var cleanStr = options.ignoreQueryPrefix ? str.replace(/^\?/, "") : str;
18248
18248
  cleanStr = cleanStr.replace(/%5B/gi, "[").replace(/%5D/gi, "]");
18249
- var limit = options.parameterLimit === Infinity ? void 0 : options.parameterLimit;
18249
+ var limit2 = options.parameterLimit === Infinity ? void 0 : options.parameterLimit;
18250
18250
  var parts = cleanStr.split(
18251
18251
  options.delimiter,
18252
- options.throwOnLimitExceeded && typeof limit !== "undefined" ? limit + 1 : limit
18252
+ options.throwOnLimitExceeded && typeof limit2 !== "undefined" ? limit2 + 1 : limit2
18253
18253
  );
18254
- if (options.throwOnLimitExceeded && typeof limit !== "undefined" && parts.length > limit) {
18255
- throw new RangeError("Parameter limit exceeded. Only " + limit + " parameter" + (limit === 1 ? "" : "s") + " allowed.");
18254
+ if (options.throwOnLimitExceeded && typeof limit2 !== "undefined" && parts.length > limit2) {
18255
+ throw new RangeError("Parameter limit exceeded. Only " + limit2 + " parameter" + (limit2 === 1 ? "" : "s") + " allowed.");
18256
18256
  }
18257
18257
  var skipIndex = -1;
18258
18258
  var i;
@@ -18600,12 +18600,12 @@ var require_urlencoded = __commonJS({
18600
18600
  }
18601
18601
  };
18602
18602
  }
18603
- function parameterCount(body, limit) {
18603
+ function parameterCount(body, limit2) {
18604
18604
  let count = 0;
18605
18605
  let index = -1;
18606
18606
  do {
18607
18607
  count++;
18608
- if (count > limit) return void 0;
18608
+ if (count > limit2) return void 0;
18609
18609
  index = body.indexOf("&", index + 1);
18610
18610
  } while (index !== -1);
18611
18611
  return count;
@@ -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
@@ -28276,8 +28278,8 @@ var Store = class {
28276
28278
  const r = this.db.prepare("INSERT INTO events(project_id,data) VALUES(?,?)").run(projectId, JSON.stringify(event));
28277
28279
  return { ...event, id: Number(r.lastInsertRowid) };
28278
28280
  }
28279
- events(projectId, after = 0, limit = 200) {
28280
- return this.db.prepare("SELECT id,data FROM events WHERE project_id=? AND id>? ORDER BY id LIMIT ?").all(projectId, after, limit).map((r) => ({ ...JSON.parse(r.data), id: r.id }));
28281
+ events(projectId, after = 0, limit2 = 200) {
28282
+ return this.db.prepare("SELECT id,data FROM events WHERE project_id=? AND id>? ORDER BY id LIMIT ?").all(projectId, after, limit2).map((r) => ({ ...JSON.parse(r.data), id: r.id }));
28281
28283
  }
28282
28284
  close() {
28283
28285
  this.db.close();
@@ -28285,9 +28287,9 @@ 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 { isAbsolute as isAbsolute2 } from "node:path";
28290
+ import { randomUUID as randomUUID6, randomBytes as randomBytes2 } from "node:crypto";
28291
+ import { realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
28292
+ import { isAbsolute as isAbsolute4 } from "node:path";
28291
28293
 
28292
28294
  // src/continuation.ts
28293
28295
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -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,789 @@ ${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
+ paneId: r.paneId,
29167
+ cleanup: r.cleanup,
29168
+ reasons: this.runReasons(t, r, true)
29169
+ })),
29170
+ delivery: this.s.store.get("delivery", taskId),
29171
+ archive,
29172
+ collectionReasons: [
29173
+ ...this.groupReasons(t),
29174
+ ...gitReason ? [gitReason] : [],
29175
+ ...!this.s.store.get("delivery", taskId) && !archive ? ["Record delivery or explicit abandonment first"] : []
29176
+ ],
29177
+ boundary: "Completion retains branches and worktrees. Collection never removes sessions or workspaces."
29178
+ };
29179
+ }
29180
+ async locked(tasks, fn) {
29181
+ if (this.stopped || tasks.some((t) => this.busy.has(t.id)))
29182
+ refuse("Cleanup is stopping or already in progress");
29183
+ tasks.forEach((t) => this.busy.add(t.id));
29184
+ try {
29185
+ return await fn();
29186
+ } finally {
29187
+ tasks.forEach((t) => this.busy.delete(t.id));
29188
+ }
29189
+ }
29190
+ recover() {
29191
+ this.stopped = false;
29192
+ for (const r of this.s.store.all("run"))
29193
+ if (r.cleanup?.state === "closing")
29194
+ this.saveRun(r, {
29195
+ ...r.cleanup,
29196
+ state: "uncertain",
29197
+ error: "Restart during terminal closure; inspect and reconcile. No automatic replay."
29198
+ });
29199
+ }
29200
+ async stop() {
29201
+ this.stopped = true;
29202
+ while (this.busy.size) await new Promise((r) => setTimeout(r, 25));
29203
+ }
29204
+ tick() {
29205
+ if (this.stopped || Date.now() - this.lastAutomatic < 1e4) return;
29206
+ this.lastAutomatic = Date.now();
29207
+ for (const t of this.s.store.all("task")) {
29208
+ if (this.busy.has(t.id)) continue;
29209
+ const p = this.policy(t.projectId);
29210
+ const r = t.runId ? this.s.store.get("run", t.runId) : void 0;
29211
+ if (p.autoRelease && r && !r.cleanup && !this.runReasons(t, r, true).length) {
29212
+ void this.locked([t], () => this.release(t, r, "Integrated outcome completed", true)).catch(
29213
+ (e) => this.s.store.event(t.projectId, "cleanup.error", String(e), t.id)
29214
+ );
29215
+ continue;
29216
+ }
29217
+ const delivery = this.s.store.get("delivery", t.id), a = this.archive(t);
29218
+ if (p.collectAfterHours === null || !delivery || delivery.disposition === "abandoned" || a && a.taskId !== t.id || this.group(t).some((x) => x.status !== "completed"))
29219
+ continue;
29220
+ if (a?.phase === "collected" && (!p.deleteMergedBranches || a.branchDeleted || delivery.disposition !== "merged"))
29221
+ continue;
29222
+ if (Date.now() - Date.parse(delivery.createdAt) < p.collectAfterHours * 36e5 || this.groupReasons(t).length)
29223
+ continue;
29224
+ if (a?.error || a?.phase === "removing") continue;
29225
+ void this.locked(this.group(t), async () => {
29226
+ const guard = () => {
29227
+ if (JSON.stringify(this.policy(t.projectId)) !== JSON.stringify(p))
29228
+ refuse("Cleanup policy changed during inspection");
29229
+ };
29230
+ const archived = a ?? await this.createArchive(t, guard);
29231
+ await this.collect(
29232
+ archived,
29233
+ p.deleteMergedBranches && delivery.disposition === "merged",
29234
+ guard
29235
+ );
29236
+ }).catch((e) => this.s.store.event(t.projectId, "cleanup.error", String(e), t.id));
29237
+ }
29238
+ }
29239
+ saveRun(r, cleanup) {
29240
+ const latest = this.s.store.get("run", r.id);
29241
+ this.s.store.put("run", r.id, { ...latest, cleanup: { ...cleanup, updatedAt: now() } });
29242
+ }
29243
+ async pinned(t, r) {
29244
+ const p = this.s.project(t.projectId), h = this.s.port(p);
29245
+ if (!r.tabId || !r.paneId || !r.terminalId || !r.nativeSession)
29246
+ refuse("A full saved worker and native session identity is required");
29247
+ const tab = (await h.call("tab.get", { tab_id: r.tabId })).tab;
29248
+ const panes = (await h.call("pane.list", { workspace_id: p.workspaceId })).panes;
29249
+ const members = panes?.filter((pane) => pane.tab_id === r.tabId);
29250
+ if (tab?.workspace_id !== p.workspaceId || tab.tab_id !== r.tabId || !r.terminalScope && (tab.pane_count !== 1 || members?.length !== 1) || members?.filter((pane) => pane.pane_id === r.paneId && pane.terminal_id === r.terminalId).length !== 1)
29251
+ refuse("Tab contents changed; refusing to close another pane");
29252
+ const a = (await h.call("agent.get", { target: r.paneId })).agent;
29253
+ 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)
29254
+ refuse("Native worker identity or readiness changed");
29255
+ return h;
29256
+ }
29257
+ async absent(t, r) {
29258
+ const h = this.s.port(this.s.project(t.projectId));
29259
+ if (r.terminalScope === "pane") {
29260
+ const panes2 = (await h.call("pane.list")).panes;
29261
+ if (!Array.isArray(panes2)) refuse("Cannot inspect worker terminal membership");
29262
+ if (panes2.some(
29263
+ (pane) => (pane.pane_id === r.paneId || pane.terminal_id === r.terminalId) && (pane.pane_id !== r.paneId || pane.tab_id !== r.tabId || pane.terminal_id !== r.terminalId)
29264
+ ))
29265
+ refuse("Original terminal may have moved; inspect it before cleanup");
29266
+ return !panes2.some(
29267
+ (pane) => pane.pane_id === r.paneId || pane.terminal_id === r.terminalId
29268
+ );
29269
+ }
29270
+ try {
29271
+ await h.call("tab.get", { tab_id: r.tabId });
29272
+ return false;
29273
+ } catch (e) {
29274
+ if (e.code !== "tab_not_found") throw e;
29275
+ }
29276
+ const panes = (await h.call("pane.list")).panes;
29277
+ if (!Array.isArray(panes) || panes.some((p) => p.pane_id === r.paneId || p.terminal_id === r.terminalId))
29278
+ refuse("Original terminal may have moved to another tab; inspect it before cleanup");
29279
+ return true;
29280
+ }
29281
+ async checkReleased(tasks) {
29282
+ for (const t of tasks) {
29283
+ for (const r of this.s.store.all("run").filter((r2) => r2.taskId === t.id && r2.paneId))
29284
+ if (r.cleanup?.state !== "closed" || !await this.absent(t, r))
29285
+ refuse(
29286
+ "A recorded worker terminal is still present; inspect it before collecting its checkout"
29287
+ );
29288
+ }
29289
+ }
29290
+ async release(t, r, reason, automatic, guard = () => {
29291
+ }) {
29292
+ if (r.cleanup?.state === "closed") return r.cleanup;
29293
+ if (r.cleanup && ["closing", "uncertain"].includes(r.cleanup.state))
29294
+ refuse("Reconcile ambiguous terminal closure first");
29295
+ try {
29296
+ const reasons = this.runReasons(this.s.task(t.id), r, automatic);
29297
+ if (reasons.length) refuse(reasons.join("; "));
29298
+ if (await this.absent(t, r)) {
29299
+ guard();
29300
+ this.saveRun(r, { state: "closed", reason, updatedAt: now(), output: t.output });
29301
+ return this.s.store.get("run", r.id).cleanup;
29302
+ }
29303
+ const h = await this.pinned(t, r);
29304
+ const output = (await h.call("pane.read", {
29305
+ pane_id: r.paneId,
29306
+ source: "recent_unwrapped",
29307
+ lines: 500,
29308
+ format: "text"
29309
+ })).read?.text ?? t.output;
29310
+ await this.pinned(t, r);
29311
+ guard();
29312
+ const current = this.s.task(t.id);
29313
+ if (current.revision !== t.revision || this.runReasons(current, this.s.store.get("run", r.id), automatic).length)
29314
+ refuse("Task changed during cleanup inspection");
29315
+ this.saveRun(r, {
29316
+ state: "closing",
29317
+ reason,
29318
+ updatedAt: now(),
29319
+ output: String(output).slice(-1e5)
29320
+ });
29321
+ if (r.terminalScope === "pane") await h.call("pane.close", { pane_id: r.paneId });
29322
+ else await h.call("tab.close", { tab_id: r.tabId });
29323
+ this.saveRun(r, { ...this.s.store.get("run", r.id).cleanup, state: "closed" });
29324
+ this.s.store.event(
29325
+ t.projectId,
29326
+ "cleanup.released",
29327
+ "Saved worker diagnostics and closed its settled terminal",
29328
+ t.id,
29329
+ { runId: r.id }
29330
+ );
29331
+ return this.s.store.get("run", r.id).cleanup;
29332
+ } catch (e) {
29333
+ const current = this.s.store.get("run", r.id).cleanup;
29334
+ this.saveRun(r, {
29335
+ ...current,
29336
+ state: current?.state === "closing" ? "uncertain" : "retained",
29337
+ reason,
29338
+ updatedAt: now(),
29339
+ error: String(e)
29340
+ });
29341
+ throw e;
29342
+ }
29343
+ }
29344
+ async clean(w) {
29345
+ await validateWorktree(w);
29346
+ const status = await git(w.path, [
29347
+ "status",
29348
+ "--porcelain",
29349
+ "--untracked-files=all",
29350
+ "--ignored"
29351
+ ]);
29352
+ if (status)
29353
+ refuse(
29354
+ "Worktree has modified, untracked, or ignored files; preserve or remove them explicitly before collection"
29355
+ );
29356
+ return git(w.path, ["rev-parse", "HEAD"]);
29357
+ }
29358
+ async checkDelivery(w, d) {
29359
+ if (await this.clean(w) !== d.head) refuse("Worktree HEAD changed after delivery");
29360
+ if (d.disposition !== "abandoned") {
29361
+ if (!d.targetRef || !d.targetCommit) refuse("Delivery target is missing");
29362
+ const current = await git(w.repositoryRoot, [
29363
+ "rev-parse",
29364
+ "--verify",
29365
+ "--end-of-options",
29366
+ `${d.targetRef}^{commit}`
29367
+ ]);
29368
+ await git(w.repositoryRoot, ["merge-base", "--is-ancestor", d.head, current]);
29369
+ }
29370
+ }
29371
+ archiveRoot(id) {
29372
+ return safePath(dirname4(this.s.store.path), `archives/${id}`);
29373
+ }
29374
+ /** Live files remain authoritative until collection actually removes the checkout. */
29375
+ evidencePath(t, input) {
29376
+ const a = this.archive(t);
29377
+ if (!a || a.phase === "archived" || existsSync3(a.worktree.path))
29378
+ return safePath(t.cwd, input, true);
29379
+ const source = resolve4(t.cwd, input);
29380
+ if (!inside(t.cwd, source)) refuse("Evidence escapes task directory");
29381
+ const f = a.files.find((f2) => f2.source === source);
29382
+ if (!f) refuse("Evidence was not preserved in this archive");
29383
+ const path = safePath(this.archiveRoot(a.id), `files/${f.digest}`, true);
29384
+ if (digest(path) !== f.digest) refuse("Archived evidence digest changed");
29385
+ return path;
29386
+ }
29387
+ async createArchive(t, guard) {
29388
+ const prior = this.archive(t);
29389
+ if (prior) return prior;
29390
+ const reasons = this.groupReasons(t);
29391
+ if (reasons.length) refuse(reasons.join("; "));
29392
+ const w = t.worktree, d = this.s.store.get("delivery", t.id);
29393
+ if (!d) refuse("Record delivery or abandonment before archiving");
29394
+ await this.checkDelivery(w, d);
29395
+ const tasks = this.group(t), taskIds = new Set(tasks.map((t2) => t2.id));
29396
+ await this.checkReleased(tasks);
29397
+ const id = randomUUID3(), root = this.archiveRoot(id);
29398
+ mkdirSync4(resolve4(root, "files"), { recursive: true, mode: 448 });
29399
+ const files = /* @__PURE__ */ new Map();
29400
+ const copy = (source, expected, alias = source) => {
29401
+ const actual = digest(source);
29402
+ if (!actual || expected && expected !== actual)
29403
+ refuse("Evidence is missing or changed before archival");
29404
+ const bytes = readFileSync4(source);
29405
+ if (hash(bytes) !== actual) refuse("Evidence changed during archival");
29406
+ writeFileSync2(resolve4(root, "files", actual), bytes, { mode: 384, flush: true });
29407
+ files.set(source, actual);
29408
+ files.set(alias, actual);
29409
+ };
29410
+ for (const item of tasks) {
29411
+ for (const v of item.verification ?? [])
29412
+ if (v.passed && v.check.type === "file")
29413
+ copy(safePath(item.cwd, v.check.path, true), v.digest, resolve4(item.cwd, v.check.path));
29414
+ for (const path of item.receipt?.artifacts ?? []) {
29415
+ const source = safePath(item.cwd, path, true);
29416
+ if (!item.ownership.some((owned) => inside(safePath(item.cwd, owned), source)))
29417
+ refuse("Reported artifact no longer belongs to the task");
29418
+ if (statSync3(source).isDirectory()) continue;
29419
+ copy(source, void 0, resolve4(item.cwd, path));
29420
+ }
29421
+ }
29422
+ for (const o of this.s.store.all("outcome")) {
29423
+ for (const ref of [
29424
+ ...o.assessments.flatMap((a2) => a2.references),
29425
+ ...o.integrated?.evidence ?? []
29426
+ ]) {
29427
+ const match = /^task:([^:]+):(.+)$/.exec(ref.path);
29428
+ if (match && taskIds.has(match[1]))
29429
+ copy(
29430
+ safePath(this.s.task(match[1]).cwd, match[2], true),
29431
+ ref.digest,
29432
+ resolve4(this.s.task(match[1]).cwd, match[2])
29433
+ );
29434
+ else if (!match) {
29435
+ const path = resolve4(this.s.project(o.projectId).root, ref.path);
29436
+ if (inside(w.path, path))
29437
+ refuse(
29438
+ "Project-relative evidence still uses this checkout; replace it with a task reference"
29439
+ );
29440
+ }
29441
+ }
29442
+ }
29443
+ await git(w.repositoryRoot, [
29444
+ "bundle",
29445
+ "create",
29446
+ resolve4(root, "commits.bundle"),
29447
+ `refs/heads/${w.branch}`
29448
+ ]);
29449
+ await git(w.repositoryRoot, ["bundle", "verify", resolve4(root, "commits.bundle")]);
29450
+ flush(resolve4(root, "commits.bundle"));
29451
+ const bundleDigest = await fileHash(resolve4(root, "commits.bundle"));
29452
+ const manifest = JSON.stringify(
29453
+ {
29454
+ tasks,
29455
+ runs: this.s.store.all("run").filter((r) => taskIds.has(r.taskId)).map(({ tokenHash, ...r }) => r),
29456
+ outcomes: this.s.store.all("outcome").filter((o) => tasks.some((t2) => t2.outcomeId === o.id)),
29457
+ delivery: d,
29458
+ files: [...files],
29459
+ archivedAt: now()
29460
+ },
29461
+ null,
29462
+ 2
29463
+ );
29464
+ writeFileSync2(resolve4(root, "manifest.json"), manifest, { mode: 384, flush: true });
29465
+ flush(resolve4(root, "files"));
29466
+ flush(root);
29467
+ flush(dirname4(root));
29468
+ flush(dirname4(dirname4(root)));
29469
+ await this.checkDelivery(w, d);
29470
+ guard();
29471
+ if (this.groupReasons(t).length || this.group(t).some((x) => !tasks.some((y) => x.id === y.id && x.revision === y.revision)))
29472
+ refuse("Checkout consumers changed during archival");
29473
+ for (const [path, expected] of files)
29474
+ if (digest(path) !== expected) refuse("Evidence changed during archival");
29475
+ const a = {
29476
+ id,
29477
+ taskId: t.id,
29478
+ projectId: t.projectId,
29479
+ taskIds: [...taskIds],
29480
+ phase: "archived",
29481
+ worktree: w,
29482
+ delivery: d,
29483
+ files: [...files].map(([source, digest2]) => ({ source, digest: digest2 })),
29484
+ manifestDigest: hash(manifest),
29485
+ bundleDigest,
29486
+ createdAt: now(),
29487
+ updatedAt: now()
29488
+ };
29489
+ this.s.store.transaction(() => {
29490
+ this.s.store.put("archive", id, a);
29491
+ for (const item of tasks) this.s.updateTask(item, { archiveId: id });
29492
+ this.s.store.event(
29493
+ t.projectId,
29494
+ "cleanup.archived",
29495
+ "Preserved evidence, diagnostics, and committed history; tasks are sealed",
29496
+ t.id,
29497
+ { archiveId: id }
29498
+ );
29499
+ });
29500
+ return a;
29501
+ }
29502
+ async verifyArchive(a) {
29503
+ const root = this.archiveRoot(a.id);
29504
+ if (hash(readFileSync4(safePath(root, "manifest.json", true))) !== a.manifestDigest || await fileHash(safePath(root, "commits.bundle", true)) !== a.bundleDigest)
29505
+ refuse("Archive integrity check failed");
29506
+ for (const f of a.files)
29507
+ if (digest(safePath(root, `files/${f.digest}`, true)) !== f.digest)
29508
+ refuse("Archived evidence integrity check failed");
29509
+ }
29510
+ saveArchive(a, patch) {
29511
+ const next = { ...this.s.store.get("archive", a.id), ...patch, updatedAt: now() };
29512
+ this.s.store.put("archive", a.id, next);
29513
+ return next;
29514
+ }
29515
+ async collect(a, deleteBranch, guard) {
29516
+ if (a.phase === "collected" && (!deleteBranch || a.branchDeleted)) return a;
29517
+ if (deleteBranch && a.delivery.disposition !== "merged" && a.delivery.disposition !== "abandoned")
29518
+ refuse("Published PR branches are retained until merged or explicitly abandoned");
29519
+ try {
29520
+ await this.verifyArchive(a);
29521
+ const t = this.s.task(a.taskId), reasons = this.groupReasons(t);
29522
+ if (reasons.length) refuse(reasons.join("; "));
29523
+ await this.checkReleased(this.group(t));
29524
+ const w = a.worktree;
29525
+ const common = realpathSync3(
29526
+ await git(w.repositoryRoot, ["rev-parse", "--path-format=absolute", "--git-common-dir"])
29527
+ );
29528
+ if (common !== w.commonDir) refuse("Source repository identity changed");
29529
+ if (existsSync3(w.path)) {
29530
+ await this.checkDelivery(w, a.delivery);
29531
+ for (const f of a.files)
29532
+ if (digest(f.source) !== f.digest) refuse("Live evidence changed after archival");
29533
+ guard();
29534
+ if (this.groupReasons(t).length) refuse("Checkout consumers changed before collection");
29535
+ a = this.saveArchive(a, { phase: "removing", error: void 0 });
29536
+ await git(w.repositoryRoot, ["worktree", "remove", "--", w.path]);
29537
+ }
29538
+ const listing = await git(w.repositoryRoot, ["worktree", "list", "--porcelain", "-z"]);
29539
+ if (common !== w.commonDir || listing.split("\0").includes(`worktree ${w.path}`) || existsSync3(w.path))
29540
+ refuse("Worktree removal is incomplete; inspect its registration");
29541
+ a = this.saveArchive(a, { phase: "worktree-removed", error: void 0 });
29542
+ if (deleteBranch) {
29543
+ const refs = await git(w.repositoryRoot, [
29544
+ "for-each-ref",
29545
+ "--format=%(refname) %(objectname)",
29546
+ `refs/heads/${w.branch}`
29547
+ ]);
29548
+ if (refs) {
29549
+ if (refs !== `refs/heads/${w.branch} ${a.delivery.head}`)
29550
+ refuse("Branch moved after archival; preserving it");
29551
+ if (listing.split("\0").includes(`branch refs/heads/${w.branch}`))
29552
+ refuse("Another checkout still uses the branch");
29553
+ if (a.delivery.disposition === "abandoned") {
29554
+ guard();
29555
+ await git(w.repositoryRoot, [
29556
+ "update-ref",
29557
+ "-d",
29558
+ `refs/heads/${w.branch}`,
29559
+ a.delivery.head
29560
+ ]);
29561
+ } else {
29562
+ const target = await git(w.repositoryRoot, [
29563
+ "rev-parse",
29564
+ "--verify",
29565
+ "--end-of-options",
29566
+ `${a.delivery.targetRef}^{commit}`
29567
+ ]);
29568
+ await git(w.repositoryRoot, ["merge-base", "--is-ancestor", a.delivery.head, target]);
29569
+ guard();
29570
+ await git(w.repositoryRoot, [
29571
+ "update-ref",
29572
+ "-d",
29573
+ `refs/heads/${w.branch}`,
29574
+ a.delivery.head
29575
+ ]);
29576
+ }
29577
+ }
29578
+ }
29579
+ a = this.saveArchive(a, {
29580
+ phase: "collected",
29581
+ branchDeleted: deleteBranch,
29582
+ error: void 0
29583
+ });
29584
+ this.s.store.event(
29585
+ a.projectId,
29586
+ "cleanup.collected",
29587
+ "Collected delivered worktree; archive retained",
29588
+ a.taskId,
29589
+ { archiveId: a.id, branchDeleted: deleteBranch }
29590
+ );
29591
+ return a;
29592
+ } catch (e) {
29593
+ this.saveArchive(a, { error: String(e) });
29594
+ throw e;
29595
+ }
29596
+ }
29597
+ async invoke(action, raw) {
29598
+ if (action === "cleanup.preview") return this.preview(external_exports.string().parse(raw.taskId));
29599
+ const c = this.s.guard(raw.lease), reason = external_exports.string().min(1).max(4e3).parse(raw.reason);
29600
+ const guard = () => {
29601
+ this.s.guard(raw.lease);
29602
+ };
29603
+ if (action === "cleanup.configure") {
29604
+ const p = cleanupPolicySchema.parse(raw.policy);
29605
+ this.s.store.put("cleanup-policy", c.projectId, p);
29606
+ this.s.store.event(c.projectId, "cleanup.policy", `${c.owner}: ${reason}`, void 0, p);
29607
+ return p;
29608
+ }
29609
+ const t = this.s.task(external_exports.string().parse(raw.taskId));
29610
+ if (t.projectId !== c.projectId) refuse("Task and lead belong to different projects");
29611
+ if (action === "cleanup.release" || action === "cleanup.reconcile") {
29612
+ const r = this.s.store.get("run", external_exports.string().parse(raw.runId ?? t.runId));
29613
+ if (!r || r.taskId !== t.id) refuse("Run does not belong to this task");
29614
+ return this.locked([t], async () => {
29615
+ if (action === "cleanup.release") return this.release(t, r, reason, false, guard);
29616
+ if (r.cleanup?.state !== "uncertain")
29617
+ refuse("Only uncertain cleanup needs reconciliation");
29618
+ const resolution = external_exports.enum(["closed", "not-closed"]).parse(raw.resolution);
29619
+ if (resolution === "closed") {
29620
+ if (await this.absent(t, r)) {
29621
+ guard();
29622
+ this.saveRun(r, { ...r.cleanup, state: "closed", reason, error: void 0 });
29623
+ return this.s.store.get("run", r.id).cleanup;
29624
+ }
29625
+ refuse("Original worker terminal is still present");
29626
+ }
29627
+ await this.pinned(t, r);
29628
+ guard();
29629
+ this.saveRun(r, { ...r.cleanup, state: "retained", reason, error: void 0 });
29630
+ return this.s.store.get("run", r.id).cleanup;
29631
+ });
29632
+ }
29633
+ if (!t.worktree) refuse("Task has no managed worktree");
29634
+ return this.locked(this.group(t), async () => {
29635
+ if (action === "cleanup.deliver") {
29636
+ const archived = this.archive(t);
29637
+ const disposition = external_exports.enum(["merged", "published", "abandoned"]).parse(raw.disposition);
29638
+ if (archived && archived.taskId !== t.id)
29639
+ refuse("Update delivery on the task that owns the archive");
29640
+ const head = archived && !existsSync3(t.worktree.path) ? await git(t.worktree.repositoryRoot, [
29641
+ "rev-parse",
29642
+ "--verify",
29643
+ "--end-of-options",
29644
+ `refs/heads/${t.worktree.branch}^{commit}`
29645
+ ]) : await this.clean(t.worktree);
29646
+ if (archived && (head !== archived.delivery.head || archived.branchDeleted))
29647
+ refuse("Archived branch changed or was already deleted");
29648
+ let targetRef, targetCommit;
29649
+ if (disposition !== "abandoned") {
29650
+ targetRef = external_exports.string().min(1).parse(raw.targetRef);
29651
+ if (!/^refs\/(heads|remotes)\//.test(targetRef) || targetRef === `refs/heads/${t.worktree.branch}` || disposition === "published" && !targetRef.startsWith("refs/remotes/"))
29652
+ refuse(
29653
+ "Use an explicit target branch ref; published delivery needs a refreshed remote-tracking ref"
29654
+ );
29655
+ await git(t.worktree.repositoryRoot, ["check-ref-format", targetRef]);
29656
+ targetCommit = await git(t.worktree.repositoryRoot, [
29657
+ "rev-parse",
29658
+ "--verify",
29659
+ "--end-of-options",
29660
+ `${targetRef}^{commit}`
29661
+ ]);
29662
+ await git(t.worktree.repositoryRoot, [
29663
+ "merge-base",
29664
+ "--is-ancestor",
29665
+ head,
29666
+ targetCommit
29667
+ ]);
29668
+ }
29669
+ guard();
29670
+ const d = {
29671
+ taskId: t.id,
29672
+ projectId: t.projectId,
29673
+ disposition,
29674
+ head,
29675
+ targetRef,
29676
+ targetCommit,
29677
+ reason,
29678
+ owner: c.owner,
29679
+ createdAt: now()
29680
+ };
29681
+ const previous = this.s.store.get("delivery", t.id);
29682
+ if (previous && JSON.stringify({ ...previous, createdAt: "" }) === JSON.stringify({ ...d, createdAt: "" }))
29683
+ return previous;
29684
+ this.s.store.put("delivery", t.id, d);
29685
+ if (archived) this.saveArchive(archived, { delivery: d, error: void 0 });
29686
+ this.s.store.event(t.projectId, "cleanup.delivered", `${disposition}: ${reason}`, t.id, d);
29687
+ return d;
29688
+ }
29689
+ if (action === "cleanup.archive") return this.createArchive(t, guard);
29690
+ if (action === "cleanup.collect") {
29691
+ const a = this.archive(t);
29692
+ if (!a || a.id !== raw.archiveId)
29693
+ refuse("Preview and supply the current archiveId before collection");
29694
+ return this.collect(a, external_exports.boolean().parse(raw.deleteBranch ?? false), guard);
29695
+ }
29696
+ throw new AppError("unknown_action", `Unknown action: ${action}`, 404);
29697
+ });
29698
+ }
29699
+ };
29700
+
28916
29701
  // src/model-catalog.ts
28917
29702
  import { spawn as spawn2 } from "node:child_process";
28918
29703
 
@@ -28973,7 +29758,7 @@ var strategySchema = external_exports.object({
28973
29758
 
28974
29759
  // src/model-catalog.ts
28975
29760
  async function metadata(binary, args2, cwd, kind) {
28976
- return new Promise((resolve12, reject) => {
29761
+ return new Promise((resolve13, reject) => {
28977
29762
  const child = spawn2(binary, args2, { cwd, stdio: ["pipe", "pipe", "pipe"] });
28978
29763
  let buffer = "", bytes = 0, finished = false, nextId = 2;
28979
29764
  const rows = [];
@@ -28983,7 +29768,7 @@ async function metadata(binary, args2, cwd, kind) {
28983
29768
  clearTimeout(timer);
28984
29769
  child.stdin.end();
28985
29770
  child.kill("SIGTERM");
28986
- error ? reject(error) : resolve12(rows);
29771
+ error ? reject(error) : resolve13(rows);
28987
29772
  };
28988
29773
  const timer = setTimeout(
28989
29774
  () => finish(new AppError("catalog_timeout", `${binary} model metadata timed out`)),
@@ -29569,9 +30354,9 @@ async function probeProfile(profile, cwd) {
29569
30354
  }
29570
30355
 
29571
30356
  // 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";
30357
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "node:fs";
30358
+ import { dirname as dirname5, resolve as resolve5, relative as relative3 } from "node:path";
30359
+ import { randomUUID as randomUUID4 } from "node:crypto";
29575
30360
  var fail = (code, message) => {
29576
30361
  throw new AppError(code, message, 409);
29577
30362
  };
@@ -29590,7 +30375,7 @@ var Orchestration = class {
29590
30375
  id: `legacy-${t.id}`,
29591
30376
  projectId: t.projectId,
29592
30377
  objective: t.prompt,
29593
- scope: t.ownership.map((path) => resolve3(t.worktree?.sourceCwd ?? t.cwd, path)),
30378
+ scope: t.ownership.map((path) => resolve5(t.worktree?.sourceCwd ?? t.cwd, path)),
29594
30379
  category: "software",
29595
30380
  criteria: t.checks.map((check, index) => ({
29596
30381
  id: `check-${index + 1}`,
@@ -29660,7 +30445,7 @@ var Orchestration = class {
29660
30445
  if (!v.passed) return false;
29661
30446
  if (v.check.type !== "file") return true;
29662
30447
  try {
29663
- return !!v.digest && digest(safePath(t.cwd, v.check.path, true)) === v.digest;
30448
+ return !!v.digest && digest(this.s.cleanup.evidencePath(t, v.check.path)) === v.digest;
29664
30449
  } catch {
29665
30450
  return false;
29666
30451
  }
@@ -29701,8 +30486,15 @@ var Orchestration = class {
29701
30486
  const t = this.s.task(taskReference[1]);
29702
30487
  if (t.projectId !== projectId)
29703
30488
  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)))
30489
+ const original = resolve5(t.cwd, taskReference[2]);
30490
+ const path = this.s.cleanup.evidencePath(t, taskReference[2]);
30491
+ const archived = !inside(t.cwd, path);
30492
+ if (!t.ownership.some(
30493
+ (owned) => inside(
30494
+ archived ? resolve5(t.cwd, owned) : safePath(t.cwd, owned),
30495
+ archived ? original : path
30496
+ )
30497
+ ))
29706
30498
  fail("evidence_scope", "Task evidence must be inside its ownership");
29707
30499
  return path;
29708
30500
  }
@@ -29737,7 +30529,7 @@ var Orchestration = class {
29737
30529
  }
29738
30530
  revision(o, reason, owner, before, after, taskId, evidence = []) {
29739
30531
  const r = {
29740
- id: randomUUID3(),
30532
+ id: randomUUID4(),
29741
30533
  projectId: o.projectId,
29742
30534
  outcomeId: o.id,
29743
30535
  revision: o.revision,
@@ -29842,6 +30634,9 @@ var Orchestration = class {
29842
30634
  }
29843
30635
  references(projectId, paths) {
29844
30636
  return external_exports.array(text).min(1).parse(paths).map((path) => {
30637
+ const taskReference = /^task:([^:]+):/.exec(path);
30638
+ if (taskReference && this.s.cleanup.active(taskReference[1]))
30639
+ fail("cleanup_busy", "Evidence is being archived; retry after cleanup finishes");
29845
30640
  const value = digest(this.evidencePath(projectId, path));
29846
30641
  if (!value)
29847
30642
  fail(
@@ -29922,6 +30717,7 @@ var Orchestration = class {
29922
30717
  fail("depth_limit", "Outcome delegation depth reached");
29923
30718
  }
29924
30719
  if (task.outcomeId) {
30720
+ this.s.cleanup.assertOutcomeMutable(task.outcomeId);
29925
30721
  const o = this.outcome(task.outcomeId);
29926
30722
  if (o.projectId !== task.projectId)
29927
30723
  fail("outcome_scope", "Outcome belongs to another project");
@@ -29932,7 +30728,7 @@ var Orchestration = class {
29932
30728
  safePath(this.s.project(task.projectId).root, scope),
29933
30729
  task.worktree ? safePath(
29934
30730
  task.worktree.sourceCwd,
29935
- relative2(task.worktree.cwd, safePath(task.cwd, owned))
30731
+ relative3(task.worktree.cwd, safePath(task.cwd, owned))
29936
30732
  ) : safePath(task.cwd, owned)
29937
30733
  )
29938
30734
  ))
@@ -29945,7 +30741,7 @@ var Orchestration = class {
29945
30741
  );
29946
30742
  } else {
29947
30743
  const o = {
29948
- id: randomUUID3(),
30744
+ id: randomUUID4(),
29949
30745
  projectId: task.projectId,
29950
30746
  objective: task.prompt,
29951
30747
  scope: task.ownership.map((p) => safePath(task.cwd, p)),
@@ -30055,6 +30851,7 @@ var Orchestration = class {
30055
30851
  }
30056
30852
  reviseTask(raw, c) {
30057
30853
  const t = this.s.task(text.parse(raw.taskId));
30854
+ this.s.cleanup.assertMutable(t);
30058
30855
  if (t.projectId !== c.projectId || !t.outcomeId)
30059
30856
  fail("project_mismatch", "Task must belong to this lead and an outcome");
30060
30857
  const o = this.outcome(t.outcomeId);
@@ -30062,6 +30859,9 @@ var Orchestration = class {
30062
30859
  if (t.revision !== external_exports.number().int().parse(raw.expectedRevision))
30063
30860
  fail("stale_revision", "Task changed; refresh before revising");
30064
30861
  const patch = planPatchSchema.parse(raw.patch), reason = text.parse(raw.reason);
30862
+ for (const id of patch.dependencies ?? [])
30863
+ if (this.s.cleanup.active(id))
30864
+ fail("cleanup_busy", "Dependency is being cleaned up; retry after it finishes");
30065
30865
  if (t.runId && this.s.store.get("run", t.runId)?.phase !== "stopped" && !["paused", "waiting"].includes(t.status))
30066
30866
  fail("task_active", "Pause and settle the task before revising it");
30067
30867
  if (patch.supersededBy) {
@@ -30070,13 +30870,15 @@ var Orchestration = class {
30070
30870
  fail("supersede_scope", "Replacement must be required work in the same outcome");
30071
30871
  }
30072
30872
  for (const check of patch.checks ?? []) if (check.type === "file") safePath(t.cwd, check.path);
30873
+ const released = t.runId && this.s.store.get("run", t.runId)?.cleanup?.state === "closed";
30073
30874
  const updated = {
30074
30875
  ...t,
30075
30876
  ...patch,
30076
30877
  revision: t.revision + 1,
30077
30878
  receipt: void 0,
30078
30879
  verification: void 0,
30079
- status: patch.supersededBy ? "cancelled" : t.runId ? "paused" : "queued",
30880
+ runId: released ? void 0 : t.runId,
30881
+ status: patch.supersededBy ? "cancelled" : t.runId && !released ? "paused" : "queued",
30080
30882
  updatedAt: now()
30081
30883
  };
30082
30884
  this.validateGraph(
@@ -30121,7 +30923,7 @@ var Orchestration = class {
30121
30923
  }
30122
30924
  if (raw.action === "finding") {
30123
30925
  const finding = {
30124
- id: randomUUID3(),
30926
+ id: randomUUID4(),
30125
30927
  projectId: t.projectId,
30126
30928
  outcomeId: t.outcomeId,
30127
30929
  taskId: t.id,
@@ -30217,9 +31019,9 @@ var Orchestration = class {
30217
31019
  "profile_changed",
30218
31020
  "Profile changed during validation; validate the current configuration"
30219
31021
  );
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 });
31022
+ const path = resolve5(dirname5(this.s.store.path), "profile-evidence", randomUUID4() + ".json");
31023
+ mkdirSync5(dirname5(path), { recursive: true, mode: 448 });
31024
+ writeFileSync3(path, result.output, { mode: 384 });
30223
31025
  const updated = {
30224
31026
  ...profile,
30225
31027
  availability,
@@ -30244,7 +31046,7 @@ var Orchestration = class {
30244
31046
  const { key, ...fields } = i;
30245
31047
  const o2 = {
30246
31048
  ...fields,
30247
- id: randomUUID3(),
31049
+ id: randomUUID4(),
30248
31050
  leadOwner: c.owner,
30249
31051
  revision: 1,
30250
31052
  status: "open",
@@ -30304,6 +31106,7 @@ var Orchestration = class {
30304
31106
  }
30305
31107
  if (action.startsWith("strategy.")) return this.strategy(action, raw, c);
30306
31108
  const o = this.outcome(text.parse(raw.outcomeId));
31109
+ this.s.cleanup.assertOutcomeMutable(o.id);
30307
31110
  if (o.projectId !== c.projectId) fail("project_mismatch", "Outcome and lead differ");
30308
31111
  this.checkRevision(o, raw.expectedRevision);
30309
31112
  if (action === "outcome.revise") {
@@ -30372,6 +31175,8 @@ var Orchestration = class {
30372
31175
  });
30373
31176
  }
30374
31177
  strategy(action, raw, c) {
31178
+ const cleanupOutcome = raw.strategy?.outcomeId ?? raw.outcomeId ?? (raw.strategyId ? this.s.store.get("strategy", raw.strategyId)?.outcomeId : void 0);
31179
+ if (cleanupOutcome) this.s.cleanup.assertOutcomeMutable(cleanupOutcome);
30375
31180
  if (action === "strategy.create") {
30376
31181
  const i = strategySchema.parse(raw.strategy), o = this.outcome(i.outcomeId);
30377
31182
  if (o.projectId !== c.projectId) fail("project_mismatch", "Strategy and lead differ");
@@ -30382,7 +31187,7 @@ var Orchestration = class {
30382
31187
  fail("quorum", "Quorum exceeds participant count");
30383
31188
  const strategy2 = {
30384
31189
  ...i,
30385
- id: randomUUID3(),
31190
+ id: randomUUID4(),
30386
31191
  projectId: c.projectId,
30387
31192
  revision: 1,
30388
31193
  round: 1,
@@ -30475,68 +31280,620 @@ var Orchestration = class {
30475
31280
  }
30476
31281
  };
30477
31282
 
30478
- // src/herdr.ts
31283
+ // src/herdr-protocol.ts
31284
+ var HERDR_METHODS = [
31285
+ "ping",
31286
+ "server.stop",
31287
+ "server.live_handoff",
31288
+ "server.reload_config",
31289
+ "server.agent_manifests",
31290
+ "server.reload_agent_manifests",
31291
+ "notification.show",
31292
+ "product_announcement.dismiss",
31293
+ "release_notes.dismiss",
31294
+ "command.invoke",
31295
+ "client.window_title.set",
31296
+ "client.window_title.clear",
31297
+ "client_shell.surface.set",
31298
+ "session.snapshot",
31299
+ "workspace.create",
31300
+ "workspace.list",
31301
+ "workspace.get",
31302
+ "workspace.focus",
31303
+ "workspace.rename",
31304
+ "workspace.move",
31305
+ "workspace.move_block",
31306
+ "workspace.report_metadata",
31307
+ "workspace.close",
31308
+ "worktree.list",
31309
+ "worktree.create",
31310
+ "worktree.open",
31311
+ "worktree.remove",
31312
+ "tab.create",
31313
+ "tab.list",
31314
+ "tab.get",
31315
+ "tab.focus",
31316
+ "tab.rename",
31317
+ "tab.move",
31318
+ "tab.close",
31319
+ "agent.list",
31320
+ "agent.get",
31321
+ "agent.read",
31322
+ "agent.explain",
31323
+ "agent.send_keys",
31324
+ "agent.rename",
31325
+ "agent.view.set",
31326
+ "agent.view.clear",
31327
+ "agent.focus",
31328
+ "agent.start",
31329
+ "agent.prompt",
31330
+ "agent.wait",
31331
+ "pane.split",
31332
+ "pane.swap",
31333
+ "pane.move",
31334
+ "pane.zoom",
31335
+ "pane.layout",
31336
+ "pane.process_info",
31337
+ "layout.export",
31338
+ "layout.apply",
31339
+ "layout.set_split_ratio",
31340
+ "pane.neighbor",
31341
+ "pane.edges",
31342
+ "pane.focus_direction",
31343
+ "pane.resize",
31344
+ "pane.scroll",
31345
+ "pane.edit_scrollback",
31346
+ "pane.selection.read",
31347
+ "pane.copy_motion",
31348
+ "pane.copy_search",
31349
+ "pane.list",
31350
+ "pane.current",
31351
+ "pane.get",
31352
+ "pane.focus",
31353
+ "pane.input.set",
31354
+ "pane.link.activate",
31355
+ "pane.rename",
31356
+ "pane.send_text",
31357
+ "pane.send_keys",
31358
+ "pane.send_input",
31359
+ "pane.read",
31360
+ "pane.graphics.set",
31361
+ "pane.graphics.clear",
31362
+ "pane.graphics.info",
31363
+ "pane.report_agent",
31364
+ "pane.report_agent_session",
31365
+ "pane.report_metadata",
31366
+ "pane.clear_agent_authority",
31367
+ "pane.release_agent",
31368
+ "pane.close",
31369
+ "popup.close",
31370
+ "events.subscribe",
31371
+ "events.wait",
31372
+ "pane.wait_for_output",
31373
+ "integration.list",
31374
+ "integration.install",
31375
+ "integration.uninstall",
31376
+ "plugin.link",
31377
+ "plugin.list",
31378
+ "plugin.unlink",
31379
+ "plugin.enable",
31380
+ "plugin.disable",
31381
+ "plugin.action.list",
31382
+ "plugin.action.invoke",
31383
+ "plugin.log.list",
31384
+ "plugin.pane.open",
31385
+ "plugin.pane.focus",
31386
+ "plugin.pane.close"
31387
+ ];
31388
+
31389
+ // src/herdr-streams.ts
31390
+ import { isAbsolute as isAbsolute3 } from "node:path";
31391
+
31392
+ // src/herdr-transport.ts
30479
31393
  import net from "node:net";
30480
- import { randomUUID as randomUUID4 } from "node:crypto";
30481
- var Herdr = class {
31394
+ import { randomUUID as randomUUID5 } from "node:crypto";
31395
+ import { isAbsolute as isAbsolute2 } from "node:path";
31396
+ var HerdrError = class extends Error {
31397
+ constructor(code, message) {
31398
+ super(message);
31399
+ this.code = code;
31400
+ this.name = "HerdrError";
31401
+ }
31402
+ code;
31403
+ };
31404
+ function validateSocketPath(path) {
31405
+ if (!isAbsolute2(path) && !/^\\\\[.?]\\pipe\\[^\\]/.test(path))
31406
+ throw new TypeError("An absolute Herdr socket path or Windows named pipe is required");
31407
+ }
31408
+ function validateTimeout(value) {
31409
+ if (value !== null && (!Number.isFinite(value) || value <= 0 || value > 2147483647))
31410
+ throw new TypeError("timeoutMs must be a positive bounded duration or null");
31411
+ }
31412
+ function limit(value, name) {
31413
+ if (!Number.isSafeInteger(value) || value <= 0)
31414
+ throw new TypeError(`${name} must be a positive integer`);
31415
+ return value;
31416
+ }
31417
+ var JsonConnection = class {
31418
+ constructor(socketPath, options = {}) {
31419
+ this.options = options;
31420
+ validateSocketPath(socketPath);
31421
+ this.maxMessageBytes = limit(options.maxResponseBytes ?? 32 * 1024 * 1024, "maxResponseBytes");
31422
+ this.maxQueuedEvents = limit(options.maxQueuedEvents ?? 1024, "maxQueuedEvents");
31423
+ this.maxQueuedBytes = limit(options.maxQueuedBytes ?? this.maxMessageBytes, "maxQueuedBytes");
31424
+ if (options.signal?.aborted)
31425
+ throw new HerdrError("herdr_aborted", "Herdr operation was aborted");
31426
+ this.closed = new Promise((resolve13, reject) => {
31427
+ this.resolveClosed = resolve13;
31428
+ this.rejectClosed = reject;
31429
+ });
31430
+ void this.closed.catch(() => {
31431
+ });
31432
+ this.socket = net.createConnection(socketPath);
31433
+ this.socket.setEncoding("utf8");
31434
+ this.abort = () => this.fail(new HerdrError("herdr_aborted", "Herdr operation was aborted"), true);
31435
+ options.signal?.addEventListener("abort", this.abort, { once: true });
31436
+ this.socket.on(
31437
+ "error",
31438
+ (error) => this.fail(new HerdrError("herdr_unavailable", error.message))
31439
+ );
31440
+ this.socket.on("close", () => {
31441
+ this.ended = true;
31442
+ this.detachAbort();
31443
+ this.rejectWrites(this.error());
31444
+ if (this.failure || !this.explicitClose) this.rejectClosed(this.error());
31445
+ else this.resolveClosed();
31446
+ this.deliver();
31447
+ });
31448
+ this.socket.on("data", (data) => this.receive(data));
31449
+ }
31450
+ options;
31451
+ id = randomUUID5();
31452
+ closed;
31453
+ resolveClosed;
31454
+ rejectClosed;
31455
+ socket;
31456
+ buffer = "";
31457
+ queue = [];
31458
+ queuedBytes = 0;
31459
+ reader;
31460
+ writes = /* @__PURE__ */ new Set();
31461
+ ended = false;
31462
+ failure;
31463
+ explicitClose = false;
31464
+ maxMessageBytes;
31465
+ maxQueuedEvents;
31466
+ maxQueuedBytes;
31467
+ abort;
31468
+ error() {
31469
+ return this.failure ?? new HerdrError(
31470
+ "herdr_disconnected",
31471
+ "Herdr disconnected before acknowledgement or stream completion"
31472
+ );
31473
+ }
31474
+ detachAbort() {
31475
+ this.options.signal?.removeEventListener("abort", this.abort);
31476
+ }
31477
+ rejectWrites(error) {
31478
+ for (const reject of this.writes) reject(error);
31479
+ this.writes.clear();
31480
+ }
31481
+ fail(error, discard = false) {
31482
+ if (this.explicitClose) return;
31483
+ this.failure ??= error;
31484
+ this.ended = true;
31485
+ this.buffer = "";
31486
+ if (discard) {
31487
+ this.queue = [];
31488
+ this.queuedBytes = 0;
31489
+ }
31490
+ this.detachAbort();
31491
+ this.socket.destroy();
31492
+ this.rejectWrites(this.failure);
31493
+ this.deliver();
31494
+ }
31495
+ close() {
31496
+ this.explicitClose = true;
31497
+ this.ended = true;
31498
+ this.buffer = "";
31499
+ this.queue = [];
31500
+ this.queuedBytes = 0;
31501
+ this.detachAbort();
31502
+ this.socket.destroy();
31503
+ this.rejectWrites(this.error());
31504
+ this.deliver();
31505
+ }
31506
+ deliver() {
31507
+ if (!this.reader) return;
31508
+ const reader = this.reader;
31509
+ if (this.failure) {
31510
+ this.reader = void 0;
31511
+ reader.reject(this.failure);
31512
+ return;
31513
+ }
31514
+ const next = this.queue.shift();
31515
+ if (next) {
31516
+ this.reader = void 0;
31517
+ this.queuedBytes -= next.bytes;
31518
+ reader.resolve(next.value);
31519
+ } else if (this.ended) {
31520
+ this.reader = void 0;
31521
+ if (this.explicitClose) reader.resolve(void 0);
31522
+ else reader.reject(this.error());
31523
+ }
31524
+ }
31525
+ receive(data) {
31526
+ if (this.ended) return;
31527
+ this.buffer += data;
31528
+ let index;
31529
+ while ((index = this.buffer.indexOf("\n")) >= 0) {
31530
+ const line = this.buffer.slice(0, index);
31531
+ this.buffer = this.buffer.slice(index + 1);
31532
+ const bytes = Buffer.byteLength(line);
31533
+ if (bytes > this.maxMessageBytes)
31534
+ return this.fail(
31535
+ new HerdrError("herdr_response_too_large", "Herdr message exceeded maxResponseBytes"),
31536
+ true
31537
+ );
31538
+ let value;
31539
+ try {
31540
+ value = JSON.parse(line);
31541
+ } catch (error) {
31542
+ return this.fail(new HerdrError("herdr_invalid_response", String(error)), true);
31543
+ }
31544
+ if (value?.id !== void 0 && value.id !== this.id && !String(value.id).startsWith(this.id + ":"))
31545
+ continue;
31546
+ if (value?.error)
31547
+ return this.fail(new HerdrError(value.error.code, value.error.message), true);
31548
+ if (this.queue.length >= this.maxQueuedEvents || this.queuedBytes + bytes > this.maxQueuedBytes)
31549
+ return this.fail(
31550
+ new HerdrError(
31551
+ "herdr_stream_overflow",
31552
+ "Herdr consumer fell behind; stream closed without silently dropping events"
31553
+ ),
31554
+ true
31555
+ );
31556
+ this.queue.push({ value, bytes });
31557
+ this.queuedBytes += bytes;
31558
+ this.deliver();
31559
+ }
31560
+ if (Buffer.byteLength(this.buffer) > this.maxMessageBytes)
31561
+ this.fail(
31562
+ new HerdrError("herdr_response_too_large", "Herdr message exceeded maxResponseBytes"),
31563
+ true
31564
+ );
31565
+ }
31566
+ read() {
31567
+ if (this.reader)
31568
+ return Promise.reject(new Error("Only one reader may consume a Herdr connection"));
31569
+ return new Promise((resolve13, reject) => {
31570
+ this.reader = { resolve: resolve13, reject };
31571
+ this.deliver();
31572
+ });
31573
+ }
31574
+ write(data) {
31575
+ if (this.ended) return Promise.reject(this.error());
31576
+ return new Promise((resolve13, reject) => {
31577
+ this.writes.add(reject);
31578
+ this.socket.write(data, (error) => {
31579
+ this.writes.delete(reject);
31580
+ if (error) {
31581
+ this.fail(new HerdrError("herdr_unavailable", error.message));
31582
+ reject(this.error());
31583
+ } else if (this.failure) reject(this.failure);
31584
+ else resolve13();
31585
+ });
31586
+ });
31587
+ }
31588
+ async within(timeoutMs, action) {
31589
+ validateTimeout(timeoutMs);
31590
+ const timer = timeoutMs === null ? void 0 : setTimeout(
31591
+ () => this.fail(
31592
+ new HerdrError(
31593
+ "herdr_timeout",
31594
+ "Herdr response timed out; delivery may be ambiguous"
31595
+ ),
31596
+ true
31597
+ ),
31598
+ timeoutMs
31599
+ );
31600
+ try {
31601
+ return await action();
31602
+ } finally {
31603
+ clearTimeout(timer);
31604
+ }
31605
+ }
31606
+ async start(method, params, timeoutMs) {
31607
+ return this.within(timeoutMs, async () => {
31608
+ await this.write(JSON.stringify({ id: this.id, method, params }) + "\n");
31609
+ const message = await this.read();
31610
+ if (!message || message.id !== this.id || !Object.hasOwn(message, "result"))
31611
+ throw new HerdrError("herdr_invalid_response", `${method}: missing correlated result`);
31612
+ return message.result;
31613
+ });
31614
+ }
31615
+ };
31616
+ async function socketRequest(socketPath, method, params, options = {}) {
31617
+ const timeoutMs = options.timeoutMs === void 0 ? 1e4 : options.timeoutMs;
31618
+ validateTimeout(timeoutMs);
31619
+ JSON.stringify(params);
31620
+ const connection = new JsonConnection(socketPath, options);
31621
+ try {
31622
+ return await connection.start(method, params, timeoutMs);
31623
+ } finally {
31624
+ connection.close();
31625
+ }
31626
+ }
31627
+
31628
+ // src/herdr-streams.ts
31629
+ var HerdrEventStream = class _HerdrEventStream {
31630
+ constructor(connection) {
31631
+ this.connection = connection;
31632
+ }
31633
+ connection;
31634
+ static async open(socketPath, subscriptions, options = {}) {
31635
+ const timeout = options.timeoutMs === void 0 ? 1e4 : options.timeoutMs;
31636
+ validateTimeout(timeout);
31637
+ JSON.stringify(subscriptions);
31638
+ const connection = new JsonConnection(socketPath, options);
31639
+ try {
31640
+ const result = await connection.start("events.subscribe", { subscriptions }, timeout);
31641
+ if (result?.type !== "subscription_started")
31642
+ throw new HerdrError("herdr_invalid_response", "Missing subscription acknowledgement");
31643
+ return new _HerdrEventStream(connection);
31644
+ } catch (error) {
31645
+ connection.close();
31646
+ throw error;
31647
+ }
31648
+ }
31649
+ get closed() {
31650
+ return this.connection.closed;
31651
+ }
31652
+ [Symbol.asyncIterator]() {
31653
+ return this;
31654
+ }
31655
+ async next() {
31656
+ const value = await this.connection.read();
31657
+ if (value === void 0) return { done: true, value: void 0 };
31658
+ if (!value || typeof value.event !== "string" || !Object.hasOwn(value, "data")) {
31659
+ const error = new HerdrError("herdr_invalid_response", "Expected a Herdr event envelope");
31660
+ this.connection.fail(error, true);
31661
+ throw error;
31662
+ }
31663
+ return { done: false, value };
31664
+ }
31665
+ close() {
31666
+ this.connection.close();
31667
+ }
31668
+ async return() {
31669
+ this.close();
31670
+ return { done: true, value: void 0 };
31671
+ }
31672
+ };
31673
+ var maxInlineBytes = 16 * 1024 * 1024;
31674
+ function integer(value, name, min = 0, max = Number.MAX_SAFE_INTEGER) {
31675
+ if (!Number.isSafeInteger(value) || value < min || value > max)
31676
+ throw new TypeError(`Invalid ${name}`);
31677
+ }
31678
+ function frameHeader(frame) {
31679
+ integer(frame.image_width, "image_width", 1, 4294967295);
31680
+ integer(frame.image_height, "image_height", 1, 4294967295);
31681
+ if (!["png", "rgb", "rgba", "bgra"].includes(frame.format))
31682
+ throw new TypeError("Invalid frame format");
31683
+ return {
31684
+ format: frame.format,
31685
+ image_width: frame.image_width,
31686
+ image_height: frame.image_height,
31687
+ ...frame.placement ? { placement: frame.placement } : {}
31688
+ };
31689
+ }
31690
+ function encodeHeader(header) {
31691
+ const line = JSON.stringify(header) + "\n";
31692
+ if (Buffer.byteLength(line) > 64 * 1024) throw new TypeError("Graphics header exceeds 64 KiB");
31693
+ return line;
31694
+ }
31695
+ var HerdrGraphicsStream = class _HerdrGraphicsStream {
31696
+ constructor(connection) {
31697
+ this.connection = connection;
31698
+ }
31699
+ connection;
31700
+ busy = false;
31701
+ static async open(socketPath, params, options = {}) {
31702
+ const timeout = options.timeoutMs === void 0 ? 1e4 : options.timeoutMs;
31703
+ validateTimeout(timeout);
31704
+ JSON.stringify(params);
31705
+ const connection = new JsonConnection(socketPath, options);
31706
+ try {
31707
+ const result = await connection.start("pane.graphics.stream", params, timeout);
31708
+ if (result?.type !== "ok")
31709
+ throw new HerdrError("herdr_invalid_response", "Missing graphics stream acknowledgement");
31710
+ return new _HerdrGraphicsStream(connection);
31711
+ } catch (error) {
31712
+ connection.close();
31713
+ throw error;
31714
+ }
31715
+ }
31716
+ /** Resolves after local close; rejects on remote failure, cancellation, or disconnect. */
31717
+ get closed() {
31718
+ return this.connection.closed;
31719
+ }
31720
+ async exclusive(action) {
31721
+ if (this.busy)
31722
+ throw new HerdrError("herdr_stream_busy", "Await the previous frame before sending another");
31723
+ this.busy = true;
31724
+ try {
31725
+ return await action();
31726
+ } finally {
31727
+ this.busy = false;
31728
+ }
31729
+ }
31730
+ /** Resolves after local socket write, not a render ACK: inline success has no wire reply. */
31731
+ async frame(frame, data, timeoutMs = 3e4) {
31732
+ validateTimeout(timeoutMs);
31733
+ if (!data.byteLength || data.byteLength > maxInlineBytes)
31734
+ throw new TypeError("Inline graphics data must be 1 byte to 16 MiB");
31735
+ const header = encodeHeader({ ...frameHeader(frame), data_length: data.byteLength });
31736
+ return this.exclusive(async () => {
31737
+ const bytes = Buffer.from(data);
31738
+ await this.connection.within(timeoutMs, async () => {
31739
+ await this.connection.write(header);
31740
+ await this.connection.write(bytes);
31741
+ });
31742
+ });
31743
+ }
31744
+ /** Keep the immutable source file until this method acknowledges its sequence and revision. */
31745
+ async fileFrame(frame, timeoutMs = 3e4) {
31746
+ validateTimeout(timeoutMs);
31747
+ if (!["rgba", "bgra"].includes(frame.format) || !isAbsolute3(frame.file.path))
31748
+ throw new TypeError("File frames require rgba/bgra and an absolute source path");
31749
+ integer(frame.sequence, "sequence");
31750
+ integer(frame.revision, "revision");
31751
+ const { sequence, revision } = frame;
31752
+ const header = encodeHeader({
31753
+ ...frameHeader(frame),
31754
+ file: { path: frame.file.path },
31755
+ sequence,
31756
+ revision
31757
+ });
31758
+ return this.exclusive(
31759
+ () => this.connection.within(timeoutMs, async () => {
31760
+ await this.connection.write(header);
31761
+ const message = await this.connection.read();
31762
+ const result = message?.result;
31763
+ if (message?.id !== `${this.connection.id}:file:${sequence}` || result?.type !== "pane_graphics_frame_ack" || result.sequence !== sequence || result.revision !== revision) {
31764
+ const error = new HerdrError(
31765
+ "herdr_invalid_response",
31766
+ "Graphics acknowledgement did not match the file frame"
31767
+ );
31768
+ this.connection.fail(error, true);
31769
+ throw error;
31770
+ }
31771
+ return result;
31772
+ })
31773
+ );
31774
+ }
31775
+ close() {
31776
+ this.connection.close();
31777
+ }
31778
+ };
31779
+
31780
+ // src/herdr-sdk.ts
31781
+ function deadline(method, params) {
31782
+ if (method === "agent.start") return (params.timeout_ms ?? 3e4) + 5e3;
31783
+ const wait = method === "agent.prompt" ? params.wait : ["agent.wait", "events.wait", "pane.wait_for_output"].includes(method) ? params : void 0;
31784
+ if (wait) return wait.timeout_ms == null ? null : wait.timeout_ms + 5e3;
31785
+ return 1e4;
31786
+ }
31787
+ var HerdrClient = class _HerdrClient {
30482
31788
  constructor(socketPath) {
30483
31789
  this.socketPath = socketPath;
31790
+ validateSocketPath(socketPath);
30484
31791
  }
30485
31792
  socketPath;
31793
+ static fromEnv(env = process.env) {
31794
+ if (env.HERDR_ENV !== "1" || !env.HERDR_SOCKET_PATH)
31795
+ throw new Error("Run inside Herdr or supply an explicitly selected socket path");
31796
+ return new _HerdrClient(env.HERDR_SOCKET_PATH);
31797
+ }
31798
+ /** Escape hatch for future one-shot methods. Streams require their dedicated transports. */
30486
31799
  call(method, params = {}, timeoutMs = 1e4) {
30487
- return new Promise((resolve12, reject) => {
30488
- const id = randomUUID4();
30489
- let buffer = "";
30490
- let settled2 = false;
30491
- const socket = net.createConnection(this.socketPath);
30492
- const finish = (error, result) => {
30493
- if (settled2) return;
30494
- settled2 = true;
30495
- clearTimeout(timer);
30496
- socket.destroy();
30497
- error ? reject(error) : resolve12(result);
30498
- };
30499
- const timer = setTimeout(
30500
- () => finish(
30501
- new AppError(
30502
- "herdr_timeout",
30503
- `${method}: response timed out; delivery may be ambiguous`,
30504
- 503
30505
- )
30506
- ),
30507
- timeoutMs
31800
+ if (method === "events.subscribe" || method === "pane.graphics.stream")
31801
+ return Promise.reject(
31802
+ new TypeError("Use subscribe(), graphicsStream(), or api for a streaming method")
30508
31803
  );
30509
- socket.on("connect", () => socket.write(JSON.stringify({ id, method, params }) + "\n"));
30510
- socket.on(
30511
- "error",
30512
- (e) => finish(new AppError("herdr_unavailable", `${method}: ${e.message}`, 503))
30513
- );
30514
- socket.on(
30515
- "close",
30516
- () => finish(
30517
- new AppError("herdr_disconnected", `${method}: disconnected before acknowledgement`, 503)
31804
+ return socketRequest(this.socketPath, method, params, { timeoutMs });
31805
+ }
31806
+ /** All schema-defined one-shot methods with exact typed parameter objects. */
31807
+ request(method, ...args2) {
31808
+ const [params = {}, options = {}] = args2;
31809
+ if (!HERDR_METHODS.includes(method) || method === "events.subscribe")
31810
+ return Promise.reject(
31811
+ new TypeError(
31812
+ "Unknown one-shot method; use call() for extensions or subscribe() for events"
30518
31813
  )
30519
31814
  );
30520
- socket.on("data", (data) => {
30521
- buffer += data.toString();
30522
- if (buffer.length > 8 * 1024 * 1024)
30523
- return finish(new Error("Herdr response exceeded 8 MiB"));
30524
- let index;
30525
- while ((index = buffer.indexOf("\n")) >= 0) {
30526
- const line = buffer.slice(0, index);
30527
- buffer = buffer.slice(index + 1);
30528
- try {
30529
- const v = JSON.parse(line);
30530
- if (v.id !== id) continue;
30531
- if (v.error) finish(new AppError(v.error.code, v.error.message, 502));
30532
- else finish(void 0, v.result);
30533
- } catch (e) {
30534
- finish(e);
30535
- }
30536
- }
30537
- });
31815
+ return socketRequest(this.socketPath, method, params, {
31816
+ ...options,
31817
+ timeoutMs: options.timeoutMs === void 0 ? deadline(method, params) : options.timeoutMs
30538
31818
  });
30539
31819
  }
31820
+ subscribe(subscriptions, options) {
31821
+ return HerdrEventStream.open(this.socketPath, subscriptions, options);
31822
+ }
31823
+ graphicsStream(params, options) {
31824
+ return HerdrGraphicsStream.open(this.socketPath, params, options);
31825
+ }
31826
+ /** One-to-one method names, including streams, for discovery and generated agent code. */
31827
+ api = Object.freeze(
31828
+ Object.fromEntries([
31829
+ ...HERDR_METHODS.map((method) => [
31830
+ method,
31831
+ method === "events.subscribe" ? (params, options) => this.subscribe(params.subscriptions, options) : (params = {}, options) => this.request(method, params, options)
31832
+ ]),
31833
+ [
31834
+ "pane.graphics.stream",
31835
+ (params, options) => this.graphicsStream(params, options)
31836
+ ]
31837
+ ])
31838
+ );
31839
+ workspace = {
31840
+ list: () => this.call("workspace.list"),
31841
+ create: (options) => this.call("workspace.create", {
31842
+ focus: false,
31843
+ ...options
31844
+ })
31845
+ };
31846
+ tab = {
31847
+ create: (workspaceId, options) => this.call("tab.create", {
31848
+ focus: false,
31849
+ ...options,
31850
+ workspace_id: workspaceId
31851
+ }),
31852
+ list: (workspaceId) => this.call("tab.list", { workspace_id: workspaceId }),
31853
+ close: (tabId) => this.call("tab.close", { tab_id: tabId })
31854
+ };
31855
+ pane = {
31856
+ list: (workspaceId) => this.call("pane.list", { workspace_id: workspaceId }),
31857
+ layout: (paneId) => this.call("pane.layout", { pane_id: paneId }),
31858
+ split: (paneId, options) => this.call("pane.split", { focus: false, ...options, target_pane_id: paneId }),
31859
+ read: (paneId, lines = 120) => this.call("pane.read", {
31860
+ pane_id: paneId,
31861
+ source: "recent_unwrapped",
31862
+ format: "text",
31863
+ lines
31864
+ }),
31865
+ sendText: (paneId, text2) => this.call("pane.send_text", { pane_id: paneId, text: text2 }),
31866
+ sendKeys: (paneId, keys) => this.call("pane.send_keys", { pane_id: paneId, keys }),
31867
+ close: (paneId) => this.call("pane.close", { pane_id: paneId })
31868
+ };
31869
+ agent = {
31870
+ start: (paneId, name, kind, args2 = [], timeoutMs = 3e4) => this.call(
31871
+ "agent.start",
31872
+ { pane_id: paneId, name, kind, args: args2, timeout_ms: timeoutMs },
31873
+ timeoutMs + 5e3
31874
+ ),
31875
+ get: (target) => this.call("agent.get", { target }),
31876
+ prompt: (target, text2, wait) => this.call(
31877
+ "agent.prompt",
31878
+ { target, text: text2, ...wait ? { wait } : {} },
31879
+ wait ? wait.timeout_ms + 5e3 : 1e4
31880
+ ),
31881
+ wait: (target, options) => this.call("agent.wait", { target, ...options }, options.timeout_ms + 5e3),
31882
+ sendKeys: (target, keys) => this.call("agent.send_keys", { target, keys })
31883
+ };
31884
+ };
31885
+
31886
+ // src/herdr.ts
31887
+ var Herdr = class extends HerdrClient {
31888
+ async call(method, params = {}, timeoutMs = 1e4) {
31889
+ try {
31890
+ return await super.call(method, params, timeoutMs);
31891
+ } catch (error) {
31892
+ if (error instanceof HerdrError)
31893
+ throw new AppError(error.code, error.message, error.code.startsWith("herdr_") ? 503 : 502);
31894
+ throw error;
31895
+ }
31896
+ }
30540
31897
  };
30541
31898
 
30542
31899
  // src/service.ts
@@ -30551,6 +31908,7 @@ var Service = class {
30551
31908
  port;
30552
31909
  orchestration = new Orchestration(this);
30553
31910
  continuation = new Continuation(this);
31911
+ cleanup = new Cleanup(this);
30554
31912
  project(id) {
30555
31913
  const p = this.store.get("project", id);
30556
31914
  if (!p) throw new AppError("not_found", "Project not found", 404);
@@ -30591,6 +31949,7 @@ var Service = class {
30591
31949
  );
30592
31950
  return {
30593
31951
  project,
31952
+ cleanupPolicy: this.cleanup.policy(projectId),
30594
31953
  lead: this.publicLead(projectId),
30595
31954
  ...this.orchestration.board(projectId),
30596
31955
  tasks,
@@ -30613,7 +31972,7 @@ var Service = class {
30613
31972
  const existing = this.store.all("question").find((q2) => q2.taskId === task.id && !q2.answeredAt && q2.text === text2);
30614
31973
  if (existing) return existing;
30615
31974
  const q = {
30616
- id: randomUUID5(),
31975
+ id: randomUUID6(),
30617
31976
  projectId: task.projectId,
30618
31977
  taskId: task.id,
30619
31978
  text: text2,
@@ -30641,6 +32000,7 @@ var Service = class {
30641
32000
  return result;
30642
32001
  }
30643
32002
  async invoke(action, raw = {}) {
32003
+ if (action.startsWith("cleanup.")) return this.cleanup.invoke(action, raw);
30644
32004
  if (/^(lead\.wait|checkpoint\.|usage\.|adapter\.)/.test(action))
30645
32005
  return this.continuation.invoke(action, raw);
30646
32006
  if (/^(outcome\.|plan\.|profile\.|limits\.|strategy\.|board\.)/.test(action))
@@ -30659,10 +32019,10 @@ var Service = class {
30659
32019
  agentArgs: external_exports.record(kindSchema, external_exports.array(external_exports.string())).default({}),
30660
32020
  trustAgyWorkspaces: external_exports.boolean().default(false)
30661
32021
  }).parse(raw);
30662
- if (!isAbsolute2(input.root) || !isAbsolute2(input.socketPath))
32022
+ if (!isAbsolute4(input.root) || !isAbsolute4(input.socketPath))
30663
32023
  throw new AppError("absolute_paths", "root and socketPath must be absolute");
30664
- const root = realpathSync2(input.root);
30665
- if (!statSync2(root).isDirectory())
32024
+ const root = realpathSync4(input.root);
32025
+ if (!statSync4(root).isDirectory())
30666
32026
  throw new AppError("root", "Project root is not a directory");
30667
32027
  const existing = this.store.all("project").find((p2) => p2.socketPath === input.socketPath && p2.workspaceId === input.workspaceId);
30668
32028
  if (existing) {
@@ -30673,11 +32033,13 @@ var Service = class {
30673
32033
  409
30674
32034
  );
30675
32035
  }
30676
- const p = { ...input, root, id: randomUUID5(), createdAt: now() };
32036
+ const p = { ...input, root, id: randomUUID6(), createdAt: now() };
30677
32037
  const h = this.port(p);
30678
32038
  await h.call("ping");
30679
32039
  await h.call("workspace.get", { workspace_id: p.workspaceId });
30680
32040
  return this.store.transaction(() => {
32041
+ for (const task of this.store.all("task"))
32042
+ if (task.worktree && inside(task.worktree.path, root)) this.cleanup.assertMutable(task);
30681
32043
  const concurrent = this.store.all("project").find((v) => v.socketPath === p.socketPath && v.workspaceId === p.workspaceId);
30682
32044
  if (concurrent) {
30683
32045
  if (concurrent.root === root) return concurrent;
@@ -30820,6 +32182,7 @@ var Service = class {
30820
32182
  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
32183
  if (c.projectId !== t.projectId)
30822
32184
  throw new AppError("project_mismatch", "Task and lease differ");
32185
+ this.cleanup.assertMutable(t);
30823
32186
  const cached = this.store.get("idempotency", t.projectId + ":retry:" + key);
30824
32187
  if (cached)
30825
32188
  return this.store.transaction(
@@ -30855,6 +32218,7 @@ var Service = class {
30855
32218
  }
30856
32219
  this.guard(raw.lease);
30857
32220
  const current = this.task(t.id);
32221
+ this.cleanup.assertMutable(current);
30858
32222
  if (current.runId !== t.runId || !["failed", "cancelled"].includes(current.status))
30859
32223
  throw new AppError(
30860
32224
  "retry_state",
@@ -30902,29 +32266,55 @@ var Service = class {
30902
32266
  "No task prompt was attempted. Inspect the workspace and reconcile creation as not-delivered."
30903
32267
  );
30904
32268
  const project = this.project(t.projectId);
30905
- const tabs = (await h.call("tab.list", { workspace_id: project.workspaceId })).tabs ?? [];
30906
- const matches = tabs.filter(
30907
- (tab) => tab.workspace_id === project.workspaceId && tab.label === r.agentName
30908
- );
30909
- if (matches.length > 1)
30910
- throw new AppError(
30911
- "identity_changed",
30912
- "Multiple tabs match the interrupted creation; manual inspection is required",
30913
- 409
32269
+ if (r.creation?.mode === "pane") {
32270
+ const all = (await h.call("pane.list", { workspace_id: project.workspaceId })).panes;
32271
+ if (!Array.isArray(all) || !r.creation.beforePaneIds)
32272
+ throw new AppError(
32273
+ "identity_changed",
32274
+ "Missing split membership; inspect the workspace",
32275
+ 409
32276
+ );
32277
+ const added = all.filter(
32278
+ (pane) => !r.creation.beforePaneIds.includes(pane.pane_id)
30914
32279
  );
30915
- if (matches.length === 1) {
30916
- const panes = ((await h.call("pane.list", { workspace_id: project.workspaceId })).panes ?? []).filter((pane) => pane.tab_id === matches[0].tab_id);
30917
- if (panes.length !== 1 || panes[0].agent || panes[0].launch_pending || panes[0].workspace_id !== project.workspaceId || panes[0].cwd !== t.cwd)
32280
+ const panes = added.filter((pane) => pane.tab_id === r.creation.tabId);
32281
+ if (added.length !== panes.length || panes.length > 1 || panes.length === 1 && (panes[0].agent || panes[0].launch_pending || panes[0].workspace_id !== project.workspaceId || panes[0].cwd !== t.cwd))
30918
32282
  throw new AppError(
30919
32283
  "identity_changed",
30920
- "The orphan tab is no longer an untouched shell; inspect it before recovery",
32284
+ "Interrupted split is ambiguous or no longer an untouched shell; inspect it before recovery",
30921
32285
  409
30922
32286
  );
30923
- Object.assign(r, {
30924
- paneId: panes[0].pane_id,
30925
- terminalId: panes[0].terminal_id,
30926
- tabId: panes[0].tab_id
30927
- });
32287
+ if (panes.length === 1)
32288
+ Object.assign(r, {
32289
+ paneId: panes[0].pane_id,
32290
+ terminalId: panes[0].terminal_id,
32291
+ tabId: panes[0].tab_id
32292
+ });
32293
+ } else {
32294
+ const tabs = (await h.call("tab.list", { workspace_id: project.workspaceId })).tabs ?? [];
32295
+ const matches = tabs.filter(
32296
+ (tab) => tab.workspace_id === project.workspaceId && tab.label === r.agentName
32297
+ );
32298
+ if (matches.length > 1)
32299
+ throw new AppError(
32300
+ "identity_changed",
32301
+ "Multiple tabs match the interrupted creation; manual inspection is required",
32302
+ 409
32303
+ );
32304
+ if (matches.length === 1) {
32305
+ const panes = ((await h.call("pane.list", { workspace_id: project.workspaceId })).panes ?? []).filter((pane) => pane.tab_id === matches[0].tab_id);
32306
+ if (panes.length !== 1 || panes[0].agent || panes[0].launch_pending || panes[0].workspace_id !== project.workspaceId || panes[0].cwd !== t.cwd)
32307
+ throw new AppError(
32308
+ "identity_changed",
32309
+ "The orphan tab is no longer an untouched shell; inspect it before recovery",
32310
+ 409
32311
+ );
32312
+ Object.assign(r, {
32313
+ paneId: panes[0].pane_id,
32314
+ terminalId: panes[0].terminal_id,
32315
+ tabId: panes[0].tab_id
32316
+ });
32317
+ }
30928
32318
  }
30929
32319
  } else {
30930
32320
  const a = (await h.call("agent.get", { target: r.paneId })).agent;
@@ -30968,7 +32358,7 @@ var Service = class {
30968
32358
  case "decision.record": {
30969
32359
  const c = this.guard(raw.lease), text2 = external_exports.string().min(1).parse(raw.text), rationale = external_exports.string().default("").parse(raw.rationale);
30970
32360
  const d = {
30971
- id: randomUUID5(),
32361
+ id: randomUUID6(),
30972
32362
  projectId: c.projectId,
30973
32363
  text: text2,
30974
32364
  rationale,
@@ -31022,12 +32412,15 @@ var Service = class {
31022
32412
  checks: external_exports.array(checkSchema).min(1).optional()
31023
32413
  }).parse(raw);
31024
32414
  return this.store.transaction(() => {
31025
- const t = this.task(i.taskId);
32415
+ let t = this.task(i.taskId);
32416
+ this.cleanup.assertMutable(t);
31026
32417
  if (t.projectId !== c.projectId)
31027
32418
  throw new AppError("project_mismatch", "Task and lease differ");
31028
32419
  return this.idempotent(c.projectId, "control:" + i.key, i, () => {
31029
32420
  if (terminalStates.has(t.status))
31030
32421
  throw new AppError("task_finished", "Task is already finished", 409);
32422
+ if (t.runId && this.store.get("run", t.runId)?.cleanup?.state === "closed")
32423
+ t = this.updateTask(t, { runId: void 0 });
31031
32424
  if (["redirect", "reply"].includes(i.type) && !i.text?.trim())
31032
32425
  throw new AppError("text_required", "Provide the new instructions or answer");
31033
32426
  if (i.type === "keys" && !i.keys)
@@ -31048,7 +32441,7 @@ var Service = class {
31048
32441
  for (const check of i.checks) if (check.type === "file") safePath(t.cwd, check.path);
31049
32442
  }
31050
32443
  const op = {
31051
- id: randomUUID5(),
32444
+ id: randomUUID6(),
31052
32445
  projectId: t.projectId,
31053
32446
  taskId: t.id,
31054
32447
  type: i.type,
@@ -31105,8 +32498,11 @@ var Service = class {
31105
32498
  const a = assignmentSchema.parse(rawAssignment);
31106
32499
  if (c.projectId !== a.projectId)
31107
32500
  throw new AppError("project_mismatch", "Lease does not match assignment");
31108
- const p = this.project(a.projectId), cwd = realpathSync2(a.cwd ?? p.root);
32501
+ const p = this.project(a.projectId), cwd = realpathSync4(a.cwd ?? p.root);
31109
32502
  const parent = a.parentId ? this.task(a.parentId) : void 0;
32503
+ if (parent) this.cleanup.assertMutable(parent);
32504
+ for (const task of this.store.all("task"))
32505
+ if (task.worktree && inside(task.worktree.path, cwd)) this.cleanup.assertMutable(task);
31110
32506
  const inheritedWorktree = parent?.projectId === p.id && parent.worktree?.state === "ready" && parent.cwd === cwd;
31111
32507
  if (!inside(p.root, cwd) && !inheritedWorktree)
31112
32508
  throw new AppError(
@@ -31114,7 +32510,7 @@ var Service = class {
31114
32510
  "Task directory must be within the registered root. Register external worktrees as projects."
31115
32511
  );
31116
32512
  for (const path of a.ownership) {
31117
- if (a.execution?.mode === "worktree" && isAbsolute2(path))
32513
+ if (a.execution?.mode === "worktree" && isAbsolute4(path))
31118
32514
  throw new AppError(
31119
32515
  "worktree_path",
31120
32516
  "Worktree ownership paths must be relative to the task directory"
@@ -31128,7 +32524,7 @@ var Service = class {
31128
32524
  }
31129
32525
  for (const check of a.checks)
31130
32526
  if (check.type === "file") {
31131
- if (a.execution?.mode === "worktree" && isAbsolute2(check.path))
32527
+ if (a.execution?.mode === "worktree" && isAbsolute4(check.path))
31132
32528
  throw new AppError(
31133
32529
  "worktree_path",
31134
32530
  "Worktree file checks must be relative to the task directory"
@@ -31137,6 +32533,12 @@ var Service = class {
31137
32533
  }
31138
32534
  for (const id of a.dependencies) {
31139
32535
  const d = this.task(id);
32536
+ if (this.cleanup.active(d.id))
32537
+ throw new AppError(
32538
+ "cleanup_busy",
32539
+ "Dependency is being cleaned up; retry after it finishes",
32540
+ 409
32541
+ );
31140
32542
  if (d.projectId !== a.projectId)
31141
32543
  throw new AppError("dependency_scope", "Dependencies must belong to the same project");
31142
32544
  }
@@ -31146,7 +32548,7 @@ var Service = class {
31146
32548
  const task = {
31147
32549
  ...fields,
31148
32550
  cwd,
31149
- id: randomUUID5(),
32551
+ id: randomUUID6(),
31150
32552
  status: a.deferStart ? "paused" : "queued",
31151
32553
  revision: 1,
31152
32554
  attempt: 0,
@@ -31168,6 +32570,7 @@ var Service = class {
31168
32570
  }
31169
32571
  workerGuard(taskId, token, revision) {
31170
32572
  const t = this.task(taskId), r = t.runId ? this.store.get("run", t.runId) : void 0;
32573
+ this.cleanup.assertMutable(t);
31171
32574
  if (!r || hash(token) !== r.tokenHash)
31172
32575
  throw new AppError("worker_auth", "Invalid or obsolete worker token", 401);
31173
32576
  if (terminalStates.has(t.status) || revision !== t.revision || r.phase === "stopped")
@@ -31185,6 +32588,7 @@ var Service = class {
31185
32588
  }).parse(raw);
31186
32589
  return this.store.transaction(() => {
31187
32590
  const t = this.task(taskId), r = t.runId ? this.store.get("run", t.runId) : void 0;
32591
+ this.cleanup.assertMutable(t);
31188
32592
  if (!r || hash(token) !== r.tokenHash)
31189
32593
  throw new AppError("worker_auth", "Invalid or obsolete worker token", 401);
31190
32594
  if (terminalStates.has(t.status) || i.revision !== t.revision)
@@ -31254,6 +32658,52 @@ var Service = class {
31254
32658
  }
31255
32659
  };
31256
32660
 
32661
+ // src/worker-layout.ts
32662
+ async function planWorkerPane(h, workspaceId, runs) {
32663
+ const panes = (await h.call("pane.list", { workspace_id: workspaceId })).panes;
32664
+ if (!Array.isArray(panes)) throw new Error("Herdr did not return workspace panes");
32665
+ const candidates = runs.filter(
32666
+ (r) => r.terminalScope === "pane" && r.paneId && r.tabId && !r.cleanup && r.phase !== "creating"
32667
+ );
32668
+ for (const tabId of new Set(candidates.map((r) => r.tabId))) {
32669
+ if (runs.some((r) => r.phase === "creating" && r.creation?.tabId === tabId)) continue;
32670
+ const members = panes.filter((p) => p.workspace_id === workspaceId && p.tab_id === tabId);
32671
+ if (!members.length || members.length >= 4) continue;
32672
+ if (!members.every(
32673
+ (p) => candidates.some(
32674
+ (r) => r.paneId === p.pane_id && r.terminalId === p.terminal_id && r.tabId === p.tab_id
32675
+ )
32676
+ ))
32677
+ continue;
32678
+ const layout = (await h.call("pane.layout", { pane_id: members[0].pane_id })).layout;
32679
+ if (layout?.workspace_id !== workspaceId || layout.tab_id !== tabId || !Array.isArray(layout.panes))
32680
+ continue;
32681
+ const choices = layout.panes.flatMap((p) => {
32682
+ if (!members.some((m) => m.pane_id === p.pane_id)) return [];
32683
+ const { width, height } = p.rect;
32684
+ const right = width >= 121 && height >= 12;
32685
+ const down = height >= 25 && width >= 60;
32686
+ if (!right && !down) return [];
32687
+ return [
32688
+ {
32689
+ paneId: p.pane_id,
32690
+ area: width * height,
32691
+ direction: right && (!down || width >= height * 3) ? "right" : "down"
32692
+ }
32693
+ ];
32694
+ }).sort((a, b) => b.area - a.area);
32695
+ if (choices.length)
32696
+ return {
32697
+ mode: "pane",
32698
+ tabId,
32699
+ targetPaneId: choices[0].paneId,
32700
+ direction: choices[0].direction,
32701
+ beforePaneIds: panes.map((p) => p.pane_id)
32702
+ };
32703
+ }
32704
+ return { mode: "tab" };
32705
+ }
32706
+
31257
32707
  // src/prompts.ts
31258
32708
  var leadContract = `Own the user's outcome through verified completion. Establish a persistent outcome with objective, explicit scope, observable criteria and required evidence before dispatch. Use criteria suited to the work: executable behavior checks for software, corroborated sources and traceable findings for research, reproducible calculations for analysis, and documented alternatives, tradeoffs and unresolved disagreements for decisions. Ask for clarification only when the evidence cannot resolve a necessary ambiguity.
31259
32709
  Choose exact available model profiles and an appropriate bounded collaboration strategy. Preserve a user's explicit model choice; if unavailable, report it and make any proposed fallback explicit. Stage independent council, debate or competing-proposal participants with deferStart: true, then use strategy_create to establish common evaluation criteria and release their initial assessments. Do not contaminate initial assessments by sharing another participant's conclusions. Use verified participant results in synthesis and preserve material disagreements. For later debate rounds, obtain fresh verified participant revisions before contributing their rebuttals.
@@ -31272,39 +32722,39 @@ var strategyInstructions = {
31272
32722
 
31273
32723
  // src/agy-trust.ts
31274
32724
  import {
31275
- existsSync as existsSync2,
31276
- mkdirSync as mkdirSync4,
31277
- openSync,
31278
- closeSync,
31279
- readFileSync as readFileSync4,
31280
- writeFileSync as writeFileSync3,
32725
+ existsSync as existsSync4,
32726
+ mkdirSync as mkdirSync6,
32727
+ openSync as openSync2,
32728
+ closeSync as closeSync2,
32729
+ readFileSync as readFileSync5,
32730
+ writeFileSync as writeFileSync4,
31281
32731
  renameSync,
31282
32732
  unlinkSync,
31283
- statSync as statSync3,
31284
- realpathSync as realpathSync3
32733
+ statSync as statSync5,
32734
+ realpathSync as realpathSync5
31285
32735
  } from "node:fs";
31286
- import { dirname as dirname4, resolve as resolve5 } from "node:path";
32736
+ import { dirname as dirname6, resolve as resolve7 } from "node:path";
31287
32737
  import { homedir as homedir2 } from "node:os";
31288
- import { randomUUID as randomUUID6 } from "node:crypto";
32738
+ import { randomUUID as randomUUID7 } from "node:crypto";
31289
32739
  function agySettingsPath() {
31290
- return resolve5(
31291
- process.env.MARIONETTE_AGY_SETTINGS ?? resolve5(homedir2(), ".gemini/antigravity-cli/settings.json")
32740
+ return resolve7(
32741
+ process.env.MARIONETTE_AGY_SETTINGS ?? resolve7(homedir2(), ".gemini/antigravity-cli/settings.json")
31292
32742
  );
31293
32743
  }
31294
32744
  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 });
32745
+ root = realpathSync5(root);
32746
+ if (!statSync5(root).isDirectory()) throw new Error("AGY trust requires a directory");
32747
+ mkdirSync6(dirname6(settingsPath), { recursive: true, mode: 448 });
31298
32748
  const lock = settingsPath + ".marionette.lock";
31299
32749
  let fd;
31300
32750
  try {
31301
- fd = openSync(lock, "wx", 384);
32751
+ fd = openSync2(lock, "wx", 384);
31302
32752
  } catch {
31303
32753
  throw new Error(`AGY settings are being edited. Retry after checking ${lock}`);
31304
32754
  }
31305
- const temp = settingsPath + "." + randomUUID6() + ".tmp";
32755
+ const temp = settingsPath + "." + randomUUID7() + ".tmp";
31306
32756
  try {
31307
- const before = existsSync2(settingsPath) ? readFileSync4(settingsPath, "utf8") : null;
32757
+ const before = existsSync4(settingsPath) ? readFileSync5(settingsPath, "utf8") : null;
31308
32758
  const settings = before === null ? {} : JSON.parse(before);
31309
32759
  if (!settings || typeof settings !== "object" || Array.isArray(settings))
31310
32760
  throw new Error("AGY settings must be a JSON object");
@@ -31313,118 +32763,23 @@ function trustAgyWorkspace(root, settingsPath = agySettingsPath()) {
31313
32763
  throw new Error("AGY trustedWorkspaces must be a string array");
31314
32764
  if (trusted.includes(root)) return { changed: false, root, settingsPath };
31315
32765
  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;
32766
+ const mode = before === null ? 384 : statSync5(settingsPath).mode & 511;
32767
+ writeFileSync4(temp, JSON.stringify(settings, null, 2) + "\n", { flag: "wx", mode });
32768
+ const current = existsSync4(settingsPath) ? readFileSync5(settingsPath, "utf8") : null;
31319
32769
  if (current !== before) throw new Error("AGY settings changed during registration; retry");
31320
32770
  renameSync(temp, settingsPath);
31321
32771
  return { changed: true, root, settingsPath };
31322
32772
  } finally {
31323
- if (existsSync2(temp)) unlinkSync(temp);
31324
- closeSync(fd);
32773
+ if (existsSync4(temp)) unlinkSync(temp);
32774
+ closeSync2(fd);
31325
32775
  unlinkSync(lock);
31326
32776
  }
31327
32777
  }
31328
32778
 
31329
32779
  // 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
32780
+ import { randomBytes as randomBytes3, randomUUID as randomUUID8 } from "node:crypto";
32781
+ import { existsSync as existsSync5, readFileSync as readFileSync6, realpathSync as realpathSync6 } from "node:fs";
32782
+ import { dirname as dirname7, resolve as resolve8 } from "node:path";
31428
32783
  var settled = (status) => status === "idle" || status === "done";
31429
32784
  var quote = (s) => "'" + s.replaceAll("'", "'\\''") + "'";
31430
32785
  function inputScreen(text2) {
@@ -31447,9 +32802,26 @@ var Supervisor = class {
31447
32802
  timer;
31448
32803
  busy = /* @__PURE__ */ new Set();
31449
32804
  stopped = false;
32805
+ layouts = /* @__PURE__ */ new Map();
32806
+ async withLayout(projectId, action) {
32807
+ const previous = this.layouts.get(projectId) ?? Promise.resolve();
32808
+ let release;
32809
+ const pending = new Promise((resolve13) => {
32810
+ release = resolve13;
32811
+ });
32812
+ this.layouts.set(projectId, pending);
32813
+ await previous;
32814
+ try {
32815
+ return await action();
32816
+ } finally {
32817
+ release();
32818
+ if (this.layouts.get(projectId) === pending) this.layouts.delete(projectId);
32819
+ }
32820
+ }
31450
32821
  start() {
31451
32822
  this.recover();
31452
32823
  this.service.continuation.recover();
32824
+ this.service.cleanup.recover();
31453
32825
  this.timer = setInterval(() => this.tick(), this.pollMs);
31454
32826
  this.tick();
31455
32827
  }
@@ -31457,6 +32829,7 @@ var Supervisor = class {
31457
32829
  this.stopped = true;
31458
32830
  if (this.timer) clearInterval(this.timer);
31459
32831
  await this.service.continuation.stop();
32832
+ await this.service.cleanup.stop();
31460
32833
  while (this.busy.size) await new Promise((r) => setTimeout(r, 50));
31461
32834
  }
31462
32835
  recover() {
@@ -31532,9 +32905,10 @@ var Supervisor = class {
31532
32905
  if (this.stopped) return;
31533
32906
  this.service.orchestration.refreshEvidence();
31534
32907
  this.service.continuation.tick();
32908
+ this.service.cleanup.tick();
31535
32909
  const s = this.service, all = s.store.all("task");
31536
32910
  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"))
32911
+ 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
32912
  this.launch(t.id, () => this.monitor(t.id));
31539
32913
  for (const t of s.store.all("task").filter(
31540
32914
  (t2) => t2.status === "waiting" || t2.resumePending && t2.status === "blocked" && t2.blockKind === "missing-report"
@@ -31551,7 +32925,9 @@ var Supervisor = class {
31551
32925
  }
31552
32926
  for (const project of s.store.all("project")) {
31553
32927
  const tasks = s.tasks(project.id);
31554
- for (const task of tasks.filter((t) => t.status === "queued" && !this.busy.has(t.id))) {
32928
+ for (const task of tasks.filter(
32929
+ (t) => t.status === "queued" && !t.archiveId && !this.service.cleanup.active(t.id) && !this.busy.has(t.id)
32930
+ )) {
31555
32931
  const dependencies = task.dependencies.map((id) => s.task(id));
31556
32932
  let waitReason = dependencies.some((d) => d.status !== "completed") ? "Waiting for dependencies to complete" : void 0;
31557
32933
  if (!waitReason) waitReason = s.orchestration.capacity(task, (t) => !!this.holds(t));
@@ -31575,10 +32951,10 @@ var Supervisor = class {
31575
32951
  return !terminalStates.has(t.status) || t.runId && this.run(t)?.phase !== "stopped";
31576
32952
  }
31577
32953
  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);
32954
+ const root = (t) => t.worktree?.cwd ?? (t.execution?.mode === "worktree" ? resolve8(dirname7(this.service.store.path), "worktrees", t.projectId, t.id) : t.cwd);
31579
32955
  const path = (t, owned) => {
31580
32956
  const cwd = root(t);
31581
- return t.execution?.mode === "worktree" && !existsSync4(cwd) ? resolve7(cwd, owned) : safePath(cwd, owned);
32957
+ return t.execution?.mode === "worktree" && !existsSync5(cwd) ? resolve8(cwd, owned) : safePath(cwd, owned);
31582
32958
  };
31583
32959
  return a.ownership.some(
31584
32960
  (x) => b.ownership.some((y) => {
@@ -31684,7 +33060,7 @@ Report format: {"revision":${t.revision},"type":"complete","summary":"What chang
31684
33060
  try {
31685
33061
  if (t.execution?.mode === "worktree") {
31686
33062
  if (!t.worktree) {
31687
- const plan = await planWorktree(t, realpathSync5(dirname6(s.store.path)));
33063
+ const plan = await planWorktree(t, realpathSync6(dirname7(s.store.path)));
31688
33064
  if (s.task(id).status !== "preparing") return;
31689
33065
  t = s.updateTask(t, { worktree: plan }, "Planned isolated Git worktree");
31690
33066
  }
@@ -31748,7 +33124,7 @@ ${JSON.stringify(children.map((child) => ({ id: child.id, title: child.title, st
31748
33124
  }
31749
33125
  const token = randomBytes3(32).toString("hex");
31750
33126
  const run = {
31751
- id: randomUUID7(),
33127
+ id: randomUUID8(),
31752
33128
  taskId: id,
31753
33129
  attempt: t.attempt + 1,
31754
33130
  revision: t.revision,
@@ -31775,23 +33151,38 @@ ${JSON.stringify(children.map((child) => ({ id: child.id, title: child.title, st
31775
33151
  t = s.updateTask(s.task(id), { runId: run.id, attempt: run.attempt });
31776
33152
  });
31777
33153
  try {
31778
- const created = await h.call("tab.create", {
31779
- workspace_id: p.workspaceId,
31780
- cwd: t.cwd,
31781
- label: run.agentName,
31782
- focus: false,
31783
- env: { MARIONETTE_WORKER_TOKEN: token, MARIONETTE_TASK_ID: id, MARIONETTE_URL: this.url }
31784
- });
31785
- const pane = created.root_pane;
31786
- if (!pane?.pane_id || pane.workspace_id !== p.workspaceId)
31787
- throw new Error("Herdr did not return the expected scoped root pane");
31788
- Object.assign(run, {
31789
- paneId: pane.pane_id,
31790
- terminalId: pane.terminal_id,
31791
- tabId: pane.tab_id,
31792
- phase: "starting"
33154
+ await this.withLayout(p.id, async () => {
33155
+ const taskIds = new Set(s.tasks(p.id).map((task) => task.id));
33156
+ const runs = s.store.all("run").filter((r) => taskIds.has(r.taskId) && !s.cleanup.active(r.taskId));
33157
+ run.creation = await planWorkerPane(h, p.workspaceId, runs);
33158
+ run.terminalScope = "pane";
33159
+ this.saveRun(run);
33160
+ const options = {
33161
+ workspace_id: p.workspaceId,
33162
+ cwd: t.cwd,
33163
+ focus: false,
33164
+ env: { MARIONETTE_WORKER_TOKEN: token, MARIONETTE_TASK_ID: id, MARIONETTE_URL: this.url }
33165
+ };
33166
+ const splitting = run.creation.mode === "pane";
33167
+ const created = await h.call(
33168
+ splitting ? "pane.split" : "tab.create",
33169
+ splitting ? {
33170
+ ...options,
33171
+ target_pane_id: run.creation.targetPaneId,
33172
+ direction: run.creation.direction
33173
+ } : { ...options, label: run.agentName }
33174
+ );
33175
+ const pane = splitting ? created.pane : created.root_pane;
33176
+ if (!pane?.pane_id || !pane.terminal_id || !pane.tab_id || pane.workspace_id !== p.workspaceId || splitting && (pane.tab_id !== run.creation.tabId || run.creation.beforePaneIds.includes(pane.pane_id)))
33177
+ throw new Error("Herdr did not return the expected new scoped pane");
33178
+ Object.assign(run, {
33179
+ paneId: pane.pane_id,
33180
+ terminalId: pane.terminal_id,
33181
+ tabId: pane.tab_id,
33182
+ phase: "starting"
33183
+ });
33184
+ this.saveRun(run);
31793
33185
  });
31794
- this.saveRun(run);
31795
33186
  for (let attempt = 0; ; attempt++) {
31796
33187
  try {
31797
33188
  await h.call(
@@ -31808,7 +33199,7 @@ ${JSON.stringify(children.map((child) => ({ id: child.id, title: child.title, st
31808
33199
  break;
31809
33200
  } catch (error) {
31810
33201
  if (/not an available shell/.test(String(error)) && attempt < 10) {
31811
- await new Promise((resolve12) => setTimeout(resolve12, 500));
33202
+ await new Promise((resolve13) => setTimeout(resolve13, 500));
31812
33203
  continue;
31813
33204
  }
31814
33205
  if (["agent_not_ready", "agent_not_found"].includes(error.code)) break;
@@ -31822,7 +33213,7 @@ ${JSON.stringify(children.map((child) => ({ id: child.id, title: child.title, st
31822
33213
  } catch (error) {
31823
33214
  if (!["agent_not_ready", "agent_not_found"].includes(error.code))
31824
33215
  throw error;
31825
- await new Promise((resolve12) => setTimeout(resolve12, 500));
33216
+ await new Promise((resolve13) => setTimeout(resolve13, 500));
31826
33217
  continue;
31827
33218
  }
31828
33219
  const read = await h.call("pane.read", {
@@ -31838,7 +33229,7 @@ ${JSON.stringify(children.map((child) => ({ id: child.id, title: child.title, st
31838
33229
  if (a.agent === run.kind && settled(a.agent_status) && !a.launch_pending && a.interactive_ready !== false)
31839
33230
  break;
31840
33231
  a = void 0;
31841
- await new Promise((resolve12) => setTimeout(resolve12, 500));
33232
+ await new Promise((resolve13) => setTimeout(resolve13, 500));
31842
33233
  }
31843
33234
  if (!a) throw new AppError("startup_timeout", "Agent did not become ready within 30 seconds");
31844
33235
  run.nativeSession = a.agent_session?.value;
@@ -32171,7 +33562,7 @@ Ownership remains ${t.ownership.join(", ")}. Current acceptance checks: ${JSON.s
32171
33562
  if (!fileDigest) throw new Error("Artifact is not a regular file");
32172
33563
  if (!check.allowUnchanged && fileDigest === r.baseline[check.path])
32173
33564
  throw new Error("Artifact is unchanged from before dispatch");
32174
- if (check.contains !== void 0 && !readFileSync5(path, "utf8").includes(check.contains))
33565
+ if (check.contains !== void 0 && !readFileSync6(path, "utf8").includes(check.contains))
32175
33566
  throw new Error("Artifact does not contain the expected content");
32176
33567
  if (check.sha256 && check.sha256 !== fileDigest)
32177
33568
  throw new Error("Artifact SHA-256 does not match");
@@ -32211,14 +33602,14 @@ ${result.output}`;
32211
33602
  };
32212
33603
 
32213
33604
  // src/version.ts
32214
- var VERSION = "0.2.0";
33605
+ var VERSION = "0.2.2";
32215
33606
  var SETUP_VERSION = 2;
32216
33607
 
32217
33608
  // src/server.ts
32218
33609
  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"));
33610
+ const config = initConfig(home2, port), lock = resolve9(home2, "supervisor.lock"), lockId = randomUUID9();
33611
+ if (existsSync6(lock)) {
33612
+ const old = JSON.parse(readFileSync7(lock, "utf8"));
32222
33613
  try {
32223
33614
  process.kill(old.pid, 0);
32224
33615
  throw new AppError(
@@ -32231,13 +33622,13 @@ async function serve(home2, port) {
32231
33622
  unlinkSync2(lock);
32232
33623
  }
32233
33624
  }
32234
- writeFileSync4(lock, JSON.stringify({ pid: process.pid, id: lockId }), {
33625
+ writeFileSync5(lock, JSON.stringify({ pid: process.pid, id: lockId }), {
32235
33626
  flag: "wx",
32236
33627
  mode: 384
32237
33628
  });
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");
33629
+ const store = new Store(resolve9(home2, "state.sqlite")), service = new Service(store);
33630
+ const projectRoot = resolve9(dirname8(fileURLToPath(import.meta.url)), "..");
33631
+ const cliPath = resolve9(projectRoot, "dist/cli.js");
32241
33632
  const url = `http://127.0.0.1:${config.port}`;
32242
33633
  const supervisor = new Supervisor(service, url, cliPath);
32243
33634
  const app = (0, import_express.default)();
@@ -32312,7 +33703,7 @@ async function serve(home2, port) {
32312
33703
  res.json({ stopping: true });
32313
33704
  setTimeout(() => void shutdown(), 20);
32314
33705
  });
32315
- app.use(import_express.default.static(resolve8(projectRoot, "public"), { index: "index.html", maxAge: 0 }));
33706
+ app.use(import_express.default.static(resolve9(projectRoot, "public"), { index: "index.html", maxAge: 0 }));
32316
33707
  app.use(
32317
33708
  (error, req, res, _next) => {
32318
33709
  const status = error instanceof AppError ? error.status : error instanceof ZodError ? 400 : 500;
@@ -32342,7 +33733,7 @@ async function serve(home2, port) {
32342
33733
  server.closeIdleConnections();
32343
33734
  });
32344
33735
  store.close();
32345
- if (existsSync5(lock) && JSON.parse(readFileSync6(lock, "utf8")).id === lockId) unlinkSync2(lock);
33736
+ if (existsSync6(lock) && JSON.parse(readFileSync7(lock, "utf8")).id === lockId) unlinkSync2(lock);
32346
33737
  }
32347
33738
  process.once("SIGTERM", () => void shutdown());
32348
33739
  process.once("SIGINT", () => void shutdown());
@@ -32354,51 +33745,51 @@ async function serve(home2, port) {
32354
33745
  }
32355
33746
 
32356
33747
  // src/runtime.ts
32357
- import { createHash as createHash2, randomUUID as randomUUID9 } from "node:crypto";
33748
+ import { createHash as createHash3, randomUUID as randomUUID10 } from "node:crypto";
32358
33749
  import {
32359
33750
  cpSync,
32360
- existsSync as existsSync6,
32361
- mkdirSync as mkdirSync6,
32362
- readFileSync as readFileSync7,
33751
+ existsSync as existsSync7,
33752
+ mkdirSync as mkdirSync7,
33753
+ readFileSync as readFileSync8,
32363
33754
  readdirSync,
32364
33755
  renameSync as renameSync2,
32365
33756
  rmSync,
32366
- writeFileSync as writeFileSync5
33757
+ writeFileSync as writeFileSync6
32367
33758
  } from "node:fs";
32368
- import { dirname as dirname8, resolve as resolve9, relative as relative4 } from "node:path";
33759
+ import { dirname as dirname9, resolve as resolve10, relative as relative4 } from "node:path";
32369
33760
  import { fileURLToPath as fileURLToPath2 } from "node:url";
32370
- var packageRoot = resolve9(dirname8(fileURLToPath2(import.meta.url)), "..");
33761
+ var packageRoot = resolve10(dirname9(fileURLToPath2(import.meta.url)), "..");
32371
33762
  function installRuntime(home2, source = packageRoot) {
32372
33763
  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");
33764
+ if (existsSync7(resolve10(source, "THIRD_PARTY_NOTICES.md"))) files.push("THIRD_PARTY_NOTICES.md");
32374
33765
  function walk(dir) {
32375
- for (const entry of readdirSync(resolve9(source, dir), { withFileTypes: true })) {
33766
+ for (const entry of readdirSync(resolve10(source, dir), { withFileTypes: true })) {
32376
33767
  const path = dir + "/" + entry.name;
32377
33768
  entry.isDirectory() ? walk(path) : files.push(path);
32378
33769
  }
32379
33770
  }
32380
- if (!existsSync6(resolve9(source, "dist/cli.js")) || !existsSync6(resolve9(source, "public/index.html")))
33771
+ if (!existsSync7(resolve10(source, "dist/cli.js")) || !existsSync7(resolve10(source, "public/index.html")))
32381
33772
  throw new Error("Build Marionette first with npm run build");
32382
33773
  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));
33774
+ const digest2 = createHash3("sha256");
33775
+ for (const file of files.sort()) digest2.update(file).update(readFileSync8(resolve10(source, file)));
33776
+ const version = JSON.parse(readFileSync8(resolve10(source, "package.json"), "utf8")).version;
33777
+ const target = resolve10(home2, "runtimes", version + "-" + digest2.digest("hex").slice(0, 16));
32387
33778
  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 });
33779
+ if (existsSync7(resolve10(target, ".complete"))) return target;
33780
+ mkdirSync7(dirname9(target), { recursive: true, mode: 448 });
33781
+ const temp = target + "." + randomUUID10();
33782
+ mkdirSync7(temp, { mode: 448 });
32392
33783
  try {
32393
33784
  for (const file of files) {
32394
- mkdirSync6(dirname8(resolve9(temp, file)), { recursive: true });
32395
- cpSync(resolve9(source, file), resolve9(temp, file));
33785
+ mkdirSync7(dirname9(resolve10(temp, file)), { recursive: true });
33786
+ cpSync(resolve10(source, file), resolve10(temp, file));
32396
33787
  }
32397
- writeFileSync5(resolve9(temp, ".complete"), "1\n");
33788
+ writeFileSync6(resolve10(temp, ".complete"), "1\n");
32398
33789
  try {
32399
33790
  renameSync2(temp, target);
32400
33791
  } catch (e) {
32401
- if (!existsSync6(resolve9(target, ".complete"))) throw e;
33792
+ if (!existsSync7(resolve10(target, ".complete"))) throw e;
32402
33793
  }
32403
33794
  } finally {
32404
33795
  rmSync(temp, { recursive: true, force: true });
@@ -32408,20 +33799,20 @@ function installRuntime(home2, source = packageRoot) {
32408
33799
 
32409
33800
  // src/setup.ts
32410
33801
  import {
32411
- existsSync as existsSync7,
32412
- mkdirSync as mkdirSync7,
32413
- readFileSync as readFileSync8,
32414
- writeFileSync as writeFileSync6,
33802
+ existsSync as existsSync8,
33803
+ mkdirSync as mkdirSync8,
33804
+ readFileSync as readFileSync9,
33805
+ writeFileSync as writeFileSync7,
32415
33806
  renameSync as renameSync3,
32416
33807
  unlinkSync as unlinkSync3,
32417
- openSync as openSync2,
32418
- closeSync as closeSync2,
32419
- realpathSync as realpathSync6,
32420
- statSync as statSync5
33808
+ openSync as openSync3,
33809
+ closeSync as closeSync3,
33810
+ realpathSync as realpathSync7,
33811
+ statSync as statSync6
32421
33812
  } from "node:fs";
32422
- import { basename, dirname as dirname9, resolve as resolve10 } from "node:path";
33813
+ import { basename, dirname as dirname10, resolve as resolve11 } from "node:path";
32423
33814
  import { homedir as homedir3 } from "node:os";
32424
- import { createHash as createHash3, randomUUID as randomUUID10 } from "node:crypto";
33815
+ import { createHash as createHash4, randomUUID as randomUUID11 } from "node:crypto";
32425
33816
  import { createInterface } from "node:readline/promises";
32426
33817
  import { execFile as execFile2, spawn as spawn3 } from "node:child_process";
32427
33818
  import { promisify as promisify2 } from "node:util";
@@ -32445,22 +33836,22 @@ var setupSchema = external_exports.object({
32445
33836
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
32446
33837
  var quote2 = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
32447
33838
  function privateJson(path, data) {
32448
- mkdirSync7(dirname9(path), { recursive: true, mode: 448 });
32449
- const temp = path + "." + randomUUID10() + ".tmp";
33839
+ mkdirSync8(dirname10(path), { recursive: true, mode: 448 });
33840
+ const temp = path + "." + randomUUID11() + ".tmp";
32450
33841
  try {
32451
- writeFileSync6(temp, JSON.stringify(data, null, 2) + "\n", { mode: 384, flag: "wx" });
33842
+ writeFileSync7(temp, JSON.stringify(data, null, 2) + "\n", { mode: 384, flag: "wx" });
32452
33843
  renameSync3(temp, path);
32453
33844
  } finally {
32454
- if (existsSync7(temp)) unlinkSync3(temp);
33845
+ if (existsSync8(temp)) unlinkSync3(temp);
32455
33846
  }
32456
33847
  }
32457
33848
  function setupPlan(input) {
32458
33849
  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;
33850
+ const root = realpathSync7(resolve11(supplied.project ?? process.cwd()));
33851
+ if (!statSync6(root).isDirectory()) throw new Error("Project must be an existing directory");
33852
+ const hash2 = createHash4("sha256").update(root).digest("hex").slice(0, 10);
33853
+ const bindingPath = resolve11(root, ".marionette/project.json");
33854
+ const binding = existsSync8(bindingPath) ? JSON.parse(readFileSync9(bindingPath, "utf8")) : null;
32464
33855
  const options = setupSchema.parse({
32465
33856
  lead: binding?.lead,
32466
33857
  leadName: binding?.leadName,
@@ -32469,13 +33860,13 @@ function setupPlan(input) {
32469
33860
  mcp: binding?.mcp,
32470
33861
  ...supplied
32471
33862
  });
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"))
33863
+ const legacy = resolve11(root, ".marionette");
33864
+ const home2 = resolve11(
33865
+ 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
33866
  );
32476
33867
  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")
33868
+ const socket = resolve11(
33869
+ options.socket ?? binding?.socket ?? resolve11(homedir3(), ".config/herdr/sessions", session, "herdr.sock")
32479
33870
  );
32480
33871
  return {
32481
33872
  ...options,
@@ -32546,11 +33937,11 @@ async function ensureHerdr(plan) {
32546
33937
  return h;
32547
33938
  } catch {
32548
33939
  }
32549
- if (plan.socket !== resolve10(homedir3(), ".config/herdr/sessions", plan.session, "herdr.sock"))
33940
+ if (plan.socket !== resolve11(homedir3(), ".config/herdr/sessions", plan.session, "herdr.sock"))
32550
33941
  throw new Error(
32551
33942
  `Cannot connect to supplied socket ${plan.socket}; start its Herdr session first`
32552
33943
  );
32553
- const log = openSync2(resolve10(plan.home, "herdr.log"), "a", 384);
33944
+ const log = openSync3(resolve11(plan.home, "herdr.log"), "a", 384);
32554
33945
  const child = spawn3("herdr", ["--session", plan.session, "server"], {
32555
33946
  detached: true,
32556
33947
  stdio: ["ignore", log, log]
@@ -32559,7 +33950,7 @@ async function ensureHerdr(plan) {
32559
33950
  child.on("error", (e) => {
32560
33951
  error = e;
32561
33952
  });
32562
- closeSync2(log);
33953
+ closeSync3(log);
32563
33954
  child.unref();
32564
33955
  for (let n = 0; n < 100; n++) {
32565
33956
  if (error) throw error;
@@ -32570,14 +33961,14 @@ async function ensureHerdr(plan) {
32570
33961
  }
32571
33962
  await sleep(100);
32572
33963
  }
32573
- throw new Error(`Herdr session did not start; inspect ${resolve10(plan.home, "herdr.log")}`);
33964
+ throw new Error(`Herdr session did not start; inspect ${resolve11(plan.home, "herdr.log")}`);
32574
33965
  }
32575
33966
  function mcpCommand(agent, name, runtime, home2) {
32576
33967
  const binary = agent === "codex-desktop" ? "codex" : agent;
32577
33968
  const serverArgs = [
32578
33969
  process.execPath,
32579
33970
  "--no-warnings",
32580
- resolve10(runtime, "dist/mcp.js"),
33971
+ resolve11(runtime, "dist/mcp.js"),
32581
33972
  "--home",
32582
33973
  home2
32583
33974
  ];
@@ -32590,7 +33981,7 @@ function mcpCommand(agent, name, runtime, home2) {
32590
33981
  };
32591
33982
  }
32592
33983
  async function installMcp(command2, name, home2) {
32593
- const receipt = resolve10(home2, "clients", command2.binary + ".json");
33984
+ const receipt = resolve11(home2, "clients", command2.binary + ".json");
32594
33985
  let found;
32595
33986
  try {
32596
33987
  const output = (await exec2(
@@ -32605,14 +33996,14 @@ async function installMcp(command2, name, home2) {
32605
33996
  }
32606
33997
  if (found) {
32607
33998
  if (found.includes(command2.server.args[1]) && found.includes(home2)) return "already-configured";
32608
- const old = existsSync7(receipt) ? JSON.parse(readFileSync8(receipt, "utf8")) : null;
33999
+ const old = existsSync8(receipt) ? JSON.parse(readFileSync9(receipt, "utf8")) : null;
32609
34000
  if (!old || old.name !== name || !found.includes(old.runtime) || !found.includes(home2))
32610
34001
  throw new Error(
32611
34002
  `MCP server ${name} already has another configuration. Use --mcp print to review it.`
32612
34003
  );
32613
34004
  }
32614
34005
  await exec2(command2.binary, command2.args, { timeout: 3e4 });
32615
- privateJson(receipt, { name, runtime: dirname9(dirname9(command2.server.args[1])) });
34006
+ privateJson(receipt, { name, runtime: dirname10(dirname10(command2.server.args[1])) });
32616
34007
  return "installed";
32617
34008
  }
32618
34009
  function leadPrompt(projectId, leadName, leasePath) {
@@ -32631,18 +34022,18 @@ async function runSetup(input) {
32631
34022
  }).catch(() => {
32632
34023
  throw new Error(`Install the selected lead CLI first, or use --mcp print.`);
32633
34024
  });
32634
- mkdirSync7(p.home, { recursive: true, mode: 448 });
32635
- const lockPath = resolve10(p.home, "setup.lock");
34025
+ mkdirSync8(p.home, { recursive: true, mode: 448 });
34026
+ const lockPath = resolve11(p.home, "setup.lock");
32636
34027
  let lock;
32637
34028
  try {
32638
- lock = openSync2(lockPath, "wx", 384);
32639
- writeFileSync6(lock, JSON.stringify({ pid: process.pid }));
34029
+ lock = openSync3(lockPath, "wx", 384);
34030
+ writeFileSync7(lock, JSON.stringify({ pid: process.pid }));
32640
34031
  } catch {
32641
34032
  throw new Error(`Another setup may be running. Check ${lockPath} before retrying.`);
32642
34033
  }
32643
34034
  try {
32644
34035
  const runtime = installRuntime(p.home);
32645
- if (!existsSync7(resolve10(p.home, "config.json")))
34036
+ if (!existsSync8(resolve11(p.home, "config.json")))
32646
34037
  initConfig(p.home, await availablePort(p.port));
32647
34038
  const config = loadConfig(p.home);
32648
34039
  if (p.port && config.port !== p.port)
@@ -32651,7 +34042,7 @@ async function runSetup(input) {
32651
34042
  );
32652
34043
  await exec2(
32653
34044
  process.execPath,
32654
- ["--no-warnings", resolve10(runtime, "dist/cli.js"), "start", "--home", p.home],
34045
+ ["--no-warnings", resolve11(runtime, "dist/cli.js"), "start", "--home", p.home],
32655
34046
  { timeout: 15e3 }
32656
34047
  );
32657
34048
  const health = await (await fetch(`http://127.0.0.1:${config.port}/health`, { signal: AbortSignal.timeout(3e3) })).json();
@@ -32672,9 +34063,9 @@ async function runSetup(input) {
32672
34063
  }
32673
34064
  await h.call("workspace.get", { workspace_id: workspaceId });
32674
34065
  privateJson(
32675
- resolve10(
34066
+ resolve11(
32676
34067
  p.home,
32677
- "setup-project-" + createHash3("sha256").update(p.root).digest("hex").slice(0, 10) + ".json"
34068
+ "setup-project-" + createHash4("sha256").update(p.root).digest("hex").slice(0, 10) + ".json"
32678
34069
  ),
32679
34070
  { root: p.root, session: p.session, socket: p.socket, workspace: workspaceId }
32680
34071
  );
@@ -32686,8 +34077,8 @@ async function runSetup(input) {
32686
34077
  workspaceId,
32687
34078
  trustAgyWorkspaces: p.trustAgy
32688
34079
  });
32689
- const leasePath = resolve10(p.home, "leads", project.id + ".json");
32690
- let lease = existsSync7(leasePath) ? credentialsSchema.parse(JSON.parse(readFileSync8(leasePath, "utf8"))) : null;
34080
+ const leasePath = resolve11(p.home, "leads", project.id + ".json");
34081
+ let lease = existsSync8(leasePath) ? credentialsSchema.parse(JSON.parse(readFileSync9(leasePath, "utf8"))) : null;
32691
34082
  const briefing = await call(p.home, "project.briefing", { projectId: project.id });
32692
34083
  const same = briefing.lead?.owner === p.leadName && briefing.lead?.agent === p.lead;
32693
34084
  if (briefing.lead && !(same && lease && lease.epoch === briefing.lead.epoch && lease.owner === briefing.lead.owner) && !p.takeover)
@@ -32724,15 +34115,15 @@ async function runSetup(input) {
32724
34115
  mcp: p.mcp
32725
34116
  };
32726
34117
  privateJson(p.bindingPath, binding);
32727
- const ignore = resolve10(dirname9(p.bindingPath), ".gitignore");
32728
- if (!existsSync7(ignore)) writeFileSync6(ignore, "*\n", { flag: "wx" });
34118
+ const ignore = resolve11(dirname10(p.bindingPath), ".gitignore");
34119
+ if (!existsSync8(ignore)) writeFileSync7(ignore, "*\n", { flag: "wx" });
32729
34120
  const trust = p.trustAgy ? trustAgyWorkspace(p.root) : { changed: false, disabled: true };
32730
34121
  const mcpName = "marionette-" + config.id.slice(0, 8);
32731
34122
  const command2 = mcpCommand(p.lead, mcpName, runtime, p.home);
32732
34123
  const mcpStatus = p.mcp === "install" ? await installMcp(command2, mcpName, p.home) : p.mcp;
32733
34124
  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 });
34125
+ const promptPath = resolve11(p.home, "leads", project.id + ".md");
34126
+ writeFileSync7(promptPath, prompt + "\n", { mode: 384 });
32736
34127
  return {
32737
34128
  ok: true,
32738
34129
  home: p.home,
@@ -32749,19 +34140,19 @@ async function runSetup(input) {
32749
34140
  prompt
32750
34141
  };
32751
34142
  } finally {
32752
- closeSync2(lock);
34143
+ closeSync3(lock);
32753
34144
  unlinkSync3(lockPath);
32754
34145
  }
32755
34146
  }
32756
34147
  async function launchLead(project, printOnly, profileId) {
32757
- while (!existsSync7(resolve10(project, ".marionette/project.json"))) {
32758
- if (dirname9(project) === project)
34148
+ while (!existsSync8(resolve11(project, ".marionette/project.json"))) {
34149
+ if (dirname10(project) === project)
32759
34150
  throw new Error("No configured Marionette project found. Run marionette setup first.");
32760
- project = dirname9(project);
34151
+ project = dirname10(project);
32761
34152
  }
32762
- const binding = JSON.parse(readFileSync8(resolve10(project, ".marionette/project.json"), "utf8"));
34153
+ const binding = JSON.parse(readFileSync9(resolve11(project, ".marionette/project.json"), "utf8"));
32763
34154
  const brief = await call(binding.home, "project.briefing", { projectId: binding.projectId });
32764
- const lease = credentialsSchema.parse(JSON.parse(readFileSync8(binding.leasePath, "utf8")));
34155
+ const lease = credentialsSchema.parse(JSON.parse(readFileSync9(binding.leasePath, "utf8")));
32765
34156
  if (!brief.lead || brief.lead.owner !== lease.owner || brief.lead.epoch !== lease.epoch)
32766
34157
  throw new Error(
32767
34158
  "The saved lead no longer controls this project. Refresh setup with an explicit handover or takeover."
@@ -32810,7 +34201,7 @@ var print = (v) => console.log(JSON.stringify(v, null, 2));
32810
34201
  async function main() {
32811
34202
  const cmd = args[0] ?? "help";
32812
34203
  if (cmd === "--version" || cmd === "version") {
32813
- console.log(JSON.parse(readFileSync9(resolve11(packageRoot, "package.json"), "utf8")).version);
34204
+ console.log(JSON.parse(readFileSync10(resolve12(packageRoot, "package.json"), "utf8")).version);
32814
34205
  return;
32815
34206
  }
32816
34207
  if (cmd === "setup" || cmd === "init") {
@@ -32868,7 +34259,7 @@ async function main() {
32868
34259
  n++;
32869
34260
  }
32870
34261
  }
32871
- let input = flag("config") ? JSON.parse(readFileSync9(flag("config"), "utf8")) : {};
34262
+ let input = flag("config") ? JSON.parse(readFileSync10(flag("config"), "utf8")) : {};
32872
34263
  for (const key of ["project", "home", "name", "session", "socket", "workspace", "lead", "mcp"])
32873
34264
  if (flag(key)) input[key] = flag(key);
32874
34265
  if (flag("lead-name")) input.leadName = flag("lead-name");
@@ -32907,7 +34298,7 @@ ${result.mcp.status === "print" ? "\nMCP install command:\n" + result.mcp.shell
32907
34298
  }
32908
34299
  if (cmd === "lead") {
32909
34300
  await launchLead(
32910
- resolve11(flag("project") ?? process.cwd()),
34301
+ resolve12(flag("project") ?? process.cwd()),
32911
34302
  args.includes("--print"),
32912
34303
  flag("profile")
32913
34304
  );
@@ -32921,7 +34312,7 @@ ${result.mcp.status === "print" ? "\nMCP install command:\n" + result.mcp.shell
32921
34312
  } = process.env;
32922
34313
  if (!url || !taskId || !token)
32923
34314
  throw new Error("worker-report must run in a Marionette-created worker pane");
32924
- const input = JSON.parse(readFileSync9(flag("file") ?? 0, "utf8"));
34315
+ const input = JSON.parse(readFileSync10(flag("file") ?? 0, "utf8"));
32925
34316
  const res = await fetch(
32926
34317
  `${url}/api/worker/${encodeURIComponent(taskId)}${cmd === "worker-call" ? "/call" : ""}`,
32927
34318
  {
@@ -32955,16 +34346,16 @@ ${result.mcp.status === "print" ? "\nMCP install command:\n" + result.mcp.shell
32955
34346
  } catch (e) {
32956
34347
  if (!String(e).includes("fetch failed") && !String(e).includes("TimeoutError")) throw e;
32957
34348
  }
32958
- const cli = resolve11(installRuntime(home), "dist/cli.js");
32959
- if (!existsSync8(cli))
34349
+ const cli = resolve12(installRuntime(home), "dist/cli.js");
34350
+ if (!existsSync9(cli))
32960
34351
  throw new Error("Run npm run build before starting the background supervisor");
32961
- const log = openSync3(resolve11(home, "supervisor.log"), "a", 384);
34352
+ const log = openSync4(resolve12(home, "supervisor.log"), "a", 384);
32962
34353
  const child = spawn4(process.execPath, ["--no-warnings", cli, "serve", "--home", home], {
32963
34354
  detached: true,
32964
34355
  stdio: ["ignore", log, log],
32965
34356
  env: process.env
32966
34357
  });
32967
- closeSync3(log);
34358
+ closeSync4(log);
32968
34359
  child.unref();
32969
34360
  for (let i = 0; i < 50; i++) {
32970
34361
  await new Promise((r) => setTimeout(r, 100));
@@ -32977,7 +34368,7 @@ ${result.mcp.status === "print" ? "\nMCP install command:\n" + result.mcp.shell
32977
34368
  } catch {
32978
34369
  }
32979
34370
  }
32980
- throw new Error(`Supervisor did not start. Read ${resolve11(home, "supervisor.log")}`);
34371
+ throw new Error(`Supervisor did not start. Read ${resolve12(home, "supervisor.log")}`);
32981
34372
  }
32982
34373
  if (cmd === "stop") {
32983
34374
  const c = loadConfig(home), url = `http://127.0.0.1:${c.port}`;
@@ -33011,7 +34402,7 @@ ${result.mcp.status === "print" ? "\nMCP install command:\n" + result.mcp.shell
33011
34402
  return;
33012
34403
  }
33013
34404
  if (cmd === "mcp-config") {
33014
- const server = resolve11(installRuntime(home), "dist/mcp.js");
34405
+ const server = resolve12(installRuntime(home), "dist/mcp.js");
33015
34406
  console.log(
33016
34407
  `[mcp_servers.marionette]
33017
34408
  command = ${JSON.stringify(process.execPath)}
@@ -33023,11 +34414,11 @@ args = ["--no-warnings", ${JSON.stringify(server)}, "--home", ${JSON.stringify(h
33023
34414
  if (cmd === "call") {
33024
34415
  const action = args[1];
33025
34416
  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"));
34417
+ const input = flag("file") ? JSON.parse(readFileSync10(flag("file"), "utf8")) : flag("json") ? JSON.parse(flag("json")) : {};
34418
+ if (flag("lease")) input.lease = JSON.parse(readFileSync10(flag("lease"), "utf8"));
33028
34419
  const result = await call(home, action, input);
33029
34420
  if (flag("save-lease") && result.lease) {
33030
- writeFileSync7(flag("save-lease"), JSON.stringify(result.lease, null, 2) + "\n", {
34421
+ writeFileSync8(flag("save-lease"), JSON.stringify(result.lease, null, 2) + "\n", {
33031
34422
  mode: 384
33032
34423
  });
33033
34424
  const { lease, ...rest } = result;