@brainervirus/workit-claude-code 7.4.0 → 7.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.4.0",
68
+ version: "7.5.1",
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.4.0",
107
- "@brainervirus/workit-cursor": "^7.4.0",
108
- "@brainervirus/workit-opencode": "^7.4.0",
106
+ "@brainervirus/workit-core": "^7.5.1",
107
+ "@brainervirus/workit-cursor": "^7.5.1",
108
+ "@brainervirus/workit-opencode": "^7.5.1",
109
109
  "@inkjs/ui": "2.0.0",
110
110
  ink: "7.1.1",
111
111
  react: "19.2.8"
@@ -63107,7 +63107,7 @@ var init_host_install = __esm(() => {
63107
63107
  });
63108
63108
 
63109
63109
  // packages/workit-core/src/core/skill-manifests.ts
63110
- import { existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync5, statSync as statSync3 } from "node:fs";
63110
+ import { cpSync as cpSync3, existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync5, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
63111
63111
  import path28 from "node:path";
63112
63112
  var WORKIT_METHOD_SKILLS, skillManifestNames = (root) => existsSync12(root) ? readdirSync5(root).filter((name) => existsSync12(path28.join(root, name, "SKILL.md"))).toSorted() : [], validateSkillManifests = (root, expected, label) => {
63113
63113
  const actual = skillManifestNames(root);
@@ -63162,7 +63162,7 @@ import {
63162
63162
  rmSync as rmSync2,
63163
63163
  statSync as statSync4,
63164
63164
  unlinkSync as unlinkSync2,
63165
- writeFileSync as writeFileSync5
63165
+ writeFileSync as writeFileSync6
63166
63166
  } from "node:fs";
63167
63167
  import os7 from "node:os";
63168
63168
  import path29 from "node:path";
@@ -64233,7 +64233,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
64233
64233
  const probe = path29.join(logsDir, "doctor-probe.tmp");
64234
64234
  try {
64235
64235
  mkdirSync4(logsDir, { recursive: true, mode: 448 });
64236
- writeFileSync5(probe, `{"probe":true}
64236
+ writeFileSync6(probe, `{"probe":true}
64237
64237
  `, { mode: 384 });
64238
64238
  return {
64239
64239
  id: "log_writable",
@@ -64398,7 +64398,7 @@ var init_doctor = __esm(() => {
64398
64398
  // packages/workit-cli/src/admin/setup.ts
64399
64399
  import {
64400
64400
  copyFileSync as copyFileSync4,
64401
- cpSync as cpSync3,
64401
+ cpSync as cpSync4,
64402
64402
  existsSync as existsSync14,
64403
64403
  lstatSync as lstatSync5,
64404
64404
  mkdirSync as mkdirSync5,
@@ -64409,7 +64409,7 @@ import {
64409
64409
  renameSync as renameSync3,
64410
64410
  rmSync as rmSync3,
64411
64411
  statSync as statSync5,
64412
- writeFileSync as writeFileSync6
64412
+ writeFileSync as writeFileSync7
64413
64413
  } from "node:fs";
64414
64414
  import os8 from "node:os";
64415
64415
  import path30 from "node:path";
@@ -64635,7 +64635,7 @@ function applyMutation(m) {
64635
64635
  return { platform: "core", file: m.path, status: "Skipped", detail: "already configured" };
64636
64636
  }
64637
64637
  mkdirSync5(dir, { recursive: true });
64638
- writeFileSync6(m.path, JSON.stringify(merged, null, 2) + `
64638
+ writeFileSync7(m.path, JSON.stringify(merged, null, 2) + `
64639
64639
  `, "utf8");
64640
64640
  return existing.kind === "record" ? { platform: "core", file: m.path, status: "Configured" } : { platform: "core", file: m.path, status: "Installed" };
64641
64641
  }
@@ -64679,7 +64679,7 @@ function applyMutation(m) {
64679
64679
  mkdirSync5(dir, { recursive: true });
64680
64680
  const tmp = `${m.path}.${process.pid}.tmp`;
64681
64681
  try {
64682
- writeFileSync6(tmp, next, "utf8");
64682
+ writeFileSync7(tmp, next, "utf8");
64683
64683
  if (workspacesRevision(readFileSafe(m.path)) !== m.expectedRevision) {
64684
64684
  rmSync3(tmp, { force: true });
64685
64685
  return {
@@ -64718,7 +64718,7 @@ function applyMutation(m) {
64718
64718
  const separator = existing && !existing.endsWith(`
64719
64719
  `) ? `
64720
64720
  ` : "";
64721
- writeFileSync6(m.path, existing + separator + (existing ? `
64721
+ writeFileSync7(m.path, existing + separator + (existing ? `
64722
64722
  ` : "") + add.join(`
64723
64723
  `) + `
64724
64724
  `, "utf8");
@@ -64758,7 +64758,7 @@ function applyMutation(m) {
64758
64758
  };
64759
64759
  }
64760
64760
  mkdirSync5(path30.dirname(m.path), { recursive: true });
64761
- writeFileSync6(m.path, JSON.stringify(merged, null, 2) + `
64761
+ writeFileSync7(m.path, JSON.stringify(merged, null, 2) + `
64762
64762
  `, "utf8");
64763
64763
  return existing.kind === "record" ? { platform: "core", file: m.path, status: "Configured" } : { platform: "core", file: m.path, status: "Installed" };
64764
64764
  }
@@ -64830,7 +64830,7 @@ function applyOpenCode(root, res) {
64830
64830
  detail: "already registered"
64831
64831
  };
64832
64832
  }
64833
- writeFileSync6(res.opencodeConfig, JSON.stringify(merged.config, null, 2) + `
64833
+ writeFileSync7(res.opencodeConfig, JSON.stringify(merged.config, null, 2) + `
64834
64834
  `, "utf8");
64835
64835
  return existing.kind === "record" ? {
64836
64836
  platform: "opencode",
@@ -64859,15 +64859,15 @@ function copyPluginDir(src, dest) {
64859
64859
  const stage = path30.join(swap, "stage");
64860
64860
  const backup = path30.join(swap, "backup");
64861
64861
  try {
64862
- cpSync3(realSrc, stage, {
64862
+ cpSync4(realSrc, stage, {
64863
64863
  recursive: true,
64864
64864
  filter: (entry) => !path30.relative(realSrc, entry).split(path30.sep).includes("node_modules")
64865
64865
  });
64866
64866
  for (const [name, content] of rules) {
64867
64867
  mkdirSync5(path30.join(stage, "rules"), { recursive: true });
64868
- writeFileSync6(path30.join(stage, "rules", name), content);
64868
+ writeFileSync7(path30.join(stage, "rules", name), content);
64869
64869
  }
64870
- writeFileSync6(path30.join(stage, ".workit-root"), realSrc + `
64870
+ writeFileSync7(path30.join(stage, ".workit-root"), realSrc + `
64871
64871
  `, "utf8");
64872
64872
  if (!samePluginContent(realSrc, stage))
64873
64873
  throw new Error("staged adapter content is incomplete");
@@ -64909,7 +64909,7 @@ function applyCursorSettings(root, res) {
64909
64909
  detail: "already registered"
64910
64910
  };
64911
64911
  }
64912
- writeFileSync6(res.cursorSettings, JSON.stringify(settings.config, null, 2) + `
64912
+ writeFileSync7(res.cursorSettings, JSON.stringify(settings.config, null, 2) + `
64913
64913
  `, "utf8");
64914
64914
  return {
64915
64915
  platform: "cursor",
@@ -64948,7 +64948,7 @@ function applyCursorMcp(root, res) {
64948
64948
  detail: "already registered"
64949
64949
  };
64950
64950
  }
64951
- writeFileSync6(res.cursorMcp, JSON.stringify(mcp.config, null, 2) + `
64951
+ writeFileSync7(res.cursorMcp, JSON.stringify(mcp.config, null, 2) + `
64952
64952
  `, "utf8");
64953
64953
  return {
64954
64954
  platform: "cursor",
@@ -65432,7 +65432,7 @@ var init_branch_policy = __esm(() => {
65432
65432
  });
65433
65433
 
65434
65434
  // packages/workit-core/src/core/workspace-branch-policy.ts
65435
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync15, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "node:fs";
65435
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync15, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "node:fs";
65436
65436
  import path31 from "node:path";
65437
65437
  import { isDeepStrictEqual as isDeepStrictEqual3 } from "node:util";
65438
65438
  function applyWorkspaceBranchPolicy(opts) {
@@ -65501,7 +65501,7 @@ function applyWorkspaceBranchPolicy(opts) {
65501
65501
  mkdirSync6(path31.dirname(wsPath), { recursive: true });
65502
65502
  const tmp = `${wsPath}.${process.pid}.tmp`;
65503
65503
  try {
65504
- writeFileSync7(tmp, JSON.stringify({ ...document2, workspaces: next }, null, 2) + `
65504
+ writeFileSync8(tmp, JSON.stringify({ ...document2, workspaces: next }, null, 2) + `
65505
65505
  `, "utf8");
65506
65506
  if (workspacesRevision(readFileSafe2(wsPath)) !== revision) {
65507
65507
  rmSync4(tmp, { force: true });
@@ -66370,7 +66370,7 @@ var init_wizard_state = __esm(() => {
66370
66370
  });
66371
66371
 
66372
66372
  // packages/workit-cli/src/admin/uninstall.ts
66373
- import { existsSync as existsSync16, readFileSync as readFileSync16, realpathSync as realpathSync8, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "node:fs";
66373
+ import { existsSync as existsSync16, readFileSync as readFileSync16, realpathSync as realpathSync8, rmSync as rmSync5, writeFileSync as writeFileSync9 } from "node:fs";
66374
66374
  import os9 from "node:os";
66375
66375
  import path33 from "node:path";
66376
66376
  function cleanCursorSettings(settings, pluginDir) {
@@ -66654,7 +66654,7 @@ var resolveUninstallPaths = (options = {}) => {
66654
66654
  return { status: "skipped", detail: "already clean" };
66655
66655
  }
66656
66656
  try {
66657
- writeFileSync8(target, serialized, "utf8");
66657
+ writeFileSync9(target, serialized, "utf8");
66658
66658
  } catch (error) {
66659
66659
  return {
66660
66660
  status: "failed",
@@ -69389,7 +69389,7 @@ import {
69389
69389
  mkdirSync as mkdirSync8,
69390
69390
  copyFileSync as copyFileSync5,
69391
69391
  chmodSync,
69392
- cpSync as cpSync4,
69392
+ cpSync as cpSync5,
69393
69393
  lstatSync as lstatSync6
69394
69394
  } from "node:fs";
69395
69395
  import path35 from "node:path";
@@ -69541,7 +69541,7 @@ function applyUpgrade(plan, deps = {}) {
69541
69541
  const run = runner(deps);
69542
69542
  const cursorRoot = path35.join(home, ".cursor/plugins/local/workit");
69543
69543
  if (plan.entries.some((entry) => entry.host === "cursor") && existsSync17(cursorRoot))
69544
- cpSync4(cursorRoot, path35.join(backup, "cursor-package"), { recursive: true });
69544
+ cpSync5(cursorRoot, path35.join(backup, "cursor-package"), { recursive: true });
69545
69545
  for (const entry of plan.entries) {
69546
69546
  if (entry.host === "opencode")
69547
69547
  return {
@@ -69619,7 +69619,7 @@ function applyUpgrade(plan, deps = {}) {
69619
69619
  if (currentRoot.status !== 0 || currentRoot.stdout.trim() !== plan.cli.root)
69620
69620
  return { ok: false, backup, error: "Global npm location changed; preview again" };
69621
69621
  const cliBackup = path35.join(backup, "cli-package");
69622
- cpSync4(path35.join(plan.cli.root, "@brainervirus/workit-cli"), cliBackup, { recursive: true });
69622
+ cpSync5(path35.join(plan.cli.root, "@brainervirus/workit-cli"), cliBackup, { recursive: true });
69623
69623
  if (run("npm", ["install", "--global", `@brainervirus/workit-cli@${plan.cli.latest}`]).status !== 0)
69624
69624
  return {
69625
69625
  ok: false,
@@ -73255,6 +73255,33 @@ var init_resolve = __esm(() => {
73255
73255
  init_gitlab();
73256
73256
  });
73257
73257
 
73258
+ // packages/workit-core/src/duration.ts
73259
+ function parseDuration2(value) {
73260
+ const text = value.trim().toLowerCase();
73261
+ if (/^\d+(?:\.\d+)?$/u.test(text))
73262
+ return Math.round(Number(text) * 1000);
73263
+ const parts = [...text.matchAll(/(\d+(?:\.\d+)?)\s*([a-z]+)\s*/gu)];
73264
+ if (parts.length === 0 || parts.map((part) => part[0]).join("") !== text)
73265
+ return null;
73266
+ let total = 0;
73267
+ for (const [, amount, unit] of parts) {
73268
+ const factor = UNITS.find(([pattern]) => pattern.test(unit))?.[1];
73269
+ if (factor === undefined)
73270
+ return null;
73271
+ total += Number(amount) * factor;
73272
+ }
73273
+ return Math.round(total);
73274
+ }
73275
+ var UNITS;
73276
+ var init_duration = __esm(() => {
73277
+ UNITS = [
73278
+ [/^(?:ms|milliseconds?)$/u, 1],
73279
+ [/^(?:s|secs?|seconds?)$/u, 1000],
73280
+ [/^(?:m|mins?|minutes?)$/u, 60000],
73281
+ [/^(?:h|hrs?|hours?)$/u, 3600000]
73282
+ ];
73283
+ });
73284
+
73258
73285
  // packages/workit-cli/src/verbs/forge-common.ts
73259
73286
  function parseFlags(argv, spec) {
73260
73287
  const parsed = { values: {}, booleans: new Set, lists: {}, positionals: [] };
@@ -73292,14 +73319,6 @@ function positiveInt(value, flag) {
73292
73319
  return null;
73293
73320
  return /^[1-9]\d{0,8}$/u.test(value) ? Number(value) : `${flag} must be a positive integer`;
73294
73321
  }
73295
- function parseDuration2(value) {
73296
- const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/u.exec(value.trim());
73297
- if (!match)
73298
- return null;
73299
- const unit = match[2] ?? "s";
73300
- const factor = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60000 : 3600000;
73301
- return Math.round(Number(match[1]) * factor);
73302
- }
73303
73322
  function releaseTrunk(io, track, branch) {
73304
73323
  const resolved = vcsConfig("resolve", io.cwd, {
73305
73324
  track,
@@ -73382,6 +73401,7 @@ var forgeDeps, usage3 = (io, message, line) => emit(io, fail("invalid_input", me
73382
73401
  var init_forge_common = __esm(() => {
73383
73402
  init_resolve();
73384
73403
  init_vcs_config();
73404
+ init_duration();
73385
73405
  init_output();
73386
73406
  forgeDeps = {
73387
73407
  runner: undefined,
@@ -75694,9 +75714,34 @@ var init_stack2 = __esm(() => {
75694
75714
  import { spawnSync as spawnSync21 } from "node:child_process";
75695
75715
  import fs23 from "node:fs";
75696
75716
  import path43 from "node:path";
75717
+ function expandBraces(pattern, limit = 64) {
75718
+ for (let index = 0;index < pattern.length; index += 1) {
75719
+ if (pattern[index] === "\\") {
75720
+ index += 1;
75721
+ continue;
75722
+ }
75723
+ const close = pattern.indexOf("}", index);
75724
+ if (pattern[index] !== "{" || close < index)
75725
+ continue;
75726
+ const head = pattern.slice(0, index);
75727
+ const tail = pattern.slice(close + 1);
75728
+ const out = [];
75729
+ for (const alternative of pattern.slice(index + 1, close).split(","))
75730
+ for (const rest of expandBraces(tail, limit)) {
75731
+ if (out.length >= limit)
75732
+ return out;
75733
+ out.push(`${head}${alternative}${rest}`);
75734
+ }
75735
+ return out;
75736
+ }
75737
+ return [pattern];
75738
+ }
75697
75739
  function scopeSamples(pattern) {
75698
75740
  if (!hasWildcard(pattern))
75699
75741
  return [unescape2(pattern)];
75742
+ return [...new Set(expandBraces(pattern).map(sampleOne))];
75743
+ }
75744
+ function sampleOne(pattern) {
75700
75745
  let out = "";
75701
75746
  for (let index = 0;index < pattern.length; index += 1) {
75702
75747
  const char = pattern[index];
@@ -75715,14 +75760,10 @@ function scopeSamples(pattern) {
75715
75760
  out += "<any>";
75716
75761
  else if (char === "?")
75717
75762
  out += "x";
75718
- else if (char === "{" && pattern.indexOf("}", index) > index) {
75719
- const close = pattern.indexOf("}", index);
75720
- out += pattern.slice(index + 1, close).split(",")[0];
75721
- index = close;
75722
- } else
75763
+ else
75723
75764
  out += char;
75724
75765
  }
75725
- return [out];
75766
+ return out;
75726
75767
  }
75727
75768
  function globSource(pattern) {
75728
75769
  let out = "";
@@ -75763,7 +75804,20 @@ function matchesScope(pattern, file) {
75763
75804
  return regex.test(file) || !hasWildcard(pattern) && file.startsWith(`${unescape2(pattern)}/`);
75764
75805
  }
75765
75806
  function normalizePattern(raw) {
75766
- let pattern = raw.trim().replace(/\\(?![[\]{}*?])/gu, "/");
75807
+ let pattern = raw.trim();
75808
+ if (pattern.includes("\\")) {
75809
+ if (/\\[*?{}]/u.test(pattern))
75810
+ return {
75811
+ error: "has \\*, \\?, \\{ or \\}: only \\[ and \\] are escapes; use / as the separator (src/**/*.ts)"
75812
+ };
75813
+ const brackets = /\\[[\]]/u.test(pattern);
75814
+ if (brackets && pattern.replace(/\\[[\]]/gu, "").includes("\\"))
75815
+ return {
75816
+ error: "mixes \\[ or \\] escapes with other backslashes, which is ambiguous: use / as the separator (app/\\[id\\]/page.tsx)"
75817
+ };
75818
+ if (!brackets)
75819
+ pattern = pattern.replaceAll("\\", "/");
75820
+ }
75767
75821
  while (pattern.startsWith("./"))
75768
75822
  pattern = pattern.slice(2);
75769
75823
  if (pattern === "." || pattern === "")
@@ -75890,7 +75944,7 @@ function parsePlan(raw, defaults, name) {
75890
75944
  id,
75891
75945
  branch,
75892
75946
  base: optional("base") ?? "",
75893
- worktree: optional("worktree") ?? path43.join(defaults.worktreeRoot, id),
75947
+ worktree: path43.resolve(defaults.root, optional("worktree") ?? path43.join(defaults.worktreeRoot, id)),
75894
75948
  tier,
75895
75949
  dependsOn: [...new Set(dependsOn ?? [])],
75896
75950
  scope,
@@ -76066,6 +76120,30 @@ function analyzeOverlap(plan, candidates) {
76066
76120
  });
76067
76121
  return { overlaps, conflicts };
76068
76122
  }
76123
+ function caseIndex(files) {
76124
+ const index = new Map;
76125
+ for (const file of files) {
76126
+ const parts = file.split("/");
76127
+ for (let end = 1;end <= parts.length; end += 1) {
76128
+ const prefix = parts.slice(0, end).join("/");
76129
+ const key = prefix.toLowerCase();
76130
+ (index.get(key) ?? index.set(key, new Set).get(key))?.add(prefix);
76131
+ }
76132
+ }
76133
+ return index;
76134
+ }
76135
+ function caseClash(index, file) {
76136
+ const parts = file.split("/");
76137
+ for (let end = 1;end <= parts.length; end += 1) {
76138
+ const prefix = parts.slice(0, end).join("/");
76139
+ if (prefix.includes("<any>"))
76140
+ return null;
76141
+ const spellings = index.get(prefix.toLowerCase());
76142
+ if (spellings && !spellings.has(prefix))
76143
+ return { path: prefix, trunk: [...spellings].toSorted()[0] };
76144
+ }
76145
+ return null;
76146
+ }
76069
76147
  function branchRef(cwd, branch) {
76070
76148
  if (!branch || branch.startsWith("-"))
76071
76149
  return null;
@@ -76088,9 +76166,12 @@ function defaultTrunk(cwd) {
76088
76166
  const base = defaultBase(cwd);
76089
76167
  return base ? base.replace(/^origin\//u, "") : "main";
76090
76168
  }
76091
- function worktreeRoot(cwd) {
76169
+ function mainCheckout(cwd) {
76092
76170
  const common = gitCommonDir(cwd);
76093
- const main = common && path43.basename(common) === ".git" ? path43.dirname(common) : cwd;
76171
+ return common && path43.basename(common) === ".git" ? path43.dirname(common) : path43.resolve(cwd);
76172
+ }
76173
+ function worktreeRoot(cwd) {
76174
+ const main = mainCheckout(cwd);
76094
76175
  return path43.join(path43.dirname(main), `${path43.basename(main)}-wt`);
76095
76176
  }
76096
76177
  function fanoutsDir(cwd) {
@@ -76226,7 +76307,11 @@ function planFanout(cwd, input, now = new Date) {
76226
76307
  const dir = fanoutsDir(cwd);
76227
76308
  if (!dir.ok)
76228
76309
  return dir;
76229
- 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);
76310
+ const parsed = parsePlan(input.trunk && isRecord8(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, {
76311
+ trunk: input.trunk ?? defaultTrunk(cwd),
76312
+ root: mainCheckout(cwd),
76313
+ worktreeRoot: worktreeRoot(cwd)
76314
+ }, input.name);
76230
76315
  if (!parsed.ok)
76231
76316
  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 });
76232
76317
  const plan = parsed.plan;
@@ -76241,6 +76326,20 @@ function planFanout(cwd, input, now = new Date) {
76241
76326
  const { overlaps, conflicts } = analyzeOverlap(plan, [...tracked, ...samples]);
76242
76327
  if (conflicts.length)
76243
76328
  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 });
76329
+ const trunkCase = caseIndex(tracked);
76330
+ const clashes = [];
76331
+ for (const slice of plan.slices) {
76332
+ const seen = new Set;
76333
+ for (const sample of [...slice.scope, ...slice.owns].flatMap(scopeSamples)) {
76334
+ const clash = caseClash(trunkCase, sample);
76335
+ if (clash && !seen.has(clash.path)) {
76336
+ seen.add(clash.path);
76337
+ clashes.push({ slice: slice.id, ...clash });
76338
+ }
76339
+ }
76340
+ }
76341
+ if (clashes.length)
76342
+ return fanoutFail("blocked", `slice ${clashes[0].slice} names ${clashes[0].path}, which differs only in case from ${plan.trunk}'s ${clashes[0].trunk} (one path on macOS and Windows)`, `use ${plan.trunk}'s spelling ${clashes[0].trunk} in slice ${clashes[0].slice}'s scope, then workit fanout plan again`, { name: plan.name, caseCollisions: clashes, overlaps });
76244
76343
  const existing = readFanout(cwd, plan.name);
76245
76344
  const previous = existing.ok ? existing.data : null;
76246
76345
  plan.createdAt = previous?.createdAt || now.toISOString();
@@ -76315,11 +76414,11 @@ var FANOUT_VERSION = 1, TIERS, fanoutFail = (code, error, unblock, data) => ({
76315
76414
  edges,
76316
76415
  text: edges.map((edge) => `add "${edge.dependsOn}" to "dependsOn" of slice ${edge.slice}`).join("; ")
76317
76416
  };
76318
- }, gitRun4 = (cwd, args) => {
76417
+ }, gitRun4 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
76319
76418
  const run = spawnSync21("git", args, {
76320
76419
  cwd,
76321
76420
  encoding: "utf8",
76322
- timeout: GIT_TIMEOUTS.local,
76421
+ timeout: timeoutMs,
76323
76422
  killSignal: "SIGKILL",
76324
76423
  windowsHide: true,
76325
76424
  maxBuffer: 64 * 1024 * 1024,
@@ -76352,6 +76451,200 @@ var init_fanout = __esm(() => {
76352
76451
  ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
76353
76452
  });
76354
76453
 
76454
+ // packages/workit-core/src/fanout-landed.ts
76455
+ import { spawnSync as spawnSync22 } from "node:child_process";
76456
+ function forgeBreaker(resolved) {
76457
+ if (!resolved)
76458
+ return { forge: null, down: () => null };
76459
+ let tripped = null;
76460
+ const guard = (call) => (...args) => {
76461
+ if (tripped)
76462
+ return { ok: false, code: "unavailable", error: tripped };
76463
+ const result = call(...args);
76464
+ if (!result.ok && result.code === "unavailable")
76465
+ tripped = result.error;
76466
+ return result;
76467
+ };
76468
+ const inner = resolved.forge;
76469
+ const forge = Object.create(inner);
76470
+ forge.findPr = guard(inner.findPr.bind(inner));
76471
+ forge.prStatus = guard(inner.prStatus.bind(inner));
76472
+ return { forge: { ...resolved, forge }, down: () => tripped };
76473
+ }
76474
+ function branchCreatedAt(cwd, branch) {
76475
+ if (!branch || branch.startsWith("-"))
76476
+ return null;
76477
+ const log = gitRun4(cwd, ["log", "-g", "--format=%H", `refs/heads/${branch}`, "--"]);
76478
+ if (!log.ok)
76479
+ return null;
76480
+ return log.stdout.trim().split(`
76481
+ `).findLast(Boolean) ?? null;
76482
+ }
76483
+ function hasOwnCommits2(cwd, branch, start, head) {
76484
+ if (!branch || branch.startsWith("-"))
76485
+ return false;
76486
+ const log = gitRun4(cwd, ["log", "-g", "--format=%H%x00%gs", `refs/heads/${branch}`, "--"]);
76487
+ if (!log.ok)
76488
+ return false;
76489
+ for (const line of log.stdout.split(`
76490
+ `)) {
76491
+ const [sha, subject = ""] = line.split("\x00");
76492
+ if (!sha)
76493
+ continue;
76494
+ const made = /^(?:commit(?: \((?:initial|amend)\))?|cherry-pick|rebase(?: -i)? \(pick\)):/u.test(subject);
76495
+ const onto = /^rebase(?: -i)? \(finish\): \S+ onto ([0-9a-f]+)/u.exec(subject)?.[1];
76496
+ if (!made && !(onto && !sha.startsWith(onto) && !onto.startsWith(sha)))
76497
+ continue;
76498
+ if (sha !== head && !isAncestor2(cwd, sha, head))
76499
+ continue;
76500
+ if (start && (sha === start || isAncestor2(cwd, sha, start)))
76501
+ continue;
76502
+ return true;
76503
+ }
76504
+ return false;
76505
+ }
76506
+ function reverted(cwd, trunk, landedHead, start) {
76507
+ const fork = mergeBase(cwd, landedHead, trunk);
76508
+ if (fork && fork !== landedHead)
76509
+ return unchangedSince(cwd, fork, landedHead, trunk);
76510
+ const before = start && start !== landedHead && isAncestor2(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
76511
+ return before ? unchangedSince(cwd, before, landedHead, trunk) : false;
76512
+ }
76513
+ function unchangedSince(cwd, before, after, trunk) {
76514
+ const paths = gitRun4(cwd, ["diff", "--name-only", "--no-renames", "-z", before, after]);
76515
+ const files = paths.ok ? paths.stdout.split("\x00").filter(Boolean) : [];
76516
+ if (files.length === 0)
76517
+ return false;
76518
+ return gitRun4(cwd, ["diff", "--quiet", before, trunk, "--", ...files]).ok;
76519
+ }
76520
+ function trunkPatchIds(ctx, fork) {
76521
+ const cache = ctx.cache ??= new Map;
76522
+ const known = cache.get(fork);
76523
+ if (known)
76524
+ return known;
76525
+ const ids = new Set;
76526
+ const log = gitRun4(ctx.cwd, [
76527
+ "log",
76528
+ "--no-merges",
76529
+ `--max-count=${TRUNK_SCAN}`,
76530
+ "-p",
76531
+ "--format=commit %H",
76532
+ "-U3",
76533
+ "--diff-algorithm=myers",
76534
+ "--indent-heuristic",
76535
+ "--no-color",
76536
+ "--no-ext-diff",
76537
+ "--no-textconv",
76538
+ "--no-renames",
76539
+ "--full-index",
76540
+ "--src-prefix=a/",
76541
+ "--dst-prefix=b/",
76542
+ `${fork}..${ctx.trunk}`,
76543
+ "--"
76544
+ ]);
76545
+ if (log.ok && log.stdout.trim()) {
76546
+ const run = spawnSync22("git", ["patch-id", "--stable"], {
76547
+ cwd: ctx.cwd,
76548
+ input: log.stdout,
76549
+ encoding: "utf8",
76550
+ timeout: GIT_TIMEOUTS.local,
76551
+ windowsHide: true,
76552
+ maxBuffer: 64 * 1024 * 1024
76553
+ });
76554
+ for (const line of (run.stdout ?? "").split(`
76555
+ `)) {
76556
+ const id = line.trim().split(/\s+/u)[0];
76557
+ if (id)
76558
+ ids.add(id);
76559
+ }
76560
+ }
76561
+ cache.set(fork, ids);
76562
+ return ids;
76563
+ }
76564
+ function linked(rows, input, slice, head) {
76565
+ const since = input.since ? Date.parse(input.since) : Number.NaN;
76566
+ return rows.some((row) => (Number.isNaN(since) || Date.parse(String(row.at)) >= since) && (row.type === "fanout.worktree.created" && row.fanout === input.fanout && row.slice === slice.id || head !== null && row.branch === slice.branch && row.head === head));
76567
+ }
76568
+ function detectLanding(ctx, slice, input) {
76569
+ const out = {
76570
+ landed: false,
76571
+ how: null,
76572
+ pr: null,
76573
+ landedHead: null,
76574
+ forgeError: null,
76575
+ note: null
76576
+ };
76577
+ const landed = (how, landedHead) => {
76578
+ if (!hasCommit(ctx.cwd, landedHead) || !reverted(ctx.cwd, ctx.trunk, landedHead, input.createdFrom))
76579
+ return { ...out, landed: true, how, landedHead };
76580
+ return {
76581
+ ...out,
76582
+ note: `landed (${how}${out.pr ? ` #${out.pr.number}` : ""}), then reverted on the trunk: its paths read as before it`
76583
+ };
76584
+ };
76585
+ const { head } = input;
76586
+ if (ctx.forge) {
76587
+ const found = ctx.forge.forge.findPr(slice.branch, {
76588
+ owner: ctx.forge.headRepo.split("/")[0] ?? null,
76589
+ projectId: ctx.forge.headProjectId,
76590
+ sha: head
76591
+ });
76592
+ if (!found.ok)
76593
+ out.forgeError = found.error;
76594
+ else if (found.data) {
76595
+ out.pr = found.data;
76596
+ if (found.data.state === "merged") {
76597
+ const merged = found.data.headSha;
76598
+ const label = `#${found.data.number}`;
76599
+ if (!head) {
76600
+ if (!linked(input.rows, input, slice, merged)) {
76601
+ out.pr = null;
76602
+ out.note = `${slice.branch} is gone; merged ${label} is not linked to this slice in the ledger (an older branch of the same name?)`;
76603
+ return out;
76604
+ }
76605
+ return merged ? landed("pr_merged", merged) : { ...out, landed: true, how: "pr_merged" };
76606
+ }
76607
+ if (!merged || head === merged || isAncestor2(ctx.cwd, head, merged))
76608
+ return landed("pr_merged", merged ?? head);
76609
+ out.note = isAncestor2(ctx.cwd, merged, head) ? `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} is ahead of it with newer commits` : `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} has diverged from it (rewritten after the merge)`;
76610
+ return out;
76611
+ }
76612
+ }
76613
+ }
76614
+ if (!head)
76615
+ return out;
76616
+ const fork = mergeBase(ctx.cwd, head, ctx.trunk);
76617
+ if (fork === head) {
76618
+ return hasOwnCommits2(ctx.cwd, slice.branch, input.createdFrom, head) ? landed("on_trunk", head) : out;
76619
+ }
76620
+ if (!fork)
76621
+ return out;
76622
+ const trunkIds = trunkPatchIds(ctx, fork);
76623
+ if (trunkIds.size === 0)
76624
+ return out;
76625
+ const froms = [fork];
76626
+ if (input.parentHead && input.parentHead !== head && isAncestor2(ctx.cwd, input.parentHead, head))
76627
+ froms.push(input.parentHead);
76628
+ for (const from of froms) {
76629
+ const id = patchId(ctx.cwd, from, head);
76630
+ if (id && trunkIds.has(id))
76631
+ return landed("squash_patch", head);
76632
+ }
76633
+ return out;
76634
+ }
76635
+ function sliceStart(cwd, rows, fanout, slice) {
76636
+ const reflog = branchCreatedAt(cwd, slice.branch);
76637
+ if (reflog)
76638
+ return reflog;
76639
+ const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string");
76640
+ return row?.baseSha ?? null;
76641
+ }
76642
+ var isAncestor2 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
76643
+ var init_fanout_landed = __esm(() => {
76644
+ init_rev();
76645
+ init_fanout();
76646
+ });
76647
+
76355
76648
  // packages/workit-core/src/fanout-check.ts
76356
76649
  function mergeConflicts(cwd, a, b) {
76357
76650
  const run = gitRun4(cwd, [
@@ -76385,10 +76678,15 @@ function checkFanout(cwd, plan, options = {}) {
76385
76678
  const tips = new Map(plan.slices.map((slice) => [slice.id, branchRef(cwd, slice.branch)]));
76386
76679
  const ledger = readLedger(cwd);
76387
76680
  const rows = ledger.ok ? ledger.value.rows : [];
76681
+ const breaker = forgeBreaker(options.forge ?? null);
76682
+ const landings = sliceLandings(cwd, plan, trunk, tips, rows, breaker.forge);
76683
+ const trunkCase = caseIndex(nulList2(gitRun4(cwd, ["ls-tree", "-r", "-z", "--name-only", trunk]).stdout));
76388
76684
  const checks = new Map;
76389
76685
  const changedBy = new Map;
76390
- for (const slice of plan.slices)
76391
- checks.set(slice.id, inspect(cwd, plan, slice, trunk, tips, rows, changedBy));
76686
+ for (const slice of plan.slices) {
76687
+ const landing = landings.get(slice.id);
76688
+ checks.set(slice.id, landing.landed ? landedCheck(slice, tips.get(slice.id) ?? null, landing) : inspect(cwd, plan, slice, { trunk, trunkCase, tips, landings, rows }, changedBy));
76689
+ }
76392
76690
  const conflicts = [];
76393
76691
  for (let i = 0;i < order.length; i += 1)
76394
76692
  for (let j = i + 1;j < order.length; j += 1) {
@@ -76396,7 +76694,9 @@ function checkFanout(cwd, plan, options = {}) {
76396
76694
  const second = order[j];
76397
76695
  const a = tips.get(first);
76398
76696
  const b = tips.get(second);
76399
- if (!a || !b || isAncestor2(cwd, a, b) || isAncestor2(cwd, b, a))
76697
+ if (landings.get(first)?.landed || landings.get(second)?.landed)
76698
+ continue;
76699
+ if (!a || !b || isAncestor3(cwd, a, b) || isAncestor3(cwd, b, a))
76400
76700
  continue;
76401
76701
  const check = checks.get(second);
76402
76702
  const theirs = new Map((changedBy.get(first) ?? []).map((file) => [file.toLowerCase(), file]));
@@ -76434,19 +76734,29 @@ function checkFanout(cwd, plan, options = {}) {
76434
76734
  }
76435
76735
  for (const id of order) {
76436
76736
  const check = checks.get(id);
76437
- check.waitsFor = (byId.get(id)?.dependsOn ?? []).filter((dep) => checks.get(dep)?.status !== "ready");
76737
+ if (check.status === "landed")
76738
+ continue;
76739
+ check.waitsFor = (byId.get(id)?.dependsOn ?? []).filter((dep) => checks.get(dep)?.status !== "ready" && checks.get(dep)?.status !== "landed");
76438
76740
  if (check.waitsFor.length)
76439
76741
  check.reasons.push(`waits for ${check.waitsFor.join(", ")}`);
76440
76742
  check.status = check.reasons.length ? "blocked" : "ready";
76441
76743
  }
76442
76744
  const selected = options.only?.length ? new Set(options.only) : null;
76443
76745
  const slices = order.filter((id) => !selected || selected.has(id)).map((id) => checks.get(id));
76444
- const ready = slices.every((check) => check.status === "ready");
76746
+ const ready = slices.every((check) => check.status !== "blocked");
76445
76747
  const blocked = slices.find((check) => check.status === "blocked");
76446
76748
  const notes = plan.v === FANOUT_VERSION ? [] : [
76447
76749
  `plan file version ${plan.v} is not ${FANOUT_VERSION}; read leniently (re-plan to rewrite it)`
76448
76750
  ];
76449
- 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)`;
76751
+ const forgeErrors = [...landings.values()].flatMap((landing) => landing.forgeError ? [landing.forgeError] : []);
76752
+ if (!options.forge)
76753
+ notes.push("landed slices detected with git only (no forge): a squash merge may be missed");
76754
+ else if (breaker.down())
76755
+ notes.push(`forge went down (${breaker.down()}); git alone decided the remaining slices`);
76756
+ else if (forgeErrors.length)
76757
+ notes.push(`forge lookups failed (${forgeErrors[0]}); git alone decided those slices`);
76758
+ const toLand = slices.filter((check) => check.status === "ready").map((check) => check.id);
76759
+ const next = blocked ? unblockFor(blocked, plan) : toLand.length ? `land in this order: ${toLand.join(", ")} (workit pr merge each once verified; stacked slices with workit stack land)` : "every slice has landed";
76450
76760
  return {
76451
76761
  ok: true,
76452
76762
  data: {
@@ -76462,28 +76772,61 @@ function checkFanout(cwd, plan, options = {}) {
76462
76772
  }
76463
76773
  };
76464
76774
  }
76465
- function inspect(cwd, plan, slice, trunk, tips, rows, changedBy) {
76466
- const head = tips.get(slice.id) ?? null;
76467
- const check = {
76468
- id: slice.id,
76469
- branch: slice.branch,
76470
- head,
76471
- base: slice.base,
76472
- status: "blocked",
76473
- changed: 0,
76474
- outOfScope: [],
76475
- trunkConflicts: [],
76476
- siblingConflicts: [],
76477
- waitsFor: [],
76478
- verdict: { accepted: false, basis: "missing" },
76479
- reasons: []
76775
+ function sliceLandings(cwd, plan, trunk, tips, rows, forge) {
76776
+ const ctx = { cwd, trunk, forge };
76777
+ const out = new Map;
76778
+ const byId = new Map(plan.slices.map((slice) => [slice.id, slice]));
76779
+ for (const id of landingOrder(plan.slices)) {
76780
+ const slice = byId.get(id);
76781
+ const parent = plan.slices.find((other) => other.branch === slice.base);
76782
+ const parentHead = parent ? tips.get(parent.id) ?? out.get(parent.id)?.landedHead ?? null : null;
76783
+ out.set(id, detectLanding(ctx, slice, {
76784
+ head: tips.get(id) ?? null,
76785
+ createdFrom: sliceStart(cwd, rows, plan.name, slice),
76786
+ parentHead,
76787
+ fanout: plan.name,
76788
+ since: plan.createdAt || null,
76789
+ rows
76790
+ }));
76791
+ }
76792
+ return out;
76793
+ }
76794
+ function landedCheck(slice, head, landing) {
76795
+ return {
76796
+ ...blank2(slice, head),
76797
+ status: "landed",
76798
+ landed: { how: landing.how ?? "pr_merged", pr: landing.pr?.number ?? null }
76480
76799
  };
76800
+ }
76801
+ function trunkCaseCollisions(cwd, ctx, head, changed) {
76802
+ const headFiles = treeFiles(cwd, head);
76803
+ const forkFiles = treeFiles(cwd, gitRun4(cwd, ["merge-base", head, ctx.trunk]).stdout.trim() || null);
76804
+ if (!headFiles)
76805
+ return [];
76806
+ const present = (files, prefix) => Boolean(files && (files.has(prefix) || [...files].some((file) => file.startsWith(`${prefix}/`))));
76807
+ const out = new Set;
76808
+ for (const file of changed) {
76809
+ if (!headFiles.has(file))
76810
+ continue;
76811
+ const clash = caseClash(ctx.trunkCase, file);
76812
+ if (!clash)
76813
+ continue;
76814
+ if (present(forkFiles, clash.trunk) && !present(headFiles, clash.trunk))
76815
+ continue;
76816
+ out.add(`${clash.path} ~ ${clash.trunk}`);
76817
+ }
76818
+ return [...out].toSorted();
76819
+ }
76820
+ function inspect(cwd, plan, slice, ctx, changedBy) {
76821
+ const { trunk, tips, rows } = ctx;
76822
+ const head = tips.get(slice.id) ?? null;
76823
+ const check = blank2(slice, head);
76481
76824
  if (!head) {
76482
- check.reasons.push(`branch ${slice.branch} not found (not started, landed, or not fetched)`);
76825
+ check.reasons.push(`branch ${slice.branch} not found (not started, or not fetched)`);
76483
76826
  return check;
76484
76827
  }
76485
76828
  const parent = plan.slices.find((other) => other.branch === slice.base);
76486
- const baseRef = slice.base === plan.trunk ? trunk : parent ? tips.get(parent.id) ?? null : branchRef(cwd, slice.base);
76829
+ const baseRef = slice.base === plan.trunk ? trunk : parent ? tips.get(parent.id) ?? ctx.landings.get(parent.id)?.landedHead ?? null : branchRef(cwd, slice.base);
76487
76830
  if (!SAFE(baseRef)) {
76488
76831
  check.reasons.push(`base ${slice.base} not found`);
76489
76832
  return check;
@@ -76501,6 +76844,9 @@ function inspect(cwd, plan, slice, trunk, tips, rows, changedBy) {
76501
76844
  check.outOfScope = changed.filter((file) => !inScope(plan, slice, file)).toSorted();
76502
76845
  if (check.outOfScope.length)
76503
76846
  check.reasons.push(`edits outside its scope: ${check.outOfScope.join(", ")}`);
76847
+ check.caseCollisions = trunkCaseCollisions(cwd, ctx, head, changed);
76848
+ if (check.caseCollisions.length)
76849
+ check.reasons.push(`differs only in case from ${plan.trunk}: ${check.caseCollisions.join(", ")}`);
76504
76850
  const trunkResult = mergeConflicts(cwd, trunk, head);
76505
76851
  if (Array.isArray(trunkResult)) {
76506
76852
  check.trunkConflicts = trunkResult;
@@ -76514,9 +76860,11 @@ function inspect(cwd, plan, slice, trunk, tips, rows, changedBy) {
76514
76860
  }
76515
76861
  function unblockFor(check, plan) {
76516
76862
  if (!check.head)
76517
- return `start or fetch ${check.branch}, then workit fanout check; if it landed, re-plan without slice ${check.id}`;
76863
+ return `start or fetch ${check.branch}, then workit fanout check (a landed slice is detected through its merged PR when the forge is reachable)`;
76518
76864
  if (check.outOfScope.length)
76519
76865
  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`;
76866
+ if (check.caseCollisions.length)
76867
+ return `rename to ${plan.trunk}'s spelling on ${check.branch} (git mv), then workit fanout check`;
76520
76868
  if (check.trunkConflicts.length)
76521
76869
  return `rebase ${check.branch} onto ${plan.trunk} and resolve ${check.trunkConflicts.join(", ")}, then workit fanout check`;
76522
76870
  if (check.siblingConflicts.length) {
@@ -76527,10 +76875,552 @@ function unblockFor(check, plan) {
76527
76875
  return `unblock ${check.waitsFor.join(", ")} first`;
76528
76876
  return check.reasons[0] ?? "workit fanout check";
76529
76877
  }
76530
- 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;
76878
+ var SAFE = (value) => Boolean(value) && !value?.startsWith("-"), isAncestor3 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, resolves = (cwd, ref) => gitRun4(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]).ok, blank2 = (slice, head) => ({
76879
+ id: slice.id,
76880
+ branch: slice.branch,
76881
+ head,
76882
+ base: slice.base,
76883
+ status: "blocked",
76884
+ changed: 0,
76885
+ outOfScope: [],
76886
+ caseCollisions: [],
76887
+ trunkConflicts: [],
76888
+ siblingConflicts: [],
76889
+ waitsFor: [],
76890
+ verdict: { accepted: false, basis: "missing" },
76891
+ landed: null,
76892
+ reasons: []
76893
+ }), treeFiles = (cwd, rev) => {
76894
+ if (!rev)
76895
+ return null;
76896
+ const run = gitRun4(cwd, ["ls-tree", "-r", "-z", "--name-only", rev]);
76897
+ return run.ok ? new Set(nulList2(run.stdout)) : null;
76898
+ };
76531
76899
  var init_fanout_check = __esm(() => {
76532
76900
  init_ledger();
76533
76901
  init_fanout();
76902
+ init_fanout_landed();
76903
+ });
76904
+
76905
+ // packages/workit-core/src/fanout-status.ts
76906
+ function timeboxMs(text) {
76907
+ if (!text || /^\s*\d+(?:\.\d+)?\s*$/u.test(text))
76908
+ return null;
76909
+ const ms = parseDuration2(text);
76910
+ return ms && ms > 0 ? ms : null;
76911
+ }
76912
+ function activity(cwd, plan, slice, rows) {
76913
+ const head = branchRef(cwd, slice.branch);
76914
+ const commit = head ? isoOf(gitRun4(cwd, ["log", "-1", "--format=%ct", head, "--"]).stdout) : null;
76915
+ const moved = /@\{(\d+)\}/u.exec(gitRun4(cwd, [
76916
+ "log",
76917
+ "-g",
76918
+ "-1",
76919
+ "--date=unix",
76920
+ "--format=%gd",
76921
+ `refs/heads/${slice.branch}`,
76922
+ "--"
76923
+ ]).stdout);
76924
+ const reflog = moved ? isoOf(moved[1]) : null;
76925
+ const ledger = rows.filter((row) => row.branch === slice.branch || row.fanout === plan.name && row.slice === slice.id).map((row) => row.at);
76926
+ return {
76927
+ head,
76928
+ commit,
76929
+ last: newest(commit, reflog, ...ledger),
76930
+ worktreeCreated: rows.some((row) => row.type === "fanout.worktree.created" && row.fanout === plan.name && row.slice === slice.id)
76931
+ };
76932
+ }
76933
+ function fanoutStatus(cwd, plan, options) {
76934
+ const now = options.now ?? new Date;
76935
+ const breaker = forgeBreaker(options.forge);
76936
+ const forge = breaker.forge;
76937
+ const trunk = trunkRef(cwd, plan.trunk);
76938
+ if (!trunk)
76939
+ return fanoutFail("unavailable", `trunk ${plan.trunk} does not resolve here`, `git fetch origin ${plan.trunk}`);
76940
+ const ledger = readLedger(cwd);
76941
+ const rows = ledger.ok ? ledger.value.rows : [];
76942
+ const notes = [];
76943
+ if (!ledger.ok)
76944
+ notes.push(`ledger unreadable (${ledger.error}); verdicts read as missing`);
76945
+ if (!forge)
76946
+ notes.push(`forge unavailable${options.forgeError ? ` (${options.forgeError})` : ""}: PR and CI not shown; landed detection uses git only, so a squash merge may be missed`);
76947
+ const tips = new Map(plan.slices.map((slice) => [slice.id, branchRef(cwd, slice.branch)]));
76948
+ const landings = sliceLandings(cwd, plan, trunk, tips, rows, forge);
76949
+ const byId = new Map(plan.slices.map((slice) => [slice.id, slice]));
76950
+ const order = landingOrder(plan.slices);
76951
+ const out = new Map;
76952
+ for (const id of order) {
76953
+ const slice = byId.get(id);
76954
+ const landing = landings.get(id);
76955
+ const seen = activity(cwd, plan, slice, rows);
76956
+ const threshold = options.stuckAfterMs ?? timeboxMs(slice.timebox) ?? DEFAULT_STUCK_MS;
76957
+ const idle = seen.last ? Math.max(0, now.getTime() - Date.parse(seen.last)) : null;
76958
+ const verdict = seen.head ? checkVerdicts(cwd, slice.branch, rows) : null;
76959
+ const row = {
76960
+ id,
76961
+ branch: slice.branch,
76962
+ base: slice.base,
76963
+ worktree: slice.worktree,
76964
+ dependsOn: slice.dependsOn,
76965
+ branchExists: seen.head !== null,
76966
+ head: seen.head,
76967
+ lastCommitAt: seen.commit,
76968
+ lastActivityAt: seen.last,
76969
+ idleMinutes: idle === null ? null : Math.floor(idle / 60000),
76970
+ stuckAfterMinutes: Math.round(threshold / 60000),
76971
+ pr: landing?.pr ? { number: landing.pr.number, url: landing.pr.url, state: landing.pr.state } : null,
76972
+ ci: "unknown",
76973
+ verdict: {
76974
+ accepted: verdict?.accepted.accepted ?? false,
76975
+ basis: verdict?.current.basis ?? "none",
76976
+ review: verdict?.review ?? "unreviewed"
76977
+ },
76978
+ landed: landing?.landed && landing.how ? { how: landing.how, pr: landing.pr?.number ?? null } : null,
76979
+ stuck: false,
76980
+ waitsFor: [],
76981
+ state: "not_started",
76982
+ reasons: [],
76983
+ notes: [
76984
+ ...landing?.note ? [landing.note] : [],
76985
+ ...landing?.forgeError ? [`forge: ${landing.forgeError}`] : []
76986
+ ]
76987
+ };
76988
+ const answered = forge !== null && !landing?.forgeError;
76989
+ if (answered && landing?.pr && landing.pr.state === "open" && !row.landed) {
76990
+ const status = forge.forge.prStatus(landing.pr.number);
76991
+ if (status.ok)
76992
+ row.ci = checksState(gatingChecks(status.data).checks, missingRequired(status.data));
76993
+ else
76994
+ row.notes.push(`forge: ${status.error}`);
76995
+ }
76996
+ row.waitsFor = slice.dependsOn.filter((dep) => !out.get(dep)?.landed);
76997
+ out.set(id, row);
76998
+ if (row.landed) {
76999
+ row.state = "landed";
77000
+ continue;
77001
+ }
77002
+ const started = row.branchExists || seen.worktreeCreated;
77003
+ if (!row.branchExists)
77004
+ row.reasons.push("no branch yet");
77005
+ if (!row.verdict.accepted)
77006
+ row.reasons.push(`verdict ${row.verdict.basis === "none" ? "missing" : row.verdict.basis} on the current head`);
77007
+ if (answered && row.branchExists) {
77008
+ if (!row.pr)
77009
+ row.reasons.push("no PR");
77010
+ else if (row.pr.state !== "open")
77011
+ row.reasons.push(`PR ${row.pr.state}`);
77012
+ else if (row.ci === "failing" || row.ci === "pending")
77013
+ row.reasons.push(`CI ${row.ci}`);
77014
+ }
77015
+ row.stuck = started && !row.verdict.accepted && idle !== null && idle > threshold;
77016
+ if (row.stuck)
77017
+ row.reasons.push(`no activity for ${row.idleMinutes} min`);
77018
+ }
77019
+ const suggested = [];
77020
+ for (const id of order) {
77021
+ const row = out.get(id);
77022
+ if (row.landed || row.reasons.length)
77023
+ continue;
77024
+ if (row.waitsFor.every((dep) => suggested.includes(dep)))
77025
+ suggested.push(id);
77026
+ }
77027
+ const spawnable = [];
77028
+ for (const id of order) {
77029
+ const row = out.get(id);
77030
+ if (row.landed)
77031
+ continue;
77032
+ const parent = plan.slices.find((other) => other.branch === row.base);
77033
+ if (suggested.includes(id))
77034
+ row.state = "ready";
77035
+ else if (row.stuck)
77036
+ row.state = "stuck";
77037
+ else if (row.branchExists)
77038
+ row.state = "active";
77039
+ else if (row.waitsFor.length === 0 || parent && row.waitsFor.length === 1 && row.waitsFor[0] === parent.id && out.get(parent.id)?.verdict.accepted) {
77040
+ const created = rows.some((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === plan.name && candidate.slice === id);
77041
+ row.state = created ? "active" : "not_started";
77042
+ if (!created)
77043
+ spawnable.push(id);
77044
+ } else
77045
+ row.state = "waiting";
77046
+ if (row.waitsFor.length && !suggested.includes(id))
77047
+ row.reasons.push(`waits for ${row.waitsFor.join(", ")}`);
77048
+ }
77049
+ const stuck = order.filter((id) => out.get(id)?.stuck);
77050
+ const down = breaker.down();
77051
+ if (down)
77052
+ notes.push(`forge went down during this run (${down}): later slices show no PR or CI, and git alone decided whether they landed`);
77053
+ const online = forge !== null && !down;
77054
+ const noPr = order.filter((id) => {
77055
+ const row = out.get(id);
77056
+ return !row.landed && row.reasons.length === 1 && row.reasons[0] === "no PR";
77057
+ });
77058
+ const next = stuck.length ? `replace ${stuck.join(", ")}: stop the worker, then workit fanout worktree release ${stuck[0]} (records git status first) and respawn it in MODE: resume` : suggested.length ? online ? `land in this order: ${suggested.join(", ")} (workit fanout check first; workit pr merge each)` : `land in this order: ${suggested.join(", ")} once each has a PR (offline: PRs unchecked; open a missing one with workit git push && workit pr create --fill, then workit fanout status online)` : noPr.length ? `open PRs for ${noPr.join(", ")}: workit git push && workit pr create --fill on each branch` : spawnable.length ? `spawn ${spawnable.join(", ")} (workit fanout worktree create <slice> on hosts without native worktrees)` : order.every((id) => out.get(id)?.landed) ? "every slice has landed" : "wait for workers, then workit fanout status again";
77059
+ return {
77060
+ ok: true,
77061
+ data: {
77062
+ name: plan.name,
77063
+ trunk: plan.trunk,
77064
+ trunkRef: trunk,
77065
+ forge: {
77066
+ available: online,
77067
+ error: online ? null : down ?? options.forgeError ?? null
77068
+ },
77069
+ slices: order.map((id) => out.get(id)),
77070
+ landingOrder: suggested,
77071
+ spawnable,
77072
+ stuck,
77073
+ next,
77074
+ notes
77075
+ }
77076
+ };
77077
+ }
77078
+ var DEFAULT_STUCK_MS, isoOf = (seconds) => {
77079
+ const value = Number(seconds.trim());
77080
+ return Number.isFinite(value) && value > 0 ? new Date(value * 1000).toISOString() : null;
77081
+ }, newest = (...values) => values.filter((value) => Boolean(value) && !Number.isNaN(Date.parse(value ?? ""))).reduce((best, value) => best === null || Date.parse(value) > Date.parse(best) ? value : best, null);
77082
+ var init_fanout_status = __esm(() => {
77083
+ init_duration();
77084
+ init_report();
77085
+ init_ledger();
77086
+ init_fanout_check();
77087
+ init_fanout_landed();
77088
+ init_fanout();
77089
+ DEFAULT_STUCK_MS = 30 * 60000;
77090
+ });
77091
+
77092
+ // packages/workit-core/src/fanout-worktree.ts
77093
+ import fs24 from "node:fs";
77094
+ import path44 from "node:path";
77095
+ function rowSample(lines) {
77096
+ const out = [];
77097
+ let bytes = 0;
77098
+ for (const line of lines) {
77099
+ const entry = line.slice(0, 200);
77100
+ bytes += Buffer.byteLength(entry) + 3;
77101
+ if (bytes > DIRTY_ROW_BYTES)
77102
+ break;
77103
+ out.push(entry);
77104
+ }
77105
+ return out;
77106
+ }
77107
+ function listWorktrees(cwd) {
77108
+ const out = [];
77109
+ let current = null;
77110
+ for (const field of gitRun4(cwd, ["worktree", "list", "--porcelain", "-z"]).stdout.split("\x00")) {
77111
+ if (field.startsWith("worktree ")) {
77112
+ current = { path: field.slice(9), head: null, branch: null, locked: false };
77113
+ out.push(current);
77114
+ } else if (!current)
77115
+ continue;
77116
+ else if (field.startsWith("HEAD "))
77117
+ current.head = field.slice(5);
77118
+ else if (field.startsWith("branch "))
77119
+ current.branch = field.slice(7);
77120
+ else if (field === "locked" || field.startsWith("locked "))
77121
+ current.locked = true;
77122
+ }
77123
+ return out;
77124
+ }
77125
+ function createdRow(cwd, plan, slice, target) {
77126
+ const ledger = readLedger(cwd);
77127
+ if (!ledger.ok)
77128
+ return null;
77129
+ const atPath = (row) => row.fanout === plan.name && row.slice === slice.id && typeof row.path === "string" && samePath(row.path, target);
77130
+ const rows = ledger.value.rows;
77131
+ const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && atPath(candidate));
77132
+ if (!row)
77133
+ return null;
77134
+ const removedSince = rows.some((candidate) => candidate.seq > row.seq && candidate.type === "fanout.worktree.released" && candidate.outcome === "removed" && atPath(candidate));
77135
+ if (removedSince)
77136
+ return null;
77137
+ const made = adminDirTime(target);
77138
+ if (made === null || made + CLOCK_SLACK_MS < Date.parse(row.at))
77139
+ return null;
77140
+ return row;
77141
+ }
77142
+ function adminDirTime(worktree) {
77143
+ const dir = gitRun4(worktree, ["rev-parse", "--absolute-git-dir"]);
77144
+ if (!dir.ok)
77145
+ return null;
77146
+ try {
77147
+ const stat = fs24.statSync(dir.stdout.trim());
77148
+ return stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs;
77149
+ } catch {
77150
+ return null;
77151
+ }
77152
+ }
77153
+ function findSlice(plan, id) {
77154
+ const slice = plan.slices.find((candidate) => candidate.id === id);
77155
+ return slice ? { ok: true, data: slice } : fanoutFail("invalid_input", `${id}: not a slice of fanout ${plan.name}`, `slices: ${plan.slices.map((candidate) => candidate.id).join(", ")}`);
77156
+ }
77157
+ function excludeScratch(cwd) {
77158
+ const common = gitCommonDir(cwd);
77159
+ if (!common)
77160
+ return;
77161
+ const file = path44.join(common, "info", "exclude");
77162
+ let text = "";
77163
+ try {
77164
+ text = fs24.readFileSync(file, "utf8");
77165
+ } catch {
77166
+ text = "";
77167
+ }
77168
+ if (text.split(/\r?\n/u).includes(EXCLUDE_LINE))
77169
+ return;
77170
+ fs24.mkdirSync(path44.dirname(file), { recursive: true });
77171
+ fs24.appendFileSync(file, `${text && !text.endsWith(`
77172
+ `) ? `
77173
+ ` : ""}# workit fanout: per-worker scratch dirs
77174
+ ${EXCLUDE_LINE}
77175
+ `);
77176
+ }
77177
+ function ensureScratch(worktree) {
77178
+ const scratch = path44.join(worktree, SCRATCH_DIR);
77179
+ fs24.mkdirSync(scratch, { recursive: true });
77180
+ return scratch;
77181
+ }
77182
+ function createSliceWorktree(cwd, plan, input) {
77183
+ const found = findSlice(plan, input.slice);
77184
+ if (!found.ok)
77185
+ return found;
77186
+ const slice = found.data;
77187
+ if (!slice.worktree)
77188
+ return fanoutFail("invalid_input", `slice ${slice.id} has no worktree path`, "re-plan it with workit fanout plan");
77189
+ const target = slicePath(cwd, slice);
77190
+ const worktrees = listWorktrees(cwd);
77191
+ const localRef = `refs/heads/${slice.branch}`;
77192
+ const existing = worktrees.find((entry) => samePath(entry.path, target));
77193
+ const base = {
77194
+ fanout: plan.name,
77195
+ slice: slice.id,
77196
+ branch: slice.branch,
77197
+ base: slice.base,
77198
+ path: target
77199
+ };
77200
+ if (existing) {
77201
+ if (existing.branch !== null && existing.branch !== localRef)
77202
+ return fanoutFail("blocked", `${target} is a worktree on ${existing.branch.replace(/^refs\/heads\//u, "")}, not ${slice.branch}`, `release it first, or set slice ${slice.id}'s worktree to another path`);
77203
+ if (!createdRow(cwd, plan, slice, target))
77204
+ return fanoutFail("blocked", `${target} is a worktree that workit fanout worktree create did not make for slice ${slice.id}`, `remove it yourself (git worktree remove ${target}) once you know what it holds`);
77205
+ excludeScratch(cwd);
77206
+ return {
77207
+ ok: true,
77208
+ data: {
77209
+ ...base,
77210
+ scratch: ensureScratch(target),
77211
+ head: existing.head ?? "",
77212
+ mode: resolveRef(cwd, localRef) ? "resume" : "new",
77213
+ created: false,
77214
+ notes: ["the worktree already exists; nothing was changed"]
77215
+ }
77216
+ };
77217
+ }
77218
+ const elsewhere = worktrees.find((entry) => entry.branch === localRef);
77219
+ if (elsewhere)
77220
+ return fanoutFail("blocked", `${slice.branch} is checked out at ${elsewhere.path}: never two live workers on one branch`, `stop that worker, then workit fanout worktree release ${slice.id} (or git worktree remove ${elsewhere.path})`);
77221
+ let dirExisted = false;
77222
+ try {
77223
+ const stat = fs24.statSync(target);
77224
+ if (!stat.isDirectory() || fs24.readdirSync(target).length > 0)
77225
+ return fanoutFail("blocked", `${target} exists and is ${stat.isDirectory() ? "not empty" : "not a directory"} (not a worktree of this repository)`, `move it away, or set slice ${slice.id}'s worktree to another path`);
77226
+ dirExisted = true;
77227
+ } catch (error) {
77228
+ if (error.code !== "ENOENT")
77229
+ return fanoutFail("failed", `cannot read ${target}: ${error.message}`);
77230
+ }
77231
+ const notes = [];
77232
+ let mode = "resume";
77233
+ if (resolveRef(cwd, localRef)) {
77234
+ const added = gitRun4(cwd, ["worktree", "add", target, slice.branch], SLOW);
77235
+ if (!added.ok)
77236
+ return worktreeFailed(added.stderr);
77237
+ } else {
77238
+ const remote = pushRemoteName(cwd) ?? "origin";
77239
+ const remoteRef = `refs/remotes/${remote}/${slice.branch}`;
77240
+ if (resolveRef(cwd, remoteRef)) {
77241
+ const added = gitRun4(cwd, ["worktree", "add", "-b", slice.branch, target, remoteRef], SLOW);
77242
+ if (!added.ok)
77243
+ return worktreeFailed(added.stderr);
77244
+ notes.push(`resumed ${slice.branch} from ${remote}/${slice.branch}`);
77245
+ } else {
77246
+ mode = "new";
77247
+ const parent = plan.slices.find((other) => other.branch === slice.base);
77248
+ const from = slice.base === plan.trunk ? trunkRef(cwd, plan.trunk) : parent ? branchRef(cwd, parent.branch) : branchRef(cwd, slice.base);
77249
+ if (!from)
77250
+ return fanoutFail("not_found", `base ${slice.base} of slice ${slice.id} does not resolve here`, parent ? `start slice ${parent.id} first (its branch ${parent.branch} is the base)` : `git fetch origin ${slice.base}`);
77251
+ const added = gitRun4(cwd, ["worktree", "add", "--detach", target, from], SLOW);
77252
+ if (!added.ok)
77253
+ return worktreeFailed(added.stderr);
77254
+ const branched = gitBranch(target, { name: slice.branch, base: slice.base });
77255
+ if (!branched.ok) {
77256
+ gitRun4(cwd, ["worktree", "remove", target], SLOW);
77257
+ return fanoutFail(branched.code, `workit git branch ${slice.branch} refused: ${branched.error}`, branched.unblock ?? `fix the branch name in the plan, then workit fanout worktree create ${slice.id}`);
77258
+ }
77259
+ notes.push(...branched.data.notes);
77260
+ }
77261
+ }
77262
+ excludeScratch(cwd);
77263
+ const scratch = ensureScratch(target);
77264
+ const head = gitRun4(target, ["rev-parse", "HEAD"]).stdout.trim();
77265
+ const baseSha = mode === "new" ? head : null;
77266
+ appendObserved(cwd, {
77267
+ type: "fanout.worktree.created",
77268
+ actor: input.actor,
77269
+ fanout: plan.name,
77270
+ slice: slice.id,
77271
+ branch: slice.branch,
77272
+ path: target,
77273
+ scratch,
77274
+ mode,
77275
+ head,
77276
+ dirExisted,
77277
+ planCreatedAt: plan.createdAt,
77278
+ ...baseSha ? { baseSha } : {}
77279
+ });
77280
+ return { ok: true, data: { ...base, scratch, head, mode, created: true, notes } };
77281
+ }
77282
+ function releaseSliceWorktree(cwd, plan, input) {
77283
+ const found = findSlice(plan, input.slice);
77284
+ if (!found.ok)
77285
+ return found;
77286
+ const slice = found.data;
77287
+ const target = slicePath(cwd, slice);
77288
+ const worktrees = listWorktrees(cwd);
77289
+ const index = worktrees.findIndex((entry) => samePath(entry.path, target));
77290
+ const out = {
77291
+ fanout: plan.name,
77292
+ slice: slice.id,
77293
+ branch: slice.branch,
77294
+ path: target,
77295
+ head: null,
77296
+ dirty: [],
77297
+ forced: input.force,
77298
+ removed: false,
77299
+ unpushed: null,
77300
+ notes: []
77301
+ };
77302
+ if (index < 0) {
77303
+ if (fs24.existsSync(target))
77304
+ return fanoutFail("blocked", `${target} is not a worktree of this repository; nothing was removed`, "remove it yourself once you know what it holds");
77305
+ out.notes.push(`no worktree at ${target}; nothing to release`);
77306
+ return { ok: true, data: out };
77307
+ }
77308
+ const entry = worktrees[index];
77309
+ if (index === 0)
77310
+ return fanoutFail("blocked", `${target} is the main checkout; it is never released`);
77311
+ if (inside2(cwd, entry.path))
77312
+ return fanoutFail("blocked", `this command runs inside ${entry.path}`, `run it from the main checkout: workit --cwd <main checkout> fanout worktree release ${slice.id}`);
77313
+ if (entry.branch !== null && entry.branch !== `refs/heads/${slice.branch}`)
77314
+ return fanoutFail("blocked", `${entry.path} is on ${entry.branch.replace(/^refs\/heads\//u, "")}, not slice ${slice.id}'s ${slice.branch}; nothing was removed`);
77315
+ const made = createdRow(cwd, plan, slice, entry.path);
77316
+ if (!made)
77317
+ return fanoutFail("blocked", `${entry.path} was not made by workit fanout worktree create for slice ${slice.id} (no ledger row); nothing was removed`, `remove it yourself (git worktree remove ${entry.path}) once you know what it holds`);
77318
+ out.head = entry.head;
77319
+ const status = gitRun4(entry.path, [
77320
+ "status",
77321
+ "--porcelain=v1",
77322
+ "-z",
77323
+ "--untracked-files=all",
77324
+ "--ignore-submodules=none"
77325
+ ]);
77326
+ if (!status.ok)
77327
+ return fanoutFail("failed", `git status in ${entry.path} failed: ${status.stderr.trim().split(`
77328
+ `)[0]}`, "nothing was removed; inspect the worktree by hand");
77329
+ const entries = porcelainEntries(status.stdout);
77330
+ const inScratch = (line) => line.slice(3).startsWith(`${SCRATCH_DIR}/`);
77331
+ out.dirty = entries.filter((line) => !inScratch(line));
77332
+ out.unpushed = unpushedCount(cwd, slice.branch);
77333
+ const refused = out.dirty.length > 0 && !input.force;
77334
+ const recorded = appendObserved(cwd, {
77335
+ type: "fanout.worktree.released",
77336
+ actor: input.actor,
77337
+ fanout: plan.name,
77338
+ slice: slice.id,
77339
+ branch: slice.branch,
77340
+ path: entry.path,
77341
+ head: entry.head,
77342
+ uncommittedCount: out.dirty.length,
77343
+ uncommitted: rowSample(out.dirty),
77344
+ forced: input.force,
77345
+ outcome: refused ? "refused" : "removed"
77346
+ });
77347
+ if (!recorded.ok)
77348
+ return fanoutFail("unavailable", `cannot record the worktree state in the ledger (${recorded.error}); nothing was removed`);
77349
+ if (refused)
77350
+ return fanoutFail("blocked", `${entry.path} has ${out.dirty.length} uncommitted change${out.dirty.length === 1 ? "" : "s"} (${out.dirty.slice(0, 3).join(", ")}${out.dirty.length > 3 ? ", …" : ""}); recorded in the ledger, nothing removed`, `commit them on ${slice.branch} (workit git commit), or pass --force to drop them`, { ...out });
77351
+ const args = ["worktree", "remove", ...input.force ? ["--force"] : [], entry.path];
77352
+ let removed = gitRun4(cwd, args, SLOW);
77353
+ if (!removed.ok && entries.some(inScratch)) {
77354
+ removeScratch(entry.path);
77355
+ removed = gitRun4(cwd, args, SLOW);
77356
+ }
77357
+ if (!removed.ok)
77358
+ return fanoutFail("failed", `git worktree remove failed: ${removed.stderr.trim().split(`
77359
+ `)[0]}`, entry.locked ? `git worktree unlock ${entry.path}, then release again` : undefined, { ...out });
77360
+ out.removed = true;
77361
+ if (made.dirExisted === true)
77362
+ fs24.mkdirSync(entry.path, { recursive: true });
77363
+ if (out.unpushed)
77364
+ out.notes.push(`${slice.branch} is kept with ${out.unpushed} commit(s) not on the remote`);
77365
+ return { ok: true, data: out };
77366
+ }
77367
+ function porcelainEntries(stdout) {
77368
+ const fields = nulList2(stdout);
77369
+ const out = [];
77370
+ for (let index = 0;index < fields.length; index += 1) {
77371
+ const entry = fields[index];
77372
+ out.push(entry);
77373
+ if (entry[0] === "R" || entry[0] === "C")
77374
+ index += 1;
77375
+ }
77376
+ return out;
77377
+ }
77378
+ function unpushedCount(cwd, branch) {
77379
+ const remote = pushRemoteName(cwd) ?? "origin";
77380
+ const local = resolveRef(cwd, `refs/heads/${branch}`);
77381
+ if (!local)
77382
+ return null;
77383
+ const tracking = resolveRef(cwd, `refs/remotes/${remote}/${branch}`);
77384
+ if (!tracking)
77385
+ return null;
77386
+ const count = gitRun4(cwd, ["rev-list", "--count", `${tracking}..${local}`]);
77387
+ return count.ok ? Number(count.stdout.trim()) : null;
77388
+ }
77389
+ function removeScratch(worktree) {
77390
+ const scratch = path44.join(worktree, SCRATCH_DIR);
77391
+ let stat;
77392
+ try {
77393
+ stat = fs24.lstatSync(scratch);
77394
+ } catch {
77395
+ return;
77396
+ }
77397
+ if (stat.isSymbolicLink() || !stat.isDirectory())
77398
+ fs24.unlinkSync(scratch);
77399
+ else
77400
+ fs24.rmSync(scratch, { recursive: true, force: true, maxRetries: 2 });
77401
+ }
77402
+ var SLOW, SCRATCH_DIR = ".workit-scratch", EXCLUDE_LINE, DIRTY_ROW_BYTES = 2000, canonical2 = (target) => {
77403
+ try {
77404
+ return fs24.realpathSync.native(target);
77405
+ } catch {
77406
+ return path44.resolve(target);
77407
+ }
77408
+ }, samePath = (a, b) => {
77409
+ const left = canonical2(a);
77410
+ const right = canonical2(b);
77411
+ return process.platform === "win32" || process.platform === "darwin" ? left.toLowerCase() === right.toLowerCase() : left === right;
77412
+ }, inside2 = (child, parent) => {
77413
+ const relative = path44.relative(canonical2(parent), canonical2(child));
77414
+ return relative === "" || !relative.startsWith("..") && !path44.isAbsolute(relative);
77415
+ }, slicePath = (cwd, slice) => path44.isAbsolute(slice.worktree) ? slice.worktree : path44.resolve(mainCheckout(cwd), slice.worktree), CLOCK_SLACK_MS = 2000, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
77416
+ `)[0] || "unknown error"}`);
77417
+ var init_fanout_worktree = __esm(() => {
77418
+ init_ops();
77419
+ init_rev();
77420
+ init_ledger();
77421
+ init_fanout();
77422
+ SLOW = GIT_TIMEOUTS.worktree;
77423
+ EXCLUDE_LINE = `/${SCRATCH_DIR}/`;
76534
77424
  });
76535
77425
 
76536
77426
  // packages/workit-cli/src/verbs/fanout.ts
@@ -76538,8 +77428,14 @@ var exports_fanout = {};
76538
77428
  __export(exports_fanout, {
76539
77429
  run: () => run15
76540
77430
  });
76541
- import fs24 from "node:fs";
76542
- import path44 from "node:path";
77431
+ import fs25 from "node:fs";
77432
+ import path45 from "node:path";
77433
+ function forgeFor(io, offline) {
77434
+ if (offline)
77435
+ return { forge: null, error: "--offline" };
77436
+ const connected = connect(io, null);
77437
+ return connected.ok ? { forge: connected.data, error: null } : { forge: null, error: connected.error };
77438
+ }
76543
77439
  function renderBlockedPlan(io, result) {
76544
77440
  if (io.json || result.code !== "blocked" && result.code !== "invalid_input")
76545
77441
  return;
@@ -76547,6 +77443,11 @@ function renderBlockedPlan(io, result) {
76547
77443
  if (Array.isArray(problems))
76548
77444
  for (const problem of problems)
76549
77445
  io.stderr(` - ${problem}
77446
+ `);
77447
+ const clashes = result.data?.caseCollisions;
77448
+ if (Array.isArray(clashes))
77449
+ for (const clash of clashes)
77450
+ io.stderr(` - ${clash.slice}: ${clash.path} ~ trunk's ${clash.trunk}
76550
77451
  `);
76551
77452
  const conflicts = result.data?.conflicts;
76552
77453
  if (Array.isArray(conflicts))
@@ -76584,10 +77485,10 @@ function plan2(argv, io) {
76584
77485
  return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE2);
76585
77486
  if (flags.positionals.length !== 1)
76586
77487
  return usage3(io, flags.positionals.length ? `unexpected argument ${flags.positionals[1]}` : "missing <plan.json>", PLAN_USAGE2);
76587
- const file = path44.resolve(io.cwd, flags.positionals[0]);
77488
+ const file = path45.resolve(io.cwd, flags.positionals[0]);
76588
77489
  let raw;
76589
77490
  try {
76590
- raw = JSON.parse(fs24.readFileSync(file, "utf8"));
77491
+ raw = JSON.parse(fs25.readFileSync(file, "utf8"));
76591
77492
  } catch (error) {
76592
77493
  return usage3(io, `cannot read ${file} as JSON: ${error.message}`, PLAN_USAGE2);
76593
77494
  }
@@ -76616,7 +77517,7 @@ function plan2(argv, io) {
76616
77517
  ]);
76617
77518
  }
76618
77519
  function check2(argv, io) {
76619
- const flags = parseFlags(argv, { name: "value", base: "value" });
77520
+ const flags = parseFlags(argv, { name: "value", base: "value", offline: "boolean" });
76620
77521
  if (typeof flags === "string")
76621
77522
  return usage3(io, flags, CHECK_USAGE);
76622
77523
  const selected = selectFanout(io.cwd, flags.values.name ?? null, currentBranch(io.cwd));
@@ -76624,7 +77525,8 @@ function check2(argv, io) {
76624
77525
  return failed(io, selected);
76625
77526
  const result = checkFanout(io.cwd, selected.data, {
76626
77527
  only: flags.positionals,
76627
- trunkRef: flags.values.base ?? null
77528
+ trunkRef: flags.values.base ?? null,
77529
+ forge: forgeFor(io, flags.booleans.has("offline")).forge
76628
77530
  });
76629
77531
  if (!result.ok)
76630
77532
  return failed(io, result);
@@ -76640,17 +77542,92 @@ function check2(argv, io) {
76640
77542
  unblock: result.data.next
76641
77543
  }));
76642
77544
  }
77545
+ function status3(argv, io) {
77546
+ const flags = parseFlags(argv, { name: "value", "stuck-after": "value", offline: "boolean" });
77547
+ if (typeof flags === "string")
77548
+ return usage3(io, flags, STATUS_USAGE3);
77549
+ if (flags.positionals.length)
77550
+ return usage3(io, `unexpected argument ${flags.positionals[0]}`, STATUS_USAGE3);
77551
+ const raw = flags.values["stuck-after"];
77552
+ const stuckAfterMs = raw === undefined ? null : parseDuration2(raw);
77553
+ if (stuckAfterMs === 0 || raw !== undefined && stuckAfterMs === null)
77554
+ return usage3(io, "--stuck-after takes a duration such as 30m, 45m or 2h", STATUS_USAGE3);
77555
+ const selected = selectFanout(io.cwd, flags.values.name ?? null, currentBranch(io.cwd));
77556
+ if (!selected.ok)
77557
+ return failed(io, selected);
77558
+ const forge = forgeFor(io, flags.booleans.has("offline"));
77559
+ const result = fanoutStatus(io.cwd, selected.data, {
77560
+ forge: forge.forge,
77561
+ forgeError: forge.error,
77562
+ stuckAfterMs
77563
+ });
77564
+ if (!result.ok)
77565
+ return failed(io, result);
77566
+ return emit(io, ok(result.data), renderStatus2);
77567
+ }
77568
+ function worktree(argv, io) {
77569
+ const [action, ...rest] = argv;
77570
+ const flags = parseFlags(rest, { name: "value", force: "boolean" });
77571
+ if (typeof flags === "string")
77572
+ return usage3(io, flags, WORKTREE_USAGE);
77573
+ if (action !== "create" && action !== "release")
77574
+ return usage3(io, action ? `unknown worktree action "${action}"` : "missing create or release", WORKTREE_USAGE);
77575
+ if (flags.positionals.length !== 1)
77576
+ return usage3(io, flags.positionals.length ? `unexpected argument ${flags.positionals[1]}` : "missing <slice>", WORKTREE_USAGE);
77577
+ if (action === "create" && flags.booleans.has("force"))
77578
+ return usage3(io, "--force applies to release only", WORKTREE_USAGE);
77579
+ const selected = selectFanout(io.cwd, flags.values.name ?? null, currentBranch(io.cwd));
77580
+ if (!selected.ok)
77581
+ return failed(io, selected);
77582
+ const actor = actorFromEnv(io.env);
77583
+ if (action === "create") {
77584
+ const result = createSliceWorktree(io.cwd, selected.data, {
77585
+ slice: flags.positionals[0],
77586
+ actor
77587
+ });
77588
+ if (!result.ok)
77589
+ return failed(io, result);
77590
+ return emit(io, ok(result.data), (data) => [
77591
+ `${data.created ? "created" : "kept"} worktree for slice ${data.slice} (${data.mode}): ${data.path}`,
77592
+ ` branch ${data.branch} <- ${data.base} at ${short5(data.head)}`,
77593
+ ` scratch ${data.scratch} (put it in the worker brief: temp files go here)`,
77594
+ ...data.notes.map((note) => `note: ${note}`)
77595
+ ]);
77596
+ }
77597
+ const result = releaseSliceWorktree(io.cwd, selected.data, {
77598
+ slice: flags.positionals[0],
77599
+ force: flags.booleans.has("force"),
77600
+ actor
77601
+ });
77602
+ if (!result.ok) {
77603
+ const code = failed(io, result);
77604
+ const dirty = result.data?.dirty;
77605
+ if (!io.json && Array.isArray(dirty))
77606
+ for (const line of dirty)
77607
+ io.stderr(` ${line}
77608
+ `);
77609
+ return code;
77610
+ }
77611
+ return emit(io, ok(result.data), (data) => [
77612
+ data.removed ? `released slice ${data.slice}: removed ${data.path}${data.dirty.length ? ` (dropped ${data.dirty.length} uncommitted change${data.dirty.length === 1 ? "" : "s"}, recorded in the ledger)` : ""}; branch ${data.branch} kept` : `slice ${data.slice}: nothing released`,
77613
+ ...data.notes.map((note) => `note: ${note}`)
77614
+ ]);
77615
+ }
76643
77616
  async function run15(argv, io) {
76644
77617
  const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
76645
77618
  if (sub === "plan")
76646
77619
  return plan2(rest, io);
76647
77620
  if (sub === "check")
76648
77621
  return check2(rest, io);
77622
+ if (sub === "status")
77623
+ return status3(rest, io);
77624
+ if (sub === "worktree")
77625
+ return worktree(rest, io);
76649
77626
  return usage3(io, sub && !sub.startsWith("-") ? `unknown fanout subcommand "${sub}"` : "missing fanout subcommand", USAGE8);
76650
77627
  }
76651
- 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) => [
77628
+ 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>] [--offline] [--json]", STATUS_USAGE3 = "workit fanout status [--name <n>] [--stuck-after 30m] [--offline] [--json]", WORKTREE_USAGE = "workit fanout worktree create <slice> [--name <n>] | worktree release <slice> [--name <n>] [--force] [--json]", USAGE8 = "workit fanout plan|check|status|worktree ... (workit help fanout)", failed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), renderCheck = (data) => [
76652
77629
  `fanout ${data.name} against ${data.trunkRef}: ${data.ready ? "ready to land" : "blocked"}`,
76653
- ...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 ? `
77630
+ ...data.slices.map((slice) => slice.status === "landed" ? ` -- ${slice.id} (${slice.branch}) landed (${slice.landed?.how}${slice.landed?.pr ? ` #${slice.landed.pr}` : ""})` : ` ${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 ? `
76654
77631
  ${slice.reasons.join(`
76655
77632
  `)}` : ""}`),
76656
77633
  ...data.conflicts.map((conflict) => ` conflict ${conflict.first} x ${conflict.second}: ${conflict.paths.join(", ")}
@@ -76658,9 +77635,37 @@ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --
76658
77635
  `landing order: ${data.landingOrder.join(", ")}`,
76659
77636
  `next: ${data.next}`,
76660
77637
  ...data.notes.map((note) => `note: ${note}`)
76661
- ];
77638
+ ], short5 = (sha) => sha ? sha.slice(0, 7) : "-", renderStatus2 = (data) => {
77639
+ const counts = new Map;
77640
+ for (const slice of data.slices)
77641
+ counts.set(slice.state, (counts.get(slice.state) ?? 0) + 1);
77642
+ return [
77643
+ `fanout ${data.name} on ${data.trunk}: ${[...counts].map(([state, count]) => `${count} ${state.replace("_", " ")}`).join(", ")} · forge ${data.forge.available ? "on" : "off"}`,
77644
+ ...data.slices.map((slice) => {
77645
+ const parts = [
77646
+ slice.branchExists ? short5(slice.head) : "no branch",
77647
+ slice.lastCommitAt && slice.idleMinutes !== null ? `idle ${slice.idleMinutes} min` : null,
77648
+ slice.pr ? `PR #${slice.pr.number} ${slice.pr.state}` : null,
77649
+ slice.ci === "unknown" ? null : `CI ${slice.ci}`,
77650
+ `verdict ${slice.verdict.accepted ? "accepted" : slice.verdict.basis === "none" ? "missing" : slice.verdict.basis}`
77651
+ ].filter(Boolean);
77652
+ const label = slice.state === "stuck" ? "STUCK" : slice.state.replace("_", " ");
77653
+ const detail = slice.landed ? `landed (${slice.landed.how}${slice.landed.pr ? ` #${slice.landed.pr}` : ""})` : parts.join(" · ");
77654
+ const why = slice.state === "landed" ? [] : [...slice.reasons, ...slice.notes];
77655
+ return ` ${label.padEnd(11)} ${slice.id} (${slice.branch}) ${detail}${why.length ? `
77656
+ ${why.join(`
77657
+ `)}` : ""}`;
77658
+ }),
77659
+ `landing order: ${data.landingOrder.length ? data.landingOrder.join(", ") : "(none ready)"}`,
77660
+ ...data.spawnable.length ? [`spawnable: ${data.spawnable.join(", ")}`] : [],
77661
+ `next: ${data.next}`,
77662
+ ...data.notes.map((note) => `note: ${note}`)
77663
+ ];
77664
+ };
76662
77665
  var init_fanout2 = __esm(() => {
76663
77666
  init_fanout_check();
77667
+ init_fanout_status();
77668
+ init_fanout_worktree();
76664
77669
  init_fanout();
76665
77670
  init_rev();
76666
77671
  init_ledger();
@@ -91815,7 +92820,7 @@ var init_ast = __esm(() => {
91815
92820
 
91816
92821
  // packages/workit-cli/src/test-audit/rules.ts
91817
92822
  import { builtinModules } from "node:module";
91818
- import path45 from "node:path";
92823
+ import path46 from "node:path";
91819
92824
  function testCallee(call) {
91820
92825
  let callee = call.callee;
91821
92826
  if (callee.type === "CallExpression")
@@ -92308,12 +93313,12 @@ function checkConstantsOnly(ctx, emit) {
92308
93313
  }
92309
93314
  function checkOverMocking(ctx, emit) {
92310
93315
  const { program } = ctx.parsed;
92311
- const unit = path45.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
93316
+ const unit = path46.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
92312
93317
  const mocked = find2(program, (node) => node.type === "CallExpression" && MODULE_MOCK.test(calleeName(node.callee) ?? "") && node.arguments[0]?.type === "StringLiteral");
92313
93318
  const internal = mocked.filter((call) => /^(?:\.|@\/|~\/)/.test(call.arguments[0].value));
92314
93319
  for (const call of mocked) {
92315
93320
  const spec = call.arguments[0].value;
92316
- const base = path45.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
93321
+ const base = path46.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
92317
93322
  if (base === unit)
92318
93323
  emit({
92319
93324
  rule: "over-mocking",
@@ -92701,11 +93706,11 @@ var init_rules = __esm(() => {
92701
93706
  });
92702
93707
 
92703
93708
  // packages/workit-cli/src/test-audit/audit.ts
92704
- import { spawnSync as spawnSync22 } from "node:child_process";
93709
+ import { spawnSync as spawnSync23 } from "node:child_process";
92705
93710
  import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync19, statSync as statSync7 } from "node:fs";
92706
- import path46 from "node:path";
93711
+ import path47 from "node:path";
92707
93712
  function git6(cwd, args) {
92708
- const run = spawnSync22("git", ["-c", "core.quotepath=off", ...args], {
93713
+ const run = spawnSync23("git", ["-c", "core.quotepath=off", ...args], {
92709
93714
  cwd,
92710
93715
  encoding: "utf8",
92711
93716
  maxBuffer: 64 * 1024 * 1024
@@ -92716,7 +93721,7 @@ function walkTests(dir, out) {
92716
93721
  for (const entry of readdirSync8(dir, { withFileTypes: true })) {
92717
93722
  if (entry.name.startsWith(".") && entry.name !== "." && entry.isDirectory())
92718
93723
  continue;
92719
- const full = path46.join(dir, entry.name);
93724
+ const full = path47.join(dir, entry.name);
92720
93725
  if (entry.isDirectory()) {
92721
93726
  if (!SKIP_DIRS.has(entry.name))
92722
93727
  walkTests(full, out);
@@ -92728,7 +93733,7 @@ function expandPaths(cwd, inputs) {
92728
93733
  const files = [];
92729
93734
  const missing = [];
92730
93735
  for (const input of inputs.length > 0 ? inputs : ["."]) {
92731
- const full = path46.resolve(cwd, input);
93736
+ const full = path47.resolve(cwd, input);
92732
93737
  if (!existsSync18(full))
92733
93738
  missing.push(input);
92734
93739
  else if (statSync7(full).isDirectory())
@@ -92765,11 +93770,11 @@ function changedLines(cwd, base) {
92765
93770
  for (let offset = 0;offset < count; offset += 1)
92766
93771
  lines.add(start + offset);
92767
93772
  }
92768
- changed.set(path46.join(root, file), lines);
93773
+ changed.set(path47.join(root, file), lines);
92769
93774
  }
92770
93775
  const untracked = git6(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
92771
93776
  for (const file of untracked.stdout.split("\x00").filter(Boolean))
92772
- changed.set(path46.join(root, file), "all");
93777
+ changed.set(path47.join(root, file), "all");
92773
93778
  return changed;
92774
93779
  }
92775
93780
  function auditFiles(files, display) {
@@ -92831,7 +93836,7 @@ function auditFiles(files, display) {
92831
93836
  durationMs: Math.round(performance.now() - started)
92832
93837
  };
92833
93838
  }
92834
- var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path46.sep).join("/"), SEVERITY_ORDER, ruleIds;
93839
+ var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path47.sep).join("/"), SEVERITY_ORDER, ruleIds;
92835
93840
  var init_audit = __esm(() => {
92836
93841
  init_ast();
92837
93842
  init_rules();
@@ -92857,11 +93862,11 @@ import {
92857
93862
  rmSync as rmSync7,
92858
93863
  statSync as statSync8,
92859
93864
  symlinkSync,
92860
- writeFileSync as writeFileSync9,
93865
+ writeFileSync as writeFileSync10,
92861
93866
  copyFileSync as copyFileSync6
92862
93867
  } from "node:fs";
92863
93868
  import os14 from "node:os";
92864
- import path47 from "node:path";
93869
+ import path48 from "node:path";
92865
93870
  function mutantsFor(source, file, lines) {
92866
93871
  const parsed = parseSource(source, file);
92867
93872
  const out = [];
@@ -92934,7 +93939,7 @@ function relatedTests(source, testFiles) {
92934
93939
  for (const match of text.matchAll(SPECIFIER)) {
92935
93940
  const spec = match[1];
92936
93941
  if (spec.startsWith(".")) {
92937
- if (stripExt(path47.posix.join(path47.posix.dirname(file), spec)) === target)
93942
+ if (stripExt(path48.posix.join(path48.posix.dirname(file), spec)) === target)
92938
93943
  return true;
92939
93944
  } else if (spec.includes("/") && stripExt(spec).split("/").slice(-2).join("/") === tail)
92940
93945
  return true;
@@ -92954,12 +93959,12 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
92954
93959
  for (const name of names) {
92955
93960
  if (!name.startsWith(TEMP_PREFIX))
92956
93961
  continue;
92957
- const dir = path47.join(tmp, name);
93962
+ const dir = path48.join(tmp, name);
92958
93963
  try {
92959
93964
  const stat = lstatSync7(dir);
92960
93965
  if (!stat.isDirectory() || uid !== undefined && stat.uid !== uid)
92961
93966
  continue;
92962
- const pid = Number(readFileSync20(path47.join(dir, PID_FILE), "utf8"));
93967
+ const pid = Number(readFileSync20(path48.join(dir, PID_FILE), "utf8"));
92963
93968
  if (Number.isInteger(pid) && pid > 0 && alive(pid))
92964
93969
  continue;
92965
93970
  } catch (error) {
@@ -92976,8 +93981,8 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
92976
93981
  function linkNodeModules(origin, target, repo, copy) {
92977
93982
  mkdirSync9(target, { recursive: true });
92978
93983
  for (const entry of readdirSync9(origin, { withFileTypes: true })) {
92979
- const from = path47.join(origin, entry.name);
92980
- const to = path47.join(target, entry.name);
93984
+ const from = path48.join(origin, entry.name);
93985
+ const to = path48.join(target, entry.name);
92981
93986
  if (entry.name.startsWith("@") && entry.isDirectory()) {
92982
93987
  linkNodeModules(from, to, repo, copy);
92983
93988
  continue;
@@ -92993,9 +93998,9 @@ function linkNodeModules(origin, target, repo, copy) {
92993
93998
  } catch {
92994
93999
  continue;
92995
94000
  }
92996
- const relative = path47.relative(repo, real);
92997
- if (!relative.startsWith("..") && !path47.isAbsolute(relative) && !relative.split(path47.sep).includes("node_modules")) {
92998
- symlinkSync(path47.join(copy, relative), to, "junction");
94001
+ const relative = path48.relative(repo, real);
94002
+ if (!relative.startsWith("..") && !path48.isAbsolute(relative) && !relative.split(path48.sep).includes("node_modules")) {
94003
+ symlinkSync(path48.join(copy, relative), to, "junction");
92999
94004
  continue;
93000
94005
  }
93001
94006
  }
@@ -93010,8 +94015,8 @@ function copySymlink(repo, tree, from, to) {
93010
94015
  real = null;
93011
94016
  }
93012
94017
  const realRepo = realpathSync9(repo);
93013
- if (real && inside2(realRepo, real)) {
93014
- symlinkSync(path47.relative(path47.dirname(to), path47.join(tree, path47.relative(realRepo, real))), to);
94018
+ if (real && inside3(realRepo, real)) {
94019
+ symlinkSync(path48.relative(path48.dirname(to), path48.join(tree, path48.relative(realRepo, real))), to);
93015
94020
  return;
93016
94021
  }
93017
94022
  if (real && statSync8(real).isFile())
@@ -93020,23 +94025,23 @@ function copySymlink(repo, tree, from, to) {
93020
94025
  symlinkSync(readlinkSync3(from), to);
93021
94026
  }
93022
94027
  function writeInside(copy, file, content) {
93023
- const target = path47.join(copy, file);
94028
+ const target = path48.join(copy, file);
93024
94029
  try {
93025
94030
  const stat = lstatSync7(target);
93026
- if (!stat.isFile() || !inside2(realpathSync9(copy), realpathSync9(path47.dirname(target))))
94031
+ if (!stat.isFile() || !inside3(realpathSync9(copy), realpathSync9(path48.dirname(target))))
93027
94032
  return false;
93028
94033
  } catch {
93029
94034
  return false;
93030
94035
  }
93031
94036
  const temp = `${target}.workit-mutant-${process.pid}`;
93032
- writeFileSync9(temp, content);
94037
+ writeFileSync10(temp, content);
93033
94038
  renameSync5(temp, target);
93034
94039
  return true;
93035
94040
  }
93036
94041
  function copyWorkingTree(repo) {
93037
- const dir = mkdtempSync4(path47.join(os14.tmpdir(), TEMP_PREFIX));
93038
- writeFileSync9(path47.join(dir, PID_FILE), String(process.pid));
93039
- const tree = path47.join(dir, "tree");
94042
+ const dir = mkdtempSync4(path48.join(os14.tmpdir(), TEMP_PREFIX));
94043
+ writeFileSync10(path48.join(dir, PID_FILE), String(process.pid));
94044
+ const tree = path48.join(dir, "tree");
93040
94045
  const head = git6(repo, ["rev-parse", "--verify", "--quiet", "HEAD"]).stdout.trim();
93041
94046
  if (head && git6(dir, ["clone", "-q", "--shared", "--no-checkout", repo, tree]).ok) {
93042
94047
  const branch = git6(repo, ["symbolic-ref", "--quiet", "--short", "HEAD"]).stdout.trim();
@@ -93053,29 +94058,29 @@ function copyWorkingTree(repo) {
93053
94058
  for (const relative of listed) {
93054
94059
  if (!relative)
93055
94060
  continue;
93056
- const from = path47.join(repo, relative);
94061
+ const from = path48.join(repo, relative);
93057
94062
  let stat;
93058
94063
  try {
93059
94064
  stat = lstatSync7(from);
93060
94065
  } catch {
93061
94066
  continue;
93062
94067
  }
93063
- const to = path47.join(tree, relative);
93064
- mkdirSync9(path47.dirname(to), { recursive: true });
94068
+ const to = path48.join(tree, relative);
94069
+ mkdirSync9(path48.dirname(to), { recursive: true });
93065
94070
  if (stat.isSymbolicLink())
93066
94071
  copySymlink(repo, tree, from, to);
93067
94072
  else if (stat.isFile())
93068
94073
  copyFileSync6(from, to);
93069
- let parent = path47.dirname(relative);
94074
+ let parent = path48.dirname(relative);
93070
94075
  while (parent !== "." && !dirs.has(parent)) {
93071
94076
  dirs.add(parent);
93072
- parent = path47.dirname(parent);
94077
+ parent = path48.dirname(parent);
93073
94078
  }
93074
94079
  }
93075
94080
  for (const parent of dirs) {
93076
- const modules = path47.join(repo, parent, "node_modules");
93077
- if (existsSync19(modules) && !existsSync19(path47.join(tree, parent, "node_modules")))
93078
- linkNodeModules(modules, path47.join(tree, parent, "node_modules"), repo, tree);
94081
+ const modules = path48.join(repo, parent, "node_modules");
94082
+ if (existsSync19(modules) && !existsSync19(path48.join(tree, parent, "node_modules")))
94083
+ linkNodeModules(modules, path48.join(tree, parent, "node_modules"), repo, tree);
93079
94084
  }
93080
94085
  return { dir, tree };
93081
94086
  }
@@ -93114,14 +94119,14 @@ async function runMutation(options) {
93114
94119
  const tracked = git6(repo, ["ls-files", "-co", "--exclude-standard", "-z"]).stdout.split("\x00").filter(Boolean);
93115
94120
  const testFiles = tracked.filter((file) => TEST_FILE.test(file)).map((file) => {
93116
94121
  try {
93117
- return { file, text: readFileSync20(path47.join(repo, file), "utf8") };
94122
+ return { file, text: readFileSync20(path48.join(repo, file), "utf8") };
93118
94123
  } catch {
93119
94124
  return { file, text: "" };
93120
94125
  }
93121
94126
  });
93122
94127
  const candidates = [];
93123
94128
  for (const [absolute, lines] of [...options.changed].toSorted(([a], [b]) => a.localeCompare(b))) {
93124
- const relative = toPosix(path47.relative(repo, absolute));
94129
+ const relative = toPosix(path48.relative(repo, absolute));
93125
94130
  if (!CODE_FILE.test(relative) || TEST_FILE.test(relative) || relative.endsWith(".d.ts"))
93126
94131
  continue;
93127
94132
  if (NOT_SOURCE.test(relative) || !existsSync19(absolute))
@@ -93135,9 +94140,9 @@ async function runMutation(options) {
93135
94140
  for (const mutant of mutants)
93136
94141
  if (!related.has(mutant.file))
93137
94142
  related.set(mutant.file, relatedTests(mutant.file, testFiles));
93138
- const cwdRelative = path47.relative(repo, options.cwd);
94143
+ const cwdRelative = path48.relative(repo, options.cwd);
93139
94144
  const asArg = (file) => {
93140
- const relative = toPosix(path47.relative(cwdRelative, file));
94145
+ const relative = toPosix(path48.relative(cwdRelative, file));
93141
94146
  return shellQuote(relative.startsWith(".") ? relative : `./${relative}`);
93142
94147
  };
93143
94148
  const commandFor = (tests) => options.command.replaceAll("{files}", () => tests.map(asArg).join(" "));
@@ -93172,7 +94177,7 @@ async function runMutation(options) {
93172
94177
  return finish2(options, 0, results, counts, started);
93173
94178
  removeStaleCopies();
93174
94179
  const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
93175
- const copyCwd = path47.join(copy, cwdRelative);
94180
+ const copyCwd = path48.join(copy, cwdRelative);
93176
94181
  let current = null;
93177
94182
  const restore = () => {
93178
94183
  if (current)
@@ -93207,7 +94212,7 @@ async function runMutation(options) {
93207
94212
  record({ ...mutant, status: "skipped", durationMs: 0, tests, reason: "budget" });
93208
94213
  continue;
93209
94214
  }
93210
- const original = readFileSync20(path47.join(copy, mutant.file), "utf8");
94215
+ const original = readFileSync20(path48.join(copy, mutant.file), "utf8");
93211
94216
  const mutated = original.slice(0, mutant.start) + mutant.replacement + original.slice(mutant.end);
93212
94217
  current = { file: mutant.file, original };
93213
94218
  if (!writeInside(copy, mutant.file, mutated)) {
@@ -93263,9 +94268,9 @@ var NOT_SOURCE, FLIPS, isStringy = (node) => node.type === "StringLiteral" || no
93263
94268
  } catch (error) {
93264
94269
  return error.code === "EPERM";
93265
94270
  }
93266
- }, inside2 = (root, file) => {
93267
- const relative = path47.relative(root, file);
93268
- return relative === "" || !relative.startsWith("..") && !path47.isAbsolute(relative);
94271
+ }, inside3 = (root, file) => {
94272
+ const relative = path48.relative(root, file);
94273
+ return relative === "" || !relative.startsWith("..") && !path48.isAbsolute(relative);
93269
94274
  }, active, killTree = (child) => {
93270
94275
  try {
93271
94276
  if (child.pid && process.platform !== "win32")
@@ -93314,7 +94319,7 @@ __export(exports_test_audit, {
93314
94319
  run: () => run17
93315
94320
  });
93316
94321
  import { existsSync as existsSync20 } from "node:fs";
93317
- import path48 from "node:path";
94322
+ import path49 from "node:path";
93318
94323
  function parse7(argv) {
93319
94324
  const options = {
93320
94325
  paths: [],
@@ -93414,8 +94419,8 @@ async function run17(argv, io) {
93414
94419
  return emit(io, fail("invalid_input", `${options}
93415
94420
  usage: ${USAGE10}`));
93416
94421
  const top = git6(io.cwd, ["rev-parse", "--show-toplevel"]);
93417
- const repo = top.ok ? path48.resolve(top.stdout.trim()) : null;
93418
- const display = (file) => toPosix(path48.relative(io.cwd, file) || file);
94422
+ const repo = top.ok ? path49.resolve(top.stdout.trim()) : null;
94423
+ const display = (file) => toPosix(path49.relative(io.cwd, file) || file);
93419
94424
  let base = null;
93420
94425
  let changed = null;
93421
94426
  if (options.diff || options.mutate) {
@@ -93658,7 +94663,7 @@ __export(exports_youtrack, {
93658
94663
  run: () => run19
93659
94664
  });
93660
94665
  import { readFileSync as readFileSync21 } from "node:fs";
93661
- import path49 from "node:path";
94666
+ import path50 from "node:path";
93662
94667
  async function run19(argv, io) {
93663
94668
  const [sub, ...rest] = argv;
93664
94669
  if (sub !== "note" && sub !== "time" && sub !== "meeting")
@@ -93704,7 +94709,7 @@ async function run19(argv, io) {
93704
94709
  let markdown = flags.values.markdown ?? "";
93705
94710
  if (flags.values.file)
93706
94711
  try {
93707
- markdown = readFileSync21(path49.resolve(io.cwd, flags.values.file), "utf8");
94712
+ markdown = readFileSync21(path50.resolve(io.cwd, flags.values.file), "utf8");
93708
94713
  } catch (error) {
93709
94714
  return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE12);
93710
94715
  }
@@ -93773,7 +94778,7 @@ __export(exports_changelog, {
93773
94778
  run: () => run20
93774
94779
  });
93775
94780
  import { readFileSync as readFileSync22 } from "node:fs";
93776
- import path50 from "node:path";
94781
+ import path51 from "node:path";
93777
94782
  async function run20(argv, io) {
93778
94783
  const [sub, ...rest] = argv;
93779
94784
  if (sub !== "apply")
@@ -93819,7 +94824,7 @@ async function run20(argv, io) {
93819
94824
  ]);
93820
94825
  }
93821
94826
  var USAGE13 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
93822
- const text = raw === "-" ? readFileSync22(0, "utf8") : raw.startsWith("@") ? readFileSync22(path50.resolve(cwd, raw.slice(1)), "utf8") : raw;
94827
+ const text = raw === "-" ? readFileSync22(0, "utf8") : raw.startsWith("@") ? readFileSync22(path51.resolve(cwd, raw.slice(1)), "utf8") : raw;
93823
94828
  return JSON.parse(text);
93824
94829
  };
93825
94830
  var init_changelog2 = __esm(() => {
@@ -93836,7 +94841,7 @@ __export(exports_handoff, {
93836
94841
  import { parseArgs as parseArgs2 } from "node:util";
93837
94842
  function human2(brief) {
93838
94843
  const lines = [
93839
- `branch: ${brief.branch ?? "(detached)"} @ ${short5(brief.head)}${brief.subject ? ` ${brief.subject}` : ""}`
94844
+ `branch: ${brief.branch ?? "(detached)"} @ ${short6(brief.head)}${brief.subject ? ` ${brief.subject}` : ""}`
93840
94845
  ];
93841
94846
  if (brief.base)
93842
94847
  lines.push(`base: ${brief.base}${brief.ahead === null ? "" : ` (ahead ${brief.ahead}, behind ${brief.behind})`}`);
@@ -93907,7 +94912,7 @@ async function run21(argv, io) {
93907
94912
  await Promise.resolve().then(() => init_task());
93908
94913
  return runTaskCommand(["handoff", ...argv]);
93909
94914
  }
93910
- var USAGE14 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short5 = (sha) => sha ? sha.slice(0, 12) : "no commit";
94915
+ var USAGE14 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short6 = (sha) => sha ? sha.slice(0, 12) : "no commit";
93911
94916
  var init_handoff = __esm(() => {
93912
94917
  init_ledger();
93913
94918
  init_output();
@@ -93916,7 +94921,7 @@ var init_handoff = __esm(() => {
93916
94921
  // packages/workit-cli/src/main.ts
93917
94922
  init_package();
93918
94923
  init_output();
93919
- import path51 from "node:path";
94924
+ import path52 from "node:path";
93920
94925
 
93921
94926
  // packages/workit-cli/src/verbs/registry.ts
93922
94927
  var TASK_FAMILY_NAMES = [
@@ -94031,8 +95036,8 @@ var VERBS = [
94031
95036
  {
94032
95037
  name: "fanout",
94033
95038
  group: "delivery",
94034
- usage: "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] | fanout check [<slice>…] [--name <n>] [--base <ref>] [--json]",
94035
- 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",
95039
+ usage: "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] | fanout check [<slice>…] [--name <n>] [--base <ref>] [--offline] | fanout status [--name <n>] [--stuck-after 30m] [--offline] | fanout worktree create|release <slice> [--force] [--json]",
95040
+ summary: "Register parallel slices (complete briefs, disjoint scopes, shared-file owners), watch them (STUCK, PR, CI, verdict, landed, landing order), make and release slice worktrees, and gate fan-in: out-of-scope edits, trunk and sibling merge conflicts",
94036
95041
  load: () => Promise.resolve().then(() => (init_fanout2(), exports_fanout))
94037
95042
  },
94038
95043
  {
@@ -94160,7 +95165,7 @@ var defaultIo = (json) => ({
94160
95165
  });
94161
95166
  async function main(argv, overrides = {}, options = {}) {
94162
95167
  const parsed = parseGlobals(argv);
94163
- const cwd = path51.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
95168
+ const cwd = path52.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
94164
95169
  const io = { ...defaultIo(parsed.json), ...overrides, json: parsed.json, cwd };
94165
95170
  if (parsed.error)
94166
95171
  return emit(io, fail("invalid_input", parsed.error));