@brainervirus/workit-claude-code 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/workit.js CHANGED
@@ -65,7 +65,7 @@ var package_default;
65
65
  var init_package = __esm(() => {
66
66
  package_default = {
67
67
  name: "@brainervirus/workit-cli",
68
- version: "2.7.0",
68
+ version: "2.8.0",
69
69
  private: false,
70
70
  description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
71
71
  keywords: [
@@ -13539,6 +13539,11 @@ function worktreeSignal(cwd, options = {}) {
13539
13539
  }
13540
13540
  return `sig:${hash.digest("hex")}`;
13541
13541
  }
13542
+ function mergeBase(cwd, a, b) {
13543
+ if (!safeArg(a) || !safeArg(b))
13544
+ return null;
13545
+ return line(git2(cwd, ["merge-base", a, b]));
13546
+ }
13542
13547
  function patchId(cwd, base, head) {
13543
13548
  if (!safeArg(base) || !safeArg(head))
13544
13549
  return null;
@@ -20350,6 +20355,27 @@ function effectiveOf(rows, authors) {
20350
20355
  }
20351
20356
  return effective;
20352
20357
  }
20358
+ function restackCarries(rows, branch, from, to) {
20359
+ if (!from || !to)
20360
+ return false;
20361
+ const edges = new Map;
20362
+ for (const row of rows)
20363
+ if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
20364
+ edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
20365
+ const seen = new Set([from]);
20366
+ const queue = [from];
20367
+ while (queue.length) {
20368
+ const next = queue.shift();
20369
+ if (next === to)
20370
+ return true;
20371
+ for (const target of edges.get(next) ?? [])
20372
+ if (!seen.has(target)) {
20373
+ seen.add(target);
20374
+ queue.push(target);
20375
+ }
20376
+ }
20377
+ return false;
20378
+ }
20353
20379
  function checkVerdicts(cwd, branch, rows) {
20354
20380
  const head = branchHead(cwd, branch);
20355
20381
  const fallbackBase = defaultBase2(cwd);
@@ -20376,7 +20402,9 @@ function checkVerdicts(cwd, branch, rows) {
20376
20402
  const row = effectiveOf(list, authors);
20377
20403
  if (!row)
20378
20404
  continue;
20379
- const basis = verdictBasis(row, { head, ...keyFor(row.base ?? fallbackBase) });
20405
+ let basis = verdictBasis(row, { head, ...keyFor(row.base ?? fallbackBase) });
20406
+ if (basis === "stale" && restackCarries(rows, branch, row.head, head))
20407
+ basis = "carried";
20380
20408
  const current = basis !== "stale";
20381
20409
  const independence = independenceReasons(row, authors);
20382
20410
  const reasons = [
@@ -76857,6 +76885,8 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
76857
76885
  });
76858
76886
  if (doc.next !== "READY")
76859
76887
  return refuse(`not_ready: ${label} is ${doc.next} (${doc.blockers.join(", ") || doc.next.toLowerCase()})`, NEXT_HINTS[doc.next] ?? "workit pr status", { reason: "not_ready", next: doc.next, blockers: doc.blockers });
76888
+ if (input.expect && (doc.base !== input.expect.base || doc.head.branch !== input.expect.branch))
76889
+ return refuse(`base_mismatch: ${label} is ${doc.head.branch} -> ${doc.base}, expected ${input.expect.branch} -> ${input.expect.base}`, "workit stack sync # retarget it, then land again", { reason: "base_mismatch" });
76860
76890
  const branch = doc.head.branch;
76861
76891
  const head = doc.head.sha;
76862
76892
  if (input.deleteBranch) {
@@ -77951,7 +77981,25 @@ async function status(argv, io) {
77951
77981
  });
77952
77982
  if (!report.ok)
77953
77983
  return forgeFail(io, report);
77954
- return emit(io, ok(report.data.doc), renderStatus);
77984
+ const doc = report.data.doc;
77985
+ return emit(io, ok({ ...doc, verdict: verdictBlock(io.cwd, doc.head.branch) }), (data) => {
77986
+ const lines = renderStatus(data);
77987
+ lines.splice(lines.length - 1, 0, data.verdict ? `verdict: ${data.verdict.accepted ? `accepted (${data.verdict.basis})` : `not accepted (${data.verdict.reasons.join(", ") || data.verdict.basis})`}` : "verdict: unknown (ledger unreadable)");
77988
+ return lines;
77989
+ });
77990
+ }
77991
+ function verdictBlock(cwd, branch) {
77992
+ const ledger = readLedger(cwd);
77993
+ if (!ledger.ok)
77994
+ return null;
77995
+ const check = checkVerdicts(cwd, branch, ledger.value.rows);
77996
+ return {
77997
+ accepted: check.accepted.accepted,
77998
+ basis: check.current.basis,
77999
+ reasons: check.accepted.reasons,
78000
+ head: check.head,
78001
+ verdictId: check.accepted.accepted && typeof check.accepted.verdict?.id === "string" ? check.accepted.verdict.id : null
78002
+ };
77955
78003
  }
77956
78004
  async function create4(argv, io) {
77957
78005
  const flags = parseFlags(argv, {
@@ -78732,12 +78780,1206 @@ var init_verify_delivery = __esm(() => {
78732
78780
  };
78733
78781
  });
78734
78782
 
78735
- // packages/workit-cli/src/verbs/stub.ts
78736
- var notImplemented = (verb, slice) => async (argv, io) => emit(io, fail("not_implemented", `${verb} is not implemented yet (planned in ${slice})`, {
78737
- data: { verb, subcommand: argv.find((arg) => !arg.startsWith("-")) ?? null, slice }
78738
- }));
78739
- var init_stub = __esm(() => {
78740
- init_output();
78783
+ // packages/workit-core/src/stack.ts
78784
+ import { spawnSync as spawnSync21 } from "node:child_process";
78785
+ import { createHash as createHash15, randomBytes as randomBytes3 } from "node:crypto";
78786
+ import fs23 from "node:fs";
78787
+ import path42 from "node:path";
78788
+ function worktreeOf(cwd, branch) {
78789
+ const list = gitRun2(cwd, ["worktree", "list", "--porcelain"]);
78790
+ if (!list.ok)
78791
+ return null;
78792
+ let current = null;
78793
+ for (const line of list.stdout.split(`
78794
+ `)) {
78795
+ if (line.startsWith("worktree "))
78796
+ current = line.slice("worktree ".length);
78797
+ else if (line === `branch refs/heads/${branch}` && current)
78798
+ return current;
78799
+ else if (line === "detached" && current && rebasingBranch(current) === `refs/heads/${branch}`)
78800
+ return current;
78801
+ }
78802
+ return null;
78803
+ }
78804
+ function rebasingBranch(dir) {
78805
+ for (const name of ["rebase-merge/head-name", "rebase-apply/head-name"]) {
78806
+ const found = gitRun2(dir, ["rev-parse", "--path-format=absolute", "--git-path", name]);
78807
+ if (!found.ok)
78808
+ continue;
78809
+ try {
78810
+ return fs23.readFileSync(found.stdout.trim(), "utf8").trim();
78811
+ } catch {}
78812
+ }
78813
+ return null;
78814
+ }
78815
+ function rebaseInProgress(dir) {
78816
+ for (const name of ["rebase-merge", "rebase-apply"]) {
78817
+ const found = gitRun2(dir, ["rev-parse", "--path-format=absolute", "--git-path", name]);
78818
+ if (found.ok && fs23.existsSync(found.stdout.trim()))
78819
+ return true;
78820
+ }
78821
+ return false;
78822
+ }
78823
+ function gitRestack(cwd, input) {
78824
+ const owner = worktreeOf(cwd, input.branch);
78825
+ let dir = owner;
78826
+ if (!dir) {
78827
+ const scratch = scratchDir(cwd);
78828
+ if (!scratch)
78829
+ return { ok: false, conflict: false, worktree: cwd, error: "no store root" };
78830
+ fs23.mkdirSync(scratch, { recursive: true });
78831
+ dir = path42.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes3(4).toString("hex")}`);
78832
+ const added = gitRun2(cwd, ["worktree", "add", "-q", dir, input.branch], GIT_TIMEOUTS.worktree);
78833
+ if (!added.ok)
78834
+ return { ok: false, conflict: false, worktree: cwd, error: firstLine(added) };
78835
+ }
78836
+ const run = gitRun2(dir, [
78837
+ "-c",
78838
+ "rebase.autoStash=false",
78839
+ "-c",
78840
+ "rebase.updateRefs=false",
78841
+ "rebase",
78842
+ "--no-autosquash",
78843
+ "--onto",
78844
+ input.onto,
78845
+ input.from
78846
+ ], stackDeps.rebaseTimeoutMs);
78847
+ if (!run.ok) {
78848
+ if (run.timedOut)
78849
+ return {
78850
+ ok: false,
78851
+ conflict: false,
78852
+ timedOut: true,
78853
+ worktree: dir,
78854
+ error: `git rebase did not finish within ${stackDeps.rebaseTimeoutMs} ms`
78855
+ };
78856
+ if (rebaseInProgress(dir))
78857
+ return { ok: false, conflict: true, worktree: dir, error: firstLine(run) };
78858
+ if (!owner)
78859
+ gitRun2(cwd, ["worktree", "remove", "--force", dir]);
78860
+ return { ok: false, conflict: false, worktree: dir, error: firstLine(run) };
78861
+ }
78862
+ if (!owner)
78863
+ gitRun2(cwd, ["worktree", "remove", "--force", dir]);
78864
+ const head = branchTip(cwd, input.branch);
78865
+ return head ? { ok: true, head } : { ok: false, conflict: false, worktree: dir, error: `${input.branch} vanished` };
78866
+ }
78867
+ function pruneScratch(cwd) {
78868
+ const scratch = scratchDir(cwd);
78869
+ if (!scratch || !fs23.existsSync(scratch))
78870
+ return;
78871
+ for (const name of fs23.readdirSync(scratch)) {
78872
+ const dir = path42.join(scratch, name);
78873
+ if (!rebaseInProgress(dir) && !trackedDirt(dir))
78874
+ gitRun2(cwd, ["worktree", "remove", "--force", dir]);
78875
+ }
78876
+ gitRun2(cwd, ["worktree", "prune"]);
78877
+ }
78878
+ function stacksDir(cwd) {
78879
+ const root = storeRoot(cwd);
78880
+ if (!root.ok)
78881
+ return stackFail(root.code, root.error, root.unblock);
78882
+ return { ok: true, data: path42.join(root.value.root, "stacks") };
78883
+ }
78884
+ function parseStack(raw) {
78885
+ if (!isRecord7(raw) || !Array.isArray(raw.branches))
78886
+ return null;
78887
+ const name = str2(raw.name);
78888
+ const trunk = str2(raw.trunk);
78889
+ if (!name || !trunk)
78890
+ return null;
78891
+ const branches = [];
78892
+ for (const item of raw.branches) {
78893
+ if (!isRecord7(item))
78894
+ return null;
78895
+ const branch = str2(item.branch);
78896
+ if (!branch)
78897
+ return null;
78898
+ const merged = isRecord7(item.merged) ? {
78899
+ pr: typeof item.merged.pr === "number" ? item.merged.pr : null,
78900
+ mergeSha: str2(item.merged.mergeSha),
78901
+ at: str2(item.merged.at) ?? ""
78902
+ } : null;
78903
+ branches.push({
78904
+ branch,
78905
+ parent: str2(item.parent) ?? trunk,
78906
+ pr: typeof item.pr === "number" ? item.pr : null,
78907
+ lastHead: str2(item.lastHead),
78908
+ lastParentHead: str2(item.lastParentHead),
78909
+ patchId: str2(item.patchId),
78910
+ merged,
78911
+ ...isRecord7(item.pendingRestack) && str2(item.pendingRestack.from) && str2(item.pendingRestack.tip) && str2(item.pendingRestack.onto) ? {
78912
+ pendingRestack: {
78913
+ from: str2(item.pendingRestack.from),
78914
+ tip: str2(item.pendingRestack.tip),
78915
+ onto: str2(item.pendingRestack.onto)
78916
+ }
78917
+ } : {}
78918
+ });
78919
+ }
78920
+ return {
78921
+ v: typeof raw.v === "number" ? raw.v : STACK_VERSION,
78922
+ name,
78923
+ trunk,
78924
+ forge: raw.forge === "github" || raw.forge === "gitlab" ? raw.forge : null,
78925
+ repo: str2(raw.repo),
78926
+ branches,
78927
+ updatedAt: str2(raw.updatedAt) ?? ""
78928
+ };
78929
+ }
78930
+ function readStack(cwd, name) {
78931
+ const dir = stacksDir(cwd);
78932
+ if (!dir.ok)
78933
+ return dir;
78934
+ const file = path42.join(dir.data, fileFor(name));
78935
+ let text;
78936
+ try {
78937
+ text = fs23.readFileSync(file, "utf8");
78938
+ } catch (error) {
78939
+ if (error.code === "ENOENT")
78940
+ return { ok: true, data: null };
78941
+ return stackFail("unavailable", `cannot read ${file}: ${error.message}`);
78942
+ }
78943
+ let parsed = null;
78944
+ try {
78945
+ parsed = parseStack(JSON.parse(text));
78946
+ } catch {
78947
+ parsed = null;
78948
+ }
78949
+ return parsed ? { ok: true, data: parsed } : stackFail("failed", `stack file ${file} is not a valid stack`, `workit stack plan --name ${name} <branch…> # rebuild it`);
78950
+ }
78951
+ function writeStack(cwd, stack, now = new Date) {
78952
+ const dir = stacksDir(cwd);
78953
+ if (!dir.ok)
78954
+ return dir;
78955
+ const file = path42.join(dir.data, fileFor(stack.name));
78956
+ const temp = `${file}.${process.pid}.tmp`;
78957
+ try {
78958
+ fs23.mkdirSync(dir.data, { recursive: true });
78959
+ stack.updatedAt = now.toISOString();
78960
+ fs23.writeFileSync(temp, `${JSON.stringify(stack, null, 2)}
78961
+ `);
78962
+ fs23.renameSync(temp, file);
78963
+ return { ok: true, data: undefined };
78964
+ } catch (error) {
78965
+ fs23.rmSync(temp, { force: true });
78966
+ return stackFail("unavailable", `cannot write ${file}: ${error.message}`);
78967
+ }
78968
+ }
78969
+ function listStacks(cwd) {
78970
+ const dir = stacksDir(cwd);
78971
+ if (!dir.ok)
78972
+ return dir;
78973
+ let names = [];
78974
+ try {
78975
+ names = fs23.readdirSync(dir.data).filter((name) => name.endsWith(".json"));
78976
+ } catch {
78977
+ return { ok: true, data: [] };
78978
+ }
78979
+ const out = [];
78980
+ for (const name of names) {
78981
+ let parsed = null;
78982
+ try {
78983
+ parsed = parseStack(JSON.parse(fs23.readFileSync(path42.join(dir.data, name), "utf8")));
78984
+ } catch {
78985
+ parsed = null;
78986
+ }
78987
+ if (parsed && fileFor(parsed.name) === name)
78988
+ out.push(parsed);
78989
+ }
78990
+ return { ok: true, data: out.toSorted((a, b) => a.name.localeCompare(b.name)) };
78991
+ }
78992
+ function selectStack(cwd, name) {
78993
+ if (name !== null && !validName(name))
78994
+ return stackFail("invalid_input", "a stack name must be 1-200 printable characters");
78995
+ if (name) {
78996
+ const read = readStack(cwd, name);
78997
+ if (!read.ok)
78998
+ return read;
78999
+ return read.data ? { ok: true, data: read.data } : stackFail("not_found", `no stack named ${name}`, `workit stack plan --name ${name} <branch…>`);
79000
+ }
79001
+ const all = listStacks(cwd);
79002
+ if (!all.ok)
79003
+ return all;
79004
+ const branch = currentBranch(cwd);
79005
+ const holding = all.data.filter((stack) => stack.branches.some((entry) => entry.branch === branch));
79006
+ if (holding.length === 1)
79007
+ return { ok: true, data: holding[0] };
79008
+ if (holding.length === 0 && all.data.length === 1)
79009
+ return { ok: true, data: all.data[0] };
79010
+ return stackFail("not_found", holding.length > 1 || all.data.length > 1 ? `several stacks match; pick one (${(holding.length > 1 ? holding : all.data).map((stack) => stack.name).join(", ")})` : "no stack is planned here", holding.length > 1 || all.data.length > 1 ? "pass --name <stack>" : "workit stack plan [<branch>…] # from the current branch chain by default");
79011
+ }
79012
+ function stackLockStale(raw, age) {
79013
+ let payload = null;
79014
+ try {
79015
+ payload = raw ? JSON.parse(raw) : null;
79016
+ } catch {
79017
+ payload = null;
79018
+ }
79019
+ if (payload === null)
79020
+ return age !== null && age > OWNERLESS_STALE_MS;
79021
+ const owner = classifyLockOwner(payload, age);
79022
+ if (owner.state === "live")
79023
+ return false;
79024
+ if (owner.state === "stale")
79025
+ return true;
79026
+ return age !== null && age > HEARTBEAT_STALE_MS;
79027
+ }
79028
+ function takeLock(lock) {
79029
+ try {
79030
+ fs23.mkdirSync(lock);
79031
+ } catch (error) {
79032
+ if (error.code === "EEXIST")
79033
+ return null;
79034
+ throw error;
79035
+ }
79036
+ const owner = {
79037
+ pid: process.pid,
79038
+ processStart: processStartOf(process.pid),
79039
+ host: localLockHost(),
79040
+ nonce: randomBytes3(8).toString("hex")
79041
+ };
79042
+ fs23.writeFileSync(path42.join(lock, OWNER), JSON.stringify(owner));
79043
+ return owner;
79044
+ }
79045
+ function reapLock(lock, observed) {
79046
+ const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
79047
+ try {
79048
+ fs23.renameSync(lock, tomb);
79049
+ } catch {
79050
+ return false;
79051
+ }
79052
+ if (readOwner(tomb).raw === observed) {
79053
+ fs23.rmSync(tomb, { recursive: true, force: true });
79054
+ return true;
79055
+ }
79056
+ try {
79057
+ fs23.renameSync(tomb, lock);
79058
+ } catch {
79059
+ fs23.rmSync(tomb, { recursive: true, force: true });
79060
+ }
79061
+ return false;
79062
+ }
79063
+ function releaseLock(lock, owner) {
79064
+ const { raw } = readOwner(lock);
79065
+ if (raw !== JSON.stringify(owner))
79066
+ return;
79067
+ const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
79068
+ try {
79069
+ fs23.renameSync(lock, tomb);
79070
+ fs23.rmSync(tomb, { recursive: true, force: true });
79071
+ } catch {}
79072
+ }
79073
+ async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
79074
+ const dir = stacksDir(cwd);
79075
+ if (!dir.ok)
79076
+ return dir;
79077
+ if (!validName(name))
79078
+ return stackFail("invalid_input", "a stack name must be 1-200 printable characters");
79079
+ const lock = path42.join(dir.data, `${fileFor(name)}.lock`);
79080
+ try {
79081
+ fs23.mkdirSync(dir.data, { recursive: true });
79082
+ } catch (error) {
79083
+ return stackFail("unavailable", `cannot create ${dir.data}: ${error.message}`);
79084
+ }
79085
+ const deadline = Date.now() + waitMs;
79086
+ let owner = null;
79087
+ for (;; ) {
79088
+ owner = takeLock(lock);
79089
+ if (owner)
79090
+ break;
79091
+ const seen = readOwner(lock);
79092
+ if (stackLockStale(seen.raw, seen.age) && reapLock(lock, seen.raw))
79093
+ continue;
79094
+ if (Date.now() >= deadline) {
79095
+ let holder = {};
79096
+ try {
79097
+ holder = seen.raw ? JSON.parse(seen.raw) : {};
79098
+ } catch {
79099
+ holder = {};
79100
+ }
79101
+ return stackFail("busy", `stack ${name} is busy: another workit stack command${holder.pid ? ` (pid ${holder.pid} on ${holder.host ?? "?"})` : ""} holds ${lock}`, "retry when it finishes; a lock whose holder stopped heartbeating is reclaimed automatically", { lock, pid: holder.pid ?? null, host: holder.host ?? null });
79102
+ }
79103
+ await new Promise((resolve) => setTimeout(resolve, 25));
79104
+ }
79105
+ const held = owner;
79106
+ const touch = () => {
79107
+ try {
79108
+ const now = new Date;
79109
+ fs23.utimesSync(path42.join(lock, OWNER), now, now);
79110
+ } catch {}
79111
+ };
79112
+ const beat = setInterval(touch, HEARTBEAT_MS);
79113
+ beat.unref?.();
79114
+ const previous = activeBeat;
79115
+ activeBeat = touch;
79116
+ try {
79117
+ return await fn();
79118
+ } finally {
79119
+ activeBeat = previous;
79120
+ clearInterval(beat);
79121
+ releaseLock(lock, held);
79122
+ }
79123
+ }
79124
+ function deriveChain(cwd, trunk, base) {
79125
+ const branch = currentBranch(cwd);
79126
+ if (!branch || branch === trunk)
79127
+ return stackFail("invalid_input", branch ? `you are on the trunk ${trunk}` : "HEAD is detached", "switch to the top branch of the stack, or name the branches: workit stack plan <bottom> … <top>");
79128
+ const refs = gitRun2(cwd, [
79129
+ "for-each-ref",
79130
+ "--format=%(refname:short) %(objectname)",
79131
+ "refs/heads"
79132
+ ]);
79133
+ const head = branchTip(cwd, branch);
79134
+ if (!refs.ok || !head)
79135
+ return stackFail("failed", "cannot list local branches");
79136
+ const below = [];
79137
+ for (const line of refs.stdout.split(`
79138
+ `)) {
79139
+ const [name, sha] = line.trim().split(" ");
79140
+ if (!name || !sha || name === branch || name === trunk)
79141
+ continue;
79142
+ if (sha === head || isAncestor(cwd, sha, base) || !isAncestor(cwd, sha, head))
79143
+ continue;
79144
+ const count = gitRun2(cwd, ["rev-list", "--count", `${base}..${sha}`]);
79145
+ below.push({ branch: name, depth: Number(count.stdout.trim()) || 0 });
79146
+ }
79147
+ below.sort((a, b) => a.depth - b.depth || a.branch.localeCompare(b.branch));
79148
+ return { ok: true, data: [...below.map((entry) => entry.branch), branch] };
79149
+ }
79150
+ function planStack(cwd, resolved, input, existing) {
79151
+ const remote = remoteFor(cwd, resolved, input.trunk);
79152
+ const notes = [];
79153
+ const trunkSha = trunkTip(cwd, remote, input.trunk);
79154
+ if (!trunkSha)
79155
+ return stackFail("not_found", `trunk ${input.trunk} is not available locally`, `git fetch ${remote} ${input.trunk} # or pass --trunk <branch>`);
79156
+ let branches = input.branches;
79157
+ if (branches.length === 0) {
79158
+ if (existing)
79159
+ branches = existing.branches.map((entry) => entry.branch);
79160
+ else {
79161
+ const derived = deriveChain(cwd, input.trunk, trunkSha);
79162
+ if (!derived.ok)
79163
+ return derived;
79164
+ branches = derived.data;
79165
+ }
79166
+ }
79167
+ if (new Set(branches).size !== branches.length)
79168
+ return stackFail("invalid_input", "a branch is listed twice");
79169
+ if (branches.includes(input.trunk))
79170
+ return stackFail("invalid_input", `the trunk ${input.trunk} cannot be in the stack`);
79171
+ for (const branch of branches)
79172
+ if (!safeRef2(branch) || !branchTip(cwd, branch))
79173
+ return stackFail("not_found", `branch ${branch} does not exist locally`, `git switch ${branch} # or git branch ${branch} ${remote}/${branch}`);
79174
+ const previous = new Map((existing?.branches ?? []).map((entry) => [entry.branch, entry]));
79175
+ const entries = [];
79176
+ const checks = [];
79177
+ let parent = input.trunk;
79178
+ let parentSha = trunkSha;
79179
+ for (const branch of branches) {
79180
+ const tip = branchTip(cwd, branch);
79181
+ const before = previous.get(branch);
79182
+ if (before?.merged) {
79183
+ let still = false;
79184
+ if (before.pr && resolved) {
79185
+ const status = resolved.forge.prStatus(before.pr);
79186
+ still = status.ok && status.data.state === "merged";
79187
+ } else
79188
+ still = hasOwnCommits(cwd, tip, before.lastParentHead) && isAncestor(cwd, tip, trunkSha);
79189
+ if (still) {
79190
+ entries.push(before);
79191
+ continue;
79192
+ }
79193
+ notes.push(`${branch} was recorded as merged but is not; it is back in the stack`);
79194
+ }
79195
+ const onParent = isAncestor(cwd, parentSha, tip);
79196
+ let builtOn = parentSha;
79197
+ if (!onParent) {
79198
+ if (before?.lastParentHead && isAncestor(cwd, before.lastParentHead, tip))
79199
+ builtOn = before.lastParentHead;
79200
+ else {
79201
+ const old = oldParentVersion(cwd, parent, parentSha, tip) ?? oldParentLookalike(cwd, parentSha, tip);
79202
+ if (old)
79203
+ return stackFail("blocked", `parent_rewritten: ${branch} is built on an older version of ${parent} (${old.slice(0, 12)}); replaying it from the merge base would bring the old ${parent} commits back`, `git rebase --onto ${parent} ${old} ${branch} # moves only ${branch}'s own commits; then workit stack plan`, { branch, parent, oldParentTip: old });
79204
+ builtOn = mergeBase(cwd, parentSha, tip) ?? parentSha;
79205
+ notes.push(`${branch} is not on ${parent} and has no recorded base; sync will replay every commit since their merge base ${builtOn.slice(0, 12)}`);
79206
+ }
79207
+ }
79208
+ let pr = before?.pr ?? null;
79209
+ let prBase = null;
79210
+ if (resolved) {
79211
+ const found = resolved.forge.findPr(branch, {
79212
+ owner: resolved.headRepo.split("/")[0] ?? null,
79213
+ projectId: resolved.headProjectId,
79214
+ sha: tip
79215
+ });
79216
+ if (found.ok && found.data && found.data.state === "open") {
79217
+ pr = found.data.number;
79218
+ const status = resolved.forge.prStatus(pr);
79219
+ if (status.ok)
79220
+ prBase = status.data.base;
79221
+ } else if (!found.ok)
79222
+ notes.push(`${branch}: PR lookup failed (${found.error})`);
79223
+ }
79224
+ entries.push({
79225
+ ...before?.pendingRestack ? { pendingRestack: before.pendingRestack } : {},
79226
+ branch,
79227
+ parent,
79228
+ pr,
79229
+ lastHead: tip,
79230
+ lastParentHead: builtOn,
79231
+ patchId: patchId(cwd, builtOn, tip),
79232
+ merged: null
79233
+ });
79234
+ checks.push({
79235
+ branch,
79236
+ parent,
79237
+ onParent,
79238
+ pr,
79239
+ prBase,
79240
+ baseMatches: prBase === null ? null : prBase === parent
79241
+ });
79242
+ if (!onParent)
79243
+ notes.push(`${branch} is not on ${parent}; workit stack sync rebases it`);
79244
+ if (prBase !== null && prBase !== parent)
79245
+ notes.push(`${branch}: PR base is ${prBase}, the stack says ${parent}; workit stack sync retargets it`);
79246
+ parent = branch;
79247
+ parentSha = tip;
79248
+ }
79249
+ if (!resolved)
79250
+ notes.push("forge not resolved: PR numbers were not looked up");
79251
+ const name = input.name ?? existing?.name ?? branches[0];
79252
+ return {
79253
+ ok: true,
79254
+ data: {
79255
+ stack: {
79256
+ v: STACK_VERSION,
79257
+ name,
79258
+ trunk: input.trunk,
79259
+ forge: resolved?.forge.kind ?? existing?.forge ?? null,
79260
+ repo: resolved?.forge.repo ?? existing?.repo ?? null,
79261
+ branches: entries,
79262
+ updatedAt: ""
79263
+ },
79264
+ created: existing === null,
79265
+ checks,
79266
+ notes
79267
+ }
79268
+ };
79269
+ }
79270
+ function hasOwnCommits(cwd, tip, base) {
79271
+ if (!base)
79272
+ return false;
79273
+ return tip !== base && !isAncestor(cwd, tip, base);
79274
+ }
79275
+ function oldParentVersion(cwd, parent, parentSha, child) {
79276
+ if (!safeRef2(parent))
79277
+ return null;
79278
+ const log = gitRun2(cwd, ["reflog", "show", "--format=%H", `refs/heads/${parent}`, "--"]);
79279
+ if (!log.ok)
79280
+ return null;
79281
+ for (const sha of log.stdout.split(`
79282
+ `).map((line) => line.trim())) {
79283
+ if (!sha || sha === parentSha)
79284
+ continue;
79285
+ if (isAncestor(cwd, sha, child) && !isAncestor(cwd, sha, parentSha))
79286
+ return sha;
79287
+ }
79288
+ return null;
79289
+ }
79290
+ function commitKeys(cwd, range) {
79291
+ const log = gitRun2(cwd, ["log", "--reverse", "--no-merges", "--format=%H%x00%s", range]);
79292
+ if (!log.ok)
79293
+ return [];
79294
+ const patches = new Map;
79295
+ const diff = gitRun2(cwd, ["log", "--no-merges", "-p", "--format=commit %H", range]);
79296
+ if (diff.ok && diff.stdout.trim()) {
79297
+ const ids = spawnSync21("git", ["patch-id", "--stable"], {
79298
+ cwd,
79299
+ input: diff.stdout,
79300
+ encoding: "utf8",
79301
+ maxBuffer: 64 * 1024 * 1024
79302
+ });
79303
+ for (const line of (ids.stdout ?? "").split(`
79304
+ `)) {
79305
+ const [patch, sha] = line.trim().split(/\s+/u);
79306
+ if (patch && sha)
79307
+ patches.set(sha, patch);
79308
+ }
79309
+ }
79310
+ return log.stdout.split(`
79311
+ `).filter(Boolean).map((line) => {
79312
+ const [sha, subject = ""] = line.split("\x00");
79313
+ return { sha, subject, patch: patches.get(sha) ?? null };
79314
+ });
79315
+ }
79316
+ function oldParentLookalike(cwd, parentSha, child) {
79317
+ const base = mergeBase(cwd, parentSha, child);
79318
+ if (!base)
79319
+ return null;
79320
+ const parentCommits = commitKeys(cwd, `${base}..${parentSha}`);
79321
+ if (parentCommits.length === 0)
79322
+ return null;
79323
+ const patches = new Set(parentCommits.map((commit) => commit.patch).filter(Boolean));
79324
+ const subjects = new Set(parentCommits.map((commit) => commit.subject).filter(Boolean));
79325
+ let last = null;
79326
+ for (const commit of commitKeys(cwd, `${base}..${child}`))
79327
+ if (commit.patch && patches.has(commit.patch) || subjects.has(commit.subject))
79328
+ last = commit.sha;
79329
+ return last;
79330
+ }
79331
+ function statusDoc(ctx, pr) {
79332
+ const status = ctx.resolved.forge.prStatus(pr);
79333
+ if (!status.ok)
79334
+ return status;
79335
+ return buildStatusDoc(ctx.cwd, ctx.resolved, status.data, { logLines: 0, behind: false });
79336
+ }
79337
+ function prBinding(ctx, entry, doc) {
79338
+ const label = noun2(ctx.resolved, entry.pr);
79339
+ if (doc.head.branch !== entry.branch)
79340
+ return `${label} is for ${doc.head.branch}, not ${entry.branch}`;
79341
+ const found = ctx.resolved.forge.findPr(entry.branch, {
79342
+ owner: ctx.resolved.headRepo.split("/")[0] ?? null,
79343
+ projectId: ctx.resolved.headProjectId,
79344
+ sha: null
79345
+ });
79346
+ if (!found.ok)
79347
+ return `cannot confirm ${label} belongs to ${entry.branch}: ${found.error}`;
79348
+ if (found.data?.number !== entry.pr)
79349
+ return `${entry.branch} from ${ctx.resolved.headRepo} is ${found.data ? noun2(ctx.resolved, found.data.number) : "without a PR"}, not ${label}`;
79350
+ return null;
79351
+ }
79352
+ function repoMismatch(ctx, stack) {
79353
+ const { kind, repo } = ctx.resolved.forge;
79354
+ if (stack.repo && stack.repo !== repo || stack.forge && stack.forge !== kind)
79355
+ return stackFail("blocked", `repo_mismatch: stack ${stack.name} was planned on ${stack.forge ?? "?"} ${stack.repo ?? "?"}, but this checkout resolves to ${kind} ${repo}`, `workit stack plan --name ${stack.name} <branch…> # re-plan it here, or run from the right checkout`, { planned: { forge: stack.forge, repo: stack.repo }, resolved: { forge: kind, repo } });
79356
+ return null;
79357
+ }
79358
+ function qualify(ctx, stack, entry, doc, verdict, grant, binding = null) {
79359
+ const label = entry.pr ? noun2(ctx.resolved, entry.pr) : entry.branch;
79360
+ const stop = (reason, detail, unblock, ready = false) => ({
79361
+ ok: false,
79362
+ reason,
79363
+ detail,
79364
+ unblock,
79365
+ ready
79366
+ });
79367
+ if (!entry.pr || !doc)
79368
+ return stop("no_pr", `${entry.branch} has no open PR`, `git switch ${entry.branch} && workit pr create --fill --base ${entry.parent} # then workit stack plan`);
79369
+ if (binding)
79370
+ return stop("pr_mismatch", binding, `workit stack plan --name ${stack.name} # re-resolve the PRs`);
79371
+ if (doc.state !== "open")
79372
+ return stop("pr_closed", `${label} is ${doc.state}`, "reopen it, or drop the branch from the stack: workit stack plan <branches…>");
79373
+ if (doc.base !== stack.trunk)
79374
+ return stop("base_not_trunk", `${label} targets ${doc.base}, not ${stack.trunk}`, "workit stack sync");
79375
+ if (doc.next !== "READY")
79376
+ return stop("not_ready", `${label} is ${doc.next}${doc.blockers.length ? ` (${doc.blockers.join(", ")})` : ""}`, doc.next === "WAITING_CI" ? `workit ci wait --pr ${entry.pr}` : `workit pr status --pr ${entry.pr}`);
79377
+ const requireVerdict = grant.allowed ? grant.requireVerdict : true;
79378
+ if (requireVerdict && !verdict.accepted)
79379
+ return stop(verdict.reasons.includes("no_verdict") ? "no_verdict" : "needs_verdict", `${label} has no accepted independent verdict for ${doc.head.sha.slice(0, 12)} (${verdict.reasons.join(", ") || "none"})`, `an independent session verifies ${entry.branch} and runs: workit ledger verdict verified --how "<what was exercised>" --branch ${entry.branch}`);
79380
+ if (!grant.allowed)
79381
+ return stop("grant_required", `${label} is verified and ready, but ${grant.error}`, grant.unblock, true);
79382
+ return { ok: true };
79383
+ }
79384
+ function stackStatus(ctx, stack) {
79385
+ const mismatch = repoMismatch(ctx, stack);
79386
+ if (mismatch)
79387
+ return mismatch;
79388
+ const ledger = readLedger(ctx.cwd);
79389
+ if (!ledger.ok)
79390
+ return stackFail(ledger.code, ledger.error, ledger.unblock);
79391
+ const remote = remoteFor(ctx.cwd, ctx.resolved, stack.trunk);
79392
+ const trunkSha = trunkTip(ctx.cwd, remote, stack.trunk);
79393
+ const grant = requireGrant(ctx.cwd, "merge");
79394
+ const branches = [];
79395
+ let verdict = null;
79396
+ let reason = null;
79397
+ let next = null;
79398
+ let parentName = stack.trunk;
79399
+ let parentSha = trunkSha;
79400
+ let mergedSeen = false;
79401
+ for (const entry of stack.branches) {
79402
+ let doc = null;
79403
+ if (entry.pr) {
79404
+ const read = statusDoc(ctx, entry.pr);
79405
+ if (!read.ok)
79406
+ return fromForge(read);
79407
+ doc = read.data;
79408
+ }
79409
+ const merged = entry.merged !== null || doc?.state === "merged";
79410
+ const head = branchTip(ctx.cwd, entry.branch);
79411
+ const row = {
79412
+ branch: entry.branch,
79413
+ parent: parentName,
79414
+ pr: entry.pr,
79415
+ merged,
79416
+ head,
79417
+ onParent: merged || !head || !parentSha ? null : isAncestor(ctx.cwd, parentSha, head),
79418
+ prState: doc?.state ?? null,
79419
+ prBase: doc?.base ?? null,
79420
+ baseMatches: doc && !merged ? doc.base === parentName : null,
79421
+ next: doc?.next ?? null,
79422
+ checks: doc?.checks.state ?? null,
79423
+ blockers: doc?.blockers ?? [],
79424
+ verdict: merged ? { accepted: true, basis: "none", reasons: [], verdictId: null } : verdictOf(ctx.cwd, entry.branch, ledger.value.rows)
79425
+ };
79426
+ branches.push(row);
79427
+ if (merged) {
79428
+ mergedSeen = true;
79429
+ continue;
79430
+ }
79431
+ if (verdict === null) {
79432
+ const stale = row.onParent === false || row.baseMatches === false;
79433
+ if (mergedSeen && stale) {
79434
+ verdict = "ADVANCE";
79435
+ reason = "a parent merged; the rest is not restacked or retargeted yet";
79436
+ next = "workit stack sync";
79437
+ } else {
79438
+ const q = qualify(ctx, stack, entry, doc, row.verdict, grant, doc ? prBinding(ctx, entry, doc) : null);
79439
+ if (q.ok) {
79440
+ verdict = "READY";
79441
+ next = "workit stack land";
79442
+ } else {
79443
+ verdict = "WAITING";
79444
+ reason = `${q.reason}: ${q.detail}`;
79445
+ next = q.unblock;
79446
+ }
79447
+ }
79448
+ }
79449
+ parentName = entry.branch;
79450
+ parentSha = head;
79451
+ }
79452
+ return {
79453
+ ok: true,
79454
+ data: {
79455
+ name: stack.name,
79456
+ trunk: stack.trunk,
79457
+ verdict: verdict ?? "COMPLETE",
79458
+ reason,
79459
+ next,
79460
+ branches
79461
+ }
79462
+ };
79463
+ }
79464
+ function detectMerged(ctx, cwd, stack, trunkSha, state) {
79465
+ const newly = [];
79466
+ for (const entry of stack.branches) {
79467
+ if (entry.merged)
79468
+ continue;
79469
+ let merged = false;
79470
+ if (entry.pr && ctx) {
79471
+ const read = statusDoc(ctx, entry.pr);
79472
+ if (!read.ok)
79473
+ return fromForge(read);
79474
+ if (read.data.head.branch !== entry.branch)
79475
+ return stackFail("blocked", `pr_mismatch: ${noun2(ctx.resolved, entry.pr)} is for ${read.data.head.branch}, not ${entry.branch}`, `workit stack plan --name ${stack.name} # re-resolve the PRs`, { branch: entry.branch, pr: entry.pr });
79476
+ state.docs.set(entry.pr, read.data);
79477
+ merged = read.data.state === "merged";
79478
+ } else {
79479
+ const tip = branchTip(cwd, entry.branch);
79480
+ merged = tip !== null && hasOwnCommits(cwd, tip, entry.lastParentHead) && isAncestor(cwd, tip, trunkSha);
79481
+ }
79482
+ if (merged) {
79483
+ entry.merged = { pr: entry.pr, mergeSha: null, at: new Date().toISOString() };
79484
+ newly.push({ branch: entry.branch, pr: entry.pr });
79485
+ }
79486
+ }
79487
+ return { ok: true, data: newly };
79488
+ }
79489
+ function pushBranch(cwd, actor, branch, priorTip) {
79490
+ const plan = stackDeps.pushPlan(cwd, branch);
79491
+ if (!plan.ok)
79492
+ return fromForge(plan);
79493
+ const remote = remoteRefTip(cwd, plan.data.rawUrl, `refs/heads/${branch}`);
79494
+ if (!remote.ok)
79495
+ return stackFail(remote.code, remote.error);
79496
+ if (remote.sha === plan.data.sha)
79497
+ return { ok: true, data: { pushed: false, forced: false, sha: plan.data.sha } };
79498
+ const grant = requireGrant(cwd, "push", { forge: !plan.data.local });
79499
+ if (!grant.allowed)
79500
+ return stackFail("blocked", grant.error, grant.unblock, { reason: grant.reason });
79501
+ const force = remote.sha !== null && !isAncestor(cwd, remote.sha, plan.data.sha);
79502
+ const expect = !force ? null : remote.sha === priorTip ? priorTip : recordedRemoteTip(cwd, plan.data) ?? null;
79503
+ const pushed = executePush(cwd, plan.data, { forceWithLease: force, expect, actor });
79504
+ if (!pushed.ok)
79505
+ return { ...fromForge(pushed), data: { branch } };
79506
+ return {
79507
+ ok: true,
79508
+ data: { pushed: pushed.data.pushed, forced: pushed.data.forced, sha: plan.data.sha }
79509
+ };
79510
+ }
79511
+ function syncStack(cwd, ctx, stack, options, actor) {
79512
+ const remote = remoteFor(cwd, ctx?.resolved ?? null, stack.trunk);
79513
+ let observedTrunk = null;
79514
+ if (!options.dryRun && safeRef2(stack.trunk)) {
79515
+ const fetched = fetchRefs(cwd, remote, [
79516
+ `+refs/heads/${stack.trunk}:refs/remotes/${remote}/${stack.trunk}`
79517
+ ]);
79518
+ if (!fetched.ok)
79519
+ return stackFail(fetched.code, fetched.error, `git fetch ${remote} ${stack.trunk}`);
79520
+ } else if (safeRef2(stack.trunk)) {
79521
+ const remoteTrunk = remoteRefTip(cwd, remote, `refs/heads/${stack.trunk}`);
79522
+ if (remoteTrunk.ok && remoteTrunk.sha) {
79523
+ if (!hasCommit(cwd, remoteTrunk.sha))
79524
+ fetchRefs(cwd, remote, [remoteTrunk.sha]);
79525
+ if (hasCommit(cwd, remoteTrunk.sha))
79526
+ observedTrunk = remoteTrunk.sha;
79527
+ }
79528
+ }
79529
+ const trunkSha = observedTrunk ?? trunkTip(cwd, remote, stack.trunk);
79530
+ if (!trunkSha)
79531
+ return stackFail("not_found", `trunk ${stack.trunk} is not available`, `git fetch ${remote} ${stack.trunk}`);
79532
+ if (ctx) {
79533
+ const mismatch = repoMismatch(ctx, stack);
79534
+ if (mismatch)
79535
+ return mismatch;
79536
+ }
79537
+ const state = { docs: new Map };
79538
+ const merged = detectMerged(ctx, cwd, stack, trunkSha, state);
79539
+ if (!merged.ok)
79540
+ return merged;
79541
+ const outcome = {
79542
+ name: stack.name,
79543
+ dryRun: options.dryRun,
79544
+ merged: merged.data,
79545
+ steps: []
79546
+ };
79547
+ pruneScratch(cwd);
79548
+ let parent = { name: stack.trunk, tip: trunkSha };
79549
+ let processed = 0;
79550
+ for (const entry of stack.branches) {
79551
+ if (entry.merged)
79552
+ continue;
79553
+ if (options.limit !== undefined && processed >= options.limit)
79554
+ break;
79555
+ processed += 1;
79556
+ touchStackLock();
79557
+ const tip = branchTip(cwd, entry.branch);
79558
+ if (!tip)
79559
+ return stackFail("not_found", `stack branch ${entry.branch} does not exist locally`, `git branch ${entry.branch} ${remote}/${entry.branch} # then workit stack sync`, { progress: outcome });
79560
+ const step = {
79561
+ branch: entry.branch,
79562
+ pr: entry.pr,
79563
+ parent: parent.name,
79564
+ restack: null,
79565
+ push: null,
79566
+ retarget: null
79567
+ };
79568
+ let newTip = tip;
79569
+ const parentTip = parent.tip;
79570
+ if (parentTip === null) {
79571
+ step.restack = {
79572
+ from: entry.lastParentHead ?? "?",
79573
+ onto: `${parent.name} (restacked)`,
79574
+ head: null,
79575
+ carried: null
79576
+ };
79577
+ newTip = null;
79578
+ } else if (!isAncestor(cwd, parentTip, tip)) {
79579
+ const pending = entry.pendingRestack ?? null;
79580
+ const from = pending && isAncestor(cwd, pending.onto, tip) ? pending.onto : entry.lastParentHead && isAncestor(cwd, entry.lastParentHead, tip) ? entry.lastParentHead : mergeBase(cwd, entry.lastParentHead ?? parentTip, tip) ?? parentTip;
79581
+ if (options.dryRun) {
79582
+ step.restack = { from, onto: parentTip, head: null, carried: null };
79583
+ newTip = null;
79584
+ } else {
79585
+ touchStackLock();
79586
+ const merges = gitRun2(cwd, ["rev-list", "--merges", `${from}..${tip}`]);
79587
+ if (!merges.ok || merges.stdout.trim())
79588
+ return stackFail("blocked", `merge_commits: ${entry.branch} has merge commits since ${from.slice(0, 12)}; restacking would drop what they resolved`, `git rebase --rebase-merges --onto ${parentTip} ${from} ${entry.branch} # check the result, then: workit stack sync`, {
79589
+ branch: entry.branch,
79590
+ merges: merges.stdout.trim().split(`
79591
+ `).filter(Boolean),
79592
+ progress: outcome
79593
+ });
79594
+ const owner = worktreeOf(cwd, entry.branch);
79595
+ if (owner && rebaseInProgress(owner))
79596
+ return stackFail("blocked", `a rebase is in progress in ${owner}`, `cd ${owner} && git rebase --continue # or --abort; then workit stack sync`, { conflict: entry.branch, worktree: owner, progress: outcome });
79597
+ if (owner && trackedDirt(owner))
79598
+ return stackFail("blocked", `dirty_worktree: ${owner} has ${entry.branch} checked out with uncommitted changes; restacking needs it clean`, `commit or stash the changes in ${owner}, then workit stack sync`, { branch: entry.branch, worktree: owner, progress: outcome });
79599
+ entry.pendingRestack = pending ? { ...pending, onto: parentTip } : { from, tip, onto: parentTip };
79600
+ const recorded = writeStack(cwd, stack);
79601
+ if (!recorded.ok)
79602
+ return recorded;
79603
+ const rebased = stackDeps.adapter.restack(cwd, {
79604
+ branch: entry.branch,
79605
+ onto: parentTip,
79606
+ from
79607
+ });
79608
+ touchStackLock();
79609
+ if (!rebased.ok) {
79610
+ outcome.steps.push(step);
79611
+ if (rebased.timedOut)
79612
+ return stackFail("failed", `timed_out: restacking ${entry.branch} onto ${parent.name}: ${rebased.error}`, `finish or abort the rebase in ${rebased.worktree} (git rebase --continue | --abort), then workit stack sync # slow hooks: raise WORKIT_STACK_REBASE_TIMEOUT_MS`, {
79613
+ reason: "timed_out",
79614
+ branch: entry.branch,
79615
+ worktree: rebased.worktree,
79616
+ progress: outcome
79617
+ });
79618
+ return rebased.conflict ? stackFail("blocked", `conflict restacking ${entry.branch} onto ${parent.name}: ${rebased.error}`, `resolve the conflicts in ${rebased.worktree}, git rebase --continue, then workit stack sync # or git rebase --abort`, { conflict: entry.branch, worktree: rebased.worktree, progress: outcome }) : stackFail("failed", `restacking ${entry.branch} failed: ${rebased.error}`, undefined, {
79619
+ branch: entry.branch,
79620
+ progress: outcome
79621
+ });
79622
+ }
79623
+ newTip = rebased.head;
79624
+ step.restack = { from, onto: parentTip, head: newTip, carried: null };
79625
+ }
79626
+ }
79627
+ if (!options.dryRun && newTip) {
79628
+ const check = entry.pendingRestack && parentTip && isAncestor(cwd, parentTip, newTip) ? entry.pendingRestack : null;
79629
+ let forced = false;
79630
+ if (check && parentTip) {
79631
+ const before = {
79632
+ d: diffHash(cwd, check.from, check.tip),
79633
+ commits: commitCount(cwd, check.from, check.tip)
79634
+ };
79635
+ const after = {
79636
+ d: diffHash(cwd, parentTip, newTip),
79637
+ commits: commitCount(cwd, parentTip, newTip)
79638
+ };
79639
+ const sameContent = before.d !== null && before.d === after.d;
79640
+ const dropped = Math.max(0, before.commits - after.commits);
79641
+ if (!sameContent || dropped > 0) {
79642
+ if (!options.force?.has(entry.branch)) {
79643
+ outcome.steps.push(step);
79644
+ const written = writeStack(cwd, stack);
79645
+ if (!written.ok)
79646
+ return written;
79647
+ return stackFail("blocked", `content_changed: ${entry.branch} on ${parent.name} is not the same change it was on ${check.from.slice(0, 12)}${dropped ? ` (${dropped} commit${dropped === 1 ? "" : "s"} dropped as empty or already applied)` : ""}; nothing was pushed`, `inspect: git range-diff ${check.from}..${check.tip} ${parentTip}..${newTip} # if intended: workit stack sync --force ${entry.branch}; to undo: git branch -f ${entry.branch} ${check.tip}`, {
79648
+ reason: "content_changed",
79649
+ branch: entry.branch,
79650
+ before: {
79651
+ base: check.from,
79652
+ head: check.tip,
79653
+ commits: before.commits,
79654
+ stat: shortStat(cwd, check.from, check.tip)
79655
+ },
79656
+ after: {
79657
+ base: parentTip,
79658
+ head: newTip,
79659
+ commits: after.commits,
79660
+ stat: shortStat(cwd, parentTip, newTip)
79661
+ },
79662
+ dropped,
79663
+ progress: outcome
79664
+ });
79665
+ }
79666
+ forced = true;
79667
+ }
79668
+ entry.pendingRestack = null;
79669
+ }
79670
+ const moved = entry.lastHead !== null && entry.lastHead !== newTip && entry.lastParentHead !== null && parentTip !== null && parentTip !== entry.lastParentHead;
79671
+ if (moved && parentTip) {
79672
+ const fromBase = entry.lastParentHead;
79673
+ const fromHead = entry.lastHead;
79674
+ const before = {
79675
+ p: patchId(cwd, fromBase, fromHead),
79676
+ d: diffHash(cwd, fromBase, fromHead)
79677
+ };
79678
+ const after = { p: patchId(cwd, parentTip, newTip), d: diffHash(cwd, parentTip, newTip) };
79679
+ const equal = before.d !== null && before.d === after.d && before.p !== null && before.p === after.p;
79680
+ appendObserved(cwd, {
79681
+ type: "stack.restacked",
79682
+ actor,
79683
+ stack: stack.name,
79684
+ branch: entry.branch,
79685
+ head: newTip,
79686
+ fromHead,
79687
+ fromBase,
79688
+ toBase: parentTip,
79689
+ patchId: after.p,
79690
+ diffHash: after.d,
79691
+ patchEqual: equal,
79692
+ forced
79693
+ });
79694
+ if (step.restack)
79695
+ step.restack.carried = equal;
79696
+ else
79697
+ step.restack = { from: fromBase, onto: parentTip, head: newTip, carried: equal };
79698
+ }
79699
+ if (options.publish) {
79700
+ touchStackLock();
79701
+ const pushed = pushBranch(cwd, actor, entry.branch, entry.lastHead ?? tip);
79702
+ touchStackLock();
79703
+ if (!pushed.ok) {
79704
+ outcome.steps.push(step);
79705
+ return { ...pushed, data: { ...pushed.data, progress: outcome } };
79706
+ }
79707
+ step.push = pushed.data;
79708
+ }
79709
+ entry.lastHead = newTip;
79710
+ entry.lastParentHead = parentTip;
79711
+ entry.patchId = parentTip ? patchId(cwd, parentTip, newTip) : null;
79712
+ }
79713
+ if (options.publish && ctx && entry.pr) {
79714
+ const doc = state.docs.get(entry.pr);
79715
+ if (doc && doc.state === "open" && doc.base !== parent.name) {
79716
+ step.retarget = { from: doc.base, to: parent.name };
79717
+ if (!options.dryRun) {
79718
+ const grant = requireGrant(cwd, "pr");
79719
+ if (!grant.allowed)
79720
+ return stackFail("blocked", grant.error, grant.unblock, {
79721
+ reason: grant.reason,
79722
+ progress: outcome
79723
+ });
79724
+ const updated = ctx.resolved.forge.updateBase(entry.pr, parent.name);
79725
+ if (!updated.ok)
79726
+ return { ...fromForge(updated), data: { branch: entry.branch, progress: outcome } };
79727
+ appendObserved(cwd, {
79728
+ type: "pr.retargeted",
79729
+ actor,
79730
+ stack: stack.name,
79731
+ branch: entry.branch,
79732
+ head: newTip,
79733
+ pr: entry.pr,
79734
+ base: parent.name,
79735
+ previousBase: doc.base
79736
+ });
79737
+ }
79738
+ }
79739
+ }
79740
+ entry.parent = parent.name;
79741
+ outcome.steps.push(step);
79742
+ if (!options.dryRun) {
79743
+ const written = writeStack(cwd, stack);
79744
+ if (!written.ok)
79745
+ return written;
79746
+ }
79747
+ parent = { name: entry.branch, tip: newTip };
79748
+ }
79749
+ if (!options.dryRun) {
79750
+ const written = writeStack(cwd, stack);
79751
+ if (!written.ok)
79752
+ return written;
79753
+ }
79754
+ return { ok: true, data: outcome };
79755
+ }
79756
+ async function awaitChecks(ctx, pr, head, options) {
79757
+ let waited = 0;
79758
+ for (let poll = 0;; poll += 1) {
79759
+ touchStackLock();
79760
+ const doc = statusDoc(ctx, pr);
79761
+ if (!doc.ok)
79762
+ return doc;
79763
+ const verdict = waitVerdict(doc.data, { head, elapsedMs: waited });
79764
+ if (verdict.state !== "waiting" || waited >= options.timeoutMs)
79765
+ return doc;
79766
+ const delay = Math.min(pollDelay(options.intervalMs, poll), Math.max(1, options.timeoutMs - waited));
79767
+ await ctx.sleep(delay);
79768
+ waited += delay;
79769
+ }
79770
+ }
79771
+ async function landStack(ctx, stack, options) {
79772
+ const outcome = {
79773
+ name: stack.name,
79774
+ dryRun: options.dryRun,
79775
+ landed: [],
79776
+ wouldLand: [],
79777
+ stoppedAt: null,
79778
+ retargeted: [],
79779
+ restacked: [],
79780
+ complete: false
79781
+ };
79782
+ const absorb = (sync) => {
79783
+ for (const step of sync.steps) {
79784
+ if (step.retarget && step.pr && !outcome.retargeted.includes(step.pr))
79785
+ outcome.retargeted.push(step.pr);
79786
+ if (step.restack?.head && !outcome.restacked.includes(step.branch))
79787
+ outcome.restacked.push(step.branch);
79788
+ }
79789
+ };
79790
+ const failWith = (error) => ({
79791
+ ...error,
79792
+ data: { ...error.data, land: outcome }
79793
+ });
79794
+ const mismatch = repoMismatch(ctx, stack);
79795
+ if (mismatch)
79796
+ return failWith(mismatch);
79797
+ if (!options.dryRun) {
79798
+ const synced = syncStack(ctx.cwd, ctx, stack, { publish: true, dryRun: false, limit: 1 }, ctx.actor);
79799
+ if (!synced.ok)
79800
+ return failWith(synced);
79801
+ absorb(synced.data);
79802
+ }
79803
+ const ledgerRows = () => {
79804
+ const ledger = readLedger(ctx.cwd);
79805
+ return ledger.ok ? ledger.value.rows : [];
79806
+ };
79807
+ const simulated = new Set;
79808
+ let justRestacked = new Set(outcome.restacked);
79809
+ for (;; ) {
79810
+ touchStackLock();
79811
+ const entry = stack.branches.find((candidate) => !candidate.merged && !simulated.has(candidate.branch));
79812
+ if (!entry) {
79813
+ outcome.complete = !options.dryRun || stack.branches.every((candidate) => candidate.merged || simulated.has(candidate.branch));
79814
+ break;
79815
+ }
79816
+ if (options.max !== null && outcome.landed.length + outcome.wouldLand.length >= options.max) {
79817
+ outcome.stoppedAt = {
79818
+ pr: entry.pr,
79819
+ branch: entry.branch,
79820
+ reason: "max_reached",
79821
+ detail: `--max ${options.max} reached`,
79822
+ unblock: "workit stack land",
79823
+ ready: false
79824
+ };
79825
+ break;
79826
+ }
79827
+ let doc = null;
79828
+ if (entry.pr) {
79829
+ const head = branchTip(ctx.cwd, entry.branch);
79830
+ const read = justRestacked.has(entry.branch) && head ? await awaitChecks(ctx, entry.pr, head, options) : statusDoc(ctx, entry.pr);
79831
+ if (!read.ok)
79832
+ return failWith(fromForge(read));
79833
+ doc = read.data;
79834
+ if (options.dryRun && doc.state === "merged") {
79835
+ simulated.add(entry.branch);
79836
+ continue;
79837
+ }
79838
+ }
79839
+ const grant = requireGrant(ctx.cwd, "merge");
79840
+ const verdict = verdictOf(ctx.cwd, entry.branch, ledgerRows());
79841
+ const effective = options.dryRun && simulated.size > 0 && doc ? { ...doc, base: stack.trunk } : doc;
79842
+ const q = qualify(ctx, stack, entry, effective, verdict, grant, doc ? prBinding(ctx, entry, doc) : null);
79843
+ if (!q.ok) {
79844
+ outcome.stoppedAt = {
79845
+ pr: entry.pr,
79846
+ branch: entry.branch,
79847
+ reason: q.reason,
79848
+ detail: q.detail,
79849
+ unblock: q.unblock,
79850
+ ready: q.ready
79851
+ };
79852
+ break;
79853
+ }
79854
+ const pr = entry.pr;
79855
+ if (options.dryRun) {
79856
+ outcome.wouldLand.push({ pr, branch: entry.branch });
79857
+ simulated.add(entry.branch);
79858
+ continue;
79859
+ }
79860
+ const merged = await mergePullRequest(ctx.cwd, ctx.resolved, {
79861
+ pr,
79862
+ method: options.method,
79863
+ deleteBranch: false,
79864
+ actor: ctx.actor,
79865
+ expect: { base: stack.trunk, branch: entry.branch }
79866
+ }, ctx.sleep);
79867
+ if (!merged.ok) {
79868
+ if (merged.code === "blocked" && "refusal" in merged && merged.refusal) {
79869
+ outcome.stoppedAt = {
79870
+ pr,
79871
+ branch: entry.branch,
79872
+ reason: "merge_refused",
79873
+ detail: merged.error,
79874
+ unblock: merged.unblock ?? `workit pr status --pr ${pr}`,
79875
+ ready: false
79876
+ };
79877
+ break;
79878
+ }
79879
+ return failWith(stackFail(merged.code, merged.error, merged.unblock));
79880
+ }
79881
+ outcome.landed.push({ pr, branch: entry.branch, mergeSha: merged.data.mergeSha });
79882
+ entry.merged = { pr, mergeSha: merged.data.mergeSha, at: new Date().toISOString() };
79883
+ const written = writeStack(ctx.cwd, stack);
79884
+ if (!written.ok)
79885
+ return failWith(written);
79886
+ const before = new Set(outcome.restacked);
79887
+ const synced = syncStack(ctx.cwd, ctx, stack, { publish: true, dryRun: false, limit: 1 }, ctx.actor);
79888
+ if (!synced.ok)
79889
+ return failWith(synced);
79890
+ absorb(synced.data);
79891
+ justRestacked = new Set(outcome.restacked.filter((branch) => !before.has(branch)));
79892
+ for (const step of synced.data.steps)
79893
+ if (step.restack?.head)
79894
+ justRestacked.add(step.branch);
79895
+ }
79896
+ return { ok: true, data: outcome };
79897
+ }
79898
+ var STACK_VERSION = 1, stackFail = (code, error, unblock, data) => ({
79899
+ ok: false,
79900
+ code,
79901
+ error,
79902
+ ...unblock ? { unblock } : {},
79903
+ ...data ? { data } : {}
79904
+ }), fromForge = (result) => stackFail(result.code, result.error, result.unblock), stackDeps, gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
79905
+ const run = spawnSync21("git", args, {
79906
+ cwd,
79907
+ encoding: "utf8",
79908
+ timeout: timeoutMs,
79909
+ killSignal: "SIGKILL",
79910
+ windowsHide: true,
79911
+ maxBuffer: 64 * 1024 * 1024,
79912
+ env: {
79913
+ ...process.env,
79914
+ GIT_TERMINAL_PROMPT: "0",
79915
+ GIT_EDITOR: "true",
79916
+ GIT_SEQUENCE_EDITOR: "true",
79917
+ LC_ALL: "C",
79918
+ LANGUAGE: "C"
79919
+ }
79920
+ });
79921
+ const failure = run.error;
79922
+ return {
79923
+ ok: run.status === 0,
79924
+ stdout: run.stdout ?? "",
79925
+ stderr: run.error ? String(run.error.message) : run.stderr ?? "",
79926
+ timedOut: failure?.code === "ETIMEDOUT"
79927
+ };
79928
+ }, safeRef2 = (value) => value.length > 0 && !value.startsWith("-"), isAncestor = (cwd, ancestor, descendant) => safeRef2(ancestor) && safeRef2(descendant) && gitRun2(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok, branchTip = (cwd, branch) => resolveRef(cwd, `refs/heads/${branch}`), trackedDirt = (dir) => {
79929
+ const status = gitRun2(dir, ["status", "--porcelain", "--untracked-files=no"]);
79930
+ return !status.ok || status.stdout.trim() !== "";
79931
+ }, scratchDir = (cwd) => {
79932
+ const dir = stacksDir(cwd);
79933
+ return dir.ok ? path42.join(dir.data, "worktrees") : null;
79934
+ }, firstLine = (run) => (`${run.stderr}
79935
+ ${run.stdout}`.split(`
79936
+ `).find((line) => line.trim()) ?? "git failed").trim().slice(0, 300), stackFileName = (name) => `${name.replace(/[^A-Za-z0-9._-]+/gu, "_").replace(/^[._]+/u, "").slice(0, 48).toLowerCase()}-${createHash15("sha256").update(name).digest("hex").slice(0, 12)}.json`, fileFor, validName = (name) => {
79937
+ if (name.length === 0 || name.length > 200)
79938
+ return false;
79939
+ for (let index = 0;index < name.length; index += 1) {
79940
+ const code = name.charCodeAt(index);
79941
+ if (code < 32 || code === 127)
79942
+ return false;
79943
+ }
79944
+ return true;
79945
+ }, isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), str2 = (value) => typeof value === "string" && value ? value : null, LOCK_WAIT_MS3 = 2000, HEARTBEAT_MS = 20000, HEARTBEAT_STALE_MS, OWNERLESS_STALE_MS = 30000, OWNER = "owner", readOwner = (lock) => {
79946
+ let raw = null;
79947
+ try {
79948
+ raw = fs23.readFileSync(path42.join(lock, OWNER), "utf8");
79949
+ } catch {
79950
+ raw = null;
79951
+ }
79952
+ let age = null;
79953
+ for (const target of [path42.join(lock, OWNER), lock])
79954
+ try {
79955
+ age = Date.now() - fs23.statSync(target).mtimeMs;
79956
+ break;
79957
+ } catch {}
79958
+ return { raw, age };
79959
+ }, activeBeat = null, touchStackLock = () => activeBeat?.(), remoteFor = (cwd, resolved, trunk) => resolved?.baseRemote ?? pushRemoteName(cwd, trunk) ?? "origin", trunkTip = (cwd, remote, trunk) => resolveRef(cwd, `refs/remotes/${remote}/${trunk}`) ?? branchTip(cwd, trunk), noun2 = (resolved, pr) => `${resolved.forge.kind === "github" ? "PR #" : "MR !"}${pr}`, verdictOf = (cwd, branch, rows) => {
79960
+ const check = checkVerdicts(cwd, branch, rows);
79961
+ return {
79962
+ accepted: check.accepted.accepted,
79963
+ basis: check.current.basis,
79964
+ reasons: check.accepted.reasons,
79965
+ verdictId: typeof check.accepted.verdict?.id === "string" && check.accepted.accepted ? check.accepted.verdict.id : null
79966
+ };
79967
+ }, commitCount = (cwd, base, head) => Number(gitRun2(cwd, ["rev-list", "--count", "--no-merges", `${base}..${head}`]).stdout.trim()) || 0, shortStat = (cwd, base, head) => gitRun2(cwd, ["diff", "--shortstat", `${base}...${head}`]).stdout.trim();
79968
+ var init_stack = __esm(() => {
79969
+ init_autonomy();
79970
+ init_store_lock();
79971
+ init_pr_ops();
79972
+ init_report();
79973
+ init_ops();
79974
+ init_rev();
79975
+ init_ledger();
79976
+ stackDeps = {
79977
+ pushPlan: (cwd, branch) => pushPreflight(cwd, { branch }),
79978
+ adapter: { name: "git", restack: (cwd, input) => gitRestack(cwd, input) },
79979
+ rebaseTimeoutMs: Number(process.env.WORKIT_STACK_REBASE_TIMEOUT_MS) || 10 * 60000
79980
+ };
79981
+ fileFor = stackFileName;
79982
+ HEARTBEAT_STALE_MS = 2 * 60000;
78741
79983
  });
78742
79984
 
78743
79985
  // packages/workit-cli/src/verbs/stack.ts
@@ -78745,10 +79987,199 @@ var exports_stack = {};
78745
79987
  __export(exports_stack, {
78746
79988
  run: () => run17
78747
79989
  });
78748
- var run17;
78749
- var init_stack = __esm(() => {
78750
- init_stub();
78751
- run17 = notImplemented("stack", "S12");
79990
+ async function plan(argv, io) {
79991
+ const flags = parseFlags(argv, { name: "value", trunk: "value" });
79992
+ if (typeof flags === "string")
79993
+ return usage3(io, flags, PLAN_USAGE);
79994
+ const name = flags.values.name ?? null;
79995
+ const existingByName = name ? readStack(io.cwd, name) : null;
79996
+ if (existingByName && !existingByName.ok)
79997
+ return stackFailed(io, existingByName);
79998
+ let existing = existingByName?.ok ? existingByName.data : null;
79999
+ if (!name && flags.positionals.length === 0) {
80000
+ const selected = selectStack(io.cwd, null);
80001
+ if (selected.ok)
80002
+ existing = selected.data;
80003
+ }
80004
+ const trunk = flags.values.trunk ?? existing?.trunk ?? defaultTrunk(io) ?? "main";
80005
+ const connected = connect(io, flags.positionals[0] ?? null);
80006
+ const resolved = connected.ok ? connected.data : null;
80007
+ const planned = planStack(io.cwd, resolved, { name, trunk, branches: flags.positionals }, existing);
80008
+ if (!planned.ok)
80009
+ return stackFailed(io, planned);
80010
+ if (!connected.ok)
80011
+ planned.data.notes.push(`forge: ${connected.error}${connected.unblock ? ` (${connected.unblock})` : ""}`);
80012
+ const result = await withStackLock(io.cwd, planned.data.stack.name, async () => {
80013
+ const written = writeStack(io.cwd, planned.data.stack);
80014
+ return written.ok ? planned : written;
80015
+ });
80016
+ if (!result.ok)
80017
+ return stackFailed(io, result);
80018
+ return emit(io, ok(result.data), (data) => [
80019
+ `${data.created ? "planned" : "re-planned"} stack ${data.stack.name} on ${data.stack.trunk}:`,
80020
+ ...data.stack.branches.map((entry, index) => {
80021
+ const check = data.checks.find((item) => item.branch === entry.branch);
80022
+ return ` ${index + 1}. ${entry.branch} <- ${entry.parent} ${entry.merged ? "merged" : label(data.stack.forge, entry.pr)}${check && !check.onParent ? " (not on its parent)" : ""}${check?.baseMatches === false ? ` (PR base ${check.prBase})` : ""}`;
80023
+ }),
80024
+ ...data.notes.map((note) => `note: ${note}`)
80025
+ ]);
80026
+ }
80027
+ async function status2(argv, io) {
80028
+ const flags = parseFlags(argv, { name: "value" });
80029
+ if (typeof flags === "string")
80030
+ return usage3(io, flags, STATUS_USAGE2);
80031
+ if (flags.positionals.length)
80032
+ return usage3(io, `unexpected argument ${flags.positionals[0]}`, STATUS_USAGE2);
80033
+ const selected = selectStack(io.cwd, flags.values.name ?? null);
80034
+ if (!selected.ok)
80035
+ return stackFailed(io, selected);
80036
+ const connected = connect(io, selected.data.branches[0]?.branch ?? null);
80037
+ if (!connected.ok)
80038
+ return forgeFail(io, connected);
80039
+ const result = stackStatus({ cwd: io.cwd, resolved: connected.data, actor: actorFromEnv(io.env), sleep: forgeDeps.sleep }, selected.data);
80040
+ if (!result.ok)
80041
+ return stackFailed(io, result);
80042
+ return emit(io, ok(result.data), (data) => [
80043
+ `stack ${data.name} on ${data.trunk}: ${data.verdict}${data.reason ? ` (${data.reason})` : ""}`,
80044
+ ...data.branches.map((row, index) => ` ${index + 1}. ${row.branch} ${label(connected.data.forge.kind, row.pr)} ${row.merged ? "merged" : `${row.next ?? "?"} · checks ${row.checks ?? "?"} · verdict ${row.verdict.accepted ? `accepted (${row.verdict.basis})` : `missing (${row.verdict.reasons.join(", ") || row.verdict.basis})`} · ${row.onParent === false ? `not on ${row.parent}` : `on ${row.parent}`}${row.baseMatches === false ? ` · PR base ${row.prBase}` : ""}`}`),
80045
+ ...data.next ? [`next: ${data.next}`] : []
80046
+ ]);
80047
+ }
80048
+ async function sync(argv, io) {
80049
+ const flags = parseFlags(argv, {
80050
+ name: "value",
80051
+ local: "boolean",
80052
+ "dry-run": "boolean",
80053
+ force: "list"
80054
+ });
80055
+ if (typeof flags === "string")
80056
+ return usage3(io, flags, SYNC_USAGE);
80057
+ if (flags.positionals.length)
80058
+ return usage3(io, `unexpected argument ${flags.positionals[0]}`, SYNC_USAGE);
80059
+ const selected = selectStack(io.cwd, flags.values.name ?? null);
80060
+ if (!selected.ok)
80061
+ return stackFailed(io, selected);
80062
+ const force = new Set(flags.lists.force ?? []);
80063
+ const unknown = [...force].filter((branch) => !selected.data.branches.some((entry) => entry.branch === branch));
80064
+ if (unknown.length)
80065
+ return usage3(io, `--force ${unknown[0]}: not a branch of stack ${selected.data.name}`, SYNC_USAGE);
80066
+ const local = flags.booleans.has("local");
80067
+ let ctx = null;
80068
+ if (!local) {
80069
+ const connected = connect(io, selected.data.branches[0]?.branch ?? null);
80070
+ if (!connected.ok)
80071
+ return forgeFail(io, connected);
80072
+ ctx = {
80073
+ cwd: io.cwd,
80074
+ resolved: connected.data,
80075
+ actor: actorFromEnv(io.env),
80076
+ sleep: forgeDeps.sleep
80077
+ };
80078
+ }
80079
+ const name = selected.data.name;
80080
+ const result = await withStackLock(io.cwd, name, async () => {
80081
+ const fresh = readStack(io.cwd, name);
80082
+ if (!fresh.ok)
80083
+ return fresh;
80084
+ return syncStack(io.cwd, ctx, fresh.data ?? selected.data, {
80085
+ publish: !local,
80086
+ dryRun: flags.booleans.has("dry-run"),
80087
+ force
80088
+ }, actorFromEnv(io.env));
80089
+ });
80090
+ if (!result.ok)
80091
+ return stackFailed(io, result);
80092
+ return emit(io, ok(result.data), renderSync);
80093
+ }
80094
+ async function land(argv, io) {
80095
+ const flags = parseFlags(argv, {
80096
+ name: "value",
80097
+ "dry-run": "boolean",
80098
+ max: "value",
80099
+ method: "value",
80100
+ timeout: "value",
80101
+ interval: "value"
80102
+ });
80103
+ if (typeof flags === "string")
80104
+ return usage3(io, flags, LAND_USAGE);
80105
+ if (flags.positionals.length)
80106
+ return usage3(io, `unexpected argument ${flags.positionals[0]}`, LAND_USAGE);
80107
+ const max = positiveInt(flags.values.max, "--max");
80108
+ if (typeof max === "string")
80109
+ return usage3(io, max, LAND_USAGE);
80110
+ const method = flags.values.method ?? "squash";
80111
+ if (!METHODS3.has(method))
80112
+ return usage3(io, "--method must be squash, merge or rebase", LAND_USAGE);
80113
+ const timeoutMs = parseDuration2(flags.values.timeout ?? "20m");
80114
+ const intervalMs = parseDuration2(flags.values.interval ?? "30s");
80115
+ if (timeoutMs === null || timeoutMs > 6 * 3600000)
80116
+ return usage3(io, "--timeout must be a duration up to 6h (20m, 90s)", LAND_USAGE);
80117
+ if (intervalMs === null || intervalMs < 1000 || intervalMs > 600000)
80118
+ return usage3(io, "--interval must be between 1s and 10m", LAND_USAGE);
80119
+ const selected = selectStack(io.cwd, flags.values.name ?? null);
80120
+ if (!selected.ok)
80121
+ return stackFailed(io, selected);
80122
+ const connected = connect(io, selected.data.branches[0]?.branch ?? null);
80123
+ if (!connected.ok)
80124
+ return forgeFail(io, connected);
80125
+ const name = selected.data.name;
80126
+ const result = await withStackLock(io.cwd, name, async () => {
80127
+ const fresh = readStack(io.cwd, name);
80128
+ if (!fresh.ok)
80129
+ return fresh;
80130
+ return landStack({
80131
+ cwd: io.cwd,
80132
+ resolved: connected.data,
80133
+ actor: actorFromEnv(io.env),
80134
+ sleep: forgeDeps.sleep
80135
+ }, fresh.data ?? selected.data, { dryRun: flags.booleans.has("dry-run"), max, method, timeoutMs, intervalMs });
80136
+ });
80137
+ if (!result.ok)
80138
+ return stackFailed(io, result);
80139
+ const kind = connected.data.forge.kind;
80140
+ return emit(io, ok(result.data), (data) => [
80141
+ ...data.dryRun ? [
80142
+ data.wouldLand.length ? `would land: ${data.wouldLand.map((item) => `${label(kind, item.pr)} (${item.branch})`).join(", ")}` : "would land nothing"
80143
+ ] : [
80144
+ data.landed.length ? `landed: ${data.landed.map((item) => `${label(kind, item.pr)} (${item.branch}) -> ${short4(item.mergeSha)}`).join(", ")}` : "landed nothing"
80145
+ ],
80146
+ ...data.retargeted.length ? [`retargeted: ${data.retargeted.map((pr) => label(kind, pr)).join(", ")}`] : [],
80147
+ ...data.restacked.length ? [`restacked: ${data.restacked.join(", ")}`] : [],
80148
+ data.stoppedAt ? `stopped at ${label(kind, data.stoppedAt.pr)} (${data.stoppedAt.branch}): ${data.stoppedAt.reason}${data.stoppedAt.ready ? " (verified, ready)" : ""} - ${data.stoppedAt.detail}
80149
+ unblock: ${data.stoppedAt.unblock}` : data.complete ? "stack complete" : ""
80150
+ ].filter(Boolean));
80151
+ }
80152
+ async function run17(argv, io) {
80153
+ const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
80154
+ if (sub === "plan")
80155
+ return plan(rest, io);
80156
+ if (sub === "status")
80157
+ return status2(rest, io);
80158
+ if (sub === "sync")
80159
+ return sync(rest, io);
80160
+ if (sub === "land")
80161
+ return land(rest, io);
80162
+ return usage3(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE5);
80163
+ }
80164
+ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b>] [<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] [--timeout 20m] [--interval 30s] [--json]", USAGE5 = "workit stack plan|status|sync|land ... (workit help stack)", short4 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), defaultTrunk = (io) => {
80165
+ const resolved = vcsConfig("resolve", io.cwd);
80166
+ return resolved.ok === false ? null : String(resolved.defaultTargetBranch ?? "") || null;
80167
+ }, label = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
80168
+ `${data.dryRun ? "would sync" : "synced"} stack ${data.name}${data.merged.length ? `; merged: ${data.merged.map((item) => item.branch).join(", ")}` : ""}`,
80169
+ ...data.steps.map((step) => [
80170
+ ` ${step.branch} on ${step.parent}:`,
80171
+ step.restack ? `restacked ${short4(step.restack.from)} -> ${short4(step.restack.onto)}${step.restack.head ? ` = ${short4(step.restack.head)}` : ""}${step.restack.carried === true ? " (same change; verdict carries)" : step.restack.carried === false ? " (change differs; verdict stale)" : ""}` : "up to date",
80172
+ step.push ? step.push.pushed ? `pushed${step.push.forced ? " (lease)" : ""}` : "remote current" : "",
80173
+ step.retarget ? `retargeted ${step.retarget.from} -> ${step.retarget.to}` : ""
80174
+ ].filter(Boolean).join(" "))
80175
+ ], METHODS3;
80176
+ var init_stack2 = __esm(() => {
80177
+ init_ledger();
80178
+ init_stack();
80179
+ init_vcs_config();
80180
+ init_output();
80181
+ init_forge_common();
80182
+ METHODS3 = new Set(["squash", "merge", "rebase"]);
78752
80183
  });
78753
80184
 
78754
80185
  // packages/workit-cli/src/verbs/ledger.ts
@@ -78866,12 +80297,12 @@ async function run18(argv, io) {
78866
80297
  evidenceRefs: values.evidence
78867
80298
  }), (row) => `recorded verdict ${row.id}: ${row.result} [${row.kind}] for ${row.branch} @ ${(row.head ?? "").slice(0, 12)}${row.self ? ` (self${row.selfReason === "no_session" ? ": WORKIT_SESSION_ID unset" : ""}; never accepted)` : ""}`);
78868
80299
  }
78869
- var USAGE5 = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS, positiveInt2 = (value, flag) => {
80300
+ var USAGE6 = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS, positiveInt2 = (value, flag) => {
78870
80301
  if (value === undefined)
78871
80302
  return;
78872
80303
  const parsed = Number(value);
78873
80304
  return Number.isInteger(parsed) && parsed > 0 ? parsed : new Error(`${flag} must be a positive integer`);
78874
- }, failed = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE5 })), verdictLines = (check) => {
80305
+ }, failed = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE6 })), verdictLines = (check) => {
78875
80306
  const lines = [
78876
80307
  `${check.branch} @ ${(check.head ?? "no commit").slice(0, 12)}: current: ${check.current.basis}${typeof check.current.verdict?.result === "string" ? ` (${check.current.verdict.result})` : ""}; ${check.accepted.accepted ? "accepted" : `not accepted (${check.accepted.reasons.join(", ")})`}`
78877
80308
  ];
@@ -93863,7 +95294,7 @@ var init_ast = __esm(() => {
93863
95294
 
93864
95295
  // packages/workit-cli/src/test-audit/rules.ts
93865
95296
  import { builtinModules } from "node:module";
93866
- import path42 from "node:path";
95297
+ import path43 from "node:path";
93867
95298
  function testCallee(call) {
93868
95299
  let callee = call.callee;
93869
95300
  if (callee.type === "CallExpression")
@@ -94356,12 +95787,12 @@ function checkConstantsOnly(ctx, emit) {
94356
95787
  }
94357
95788
  function checkOverMocking(ctx, emit) {
94358
95789
  const { program } = ctx.parsed;
94359
- const unit = path42.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
95790
+ const unit = path43.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
94360
95791
  const mocked = find2(program, (node) => node.type === "CallExpression" && MODULE_MOCK.test(calleeName(node.callee) ?? "") && node.arguments[0]?.type === "StringLiteral");
94361
95792
  const internal = mocked.filter((call) => /^(?:\.|@\/|~\/)/.test(call.arguments[0].value));
94362
95793
  for (const call of mocked) {
94363
95794
  const spec = call.arguments[0].value;
94364
- const base = path42.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
95795
+ const base = path43.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
94365
95796
  if (base === unit)
94366
95797
  emit({
94367
95798
  rule: "over-mocking",
@@ -94749,11 +96180,11 @@ var init_rules = __esm(() => {
94749
96180
  });
94750
96181
 
94751
96182
  // packages/workit-cli/src/test-audit/audit.ts
94752
- import { spawnSync as spawnSync21 } from "node:child_process";
96183
+ import { spawnSync as spawnSync22 } from "node:child_process";
94753
96184
  import { existsSync as existsSync20, readdirSync as readdirSync10, readFileSync as readFileSync23, statSync as statSync7 } from "node:fs";
94754
- import path43 from "node:path";
96185
+ import path44 from "node:path";
94755
96186
  function git5(cwd, args) {
94756
- const run = spawnSync21("git", ["-c", "core.quotepath=off", ...args], {
96187
+ const run = spawnSync22("git", ["-c", "core.quotepath=off", ...args], {
94757
96188
  cwd,
94758
96189
  encoding: "utf8",
94759
96190
  maxBuffer: 64 * 1024 * 1024
@@ -94764,7 +96195,7 @@ function walkTests(dir, out) {
94764
96195
  for (const entry of readdirSync10(dir, { withFileTypes: true })) {
94765
96196
  if (entry.name.startsWith(".") && entry.name !== "." && entry.isDirectory())
94766
96197
  continue;
94767
- const full = path43.join(dir, entry.name);
96198
+ const full = path44.join(dir, entry.name);
94768
96199
  if (entry.isDirectory()) {
94769
96200
  if (!SKIP_DIRS.has(entry.name))
94770
96201
  walkTests(full, out);
@@ -94776,7 +96207,7 @@ function expandPaths(cwd, inputs) {
94776
96207
  const files = [];
94777
96208
  const missing = [];
94778
96209
  for (const input of inputs.length > 0 ? inputs : ["."]) {
94779
- const full = path43.resolve(cwd, input);
96210
+ const full = path44.resolve(cwd, input);
94780
96211
  if (!existsSync20(full))
94781
96212
  missing.push(input);
94782
96213
  else if (statSync7(full).isDirectory())
@@ -94813,11 +96244,11 @@ function changedLines(cwd, base) {
94813
96244
  for (let offset = 0;offset < count; offset += 1)
94814
96245
  lines.add(start + offset);
94815
96246
  }
94816
- changed.set(path43.join(root, file), lines);
96247
+ changed.set(path44.join(root, file), lines);
94817
96248
  }
94818
96249
  const untracked = git5(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
94819
96250
  for (const file of untracked.stdout.split("\x00").filter(Boolean))
94820
- changed.set(path43.join(root, file), "all");
96251
+ changed.set(path44.join(root, file), "all");
94821
96252
  return changed;
94822
96253
  }
94823
96254
  function auditFiles(files, display) {
@@ -94879,7 +96310,7 @@ function auditFiles(files, display) {
94879
96310
  durationMs: Math.round(performance.now() - started)
94880
96311
  };
94881
96312
  }
94882
- var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path43.sep).join("/"), SEVERITY_ORDER, ruleIds;
96313
+ var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path44.sep).join("/"), SEVERITY_ORDER, ruleIds;
94883
96314
  var init_audit = __esm(() => {
94884
96315
  init_ast();
94885
96316
  init_rules();
@@ -94909,7 +96340,7 @@ import {
94909
96340
  copyFileSync as copyFileSync4
94910
96341
  } from "node:fs";
94911
96342
  import os14 from "node:os";
94912
- import path44 from "node:path";
96343
+ import path45 from "node:path";
94913
96344
  function mutantsFor(source, file, lines) {
94914
96345
  const parsed = parseSource(source, file);
94915
96346
  const out = [];
@@ -94982,7 +96413,7 @@ function relatedTests(source, testFiles) {
94982
96413
  for (const match of text.matchAll(SPECIFIER)) {
94983
96414
  const spec = match[1];
94984
96415
  if (spec.startsWith(".")) {
94985
- if (stripExt(path44.posix.join(path44.posix.dirname(file), spec)) === target)
96416
+ if (stripExt(path45.posix.join(path45.posix.dirname(file), spec)) === target)
94986
96417
  return true;
94987
96418
  } else if (spec.includes("/") && stripExt(spec).split("/").slice(-2).join("/") === tail)
94988
96419
  return true;
@@ -95002,12 +96433,12 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
95002
96433
  for (const name of names) {
95003
96434
  if (!name.startsWith(TEMP_PREFIX))
95004
96435
  continue;
95005
- const dir = path44.join(tmp, name);
96436
+ const dir = path45.join(tmp, name);
95006
96437
  try {
95007
96438
  const stat = lstatSync9(dir);
95008
96439
  if (!stat.isDirectory() || uid !== undefined && stat.uid !== uid)
95009
96440
  continue;
95010
- const pid = Number(readFileSync24(path44.join(dir, PID_FILE), "utf8"));
96441
+ const pid = Number(readFileSync24(path45.join(dir, PID_FILE), "utf8"));
95011
96442
  if (Number.isInteger(pid) && pid > 0 && alive(pid))
95012
96443
  continue;
95013
96444
  } catch (error) {
@@ -95024,8 +96455,8 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
95024
96455
  function linkNodeModules(origin, target, repo, copy) {
95025
96456
  mkdirSync10(target, { recursive: true });
95026
96457
  for (const entry of readdirSync11(origin, { withFileTypes: true })) {
95027
- const from = path44.join(origin, entry.name);
95028
- const to = path44.join(target, entry.name);
96458
+ const from = path45.join(origin, entry.name);
96459
+ const to = path45.join(target, entry.name);
95029
96460
  if (entry.name.startsWith("@") && entry.isDirectory()) {
95030
96461
  linkNodeModules(from, to, repo, copy);
95031
96462
  continue;
@@ -95041,9 +96472,9 @@ function linkNodeModules(origin, target, repo, copy) {
95041
96472
  } catch {
95042
96473
  continue;
95043
96474
  }
95044
- const relative = path44.relative(repo, real);
95045
- if (!relative.startsWith("..") && !path44.isAbsolute(relative) && !relative.split(path44.sep).includes("node_modules")) {
95046
- symlinkSync(path44.join(copy, relative), to, "junction");
96475
+ const relative = path45.relative(repo, real);
96476
+ if (!relative.startsWith("..") && !path45.isAbsolute(relative) && !relative.split(path45.sep).includes("node_modules")) {
96477
+ symlinkSync(path45.join(copy, relative), to, "junction");
95047
96478
  continue;
95048
96479
  }
95049
96480
  }
@@ -95059,7 +96490,7 @@ function copySymlink(repo, tree, from, to) {
95059
96490
  }
95060
96491
  const realRepo = realpathSync12(repo);
95061
96492
  if (real && inside2(realRepo, real)) {
95062
- symlinkSync(path44.relative(path44.dirname(to), path44.join(tree, path44.relative(realRepo, real))), to);
96493
+ symlinkSync(path45.relative(path45.dirname(to), path45.join(tree, path45.relative(realRepo, real))), to);
95063
96494
  return;
95064
96495
  }
95065
96496
  if (real && statSync8(real).isFile())
@@ -95068,10 +96499,10 @@ function copySymlink(repo, tree, from, to) {
95068
96499
  symlinkSync(readlinkSync3(from), to);
95069
96500
  }
95070
96501
  function writeInside(copy, file, content) {
95071
- const target = path44.join(copy, file);
96502
+ const target = path45.join(copy, file);
95072
96503
  try {
95073
96504
  const stat = lstatSync9(target);
95074
- if (!stat.isFile() || !inside2(realpathSync12(copy), realpathSync12(path44.dirname(target))))
96505
+ if (!stat.isFile() || !inside2(realpathSync12(copy), realpathSync12(path45.dirname(target))))
95075
96506
  return false;
95076
96507
  } catch {
95077
96508
  return false;
@@ -95082,9 +96513,9 @@ function writeInside(copy, file, content) {
95082
96513
  return true;
95083
96514
  }
95084
96515
  function copyWorkingTree(repo) {
95085
- const dir = mkdtempSync2(path44.join(os14.tmpdir(), TEMP_PREFIX));
95086
- writeFileSync9(path44.join(dir, PID_FILE), String(process.pid));
95087
- const tree = path44.join(dir, "tree");
96516
+ const dir = mkdtempSync2(path45.join(os14.tmpdir(), TEMP_PREFIX));
96517
+ writeFileSync9(path45.join(dir, PID_FILE), String(process.pid));
96518
+ const tree = path45.join(dir, "tree");
95088
96519
  const head = git5(repo, ["rev-parse", "--verify", "--quiet", "HEAD"]).stdout.trim();
95089
96520
  if (head && git5(dir, ["clone", "-q", "--shared", "--no-checkout", repo, tree]).ok) {
95090
96521
  const branch = git5(repo, ["symbolic-ref", "--quiet", "--short", "HEAD"]).stdout.trim();
@@ -95101,29 +96532,29 @@ function copyWorkingTree(repo) {
95101
96532
  for (const relative of listed) {
95102
96533
  if (!relative)
95103
96534
  continue;
95104
- const from = path44.join(repo, relative);
96535
+ const from = path45.join(repo, relative);
95105
96536
  let stat;
95106
96537
  try {
95107
96538
  stat = lstatSync9(from);
95108
96539
  } catch {
95109
96540
  continue;
95110
96541
  }
95111
- const to = path44.join(tree, relative);
95112
- mkdirSync10(path44.dirname(to), { recursive: true });
96542
+ const to = path45.join(tree, relative);
96543
+ mkdirSync10(path45.dirname(to), { recursive: true });
95113
96544
  if (stat.isSymbolicLink())
95114
96545
  copySymlink(repo, tree, from, to);
95115
96546
  else if (stat.isFile())
95116
96547
  copyFileSync4(from, to);
95117
- let parent = path44.dirname(relative);
96548
+ let parent = path45.dirname(relative);
95118
96549
  while (parent !== "." && !dirs.has(parent)) {
95119
96550
  dirs.add(parent);
95120
- parent = path44.dirname(parent);
96551
+ parent = path45.dirname(parent);
95121
96552
  }
95122
96553
  }
95123
96554
  for (const parent of dirs) {
95124
- const modules = path44.join(repo, parent, "node_modules");
95125
- if (existsSync21(modules) && !existsSync21(path44.join(tree, parent, "node_modules")))
95126
- linkNodeModules(modules, path44.join(tree, parent, "node_modules"), repo, tree);
96555
+ const modules = path45.join(repo, parent, "node_modules");
96556
+ if (existsSync21(modules) && !existsSync21(path45.join(tree, parent, "node_modules")))
96557
+ linkNodeModules(modules, path45.join(tree, parent, "node_modules"), repo, tree);
95127
96558
  }
95128
96559
  return { dir, tree };
95129
96560
  }
@@ -95162,14 +96593,14 @@ async function runMutation(options) {
95162
96593
  const tracked = git5(repo, ["ls-files", "-co", "--exclude-standard", "-z"]).stdout.split("\x00").filter(Boolean);
95163
96594
  const testFiles = tracked.filter((file) => TEST_FILE.test(file)).map((file) => {
95164
96595
  try {
95165
- return { file, text: readFileSync24(path44.join(repo, file), "utf8") };
96596
+ return { file, text: readFileSync24(path45.join(repo, file), "utf8") };
95166
96597
  } catch {
95167
96598
  return { file, text: "" };
95168
96599
  }
95169
96600
  });
95170
96601
  const candidates = [];
95171
96602
  for (const [absolute, lines] of [...options.changed].toSorted(([a], [b]) => a.localeCompare(b))) {
95172
- const relative = toPosix(path44.relative(repo, absolute));
96603
+ const relative = toPosix(path45.relative(repo, absolute));
95173
96604
  if (!CODE_FILE.test(relative) || TEST_FILE.test(relative) || relative.endsWith(".d.ts"))
95174
96605
  continue;
95175
96606
  if (NOT_SOURCE.test(relative) || !existsSync21(absolute))
@@ -95183,9 +96614,9 @@ async function runMutation(options) {
95183
96614
  for (const mutant of mutants)
95184
96615
  if (!related.has(mutant.file))
95185
96616
  related.set(mutant.file, relatedTests(mutant.file, testFiles));
95186
- const cwdRelative = path44.relative(repo, options.cwd);
96617
+ const cwdRelative = path45.relative(repo, options.cwd);
95187
96618
  const asArg = (file) => {
95188
- const relative = toPosix(path44.relative(cwdRelative, file));
96619
+ const relative = toPosix(path45.relative(cwdRelative, file));
95189
96620
  return shellQuote(relative.startsWith(".") ? relative : `./${relative}`);
95190
96621
  };
95191
96622
  const commandFor = (tests) => options.command.replaceAll("{files}", () => tests.map(asArg).join(" "));
@@ -95220,7 +96651,7 @@ async function runMutation(options) {
95220
96651
  return finish2(options, 0, results, counts, started);
95221
96652
  removeStaleCopies();
95222
96653
  const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
95223
- const copyCwd = path44.join(copy, cwdRelative);
96654
+ const copyCwd = path45.join(copy, cwdRelative);
95224
96655
  let current = null;
95225
96656
  const restore = () => {
95226
96657
  if (current)
@@ -95255,7 +96686,7 @@ async function runMutation(options) {
95255
96686
  record({ ...mutant, status: "skipped", durationMs: 0, tests, reason: "budget" });
95256
96687
  continue;
95257
96688
  }
95258
- const original = readFileSync24(path44.join(copy, mutant.file), "utf8");
96689
+ const original = readFileSync24(path45.join(copy, mutant.file), "utf8");
95259
96690
  const mutated = original.slice(0, mutant.start) + mutant.replacement + original.slice(mutant.end);
95260
96691
  current = { file: mutant.file, original };
95261
96692
  if (!writeInside(copy, mutant.file, mutated)) {
@@ -95312,8 +96743,8 @@ var NOT_SOURCE, FLIPS, isStringy = (node) => node.type === "StringLiteral" || no
95312
96743
  return error.code === "EPERM";
95313
96744
  }
95314
96745
  }, inside2 = (root, file) => {
95315
- const relative = path44.relative(root, file);
95316
- return relative === "" || !relative.startsWith("..") && !path44.isAbsolute(relative);
96746
+ const relative = path45.relative(root, file);
96747
+ return relative === "" || !relative.startsWith("..") && !path45.isAbsolute(relative);
95317
96748
  }, active, killTree = (child) => {
95318
96749
  try {
95319
96750
  if (child.pid && process.platform !== "win32")
@@ -95362,7 +96793,7 @@ __export(exports_test_audit, {
95362
96793
  run: () => run19
95363
96794
  });
95364
96795
  import { existsSync as existsSync22 } from "node:fs";
95365
- import path45 from "node:path";
96796
+ import path46 from "node:path";
95366
96797
  function parse6(argv) {
95367
96798
  const options = {
95368
96799
  paths: [],
@@ -95460,10 +96891,10 @@ async function run19(argv, io) {
95460
96891
  const options = parse6(argv);
95461
96892
  if (typeof options === "string")
95462
96893
  return emit(io, fail("invalid_input", `${options}
95463
- usage: ${USAGE6}`));
96894
+ usage: ${USAGE7}`));
95464
96895
  const top = git5(io.cwd, ["rev-parse", "--show-toplevel"]);
95465
- const repo = top.ok ? path45.resolve(top.stdout.trim()) : null;
95466
- const display = (file) => toPosix(path45.relative(io.cwd, file) || file);
96896
+ const repo = top.ok ? path46.resolve(top.stdout.trim()) : null;
96897
+ const display = (file) => toPosix(path46.relative(io.cwd, file) || file);
95467
96898
  let base = null;
95468
96899
  let changed = null;
95469
96900
  if (options.diff || options.mutate) {
@@ -95546,7 +96977,7 @@ usage: ${USAGE6}`));
95546
96977
  }
95547
96978
  return emit(io, ok(data), human);
95548
96979
  }
95549
- var USAGE6 = "workit test-audit [paths…] [--diff [base]] [--rule <ids>] [--min-severity <level>] [--fail-on <level>] [--mutate [--test-cmd <cmd>] [--max-mutants <n>] [--budget <s>] [--timeout <s>]] [--json]", LEVELS, VALUE_FLAGS, quote = (arg) => /^[\w./@:=+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
96980
+ var USAGE7 = "workit test-audit [paths…] [--diff [base]] [--rule <ids>] [--min-severity <level>] [--fail-on <level>] [--mutate [--test-cmd <cmd>] [--max-mutants <n>] [--budget <s>] [--timeout <s>]] [--json]", LEVELS, VALUE_FLAGS, quote = (arg) => /^[\w./@:=+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
95550
96981
  var init_test_audit = __esm(() => {
95551
96982
  init_audit();
95552
96983
  init_mutate();
@@ -95572,7 +97003,7 @@ __export(exports_handoff, {
95572
97003
  import { parseArgs as parseArgs2 } from "node:util";
95573
97004
  function human2(brief) {
95574
97005
  const lines = [
95575
- `branch: ${brief.branch ?? "(detached)"} @ ${short4(brief.head)}${brief.subject ? ` ${brief.subject}` : ""}`
97006
+ `branch: ${brief.branch ?? "(detached)"} @ ${short5(brief.head)}${brief.subject ? ` ${brief.subject}` : ""}`
95576
97007
  ];
95577
97008
  if (brief.base)
95578
97009
  lines.push(`base: ${brief.base}${brief.ahead === null ? "" : ` (ahead ${brief.ahead}, behind ${brief.behind})`}`);
@@ -95615,11 +97046,11 @@ async function resumeBrief(argv, io) {
95615
97046
  strict: true
95616
97047
  }));
95617
97048
  } catch (error) {
95618
- return emit(io, fail("invalid_input", error.message, { unblock: USAGE7 }));
97049
+ return emit(io, fail("invalid_input", error.message, { unblock: USAGE8 }));
95619
97050
  }
95620
97051
  const last = values.last === undefined ? 10 : Number(values.last);
95621
97052
  if (!Number.isInteger(last) || last < 1)
95622
- return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock: USAGE7 }));
97053
+ return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock: USAGE8 }));
95623
97054
  const built = buildHandoff(io.cwd, {
95624
97055
  last,
95625
97056
  note: values.note ?? null,
@@ -95643,7 +97074,7 @@ async function run20(argv, io) {
95643
97074
  await Promise.resolve().then(() => init_task());
95644
97075
  return runTaskCommand(["handoff", ...argv]);
95645
97076
  }
95646
- var USAGE7 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short4 = (sha) => sha ? sha.slice(0, 12) : "no commit";
97077
+ var USAGE8 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short5 = (sha) => sha ? sha.slice(0, 12) : "no commit";
95647
97078
  var init_handoff = __esm(() => {
95648
97079
  init_ledger();
95649
97080
  init_output();
@@ -95652,7 +97083,7 @@ var init_handoff = __esm(() => {
95652
97083
  // packages/workit-cli/src/main.ts
95653
97084
  init_package();
95654
97085
  init_output();
95655
- import path46 from "node:path";
97086
+ import path47 from "node:path";
95656
97087
 
95657
97088
  // packages/workit-cli/src/verbs/registry.ts
95658
97089
  var TASK_FAMILY_NAMES = [
@@ -95768,10 +97199,9 @@ var VERBS = [
95768
97199
  {
95769
97200
  name: "stack",
95770
97201
  group: "delivery",
95771
- usage: "workit stack plan|sync|land [--json]",
95772
- summary: "Plan, restack and land a stack of PRs",
95773
- planned: "S12",
95774
- load: () => Promise.resolve().then(() => (init_stack(), exports_stack))
97202
+ usage: "workit stack plan [<bottom> … <top>] | status | sync [--local] | land [--dry-run] [--max <n>] [--json]",
97203
+ summary: "Plan, restack (lease push + retarget) and land a base-branch stack of PRs, root first",
97204
+ load: () => Promise.resolve().then(() => (init_stack2(), exports_stack))
95775
97205
  },
95776
97206
  {
95777
97207
  name: "ledger",
@@ -95877,7 +97307,7 @@ var defaultIo = (json) => ({
95877
97307
  });
95878
97308
  async function main(argv, overrides = {}, options = {}) {
95879
97309
  const parsed = parseGlobals(argv);
95880
- const cwd = path46.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
97310
+ const cwd = path47.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
95881
97311
  const io = { ...defaultIo(parsed.json), ...overrides, json: parsed.json, cwd };
95882
97312
  if (parsed.error)
95883
97313
  return emit(io, fail("invalid_input", parsed.error));