@brainervirus/workit-claude-code 7.2.0 → 7.3.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: "7.2.0",
68
+ version: "7.3.0",
69
69
  private: false,
70
70
  description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
71
71
  keywords: [
@@ -103,9 +103,9 @@ var init_package = __esm(() => {
103
103
  },
104
104
  dependencies: {
105
105
  "@babel/parser": "7.29.9",
106
- "@brainervirus/workit-core": "^7.2.0",
107
- "@brainervirus/workit-cursor": "^7.2.0",
108
- "@brainervirus/workit-opencode": "^7.2.0",
106
+ "@brainervirus/workit-core": "^7.3.0",
107
+ "@brainervirus/workit-cursor": "^7.3.0",
108
+ "@brainervirus/workit-opencode": "^7.3.0",
109
109
  "@inkjs/ui": "2.0.0",
110
110
  ink: "7.1.1",
111
111
  react: "19.2.8"
@@ -73017,6 +73017,20 @@ function parseDuration2(value) {
73017
73017
  const factor = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60000 : 3600000;
73018
73018
  return Math.round(Number(match[1]) * factor);
73019
73019
  }
73020
+ function releaseTrunk(io, track, branch) {
73021
+ const resolved = vcsConfig("resolve", io.cwd, {
73022
+ track,
73023
+ ...branch ? { branch } : {}
73024
+ });
73025
+ if (resolved.ok === false)
73026
+ return track !== null || String(resolved.configPath ?? "").endsWith("workspaces.json") ? { error: String(resolved.error) } : { trunk: null, tracked: false };
73027
+ if (resolved.releaseTrack?.blocking)
73028
+ return { error: String(resolved.releaseTrack.blocking) };
73029
+ return {
73030
+ trunk: String(resolved.defaultTargetBranch ?? "") || null,
73031
+ tracked: Boolean(resolved.releaseTrack)
73032
+ };
73033
+ }
73020
73034
  function connect(io, branch) {
73021
73035
  const resolved = resolveForge(io.cwd, {
73022
73036
  branch,
@@ -73084,6 +73098,7 @@ function renderStatus(doc) {
73084
73098
  var forgeDeps, usage3 = (io, message, line) => emit(io, fail("invalid_input", message, { unblock: line })), forgeFail = (io, result, data) => emit(io, fail(result.code, result.error, { data, unblock: result.unblock })), short = (sha) => sha ? sha.slice(0, 7) : "?";
73085
73099
  var init_forge_common = __esm(() => {
73086
73100
  init_resolve();
73101
+ init_vcs_config();
73087
73102
  init_output();
73088
73103
  forgeDeps = {
73089
73104
  runner: undefined,
@@ -75207,7 +75222,7 @@ async function plan(argv, io) {
75207
75222
  return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE);
75208
75223
  let trunk = flags.values.trunk ?? existing?.trunk ?? null;
75209
75224
  if (trunk === null || track !== null) {
75210
- const derived = defaultTrunk(io, track, flags.positionals[0] ?? null);
75225
+ const derived = releaseTrunk(io, track, flags.positionals[0] ?? null);
75211
75226
  if ("error" in derived)
75212
75227
  return emit(io, fail("blocked", derived.error, {
75213
75228
  unblock: "workit stack plan --track <name> … # or --trunk <branch>"
@@ -75375,17 +75390,7 @@ async function run14(argv, io) {
75375
75390
  return land(rest, io);
75376
75391
  return usage3(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE7);
75377
75392
  }
75378
- 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] [--timeout 20m] [--interval 30s] [--json]", USAGE7 = "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, track, branch) => {
75379
- const resolved = vcsConfig("resolve", io.cwd, {
75380
- track,
75381
- ...branch ? { branch } : {}
75382
- });
75383
- if (resolved.ok === false)
75384
- return track !== null || String(resolved.configPath ?? "").endsWith("workspaces.json") ? { error: String(resolved.error) } : { trunk: null };
75385
- if (resolved.releaseTrack?.blocking)
75386
- return { error: String(resolved.releaseTrack.blocking) };
75387
- return { trunk: String(resolved.defaultTargetBranch ?? "") || null };
75388
- }, label = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
75393
+ 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] [--timeout 20m] [--interval 30s] [--json]", USAGE7 = "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 ?? {} })), label = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
75389
75394
  `${data.dryRun ? "would sync" : "synced"} stack ${data.name}${data.merged.length ? `; merged: ${data.merged.map((item) => item.branch).join(", ")}` : ""}`,
75390
75395
  ...data.steps.map((step) => [
75391
75396
  ` ${step.branch} on ${step.parent}:`,
@@ -75397,16 +75402,993 @@ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<b
75397
75402
  var init_stack2 = __esm(() => {
75398
75403
  init_ledger();
75399
75404
  init_stack();
75400
- init_vcs_config();
75401
75405
  init_output();
75402
75406
  init_forge_common();
75403
75407
  METHODS3 = new Set(["squash", "merge", "rebase"]);
75404
75408
  });
75405
75409
 
75410
+ // packages/workit-core/src/fanout.ts
75411
+ import { spawnSync as spawnSync20 } from "node:child_process";
75412
+ import fs22 from "node:fs";
75413
+ import path42 from "node:path";
75414
+ function scopeSamples(pattern) {
75415
+ if (!hasWildcard(pattern))
75416
+ return [unescape2(pattern)];
75417
+ let out = "";
75418
+ for (let index = 0;index < pattern.length; index += 1) {
75419
+ const char = pattern[index];
75420
+ if (char === "\\" && index + 1 < pattern.length) {
75421
+ out += pattern[index + 1];
75422
+ index += 1;
75423
+ } else if (char === "*" && pattern[index + 1] === "*") {
75424
+ const atSegmentStart = index === 0 || pattern[index - 1] === "/";
75425
+ if (atSegmentStart && pattern[index + 2] === "/")
75426
+ index += 2;
75427
+ else {
75428
+ out += "<any>";
75429
+ index += 1;
75430
+ }
75431
+ } else if (char === "*")
75432
+ out += "<any>";
75433
+ else if (char === "?")
75434
+ out += "x";
75435
+ else if (char === "{" && pattern.indexOf("}", index) > index) {
75436
+ const close = pattern.indexOf("}", index);
75437
+ out += pattern.slice(index + 1, close).split(",")[0];
75438
+ index = close;
75439
+ } else
75440
+ out += char;
75441
+ }
75442
+ return [out];
75443
+ }
75444
+ function globSource(pattern) {
75445
+ let out = "";
75446
+ for (let index = 0;index < pattern.length; index += 1) {
75447
+ const char = pattern[index];
75448
+ if (char === "\\" && index + 1 < pattern.length) {
75449
+ out += escapeRegExp4(pattern[index + 1]);
75450
+ index += 1;
75451
+ } else if (char === "*" && pattern[index + 1] === "*") {
75452
+ const atSegmentStart = index === 0 || pattern[index - 1] === "/";
75453
+ if (atSegmentStart && pattern[index + 2] === "/") {
75454
+ out += "(?:[^/]*/)*";
75455
+ index += 2;
75456
+ } else {
75457
+ out += ".*";
75458
+ index += 1;
75459
+ }
75460
+ } else if (char === "*")
75461
+ out += "[^/]*";
75462
+ else if (char === "?")
75463
+ out += "[^/]";
75464
+ else if (char === "{" && pattern.indexOf("}", index) > index) {
75465
+ const close = pattern.indexOf("}", index);
75466
+ const alternatives = pattern.slice(index + 1, close).split(",").map(globSource);
75467
+ out += `(?:${alternatives.join("|")})`;
75468
+ index = close;
75469
+ } else
75470
+ out += escapeRegExp4(char);
75471
+ }
75472
+ return out;
75473
+ }
75474
+ function matchesScope(pattern, file) {
75475
+ let regex = compiled.get(pattern);
75476
+ if (!regex) {
75477
+ regex = new RegExp(`^${globSource(pattern)}$`, "u");
75478
+ compiled.set(pattern, regex);
75479
+ }
75480
+ return regex.test(file) || !hasWildcard(pattern) && file.startsWith(`${unescape2(pattern)}/`);
75481
+ }
75482
+ function normalizePattern(raw) {
75483
+ let pattern = raw.trim().replace(/\\(?![[\]{}*?])/gu, "/");
75484
+ while (pattern.startsWith("./"))
75485
+ pattern = pattern.slice(2);
75486
+ if (pattern === "." || pattern === "")
75487
+ pattern = raw.trim() ? "**" : "";
75488
+ if (pattern.endsWith("/"))
75489
+ pattern = `${pattern}**`;
75490
+ if (!pattern)
75491
+ return { error: "is empty" };
75492
+ if (pattern.startsWith("/") || /^[A-Za-z]:/u.test(pattern))
75493
+ return { error: "must be relative to the repository root" };
75494
+ if (pattern.split("/").includes(".."))
75495
+ return { error: "must not contain .." };
75496
+ const bare = pattern.replace(/\\./gu, "");
75497
+ if (/[[\]]/u.test(bare))
75498
+ return {
75499
+ error: 'has an unescaped [ or ]: write \\[ and \\] for literal brackets (app/\\[id\\]/page.tsx; in JSON "app/\\\\[id\\\\]/page.tsx")'
75500
+ };
75501
+ if (bare.startsWith("!"))
75502
+ return { error: "uses ! (negation is not supported)" };
75503
+ let depth = 0;
75504
+ for (const char of bare) {
75505
+ if (char === "{")
75506
+ depth += 1;
75507
+ if (char === "}")
75508
+ depth -= 1;
75509
+ if (depth < 0 || depth > 1)
75510
+ return { error: "has unbalanced or nested braces" };
75511
+ }
75512
+ if (depth !== 0)
75513
+ return { error: "has unbalanced or nested braces" };
75514
+ return { pattern };
75515
+ }
75516
+ function inScope(plan, slice, file) {
75517
+ if (matchesAny(slice.owns, file))
75518
+ return true;
75519
+ if (!matchesAny(slice.scope, file))
75520
+ return false;
75521
+ return !plan.slices.some((other) => other.id !== slice.id && matchesAny(other.owns, file));
75522
+ }
75523
+ function parsePlan(raw, defaults, name) {
75524
+ const problems = [];
75525
+ if (!isRecord8(raw))
75526
+ return { ok: false, problems: ["the plan must be a JSON object"] };
75527
+ const planName = name ?? (typeof raw.name === "string" ? raw.name.trim() : "");
75528
+ if (!planName || planName.length > 200 || /\p{Cc}/u.test(planName))
75529
+ problems.push('name: pass --name or set a 1-200 character "name"');
75530
+ const trunk = typeof raw.trunk === "string" && raw.trunk.trim() ? raw.trunk.trim() : defaults.trunk;
75531
+ const shared = [];
75532
+ for (const entry of textList(raw.shared ?? []) ?? ["<not a list>"]) {
75533
+ const normalized = normalizePattern(entry);
75534
+ if ("error" in normalized)
75535
+ problems.push(`shared "${entry}" ${normalized.error}`);
75536
+ else
75537
+ shared.push(normalized.pattern);
75538
+ }
75539
+ if (!Array.isArray(raw.slices) || raw.slices.length === 0)
75540
+ return { ok: false, problems: [...problems, "slices: list at least one slice"] };
75541
+ const slices = [];
75542
+ const seenIds = new Set;
75543
+ const seenBranches = new Map;
75544
+ raw.slices.forEach((item, index) => {
75545
+ const where = isRecord8(item) && typeof item.id === "string" ? `slice ${item.id}` : `slice #${index + 1}`;
75546
+ if (!isRecord8(item)) {
75547
+ problems.push(`${where}: must be an object`);
75548
+ return;
75549
+ }
75550
+ const id = typeof item.id === "string" ? item.id : "";
75551
+ if (!ID.test(id))
75552
+ problems.push(`${where}: id must match ${ID.source}`);
75553
+ else if (seenIds.has(id))
75554
+ problems.push(`${where}: duplicate id`);
75555
+ seenIds.add(id);
75556
+ const branch = typeof item.branch === "string" ? item.branch.trim() : "";
75557
+ if (!branch)
75558
+ problems.push(`${where}: branch is empty`);
75559
+ else if (seenBranches.has(branch))
75560
+ problems.push(`${where}: branch ${branch} is also slice ${seenBranches.get(branch)}'s`);
75561
+ else
75562
+ seenBranches.set(branch, id);
75563
+ if (branch === trunk)
75564
+ problems.push(`${where}: branch must not be the trunk ${trunk}`);
75565
+ const goal = typeof item.goal === "string" ? item.goal.trim() : "";
75566
+ if (!filled(goal))
75567
+ problems.push(`${where}: goal is empty`);
75568
+ const brief = (field) => {
75569
+ const list = textList(item[field]);
75570
+ const kept = (list ?? []).map((entry) => entry.trim()).filter(filled);
75571
+ if (kept.length === 0)
75572
+ problems.push(`${where}: ${field} is empty`);
75573
+ return kept;
75574
+ };
75575
+ const acceptance = brief("acceptance");
75576
+ const verify = brief("verify");
75577
+ const forbidden = brief("forbidden");
75578
+ const scope = [];
75579
+ const owns = [];
75580
+ for (const [field, out] of [
75581
+ ["scope", scope],
75582
+ ["owns", owns]
75583
+ ]) {
75584
+ const list = textList(item[field] ?? []);
75585
+ if (list === null) {
75586
+ problems.push(`${where}: ${field} must be a list of globs`);
75587
+ continue;
75588
+ }
75589
+ for (const entry of list.filter(filled)) {
75590
+ const normalized = normalizePattern(entry);
75591
+ if ("error" in normalized)
75592
+ problems.push(`${where}: ${field} "${entry}" ${normalized.error}`);
75593
+ else
75594
+ out.push(normalized.pattern);
75595
+ }
75596
+ }
75597
+ if (scope.length === 0 && textList(item.scope ?? []) !== null)
75598
+ problems.push(`${where}: scope is empty`);
75599
+ const tier = item.tier;
75600
+ if (typeof tier !== "string" || !TIERS.includes(tier))
75601
+ problems.push(`${where}: tier must be one of ${TIERS.join(", ")}`);
75602
+ const dependsOn = textList(item.dependsOn ?? []);
75603
+ if (dependsOn === null)
75604
+ problems.push(`${where}: dependsOn must be a list of slice ids`);
75605
+ const optional = (field) => typeof item[field] === "string" && filled(item[field]) ? item[field].trim() : null;
75606
+ slices.push({
75607
+ id,
75608
+ branch,
75609
+ base: optional("base") ?? "",
75610
+ worktree: optional("worktree") ?? path42.join(defaults.worktreeRoot, id),
75611
+ tier,
75612
+ dependsOn: [...new Set(dependsOn ?? [])],
75613
+ scope,
75614
+ owns,
75615
+ goal,
75616
+ acceptance,
75617
+ verify,
75618
+ forbidden,
75619
+ context: optional("context"),
75620
+ timebox: optional("timebox")
75621
+ });
75622
+ });
75623
+ const byId = new Map(slices.map((slice) => [slice.id, slice]));
75624
+ const byBranch = new Map(slices.map((slice) => [slice.branch, slice]));
75625
+ for (const slice of slices) {
75626
+ for (const dep of slice.dependsOn) {
75627
+ if (dep === slice.id)
75628
+ problems.push(`slice ${slice.id}: depends on itself`);
75629
+ else if (!byId.has(dep))
75630
+ problems.push(`slice ${slice.id}: depends on unknown slice ${dep}`);
75631
+ }
75632
+ const explicitBase = slice.base;
75633
+ if (explicitBase) {
75634
+ const parent = byBranch.get(explicitBase);
75635
+ if (parent && !slice.dependsOn.includes(parent.id))
75636
+ problems.push(`slice ${slice.id}: base ${explicitBase} is slice ${parent.id}'s branch; add "${parent.id}" to its dependsOn`);
75637
+ } else {
75638
+ const single = slice.dependsOn.length === 1 ? byId.get(slice.dependsOn[0]) : undefined;
75639
+ slice.base = single ? single.branch : trunk;
75640
+ }
75641
+ }
75642
+ const cycle = findCycle(slices);
75643
+ if (cycle)
75644
+ problems.push(`dependency cycle: ${cycle.join(" -> ")}`);
75645
+ if (problems.length)
75646
+ return { ok: false, problems };
75647
+ return {
75648
+ ok: true,
75649
+ plan: {
75650
+ v: FANOUT_VERSION,
75651
+ name: planName,
75652
+ trunk,
75653
+ shared,
75654
+ slices,
75655
+ createdAt: "",
75656
+ updatedAt: ""
75657
+ }
75658
+ };
75659
+ }
75660
+ function findCycle(slices) {
75661
+ const byId = new Map(slices.map((slice) => [slice.id, slice]));
75662
+ const state = new Map;
75663
+ const trail = [];
75664
+ const visit = (id) => {
75665
+ if (state.get(id) === "done")
75666
+ return null;
75667
+ if (state.get(id) === "open")
75668
+ return [...trail.slice(trail.indexOf(id)), id];
75669
+ state.set(id, "open");
75670
+ trail.push(id);
75671
+ for (const dep of byId.get(id)?.dependsOn ?? []) {
75672
+ if (!byId.has(dep) || dep === id)
75673
+ continue;
75674
+ const found = visit(dep);
75675
+ if (found)
75676
+ return found;
75677
+ }
75678
+ trail.pop();
75679
+ state.set(id, "done");
75680
+ return null;
75681
+ };
75682
+ for (const slice of slices) {
75683
+ const found = visit(slice.id);
75684
+ if (found)
75685
+ return found;
75686
+ }
75687
+ return null;
75688
+ }
75689
+ function landingOrder(slices) {
75690
+ const order = [];
75691
+ const placed = new Set;
75692
+ const known = new Set(slices.map((slice) => slice.id));
75693
+ while (order.length < slices.length) {
75694
+ const next = slices.find((slice) => !placed.has(slice.id) && slice.dependsOn.every((dep) => placed.has(dep) || !known.has(dep)));
75695
+ if (!next)
75696
+ break;
75697
+ order.push(next.id);
75698
+ placed.add(next.id);
75699
+ }
75700
+ return order;
75701
+ }
75702
+ function waves(slices) {
75703
+ const level = new Map;
75704
+ const byId = new Map(slices.map((slice) => [slice.id, slice]));
75705
+ for (const id of landingOrder(slices)) {
75706
+ const deps = byId.get(id)?.dependsOn ?? [];
75707
+ level.set(id, deps.length ? Math.max(...deps.map((dep) => level.get(dep) ?? 0)) + 1 : 0);
75708
+ }
75709
+ const out = [];
75710
+ for (const slice of slices)
75711
+ (out[level.get(slice.id) ?? 0] ??= []).push(slice.id);
75712
+ return out.filter(Boolean);
75713
+ }
75714
+ function ancestors(slices) {
75715
+ const byId = new Map(slices.map((slice) => [slice.id, slice]));
75716
+ const out = new Map;
75717
+ for (const id of landingOrder(slices)) {
75718
+ const set = new Set;
75719
+ for (const dep of byId.get(id)?.dependsOn ?? []) {
75720
+ set.add(dep);
75721
+ for (const deeper of out.get(dep) ?? [])
75722
+ set.add(deeper);
75723
+ }
75724
+ out.set(id, set);
75725
+ }
75726
+ return out;
75727
+ }
75728
+ function analyzeOverlap(plan, candidates) {
75729
+ const shared = [...DEFAULT_SHARED, ...plan.shared];
75730
+ const before = ancestors(plan.slices);
75731
+ const related = (a, b) => Boolean(before.get(a)?.has(b) || before.get(b)?.has(a));
75732
+ const overlaps = [];
75733
+ const groups = new Map;
75734
+ const byCase = new Map;
75735
+ for (const file of [...new Set(candidates)].toSorted())
75736
+ (byCase.get(file.toLowerCase()) ?? byCase.set(file.toLowerCase(), []).get(file.toLowerCase()))?.push(file);
75737
+ for (const spellings of byCase.values()) {
75738
+ const file = spellings.join(" ~ ");
75739
+ const any = (patterns) => spellings.some((spelling) => matchesAny(patterns, spelling));
75740
+ const matching = plan.slices.filter((slice) => any(slice.scope) || any(slice.owns));
75741
+ if (matching.length < 2)
75742
+ continue;
75743
+ const ids = matching.map((slice) => slice.id);
75744
+ const owners = matching.filter((slice) => any(slice.owns));
75745
+ if (owners.length === 1) {
75746
+ overlaps.push({ path: file, slices: ids, resolution: "owner", owner: owners[0].id });
75747
+ continue;
75748
+ }
75749
+ if (owners.length === 0 && matching.every((a, i) => matching.slice(i + 1).every((b) => related(a.id, b.id)))) {
75750
+ overlaps.push({ path: file, slices: ids, resolution: "serialized" });
75751
+ continue;
75752
+ }
75753
+ const members = owners.length > 1 ? owners : matching;
75754
+ const reason = owners.length > 1 ? "several_owners" : "overlap";
75755
+ const isShared = any(shared);
75756
+ const key = `${reason}|${isShared}|${members.map((slice) => slice.id).join(",")}`;
75757
+ const group = groups.get(key);
75758
+ if (group)
75759
+ group.paths.push(file);
75760
+ else
75761
+ groups.set(key, {
75762
+ slices: members.map((slice) => slice.id),
75763
+ paths: [file],
75764
+ shared: isShared,
75765
+ reason,
75766
+ members,
75767
+ suggestion: ownerSuggestion(members[0].id, [])
75768
+ });
75769
+ }
75770
+ const conflicts = [...groups.values()].map(({ members, ...group }) => {
75771
+ const literal = members.find((slice) => slice.scope.some((pattern) => group.paths.includes(pattern)));
75772
+ const owner = ownerSuggestion((literal ?? members[0]).id, group.paths);
75773
+ if (group.reason === "several_owners")
75774
+ return {
75775
+ ...group,
75776
+ suggestion: {
75777
+ ...owner,
75778
+ text: `keep ${quoteList(group.paths)} in "owns" of slice ${owner.slice} only`
75779
+ }
75780
+ };
75781
+ const serialize = serializeSuggestion(group.slices);
75782
+ return group.shared ? { ...group, suggestion: owner, alternative: serialize } : { ...group, suggestion: serialize, alternative: owner };
75783
+ });
75784
+ return { overlaps, conflicts };
75785
+ }
75786
+ function branchRef(cwd, branch) {
75787
+ if (!branch || branch.startsWith("-"))
75788
+ return null;
75789
+ const local = resolveRef(cwd, `refs/heads/${branch}`);
75790
+ if (local)
75791
+ return local;
75792
+ const remote = pushRemoteName(cwd) ?? "origin";
75793
+ return resolveRef(cwd, `refs/remotes/${remote}/${branch}`);
75794
+ }
75795
+ function trunkRef(cwd, trunk) {
75796
+ if (!trunk || trunk.startsWith("-"))
75797
+ return null;
75798
+ const remote = pushRemoteName(cwd) ?? "origin";
75799
+ for (const ref of [`refs/remotes/${remote}/${trunk}`, `refs/heads/${trunk}`, trunk])
75800
+ if (resolveRef(cwd, ref))
75801
+ return ref;
75802
+ return null;
75803
+ }
75804
+ function defaultTrunk(cwd) {
75805
+ const base = defaultBase(cwd);
75806
+ return base ? base.replace(/^origin\//u, "") : "main";
75807
+ }
75808
+ function worktreeRoot(cwd) {
75809
+ const common = gitCommonDir(cwd);
75810
+ const main = common && path42.basename(common) === ".git" ? path42.dirname(common) : cwd;
75811
+ return path42.join(path42.dirname(main), `${path42.basename(main)}-wt`);
75812
+ }
75813
+ function fanoutsDir(cwd) {
75814
+ const root = storeRoot(cwd);
75815
+ if (!root.ok)
75816
+ return fanoutFail(root.code, root.error, root.unblock);
75817
+ return { ok: true, data: path42.join(root.value.root, "fanouts") };
75818
+ }
75819
+ function parseStored(raw) {
75820
+ if (!isRecord8(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
75821
+ return null;
75822
+ if (!Array.isArray(raw.slices))
75823
+ return null;
75824
+ const list = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
75825
+ const text = (value) => typeof value === "string" ? value : "";
75826
+ let globsValid = true;
75827
+ const globs = (value) => list(value).map((entry) => {
75828
+ const normalized = normalizePattern(entry);
75829
+ if ("error" in normalized) {
75830
+ globsValid = false;
75831
+ return entry;
75832
+ }
75833
+ return normalized.pattern;
75834
+ });
75835
+ const slices = [];
75836
+ for (const item of raw.slices) {
75837
+ if (!isRecord8(item) || typeof item.id !== "string" || typeof item.branch !== "string")
75838
+ return null;
75839
+ slices.push({
75840
+ id: item.id,
75841
+ branch: item.branch,
75842
+ base: text(item.base) || raw.trunk,
75843
+ worktree: text(item.worktree),
75844
+ tier: TIERS.includes(item.tier) ? item.tier : "standard",
75845
+ dependsOn: list(item.dependsOn),
75846
+ scope: globs(item.scope),
75847
+ owns: globs(item.owns),
75848
+ goal: text(item.goal),
75849
+ acceptance: list(item.acceptance),
75850
+ verify: list(item.verify),
75851
+ forbidden: list(item.forbidden),
75852
+ context: typeof item.context === "string" ? item.context : null,
75853
+ timebox: typeof item.timebox === "string" ? item.timebox : null
75854
+ });
75855
+ }
75856
+ const shared = globs(raw.shared);
75857
+ const ids = new Set(slices.map((slice) => slice.id));
75858
+ if (!globsValid || ids.size !== slices.length || slices.some((slice) => slice.dependsOn.some((dep) => dep === slice.id || !ids.has(dep))) || findCycle(slices))
75859
+ return null;
75860
+ return {
75861
+ v: typeof raw.v === "number" ? raw.v : FANOUT_VERSION,
75862
+ name: raw.name,
75863
+ trunk: raw.trunk,
75864
+ shared,
75865
+ slices,
75866
+ createdAt: text(raw.createdAt),
75867
+ updatedAt: text(raw.updatedAt)
75868
+ };
75869
+ }
75870
+ function readFanout(cwd, name) {
75871
+ const dir = fanoutsDir(cwd);
75872
+ if (!dir.ok)
75873
+ return dir;
75874
+ const file = path42.join(dir.data, stackFileName(name));
75875
+ let parsed = null;
75876
+ try {
75877
+ parsed = parseStored(JSON.parse(fs22.readFileSync(file, "utf8")));
75878
+ } catch (error) {
75879
+ if (error.code === "ENOENT")
75880
+ return { ok: true, data: null };
75881
+ parsed = null;
75882
+ }
75883
+ return parsed ? { ok: true, data: parsed } : fanoutFail("failed", `fanout file ${file} is not a valid plan`, `workit fanout plan <plan.json> --name ${name} # rewrite it`);
75884
+ }
75885
+ function listFanouts(cwd) {
75886
+ const dir = fanoutsDir(cwd);
75887
+ if (!dir.ok)
75888
+ return dir;
75889
+ let names = [];
75890
+ try {
75891
+ names = fs22.readdirSync(dir.data).filter((entry) => entry.endsWith(".json"));
75892
+ } catch {
75893
+ return { ok: true, data: [] };
75894
+ }
75895
+ const out = [];
75896
+ for (const entry of names) {
75897
+ let parsed = null;
75898
+ try {
75899
+ parsed = parseStored(JSON.parse(fs22.readFileSync(path42.join(dir.data, entry), "utf8")));
75900
+ } catch {
75901
+ parsed = null;
75902
+ }
75903
+ if (parsed && stackFileName(parsed.name) === entry)
75904
+ out.push(parsed);
75905
+ }
75906
+ return { ok: true, data: out.toSorted((a, b) => a.name.localeCompare(b.name)) };
75907
+ }
75908
+ function selectFanout(cwd, name, current) {
75909
+ if (name) {
75910
+ const read = readFanout(cwd, name);
75911
+ if (!read.ok)
75912
+ return read;
75913
+ return read.data ? { ok: true, data: read.data } : fanoutFail("not_found", `no fanout plan named ${name}`, "workit fanout plan <plan.json>");
75914
+ }
75915
+ const all = listFanouts(cwd);
75916
+ if (!all.ok)
75917
+ return all;
75918
+ const holding = all.data.filter((plan) => plan.slices.some((slice) => slice.branch === current));
75919
+ if (holding.length === 1)
75920
+ return { ok: true, data: holding[0] };
75921
+ if (all.data.length === 1)
75922
+ return { ok: true, data: all.data[0] };
75923
+ return all.data.length === 0 ? fanoutFail("not_found", "no fanout plan is recorded here", "workit fanout plan <plan.json>") : fanoutFail("invalid_input", `several fanout plans match; pick one (${all.data.map((plan) => plan.name).join(", ")})`, "pass --name <plan>");
75924
+ }
75925
+ function writeFanout(cwd, plan) {
75926
+ const dir = fanoutsDir(cwd);
75927
+ if (!dir.ok)
75928
+ return dir;
75929
+ const file = path42.join(dir.data, stackFileName(plan.name));
75930
+ const temp = `${file}.${process.pid}.tmp`;
75931
+ try {
75932
+ fs22.mkdirSync(dir.data, { recursive: true });
75933
+ fs22.writeFileSync(temp, `${JSON.stringify(plan, null, 2)}
75934
+ `);
75935
+ fs22.renameSync(temp, file);
75936
+ return { ok: true, data: file };
75937
+ } catch (error) {
75938
+ fs22.rmSync(temp, { force: true });
75939
+ return fanoutFail("unavailable", `cannot write ${file}: ${error.message}`);
75940
+ }
75941
+ }
75942
+ function planFanout(cwd, input, now = new Date) {
75943
+ const dir = fanoutsDir(cwd);
75944
+ if (!dir.ok)
75945
+ return dir;
75946
+ const parsed = parsePlan(input.trunk && isRecord8(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, { trunk: input.trunk ?? defaultTrunk(cwd), worktreeRoot: worktreeRoot(cwd) }, input.name);
75947
+ if (!parsed.ok)
75948
+ return fanoutFail("invalid_input", `the plan is incomplete: ${parsed.problems[0]}${parsed.problems.length > 1 ? ` (+${parsed.problems.length - 1} more)` : ""}`, "fill every listed field, then run workit fanout plan again", { problems: parsed.problems });
75949
+ const plan = parsed.plan;
75950
+ const badBranches = plan.slices.filter((slice) => !gitRun4(cwd, ["check-ref-format", "--branch", slice.branch]).ok).map((slice) => `slice ${slice.id}: ${slice.branch} is not a valid branch name`);
75951
+ if (badBranches.length)
75952
+ return fanoutFail("invalid_input", badBranches[0], "rename the branch in the plan", {
75953
+ problems: badBranches
75954
+ });
75955
+ const ref = trunkRef(cwd, plan.trunk);
75956
+ const tracked = ref ? nulList2(gitRun4(cwd, ["ls-tree", "-r", "-z", "--name-only", ref]).stdout) : [];
75957
+ const samples = plan.slices.flatMap((slice) => [...slice.scope, ...slice.owns].flatMap(scopeSamples));
75958
+ const { overlaps, conflicts } = analyzeOverlap(plan, [...tracked, ...samples]);
75959
+ if (conflicts.length)
75960
+ return fanoutFail("blocked", `${conflicts.length} file-scope overlap${conflicts.length === 1 ? "" : "s"} between slices: ${conflicts[0].slices.join(" and ")} on ${conflicts[0].paths[0]}${conflicts[0].paths.length > 1 ? ` (+${conflicts[0].paths.length - 1})` : ""}`, conflicts[0].suggestion.text, { name: plan.name, conflicts, overlaps });
75961
+ const existing = readFanout(cwd, plan.name);
75962
+ const previous = existing.ok ? existing.data : null;
75963
+ plan.createdAt = previous?.createdAt || now.toISOString();
75964
+ plan.updatedAt = now.toISOString();
75965
+ const written = writeFanout(cwd, plan);
75966
+ if (!written.ok)
75967
+ return written;
75968
+ appendObserved(cwd, {
75969
+ type: "fanout.planned",
75970
+ actor: input.actor,
75971
+ fanout: plan.name,
75972
+ trunk: plan.trunk,
75973
+ slices: plan.slices.length,
75974
+ ids: plan.slices.slice(0, 20).map((slice) => slice.id)
75975
+ });
75976
+ const grouped = waves(plan.slices);
75977
+ const notes = [];
75978
+ if (!ref)
75979
+ notes.push(`trunk ${plan.trunk} does not resolve here; only sample paths of each glob were compared`);
75980
+ grouped.forEach((wave, index) => {
75981
+ if (wave.length > IN_FLIGHT_CAP)
75982
+ notes.push(`wave ${index + 1} has ${wave.length} slices; keep 4-${IN_FLIGHT_CAP} in flight and queue the rest`);
75983
+ });
75984
+ for (const slice of plan.slices)
75985
+ if (slice.dependsOn.length > 1 && slice.base === plan.trunk)
75986
+ notes.push(`slice ${slice.id} depends on ${slice.dependsOn.join(", ")}: start it from ${plan.trunk} after they land`);
75987
+ return {
75988
+ ok: true,
75989
+ data: {
75990
+ name: plan.name,
75991
+ trunk: plan.trunk,
75992
+ file: written.data,
75993
+ created: previous === null,
75994
+ slices: plan.slices.map(({ id, branch, base, worktree, tier, dependsOn, scope, owns }) => ({
75995
+ id,
75996
+ branch,
75997
+ base,
75998
+ worktree,
75999
+ tier,
76000
+ dependsOn,
76001
+ scope,
76002
+ owns
76003
+ })),
76004
+ overlaps,
76005
+ waves: grouped,
76006
+ landingOrder: landingOrder(plan.slices),
76007
+ notes
76008
+ }
76009
+ };
76010
+ }
76011
+ var FANOUT_VERSION = 1, TIERS, fanoutFail = (code, error, unblock, data) => ({
76012
+ ok: false,
76013
+ code,
76014
+ error,
76015
+ ...unblock ? { unblock } : {},
76016
+ ...data ? { data } : {}
76017
+ }), DEFAULT_SHARED, escapeRegExp4 = (text) => text.replace(/[.+^$()|[\]\\{}*?]/gu, "\\$&"), unescape2 = (pattern) => pattern.replace(/\\(.)/gu, "$1"), hasWildcard = (pattern) => /[*?{]/u.test(pattern.replace(/\\./gu, "")), compiled, matchesAny = (patterns, file) => patterns.some((pattern) => matchesScope(pattern, file)), isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), PLACEHOLDER2, filled = (text) => text.trim() !== "" && !PLACEHOLDER2.test(text.trim()), textList = (value) => {
76018
+ if (typeof value === "string")
76019
+ return [value];
76020
+ if (Array.isArray(value) && value.every((item) => typeof item === "string"))
76021
+ return value;
76022
+ return null;
76023
+ }, ID, quoteList = (items) => items.map((item) => `"${item}"`).join(", "), ownerSuggestion = (slice, paths) => ({
76024
+ kind: "owner",
76025
+ slice,
76026
+ paths,
76027
+ text: `add ${quoteList(paths)} to "owns" of slice ${slice}`
76028
+ }), serializeSuggestion = (ids) => {
76029
+ const edges = ids.slice(1).map((slice, index) => ({ slice, dependsOn: ids[index] }));
76030
+ return {
76031
+ kind: "serialize",
76032
+ edges,
76033
+ text: edges.map((edge) => `add "${edge.dependsOn}" to "dependsOn" of slice ${edge.slice}`).join("; ")
76034
+ };
76035
+ }, gitRun4 = (cwd, args) => {
76036
+ const run = spawnSync20("git", args, {
76037
+ cwd,
76038
+ encoding: "utf8",
76039
+ timeout: GIT_TIMEOUTS.local,
76040
+ killSignal: "SIGKILL",
76041
+ windowsHide: true,
76042
+ maxBuffer: 64 * 1024 * 1024,
76043
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", LC_ALL: "C", LANGUAGE: "C" }
76044
+ });
76045
+ return {
76046
+ ok: run.status === 0,
76047
+ status: run.status,
76048
+ stdout: run.stdout ?? "",
76049
+ stderr: run.error ? String(run.error.message) : run.stderr ?? ""
76050
+ };
76051
+ }, nulList2 = (text) => text.split("\x00").filter(Boolean), IN_FLIGHT_CAP = 6;
76052
+ var init_fanout = __esm(() => {
76053
+ init_rev();
76054
+ init_ledger();
76055
+ init_stack();
76056
+ TIERS = ["mundane", "standard", "hard"];
76057
+ DEFAULT_SHARED = [
76058
+ "**/package.json",
76059
+ "**/{package-lock.json,npm-shrinkwrap.json,bun.lock,bun.lockb,pnpm-lock.yaml,pnpm-workspace.yaml,yarn.lock}",
76060
+ "**/{Cargo.lock,go.mod,go.sum,poetry.lock,uv.lock,Gemfile.lock,composer.lock}",
76061
+ "**/index.{ts,tsx,js,jsx,mjs,cjs}",
76062
+ "**/tsconfig.json",
76063
+ ".github/**",
76064
+ ".gitlab-ci.yml",
76065
+ "CHANGELOG.md"
76066
+ ];
76067
+ compiled = new Map;
76068
+ PLACEHOLDER2 = /^(?:<[^>]*>|tbd|todo|fixme|\?+|\.\.\.|…)$/iu;
76069
+ ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
76070
+ });
76071
+
76072
+ // packages/workit-core/src/fanout-check.ts
76073
+ function mergeConflicts(cwd, a, b) {
76074
+ const run = gitRun4(cwd, [
76075
+ "merge-tree",
76076
+ "--write-tree",
76077
+ "--name-only",
76078
+ "--no-messages",
76079
+ "-z",
76080
+ a,
76081
+ b
76082
+ ]);
76083
+ if (run.status === 0)
76084
+ return [];
76085
+ if (run.status === 1)
76086
+ return [...new Set(nulList2(run.stdout).slice(1))];
76087
+ const first = run.stderr.trim().split(`
76088
+ `)[0] || `git merge-tree exited ${run.status}`;
76089
+ return {
76090
+ error: /write-tree|usage: git merge-tree/u.test(run.stderr) ? `${first} (needs git >= 2.38 for merge-tree --write-tree)` : first
76091
+ };
76092
+ }
76093
+ function checkFanout(cwd, plan, options = {}) {
76094
+ const unknown = (options.only ?? []).filter((id) => !plan.slices.some((s) => s.id === id));
76095
+ if (unknown.length)
76096
+ return fanoutFail("invalid_input", `${unknown.join(", ")}: not a slice of fanout ${plan.name}`, `slices: ${plan.slices.map((slice) => slice.id).join(", ")}`);
76097
+ const trunk = options.trunkRef ?? trunkRef(cwd, plan.trunk);
76098
+ if (!SAFE(trunk) || !resolves(cwd, trunk))
76099
+ return fanoutFail("unavailable", `trunk ${options.trunkRef ?? plan.trunk} does not resolve here`, `git fetch, or pass --base <ref>`);
76100
+ const order = landingOrder(plan.slices);
76101
+ const byId = new Map(plan.slices.map((slice) => [slice.id, slice]));
76102
+ const tips = new Map(plan.slices.map((slice) => [slice.id, branchRef(cwd, slice.branch)]));
76103
+ const ledger = readLedger(cwd);
76104
+ const rows = ledger.ok ? ledger.value.rows : [];
76105
+ const checks = new Map;
76106
+ const changedBy = new Map;
76107
+ for (const slice of plan.slices)
76108
+ checks.set(slice.id, inspect(cwd, plan, slice, trunk, tips, rows, changedBy));
76109
+ const conflicts = [];
76110
+ for (let i = 0;i < order.length; i += 1)
76111
+ for (let j = i + 1;j < order.length; j += 1) {
76112
+ const first = order[i];
76113
+ const second = order[j];
76114
+ const a = tips.get(first);
76115
+ const b = tips.get(second);
76116
+ if (!a || !b || isAncestor2(cwd, a, b) || isAncestor2(cwd, b, a))
76117
+ continue;
76118
+ const check = checks.get(second);
76119
+ const theirs = new Map((changedBy.get(first) ?? []).map((file) => [file.toLowerCase(), file]));
76120
+ const cased = (changedBy.get(second) ?? []).flatMap((file) => {
76121
+ const other = theirs.get(file.toLowerCase());
76122
+ return other !== undefined && other !== file ? [`${other} ~ ${file}`] : [];
76123
+ });
76124
+ if (cased.length) {
76125
+ conflicts.push({
76126
+ first,
76127
+ second,
76128
+ paths: cased,
76129
+ caseOnly: true,
76130
+ resolution: `use ${first}'s spelling on ${second} (git mv), then workit fanout check`
76131
+ });
76132
+ check.siblingConflicts.push({ with: first, paths: cased });
76133
+ check.reasons.push(`differs only in case from ${first}: ${cased.join(", ")}`);
76134
+ }
76135
+ const result = mergeConflicts(cwd, a, b);
76136
+ if (!Array.isArray(result)) {
76137
+ check.reasons.push(`cannot merge with ${first}: ${result.error}`);
76138
+ continue;
76139
+ }
76140
+ if (result.length === 0)
76141
+ continue;
76142
+ const related = byId.get(second)?.dependsOn.includes(first);
76143
+ conflicts.push({
76144
+ first,
76145
+ second,
76146
+ paths: result,
76147
+ resolution: related ? `rebase ${second} onto ${byId.get(first)?.branch} (its dependency), then workit fanout check` : `land ${first} first, then rebase ${second} onto ${plan.trunk} and run workit fanout check; or make ${second} depend on ${first}`
76148
+ });
76149
+ check.siblingConflicts.push({ with: first, paths: result });
76150
+ check.reasons.push(`conflicts with ${first} on ${result.join(", ")}`);
76151
+ }
76152
+ for (const id of order) {
76153
+ const check = checks.get(id);
76154
+ check.waitsFor = (byId.get(id)?.dependsOn ?? []).filter((dep) => checks.get(dep)?.status !== "ready");
76155
+ if (check.waitsFor.length)
76156
+ check.reasons.push(`waits for ${check.waitsFor.join(", ")}`);
76157
+ check.status = check.reasons.length ? "blocked" : "ready";
76158
+ }
76159
+ const selected = options.only?.length ? new Set(options.only) : null;
76160
+ const slices = order.filter((id) => !selected || selected.has(id)).map((id) => checks.get(id));
76161
+ const ready = slices.every((check) => check.status === "ready");
76162
+ const blocked = slices.find((check) => check.status === "blocked");
76163
+ const notes = plan.v === FANOUT_VERSION ? [] : [
76164
+ `plan file version ${plan.v} is not ${FANOUT_VERSION}; read leniently (re-plan to rewrite it)`
76165
+ ];
76166
+ const next = blocked ? unblockFor(blocked, plan) : `land in this order: ${slices.map((check) => check.id).join(", ")} (workit pr merge each once verified; stacked slices with workit stack land)`;
76167
+ return {
76168
+ ok: true,
76169
+ data: {
76170
+ name: plan.name,
76171
+ trunk: plan.trunk,
76172
+ trunkRef: trunk,
76173
+ ready,
76174
+ slices,
76175
+ conflicts: conflicts.filter((conflict) => !selected || selected.has(conflict.first) || selected.has(conflict.second)),
76176
+ landingOrder: order,
76177
+ notes,
76178
+ next
76179
+ }
76180
+ };
76181
+ }
76182
+ function inspect(cwd, plan, slice, trunk, tips, rows, changedBy) {
76183
+ const head = tips.get(slice.id) ?? null;
76184
+ const check = {
76185
+ id: slice.id,
76186
+ branch: slice.branch,
76187
+ head,
76188
+ base: slice.base,
76189
+ status: "blocked",
76190
+ changed: 0,
76191
+ outOfScope: [],
76192
+ trunkConflicts: [],
76193
+ siblingConflicts: [],
76194
+ waitsFor: [],
76195
+ verdict: { accepted: false, basis: "missing" },
76196
+ reasons: []
76197
+ };
76198
+ if (!head) {
76199
+ check.reasons.push(`branch ${slice.branch} not found (not started, landed, or not fetched)`);
76200
+ return check;
76201
+ }
76202
+ const parent = plan.slices.find((other) => other.branch === slice.base);
76203
+ const baseRef = slice.base === plan.trunk ? trunk : parent ? tips.get(parent.id) ?? null : branchRef(cwd, slice.base);
76204
+ if (!SAFE(baseRef)) {
76205
+ check.reasons.push(`base ${slice.base} not found`);
76206
+ return check;
76207
+ }
76208
+ const diff = gitRun4(cwd, ["diff", "--name-only", "--no-renames", "-z", `${baseRef}...${head}`]);
76209
+ if (!diff.ok) {
76210
+ check.reasons.push(`cannot diff ${slice.base}...${slice.branch}: ${diff.stderr.trim()}`);
76211
+ return check;
76212
+ }
76213
+ const changed = nulList2(diff.stdout);
76214
+ changedBy.set(slice.id, changed);
76215
+ check.changed = changed.length;
76216
+ if (changed.length === 0)
76217
+ check.reasons.push(`no changes beyond ${slice.base}`);
76218
+ check.outOfScope = changed.filter((file) => !inScope(plan, slice, file)).toSorted();
76219
+ if (check.outOfScope.length)
76220
+ check.reasons.push(`edits outside its scope: ${check.outOfScope.join(", ")}`);
76221
+ const trunkResult = mergeConflicts(cwd, trunk, head);
76222
+ if (Array.isArray(trunkResult)) {
76223
+ check.trunkConflicts = trunkResult;
76224
+ if (trunkResult.length)
76225
+ check.reasons.push(`conflicts with ${plan.trunk} on ${trunkResult.join(", ")}`);
76226
+ } else
76227
+ check.reasons.push(`cannot merge with ${plan.trunk}: ${trunkResult.error}`);
76228
+ const verdict = checkVerdicts(cwd, slice.branch, rows);
76229
+ check.verdict = { accepted: verdict.accepted.accepted, basis: verdict.current.basis };
76230
+ return check;
76231
+ }
76232
+ function unblockFor(check, plan) {
76233
+ if (!check.head)
76234
+ return `start or fetch ${check.branch}, then workit fanout check; if it landed, re-plan without slice ${check.id}`;
76235
+ if (check.outOfScope.length)
76236
+ return `move ${check.outOfScope.join(", ")} off ${check.branch} (a follow-up slice), or widen slice ${check.id}'s scope with workit fanout plan, then workit fanout check`;
76237
+ if (check.trunkConflicts.length)
76238
+ return `rebase ${check.branch} onto ${plan.trunk} and resolve ${check.trunkConflicts.join(", ")}, then workit fanout check`;
76239
+ if (check.siblingConflicts.length) {
76240
+ const first = check.siblingConflicts[0].with;
76241
+ return `land ${first} first, then rebase ${check.branch} onto ${plan.trunk} and run workit fanout check`;
76242
+ }
76243
+ if (check.waitsFor.length)
76244
+ return `unblock ${check.waitsFor.join(", ")} first`;
76245
+ return check.reasons[0] ?? "workit fanout check";
76246
+ }
76247
+ var SAFE = (value) => Boolean(value) && !value?.startsWith("-"), isAncestor2 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, resolves = (cwd, ref) => gitRun4(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]).ok;
76248
+ var init_fanout_check = __esm(() => {
76249
+ init_ledger();
76250
+ init_fanout();
76251
+ });
76252
+
76253
+ // packages/workit-cli/src/verbs/fanout.ts
76254
+ var exports_fanout = {};
76255
+ __export(exports_fanout, {
76256
+ run: () => run15
76257
+ });
76258
+ import fs23 from "node:fs";
76259
+ import path43 from "node:path";
76260
+ function renderBlockedPlan(io, result) {
76261
+ if (io.json || result.code !== "blocked" && result.code !== "invalid_input")
76262
+ return;
76263
+ const problems = result.data?.problems;
76264
+ if (Array.isArray(problems))
76265
+ for (const problem of problems)
76266
+ io.stderr(` - ${problem}
76267
+ `);
76268
+ const conflicts = result.data?.conflicts;
76269
+ if (Array.isArray(conflicts))
76270
+ for (const conflict of conflicts)
76271
+ io.stderr(` - ${conflict.slices.join(" + ")}: ${conflict.paths.join(", ")}
76272
+ fix: ${conflict.suggestion.text}${conflict.alternative ? `
76273
+ or: ${conflict.alternative.text}` : ""}
76274
+ `);
76275
+ }
76276
+ function resolveTrunk(io, raw, flag, track) {
76277
+ if (flag !== null)
76278
+ return { trunk: flag };
76279
+ const named = typeof raw === "object" && raw !== null && typeof raw.trunk === "string" ? raw.trunk.trim() || null : null;
76280
+ if (named !== null && track === null)
76281
+ return { trunk: null };
76282
+ const derived = releaseTrunk(io, track, null);
76283
+ if ("error" in derived)
76284
+ return {
76285
+ code: emit(io, fail("blocked", derived.error, {
76286
+ unblock: "workit fanout plan <plan.json> --track <name> # or --trunk <branch>"
76287
+ }))
76288
+ };
76289
+ if (named !== null && derived.trunk !== named)
76290
+ return {
76291
+ code: usage3(io, `the plan names trunk ${named}; --track ${track} targets ${derived.trunk ?? "(none)"}`, PLAN_USAGE2)
76292
+ };
76293
+ return { trunk: derived.tracked || track !== null ? derived.trunk : null };
76294
+ }
76295
+ function plan2(argv, io) {
76296
+ const flags = parseFlags(argv, { name: "value", trunk: "value", track: "value" });
76297
+ if (typeof flags === "string")
76298
+ return usage3(io, flags, PLAN_USAGE2);
76299
+ const track = flags.values.track ?? null;
76300
+ if (track !== null && flags.values.trunk !== undefined)
76301
+ return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE2);
76302
+ if (flags.positionals.length !== 1)
76303
+ return usage3(io, flags.positionals.length ? `unexpected argument ${flags.positionals[1]}` : "missing <plan.json>", PLAN_USAGE2);
76304
+ const file = path43.resolve(io.cwd, flags.positionals[0]);
76305
+ let raw;
76306
+ try {
76307
+ raw = JSON.parse(fs23.readFileSync(file, "utf8"));
76308
+ } catch (error) {
76309
+ return usage3(io, `cannot read ${file} as JSON: ${error.message}`, PLAN_USAGE2);
76310
+ }
76311
+ const trunk = resolveTrunk(io, raw, flags.values.trunk ?? null, track);
76312
+ if ("code" in trunk)
76313
+ return trunk.code;
76314
+ const result = planFanout(io.cwd, {
76315
+ raw,
76316
+ name: flags.values.name ?? null,
76317
+ trunk: trunk.trunk,
76318
+ actor: actorFromEnv(io.env)
76319
+ });
76320
+ if (!result.ok) {
76321
+ const code = failed(io, result);
76322
+ renderBlockedPlan(io, result);
76323
+ return code;
76324
+ }
76325
+ return emit(io, ok(result.data), (data) => [
76326
+ `${data.created ? "planned" : "re-planned"} fanout ${data.name} on ${data.trunk}: ${data.slices.length} slice${data.slices.length === 1 ? "" : "s"}`,
76327
+ ...data.slices.map((slice) => ` ${slice.id} [${slice.tier}] ${slice.branch} <- ${slice.base}${slice.dependsOn.length ? ` after ${slice.dependsOn.join(", ")}` : ""}
76328
+ worktree ${slice.worktree}`),
76329
+ ...data.overlaps.map((overlap) => ` shared ${overlap.path}: ${overlap.resolution === "owner" ? `owned by ${overlap.owner}` : `serialized (${overlap.slices.join(" -> ")})`}`),
76330
+ `waves: ${data.waves.map((wave) => wave.join(", ")).join(" | ")}`,
76331
+ `landing order: ${data.landingOrder.join(", ")}`,
76332
+ ...data.notes.map((note) => `note: ${note}`)
76333
+ ]);
76334
+ }
76335
+ function check2(argv, io) {
76336
+ const flags = parseFlags(argv, { name: "value", base: "value" });
76337
+ if (typeof flags === "string")
76338
+ return usage3(io, flags, CHECK_USAGE);
76339
+ const selected = selectFanout(io.cwd, flags.values.name ?? null, currentBranch(io.cwd));
76340
+ if (!selected.ok)
76341
+ return failed(io, selected);
76342
+ const result = checkFanout(io.cwd, selected.data, {
76343
+ only: flags.positionals,
76344
+ trunkRef: flags.values.base ?? null
76345
+ });
76346
+ if (!result.ok)
76347
+ return failed(io, result);
76348
+ if (result.data.ready)
76349
+ return emit(io, ok(result.data), renderCheck);
76350
+ if (!io.json)
76351
+ for (const line of renderCheck(result.data))
76352
+ io.stdout(`${line}
76353
+ `);
76354
+ const first = result.data.slices.find((slice) => slice.status === "blocked");
76355
+ return emit(io, fail("blocked", `slice ${first?.id ?? "?"} is not ready: ${first?.reasons[0] ?? "blocked"}`, {
76356
+ data: result.data,
76357
+ unblock: result.data.next
76358
+ }));
76359
+ }
76360
+ async function run15(argv, io) {
76361
+ const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
76362
+ if (sub === "plan")
76363
+ return plan2(rest, io);
76364
+ if (sub === "check")
76365
+ return check2(rest, io);
76366
+ return usage3(io, sub && !sub.startsWith("-") ? `unknown fanout subcommand "${sub}"` : "missing fanout subcommand", USAGE8);
76367
+ }
76368
+ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--json]", CHECK_USAGE = "workit fanout check [<slice>…] [--name <n>] [--base <ref>] [--json]", USAGE8 = "workit fanout plan|check ... (workit help fanout)", failed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), renderCheck = (data) => [
76369
+ `fanout ${data.name} against ${data.trunkRef}: ${data.ready ? "ready to land" : "blocked"}`,
76370
+ ...data.slices.map((slice) => ` ${slice.status === "ready" ? "ok " : "x "}${slice.id} (${slice.branch}) ${slice.changed} file${slice.changed === 1 ? "" : "s"} · verdict ${slice.verdict.accepted ? "accepted" : slice.verdict.basis}${slice.reasons.length ? `
76371
+ ${slice.reasons.join(`
76372
+ `)}` : ""}`),
76373
+ ...data.conflicts.map((conflict) => ` conflict ${conflict.first} x ${conflict.second}: ${conflict.paths.join(", ")}
76374
+ fix: ${conflict.resolution}`),
76375
+ `landing order: ${data.landingOrder.join(", ")}`,
76376
+ `next: ${data.next}`,
76377
+ ...data.notes.map((note) => `note: ${note}`)
76378
+ ];
76379
+ var init_fanout2 = __esm(() => {
76380
+ init_fanout_check();
76381
+ init_fanout();
76382
+ init_rev();
76383
+ init_ledger();
76384
+ init_output();
76385
+ init_forge_common();
76386
+ });
76387
+
75406
76388
  // packages/workit-cli/src/verbs/ledger.ts
75407
76389
  var exports_ledger = {};
75408
76390
  __export(exports_ledger, {
75409
- run: () => run15
76391
+ run: () => run16
75410
76392
  });
75411
76393
  import { randomBytes as randomBytes4 } from "node:crypto";
75412
76394
  import { parseArgs } from "node:util";
@@ -75430,7 +76412,7 @@ function targetBranch(io, rows, branch, pr) {
75430
76412
  }
75431
76413
  return { ok: true, value: branch ?? null };
75432
76414
  }
75433
- async function run15(argv, io) {
76415
+ async function run16(argv, io) {
75434
76416
  let parsed;
75435
76417
  try {
75436
76418
  parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
@@ -75458,7 +76440,7 @@ async function run15(argv, io) {
75458
76440
  return usage4(io, `unknown ledger subcommand "${sub}"`);
75459
76441
  const ledger = readLedger(io.cwd);
75460
76442
  if (!ledger.ok)
75461
- return failed(io, ledger);
76443
+ return failed2(io, ledger);
75462
76444
  const { rows } = ledger.value;
75463
76445
  if (sub === "list" || sub === "show") {
75464
76446
  if (rest.length)
@@ -75480,7 +76462,7 @@ async function run15(argv, io) {
75480
76462
  return usage4(io, `unexpected argument: ${rest.at(-1)}`);
75481
76463
  const target = targetBranch(io, rows, rest[0] ?? values.branch, pr);
75482
76464
  if (!target.ok)
75483
- return failed(io, target);
76465
+ return failed2(io, target);
75484
76466
  const branch = target.value ?? currentBranch(io.cwd);
75485
76467
  if (!branch)
75486
76468
  return emit(io, fail("invalid_input", "HEAD is detached; name the branch", {
@@ -75490,7 +76472,7 @@ async function run15(argv, io) {
75490
76472
  }
75491
76473
  const target = targetBranch(io, rows, values.branch, pr);
75492
76474
  if (!target.ok)
75493
- return failed(io, target);
76475
+ return failed2(io, target);
75494
76476
  const acting = actorFor(io, values);
75495
76477
  if (acting instanceof Error)
75496
76478
  return usage4(io, acting.message);
@@ -75529,7 +76511,7 @@ async function run15(argv, io) {
75529
76511
  ..."derivedFrom" in acting ? { derivedFrom: acting.derivedFrom } : {}
75530
76512
  })), (row) => `recorded verdict ${row.id}: ${row.result} [${row.kind}] for ${row.branch} @ ${(row.head ?? "").slice(0, 12)} as ${actor.session ?? "no session"}${row.self ? ` (self${row.selfReason === "no_session" ? ": WORKIT_SESSION_ID unset" : ""}; never accepted)` : ""}`);
75531
76513
  }
75532
- var USAGE8 = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS, SESSION_SAFE2, ROLE_SAFE, actorFor = (io, values) => {
76514
+ var USAGE9 = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS, SESSION_SAFE2, ROLE_SAFE, actorFor = (io, values) => {
75533
76515
  const actor = actorFromEnv(io.env);
75534
76516
  if (values.session !== undefined && values.as !== undefined)
75535
76517
  return new Error("pass --session or --as, not both");
@@ -75553,7 +76535,7 @@ var USAGE8 = "workit ledger decision|ruling|verdict|list|check ... (workit help
75553
76535
  return;
75554
76536
  const parsed = Number(value);
75555
76537
  return Number.isInteger(parsed) && parsed > 0 ? parsed : new Error(`${flag} must be a positive integer`);
75556
- }, 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: USAGE8 })), verdictLines = (check) => {
76538
+ }, failed2 = (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) : failed2(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE9 })), verdictLines = (check) => {
75557
76539
  const lines = [
75558
76540
  `${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(", ")})`}; review: ${check.review}`
75559
76541
  ];
@@ -90550,7 +91532,7 @@ var init_ast = __esm(() => {
90550
91532
 
90551
91533
  // packages/workit-cli/src/test-audit/rules.ts
90552
91534
  import { builtinModules } from "node:module";
90553
- import path42 from "node:path";
91535
+ import path44 from "node:path";
90554
91536
  function testCallee(call) {
90555
91537
  let callee = call.callee;
90556
91538
  if (callee.type === "CallExpression")
@@ -91043,12 +92025,12 @@ function checkConstantsOnly(ctx, emit) {
91043
92025
  }
91044
92026
  function checkOverMocking(ctx, emit) {
91045
92027
  const { program } = ctx.parsed;
91046
- const unit = path42.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
92028
+ const unit = path44.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
91047
92029
  const mocked = find2(program, (node) => node.type === "CallExpression" && MODULE_MOCK.test(calleeName(node.callee) ?? "") && node.arguments[0]?.type === "StringLiteral");
91048
92030
  const internal = mocked.filter((call) => /^(?:\.|@\/|~\/)/.test(call.arguments[0].value));
91049
92031
  for (const call of mocked) {
91050
92032
  const spec = call.arguments[0].value;
91051
- const base = path42.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
92033
+ const base = path44.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
91052
92034
  if (base === unit)
91053
92035
  emit({
91054
92036
  rule: "over-mocking",
@@ -91436,11 +92418,11 @@ var init_rules = __esm(() => {
91436
92418
  });
91437
92419
 
91438
92420
  // packages/workit-cli/src/test-audit/audit.ts
91439
- import { spawnSync as spawnSync20 } from "node:child_process";
92421
+ import { spawnSync as spawnSync21 } from "node:child_process";
91440
92422
  import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync19, statSync as statSync7 } from "node:fs";
91441
- import path43 from "node:path";
92423
+ import path45 from "node:path";
91442
92424
  function git6(cwd, args) {
91443
- const run = spawnSync20("git", ["-c", "core.quotepath=off", ...args], {
92425
+ const run = spawnSync21("git", ["-c", "core.quotepath=off", ...args], {
91444
92426
  cwd,
91445
92427
  encoding: "utf8",
91446
92428
  maxBuffer: 64 * 1024 * 1024
@@ -91451,7 +92433,7 @@ function walkTests(dir, out) {
91451
92433
  for (const entry of readdirSync8(dir, { withFileTypes: true })) {
91452
92434
  if (entry.name.startsWith(".") && entry.name !== "." && entry.isDirectory())
91453
92435
  continue;
91454
- const full = path43.join(dir, entry.name);
92436
+ const full = path45.join(dir, entry.name);
91455
92437
  if (entry.isDirectory()) {
91456
92438
  if (!SKIP_DIRS.has(entry.name))
91457
92439
  walkTests(full, out);
@@ -91463,7 +92445,7 @@ function expandPaths(cwd, inputs) {
91463
92445
  const files = [];
91464
92446
  const missing = [];
91465
92447
  for (const input of inputs.length > 0 ? inputs : ["."]) {
91466
- const full = path43.resolve(cwd, input);
92448
+ const full = path45.resolve(cwd, input);
91467
92449
  if (!existsSync18(full))
91468
92450
  missing.push(input);
91469
92451
  else if (statSync7(full).isDirectory())
@@ -91500,11 +92482,11 @@ function changedLines(cwd, base) {
91500
92482
  for (let offset = 0;offset < count; offset += 1)
91501
92483
  lines.add(start + offset);
91502
92484
  }
91503
- changed.set(path43.join(root, file), lines);
92485
+ changed.set(path45.join(root, file), lines);
91504
92486
  }
91505
92487
  const untracked = git6(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
91506
92488
  for (const file of untracked.stdout.split("\x00").filter(Boolean))
91507
- changed.set(path43.join(root, file), "all");
92489
+ changed.set(path45.join(root, file), "all");
91508
92490
  return changed;
91509
92491
  }
91510
92492
  function auditFiles(files, display) {
@@ -91566,7 +92548,7 @@ function auditFiles(files, display) {
91566
92548
  durationMs: Math.round(performance.now() - started)
91567
92549
  };
91568
92550
  }
91569
- var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path43.sep).join("/"), SEVERITY_ORDER, ruleIds;
92551
+ var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path45.sep).join("/"), SEVERITY_ORDER, ruleIds;
91570
92552
  var init_audit = __esm(() => {
91571
92553
  init_ast();
91572
92554
  init_rules();
@@ -91596,7 +92578,7 @@ import {
91596
92578
  copyFileSync as copyFileSync6
91597
92579
  } from "node:fs";
91598
92580
  import os14 from "node:os";
91599
- import path44 from "node:path";
92581
+ import path46 from "node:path";
91600
92582
  function mutantsFor(source, file, lines) {
91601
92583
  const parsed = parseSource(source, file);
91602
92584
  const out = [];
@@ -91669,7 +92651,7 @@ function relatedTests(source, testFiles) {
91669
92651
  for (const match of text.matchAll(SPECIFIER)) {
91670
92652
  const spec = match[1];
91671
92653
  if (spec.startsWith(".")) {
91672
- if (stripExt(path44.posix.join(path44.posix.dirname(file), spec)) === target)
92654
+ if (stripExt(path46.posix.join(path46.posix.dirname(file), spec)) === target)
91673
92655
  return true;
91674
92656
  } else if (spec.includes("/") && stripExt(spec).split("/").slice(-2).join("/") === tail)
91675
92657
  return true;
@@ -91689,12 +92671,12 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
91689
92671
  for (const name of names) {
91690
92672
  if (!name.startsWith(TEMP_PREFIX))
91691
92673
  continue;
91692
- const dir = path44.join(tmp, name);
92674
+ const dir = path46.join(tmp, name);
91693
92675
  try {
91694
92676
  const stat = lstatSync7(dir);
91695
92677
  if (!stat.isDirectory() || uid !== undefined && stat.uid !== uid)
91696
92678
  continue;
91697
- const pid = Number(readFileSync20(path44.join(dir, PID_FILE), "utf8"));
92679
+ const pid = Number(readFileSync20(path46.join(dir, PID_FILE), "utf8"));
91698
92680
  if (Number.isInteger(pid) && pid > 0 && alive(pid))
91699
92681
  continue;
91700
92682
  } catch (error) {
@@ -91711,8 +92693,8 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
91711
92693
  function linkNodeModules(origin, target, repo, copy) {
91712
92694
  mkdirSync9(target, { recursive: true });
91713
92695
  for (const entry of readdirSync9(origin, { withFileTypes: true })) {
91714
- const from = path44.join(origin, entry.name);
91715
- const to = path44.join(target, entry.name);
92696
+ const from = path46.join(origin, entry.name);
92697
+ const to = path46.join(target, entry.name);
91716
92698
  if (entry.name.startsWith("@") && entry.isDirectory()) {
91717
92699
  linkNodeModules(from, to, repo, copy);
91718
92700
  continue;
@@ -91728,9 +92710,9 @@ function linkNodeModules(origin, target, repo, copy) {
91728
92710
  } catch {
91729
92711
  continue;
91730
92712
  }
91731
- const relative = path44.relative(repo, real);
91732
- if (!relative.startsWith("..") && !path44.isAbsolute(relative) && !relative.split(path44.sep).includes("node_modules")) {
91733
- symlinkSync(path44.join(copy, relative), to, "junction");
92713
+ const relative = path46.relative(repo, real);
92714
+ if (!relative.startsWith("..") && !path46.isAbsolute(relative) && !relative.split(path46.sep).includes("node_modules")) {
92715
+ symlinkSync(path46.join(copy, relative), to, "junction");
91734
92716
  continue;
91735
92717
  }
91736
92718
  }
@@ -91746,7 +92728,7 @@ function copySymlink(repo, tree, from, to) {
91746
92728
  }
91747
92729
  const realRepo = realpathSync9(repo);
91748
92730
  if (real && inside2(realRepo, real)) {
91749
- symlinkSync(path44.relative(path44.dirname(to), path44.join(tree, path44.relative(realRepo, real))), to);
92731
+ symlinkSync(path46.relative(path46.dirname(to), path46.join(tree, path46.relative(realRepo, real))), to);
91750
92732
  return;
91751
92733
  }
91752
92734
  if (real && statSync8(real).isFile())
@@ -91755,10 +92737,10 @@ function copySymlink(repo, tree, from, to) {
91755
92737
  symlinkSync(readlinkSync3(from), to);
91756
92738
  }
91757
92739
  function writeInside(copy, file, content) {
91758
- const target = path44.join(copy, file);
92740
+ const target = path46.join(copy, file);
91759
92741
  try {
91760
92742
  const stat = lstatSync7(target);
91761
- if (!stat.isFile() || !inside2(realpathSync9(copy), realpathSync9(path44.dirname(target))))
92743
+ if (!stat.isFile() || !inside2(realpathSync9(copy), realpathSync9(path46.dirname(target))))
91762
92744
  return false;
91763
92745
  } catch {
91764
92746
  return false;
@@ -91769,9 +92751,9 @@ function writeInside(copy, file, content) {
91769
92751
  return true;
91770
92752
  }
91771
92753
  function copyWorkingTree(repo) {
91772
- const dir = mkdtempSync4(path44.join(os14.tmpdir(), TEMP_PREFIX));
91773
- writeFileSync9(path44.join(dir, PID_FILE), String(process.pid));
91774
- const tree = path44.join(dir, "tree");
92754
+ const dir = mkdtempSync4(path46.join(os14.tmpdir(), TEMP_PREFIX));
92755
+ writeFileSync9(path46.join(dir, PID_FILE), String(process.pid));
92756
+ const tree = path46.join(dir, "tree");
91775
92757
  const head = git6(repo, ["rev-parse", "--verify", "--quiet", "HEAD"]).stdout.trim();
91776
92758
  if (head && git6(dir, ["clone", "-q", "--shared", "--no-checkout", repo, tree]).ok) {
91777
92759
  const branch = git6(repo, ["symbolic-ref", "--quiet", "--short", "HEAD"]).stdout.trim();
@@ -91788,29 +92770,29 @@ function copyWorkingTree(repo) {
91788
92770
  for (const relative of listed) {
91789
92771
  if (!relative)
91790
92772
  continue;
91791
- const from = path44.join(repo, relative);
92773
+ const from = path46.join(repo, relative);
91792
92774
  let stat;
91793
92775
  try {
91794
92776
  stat = lstatSync7(from);
91795
92777
  } catch {
91796
92778
  continue;
91797
92779
  }
91798
- const to = path44.join(tree, relative);
91799
- mkdirSync9(path44.dirname(to), { recursive: true });
92780
+ const to = path46.join(tree, relative);
92781
+ mkdirSync9(path46.dirname(to), { recursive: true });
91800
92782
  if (stat.isSymbolicLink())
91801
92783
  copySymlink(repo, tree, from, to);
91802
92784
  else if (stat.isFile())
91803
92785
  copyFileSync6(from, to);
91804
- let parent = path44.dirname(relative);
92786
+ let parent = path46.dirname(relative);
91805
92787
  while (parent !== "." && !dirs.has(parent)) {
91806
92788
  dirs.add(parent);
91807
- parent = path44.dirname(parent);
92789
+ parent = path46.dirname(parent);
91808
92790
  }
91809
92791
  }
91810
92792
  for (const parent of dirs) {
91811
- const modules = path44.join(repo, parent, "node_modules");
91812
- if (existsSync19(modules) && !existsSync19(path44.join(tree, parent, "node_modules")))
91813
- linkNodeModules(modules, path44.join(tree, parent, "node_modules"), repo, tree);
92793
+ const modules = path46.join(repo, parent, "node_modules");
92794
+ if (existsSync19(modules) && !existsSync19(path46.join(tree, parent, "node_modules")))
92795
+ linkNodeModules(modules, path46.join(tree, parent, "node_modules"), repo, tree);
91814
92796
  }
91815
92797
  return { dir, tree };
91816
92798
  }
@@ -91849,14 +92831,14 @@ async function runMutation(options) {
91849
92831
  const tracked = git6(repo, ["ls-files", "-co", "--exclude-standard", "-z"]).stdout.split("\x00").filter(Boolean);
91850
92832
  const testFiles = tracked.filter((file) => TEST_FILE.test(file)).map((file) => {
91851
92833
  try {
91852
- return { file, text: readFileSync20(path44.join(repo, file), "utf8") };
92834
+ return { file, text: readFileSync20(path46.join(repo, file), "utf8") };
91853
92835
  } catch {
91854
92836
  return { file, text: "" };
91855
92837
  }
91856
92838
  });
91857
92839
  const candidates = [];
91858
92840
  for (const [absolute, lines] of [...options.changed].toSorted(([a], [b]) => a.localeCompare(b))) {
91859
- const relative = toPosix(path44.relative(repo, absolute));
92841
+ const relative = toPosix(path46.relative(repo, absolute));
91860
92842
  if (!CODE_FILE.test(relative) || TEST_FILE.test(relative) || relative.endsWith(".d.ts"))
91861
92843
  continue;
91862
92844
  if (NOT_SOURCE.test(relative) || !existsSync19(absolute))
@@ -91870,9 +92852,9 @@ async function runMutation(options) {
91870
92852
  for (const mutant of mutants)
91871
92853
  if (!related.has(mutant.file))
91872
92854
  related.set(mutant.file, relatedTests(mutant.file, testFiles));
91873
- const cwdRelative = path44.relative(repo, options.cwd);
92855
+ const cwdRelative = path46.relative(repo, options.cwd);
91874
92856
  const asArg = (file) => {
91875
- const relative = toPosix(path44.relative(cwdRelative, file));
92857
+ const relative = toPosix(path46.relative(cwdRelative, file));
91876
92858
  return shellQuote(relative.startsWith(".") ? relative : `./${relative}`);
91877
92859
  };
91878
92860
  const commandFor = (tests) => options.command.replaceAll("{files}", () => tests.map(asArg).join(" "));
@@ -91907,7 +92889,7 @@ async function runMutation(options) {
91907
92889
  return finish2(options, 0, results, counts, started);
91908
92890
  removeStaleCopies();
91909
92891
  const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
91910
- const copyCwd = path44.join(copy, cwdRelative);
92892
+ const copyCwd = path46.join(copy, cwdRelative);
91911
92893
  let current = null;
91912
92894
  const restore = () => {
91913
92895
  if (current)
@@ -91942,7 +92924,7 @@ async function runMutation(options) {
91942
92924
  record({ ...mutant, status: "skipped", durationMs: 0, tests, reason: "budget" });
91943
92925
  continue;
91944
92926
  }
91945
- const original = readFileSync20(path44.join(copy, mutant.file), "utf8");
92927
+ const original = readFileSync20(path46.join(copy, mutant.file), "utf8");
91946
92928
  const mutated = original.slice(0, mutant.start) + mutant.replacement + original.slice(mutant.end);
91947
92929
  current = { file: mutant.file, original };
91948
92930
  if (!writeInside(copy, mutant.file, mutated)) {
@@ -91999,8 +92981,8 @@ var NOT_SOURCE, FLIPS, isStringy = (node) => node.type === "StringLiteral" || no
91999
92981
  return error.code === "EPERM";
92000
92982
  }
92001
92983
  }, inside2 = (root, file) => {
92002
- const relative = path44.relative(root, file);
92003
- return relative === "" || !relative.startsWith("..") && !path44.isAbsolute(relative);
92984
+ const relative = path46.relative(root, file);
92985
+ return relative === "" || !relative.startsWith("..") && !path46.isAbsolute(relative);
92004
92986
  }, active, killTree = (child) => {
92005
92987
  try {
92006
92988
  if (child.pid && process.platform !== "win32")
@@ -92046,10 +93028,10 @@ var init_mutate = __esm(() => {
92046
93028
  // packages/workit-cli/src/verbs/test-audit.ts
92047
93029
  var exports_test_audit = {};
92048
93030
  __export(exports_test_audit, {
92049
- run: () => run16
93031
+ run: () => run17
92050
93032
  });
92051
93033
  import { existsSync as existsSync20 } from "node:fs";
92052
- import path45 from "node:path";
93034
+ import path47 from "node:path";
92053
93035
  function parse7(argv) {
92054
93036
  const options = {
92055
93037
  paths: [],
@@ -92143,14 +93125,14 @@ function human(data) {
92143
93125
  }
92144
93126
  return lines;
92145
93127
  }
92146
- async function run16(argv, io) {
93128
+ async function run17(argv, io) {
92147
93129
  const options = parse7(argv);
92148
93130
  if (typeof options === "string")
92149
93131
  return emit(io, fail("invalid_input", `${options}
92150
- usage: ${USAGE9}`));
93132
+ usage: ${USAGE10}`));
92151
93133
  const top = git6(io.cwd, ["rev-parse", "--show-toplevel"]);
92152
- const repo = top.ok ? path45.resolve(top.stdout.trim()) : null;
92153
- const display = (file) => toPosix(path45.relative(io.cwd, file) || file);
93134
+ const repo = top.ok ? path47.resolve(top.stdout.trim()) : null;
93135
+ const display = (file) => toPosix(path47.relative(io.cwd, file) || file);
92154
93136
  let base = null;
92155
93137
  let changed = null;
92156
93138
  if (options.diff || options.mutate) {
@@ -92233,7 +93215,7 @@ usage: ${USAGE9}`));
92233
93215
  }
92234
93216
  return emit(io, ok(data), human);
92235
93217
  }
92236
- var USAGE9 = "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_FLAGS2, quote = (arg) => /^[\w./@:=+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
93218
+ var USAGE10 = "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_FLAGS2, quote = (arg) => /^[\w./@:=+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
92237
93219
  var init_test_audit = __esm(() => {
92238
93220
  init_audit();
92239
93221
  init_mutate();
@@ -92358,14 +93340,14 @@ var init_youtrack_writes = __esm(() => {
92358
93340
  // packages/workit-cli/src/verbs/youtrack.ts
92359
93341
  var exports_youtrack = {};
92360
93342
  __export(exports_youtrack, {
92361
- run: () => run17
93343
+ run: () => run18
92362
93344
  });
92363
93345
  import { readFileSync as readFileSync21 } from "node:fs";
92364
- import path46 from "node:path";
92365
- async function run17(argv, io) {
93346
+ import path48 from "node:path";
93347
+ async function run18(argv, io) {
92366
93348
  const [sub, ...rest] = argv;
92367
93349
  if (sub !== "note" && sub !== "time" && sub !== "meeting")
92368
- return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE10);
93350
+ return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE11);
92369
93351
  const flags = parseFlags(rest, {
92370
93352
  markdown: "value",
92371
93353
  file: "value",
@@ -92374,18 +93356,18 @@ async function run17(argv, io) {
92374
93356
  date: "value"
92375
93357
  });
92376
93358
  if (typeof flags === "string")
92377
- return usage3(io, flags, USAGE10);
93359
+ return usage3(io, flags, USAGE11);
92378
93360
  const [issueId, extra] = flags.positionals;
92379
93361
  if (!issueId || !ISSUE_RE2.test(issueId))
92380
- return usage3(io, "an issue id like ABC-123 is required", USAGE10);
93362
+ return usage3(io, "an issue id like ABC-123 is required", USAGE11);
92381
93363
  if (extra !== undefined)
92382
- return usage3(io, `unexpected argument ${extra}`, USAGE10);
93364
+ return usage3(io, `unexpected argument ${extra}`, USAGE11);
92383
93365
  const minutes = positiveInt(flags.values.minutes, "--minutes");
92384
93366
  if (typeof minutes === "string")
92385
- return usage3(io, minutes, USAGE10);
93367
+ return usage3(io, minutes, USAGE11);
92386
93368
  const date = youTrackWorkDateMs(flags.values.date ?? "auto");
92387
93369
  if ("error" in date)
92388
- return usage3(io, date.error, USAGE10);
93370
+ return usage3(io, date.error, USAGE11);
92389
93371
  const dateMs = date.data.dateMs;
92390
93372
  const markedDay = flags.values.date !== undefined && flags.values.date !== "auto" ? { dateMs } : {};
92391
93373
  const root = io.cwd;
@@ -92403,24 +93385,24 @@ async function run17(argv, io) {
92403
93385
  };
92404
93386
  if (sub === "note") {
92405
93387
  if (Boolean(flags.values.markdown) === Boolean(flags.values.file))
92406
- return usage3(io, "pass exactly one of --markdown or --file", USAGE10);
93388
+ return usage3(io, "pass exactly one of --markdown or --file", USAGE11);
92407
93389
  let markdown = flags.values.markdown ?? "";
92408
93390
  if (flags.values.file)
92409
93391
  try {
92410
- markdown = readFileSync21(path46.resolve(io.cwd, flags.values.file), "utf8");
93392
+ markdown = readFileSync21(path48.resolve(io.cwd, flags.values.file), "utf8");
92411
93393
  } catch (error) {
92412
- return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE10);
93394
+ return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE11);
92413
93395
  }
92414
93396
  if (!markdown.trim())
92415
- return usage3(io, "the note is empty", USAGE10);
93397
+ return usage3(io, "the note is empty", USAGE11);
92416
93398
  const missing = step("comment", "comments", { operation: "note", issueId, markdown }, markdown, async (text) => commentResult(await postComment(issueId, text, root))) ?? (minutes ? step("time", "workItems", { operation: "note.time", issueId, markdown, minutes, ...markedDay }, "workit update", (text) => logTimeUpdate({ issueId, minutes, text, dateMs, workspace_root: root })) : null);
92417
93399
  if (missing)
92418
93400
  return emit(io, fail("unavailable", missing));
92419
93401
  } else {
92420
93402
  if (!minutes)
92421
- return usage3(io, `${sub} needs --minutes <n>`, USAGE10);
93403
+ return usage3(io, `${sub} needs --minutes <n>`, USAGE11);
92422
93404
  if (sub === "meeting" && !flags.values.text)
92423
- return usage3(io, "meeting needs --text <what the meeting was>", USAGE10);
93405
+ return usage3(io, "meeting needs --text <what the meeting was>", USAGE11);
92424
93406
  const text = flags.values.text ?? "";
92425
93407
  const missing = step(sub, "workItems", { operation: sub, issueId, minutes, text, ...markedDay }, text, (marked) => logTimeUpdate({ issueId, minutes, text: marked, dateMs, workspace_root: root }));
92426
93408
  if (missing)
@@ -92449,7 +93431,7 @@ async function run17(argv, io) {
92449
93431
  ...data.steps.flatMap((entry) => entry.note ? [` note (${entry.step}): ${entry.note}`] : [])
92450
93432
  ]);
92451
93433
  }
92452
- var USAGE10 = "workit youtrack note <ISSUE> (--markdown <t> | --file <p>) [--minutes <n>] [--date auto|YYYY-MM-DD] | youtrack time <ISSUE> --minutes <n> [--text <t>] [--date …] | youtrack meeting <ISSUE> --minutes <n> --text <t> [--date …]", commentResult = (value) => {
93434
+ var USAGE11 = "workit youtrack note <ISSUE> (--markdown <t> | --file <p>) [--minutes <n>] [--date auto|YYYY-MM-DD] | youtrack time <ISSUE> --minutes <n> [--text <t>] [--date …] | youtrack meeting <ISSUE> --minutes <n> --text <t> [--date …]", commentResult = (value) => {
92453
93435
  try {
92454
93436
  return { ok: true, data: unwrap2(value) };
92455
93437
  } catch (error) {
@@ -92473,14 +93455,14 @@ var init_youtrack2 = __esm(() => {
92473
93455
  // packages/workit-cli/src/verbs/changelog.ts
92474
93456
  var exports_changelog = {};
92475
93457
  __export(exports_changelog, {
92476
- run: () => run18
93458
+ run: () => run19
92477
93459
  });
92478
93460
  import { readFileSync as readFileSync22 } from "node:fs";
92479
- import path47 from "node:path";
92480
- async function run18(argv, io) {
93461
+ import path49 from "node:path";
93462
+ async function run19(argv, io) {
92481
93463
  const [sub, ...rest] = argv;
92482
93464
  if (sub !== "apply")
92483
- return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE11);
93465
+ return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE12);
92484
93466
  const flags = parseFlags(rest, {
92485
93467
  entries: "value",
92486
93468
  path: "value",
@@ -92488,18 +93470,18 @@ async function run18(argv, io) {
92488
93470
  preview: "boolean"
92489
93471
  });
92490
93472
  if (typeof flags === "string")
92491
- return usage3(io, flags, USAGE11);
93473
+ return usage3(io, flags, USAGE12);
92492
93474
  if (flags.positionals.length)
92493
- return usage3(io, `unexpected argument ${flags.positionals[0]}`, USAGE11);
93475
+ return usage3(io, `unexpected argument ${flags.positionals[0]}`, USAGE12);
92494
93476
  const normalizeOnly = flags.booleans.has("normalize-only");
92495
93477
  if (!normalizeOnly && !flags.values.entries)
92496
- return usage3(io, "pass --entries <JSON> or --normalize-only", USAGE11);
93478
+ return usage3(io, "pass --entries <JSON> or --normalize-only", USAGE12);
92497
93479
  let entries;
92498
93480
  if (flags.values.entries)
92499
93481
  try {
92500
93482
  entries = readEntries(flags.values.entries, io.cwd);
92501
93483
  } catch (error) {
92502
- return usage3(io, `--entries is not valid JSON: ${String(error)}`, USAGE11);
93484
+ return usage3(io, `--entries is not valid JSON: ${String(error)}`, USAGE12);
92503
93485
  }
92504
93486
  const input = {
92505
93487
  entries,
@@ -92521,8 +93503,8 @@ async function run18(argv, io) {
92521
93503
  `changelog updated: ${String(applied.path ?? "CHANGELOG.md")}`
92522
93504
  ]);
92523
93505
  }
92524
- var USAGE11 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
92525
- const text = raw === "-" ? readFileSync22(0, "utf8") : raw.startsWith("@") ? readFileSync22(path47.resolve(cwd, raw.slice(1)), "utf8") : raw;
93506
+ var USAGE12 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
93507
+ const text = raw === "-" ? readFileSync22(0, "utf8") : raw.startsWith("@") ? readFileSync22(path49.resolve(cwd, raw.slice(1)), "utf8") : raw;
92526
93508
  return JSON.parse(text);
92527
93509
  };
92528
93510
  var init_changelog2 = __esm(() => {
@@ -92534,7 +93516,7 @@ var init_changelog2 = __esm(() => {
92534
93516
  // packages/workit-cli/src/verbs/handoff.ts
92535
93517
  var exports_handoff = {};
92536
93518
  __export(exports_handoff, {
92537
- run: () => run19
93519
+ run: () => run20
92538
93520
  });
92539
93521
  import { parseArgs as parseArgs2 } from "node:util";
92540
93522
  function human2(brief) {
@@ -92582,11 +93564,11 @@ async function resumeBrief(argv, io) {
92582
93564
  strict: true
92583
93565
  }));
92584
93566
  } catch (error) {
92585
- return emit(io, fail("invalid_input", error.message, { unblock: USAGE12 }));
93567
+ return emit(io, fail("invalid_input", error.message, { unblock: USAGE13 }));
92586
93568
  }
92587
93569
  const last = values.last === undefined ? 10 : Number(values.last);
92588
93570
  if (!Number.isInteger(last) || last < 1)
92589
- return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock: USAGE12 }));
93571
+ return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock: USAGE13 }));
92590
93572
  const built = buildHandoff(io.cwd, {
92591
93573
  last,
92592
93574
  note: values.note ?? null,
@@ -92604,13 +93586,13 @@ async function resumeBrief(argv, io) {
92604
93586
  }
92605
93587
  return emit(io, ok({ ...data, recorded }), human2);
92606
93588
  }
92607
- async function run19(argv, io) {
93589
+ async function run20(argv, io) {
92608
93590
  if (!argv.includes("--task"))
92609
93591
  return resumeBrief(argv, io);
92610
93592
  await Promise.resolve().then(() => init_task());
92611
93593
  return runTaskCommand(["handoff", ...argv]);
92612
93594
  }
92613
- var USAGE12 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short5 = (sha) => sha ? sha.slice(0, 12) : "no commit";
93595
+ var USAGE13 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short5 = (sha) => sha ? sha.slice(0, 12) : "no commit";
92614
93596
  var init_handoff = __esm(() => {
92615
93597
  init_ledger();
92616
93598
  init_output();
@@ -92619,7 +93601,7 @@ var init_handoff = __esm(() => {
92619
93601
  // packages/workit-cli/src/main.ts
92620
93602
  init_package();
92621
93603
  init_output();
92622
- import path48 from "node:path";
93604
+ import path50 from "node:path";
92623
93605
 
92624
93606
  // packages/workit-cli/src/verbs/registry.ts
92625
93607
  var TASK_FAMILY_NAMES = [
@@ -92731,6 +93713,13 @@ var VERBS = [
92731
93713
  summary: "Plan, restack (lease push + retarget) and land a base-branch stack of PRs, root first",
92732
93714
  load: () => Promise.resolve().then(() => (init_stack2(), exports_stack))
92733
93715
  },
93716
+ {
93717
+ name: "fanout",
93718
+ group: "delivery",
93719
+ usage: "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] | fanout check [<slice>…] [--name <n>] [--base <ref>] [--json]",
93720
+ summary: "Register parallel slices (complete briefs, disjoint scopes, shared-file owners), then gate fan-in: out-of-scope edits, trunk and sibling merge conflicts, landing order",
93721
+ load: () => Promise.resolve().then(() => (init_fanout2(), exports_fanout))
93722
+ },
92734
93723
  {
92735
93724
  name: "ledger",
92736
93725
  group: "delivery",
@@ -92849,7 +93838,7 @@ var defaultIo = (json) => ({
92849
93838
  });
92850
93839
  async function main(argv, overrides = {}, options = {}) {
92851
93840
  const parsed = parseGlobals(argv);
92852
- const cwd = path48.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
93841
+ const cwd = path50.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
92853
93842
  const io = { ...defaultIo(parsed.json), ...overrides, json: parsed.json, cwd };
92854
93843
  if (parsed.error)
92855
93844
  return emit(io, fail("invalid_input", parsed.error));