@brainervirus/workit-claude-code 8.38.5 → 8.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "workit",
3
3
  "displayName": "Workit",
4
- "version": "8.38.5",
4
+ "version": "8.39.0",
5
5
  "description": "Workflow rails for agentic coding: task context on every session, branch policy on shell commands, and the workit method skills",
6
6
  "author": {
7
7
  "name": "BrainerVirus"
package/dist/workit.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.0",
9321
9321
  private: false,
9322
9322
  description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
9323
9323
  keywords: [
@@ -9355,13 +9355,13 @@ var init_package = __esm(() => {
9355
9355
  },
9356
9356
  dependencies: {
9357
9357
  "@babel/parser": "7.29.9",
9358
- "@brainervirus/workit-cursor": "^8.38.5",
9358
+ "@brainervirus/workit-cursor": "^8.39.0",
9359
9359
  "@inkjs/ui": "2.0.0",
9360
9360
  ink: "7.1.1",
9361
9361
  react: "19.2.8"
9362
9362
  },
9363
9363
  devDependencies: {
9364
- "@brainervirus/workit-core": "^8.38.5"
9364
+ "@brainervirus/workit-core": "^8.39.0"
9365
9365
  },
9366
9366
  engines: {
9367
9367
  node: ">=24"
@@ -82478,6 +82478,74 @@ function syncStack(cwd, ctx, stack, options, actor) {
82478
82478
  }
82479
82479
  return { ok: true, data: outcome };
82480
82480
  }
82481
+ function pruneMergedBranches(cwd, ctx, stack, options) {
82482
+ const outcome = { deleted: [], kept: [] };
82483
+ const remote = remoteFor(cwd, ctx?.resolved ?? null, stack.trunk);
82484
+ const trunkSha = trunkTip(cwd, remote, stack.trunk);
82485
+ const keep = (branch, reason) => outcome.kept.push({ branch, reason });
82486
+ for (const entry of stack.branches) {
82487
+ if (!entry.merged || !safeRef3(entry.branch) || entry.branch === stack.trunk)
82488
+ continue;
82489
+ const tip = branchTip(cwd, entry.branch);
82490
+ if (!tip)
82491
+ continue;
82492
+ if (isProtectedTarget(cwd, entry.branch)) {
82493
+ keep(entry.branch, "a protected branch under the workspace branch policy; never deleted");
82494
+ continue;
82495
+ }
82496
+ if (entry.lastHead === null || tip !== entry.lastHead) {
82497
+ keep(entry.branch, `its tip ${tip.slice(0, 12)} is not recorded by the stack (last ${entry.lastHead?.slice(0, 12) ?? "none"}); commits made after it merged stay`);
82498
+ continue;
82499
+ }
82500
+ const owner = worktreeOf(cwd, entry.branch);
82501
+ if (owner) {
82502
+ keep(entry.branch, `checked out in ${owner}; switch it away, then git branch -D ${entry.branch}`);
82503
+ continue;
82504
+ }
82505
+ const safe = safeToDelete(cwd, ctx, remote, trunkSha, entry, tip);
82506
+ if (safe === null) {
82507
+ keep(entry.branch, `${tip.slice(0, 12)} is not on ${stack.trunk}, not the head its PR merged at and not on ${remote}/${entry.branch}: deleting it could lose commits (push them, or delete it yourself)`);
82508
+ continue;
82509
+ }
82510
+ if (!options.dryRun) {
82511
+ const removed = gitRun3(cwd, ["branch", "-D", "--", entry.branch]);
82512
+ if (!removed.ok) {
82513
+ keep(entry.branch, `git branch -D failed: ${removed.stderr.trim().split(`
82514
+ `)[0] ?? ""}`);
82515
+ continue;
82516
+ }
82517
+ appendObserved(cwd, {
82518
+ type: "stack.branch.deleted",
82519
+ actor: options.actor,
82520
+ stack: stack.name,
82521
+ branch: entry.branch,
82522
+ head: tip,
82523
+ pr: entry.pr,
82524
+ because: safe
82525
+ });
82526
+ }
82527
+ outcome.deleted.push({ branch: entry.branch, sha: tip });
82528
+ }
82529
+ return outcome;
82530
+ }
82531
+ function safeToDelete(cwd, ctx, remote, trunkSha, entry, tip) {
82532
+ if (trunkSha && isAncestor2(cwd, tip, trunkSha))
82533
+ return "on_trunk";
82534
+ if (ctx && entry.pr) {
82535
+ const read = statusDoc(ctx, entry.pr);
82536
+ if (read.ok && read.data.state === "merged" && read.data.head.sha === tip)
82537
+ return "merged_head";
82538
+ }
82539
+ const own = pushRemoteName(cwd, entry.branch) ?? remote;
82540
+ const remoteTip = remoteRefTip(cwd, own, `refs/heads/${entry.branch}`);
82541
+ if (remoteTip.ok && remoteTip.sha) {
82542
+ if (!hasCommit(cwd, remoteTip.sha))
82543
+ fetchRefs(cwd, own, [remoteTip.sha]);
82544
+ if (hasCommit(cwd, remoteTip.sha) && isAncestor2(cwd, tip, remoteTip.sha))
82545
+ return "on_remote";
82546
+ }
82547
+ return null;
82548
+ }
82481
82549
  async function awaitChecks(ctx, pr, head, options) {
82482
82550
  let waited = 0;
82483
82551
  for (let poll = 0;; poll += 1) {
@@ -82718,6 +82786,7 @@ ${run.stdout}`.split(`
82718
82786
  }, commitCount = (cwd, base, head) => Number(gitRun3(cwd, ["rev-list", "--count", "--no-merges", `${base}..${head}`]).stdout.trim()) || 0, shortStat = (cwd, base, head) => gitRun3(cwd, ["diff", "--shortstat", `${base}...${head}`]).stdout.trim();
82719
82787
  var init_stack = __esm(() => {
82720
82788
  init_autonomy();
82789
+ init_branch();
82721
82790
  init_store_lock();
82722
82791
  init_pr_ops();
82723
82792
  init_report2();
@@ -85168,8 +85237,213 @@ var init_pr_edit = __esm(() => {
85168
85237
  init_report2();
85169
85238
  });
85170
85239
 
85171
- // packages/workit-core/src/core/live-session.ts
85240
+ // packages/workit-core/src/fanout-landed.ts
85172
85241
  import { spawnSync as spawnSync26 } from "node:child_process";
85242
+ function forgeBreaker(resolved) {
85243
+ if (!resolved)
85244
+ return { forge: null, down: () => null };
85245
+ let tripped = null;
85246
+ const guard = (call) => (...args) => {
85247
+ if (tripped)
85248
+ return { ok: false, code: "unavailable", error: tripped };
85249
+ const result = call(...args);
85250
+ if (!result.ok && result.code === "unavailable")
85251
+ tripped = result.error;
85252
+ return result;
85253
+ };
85254
+ const inner = resolved.forge;
85255
+ const forge = Object.create(inner);
85256
+ forge.findPr = guard(inner.findPr.bind(inner));
85257
+ forge.prStatus = guard(inner.prStatus.bind(inner));
85258
+ return { forge: { ...resolved, forge }, down: () => tripped };
85259
+ }
85260
+ function branchCreatedAt(cwd, branch) {
85261
+ if (!branch || branch.startsWith("-"))
85262
+ return null;
85263
+ const log = gitRun4(cwd, ["log", "-g", "--format=%H", `refs/heads/${branch}`, "--"]);
85264
+ if (!log.ok)
85265
+ return null;
85266
+ return log.stdout.trim().split(`
85267
+ `).findLast(Boolean) ?? null;
85268
+ }
85269
+ function hasOwnCommits2(cwd, branch, start, head) {
85270
+ if (!branch || branch.startsWith("-"))
85271
+ return false;
85272
+ const log = gitRun4(cwd, ["log", "-g", "--format=%H%x00%gs", `refs/heads/${branch}`, "--"]);
85273
+ if (!log.ok)
85274
+ return false;
85275
+ for (const line of log.stdout.split(`
85276
+ `)) {
85277
+ const [sha, subject = ""] = line.split("\x00");
85278
+ if (!sha)
85279
+ continue;
85280
+ const made = /^(?:commit(?: \((?:initial|amend)\))?|cherry-pick|rebase(?: -i)? \(pick\)):/u.test(subject);
85281
+ const onto = /^rebase(?: -i)? \(finish\): \S+ onto ([0-9a-f]+)/u.exec(subject)?.[1];
85282
+ if (!made && !(onto && !sha.startsWith(onto) && !onto.startsWith(sha)))
85283
+ continue;
85284
+ if (sha !== head && !isAncestor3(cwd, sha, head))
85285
+ continue;
85286
+ if (start && (sha === start || isAncestor3(cwd, sha, start)))
85287
+ continue;
85288
+ return true;
85289
+ }
85290
+ return false;
85291
+ }
85292
+ function reverted(cwd, trunk, landedHead, start) {
85293
+ const fork = mergeBase(cwd, landedHead, trunk);
85294
+ if (fork && fork !== landedHead)
85295
+ return unchangedSince(cwd, fork, landedHead, trunk);
85296
+ const before = start && start !== landedHead && isAncestor3(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
85297
+ return before ? unchangedSince(cwd, before, landedHead, trunk) : false;
85298
+ }
85299
+ function unchangedSince(cwd, before, after, trunk) {
85300
+ const paths = gitRun4(cwd, ["diff", "--name-only", "--no-renames", "-z", before, after]);
85301
+ const files = paths.ok ? paths.stdout.split("\x00").filter(Boolean) : [];
85302
+ if (files.length === 0)
85303
+ return false;
85304
+ return gitRun4(cwd, ["diff", "--quiet", before, trunk, "--", ...files]).ok;
85305
+ }
85306
+ function trunkPatchIds(ctx, fork) {
85307
+ const cache = ctx.cache ??= new Map;
85308
+ const known = cache.get(fork);
85309
+ if (known)
85310
+ return known;
85311
+ const ids = new Set;
85312
+ const log = gitRun4(ctx.cwd, [
85313
+ "log",
85314
+ "--no-merges",
85315
+ `--max-count=${TRUNK_SCAN}`,
85316
+ "-p",
85317
+ "--format=commit %H",
85318
+ "-U3",
85319
+ "--diff-algorithm=myers",
85320
+ "--indent-heuristic",
85321
+ "--no-color",
85322
+ "--no-ext-diff",
85323
+ "--no-textconv",
85324
+ "--no-renames",
85325
+ "--full-index",
85326
+ "--src-prefix=a/",
85327
+ "--dst-prefix=b/",
85328
+ `${fork}..${ctx.trunk}`,
85329
+ "--"
85330
+ ]);
85331
+ if (log.ok && log.stdout.trim()) {
85332
+ const run = spawnSync26("git", ["patch-id", "--stable"], {
85333
+ cwd: ctx.cwd,
85334
+ input: log.stdout,
85335
+ encoding: "utf8",
85336
+ timeout: GIT_TIMEOUTS.local,
85337
+ windowsHide: true,
85338
+ maxBuffer: 64 * 1024 * 1024
85339
+ });
85340
+ for (const line of (run.stdout ?? "").split(`
85341
+ `)) {
85342
+ const id = line.trim().split(/\s+/u)[0];
85343
+ if (id)
85344
+ ids.add(id);
85345
+ }
85346
+ }
85347
+ cache.set(fork, ids);
85348
+ return ids;
85349
+ }
85350
+ function inSliceRun(row, slice) {
85351
+ if (row.type === "fanout.worktree.created" && typeof row.sliceHash === "string" && slice.hash)
85352
+ return row.sliceHash === slice.hash;
85353
+ const since = Date.parse(slice.hashSince);
85354
+ return Number.isNaN(since) || Date.parse(String(row.at)) >= since;
85355
+ }
85356
+ function linked(rows, input, slice, head) {
85357
+ const since = input.since ? Date.parse(input.since) : Number.NaN;
85358
+ const recent = (row) => Number.isNaN(since) || Date.parse(String(row.at)) >= since;
85359
+ return rows.some((row) => {
85360
+ if (row.type === "fanout.worktree.created" && row.fanout === input.fanout && row.slice === slice.id)
85361
+ return typeof row.sliceHash === "string" && input.sliceHash ? row.sliceHash === input.sliceHash : recent(row);
85362
+ return recent(row) && head !== null && row.branch === slice.branch && row.head === head;
85363
+ });
85364
+ }
85365
+ function detectLanding(ctx, slice, input) {
85366
+ const out = {
85367
+ landed: false,
85368
+ how: null,
85369
+ pr: null,
85370
+ landedHead: null,
85371
+ forgeError: null,
85372
+ note: null
85373
+ };
85374
+ const landed = (how, landedHead) => {
85375
+ if (!hasCommit(ctx.cwd, landedHead) || !reverted(ctx.cwd, ctx.trunk, landedHead, input.createdFrom))
85376
+ return { ...out, landed: true, how, landedHead };
85377
+ return {
85378
+ ...out,
85379
+ note: `landed (${how}${out.pr ? ` #${out.pr.number}` : ""}), then reverted on the trunk: its paths read as before it`
85380
+ };
85381
+ };
85382
+ const { head } = input;
85383
+ if (ctx.forge) {
85384
+ const found = ctx.forge.forge.findPr(slice.branch, {
85385
+ owner: ctx.forge.headRepo.split("/")[0] ?? null,
85386
+ projectId: ctx.forge.headProjectId,
85387
+ sha: head
85388
+ });
85389
+ if (!found.ok)
85390
+ out.forgeError = found.error;
85391
+ else if (found.data) {
85392
+ out.pr = found.data;
85393
+ if (found.data.state === "merged") {
85394
+ const merged = found.data.headSha;
85395
+ const label = `#${found.data.number}`;
85396
+ if (!head) {
85397
+ if (!linked(input.rows, input, slice, merged)) {
85398
+ out.pr = null;
85399
+ out.note = `${slice.branch} is gone; merged ${label} is not linked to this slice in the ledger (an older branch of the same name?)`;
85400
+ return out;
85401
+ }
85402
+ return merged ? landed("pr_merged", merged) : { ...out, landed: true, how: "pr_merged" };
85403
+ }
85404
+ if (!merged || head === merged || isAncestor3(ctx.cwd, head, merged))
85405
+ return landed("pr_merged", merged ?? head);
85406
+ out.note = isAncestor3(ctx.cwd, merged, head) ? `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} is ahead of it with newer commits` : `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} has diverged from it (rewritten after the merge)`;
85407
+ return out;
85408
+ }
85409
+ }
85410
+ }
85411
+ if (!head)
85412
+ return out;
85413
+ const fork = mergeBase(ctx.cwd, head, ctx.trunk);
85414
+ if (fork === head) {
85415
+ return hasOwnCommits2(ctx.cwd, slice.branch, input.createdFrom, head) ? landed("on_trunk", head) : out;
85416
+ }
85417
+ if (!fork)
85418
+ return out;
85419
+ const trunkIds = trunkPatchIds(ctx, fork);
85420
+ if (trunkIds.size === 0)
85421
+ return out;
85422
+ const froms = [fork];
85423
+ if (input.parentHead && input.parentHead !== head && isAncestor3(ctx.cwd, input.parentHead, head))
85424
+ froms.push(input.parentHead);
85425
+ for (const from of froms) {
85426
+ const id = patchId(ctx.cwd, from, head);
85427
+ if (id && trunkIds.has(id))
85428
+ return landed("squash_patch", head);
85429
+ }
85430
+ return out;
85431
+ }
85432
+ function sliceStart(cwd, rows, fanout, slice) {
85433
+ const reflog = branchCreatedAt(cwd, slice.branch);
85434
+ if (reflog)
85435
+ return reflog;
85436
+ const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string" && inSliceRun(candidate, slice));
85437
+ return row?.baseSha ?? null;
85438
+ }
85439
+ var isAncestor3 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
85440
+ var init_fanout_landed = __esm(() => {
85441
+ init_rev();
85442
+ init_fanout();
85443
+ });
85444
+
85445
+ // packages/workit-core/src/core/live-session.ts
85446
+ import { spawnSync as spawnSync27 } from "node:child_process";
85173
85447
  var LIVE_SESSION_TTL_MS, liveProbe, PIDS;
85174
85448
  var init_live_session = __esm(() => {
85175
85449
  init_paths();
@@ -85178,7 +85452,7 @@ var init_live_session = __esm(() => {
85178
85452
  LIVE_SESSION_TTL_MS = 15 * 60000;
85179
85453
  liveProbe = {
85180
85454
  platform: process.platform,
85181
- run: (args) => spawnSync26("ps", args, { encoding: "utf8", timeout: 2000 }).stdout ?? "",
85455
+ run: (args) => spawnSync27("ps", args, { encoding: "utf8", timeout: 2000 }).stdout ?? "",
85182
85456
  start: (pid) => processStartOf(pid)
85183
85457
  };
85184
85458
  PIDS = new Map;
@@ -85471,8 +85745,65 @@ function worktreeDependencies(main, worktree) {
85471
85745
  ]
85472
85746
  };
85473
85747
  }
85748
+ const stale = staleBunInstall(main);
85749
+ if (stale && !fs32.existsSync(path64.join(worktree, "node_modules"))) {
85750
+ const command = `bun install --frozen-lockfile --cwd ${worktree}`;
85751
+ return {
85752
+ linked: null,
85753
+ install: command,
85754
+ notes: [
85755
+ `the main checkout's node_modules (${main}) do not match its bun.lock (${stale}), so they were not linked; install dependencies first: ${command}`
85756
+ ]
85757
+ };
85758
+ }
85474
85759
  return { ...linkWorktreeDependencies(main, worktree), install: null };
85475
85760
  }
85761
+ function staleBunInstall(main) {
85762
+ if (!fs32.existsSync(path64.join(main, "node_modules")))
85763
+ return null;
85764
+ const text = readBytes(path64.join(main, "bun.lock"))?.toString("utf8");
85765
+ if (!text)
85766
+ return null;
85767
+ let packages;
85768
+ try {
85769
+ const parsed = JSON.parse(text.replace(/,(\s*[}\]])/gu, "$1"));
85770
+ if (!parsed.packages || typeof parsed.packages !== "object")
85771
+ return null;
85772
+ packages = parsed.packages;
85773
+ } catch {
85774
+ return null;
85775
+ }
85776
+ const problems = [];
85777
+ let scanned = 0;
85778
+ for (const [key, entry] of Object.entries(packages)) {
85779
+ if (scanned++ >= STALE_SCAN || problems.length >= 3)
85780
+ break;
85781
+ const spec = Array.isArray(entry) && typeof entry[0] === "string" ? entry[0] : null;
85782
+ if (!spec)
85783
+ continue;
85784
+ const at = spec.lastIndexOf("@");
85785
+ if (at <= 0)
85786
+ continue;
85787
+ const name = spec.slice(0, at);
85788
+ const version = spec.slice(at + 1);
85789
+ if (name !== key || version.includes(":"))
85790
+ continue;
85791
+ const manifest = readBytes(path64.join(main, "node_modules", key, "package.json"));
85792
+ if (!manifest) {
85793
+ problems.push(`${key} missing`);
85794
+ continue;
85795
+ }
85796
+ let installed;
85797
+ try {
85798
+ installed = JSON.parse(manifest.toString("utf8")).version;
85799
+ } catch {
85800
+ installed = null;
85801
+ }
85802
+ if (installed !== version)
85803
+ problems.push(`${key} ${String(installed ?? "?")} installed, bun.lock has ${version}`);
85804
+ }
85805
+ return problems.length ? problems.join("; ") : null;
85806
+ }
85476
85807
  function ensureScratch(worktree) {
85477
85808
  const scratch = path64.join(worktree, SCRATCH_DIR);
85478
85809
  fs32.mkdirSync(scratch, { recursive: true });
@@ -85549,14 +85880,19 @@ function createSliceWorktree(cwd, plan, input) {
85549
85880
  notes.push(`resumed ${slice.branch} from ${remote}/${slice.branch}`);
85550
85881
  } else {
85551
85882
  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);
85883
+ const landedOn = dependenciesLanded(cwd, plan, slice, input.forge);
85884
+ if (landedOn) {
85885
+ base.base = plan.trunk;
85886
+ notes.push(landedOn);
85887
+ }
85888
+ const parent = landedOn ? undefined : plan.slices.find((other) => other.branch === slice.base);
85889
+ const from = base.base === plan.trunk ? trunkRef(cwd, plan.trunk) : parent ? branchRef(cwd, parent.branch) : branchRef(cwd, slice.base);
85554
85890
  if (!from)
85555
85891
  return fanoutFail("not_found", `base ${slice.base} of slice ${slice.id} does not resolve here`, parent ? `start slice ${parent.id} first (its branch ${parent.branch} is the base)` : `git fetch origin ${slice.base}`);
85556
85892
  const added = gitRun4(cwd, ["worktree", "add", "--detach", target, from], SLOW);
85557
85893
  if (!added.ok)
85558
85894
  return worktreeFailed(added.stderr);
85559
- const branched = gitBranch(target, { name: slice.branch, base: slice.base });
85895
+ const branched = gitBranch(target, { name: slice.branch, base: base.base });
85560
85896
  if (!branched.ok) {
85561
85897
  gitRun4(cwd, ["worktree", "remove", target], SLOW);
85562
85898
  return fanoutFail(branched.code, `workit git branch ${slice.branch} refused: ${branched.error}`, branched.unblock ?? `fix the branch name in the plan, then workit fanout worktree create ${slice.id}`);
@@ -85586,6 +85922,7 @@ function createSliceWorktree(cwd, plan, input) {
85586
85922
  sliceHash: slice.hash,
85587
85923
  ...admin ? { adminId: admin.id, adminIno: admin.ino, adminToken: admin.token } : {},
85588
85924
  ...baseSha ? { baseSha } : {},
85925
+ ...base.base === slice.base ? {} : { startBase: base.base },
85589
85926
  ...deps.install ? { install: deps.install } : {}
85590
85927
  });
85591
85928
  return {
@@ -85602,6 +85939,35 @@ function createSliceWorktree(cwd, plan, input) {
85602
85939
  }
85603
85940
  };
85604
85941
  }
85942
+ function dependenciesLanded(cwd, plan, slice, forge) {
85943
+ if (slice.base === plan.trunk || slice.dependsOn.length === 0)
85944
+ return null;
85945
+ const deps = slice.dependsOn.map((id) => plan.slices.find((other) => other.id === id));
85946
+ if (deps.some((dep) => !dep))
85947
+ return null;
85948
+ const trunk = trunkRef(cwd, plan.trunk);
85949
+ if (!trunk)
85950
+ return null;
85951
+ const ledger = readLedger(cwd);
85952
+ const rows = ledger.ok ? ledger.value.rows : [];
85953
+ const ctx = { cwd, trunk, forge: forgeBreaker(forge?.() ?? null).forge };
85954
+ const landed = [];
85955
+ for (const dep of deps) {
85956
+ const landing = detectLanding(ctx, dep, {
85957
+ head: branchRef(cwd, dep.branch),
85958
+ createdFrom: sliceStart(cwd, rows, plan.name, dep),
85959
+ parentHead: null,
85960
+ fanout: plan.name,
85961
+ since: dep.hashSince || plan.createdAt || null,
85962
+ sliceHash: dep.hash || null,
85963
+ rows
85964
+ });
85965
+ if (!landing.landed)
85966
+ return null;
85967
+ landed.push(`${dep.branch} landed (${landing.how ?? "pr_merged"}${landing.pr ? ` #${landing.pr.number}` : ""})`);
85968
+ }
85969
+ return `${landed.join(", ")}: started from ${plan.trunk}, not from ${slice.base}'s pre-merge tip`;
85970
+ }
85605
85971
  function releaseSliceWorktree(cwd, plan, input) {
85606
85972
  const found = findSlice(plan, input.slice);
85607
85973
  if (!found.ok)
@@ -85744,12 +86110,13 @@ var SLOW, SCRATCH_DIR = ".workit-scratch", EXCLUDE_LINE, DIRTY_ROW_BYTES = 2000,
85744
86110
  } catch {
85745
86111
  return null;
85746
86112
  }
85747
- }, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
86113
+ }, STALE_SCAN = 5000, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
85748
86114
  `)[0] || "unknown error"}`);
85749
86115
  var init_fanout_worktree = __esm(() => {
85750
86116
  init_ops();
85751
86117
  init_rev();
85752
86118
  init_ledger();
86119
+ init_fanout_landed();
85753
86120
  init_worktree_confine();
85754
86121
  init_worktree_deps();
85755
86122
  init_fanout();
@@ -87382,7 +87749,7 @@ var init_git2 = __esm(() => {
87382
87749
  });
87383
87750
 
87384
87751
  // packages/workit-core/src/forge/verify.ts
87385
- import { spawnSync as spawnSync27 } from "node:child_process";
87752
+ import { spawnSync as spawnSync28 } from "node:child_process";
87386
87753
  function finish(cwd, report, actor) {
87387
87754
  const delivered = report.observations.length > 0 && report.observations.every((entry) => entry.ok);
87388
87755
  const out = { ...report, delivered };
@@ -87595,7 +87962,7 @@ var NPM_NAME, SEMVER2, SAFE_ARG, systemNpm = (args) => {
87595
87962
  };
87596
87963
  const found = process.platform === "win32" ? findExecutable("npm.cmd", process.env) : null;
87597
87964
  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);
87965
+ const result = process.platform === "win32" ? spawnSync28(`${npmCmd} ${args.join(" ")}`, { ...options, shell: true }) : spawnSync28("npm", [...args], options);
87599
87966
  return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
87600
87967
  }, short3 = (sha) => sha ? sha.slice(0, 12) : "(absent)", npmView = (stdout) => {
87601
87968
  try {
@@ -87820,7 +88187,7 @@ function planReleaseFinish(cwd, input) {
87820
88187
  return failure3("not_found", `${name} exists neither locally nor on ${remote ?? "a remote"}`);
87821
88188
  if (!tracked || name === openRelease)
87822
88189
  continue;
87823
- if (!isAncestor3(cwd, tracked, local))
88190
+ if (!isAncestor4(cwd, tracked, local))
87824
88191
  return failure3("blocked", `branch_behind: local ${name} is behind or diverged from ${remote}/${name}; git-flow finishes only onto up-to-date branches`, `git switch ${name} && git merge --ff-only ${remote}/${name} # then: workit release finish ${version}`);
87825
88192
  const productionTracked = remote ? resolveRef(cwd, `refs/remotes/${remote}/${line.production}`) : null;
87826
88193
  const foreign = git5(cwd, [
@@ -87830,16 +88197,16 @@ function planReleaseFinish(cwd, input) {
87830
88197
  `^${releaseTip}`,
87831
88198
  ...productionTracked ? [`^${productionTracked}`] : []
87832
88199
  ]).stdout.split(`
87833
- `).filter(Boolean).filter((sha) => !(git5(cwd, ["rev-parse", "-q", "--verify", `${sha}^2`]).ok && isAncestor3(cwd, releaseTip, sha)));
88200
+ `).filter(Boolean).filter((sha) => !(git5(cwd, ["rev-parse", "-q", "--verify", `${sha}^2`]).ok && isAncestor4(cwd, releaseTip, sha)));
87834
88201
  if (foreign.length)
87835
88202
  return failure3("blocked", `branch_ahead: local ${name} has ${foreign.length} commit(s) ${remote}/${name} lacks that are not part of ${branch}; the finish push would publish them`, `land them through review first, or: git switch ${name} && git reset --keep ${remote}/${name} # then: workit release finish ${version}`);
87836
88203
  }
87837
- if (branchTip && remoteBranch && !isAncestor3(cwd, remoteBranch, branchTip))
88204
+ if (branchTip && remoteBranch && !isAncestor4(cwd, remoteBranch, branchTip))
87838
88205
  return failure3("blocked", `release_behind: ${remote}/${branch} has commits local ${branch} lacks`, `git switch ${branch} && git merge --ff-only ${remote}/${branch}`);
87839
88206
  const production = resolveRef(cwd, `refs/heads/${line.production}`);
87840
- const merged = isAncestor3(cwd, releaseTip, production);
88207
+ const merged = isAncestor4(cwd, releaseTip, production);
87841
88208
  const localTag = resolveRef(cwd, `refs/tags/${tag}`);
87842
- if (localTag && !(isAncestor3(cwd, localTag, production) && isAncestor3(cwd, releaseTip, localTag)))
88209
+ if (localTag && !(isAncestor4(cwd, localTag, production) && isAncestor4(cwd, releaseTip, localTag)))
87843
88210
  return failure3("blocked", `tag_exists: ${tag} (${localTag.slice(0, 12)}) is not this release's merge on ${line.production}`, "a release tag never moves; finish under the next version");
87844
88211
  if (remote) {
87845
88212
  const remoteTag = remoteRefTip(cwd, remote, `refs/tags/${tag}`);
@@ -87853,7 +88220,7 @@ function planReleaseFinish(cwd, input) {
87853
88220
  tagged: localTag !== null,
87854
88221
  mergedBack: Object.fromEntries(mergeBack.map((name) => [
87855
88222
  name,
87856
- localTag !== null && isAncestor3(cwd, localTag, resolveRef(cwd, `refs/heads/${name}`))
88223
+ localTag !== null && isAncestor4(cwd, localTag, resolveRef(cwd, `refs/heads/${name}`))
87857
88224
  ])),
87858
88225
  branchDeleted: branchTip === null
87859
88226
  };
@@ -88015,7 +88382,7 @@ var VERSION, PLACEHOLDER4, validVersion = (version) => VERSION.test(version) &&
88015
88382
  return ledger.value.rows.filter((row) => row.type.startsWith("release."));
88016
88383
  }, record2 = (cwd, actor, row) => {
88017
88384
  appendObserved(cwd, { ...row, actor });
88018
- }, KINDS, isAncestor3 = (cwd, ancestor, descendant) => git5(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok, switchTo = (cwd, branch) => {
88385
+ }, KINDS, isAncestor4 = (cwd, ancestor, descendant) => git5(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok, switchTo = (cwd, branch) => {
88019
88386
  if (currentBranch(cwd) === branch)
88020
88387
  return success2(undefined);
88021
88388
  const switched = git5(cwd, ["switch", "-q", branch]);
@@ -88481,24 +88848,25 @@ async function status3(argv, io) {
88481
88848
  ...data.next ? [`next: ${data.next}`] : []
88482
88849
  ]);
88483
88850
  }
88484
- async function sync(argv, io) {
88851
+ async function sync(argv, io, restack = false) {
88852
+ const usageText = restack ? RESTACK_USAGE : SYNC_USAGE;
88485
88853
  const flags = parseFlags(argv, {
88486
88854
  name: "value",
88487
- local: "boolean",
88855
+ ...restack ? {} : { local: "boolean" },
88488
88856
  "dry-run": "boolean",
88489
88857
  force: "list"
88490
88858
  });
88491
88859
  if (typeof flags === "string")
88492
- return usage4(io, flags, SYNC_USAGE);
88860
+ return usage4(io, flags, usageText);
88493
88861
  if (flags.positionals.length)
88494
- return usage4(io, `unexpected argument ${flags.positionals[0]}`, SYNC_USAGE);
88862
+ return usage4(io, `unexpected argument ${flags.positionals[0]}`, usageText);
88495
88863
  const selected = selectStack(io.cwd, flags.values.name ?? null);
88496
88864
  if (!selected.ok)
88497
88865
  return stackFailed(io, selected);
88498
88866
  const force = new Set(flags.lists.force ?? []);
88499
88867
  const unknown = [...force].filter((branch) => !selected.data.branches.some((entry) => entry.branch === branch));
88500
88868
  if (unknown.length)
88501
- return usage4(io, `--force ${unknown[0]}: not a branch of stack ${selected.data.name}`, SYNC_USAGE);
88869
+ return usage4(io, `--force ${unknown[0]}: not a branch of stack ${selected.data.name}`, usageText);
88502
88870
  const local = flags.booleans.has("local");
88503
88871
  let ctx = null;
88504
88872
  if (!local) {
@@ -88517,14 +88885,23 @@ async function sync(argv, io) {
88517
88885
  const fresh = readStack(io.cwd, name);
88518
88886
  if (!fresh.ok)
88519
88887
  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));
88888
+ const stack = fresh.data ?? selected.data;
88889
+ const dryRun = flags.booleans.has("dry-run");
88890
+ const synced = syncStack(io.cwd, ctx, stack, { publish: !local, dryRun, force }, actorFromEnv(io.env));
88891
+ if (!synced.ok || !restack)
88892
+ return synced;
88893
+ return {
88894
+ ok: true,
88895
+ data: {
88896
+ ...synced.data,
88897
+ pruned: pruneMergedBranches(io.cwd, ctx, stack, { dryRun, actor: actorFromEnv(io.env) })
88898
+ }
88899
+ };
88525
88900
  });
88526
88901
  if (!result.ok)
88527
88902
  return stackFailed(io, result);
88903
+ if (restack)
88904
+ return emit(io, ok2(result.data), renderRestack);
88528
88905
  return emit(io, ok2(result.data), renderSync);
88529
88906
  }
88530
88907
  async function land(argv, io) {
@@ -88611,11 +88988,13 @@ async function run20(argv, io) {
88611
88988
  return status3(rest, io);
88612
88989
  if (sub === "sync")
88613
88990
  return sync(rest, io);
88991
+ if (sub === "restack")
88992
+ return sync(rest, io, true);
88614
88993
  if (sub === "land")
88615
88994
  return land(rest, io);
88616
88995
  return usage4(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE13);
88617
88996
  }
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, {
88997
+ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", RESTACK_USAGE = "workit stack restack [--name <n>] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--unverified --reason <why>] [--base-red-ok --reason <why>] [--timeout 20m] [--interval 30s] [--json]", USAGE13 = "workit stack plan|status|sync|restack|land ... (workit help stack)", short5 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail2(result.code, result.error, {
88619
88998
  unblock: result.unblock,
88620
88999
  data: result.data ?? {}
88621
89000
  })), label3 = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
@@ -88626,6 +89005,10 @@ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<b
88626
89005
  step.push ? step.push.pushed ? `pushed${step.push.forced ? " (lease)" : ""}` : "remote current" : "",
88627
89006
  step.retarget ? `retargeted ${step.retarget.from} -> ${step.retarget.to}` : ""
88628
89007
  ].filter(Boolean).join(" "))
89008
+ ], renderRestack = (data) => [
89009
+ ...renderSync(data),
89010
+ ...data.pruned.deleted.map((item) => `${data.dryRun ? "would delete" : "deleted"} merged branch ${item.branch} at ${item.sha} (local; restore: git branch ${item.branch} ${item.sha})`),
89011
+ ...data.pruned.kept.map((item) => `kept merged branch ${item.branch}: ${item.reason}`)
88629
89012
  ], METHODS3;
88630
89013
  var init_stack2 = __esm(() => {
88631
89014
  init_ledger();
@@ -88665,6 +89048,7 @@ function renderBrief(cwd, plan, input) {
88665
89048
  const planned = `${slicePath(cwd, slice)}${input.attempt === null ? "" : `+try${input.attempt}`}`;
88666
89049
  const made = ledger.value.rows.findLast((row) => row.type === "fanout.worktree.created" && row.fanout === plan.name && row.slice === slice.id && typeof row.path === "string" && samePath(row.path, planned));
88667
89050
  const fresh = Boolean(head) && made?.baseSha === head;
89051
+ const sliceBase = typeof made?.startBase === "string" ? made.startBase : slice.base;
88668
89052
  const mode = input.mode ?? (head && !fresh ? "resume" : "new");
88669
89053
  if (mode === "resume" && !head)
88670
89054
  return fanoutFail("not_found", `MODE resume needs the branch ${branch}, which does not exist here`, `workit fanout brief ${slice.id} --mode new`);
@@ -88693,13 +89077,13 @@ function renderBrief(cwd, plan, input) {
88693
89077
  `GOAL: ${slice.goal}`,
88694
89078
  `SCOPE: ${slice.scope.join(", ")}`,
88695
89079
  ...slice.owns.length ? [` owns: ${slice.owns.join(", ")}`] : [],
88696
- ` branch: ${branch} base: ${slice.base}`,
89080
+ ` branch: ${branch} base: ${sliceBase}`,
88697
89081
  `CONTEXT: ${slice.context ?? "none given: read the files in SCOPE and the neighbours they import"}`,
88698
89082
  ...slice.issues.length ? [
88699
89083
  `ISSUE: ${slice.issues.join(", ")} (\`workit pr create\` on ${branch} links ${slice.issues.length === 1 ? "it" : "them"} by itself; no --issue needed)`
88700
89084
  ] : [],
88701
89085
  ...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)`
89086
+ ` resume: continue ${branch} from ${head.slice(0, 12)}; read \`git log ${sliceBase}..${branch}\` and \`workit ledger list --branch ${branch}\` (the last report, verdicts, rulings)`
88703
89087
  ] : [],
88704
89088
  "ACCEPTANCE:",
88705
89089
  ...slice.acceptance.map((line) => ` - ${line}`),
@@ -88722,7 +89106,7 @@ function renderBrief(cwd, plan, input) {
88722
89106
  exists ? `WORKTREE: ${planned} (made by \`workit fanout worktree create ${slice.id}\`; yours alone)` : `WORKTREE: ${planned} (not made yet: the lead runs \`workit fanout worktree create ${slice.id}\` before spawning, then renders this brief again)`,
88723
89107
  ...exists && typeof made?.install === "string" && !fs35.existsSync(path68.join(planned, "node_modules")) ? [`INSTALL: \`${made.install}\` first: the main checkout's node_modules was not linked here`] : [],
88724
89108
  ...identityLines(identity, minted),
88725
- ...rules(mode, branch, slice.base, exists ? planned : null)
89109
+ ...rules(mode, branch, sliceBase, exists ? planned : null)
88726
89110
  ];
88727
89111
  return {
88728
89112
  ok: true,
@@ -88732,7 +89116,7 @@ function renderBrief(cwd, plan, input) {
88732
89116
  mode,
88733
89117
  attempt: input.attempt,
88734
89118
  branch,
88735
- base: slice.base,
89119
+ base: sliceBase,
88736
89120
  tier: slice.tier,
88737
89121
  issues: slice.issues,
88738
89122
  model: model?.model ?? null,
@@ -88797,211 +89181,6 @@ var init_fanout_brief = __esm(() => {
88797
89181
  ];
88798
89182
  });
88799
89183
 
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
89184
  // packages/workit-core/src/fanout-check.ts
89006
89185
  function mergeConflicts(cwd, a, b) {
89007
89186
  const run = gitRun4(cwd, [
@@ -89184,7 +89363,9 @@ function inspect(cwd, plan, slice, ctx, changedBy) {
89184
89363
  return check;
89185
89364
  }
89186
89365
  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);
89366
+ const parentLanded = parent ? ctx.landings.get(parent.id)?.landed === true : false;
89367
+ const parentRef = parent ? tips.get(parent.id) ?? ctx.landings.get(parent.id)?.landedHead ?? null : null;
89368
+ const baseRef = slice.base === plan.trunk ? trunk : parent ? parentLanded && !(parentRef && isAncestor5(cwd, parentRef, head)) ? trunk : parentRef : branchRef(cwd, slice.base);
89188
89369
  if (!SAFE(baseRef)) {
89189
89370
  check.reasons.push(`base ${slice.base} not found`);
89190
89371
  return check;
@@ -89652,7 +89833,8 @@ function worktree(argv, io) {
89652
89833
  if (action === "create") {
89653
89834
  const result = createSliceWorktree(io.cwd, selected.data, {
89654
89835
  slice: flags.positionals[0],
89655
- actor
89836
+ actor,
89837
+ forge: () => forgeFor(io, false).forge
89656
89838
  });
89657
89839
  if (!result.ok)
89658
89840
  return failed(io, result);
@@ -107587,6 +107769,11 @@ var VERBS = [
107587
107769
  usage: "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]…",
107588
107770
  summary: "Restack onto the moved bases, lease-push and retarget the PRs"
107589
107771
  },
107772
+ {
107773
+ name: "restack",
107774
+ usage: "workit stack restack [--name <n>] [--dry-run] [--force <branch>]…",
107775
+ summary: "After a parent merged (squash too): sync, then delete the merged stack branches locally"
107776
+ },
107590
107777
  {
107591
107778
  name: "land",
107592
107779
  usage: "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--unverified --reason <why>] [--base-red-ok --reason <why>] [--timeout 20m] [--interval 30s]",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-claude-code",
3
- "version": "8.38.5",
3
+ "version": "8.39.0",
4
4
  "private": false,
5
5
  "description": "Workit Claude Code plugin — session and per-turn task context, branch policy on git shell commands, workit method skills, and verifier/reviewer/implementer agents",
6
6
  "keywords": [
@@ -39,8 +39,8 @@
39
39
  "build": "bun scripts/build.ts"
40
40
  },
41
41
  "devDependencies": {
42
- "@brainervirus/workit-cli": "^8.38.5",
43
- "@brainervirus/workit-core": "^8.38.5"
42
+ "@brainervirus/workit-cli": "^8.39.0",
43
+ "@brainervirus/workit-core": "^8.39.0"
44
44
  },
45
45
  "engines": {
46
46
  "node": ">=24"