@tablation/crew 0.80.1 → 0.82.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/crew.example.yaml +15 -0
- package/dist/cli.js +587 -330
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2055,6 +2055,7 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
|
|
|
2055
2055
|
"baseUrl",
|
|
2056
2056
|
"apiKey",
|
|
2057
2057
|
"maxConcurrentAgents",
|
|
2058
|
+
"roleConcurrency",
|
|
2058
2059
|
"streamRetentionDays",
|
|
2059
2060
|
"eventsRotateBytes",
|
|
2060
2061
|
"reposBasePath",
|
|
@@ -2364,10 +2365,34 @@ Create the first of those:
|
|
|
2364
2365
|
if (declaredPlatform && !isShipPlatform(declaredPlatform)) {
|
|
2365
2366
|
missing.add(`ship.platform must be one of macos|linux|windows (got "${declaredPlatform}")`);
|
|
2366
2367
|
}
|
|
2367
|
-
const
|
|
2368
|
+
const maxConcurrentAgentsSet = shipRaw.maxConcurrentAgents !== void 0;
|
|
2369
|
+
let maxConcurrentAgents = maxConcurrentAgentsSet ? Number(shipRaw.maxConcurrentAgents) : 2;
|
|
2368
2370
|
if (!Number.isInteger(maxConcurrentAgents) || maxConcurrentAgents < 1) {
|
|
2369
2371
|
missing.add(`ship.maxConcurrentAgents must be a positive integer (got "${shipRaw.maxConcurrentAgents}")`);
|
|
2370
2372
|
}
|
|
2373
|
+
const roleConcurrency = {};
|
|
2374
|
+
const roleConcurrencyRaw = shipRaw.roleConcurrency;
|
|
2375
|
+
if (roleConcurrencyRaw !== void 0 && roleConcurrencyRaw !== null) {
|
|
2376
|
+
if (typeof roleConcurrencyRaw !== "object" || Array.isArray(roleConcurrencyRaw)) {
|
|
2377
|
+
missing.add("ship.roleConcurrency must be a map of role to a positive integer");
|
|
2378
|
+
} else {
|
|
2379
|
+
for (const [role, v] of Object.entries(roleConcurrencyRaw)) {
|
|
2380
|
+
if (!ROLE_NAMES.includes(role)) {
|
|
2381
|
+
missing.add(`ship.roleConcurrency.${role} is not a role (expected one of ${ROLE_NAMES.join(", ")})`);
|
|
2382
|
+
continue;
|
|
2383
|
+
}
|
|
2384
|
+
const n = Number(v);
|
|
2385
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
2386
|
+
missing.add(`ship.roleConcurrency.${role} must be a positive integer (got "${String(v)}")`);
|
|
2387
|
+
continue;
|
|
2388
|
+
}
|
|
2389
|
+
roleConcurrency[role] = n;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
if (!maxConcurrentAgentsSet && Object.keys(roleConcurrency).length > 0) {
|
|
2394
|
+
maxConcurrentAgents = ROLE_NAMES.filter((r) => r !== "pair").reduce((n, r) => n + (roleConcurrency[r] ?? 1), 0);
|
|
2395
|
+
}
|
|
2371
2396
|
const streamRetentionDays = shipRaw.streamRetentionDays === void 0 ? 7 : Number(shipRaw.streamRetentionDays);
|
|
2372
2397
|
if (!Number.isInteger(streamRetentionDays) || streamRetentionDays < 1) {
|
|
2373
2398
|
missing.add(`ship.streamRetentionDays must be a positive integer (got "${shipRaw.streamRetentionDays}")`);
|
|
@@ -2431,6 +2456,8 @@ Create the first of those:
|
|
|
2431
2456
|
logFile: expand(shipRaw.logFile ?? join9(tmpdir(), "crew.log"), base),
|
|
2432
2457
|
userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
|
|
2433
2458
|
maxConcurrentAgents,
|
|
2459
|
+
maxConcurrentAgentsSet,
|
|
2460
|
+
roleConcurrency,
|
|
2434
2461
|
streamRetentionDays,
|
|
2435
2462
|
eventsRotateBytes,
|
|
2436
2463
|
relayHost: shipRaw.relayHost ? String(shipRaw.relayHost) : void 0,
|
|
@@ -4541,6 +4568,10 @@ function formatStaleOwnLock(scope, holder) {
|
|
|
4541
4568
|
return `WARN lock ${scope} is held by ${holder}, but that process is gone \u2014 the next release cycle reclaims it
|
|
4542
4569
|
`;
|
|
4543
4570
|
}
|
|
4571
|
+
function roleCapWarnings(ship) {
|
|
4572
|
+
if (!ship.maxConcurrentAgentsSet) return [];
|
|
4573
|
+
return Object.entries(ship.roleConcurrency).filter(([, n]) => (n ?? 0) > ship.maxConcurrentAgents).map(([role, n]) => `ship.roleConcurrency.${role} is ${n}, above ship.maxConcurrentAgents (${ship.maxConcurrentAgents}); the ship-wide cap wins`);
|
|
4574
|
+
}
|
|
4544
4575
|
|
|
4545
4576
|
// src/selftest.ts
|
|
4546
4577
|
import { spawn as spawn3 } from "child_process";
|
|
@@ -5654,22 +5685,36 @@ var State = class _State {
|
|
|
5654
5685
|
return { ok: false, heldBy };
|
|
5655
5686
|
}
|
|
5656
5687
|
/**
|
|
5657
|
-
* The run lock (ISSUE-381): role-
|
|
5658
|
-
* together.
|
|
5688
|
+
* The run lock (ISSUE-381, CREW-2821): role-capped AND capacity-limited,
|
|
5689
|
+
* checked together.
|
|
5659
5690
|
*
|
|
5660
|
-
*
|
|
5661
|
-
*
|
|
5662
|
-
*
|
|
5663
|
-
*
|
|
5664
|
-
*
|
|
5665
|
-
*
|
|
5666
|
-
*
|
|
5667
|
-
*
|
|
5691
|
+
* Two sessions of one role must never take the same ticket. Before
|
|
5692
|
+
* CREW-2821 that was guaranteed by an exclusive per-role lock; now the
|
|
5693
|
+
* guarantee is the per-ticket lock (`acquireTicket`), so the role lock is a
|
|
5694
|
+
* counted one: `roleMax` interchangeable slots, default 1. Slot 1 keeps the
|
|
5695
|
+
* historical name `crew-role-<role>` (crew-macos reads it for the live pid),
|
|
5696
|
+
* further slots are `crew-role-<role>-<n>`. `max` is this machine's separate
|
|
5697
|
+
* limit on how many runs of ANY role may overlap; a role that clears its own
|
|
5698
|
+
* lock but finds every slot taken must give its role lock back, or a
|
|
5699
|
+
* locked-but-not-running role would wedge itself out for the rest of the
|
|
5700
|
+
* machine's uptime.
|
|
5668
5701
|
*/
|
|
5669
|
-
acquireRun(role, max) {
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5702
|
+
acquireRun(role, max, roleMax = 1) {
|
|
5703
|
+
let roleLock;
|
|
5704
|
+
const roleHolders = [];
|
|
5705
|
+
for (let i = 1; i <= roleMax; i++) {
|
|
5706
|
+
const got = this.acquire(i === 1 ? `crew-role-${role}` : `crew-role-${role}-${i}`);
|
|
5707
|
+
if (got.ok) {
|
|
5708
|
+
roleLock = got;
|
|
5709
|
+
break;
|
|
5710
|
+
}
|
|
5711
|
+
roleHolders.push(got.heldBy);
|
|
5712
|
+
}
|
|
5713
|
+
if (!roleLock) {
|
|
5714
|
+
return {
|
|
5715
|
+
ok: false,
|
|
5716
|
+
reason: roleMax === 1 ? `${role} is already running (pid ${roleHolders[0]})` : `${role} is already running ${roleMax} agents (its roleConcurrency cap; pid${roleHolders.length > 1 ? "s" : ""} ${[...new Set(roleHolders)].join(", ")})`
|
|
5717
|
+
};
|
|
5673
5718
|
}
|
|
5674
5719
|
const slot = this.acquireN("crew-slot", max);
|
|
5675
5720
|
if (!slot.ok) {
|
|
@@ -5684,6 +5729,27 @@ var State = class _State {
|
|
|
5684
5729
|
roleLock.release();
|
|
5685
5730
|
} };
|
|
5686
5731
|
}
|
|
5732
|
+
/**
|
|
5733
|
+
* The per-ticket run lock (CREW-2821): held for the life of a run that works
|
|
5734
|
+
* the ticket (`crew-ticket-<record id>`), so a second run of the same role
|
|
5735
|
+
* on this ship walks past it instead of being handed the first run's
|
|
5736
|
+
* ticket. Same stale-pid rule as every lock here.
|
|
5737
|
+
*/
|
|
5738
|
+
acquireTicket(recordId) {
|
|
5739
|
+
return this.acquire(`crew-ticket-${recordId}`);
|
|
5740
|
+
}
|
|
5741
|
+
/** Whether a live run on this ship holds `recordId`'s ticket lock. */
|
|
5742
|
+
ticketInFlight(recordId) {
|
|
5743
|
+
const file = this.path(`.${_State.safe(`crew-ticket-${recordId}`)}.lock`);
|
|
5744
|
+
try {
|
|
5745
|
+
const pid = Number.parseInt(readFileSync12(file, "utf8").trim(), 10);
|
|
5746
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
5747
|
+
process.kill(pid, 0);
|
|
5748
|
+
return true;
|
|
5749
|
+
} catch {
|
|
5750
|
+
return false;
|
|
5751
|
+
}
|
|
5752
|
+
}
|
|
5687
5753
|
/**
|
|
5688
5754
|
* How many consecutive cycles the release phase has refused to deploy.
|
|
5689
5755
|
*
|
|
@@ -6413,7 +6479,7 @@ var Tracker = class {
|
|
|
6413
6479
|
* `beatIdle` in a `finally` — the same shape as `setCrewStatus`'s
|
|
6414
6480
|
* working/idle pair, one level up (ship, not seat).
|
|
6415
6481
|
*/
|
|
6416
|
-
async beatEngaged(shipName2, connectionName, ticketRecordId) {
|
|
6482
|
+
async beatEngaged(shipName2, connectionName, ticketRecordId, since2) {
|
|
6417
6483
|
const row = await this.myShipRow(shipName2);
|
|
6418
6484
|
if (!row) return;
|
|
6419
6485
|
await this.client.records.update(this.route.resolved.shipsModelId, row.id, {
|
|
@@ -6421,7 +6487,7 @@ var Tracker = class {
|
|
|
6421
6487
|
host: hostname(),
|
|
6422
6488
|
pid: process.pid,
|
|
6423
6489
|
engaged: true,
|
|
6424
|
-
engaged_since: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6490
|
+
engaged_since: since2 ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
6425
6491
|
engaged_connection: connectionName,
|
|
6426
6492
|
engaged_ticket_id: ticketRecordId,
|
|
6427
6493
|
...this.attentionPatch(row)
|
|
@@ -7063,7 +7129,14 @@ function newBranchName(cfg, t, role) {
|
|
|
7063
7129
|
function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
7064
7130
|
const { remote, base } = cfg.branch;
|
|
7065
7131
|
const hasRemote = remoteConfigured(dir, remote);
|
|
7066
|
-
if (hasRemote)
|
|
7132
|
+
if (hasRemote) {
|
|
7133
|
+
const unlock = acquireCheckoutLock(dir);
|
|
7134
|
+
try {
|
|
7135
|
+
fetchRemote(dir, remote);
|
|
7136
|
+
} finally {
|
|
7137
|
+
unlock();
|
|
7138
|
+
}
|
|
7139
|
+
}
|
|
7067
7140
|
const num = t.issue_id.replace(/^\D+/, "");
|
|
7068
7141
|
const existing = worktreeForNumber(dir, num);
|
|
7069
7142
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
@@ -8373,6 +8446,7 @@ var AgentError = class extends Error {
|
|
|
8373
8446
|
};
|
|
8374
8447
|
var AgentSpawnError = class extends AgentError {
|
|
8375
8448
|
};
|
|
8449
|
+
var digestPathFor = (stateDir, routeLabel, role, runId) => join19(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
8376
8450
|
function planAgentRun(o) {
|
|
8377
8451
|
const briefPath = join19(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
8378
8452
|
if (!existsSync18(briefPath)) {
|
|
@@ -8382,7 +8456,8 @@ function planAgentRun(o) {
|
|
|
8382
8456
|
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync18(commonPath);
|
|
8383
8457
|
const now = o.now ?? (() => Date.now());
|
|
8384
8458
|
const routeLabel = routeSlug(o.route.route);
|
|
8385
|
-
const
|
|
8459
|
+
const ownDigest = o.runId ? digestPathFor(o.stateDir, routeLabel, o.role, o.runId) : void 0;
|
|
8460
|
+
const digestPath2 = ownDigest && existsSync18(ownDigest) ? ownDigest : digestPathFor(o.stateDir, routeLabel, o.role);
|
|
8386
8461
|
let digest;
|
|
8387
8462
|
let ageSeconds;
|
|
8388
8463
|
if (existsSync18(digestPath2)) {
|
|
@@ -8413,7 +8488,7 @@ function planAgentRun(o) {
|
|
|
8413
8488
|
});
|
|
8414
8489
|
const promptSha = createHash3("sha256").update(expanded.text).digest("hex");
|
|
8415
8490
|
const streamsDir = join19(o.stateDir, "streams");
|
|
8416
|
-
const base = `${routeLabel}-${o.role}-${o.cycle}`;
|
|
8491
|
+
const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
|
|
8417
8492
|
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
8418
8493
|
const { toolchain, model } = seat;
|
|
8419
8494
|
const adapter = adapterFor(toolchain.harness);
|
|
@@ -8486,6 +8561,8 @@ function planAgentRun(o) {
|
|
|
8486
8561
|
eventsPath: join19(streamsDir, `${base}.events.jsonl`),
|
|
8487
8562
|
ticket: o.ticket,
|
|
8488
8563
|
ticketRecordId: o.ticketRecordId ?? null,
|
|
8564
|
+
repo: o.repo ?? null,
|
|
8565
|
+
routeName: o.route.route,
|
|
8489
8566
|
agentLog,
|
|
8490
8567
|
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
8491
8568
|
promptSha
|
|
@@ -8900,6 +8977,8 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
8900
8977
|
route: plan.route,
|
|
8901
8978
|
ticket: plan.ticket ?? null,
|
|
8902
8979
|
ticketRecordId: plan.ticketRecordId ?? null,
|
|
8980
|
+
repo: plan.repo ?? null,
|
|
8981
|
+
routeName: plan.routeName,
|
|
8903
8982
|
model: plan.model,
|
|
8904
8983
|
toolchain: id.toolchain,
|
|
8905
8984
|
harness: id.harness,
|
|
@@ -9471,7 +9550,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
9471
9550
|
}
|
|
9472
9551
|
|
|
9473
9552
|
// src/cli.ts
|
|
9474
|
-
import { resolve as resolve5, dirname as dirname20, join as
|
|
9553
|
+
import { resolve as resolve5, dirname as dirname20, join as join38, basename as basename12 } from "path";
|
|
9475
9554
|
import { fileURLToPath } from "url";
|
|
9476
9555
|
import { hostname as hostname2, homedir as homedir10 } from "os";
|
|
9477
9556
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
@@ -10955,8 +11034,15 @@ async function decideCycle(o) {
|
|
|
10955
11034
|
}
|
|
10956
11035
|
return decision;
|
|
10957
11036
|
}
|
|
10958
|
-
var digestPath = (stateDir, route, role) => join23(stateDir, `digest-${route}-${role}.md`);
|
|
10959
|
-
function
|
|
11037
|
+
var digestPath = (stateDir, route, role, runId) => join23(stateDir, `digest-${route}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
11038
|
+
function otherRunsRemoved(tickets, own) {
|
|
11039
|
+
if (!own) return tickets;
|
|
11040
|
+
return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
|
|
11041
|
+
}
|
|
11042
|
+
function ownRunBanner(own) {
|
|
11043
|
+
return `**Your ticket for this run: ${own.ticket.issue_id}.** Work this one. Any other ticket a live run on this ship is already working has been left out of this digest: it belongs to that run.`;
|
|
11044
|
+
}
|
|
11045
|
+
function writeDigest(o, d, role, stateDir, runId, own) {
|
|
10960
11046
|
const me = o.route.resolved?.seats[role];
|
|
10961
11047
|
if (!me) return false;
|
|
10962
11048
|
try {
|
|
@@ -11002,7 +11088,7 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
11002
11088
|
}
|
|
11003
11089
|
return existingBranchForTicket(dir, repoFor(dir), t, role);
|
|
11004
11090
|
},
|
|
11005
|
-
tickets: role === "triage" ? triageSlice(d.tickets, o.route.resolved?.seats.triage, d.selectionInput.workModes) : withinWorkMode(sliceFor(d.tickets, role), d.selectionInput.workModes),
|
|
11091
|
+
tickets: role === "triage" ? triageSlice(d.tickets, o.route.resolved?.seats.triage, d.selectionInput.workModes) : withinWorkMode(sliceFor(otherRunsRemoved(d.tickets, own), role), d.selectionInput.workModes),
|
|
11006
11092
|
workModes: d.selectionInput.workModes,
|
|
11007
11093
|
comments: d.comments,
|
|
11008
11094
|
commentColumns: d.selectionInput.contract?.comments,
|
|
@@ -11021,8 +11107,11 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
11021
11107
|
blocked: d.blocked,
|
|
11022
11108
|
blockerInfo: d.info
|
|
11023
11109
|
};
|
|
11024
|
-
const
|
|
11025
|
-
const
|
|
11110
|
+
const body = role === "qa" ? qaDigest(input) : buildingDigest(input);
|
|
11111
|
+
const text = own ? `${ownRunBanner(own)}
|
|
11112
|
+
|
|
11113
|
+
${body}` : body;
|
|
11114
|
+
const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
|
|
11026
11115
|
writeFileSync14(path, text);
|
|
11027
11116
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
|
|
11028
11117
|
data: { role, bytes: Buffer.byteLength(text) }
|
|
@@ -11063,7 +11152,7 @@ async function stampResumption(tracker, t, affinity) {
|
|
|
11063
11152
|
}
|
|
11064
11153
|
return { outcome: "unknown" };
|
|
11065
11154
|
}
|
|
11066
|
-
async function resolveTopCandidate(tracker, candidates, seat, approvedStatus, buildingStatus, isServable, affinity) {
|
|
11155
|
+
async function resolveTopCandidate(tracker, candidates, seat, approvedStatus, buildingStatus, isServable, affinity, ticketLock) {
|
|
11067
11156
|
const contended = [];
|
|
11068
11157
|
const unservable = [];
|
|
11069
11158
|
const held = [];
|
|
@@ -11076,41 +11165,58 @@ async function resolveTopCandidate(tracker, candidates, seat, approvedStatus, bu
|
|
|
11076
11165
|
unservable.push(t.issue_id);
|
|
11077
11166
|
continue;
|
|
11078
11167
|
}
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
const stamp = await stampResumption(tracker, t, affinity);
|
|
11085
|
-
if (stamp.outcome === "stamped") {
|
|
11086
|
-
return { ticket: stamp.ticket, claimed: false, contended, unservable, held, reaccepted: false, tookOverFrom: displacedHolder(t, affinity) };
|
|
11087
|
-
}
|
|
11088
|
-
(stamp.outcome === "held" ? held : contended).push(t.issue_id);
|
|
11168
|
+
let releaseTicket;
|
|
11169
|
+
if (ticketLock) {
|
|
11170
|
+
const got = ticketLock(t);
|
|
11171
|
+
if (!got) {
|
|
11172
|
+
contended.push(t.issue_id);
|
|
11089
11173
|
continue;
|
|
11090
11174
|
}
|
|
11175
|
+
releaseTicket = got;
|
|
11176
|
+
}
|
|
11177
|
+
const result = await resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affinity, contended, unservable, held);
|
|
11178
|
+
if (result === "walk") {
|
|
11179
|
+
releaseTicket?.();
|
|
11180
|
+
continue;
|
|
11181
|
+
}
|
|
11182
|
+
return releaseTicket ? { ...result, releaseTicket } : result;
|
|
11183
|
+
}
|
|
11184
|
+
return { ticket: null, claimed: false, contended, unservable, held, reaccepted: false, tookOverFrom: null };
|
|
11185
|
+
}
|
|
11186
|
+
async function resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affinity, contended, unservable, held) {
|
|
11187
|
+
if (t.status !== approvedStatus) {
|
|
11188
|
+
if (affinity?.isLiveHoldElsewhere?.(t)) {
|
|
11091
11189
|
return { ticket: t, claimed: false, contended, unservable, held, reaccepted: false, tookOverFrom: null };
|
|
11092
11190
|
}
|
|
11093
|
-
|
|
11094
|
-
const
|
|
11095
|
-
|
|
11096
|
-
{
|
|
11097
|
-
status: buildingStatus,
|
|
11098
|
-
assignee_id: seat,
|
|
11099
|
-
...affinity ? { [affinity.column]: affinity.shipId } : {}
|
|
11100
|
-
},
|
|
11101
|
-
t.updated_at
|
|
11102
|
-
);
|
|
11103
|
-
const reaccepted = !!affinity && t[affinity.column] === affinity.shipId;
|
|
11104
|
-
return { ticket: { ...t, ...claimed }, claimed: true, contended, unservable, held, reaccepted, tookOverFrom: displacedHolder(t, affinity) };
|
|
11105
|
-
} catch (e) {
|
|
11106
|
-
if (e instanceof StaleWriteError6) {
|
|
11107
|
-
contended.push(t.issue_id);
|
|
11108
|
-
continue;
|
|
11191
|
+
if (affinity && t[affinity.column] !== affinity.shipId) {
|
|
11192
|
+
const stamp = await stampResumption(tracker, t, affinity);
|
|
11193
|
+
if (stamp.outcome === "stamped") {
|
|
11194
|
+
return { ticket: stamp.ticket, claimed: false, contended, unservable, held, reaccepted: false, tookOverFrom: displacedHolder(t, affinity) };
|
|
11109
11195
|
}
|
|
11110
|
-
|
|
11196
|
+
(stamp.outcome === "held" ? held : contended).push(t.issue_id);
|
|
11197
|
+
return "walk";
|
|
11111
11198
|
}
|
|
11199
|
+
return { ticket: t, claimed: false, contended, unservable, held, reaccepted: false, tookOverFrom: null };
|
|
11200
|
+
}
|
|
11201
|
+
try {
|
|
11202
|
+
const claimed = await tracker.updateTicket(
|
|
11203
|
+
t.id,
|
|
11204
|
+
{
|
|
11205
|
+
status: buildingStatus,
|
|
11206
|
+
assignee_id: seat,
|
|
11207
|
+
...affinity ? { [affinity.column]: affinity.shipId } : {}
|
|
11208
|
+
},
|
|
11209
|
+
t.updated_at
|
|
11210
|
+
);
|
|
11211
|
+
const reaccepted = !!affinity && t[affinity.column] === affinity.shipId;
|
|
11212
|
+
return { ticket: { ...t, ...claimed }, claimed: true, contended, unservable, held, reaccepted, tookOverFrom: displacedHolder(t, affinity) };
|
|
11213
|
+
} catch (e) {
|
|
11214
|
+
if (e instanceof StaleWriteError6) {
|
|
11215
|
+
contended.push(t.issue_id);
|
|
11216
|
+
return "walk";
|
|
11217
|
+
}
|
|
11218
|
+
throw e;
|
|
11112
11219
|
}
|
|
11113
|
-
return { ticket: null, claimed: false, contended, unservable, held, reaccepted: false, tookOverFrom: null };
|
|
11114
11220
|
}
|
|
11115
11221
|
|
|
11116
11222
|
// src/conflict.ts
|
|
@@ -13277,6 +13383,164 @@ Passed: ${passed}.${willClose ? " Closing this alert." : ""}`,
|
|
|
13277
13383
|
return out;
|
|
13278
13384
|
}
|
|
13279
13385
|
|
|
13386
|
+
// src/live-runs.ts
|
|
13387
|
+
import { existsSync as existsSync24, readdirSync as readdirSync10 } from "fs";
|
|
13388
|
+
import { join as join28 } from "path";
|
|
13389
|
+
|
|
13390
|
+
// src/interrupted.ts
|
|
13391
|
+
import { existsSync as existsSync23, readdirSync as readdirSync9, readFileSync as readFileSync21 } from "fs";
|
|
13392
|
+
import { join as join27 } from "path";
|
|
13393
|
+
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
13394
|
+
function interruptedComment(b) {
|
|
13395
|
+
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.";
|
|
13396
|
+
return `${INTERRUPTED_MARKER}
|
|
13397
|
+
|
|
13398
|
+
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.`;
|
|
13399
|
+
}
|
|
13400
|
+
var handled = /* @__PURE__ */ new Set();
|
|
13401
|
+
var pidAlive = (pid) => {
|
|
13402
|
+
try {
|
|
13403
|
+
process.kill(pid, 0);
|
|
13404
|
+
return true;
|
|
13405
|
+
} catch (e) {
|
|
13406
|
+
return e.code === "EPERM";
|
|
13407
|
+
}
|
|
13408
|
+
};
|
|
13409
|
+
function runFinished(streamPath) {
|
|
13410
|
+
const path = `${streamPath}.meta.json`;
|
|
13411
|
+
if (!existsSync23(path)) return false;
|
|
13412
|
+
try {
|
|
13413
|
+
return JSON.parse(readFileSync21(path, "utf8")).running !== true;
|
|
13414
|
+
} catch {
|
|
13415
|
+
return true;
|
|
13416
|
+
}
|
|
13417
|
+
}
|
|
13418
|
+
function orphanedStreams(o) {
|
|
13419
|
+
const dir = join27(o.stateDir, "streams");
|
|
13420
|
+
if (!existsSync23(dir)) return [];
|
|
13421
|
+
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
13422
|
+
const out = [];
|
|
13423
|
+
for (const name of readdirSync9(dir)) {
|
|
13424
|
+
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
13425
|
+
const streamPath = join27(dir, name);
|
|
13426
|
+
if (runFinished(streamPath)) continue;
|
|
13427
|
+
const sidecar = readRunSidecar(streamPath);
|
|
13428
|
+
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
13429
|
+
if (ownerAlive(sidecar.ownerPid)) continue;
|
|
13430
|
+
out.push({ streamPath, sidecar });
|
|
13431
|
+
}
|
|
13432
|
+
return out;
|
|
13433
|
+
}
|
|
13434
|
+
async function reconcileRuns(o) {
|
|
13435
|
+
let adopted = 0;
|
|
13436
|
+
let interrupted = 0;
|
|
13437
|
+
for (const { streamPath, sidecar } of orphanedStreams(o)) {
|
|
13438
|
+
if (handled.has(streamPath)) continue;
|
|
13439
|
+
handled.add(streamPath);
|
|
13440
|
+
const isAlive = o.isAlive ?? runIsAlive;
|
|
13441
|
+
const lock = isAlive(sidecar) ? o.state.acquireRun(sidecar.role, o.maxConcurrentAgents, o.roleConcurrency?.[sidecar.role] ?? 1) : null;
|
|
13442
|
+
const ticketLock = isAlive(sidecar) && sidecar.ticketRecordId ? o.state.acquireTicket(sidecar.ticketRecordId) : null;
|
|
13443
|
+
const work = (async () => {
|
|
13444
|
+
try {
|
|
13445
|
+
const result = await reattachRun(streamPath, sidecar, o.emit, {
|
|
13446
|
+
agentLogKey: await o.agentLogKey?.(sidecar.role).catch(() => void 0),
|
|
13447
|
+
isAlive: o.isAlive,
|
|
13448
|
+
pollMs: o.pollMs,
|
|
13449
|
+
onAdopted: () => o.emit.emit(`adopted ${sidecar.role} run (pid ${sidecar.pid}) left by a previous crew process`, { step: "agent", role: sidecar.role })
|
|
13450
|
+
});
|
|
13451
|
+
if (result.adopted) adopted++;
|
|
13452
|
+
else interrupted++;
|
|
13453
|
+
try {
|
|
13454
|
+
await o.afterRun?.(sidecar, result);
|
|
13455
|
+
} catch (e) {
|
|
13456
|
+
o.emit.warn(`could not follow up on ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
13457
|
+
}
|
|
13458
|
+
} catch (e) {
|
|
13459
|
+
o.emit.warn(`could not reattach ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
13460
|
+
} finally {
|
|
13461
|
+
if (lock?.ok) lock.release();
|
|
13462
|
+
if (ticketLock?.ok) ticketLock.release();
|
|
13463
|
+
}
|
|
13464
|
+
})();
|
|
13465
|
+
if (isAlive(sidecar)) {
|
|
13466
|
+
if (o.inFlight) {
|
|
13467
|
+
o.inFlight.add(work);
|
|
13468
|
+
void work.finally(() => o.inFlight.delete(work));
|
|
13469
|
+
}
|
|
13470
|
+
} else {
|
|
13471
|
+
await work;
|
|
13472
|
+
}
|
|
13473
|
+
}
|
|
13474
|
+
return { adopted, interrupted };
|
|
13475
|
+
}
|
|
13476
|
+
|
|
13477
|
+
// src/live-runs.ts
|
|
13478
|
+
function liveRuns(stateDir, isAlive = runIsAlive) {
|
|
13479
|
+
const dir = join28(stateDir, "streams");
|
|
13480
|
+
if (!existsSync24(dir)) return [];
|
|
13481
|
+
const out = [];
|
|
13482
|
+
for (const name of readdirSync10(dir)) {
|
|
13483
|
+
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
13484
|
+
const streamPath = join28(dir, name);
|
|
13485
|
+
if (runFinished(streamPath)) continue;
|
|
13486
|
+
const sidecar = readRunSidecar(streamPath);
|
|
13487
|
+
if (!sidecar || !isAlive(sidecar)) continue;
|
|
13488
|
+
out.push({ ...sidecar, streamPath });
|
|
13489
|
+
}
|
|
13490
|
+
return out.sort((a, b) => a.spawnedAt.localeCompare(b.spawnedAt));
|
|
13491
|
+
}
|
|
13492
|
+
function siblingRuns(runs, ownTicketRecordId) {
|
|
13493
|
+
return runs.filter((r) => !ownTicketRecordId || r.ticketRecordId !== ownTicketRecordId);
|
|
13494
|
+
}
|
|
13495
|
+
async function settleRunEnd(o) {
|
|
13496
|
+
const { tracker, emit } = o;
|
|
13497
|
+
const all = o.runs ?? liveRuns(o.stateDir);
|
|
13498
|
+
const slug = routeSlug(o.route.route);
|
|
13499
|
+
const seatSiblings = siblingRuns(all.filter((r) => r.role === o.role && r.route === slug), o.ownTicketRecordId);
|
|
13500
|
+
const shipSiblings = siblingRuns(all, o.ownTicketRecordId);
|
|
13501
|
+
const newest = (rs) => rs[rs.length - 1];
|
|
13502
|
+
if (o.memberId) {
|
|
13503
|
+
try {
|
|
13504
|
+
if (seatSiblings.length) await tracker.setCrewStatus(o.memberId, "working", newest(seatSiblings).ticketRecordId);
|
|
13505
|
+
else await tracker.setCrewStatus(o.memberId, "idle");
|
|
13506
|
+
} catch (e) {
|
|
13507
|
+
emit.warn(`could not clear crew status: ${e.message}`, { step: "agent" });
|
|
13508
|
+
}
|
|
13509
|
+
}
|
|
13510
|
+
try {
|
|
13511
|
+
if (shipSiblings.length) {
|
|
13512
|
+
const n = newest(shipSiblings);
|
|
13513
|
+
await tracker.beatEngaged(o.shipName, n.routeName ?? n.route, n.ticketRecordId, n.spawnedAt);
|
|
13514
|
+
} else {
|
|
13515
|
+
await tracker.beatIdle(o.shipName);
|
|
13516
|
+
}
|
|
13517
|
+
} catch (e) {
|
|
13518
|
+
emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
|
|
13519
|
+
}
|
|
13520
|
+
}
|
|
13521
|
+
function describeRun(run2, route, seatIds, worktreeOf = (d, n) => {
|
|
13522
|
+
try {
|
|
13523
|
+
return worktreeForNumber(d, n)?.path ?? null;
|
|
13524
|
+
} catch {
|
|
13525
|
+
return null;
|
|
13526
|
+
}
|
|
13527
|
+
}) {
|
|
13528
|
+
const ownRoute = run2.route === routeSlug(route.route);
|
|
13529
|
+
const repoDir = run2.repo ? reposOf(route).find((t) => t.name === run2.repo)?.dir : void 0;
|
|
13530
|
+
const num = run2.ticket?.replace(/^\D+/, "") ?? "";
|
|
13531
|
+
return {
|
|
13532
|
+
role: run2.role,
|
|
13533
|
+
seat: ownRoute ? seatIds[run2.role] ?? null : null,
|
|
13534
|
+
ticket: run2.ticket,
|
|
13535
|
+
ticketRecordId: run2.ticketRecordId,
|
|
13536
|
+
repo: run2.repo ?? null,
|
|
13537
|
+
worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
|
|
13538
|
+
stream: run2.streamPath,
|
|
13539
|
+
pid: run2.pid,
|
|
13540
|
+
startedAt: run2.spawnedAt
|
|
13541
|
+
};
|
|
13542
|
+
}
|
|
13543
|
+
|
|
13280
13544
|
// src/projects-cmd.ts
|
|
13281
13545
|
async function buildProjectsReport(routes, open) {
|
|
13282
13546
|
return Promise.all(routes.map(async (r) => {
|
|
@@ -13486,7 +13750,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
13486
13750
|
}
|
|
13487
13751
|
|
|
13488
13752
|
// src/watch.ts
|
|
13489
|
-
import { watch as watchFile, existsSync as
|
|
13753
|
+
import { watch as watchFile, existsSync as existsSync25, statSync as statSync7, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
13490
13754
|
var E = "\x1B[";
|
|
13491
13755
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
13492
13756
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -13587,7 +13851,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
13587
13851
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
13588
13852
|
}
|
|
13589
13853
|
function readFrom(file, from) {
|
|
13590
|
-
if (!
|
|
13854
|
+
if (!existsSync25(file)) return { events: [], offset: from };
|
|
13591
13855
|
const size = statSync7(file).size;
|
|
13592
13856
|
if (size < from) return readFrom(file, 0);
|
|
13593
13857
|
if (size === from) return { events: [], offset: from };
|
|
@@ -13830,17 +14094,17 @@ function nextRoles(decision, ran) {
|
|
|
13830
14094
|
}
|
|
13831
14095
|
|
|
13832
14096
|
// src/ship-name.ts
|
|
13833
|
-
import { existsSync as
|
|
14097
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync16, readFileSync as readFileSync23, renameSync as renameSync2, writeFileSync as writeFileSync18 } from "fs";
|
|
13834
14098
|
import { dirname as dirname14 } from "path";
|
|
13835
14099
|
import { parseDocument } from "yaml";
|
|
13836
14100
|
|
|
13837
14101
|
// src/machine-id.ts
|
|
13838
14102
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
13839
14103
|
import { createHash as createHash4 } from "crypto";
|
|
13840
|
-
import { existsSync as
|
|
13841
|
-
import { dirname as dirname13, join as
|
|
14104
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync15, readFileSync as readFileSync22, writeFileSync as writeFileSync17 } from "fs";
|
|
14105
|
+
import { dirname as dirname13, join as join29 } from "path";
|
|
13842
14106
|
function machineIdPathFor(stateDir) {
|
|
13843
|
-
return
|
|
14107
|
+
return join29(stateDir, "machine-id");
|
|
13844
14108
|
}
|
|
13845
14109
|
var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
|
|
13846
14110
|
function readRawMachineId(platform2 = process.platform) {
|
|
@@ -13853,8 +14117,8 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
13853
14117
|
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
|
|
13854
14118
|
}
|
|
13855
14119
|
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
13856
|
-
if (
|
|
13857
|
-
const id =
|
|
14120
|
+
if (existsSync26(p)) {
|
|
14121
|
+
const id = readFileSync22(p, "utf8").trim();
|
|
13858
14122
|
if (id) return id;
|
|
13859
14123
|
}
|
|
13860
14124
|
}
|
|
@@ -13865,8 +14129,8 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
13865
14129
|
function ensureMachineId(stateDir, opts = {}) {
|
|
13866
14130
|
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
13867
14131
|
const path = machineIdPathFor(stateDir);
|
|
13868
|
-
if (
|
|
13869
|
-
const cached =
|
|
14132
|
+
if (existsSync26(path)) {
|
|
14133
|
+
const cached = readFileSync22(path, "utf8").trim();
|
|
13870
14134
|
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
13871
14135
|
}
|
|
13872
14136
|
const raw = (opts.read ?? readRawMachineId)();
|
|
@@ -13944,19 +14208,19 @@ async function syncRouteName(i) {
|
|
|
13944
14208
|
return { status: "ok", ...base, action, name: i.local };
|
|
13945
14209
|
}
|
|
13946
14210
|
function readSyncedName(resolvedFile) {
|
|
13947
|
-
if (!
|
|
14211
|
+
if (!existsSync27(resolvedFile)) return void 0;
|
|
13948
14212
|
try {
|
|
13949
|
-
const v = JSON.parse(
|
|
14213
|
+
const v = JSON.parse(readFileSync23(resolvedFile, "utf8")).shipName;
|
|
13950
14214
|
return typeof v === "string" ? v : void 0;
|
|
13951
14215
|
} catch {
|
|
13952
14216
|
return void 0;
|
|
13953
14217
|
}
|
|
13954
14218
|
}
|
|
13955
14219
|
function writeSyncedName(resolvedFile, name) {
|
|
13956
|
-
if (!
|
|
14220
|
+
if (!existsSync27(resolvedFile)) return;
|
|
13957
14221
|
let raw;
|
|
13958
14222
|
try {
|
|
13959
|
-
raw = JSON.parse(
|
|
14223
|
+
raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
|
|
13960
14224
|
} catch {
|
|
13961
14225
|
return;
|
|
13962
14226
|
}
|
|
@@ -13971,7 +14235,7 @@ function writeSyncedName(resolvedFile, name) {
|
|
|
13971
14235
|
async function reconcileShipName(i) {
|
|
13972
14236
|
let local = i.ship.name;
|
|
13973
14237
|
try {
|
|
13974
|
-
local = shipNameOfYaml(
|
|
14238
|
+
local = shipNameOfYaml(readFileSync23(i.configFile, "utf8")) ?? local;
|
|
13975
14239
|
} catch {
|
|
13976
14240
|
}
|
|
13977
14241
|
const resolvedFile = resolvedPathFor(i.stateDir, i.route);
|
|
@@ -13995,7 +14259,7 @@ async function reconcileShipName(i) {
|
|
|
13995
14259
|
}
|
|
13996
14260
|
if (result.status !== "ok") return result;
|
|
13997
14261
|
if (result.action === "adopt" || result.action === "row-wins") {
|
|
13998
|
-
const text =
|
|
14262
|
+
const text = readFileSync23(i.configFile, "utf8");
|
|
13999
14263
|
writeFileSync18(i.configFile, setShipNameInYaml(text, result.name));
|
|
14000
14264
|
i.ship.name = result.name;
|
|
14001
14265
|
if (result.action === "row-wins") {
|
|
@@ -14641,7 +14905,7 @@ function describeCapability(i) {
|
|
|
14641
14905
|
}
|
|
14642
14906
|
|
|
14643
14907
|
// src/agents-sync-run.ts
|
|
14644
|
-
import { existsSync as
|
|
14908
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync17, readFileSync as readFileSync24, writeFileSync as writeFileSync19 } from "fs";
|
|
14645
14909
|
import { dirname as dirname15 } from "path";
|
|
14646
14910
|
async function runAgentsSync(target, o) {
|
|
14647
14911
|
const dryRun = o.dryRun ?? false;
|
|
@@ -14675,7 +14939,7 @@ async function runAgentsSync(target, o) {
|
|
|
14675
14939
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
14676
14940
|
if (!dryRun) {
|
|
14677
14941
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
14678
|
-
const raw =
|
|
14942
|
+
const raw = existsSync28(resolvedPath) ? JSON.parse(readFileSync24(resolvedPath, "utf8")) : {};
|
|
14679
14943
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
14680
14944
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
14681
14945
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
@@ -14784,7 +15048,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
14784
15048
|
|
|
14785
15049
|
// src/repos-check.ts
|
|
14786
15050
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
14787
|
-
import { readFileSync as
|
|
15051
|
+
import { readFileSync as readFileSync25 } from "fs";
|
|
14788
15052
|
import { basename as basename8 } from "path";
|
|
14789
15053
|
var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
|
|
14790
15054
|
function checkHookSyntax(script, shell) {
|
|
@@ -14825,7 +15089,7 @@ function checkRepoSpec(dir, opts = {}) {
|
|
|
14825
15089
|
}
|
|
14826
15090
|
let parsed;
|
|
14827
15091
|
try {
|
|
14828
|
-
parsed = parseRepoConfig(
|
|
15092
|
+
parsed = parseRepoConfig(readFileSync25(file, "utf8"), file);
|
|
14829
15093
|
} catch (e) {
|
|
14830
15094
|
if (!(e instanceof RepoConfigError)) throw e;
|
|
14831
15095
|
return { ...empty, file, error: e.message, exitCode: 1 };
|
|
@@ -14882,7 +15146,7 @@ import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml"
|
|
|
14882
15146
|
|
|
14883
15147
|
// src/repos-cmd.ts
|
|
14884
15148
|
import { homedir as homedir9 } from "os";
|
|
14885
|
-
import { basename as basename9, join as
|
|
15149
|
+
import { basename as basename9, join as join30, resolve as resolve3 } from "path";
|
|
14886
15150
|
import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
|
|
14887
15151
|
var ReposError = class extends Error {
|
|
14888
15152
|
};
|
|
@@ -15160,7 +15424,7 @@ function planRepoClone(i) {
|
|
|
15160
15424
|
}
|
|
15161
15425
|
const ship = doc.get("ship", true);
|
|
15162
15426
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
15163
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
15427
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join30(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
15164
15428
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
|
|
15165
15429
|
return { ...add, remote, target };
|
|
15166
15430
|
}
|
|
@@ -15559,7 +15823,7 @@ ${run2.err}` });
|
|
|
15559
15823
|
}
|
|
15560
15824
|
|
|
15561
15825
|
// src/sample-ticket.ts
|
|
15562
|
-
import { basename as basename10, dirname as dirname17, join as
|
|
15826
|
+
import { basename as basename10, dirname as dirname17, join as join31 } from "path";
|
|
15563
15827
|
var SAMPLE_REPO = "crew-sample";
|
|
15564
15828
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
15565
15829
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -15582,8 +15846,8 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
15582
15846
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
15583
15847
|
`;
|
|
15584
15848
|
function sampleDir(existing, reposBase, workspace) {
|
|
15585
|
-
if (existing.length > 0) return
|
|
15586
|
-
return
|
|
15849
|
+
if (existing.length > 0) return join31(dirname17(existing[0]), SAMPLE_REPO);
|
|
15850
|
+
return join31(reposBase, workspace, SAMPLE_REPO);
|
|
15587
15851
|
}
|
|
15588
15852
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
15589
15853
|
function sampleDescription() {
|
|
@@ -15675,8 +15939,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
15675
15939
|
}
|
|
15676
15940
|
|
|
15677
15941
|
// src/worktree-sweep.ts
|
|
15678
|
-
import { existsSync as
|
|
15679
|
-
import { basename as basename11, dirname as dirname18, join as
|
|
15942
|
+
import { existsSync as existsSync29, readFileSync as readFileSync26, readdirSync as readdirSync11 } from "fs";
|
|
15943
|
+
import { basename as basename11, dirname as dirname18, join as join32, resolve as resolve4 } from "path";
|
|
15680
15944
|
function planWorktreeSweep(target, tickets, contract) {
|
|
15681
15945
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
15682
15946
|
const verified = contract.statuses.verified;
|
|
@@ -15735,7 +15999,7 @@ function siblingWorktreeDirs(target) {
|
|
|
15735
15999
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
15736
16000
|
let names;
|
|
15737
16001
|
try {
|
|
15738
|
-
names =
|
|
16002
|
+
names = readdirSync11(parent);
|
|
15739
16003
|
} catch {
|
|
15740
16004
|
return [];
|
|
15741
16005
|
}
|
|
@@ -15743,7 +16007,7 @@ function siblingWorktreeDirs(target) {
|
|
|
15743
16007
|
for (const name of names) {
|
|
15744
16008
|
if (!name.startsWith(stem)) continue;
|
|
15745
16009
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
15746
|
-
const path =
|
|
16010
|
+
const path = join32(parent, name);
|
|
15747
16011
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
15748
16012
|
}
|
|
15749
16013
|
return out;
|
|
@@ -15778,7 +16042,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
15778
16042
|
let removed = 0;
|
|
15779
16043
|
let keptBranches = 0;
|
|
15780
16044
|
for (const a of actions) {
|
|
15781
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
16045
|
+
if (!dryRun && base && !a.keepBranch && existsSync29(a.path) && isHandBackMerge(a.path, base)) {
|
|
15782
16046
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
15783
16047
|
log.emit(
|
|
15784
16048
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -15786,7 +16050,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
15786
16050
|
);
|
|
15787
16051
|
}
|
|
15788
16052
|
}
|
|
15789
|
-
if (!dryRun &&
|
|
16053
|
+
if (!dryRun && existsSync29(a.path)) {
|
|
15790
16054
|
let dirty = false;
|
|
15791
16055
|
try {
|
|
15792
16056
|
dirty = status(a.path).length > 0;
|
|
@@ -15797,7 +16061,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
15797
16061
|
continue;
|
|
15798
16062
|
}
|
|
15799
16063
|
}
|
|
15800
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
16064
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync29(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
15801
16065
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
15802
16066
|
continue;
|
|
15803
16067
|
}
|
|
@@ -15846,7 +16110,7 @@ function isHandBackMerge(path, base) {
|
|
|
15846
16110
|
if (!msgPath) return false;
|
|
15847
16111
|
let first;
|
|
15848
16112
|
try {
|
|
15849
|
-
first =
|
|
16113
|
+
first = readFileSync26(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
15850
16114
|
} catch {
|
|
15851
16115
|
return false;
|
|
15852
16116
|
}
|
|
@@ -15951,14 +16215,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
15951
16215
|
}
|
|
15952
16216
|
|
|
15953
16217
|
// src/stream-sweep.ts
|
|
15954
|
-
import { existsSync as
|
|
15955
|
-
import { join as
|
|
16218
|
+
import { existsSync as existsSync30, readdirSync as readdirSync12, rmSync as rmSync13, statSync as statSync8 } from "fs";
|
|
16219
|
+
import { join as join33 } from "path";
|
|
15956
16220
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
15957
16221
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
15958
|
-
const dir =
|
|
15959
|
-
if (!
|
|
16222
|
+
const dir = join33(stateDir, "streams");
|
|
16223
|
+
if (!existsSync30(dir)) return [];
|
|
15960
16224
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
15961
|
-
return
|
|
16225
|
+
return readdirSync12(dir).map((name) => join33(dir, name)).filter((path) => {
|
|
15962
16226
|
try {
|
|
15963
16227
|
return statSync8(path).mtimeMs < cutoff;
|
|
15964
16228
|
} catch {
|
|
@@ -15984,9 +16248,9 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
15984
16248
|
return removed;
|
|
15985
16249
|
}
|
|
15986
16250
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
15987
|
-
if (!
|
|
16251
|
+
if (!existsSync30(stateDir)) return [];
|
|
15988
16252
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
15989
|
-
return
|
|
16253
|
+
return readdirSync12(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join33(stateDir, name)).filter((path) => {
|
|
15990
16254
|
try {
|
|
15991
16255
|
return statSync8(path).mtimeMs < cutoff;
|
|
15992
16256
|
} catch {
|
|
@@ -15996,23 +16260,23 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
15996
16260
|
}
|
|
15997
16261
|
|
|
15998
16262
|
// src/cli.ts
|
|
15999
|
-
import { realpathSync as realpathSync3, readFileSync as readFileSync31, existsSync as
|
|
16263
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync31, existsSync as existsSync35, mkdirSync as mkdirSync22, writeFileSync as writeFileSync23, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync16 } from "fs";
|
|
16000
16264
|
import { createInterface } from "readline/promises";
|
|
16001
16265
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
16002
16266
|
|
|
16003
16267
|
// src/ssh-keys.ts
|
|
16004
16268
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
16005
|
-
import { existsSync as
|
|
16006
|
-
import { dirname as dirname19, join as
|
|
16269
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync18, readFileSync as readFileSync27, chmodSync } from "fs";
|
|
16270
|
+
import { dirname as dirname19, join as join34 } from "path";
|
|
16007
16271
|
var SshKeyError = class extends Error {
|
|
16008
16272
|
};
|
|
16009
16273
|
function shipSshKeyPathFor(stateDir) {
|
|
16010
|
-
return
|
|
16274
|
+
return join34(stateDir, "ssh", "id_ed25519");
|
|
16011
16275
|
}
|
|
16012
16276
|
function ensureShipSshKeypair(stateDir) {
|
|
16013
16277
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
16014
16278
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
16015
|
-
if (!
|
|
16279
|
+
if (!existsSync31(privateKeyPath)) {
|
|
16016
16280
|
mkdirSync18(dirname19(privateKeyPath), { recursive: true });
|
|
16017
16281
|
try {
|
|
16018
16282
|
execFileSync6("ssh-keygen", [
|
|
@@ -16033,10 +16297,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
16033
16297
|
}
|
|
16034
16298
|
chmodSync(privateKeyPath, 384);
|
|
16035
16299
|
}
|
|
16036
|
-
if (!
|
|
16300
|
+
if (!existsSync31(publicKeyPath)) {
|
|
16037
16301
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
16038
16302
|
}
|
|
16039
|
-
const publicKey =
|
|
16303
|
+
const publicKey = readFileSync27(publicKeyPath, "utf8").trim();
|
|
16040
16304
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
16041
16305
|
}
|
|
16042
16306
|
function sshKeygenAvailable() {
|
|
@@ -16253,9 +16517,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
16253
16517
|
|
|
16254
16518
|
// src/tunnel.ts
|
|
16255
16519
|
import { spawn as spawn5 } from "child_process";
|
|
16256
|
-
import { existsSync as
|
|
16520
|
+
import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync28, writeFileSync as writeFileSync20, unlinkSync as unlinkSync3, readdirSync as readdirSync13 } from "fs";
|
|
16257
16521
|
import { randomInt } from "crypto";
|
|
16258
|
-
import { join as
|
|
16522
|
+
import { join as join35 } from "path";
|
|
16259
16523
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
16260
16524
|
var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
16261
16525
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -16322,20 +16586,20 @@ function startTunnel(opts) {
|
|
|
16322
16586
|
};
|
|
16323
16587
|
}
|
|
16324
16588
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
16325
|
-
return
|
|
16589
|
+
return join35(stateDir, "passengers", `${workspaceId}.json`);
|
|
16326
16590
|
}
|
|
16327
16591
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
16328
16592
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
16329
|
-
if (!
|
|
16593
|
+
if (!existsSync32(path)) return void 0;
|
|
16330
16594
|
try {
|
|
16331
|
-
return JSON.parse(
|
|
16595
|
+
return JSON.parse(readFileSync28(path, "utf8"));
|
|
16332
16596
|
} catch {
|
|
16333
16597
|
return void 0;
|
|
16334
16598
|
}
|
|
16335
16599
|
}
|
|
16336
16600
|
function writePersistedTunnel(stateDir, state) {
|
|
16337
16601
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
16338
|
-
mkdirSync19(
|
|
16602
|
+
mkdirSync19(join35(stateDir, "passengers"), { recursive: true });
|
|
16339
16603
|
writeFileSync20(path, `${JSON.stringify(state, null, 2)}
|
|
16340
16604
|
`);
|
|
16341
16605
|
}
|
|
@@ -16433,9 +16697,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
16433
16697
|
}
|
|
16434
16698
|
}
|
|
16435
16699
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
16436
|
-
const dir =
|
|
16437
|
-
if (!
|
|
16438
|
-
return
|
|
16700
|
+
const dir = join35(stateDir, "passengers");
|
|
16701
|
+
if (!existsSync32(dir)) return [];
|
|
16702
|
+
return readdirSync13(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
16439
16703
|
}
|
|
16440
16704
|
|
|
16441
16705
|
// src/mcp-endpoint-registry.ts
|
|
@@ -16555,8 +16819,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
16555
16819
|
import { createServer as createHttpServer } from "http";
|
|
16556
16820
|
import { spawn as spawn6 } from "child_process";
|
|
16557
16821
|
import { randomBytes } from "crypto";
|
|
16558
|
-
import { existsSync as
|
|
16559
|
-
import { join as
|
|
16822
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync29, writeFileSync as writeFileSync21, unlinkSync as unlinkSync4, readdirSync as readdirSync14 } from "fs";
|
|
16823
|
+
import { join as join36 } from "path";
|
|
16560
16824
|
|
|
16561
16825
|
// src/secret-compare.ts
|
|
16562
16826
|
import { timingSafeEqual } from "crypto";
|
|
@@ -16645,20 +16909,20 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
16645
16909
|
return child;
|
|
16646
16910
|
};
|
|
16647
16911
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
16648
|
-
return
|
|
16912
|
+
return join36(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
16649
16913
|
}
|
|
16650
16914
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
16651
16915
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
16652
|
-
if (!
|
|
16916
|
+
if (!existsSync33(path)) return void 0;
|
|
16653
16917
|
try {
|
|
16654
|
-
return JSON.parse(
|
|
16918
|
+
return JSON.parse(readFileSync29(path, "utf8"));
|
|
16655
16919
|
} catch {
|
|
16656
16920
|
return void 0;
|
|
16657
16921
|
}
|
|
16658
16922
|
}
|
|
16659
16923
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
16660
16924
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
16661
|
-
mkdirSync20(
|
|
16925
|
+
mkdirSync20(join36(stateDir, "passengers"), { recursive: true });
|
|
16662
16926
|
writeFileSync21(path, `${JSON.stringify(state, null, 2)}
|
|
16663
16927
|
`);
|
|
16664
16928
|
}
|
|
@@ -16679,10 +16943,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
16679
16943
|
}
|
|
16680
16944
|
}
|
|
16681
16945
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
16682
|
-
const dir =
|
|
16683
|
-
if (!
|
|
16946
|
+
const dir = join36(stateDir, "passengers");
|
|
16947
|
+
if (!existsSync33(dir)) return [];
|
|
16684
16948
|
const suffix = "-sync.json";
|
|
16685
|
-
return
|
|
16949
|
+
return readdirSync14(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
16686
16950
|
}
|
|
16687
16951
|
function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
16688
16952
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
@@ -16768,95 +17032,9 @@ function describeCause(err) {
|
|
|
16768
17032
|
}
|
|
16769
17033
|
|
|
16770
17034
|
// src/daemon.ts
|
|
16771
|
-
import { existsSync as
|
|
16772
|
-
import { join as
|
|
16773
|
-
|
|
16774
|
-
// src/interrupted.ts
|
|
16775
|
-
import { existsSync as existsSync32, readdirSync as readdirSync13, readFileSync as readFileSync29 } from "fs";
|
|
16776
|
-
import { join as join35 } from "path";
|
|
16777
|
-
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
16778
|
-
function interruptedComment(b) {
|
|
16779
|
-
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.";
|
|
16780
|
-
return `${INTERRUPTED_MARKER}
|
|
16781
|
-
|
|
16782
|
-
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.`;
|
|
16783
|
-
}
|
|
16784
|
-
var handled = /* @__PURE__ */ new Set();
|
|
16785
|
-
var pidAlive = (pid) => {
|
|
16786
|
-
try {
|
|
16787
|
-
process.kill(pid, 0);
|
|
16788
|
-
return true;
|
|
16789
|
-
} catch (e) {
|
|
16790
|
-
return e.code === "EPERM";
|
|
16791
|
-
}
|
|
16792
|
-
};
|
|
16793
|
-
function runFinished(streamPath) {
|
|
16794
|
-
const path = `${streamPath}.meta.json`;
|
|
16795
|
-
if (!existsSync32(path)) return false;
|
|
16796
|
-
try {
|
|
16797
|
-
return JSON.parse(readFileSync29(path, "utf8")).running !== true;
|
|
16798
|
-
} catch {
|
|
16799
|
-
return true;
|
|
16800
|
-
}
|
|
16801
|
-
}
|
|
16802
|
-
function orphanedStreams(o) {
|
|
16803
|
-
const dir = join35(o.stateDir, "streams");
|
|
16804
|
-
if (!existsSync32(dir)) return [];
|
|
16805
|
-
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
16806
|
-
const out = [];
|
|
16807
|
-
for (const name of readdirSync13(dir)) {
|
|
16808
|
-
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
16809
|
-
const streamPath = join35(dir, name);
|
|
16810
|
-
if (runFinished(streamPath)) continue;
|
|
16811
|
-
const sidecar = readRunSidecar(streamPath);
|
|
16812
|
-
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
16813
|
-
if (ownerAlive(sidecar.ownerPid)) continue;
|
|
16814
|
-
out.push({ streamPath, sidecar });
|
|
16815
|
-
}
|
|
16816
|
-
return out;
|
|
16817
|
-
}
|
|
16818
|
-
async function reconcileRuns(o) {
|
|
16819
|
-
let adopted = 0;
|
|
16820
|
-
let interrupted = 0;
|
|
16821
|
-
for (const { streamPath, sidecar } of orphanedStreams(o)) {
|
|
16822
|
-
if (handled.has(streamPath)) continue;
|
|
16823
|
-
handled.add(streamPath);
|
|
16824
|
-
const isAlive = o.isAlive ?? runIsAlive;
|
|
16825
|
-
const lock = isAlive(sidecar) ? o.state.acquireRun(sidecar.role, o.maxConcurrentAgents) : null;
|
|
16826
|
-
const work = (async () => {
|
|
16827
|
-
try {
|
|
16828
|
-
const result = await reattachRun(streamPath, sidecar, o.emit, {
|
|
16829
|
-
agentLogKey: await o.agentLogKey?.(sidecar.role).catch(() => void 0),
|
|
16830
|
-
isAlive: o.isAlive,
|
|
16831
|
-
pollMs: o.pollMs,
|
|
16832
|
-
onAdopted: () => o.emit.emit(`adopted ${sidecar.role} run (pid ${sidecar.pid}) left by a previous crew process`, { step: "agent", role: sidecar.role })
|
|
16833
|
-
});
|
|
16834
|
-
if (result.adopted) adopted++;
|
|
16835
|
-
else interrupted++;
|
|
16836
|
-
try {
|
|
16837
|
-
await o.afterRun?.(sidecar, result);
|
|
16838
|
-
} catch (e) {
|
|
16839
|
-
o.emit.warn(`could not follow up on ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
16840
|
-
}
|
|
16841
|
-
} catch (e) {
|
|
16842
|
-
o.emit.warn(`could not reattach ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
16843
|
-
} finally {
|
|
16844
|
-
if (lock?.ok) lock.release();
|
|
16845
|
-
}
|
|
16846
|
-
})();
|
|
16847
|
-
if (isAlive(sidecar)) {
|
|
16848
|
-
if (o.inFlight) {
|
|
16849
|
-
o.inFlight.add(work);
|
|
16850
|
-
void work.finally(() => o.inFlight.delete(work));
|
|
16851
|
-
}
|
|
16852
|
-
} else {
|
|
16853
|
-
await work;
|
|
16854
|
-
}
|
|
16855
|
-
}
|
|
16856
|
-
return { adopted, interrupted };
|
|
16857
|
-
}
|
|
16858
|
-
|
|
16859
|
-
// src/daemon.ts
|
|
17035
|
+
import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync30, readdirSync as readdirSync15, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync22 } from "fs";
|
|
17036
|
+
import { join as join37 } from "path";
|
|
17037
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
16860
17038
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
16861
17039
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
16862
17040
|
var defaultSleep = (ms2) => new Promise((resolve6) => setTimeout(resolve6, ms2));
|
|
@@ -16925,8 +17103,8 @@ function latestMtime(path) {
|
|
|
16925
17103
|
if (stat.isFile()) return stat.mtimeMs;
|
|
16926
17104
|
if (!stat.isDirectory()) return 0;
|
|
16927
17105
|
let latest = stat.mtimeMs;
|
|
16928
|
-
for (const entry of
|
|
16929
|
-
latest = Math.max(latest, latestMtime(
|
|
17106
|
+
for (const entry of readdirSync15(path, { withFileTypes: true })) {
|
|
17107
|
+
latest = Math.max(latest, latestMtime(join37(path, entry.name)));
|
|
16930
17108
|
}
|
|
16931
17109
|
return latest;
|
|
16932
17110
|
}
|
|
@@ -16935,13 +17113,13 @@ function computeTreeSignature(paths) {
|
|
|
16935
17113
|
}
|
|
16936
17114
|
function makeStaleChecker(paths, opts = {}) {
|
|
16937
17115
|
const initial = computeTreeSignature(paths);
|
|
16938
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
17116
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync34(opts.reloadSentinel);
|
|
16939
17117
|
}
|
|
16940
17118
|
function reloadSentinelFor(stateDir) {
|
|
16941
|
-
return
|
|
17119
|
+
return join37(stateDir, "reload-requested");
|
|
16942
17120
|
}
|
|
16943
17121
|
function daemonStampFor(stateDir) {
|
|
16944
|
-
return
|
|
17122
|
+
return join37(stateDir, "daemon.started");
|
|
16945
17123
|
}
|
|
16946
17124
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
16947
17125
|
mkdirSync21(stateDir, { recursive: true });
|
|
@@ -16966,6 +17144,7 @@ function configChangedAfter(files, startedAt) {
|
|
|
16966
17144
|
}
|
|
16967
17145
|
return null;
|
|
16968
17146
|
}
|
|
17147
|
+
var TICKET_ROLES = /* @__PURE__ */ new Set(["dev", "design", "qa"]);
|
|
16969
17148
|
var SPAWN_PARK_AFTER = 3;
|
|
16970
17149
|
var SpawnBreaker = class {
|
|
16971
17150
|
states = /* @__PURE__ */ new Map();
|
|
@@ -17063,46 +17242,69 @@ async function runOnePassUnguarded(o) {
|
|
|
17063
17242
|
);
|
|
17064
17243
|
const started = [];
|
|
17065
17244
|
const skipped = [];
|
|
17066
|
-
|
|
17067
|
-
|
|
17068
|
-
|
|
17069
|
-
|
|
17070
|
-
|
|
17071
|
-
|
|
17072
|
-
|
|
17073
|
-
|
|
17074
|
-
|
|
17075
|
-
|
|
17076
|
-
started.push(role);
|
|
17077
|
-
const run2 = spawn7(role, decision, ctx).then(() => {
|
|
17078
|
-
breaker.recordSuccess(role);
|
|
17079
|
-
clearShipAttention(o.state, `role_parked:${role}`);
|
|
17080
|
-
}).catch(async (e) => {
|
|
17081
|
-
const message = e.message;
|
|
17082
|
-
if (!(e instanceof AgentSpawnError)) {
|
|
17083
|
-
emit.warn(`${role} agent run failed: ${message}`, { step: "agent", role });
|
|
17084
|
-
return;
|
|
17245
|
+
const roleCap = (role) => TICKET_ROLES.has(role) ? o.roleConcurrency?.[role] ?? 1 : 1;
|
|
17246
|
+
const live = new Map(order.map((r) => [r, 0]));
|
|
17247
|
+
while (live.size) {
|
|
17248
|
+
for (const role of order) {
|
|
17249
|
+
const count = live.get(role);
|
|
17250
|
+
if (count === void 0) continue;
|
|
17251
|
+
if (!breaker.allows(role)) {
|
|
17252
|
+
if (count === 0) skipped.push({ role, reason: "agent recently failed to start \u2014 waiting before retry" });
|
|
17253
|
+
live.delete(role);
|
|
17254
|
+
continue;
|
|
17085
17255
|
}
|
|
17086
|
-
const
|
|
17087
|
-
if (
|
|
17088
|
-
|
|
17256
|
+
const lock = o.state.acquireRun(role, o.maxConcurrentAgents, roleCap(role));
|
|
17257
|
+
if (!lock.ok) {
|
|
17258
|
+
if (count === 0) skipped.push({ role, reason: lock.reason });
|
|
17259
|
+
live.delete(role);
|
|
17260
|
+
continue;
|
|
17089
17261
|
}
|
|
17090
|
-
|
|
17091
|
-
|
|
17092
|
-
|
|
17093
|
-
|
|
17094
|
-
|
|
17095
|
-
|
|
17096
|
-
|
|
17097
|
-
|
|
17098
|
-
|
|
17099
|
-
|
|
17262
|
+
started.push(role);
|
|
17263
|
+
let ready = () => {
|
|
17264
|
+
};
|
|
17265
|
+
const settled = new Promise((resolve6) => {
|
|
17266
|
+
ready = resolve6;
|
|
17267
|
+
});
|
|
17268
|
+
const run2 = spawn7(role, decision, ctx, { ready }).then(() => {
|
|
17269
|
+
breaker.recordSuccess(role);
|
|
17270
|
+
clearShipAttention(o.state, `role_parked:${role}`);
|
|
17271
|
+
}).catch(async (e) => {
|
|
17272
|
+
const message = e.message;
|
|
17273
|
+
if (!(e instanceof AgentSpawnError)) {
|
|
17274
|
+
emit.warn(`${role} agent run failed: ${message}`, { step: "agent", role });
|
|
17275
|
+
return;
|
|
17276
|
+
}
|
|
17277
|
+
const report = breaker.recordFailure(role, message);
|
|
17278
|
+
if (report.firstOfMessage || report.parked) {
|
|
17279
|
+
emit.warn(`${role} agent could not start: ${message}`, { step: "agent", role });
|
|
17280
|
+
}
|
|
17281
|
+
if (report.parked) {
|
|
17282
|
+
emit.error(
|
|
17283
|
+
`${role} parked for ${Math.round(report.retryInMs / 1e3)}s after ${report.failures} consecutive failed starts \u2014 check that ship.agent.bin is installed and on PATH`
|
|
17284
|
+
);
|
|
17285
|
+
await raiseShipAttention({ state: o.state, emit, route: o.route, ship: o.ship }, {
|
|
17286
|
+
kind: "role_parked",
|
|
17287
|
+
key: `role_parked:${role}`,
|
|
17288
|
+
message: `${role} agent is parked after ${report.failures} failed starts: ${message}`,
|
|
17289
|
+
since: (/* @__PURE__ */ new Date()).toISOString()
|
|
17290
|
+
});
|
|
17291
|
+
}
|
|
17292
|
+
}).finally(() => {
|
|
17293
|
+
lock.release();
|
|
17294
|
+
ready(false);
|
|
17295
|
+
});
|
|
17296
|
+
if (o.inFlight) {
|
|
17297
|
+
const set = o.inFlight;
|
|
17298
|
+
set.add(run2);
|
|
17299
|
+
void run2.finally(() => set.delete(run2));
|
|
17300
|
+
}
|
|
17301
|
+
if (roleCap(role) <= 1) {
|
|
17302
|
+
live.delete(role);
|
|
17303
|
+
continue;
|
|
17100
17304
|
}
|
|
17101
|
-
|
|
17102
|
-
|
|
17103
|
-
|
|
17104
|
-
set.add(run2);
|
|
17105
|
-
void run2.finally(() => set.delete(run2));
|
|
17305
|
+
const tookTicket = await settled;
|
|
17306
|
+
if (tookTicket && count + 1 < roleCap(role)) live.set(role, count + 1);
|
|
17307
|
+
else live.delete(role);
|
|
17106
17308
|
}
|
|
17107
17309
|
}
|
|
17108
17310
|
await publishShipAttention(o);
|
|
@@ -17151,6 +17353,7 @@ async function reconcileForRoute(ctx, o) {
|
|
|
17151
17353
|
emit,
|
|
17152
17354
|
state: o.state,
|
|
17153
17355
|
maxConcurrentAgents: o.maxConcurrentAgents,
|
|
17356
|
+
roleConcurrency: o.roleConcurrency,
|
|
17154
17357
|
inFlight: o.inFlight,
|
|
17155
17358
|
agentLogKey: async (role) => (await resolveSeatKey(route, role, ctx.getSessionStore)).apiKey,
|
|
17156
17359
|
afterRun: async (sc, result) => {
|
|
@@ -17179,20 +17382,32 @@ async function reconcileForRoute(ctx, o) {
|
|
|
17179
17382
|
await guardRunHandoff(route, ship, tracker, sc.role, sc.ticketRecordId, emit, sc.spawnedAt);
|
|
17180
17383
|
}
|
|
17181
17384
|
} finally {
|
|
17182
|
-
|
|
17183
|
-
|
|
17184
|
-
|
|
17185
|
-
|
|
17186
|
-
|
|
17187
|
-
|
|
17188
|
-
|
|
17385
|
+
await settleRunEnd({
|
|
17386
|
+
tracker,
|
|
17387
|
+
shipName: ship.name,
|
|
17388
|
+
route,
|
|
17389
|
+
role: sc.role,
|
|
17390
|
+
memberId,
|
|
17391
|
+
ownTicketRecordId: sc.ticketRecordId,
|
|
17392
|
+
stateDir: ship.stateDir,
|
|
17393
|
+
emit
|
|
17189
17394
|
});
|
|
17190
17395
|
}
|
|
17191
17396
|
}
|
|
17192
17397
|
});
|
|
17193
17398
|
}
|
|
17194
|
-
async function runRoleAgent(role, decision, ctx) {
|
|
17399
|
+
async function runRoleAgent(role, decision, ctx, hooks) {
|
|
17400
|
+
const held = {};
|
|
17401
|
+
try {
|
|
17402
|
+
await runRoleAgentLocked(role, decision, ctx, hooks, held);
|
|
17403
|
+
} finally {
|
|
17404
|
+
held.release?.();
|
|
17405
|
+
hooks?.ready(false);
|
|
17406
|
+
}
|
|
17407
|
+
}
|
|
17408
|
+
async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
17195
17409
|
const { route, ship, emit } = ctx;
|
|
17410
|
+
const runId = randomBytes2(3).toString("hex");
|
|
17196
17411
|
const identity = await resolveSeatKey(route, role, ctx.getSessionStore);
|
|
17197
17412
|
if (identity.kind === "ship" && SEAT_KEY_ROLES.includes(role)) {
|
|
17198
17413
|
emit.warn(`${role} writes as the ship, not its own agent: ${identity.reason}`, { step: "agent", role });
|
|
@@ -17215,8 +17430,13 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
17215
17430
|
claimStatuses(role, contract).approved,
|
|
17216
17431
|
claimStatuses(role, contract).building,
|
|
17217
17432
|
(t) => dirForRepo(route, t.repo_id) !== null,
|
|
17218
|
-
withHoldCheck(await tracker.claimAffinity(), decision.selectionInput)
|
|
17433
|
+
withHoldCheck(await tracker.claimAffinity(), decision.selectionInput),
|
|
17434
|
+
(t) => {
|
|
17435
|
+
const got = ctx.state.acquireTicket(t.id);
|
|
17436
|
+
return got.ok ? got.release : null;
|
|
17437
|
+
}
|
|
17219
17438
|
);
|
|
17439
|
+
held.release = result.releaseTicket;
|
|
17220
17440
|
if (result.contended.length) {
|
|
17221
17441
|
emit.emit(`claim contended for ${result.contended.join(", ")} \u2014 moved to the next candidate`, {
|
|
17222
17442
|
step: "select",
|
|
@@ -17253,6 +17473,11 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
17253
17473
|
if (idx >= 0) decision.tickets[idx] = result.ticket;
|
|
17254
17474
|
writeDigest({ route, ship, state: ctx.state, emit }, decision, role, ship.stateDir);
|
|
17255
17475
|
}
|
|
17476
|
+
writeDigest({ route, ship, state: ctx.state, emit }, decision, role, ship.stateDir, runId, {
|
|
17477
|
+
ticket: result.ticket,
|
|
17478
|
+
inFlight: (id) => ctx.state.ticketInFlight(id)
|
|
17479
|
+
});
|
|
17480
|
+
hooks?.ready(true);
|
|
17256
17481
|
ticketHint = result.ticket.issue_id;
|
|
17257
17482
|
workingId = result.ticket.id;
|
|
17258
17483
|
const target = repoTargetFor(route, result.ticket.repo_id);
|
|
@@ -17278,6 +17503,7 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
17278
17503
|
environment: await buildEnvironment(route, ship, ticketHint),
|
|
17279
17504
|
apiKey: identity.apiKey,
|
|
17280
17505
|
cycle: emit.cycle,
|
|
17506
|
+
runId,
|
|
17281
17507
|
ticket: ticketHint,
|
|
17282
17508
|
repo: ticketRepo,
|
|
17283
17509
|
ticketRecordId: workingId,
|
|
@@ -17318,18 +17544,16 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
17318
17544
|
await spawnAgent(plan, emit, hintMoved);
|
|
17319
17545
|
await guardRunHandoff(route, ship, tracker, role, workingId, emit, runStart);
|
|
17320
17546
|
} finally {
|
|
17321
|
-
|
|
17322
|
-
|
|
17323
|
-
|
|
17324
|
-
|
|
17325
|
-
|
|
17326
|
-
|
|
17327
|
-
|
|
17328
|
-
|
|
17329
|
-
|
|
17330
|
-
}
|
|
17331
|
-
emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
|
|
17332
|
-
}
|
|
17547
|
+
await settleRunEnd({
|
|
17548
|
+
tracker,
|
|
17549
|
+
shipName: ship.name,
|
|
17550
|
+
route,
|
|
17551
|
+
role,
|
|
17552
|
+
memberId,
|
|
17553
|
+
ownTicketRecordId: workingId,
|
|
17554
|
+
stateDir: ship.stateDir,
|
|
17555
|
+
emit
|
|
17556
|
+
});
|
|
17333
17557
|
}
|
|
17334
17558
|
}
|
|
17335
17559
|
async function buildEnvironment(route, ship, ticket) {
|
|
@@ -17771,7 +17995,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
17771
17995
|
} else {
|
|
17772
17996
|
const toleratesNoRoutes = command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
17773
17997
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
17774
|
-
if (configless && !
|
|
17998
|
+
if (configless && !existsSync35(findConfigFile(CREW_HOME))) {
|
|
17775
17999
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
17776
18000
|
} else {
|
|
17777
18001
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -18009,8 +18233,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
18009
18233
|
}
|
|
18010
18234
|
if (host === "linux" && hasSystemd) {
|
|
18011
18235
|
const label = labelFor(CREW_HOME, "run");
|
|
18012
|
-
const staleTimer =
|
|
18013
|
-
if (
|
|
18236
|
+
const staleTimer = join38(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
18237
|
+
if (existsSync35(staleTimer)) {
|
|
18014
18238
|
process.stdout.write(
|
|
18015
18239
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
18016
18240
|
`
|
|
@@ -18037,7 +18261,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18037
18261
|
const tracker = new Tracker(c, cfg.ship);
|
|
18038
18262
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
18039
18263
|
for (const r of await resolvedRepos(c)) {
|
|
18040
|
-
if (!
|
|
18264
|
+
if (!existsSync35(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
18041
18265
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
18042
18266
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
18043
18267
|
}
|
|
@@ -18055,7 +18279,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18055
18279
|
}
|
|
18056
18280
|
async function doctorFix(o) {
|
|
18057
18281
|
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))));
|
|
18058
|
-
const unitExists = (p) =>
|
|
18282
|
+
const unitExists = (p) => existsSync35(p);
|
|
18059
18283
|
const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
18060
18284
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
18061
18285
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -18718,7 +18942,7 @@ ${cu.tail.trimEnd()}
|
|
|
18718
18942
|
`);
|
|
18719
18943
|
break;
|
|
18720
18944
|
}
|
|
18721
|
-
const fleetLock = state.acquireRun(w.role, cfg.ship.maxConcurrentAgents);
|
|
18945
|
+
const fleetLock = state.acquireRun(w.role, cfg.ship.maxConcurrentAgents, cfg.ship.roleConcurrency[w.role] ?? 1);
|
|
18722
18946
|
if (!fleetLock.ok) {
|
|
18723
18947
|
wemit.emit(`${w.role} skipped this cycle \u2014 ${fleetLock.reason}`);
|
|
18724
18948
|
if (!skipInlineRelease) await releaseFleet();
|
|
@@ -18741,8 +18965,20 @@ ${cu.tail.trimEnd()}
|
|
|
18741
18965
|
claimStatuses(w.role, contract).approved,
|
|
18742
18966
|
claimStatuses(w.role, contract).building,
|
|
18743
18967
|
(t) => dirForRepo(w.route, t.repo_id) !== null,
|
|
18744
|
-
withHoldCheck(await fleetTracker.claimAffinity(), w.decision.selectionInput)
|
|
18968
|
+
withHoldCheck(await fleetTracker.claimAffinity(), w.decision.selectionInput),
|
|
18969
|
+
(t) => {
|
|
18970
|
+
const got = state.acquireTicket(t.id);
|
|
18971
|
+
return got.ok ? got.release : null;
|
|
18972
|
+
}
|
|
18745
18973
|
);
|
|
18974
|
+
if (result.releaseTicket) {
|
|
18975
|
+
const roleLock = lock;
|
|
18976
|
+
const releaseTicket = result.releaseTicket;
|
|
18977
|
+
lock = { release: () => {
|
|
18978
|
+
roleLock?.release();
|
|
18979
|
+
releaseTicket();
|
|
18980
|
+
} };
|
|
18981
|
+
}
|
|
18746
18982
|
if (result.contended.length) {
|
|
18747
18983
|
wemit.emit(`claim contended for ${result.contended.join(", ")} \u2014 moved to the next candidate`, {
|
|
18748
18984
|
step: "select",
|
|
@@ -18832,18 +19068,16 @@ ${cu.tail.trimEnd()}
|
|
|
18832
19068
|
await guardRunHandoff(w.route, cfg.ship, fleetTracker, w.role, fleetWorkingId, wemit, runStart);
|
|
18833
19069
|
} finally {
|
|
18834
19070
|
dropLock();
|
|
18835
|
-
|
|
18836
|
-
|
|
18837
|
-
|
|
18838
|
-
|
|
18839
|
-
|
|
18840
|
-
|
|
18841
|
-
|
|
18842
|
-
|
|
18843
|
-
|
|
18844
|
-
}
|
|
18845
|
-
wemit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
|
|
18846
|
-
}
|
|
19071
|
+
await settleRunEnd({
|
|
19072
|
+
tracker: fleetTracker,
|
|
19073
|
+
shipName: cfg.ship.name,
|
|
19074
|
+
route: w.route,
|
|
19075
|
+
role: w.role,
|
|
19076
|
+
memberId: fleetMemberId,
|
|
19077
|
+
ownTicketRecordId: fleetWorkingId,
|
|
19078
|
+
stateDir: cfg.ship.stateDir,
|
|
19079
|
+
emit: wemit
|
|
19080
|
+
});
|
|
18847
19081
|
}
|
|
18848
19082
|
if (!skipInlineRelease) await releaseFleet();
|
|
18849
19083
|
break;
|
|
@@ -18914,7 +19148,7 @@ ${cu.tail.trimEnd()}
|
|
|
18914
19148
|
const tracker2 = new Tracker(route, cfg.ship);
|
|
18915
19149
|
while (current) {
|
|
18916
19150
|
if (!dryRun) {
|
|
18917
|
-
const got = state.acquireRun(current, cfg.ship.maxConcurrentAgents);
|
|
19151
|
+
const got = state.acquireRun(current, cfg.ship.maxConcurrentAgents, cfg.ship.roleConcurrency[current] ?? 1);
|
|
18918
19152
|
if (!got.ok) {
|
|
18919
19153
|
emit.emit(`${current} skipped this cycle \u2014 ${got.reason}`);
|
|
18920
19154
|
if (explicitRole) {
|
|
@@ -18942,8 +19176,20 @@ ${cu.tail.trimEnd()}
|
|
|
18942
19176
|
claimStatuses(current, contract).approved,
|
|
18943
19177
|
claimStatuses(current, contract).building,
|
|
18944
19178
|
(t) => dirForRepo(route, t.repo_id) !== null,
|
|
18945
|
-
withHoldCheck(await tracker2.claimAffinity(), decision.selectionInput)
|
|
19179
|
+
withHoldCheck(await tracker2.claimAffinity(), decision.selectionInput),
|
|
19180
|
+
(t) => {
|
|
19181
|
+
const got = state.acquireTicket(t.id);
|
|
19182
|
+
return got.ok ? got.release : null;
|
|
19183
|
+
}
|
|
18946
19184
|
);
|
|
19185
|
+
if (result.releaseTicket) {
|
|
19186
|
+
const roleLock = lock;
|
|
19187
|
+
const releaseTicket = result.releaseTicket;
|
|
19188
|
+
lock = { release: () => {
|
|
19189
|
+
roleLock?.release();
|
|
19190
|
+
releaseTicket();
|
|
19191
|
+
} };
|
|
19192
|
+
}
|
|
18947
19193
|
if (result.contended.length) {
|
|
18948
19194
|
emit.emit(`claim contended for ${result.contended.join(", ")} \u2014 moved to the next candidate`, {
|
|
18949
19195
|
step: "select",
|
|
@@ -19061,18 +19307,16 @@ ${cu.tail.trimEnd()}
|
|
|
19061
19307
|
await guardRunHandoff(route, cfg.ship, tracker2, current, workingId, emit, runStart);
|
|
19062
19308
|
} finally {
|
|
19063
19309
|
dropLock();
|
|
19064
|
-
|
|
19065
|
-
|
|
19066
|
-
|
|
19067
|
-
|
|
19068
|
-
|
|
19069
|
-
|
|
19070
|
-
|
|
19071
|
-
|
|
19072
|
-
|
|
19073
|
-
}
|
|
19074
|
-
emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
|
|
19075
|
-
}
|
|
19310
|
+
await settleRunEnd({
|
|
19311
|
+
tracker: tracker2,
|
|
19312
|
+
shipName: cfg.ship.name,
|
|
19313
|
+
route,
|
|
19314
|
+
role: current,
|
|
19315
|
+
memberId,
|
|
19316
|
+
ownTicketRecordId: workingId,
|
|
19317
|
+
stateDir: cfg.ship.stateDir,
|
|
19318
|
+
emit
|
|
19319
|
+
});
|
|
19076
19320
|
}
|
|
19077
19321
|
ran.push(current);
|
|
19078
19322
|
if (ran.length >= maxRoles || explicitRole) break;
|
|
@@ -19156,7 +19400,7 @@ ${cu.tail.trimEnd()}
|
|
|
19156
19400
|
process.exit(1);
|
|
19157
19401
|
}
|
|
19158
19402
|
requireArmed("run the daemon loop");
|
|
19159
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
19403
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join38(CREW_HOME, "src"), join38(CREW_HOME, "dist"), join38(CREW_HOME, "bin")];
|
|
19160
19404
|
const configPaths = daemonConfigPaths(cfg);
|
|
19161
19405
|
watchPaths.push(...configPaths);
|
|
19162
19406
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -19167,6 +19411,7 @@ ${cu.tail.trimEnd()}
|
|
|
19167
19411
|
ship: cfg.ship,
|
|
19168
19412
|
state,
|
|
19169
19413
|
maxConcurrentAgents: cfg.ship.maxConcurrentAgents,
|
|
19414
|
+
roleConcurrency: cfg.ship.roleConcurrency,
|
|
19170
19415
|
spawnBreaker,
|
|
19171
19416
|
configFile: cfg.configFile,
|
|
19172
19417
|
newEmitter: () => new Emitter({
|
|
@@ -19384,7 +19629,7 @@ ${cu.tail.trimEnd()}
|
|
|
19384
19629
|
`);
|
|
19385
19630
|
if (!dryRun) {
|
|
19386
19631
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
19387
|
-
const raw =
|
|
19632
|
+
const raw = existsSync35(resolvedPath) ? JSON.parse(readFileSync31(resolvedPath, "utf8")) : {};
|
|
19388
19633
|
raw.agentSkills = result.agentSkills;
|
|
19389
19634
|
mkdirSync22(dirOf(resolvedPath), { recursive: true });
|
|
19390
19635
|
writeFileSync23(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -19488,10 +19733,10 @@ ${cu.tail.trimEnd()}
|
|
|
19488
19733
|
try {
|
|
19489
19734
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
19490
19735
|
const dir = repoDirOf(configText, dirname20(cfg.configFile), a.route, a.name);
|
|
19491
|
-
if (!
|
|
19492
|
-
const files = { exists: (r) =>
|
|
19736
|
+
if (!existsSync35(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
19737
|
+
const files = { exists: (r) => existsSync35(resolvePath(dir, r)), list: (r) => {
|
|
19493
19738
|
try {
|
|
19494
|
-
return
|
|
19739
|
+
return readdirSync16(resolvePath(dir, r));
|
|
19495
19740
|
} catch {
|
|
19496
19741
|
return [];
|
|
19497
19742
|
}
|
|
@@ -19607,7 +19852,7 @@ ${cu.tail.trimEnd()}
|
|
|
19607
19852
|
if (sub === "list" && !routeName) usage2();
|
|
19608
19853
|
if (sub === "add") {
|
|
19609
19854
|
try {
|
|
19610
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
19855
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync35(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
|
|
19611
19856
|
routeName = a.route;
|
|
19612
19857
|
addPath = a.path;
|
|
19613
19858
|
addName = a.name;
|
|
@@ -19636,7 +19881,7 @@ ${cu.tail.trimEnd()}
|
|
|
19636
19881
|
}
|
|
19637
19882
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
19638
19883
|
let resolvedRemotes;
|
|
19639
|
-
if (
|
|
19884
|
+
if (existsSync35(resolvedFile)) {
|
|
19640
19885
|
try {
|
|
19641
19886
|
resolvedRemotes = JSON.parse(readFileSync31(resolvedFile, "utf8"));
|
|
19642
19887
|
} catch {
|
|
@@ -19645,7 +19890,7 @@ ${cu.tail.trimEnd()}
|
|
|
19645
19890
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
19646
19891
|
const text = configText;
|
|
19647
19892
|
const base = dirname20(cfg.configFile);
|
|
19648
|
-
const isCheckout = (d) =>
|
|
19893
|
+
const isCheckout = (d) => existsSync35(`${d}/.git`);
|
|
19649
19894
|
try {
|
|
19650
19895
|
if (sub === "list") {
|
|
19651
19896
|
if (flag("json")) {
|
|
@@ -19655,7 +19900,7 @@ ${cu.tail.trimEnd()}
|
|
|
19655
19900
|
base,
|
|
19656
19901
|
resolved: resolvedRemotes,
|
|
19657
19902
|
originOf,
|
|
19658
|
-
exists: (d) =>
|
|
19903
|
+
exists: (d) => existsSync35(d),
|
|
19659
19904
|
isCheckout,
|
|
19660
19905
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
19661
19906
|
try {
|
|
@@ -19676,7 +19921,7 @@ ${cu.tail.trimEnd()}
|
|
|
19676
19921
|
for (const r of repos) {
|
|
19677
19922
|
const origin2 = originOf(r.dir);
|
|
19678
19923
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
19679
|
-
const note = !
|
|
19924
|
+
const note = !existsSync35(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
19680
19925
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
19681
19926
|
`);
|
|
19682
19927
|
}
|
|
@@ -19688,9 +19933,9 @@ ${cu.tail.trimEnd()}
|
|
|
19688
19933
|
` : `${human}
|
|
19689
19934
|
`);
|
|
19690
19935
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
19691
|
-
if (
|
|
19936
|
+
if (existsSync35(plan2.target) && (existsSync35(`${plan2.target}/.git`) || readdirSync16(plan2.target).length > 0)) {
|
|
19692
19937
|
throw new ReposError(
|
|
19693
|
-
|
|
19938
|
+
existsSync35(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
|
|
19694
19939
|
);
|
|
19695
19940
|
}
|
|
19696
19941
|
if (dryRun) {
|
|
@@ -19715,7 +19960,7 @@ ${cu.tail.trimEnd()}
|
|
|
19715
19960
|
break;
|
|
19716
19961
|
}
|
|
19717
19962
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
|
|
19718
|
-
const top =
|
|
19963
|
+
const top = existsSync35(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
19719
19964
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
19720
19965
|
const origin = originOf(top);
|
|
19721
19966
|
const plan = planRepoAdd({
|
|
@@ -19767,7 +20012,7 @@ ${cu.tail.trimEnd()}
|
|
|
19767
20012
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
19768
20013
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
19769
20014
|
`);
|
|
19770
|
-
if (
|
|
20015
|
+
if (existsSync35(resolvedFile)) {
|
|
19771
20016
|
const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
|
|
19772
20017
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
19773
20018
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
@@ -20184,7 +20429,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20184
20429
|
}
|
|
20185
20430
|
}
|
|
20186
20431
|
let previous;
|
|
20187
|
-
const hadPreviousFile =
|
|
20432
|
+
const hadPreviousFile = existsSync35(resolvedPath);
|
|
20188
20433
|
if (hadPreviousFile) {
|
|
20189
20434
|
try {
|
|
20190
20435
|
previous = JSON.parse(readFileSync31(resolvedPath, "utf8"));
|
|
@@ -21226,7 +21471,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21226
21471
|
}
|
|
21227
21472
|
if (row) {
|
|
21228
21473
|
await tracker.deleteRepoRow(row.id);
|
|
21229
|
-
if (
|
|
21474
|
+
if (existsSync35(resolvedFile)) {
|
|
21230
21475
|
const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
|
|
21231
21476
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
21232
21477
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
@@ -21234,21 +21479,21 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21234
21479
|
`);
|
|
21235
21480
|
}
|
|
21236
21481
|
}
|
|
21237
|
-
if (
|
|
21482
|
+
if (existsSync35(join38(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
|
|
21238
21483
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
21239
21484
|
break;
|
|
21240
21485
|
}
|
|
21241
21486
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
21242
|
-
const fresh = !
|
|
21243
|
-
if (
|
|
21487
|
+
const fresh = !existsSync35(join38(dir, ".git"));
|
|
21488
|
+
if (existsSync35(dir) && fresh && readdirSync16(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
21244
21489
|
if (dryRun) {
|
|
21245
21490
|
say("dry-run", { dir, newCheckout: fresh, repoRow: row?.id ?? null }, `would ${fresh ? "create" : "reuse"} ${dir}${row ? "" : ", create the Repos row"}, and file one ${tracker.contract.statuses.approved} ticket`);
|
|
21246
21491
|
break;
|
|
21247
21492
|
}
|
|
21248
21493
|
if (fresh) {
|
|
21249
21494
|
mkdirSync22(dir, { recursive: true });
|
|
21250
|
-
writeFileSync23(
|
|
21251
|
-
writeFileSync23(
|
|
21495
|
+
writeFileSync23(join38(dir, "README.md"), SAMPLE_README);
|
|
21496
|
+
writeFileSync23(join38(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
21252
21497
|
const g = (...a) => {
|
|
21253
21498
|
const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
|
|
21254
21499
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -21266,7 +21511,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21266
21511
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
21267
21512
|
});
|
|
21268
21513
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
21269
|
-
if (
|
|
21514
|
+
if (existsSync35(resolvedFile)) {
|
|
21270
21515
|
const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
|
|
21271
21516
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
21272
21517
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
@@ -21397,6 +21642,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21397
21642
|
const todo = await statusTracker.openTickets().then((open) => operatorTodo(open, statusTracker.contract, route.resolved?.operator ?? "")).catch(() => []);
|
|
21398
21643
|
const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
|
|
21399
21644
|
const crewMinVersion = await readCrewFloor(route, cfg.ship) ?? null;
|
|
21645
|
+
const runs = liveRuns(cfg.ship.stateDir);
|
|
21400
21646
|
if (flag("json")) {
|
|
21401
21647
|
const nameSync = await reconcileRouteShipName({ route, ship: cfg.ship, configFile: cfg.configFile, readOnly: true });
|
|
21402
21648
|
const repos = await resolvedRepos(route);
|
|
@@ -21414,7 +21660,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21414
21660
|
label: ROLE_LABEL[r],
|
|
21415
21661
|
crewId: seatId ?? null,
|
|
21416
21662
|
name: crewRow?.name ?? null,
|
|
21417
|
-
personaName
|
|
21663
|
+
personaName,
|
|
21664
|
+
// Additive (CREW-2822): how many runs this seat has live right now.
|
|
21665
|
+
running: runs.filter((x) => x.role === r && x.route === routeSlug(route.route)).length
|
|
21418
21666
|
};
|
|
21419
21667
|
});
|
|
21420
21668
|
process.stdout.write(`${JSON.stringify({
|
|
@@ -21457,6 +21705,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21457
21705
|
roles: ROLE_NAMES.map((r) => ({ name: r, label: ROLE_LABEL[r], paused: state.isRolePaused(r) })),
|
|
21458
21706
|
seats: seats2
|
|
21459
21707
|
},
|
|
21708
|
+
// Additive (CREW-2822): every live run on this ship. `version` stays 1.
|
|
21709
|
+
runs: runs.map((x) => describeRun(x, route, seatIds)),
|
|
21460
21710
|
watermark: state.watermark(),
|
|
21461
21711
|
route: {
|
|
21462
21712
|
name: route.route,
|
|
@@ -21569,6 +21819,10 @@ watermark: ${state.watermark()}
|
|
|
21569
21819
|
`
|
|
21570
21820
|
);
|
|
21571
21821
|
}
|
|
21822
|
+
for (const x of runs) {
|
|
21823
|
+
process.stdout.write(`running: ${x.role} \u2014 ${x.ticket ?? "(no ticket)"}${x.repo ? ` in ${x.repo}` : ""} (pid ${x.pid}, since ${x.spawnedAt})
|
|
21824
|
+
`);
|
|
21825
|
+
}
|
|
21572
21826
|
if (shipRows.length) {
|
|
21573
21827
|
process.stdout.write("ships:\n");
|
|
21574
21828
|
for (const s of shipRows) {
|
|
@@ -21597,7 +21851,7 @@ watermark: ${state.watermark()}
|
|
|
21597
21851
|
}
|
|
21598
21852
|
for (const r of repos) {
|
|
21599
21853
|
const remote = r.config.branch.remote;
|
|
21600
|
-
if (
|
|
21854
|
+
if (existsSync35(r.dir)) {
|
|
21601
21855
|
pushAuth.push({
|
|
21602
21856
|
route: c.route,
|
|
21603
21857
|
repo: r.name,
|
|
@@ -21642,6 +21896,7 @@ watermark: ${state.watermark()}
|
|
|
21642
21896
|
} else if (p.state === "ok") passed++;
|
|
21643
21897
|
}
|
|
21644
21898
|
}
|
|
21899
|
+
for (const message of roleCapWarnings(cfg.ship)) problems.push({ severity: "note", kind: "role_cap_above_ship_cap", message });
|
|
21645
21900
|
problems.push(...attentionProblems(openShipAttention(state), problems));
|
|
21646
21901
|
if (problems.some((p) => p.severity === "blocker")) process.exitCode = 1;
|
|
21647
21902
|
process.stdout.write(`${JSON.stringify({ offline: flag("offline"), pushAuth, tools: toolRows, problems, passed }, null, 2)}
|
|
@@ -21653,9 +21908,11 @@ watermark: ${state.watermark()}
|
|
|
21653
21908
|
routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
21654
21909
|
`
|
|
21655
21910
|
);
|
|
21911
|
+
for (const w of roleCapWarnings(cfg.ship)) process.stdout.write(`role concurrency: WARN ${w}
|
|
21912
|
+
`);
|
|
21656
21913
|
try {
|
|
21657
21914
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
21658
|
-
const evSize =
|
|
21915
|
+
const evSize = existsSync35(evFile) ? statSync10(evFile).size : 0;
|
|
21659
21916
|
process.stdout.write(
|
|
21660
21917
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
21661
21918
|
` : "events log: ok\n"
|