@tablation/crew 0.49.6 → 0.50.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +601 -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([
|
|
@@ -240,7 +240,7 @@ function mapStreamLine(line) {
|
|
|
240
240
|
if (block.type === "thinking") out.push({ kind: "thought", text: block.thinking });
|
|
241
241
|
else if (block.type === "text") out.push({ kind: "text", text: block.text });
|
|
242
242
|
else if (block.type === "tool_use") {
|
|
243
|
-
out.push({ kind: "tool", tool: block.name, target: toolTarget(block.name, block.input) });
|
|
243
|
+
out.push({ kind: "tool", tool: block.name, target: toolTarget(block.name, block.input), input: block.input });
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
return out;
|
|
@@ -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);
|
|
@@ -6120,7 +6132,7 @@ function operatorTodo(tickets, contract, operatorId) {
|
|
|
6120
6132
|
}
|
|
6121
6133
|
|
|
6122
6134
|
// src/digest.ts
|
|
6123
|
-
var DIRECTION_EVENT_TYPES = ["conflict_handed_back", "requeued", "bounced", "conflict_escalated"];
|
|
6135
|
+
var DIRECTION_EVENT_TYPES = ["conflict_handed_back", "requeued", "bounced", "conflict_escalated", "interrupted"];
|
|
6124
6136
|
function displayKey2(t) {
|
|
6125
6137
|
return t.issue_tag ?? t.issue_id ?? "?";
|
|
6126
6138
|
}
|
|
@@ -6249,7 +6261,7 @@ ${sorted(held).map((t) => `| ${displayKey2(t)} | ${stopOf(t)} |`).join("\n")}
|
|
|
6249
6261
|
"## Current queue \u2014 built for you by the poll\n",
|
|
6250
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",
|
|
6251
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',
|
|
6252
|
-
'\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',
|
|
6253
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",
|
|
6254
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"] : [],
|
|
6255
6267
|
"\n### Step 1 \u2014 open tickets that may be yours to act on\n",
|
|
@@ -7113,8 +7125,9 @@ ${g.tail}
|
|
|
7113
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)}`));
|
|
7114
7126
|
|
|
7115
7127
|
// src/agent.ts
|
|
7116
|
-
import { spawn as spawn2 } from "child_process";
|
|
7117
|
-
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";
|
|
7118
7131
|
import { finished } from "stream/promises";
|
|
7119
7132
|
import { join as join14, dirname as dirname6 } from "path";
|
|
7120
7133
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -8148,6 +8161,15 @@ function ticketMentions(text) {
|
|
|
8148
8161
|
var TicketAttributionTracker = class {
|
|
8149
8162
|
#tokensByTicket = /* @__PURE__ */ new Map();
|
|
8150
8163
|
#current;
|
|
8164
|
+
/** The ticket key most recently named in the transcript, or undefined before any. */
|
|
8165
|
+
get current() {
|
|
8166
|
+
return this.#current;
|
|
8167
|
+
}
|
|
8168
|
+
/** Record the mentions in `text` without crediting tokens (a claim needs "which ticket" before the turn's usage line arrives). */
|
|
8169
|
+
note(text) {
|
|
8170
|
+
const mentions = ticketMentions(text);
|
|
8171
|
+
if (mentions.length > 0) this.#current = mentions[mentions.length - 1];
|
|
8172
|
+
}
|
|
8151
8173
|
/** One assistant turn: its combined visible text (thinking + text blocks) and its own token cost. */
|
|
8152
8174
|
add(text, tokens) {
|
|
8153
8175
|
const mentions = ticketMentions(text);
|
|
@@ -8173,6 +8195,11 @@ var TicketAttributionTracker = class {
|
|
|
8173
8195
|
return best;
|
|
8174
8196
|
}
|
|
8175
8197
|
};
|
|
8198
|
+
function isClaimWrite(tool, input) {
|
|
8199
|
+
if (!tool || !/records_controller_update$/.test(tool)) return false;
|
|
8200
|
+
const body = input?.body;
|
|
8201
|
+
return typeof body === "object" && body !== null && body.status === "in_progress";
|
|
8202
|
+
}
|
|
8176
8203
|
|
|
8177
8204
|
// src/agent.ts
|
|
8178
8205
|
var DIGEST_MAX_AGE_SECONDS = 600;
|
|
@@ -8291,6 +8318,7 @@ function planAgentRun(o) {
|
|
|
8291
8318
|
streamPath: join14(streamsDir, `${base}.jsonl`),
|
|
8292
8319
|
eventsPath: join14(streamsDir, `${base}.events.jsonl`),
|
|
8293
8320
|
ticket: o.ticket,
|
|
8321
|
+
ticketRecordId: o.ticketRecordId ?? null,
|
|
8294
8322
|
agentLog,
|
|
8295
8323
|
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
8296
8324
|
promptSha
|
|
@@ -8314,6 +8342,43 @@ function describePlan(p) {
|
|
|
8314
8342
|
`stream: ${p.streamPath}`
|
|
8315
8343
|
].join("\n");
|
|
8316
8344
|
}
|
|
8345
|
+
var pidPathFor = (streamPath) => `${streamPath}.pid`;
|
|
8346
|
+
function readRunSidecar(streamPath) {
|
|
8347
|
+
try {
|
|
8348
|
+
const sc = JSON.parse(readFileSync12(pidPathFor(streamPath), "utf8"));
|
|
8349
|
+
return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
|
|
8350
|
+
} catch {
|
|
8351
|
+
return null;
|
|
8352
|
+
}
|
|
8353
|
+
}
|
|
8354
|
+
function runIsAlive(sc) {
|
|
8355
|
+
try {
|
|
8356
|
+
process.kill(sc.pid, 0);
|
|
8357
|
+
} catch (e) {
|
|
8358
|
+
if (e.code !== "EPERM") return false;
|
|
8359
|
+
}
|
|
8360
|
+
const ps = spawnSync5("ps", ["-o", "lstart=", "-p", String(sc.pid)], { encoding: "utf8" });
|
|
8361
|
+
const started = Date.parse(ps.stdout.trim());
|
|
8362
|
+
if (ps.status !== 0 || Number.isNaN(started)) return ps.status === 0;
|
|
8363
|
+
return Math.abs(started - Date.parse(sc.spawnedAt)) < 15e3;
|
|
8364
|
+
}
|
|
8365
|
+
function identityOf(plan, adapterHarness) {
|
|
8366
|
+
return {
|
|
8367
|
+
role: plan.role,
|
|
8368
|
+
route: plan.route,
|
|
8369
|
+
ticket: plan.ticket ?? null,
|
|
8370
|
+
model: plan.model,
|
|
8371
|
+
toolchain: plan.toolchain ?? LEGACY_TOOLCHAIN_HARNESS,
|
|
8372
|
+
harness: adapterHarness,
|
|
8373
|
+
provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER,
|
|
8374
|
+
eventsPath: plan.eventsPath,
|
|
8375
|
+
streamPath: plan.streamPath,
|
|
8376
|
+
agentLog: plan.agentLog ? withoutKey(plan.agentLog) : void 0,
|
|
8377
|
+
promptVersion: plan.promptVersion,
|
|
8378
|
+
promptSha: plan.promptSha
|
|
8379
|
+
};
|
|
8380
|
+
}
|
|
8381
|
+
var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
|
|
8317
8382
|
function openSink(path) {
|
|
8318
8383
|
try {
|
|
8319
8384
|
mkdirSync6(dirname6(path), { recursive: true });
|
|
@@ -8327,172 +8392,347 @@ function endSink(sink) {
|
|
|
8327
8392
|
sink.end();
|
|
8328
8393
|
return finished(sink).catch(() => void 0);
|
|
8329
8394
|
}
|
|
8395
|
+
var FileTail = class {
|
|
8396
|
+
offset = 0;
|
|
8397
|
+
decoder = new StringDecoder("utf8");
|
|
8398
|
+
path;
|
|
8399
|
+
constructor(path) {
|
|
8400
|
+
this.path = path;
|
|
8401
|
+
}
|
|
8402
|
+
poll() {
|
|
8403
|
+
let fd;
|
|
8404
|
+
try {
|
|
8405
|
+
fd = openSync3(this.path, "r");
|
|
8406
|
+
} catch {
|
|
8407
|
+
return "";
|
|
8408
|
+
}
|
|
8409
|
+
try {
|
|
8410
|
+
let text = "";
|
|
8411
|
+
const chunk = Buffer.alloc(64 * 1024);
|
|
8412
|
+
for (; ; ) {
|
|
8413
|
+
const n = readSync(fd, chunk, 0, chunk.length, this.offset);
|
|
8414
|
+
if (n <= 0) break;
|
|
8415
|
+
this.offset += n;
|
|
8416
|
+
text += this.decoder.write(chunk.subarray(0, n));
|
|
8417
|
+
}
|
|
8418
|
+
return text;
|
|
8419
|
+
} finally {
|
|
8420
|
+
closeSync3(fd);
|
|
8421
|
+
}
|
|
8422
|
+
}
|
|
8423
|
+
};
|
|
8424
|
+
var TAIL_POLL_MS = 200;
|
|
8425
|
+
var RunObserver = class {
|
|
8426
|
+
cycles = [];
|
|
8427
|
+
attribution = new TicketAttributionTracker();
|
|
8428
|
+
result;
|
|
8429
|
+
/** Set once the stream carries the harness's final result (turn count or cost). */
|
|
8430
|
+
sawResult = false;
|
|
8431
|
+
muted = false;
|
|
8432
|
+
/**
|
|
8433
|
+
* The label stamped on events. Starts as the poll hint and switches to the
|
|
8434
|
+
* ticket the run actually claims (CREW-1776); `id.ticket` stays the hint.
|
|
8435
|
+
*/
|
|
8436
|
+
liveTicket;
|
|
8437
|
+
/** Called after `liveTicket` changes, so the sidecar can be rewritten. */
|
|
8438
|
+
onClaim;
|
|
8439
|
+
buf = "";
|
|
8440
|
+
mapper;
|
|
8441
|
+
id;
|
|
8442
|
+
adapter;
|
|
8443
|
+
eventsSink;
|
|
8444
|
+
emitCycle;
|
|
8445
|
+
constructor(id, adapter, eventsSink, emitCycle) {
|
|
8446
|
+
this.id = id;
|
|
8447
|
+
this.adapter = adapter;
|
|
8448
|
+
this.eventsSink = eventsSink;
|
|
8449
|
+
this.emitCycle = emitCycle;
|
|
8450
|
+
this.liveTicket = id.ticket ?? void 0;
|
|
8451
|
+
this.mapper = adapter.createStreamMapper?.() ?? { map: (p) => adapter.mapStreamLine(p), flush: () => [] };
|
|
8452
|
+
}
|
|
8453
|
+
feed(text) {
|
|
8454
|
+
this.buf += text;
|
|
8455
|
+
const lines = this.buf.split("\n");
|
|
8456
|
+
this.buf = lines.pop() ?? "";
|
|
8457
|
+
for (const line of lines) this.handleLine(line);
|
|
8458
|
+
}
|
|
8459
|
+
end() {
|
|
8460
|
+
if (this.buf) this.handleLine(this.buf);
|
|
8461
|
+
this.buf = "";
|
|
8462
|
+
try {
|
|
8463
|
+
this.consume(void 0, this.mapper.flush());
|
|
8464
|
+
} catch {
|
|
8465
|
+
}
|
|
8466
|
+
}
|
|
8467
|
+
handleLine(line) {
|
|
8468
|
+
if (!line) return;
|
|
8469
|
+
let parsed;
|
|
8470
|
+
try {
|
|
8471
|
+
parsed = JSON.parse(line);
|
|
8472
|
+
} catch {
|
|
8473
|
+
return;
|
|
8474
|
+
}
|
|
8475
|
+
try {
|
|
8476
|
+
const got = this.adapter.extractResult(parsed);
|
|
8477
|
+
if (got) {
|
|
8478
|
+
this.result = { ...this.result, ...Object.fromEntries(Object.entries(got).filter(([, v]) => v !== void 0)) };
|
|
8479
|
+
if (got.numTurns !== void 0 || got.costUsd !== void 0) this.sawResult = true;
|
|
8480
|
+
}
|
|
8481
|
+
this.consume(parsed, this.mapper.map(parsed));
|
|
8482
|
+
} catch {
|
|
8483
|
+
}
|
|
8484
|
+
}
|
|
8485
|
+
// Turns mapped events (and, when there is a source line, its token usage)
|
|
8486
|
+
// into sink writes and buffered cycles. `parsed` is undefined for events
|
|
8487
|
+
// the mapper flushes at end of stream, which have no line of their own.
|
|
8488
|
+
consume(parsed, events) {
|
|
8489
|
+
const plan = this.id;
|
|
8490
|
+
let turnText = "";
|
|
8491
|
+
for (const ev of events) if ((ev.kind === "thought" || ev.kind === "text") && ev.text) this.attribution.note(ev.text);
|
|
8492
|
+
for (const ev of events) {
|
|
8493
|
+
if (ev.kind === "tool" && isClaimWrite(ev.tool, ev.input)) {
|
|
8494
|
+
const claimed = this.attribution.current;
|
|
8495
|
+
if (claimed && claimed !== this.liveTicket) {
|
|
8496
|
+
if (!this.muted) {
|
|
8497
|
+
this.eventsSink?.write(`${JSON.stringify({
|
|
8498
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8499
|
+
cycle: this.emitCycle,
|
|
8500
|
+
route: plan.route,
|
|
8501
|
+
step: "agent",
|
|
8502
|
+
level: "info",
|
|
8503
|
+
message: `ticket: ${this.liveTicket ?? "none"} \u2192 ${claimed}`,
|
|
8504
|
+
role: plan.role,
|
|
8505
|
+
ticket: claimed,
|
|
8506
|
+
data: { kind: "ticket", from: this.liveTicket ?? null, to: claimed }
|
|
8507
|
+
})}
|
|
8508
|
+
`);
|
|
8509
|
+
}
|
|
8510
|
+
this.liveTicket = claimed;
|
|
8511
|
+
if (!this.muted) this.onClaim?.();
|
|
8512
|
+
}
|
|
8513
|
+
}
|
|
8514
|
+
const e = {
|
|
8515
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8516
|
+
cycle: this.emitCycle,
|
|
8517
|
+
route: plan.route,
|
|
8518
|
+
step: "agent",
|
|
8519
|
+
level: "info",
|
|
8520
|
+
message: ev.kind === "tool" ? `${ev.tool ?? "tool"}${ev.target ? ` ${ev.target}` : ""}` : ev.kind,
|
|
8521
|
+
role: plan.role,
|
|
8522
|
+
...this.liveTicket ? { ticket: this.liveTicket } : {},
|
|
8523
|
+
data: { kind: ev.kind, ...ev.text ? { text: ev.text } : {}, ...ev.tool ? { tool: ev.tool } : {}, ...ev.target ? { target: ev.target } : {} }
|
|
8524
|
+
};
|
|
8525
|
+
if (!this.muted) this.eventsSink?.write(`${JSON.stringify(e)}
|
|
8526
|
+
`);
|
|
8527
|
+
if ((ev.kind === "thought" || ev.kind === "text") && ev.text) {
|
|
8528
|
+
turnText += `${ev.text}
|
|
8529
|
+
`;
|
|
8530
|
+
const cycleIndex = this.cycles.length;
|
|
8531
|
+
this.cycles.push({
|
|
8532
|
+
cycleIndex,
|
|
8533
|
+
occurredAt: e.at,
|
|
8534
|
+
thinking: ev.text,
|
|
8535
|
+
// A conversational `text` turn is tagged 'assistant' so it's
|
|
8536
|
+
// distinguishable from a thinking block once both share this
|
|
8537
|
+
// buffer (ISSUE-594) — matching the precedent
|
|
8538
|
+
// `agent-chat.service.ts`'s logChatRun (ISSUE-504) already set
|
|
8539
|
+
// for the same column. A `thought` cycle stays untagged, same
|
|
8540
|
+
// as before this change.
|
|
8541
|
+
...ev.kind === "text" ? { role: "assistant" } : {}
|
|
8542
|
+
});
|
|
8543
|
+
if (!this.muted) {
|
|
8544
|
+
this.eventsSink?.write(`${JSON.stringify({
|
|
8545
|
+
...e,
|
|
8546
|
+
message: "cycle",
|
|
8547
|
+
data: { kind: "cycle", cycleIndex, text: ev.text }
|
|
8548
|
+
})}
|
|
8549
|
+
`);
|
|
8550
|
+
}
|
|
8551
|
+
}
|
|
8552
|
+
}
|
|
8553
|
+
const turnTokens = parsed === void 0 ? void 0 : this.adapter.extractTurnTokens(parsed);
|
|
8554
|
+
if (turnTokens !== void 0) this.attribution.add(turnText, turnTokens);
|
|
8555
|
+
}
|
|
8556
|
+
};
|
|
8557
|
+
function writeRunMeta(id, startedAt, liveTicket, extra) {
|
|
8558
|
+
try {
|
|
8559
|
+
writeFileSync8(`${id.streamPath}.meta.json`, JSON.stringify({
|
|
8560
|
+
route: id.route,
|
|
8561
|
+
role: id.role,
|
|
8562
|
+
hint: id.ticket,
|
|
8563
|
+
ticket: liveTicket ?? id.ticket,
|
|
8564
|
+
startedAt,
|
|
8565
|
+
model: id.model,
|
|
8566
|
+
toolchain: id.toolchain,
|
|
8567
|
+
harness: id.harness,
|
|
8568
|
+
provider: id.provider,
|
|
8569
|
+
exitCode: null,
|
|
8570
|
+
running: true,
|
|
8571
|
+
...extra
|
|
8572
|
+
}));
|
|
8573
|
+
} catch {
|
|
8574
|
+
}
|
|
8575
|
+
}
|
|
8576
|
+
async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
|
|
8577
|
+
const attributedTicket = obs.attribution.winner(obs.liveTicket);
|
|
8578
|
+
const identity = { toolchain: id.toolchain, harness: id.harness, provider: id.provider };
|
|
8579
|
+
const { result } = obs;
|
|
8580
|
+
try {
|
|
8581
|
+
writeRunMeta(id, startedAt, obs.liveTicket, {
|
|
8582
|
+
attributedTicket: attributedTicket ?? null,
|
|
8583
|
+
running: void 0,
|
|
8584
|
+
exitCode: end.code,
|
|
8585
|
+
...end.interrupted ? { interrupted: end.interrupted } : {},
|
|
8586
|
+
sessionId: result?.sessionId ?? null,
|
|
8587
|
+
numTurns: result?.numTurns ?? null,
|
|
8588
|
+
totalCostUsd: result?.costUsd ?? null
|
|
8589
|
+
});
|
|
8590
|
+
} catch {
|
|
8591
|
+
}
|
|
8592
|
+
const ms2 = Date.now() - Date.parse(startedAt);
|
|
8593
|
+
emit.emit(end.interrupted ? "agent run interrupted" : `agent run finished (exit ${end.code ?? "unknown"})`, {
|
|
8594
|
+
data: {
|
|
8595
|
+
ms: ms2,
|
|
8596
|
+
code: end.code,
|
|
8597
|
+
...end.interrupted ? { interrupted: end.interrupted } : {},
|
|
8598
|
+
...identity,
|
|
8599
|
+
...result?.sessionId ? { sessionId: result.sessionId } : {},
|
|
8600
|
+
...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
|
|
8601
|
+
...result?.costUsd !== void 0 ? { totalCostUsd: result.costUsd } : {}
|
|
8602
|
+
}
|
|
8603
|
+
});
|
|
8604
|
+
if (id.agentLog && agentLogKey) {
|
|
8605
|
+
try {
|
|
8606
|
+
await reportAgentRun({ ...id.agentLog, apiKey: agentLogKey }, {
|
|
8607
|
+
ticketReference: attributedTicket,
|
|
8608
|
+
outcome: end.interrupted ? "interrupted" : end.code === 0 || end.code === null && obs.sawResult ? "success" : "error",
|
|
8609
|
+
startedAt,
|
|
8610
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8611
|
+
cycles: obs.cycles,
|
|
8612
|
+
model: id.model,
|
|
8613
|
+
tokensIn: result?.inputTokens,
|
|
8614
|
+
tokensOut: result?.outputTokens,
|
|
8615
|
+
cacheReadTokens: result?.cacheReadTokens,
|
|
8616
|
+
cacheWriteTokens: result?.cacheWriteTokens,
|
|
8617
|
+
costUsd: result?.costUsd,
|
|
8618
|
+
harness: identity.harness,
|
|
8619
|
+
provider: identity.provider,
|
|
8620
|
+
promptVersion: id.promptVersion,
|
|
8621
|
+
promptSha: id.promptSha
|
|
8622
|
+
});
|
|
8623
|
+
} catch (e) {
|
|
8624
|
+
emit.warn(`could not report agent log: ${e.message}`, { step: "agent" });
|
|
8625
|
+
}
|
|
8626
|
+
}
|
|
8627
|
+
return { ms: ms2, ticket: attributedTicket };
|
|
8628
|
+
}
|
|
8330
8629
|
function spawnAgent(plan, emit) {
|
|
8331
8630
|
const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
|
|
8332
8631
|
const started = Date.now();
|
|
8333
8632
|
const startedAt = new Date(started).toISOString();
|
|
8334
8633
|
for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
|
|
8335
|
-
const rawSink = openSink(plan.streamPath);
|
|
8336
8634
|
const eventsSink = openSink(plan.eventsPath);
|
|
8635
|
+
const id = identityOf(plan, adapter.harness);
|
|
8337
8636
|
return new Promise((resolve5, reject) => {
|
|
8637
|
+
let streamFd;
|
|
8638
|
+
try {
|
|
8639
|
+
mkdirSync6(dirname6(plan.streamPath), { recursive: true });
|
|
8640
|
+
streamFd = openSync3(plan.streamPath, "a");
|
|
8641
|
+
} catch (e) {
|
|
8642
|
+
void endSink(eventsSink);
|
|
8643
|
+
reject(new AgentSpawnError(`cannot open the stream file ${plan.streamPath}: ${e.message}`));
|
|
8644
|
+
return;
|
|
8645
|
+
}
|
|
8338
8646
|
const child = spawn2(plan.bin, plan.args, {
|
|
8339
8647
|
cwd: plan.cwd,
|
|
8340
8648
|
env: { ...scrubbedEnv(process.env, adapter.harness), ...plan.setEnv },
|
|
8341
|
-
stdio: ["pipe",
|
|
8649
|
+
stdio: ["pipe", streamFd, "inherit"],
|
|
8650
|
+
detached: true
|
|
8342
8651
|
});
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8652
|
+
closeSync3(streamFd);
|
|
8653
|
+
if (child.pid !== void 0) {
|
|
8654
|
+
const sidecar = {
|
|
8655
|
+
pid: child.pid,
|
|
8656
|
+
ownerPid: process.pid,
|
|
8657
|
+
spawnedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8658
|
+
role: plan.role,
|
|
8659
|
+
route: plan.route,
|
|
8660
|
+
ticket: plan.ticket ?? null,
|
|
8661
|
+
ticketRecordId: plan.ticketRecordId ?? null,
|
|
8662
|
+
model: plan.model,
|
|
8663
|
+
toolchain: id.toolchain,
|
|
8664
|
+
harness: id.harness,
|
|
8665
|
+
provider: id.provider,
|
|
8666
|
+
eventsPath: plan.eventsPath,
|
|
8667
|
+
agentLog: id.agentLog,
|
|
8668
|
+
promptVersion: plan.promptVersion,
|
|
8669
|
+
promptSha: plan.promptSha
|
|
8670
|
+
};
|
|
8361
8671
|
try {
|
|
8362
|
-
|
|
8363
|
-
if (got) result = { ...result, ...Object.fromEntries(Object.entries(got).filter(([, v]) => v !== void 0)) };
|
|
8364
|
-
consume(parsed, mapper.map(parsed));
|
|
8672
|
+
writeFileSync8(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
|
|
8365
8673
|
} catch {
|
|
8366
8674
|
}
|
|
8367
|
-
}
|
|
8368
|
-
const
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
cycle: emit.cycle,
|
|
8375
|
-
route: plan.route,
|
|
8376
|
-
step: "agent",
|
|
8377
|
-
level: "info",
|
|
8378
|
-
message: ev.kind === "tool" ? `${ev.tool ?? "tool"}${ev.target ? ` ${ev.target}` : ""}` : ev.kind,
|
|
8379
|
-
role: plan.role,
|
|
8380
|
-
...plan.ticket ? { ticket: plan.ticket } : {},
|
|
8381
|
-
data: { kind: ev.kind, ...ev.text ? { text: ev.text } : {}, ...ev.tool ? { tool: ev.tool } : {}, ...ev.target ? { target: ev.target } : {} }
|
|
8382
|
-
};
|
|
8383
|
-
eventsSink?.write(`${JSON.stringify(e)}
|
|
8384
|
-
`);
|
|
8385
|
-
if ((ev.kind === "thought" || ev.kind === "text") && ev.text) {
|
|
8386
|
-
turnText += `${ev.text}
|
|
8387
|
-
`;
|
|
8388
|
-
const cycleIndex = cycles.length;
|
|
8389
|
-
cycles.push({
|
|
8390
|
-
cycleIndex,
|
|
8391
|
-
occurredAt: e.at,
|
|
8392
|
-
thinking: ev.text,
|
|
8393
|
-
// A conversational `text` turn is tagged 'assistant' so it's
|
|
8394
|
-
// distinguishable from a thinking block once both share this
|
|
8395
|
-
// buffer (ISSUE-594) — matching the precedent
|
|
8396
|
-
// `agent-chat.service.ts`'s logChatRun (ISSUE-504) already set
|
|
8397
|
-
// for the same column. A `thought` cycle stays untagged, same
|
|
8398
|
-
// as before this change.
|
|
8399
|
-
...ev.kind === "text" ? { role: "assistant" } : {}
|
|
8400
|
-
});
|
|
8401
|
-
eventsSink?.write(`${JSON.stringify({
|
|
8402
|
-
...e,
|
|
8403
|
-
message: "cycle",
|
|
8404
|
-
data: { kind: "cycle", cycleIndex, text: ev.text }
|
|
8405
|
-
})}
|
|
8406
|
-
`);
|
|
8407
|
-
}
|
|
8408
|
-
}
|
|
8409
|
-
const turnTokens = parsed === void 0 ? void 0 : adapter.extractTurnTokens(parsed);
|
|
8410
|
-
if (turnTokens !== void 0) attribution.add(turnText, turnTokens);
|
|
8411
|
-
}
|
|
8412
|
-
};
|
|
8413
|
-
child.stdout.on("data", (chunk) => {
|
|
8414
|
-
buf += chunk.toString("utf8");
|
|
8415
|
-
const lines = buf.split("\n");
|
|
8416
|
-
buf = lines.pop() ?? "";
|
|
8417
|
-
for (const line of lines) handleLine(line);
|
|
8418
|
-
});
|
|
8675
|
+
}
|
|
8676
|
+
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
8677
|
+
const meta = { pid: child.pid ?? null };
|
|
8678
|
+
obs.onClaim = () => writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
8679
|
+
if (child.pid !== void 0) writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
8680
|
+
const tail = new FileTail(plan.streamPath);
|
|
8681
|
+
const timer = setInterval(() => obs.feed(tail.poll()), TAIL_POLL_MS);
|
|
8419
8682
|
let spawnFailed = false;
|
|
8420
8683
|
child.on("error", (err) => {
|
|
8421
8684
|
spawnFailed = true;
|
|
8685
|
+
clearInterval(timer);
|
|
8422
8686
|
reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err.message}`));
|
|
8423
8687
|
});
|
|
8424
8688
|
child.on("close", async (code) => {
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
}
|
|
8430
|
-
await Promise.all([endSink(rawSink), endSink(eventsSink)]);
|
|
8689
|
+
clearInterval(timer);
|
|
8690
|
+
obs.feed(tail.poll());
|
|
8691
|
+
obs.end();
|
|
8692
|
+
await endSink(eventsSink);
|
|
8431
8693
|
if (spawnFailed) return;
|
|
8432
|
-
const
|
|
8433
|
-
const identity = {
|
|
8434
|
-
toolchain: plan.toolchain ?? LEGACY_TOOLCHAIN_HARNESS,
|
|
8435
|
-
harness: adapter.harness,
|
|
8436
|
-
provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER
|
|
8437
|
-
};
|
|
8438
|
-
try {
|
|
8439
|
-
writeFileSync8(`${plan.streamPath}.meta.json`, JSON.stringify({
|
|
8440
|
-
route: plan.route,
|
|
8441
|
-
role: plan.role,
|
|
8442
|
-
ticket: plan.ticket ?? null,
|
|
8443
|
-
attributedTicket: attributedTicket ?? null,
|
|
8444
|
-
startedAt,
|
|
8445
|
-
exitCode: code ?? 1,
|
|
8446
|
-
model: plan.model,
|
|
8447
|
-
...identity,
|
|
8448
|
-
sessionId: result?.sessionId ?? null,
|
|
8449
|
-
numTurns: result?.numTurns ?? null,
|
|
8450
|
-
totalCostUsd: result?.costUsd ?? null
|
|
8451
|
-
}));
|
|
8452
|
-
} catch {
|
|
8453
|
-
}
|
|
8454
|
-
const ms2 = Date.now() - started;
|
|
8455
|
-
emit.emit(`agent run finished (exit ${code ?? 1})`, {
|
|
8456
|
-
data: {
|
|
8457
|
-
ms: ms2,
|
|
8458
|
-
code: code ?? 1,
|
|
8459
|
-
...identity,
|
|
8460
|
-
...result?.sessionId ? { sessionId: result.sessionId } : {},
|
|
8461
|
-
...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
|
|
8462
|
-
...result?.costUsd !== void 0 ? { totalCostUsd: result.costUsd } : {}
|
|
8463
|
-
}
|
|
8464
|
-
});
|
|
8465
|
-
if (plan.agentLog) {
|
|
8466
|
-
try {
|
|
8467
|
-
await reportAgentRun(plan.agentLog, {
|
|
8468
|
-
ticketReference: attributedTicket,
|
|
8469
|
-
outcome: (code ?? 1) === 0 ? "success" : "error",
|
|
8470
|
-
startedAt,
|
|
8471
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8472
|
-
cycles,
|
|
8473
|
-
model: plan.model,
|
|
8474
|
-
tokensIn: result?.inputTokens,
|
|
8475
|
-
tokensOut: result?.outputTokens,
|
|
8476
|
-
cacheReadTokens: result?.cacheReadTokens,
|
|
8477
|
-
cacheWriteTokens: result?.cacheWriteTokens,
|
|
8478
|
-
costUsd: result?.costUsd,
|
|
8479
|
-
harness: identity.harness,
|
|
8480
|
-
provider: identity.provider,
|
|
8481
|
-
promptVersion: plan.promptVersion,
|
|
8482
|
-
promptSha: plan.promptSha
|
|
8483
|
-
});
|
|
8484
|
-
} catch (e) {
|
|
8485
|
-
emit.warn(`could not report agent log: ${e.message}`, { step: "agent" });
|
|
8486
|
-
}
|
|
8487
|
-
}
|
|
8694
|
+
const { ms: ms2 } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey);
|
|
8488
8695
|
resolve5({ code: code ?? 1, ms: ms2 });
|
|
8489
8696
|
});
|
|
8490
|
-
child.stdin
|
|
8697
|
+
child.stdin?.on("error", (err) => {
|
|
8491
8698
|
emit.warn(`agent closed stdin before the prompt was delivered: ${err.message}`, { step: "agent" });
|
|
8492
8699
|
});
|
|
8493
|
-
child.stdin
|
|
8700
|
+
child.stdin?.end(plan.prompt);
|
|
8494
8701
|
});
|
|
8495
8702
|
}
|
|
8703
|
+
async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
8704
|
+
const adapter = adapterFor(sc.harness);
|
|
8705
|
+
const isAlive = o.isAlive ?? runIsAlive;
|
|
8706
|
+
const id = { ...sc, streamPath };
|
|
8707
|
+
const eventsSink = openSink(sc.eventsPath);
|
|
8708
|
+
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
8709
|
+
const tail = new FileTail(streamPath);
|
|
8710
|
+
obs.muted = true;
|
|
8711
|
+
obs.feed(tail.poll());
|
|
8712
|
+
obs.muted = false;
|
|
8713
|
+
const adopted = isAlive(sc);
|
|
8714
|
+
if (adopted) {
|
|
8715
|
+
o.onAdopted?.();
|
|
8716
|
+
const pollMs = o.pollMs ?? TAIL_POLL_MS;
|
|
8717
|
+
while (isAlive(sc)) {
|
|
8718
|
+
obs.feed(tail.poll());
|
|
8719
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
8720
|
+
}
|
|
8721
|
+
}
|
|
8722
|
+
obs.feed(tail.poll());
|
|
8723
|
+
obs.end();
|
|
8724
|
+
await endSink(eventsSink);
|
|
8725
|
+
const interrupted = obs.sawResult ? void 0 : adopted ? "no-result" : "daemon-exit";
|
|
8726
|
+
const { ms: ms2, ticket } = await finishRun(
|
|
8727
|
+
id,
|
|
8728
|
+
obs,
|
|
8729
|
+
emit,
|
|
8730
|
+
sc.spawnedAt,
|
|
8731
|
+
{ code: null, ...interrupted ? { interrupted } : {} },
|
|
8732
|
+
o.agentLogKey
|
|
8733
|
+
);
|
|
8734
|
+
return { adopted, ...interrupted ? { interrupted } : {}, ms: ms2, ticket };
|
|
8735
|
+
}
|
|
8496
8736
|
|
|
8497
8737
|
// src/run-worktree.ts
|
|
8498
8738
|
import { existsSync as existsSync14 } from "fs";
|
|
@@ -8601,6 +8841,8 @@ var describeRunWorktreePlan = (p) => {
|
|
|
8601
8841
|
return `reusing ${p.path} (${p.branch} is already level with the remote)`;
|
|
8602
8842
|
case "recut":
|
|
8603
8843
|
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}`;
|
|
8844
|
+
case "resume-dirty":
|
|
8845
|
+
return `${p.path} has uncommitted changes from this seat's previous run (interrupted) \u2014 resuming`;
|
|
8604
8846
|
case "leave-dirty":
|
|
8605
8847
|
return `left ${p.path} alone: it has uncommitted changes (someone may be editing it by hand)`;
|
|
8606
8848
|
case "leave-unpushed":
|
|
@@ -8612,7 +8854,7 @@ function newBranchName(cfg, t, role) {
|
|
|
8612
8854
|
const { prefix } = ticketBranchContext(t);
|
|
8613
8855
|
return branchRenderer(t, role)(effectiveBranchTemplate(cfg, prefix));
|
|
8614
8856
|
}
|
|
8615
|
-
function planRunWorktree(dir, cfg, t, role) {
|
|
8857
|
+
function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
8616
8858
|
const { remote, base } = cfg.branch;
|
|
8617
8859
|
const hasRemote = remoteConfigured(dir, remote);
|
|
8618
8860
|
if (hasRemote) fetchRemote(dir, remote);
|
|
@@ -8622,7 +8864,9 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
8622
8864
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
8623
8865
|
if (existing && !existing.prunable && existsSync14(existing.path)) {
|
|
8624
8866
|
const branch = existing.branch;
|
|
8625
|
-
if (status(existing.path).length > 0)
|
|
8867
|
+
if (status(existing.path).length > 0) {
|
|
8868
|
+
return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
|
|
8869
|
+
}
|
|
8626
8870
|
if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
|
|
8627
8871
|
if (!hasRemote) return { action: "reuse", path: existing.path, branch };
|
|
8628
8872
|
if (remoteBranchExists(dir, remote, branch)) {
|
|
@@ -8649,6 +8893,8 @@ function applyRunWorktree(dir, cfg, plan) {
|
|
|
8649
8893
|
case "leave-dirty":
|
|
8650
8894
|
case "leave-unpushed":
|
|
8651
8895
|
return null;
|
|
8896
|
+
case "resume-dirty":
|
|
8897
|
+
return plan.path;
|
|
8652
8898
|
case "reuse":
|
|
8653
8899
|
if (!worktrees(dir).some((w) => w.path === plan.path)) git(dir, ["worktree", "add", plan.path, plan.branch]);
|
|
8654
8900
|
return plan.path;
|
|
@@ -10464,7 +10710,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
10464
10710
|
}
|
|
10465
10711
|
|
|
10466
10712
|
// src/watch.ts
|
|
10467
|
-
import { watch as watchFile, existsSync as existsSync17, statSync as statSync6, openSync as
|
|
10713
|
+
import { watch as watchFile, existsSync as existsSync17, statSync as statSync6, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
10468
10714
|
var E = "\x1B[";
|
|
10469
10715
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
10470
10716
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -10569,10 +10815,10 @@ function readFrom(file, from) {
|
|
|
10569
10815
|
const size = statSync6(file).size;
|
|
10570
10816
|
if (size < from) return readFrom(file, 0);
|
|
10571
10817
|
if (size === from) return { events: [], offset: from };
|
|
10572
|
-
const fd =
|
|
10818
|
+
const fd = openSync4(file, "r");
|
|
10573
10819
|
const buf = Buffer.alloc(size - from);
|
|
10574
|
-
|
|
10575
|
-
|
|
10820
|
+
readSync2(fd, buf, 0, buf.length, from);
|
|
10821
|
+
closeSync4(fd);
|
|
10576
10822
|
const text = buf.toString("utf8");
|
|
10577
10823
|
const lastNewline = text.lastIndexOf("\n");
|
|
10578
10824
|
if (lastNewline === -1) return { events: [], offset: from };
|
|
@@ -11811,7 +12057,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
11811
12057
|
}
|
|
11812
12058
|
|
|
11813
12059
|
// src/cli.ts
|
|
11814
|
-
import { readFileSync as
|
|
12060
|
+
import { readFileSync as readFileSync19, existsSync as existsSync25, mkdirSync as mkdirSync13, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
|
|
11815
12061
|
import { createInterface } from "readline/promises";
|
|
11816
12062
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
11817
12063
|
|
|
@@ -12551,8 +12797,94 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
12551
12797
|
}
|
|
12552
12798
|
|
|
12553
12799
|
// src/daemon.ts
|
|
12554
|
-
import { existsSync as
|
|
12800
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync18, readdirSync as readdirSync9, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
|
|
12801
|
+
import { join as join24 } from "path";
|
|
12802
|
+
|
|
12803
|
+
// src/interrupted.ts
|
|
12804
|
+
import { existsSync as existsSync23, readdirSync as readdirSync8, readFileSync as readFileSync17 } from "fs";
|
|
12555
12805
|
import { join as join23 } from "path";
|
|
12806
|
+
var INTERRUPTED_MARKER = "<!-- crew:interrupted -->";
|
|
12807
|
+
function interruptedComment(b) {
|
|
12808
|
+
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.";
|
|
12809
|
+
return `${INTERRUPTED_MARKER}
|
|
12810
|
+
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.`;
|
|
12811
|
+
}
|
|
12812
|
+
var handled = /* @__PURE__ */ new Set();
|
|
12813
|
+
var pidAlive = (pid) => {
|
|
12814
|
+
try {
|
|
12815
|
+
process.kill(pid, 0);
|
|
12816
|
+
return true;
|
|
12817
|
+
} catch (e) {
|
|
12818
|
+
return e.code === "EPERM";
|
|
12819
|
+
}
|
|
12820
|
+
};
|
|
12821
|
+
function runFinished(streamPath) {
|
|
12822
|
+
const path = `${streamPath}.meta.json`;
|
|
12823
|
+
if (!existsSync23(path)) return false;
|
|
12824
|
+
try {
|
|
12825
|
+
return JSON.parse(readFileSync17(path, "utf8")).running !== true;
|
|
12826
|
+
} catch {
|
|
12827
|
+
return true;
|
|
12828
|
+
}
|
|
12829
|
+
}
|
|
12830
|
+
function orphanedStreams(o) {
|
|
12831
|
+
const dir = join23(o.stateDir, "streams");
|
|
12832
|
+
if (!existsSync23(dir)) return [];
|
|
12833
|
+
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
12834
|
+
const out = [];
|
|
12835
|
+
for (const name of readdirSync8(dir)) {
|
|
12836
|
+
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
12837
|
+
const streamPath = join23(dir, name);
|
|
12838
|
+
if (runFinished(streamPath)) continue;
|
|
12839
|
+
const sidecar = readRunSidecar(streamPath);
|
|
12840
|
+
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
12841
|
+
if (ownerAlive(sidecar.ownerPid)) continue;
|
|
12842
|
+
out.push({ streamPath, sidecar });
|
|
12843
|
+
}
|
|
12844
|
+
return out;
|
|
12845
|
+
}
|
|
12846
|
+
async function reconcileRuns(o) {
|
|
12847
|
+
let adopted = 0;
|
|
12848
|
+
let interrupted = 0;
|
|
12849
|
+
for (const { streamPath, sidecar } of orphanedStreams(o)) {
|
|
12850
|
+
if (handled.has(streamPath)) continue;
|
|
12851
|
+
handled.add(streamPath);
|
|
12852
|
+
const isAlive = o.isAlive ?? runIsAlive;
|
|
12853
|
+
const lock = isAlive(sidecar) ? o.state.acquireRun(sidecar.role, o.maxConcurrentAgents) : null;
|
|
12854
|
+
const work = (async () => {
|
|
12855
|
+
try {
|
|
12856
|
+
const result = await reattachRun(streamPath, sidecar, o.emit, {
|
|
12857
|
+
agentLogKey: await o.agentLogKey?.(sidecar.role).catch(() => void 0),
|
|
12858
|
+
isAlive: o.isAlive,
|
|
12859
|
+
pollMs: o.pollMs,
|
|
12860
|
+
onAdopted: () => o.emit.emit(`adopted ${sidecar.role} run (pid ${sidecar.pid}) left by a previous crew process`, { step: "agent", role: sidecar.role })
|
|
12861
|
+
});
|
|
12862
|
+
if (result.adopted) adopted++;
|
|
12863
|
+
else interrupted++;
|
|
12864
|
+
try {
|
|
12865
|
+
await o.afterRun?.(sidecar, result);
|
|
12866
|
+
} catch (e) {
|
|
12867
|
+
o.emit.warn(`could not follow up on ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
12868
|
+
}
|
|
12869
|
+
} catch (e) {
|
|
12870
|
+
o.emit.warn(`could not reattach ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
12871
|
+
} finally {
|
|
12872
|
+
if (lock?.ok) lock.release();
|
|
12873
|
+
}
|
|
12874
|
+
})();
|
|
12875
|
+
if (isAlive(sidecar)) {
|
|
12876
|
+
if (o.inFlight) {
|
|
12877
|
+
o.inFlight.add(work);
|
|
12878
|
+
void work.finally(() => o.inFlight.delete(work));
|
|
12879
|
+
}
|
|
12880
|
+
} else {
|
|
12881
|
+
await work;
|
|
12882
|
+
}
|
|
12883
|
+
}
|
|
12884
|
+
return { adopted, interrupted };
|
|
12885
|
+
}
|
|
12886
|
+
|
|
12887
|
+
// src/daemon.ts
|
|
12556
12888
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
12557
12889
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
12558
12890
|
var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
|
|
@@ -12601,8 +12933,8 @@ function latestMtime(path) {
|
|
|
12601
12933
|
if (stat.isFile()) return stat.mtimeMs;
|
|
12602
12934
|
if (!stat.isDirectory()) return 0;
|
|
12603
12935
|
let latest = stat.mtimeMs;
|
|
12604
|
-
for (const entry of
|
|
12605
|
-
latest = Math.max(latest, latestMtime(
|
|
12936
|
+
for (const entry of readdirSync9(path, { withFileTypes: true })) {
|
|
12937
|
+
latest = Math.max(latest, latestMtime(join24(path, entry.name)));
|
|
12606
12938
|
}
|
|
12607
12939
|
return latest;
|
|
12608
12940
|
}
|
|
@@ -12611,13 +12943,13 @@ function computeTreeSignature(paths) {
|
|
|
12611
12943
|
}
|
|
12612
12944
|
function makeStaleChecker(paths, opts = {}) {
|
|
12613
12945
|
const initial = computeTreeSignature(paths);
|
|
12614
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
12946
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync24(opts.reloadSentinel);
|
|
12615
12947
|
}
|
|
12616
12948
|
function reloadSentinelFor(stateDir) {
|
|
12617
|
-
return
|
|
12949
|
+
return join24(stateDir, "reload-requested");
|
|
12618
12950
|
}
|
|
12619
12951
|
function daemonStampFor(stateDir) {
|
|
12620
|
-
return
|
|
12952
|
+
return join24(stateDir, "daemon.started");
|
|
12621
12953
|
}
|
|
12622
12954
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
12623
12955
|
mkdirSync12(stateDir, { recursive: true });
|
|
@@ -12627,7 +12959,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
|
12627
12959
|
}
|
|
12628
12960
|
function readDaemonStarted(stateDir) {
|
|
12629
12961
|
try {
|
|
12630
|
-
const v = JSON.parse(
|
|
12962
|
+
const v = JSON.parse(readFileSync18(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
12631
12963
|
return typeof v === "number" ? v : null;
|
|
12632
12964
|
} catch {
|
|
12633
12965
|
return null;
|
|
@@ -12680,6 +13012,11 @@ async function runOnePass(o) {
|
|
|
12680
13012
|
const spawn5 = o.spawnRoleAgent ?? runRoleAgent;
|
|
12681
13013
|
const breaker = o.spawnBreaker ?? new SpawnBreaker();
|
|
12682
13014
|
const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
|
|
13015
|
+
try {
|
|
13016
|
+
await (o.reconcile ?? reconcileForRoute)(ctx, o);
|
|
13017
|
+
} catch (e) {
|
|
13018
|
+
emit.warn(`could not reconcile earlier agent runs: ${e.message}`, { step: "agent" });
|
|
13019
|
+
}
|
|
12683
13020
|
const decision = await decide(ctx);
|
|
12684
13021
|
o.state.attention(o.route.route).persist(decision.attention);
|
|
12685
13022
|
if (decision.sweep.length) {
|
|
@@ -12777,6 +13114,54 @@ async function publishShipAttention(o) {
|
|
|
12777
13114
|
} catch {
|
|
12778
13115
|
}
|
|
12779
13116
|
}
|
|
13117
|
+
async function reconcileForRoute(ctx, o) {
|
|
13118
|
+
const { route, ship, emit } = ctx;
|
|
13119
|
+
await reconcileRuns({
|
|
13120
|
+
stateDir: ship.stateDir,
|
|
13121
|
+
routeLabel: routeSlug(route.route),
|
|
13122
|
+
emit,
|
|
13123
|
+
state: o.state,
|
|
13124
|
+
maxConcurrentAgents: o.maxConcurrentAgents,
|
|
13125
|
+
inFlight: o.inFlight,
|
|
13126
|
+
agentLogKey: async (role) => (await resolveSeatKey(route, role, ctx.getSessionStore)).apiKey,
|
|
13127
|
+
afterRun: async (sc, result) => {
|
|
13128
|
+
const identity = await resolveSeatKey(route, sc.role, ctx.getSessionStore);
|
|
13129
|
+
const tracker = new Tracker(route, ship, { apiKey: identity.apiKey });
|
|
13130
|
+
const memberId = route.resolved?.seats[sc.role];
|
|
13131
|
+
try {
|
|
13132
|
+
if (!sc.ticketRecordId) return;
|
|
13133
|
+
if (result.interrupted) {
|
|
13134
|
+
if (!memberId) return;
|
|
13135
|
+
const t = await tracker.ticket(sc.ticketRecordId);
|
|
13136
|
+
const target = repoTargetFor(route, t.repo_id);
|
|
13137
|
+
const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
|
|
13138
|
+
const wt = target && num ? worktreeForNumber(target.dir, num) : null;
|
|
13139
|
+
let dirty = null;
|
|
13140
|
+
try {
|
|
13141
|
+
if (wt) dirty = status(wt.path).length > 0;
|
|
13142
|
+
} catch {
|
|
13143
|
+
}
|
|
13144
|
+
await tracker.postEvent(sc.ticketRecordId, interruptedComment({
|
|
13145
|
+
branch: wt?.branch ?? null,
|
|
13146
|
+
dirty,
|
|
13147
|
+
reason: result.interrupted === "daemon-exit" ? "the crew daemon exited" : "it exited without a result"
|
|
13148
|
+
}), memberId, "interrupted");
|
|
13149
|
+
} else {
|
|
13150
|
+
await guardRunHandoff(route, ship, tracker, sc.role, sc.ticketRecordId, emit, sc.spawnedAt);
|
|
13151
|
+
}
|
|
13152
|
+
} finally {
|
|
13153
|
+
if (memberId) {
|
|
13154
|
+
await tracker.setCrewStatus(memberId, "idle").catch((e) => {
|
|
13155
|
+
emit.warn(`could not clear crew status: ${e.message}`, { step: "agent" });
|
|
13156
|
+
});
|
|
13157
|
+
}
|
|
13158
|
+
await tracker.beatIdle(ship.name).catch((e) => {
|
|
13159
|
+
emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
|
|
13160
|
+
});
|
|
13161
|
+
}
|
|
13162
|
+
}
|
|
13163
|
+
});
|
|
13164
|
+
}
|
|
12780
13165
|
async function runRoleAgent(role, decision, ctx) {
|
|
12781
13166
|
const { route, ship, emit } = ctx;
|
|
12782
13167
|
const identity = await resolveSeatKey(route, role, ctx.getSessionStore);
|
|
@@ -12866,6 +13251,7 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
12866
13251
|
cycle: emit.cycle,
|
|
12867
13252
|
ticket: ticketHint,
|
|
12868
13253
|
repo: ticketRepo,
|
|
13254
|
+
ticketRecordId: workingId,
|
|
12869
13255
|
...personaPromptOptions(await fetchPersonaPrompt(route, role, { userAgent: ship.userAgent })),
|
|
12870
13256
|
skillOverrides: await fetchSkillOverrides(route, { userAgent: ship.userAgent }),
|
|
12871
13257
|
agentPersona: await fetchSeatPersona(route, role, { userAgent: ship.userAgent }),
|
|
@@ -13442,14 +13828,14 @@ if (command === "passenger-sync-daemon") {
|
|
|
13442
13828
|
}
|
|
13443
13829
|
if (host === "linux" && hasSystemd) {
|
|
13444
13830
|
const label = labelFor(CREW_HOME, "run");
|
|
13445
|
-
const staleTimer =
|
|
13446
|
-
if (
|
|
13831
|
+
const staleTimer = join25(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
13832
|
+
if (existsSync25(staleTimer)) {
|
|
13447
13833
|
process.stdout.write(
|
|
13448
13834
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
13449
13835
|
`
|
|
13450
13836
|
);
|
|
13451
13837
|
if (!dryRun) {
|
|
13452
|
-
|
|
13838
|
+
spawnSync6("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
|
|
13453
13839
|
try {
|
|
13454
13840
|
unlinkSync5(staleTimer);
|
|
13455
13841
|
} catch {
|
|
@@ -13469,7 +13855,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
13469
13855
|
const tracker = new Tracker(c, cfg.ship);
|
|
13470
13856
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
13471
13857
|
for (const r of await resolvedRepos(c)) {
|
|
13472
|
-
if (!
|
|
13858
|
+
if (!existsSync25(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
13473
13859
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
13474
13860
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
13475
13861
|
}
|
|
@@ -13487,8 +13873,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
13487
13873
|
}
|
|
13488
13874
|
async function doctorFix(o) {
|
|
13489
13875
|
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))));
|
|
13490
|
-
const unitExists = (p) =>
|
|
13491
|
-
const crontab = o.host === "linux" ?
|
|
13876
|
+
const unitExists = (p) => existsSync25(p);
|
|
13877
|
+
const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
13492
13878
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
13493
13879
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
13494
13880
|
for (const n of plan.notes) process.stdout.write(`fix: ${n}
|
|
@@ -13504,7 +13890,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
13504
13890
|
for (const a of plan.actions) {
|
|
13505
13891
|
if (a.kind === "enable") {
|
|
13506
13892
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
13507
|
-
writeFileSync14(cfg.configFile, planEnable(
|
|
13893
|
+
writeFileSync14(cfg.configFile, planEnable(readFileSync19(cfg.configFile, "utf8"), a.route));
|
|
13508
13894
|
enabledSound.add(a.route);
|
|
13509
13895
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
13510
13896
|
} else {
|
|
@@ -14439,7 +14825,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14439
14825
|
}
|
|
14440
14826
|
let failed = false;
|
|
14441
14827
|
for (const cmd2 of commands) {
|
|
14442
|
-
const res2 =
|
|
14828
|
+
const res2 = spawnSync6(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
14443
14829
|
if (res2.stdout) process.stdout.write(res2.stdout);
|
|
14444
14830
|
if (res2.stderr) process.stderr.write(res2.stderr);
|
|
14445
14831
|
if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
|
|
@@ -14457,7 +14843,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14457
14843
|
`);
|
|
14458
14844
|
break;
|
|
14459
14845
|
}
|
|
14460
|
-
const res =
|
|
14846
|
+
const res = spawnSync6(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
14461
14847
|
if (res.stdout) process.stdout.write(res.stdout);
|
|
14462
14848
|
if (res.stderr) process.stderr.write(res.stderr);
|
|
14463
14849
|
if (daemonSub !== "status" && (res.error || res.status !== 0)) {
|
|
@@ -14480,7 +14866,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14480
14866
|
process.exit(1);
|
|
14481
14867
|
}
|
|
14482
14868
|
requireArmed("run the daemon loop");
|
|
14483
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
14869
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join25(CREW_HOME, "src"), join25(CREW_HOME, "dist"), join25(CREW_HOME, "bin")];
|
|
14484
14870
|
const configPaths = daemonConfigPaths(cfg);
|
|
14485
14871
|
watchPaths.push(...configPaths);
|
|
14486
14872
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -14642,7 +15028,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14642
15028
|
`);
|
|
14643
15029
|
if (!dryRun) {
|
|
14644
15030
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
14645
|
-
const raw =
|
|
15031
|
+
const raw = existsSync25(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
|
|
14646
15032
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
14647
15033
|
raw.agentSkills = skillResult.agentSkills;
|
|
14648
15034
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
@@ -14702,7 +15088,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14702
15088
|
`);
|
|
14703
15089
|
if (!dryRun) {
|
|
14704
15090
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
14705
|
-
const raw =
|
|
15091
|
+
const raw = existsSync25(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
|
|
14706
15092
|
raw.agentSkills = result.agentSkills;
|
|
14707
15093
|
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
14708
15094
|
writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -14718,14 +15104,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14718
15104
|
process.exit(2);
|
|
14719
15105
|
};
|
|
14720
15106
|
if (sub !== "add" && sub !== "list") usage2();
|
|
14721
|
-
const configText =
|
|
15107
|
+
const configText = readFileSync19(cfg.configFile, "utf8");
|
|
14722
15108
|
let routeName = positional[2];
|
|
14723
15109
|
let addPath = ".";
|
|
14724
15110
|
let addName;
|
|
14725
15111
|
if (sub === "list" && !routeName) usage2();
|
|
14726
15112
|
if (sub === "add") {
|
|
14727
15113
|
try {
|
|
14728
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
15114
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync25(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
|
|
14729
15115
|
routeName = a.route;
|
|
14730
15116
|
addPath = a.path;
|
|
14731
15117
|
addName = a.name;
|
|
@@ -14740,9 +15126,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14740
15126
|
}
|
|
14741
15127
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
14742
15128
|
let resolvedRemotes;
|
|
14743
|
-
if (
|
|
15129
|
+
if (existsSync25(resolvedFile)) {
|
|
14744
15130
|
try {
|
|
14745
|
-
resolvedRemotes = JSON.parse(
|
|
15131
|
+
resolvedRemotes = JSON.parse(readFileSync19(resolvedFile, "utf8"));
|
|
14746
15132
|
} catch {
|
|
14747
15133
|
}
|
|
14748
15134
|
}
|
|
@@ -14757,14 +15143,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14757
15143
|
for (const r of repos) {
|
|
14758
15144
|
const origin2 = originOf(r.dir);
|
|
14759
15145
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
14760
|
-
const note = !
|
|
15146
|
+
const note = !existsSync25(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
14761
15147
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
14762
15148
|
`);
|
|
14763
15149
|
}
|
|
14764
15150
|
break;
|
|
14765
15151
|
}
|
|
14766
15152
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
|
|
14767
|
-
const top =
|
|
15153
|
+
const top = existsSync25(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
14768
15154
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
14769
15155
|
const origin = originOf(top);
|
|
14770
15156
|
const plan = planRepoAdd({
|
|
@@ -14813,8 +15199,8 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14813
15199
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
14814
15200
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
14815
15201
|
`);
|
|
14816
|
-
if (
|
|
14817
|
-
const raw = JSON.parse(
|
|
15202
|
+
if (existsSync25(resolvedFile)) {
|
|
15203
|
+
const raw = JSON.parse(readFileSync19(resolvedFile, "utf8"));
|
|
14818
15204
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
14819
15205
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
14820
15206
|
writeFileSync14(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -15060,10 +15446,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15060
15446
|
operator = await pickOperator(found.holds);
|
|
15061
15447
|
}
|
|
15062
15448
|
let previous;
|
|
15063
|
-
const hadPreviousFile =
|
|
15449
|
+
const hadPreviousFile = existsSync25(resolvedPath);
|
|
15064
15450
|
if (hadPreviousFile) {
|
|
15065
15451
|
try {
|
|
15066
|
-
previous = JSON.parse(
|
|
15452
|
+
previous = JSON.parse(readFileSync19(resolvedPath, "utf8"));
|
|
15067
15453
|
} catch {
|
|
15068
15454
|
}
|
|
15069
15455
|
}
|
|
@@ -15163,7 +15549,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15163
15549
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
15164
15550
|
const wizard = firstRun;
|
|
15165
15551
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
15166
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
15552
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join25(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
15167
15553
|
const routeBlock = renderConnection(
|
|
15168
15554
|
found,
|
|
15169
15555
|
route2,
|
|
@@ -15539,7 +15925,19 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15539
15925
|
process.exit(2);
|
|
15540
15926
|
}
|
|
15541
15927
|
const roleWt = positional.find((p) => p === "dev" || p === "design" || p === "qa");
|
|
15542
|
-
|
|
15928
|
+
let resumeWt = false;
|
|
15929
|
+
try {
|
|
15930
|
+
const trWt = new Tracker(routeWt, cfg.ship);
|
|
15931
|
+
const heldWt = Object.values(routeWt.resolved?.seats ?? {}).includes(ticketWt[trWt.contract.columns.assignee]);
|
|
15932
|
+
if (heldWt) {
|
|
15933
|
+
const cut = await trWt.latestEvent(ticketWt.id, "interrupted");
|
|
15934
|
+
const pushed = await trWt.latestEvent(ticketWt.id, "pushed");
|
|
15935
|
+
const at = (e) => e?.occurred_at ?? e?.created_at ?? "";
|
|
15936
|
+
resumeWt = cut !== void 0 && at(cut) > at(pushed);
|
|
15937
|
+
}
|
|
15938
|
+
} catch {
|
|
15939
|
+
}
|
|
15940
|
+
const planWt = planRunWorktree(repoWt.dir, repoWt.config, ticketWt, roleWt, { resumeInterrupted: resumeWt });
|
|
15543
15941
|
if (dryRun) {
|
|
15544
15942
|
emit.emit(`would ${describeRunWorktreePlan(planWt)}`);
|
|
15545
15943
|
break;
|
|
@@ -15685,7 +16083,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15685
16083
|
}
|
|
15686
16084
|
const host = hostPlatform();
|
|
15687
16085
|
const control = host === "windows" ? null : planDaemonControl(CREW_HOME, host, detectSystemd());
|
|
15688
|
-
const unitStatus = control?.statusCommand ?
|
|
16086
|
+
const unitStatus = control?.statusCommand ? spawnSync6(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
|
|
15689
16087
|
const unitRunning = unitStatus !== null && !unitStatus.error && unitStatus.status === 0;
|
|
15690
16088
|
if (!unitRunning) {
|
|
15691
16089
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
@@ -15697,7 +16095,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15697
16095
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
15698
16096
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
15699
16097
|
for (const cmd of [control.stopCommand, control.startCommand]) {
|
|
15700
|
-
const res =
|
|
16098
|
+
const res = spawnSync6(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
15701
16099
|
if (cmd === control.startCommand && (res.error || res.status !== 0)) {
|
|
15702
16100
|
process.stderr.write(`crew reload: ${cmd.join(" ")} exited ${res.status ?? "error"}
|
|
15703
16101
|
`);
|
|
@@ -15743,7 +16141,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15743
16141
|
}
|
|
15744
16142
|
case "log": {
|
|
15745
16143
|
try {
|
|
15746
|
-
const text =
|
|
16144
|
+
const text = readFileSync19(cfg.ship.logFile, "utf8").split("\n");
|
|
15747
16145
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
15748
16146
|
`);
|
|
15749
16147
|
} catch {
|
|
@@ -15953,13 +16351,28 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
15953
16351
|
);
|
|
15954
16352
|
try {
|
|
15955
16353
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
15956
|
-
const evSize =
|
|
16354
|
+
const evSize = existsSync25(evFile) ? statSync9(evFile).size : 0;
|
|
15957
16355
|
process.stdout.write(
|
|
15958
16356
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
15959
16357
|
` : "events log: ok\n"
|
|
15960
16358
|
);
|
|
15961
16359
|
} catch {
|
|
15962
16360
|
}
|
|
16361
|
+
if (host !== "windows") {
|
|
16362
|
+
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
16363
|
+
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
16364
|
+
try {
|
|
16365
|
+
return readFileSync19(p, "utf8");
|
|
16366
|
+
} catch {
|
|
16367
|
+
return null;
|
|
16368
|
+
}
|
|
16369
|
+
});
|
|
16370
|
+
if (abandons === false) {
|
|
16371
|
+
process.stdout.write(
|
|
16372
|
+
"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"
|
|
16373
|
+
);
|
|
16374
|
+
}
|
|
16375
|
+
}
|
|
15963
16376
|
const daemonStartedAt = readDaemonStarted(cfg.ship.stateDir);
|
|
15964
16377
|
if (daemonStartedAt !== null) {
|
|
15965
16378
|
const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
|
package/package.json
CHANGED