@tablation/crew 0.49.5 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +562 -188
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { resolve as resolve4, dirname as dirname12, join as
|
|
4
|
+
import { resolve as resolve4, dirname as dirname12, join as join25, basename as basename9 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { hostname as hostname2, homedir as homedir8 } from "os";
|
|
7
|
-
import { spawnSync as
|
|
7
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
8
8
|
|
|
9
9
|
// src/argv.ts
|
|
10
10
|
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -3586,6 +3586,8 @@ function planLaunchd(ship, crewHome, job) {
|
|
|
3586
3586
|
const scheduleKeys = persistent ? ` <key>RunAtLoad</key>
|
|
3587
3587
|
<true/>
|
|
3588
3588
|
<key>KeepAlive</key>
|
|
3589
|
+
<true/>
|
|
3590
|
+
<key>AbandonProcessGroup</key>
|
|
3589
3591
|
<true/>` : ` <key>StartInterval</key>
|
|
3590
3592
|
<integer>${INTERVAL_SECONDS}</integer>
|
|
3591
3593
|
<key>RunAtLoad</key>
|
|
@@ -3650,6 +3652,9 @@ StandardOutput=append:${schedulerLog}
|
|
|
3650
3652
|
StandardError=append:${schedulerLog}
|
|
3651
3653
|
Restart=on-failure
|
|
3652
3654
|
RestartSec=5
|
|
3655
|
+
# CREW-1775: a restart stops only the daemon; agent runs it started live in
|
|
3656
|
+
# their own process groups and the next daemon reattaches to them.
|
|
3657
|
+
KillMode=process
|
|
3653
3658
|
|
|
3654
3659
|
[Install]
|
|
3655
3660
|
WantedBy=default.target
|
|
@@ -3727,6 +3732,13 @@ function planCron(ship, crewHome, job) {
|
|
|
3727
3732
|
schedulerLog
|
|
3728
3733
|
};
|
|
3729
3734
|
}
|
|
3735
|
+
function unitAbandonsAgents(plan, read) {
|
|
3736
|
+
const path = plan.unitPaths[0];
|
|
3737
|
+
if (!path || plan.mechanism !== "launchd" && plan.mechanism !== "systemd") return null;
|
|
3738
|
+
const text = read(path);
|
|
3739
|
+
if (text === null) return null;
|
|
3740
|
+
return plan.mechanism === "launchd" ? /<key>AbandonProcessGroup<\/key>\s*<true\/>/.test(text) : /^KillMode=process\s*$/m.test(text);
|
|
3741
|
+
}
|
|
3730
3742
|
function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
3731
3743
|
if (hostShip === "macos") return planLaunchd(ship, crewHome, job);
|
|
3732
3744
|
if (hostShip === "linux" && hasSystemd) return planSystemd(ship, crewHome, job);
|
|
@@ -4931,6 +4943,8 @@ var Tracker = class {
|
|
|
4931
4943
|
* writes pass its seat's agent key so the event log and rule context see an
|
|
4932
4944
|
* agent, not the ship's human principal.
|
|
4933
4945
|
*/
|
|
4946
|
+
/** Told once per failed best-effort read (`events`); poll.ts points it at the pass's emitter. */
|
|
4947
|
+
onReadFailure;
|
|
4934
4948
|
constructor(route, ship, opts = {}) {
|
|
4935
4949
|
this.route = route;
|
|
4936
4950
|
if (!route.resolved) {
|
|
@@ -5372,10 +5386,16 @@ var Tracker = class {
|
|
|
5372
5386
|
*/
|
|
5373
5387
|
async events(type, limit = 500) {
|
|
5374
5388
|
const filters = encodeURIComponent(JSON.stringify([{ columnName: "event_type", operator: "EQ", value: type }]));
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5389
|
+
let rows;
|
|
5390
|
+
try {
|
|
5391
|
+
const res = await this.eventsFetch("GET", `/events?limit=${limit}&filters=${filters}`);
|
|
5392
|
+
if (res.status === 404) return [];
|
|
5393
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
5394
|
+
rows = await res.json();
|
|
5395
|
+
} catch (err) {
|
|
5396
|
+
this.onReadFailure?.(`reading ${type} events failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5397
|
+
return [];
|
|
5398
|
+
}
|
|
5379
5399
|
return rows.filter((e) => e.event_type === type).sort((a, b) => Date.parse(b.occurred_at ?? b.created_at ?? "") - Date.parse(a.occurred_at ?? a.created_at ?? ""));
|
|
5380
5400
|
}
|
|
5381
5401
|
/** The newest event of `type` on a ticket, or undefined — e.g. the `pushed` tip QA verifies against. */
|
|
@@ -6112,7 +6132,7 @@ function operatorTodo(tickets, contract, operatorId) {
|
|
|
6112
6132
|
}
|
|
6113
6133
|
|
|
6114
6134
|
// src/digest.ts
|
|
6115
|
-
var DIRECTION_EVENT_TYPES = ["conflict_handed_back", "requeued", "bounced", "conflict_escalated"];
|
|
6135
|
+
var DIRECTION_EVENT_TYPES = ["conflict_handed_back", "requeued", "bounced", "conflict_escalated", "interrupted"];
|
|
6116
6136
|
function displayKey2(t) {
|
|
6117
6137
|
return t.issue_tag ?? t.issue_id ?? "?";
|
|
6118
6138
|
}
|
|
@@ -6241,7 +6261,7 @@ ${sorted(held).map((t) => `| ${displayKey2(t)} | ${stopOf(t)} |`).join("\n")}
|
|
|
6241
6261
|
"## Current queue \u2014 built for you by the poll\n",
|
|
6242
6262
|
"\nAlready filtered to your lane, and already ordered by the Step 2 rule.\nTicket bodies are deliberately omitted: fetch the full record of only the\nticket you actually pick up. **Do not re-fetch the whole tracker.** This\ndigest comes from the same API call the poll just made, moments ago.\n",
|
|
6243
6263
|
'\n"new since last poll" counts comments from someone other than you since\nthe poll watermark \u2014 the same signal that woke this run.\n',
|
|
6244
|
-
'\n"runner direction" is the newest instruction the crew itself posted to the\nevent log (not Comments) that no push has answered yet: `conflict_handed_back`\n(merge the base into the branch and resolve), `requeued`, `bounced`,\n`conflict_escalated
|
|
6264
|
+
'\n"runner direction" is the newest instruction the crew itself posted to the\nevent log (not Comments) that no push has answered yet: `conflict_handed_back`\n(merge the base into the branch and resolve), `requeued`, `bounced`,\n`conflict_escalated`, `interrupted` (an earlier run on this seat was cut short; its\nuncommitted worktree is yours, resume it). It is direction, same as a comment from a hold: act on it.\n',
|
|
6245
6265
|
"\n**`repo` is the checkout a ticket's work happens in** \u2014 an area spans\nseveral repositories, so cut the worktree beside THAT directory, not beside\nwhichever one you started in. A ticket marked **NO CHECKOUT** is not yours:\nthis ship has no clone of its repository, and another ship may serve it.\n\n**Use the `branch` and `worktree` columns verbatim.** They are that\nrepository's own naming convention, rendered for THIS ticket \u2014 cut the\nworktree at `<repo>/../<worktree>`, on branch `<branch>`. Do not derive\neither yourself, and prefer these over the Environment section's own worked\nexample if the two ever disagree \u2014 that example is illustrative only,\nrendered before any ticket is picked.\n",
|
|
6246
6266
|
...i.workModes && [...i.workModes.modes.values()].some((m) => m !== "automatic") ? ["\n**Manual / hybrid projects.** Some projects on this board are not fully automatic. In a `manual` project no lane touches any ticket, and a ticket with no project is treated the same way, so none appear below. In a `hybrid` project a ticket appears below only when it is assigned to an agent. An `accepted` ticket that is absent for either reason is not yours to claim \u2014 do not go looking for it.\n"] : [],
|
|
6247
6267
|
"\n### Step 1 \u2014 open tickets that may be yours to act on\n",
|
|
@@ -6455,14 +6475,23 @@ async function runStalledSweep(o, stalled) {
|
|
|
6455
6475
|
async function decideCycle(o) {
|
|
6456
6476
|
const { route, ship, state, emit } = o;
|
|
6457
6477
|
const tracker = new Tracker(route, ship);
|
|
6478
|
+
tracker.onReadFailure = (m) => emit.warn(m);
|
|
6458
6479
|
emit.enter("poll");
|
|
6480
|
+
const optional = async (what, read, fallback) => {
|
|
6481
|
+
try {
|
|
6482
|
+
return await read;
|
|
6483
|
+
} catch (err) {
|
|
6484
|
+
emit.warn(`reading ${what} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
6485
|
+
return fallback;
|
|
6486
|
+
}
|
|
6487
|
+
};
|
|
6459
6488
|
const [tickets, comments, crewRows, epicRows, ships, projectModes, stalledEvents] = await Promise.all([
|
|
6460
6489
|
tracker.openTickets(),
|
|
6461
6490
|
tracker.comments(200),
|
|
6462
6491
|
tracker.crewRows(),
|
|
6463
|
-
tracker.epicRows(),
|
|
6464
|
-
tracker.shipRows(),
|
|
6465
|
-
tracker.projectWorkModes(),
|
|
6492
|
+
optional("epics", tracker.epicRows(), []),
|
|
6493
|
+
optional("ships", tracker.shipRows(), []),
|
|
6494
|
+
optional("project work modes", tracker.projectWorkModes(), /* @__PURE__ */ new Map()),
|
|
6466
6495
|
tracker.events("stalled")
|
|
6467
6496
|
]);
|
|
6468
6497
|
const directionEvents = (await Promise.all([...DIRECTION_EVENT_TYPES, "pushed"].map((t) => tracker.events(t)))).flat();
|
|
@@ -7096,8 +7125,9 @@ ${g.tail}
|
|
|
7096
7125
|
var alreadyReported = (ticket, g, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "gate_red" && (e.message ?? "").includes(`${GATE_RED_MARKER} ${g.sha.slice(0, 8)}`));
|
|
7097
7126
|
|
|
7098
7127
|
// src/agent.ts
|
|
7099
|
-
import { spawn as spawn2 } from "child_process";
|
|
7100
|
-
import { readFileSync as readFileSync12, existsSync as existsSync12, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, createWriteStream } from "fs";
|
|
7128
|
+
import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
|
|
7129
|
+
import { readFileSync as readFileSync12, existsSync as existsSync12, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
|
|
7130
|
+
import { StringDecoder } from "string_decoder";
|
|
7101
7131
|
import { finished } from "stream/promises";
|
|
7102
7132
|
import { join as join14, dirname as dirname6 } from "path";
|
|
7103
7133
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -8274,6 +8304,7 @@ function planAgentRun(o) {
|
|
|
8274
8304
|
streamPath: join14(streamsDir, `${base}.jsonl`),
|
|
8275
8305
|
eventsPath: join14(streamsDir, `${base}.events.jsonl`),
|
|
8276
8306
|
ticket: o.ticket,
|
|
8307
|
+
ticketRecordId: o.ticketRecordId ?? null,
|
|
8277
8308
|
agentLog,
|
|
8278
8309
|
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
8279
8310
|
promptSha
|
|
@@ -8297,6 +8328,43 @@ function describePlan(p) {
|
|
|
8297
8328
|
`stream: ${p.streamPath}`
|
|
8298
8329
|
].join("\n");
|
|
8299
8330
|
}
|
|
8331
|
+
var pidPathFor = (streamPath) => `${streamPath}.pid`;
|
|
8332
|
+
function readRunSidecar(streamPath) {
|
|
8333
|
+
try {
|
|
8334
|
+
const sc = JSON.parse(readFileSync12(pidPathFor(streamPath), "utf8"));
|
|
8335
|
+
return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
|
|
8336
|
+
} catch {
|
|
8337
|
+
return null;
|
|
8338
|
+
}
|
|
8339
|
+
}
|
|
8340
|
+
function runIsAlive(sc) {
|
|
8341
|
+
try {
|
|
8342
|
+
process.kill(sc.pid, 0);
|
|
8343
|
+
} catch (e) {
|
|
8344
|
+
if (e.code !== "EPERM") return false;
|
|
8345
|
+
}
|
|
8346
|
+
const ps = spawnSync5("ps", ["-o", "lstart=", "-p", String(sc.pid)], { encoding: "utf8" });
|
|
8347
|
+
const started = Date.parse(ps.stdout.trim());
|
|
8348
|
+
if (ps.status !== 0 || Number.isNaN(started)) return ps.status === 0;
|
|
8349
|
+
return Math.abs(started - Date.parse(sc.spawnedAt)) < 15e3;
|
|
8350
|
+
}
|
|
8351
|
+
function identityOf(plan, adapterHarness) {
|
|
8352
|
+
return {
|
|
8353
|
+
role: plan.role,
|
|
8354
|
+
route: plan.route,
|
|
8355
|
+
ticket: plan.ticket ?? null,
|
|
8356
|
+
model: plan.model,
|
|
8357
|
+
toolchain: plan.toolchain ?? LEGACY_TOOLCHAIN_HARNESS,
|
|
8358
|
+
harness: adapterHarness,
|
|
8359
|
+
provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER,
|
|
8360
|
+
eventsPath: plan.eventsPath,
|
|
8361
|
+
streamPath: plan.streamPath,
|
|
8362
|
+
agentLog: plan.agentLog ? withoutKey(plan.agentLog) : void 0,
|
|
8363
|
+
promptVersion: plan.promptVersion,
|
|
8364
|
+
promptSha: plan.promptSha
|
|
8365
|
+
};
|
|
8366
|
+
}
|
|
8367
|
+
var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
|
|
8300
8368
|
function openSink(path) {
|
|
8301
8369
|
try {
|
|
8302
8370
|
mkdirSync6(dirname6(path), { recursive: true });
|
|
@@ -8310,172 +8378,300 @@ function endSink(sink) {
|
|
|
8310
8378
|
sink.end();
|
|
8311
8379
|
return finished(sink).catch(() => void 0);
|
|
8312
8380
|
}
|
|
8381
|
+
var FileTail = class {
|
|
8382
|
+
offset = 0;
|
|
8383
|
+
decoder = new StringDecoder("utf8");
|
|
8384
|
+
path;
|
|
8385
|
+
constructor(path) {
|
|
8386
|
+
this.path = path;
|
|
8387
|
+
}
|
|
8388
|
+
poll() {
|
|
8389
|
+
let fd;
|
|
8390
|
+
try {
|
|
8391
|
+
fd = openSync3(this.path, "r");
|
|
8392
|
+
} catch {
|
|
8393
|
+
return "";
|
|
8394
|
+
}
|
|
8395
|
+
try {
|
|
8396
|
+
let text = "";
|
|
8397
|
+
const chunk = Buffer.alloc(64 * 1024);
|
|
8398
|
+
for (; ; ) {
|
|
8399
|
+
const n = readSync(fd, chunk, 0, chunk.length, this.offset);
|
|
8400
|
+
if (n <= 0) break;
|
|
8401
|
+
this.offset += n;
|
|
8402
|
+
text += this.decoder.write(chunk.subarray(0, n));
|
|
8403
|
+
}
|
|
8404
|
+
return text;
|
|
8405
|
+
} finally {
|
|
8406
|
+
closeSync3(fd);
|
|
8407
|
+
}
|
|
8408
|
+
}
|
|
8409
|
+
};
|
|
8410
|
+
var TAIL_POLL_MS = 200;
|
|
8411
|
+
var RunObserver = class {
|
|
8412
|
+
cycles = [];
|
|
8413
|
+
attribution = new TicketAttributionTracker();
|
|
8414
|
+
result;
|
|
8415
|
+
/** Set once the stream carries the harness's final result (turn count or cost). */
|
|
8416
|
+
sawResult = false;
|
|
8417
|
+
muted = false;
|
|
8418
|
+
buf = "";
|
|
8419
|
+
mapper;
|
|
8420
|
+
id;
|
|
8421
|
+
adapter;
|
|
8422
|
+
eventsSink;
|
|
8423
|
+
emitCycle;
|
|
8424
|
+
constructor(id, adapter, eventsSink, emitCycle) {
|
|
8425
|
+
this.id = id;
|
|
8426
|
+
this.adapter = adapter;
|
|
8427
|
+
this.eventsSink = eventsSink;
|
|
8428
|
+
this.emitCycle = emitCycle;
|
|
8429
|
+
this.mapper = adapter.createStreamMapper?.() ?? { map: (p) => adapter.mapStreamLine(p), flush: () => [] };
|
|
8430
|
+
}
|
|
8431
|
+
feed(text) {
|
|
8432
|
+
this.buf += text;
|
|
8433
|
+
const lines = this.buf.split("\n");
|
|
8434
|
+
this.buf = lines.pop() ?? "";
|
|
8435
|
+
for (const line of lines) this.handleLine(line);
|
|
8436
|
+
}
|
|
8437
|
+
end() {
|
|
8438
|
+
if (this.buf) this.handleLine(this.buf);
|
|
8439
|
+
this.buf = "";
|
|
8440
|
+
try {
|
|
8441
|
+
this.consume(void 0, this.mapper.flush());
|
|
8442
|
+
} catch {
|
|
8443
|
+
}
|
|
8444
|
+
}
|
|
8445
|
+
handleLine(line) {
|
|
8446
|
+
if (!line) return;
|
|
8447
|
+
let parsed;
|
|
8448
|
+
try {
|
|
8449
|
+
parsed = JSON.parse(line);
|
|
8450
|
+
} catch {
|
|
8451
|
+
return;
|
|
8452
|
+
}
|
|
8453
|
+
try {
|
|
8454
|
+
const got = this.adapter.extractResult(parsed);
|
|
8455
|
+
if (got) {
|
|
8456
|
+
this.result = { ...this.result, ...Object.fromEntries(Object.entries(got).filter(([, v]) => v !== void 0)) };
|
|
8457
|
+
if (got.numTurns !== void 0 || got.costUsd !== void 0) this.sawResult = true;
|
|
8458
|
+
}
|
|
8459
|
+
this.consume(parsed, this.mapper.map(parsed));
|
|
8460
|
+
} catch {
|
|
8461
|
+
}
|
|
8462
|
+
}
|
|
8463
|
+
// Turns mapped events (and, when there is a source line, its token usage)
|
|
8464
|
+
// into sink writes and buffered cycles. `parsed` is undefined for events
|
|
8465
|
+
// the mapper flushes at end of stream, which have no line of their own.
|
|
8466
|
+
consume(parsed, events) {
|
|
8467
|
+
const plan = this.id;
|
|
8468
|
+
let turnText = "";
|
|
8469
|
+
for (const ev of events) {
|
|
8470
|
+
const e = {
|
|
8471
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8472
|
+
cycle: this.emitCycle,
|
|
8473
|
+
route: plan.route,
|
|
8474
|
+
step: "agent",
|
|
8475
|
+
level: "info",
|
|
8476
|
+
message: ev.kind === "tool" ? `${ev.tool ?? "tool"}${ev.target ? ` ${ev.target}` : ""}` : ev.kind,
|
|
8477
|
+
role: plan.role,
|
|
8478
|
+
...plan.ticket ? { ticket: plan.ticket } : {},
|
|
8479
|
+
data: { kind: ev.kind, ...ev.text ? { text: ev.text } : {}, ...ev.tool ? { tool: ev.tool } : {}, ...ev.target ? { target: ev.target } : {} }
|
|
8480
|
+
};
|
|
8481
|
+
if (!this.muted) this.eventsSink?.write(`${JSON.stringify(e)}
|
|
8482
|
+
`);
|
|
8483
|
+
if ((ev.kind === "thought" || ev.kind === "text") && ev.text) {
|
|
8484
|
+
turnText += `${ev.text}
|
|
8485
|
+
`;
|
|
8486
|
+
const cycleIndex = this.cycles.length;
|
|
8487
|
+
this.cycles.push({
|
|
8488
|
+
cycleIndex,
|
|
8489
|
+
occurredAt: e.at,
|
|
8490
|
+
thinking: ev.text,
|
|
8491
|
+
// A conversational `text` turn is tagged 'assistant' so it's
|
|
8492
|
+
// distinguishable from a thinking block once both share this
|
|
8493
|
+
// buffer (ISSUE-594) — matching the precedent
|
|
8494
|
+
// `agent-chat.service.ts`'s logChatRun (ISSUE-504) already set
|
|
8495
|
+
// for the same column. A `thought` cycle stays untagged, same
|
|
8496
|
+
// as before this change.
|
|
8497
|
+
...ev.kind === "text" ? { role: "assistant" } : {}
|
|
8498
|
+
});
|
|
8499
|
+
if (!this.muted) {
|
|
8500
|
+
this.eventsSink?.write(`${JSON.stringify({
|
|
8501
|
+
...e,
|
|
8502
|
+
message: "cycle",
|
|
8503
|
+
data: { kind: "cycle", cycleIndex, text: ev.text }
|
|
8504
|
+
})}
|
|
8505
|
+
`);
|
|
8506
|
+
}
|
|
8507
|
+
}
|
|
8508
|
+
}
|
|
8509
|
+
const turnTokens = parsed === void 0 ? void 0 : this.adapter.extractTurnTokens(parsed);
|
|
8510
|
+
if (turnTokens !== void 0) this.attribution.add(turnText, turnTokens);
|
|
8511
|
+
}
|
|
8512
|
+
};
|
|
8513
|
+
async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
|
|
8514
|
+
const attributedTicket = obs.attribution.winner(id.ticket ?? void 0);
|
|
8515
|
+
const identity = { toolchain: id.toolchain, harness: id.harness, provider: id.provider };
|
|
8516
|
+
const { result } = obs;
|
|
8517
|
+
try {
|
|
8518
|
+
writeFileSync8(`${id.streamPath}.meta.json`, JSON.stringify({
|
|
8519
|
+
route: id.route,
|
|
8520
|
+
role: id.role,
|
|
8521
|
+
ticket: id.ticket,
|
|
8522
|
+
attributedTicket: attributedTicket ?? null,
|
|
8523
|
+
startedAt,
|
|
8524
|
+
exitCode: end.code,
|
|
8525
|
+
model: id.model,
|
|
8526
|
+
...end.interrupted ? { interrupted: end.interrupted } : {},
|
|
8527
|
+
...identity,
|
|
8528
|
+
sessionId: result?.sessionId ?? null,
|
|
8529
|
+
numTurns: result?.numTurns ?? null,
|
|
8530
|
+
totalCostUsd: result?.costUsd ?? null
|
|
8531
|
+
}));
|
|
8532
|
+
} catch {
|
|
8533
|
+
}
|
|
8534
|
+
const ms2 = Date.now() - Date.parse(startedAt);
|
|
8535
|
+
emit.emit(end.interrupted ? "agent run interrupted" : `agent run finished (exit ${end.code ?? "unknown"})`, {
|
|
8536
|
+
data: {
|
|
8537
|
+
ms: ms2,
|
|
8538
|
+
code: end.code,
|
|
8539
|
+
...end.interrupted ? { interrupted: end.interrupted } : {},
|
|
8540
|
+
...identity,
|
|
8541
|
+
...result?.sessionId ? { sessionId: result.sessionId } : {},
|
|
8542
|
+
...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
|
|
8543
|
+
...result?.costUsd !== void 0 ? { totalCostUsd: result.costUsd } : {}
|
|
8544
|
+
}
|
|
8545
|
+
});
|
|
8546
|
+
if (id.agentLog && agentLogKey) {
|
|
8547
|
+
try {
|
|
8548
|
+
await reportAgentRun({ ...id.agentLog, apiKey: agentLogKey }, {
|
|
8549
|
+
ticketReference: attributedTicket,
|
|
8550
|
+
outcome: end.interrupted ? "interrupted" : end.code === 0 || end.code === null && obs.sawResult ? "success" : "error",
|
|
8551
|
+
startedAt,
|
|
8552
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8553
|
+
cycles: obs.cycles,
|
|
8554
|
+
model: id.model,
|
|
8555
|
+
tokensIn: result?.inputTokens,
|
|
8556
|
+
tokensOut: result?.outputTokens,
|
|
8557
|
+
cacheReadTokens: result?.cacheReadTokens,
|
|
8558
|
+
cacheWriteTokens: result?.cacheWriteTokens,
|
|
8559
|
+
costUsd: result?.costUsd,
|
|
8560
|
+
harness: identity.harness,
|
|
8561
|
+
provider: identity.provider,
|
|
8562
|
+
promptVersion: id.promptVersion,
|
|
8563
|
+
promptSha: id.promptSha
|
|
8564
|
+
});
|
|
8565
|
+
} catch (e) {
|
|
8566
|
+
emit.warn(`could not report agent log: ${e.message}`, { step: "agent" });
|
|
8567
|
+
}
|
|
8568
|
+
}
|
|
8569
|
+
return { ms: ms2, ticket: attributedTicket };
|
|
8570
|
+
}
|
|
8313
8571
|
function spawnAgent(plan, emit) {
|
|
8314
8572
|
const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
|
|
8315
8573
|
const started = Date.now();
|
|
8316
8574
|
const startedAt = new Date(started).toISOString();
|
|
8317
8575
|
for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
|
|
8318
|
-
const rawSink = openSink(plan.streamPath);
|
|
8319
8576
|
const eventsSink = openSink(plan.eventsPath);
|
|
8577
|
+
const id = identityOf(plan, adapter.harness);
|
|
8320
8578
|
return new Promise((resolve5, reject) => {
|
|
8579
|
+
let streamFd;
|
|
8580
|
+
try {
|
|
8581
|
+
mkdirSync6(dirname6(plan.streamPath), { recursive: true });
|
|
8582
|
+
streamFd = openSync3(plan.streamPath, "a");
|
|
8583
|
+
} catch (e) {
|
|
8584
|
+
void endSink(eventsSink);
|
|
8585
|
+
reject(new AgentSpawnError(`cannot open the stream file ${plan.streamPath}: ${e.message}`));
|
|
8586
|
+
return;
|
|
8587
|
+
}
|
|
8321
8588
|
const child = spawn2(plan.bin, plan.args, {
|
|
8322
8589
|
cwd: plan.cwd,
|
|
8323
8590
|
env: { ...scrubbedEnv(process.env, adapter.harness), ...plan.setEnv },
|
|
8324
|
-
stdio: ["pipe",
|
|
8591
|
+
stdio: ["pipe", streamFd, "inherit"],
|
|
8592
|
+
detached: true
|
|
8325
8593
|
});
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8594
|
+
closeSync3(streamFd);
|
|
8595
|
+
if (child.pid !== void 0) {
|
|
8596
|
+
const sidecar = {
|
|
8597
|
+
pid: child.pid,
|
|
8598
|
+
ownerPid: process.pid,
|
|
8599
|
+
spawnedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8600
|
+
role: plan.role,
|
|
8601
|
+
route: plan.route,
|
|
8602
|
+
ticket: plan.ticket ?? null,
|
|
8603
|
+
ticketRecordId: plan.ticketRecordId ?? null,
|
|
8604
|
+
model: plan.model,
|
|
8605
|
+
toolchain: id.toolchain,
|
|
8606
|
+
harness: id.harness,
|
|
8607
|
+
provider: id.provider,
|
|
8608
|
+
eventsPath: plan.eventsPath,
|
|
8609
|
+
agentLog: id.agentLog,
|
|
8610
|
+
promptVersion: plan.promptVersion,
|
|
8611
|
+
promptSha: plan.promptSha
|
|
8612
|
+
};
|
|
8344
8613
|
try {
|
|
8345
|
-
|
|
8346
|
-
if (got) result = { ...result, ...Object.fromEntries(Object.entries(got).filter(([, v]) => v !== void 0)) };
|
|
8347
|
-
consume(parsed, mapper.map(parsed));
|
|
8614
|
+
writeFileSync8(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
|
|
8348
8615
|
} catch {
|
|
8349
8616
|
}
|
|
8350
|
-
}
|
|
8351
|
-
const
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
for (const ev of events) {
|
|
8355
|
-
const e = {
|
|
8356
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8357
|
-
cycle: emit.cycle,
|
|
8358
|
-
route: plan.route,
|
|
8359
|
-
step: "agent",
|
|
8360
|
-
level: "info",
|
|
8361
|
-
message: ev.kind === "tool" ? `${ev.tool ?? "tool"}${ev.target ? ` ${ev.target}` : ""}` : ev.kind,
|
|
8362
|
-
role: plan.role,
|
|
8363
|
-
...plan.ticket ? { ticket: plan.ticket } : {},
|
|
8364
|
-
data: { kind: ev.kind, ...ev.text ? { text: ev.text } : {}, ...ev.tool ? { tool: ev.tool } : {}, ...ev.target ? { target: ev.target } : {} }
|
|
8365
|
-
};
|
|
8366
|
-
eventsSink?.write(`${JSON.stringify(e)}
|
|
8367
|
-
`);
|
|
8368
|
-
if ((ev.kind === "thought" || ev.kind === "text") && ev.text) {
|
|
8369
|
-
turnText += `${ev.text}
|
|
8370
|
-
`;
|
|
8371
|
-
const cycleIndex = cycles.length;
|
|
8372
|
-
cycles.push({
|
|
8373
|
-
cycleIndex,
|
|
8374
|
-
occurredAt: e.at,
|
|
8375
|
-
thinking: ev.text,
|
|
8376
|
-
// A conversational `text` turn is tagged 'assistant' so it's
|
|
8377
|
-
// distinguishable from a thinking block once both share this
|
|
8378
|
-
// buffer (ISSUE-594) — matching the precedent
|
|
8379
|
-
// `agent-chat.service.ts`'s logChatRun (ISSUE-504) already set
|
|
8380
|
-
// for the same column. A `thought` cycle stays untagged, same
|
|
8381
|
-
// as before this change.
|
|
8382
|
-
...ev.kind === "text" ? { role: "assistant" } : {}
|
|
8383
|
-
});
|
|
8384
|
-
eventsSink?.write(`${JSON.stringify({
|
|
8385
|
-
...e,
|
|
8386
|
-
message: "cycle",
|
|
8387
|
-
data: { kind: "cycle", cycleIndex, text: ev.text }
|
|
8388
|
-
})}
|
|
8389
|
-
`);
|
|
8390
|
-
}
|
|
8391
|
-
}
|
|
8392
|
-
const turnTokens = parsed === void 0 ? void 0 : adapter.extractTurnTokens(parsed);
|
|
8393
|
-
if (turnTokens !== void 0) attribution.add(turnText, turnTokens);
|
|
8394
|
-
}
|
|
8395
|
-
};
|
|
8396
|
-
child.stdout.on("data", (chunk) => {
|
|
8397
|
-
buf += chunk.toString("utf8");
|
|
8398
|
-
const lines = buf.split("\n");
|
|
8399
|
-
buf = lines.pop() ?? "";
|
|
8400
|
-
for (const line of lines) handleLine(line);
|
|
8401
|
-
});
|
|
8617
|
+
}
|
|
8618
|
+
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
8619
|
+
const tail = new FileTail(plan.streamPath);
|
|
8620
|
+
const timer = setInterval(() => obs.feed(tail.poll()), TAIL_POLL_MS);
|
|
8402
8621
|
let spawnFailed = false;
|
|
8403
8622
|
child.on("error", (err) => {
|
|
8404
8623
|
spawnFailed = true;
|
|
8624
|
+
clearInterval(timer);
|
|
8405
8625
|
reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err.message}`));
|
|
8406
8626
|
});
|
|
8407
8627
|
child.on("close", async (code) => {
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
}
|
|
8413
|
-
await Promise.all([endSink(rawSink), endSink(eventsSink)]);
|
|
8628
|
+
clearInterval(timer);
|
|
8629
|
+
obs.feed(tail.poll());
|
|
8630
|
+
obs.end();
|
|
8631
|
+
await endSink(eventsSink);
|
|
8414
8632
|
if (spawnFailed) return;
|
|
8415
|
-
const
|
|
8416
|
-
const identity = {
|
|
8417
|
-
toolchain: plan.toolchain ?? LEGACY_TOOLCHAIN_HARNESS,
|
|
8418
|
-
harness: adapter.harness,
|
|
8419
|
-
provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER
|
|
8420
|
-
};
|
|
8421
|
-
try {
|
|
8422
|
-
writeFileSync8(`${plan.streamPath}.meta.json`, JSON.stringify({
|
|
8423
|
-
route: plan.route,
|
|
8424
|
-
role: plan.role,
|
|
8425
|
-
ticket: plan.ticket ?? null,
|
|
8426
|
-
attributedTicket: attributedTicket ?? null,
|
|
8427
|
-
startedAt,
|
|
8428
|
-
exitCode: code ?? 1,
|
|
8429
|
-
model: plan.model,
|
|
8430
|
-
...identity,
|
|
8431
|
-
sessionId: result?.sessionId ?? null,
|
|
8432
|
-
numTurns: result?.numTurns ?? null,
|
|
8433
|
-
totalCostUsd: result?.costUsd ?? null
|
|
8434
|
-
}));
|
|
8435
|
-
} catch {
|
|
8436
|
-
}
|
|
8437
|
-
const ms2 = Date.now() - started;
|
|
8438
|
-
emit.emit(`agent run finished (exit ${code ?? 1})`, {
|
|
8439
|
-
data: {
|
|
8440
|
-
ms: ms2,
|
|
8441
|
-
code: code ?? 1,
|
|
8442
|
-
...identity,
|
|
8443
|
-
...result?.sessionId ? { sessionId: result.sessionId } : {},
|
|
8444
|
-
...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
|
|
8445
|
-
...result?.costUsd !== void 0 ? { totalCostUsd: result.costUsd } : {}
|
|
8446
|
-
}
|
|
8447
|
-
});
|
|
8448
|
-
if (plan.agentLog) {
|
|
8449
|
-
try {
|
|
8450
|
-
await reportAgentRun(plan.agentLog, {
|
|
8451
|
-
ticketReference: attributedTicket,
|
|
8452
|
-
outcome: (code ?? 1) === 0 ? "success" : "error",
|
|
8453
|
-
startedAt,
|
|
8454
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8455
|
-
cycles,
|
|
8456
|
-
model: plan.model,
|
|
8457
|
-
tokensIn: result?.inputTokens,
|
|
8458
|
-
tokensOut: result?.outputTokens,
|
|
8459
|
-
cacheReadTokens: result?.cacheReadTokens,
|
|
8460
|
-
cacheWriteTokens: result?.cacheWriteTokens,
|
|
8461
|
-
costUsd: result?.costUsd,
|
|
8462
|
-
harness: identity.harness,
|
|
8463
|
-
provider: identity.provider,
|
|
8464
|
-
promptVersion: plan.promptVersion,
|
|
8465
|
-
promptSha: plan.promptSha
|
|
8466
|
-
});
|
|
8467
|
-
} catch (e) {
|
|
8468
|
-
emit.warn(`could not report agent log: ${e.message}`, { step: "agent" });
|
|
8469
|
-
}
|
|
8470
|
-
}
|
|
8633
|
+
const { ms: ms2 } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey);
|
|
8471
8634
|
resolve5({ code: code ?? 1, ms: ms2 });
|
|
8472
8635
|
});
|
|
8473
|
-
child.stdin
|
|
8636
|
+
child.stdin?.on("error", (err) => {
|
|
8474
8637
|
emit.warn(`agent closed stdin before the prompt was delivered: ${err.message}`, { step: "agent" });
|
|
8475
8638
|
});
|
|
8476
|
-
child.stdin
|
|
8639
|
+
child.stdin?.end(plan.prompt);
|
|
8477
8640
|
});
|
|
8478
8641
|
}
|
|
8642
|
+
async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
8643
|
+
const adapter = adapterFor(sc.harness);
|
|
8644
|
+
const isAlive = o.isAlive ?? runIsAlive;
|
|
8645
|
+
const id = { ...sc, streamPath };
|
|
8646
|
+
const eventsSink = openSink(sc.eventsPath);
|
|
8647
|
+
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
8648
|
+
const tail = new FileTail(streamPath);
|
|
8649
|
+
obs.muted = true;
|
|
8650
|
+
obs.feed(tail.poll());
|
|
8651
|
+
obs.muted = false;
|
|
8652
|
+
const adopted = isAlive(sc);
|
|
8653
|
+
if (adopted) {
|
|
8654
|
+
o.onAdopted?.();
|
|
8655
|
+
const pollMs = o.pollMs ?? TAIL_POLL_MS;
|
|
8656
|
+
while (isAlive(sc)) {
|
|
8657
|
+
obs.feed(tail.poll());
|
|
8658
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
8659
|
+
}
|
|
8660
|
+
}
|
|
8661
|
+
obs.feed(tail.poll());
|
|
8662
|
+
obs.end();
|
|
8663
|
+
await endSink(eventsSink);
|
|
8664
|
+
const interrupted = obs.sawResult ? void 0 : adopted ? "no-result" : "daemon-exit";
|
|
8665
|
+
const { ms: ms2, ticket } = await finishRun(
|
|
8666
|
+
id,
|
|
8667
|
+
obs,
|
|
8668
|
+
emit,
|
|
8669
|
+
sc.spawnedAt,
|
|
8670
|
+
{ code: null, ...interrupted ? { interrupted } : {} },
|
|
8671
|
+
o.agentLogKey
|
|
8672
|
+
);
|
|
8673
|
+
return { adopted, ...interrupted ? { interrupted } : {}, ms: ms2, ticket };
|
|
8674
|
+
}
|
|
8479
8675
|
|
|
8480
8676
|
// src/run-worktree.ts
|
|
8481
8677
|
import { existsSync as existsSync14 } from "fs";
|
|
@@ -8584,6 +8780,8 @@ var describeRunWorktreePlan = (p) => {
|
|
|
8584
8780
|
return `reusing ${p.path} (${p.branch} is already level with the remote)`;
|
|
8585
8781
|
case "recut":
|
|
8586
8782
|
return `${p.branch} is gone from the remote and ${p.path} held no work of its own \u2014 re-cut it fresh from ${p.base}`;
|
|
8783
|
+
case "resume-dirty":
|
|
8784
|
+
return `${p.path} has uncommitted changes from this seat's previous run (interrupted) \u2014 resuming`;
|
|
8587
8785
|
case "leave-dirty":
|
|
8588
8786
|
return `left ${p.path} alone: it has uncommitted changes (someone may be editing it by hand)`;
|
|
8589
8787
|
case "leave-unpushed":
|
|
@@ -8595,7 +8793,7 @@ function newBranchName(cfg, t, role) {
|
|
|
8595
8793
|
const { prefix } = ticketBranchContext(t);
|
|
8596
8794
|
return branchRenderer(t, role)(effectiveBranchTemplate(cfg, prefix));
|
|
8597
8795
|
}
|
|
8598
|
-
function planRunWorktree(dir, cfg, t, role) {
|
|
8796
|
+
function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
8599
8797
|
const { remote, base } = cfg.branch;
|
|
8600
8798
|
const hasRemote = remoteConfigured(dir, remote);
|
|
8601
8799
|
if (hasRemote) fetchRemote(dir, remote);
|
|
@@ -8605,7 +8803,9 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
8605
8803
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
8606
8804
|
if (existing && !existing.prunable && existsSync14(existing.path)) {
|
|
8607
8805
|
const branch = existing.branch;
|
|
8608
|
-
if (status(existing.path).length > 0)
|
|
8806
|
+
if (status(existing.path).length > 0) {
|
|
8807
|
+
return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
|
|
8808
|
+
}
|
|
8609
8809
|
if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
|
|
8610
8810
|
if (!hasRemote) return { action: "reuse", path: existing.path, branch };
|
|
8611
8811
|
if (remoteBranchExists(dir, remote, branch)) {
|
|
@@ -8632,6 +8832,8 @@ function applyRunWorktree(dir, cfg, plan) {
|
|
|
8632
8832
|
case "leave-dirty":
|
|
8633
8833
|
case "leave-unpushed":
|
|
8634
8834
|
return null;
|
|
8835
|
+
case "resume-dirty":
|
|
8836
|
+
return plan.path;
|
|
8635
8837
|
case "reuse":
|
|
8636
8838
|
if (!worktrees(dir).some((w) => w.path === plan.path)) git(dir, ["worktree", "add", plan.path, plan.branch]);
|
|
8637
8839
|
return plan.path;
|
|
@@ -10447,7 +10649,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
10447
10649
|
}
|
|
10448
10650
|
|
|
10449
10651
|
// src/watch.ts
|
|
10450
|
-
import { watch as watchFile, existsSync as existsSync17, statSync as statSync6, openSync as
|
|
10652
|
+
import { watch as watchFile, existsSync as existsSync17, statSync as statSync6, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
10451
10653
|
var E = "\x1B[";
|
|
10452
10654
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
10453
10655
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -10552,10 +10754,10 @@ function readFrom(file, from) {
|
|
|
10552
10754
|
const size = statSync6(file).size;
|
|
10553
10755
|
if (size < from) return readFrom(file, 0);
|
|
10554
10756
|
if (size === from) return { events: [], offset: from };
|
|
10555
|
-
const fd =
|
|
10757
|
+
const fd = openSync4(file, "r");
|
|
10556
10758
|
const buf = Buffer.alloc(size - from);
|
|
10557
|
-
|
|
10558
|
-
|
|
10759
|
+
readSync2(fd, buf, 0, buf.length, from);
|
|
10760
|
+
closeSync4(fd);
|
|
10559
10761
|
const text = buf.toString("utf8");
|
|
10560
10762
|
const lastNewline = text.lastIndexOf("\n");
|
|
10561
10763
|
if (lastNewline === -1) return { events: [], offset: from };
|
|
@@ -11794,7 +11996,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
11794
11996
|
}
|
|
11795
11997
|
|
|
11796
11998
|
// src/cli.ts
|
|
11797
|
-
import { readFileSync as readFileSync18, existsSync as
|
|
11999
|
+
import { readFileSync as readFileSync18, existsSync as existsSync25, mkdirSync as mkdirSync13, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
|
|
11798
12000
|
import { createInterface } from "readline/promises";
|
|
11799
12001
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
11800
12002
|
|
|
@@ -12534,8 +12736,85 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
12534
12736
|
}
|
|
12535
12737
|
|
|
12536
12738
|
// src/daemon.ts
|
|
12537
|
-
import { existsSync as
|
|
12739
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync17, readdirSync as readdirSync9, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
|
|
12740
|
+
import { join as join24 } from "path";
|
|
12741
|
+
|
|
12742
|
+
// src/interrupted.ts
|
|
12743
|
+
import { existsSync as existsSync23, readdirSync as readdirSync8 } from "fs";
|
|
12538
12744
|
import { join as join23 } from "path";
|
|
12745
|
+
var INTERRUPTED_MARKER = "<!-- crew:interrupted -->";
|
|
12746
|
+
function interruptedComment(b) {
|
|
12747
|
+
const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
|
|
12748
|
+
return `${INTERRUPTED_MARKER}
|
|
12749
|
+
The agent run on this ticket was cut short (${b.reason}) before it handed on${b.branch ? `; its branch is \`${b.branch}\`` : ""}. ${tree} The next run on this seat resumes from the worktree rather than starting over.`;
|
|
12750
|
+
}
|
|
12751
|
+
var handled = /* @__PURE__ */ new Set();
|
|
12752
|
+
var pidAlive = (pid) => {
|
|
12753
|
+
try {
|
|
12754
|
+
process.kill(pid, 0);
|
|
12755
|
+
return true;
|
|
12756
|
+
} catch (e) {
|
|
12757
|
+
return e.code === "EPERM";
|
|
12758
|
+
}
|
|
12759
|
+
};
|
|
12760
|
+
function orphanedStreams(o) {
|
|
12761
|
+
const dir = join23(o.stateDir, "streams");
|
|
12762
|
+
if (!existsSync23(dir)) return [];
|
|
12763
|
+
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
12764
|
+
const out = [];
|
|
12765
|
+
for (const name of readdirSync8(dir)) {
|
|
12766
|
+
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
12767
|
+
const streamPath = join23(dir, name);
|
|
12768
|
+
if (existsSync23(`${streamPath}.meta.json`)) continue;
|
|
12769
|
+
const sidecar = readRunSidecar(streamPath);
|
|
12770
|
+
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
12771
|
+
if (ownerAlive(sidecar.ownerPid)) continue;
|
|
12772
|
+
out.push({ streamPath, sidecar });
|
|
12773
|
+
}
|
|
12774
|
+
return out;
|
|
12775
|
+
}
|
|
12776
|
+
async function reconcileRuns(o) {
|
|
12777
|
+
let adopted = 0;
|
|
12778
|
+
let interrupted = 0;
|
|
12779
|
+
for (const { streamPath, sidecar } of orphanedStreams(o)) {
|
|
12780
|
+
if (handled.has(streamPath)) continue;
|
|
12781
|
+
handled.add(streamPath);
|
|
12782
|
+
const isAlive = o.isAlive ?? runIsAlive;
|
|
12783
|
+
const lock = isAlive(sidecar) ? o.state.acquireRun(sidecar.role, o.maxConcurrentAgents) : null;
|
|
12784
|
+
const work = (async () => {
|
|
12785
|
+
try {
|
|
12786
|
+
const result = await reattachRun(streamPath, sidecar, o.emit, {
|
|
12787
|
+
agentLogKey: await o.agentLogKey?.(sidecar.role).catch(() => void 0),
|
|
12788
|
+
isAlive: o.isAlive,
|
|
12789
|
+
pollMs: o.pollMs,
|
|
12790
|
+
onAdopted: () => o.emit.emit(`adopted ${sidecar.role} run (pid ${sidecar.pid}) left by a previous crew process`, { step: "agent", role: sidecar.role })
|
|
12791
|
+
});
|
|
12792
|
+
if (result.adopted) adopted++;
|
|
12793
|
+
else interrupted++;
|
|
12794
|
+
try {
|
|
12795
|
+
await o.afterRun?.(sidecar, result);
|
|
12796
|
+
} catch (e) {
|
|
12797
|
+
o.emit.warn(`could not follow up on ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
12798
|
+
}
|
|
12799
|
+
} catch (e) {
|
|
12800
|
+
o.emit.warn(`could not reattach ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
12801
|
+
} finally {
|
|
12802
|
+
if (lock?.ok) lock.release();
|
|
12803
|
+
}
|
|
12804
|
+
})();
|
|
12805
|
+
if (isAlive(sidecar)) {
|
|
12806
|
+
if (o.inFlight) {
|
|
12807
|
+
o.inFlight.add(work);
|
|
12808
|
+
void work.finally(() => o.inFlight.delete(work));
|
|
12809
|
+
}
|
|
12810
|
+
} else {
|
|
12811
|
+
await work;
|
|
12812
|
+
}
|
|
12813
|
+
}
|
|
12814
|
+
return { adopted, interrupted };
|
|
12815
|
+
}
|
|
12816
|
+
|
|
12817
|
+
// src/daemon.ts
|
|
12539
12818
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
12540
12819
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
12541
12820
|
var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
|
|
@@ -12547,7 +12826,18 @@ async function runDaemonLoop(o) {
|
|
|
12547
12826
|
let passes = 0;
|
|
12548
12827
|
for (; ; ) {
|
|
12549
12828
|
if (o.signal?.aborted) return "aborted";
|
|
12550
|
-
|
|
12829
|
+
let result;
|
|
12830
|
+
try {
|
|
12831
|
+
result = await o.runPass();
|
|
12832
|
+
} catch (err) {
|
|
12833
|
+
passes++;
|
|
12834
|
+
o.onPassError?.(err);
|
|
12835
|
+
if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
|
|
12836
|
+
if (o.signal?.aborted) return "aborted";
|
|
12837
|
+
await sleep(backoff);
|
|
12838
|
+
backoff = Math.min(backoff * 2, ceiling);
|
|
12839
|
+
continue;
|
|
12840
|
+
}
|
|
12551
12841
|
passes++;
|
|
12552
12842
|
if (result.started.length > 0) {
|
|
12553
12843
|
backoff = floor;
|
|
@@ -12573,8 +12863,8 @@ function latestMtime(path) {
|
|
|
12573
12863
|
if (stat.isFile()) return stat.mtimeMs;
|
|
12574
12864
|
if (!stat.isDirectory()) return 0;
|
|
12575
12865
|
let latest = stat.mtimeMs;
|
|
12576
|
-
for (const entry of
|
|
12577
|
-
latest = Math.max(latest, latestMtime(
|
|
12866
|
+
for (const entry of readdirSync9(path, { withFileTypes: true })) {
|
|
12867
|
+
latest = Math.max(latest, latestMtime(join24(path, entry.name)));
|
|
12578
12868
|
}
|
|
12579
12869
|
return latest;
|
|
12580
12870
|
}
|
|
@@ -12583,13 +12873,13 @@ function computeTreeSignature(paths) {
|
|
|
12583
12873
|
}
|
|
12584
12874
|
function makeStaleChecker(paths, opts = {}) {
|
|
12585
12875
|
const initial = computeTreeSignature(paths);
|
|
12586
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
12876
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync24(opts.reloadSentinel);
|
|
12587
12877
|
}
|
|
12588
12878
|
function reloadSentinelFor(stateDir) {
|
|
12589
|
-
return
|
|
12879
|
+
return join24(stateDir, "reload-requested");
|
|
12590
12880
|
}
|
|
12591
12881
|
function daemonStampFor(stateDir) {
|
|
12592
|
-
return
|
|
12882
|
+
return join24(stateDir, "daemon.started");
|
|
12593
12883
|
}
|
|
12594
12884
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
12595
12885
|
mkdirSync12(stateDir, { recursive: true });
|
|
@@ -12652,6 +12942,11 @@ async function runOnePass(o) {
|
|
|
12652
12942
|
const spawn5 = o.spawnRoleAgent ?? runRoleAgent;
|
|
12653
12943
|
const breaker = o.spawnBreaker ?? new SpawnBreaker();
|
|
12654
12944
|
const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
|
|
12945
|
+
try {
|
|
12946
|
+
await (o.reconcile ?? reconcileForRoute)(ctx, o);
|
|
12947
|
+
} catch (e) {
|
|
12948
|
+
emit.warn(`could not reconcile earlier agent runs: ${e.message}`, { step: "agent" });
|
|
12949
|
+
}
|
|
12655
12950
|
const decision = await decide(ctx);
|
|
12656
12951
|
o.state.attention(o.route.route).persist(decision.attention);
|
|
12657
12952
|
if (decision.sweep.length) {
|
|
@@ -12749,6 +13044,54 @@ async function publishShipAttention(o) {
|
|
|
12749
13044
|
} catch {
|
|
12750
13045
|
}
|
|
12751
13046
|
}
|
|
13047
|
+
async function reconcileForRoute(ctx, o) {
|
|
13048
|
+
const { route, ship, emit } = ctx;
|
|
13049
|
+
await reconcileRuns({
|
|
13050
|
+
stateDir: ship.stateDir,
|
|
13051
|
+
routeLabel: routeSlug(route.route),
|
|
13052
|
+
emit,
|
|
13053
|
+
state: o.state,
|
|
13054
|
+
maxConcurrentAgents: o.maxConcurrentAgents,
|
|
13055
|
+
inFlight: o.inFlight,
|
|
13056
|
+
agentLogKey: async (role) => (await resolveSeatKey(route, role, ctx.getSessionStore)).apiKey,
|
|
13057
|
+
afterRun: async (sc, result) => {
|
|
13058
|
+
const identity = await resolveSeatKey(route, sc.role, ctx.getSessionStore);
|
|
13059
|
+
const tracker = new Tracker(route, ship, { apiKey: identity.apiKey });
|
|
13060
|
+
const memberId = route.resolved?.seats[sc.role];
|
|
13061
|
+
try {
|
|
13062
|
+
if (!sc.ticketRecordId) return;
|
|
13063
|
+
if (result.interrupted) {
|
|
13064
|
+
if (!memberId) return;
|
|
13065
|
+
const t = await tracker.ticket(sc.ticketRecordId);
|
|
13066
|
+
const target = repoTargetFor(route, t.repo_id);
|
|
13067
|
+
const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
|
|
13068
|
+
const wt = target && num ? worktreeForNumber(target.dir, num) : null;
|
|
13069
|
+
let dirty = null;
|
|
13070
|
+
try {
|
|
13071
|
+
if (wt) dirty = status(wt.path).length > 0;
|
|
13072
|
+
} catch {
|
|
13073
|
+
}
|
|
13074
|
+
await tracker.postEvent(sc.ticketRecordId, interruptedComment({
|
|
13075
|
+
branch: wt?.branch ?? null,
|
|
13076
|
+
dirty,
|
|
13077
|
+
reason: result.interrupted === "daemon-exit" ? "the crew daemon exited" : "it exited without a result"
|
|
13078
|
+
}), memberId, "interrupted");
|
|
13079
|
+
} else {
|
|
13080
|
+
await guardRunHandoff(route, ship, tracker, sc.role, sc.ticketRecordId, emit, sc.spawnedAt);
|
|
13081
|
+
}
|
|
13082
|
+
} finally {
|
|
13083
|
+
if (memberId) {
|
|
13084
|
+
await tracker.setCrewStatus(memberId, "idle").catch((e) => {
|
|
13085
|
+
emit.warn(`could not clear crew status: ${e.message}`, { step: "agent" });
|
|
13086
|
+
});
|
|
13087
|
+
}
|
|
13088
|
+
await tracker.beatIdle(ship.name).catch((e) => {
|
|
13089
|
+
emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
|
|
13090
|
+
});
|
|
13091
|
+
}
|
|
13092
|
+
}
|
|
13093
|
+
});
|
|
13094
|
+
}
|
|
12752
13095
|
async function runRoleAgent(role, decision, ctx) {
|
|
12753
13096
|
const { route, ship, emit } = ctx;
|
|
12754
13097
|
const identity = await resolveSeatKey(route, role, ctx.getSessionStore);
|
|
@@ -12838,6 +13181,7 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
12838
13181
|
cycle: emit.cycle,
|
|
12839
13182
|
ticket: ticketHint,
|
|
12840
13183
|
repo: ticketRepo,
|
|
13184
|
+
ticketRecordId: workingId,
|
|
12841
13185
|
...personaPromptOptions(await fetchPersonaPrompt(route, role, { userAgent: ship.userAgent })),
|
|
12842
13186
|
skillOverrides: await fetchSkillOverrides(route, { userAgent: ship.userAgent }),
|
|
12843
13187
|
agentPersona: await fetchSeatPersona(route, role, { userAgent: ship.userAgent }),
|
|
@@ -13414,14 +13758,14 @@ if (command === "passenger-sync-daemon") {
|
|
|
13414
13758
|
}
|
|
13415
13759
|
if (host === "linux" && hasSystemd) {
|
|
13416
13760
|
const label = labelFor(CREW_HOME, "run");
|
|
13417
|
-
const staleTimer =
|
|
13418
|
-
if (
|
|
13761
|
+
const staleTimer = join25(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
13762
|
+
if (existsSync25(staleTimer)) {
|
|
13419
13763
|
process.stdout.write(
|
|
13420
13764
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
13421
13765
|
`
|
|
13422
13766
|
);
|
|
13423
13767
|
if (!dryRun) {
|
|
13424
|
-
|
|
13768
|
+
spawnSync6("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
|
|
13425
13769
|
try {
|
|
13426
13770
|
unlinkSync5(staleTimer);
|
|
13427
13771
|
} catch {
|
|
@@ -13441,7 +13785,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
13441
13785
|
const tracker = new Tracker(c, cfg.ship);
|
|
13442
13786
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
13443
13787
|
for (const r of await resolvedRepos(c)) {
|
|
13444
|
-
if (!
|
|
13788
|
+
if (!existsSync25(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
13445
13789
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
13446
13790
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
13447
13791
|
}
|
|
@@ -13459,8 +13803,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
13459
13803
|
}
|
|
13460
13804
|
async function doctorFix(o) {
|
|
13461
13805
|
const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
|
|
13462
|
-
const unitExists = (p) =>
|
|
13463
|
-
const crontab = o.host === "linux" ?
|
|
13806
|
+
const unitExists = (p) => existsSync25(p);
|
|
13807
|
+
const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
13464
13808
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
13465
13809
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
13466
13810
|
for (const n of plan.notes) process.stdout.write(`fix: ${n}
|
|
@@ -14411,7 +14755,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14411
14755
|
}
|
|
14412
14756
|
let failed = false;
|
|
14413
14757
|
for (const cmd2 of commands) {
|
|
14414
|
-
const res2 =
|
|
14758
|
+
const res2 = spawnSync6(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
14415
14759
|
if (res2.stdout) process.stdout.write(res2.stdout);
|
|
14416
14760
|
if (res2.stderr) process.stderr.write(res2.stderr);
|
|
14417
14761
|
if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
|
|
@@ -14429,7 +14773,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14429
14773
|
`);
|
|
14430
14774
|
break;
|
|
14431
14775
|
}
|
|
14432
|
-
const res =
|
|
14776
|
+
const res = spawnSync6(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
14433
14777
|
if (res.stdout) process.stdout.write(res.stdout);
|
|
14434
14778
|
if (res.stderr) process.stderr.write(res.stderr);
|
|
14435
14779
|
if (daemonSub !== "status" && (res.error || res.status !== 0)) {
|
|
@@ -14452,7 +14796,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14452
14796
|
process.exit(1);
|
|
14453
14797
|
}
|
|
14454
14798
|
requireArmed("run the daemon loop");
|
|
14455
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
14799
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join25(CREW_HOME, "src"), join25(CREW_HOME, "dist"), join25(CREW_HOME, "bin")];
|
|
14456
14800
|
const configPaths = daemonConfigPaths(cfg);
|
|
14457
14801
|
watchPaths.push(...configPaths);
|
|
14458
14802
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -14473,6 +14817,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14473
14817
|
`)
|
|
14474
14818
|
})
|
|
14475
14819
|
}),
|
|
14820
|
+
onPassError: (err) => emit.error(
|
|
14821
|
+
`daemon pass failed, continuing: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
|
|
14822
|
+
),
|
|
14476
14823
|
checkStale: makeStaleChecker(watchPaths, { reloadSentinel: reloadSentinelFor(cfg.ship.stateDir) }),
|
|
14477
14824
|
onPass: (result) => {
|
|
14478
14825
|
if (result.started.length) emit.emit(`daemon pass started: ${result.started.join(", ")}`);
|
|
@@ -14611,7 +14958,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14611
14958
|
`);
|
|
14612
14959
|
if (!dryRun) {
|
|
14613
14960
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
14614
|
-
const raw =
|
|
14961
|
+
const raw = existsSync25(resolvedPath) ? JSON.parse(readFileSync18(resolvedPath, "utf8")) : {};
|
|
14615
14962
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
14616
14963
|
raw.agentSkills = skillResult.agentSkills;
|
|
14617
14964
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
@@ -14671,7 +15018,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14671
15018
|
`);
|
|
14672
15019
|
if (!dryRun) {
|
|
14673
15020
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
14674
|
-
const raw =
|
|
15021
|
+
const raw = existsSync25(resolvedPath) ? JSON.parse(readFileSync18(resolvedPath, "utf8")) : {};
|
|
14675
15022
|
raw.agentSkills = result.agentSkills;
|
|
14676
15023
|
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
14677
15024
|
writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -14694,7 +15041,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14694
15041
|
if (sub === "list" && !routeName) usage2();
|
|
14695
15042
|
if (sub === "add") {
|
|
14696
15043
|
try {
|
|
14697
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
15044
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync25(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
|
|
14698
15045
|
routeName = a.route;
|
|
14699
15046
|
addPath = a.path;
|
|
14700
15047
|
addName = a.name;
|
|
@@ -14709,7 +15056,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14709
15056
|
}
|
|
14710
15057
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
14711
15058
|
let resolvedRemotes;
|
|
14712
|
-
if (
|
|
15059
|
+
if (existsSync25(resolvedFile)) {
|
|
14713
15060
|
try {
|
|
14714
15061
|
resolvedRemotes = JSON.parse(readFileSync18(resolvedFile, "utf8"));
|
|
14715
15062
|
} catch {
|
|
@@ -14726,14 +15073,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14726
15073
|
for (const r of repos) {
|
|
14727
15074
|
const origin2 = originOf(r.dir);
|
|
14728
15075
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
14729
|
-
const note = !
|
|
15076
|
+
const note = !existsSync25(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
14730
15077
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
14731
15078
|
`);
|
|
14732
15079
|
}
|
|
14733
15080
|
break;
|
|
14734
15081
|
}
|
|
14735
15082
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
|
|
14736
|
-
const top =
|
|
15083
|
+
const top = existsSync25(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
14737
15084
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
14738
15085
|
const origin = originOf(top);
|
|
14739
15086
|
const plan = planRepoAdd({
|
|
@@ -14782,7 +15129,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14782
15129
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
14783
15130
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
14784
15131
|
`);
|
|
14785
|
-
if (
|
|
15132
|
+
if (existsSync25(resolvedFile)) {
|
|
14786
15133
|
const raw = JSON.parse(readFileSync18(resolvedFile, "utf8"));
|
|
14787
15134
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
14788
15135
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
@@ -15029,7 +15376,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15029
15376
|
operator = await pickOperator(found.holds);
|
|
15030
15377
|
}
|
|
15031
15378
|
let previous;
|
|
15032
|
-
const hadPreviousFile =
|
|
15379
|
+
const hadPreviousFile = existsSync25(resolvedPath);
|
|
15033
15380
|
if (hadPreviousFile) {
|
|
15034
15381
|
try {
|
|
15035
15382
|
previous = JSON.parse(readFileSync18(resolvedPath, "utf8"));
|
|
@@ -15132,7 +15479,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15132
15479
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
15133
15480
|
const wizard = firstRun;
|
|
15134
15481
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
15135
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
15482
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join25(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
15136
15483
|
const routeBlock = renderConnection(
|
|
15137
15484
|
found,
|
|
15138
15485
|
route2,
|
|
@@ -15508,7 +15855,19 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15508
15855
|
process.exit(2);
|
|
15509
15856
|
}
|
|
15510
15857
|
const roleWt = positional.find((p) => p === "dev" || p === "design" || p === "qa");
|
|
15511
|
-
|
|
15858
|
+
let resumeWt = false;
|
|
15859
|
+
try {
|
|
15860
|
+
const trWt = new Tracker(routeWt, cfg.ship);
|
|
15861
|
+
const heldWt = Object.values(routeWt.resolved?.seats ?? {}).includes(ticketWt[trWt.contract.columns.assignee]);
|
|
15862
|
+
if (heldWt) {
|
|
15863
|
+
const cut = await trWt.latestEvent(ticketWt.id, "interrupted");
|
|
15864
|
+
const pushed = await trWt.latestEvent(ticketWt.id, "pushed");
|
|
15865
|
+
const at = (e) => e?.occurred_at ?? e?.created_at ?? "";
|
|
15866
|
+
resumeWt = cut !== void 0 && at(cut) > at(pushed);
|
|
15867
|
+
}
|
|
15868
|
+
} catch {
|
|
15869
|
+
}
|
|
15870
|
+
const planWt = planRunWorktree(repoWt.dir, repoWt.config, ticketWt, roleWt, { resumeInterrupted: resumeWt });
|
|
15512
15871
|
if (dryRun) {
|
|
15513
15872
|
emit.emit(`would ${describeRunWorktreePlan(planWt)}`);
|
|
15514
15873
|
break;
|
|
@@ -15654,7 +16013,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15654
16013
|
}
|
|
15655
16014
|
const host = hostPlatform();
|
|
15656
16015
|
const control = host === "windows" ? null : planDaemonControl(CREW_HOME, host, detectSystemd());
|
|
15657
|
-
const unitStatus = control?.statusCommand ?
|
|
16016
|
+
const unitStatus = control?.statusCommand ? spawnSync6(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
|
|
15658
16017
|
const unitRunning = unitStatus !== null && !unitStatus.error && unitStatus.status === 0;
|
|
15659
16018
|
if (!unitRunning) {
|
|
15660
16019
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
@@ -15666,7 +16025,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15666
16025
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
15667
16026
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
15668
16027
|
for (const cmd of [control.stopCommand, control.startCommand]) {
|
|
15669
|
-
const res =
|
|
16028
|
+
const res = spawnSync6(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
15670
16029
|
if (cmd === control.startCommand && (res.error || res.status !== 0)) {
|
|
15671
16030
|
process.stderr.write(`crew reload: ${cmd.join(" ")} exited ${res.status ?? "error"}
|
|
15672
16031
|
`);
|
|
@@ -15922,13 +16281,28 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
15922
16281
|
);
|
|
15923
16282
|
try {
|
|
15924
16283
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
15925
|
-
const evSize =
|
|
16284
|
+
const evSize = existsSync25(evFile) ? statSync9(evFile).size : 0;
|
|
15926
16285
|
process.stdout.write(
|
|
15927
16286
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
15928
16287
|
` : "events log: ok\n"
|
|
15929
16288
|
);
|
|
15930
16289
|
} catch {
|
|
15931
16290
|
}
|
|
16291
|
+
if (host !== "windows") {
|
|
16292
|
+
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
16293
|
+
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
16294
|
+
try {
|
|
16295
|
+
return readFileSync18(p, "utf8");
|
|
16296
|
+
} catch {
|
|
16297
|
+
return null;
|
|
16298
|
+
}
|
|
16299
|
+
});
|
|
16300
|
+
if (abandons === false) {
|
|
16301
|
+
process.stdout.write(
|
|
16302
|
+
"scheduler unit: WARN the installed unit does not leave agent runs alone when the daemon restarts (no AbandonProcessGroup / KillMode=process) \u2014 a daemon crash or relaunch kills the run in flight; run `crew install`\n"
|
|
16303
|
+
);
|
|
16304
|
+
}
|
|
16305
|
+
}
|
|
15932
16306
|
const daemonStartedAt = readDaemonStarted(cfg.ship.stateDir);
|
|
15933
16307
|
if (daemonStartedAt !== null) {
|
|
15934
16308
|
const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
|
package/package.json
CHANGED