@brainervirus/workit-cli 8.38.5 → 8.39.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +446 -246
- 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.1",
|
|
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.1",
|
|
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.1"
|
|
9365
9365
|
},
|
|
9366
9366
|
engines: {
|
|
9367
9367
|
node: ">=24"
|
|
@@ -80765,12 +80765,15 @@ function waitVerdict(doc, options) {
|
|
|
80765
80765
|
return { state: "waiting", reason: "checks_pending" };
|
|
80766
80766
|
if (doc.checks.pending.length > 0)
|
|
80767
80767
|
return { state: "waiting", reason: "checks_pending" };
|
|
80768
|
+
const grace = options.noChecksGraceMs ?? NO_CHECKS_GRACE_MS;
|
|
80769
|
+
if ((options.expectedMissing?.length ?? 0) > 0 && options.elapsedMs < grace)
|
|
80770
|
+
return { state: "waiting", reason: "checks_not_registered" };
|
|
80768
80771
|
if (checks === "passing")
|
|
80769
80772
|
return { state: "ready", reason: "checks_passing" };
|
|
80770
|
-
const ciRequired = doc.checks.missingRequired
|
|
80773
|
+
const ciRequired = (doc.checks.missingRequired?.length ?? 0) > 0 || doc.mergeState === "ci_must_pass" || doc.mergeState === "ci_still_running";
|
|
80771
80774
|
if (ciRequired)
|
|
80772
80775
|
return { state: "waiting", reason: "no_checks_yet" };
|
|
80773
|
-
if (options.elapsedMs <
|
|
80776
|
+
if (options.elapsedMs < grace)
|
|
80774
80777
|
return { state: "waiting", reason: "no_checks_yet" };
|
|
80775
80778
|
return { state: "ready", reason: "no_checks" };
|
|
80776
80779
|
}
|
|
@@ -80865,7 +80868,7 @@ function recordAccepted(cwd, resolved, status, accepted, options) {
|
|
|
80865
80868
|
}));
|
|
80866
80869
|
return recordReruns(cwd, rows);
|
|
80867
80870
|
}
|
|
80868
|
-
var MAX_LOG_TAILS = 5, GITLAB_MERGEABLE, GITHUB_MERGEABLE, NO_CHECKS_GRACE_MS =
|
|
80871
|
+
var MAX_LOG_TAILS = 5, GITLAB_MERGEABLE, GITHUB_MERGEABLE, NO_CHECKS_GRACE_MS = 120000;
|
|
80869
80872
|
var init_report2 = __esm(() => {
|
|
80870
80873
|
init_rev();
|
|
80871
80874
|
init_ledger();
|
|
@@ -82478,6 +82481,74 @@ function syncStack(cwd, ctx, stack, options, actor) {
|
|
|
82478
82481
|
}
|
|
82479
82482
|
return { ok: true, data: outcome };
|
|
82480
82483
|
}
|
|
82484
|
+
function pruneMergedBranches(cwd, ctx, stack, options) {
|
|
82485
|
+
const outcome = { deleted: [], kept: [] };
|
|
82486
|
+
const remote = remoteFor(cwd, ctx?.resolved ?? null, stack.trunk);
|
|
82487
|
+
const trunkSha = trunkTip(cwd, remote, stack.trunk);
|
|
82488
|
+
const keep = (branch, reason) => outcome.kept.push({ branch, reason });
|
|
82489
|
+
for (const entry of stack.branches) {
|
|
82490
|
+
if (!entry.merged || !safeRef3(entry.branch) || entry.branch === stack.trunk)
|
|
82491
|
+
continue;
|
|
82492
|
+
const tip = branchTip(cwd, entry.branch);
|
|
82493
|
+
if (!tip)
|
|
82494
|
+
continue;
|
|
82495
|
+
if (isProtectedTarget(cwd, entry.branch)) {
|
|
82496
|
+
keep(entry.branch, "a protected branch under the workspace branch policy; never deleted");
|
|
82497
|
+
continue;
|
|
82498
|
+
}
|
|
82499
|
+
if (entry.lastHead === null || tip !== entry.lastHead) {
|
|
82500
|
+
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`);
|
|
82501
|
+
continue;
|
|
82502
|
+
}
|
|
82503
|
+
const owner = worktreeOf(cwd, entry.branch);
|
|
82504
|
+
if (owner) {
|
|
82505
|
+
keep(entry.branch, `checked out in ${owner}; switch it away, then git branch -D ${entry.branch}`);
|
|
82506
|
+
continue;
|
|
82507
|
+
}
|
|
82508
|
+
const safe = safeToDelete(cwd, ctx, remote, trunkSha, entry, tip);
|
|
82509
|
+
if (safe === null) {
|
|
82510
|
+
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)`);
|
|
82511
|
+
continue;
|
|
82512
|
+
}
|
|
82513
|
+
if (!options.dryRun) {
|
|
82514
|
+
const removed = gitRun3(cwd, ["branch", "-D", "--", entry.branch]);
|
|
82515
|
+
if (!removed.ok) {
|
|
82516
|
+
keep(entry.branch, `git branch -D failed: ${removed.stderr.trim().split(`
|
|
82517
|
+
`)[0] ?? ""}`);
|
|
82518
|
+
continue;
|
|
82519
|
+
}
|
|
82520
|
+
appendObserved(cwd, {
|
|
82521
|
+
type: "stack.branch.deleted",
|
|
82522
|
+
actor: options.actor,
|
|
82523
|
+
stack: stack.name,
|
|
82524
|
+
branch: entry.branch,
|
|
82525
|
+
head: tip,
|
|
82526
|
+
pr: entry.pr,
|
|
82527
|
+
because: safe
|
|
82528
|
+
});
|
|
82529
|
+
}
|
|
82530
|
+
outcome.deleted.push({ branch: entry.branch, sha: tip });
|
|
82531
|
+
}
|
|
82532
|
+
return outcome;
|
|
82533
|
+
}
|
|
82534
|
+
function safeToDelete(cwd, ctx, remote, trunkSha, entry, tip) {
|
|
82535
|
+
if (trunkSha && isAncestor2(cwd, tip, trunkSha))
|
|
82536
|
+
return "on_trunk";
|
|
82537
|
+
if (ctx && entry.pr) {
|
|
82538
|
+
const read = statusDoc(ctx, entry.pr);
|
|
82539
|
+
if (read.ok && read.data.state === "merged" && read.data.head.sha === tip)
|
|
82540
|
+
return "merged_head";
|
|
82541
|
+
}
|
|
82542
|
+
const own = pushRemoteName(cwd, entry.branch) ?? remote;
|
|
82543
|
+
const remoteTip = remoteRefTip(cwd, own, `refs/heads/${entry.branch}`);
|
|
82544
|
+
if (remoteTip.ok && remoteTip.sha) {
|
|
82545
|
+
if (!hasCommit(cwd, remoteTip.sha))
|
|
82546
|
+
fetchRefs(cwd, own, [remoteTip.sha]);
|
|
82547
|
+
if (hasCommit(cwd, remoteTip.sha) && isAncestor2(cwd, tip, remoteTip.sha))
|
|
82548
|
+
return "on_remote";
|
|
82549
|
+
}
|
|
82550
|
+
return null;
|
|
82551
|
+
}
|
|
82481
82552
|
async function awaitChecks(ctx, pr, head, options) {
|
|
82482
82553
|
let waited = 0;
|
|
82483
82554
|
for (let poll = 0;; poll += 1) {
|
|
@@ -82718,6 +82789,7 @@ ${run.stdout}`.split(`
|
|
|
82718
82789
|
}, 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();
|
|
82719
82790
|
var init_stack = __esm(() => {
|
|
82720
82791
|
init_autonomy();
|
|
82792
|
+
init_branch();
|
|
82721
82793
|
init_store_lock();
|
|
82722
82794
|
init_pr_ops();
|
|
82723
82795
|
init_report2();
|
|
@@ -85168,8 +85240,213 @@ var init_pr_edit = __esm(() => {
|
|
|
85168
85240
|
init_report2();
|
|
85169
85241
|
});
|
|
85170
85242
|
|
|
85171
|
-
// packages/workit-core/src/
|
|
85243
|
+
// packages/workit-core/src/fanout-landed.ts
|
|
85172
85244
|
import { spawnSync as spawnSync26 } from "node:child_process";
|
|
85245
|
+
function forgeBreaker(resolved) {
|
|
85246
|
+
if (!resolved)
|
|
85247
|
+
return { forge: null, down: () => null };
|
|
85248
|
+
let tripped = null;
|
|
85249
|
+
const guard = (call) => (...args) => {
|
|
85250
|
+
if (tripped)
|
|
85251
|
+
return { ok: false, code: "unavailable", error: tripped };
|
|
85252
|
+
const result = call(...args);
|
|
85253
|
+
if (!result.ok && result.code === "unavailable")
|
|
85254
|
+
tripped = result.error;
|
|
85255
|
+
return result;
|
|
85256
|
+
};
|
|
85257
|
+
const inner = resolved.forge;
|
|
85258
|
+
const forge = Object.create(inner);
|
|
85259
|
+
forge.findPr = guard(inner.findPr.bind(inner));
|
|
85260
|
+
forge.prStatus = guard(inner.prStatus.bind(inner));
|
|
85261
|
+
return { forge: { ...resolved, forge }, down: () => tripped };
|
|
85262
|
+
}
|
|
85263
|
+
function branchCreatedAt(cwd, branch) {
|
|
85264
|
+
if (!branch || branch.startsWith("-"))
|
|
85265
|
+
return null;
|
|
85266
|
+
const log = gitRun4(cwd, ["log", "-g", "--format=%H", `refs/heads/${branch}`, "--"]);
|
|
85267
|
+
if (!log.ok)
|
|
85268
|
+
return null;
|
|
85269
|
+
return log.stdout.trim().split(`
|
|
85270
|
+
`).findLast(Boolean) ?? null;
|
|
85271
|
+
}
|
|
85272
|
+
function hasOwnCommits2(cwd, branch, start, head) {
|
|
85273
|
+
if (!branch || branch.startsWith("-"))
|
|
85274
|
+
return false;
|
|
85275
|
+
const log = gitRun4(cwd, ["log", "-g", "--format=%H%x00%gs", `refs/heads/${branch}`, "--"]);
|
|
85276
|
+
if (!log.ok)
|
|
85277
|
+
return false;
|
|
85278
|
+
for (const line of log.stdout.split(`
|
|
85279
|
+
`)) {
|
|
85280
|
+
const [sha, subject = ""] = line.split("\x00");
|
|
85281
|
+
if (!sha)
|
|
85282
|
+
continue;
|
|
85283
|
+
const made = /^(?:commit(?: \((?:initial|amend)\))?|cherry-pick|rebase(?: -i)? \(pick\)):/u.test(subject);
|
|
85284
|
+
const onto = /^rebase(?: -i)? \(finish\): \S+ onto ([0-9a-f]+)/u.exec(subject)?.[1];
|
|
85285
|
+
if (!made && !(onto && !sha.startsWith(onto) && !onto.startsWith(sha)))
|
|
85286
|
+
continue;
|
|
85287
|
+
if (sha !== head && !isAncestor3(cwd, sha, head))
|
|
85288
|
+
continue;
|
|
85289
|
+
if (start && (sha === start || isAncestor3(cwd, sha, start)))
|
|
85290
|
+
continue;
|
|
85291
|
+
return true;
|
|
85292
|
+
}
|
|
85293
|
+
return false;
|
|
85294
|
+
}
|
|
85295
|
+
function reverted(cwd, trunk, landedHead, start) {
|
|
85296
|
+
const fork = mergeBase(cwd, landedHead, trunk);
|
|
85297
|
+
if (fork && fork !== landedHead)
|
|
85298
|
+
return unchangedSince(cwd, fork, landedHead, trunk);
|
|
85299
|
+
const before = start && start !== landedHead && isAncestor3(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
|
|
85300
|
+
return before ? unchangedSince(cwd, before, landedHead, trunk) : false;
|
|
85301
|
+
}
|
|
85302
|
+
function unchangedSince(cwd, before, after, trunk) {
|
|
85303
|
+
const paths = gitRun4(cwd, ["diff", "--name-only", "--no-renames", "-z", before, after]);
|
|
85304
|
+
const files = paths.ok ? paths.stdout.split("\x00").filter(Boolean) : [];
|
|
85305
|
+
if (files.length === 0)
|
|
85306
|
+
return false;
|
|
85307
|
+
return gitRun4(cwd, ["diff", "--quiet", before, trunk, "--", ...files]).ok;
|
|
85308
|
+
}
|
|
85309
|
+
function trunkPatchIds(ctx, fork) {
|
|
85310
|
+
const cache = ctx.cache ??= new Map;
|
|
85311
|
+
const known = cache.get(fork);
|
|
85312
|
+
if (known)
|
|
85313
|
+
return known;
|
|
85314
|
+
const ids = new Set;
|
|
85315
|
+
const log = gitRun4(ctx.cwd, [
|
|
85316
|
+
"log",
|
|
85317
|
+
"--no-merges",
|
|
85318
|
+
`--max-count=${TRUNK_SCAN}`,
|
|
85319
|
+
"-p",
|
|
85320
|
+
"--format=commit %H",
|
|
85321
|
+
"-U3",
|
|
85322
|
+
"--diff-algorithm=myers",
|
|
85323
|
+
"--indent-heuristic",
|
|
85324
|
+
"--no-color",
|
|
85325
|
+
"--no-ext-diff",
|
|
85326
|
+
"--no-textconv",
|
|
85327
|
+
"--no-renames",
|
|
85328
|
+
"--full-index",
|
|
85329
|
+
"--src-prefix=a/",
|
|
85330
|
+
"--dst-prefix=b/",
|
|
85331
|
+
`${fork}..${ctx.trunk}`,
|
|
85332
|
+
"--"
|
|
85333
|
+
]);
|
|
85334
|
+
if (log.ok && log.stdout.trim()) {
|
|
85335
|
+
const run = spawnSync26("git", ["patch-id", "--stable"], {
|
|
85336
|
+
cwd: ctx.cwd,
|
|
85337
|
+
input: log.stdout,
|
|
85338
|
+
encoding: "utf8",
|
|
85339
|
+
timeout: GIT_TIMEOUTS.local,
|
|
85340
|
+
windowsHide: true,
|
|
85341
|
+
maxBuffer: 64 * 1024 * 1024
|
|
85342
|
+
});
|
|
85343
|
+
for (const line of (run.stdout ?? "").split(`
|
|
85344
|
+
`)) {
|
|
85345
|
+
const id = line.trim().split(/\s+/u)[0];
|
|
85346
|
+
if (id)
|
|
85347
|
+
ids.add(id);
|
|
85348
|
+
}
|
|
85349
|
+
}
|
|
85350
|
+
cache.set(fork, ids);
|
|
85351
|
+
return ids;
|
|
85352
|
+
}
|
|
85353
|
+
function inSliceRun(row, slice) {
|
|
85354
|
+
if (row.type === "fanout.worktree.created" && typeof row.sliceHash === "string" && slice.hash)
|
|
85355
|
+
return row.sliceHash === slice.hash;
|
|
85356
|
+
const since = Date.parse(slice.hashSince);
|
|
85357
|
+
return Number.isNaN(since) || Date.parse(String(row.at)) >= since;
|
|
85358
|
+
}
|
|
85359
|
+
function linked(rows, input, slice, head) {
|
|
85360
|
+
const since = input.since ? Date.parse(input.since) : Number.NaN;
|
|
85361
|
+
const recent = (row) => Number.isNaN(since) || Date.parse(String(row.at)) >= since;
|
|
85362
|
+
return rows.some((row) => {
|
|
85363
|
+
if (row.type === "fanout.worktree.created" && row.fanout === input.fanout && row.slice === slice.id)
|
|
85364
|
+
return typeof row.sliceHash === "string" && input.sliceHash ? row.sliceHash === input.sliceHash : recent(row);
|
|
85365
|
+
return recent(row) && head !== null && row.branch === slice.branch && row.head === head;
|
|
85366
|
+
});
|
|
85367
|
+
}
|
|
85368
|
+
function detectLanding(ctx, slice, input) {
|
|
85369
|
+
const out = {
|
|
85370
|
+
landed: false,
|
|
85371
|
+
how: null,
|
|
85372
|
+
pr: null,
|
|
85373
|
+
landedHead: null,
|
|
85374
|
+
forgeError: null,
|
|
85375
|
+
note: null
|
|
85376
|
+
};
|
|
85377
|
+
const landed = (how, landedHead) => {
|
|
85378
|
+
if (!hasCommit(ctx.cwd, landedHead) || !reverted(ctx.cwd, ctx.trunk, landedHead, input.createdFrom))
|
|
85379
|
+
return { ...out, landed: true, how, landedHead };
|
|
85380
|
+
return {
|
|
85381
|
+
...out,
|
|
85382
|
+
note: `landed (${how}${out.pr ? ` #${out.pr.number}` : ""}), then reverted on the trunk: its paths read as before it`
|
|
85383
|
+
};
|
|
85384
|
+
};
|
|
85385
|
+
const { head } = input;
|
|
85386
|
+
if (ctx.forge) {
|
|
85387
|
+
const found = ctx.forge.forge.findPr(slice.branch, {
|
|
85388
|
+
owner: ctx.forge.headRepo.split("/")[0] ?? null,
|
|
85389
|
+
projectId: ctx.forge.headProjectId,
|
|
85390
|
+
sha: head
|
|
85391
|
+
});
|
|
85392
|
+
if (!found.ok)
|
|
85393
|
+
out.forgeError = found.error;
|
|
85394
|
+
else if (found.data) {
|
|
85395
|
+
out.pr = found.data;
|
|
85396
|
+
if (found.data.state === "merged") {
|
|
85397
|
+
const merged = found.data.headSha;
|
|
85398
|
+
const label = `#${found.data.number}`;
|
|
85399
|
+
if (!head) {
|
|
85400
|
+
if (!linked(input.rows, input, slice, merged)) {
|
|
85401
|
+
out.pr = null;
|
|
85402
|
+
out.note = `${slice.branch} is gone; merged ${label} is not linked to this slice in the ledger (an older branch of the same name?)`;
|
|
85403
|
+
return out;
|
|
85404
|
+
}
|
|
85405
|
+
return merged ? landed("pr_merged", merged) : { ...out, landed: true, how: "pr_merged" };
|
|
85406
|
+
}
|
|
85407
|
+
if (!merged || head === merged || isAncestor3(ctx.cwd, head, merged))
|
|
85408
|
+
return landed("pr_merged", merged ?? head);
|
|
85409
|
+
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)`;
|
|
85410
|
+
return out;
|
|
85411
|
+
}
|
|
85412
|
+
}
|
|
85413
|
+
}
|
|
85414
|
+
if (!head)
|
|
85415
|
+
return out;
|
|
85416
|
+
const fork = mergeBase(ctx.cwd, head, ctx.trunk);
|
|
85417
|
+
if (fork === head) {
|
|
85418
|
+
return hasOwnCommits2(ctx.cwd, slice.branch, input.createdFrom, head) ? landed("on_trunk", head) : out;
|
|
85419
|
+
}
|
|
85420
|
+
if (!fork)
|
|
85421
|
+
return out;
|
|
85422
|
+
const trunkIds = trunkPatchIds(ctx, fork);
|
|
85423
|
+
if (trunkIds.size === 0)
|
|
85424
|
+
return out;
|
|
85425
|
+
const froms = [fork];
|
|
85426
|
+
if (input.parentHead && input.parentHead !== head && isAncestor3(ctx.cwd, input.parentHead, head))
|
|
85427
|
+
froms.push(input.parentHead);
|
|
85428
|
+
for (const from of froms) {
|
|
85429
|
+
const id = patchId(ctx.cwd, from, head);
|
|
85430
|
+
if (id && trunkIds.has(id))
|
|
85431
|
+
return landed("squash_patch", head);
|
|
85432
|
+
}
|
|
85433
|
+
return out;
|
|
85434
|
+
}
|
|
85435
|
+
function sliceStart(cwd, rows, fanout, slice) {
|
|
85436
|
+
const reflog = branchCreatedAt(cwd, slice.branch);
|
|
85437
|
+
if (reflog)
|
|
85438
|
+
return reflog;
|
|
85439
|
+
const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string" && inSliceRun(candidate, slice));
|
|
85440
|
+
return row?.baseSha ?? null;
|
|
85441
|
+
}
|
|
85442
|
+
var isAncestor3 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
|
|
85443
|
+
var init_fanout_landed = __esm(() => {
|
|
85444
|
+
init_rev();
|
|
85445
|
+
init_fanout();
|
|
85446
|
+
});
|
|
85447
|
+
|
|
85448
|
+
// packages/workit-core/src/core/live-session.ts
|
|
85449
|
+
import { spawnSync as spawnSync27 } from "node:child_process";
|
|
85173
85450
|
var LIVE_SESSION_TTL_MS, liveProbe, PIDS;
|
|
85174
85451
|
var init_live_session = __esm(() => {
|
|
85175
85452
|
init_paths();
|
|
@@ -85178,7 +85455,7 @@ var init_live_session = __esm(() => {
|
|
|
85178
85455
|
LIVE_SESSION_TTL_MS = 15 * 60000;
|
|
85179
85456
|
liveProbe = {
|
|
85180
85457
|
platform: process.platform,
|
|
85181
|
-
run: (args) =>
|
|
85458
|
+
run: (args) => spawnSync27("ps", args, { encoding: "utf8", timeout: 2000 }).stdout ?? "",
|
|
85182
85459
|
start: (pid) => processStartOf(pid)
|
|
85183
85460
|
};
|
|
85184
85461
|
PIDS = new Map;
|
|
@@ -85471,8 +85748,65 @@ function worktreeDependencies(main, worktree) {
|
|
|
85471
85748
|
]
|
|
85472
85749
|
};
|
|
85473
85750
|
}
|
|
85751
|
+
const stale = staleBunInstall(main);
|
|
85752
|
+
if (stale && !fs32.existsSync(path64.join(worktree, "node_modules"))) {
|
|
85753
|
+
const command = `bun install --frozen-lockfile --cwd ${worktree}`;
|
|
85754
|
+
return {
|
|
85755
|
+
linked: null,
|
|
85756
|
+
install: command,
|
|
85757
|
+
notes: [
|
|
85758
|
+
`the main checkout's node_modules (${main}) do not match its bun.lock (${stale}), so they were not linked; install dependencies first: ${command}`
|
|
85759
|
+
]
|
|
85760
|
+
};
|
|
85761
|
+
}
|
|
85474
85762
|
return { ...linkWorktreeDependencies(main, worktree), install: null };
|
|
85475
85763
|
}
|
|
85764
|
+
function staleBunInstall(main) {
|
|
85765
|
+
if (!fs32.existsSync(path64.join(main, "node_modules")))
|
|
85766
|
+
return null;
|
|
85767
|
+
const text = readBytes(path64.join(main, "bun.lock"))?.toString("utf8");
|
|
85768
|
+
if (!text)
|
|
85769
|
+
return null;
|
|
85770
|
+
let packages;
|
|
85771
|
+
try {
|
|
85772
|
+
const parsed = JSON.parse(text.replace(/,(\s*[}\]])/gu, "$1"));
|
|
85773
|
+
if (!parsed.packages || typeof parsed.packages !== "object")
|
|
85774
|
+
return null;
|
|
85775
|
+
packages = parsed.packages;
|
|
85776
|
+
} catch {
|
|
85777
|
+
return null;
|
|
85778
|
+
}
|
|
85779
|
+
const problems = [];
|
|
85780
|
+
let scanned = 0;
|
|
85781
|
+
for (const [key, entry] of Object.entries(packages)) {
|
|
85782
|
+
if (scanned++ >= STALE_SCAN || problems.length >= 3)
|
|
85783
|
+
break;
|
|
85784
|
+
const spec = Array.isArray(entry) && typeof entry[0] === "string" ? entry[0] : null;
|
|
85785
|
+
if (!spec)
|
|
85786
|
+
continue;
|
|
85787
|
+
const at = spec.lastIndexOf("@");
|
|
85788
|
+
if (at <= 0)
|
|
85789
|
+
continue;
|
|
85790
|
+
const name = spec.slice(0, at);
|
|
85791
|
+
const version = spec.slice(at + 1);
|
|
85792
|
+
if (name !== key || version.includes(":"))
|
|
85793
|
+
continue;
|
|
85794
|
+
const manifest = readBytes(path64.join(main, "node_modules", key, "package.json"));
|
|
85795
|
+
if (!manifest) {
|
|
85796
|
+
problems.push(`${key} missing`);
|
|
85797
|
+
continue;
|
|
85798
|
+
}
|
|
85799
|
+
let installed;
|
|
85800
|
+
try {
|
|
85801
|
+
installed = JSON.parse(manifest.toString("utf8")).version;
|
|
85802
|
+
} catch {
|
|
85803
|
+
installed = null;
|
|
85804
|
+
}
|
|
85805
|
+
if (installed !== version)
|
|
85806
|
+
problems.push(`${key} ${String(installed ?? "?")} installed, bun.lock has ${version}`);
|
|
85807
|
+
}
|
|
85808
|
+
return problems.length ? problems.join("; ") : null;
|
|
85809
|
+
}
|
|
85476
85810
|
function ensureScratch(worktree) {
|
|
85477
85811
|
const scratch = path64.join(worktree, SCRATCH_DIR);
|
|
85478
85812
|
fs32.mkdirSync(scratch, { recursive: true });
|
|
@@ -85549,14 +85883,19 @@ function createSliceWorktree(cwd, plan, input) {
|
|
|
85549
85883
|
notes.push(`resumed ${slice.branch} from ${remote}/${slice.branch}`);
|
|
85550
85884
|
} else {
|
|
85551
85885
|
mode = "new";
|
|
85552
|
-
const
|
|
85553
|
-
|
|
85886
|
+
const landedOn = dependenciesLanded(cwd, plan, slice, input.forge);
|
|
85887
|
+
if (landedOn) {
|
|
85888
|
+
base.base = plan.trunk;
|
|
85889
|
+
notes.push(landedOn);
|
|
85890
|
+
}
|
|
85891
|
+
const parent = landedOn ? undefined : plan.slices.find((other) => other.branch === slice.base);
|
|
85892
|
+
const from = base.base === plan.trunk ? trunkRef(cwd, plan.trunk) : parent ? branchRef(cwd, parent.branch) : branchRef(cwd, slice.base);
|
|
85554
85893
|
if (!from)
|
|
85555
85894
|
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}`);
|
|
85556
85895
|
const added = gitRun4(cwd, ["worktree", "add", "--detach", target, from], SLOW);
|
|
85557
85896
|
if (!added.ok)
|
|
85558
85897
|
return worktreeFailed(added.stderr);
|
|
85559
|
-
const branched = gitBranch(target, { name: slice.branch, base:
|
|
85898
|
+
const branched = gitBranch(target, { name: slice.branch, base: base.base });
|
|
85560
85899
|
if (!branched.ok) {
|
|
85561
85900
|
gitRun4(cwd, ["worktree", "remove", target], SLOW);
|
|
85562
85901
|
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}`);
|
|
@@ -85586,6 +85925,7 @@ function createSliceWorktree(cwd, plan, input) {
|
|
|
85586
85925
|
sliceHash: slice.hash,
|
|
85587
85926
|
...admin ? { adminId: admin.id, adminIno: admin.ino, adminToken: admin.token } : {},
|
|
85588
85927
|
...baseSha ? { baseSha } : {},
|
|
85928
|
+
...base.base === slice.base ? {} : { startBase: base.base },
|
|
85589
85929
|
...deps.install ? { install: deps.install } : {}
|
|
85590
85930
|
});
|
|
85591
85931
|
return {
|
|
@@ -85602,6 +85942,35 @@ function createSliceWorktree(cwd, plan, input) {
|
|
|
85602
85942
|
}
|
|
85603
85943
|
};
|
|
85604
85944
|
}
|
|
85945
|
+
function dependenciesLanded(cwd, plan, slice, forge) {
|
|
85946
|
+
if (slice.base === plan.trunk || slice.dependsOn.length === 0)
|
|
85947
|
+
return null;
|
|
85948
|
+
const deps = slice.dependsOn.map((id) => plan.slices.find((other) => other.id === id));
|
|
85949
|
+
if (deps.some((dep) => !dep))
|
|
85950
|
+
return null;
|
|
85951
|
+
const trunk = trunkRef(cwd, plan.trunk);
|
|
85952
|
+
if (!trunk)
|
|
85953
|
+
return null;
|
|
85954
|
+
const ledger = readLedger(cwd);
|
|
85955
|
+
const rows = ledger.ok ? ledger.value.rows : [];
|
|
85956
|
+
const ctx = { cwd, trunk, forge: forgeBreaker(forge?.() ?? null).forge };
|
|
85957
|
+
const landed = [];
|
|
85958
|
+
for (const dep of deps) {
|
|
85959
|
+
const landing = detectLanding(ctx, dep, {
|
|
85960
|
+
head: branchRef(cwd, dep.branch),
|
|
85961
|
+
createdFrom: sliceStart(cwd, rows, plan.name, dep),
|
|
85962
|
+
parentHead: null,
|
|
85963
|
+
fanout: plan.name,
|
|
85964
|
+
since: dep.hashSince || plan.createdAt || null,
|
|
85965
|
+
sliceHash: dep.hash || null,
|
|
85966
|
+
rows
|
|
85967
|
+
});
|
|
85968
|
+
if (!landing.landed)
|
|
85969
|
+
return null;
|
|
85970
|
+
landed.push(`${dep.branch} landed (${landing.how ?? "pr_merged"}${landing.pr ? ` #${landing.pr.number}` : ""})`);
|
|
85971
|
+
}
|
|
85972
|
+
return `${landed.join(", ")}: started from ${plan.trunk}, not from ${slice.base}'s pre-merge tip`;
|
|
85973
|
+
}
|
|
85605
85974
|
function releaseSliceWorktree(cwd, plan, input) {
|
|
85606
85975
|
const found = findSlice(plan, input.slice);
|
|
85607
85976
|
if (!found.ok)
|
|
@@ -85744,12 +86113,13 @@ var SLOW, SCRATCH_DIR = ".workit-scratch", EXCLUDE_LINE, DIRTY_ROW_BYTES = 2000,
|
|
|
85744
86113
|
} catch {
|
|
85745
86114
|
return null;
|
|
85746
86115
|
}
|
|
85747
|
-
}, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
|
|
86116
|
+
}, STALE_SCAN = 5000, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
|
|
85748
86117
|
`)[0] || "unknown error"}`);
|
|
85749
86118
|
var init_fanout_worktree = __esm(() => {
|
|
85750
86119
|
init_ops();
|
|
85751
86120
|
init_rev();
|
|
85752
86121
|
init_ledger();
|
|
86122
|
+
init_fanout_landed();
|
|
85753
86123
|
init_worktree_confine();
|
|
85754
86124
|
init_worktree_deps();
|
|
85755
86125
|
init_fanout();
|
|
@@ -86865,6 +87235,8 @@ async function wait2(argv, io) {
|
|
|
86865
87235
|
let errors = 0;
|
|
86866
87236
|
let status = null;
|
|
86867
87237
|
let verdict = { state: "waiting", reason: "not_polled" };
|
|
87238
|
+
let baseContexts = null;
|
|
87239
|
+
let expectedMissing = [];
|
|
86868
87240
|
for (;; ) {
|
|
86869
87241
|
const read = resolved.forge.prStatus(number.data);
|
|
86870
87242
|
polls += 1;
|
|
@@ -86876,7 +87248,13 @@ async function wait2(argv, io) {
|
|
|
86876
87248
|
if (!doc.ok)
|
|
86877
87249
|
return forgeFail(io, doc);
|
|
86878
87250
|
recordPrStatus(io.cwd, doc.data, actor);
|
|
86879
|
-
|
|
87251
|
+
if (status.requiredContexts === null && baseContexts === null) {
|
|
87252
|
+
const base = resolved.forge.branchChecks(status.base);
|
|
87253
|
+
baseContexts = base.ok ? [...new Set(base.data.checks.map((check) => check.context))] : [];
|
|
87254
|
+
}
|
|
87255
|
+
const seen = new Set(status.checks.map((check) => check.context));
|
|
87256
|
+
expectedMissing = (baseContexts ?? []).filter((context) => !seen.has(context));
|
|
87257
|
+
verdict = waitVerdict(doc.data, { head, elapsedMs, expectedMissing });
|
|
86880
87258
|
if (verdict.state !== "waiting")
|
|
86881
87259
|
break;
|
|
86882
87260
|
} else {
|
|
@@ -86908,6 +87286,7 @@ async function wait2(argv, io) {
|
|
|
86908
87286
|
state: verdict.state === "waiting" ? "in_progress" : verdict.state,
|
|
86909
87287
|
...verdict.state === "failed" && doc.checks.pending.length > 0 ? { failFast: true, stillRunning: doc.checks.pending } : {},
|
|
86910
87288
|
reason: verdict.reason,
|
|
87289
|
+
...expectedMissing.length > 0 ? { expectedMissing } : {},
|
|
86911
87290
|
polls,
|
|
86912
87291
|
elapsedMs,
|
|
86913
87292
|
timeoutMs,
|
|
@@ -86917,13 +87296,14 @@ async function wait2(argv, io) {
|
|
|
86917
87296
|
for (const value of renderStatus(doc))
|
|
86918
87297
|
io.stdout(`${value}
|
|
86919
87298
|
`);
|
|
87299
|
+
const never = expectedMissing.length > 0 ? `; checks the base ran never registered: ${expectedMissing.join(", ")}` : "";
|
|
86920
87300
|
if (verdict.state === "ready")
|
|
86921
87301
|
return emit(io, ok2(data), () => [
|
|
86922
|
-
`CI
|
|
87302
|
+
(verdict.reason === "no_checks" ? `CI none: no CI configured for this head (no checks registered within the grace) after ${polls} poll${polls === 1 ? "" : "s"}, ${seconds(elapsedMs)}` : `CI pass (${verdict.reason}) after ${polls} poll${polls === 1 ? "" : "s"}, ${seconds(elapsedMs)}`) + never,
|
|
86923
87303
|
...renderStatus(doc)
|
|
86924
87304
|
]);
|
|
86925
87305
|
if (verdict.state === "failed")
|
|
86926
|
-
return emit(io, fail2("failed", `CI
|
|
87306
|
+
return emit(io, fail2("failed", `CI fail: ${doc.checks.failing.map((check) => check.name).join(", ")}`, {
|
|
86927
87307
|
data,
|
|
86928
87308
|
unblock: "fix the failure, or if it is a flake: workit ci rerun --failed --reason flake"
|
|
86929
87309
|
}));
|
|
@@ -87382,7 +87762,7 @@ var init_git2 = __esm(() => {
|
|
|
87382
87762
|
});
|
|
87383
87763
|
|
|
87384
87764
|
// packages/workit-core/src/forge/verify.ts
|
|
87385
|
-
import { spawnSync as
|
|
87765
|
+
import { spawnSync as spawnSync28 } from "node:child_process";
|
|
87386
87766
|
function finish(cwd, report, actor) {
|
|
87387
87767
|
const delivered = report.observations.length > 0 && report.observations.every((entry) => entry.ok);
|
|
87388
87768
|
const out = { ...report, delivered };
|
|
@@ -87595,7 +87975,7 @@ var NPM_NAME, SEMVER2, SAFE_ARG, systemNpm = (args) => {
|
|
|
87595
87975
|
};
|
|
87596
87976
|
const found = process.platform === "win32" ? findExecutable("npm.cmd", process.env) : null;
|
|
87597
87977
|
const npmCmd = found && !/["%!^]/u.test(found) ? `"${found}"` : "npm.cmd";
|
|
87598
|
-
const result = process.platform === "win32" ?
|
|
87978
|
+
const result = process.platform === "win32" ? spawnSync28(`${npmCmd} ${args.join(" ")}`, { ...options, shell: true }) : spawnSync28("npm", [...args], options);
|
|
87599
87979
|
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
87600
87980
|
}, short3 = (sha) => sha ? sha.slice(0, 12) : "(absent)", npmView = (stdout) => {
|
|
87601
87981
|
try {
|
|
@@ -87820,7 +88200,7 @@ function planReleaseFinish(cwd, input) {
|
|
|
87820
88200
|
return failure3("not_found", `${name} exists neither locally nor on ${remote ?? "a remote"}`);
|
|
87821
88201
|
if (!tracked || name === openRelease)
|
|
87822
88202
|
continue;
|
|
87823
|
-
if (!
|
|
88203
|
+
if (!isAncestor4(cwd, tracked, local))
|
|
87824
88204
|
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}`);
|
|
87825
88205
|
const productionTracked = remote ? resolveRef(cwd, `refs/remotes/${remote}/${line.production}`) : null;
|
|
87826
88206
|
const foreign = git5(cwd, [
|
|
@@ -87830,16 +88210,16 @@ function planReleaseFinish(cwd, input) {
|
|
|
87830
88210
|
`^${releaseTip}`,
|
|
87831
88211
|
...productionTracked ? [`^${productionTracked}`] : []
|
|
87832
88212
|
]).stdout.split(`
|
|
87833
|
-
`).filter(Boolean).filter((sha) => !(git5(cwd, ["rev-parse", "-q", "--verify", `${sha}^2`]).ok &&
|
|
88213
|
+
`).filter(Boolean).filter((sha) => !(git5(cwd, ["rev-parse", "-q", "--verify", `${sha}^2`]).ok && isAncestor4(cwd, releaseTip, sha)));
|
|
87834
88214
|
if (foreign.length)
|
|
87835
88215
|
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}`);
|
|
87836
88216
|
}
|
|
87837
|
-
if (branchTip && remoteBranch && !
|
|
88217
|
+
if (branchTip && remoteBranch && !isAncestor4(cwd, remoteBranch, branchTip))
|
|
87838
88218
|
return failure3("blocked", `release_behind: ${remote}/${branch} has commits local ${branch} lacks`, `git switch ${branch} && git merge --ff-only ${remote}/${branch}`);
|
|
87839
88219
|
const production = resolveRef(cwd, `refs/heads/${line.production}`);
|
|
87840
|
-
const merged =
|
|
88220
|
+
const merged = isAncestor4(cwd, releaseTip, production);
|
|
87841
88221
|
const localTag = resolveRef(cwd, `refs/tags/${tag}`);
|
|
87842
|
-
if (localTag && !(
|
|
88222
|
+
if (localTag && !(isAncestor4(cwd, localTag, production) && isAncestor4(cwd, releaseTip, localTag)))
|
|
87843
88223
|
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");
|
|
87844
88224
|
if (remote) {
|
|
87845
88225
|
const remoteTag = remoteRefTip(cwd, remote, `refs/tags/${tag}`);
|
|
@@ -87853,7 +88233,7 @@ function planReleaseFinish(cwd, input) {
|
|
|
87853
88233
|
tagged: localTag !== null,
|
|
87854
88234
|
mergedBack: Object.fromEntries(mergeBack.map((name) => [
|
|
87855
88235
|
name,
|
|
87856
|
-
localTag !== null &&
|
|
88236
|
+
localTag !== null && isAncestor4(cwd, localTag, resolveRef(cwd, `refs/heads/${name}`))
|
|
87857
88237
|
])),
|
|
87858
88238
|
branchDeleted: branchTip === null
|
|
87859
88239
|
};
|
|
@@ -88015,7 +88395,7 @@ var VERSION, PLACEHOLDER4, validVersion = (version) => VERSION.test(version) &&
|
|
|
88015
88395
|
return ledger.value.rows.filter((row) => row.type.startsWith("release."));
|
|
88016
88396
|
}, record2 = (cwd, actor, row) => {
|
|
88017
88397
|
appendObserved(cwd, { ...row, actor });
|
|
88018
|
-
}, KINDS,
|
|
88398
|
+
}, KINDS, isAncestor4 = (cwd, ancestor, descendant) => git5(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok, switchTo = (cwd, branch) => {
|
|
88019
88399
|
if (currentBranch(cwd) === branch)
|
|
88020
88400
|
return success2(undefined);
|
|
88021
88401
|
const switched = git5(cwd, ["switch", "-q", branch]);
|
|
@@ -88481,24 +88861,25 @@ async function status3(argv, io) {
|
|
|
88481
88861
|
...data.next ? [`next: ${data.next}`] : []
|
|
88482
88862
|
]);
|
|
88483
88863
|
}
|
|
88484
|
-
async function sync(argv, io) {
|
|
88864
|
+
async function sync(argv, io, restack = false) {
|
|
88865
|
+
const usageText = restack ? RESTACK_USAGE : SYNC_USAGE;
|
|
88485
88866
|
const flags = parseFlags(argv, {
|
|
88486
88867
|
name: "value",
|
|
88487
|
-
local: "boolean",
|
|
88868
|
+
...restack ? {} : { local: "boolean" },
|
|
88488
88869
|
"dry-run": "boolean",
|
|
88489
88870
|
force: "list"
|
|
88490
88871
|
});
|
|
88491
88872
|
if (typeof flags === "string")
|
|
88492
|
-
return usage4(io, flags,
|
|
88873
|
+
return usage4(io, flags, usageText);
|
|
88493
88874
|
if (flags.positionals.length)
|
|
88494
|
-
return usage4(io, `unexpected argument ${flags.positionals[0]}`,
|
|
88875
|
+
return usage4(io, `unexpected argument ${flags.positionals[0]}`, usageText);
|
|
88495
88876
|
const selected = selectStack(io.cwd, flags.values.name ?? null);
|
|
88496
88877
|
if (!selected.ok)
|
|
88497
88878
|
return stackFailed(io, selected);
|
|
88498
88879
|
const force = new Set(flags.lists.force ?? []);
|
|
88499
88880
|
const unknown = [...force].filter((branch) => !selected.data.branches.some((entry) => entry.branch === branch));
|
|
88500
88881
|
if (unknown.length)
|
|
88501
|
-
return usage4(io, `--force ${unknown[0]}: not a branch of stack ${selected.data.name}`,
|
|
88882
|
+
return usage4(io, `--force ${unknown[0]}: not a branch of stack ${selected.data.name}`, usageText);
|
|
88502
88883
|
const local = flags.booleans.has("local");
|
|
88503
88884
|
let ctx = null;
|
|
88504
88885
|
if (!local) {
|
|
@@ -88517,14 +88898,23 @@ async function sync(argv, io) {
|
|
|
88517
88898
|
const fresh = readStack(io.cwd, name);
|
|
88518
88899
|
if (!fresh.ok)
|
|
88519
88900
|
return fresh;
|
|
88520
|
-
|
|
88521
|
-
|
|
88522
|
-
|
|
88523
|
-
|
|
88524
|
-
|
|
88901
|
+
const stack = fresh.data ?? selected.data;
|
|
88902
|
+
const dryRun = flags.booleans.has("dry-run");
|
|
88903
|
+
const synced = syncStack(io.cwd, ctx, stack, { publish: !local, dryRun, force }, actorFromEnv(io.env));
|
|
88904
|
+
if (!synced.ok || !restack)
|
|
88905
|
+
return synced;
|
|
88906
|
+
return {
|
|
88907
|
+
ok: true,
|
|
88908
|
+
data: {
|
|
88909
|
+
...synced.data,
|
|
88910
|
+
pruned: pruneMergedBranches(io.cwd, ctx, stack, { dryRun, actor: actorFromEnv(io.env) })
|
|
88911
|
+
}
|
|
88912
|
+
};
|
|
88525
88913
|
});
|
|
88526
88914
|
if (!result.ok)
|
|
88527
88915
|
return stackFailed(io, result);
|
|
88916
|
+
if (restack)
|
|
88917
|
+
return emit(io, ok2(result.data), renderRestack);
|
|
88528
88918
|
return emit(io, ok2(result.data), renderSync);
|
|
88529
88919
|
}
|
|
88530
88920
|
async function land(argv, io) {
|
|
@@ -88611,11 +89001,13 @@ async function run20(argv, io) {
|
|
|
88611
89001
|
return status3(rest, io);
|
|
88612
89002
|
if (sub === "sync")
|
|
88613
89003
|
return sync(rest, io);
|
|
89004
|
+
if (sub === "restack")
|
|
89005
|
+
return sync(rest, io, true);
|
|
88614
89006
|
if (sub === "land")
|
|
88615
89007
|
return land(rest, io);
|
|
88616
89008
|
return usage4(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE13);
|
|
88617
89009
|
}
|
|
88618
|
-
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, {
|
|
89010
|
+
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, {
|
|
88619
89011
|
unblock: result.unblock,
|
|
88620
89012
|
data: result.data ?? {}
|
|
88621
89013
|
})), label3 = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
|
|
@@ -88626,6 +89018,10 @@ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<b
|
|
|
88626
89018
|
step.push ? step.push.pushed ? `pushed${step.push.forced ? " (lease)" : ""}` : "remote current" : "",
|
|
88627
89019
|
step.retarget ? `retargeted ${step.retarget.from} -> ${step.retarget.to}` : ""
|
|
88628
89020
|
].filter(Boolean).join(" "))
|
|
89021
|
+
], renderRestack = (data) => [
|
|
89022
|
+
...renderSync(data),
|
|
89023
|
+
...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})`),
|
|
89024
|
+
...data.pruned.kept.map((item) => `kept merged branch ${item.branch}: ${item.reason}`)
|
|
88629
89025
|
], METHODS3;
|
|
88630
89026
|
var init_stack2 = __esm(() => {
|
|
88631
89027
|
init_ledger();
|
|
@@ -88665,6 +89061,7 @@ function renderBrief(cwd, plan, input) {
|
|
|
88665
89061
|
const planned = `${slicePath(cwd, slice)}${input.attempt === null ? "" : `+try${input.attempt}`}`;
|
|
88666
89062
|
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));
|
|
88667
89063
|
const fresh = Boolean(head) && made?.baseSha === head;
|
|
89064
|
+
const sliceBase = typeof made?.startBase === "string" ? made.startBase : slice.base;
|
|
88668
89065
|
const mode = input.mode ?? (head && !fresh ? "resume" : "new");
|
|
88669
89066
|
if (mode === "resume" && !head)
|
|
88670
89067
|
return fanoutFail("not_found", `MODE resume needs the branch ${branch}, which does not exist here`, `workit fanout brief ${slice.id} --mode new`);
|
|
@@ -88693,13 +89090,13 @@ function renderBrief(cwd, plan, input) {
|
|
|
88693
89090
|
`GOAL: ${slice.goal}`,
|
|
88694
89091
|
`SCOPE: ${slice.scope.join(", ")}`,
|
|
88695
89092
|
...slice.owns.length ? [` owns: ${slice.owns.join(", ")}`] : [],
|
|
88696
|
-
` branch: ${branch} base: ${
|
|
89093
|
+
` branch: ${branch} base: ${sliceBase}`,
|
|
88697
89094
|
`CONTEXT: ${slice.context ?? "none given: read the files in SCOPE and the neighbours they import"}`,
|
|
88698
89095
|
...slice.issues.length ? [
|
|
88699
89096
|
`ISSUE: ${slice.issues.join(", ")} (\`workit pr create\` on ${branch} links ${slice.issues.length === 1 ? "it" : "them"} by itself; no --issue needed)`
|
|
88700
89097
|
] : [],
|
|
88701
89098
|
...mode === "resume" && head ? [
|
|
88702
|
-
` resume: continue ${branch} from ${head.slice(0, 12)}; read \`git log ${
|
|
89099
|
+
` resume: continue ${branch} from ${head.slice(0, 12)}; read \`git log ${sliceBase}..${branch}\` and \`workit ledger list --branch ${branch}\` (the last report, verdicts, rulings)`
|
|
88703
89100
|
] : [],
|
|
88704
89101
|
"ACCEPTANCE:",
|
|
88705
89102
|
...slice.acceptance.map((line) => ` - ${line}`),
|
|
@@ -88722,7 +89119,7 @@ function renderBrief(cwd, plan, input) {
|
|
|
88722
89119
|
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)`,
|
|
88723
89120
|
...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`] : [],
|
|
88724
89121
|
...identityLines(identity, minted),
|
|
88725
|
-
...rules(mode, branch,
|
|
89122
|
+
...rules(mode, branch, sliceBase, exists ? planned : null)
|
|
88726
89123
|
];
|
|
88727
89124
|
return {
|
|
88728
89125
|
ok: true,
|
|
@@ -88732,7 +89129,7 @@ function renderBrief(cwd, plan, input) {
|
|
|
88732
89129
|
mode,
|
|
88733
89130
|
attempt: input.attempt,
|
|
88734
89131
|
branch,
|
|
88735
|
-
base:
|
|
89132
|
+
base: sliceBase,
|
|
88736
89133
|
tier: slice.tier,
|
|
88737
89134
|
issues: slice.issues,
|
|
88738
89135
|
model: model?.model ?? null,
|
|
@@ -88797,211 +89194,6 @@ var init_fanout_brief = __esm(() => {
|
|
|
88797
89194
|
];
|
|
88798
89195
|
});
|
|
88799
89196
|
|
|
88800
|
-
// packages/workit-core/src/fanout-landed.ts
|
|
88801
|
-
import { spawnSync as spawnSync28 } from "node:child_process";
|
|
88802
|
-
function forgeBreaker(resolved) {
|
|
88803
|
-
if (!resolved)
|
|
88804
|
-
return { forge: null, down: () => null };
|
|
88805
|
-
let tripped = null;
|
|
88806
|
-
const guard = (call) => (...args) => {
|
|
88807
|
-
if (tripped)
|
|
88808
|
-
return { ok: false, code: "unavailable", error: tripped };
|
|
88809
|
-
const result = call(...args);
|
|
88810
|
-
if (!result.ok && result.code === "unavailable")
|
|
88811
|
-
tripped = result.error;
|
|
88812
|
-
return result;
|
|
88813
|
-
};
|
|
88814
|
-
const inner = resolved.forge;
|
|
88815
|
-
const forge = Object.create(inner);
|
|
88816
|
-
forge.findPr = guard(inner.findPr.bind(inner));
|
|
88817
|
-
forge.prStatus = guard(inner.prStatus.bind(inner));
|
|
88818
|
-
return { forge: { ...resolved, forge }, down: () => tripped };
|
|
88819
|
-
}
|
|
88820
|
-
function branchCreatedAt(cwd, branch) {
|
|
88821
|
-
if (!branch || branch.startsWith("-"))
|
|
88822
|
-
return null;
|
|
88823
|
-
const log = gitRun4(cwd, ["log", "-g", "--format=%H", `refs/heads/${branch}`, "--"]);
|
|
88824
|
-
if (!log.ok)
|
|
88825
|
-
return null;
|
|
88826
|
-
return log.stdout.trim().split(`
|
|
88827
|
-
`).findLast(Boolean) ?? null;
|
|
88828
|
-
}
|
|
88829
|
-
function hasOwnCommits2(cwd, branch, start, head) {
|
|
88830
|
-
if (!branch || branch.startsWith("-"))
|
|
88831
|
-
return false;
|
|
88832
|
-
const log = gitRun4(cwd, ["log", "-g", "--format=%H%x00%gs", `refs/heads/${branch}`, "--"]);
|
|
88833
|
-
if (!log.ok)
|
|
88834
|
-
return false;
|
|
88835
|
-
for (const line of log.stdout.split(`
|
|
88836
|
-
`)) {
|
|
88837
|
-
const [sha, subject = ""] = line.split("\x00");
|
|
88838
|
-
if (!sha)
|
|
88839
|
-
continue;
|
|
88840
|
-
const made = /^(?:commit(?: \((?:initial|amend)\))?|cherry-pick|rebase(?: -i)? \(pick\)):/u.test(subject);
|
|
88841
|
-
const onto = /^rebase(?: -i)? \(finish\): \S+ onto ([0-9a-f]+)/u.exec(subject)?.[1];
|
|
88842
|
-
if (!made && !(onto && !sha.startsWith(onto) && !onto.startsWith(sha)))
|
|
88843
|
-
continue;
|
|
88844
|
-
if (sha !== head && !isAncestor4(cwd, sha, head))
|
|
88845
|
-
continue;
|
|
88846
|
-
if (start && (sha === start || isAncestor4(cwd, sha, start)))
|
|
88847
|
-
continue;
|
|
88848
|
-
return true;
|
|
88849
|
-
}
|
|
88850
|
-
return false;
|
|
88851
|
-
}
|
|
88852
|
-
function reverted(cwd, trunk, landedHead, start) {
|
|
88853
|
-
const fork = mergeBase(cwd, landedHead, trunk);
|
|
88854
|
-
if (fork && fork !== landedHead)
|
|
88855
|
-
return unchangedSince(cwd, fork, landedHead, trunk);
|
|
88856
|
-
const before = start && start !== landedHead && isAncestor4(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
|
|
88857
|
-
return before ? unchangedSince(cwd, before, landedHead, trunk) : false;
|
|
88858
|
-
}
|
|
88859
|
-
function unchangedSince(cwd, before, after, trunk) {
|
|
88860
|
-
const paths = gitRun4(cwd, ["diff", "--name-only", "--no-renames", "-z", before, after]);
|
|
88861
|
-
const files = paths.ok ? paths.stdout.split("\x00").filter(Boolean) : [];
|
|
88862
|
-
if (files.length === 0)
|
|
88863
|
-
return false;
|
|
88864
|
-
return gitRun4(cwd, ["diff", "--quiet", before, trunk, "--", ...files]).ok;
|
|
88865
|
-
}
|
|
88866
|
-
function trunkPatchIds(ctx, fork) {
|
|
88867
|
-
const cache = ctx.cache ??= new Map;
|
|
88868
|
-
const known = cache.get(fork);
|
|
88869
|
-
if (known)
|
|
88870
|
-
return known;
|
|
88871
|
-
const ids = new Set;
|
|
88872
|
-
const log = gitRun4(ctx.cwd, [
|
|
88873
|
-
"log",
|
|
88874
|
-
"--no-merges",
|
|
88875
|
-
`--max-count=${TRUNK_SCAN}`,
|
|
88876
|
-
"-p",
|
|
88877
|
-
"--format=commit %H",
|
|
88878
|
-
"-U3",
|
|
88879
|
-
"--diff-algorithm=myers",
|
|
88880
|
-
"--indent-heuristic",
|
|
88881
|
-
"--no-color",
|
|
88882
|
-
"--no-ext-diff",
|
|
88883
|
-
"--no-textconv",
|
|
88884
|
-
"--no-renames",
|
|
88885
|
-
"--full-index",
|
|
88886
|
-
"--src-prefix=a/",
|
|
88887
|
-
"--dst-prefix=b/",
|
|
88888
|
-
`${fork}..${ctx.trunk}`,
|
|
88889
|
-
"--"
|
|
88890
|
-
]);
|
|
88891
|
-
if (log.ok && log.stdout.trim()) {
|
|
88892
|
-
const run = spawnSync28("git", ["patch-id", "--stable"], {
|
|
88893
|
-
cwd: ctx.cwd,
|
|
88894
|
-
input: log.stdout,
|
|
88895
|
-
encoding: "utf8",
|
|
88896
|
-
timeout: GIT_TIMEOUTS.local,
|
|
88897
|
-
windowsHide: true,
|
|
88898
|
-
maxBuffer: 64 * 1024 * 1024
|
|
88899
|
-
});
|
|
88900
|
-
for (const line of (run.stdout ?? "").split(`
|
|
88901
|
-
`)) {
|
|
88902
|
-
const id = line.trim().split(/\s+/u)[0];
|
|
88903
|
-
if (id)
|
|
88904
|
-
ids.add(id);
|
|
88905
|
-
}
|
|
88906
|
-
}
|
|
88907
|
-
cache.set(fork, ids);
|
|
88908
|
-
return ids;
|
|
88909
|
-
}
|
|
88910
|
-
function inSliceRun(row, slice) {
|
|
88911
|
-
if (row.type === "fanout.worktree.created" && typeof row.sliceHash === "string" && slice.hash)
|
|
88912
|
-
return row.sliceHash === slice.hash;
|
|
88913
|
-
const since = Date.parse(slice.hashSince);
|
|
88914
|
-
return Number.isNaN(since) || Date.parse(String(row.at)) >= since;
|
|
88915
|
-
}
|
|
88916
|
-
function linked(rows, input, slice, head) {
|
|
88917
|
-
const since = input.since ? Date.parse(input.since) : Number.NaN;
|
|
88918
|
-
const recent = (row) => Number.isNaN(since) || Date.parse(String(row.at)) >= since;
|
|
88919
|
-
return rows.some((row) => {
|
|
88920
|
-
if (row.type === "fanout.worktree.created" && row.fanout === input.fanout && row.slice === slice.id)
|
|
88921
|
-
return typeof row.sliceHash === "string" && input.sliceHash ? row.sliceHash === input.sliceHash : recent(row);
|
|
88922
|
-
return recent(row) && head !== null && row.branch === slice.branch && row.head === head;
|
|
88923
|
-
});
|
|
88924
|
-
}
|
|
88925
|
-
function detectLanding(ctx, slice, input) {
|
|
88926
|
-
const out = {
|
|
88927
|
-
landed: false,
|
|
88928
|
-
how: null,
|
|
88929
|
-
pr: null,
|
|
88930
|
-
landedHead: null,
|
|
88931
|
-
forgeError: null,
|
|
88932
|
-
note: null
|
|
88933
|
-
};
|
|
88934
|
-
const landed = (how, landedHead) => {
|
|
88935
|
-
if (!hasCommit(ctx.cwd, landedHead) || !reverted(ctx.cwd, ctx.trunk, landedHead, input.createdFrom))
|
|
88936
|
-
return { ...out, landed: true, how, landedHead };
|
|
88937
|
-
return {
|
|
88938
|
-
...out,
|
|
88939
|
-
note: `landed (${how}${out.pr ? ` #${out.pr.number}` : ""}), then reverted on the trunk: its paths read as before it`
|
|
88940
|
-
};
|
|
88941
|
-
};
|
|
88942
|
-
const { head } = input;
|
|
88943
|
-
if (ctx.forge) {
|
|
88944
|
-
const found = ctx.forge.forge.findPr(slice.branch, {
|
|
88945
|
-
owner: ctx.forge.headRepo.split("/")[0] ?? null,
|
|
88946
|
-
projectId: ctx.forge.headProjectId,
|
|
88947
|
-
sha: head
|
|
88948
|
-
});
|
|
88949
|
-
if (!found.ok)
|
|
88950
|
-
out.forgeError = found.error;
|
|
88951
|
-
else if (found.data) {
|
|
88952
|
-
out.pr = found.data;
|
|
88953
|
-
if (found.data.state === "merged") {
|
|
88954
|
-
const merged = found.data.headSha;
|
|
88955
|
-
const label = `#${found.data.number}`;
|
|
88956
|
-
if (!head) {
|
|
88957
|
-
if (!linked(input.rows, input, slice, merged)) {
|
|
88958
|
-
out.pr = null;
|
|
88959
|
-
out.note = `${slice.branch} is gone; merged ${label} is not linked to this slice in the ledger (an older branch of the same name?)`;
|
|
88960
|
-
return out;
|
|
88961
|
-
}
|
|
88962
|
-
return merged ? landed("pr_merged", merged) : { ...out, landed: true, how: "pr_merged" };
|
|
88963
|
-
}
|
|
88964
|
-
if (!merged || head === merged || isAncestor4(ctx.cwd, head, merged))
|
|
88965
|
-
return landed("pr_merged", merged ?? head);
|
|
88966
|
-
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)`;
|
|
88967
|
-
return out;
|
|
88968
|
-
}
|
|
88969
|
-
}
|
|
88970
|
-
}
|
|
88971
|
-
if (!head)
|
|
88972
|
-
return out;
|
|
88973
|
-
const fork = mergeBase(ctx.cwd, head, ctx.trunk);
|
|
88974
|
-
if (fork === head) {
|
|
88975
|
-
return hasOwnCommits2(ctx.cwd, slice.branch, input.createdFrom, head) ? landed("on_trunk", head) : out;
|
|
88976
|
-
}
|
|
88977
|
-
if (!fork)
|
|
88978
|
-
return out;
|
|
88979
|
-
const trunkIds = trunkPatchIds(ctx, fork);
|
|
88980
|
-
if (trunkIds.size === 0)
|
|
88981
|
-
return out;
|
|
88982
|
-
const froms = [fork];
|
|
88983
|
-
if (input.parentHead && input.parentHead !== head && isAncestor4(ctx.cwd, input.parentHead, head))
|
|
88984
|
-
froms.push(input.parentHead);
|
|
88985
|
-
for (const from of froms) {
|
|
88986
|
-
const id = patchId(ctx.cwd, from, head);
|
|
88987
|
-
if (id && trunkIds.has(id))
|
|
88988
|
-
return landed("squash_patch", head);
|
|
88989
|
-
}
|
|
88990
|
-
return out;
|
|
88991
|
-
}
|
|
88992
|
-
function sliceStart(cwd, rows, fanout, slice) {
|
|
88993
|
-
const reflog = branchCreatedAt(cwd, slice.branch);
|
|
88994
|
-
if (reflog)
|
|
88995
|
-
return reflog;
|
|
88996
|
-
const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string" && inSliceRun(candidate, slice));
|
|
88997
|
-
return row?.baseSha ?? null;
|
|
88998
|
-
}
|
|
88999
|
-
var isAncestor4 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
|
|
89000
|
-
var init_fanout_landed = __esm(() => {
|
|
89001
|
-
init_rev();
|
|
89002
|
-
init_fanout();
|
|
89003
|
-
});
|
|
89004
|
-
|
|
89005
89197
|
// packages/workit-core/src/fanout-check.ts
|
|
89006
89198
|
function mergeConflicts(cwd, a, b) {
|
|
89007
89199
|
const run = gitRun4(cwd, [
|
|
@@ -89184,7 +89376,9 @@ function inspect(cwd, plan, slice, ctx, changedBy) {
|
|
|
89184
89376
|
return check;
|
|
89185
89377
|
}
|
|
89186
89378
|
const parent = plan.slices.find((other) => other.branch === slice.base);
|
|
89187
|
-
const
|
|
89379
|
+
const parentLanded = parent ? ctx.landings.get(parent.id)?.landed === true : false;
|
|
89380
|
+
const parentRef = parent ? tips.get(parent.id) ?? ctx.landings.get(parent.id)?.landedHead ?? null : null;
|
|
89381
|
+
const baseRef = slice.base === plan.trunk ? trunk : parent ? parentLanded && !(parentRef && isAncestor5(cwd, parentRef, head)) ? trunk : parentRef : branchRef(cwd, slice.base);
|
|
89188
89382
|
if (!SAFE(baseRef)) {
|
|
89189
89383
|
check.reasons.push(`base ${slice.base} not found`);
|
|
89190
89384
|
return check;
|
|
@@ -89652,7 +89846,8 @@ function worktree(argv, io) {
|
|
|
89652
89846
|
if (action === "create") {
|
|
89653
89847
|
const result = createSliceWorktree(io.cwd, selected.data, {
|
|
89654
89848
|
slice: flags.positionals[0],
|
|
89655
|
-
actor
|
|
89849
|
+
actor,
|
|
89850
|
+
forge: () => forgeFor(io, false).forge
|
|
89656
89851
|
});
|
|
89657
89852
|
if (!result.ok)
|
|
89658
89853
|
return failed(io, result);
|
|
@@ -107587,6 +107782,11 @@ var VERBS = [
|
|
|
107587
107782
|
usage: "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]…",
|
|
107588
107783
|
summary: "Restack onto the moved bases, lease-push and retarget the PRs"
|
|
107589
107784
|
},
|
|
107785
|
+
{
|
|
107786
|
+
name: "restack",
|
|
107787
|
+
usage: "workit stack restack [--name <n>] [--dry-run] [--force <branch>]…",
|
|
107788
|
+
summary: "After a parent merged (squash too): sync, then delete the merged stack branches locally"
|
|
107789
|
+
},
|
|
107590
107790
|
{
|
|
107591
107791
|
name: "land",
|
|
107592
107792
|
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]",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brainervirus/workit-cli",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.39.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
|
|
6
6
|
"keywords": [
|
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@babel/parser": "7.29.9",
|
|
41
|
-
"@brainervirus/workit-cursor": "^8.
|
|
41
|
+
"@brainervirus/workit-cursor": "^8.39.1",
|
|
42
42
|
"@inkjs/ui": "2.0.0",
|
|
43
43
|
"ink": "7.1.1",
|
|
44
44
|
"react": "19.2.8"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@brainervirus/workit-core": "^8.
|
|
47
|
+
"@brainervirus/workit-core": "^8.39.1"
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
|
50
50
|
"node": ">=24"
|