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