@tablation/crew 0.80.0 → 0.81.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 +318 -97
- package/package.json +1 -1
package/crew.example.yaml
CHANGED
|
@@ -27,6 +27,21 @@ ship:
|
|
|
27
27
|
# to disable (less latency/cost, no cycles reported).
|
|
28
28
|
# maxThinkingTokens: 4096
|
|
29
29
|
|
|
30
|
+
# Optional (CREW-2821): how many agents this ship runs at once. `roleConcurrency`
|
|
31
|
+
# is the cap on runs of ONE role (default 1 per role, so unset behaves as it
|
|
32
|
+
# always has: dev beside qa, never two devs). `maxConcurrentAgents` is the
|
|
33
|
+
# ship-wide cap across every role. Default 2 when `roleConcurrency` is unset;
|
|
34
|
+
# when `roleConcurrency` is set and `maxConcurrentAgents` is not, it defaults
|
|
35
|
+
# to the SUM of the role caps (plus 1 for each of dev/design/qa/triage the map
|
|
36
|
+
# leaves out), so `roleConcurrency: { dev: 2, qa: 2 }` alone allows 6 runs.
|
|
37
|
+
# Set it explicitly to hold a machine-wide budget below that sum;
|
|
38
|
+
# `crew doctor` warns when it is lower than any single role's cap. Each run
|
|
39
|
+
# takes its own ticket (a per-ticket lock keeps two runs off the same one).
|
|
40
|
+
# `dev`, `design` and `qa` can run several; `triage` is always one at a time.
|
|
41
|
+
# The release route is unaffected: one release per repository at a time.
|
|
42
|
+
# maxConcurrentAgents: 4
|
|
43
|
+
# roleConcurrency: { dev: 2, qa: 2 }
|
|
44
|
+
|
|
30
45
|
# Optional (CREW-1511): named toolchains — a harness adapter plus the binary
|
|
31
46
|
# and model to run it with. The `agent:` block above keeps working and
|
|
32
47
|
# defines an implicit toolchain named `claude`. Harnesses: claude, codex, gemini. See docs/toolchains.md.
|
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
|
*
|
|
@@ -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);
|
|
@@ -10955,8 +11030,15 @@ async function decideCycle(o) {
|
|
|
10955
11030
|
}
|
|
10956
11031
|
return decision;
|
|
10957
11032
|
}
|
|
10958
|
-
var digestPath = (stateDir, route, role) => join23(stateDir, `digest-${route}-${role}.md`);
|
|
10959
|
-
function
|
|
11033
|
+
var digestPath = (stateDir, route, role, runId) => join23(stateDir, `digest-${route}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
11034
|
+
function otherRunsRemoved(tickets, own) {
|
|
11035
|
+
if (!own) return tickets;
|
|
11036
|
+
return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
|
|
11037
|
+
}
|
|
11038
|
+
function ownRunBanner(own) {
|
|
11039
|
+
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.`;
|
|
11040
|
+
}
|
|
11041
|
+
function writeDigest(o, d, role, stateDir, runId, own) {
|
|
10960
11042
|
const me = o.route.resolved?.seats[role];
|
|
10961
11043
|
if (!me) return false;
|
|
10962
11044
|
try {
|
|
@@ -11002,7 +11084,7 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
11002
11084
|
}
|
|
11003
11085
|
return existingBranchForTicket(dir, repoFor(dir), t, role);
|
|
11004
11086
|
},
|
|
11005
|
-
tickets: role === "triage" ? triageSlice(d.tickets, o.route.resolved?.seats.triage, d.selectionInput.workModes) : withinWorkMode(sliceFor(d.tickets, role), d.selectionInput.workModes),
|
|
11087
|
+
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
11088
|
workModes: d.selectionInput.workModes,
|
|
11007
11089
|
comments: d.comments,
|
|
11008
11090
|
commentColumns: d.selectionInput.contract?.comments,
|
|
@@ -11021,8 +11103,11 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
11021
11103
|
blocked: d.blocked,
|
|
11022
11104
|
blockerInfo: d.info
|
|
11023
11105
|
};
|
|
11024
|
-
const
|
|
11025
|
-
const
|
|
11106
|
+
const body = role === "qa" ? qaDigest(input) : buildingDigest(input);
|
|
11107
|
+
const text = own ? `${ownRunBanner(own)}
|
|
11108
|
+
|
|
11109
|
+
${body}` : body;
|
|
11110
|
+
const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
|
|
11026
11111
|
writeFileSync14(path, text);
|
|
11027
11112
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
|
|
11028
11113
|
data: { role, bytes: Buffer.byteLength(text) }
|
|
@@ -11063,7 +11148,7 @@ async function stampResumption(tracker, t, affinity) {
|
|
|
11063
11148
|
}
|
|
11064
11149
|
return { outcome: "unknown" };
|
|
11065
11150
|
}
|
|
11066
|
-
async function resolveTopCandidate(tracker, candidates, seat, approvedStatus, buildingStatus, isServable, affinity) {
|
|
11151
|
+
async function resolveTopCandidate(tracker, candidates, seat, approvedStatus, buildingStatus, isServable, affinity, ticketLock) {
|
|
11067
11152
|
const contended = [];
|
|
11068
11153
|
const unservable = [];
|
|
11069
11154
|
const held = [];
|
|
@@ -11076,41 +11161,58 @@ async function resolveTopCandidate(tracker, candidates, seat, approvedStatus, bu
|
|
|
11076
11161
|
unservable.push(t.issue_id);
|
|
11077
11162
|
continue;
|
|
11078
11163
|
}
|
|
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);
|
|
11164
|
+
let releaseTicket;
|
|
11165
|
+
if (ticketLock) {
|
|
11166
|
+
const got = ticketLock(t);
|
|
11167
|
+
if (!got) {
|
|
11168
|
+
contended.push(t.issue_id);
|
|
11089
11169
|
continue;
|
|
11090
11170
|
}
|
|
11171
|
+
releaseTicket = got;
|
|
11172
|
+
}
|
|
11173
|
+
const result = await resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affinity, contended, unservable, held);
|
|
11174
|
+
if (result === "walk") {
|
|
11175
|
+
releaseTicket?.();
|
|
11176
|
+
continue;
|
|
11177
|
+
}
|
|
11178
|
+
return releaseTicket ? { ...result, releaseTicket } : result;
|
|
11179
|
+
}
|
|
11180
|
+
return { ticket: null, claimed: false, contended, unservable, held, reaccepted: false, tookOverFrom: null };
|
|
11181
|
+
}
|
|
11182
|
+
async function resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affinity, contended, unservable, held) {
|
|
11183
|
+
if (t.status !== approvedStatus) {
|
|
11184
|
+
if (affinity?.isLiveHoldElsewhere?.(t)) {
|
|
11091
11185
|
return { ticket: t, claimed: false, contended, unservable, held, reaccepted: false, tookOverFrom: null };
|
|
11092
11186
|
}
|
|
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;
|
|
11187
|
+
if (affinity && t[affinity.column] !== affinity.shipId) {
|
|
11188
|
+
const stamp = await stampResumption(tracker, t, affinity);
|
|
11189
|
+
if (stamp.outcome === "stamped") {
|
|
11190
|
+
return { ticket: stamp.ticket, claimed: false, contended, unservable, held, reaccepted: false, tookOverFrom: displacedHolder(t, affinity) };
|
|
11109
11191
|
}
|
|
11110
|
-
|
|
11192
|
+
(stamp.outcome === "held" ? held : contended).push(t.issue_id);
|
|
11193
|
+
return "walk";
|
|
11111
11194
|
}
|
|
11195
|
+
return { ticket: t, claimed: false, contended, unservable, held, reaccepted: false, tookOverFrom: null };
|
|
11196
|
+
}
|
|
11197
|
+
try {
|
|
11198
|
+
const claimed = await tracker.updateTicket(
|
|
11199
|
+
t.id,
|
|
11200
|
+
{
|
|
11201
|
+
status: buildingStatus,
|
|
11202
|
+
assignee_id: seat,
|
|
11203
|
+
...affinity ? { [affinity.column]: affinity.shipId } : {}
|
|
11204
|
+
},
|
|
11205
|
+
t.updated_at
|
|
11206
|
+
);
|
|
11207
|
+
const reaccepted = !!affinity && t[affinity.column] === affinity.shipId;
|
|
11208
|
+
return { ticket: { ...t, ...claimed }, claimed: true, contended, unservable, held, reaccepted, tookOverFrom: displacedHolder(t, affinity) };
|
|
11209
|
+
} catch (e) {
|
|
11210
|
+
if (e instanceof StaleWriteError6) {
|
|
11211
|
+
contended.push(t.issue_id);
|
|
11212
|
+
return "walk";
|
|
11213
|
+
}
|
|
11214
|
+
throw e;
|
|
11112
11215
|
}
|
|
11113
|
-
return { ticket: null, claimed: false, contended, unservable, held, reaccepted: false, tookOverFrom: null };
|
|
11114
11216
|
}
|
|
11115
11217
|
|
|
11116
11218
|
// src/conflict.ts
|
|
@@ -11975,6 +12077,27 @@ function planStamp(cwd, tickets, contract, fromSha, toSha, merged = /* @__PURE__
|
|
|
11975
12077
|
function mergedAtFor(cwd, sha, fallback) {
|
|
11976
12078
|
return cwd && commitDate(cwd, sha) || fallback;
|
|
11977
12079
|
}
|
|
12080
|
+
async function applyMergeStamp(tracker, merged, tickets, emit, dryRun, cwd) {
|
|
12081
|
+
let stamped = 0;
|
|
12082
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
12083
|
+
for (const ticket of tickets) {
|
|
12084
|
+
const sha = merged.get(ticket.issue_id);
|
|
12085
|
+
if (dryRun) {
|
|
12086
|
+
emit.emit(`would stamp commit_sha${sha ? ` (${sha.slice(0, 8)})` : ""}`, { ticket: ticket.issue_id });
|
|
12087
|
+
stamped++;
|
|
12088
|
+
continue;
|
|
12089
|
+
}
|
|
12090
|
+
if (!sha || ticket.commit_sha === sha) continue;
|
|
12091
|
+
try {
|
|
12092
|
+
await tracker.updateTicket(ticket.id, { commit_sha: sha, merged_at: mergedAtFor(cwd, sha, at) });
|
|
12093
|
+
emit.emit(`stamped commit_sha (${sha.slice(0, 8)})`, { ticket: ticket.issue_id });
|
|
12094
|
+
stamped++;
|
|
12095
|
+
} catch (e) {
|
|
12096
|
+
emit.warn(`could not stamp commit_sha: ${e.message}`, { ticket: ticket.issue_id });
|
|
12097
|
+
}
|
|
12098
|
+
}
|
|
12099
|
+
return stamped;
|
|
12100
|
+
}
|
|
11978
12101
|
async function applyStamp(tracker, plan, version, contract, emit, dryRun, cwd) {
|
|
11979
12102
|
let stamped = 0;
|
|
11980
12103
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -11987,6 +12110,7 @@ async function applyStamp(tracker, plan, version, contract, emit, dryRun, cwd) {
|
|
|
11987
12110
|
stamped++;
|
|
11988
12111
|
continue;
|
|
11989
12112
|
}
|
|
12113
|
+
const recordMerge = !!sha && !ticket.commit_sha;
|
|
11990
12114
|
try {
|
|
11991
12115
|
await tracker.updateTicket(ticket.id, {
|
|
11992
12116
|
status: contract.statuses.deployed,
|
|
@@ -11997,7 +12121,7 @@ async function applyStamp(tracker, plan, version, contract, emit, dryRun, cwd) {
|
|
|
11997
12121
|
// ticket's record should mean the same thing regardless of which
|
|
11998
12122
|
// release mode closed it (ISSUE-218). `merged_at` is the commit's own
|
|
11999
12123
|
// date, not the stamp time (which trails the deploy in ci_* modes).
|
|
12000
|
-
...sha ? { commit_sha: sha, merged_at: mergedAtFor(cwd, sha, at) } : {}
|
|
12124
|
+
...recordMerge && sha ? { commit_sha: sha, merged_at: mergedAtFor(cwd, sha, at) } : {}
|
|
12001
12125
|
});
|
|
12002
12126
|
emit.emit(`stamped ${contract.statuses.deployed}${version ? ` (${version})` : ""}${shaNote}`, {
|
|
12003
12127
|
ticket: ticket.issue_id
|
|
@@ -13072,6 +13196,13 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
|
|
|
13072
13196
|
}
|
|
13073
13197
|
}
|
|
13074
13198
|
}
|
|
13199
|
+
if (o.repo.release.mode === "ci_auto" || o.repo.release.mode === "ci_manual") {
|
|
13200
|
+
try {
|
|
13201
|
+
await o.onBasePushed?.(merged);
|
|
13202
|
+
} catch (e) {
|
|
13203
|
+
o.emit.warn(`could not record merged tickets: ${e.message}`);
|
|
13204
|
+
}
|
|
13205
|
+
}
|
|
13075
13206
|
let deployed = false;
|
|
13076
13207
|
let integrated = false;
|
|
13077
13208
|
if (o.repo.release.mode === "integrate") {
|
|
@@ -16741,6 +16872,7 @@ function describeCause(err) {
|
|
|
16741
16872
|
// src/daemon.ts
|
|
16742
16873
|
import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync30, readdirSync as readdirSync14, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync22 } from "fs";
|
|
16743
16874
|
import { join as join36 } from "path";
|
|
16875
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
16744
16876
|
|
|
16745
16877
|
// src/interrupted.ts
|
|
16746
16878
|
import { existsSync as existsSync32, readdirSync as readdirSync13, readFileSync as readFileSync29 } from "fs";
|
|
@@ -16793,7 +16925,8 @@ async function reconcileRuns(o) {
|
|
|
16793
16925
|
if (handled.has(streamPath)) continue;
|
|
16794
16926
|
handled.add(streamPath);
|
|
16795
16927
|
const isAlive = o.isAlive ?? runIsAlive;
|
|
16796
|
-
const lock = isAlive(sidecar) ? o.state.acquireRun(sidecar.role, o.maxConcurrentAgents) : null;
|
|
16928
|
+
const lock = isAlive(sidecar) ? o.state.acquireRun(sidecar.role, o.maxConcurrentAgents, o.roleConcurrency?.[sidecar.role] ?? 1) : null;
|
|
16929
|
+
const ticketLock = isAlive(sidecar) && sidecar.ticketRecordId ? o.state.acquireTicket(sidecar.ticketRecordId) : null;
|
|
16797
16930
|
const work = (async () => {
|
|
16798
16931
|
try {
|
|
16799
16932
|
const result = await reattachRun(streamPath, sidecar, o.emit, {
|
|
@@ -16813,6 +16946,7 @@ async function reconcileRuns(o) {
|
|
|
16813
16946
|
o.emit.warn(`could not reattach ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
16814
16947
|
} finally {
|
|
16815
16948
|
if (lock?.ok) lock.release();
|
|
16949
|
+
if (ticketLock?.ok) ticketLock.release();
|
|
16816
16950
|
}
|
|
16817
16951
|
})();
|
|
16818
16952
|
if (isAlive(sidecar)) {
|
|
@@ -16937,6 +17071,7 @@ function configChangedAfter(files, startedAt) {
|
|
|
16937
17071
|
}
|
|
16938
17072
|
return null;
|
|
16939
17073
|
}
|
|
17074
|
+
var TICKET_ROLES = /* @__PURE__ */ new Set(["dev", "design", "qa"]);
|
|
16940
17075
|
var SPAWN_PARK_AFTER = 3;
|
|
16941
17076
|
var SpawnBreaker = class {
|
|
16942
17077
|
states = /* @__PURE__ */ new Map();
|
|
@@ -17034,46 +17169,69 @@ async function runOnePassUnguarded(o) {
|
|
|
17034
17169
|
);
|
|
17035
17170
|
const started = [];
|
|
17036
17171
|
const skipped = [];
|
|
17037
|
-
|
|
17038
|
-
|
|
17039
|
-
|
|
17040
|
-
|
|
17041
|
-
|
|
17042
|
-
|
|
17043
|
-
|
|
17044
|
-
|
|
17045
|
-
|
|
17046
|
-
|
|
17047
|
-
started.push(role);
|
|
17048
|
-
const run2 = spawn7(role, decision, ctx).then(() => {
|
|
17049
|
-
breaker.recordSuccess(role);
|
|
17050
|
-
clearShipAttention(o.state, `role_parked:${role}`);
|
|
17051
|
-
}).catch(async (e) => {
|
|
17052
|
-
const message = e.message;
|
|
17053
|
-
if (!(e instanceof AgentSpawnError)) {
|
|
17054
|
-
emit.warn(`${role} agent run failed: ${message}`, { step: "agent", role });
|
|
17055
|
-
return;
|
|
17172
|
+
const roleCap = (role) => TICKET_ROLES.has(role) ? o.roleConcurrency?.[role] ?? 1 : 1;
|
|
17173
|
+
const live = new Map(order.map((r) => [r, 0]));
|
|
17174
|
+
while (live.size) {
|
|
17175
|
+
for (const role of order) {
|
|
17176
|
+
const count = live.get(role);
|
|
17177
|
+
if (count === void 0) continue;
|
|
17178
|
+
if (!breaker.allows(role)) {
|
|
17179
|
+
if (count === 0) skipped.push({ role, reason: "agent recently failed to start \u2014 waiting before retry" });
|
|
17180
|
+
live.delete(role);
|
|
17181
|
+
continue;
|
|
17056
17182
|
}
|
|
17057
|
-
const
|
|
17058
|
-
if (
|
|
17059
|
-
|
|
17183
|
+
const lock = o.state.acquireRun(role, o.maxConcurrentAgents, roleCap(role));
|
|
17184
|
+
if (!lock.ok) {
|
|
17185
|
+
if (count === 0) skipped.push({ role, reason: lock.reason });
|
|
17186
|
+
live.delete(role);
|
|
17187
|
+
continue;
|
|
17060
17188
|
}
|
|
17061
|
-
|
|
17062
|
-
|
|
17063
|
-
|
|
17064
|
-
|
|
17065
|
-
|
|
17066
|
-
|
|
17067
|
-
|
|
17068
|
-
|
|
17069
|
-
|
|
17070
|
-
|
|
17189
|
+
started.push(role);
|
|
17190
|
+
let ready = () => {
|
|
17191
|
+
};
|
|
17192
|
+
const settled = new Promise((resolve6) => {
|
|
17193
|
+
ready = resolve6;
|
|
17194
|
+
});
|
|
17195
|
+
const run2 = spawn7(role, decision, ctx, { ready }).then(() => {
|
|
17196
|
+
breaker.recordSuccess(role);
|
|
17197
|
+
clearShipAttention(o.state, `role_parked:${role}`);
|
|
17198
|
+
}).catch(async (e) => {
|
|
17199
|
+
const message = e.message;
|
|
17200
|
+
if (!(e instanceof AgentSpawnError)) {
|
|
17201
|
+
emit.warn(`${role} agent run failed: ${message}`, { step: "agent", role });
|
|
17202
|
+
return;
|
|
17203
|
+
}
|
|
17204
|
+
const report = breaker.recordFailure(role, message);
|
|
17205
|
+
if (report.firstOfMessage || report.parked) {
|
|
17206
|
+
emit.warn(`${role} agent could not start: ${message}`, { step: "agent", role });
|
|
17207
|
+
}
|
|
17208
|
+
if (report.parked) {
|
|
17209
|
+
emit.error(
|
|
17210
|
+
`${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`
|
|
17211
|
+
);
|
|
17212
|
+
await raiseShipAttention({ state: o.state, emit, route: o.route, ship: o.ship }, {
|
|
17213
|
+
kind: "role_parked",
|
|
17214
|
+
key: `role_parked:${role}`,
|
|
17215
|
+
message: `${role} agent is parked after ${report.failures} failed starts: ${message}`,
|
|
17216
|
+
since: (/* @__PURE__ */ new Date()).toISOString()
|
|
17217
|
+
});
|
|
17218
|
+
}
|
|
17219
|
+
}).finally(() => {
|
|
17220
|
+
lock.release();
|
|
17221
|
+
ready(false);
|
|
17222
|
+
});
|
|
17223
|
+
if (o.inFlight) {
|
|
17224
|
+
const set = o.inFlight;
|
|
17225
|
+
set.add(run2);
|
|
17226
|
+
void run2.finally(() => set.delete(run2));
|
|
17071
17227
|
}
|
|
17072
|
-
|
|
17073
|
-
|
|
17074
|
-
|
|
17075
|
-
|
|
17076
|
-
|
|
17228
|
+
if (roleCap(role) <= 1) {
|
|
17229
|
+
live.delete(role);
|
|
17230
|
+
continue;
|
|
17231
|
+
}
|
|
17232
|
+
const tookTicket = await settled;
|
|
17233
|
+
if (tookTicket && count + 1 < roleCap(role)) live.set(role, count + 1);
|
|
17234
|
+
else live.delete(role);
|
|
17077
17235
|
}
|
|
17078
17236
|
}
|
|
17079
17237
|
await publishShipAttention(o);
|
|
@@ -17122,6 +17280,7 @@ async function reconcileForRoute(ctx, o) {
|
|
|
17122
17280
|
emit,
|
|
17123
17281
|
state: o.state,
|
|
17124
17282
|
maxConcurrentAgents: o.maxConcurrentAgents,
|
|
17283
|
+
roleConcurrency: o.roleConcurrency,
|
|
17125
17284
|
inFlight: o.inFlight,
|
|
17126
17285
|
agentLogKey: async (role) => (await resolveSeatKey(route, role, ctx.getSessionStore)).apiKey,
|
|
17127
17286
|
afterRun: async (sc, result) => {
|
|
@@ -17162,8 +17321,18 @@ async function reconcileForRoute(ctx, o) {
|
|
|
17162
17321
|
}
|
|
17163
17322
|
});
|
|
17164
17323
|
}
|
|
17165
|
-
async function runRoleAgent(role, decision, ctx) {
|
|
17324
|
+
async function runRoleAgent(role, decision, ctx, hooks) {
|
|
17325
|
+
const held = {};
|
|
17326
|
+
try {
|
|
17327
|
+
await runRoleAgentLocked(role, decision, ctx, hooks, held);
|
|
17328
|
+
} finally {
|
|
17329
|
+
held.release?.();
|
|
17330
|
+
hooks?.ready(false);
|
|
17331
|
+
}
|
|
17332
|
+
}
|
|
17333
|
+
async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
17166
17334
|
const { route, ship, emit } = ctx;
|
|
17335
|
+
const runId = randomBytes2(3).toString("hex");
|
|
17167
17336
|
const identity = await resolveSeatKey(route, role, ctx.getSessionStore);
|
|
17168
17337
|
if (identity.kind === "ship" && SEAT_KEY_ROLES.includes(role)) {
|
|
17169
17338
|
emit.warn(`${role} writes as the ship, not its own agent: ${identity.reason}`, { step: "agent", role });
|
|
@@ -17186,8 +17355,13 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
17186
17355
|
claimStatuses(role, contract).approved,
|
|
17187
17356
|
claimStatuses(role, contract).building,
|
|
17188
17357
|
(t) => dirForRepo(route, t.repo_id) !== null,
|
|
17189
|
-
withHoldCheck(await tracker.claimAffinity(), decision.selectionInput)
|
|
17358
|
+
withHoldCheck(await tracker.claimAffinity(), decision.selectionInput),
|
|
17359
|
+
(t) => {
|
|
17360
|
+
const got = ctx.state.acquireTicket(t.id);
|
|
17361
|
+
return got.ok ? got.release : null;
|
|
17362
|
+
}
|
|
17190
17363
|
);
|
|
17364
|
+
held.release = result.releaseTicket;
|
|
17191
17365
|
if (result.contended.length) {
|
|
17192
17366
|
emit.emit(`claim contended for ${result.contended.join(", ")} \u2014 moved to the next candidate`, {
|
|
17193
17367
|
step: "select",
|
|
@@ -17224,6 +17398,11 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
17224
17398
|
if (idx >= 0) decision.tickets[idx] = result.ticket;
|
|
17225
17399
|
writeDigest({ route, ship, state: ctx.state, emit }, decision, role, ship.stateDir);
|
|
17226
17400
|
}
|
|
17401
|
+
writeDigest({ route, ship, state: ctx.state, emit }, decision, role, ship.stateDir, runId, {
|
|
17402
|
+
ticket: result.ticket,
|
|
17403
|
+
inFlight: (id) => ctx.state.ticketInFlight(id)
|
|
17404
|
+
});
|
|
17405
|
+
hooks?.ready(true);
|
|
17227
17406
|
ticketHint = result.ticket.issue_id;
|
|
17228
17407
|
workingId = result.ticket.id;
|
|
17229
17408
|
const target = repoTargetFor(route, result.ticket.repo_id);
|
|
@@ -17249,6 +17428,7 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
17249
17428
|
environment: await buildEnvironment(route, ship, ticketHint),
|
|
17250
17429
|
apiKey: identity.apiKey,
|
|
17251
17430
|
cycle: emit.cycle,
|
|
17431
|
+
runId,
|
|
17252
17432
|
ticket: ticketHint,
|
|
17253
17433
|
repo: ticketRepo,
|
|
17254
17434
|
ticketRecordId: workingId,
|
|
@@ -18292,6 +18472,19 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18292
18472
|
skipTests: flag("skip-tests"),
|
|
18293
18473
|
shell: cfg.ship.shell,
|
|
18294
18474
|
mergeOnly: command === "merge",
|
|
18475
|
+
// ci_* modes: record the merge on the board when it lands on base,
|
|
18476
|
+
// not only once CI confirms the release live (CREW-2820).
|
|
18477
|
+
onBasePushed: async (merged) => {
|
|
18478
|
+
remit.enter("reconcile");
|
|
18479
|
+
await applyMergeStamp(
|
|
18480
|
+
tracker,
|
|
18481
|
+
new Map(merged.map((m) => [m.ticket.issue_id, m.sha])),
|
|
18482
|
+
merged.map((m) => m.ticket),
|
|
18483
|
+
remit,
|
|
18484
|
+
dryRun,
|
|
18485
|
+
target.dir
|
|
18486
|
+
);
|
|
18487
|
+
},
|
|
18295
18488
|
// `deploy` is the "go now" button: it exists for a commit a previous
|
|
18296
18489
|
// deploy failed on, where nothing new will merge but the work is
|
|
18297
18490
|
// genuinely unreleased.
|
|
@@ -18676,7 +18869,7 @@ ${cu.tail.trimEnd()}
|
|
|
18676
18869
|
`);
|
|
18677
18870
|
break;
|
|
18678
18871
|
}
|
|
18679
|
-
const fleetLock = state.acquireRun(w.role, cfg.ship.maxConcurrentAgents);
|
|
18872
|
+
const fleetLock = state.acquireRun(w.role, cfg.ship.maxConcurrentAgents, cfg.ship.roleConcurrency[w.role] ?? 1);
|
|
18680
18873
|
if (!fleetLock.ok) {
|
|
18681
18874
|
wemit.emit(`${w.role} skipped this cycle \u2014 ${fleetLock.reason}`);
|
|
18682
18875
|
if (!skipInlineRelease) await releaseFleet();
|
|
@@ -18699,8 +18892,20 @@ ${cu.tail.trimEnd()}
|
|
|
18699
18892
|
claimStatuses(w.role, contract).approved,
|
|
18700
18893
|
claimStatuses(w.role, contract).building,
|
|
18701
18894
|
(t) => dirForRepo(w.route, t.repo_id) !== null,
|
|
18702
|
-
withHoldCheck(await fleetTracker.claimAffinity(), w.decision.selectionInput)
|
|
18895
|
+
withHoldCheck(await fleetTracker.claimAffinity(), w.decision.selectionInput),
|
|
18896
|
+
(t) => {
|
|
18897
|
+
const got = state.acquireTicket(t.id);
|
|
18898
|
+
return got.ok ? got.release : null;
|
|
18899
|
+
}
|
|
18703
18900
|
);
|
|
18901
|
+
if (result.releaseTicket) {
|
|
18902
|
+
const roleLock = lock;
|
|
18903
|
+
const releaseTicket = result.releaseTicket;
|
|
18904
|
+
lock = { release: () => {
|
|
18905
|
+
roleLock?.release();
|
|
18906
|
+
releaseTicket();
|
|
18907
|
+
} };
|
|
18908
|
+
}
|
|
18704
18909
|
if (result.contended.length) {
|
|
18705
18910
|
wemit.emit(`claim contended for ${result.contended.join(", ")} \u2014 moved to the next candidate`, {
|
|
18706
18911
|
step: "select",
|
|
@@ -18872,7 +19077,7 @@ ${cu.tail.trimEnd()}
|
|
|
18872
19077
|
const tracker2 = new Tracker(route, cfg.ship);
|
|
18873
19078
|
while (current) {
|
|
18874
19079
|
if (!dryRun) {
|
|
18875
|
-
const got = state.acquireRun(current, cfg.ship.maxConcurrentAgents);
|
|
19080
|
+
const got = state.acquireRun(current, cfg.ship.maxConcurrentAgents, cfg.ship.roleConcurrency[current] ?? 1);
|
|
18876
19081
|
if (!got.ok) {
|
|
18877
19082
|
emit.emit(`${current} skipped this cycle \u2014 ${got.reason}`);
|
|
18878
19083
|
if (explicitRole) {
|
|
@@ -18900,8 +19105,20 @@ ${cu.tail.trimEnd()}
|
|
|
18900
19105
|
claimStatuses(current, contract).approved,
|
|
18901
19106
|
claimStatuses(current, contract).building,
|
|
18902
19107
|
(t) => dirForRepo(route, t.repo_id) !== null,
|
|
18903
|
-
withHoldCheck(await tracker2.claimAffinity(), decision.selectionInput)
|
|
19108
|
+
withHoldCheck(await tracker2.claimAffinity(), decision.selectionInput),
|
|
19109
|
+
(t) => {
|
|
19110
|
+
const got = state.acquireTicket(t.id);
|
|
19111
|
+
return got.ok ? got.release : null;
|
|
19112
|
+
}
|
|
18904
19113
|
);
|
|
19114
|
+
if (result.releaseTicket) {
|
|
19115
|
+
const roleLock = lock;
|
|
19116
|
+
const releaseTicket = result.releaseTicket;
|
|
19117
|
+
lock = { release: () => {
|
|
19118
|
+
roleLock?.release();
|
|
19119
|
+
releaseTicket();
|
|
19120
|
+
} };
|
|
19121
|
+
}
|
|
18905
19122
|
if (result.contended.length) {
|
|
18906
19123
|
emit.emit(`claim contended for ${result.contended.join(", ")} \u2014 moved to the next candidate`, {
|
|
18907
19124
|
step: "select",
|
|
@@ -19125,6 +19342,7 @@ ${cu.tail.trimEnd()}
|
|
|
19125
19342
|
ship: cfg.ship,
|
|
19126
19343
|
state,
|
|
19127
19344
|
maxConcurrentAgents: cfg.ship.maxConcurrentAgents,
|
|
19345
|
+
roleConcurrency: cfg.ship.roleConcurrency,
|
|
19128
19346
|
spawnBreaker,
|
|
19129
19347
|
configFile: cfg.configFile,
|
|
19130
19348
|
newEmitter: () => new Emitter({
|
|
@@ -21600,6 +21818,7 @@ watermark: ${state.watermark()}
|
|
|
21600
21818
|
} else if (p.state === "ok") passed++;
|
|
21601
21819
|
}
|
|
21602
21820
|
}
|
|
21821
|
+
for (const message of roleCapWarnings(cfg.ship)) problems.push({ severity: "note", kind: "role_cap_above_ship_cap", message });
|
|
21603
21822
|
problems.push(...attentionProblems(openShipAttention(state), problems));
|
|
21604
21823
|
if (problems.some((p) => p.severity === "blocker")) process.exitCode = 1;
|
|
21605
21824
|
process.stdout.write(`${JSON.stringify({ offline: flag("offline"), pushAuth, tools: toolRows, problems, passed }, null, 2)}
|
|
@@ -21611,6 +21830,8 @@ watermark: ${state.watermark()}
|
|
|
21611
21830
|
routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
21612
21831
|
`
|
|
21613
21832
|
);
|
|
21833
|
+
for (const w of roleCapWarnings(cfg.ship)) process.stdout.write(`role concurrency: WARN ${w}
|
|
21834
|
+
`);
|
|
21614
21835
|
try {
|
|
21615
21836
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
21616
21837
|
const evSize = existsSync34(evFile) ? statSync10(evFile).size : 0;
|
package/package.json
CHANGED