@brainervirus/workit-cli 8.38.4 → 8.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +695 -329
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -9317,7 +9317,7 @@ var package_default;
|
|
|
9317
9317
|
var init_package = __esm(() => {
|
|
9318
9318
|
package_default = {
|
|
9319
9319
|
name: "@brainervirus/workit-cli",
|
|
9320
|
-
version: "8.
|
|
9320
|
+
version: "8.39.0",
|
|
9321
9321
|
private: false,
|
|
9322
9322
|
description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
|
|
9323
9323
|
keywords: [
|
|
@@ -9355,13 +9355,13 @@ var init_package = __esm(() => {
|
|
|
9355
9355
|
},
|
|
9356
9356
|
dependencies: {
|
|
9357
9357
|
"@babel/parser": "7.29.9",
|
|
9358
|
-
"@brainervirus/workit-cursor": "^8.
|
|
9358
|
+
"@brainervirus/workit-cursor": "^8.39.0",
|
|
9359
9359
|
"@inkjs/ui": "2.0.0",
|
|
9360
9360
|
ink: "7.1.1",
|
|
9361
9361
|
react: "19.2.8"
|
|
9362
9362
|
},
|
|
9363
9363
|
devDependencies: {
|
|
9364
|
-
"@brainervirus/workit-core": "^8.
|
|
9364
|
+
"@brainervirus/workit-core": "^8.39.0"
|
|
9365
9365
|
},
|
|
9366
9366
|
engines: {
|
|
9367
9367
|
node: ">=24"
|
|
@@ -77869,6 +77869,7 @@ function resolveResourceSettings(sources, host = hostShape()) {
|
|
|
77869
77869
|
const settings = {
|
|
77870
77870
|
slots: DEFAULT_SLOTS,
|
|
77871
77871
|
heavy: [...DEFAULT_HEAVY_CHECKS],
|
|
77872
|
+
light: [...DEFAULT_LIGHT_CHECKS],
|
|
77872
77873
|
memoryMax: "",
|
|
77873
77874
|
cpuQuota: "",
|
|
77874
77875
|
waitMs: DEFAULT_WAIT_S * 1000,
|
|
@@ -77910,8 +77911,21 @@ function resolveResourceSettings(sources, host = hostShape()) {
|
|
|
77910
77911
|
const heavy = isRecord9(value.heavy) ? Object.keys(value.heavy) : value.heavy;
|
|
77911
77912
|
if (!Array.isArray(heavy) || !heavy.every((item) => typeof item === "string"))
|
|
77912
77913
|
warn("heavy", "must be a list of check names");
|
|
77913
|
-
else
|
|
77914
|
+
else {
|
|
77914
77915
|
settings.heavy = repo ? [...new Set([...settings.heavy, ...heavy])] : heavy;
|
|
77916
|
+
settings.light = settings.light.filter((name) => !heavy.includes(name));
|
|
77917
|
+
}
|
|
77918
|
+
}
|
|
77919
|
+
if (value.light !== undefined) {
|
|
77920
|
+
const light = value.light;
|
|
77921
|
+
if (repo)
|
|
77922
|
+
machineOnly("light");
|
|
77923
|
+
else if (!Array.isArray(light) || !light.every((item) => typeof item === "string"))
|
|
77924
|
+
warn("light", "must be a list of check names");
|
|
77925
|
+
else {
|
|
77926
|
+
settings.light = light;
|
|
77927
|
+
settings.heavy = settings.heavy.filter((name) => !light.includes(name));
|
|
77928
|
+
}
|
|
77915
77929
|
}
|
|
77916
77930
|
if (value.memoryMax !== undefined) {
|
|
77917
77931
|
const next = memoryValue(value.memoryMax, host);
|
|
@@ -77932,10 +77946,11 @@ function resolveResourceSettings(sources, host = hostShape()) {
|
|
|
77932
77946
|
cpu = next;
|
|
77933
77947
|
}
|
|
77934
77948
|
if (value.wait !== undefined) {
|
|
77935
|
-
|
|
77936
|
-
|
|
77949
|
+
const wait = typeof value.wait === "number" ? value.wait >= 0 ? value.wait * 1000 : null : typeof value.wait === "string" ? parseWait(value.wait) : null;
|
|
77950
|
+
if (wait !== null)
|
|
77951
|
+
settings.waitMs = wait;
|
|
77937
77952
|
else
|
|
77938
|
-
warn("wait",
|
|
77953
|
+
warn("wait", 'must be a number of seconds or a duration ("30m")');
|
|
77939
77954
|
}
|
|
77940
77955
|
}
|
|
77941
77956
|
settings.memoryMax = String(memory);
|
|
@@ -78053,7 +78068,65 @@ function removeOwn(file, id) {
|
|
|
78053
78068
|
if (parseLease(readText5(file))?.id === id)
|
|
78054
78069
|
fs24.rmSync(file, { force: true });
|
|
78055
78070
|
}
|
|
78071
|
+
function leaseQueue(dir, probe = {}) {
|
|
78072
|
+
let names;
|
|
78073
|
+
try {
|
|
78074
|
+
names = fs24.readdirSync(queueDir(dir));
|
|
78075
|
+
} catch {
|
|
78076
|
+
return [];
|
|
78077
|
+
}
|
|
78078
|
+
const entries = [];
|
|
78079
|
+
for (const name of names.filter((item) => item.endsWith(".json")).toSorted()) {
|
|
78080
|
+
const file = path52.join(queueDir(dir), name);
|
|
78081
|
+
const record = parseLease(readText5(file));
|
|
78082
|
+
if (!record)
|
|
78083
|
+
continue;
|
|
78084
|
+
entries.push({
|
|
78085
|
+
file,
|
|
78086
|
+
holder: holderOf(record),
|
|
78087
|
+
id: record.id,
|
|
78088
|
+
stale: staleReason(record, probe)
|
|
78089
|
+
});
|
|
78090
|
+
}
|
|
78091
|
+
return entries;
|
|
78092
|
+
}
|
|
78093
|
+
function waitersAhead(options, own) {
|
|
78094
|
+
let ahead = 0;
|
|
78095
|
+
for (const entry of leaseQueue(options.dir, options.probe)) {
|
|
78096
|
+
if (path52.basename(entry.file) >= own)
|
|
78097
|
+
break;
|
|
78098
|
+
if (!TICKET.test(path52.basename(entry.file)))
|
|
78099
|
+
continue;
|
|
78100
|
+
if (entry.stale && reclaim(entry.file, (current) => !current || staleReason(current, options.probe) !== null))
|
|
78101
|
+
continue;
|
|
78102
|
+
ahead += 1;
|
|
78103
|
+
}
|
|
78104
|
+
return ahead;
|
|
78105
|
+
}
|
|
78106
|
+
function liveHolders(options) {
|
|
78107
|
+
return leaseSlots(options.dir, options.slots, options.probe).filter((slot) => slot.holder && !slot.stale).map((slot) => slot.holder);
|
|
78108
|
+
}
|
|
78109
|
+
function writeTicket(file, record) {
|
|
78110
|
+
fs24.mkdirSync(path52.dirname(file), { recursive: true, mode: 448 });
|
|
78111
|
+
fs24.writeFileSync(file, `${JSON.stringify(record)}
|
|
78112
|
+
`, { mode: 384 });
|
|
78113
|
+
}
|
|
78114
|
+
function parseWait(text) {
|
|
78115
|
+
const match = /^(\d+(?:\.\d+)?)([smh]?)$/u.exec(text.trim());
|
|
78116
|
+
if (!match)
|
|
78117
|
+
return null;
|
|
78118
|
+
const unit = { "": 1000, s: 1000, m: 60000, h: 3600000 }[match[2]] ?? 1000;
|
|
78119
|
+
return Math.round(Number(match[1]) * unit);
|
|
78120
|
+
}
|
|
78121
|
+
function formatWait(ms) {
|
|
78122
|
+
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
78123
|
+
if (seconds < 60)
|
|
78124
|
+
return `${seconds}s`;
|
|
78125
|
+
const minutes = Math.floor(seconds / 60);
|
|
78126
|
+
return seconds % 60 ? `${minutes}m ${seconds % 60}s` : `${minutes}m`;
|
|
78127
|
+
}
|
|
78056
78128
|
async function acquireLease(options) {
|
|
78129
|
+
const clock = options.clock ?? realClock;
|
|
78057
78130
|
const pid = options.pid ?? process.pid;
|
|
78058
78131
|
const boot = bootId();
|
|
78059
78132
|
const record = {
|
|
@@ -78067,7 +78140,7 @@ async function acquireLease(options) {
|
|
|
78067
78140
|
check: options.check,
|
|
78068
78141
|
cwd: options.cwd,
|
|
78069
78142
|
session: options.session,
|
|
78070
|
-
startedAt: new Date().toISOString(),
|
|
78143
|
+
startedAt: new Date(clock.now()).toISOString(),
|
|
78071
78144
|
scope: null
|
|
78072
78145
|
};
|
|
78073
78146
|
try {
|
|
@@ -78075,48 +78148,104 @@ async function acquireLease(options) {
|
|
|
78075
78148
|
} catch (error) {
|
|
78076
78149
|
return { ok: false, reason: "unavailable", error: error.message };
|
|
78077
78150
|
}
|
|
78078
|
-
const started =
|
|
78151
|
+
const started = clock.now();
|
|
78079
78152
|
const reclaimed = [];
|
|
78080
78153
|
let waitedOn = [];
|
|
78081
|
-
|
|
78082
|
-
|
|
78083
|
-
|
|
78084
|
-
|
|
78085
|
-
|
|
78086
|
-
|
|
78087
|
-
|
|
78088
|
-
|
|
78089
|
-
|
|
78090
|
-
|
|
78091
|
-
|
|
78092
|
-
|
|
78093
|
-
|
|
78094
|
-
|
|
78095
|
-
|
|
78096
|
-
|
|
78097
|
-
|
|
78098
|
-
|
|
78099
|
-
|
|
78100
|
-
|
|
78101
|
-
|
|
78102
|
-
|
|
78103
|
-
|
|
78104
|
-
|
|
78105
|
-
|
|
78106
|
-
|
|
78107
|
-
|
|
78108
|
-
|
|
78154
|
+
const name = ticketName(started, record.id);
|
|
78155
|
+
const ticket = path52.join(queueDir(options.dir), name);
|
|
78156
|
+
const ticketRecord = {
|
|
78157
|
+
...record,
|
|
78158
|
+
maxAgeMs: options.waitMs + 60000
|
|
78159
|
+
};
|
|
78160
|
+
let queued = false;
|
|
78161
|
+
const onSignal = (signal) => {
|
|
78162
|
+
leave();
|
|
78163
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
78164
|
+
};
|
|
78165
|
+
const leave = () => {
|
|
78166
|
+
process.off("exit", leave);
|
|
78167
|
+
process.off("SIGINT", onSignal);
|
|
78168
|
+
process.off("SIGTERM", onSignal);
|
|
78169
|
+
if (queued)
|
|
78170
|
+
removeOwn(ticket, record.id);
|
|
78171
|
+
queued = false;
|
|
78172
|
+
};
|
|
78173
|
+
let nextStatus = 0;
|
|
78174
|
+
try {
|
|
78175
|
+
for (;; ) {
|
|
78176
|
+
const waited = clock.now() - started;
|
|
78177
|
+
const ahead = queued ? waitersAhead(options, name) : waitersAhead(options, "~");
|
|
78178
|
+
const free = options.slots - liveHolders(options).length;
|
|
78179
|
+
if (ahead < free) {
|
|
78180
|
+
const outcome = tryOnce(options, record, reclaimed);
|
|
78181
|
+
if ("slot" in outcome) {
|
|
78182
|
+
const waitedMs = queued ? waited : 0;
|
|
78183
|
+
leave();
|
|
78184
|
+
const { file, slot } = outcome;
|
|
78185
|
+
let released = false;
|
|
78186
|
+
const release = () => {
|
|
78187
|
+
if (released)
|
|
78188
|
+
return;
|
|
78189
|
+
released = true;
|
|
78190
|
+
process.off("exit", release);
|
|
78191
|
+
removeOwn(file, record.id);
|
|
78192
|
+
};
|
|
78193
|
+
process.on("exit", release);
|
|
78194
|
+
return {
|
|
78195
|
+
ok: true,
|
|
78196
|
+
lease: {
|
|
78197
|
+
record,
|
|
78198
|
+
slot,
|
|
78199
|
+
file,
|
|
78200
|
+
waitedOn,
|
|
78201
|
+
waitedMs,
|
|
78202
|
+
reclaimed,
|
|
78203
|
+
release
|
|
78204
|
+
}
|
|
78205
|
+
};
|
|
78109
78206
|
}
|
|
78110
|
-
}
|
|
78111
|
-
|
|
78112
|
-
|
|
78113
|
-
|
|
78114
|
-
|
|
78115
|
-
|
|
78207
|
+
}
|
|
78208
|
+
const holders = liveHolders(options);
|
|
78209
|
+
if (!waitedOn.length) {
|
|
78210
|
+
waitedOn = holders;
|
|
78211
|
+
options.onWait?.(holders);
|
|
78212
|
+
}
|
|
78213
|
+
if (waited >= options.waitMs)
|
|
78214
|
+
return {
|
|
78215
|
+
ok: false,
|
|
78216
|
+
reason: "timeout",
|
|
78217
|
+
holders,
|
|
78218
|
+
waitedMs: waited,
|
|
78219
|
+
position: ahead + 1
|
|
78220
|
+
};
|
|
78221
|
+
if (!queued) {
|
|
78222
|
+
writeTicket(ticket, ticketRecord);
|
|
78223
|
+
queued = true;
|
|
78224
|
+
process.on("exit", leave);
|
|
78225
|
+
process.on("SIGINT", onSignal);
|
|
78226
|
+
process.on("SIGTERM", onSignal);
|
|
78227
|
+
} else if (!fs24.existsSync(ticket)) {
|
|
78228
|
+
writeTicket(ticket, ticketRecord);
|
|
78229
|
+
}
|
|
78230
|
+
if (waited >= nextStatus) {
|
|
78231
|
+
options.onStatus?.({
|
|
78232
|
+
position: ahead + 1,
|
|
78233
|
+
ahead,
|
|
78234
|
+
holders,
|
|
78235
|
+
waitedMs: waited
|
|
78236
|
+
});
|
|
78237
|
+
nextStatus = waited + (options.statusEveryMs ?? 30000);
|
|
78238
|
+
}
|
|
78239
|
+
await clock.sleep(Math.min(options.pollMs ?? 2000, Math.max(1, options.waitMs - waited)));
|
|
78116
78240
|
}
|
|
78117
|
-
|
|
78118
|
-
|
|
78119
|
-
|
|
78241
|
+
} catch (error) {
|
|
78242
|
+
return {
|
|
78243
|
+
ok: false,
|
|
78244
|
+
reason: "unavailable",
|
|
78245
|
+
error: error.message
|
|
78246
|
+
};
|
|
78247
|
+
} finally {
|
|
78248
|
+
leave();
|
|
78120
78249
|
}
|
|
78121
78250
|
}
|
|
78122
78251
|
function updateLease(lease, patch) {
|
|
@@ -78229,7 +78358,7 @@ function leaseSlots(dir, slots, probe = {}) {
|
|
|
78229
78358
|
}
|
|
78230
78359
|
return states;
|
|
78231
78360
|
}
|
|
78232
|
-
var DEFAULT_HEAVY_CHECKS, DEFAULT_SLOTS = 1, DEFAULT_WAIT_S =
|
|
78361
|
+
var DEFAULT_HEAVY_CHECKS, DEFAULT_LIGHT_CHECKS, DEFAULT_SLOTS = 1, DEFAULT_WAIT_S = 1800, DEFAULT_MEMORY_SHARE = 0.4, DEFAULT_CPU_SHARE = 0.7, MAX_SLOTS = 64, UNREADABLE_TTL_MS = 30000, GUARD_TTL_MS = 30000, hostShape = () => ({
|
|
78233
78362
|
totalMemBytes: os20.totalmem(),
|
|
78234
78363
|
cpus: Math.max(1, os20.availableParallelism?.() ?? os20.cpus().length)
|
|
78235
78364
|
}), isRecord9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), SIZE, PERCENT, UNITS, leaseDir = (env) => path52.join(stateDir2(env), "leases"), readText5 = (file) => {
|
|
@@ -78263,16 +78392,22 @@ var DEFAULT_HEAVY_CHECKS, DEFAULT_SLOTS = 1, DEFAULT_WAIT_S = 600, DEFAULT_MEMOR
|
|
|
78263
78392
|
cwd: lease.cwd,
|
|
78264
78393
|
session: lease.session,
|
|
78265
78394
|
startedAt: lease.startedAt
|
|
78266
|
-
}),
|
|
78395
|
+
}), realClock, queueDir = (dir) => path52.join(dir, "queue"), TICKET, ticketName = (now, id) => `${String(now).padStart(15, "0")}-${String(process.hrtime.bigint() % 10n ** 16n).padStart(16, "0")}-${id}.json`, SCOPE_START_FAILURE, NOTICE_TTL_MS;
|
|
78267
78396
|
var init_resources = __esm(() => {
|
|
78268
78397
|
init_checks3();
|
|
78269
78398
|
init_heavy();
|
|
78270
78399
|
init_heavy_scope();
|
|
78271
|
-
DEFAULT_HEAVY_CHECKS = ["test", "build"
|
|
78400
|
+
DEFAULT_HEAVY_CHECKS = ["test", "build"];
|
|
78401
|
+
DEFAULT_LIGHT_CHECKS = ["lint", "typecheck", "format"];
|
|
78272
78402
|
SIZE = /^(\d+(?:\.\d+)?)([KMGT]?)$/iu;
|
|
78273
78403
|
PERCENT = /^(\d+(?:\.\d+)?)%$/u;
|
|
78274
78404
|
UNITS = { "": 1, K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4 };
|
|
78275
78405
|
SCOPE_UNIT = /^[\w.@-]+\.scope$/u;
|
|
78406
|
+
realClock = {
|
|
78407
|
+
now: () => Date.now(),
|
|
78408
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
78409
|
+
};
|
|
78410
|
+
TICKET = /^\d{15}-\d{16}-[\w-]+\.json$/u;
|
|
78276
78411
|
SCOPE_START_FAILURE = /^Failed to (?:connect to bus|create bus connection|start transient scope unit|parse \S+?=)/u;
|
|
78277
78412
|
NOTICE_TTL_MS = 7 * 86400000;
|
|
78278
78413
|
});
|
|
@@ -78283,7 +78418,6 @@ __export(exports_check, {
|
|
|
78283
78418
|
OUTPUT_LINES: () => OUTPUT_LINES,
|
|
78284
78419
|
boundedOutput: () => boundedOutput,
|
|
78285
78420
|
countWarnings: () => countWarnings,
|
|
78286
|
-
hostToolWaitCapMs: () => hostToolWaitCapMs,
|
|
78287
78421
|
run: () => run12,
|
|
78288
78422
|
workspaceResources: () => workspaceResources
|
|
78289
78423
|
});
|
|
@@ -78306,6 +78440,7 @@ function parse6(argv) {
|
|
|
78306
78440
|
positionals: [],
|
|
78307
78441
|
command: split >= 0 ? argv.slice(split + 1) : null
|
|
78308
78442
|
};
|
|
78443
|
+
let noWait = false;
|
|
78309
78444
|
for (let index = 0;index < head.length; index += 1) {
|
|
78310
78445
|
const arg = head[index];
|
|
78311
78446
|
const [flag, inline] = arg.startsWith("--") && arg.includes("=") ? arg.split(/=(.*)/su) : [arg];
|
|
@@ -78329,6 +78464,13 @@ function parse6(argv) {
|
|
|
78329
78464
|
options.noLease = true;
|
|
78330
78465
|
continue;
|
|
78331
78466
|
}
|
|
78467
|
+
if (flag === "--no-wait") {
|
|
78468
|
+
if (options.waitMs !== null && !noWait)
|
|
78469
|
+
return new Error("--no-wait goes without --wait");
|
|
78470
|
+
noWait = true;
|
|
78471
|
+
options.waitMs = 0;
|
|
78472
|
+
continue;
|
|
78473
|
+
}
|
|
78332
78474
|
if (flag === "--name" || flag === "--task" || flag === "--base" || flag === "--timeout" || flag === "--wait" || flag === "--reason") {
|
|
78333
78475
|
const got = value();
|
|
78334
78476
|
if (got instanceof Error)
|
|
@@ -78338,10 +78480,12 @@ function parse6(argv) {
|
|
|
78338
78480
|
else if (flag === "--reason")
|
|
78339
78481
|
options.reason = got.trim() || null;
|
|
78340
78482
|
else if (flag === "--wait") {
|
|
78341
|
-
|
|
78342
|
-
|
|
78343
|
-
|
|
78344
|
-
|
|
78483
|
+
if (noWait)
|
|
78484
|
+
return new Error("--no-wait goes without --wait");
|
|
78485
|
+
const waitMs = parseWait(got);
|
|
78486
|
+
if (waitMs === null)
|
|
78487
|
+
return new Error('--wait must be seconds or a duration ("90", "90s", "30m", "1h")');
|
|
78488
|
+
options.waitMs = waitMs;
|
|
78345
78489
|
} else if (flag === "--task")
|
|
78346
78490
|
options.task = got;
|
|
78347
78491
|
else if (flag === "--base")
|
|
@@ -78471,22 +78615,30 @@ function countWarnings(log) {
|
|
|
78471
78615
|
return redactLog(log).replace(ANSI_COLOR, "").split(`
|
|
78472
78616
|
`).filter((line) => WARNING_LINE.test(line) && !NO_WARNINGS.test(line)).length;
|
|
78473
78617
|
}
|
|
78474
|
-
function boundedOutput(log, lines) {
|
|
78618
|
+
function boundedOutput(log, lines, stderr = "") {
|
|
78475
78619
|
const all = tailLines(log, Number.MAX_SAFE_INTEGER);
|
|
78476
78620
|
if (all.length === 0)
|
|
78477
78621
|
return "";
|
|
78478
|
-
|
|
78622
|
+
let shown = all.slice(-lines);
|
|
78623
|
+
const errors = tailLines(redactLog(stderr), STDERR_TAIL_LINES);
|
|
78624
|
+
let missing = [];
|
|
78625
|
+
if (errors.length) {
|
|
78626
|
+
const tail = new Set(shown);
|
|
78627
|
+
if (errors.some((line) => !tail.has(line))) {
|
|
78628
|
+
shown = all.slice(-Math.max(1, lines - errors.length - 2));
|
|
78629
|
+
const kept = new Set(shown);
|
|
78630
|
+
missing = errors.filter((line) => !kept.has(line));
|
|
78631
|
+
}
|
|
78632
|
+
}
|
|
78479
78633
|
const omitted = all.length - shown.length;
|
|
78634
|
+
const footer = missing.length ? `… the last stderr lines, which the merged log holds earlier:
|
|
78635
|
+
${missing.join(`
|
|
78636
|
+
`)}
|
|
78637
|
+
` : "";
|
|
78480
78638
|
return `${omitted ? `… ${omitted} earlier line${omitted === 1 ? "" : "s"} omitted (the full log is named below; --full streams it)
|
|
78481
78639
|
` : ""}${shown.join(`
|
|
78482
78640
|
`)}
|
|
78483
|
-
`;
|
|
78484
|
-
}
|
|
78485
|
-
function hostToolWaitCapMs(env) {
|
|
78486
|
-
if (!env.CLAUDECODE && !env.OPENCODE && !env.OPENCODE_SESSION_ID)
|
|
78487
|
-
return null;
|
|
78488
|
-
const override = Number(env.WORKIT_HOST_WAIT_CAP_MS);
|
|
78489
|
-
return Number.isFinite(override) && override >= 0 && env.WORKIT_HOST_WAIT_CAP_MS ? override : HOST_TOOL_WAIT_CAP_MS;
|
|
78641
|
+
${footer}`;
|
|
78490
78642
|
}
|
|
78491
78643
|
async function workspaceResources(io) {
|
|
78492
78644
|
const env = io.env;
|
|
@@ -78520,14 +78672,26 @@ async function prepareHeavy(io, settings, label, runCwd, timeoutMs) {
|
|
|
78520
78672
|
waitMs: settings.waitMs,
|
|
78521
78673
|
pollMs: 1000,
|
|
78522
78674
|
maxAgeMs: (timeoutMs || MAX_UNVERIFIED_RUN_MS) + 60000,
|
|
78523
|
-
|
|
78524
|
-
|
|
78675
|
+
onStatus: (status) => {
|
|
78676
|
+
const slots = `${settings.slots} slot${settings.slots === 1 ? "" : "s"}`;
|
|
78677
|
+
const holders = status.holders.map((holder) => describeHolder(holder)).join("; ");
|
|
78678
|
+
io.stderr(status.waitedMs === 0 ? `workit: waiting for the heavy-check lease (${slots}): held by ${holders || "nobody; waiters ahead"}; queued at position ${status.position}, waiting up to ${formatWait(settings.waitMs)} (--wait <dur> sets it)
|
|
78679
|
+
` : `workit: still queued for the heavy-check lease: position ${status.position}, ${formatWait(status.waitedMs)} of ${formatWait(settings.waitMs)} waited; held by ${holders || "nobody; waiters ahead"}
|
|
78680
|
+
`);
|
|
78681
|
+
}
|
|
78525
78682
|
});
|
|
78526
78683
|
if (!acquired.ok && acquired.reason === "timeout") {
|
|
78527
78684
|
const holders = acquired.holders.map((holder) => describeHolder(holder)).join("; ");
|
|
78528
|
-
|
|
78529
|
-
|
|
78530
|
-
|
|
78685
|
+
const waited = formatWait(acquired.waitedMs);
|
|
78686
|
+
emit(io, fail2("busy", settings.waitMs === 0 ? `the heavy-check lease is busy (--no-wait): held by ${holders || "nobody; waiters ahead"}` : `the heavy-check lease stayed busy for ${waited} (position ${acquired.position} in the queue)`, {
|
|
78687
|
+
data: {
|
|
78688
|
+
lease: {
|
|
78689
|
+
waitedMs: acquired.waitedMs,
|
|
78690
|
+
holders: acquired.holders,
|
|
78691
|
+
position: acquired.position
|
|
78692
|
+
}
|
|
78693
|
+
},
|
|
78694
|
+
unblock: `wait for ${holders || "the waiters ahead"} to finish (each holder's age is shown; a full test suite takes minutes), then run it again, or queue longer with --wait 30m`
|
|
78531
78695
|
}));
|
|
78532
78696
|
return 4;
|
|
78533
78697
|
}
|
|
@@ -78627,11 +78791,8 @@ async function run12(argv, io) {
|
|
|
78627
78791
|
{ label: "workspaces.json", trust: "machine", value: workspace.value },
|
|
78628
78792
|
{ label: CHECKS_FILE, trust: "repo", value: config.resources }
|
|
78629
78793
|
]);
|
|
78630
|
-
const cap = hostToolWaitCapMs(io.env);
|
|
78631
78794
|
if (options.waitMs !== null)
|
|
78632
78795
|
settings.waitMs = options.waitMs;
|
|
78633
|
-
else if (cap !== null)
|
|
78634
|
-
settings.waitMs = Math.min(settings.waitMs, cap);
|
|
78635
78796
|
for (const warning of [workspace.warning, ...warnings])
|
|
78636
78797
|
if (warning)
|
|
78637
78798
|
io.stderr(`workit: resources: ${warning}
|
|
@@ -78648,6 +78809,7 @@ async function run12(argv, io) {
|
|
|
78648
78809
|
`);
|
|
78649
78810
|
const lease = heavy?.lease ?? null;
|
|
78650
78811
|
let result;
|
|
78812
|
+
let stderrTail = "";
|
|
78651
78813
|
try {
|
|
78652
78814
|
const scoped = await runScoped((wrapper, sinks) => runCheckCommand(command, {
|
|
78653
78815
|
cwd: runCwd,
|
|
@@ -78657,7 +78819,12 @@ async function run12(argv, io) {
|
|
|
78657
78819
|
...sinks,
|
|
78658
78820
|
...wrapper ? { wrapper } : {},
|
|
78659
78821
|
...lease ? { onSpawn: (pid) => updateLease(lease, { pgid: pid }) } : {}
|
|
78660
|
-
}), heavy?.wrapper ?? null, options.full ? { onStdout: io.json ? io.stderr : io.stdout, onStderr: io.stderr } : {
|
|
78822
|
+
}), heavy?.wrapper ?? null, options.full ? { onStdout: io.json ? io.stderr : io.stdout, onStderr: io.stderr } : {
|
|
78823
|
+
onStdout: () => {},
|
|
78824
|
+
onStderr: (text) => {
|
|
78825
|
+
stderrTail = (stderrTail + text).slice(-STDERR_KEEP_CHARS);
|
|
78826
|
+
}
|
|
78827
|
+
});
|
|
78661
78828
|
result = scoped.result;
|
|
78662
78829
|
if (heavy && scoped.fallback) {
|
|
78663
78830
|
io.stderr(`workit: the memory/CPU scope could not start (${scoped.fallback}); ran without it
|
|
@@ -78769,7 +78936,7 @@ async function run12(argv, io) {
|
|
|
78769
78936
|
`);
|
|
78770
78937
|
else {
|
|
78771
78938
|
if (!options.full)
|
|
78772
|
-
io.stdout(boundedOutput(result.log, OUTPUT_LINES));
|
|
78939
|
+
io.stdout(boundedOutput(result.log, OUTPUT_LINES, stderrTail));
|
|
78773
78940
|
io.stderr(`${summary}
|
|
78774
78941
|
`);
|
|
78775
78942
|
if (recordError && passed)
|
|
@@ -78786,7 +78953,7 @@ async function run12(argv, io) {
|
|
|
78786
78953
|
return recordError.code === "busy" ? 4 : recordError.code === "invalid_input" ? 2 : 5;
|
|
78787
78954
|
return 0;
|
|
78788
78955
|
}
|
|
78789
|
-
var USAGE6 = 'workit check <name> [--full] [--no-lease --reason "<why>"] | workit check [--name <n>] [--shell] [--heavy] [--full] [--wait <
|
|
78956
|
+
var USAGE6 = 'workit check <name> [--full] [--wait <dur> | --no-wait] [--no-lease --reason "<why>"] | workit check [--name <n>] [--shell] [--heavy] [--full] [--wait <dur> | --no-wait] [--timeout <s>] [--task <id>] [--json] -- <cmd…>', OUTPUT_LINES = 60, MAX_UNVERIFIED_RUN_MS, RECORD_TAIL_LINES = 20, RECORD_TAIL_CHARS = 200, posix = (value) => value.split(path53.sep).join("/") || ".", ENGINE_CODES2, FINGERPRINT_VARS, ANSI_COLOR, WARNING_LINE, NO_WARNINGS, STDERR_TAIL_LINES = 10, STDERR_KEEP_CHARS = 65536;
|
|
78790
78957
|
var init_check = __esm(() => {
|
|
78791
78958
|
init_check_config();
|
|
78792
78959
|
init_checks3();
|
|
@@ -78824,25 +78991,35 @@ async function settingsFor(io) {
|
|
|
78824
78991
|
{ label: CHECKS_FILE, trust: "repo", value: config.resources }
|
|
78825
78992
|
]).settings;
|
|
78826
78993
|
}
|
|
78827
|
-
function slotLines(slots) {
|
|
78994
|
+
function slotLines(slots, queue = []) {
|
|
78828
78995
|
const busy = held(slots).length;
|
|
78996
|
+
const waiting = queue.filter((entry) => !entry.stale);
|
|
78829
78997
|
return [
|
|
78830
78998
|
`heavy-check lease: ${busy} of ${slots.length} slot${slots.length === 1 ? "" : "s"} held`,
|
|
78831
|
-
...slots.map((slot) => slot.holder ? ` slot ${slot.slot}: ${describeHolder(slot.holder)}${slot.stale ? ` (stale: ${slot.stale}; the next heavy check reclaims it)` : ""}` : ` slot ${slot.slot}: free`)
|
|
78999
|
+
...slots.map((slot) => slot.holder ? ` slot ${slot.slot}: ${describeHolder(slot.holder)}${slot.stale ? ` (stale: ${slot.stale}; the next heavy check reclaims it)` : ""}` : ` slot ${slot.slot}: free`),
|
|
79000
|
+
`queue: ${waiting.length ? `${waiting.length} waiting, first come, first served` : "empty"}`,
|
|
79001
|
+
...waiting.map((entry, index) => ` ${index + 1}. ${describeHolder(entry.holder)}`)
|
|
78832
79002
|
];
|
|
78833
79003
|
}
|
|
78834
79004
|
async function status(io) {
|
|
78835
79005
|
const settings = await settingsFor(io);
|
|
78836
79006
|
const slots = leaseSlots(leaseDir(io.env), settings.slots);
|
|
78837
|
-
|
|
79007
|
+
const queue = leaseQueue(leaseDir(io.env));
|
|
79008
|
+
const waiting = queue.filter((entry) => !entry.stale).map((entry) => entry.holder);
|
|
79009
|
+
return emit(io, ok2({
|
|
79010
|
+
dir: leaseDir(io.env),
|
|
79011
|
+
slots,
|
|
79012
|
+
queue: waiting,
|
|
79013
|
+
waitMs: settings.waitMs,
|
|
79014
|
+
light: settings.light
|
|
79015
|
+
}), () => slotLines(slots, queue));
|
|
78838
79016
|
}
|
|
78839
79017
|
function timeoutOf(args) {
|
|
78840
79018
|
const at = args.findIndex((arg) => arg === "--timeout" || arg.startsWith("--timeout="));
|
|
78841
79019
|
if (at < 0)
|
|
78842
79020
|
return null;
|
|
78843
79021
|
const raw = args[at].includes("=") ? args[at].split("=")[1] : args[at + 1];
|
|
78844
|
-
|
|
78845
|
-
return Number.isFinite(seconds) && seconds >= 0 ? Math.round(seconds * 1000) : new Error("--timeout must be a number of seconds");
|
|
79022
|
+
return parseWait(raw ?? "") ?? new Error('--timeout must be seconds or a duration ("90", "90s", "30m", "1h")');
|
|
78846
79023
|
}
|
|
78847
79024
|
async function wait(args, io) {
|
|
78848
79025
|
const timeout = timeoutOf(args);
|
|
@@ -78859,10 +79036,10 @@ async function wait(args, io) {
|
|
|
78859
79036
|
const waitedMs = Date.now() - started;
|
|
78860
79037
|
if (busy.length < slots.length)
|
|
78861
79038
|
return emit(io, ok2({ free: true, waitedMs, slots }), () => [
|
|
78862
|
-
`workit: the heavy-check lease has a free slot${waitedMs ? ` after ${
|
|
79039
|
+
`workit: the heavy-check lease has a free slot${waitedMs ? ` after ${formatWait(waitedMs)}` : ""}`
|
|
78863
79040
|
]);
|
|
78864
79041
|
if (waitedMs >= limit)
|
|
78865
|
-
return emit(io, fail2("busy", `the heavy-check lease stayed busy for ${
|
|
79042
|
+
return emit(io, fail2("busy", `the heavy-check lease stayed busy for ${formatWait(waitedMs)}`, {
|
|
78866
79043
|
data: { free: false, waitedMs, slots },
|
|
78867
79044
|
unblock: "workit lock status"
|
|
78868
79045
|
}));
|
|
@@ -78871,16 +79048,16 @@ async function wait(args, io) {
|
|
|
78871
79048
|
io.stderr(`workit: waiting for the heavy-check lease: held by ${busy.flatMap((slot) => slot.holder ? [describeHolder(slot.holder)] : []).join("; ")}
|
|
78872
79049
|
`);
|
|
78873
79050
|
}
|
|
78874
|
-
await
|
|
79051
|
+
await sleep(Math.min(1000, Math.max(1, limit - waitedMs)));
|
|
78875
79052
|
}
|
|
78876
79053
|
}
|
|
78877
79054
|
async function runHeavy(args, io) {
|
|
78878
79055
|
const split = args.indexOf("--");
|
|
78879
79056
|
const head = split < 0 ? args : args.slice(0, split);
|
|
78880
|
-
const unknown = head.find((arg, index) => arg !== "--shell" && arg !== "--json" && !arg.startsWith("--timeout") && !arg.startsWith("--wait") && !((head[index - 1] === "--timeout" || head[index - 1] === "--wait") && !arg.startsWith("-")));
|
|
79057
|
+
const unknown = head.find((arg, index) => arg !== "--shell" && arg !== "--json" && arg !== "--no-wait" && !arg.startsWith("--timeout") && !arg.startsWith("--wait") && !((head[index - 1] === "--timeout" || head[index - 1] === "--wait") && !arg.startsWith("-")));
|
|
78881
79058
|
if (split < 0 || unknown)
|
|
78882
79059
|
return emit(io, fail2("invalid_input", unknown ? `unexpected ${unknown}` : "no command after --", {
|
|
78883
|
-
unblock: "workit lock run [--shell] [--wait <
|
|
79060
|
+
unblock: "workit lock run [--shell] [--wait <dur> | --no-wait] [--timeout <s>] -- <cmd…>"
|
|
78884
79061
|
}));
|
|
78885
79062
|
const check = await Promise.resolve().then(() => (init_check(), {}));
|
|
78886
79063
|
return run12([`--heavy`, ...head, ...args.slice(split)], io);
|
|
@@ -78966,7 +79143,7 @@ async function run13(argv, io) {
|
|
|
78966
79143
|
return heavy(rest, io);
|
|
78967
79144
|
return emit(io, fail2("invalid_input", `unknown lock subcommand ${sub ?? "(none)"}`, { unblock: USAGE7 }));
|
|
78968
79145
|
}
|
|
78969
|
-
var USAGE7 = "workit lock status | wait [--timeout <
|
|
79146
|
+
var USAGE7 = "workit lock status | wait [--timeout <dur>] | run [--shell] [--wait <dur> | --no-wait] [--timeout <s>] -- <cmd…> | heavy list|add|remove|test", held = (slots) => slots.filter((slot) => slot.holder && !slot.stale), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), commandText = (args) => {
|
|
78970
79147
|
const split = args.indexOf("--");
|
|
78971
79148
|
return (split < 0 ? args : args.slice(split + 1)).join(" ").trim();
|
|
78972
79149
|
};
|
|
@@ -82301,6 +82478,74 @@ function syncStack(cwd, ctx, stack, options, actor) {
|
|
|
82301
82478
|
}
|
|
82302
82479
|
return { ok: true, data: outcome };
|
|
82303
82480
|
}
|
|
82481
|
+
function pruneMergedBranches(cwd, ctx, stack, options) {
|
|
82482
|
+
const outcome = { deleted: [], kept: [] };
|
|
82483
|
+
const remote = remoteFor(cwd, ctx?.resolved ?? null, stack.trunk);
|
|
82484
|
+
const trunkSha = trunkTip(cwd, remote, stack.trunk);
|
|
82485
|
+
const keep = (branch, reason) => outcome.kept.push({ branch, reason });
|
|
82486
|
+
for (const entry of stack.branches) {
|
|
82487
|
+
if (!entry.merged || !safeRef3(entry.branch) || entry.branch === stack.trunk)
|
|
82488
|
+
continue;
|
|
82489
|
+
const tip = branchTip(cwd, entry.branch);
|
|
82490
|
+
if (!tip)
|
|
82491
|
+
continue;
|
|
82492
|
+
if (isProtectedTarget(cwd, entry.branch)) {
|
|
82493
|
+
keep(entry.branch, "a protected branch under the workspace branch policy; never deleted");
|
|
82494
|
+
continue;
|
|
82495
|
+
}
|
|
82496
|
+
if (entry.lastHead === null || tip !== entry.lastHead) {
|
|
82497
|
+
keep(entry.branch, `its tip ${tip.slice(0, 12)} is not recorded by the stack (last ${entry.lastHead?.slice(0, 12) ?? "none"}); commits made after it merged stay`);
|
|
82498
|
+
continue;
|
|
82499
|
+
}
|
|
82500
|
+
const owner = worktreeOf(cwd, entry.branch);
|
|
82501
|
+
if (owner) {
|
|
82502
|
+
keep(entry.branch, `checked out in ${owner}; switch it away, then git branch -D ${entry.branch}`);
|
|
82503
|
+
continue;
|
|
82504
|
+
}
|
|
82505
|
+
const safe = safeToDelete(cwd, ctx, remote, trunkSha, entry, tip);
|
|
82506
|
+
if (safe === null) {
|
|
82507
|
+
keep(entry.branch, `${tip.slice(0, 12)} is not on ${stack.trunk}, not the head its PR merged at and not on ${remote}/${entry.branch}: deleting it could lose commits (push them, or delete it yourself)`);
|
|
82508
|
+
continue;
|
|
82509
|
+
}
|
|
82510
|
+
if (!options.dryRun) {
|
|
82511
|
+
const removed = gitRun3(cwd, ["branch", "-D", "--", entry.branch]);
|
|
82512
|
+
if (!removed.ok) {
|
|
82513
|
+
keep(entry.branch, `git branch -D failed: ${removed.stderr.trim().split(`
|
|
82514
|
+
`)[0] ?? ""}`);
|
|
82515
|
+
continue;
|
|
82516
|
+
}
|
|
82517
|
+
appendObserved(cwd, {
|
|
82518
|
+
type: "stack.branch.deleted",
|
|
82519
|
+
actor: options.actor,
|
|
82520
|
+
stack: stack.name,
|
|
82521
|
+
branch: entry.branch,
|
|
82522
|
+
head: tip,
|
|
82523
|
+
pr: entry.pr,
|
|
82524
|
+
because: safe
|
|
82525
|
+
});
|
|
82526
|
+
}
|
|
82527
|
+
outcome.deleted.push({ branch: entry.branch, sha: tip });
|
|
82528
|
+
}
|
|
82529
|
+
return outcome;
|
|
82530
|
+
}
|
|
82531
|
+
function safeToDelete(cwd, ctx, remote, trunkSha, entry, tip) {
|
|
82532
|
+
if (trunkSha && isAncestor2(cwd, tip, trunkSha))
|
|
82533
|
+
return "on_trunk";
|
|
82534
|
+
if (ctx && entry.pr) {
|
|
82535
|
+
const read = statusDoc(ctx, entry.pr);
|
|
82536
|
+
if (read.ok && read.data.state === "merged" && read.data.head.sha === tip)
|
|
82537
|
+
return "merged_head";
|
|
82538
|
+
}
|
|
82539
|
+
const own = pushRemoteName(cwd, entry.branch) ?? remote;
|
|
82540
|
+
const remoteTip = remoteRefTip(cwd, own, `refs/heads/${entry.branch}`);
|
|
82541
|
+
if (remoteTip.ok && remoteTip.sha) {
|
|
82542
|
+
if (!hasCommit(cwd, remoteTip.sha))
|
|
82543
|
+
fetchRefs(cwd, own, [remoteTip.sha]);
|
|
82544
|
+
if (hasCommit(cwd, remoteTip.sha) && isAncestor2(cwd, tip, remoteTip.sha))
|
|
82545
|
+
return "on_remote";
|
|
82546
|
+
}
|
|
82547
|
+
return null;
|
|
82548
|
+
}
|
|
82304
82549
|
async function awaitChecks(ctx, pr, head, options) {
|
|
82305
82550
|
let waited = 0;
|
|
82306
82551
|
for (let poll = 0;; poll += 1) {
|
|
@@ -82541,6 +82786,7 @@ ${run.stdout}`.split(`
|
|
|
82541
82786
|
}, commitCount = (cwd, base, head) => Number(gitRun3(cwd, ["rev-list", "--count", "--no-merges", `${base}..${head}`]).stdout.trim()) || 0, shortStat = (cwd, base, head) => gitRun3(cwd, ["diff", "--shortstat", `${base}...${head}`]).stdout.trim();
|
|
82542
82787
|
var init_stack = __esm(() => {
|
|
82543
82788
|
init_autonomy();
|
|
82789
|
+
init_branch();
|
|
82544
82790
|
init_store_lock();
|
|
82545
82791
|
init_pr_ops();
|
|
82546
82792
|
init_report2();
|
|
@@ -84991,8 +85237,213 @@ var init_pr_edit = __esm(() => {
|
|
|
84991
85237
|
init_report2();
|
|
84992
85238
|
});
|
|
84993
85239
|
|
|
84994
|
-
// packages/workit-core/src/
|
|
85240
|
+
// packages/workit-core/src/fanout-landed.ts
|
|
84995
85241
|
import { spawnSync as spawnSync26 } from "node:child_process";
|
|
85242
|
+
function forgeBreaker(resolved) {
|
|
85243
|
+
if (!resolved)
|
|
85244
|
+
return { forge: null, down: () => null };
|
|
85245
|
+
let tripped = null;
|
|
85246
|
+
const guard = (call) => (...args) => {
|
|
85247
|
+
if (tripped)
|
|
85248
|
+
return { ok: false, code: "unavailable", error: tripped };
|
|
85249
|
+
const result = call(...args);
|
|
85250
|
+
if (!result.ok && result.code === "unavailable")
|
|
85251
|
+
tripped = result.error;
|
|
85252
|
+
return result;
|
|
85253
|
+
};
|
|
85254
|
+
const inner = resolved.forge;
|
|
85255
|
+
const forge = Object.create(inner);
|
|
85256
|
+
forge.findPr = guard(inner.findPr.bind(inner));
|
|
85257
|
+
forge.prStatus = guard(inner.prStatus.bind(inner));
|
|
85258
|
+
return { forge: { ...resolved, forge }, down: () => tripped };
|
|
85259
|
+
}
|
|
85260
|
+
function branchCreatedAt(cwd, branch) {
|
|
85261
|
+
if (!branch || branch.startsWith("-"))
|
|
85262
|
+
return null;
|
|
85263
|
+
const log = gitRun4(cwd, ["log", "-g", "--format=%H", `refs/heads/${branch}`, "--"]);
|
|
85264
|
+
if (!log.ok)
|
|
85265
|
+
return null;
|
|
85266
|
+
return log.stdout.trim().split(`
|
|
85267
|
+
`).findLast(Boolean) ?? null;
|
|
85268
|
+
}
|
|
85269
|
+
function hasOwnCommits2(cwd, branch, start, head) {
|
|
85270
|
+
if (!branch || branch.startsWith("-"))
|
|
85271
|
+
return false;
|
|
85272
|
+
const log = gitRun4(cwd, ["log", "-g", "--format=%H%x00%gs", `refs/heads/${branch}`, "--"]);
|
|
85273
|
+
if (!log.ok)
|
|
85274
|
+
return false;
|
|
85275
|
+
for (const line of log.stdout.split(`
|
|
85276
|
+
`)) {
|
|
85277
|
+
const [sha, subject = ""] = line.split("\x00");
|
|
85278
|
+
if (!sha)
|
|
85279
|
+
continue;
|
|
85280
|
+
const made = /^(?:commit(?: \((?:initial|amend)\))?|cherry-pick|rebase(?: -i)? \(pick\)):/u.test(subject);
|
|
85281
|
+
const onto = /^rebase(?: -i)? \(finish\): \S+ onto ([0-9a-f]+)/u.exec(subject)?.[1];
|
|
85282
|
+
if (!made && !(onto && !sha.startsWith(onto) && !onto.startsWith(sha)))
|
|
85283
|
+
continue;
|
|
85284
|
+
if (sha !== head && !isAncestor3(cwd, sha, head))
|
|
85285
|
+
continue;
|
|
85286
|
+
if (start && (sha === start || isAncestor3(cwd, sha, start)))
|
|
85287
|
+
continue;
|
|
85288
|
+
return true;
|
|
85289
|
+
}
|
|
85290
|
+
return false;
|
|
85291
|
+
}
|
|
85292
|
+
function reverted(cwd, trunk, landedHead, start) {
|
|
85293
|
+
const fork = mergeBase(cwd, landedHead, trunk);
|
|
85294
|
+
if (fork && fork !== landedHead)
|
|
85295
|
+
return unchangedSince(cwd, fork, landedHead, trunk);
|
|
85296
|
+
const before = start && start !== landedHead && isAncestor3(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
|
|
85297
|
+
return before ? unchangedSince(cwd, before, landedHead, trunk) : false;
|
|
85298
|
+
}
|
|
85299
|
+
function unchangedSince(cwd, before, after, trunk) {
|
|
85300
|
+
const paths = gitRun4(cwd, ["diff", "--name-only", "--no-renames", "-z", before, after]);
|
|
85301
|
+
const files = paths.ok ? paths.stdout.split("\x00").filter(Boolean) : [];
|
|
85302
|
+
if (files.length === 0)
|
|
85303
|
+
return false;
|
|
85304
|
+
return gitRun4(cwd, ["diff", "--quiet", before, trunk, "--", ...files]).ok;
|
|
85305
|
+
}
|
|
85306
|
+
function trunkPatchIds(ctx, fork) {
|
|
85307
|
+
const cache = ctx.cache ??= new Map;
|
|
85308
|
+
const known = cache.get(fork);
|
|
85309
|
+
if (known)
|
|
85310
|
+
return known;
|
|
85311
|
+
const ids = new Set;
|
|
85312
|
+
const log = gitRun4(ctx.cwd, [
|
|
85313
|
+
"log",
|
|
85314
|
+
"--no-merges",
|
|
85315
|
+
`--max-count=${TRUNK_SCAN}`,
|
|
85316
|
+
"-p",
|
|
85317
|
+
"--format=commit %H",
|
|
85318
|
+
"-U3",
|
|
85319
|
+
"--diff-algorithm=myers",
|
|
85320
|
+
"--indent-heuristic",
|
|
85321
|
+
"--no-color",
|
|
85322
|
+
"--no-ext-diff",
|
|
85323
|
+
"--no-textconv",
|
|
85324
|
+
"--no-renames",
|
|
85325
|
+
"--full-index",
|
|
85326
|
+
"--src-prefix=a/",
|
|
85327
|
+
"--dst-prefix=b/",
|
|
85328
|
+
`${fork}..${ctx.trunk}`,
|
|
85329
|
+
"--"
|
|
85330
|
+
]);
|
|
85331
|
+
if (log.ok && log.stdout.trim()) {
|
|
85332
|
+
const run = spawnSync26("git", ["patch-id", "--stable"], {
|
|
85333
|
+
cwd: ctx.cwd,
|
|
85334
|
+
input: log.stdout,
|
|
85335
|
+
encoding: "utf8",
|
|
85336
|
+
timeout: GIT_TIMEOUTS.local,
|
|
85337
|
+
windowsHide: true,
|
|
85338
|
+
maxBuffer: 64 * 1024 * 1024
|
|
85339
|
+
});
|
|
85340
|
+
for (const line of (run.stdout ?? "").split(`
|
|
85341
|
+
`)) {
|
|
85342
|
+
const id = line.trim().split(/\s+/u)[0];
|
|
85343
|
+
if (id)
|
|
85344
|
+
ids.add(id);
|
|
85345
|
+
}
|
|
85346
|
+
}
|
|
85347
|
+
cache.set(fork, ids);
|
|
85348
|
+
return ids;
|
|
85349
|
+
}
|
|
85350
|
+
function inSliceRun(row, slice) {
|
|
85351
|
+
if (row.type === "fanout.worktree.created" && typeof row.sliceHash === "string" && slice.hash)
|
|
85352
|
+
return row.sliceHash === slice.hash;
|
|
85353
|
+
const since = Date.parse(slice.hashSince);
|
|
85354
|
+
return Number.isNaN(since) || Date.parse(String(row.at)) >= since;
|
|
85355
|
+
}
|
|
85356
|
+
function linked(rows, input, slice, head) {
|
|
85357
|
+
const since = input.since ? Date.parse(input.since) : Number.NaN;
|
|
85358
|
+
const recent = (row) => Number.isNaN(since) || Date.parse(String(row.at)) >= since;
|
|
85359
|
+
return rows.some((row) => {
|
|
85360
|
+
if (row.type === "fanout.worktree.created" && row.fanout === input.fanout && row.slice === slice.id)
|
|
85361
|
+
return typeof row.sliceHash === "string" && input.sliceHash ? row.sliceHash === input.sliceHash : recent(row);
|
|
85362
|
+
return recent(row) && head !== null && row.branch === slice.branch && row.head === head;
|
|
85363
|
+
});
|
|
85364
|
+
}
|
|
85365
|
+
function detectLanding(ctx, slice, input) {
|
|
85366
|
+
const out = {
|
|
85367
|
+
landed: false,
|
|
85368
|
+
how: null,
|
|
85369
|
+
pr: null,
|
|
85370
|
+
landedHead: null,
|
|
85371
|
+
forgeError: null,
|
|
85372
|
+
note: null
|
|
85373
|
+
};
|
|
85374
|
+
const landed = (how, landedHead) => {
|
|
85375
|
+
if (!hasCommit(ctx.cwd, landedHead) || !reverted(ctx.cwd, ctx.trunk, landedHead, input.createdFrom))
|
|
85376
|
+
return { ...out, landed: true, how, landedHead };
|
|
85377
|
+
return {
|
|
85378
|
+
...out,
|
|
85379
|
+
note: `landed (${how}${out.pr ? ` #${out.pr.number}` : ""}), then reverted on the trunk: its paths read as before it`
|
|
85380
|
+
};
|
|
85381
|
+
};
|
|
85382
|
+
const { head } = input;
|
|
85383
|
+
if (ctx.forge) {
|
|
85384
|
+
const found = ctx.forge.forge.findPr(slice.branch, {
|
|
85385
|
+
owner: ctx.forge.headRepo.split("/")[0] ?? null,
|
|
85386
|
+
projectId: ctx.forge.headProjectId,
|
|
85387
|
+
sha: head
|
|
85388
|
+
});
|
|
85389
|
+
if (!found.ok)
|
|
85390
|
+
out.forgeError = found.error;
|
|
85391
|
+
else if (found.data) {
|
|
85392
|
+
out.pr = found.data;
|
|
85393
|
+
if (found.data.state === "merged") {
|
|
85394
|
+
const merged = found.data.headSha;
|
|
85395
|
+
const label = `#${found.data.number}`;
|
|
85396
|
+
if (!head) {
|
|
85397
|
+
if (!linked(input.rows, input, slice, merged)) {
|
|
85398
|
+
out.pr = null;
|
|
85399
|
+
out.note = `${slice.branch} is gone; merged ${label} is not linked to this slice in the ledger (an older branch of the same name?)`;
|
|
85400
|
+
return out;
|
|
85401
|
+
}
|
|
85402
|
+
return merged ? landed("pr_merged", merged) : { ...out, landed: true, how: "pr_merged" };
|
|
85403
|
+
}
|
|
85404
|
+
if (!merged || head === merged || isAncestor3(ctx.cwd, head, merged))
|
|
85405
|
+
return landed("pr_merged", merged ?? head);
|
|
85406
|
+
out.note = isAncestor3(ctx.cwd, merged, head) ? `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} is ahead of it with newer commits` : `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} has diverged from it (rewritten after the merge)`;
|
|
85407
|
+
return out;
|
|
85408
|
+
}
|
|
85409
|
+
}
|
|
85410
|
+
}
|
|
85411
|
+
if (!head)
|
|
85412
|
+
return out;
|
|
85413
|
+
const fork = mergeBase(ctx.cwd, head, ctx.trunk);
|
|
85414
|
+
if (fork === head) {
|
|
85415
|
+
return hasOwnCommits2(ctx.cwd, slice.branch, input.createdFrom, head) ? landed("on_trunk", head) : out;
|
|
85416
|
+
}
|
|
85417
|
+
if (!fork)
|
|
85418
|
+
return out;
|
|
85419
|
+
const trunkIds = trunkPatchIds(ctx, fork);
|
|
85420
|
+
if (trunkIds.size === 0)
|
|
85421
|
+
return out;
|
|
85422
|
+
const froms = [fork];
|
|
85423
|
+
if (input.parentHead && input.parentHead !== head && isAncestor3(ctx.cwd, input.parentHead, head))
|
|
85424
|
+
froms.push(input.parentHead);
|
|
85425
|
+
for (const from of froms) {
|
|
85426
|
+
const id = patchId(ctx.cwd, from, head);
|
|
85427
|
+
if (id && trunkIds.has(id))
|
|
85428
|
+
return landed("squash_patch", head);
|
|
85429
|
+
}
|
|
85430
|
+
return out;
|
|
85431
|
+
}
|
|
85432
|
+
function sliceStart(cwd, rows, fanout, slice) {
|
|
85433
|
+
const reflog = branchCreatedAt(cwd, slice.branch);
|
|
85434
|
+
if (reflog)
|
|
85435
|
+
return reflog;
|
|
85436
|
+
const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string" && inSliceRun(candidate, slice));
|
|
85437
|
+
return row?.baseSha ?? null;
|
|
85438
|
+
}
|
|
85439
|
+
var isAncestor3 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
|
|
85440
|
+
var init_fanout_landed = __esm(() => {
|
|
85441
|
+
init_rev();
|
|
85442
|
+
init_fanout();
|
|
85443
|
+
});
|
|
85444
|
+
|
|
85445
|
+
// packages/workit-core/src/core/live-session.ts
|
|
85446
|
+
import { spawnSync as spawnSync27 } from "node:child_process";
|
|
84996
85447
|
var LIVE_SESSION_TTL_MS, liveProbe, PIDS;
|
|
84997
85448
|
var init_live_session = __esm(() => {
|
|
84998
85449
|
init_paths();
|
|
@@ -85001,7 +85452,7 @@ var init_live_session = __esm(() => {
|
|
|
85001
85452
|
LIVE_SESSION_TTL_MS = 15 * 60000;
|
|
85002
85453
|
liveProbe = {
|
|
85003
85454
|
platform: process.platform,
|
|
85004
|
-
run: (args) =>
|
|
85455
|
+
run: (args) => spawnSync27("ps", args, { encoding: "utf8", timeout: 2000 }).stdout ?? "",
|
|
85005
85456
|
start: (pid) => processStartOf(pid)
|
|
85006
85457
|
};
|
|
85007
85458
|
PIDS = new Map;
|
|
@@ -85294,8 +85745,65 @@ function worktreeDependencies(main, worktree) {
|
|
|
85294
85745
|
]
|
|
85295
85746
|
};
|
|
85296
85747
|
}
|
|
85748
|
+
const stale = staleBunInstall(main);
|
|
85749
|
+
if (stale && !fs32.existsSync(path64.join(worktree, "node_modules"))) {
|
|
85750
|
+
const command = `bun install --frozen-lockfile --cwd ${worktree}`;
|
|
85751
|
+
return {
|
|
85752
|
+
linked: null,
|
|
85753
|
+
install: command,
|
|
85754
|
+
notes: [
|
|
85755
|
+
`the main checkout's node_modules (${main}) do not match its bun.lock (${stale}), so they were not linked; install dependencies first: ${command}`
|
|
85756
|
+
]
|
|
85757
|
+
};
|
|
85758
|
+
}
|
|
85297
85759
|
return { ...linkWorktreeDependencies(main, worktree), install: null };
|
|
85298
85760
|
}
|
|
85761
|
+
function staleBunInstall(main) {
|
|
85762
|
+
if (!fs32.existsSync(path64.join(main, "node_modules")))
|
|
85763
|
+
return null;
|
|
85764
|
+
const text = readBytes(path64.join(main, "bun.lock"))?.toString("utf8");
|
|
85765
|
+
if (!text)
|
|
85766
|
+
return null;
|
|
85767
|
+
let packages;
|
|
85768
|
+
try {
|
|
85769
|
+
const parsed = JSON.parse(text.replace(/,(\s*[}\]])/gu, "$1"));
|
|
85770
|
+
if (!parsed.packages || typeof parsed.packages !== "object")
|
|
85771
|
+
return null;
|
|
85772
|
+
packages = parsed.packages;
|
|
85773
|
+
} catch {
|
|
85774
|
+
return null;
|
|
85775
|
+
}
|
|
85776
|
+
const problems = [];
|
|
85777
|
+
let scanned = 0;
|
|
85778
|
+
for (const [key, entry] of Object.entries(packages)) {
|
|
85779
|
+
if (scanned++ >= STALE_SCAN || problems.length >= 3)
|
|
85780
|
+
break;
|
|
85781
|
+
const spec = Array.isArray(entry) && typeof entry[0] === "string" ? entry[0] : null;
|
|
85782
|
+
if (!spec)
|
|
85783
|
+
continue;
|
|
85784
|
+
const at = spec.lastIndexOf("@");
|
|
85785
|
+
if (at <= 0)
|
|
85786
|
+
continue;
|
|
85787
|
+
const name = spec.slice(0, at);
|
|
85788
|
+
const version = spec.slice(at + 1);
|
|
85789
|
+
if (name !== key || version.includes(":"))
|
|
85790
|
+
continue;
|
|
85791
|
+
const manifest = readBytes(path64.join(main, "node_modules", key, "package.json"));
|
|
85792
|
+
if (!manifest) {
|
|
85793
|
+
problems.push(`${key} missing`);
|
|
85794
|
+
continue;
|
|
85795
|
+
}
|
|
85796
|
+
let installed;
|
|
85797
|
+
try {
|
|
85798
|
+
installed = JSON.parse(manifest.toString("utf8")).version;
|
|
85799
|
+
} catch {
|
|
85800
|
+
installed = null;
|
|
85801
|
+
}
|
|
85802
|
+
if (installed !== version)
|
|
85803
|
+
problems.push(`${key} ${String(installed ?? "?")} installed, bun.lock has ${version}`);
|
|
85804
|
+
}
|
|
85805
|
+
return problems.length ? problems.join("; ") : null;
|
|
85806
|
+
}
|
|
85299
85807
|
function ensureScratch(worktree) {
|
|
85300
85808
|
const scratch = path64.join(worktree, SCRATCH_DIR);
|
|
85301
85809
|
fs32.mkdirSync(scratch, { recursive: true });
|
|
@@ -85372,14 +85880,19 @@ function createSliceWorktree(cwd, plan, input) {
|
|
|
85372
85880
|
notes.push(`resumed ${slice.branch} from ${remote}/${slice.branch}`);
|
|
85373
85881
|
} else {
|
|
85374
85882
|
mode = "new";
|
|
85375
|
-
const
|
|
85376
|
-
|
|
85883
|
+
const landedOn = dependenciesLanded(cwd, plan, slice, input.forge);
|
|
85884
|
+
if (landedOn) {
|
|
85885
|
+
base.base = plan.trunk;
|
|
85886
|
+
notes.push(landedOn);
|
|
85887
|
+
}
|
|
85888
|
+
const parent = landedOn ? undefined : plan.slices.find((other) => other.branch === slice.base);
|
|
85889
|
+
const from = base.base === plan.trunk ? trunkRef(cwd, plan.trunk) : parent ? branchRef(cwd, parent.branch) : branchRef(cwd, slice.base);
|
|
85377
85890
|
if (!from)
|
|
85378
85891
|
return fanoutFail("not_found", `base ${slice.base} of slice ${slice.id} does not resolve here`, parent ? `start slice ${parent.id} first (its branch ${parent.branch} is the base)` : `git fetch origin ${slice.base}`);
|
|
85379
85892
|
const added = gitRun4(cwd, ["worktree", "add", "--detach", target, from], SLOW);
|
|
85380
85893
|
if (!added.ok)
|
|
85381
85894
|
return worktreeFailed(added.stderr);
|
|
85382
|
-
const branched = gitBranch(target, { name: slice.branch, base:
|
|
85895
|
+
const branched = gitBranch(target, { name: slice.branch, base: base.base });
|
|
85383
85896
|
if (!branched.ok) {
|
|
85384
85897
|
gitRun4(cwd, ["worktree", "remove", target], SLOW);
|
|
85385
85898
|
return fanoutFail(branched.code, `workit git branch ${slice.branch} refused: ${branched.error}`, branched.unblock ?? `fix the branch name in the plan, then workit fanout worktree create ${slice.id}`);
|
|
@@ -85409,6 +85922,7 @@ function createSliceWorktree(cwd, plan, input) {
|
|
|
85409
85922
|
sliceHash: slice.hash,
|
|
85410
85923
|
...admin ? { adminId: admin.id, adminIno: admin.ino, adminToken: admin.token } : {},
|
|
85411
85924
|
...baseSha ? { baseSha } : {},
|
|
85925
|
+
...base.base === slice.base ? {} : { startBase: base.base },
|
|
85412
85926
|
...deps.install ? { install: deps.install } : {}
|
|
85413
85927
|
});
|
|
85414
85928
|
return {
|
|
@@ -85425,6 +85939,35 @@ function createSliceWorktree(cwd, plan, input) {
|
|
|
85425
85939
|
}
|
|
85426
85940
|
};
|
|
85427
85941
|
}
|
|
85942
|
+
function dependenciesLanded(cwd, plan, slice, forge) {
|
|
85943
|
+
if (slice.base === plan.trunk || slice.dependsOn.length === 0)
|
|
85944
|
+
return null;
|
|
85945
|
+
const deps = slice.dependsOn.map((id) => plan.slices.find((other) => other.id === id));
|
|
85946
|
+
if (deps.some((dep) => !dep))
|
|
85947
|
+
return null;
|
|
85948
|
+
const trunk = trunkRef(cwd, plan.trunk);
|
|
85949
|
+
if (!trunk)
|
|
85950
|
+
return null;
|
|
85951
|
+
const ledger = readLedger(cwd);
|
|
85952
|
+
const rows = ledger.ok ? ledger.value.rows : [];
|
|
85953
|
+
const ctx = { cwd, trunk, forge: forgeBreaker(forge?.() ?? null).forge };
|
|
85954
|
+
const landed = [];
|
|
85955
|
+
for (const dep of deps) {
|
|
85956
|
+
const landing = detectLanding(ctx, dep, {
|
|
85957
|
+
head: branchRef(cwd, dep.branch),
|
|
85958
|
+
createdFrom: sliceStart(cwd, rows, plan.name, dep),
|
|
85959
|
+
parentHead: null,
|
|
85960
|
+
fanout: plan.name,
|
|
85961
|
+
since: dep.hashSince || plan.createdAt || null,
|
|
85962
|
+
sliceHash: dep.hash || null,
|
|
85963
|
+
rows
|
|
85964
|
+
});
|
|
85965
|
+
if (!landing.landed)
|
|
85966
|
+
return null;
|
|
85967
|
+
landed.push(`${dep.branch} landed (${landing.how ?? "pr_merged"}${landing.pr ? ` #${landing.pr.number}` : ""})`);
|
|
85968
|
+
}
|
|
85969
|
+
return `${landed.join(", ")}: started from ${plan.trunk}, not from ${slice.base}'s pre-merge tip`;
|
|
85970
|
+
}
|
|
85428
85971
|
function releaseSliceWorktree(cwd, plan, input) {
|
|
85429
85972
|
const found = findSlice(plan, input.slice);
|
|
85430
85973
|
if (!found.ok)
|
|
@@ -85567,12 +86110,13 @@ var SLOW, SCRATCH_DIR = ".workit-scratch", EXCLUDE_LINE, DIRTY_ROW_BYTES = 2000,
|
|
|
85567
86110
|
} catch {
|
|
85568
86111
|
return null;
|
|
85569
86112
|
}
|
|
85570
|
-
}, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
|
|
86113
|
+
}, STALE_SCAN = 5000, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
|
|
85571
86114
|
`)[0] || "unknown error"}`);
|
|
85572
86115
|
var init_fanout_worktree = __esm(() => {
|
|
85573
86116
|
init_ops();
|
|
85574
86117
|
init_rev();
|
|
85575
86118
|
init_ledger();
|
|
86119
|
+
init_fanout_landed();
|
|
85576
86120
|
init_worktree_confine();
|
|
85577
86121
|
init_worktree_deps();
|
|
85578
86122
|
init_fanout();
|
|
@@ -87205,7 +87749,7 @@ var init_git2 = __esm(() => {
|
|
|
87205
87749
|
});
|
|
87206
87750
|
|
|
87207
87751
|
// packages/workit-core/src/forge/verify.ts
|
|
87208
|
-
import { spawnSync as
|
|
87752
|
+
import { spawnSync as spawnSync28 } from "node:child_process";
|
|
87209
87753
|
function finish(cwd, report, actor) {
|
|
87210
87754
|
const delivered = report.observations.length > 0 && report.observations.every((entry) => entry.ok);
|
|
87211
87755
|
const out = { ...report, delivered };
|
|
@@ -87418,7 +87962,7 @@ var NPM_NAME, SEMVER2, SAFE_ARG, systemNpm = (args) => {
|
|
|
87418
87962
|
};
|
|
87419
87963
|
const found = process.platform === "win32" ? findExecutable("npm.cmd", process.env) : null;
|
|
87420
87964
|
const npmCmd = found && !/["%!^]/u.test(found) ? `"${found}"` : "npm.cmd";
|
|
87421
|
-
const result = process.platform === "win32" ?
|
|
87965
|
+
const result = process.platform === "win32" ? spawnSync28(`${npmCmd} ${args.join(" ")}`, { ...options, shell: true }) : spawnSync28("npm", [...args], options);
|
|
87422
87966
|
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
87423
87967
|
}, short3 = (sha) => sha ? sha.slice(0, 12) : "(absent)", npmView = (stdout) => {
|
|
87424
87968
|
try {
|
|
@@ -87643,7 +88187,7 @@ function planReleaseFinish(cwd, input) {
|
|
|
87643
88187
|
return failure3("not_found", `${name} exists neither locally nor on ${remote ?? "a remote"}`);
|
|
87644
88188
|
if (!tracked || name === openRelease)
|
|
87645
88189
|
continue;
|
|
87646
|
-
if (!
|
|
88190
|
+
if (!isAncestor4(cwd, tracked, local))
|
|
87647
88191
|
return failure3("blocked", `branch_behind: local ${name} is behind or diverged from ${remote}/${name}; git-flow finishes only onto up-to-date branches`, `git switch ${name} && git merge --ff-only ${remote}/${name} # then: workit release finish ${version}`);
|
|
87648
88192
|
const productionTracked = remote ? resolveRef(cwd, `refs/remotes/${remote}/${line.production}`) : null;
|
|
87649
88193
|
const foreign = git5(cwd, [
|
|
@@ -87653,16 +88197,16 @@ function planReleaseFinish(cwd, input) {
|
|
|
87653
88197
|
`^${releaseTip}`,
|
|
87654
88198
|
...productionTracked ? [`^${productionTracked}`] : []
|
|
87655
88199
|
]).stdout.split(`
|
|
87656
|
-
`).filter(Boolean).filter((sha) => !(git5(cwd, ["rev-parse", "-q", "--verify", `${sha}^2`]).ok &&
|
|
88200
|
+
`).filter(Boolean).filter((sha) => !(git5(cwd, ["rev-parse", "-q", "--verify", `${sha}^2`]).ok && isAncestor4(cwd, releaseTip, sha)));
|
|
87657
88201
|
if (foreign.length)
|
|
87658
88202
|
return failure3("blocked", `branch_ahead: local ${name} has ${foreign.length} commit(s) ${remote}/${name} lacks that are not part of ${branch}; the finish push would publish them`, `land them through review first, or: git switch ${name} && git reset --keep ${remote}/${name} # then: workit release finish ${version}`);
|
|
87659
88203
|
}
|
|
87660
|
-
if (branchTip && remoteBranch && !
|
|
88204
|
+
if (branchTip && remoteBranch && !isAncestor4(cwd, remoteBranch, branchTip))
|
|
87661
88205
|
return failure3("blocked", `release_behind: ${remote}/${branch} has commits local ${branch} lacks`, `git switch ${branch} && git merge --ff-only ${remote}/${branch}`);
|
|
87662
88206
|
const production = resolveRef(cwd, `refs/heads/${line.production}`);
|
|
87663
|
-
const merged =
|
|
88207
|
+
const merged = isAncestor4(cwd, releaseTip, production);
|
|
87664
88208
|
const localTag = resolveRef(cwd, `refs/tags/${tag}`);
|
|
87665
|
-
if (localTag && !(
|
|
88209
|
+
if (localTag && !(isAncestor4(cwd, localTag, production) && isAncestor4(cwd, releaseTip, localTag)))
|
|
87666
88210
|
return failure3("blocked", `tag_exists: ${tag} (${localTag.slice(0, 12)}) is not this release's merge on ${line.production}`, "a release tag never moves; finish under the next version");
|
|
87667
88211
|
if (remote) {
|
|
87668
88212
|
const remoteTag = remoteRefTip(cwd, remote, `refs/tags/${tag}`);
|
|
@@ -87676,7 +88220,7 @@ function planReleaseFinish(cwd, input) {
|
|
|
87676
88220
|
tagged: localTag !== null,
|
|
87677
88221
|
mergedBack: Object.fromEntries(mergeBack.map((name) => [
|
|
87678
88222
|
name,
|
|
87679
|
-
localTag !== null &&
|
|
88223
|
+
localTag !== null && isAncestor4(cwd, localTag, resolveRef(cwd, `refs/heads/${name}`))
|
|
87680
88224
|
])),
|
|
87681
88225
|
branchDeleted: branchTip === null
|
|
87682
88226
|
};
|
|
@@ -87838,7 +88382,7 @@ var VERSION, PLACEHOLDER4, validVersion = (version) => VERSION.test(version) &&
|
|
|
87838
88382
|
return ledger.value.rows.filter((row) => row.type.startsWith("release."));
|
|
87839
88383
|
}, record2 = (cwd, actor, row) => {
|
|
87840
88384
|
appendObserved(cwd, { ...row, actor });
|
|
87841
|
-
}, KINDS,
|
|
88385
|
+
}, KINDS, isAncestor4 = (cwd, ancestor, descendant) => git5(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok, switchTo = (cwd, branch) => {
|
|
87842
88386
|
if (currentBranch(cwd) === branch)
|
|
87843
88387
|
return success2(undefined);
|
|
87844
88388
|
const switched = git5(cwd, ["switch", "-q", branch]);
|
|
@@ -88304,24 +88848,25 @@ async function status3(argv, io) {
|
|
|
88304
88848
|
...data.next ? [`next: ${data.next}`] : []
|
|
88305
88849
|
]);
|
|
88306
88850
|
}
|
|
88307
|
-
async function sync(argv, io) {
|
|
88851
|
+
async function sync(argv, io, restack = false) {
|
|
88852
|
+
const usageText = restack ? RESTACK_USAGE : SYNC_USAGE;
|
|
88308
88853
|
const flags = parseFlags(argv, {
|
|
88309
88854
|
name: "value",
|
|
88310
|
-
local: "boolean",
|
|
88855
|
+
...restack ? {} : { local: "boolean" },
|
|
88311
88856
|
"dry-run": "boolean",
|
|
88312
88857
|
force: "list"
|
|
88313
88858
|
});
|
|
88314
88859
|
if (typeof flags === "string")
|
|
88315
|
-
return usage4(io, flags,
|
|
88860
|
+
return usage4(io, flags, usageText);
|
|
88316
88861
|
if (flags.positionals.length)
|
|
88317
|
-
return usage4(io, `unexpected argument ${flags.positionals[0]}`,
|
|
88862
|
+
return usage4(io, `unexpected argument ${flags.positionals[0]}`, usageText);
|
|
88318
88863
|
const selected = selectStack(io.cwd, flags.values.name ?? null);
|
|
88319
88864
|
if (!selected.ok)
|
|
88320
88865
|
return stackFailed(io, selected);
|
|
88321
88866
|
const force = new Set(flags.lists.force ?? []);
|
|
88322
88867
|
const unknown = [...force].filter((branch) => !selected.data.branches.some((entry) => entry.branch === branch));
|
|
88323
88868
|
if (unknown.length)
|
|
88324
|
-
return usage4(io, `--force ${unknown[0]}: not a branch of stack ${selected.data.name}`,
|
|
88869
|
+
return usage4(io, `--force ${unknown[0]}: not a branch of stack ${selected.data.name}`, usageText);
|
|
88325
88870
|
const local = flags.booleans.has("local");
|
|
88326
88871
|
let ctx = null;
|
|
88327
88872
|
if (!local) {
|
|
@@ -88340,14 +88885,23 @@ async function sync(argv, io) {
|
|
|
88340
88885
|
const fresh = readStack(io.cwd, name);
|
|
88341
88886
|
if (!fresh.ok)
|
|
88342
88887
|
return fresh;
|
|
88343
|
-
|
|
88344
|
-
|
|
88345
|
-
|
|
88346
|
-
|
|
88347
|
-
|
|
88888
|
+
const stack = fresh.data ?? selected.data;
|
|
88889
|
+
const dryRun = flags.booleans.has("dry-run");
|
|
88890
|
+
const synced = syncStack(io.cwd, ctx, stack, { publish: !local, dryRun, force }, actorFromEnv(io.env));
|
|
88891
|
+
if (!synced.ok || !restack)
|
|
88892
|
+
return synced;
|
|
88893
|
+
return {
|
|
88894
|
+
ok: true,
|
|
88895
|
+
data: {
|
|
88896
|
+
...synced.data,
|
|
88897
|
+
pruned: pruneMergedBranches(io.cwd, ctx, stack, { dryRun, actor: actorFromEnv(io.env) })
|
|
88898
|
+
}
|
|
88899
|
+
};
|
|
88348
88900
|
});
|
|
88349
88901
|
if (!result.ok)
|
|
88350
88902
|
return stackFailed(io, result);
|
|
88903
|
+
if (restack)
|
|
88904
|
+
return emit(io, ok2(result.data), renderRestack);
|
|
88351
88905
|
return emit(io, ok2(result.data), renderSync);
|
|
88352
88906
|
}
|
|
88353
88907
|
async function land(argv, io) {
|
|
@@ -88434,11 +88988,13 @@ async function run20(argv, io) {
|
|
|
88434
88988
|
return status3(rest, io);
|
|
88435
88989
|
if (sub === "sync")
|
|
88436
88990
|
return sync(rest, io);
|
|
88991
|
+
if (sub === "restack")
|
|
88992
|
+
return sync(rest, io, true);
|
|
88437
88993
|
if (sub === "land")
|
|
88438
88994
|
return land(rest, io);
|
|
88439
88995
|
return usage4(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE13);
|
|
88440
88996
|
}
|
|
88441
|
-
var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--unverified --reason <why>] [--base-red-ok --reason <why>] [--timeout 20m] [--interval 30s] [--json]", USAGE13 = "workit stack plan|status|sync|land ... (workit help stack)", short5 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail2(result.code, result.error, {
|
|
88997
|
+
var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", RESTACK_USAGE = "workit stack restack [--name <n>] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--unverified --reason <why>] [--base-red-ok --reason <why>] [--timeout 20m] [--interval 30s] [--json]", USAGE13 = "workit stack plan|status|sync|restack|land ... (workit help stack)", short5 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail2(result.code, result.error, {
|
|
88442
88998
|
unblock: result.unblock,
|
|
88443
88999
|
data: result.data ?? {}
|
|
88444
89000
|
})), label3 = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
|
|
@@ -88449,6 +89005,10 @@ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<b
|
|
|
88449
89005
|
step.push ? step.push.pushed ? `pushed${step.push.forced ? " (lease)" : ""}` : "remote current" : "",
|
|
88450
89006
|
step.retarget ? `retargeted ${step.retarget.from} -> ${step.retarget.to}` : ""
|
|
88451
89007
|
].filter(Boolean).join(" "))
|
|
89008
|
+
], renderRestack = (data) => [
|
|
89009
|
+
...renderSync(data),
|
|
89010
|
+
...data.pruned.deleted.map((item) => `${data.dryRun ? "would delete" : "deleted"} merged branch ${item.branch} at ${item.sha} (local; restore: git branch ${item.branch} ${item.sha})`),
|
|
89011
|
+
...data.pruned.kept.map((item) => `kept merged branch ${item.branch}: ${item.reason}`)
|
|
88452
89012
|
], METHODS3;
|
|
88453
89013
|
var init_stack2 = __esm(() => {
|
|
88454
89014
|
init_ledger();
|
|
@@ -88488,6 +89048,7 @@ function renderBrief(cwd, plan, input) {
|
|
|
88488
89048
|
const planned = `${slicePath(cwd, slice)}${input.attempt === null ? "" : `+try${input.attempt}`}`;
|
|
88489
89049
|
const made = ledger.value.rows.findLast((row) => row.type === "fanout.worktree.created" && row.fanout === plan.name && row.slice === slice.id && typeof row.path === "string" && samePath(row.path, planned));
|
|
88490
89050
|
const fresh = Boolean(head) && made?.baseSha === head;
|
|
89051
|
+
const sliceBase = typeof made?.startBase === "string" ? made.startBase : slice.base;
|
|
88491
89052
|
const mode = input.mode ?? (head && !fresh ? "resume" : "new");
|
|
88492
89053
|
if (mode === "resume" && !head)
|
|
88493
89054
|
return fanoutFail("not_found", `MODE resume needs the branch ${branch}, which does not exist here`, `workit fanout brief ${slice.id} --mode new`);
|
|
@@ -88516,13 +89077,13 @@ function renderBrief(cwd, plan, input) {
|
|
|
88516
89077
|
`GOAL: ${slice.goal}`,
|
|
88517
89078
|
`SCOPE: ${slice.scope.join(", ")}`,
|
|
88518
89079
|
...slice.owns.length ? [` owns: ${slice.owns.join(", ")}`] : [],
|
|
88519
|
-
` branch: ${branch} base: ${
|
|
89080
|
+
` branch: ${branch} base: ${sliceBase}`,
|
|
88520
89081
|
`CONTEXT: ${slice.context ?? "none given: read the files in SCOPE and the neighbours they import"}`,
|
|
88521
89082
|
...slice.issues.length ? [
|
|
88522
89083
|
`ISSUE: ${slice.issues.join(", ")} (\`workit pr create\` on ${branch} links ${slice.issues.length === 1 ? "it" : "them"} by itself; no --issue needed)`
|
|
88523
89084
|
] : [],
|
|
88524
89085
|
...mode === "resume" && head ? [
|
|
88525
|
-
` resume: continue ${branch} from ${head.slice(0, 12)}; read \`git log ${
|
|
89086
|
+
` resume: continue ${branch} from ${head.slice(0, 12)}; read \`git log ${sliceBase}..${branch}\` and \`workit ledger list --branch ${branch}\` (the last report, verdicts, rulings)`
|
|
88526
89087
|
] : [],
|
|
88527
89088
|
"ACCEPTANCE:",
|
|
88528
89089
|
...slice.acceptance.map((line) => ` - ${line}`),
|
|
@@ -88545,7 +89106,7 @@ function renderBrief(cwd, plan, input) {
|
|
|
88545
89106
|
exists ? `WORKTREE: ${planned} (made by \`workit fanout worktree create ${slice.id}\`; yours alone)` : `WORKTREE: ${planned} (not made yet: the lead runs \`workit fanout worktree create ${slice.id}\` before spawning, then renders this brief again)`,
|
|
88546
89107
|
...exists && typeof made?.install === "string" && !fs35.existsSync(path68.join(planned, "node_modules")) ? [`INSTALL: \`${made.install}\` first: the main checkout's node_modules was not linked here`] : [],
|
|
88547
89108
|
...identityLines(identity, minted),
|
|
88548
|
-
...rules(mode, branch,
|
|
89109
|
+
...rules(mode, branch, sliceBase, exists ? planned : null)
|
|
88549
89110
|
];
|
|
88550
89111
|
return {
|
|
88551
89112
|
ok: true,
|
|
@@ -88555,7 +89116,7 @@ function renderBrief(cwd, plan, input) {
|
|
|
88555
89116
|
mode,
|
|
88556
89117
|
attempt: input.attempt,
|
|
88557
89118
|
branch,
|
|
88558
|
-
base:
|
|
89119
|
+
base: sliceBase,
|
|
88559
89120
|
tier: slice.tier,
|
|
88560
89121
|
issues: slice.issues,
|
|
88561
89122
|
model: model?.model ?? null,
|
|
@@ -88620,211 +89181,6 @@ var init_fanout_brief = __esm(() => {
|
|
|
88620
89181
|
];
|
|
88621
89182
|
});
|
|
88622
89183
|
|
|
88623
|
-
// packages/workit-core/src/fanout-landed.ts
|
|
88624
|
-
import { spawnSync as spawnSync28 } from "node:child_process";
|
|
88625
|
-
function forgeBreaker(resolved) {
|
|
88626
|
-
if (!resolved)
|
|
88627
|
-
return { forge: null, down: () => null };
|
|
88628
|
-
let tripped = null;
|
|
88629
|
-
const guard = (call) => (...args) => {
|
|
88630
|
-
if (tripped)
|
|
88631
|
-
return { ok: false, code: "unavailable", error: tripped };
|
|
88632
|
-
const result = call(...args);
|
|
88633
|
-
if (!result.ok && result.code === "unavailable")
|
|
88634
|
-
tripped = result.error;
|
|
88635
|
-
return result;
|
|
88636
|
-
};
|
|
88637
|
-
const inner = resolved.forge;
|
|
88638
|
-
const forge = Object.create(inner);
|
|
88639
|
-
forge.findPr = guard(inner.findPr.bind(inner));
|
|
88640
|
-
forge.prStatus = guard(inner.prStatus.bind(inner));
|
|
88641
|
-
return { forge: { ...resolved, forge }, down: () => tripped };
|
|
88642
|
-
}
|
|
88643
|
-
function branchCreatedAt(cwd, branch) {
|
|
88644
|
-
if (!branch || branch.startsWith("-"))
|
|
88645
|
-
return null;
|
|
88646
|
-
const log = gitRun4(cwd, ["log", "-g", "--format=%H", `refs/heads/${branch}`, "--"]);
|
|
88647
|
-
if (!log.ok)
|
|
88648
|
-
return null;
|
|
88649
|
-
return log.stdout.trim().split(`
|
|
88650
|
-
`).findLast(Boolean) ?? null;
|
|
88651
|
-
}
|
|
88652
|
-
function hasOwnCommits2(cwd, branch, start, head) {
|
|
88653
|
-
if (!branch || branch.startsWith("-"))
|
|
88654
|
-
return false;
|
|
88655
|
-
const log = gitRun4(cwd, ["log", "-g", "--format=%H%x00%gs", `refs/heads/${branch}`, "--"]);
|
|
88656
|
-
if (!log.ok)
|
|
88657
|
-
return false;
|
|
88658
|
-
for (const line of log.stdout.split(`
|
|
88659
|
-
`)) {
|
|
88660
|
-
const [sha, subject = ""] = line.split("\x00");
|
|
88661
|
-
if (!sha)
|
|
88662
|
-
continue;
|
|
88663
|
-
const made = /^(?:commit(?: \((?:initial|amend)\))?|cherry-pick|rebase(?: -i)? \(pick\)):/u.test(subject);
|
|
88664
|
-
const onto = /^rebase(?: -i)? \(finish\): \S+ onto ([0-9a-f]+)/u.exec(subject)?.[1];
|
|
88665
|
-
if (!made && !(onto && !sha.startsWith(onto) && !onto.startsWith(sha)))
|
|
88666
|
-
continue;
|
|
88667
|
-
if (sha !== head && !isAncestor4(cwd, sha, head))
|
|
88668
|
-
continue;
|
|
88669
|
-
if (start && (sha === start || isAncestor4(cwd, sha, start)))
|
|
88670
|
-
continue;
|
|
88671
|
-
return true;
|
|
88672
|
-
}
|
|
88673
|
-
return false;
|
|
88674
|
-
}
|
|
88675
|
-
function reverted(cwd, trunk, landedHead, start) {
|
|
88676
|
-
const fork = mergeBase(cwd, landedHead, trunk);
|
|
88677
|
-
if (fork && fork !== landedHead)
|
|
88678
|
-
return unchangedSince(cwd, fork, landedHead, trunk);
|
|
88679
|
-
const before = start && start !== landedHead && isAncestor4(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
|
|
88680
|
-
return before ? unchangedSince(cwd, before, landedHead, trunk) : false;
|
|
88681
|
-
}
|
|
88682
|
-
function unchangedSince(cwd, before, after, trunk) {
|
|
88683
|
-
const paths = gitRun4(cwd, ["diff", "--name-only", "--no-renames", "-z", before, after]);
|
|
88684
|
-
const files = paths.ok ? paths.stdout.split("\x00").filter(Boolean) : [];
|
|
88685
|
-
if (files.length === 0)
|
|
88686
|
-
return false;
|
|
88687
|
-
return gitRun4(cwd, ["diff", "--quiet", before, trunk, "--", ...files]).ok;
|
|
88688
|
-
}
|
|
88689
|
-
function trunkPatchIds(ctx, fork) {
|
|
88690
|
-
const cache = ctx.cache ??= new Map;
|
|
88691
|
-
const known = cache.get(fork);
|
|
88692
|
-
if (known)
|
|
88693
|
-
return known;
|
|
88694
|
-
const ids = new Set;
|
|
88695
|
-
const log = gitRun4(ctx.cwd, [
|
|
88696
|
-
"log",
|
|
88697
|
-
"--no-merges",
|
|
88698
|
-
`--max-count=${TRUNK_SCAN}`,
|
|
88699
|
-
"-p",
|
|
88700
|
-
"--format=commit %H",
|
|
88701
|
-
"-U3",
|
|
88702
|
-
"--diff-algorithm=myers",
|
|
88703
|
-
"--indent-heuristic",
|
|
88704
|
-
"--no-color",
|
|
88705
|
-
"--no-ext-diff",
|
|
88706
|
-
"--no-textconv",
|
|
88707
|
-
"--no-renames",
|
|
88708
|
-
"--full-index",
|
|
88709
|
-
"--src-prefix=a/",
|
|
88710
|
-
"--dst-prefix=b/",
|
|
88711
|
-
`${fork}..${ctx.trunk}`,
|
|
88712
|
-
"--"
|
|
88713
|
-
]);
|
|
88714
|
-
if (log.ok && log.stdout.trim()) {
|
|
88715
|
-
const run = spawnSync28("git", ["patch-id", "--stable"], {
|
|
88716
|
-
cwd: ctx.cwd,
|
|
88717
|
-
input: log.stdout,
|
|
88718
|
-
encoding: "utf8",
|
|
88719
|
-
timeout: GIT_TIMEOUTS.local,
|
|
88720
|
-
windowsHide: true,
|
|
88721
|
-
maxBuffer: 64 * 1024 * 1024
|
|
88722
|
-
});
|
|
88723
|
-
for (const line of (run.stdout ?? "").split(`
|
|
88724
|
-
`)) {
|
|
88725
|
-
const id = line.trim().split(/\s+/u)[0];
|
|
88726
|
-
if (id)
|
|
88727
|
-
ids.add(id);
|
|
88728
|
-
}
|
|
88729
|
-
}
|
|
88730
|
-
cache.set(fork, ids);
|
|
88731
|
-
return ids;
|
|
88732
|
-
}
|
|
88733
|
-
function inSliceRun(row, slice) {
|
|
88734
|
-
if (row.type === "fanout.worktree.created" && typeof row.sliceHash === "string" && slice.hash)
|
|
88735
|
-
return row.sliceHash === slice.hash;
|
|
88736
|
-
const since = Date.parse(slice.hashSince);
|
|
88737
|
-
return Number.isNaN(since) || Date.parse(String(row.at)) >= since;
|
|
88738
|
-
}
|
|
88739
|
-
function linked(rows, input, slice, head) {
|
|
88740
|
-
const since = input.since ? Date.parse(input.since) : Number.NaN;
|
|
88741
|
-
const recent = (row) => Number.isNaN(since) || Date.parse(String(row.at)) >= since;
|
|
88742
|
-
return rows.some((row) => {
|
|
88743
|
-
if (row.type === "fanout.worktree.created" && row.fanout === input.fanout && row.slice === slice.id)
|
|
88744
|
-
return typeof row.sliceHash === "string" && input.sliceHash ? row.sliceHash === input.sliceHash : recent(row);
|
|
88745
|
-
return recent(row) && head !== null && row.branch === slice.branch && row.head === head;
|
|
88746
|
-
});
|
|
88747
|
-
}
|
|
88748
|
-
function detectLanding(ctx, slice, input) {
|
|
88749
|
-
const out = {
|
|
88750
|
-
landed: false,
|
|
88751
|
-
how: null,
|
|
88752
|
-
pr: null,
|
|
88753
|
-
landedHead: null,
|
|
88754
|
-
forgeError: null,
|
|
88755
|
-
note: null
|
|
88756
|
-
};
|
|
88757
|
-
const landed = (how, landedHead) => {
|
|
88758
|
-
if (!hasCommit(ctx.cwd, landedHead) || !reverted(ctx.cwd, ctx.trunk, landedHead, input.createdFrom))
|
|
88759
|
-
return { ...out, landed: true, how, landedHead };
|
|
88760
|
-
return {
|
|
88761
|
-
...out,
|
|
88762
|
-
note: `landed (${how}${out.pr ? ` #${out.pr.number}` : ""}), then reverted on the trunk: its paths read as before it`
|
|
88763
|
-
};
|
|
88764
|
-
};
|
|
88765
|
-
const { head } = input;
|
|
88766
|
-
if (ctx.forge) {
|
|
88767
|
-
const found = ctx.forge.forge.findPr(slice.branch, {
|
|
88768
|
-
owner: ctx.forge.headRepo.split("/")[0] ?? null,
|
|
88769
|
-
projectId: ctx.forge.headProjectId,
|
|
88770
|
-
sha: head
|
|
88771
|
-
});
|
|
88772
|
-
if (!found.ok)
|
|
88773
|
-
out.forgeError = found.error;
|
|
88774
|
-
else if (found.data) {
|
|
88775
|
-
out.pr = found.data;
|
|
88776
|
-
if (found.data.state === "merged") {
|
|
88777
|
-
const merged = found.data.headSha;
|
|
88778
|
-
const label = `#${found.data.number}`;
|
|
88779
|
-
if (!head) {
|
|
88780
|
-
if (!linked(input.rows, input, slice, merged)) {
|
|
88781
|
-
out.pr = null;
|
|
88782
|
-
out.note = `${slice.branch} is gone; merged ${label} is not linked to this slice in the ledger (an older branch of the same name?)`;
|
|
88783
|
-
return out;
|
|
88784
|
-
}
|
|
88785
|
-
return merged ? landed("pr_merged", merged) : { ...out, landed: true, how: "pr_merged" };
|
|
88786
|
-
}
|
|
88787
|
-
if (!merged || head === merged || isAncestor4(ctx.cwd, head, merged))
|
|
88788
|
-
return landed("pr_merged", merged ?? head);
|
|
88789
|
-
out.note = isAncestor4(ctx.cwd, merged, head) ? `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} is ahead of it with newer commits` : `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} has diverged from it (rewritten after the merge)`;
|
|
88790
|
-
return out;
|
|
88791
|
-
}
|
|
88792
|
-
}
|
|
88793
|
-
}
|
|
88794
|
-
if (!head)
|
|
88795
|
-
return out;
|
|
88796
|
-
const fork = mergeBase(ctx.cwd, head, ctx.trunk);
|
|
88797
|
-
if (fork === head) {
|
|
88798
|
-
return hasOwnCommits2(ctx.cwd, slice.branch, input.createdFrom, head) ? landed("on_trunk", head) : out;
|
|
88799
|
-
}
|
|
88800
|
-
if (!fork)
|
|
88801
|
-
return out;
|
|
88802
|
-
const trunkIds = trunkPatchIds(ctx, fork);
|
|
88803
|
-
if (trunkIds.size === 0)
|
|
88804
|
-
return out;
|
|
88805
|
-
const froms = [fork];
|
|
88806
|
-
if (input.parentHead && input.parentHead !== head && isAncestor4(ctx.cwd, input.parentHead, head))
|
|
88807
|
-
froms.push(input.parentHead);
|
|
88808
|
-
for (const from of froms) {
|
|
88809
|
-
const id = patchId(ctx.cwd, from, head);
|
|
88810
|
-
if (id && trunkIds.has(id))
|
|
88811
|
-
return landed("squash_patch", head);
|
|
88812
|
-
}
|
|
88813
|
-
return out;
|
|
88814
|
-
}
|
|
88815
|
-
function sliceStart(cwd, rows, fanout, slice) {
|
|
88816
|
-
const reflog = branchCreatedAt(cwd, slice.branch);
|
|
88817
|
-
if (reflog)
|
|
88818
|
-
return reflog;
|
|
88819
|
-
const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string" && inSliceRun(candidate, slice));
|
|
88820
|
-
return row?.baseSha ?? null;
|
|
88821
|
-
}
|
|
88822
|
-
var isAncestor4 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
|
|
88823
|
-
var init_fanout_landed = __esm(() => {
|
|
88824
|
-
init_rev();
|
|
88825
|
-
init_fanout();
|
|
88826
|
-
});
|
|
88827
|
-
|
|
88828
89184
|
// packages/workit-core/src/fanout-check.ts
|
|
88829
89185
|
function mergeConflicts(cwd, a, b) {
|
|
88830
89186
|
const run = gitRun4(cwd, [
|
|
@@ -89007,7 +89363,9 @@ function inspect(cwd, plan, slice, ctx, changedBy) {
|
|
|
89007
89363
|
return check;
|
|
89008
89364
|
}
|
|
89009
89365
|
const parent = plan.slices.find((other) => other.branch === slice.base);
|
|
89010
|
-
const
|
|
89366
|
+
const parentLanded = parent ? ctx.landings.get(parent.id)?.landed === true : false;
|
|
89367
|
+
const parentRef = parent ? tips.get(parent.id) ?? ctx.landings.get(parent.id)?.landedHead ?? null : null;
|
|
89368
|
+
const baseRef = slice.base === plan.trunk ? trunk : parent ? parentLanded && !(parentRef && isAncestor5(cwd, parentRef, head)) ? trunk : parentRef : branchRef(cwd, slice.base);
|
|
89011
89369
|
if (!SAFE(baseRef)) {
|
|
89012
89370
|
check.reasons.push(`base ${slice.base} not found`);
|
|
89013
89371
|
return check;
|
|
@@ -89475,7 +89833,8 @@ function worktree(argv, io) {
|
|
|
89475
89833
|
if (action === "create") {
|
|
89476
89834
|
const result = createSliceWorktree(io.cwd, selected.data, {
|
|
89477
89835
|
slice: flags.positionals[0],
|
|
89478
|
-
actor
|
|
89836
|
+
actor,
|
|
89837
|
+
forge: () => forgeFor(io, false).forge
|
|
89479
89838
|
});
|
|
89480
89839
|
if (!result.ok)
|
|
89481
89840
|
return failed(io, result);
|
|
@@ -107041,9 +107400,10 @@ function withSubcommands(entry, options = {}) {
|
|
|
107041
107400
|
};
|
|
107042
107401
|
}
|
|
107043
107402
|
var LEASE_NOTES = [
|
|
107044
|
-
"The heavy-check lease: heavy work (tests, builds,
|
|
107045
|
-
"
|
|
107046
|
-
"
|
|
107403
|
+
"The heavy-check lease: heavy work (tests, builds, installs, image builds) shares one machine-wide lease, so parallel agents and worktrees never exhaust this machine's memory and CPU. `slots` (default 1) heavy checks run at once; on Linux each also runs in a systemd scope with MemoryMax and CPUQuota.",
|
|
107404
|
+
"Queueing: a heavy check that finds every slot held joins a first-come, first-served queue and prints `waiting for the heavy-check lease` naming the holder (check, pid, session, age, directory) and its position, then a status line every 30 s. It waits `--wait <dur>` (90, 90s, 30m, 1h), else `resources.wait` (default 30 min), and exits 4 (busy) only past that wait or at once with --no-wait. A waiter that is killed gives up its place. A wait longer than your shell tool's timeout belongs in a background call. `workit lock status` shows the holders and the queue.",
|
|
107405
|
+
"Light checks: the named checks lint, typecheck and format (`resources.light`, set in workspaces.json) never take the lease, so they run at once beside a long suite.",
|
|
107406
|
+
"--heavy: an ad-hoc `workit check -- <cmd>` takes the lease only with --heavy; the named checks test and build always do. The shell hook runs a raw heavy command (`npx jest`, `npm run test:ci`, `cargo test`, `docker build`) as `workit check --heavy -- <cmd>` on Claude Code, OpenCode and Pi, and denies it with that exact command on Codex and Cursor.",
|
|
107047
107407
|
'--no-lease skips the lease and needs --reason "<why>". Never hand-roll a flock: the lease already queues every agent on the machine.',
|
|
107048
107408
|
"Output: a check prints the last 60 lines of its output and one summary line with `log: <path>` to the full log; --full streams all of it."
|
|
107049
107409
|
];
|
|
@@ -107160,7 +107520,7 @@ var VERBS = [
|
|
|
107160
107520
|
{
|
|
107161
107521
|
name: "check",
|
|
107162
107522
|
group: "delivery",
|
|
107163
|
-
usage: 'workit check <name> [--full] [--task <id>] [--base <ref>] [--no-lease --reason "<why>"] | workit check [--name <n>] [--shell] [--full] [--timeout <s>] [--task <id>] [--base <ref>] [--heavy [--wait <
|
|
107523
|
+
usage: 'workit check <name> [--full] [--task <id>] [--base <ref>] [--wait <dur> | --no-wait] [--no-lease --reason "<why>"] | workit check [--name <n>] [--shell] [--full] [--timeout <s>] [--task <id>] [--base <ref>] [--heavy [--wait <dur> | --no-wait]] [--no-lease --reason "<why>"] -- <cmd…>',
|
|
107164
107524
|
flagHints: {
|
|
107165
107525
|
list: "workit check <name> runs a configured check; a name that is not configured prints the valid ones (workit check test|lint|typecheck|check)"
|
|
107166
107526
|
},
|
|
@@ -107168,6 +107528,7 @@ var VERBS = [
|
|
|
107168
107528
|
examples: [
|
|
107169
107529
|
"workit check test",
|
|
107170
107530
|
"workit check --heavy -- npx jest",
|
|
107531
|
+
"workit check test --wait 45m",
|
|
107171
107532
|
"workit check --name unit -- bun test test/unit",
|
|
107172
107533
|
'workit check test --no-lease --reason "machine is idle, debugging a hang"'
|
|
107173
107534
|
],
|
|
@@ -107182,16 +107543,16 @@ var VERBS = [
|
|
|
107182
107543
|
{
|
|
107183
107544
|
name: "status",
|
|
107184
107545
|
usage: "workit lock status",
|
|
107185
|
-
summary: "Show each lease slot
|
|
107546
|
+
summary: "Show each lease slot (free, held by check, pid, session, age, directory, or stale) and the queue of waiting checks in arrival order"
|
|
107186
107547
|
},
|
|
107187
107548
|
{
|
|
107188
107549
|
name: "wait",
|
|
107189
|
-
usage: "workit lock wait [--timeout <
|
|
107550
|
+
usage: "workit lock wait [--timeout <dur>]",
|
|
107190
107551
|
summary: "Wait until a lease slot is free, without taking it (exit 0 free, 4 still busy at the timeout)"
|
|
107191
107552
|
},
|
|
107192
107553
|
{
|
|
107193
107554
|
name: "run",
|
|
107194
|
-
usage: "workit lock run [--shell] [--wait <
|
|
107555
|
+
usage: "workit lock run [--shell] [--wait <dur> | --no-wait] [--timeout <s>] -- <cmd…>",
|
|
107195
107556
|
summary: "Run a command under the lease: the same as workit check --heavy -- <cmd…>"
|
|
107196
107557
|
},
|
|
107197
107558
|
{
|
|
@@ -107408,6 +107769,11 @@ var VERBS = [
|
|
|
107408
107769
|
usage: "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]…",
|
|
107409
107770
|
summary: "Restack onto the moved bases, lease-push and retarget the PRs"
|
|
107410
107771
|
},
|
|
107772
|
+
{
|
|
107773
|
+
name: "restack",
|
|
107774
|
+
usage: "workit stack restack [--name <n>] [--dry-run] [--force <branch>]…",
|
|
107775
|
+
summary: "After a parent merged (squash too): sync, then delete the merged stack branches locally"
|
|
107776
|
+
},
|
|
107411
107777
|
{
|
|
107412
107778
|
name: "land",
|
|
107413
107779
|
usage: "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--unverified --reason <why>] [--base-red-ok --reason <why>] [--timeout 20m] [--interval 30s]",
|