@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.
Files changed (2) hide show
  1. package/dist/index.js +446 -246
  2. 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.38.5",
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.38.5",
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.38.5"
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 === null || doc.mergeState === "ci_must_pass" || doc.mergeState === "ci_still_running";
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 < (options.noChecksGraceMs ?? NO_CHECKS_GRACE_MS))
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 = 90000;
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/core/live-session.ts
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) => spawnSync26("ps", args, { encoding: "utf8", timeout: 2000 }).stdout ?? "",
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 parent = plan.slices.find((other) => other.branch === slice.base);
85553
- const from = slice.base === plan.trunk ? trunkRef(cwd, plan.trunk) : parent ? branchRef(cwd, parent.branch) : branchRef(cwd, slice.base);
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: slice.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
- verdict = waitVerdict(doc.data, { head, elapsedMs });
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 ready (${verdict.reason}) after ${polls} poll${polls === 1 ? "" : "s"}, ${seconds(elapsedMs)}`,
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 failed: ${doc.checks.failing.map((check) => check.name).join(", ")}`, {
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 spawnSync27 } from "node:child_process";
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" ? spawnSync27(`${npmCmd} ${args.join(" ")}`, { ...options, shell: true }) : spawnSync27("npm", [...args], options);
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 (!isAncestor3(cwd, tracked, local))
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 && isAncestor3(cwd, releaseTip, sha)));
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 && !isAncestor3(cwd, remoteBranch, branchTip))
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 = isAncestor3(cwd, releaseTip, production);
88220
+ const merged = isAncestor4(cwd, releaseTip, production);
87841
88221
  const localTag = resolveRef(cwd, `refs/tags/${tag}`);
87842
- if (localTag && !(isAncestor3(cwd, localTag, production) && isAncestor3(cwd, releaseTip, 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 && isAncestor3(cwd, localTag, resolveRef(cwd, `refs/heads/${name}`))
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, isAncestor3 = (cwd, ancestor, descendant) => git5(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok, switchTo = (cwd, branch) => {
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, SYNC_USAGE);
88873
+ return usage4(io, flags, usageText);
88493
88874
  if (flags.positionals.length)
88494
- return usage4(io, `unexpected argument ${flags.positionals[0]}`, SYNC_USAGE);
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}`, SYNC_USAGE);
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
- return syncStack(io.cwd, ctx, fresh.data ?? selected.data, {
88521
- publish: !local,
88522
- dryRun: flags.booleans.has("dry-run"),
88523
- force
88524
- }, actorFromEnv(io.env));
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: ${slice.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 ${slice.base}..${branch}\` and \`workit ledger list --branch ${branch}\` (the last report, verdicts, rulings)`
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, slice.base, exists ? planned : null)
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: slice.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 baseRef = slice.base === plan.trunk ? trunk : parent ? tips.get(parent.id) ?? ctx.landings.get(parent.id)?.landedHead ?? null : branchRef(cwd, slice.base);
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.38.5",
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.38.5",
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.38.5"
47
+ "@brainervirus/workit-core": "^8.39.1"
48
48
  },
49
49
  "engines": {
50
50
  "node": ">=24"