@brainervirus/workit-claude-code 7.1.0 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/workit.js CHANGED
@@ -65,7 +65,7 @@ var package_default;
65
65
  var init_package = __esm(() => {
66
66
  package_default = {
67
67
  name: "@brainervirus/workit-cli",
68
- version: "7.1.0",
68
+ version: "7.3.0",
69
69
  private: false,
70
70
  description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
71
71
  keywords: [
@@ -103,9 +103,9 @@ var init_package = __esm(() => {
103
103
  },
104
104
  dependencies: {
105
105
  "@babel/parser": "7.29.9",
106
- "@brainervirus/workit-core": "^7.1.0",
107
- "@brainervirus/workit-cursor": "^7.1.0",
108
- "@brainervirus/workit-opencode": "^7.1.0",
106
+ "@brainervirus/workit-core": "^7.3.0",
107
+ "@brainervirus/workit-cursor": "^7.3.0",
108
+ "@brainervirus/workit-opencode": "^7.3.0",
109
109
  "@inkjs/ui": "2.0.0",
110
110
  ink: "7.1.1",
111
111
  react: "19.2.8"
@@ -15473,7 +15473,7 @@ var init_workspaces = __esm(() => {
15473
15473
  account: nonBlank.optional()
15474
15474
  }).passthrough().optional(),
15475
15475
  autoApprove: union([boolean2(), array(nonBlank)]).optional(),
15476
- defaultEndpoint: _enum(["commit", "pr"]).optional(),
15476
+ defaultEndpoint: unknown().optional(),
15477
15477
  verification: _enum(["self", "independent"]).optional(),
15478
15478
  autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
15479
15479
  kind,
@@ -16457,6 +16457,46 @@ var init_safe_write = () => {};
16457
16457
  import { copyFileSync as copyFileSync3, existsSync as existsSync6 } from "node:fs";
16458
16458
  import os3 from "node:os";
16459
16459
  import path18 from "node:path";
16460
+ function endpointOf(entry) {
16461
+ const raw = entry?.defaultEndpoint;
16462
+ if (raw === undefined)
16463
+ return { raw, endpoint: DEFAULT_ENDPOINT };
16464
+ if (isDefaultEndpoint(raw))
16465
+ return { raw, endpoint: raw };
16466
+ return {
16467
+ raw,
16468
+ endpoint: DEFAULT_ENDPOINT,
16469
+ issue: `defaultEndpoint ${JSON.stringify(raw)} is not one of ${DEFAULT_ENDPOINTS.join(", ")}; using ${DEFAULT_ENDPOINT}`
16470
+ };
16471
+ }
16472
+ function effectiveEndpoint(endpoint, context) {
16473
+ if (endpoint === "commit")
16474
+ return { endpoint };
16475
+ const missing = (kind) => `${kind} grant missing; ${grantHint(context.workspace, kind)}`;
16476
+ if (context.grants.push === false)
16477
+ return { endpoint: "commit", reason: missing("push") };
16478
+ if (context.grants.pr === false)
16479
+ return { endpoint: "commit", reason: missing("pr") };
16480
+ if (context.configured.length > 0 && !context.accountConfigured)
16481
+ return {
16482
+ endpoint: "commit",
16483
+ reason: `vcs.account missing; set vcs.account for workspace "${context.workspace ?? "?"}" in ~/.config/workit/workspaces.json`
16484
+ };
16485
+ if (endpoint === "merged" && context.grants.merge === false)
16486
+ return { endpoint: "green", reason: missing("merge") };
16487
+ return { endpoint };
16488
+ }
16489
+ function endpointView(entry, context) {
16490
+ const { raw, endpoint, issue } = endpointOf(entry);
16491
+ const effective = effectiveEndpoint(endpoint, context);
16492
+ return {
16493
+ defaultEndpoint: endpoint,
16494
+ configuredEndpoint: raw ?? null,
16495
+ effectiveEndpoint: effective.endpoint,
16496
+ ...effective.reason ? { endpointReason: effective.reason } : {},
16497
+ ...issue ? { endpointIssue: issue } : {}
16498
+ };
16499
+ }
16460
16500
  function configuredGrants(entry) {
16461
16501
  const record = entry ?? {};
16462
16502
  const raw = record.autonomy;
@@ -16482,18 +16522,28 @@ function resolveAutonomy(cwd) {
16482
16522
  source: "default",
16483
16523
  accountConfigured: false,
16484
16524
  defaultEndpoint: DEFAULT_ENDPOINT,
16525
+ configuredEndpoint: null,
16526
+ effectiveEndpoint: DEFAULT_ENDPOINT,
16485
16527
  verification: DEFAULT_VERIFICATION,
16486
16528
  note: override
16487
16529
  };
16488
16530
  const workspace = resolveWorkspaceFrom(cwd, grantsDir(), process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined);
16489
16531
  const { grants, source } = configuredGrants(workspace);
16532
+ const full = { ...DEFAULT_GRANTS, ...grants };
16533
+ const configured = GRANT_KINDS.filter((kind) => grants[kind] !== undefined);
16534
+ const accountConfigured = Boolean(workspace?.vcs?.account);
16490
16535
  return {
16491
16536
  workspace: workspace?.name ?? null,
16492
- grants: { ...DEFAULT_GRANTS, ...grants },
16493
- configured: GRANT_KINDS.filter((kind) => grants[kind] !== undefined),
16537
+ grants: full,
16538
+ configured,
16494
16539
  source,
16495
- accountConfigured: Boolean(workspace?.vcs?.account),
16496
- defaultEndpoint: endpointOf(workspace),
16540
+ accountConfigured,
16541
+ ...endpointView(workspace, {
16542
+ workspace: workspace?.name ?? null,
16543
+ grants: full,
16544
+ configured,
16545
+ accountConfigured
16546
+ }),
16497
16547
  verification: verificationOf(workspace)
16498
16548
  };
16499
16549
  }
@@ -16599,14 +16649,21 @@ function writeGrants(workspaceName, changes, dir = grantsDir(), endpoint, verifi
16599
16649
  error: `failed to write ${current.path}: ${error instanceof Error ? error.message : String(error)}`
16600
16650
  };
16601
16651
  }
16652
+ const full = { ...DEFAULT_GRANTS, ...grants };
16653
+ const configured = GRANT_KINDS.filter((kind) => grants[kind] !== undefined);
16602
16654
  return {
16603
16655
  ok: true,
16604
16656
  path: current.path,
16605
16657
  backup,
16606
16658
  workspace: workspaceName,
16607
- grants: { ...DEFAULT_GRANTS, ...grants },
16608
- configured: GRANT_KINDS.filter((kind) => grants[kind] !== undefined),
16609
- defaultEndpoint: endpointOf(entry),
16659
+ grants: full,
16660
+ configured,
16661
+ ...endpointView(entry, {
16662
+ workspace: workspaceName,
16663
+ grants: full,
16664
+ configured,
16665
+ accountConfigured: Boolean(accountOf(entry))
16666
+ }),
16610
16667
  verification: verificationOf(entry)
16611
16668
  };
16612
16669
  }
@@ -16618,12 +16675,19 @@ function listGrants(dir = grantsDir()) {
16618
16675
  path: current.path,
16619
16676
  workspaces: current.entries.map((entry) => {
16620
16677
  const { grants } = configuredGrants(entry);
16678
+ const full = { ...DEFAULT_GRANTS, ...grants };
16679
+ const configured = GRANT_KINDS.filter((kind) => grants[kind] !== undefined);
16621
16680
  return {
16622
16681
  name: entry.name,
16623
16682
  glob: entry.glob,
16624
- grants: { ...DEFAULT_GRANTS, ...grants },
16625
- configured: GRANT_KINDS.filter((kind) => grants[kind] !== undefined),
16626
- defaultEndpoint: endpointOf(entry),
16683
+ grants: full,
16684
+ configured,
16685
+ ...endpointView(entry, {
16686
+ workspace: entry.name,
16687
+ grants: full,
16688
+ configured,
16689
+ accountConfigured: Boolean(entry.vcs?.account)
16690
+ }),
16627
16691
  verification: verificationOf(entry)
16628
16692
  };
16629
16693
  })
@@ -16638,7 +16702,7 @@ var accountHome = () => {
16638
16702
  }, grantsHome, grantsDir = () => path18.join(grantsHome.resolve(), ".config", "workit"), grantsOverride = () => {
16639
16703
  const configured = path18.resolve(resolveConfigDir());
16640
16704
  return configured === path18.resolve(grantsDir()) ? null : `the config directory is redirected to ${configured} (WORKFLOW_TOOLKIT_CONFIG, WORKFLOW_TOOLKIT_CONFIG_DIR or XDG_CONFIG_HOME); grants are read only from ${path18.join(grantsDir(), "workspaces.json")}, so the D4 defaults apply`;
16641
- }, GRANT_KINDS, DEFAULT_GRANTS, DEFAULT_ENDPOINT = "commit", DEFAULT_VERIFICATION = "self", verificationOf = (entry) => entry?.verification === "independent" ? "independent" : "self", endpointOf = (entry) => entry?.defaultEndpoint === "pr" ? "pr" : "commit", isGrantKind = (value) => typeof value === "string" && GRANT_KINDS.includes(value), isGrantValue = (value) => value === true || value === false || value === "verified", normalized = (kind, value) => value === "verified" && kind !== "merge" ? true : value, fromAutoApprove = (value) => {
16705
+ }, GRANT_KINDS, DEFAULT_GRANTS, DEFAULT_ENDPOINTS, DEFAULT_ENDPOINT = "commit", isDefaultEndpoint = (value) => typeof value === "string" && DEFAULT_ENDPOINTS.includes(value), raisesEndpoint = (current, next) => DEFAULT_ENDPOINTS.indexOf(next) > DEFAULT_ENDPOINTS.indexOf(current), DEFAULT_VERIFICATION = "self", verificationOf = (entry) => entry?.verification === "independent" ? "independent" : "self", isGrantKind = (value) => typeof value === "string" && GRANT_KINDS.includes(value), isGrantValue = (value) => value === true || value === false || value === "verified", normalized = (kind, value) => value === "verified" && kind !== "merge" ? true : value, fromAutoApprove = (value) => {
16642
16706
  if (value === true)
16643
16707
  return { push: true, pr: true, merge: "verified" };
16644
16708
  if (!Array.isArray(value))
@@ -16650,7 +16714,7 @@ var accountHome = () => {
16650
16714
  else if (item === "merge")
16651
16715
  grants.merge = "verified";
16652
16716
  return Object.keys(grants).length > 0 ? grants : null;
16653
- }, suggested = (kind) => kind === "merge" ? "verified" : "true", grantHint = (workspace, kind) => `ask the user to run, in their own terminal: workit grant set ${workspace ?? "<workspace>"} ${kind}=${suggested(kind)}`, raises = (kind, current, next) => {
16717
+ }, accountOf = (entry) => entry?.vcs?.account, suggested = (kind) => kind === "merge" ? "verified" : "true", grantHint = (workspace, kind) => `ask the user to run, in their own terminal: workit grant set ${workspace ?? "<workspace>"} ${kind}=${suggested(kind)}`, raises = (kind, current, next) => {
16654
16718
  const rank = (value) => value === true ? 2 : value === "verified" ? 1 : 0;
16655
16719
  return rank(normalized(kind, next)) > rank(normalized(kind, current));
16656
16720
  };
@@ -16667,6 +16731,7 @@ var init_autonomy = __esm(() => {
16667
16731
  release: false,
16668
16732
  rerun: true
16669
16733
  });
16734
+ DEFAULT_ENDPOINTS = ["commit", "pr", "green", "merged"];
16670
16735
  });
16671
16736
 
16672
16737
  // packages/workit-core/src/core/task-engine.ts
@@ -70508,7 +70573,7 @@ function show(argv, io) {
70508
70573
  return emit(io, fail("not_found", `no workspace named "${named[0]}" in ${listed.path}`));
70509
70574
  return emit(io, ok({ path: listed.path, defaults: DEFAULT_GRANTS, workspaces: rows }), () => rows.flatMap((entry) => [
70510
70575
  `${entry.name} (${entry.glob})`,
70511
- ...describe2(entry.grants, entry.configured, entry.defaultEndpoint, entry.verification)
70576
+ ...describe2(entry.grants, entry.configured, entry, entry.verification)
70512
70577
  ]));
70513
70578
  }
70514
70579
  let autonomy;
@@ -70522,7 +70587,7 @@ function show(argv, io) {
70522
70587
  return emit(io, ok(shown), (data) => [
70523
70588
  ...data.note ? [`note: ${data.note}`] : [],
70524
70589
  data.workspace ? `workspace ${data.workspace} (${data.source === "default" ? "D4 defaults" : `from ${data.source}`})` : "no workspace matches this checkout: D4 defaults apply",
70525
- ...describe2(data.grants, data.configured, data.defaultEndpoint, data.verification),
70590
+ ...describe2(data.grants, data.configured, data, data.verification),
70526
70591
  ...describeReleaseTracks(tracks).map((line) => ` ${line}`),
70527
70592
  "raising a grant needs the user: workit grant set <workspace> <kind>=<value>"
70528
70593
  ]);
@@ -70545,10 +70610,10 @@ async function change(verb, argv, io, deps) {
70545
70610
  if (kind === "defaultEndpoint") {
70546
70611
  if (verb === "unset")
70547
70612
  endpoint = null;
70548
- else if (raw === "commit" || raw === "pr")
70613
+ else if (isDefaultEndpoint(raw))
70549
70614
  endpoint = raw;
70550
70615
  else
70551
- return usage2(io, `${spec}: defaultEndpoint must be commit or pr`);
70616
+ return usage2(io, `${spec}: defaultEndpoint must be ${DEFAULT_ENDPOINTS.join(", ")}`);
70552
70617
  continue;
70553
70618
  }
70554
70619
  if (kind === "verification") {
@@ -70572,8 +70637,8 @@ async function change(verb, argv, io, deps) {
70572
70637
  changes.push({ kind, value });
70573
70638
  }
70574
70639
  const raised = changes.filter(({ kind, value }) => raises(kind, entry.grants[kind], value ?? DEFAULT_GRANTS[kind])).map(({ kind, value }) => ({ kind, value: String(value ?? DEFAULT_GRANTS[kind]) }));
70575
- if (endpoint === "pr" && entry.defaultEndpoint !== "pr")
70576
- raised.push({ kind: "defaultEndpoint", value: "pr" });
70640
+ if (endpoint && raisesEndpoint(entry.defaultEndpoint, endpoint))
70641
+ raised.push({ kind: "defaultEndpoint", value: endpoint });
70577
70642
  if (verification !== undefined && verification !== "independent" && entry.verification === "independent")
70578
70643
  raised.push({ kind: "verification", value: "self" });
70579
70644
  if (raised.length > 0) {
@@ -70592,7 +70657,7 @@ async function change(verb, argv, io, deps) {
70592
70657
  return emit(io, fail(written.code, written.error));
70593
70658
  return emit(io, ok(written), (data) => [
70594
70659
  `${data.workspace}: grants updated in ${data.path}${data.backup ? ` (previous copy: ${data.backup})` : ""}`,
70595
- ...describe2(data.grants, data.configured, data.defaultEndpoint, data.verification)
70660
+ ...describe2(data.grants, data.configured, data, data.verification)
70596
70661
  ]);
70597
70662
  }
70598
70663
  async function runGrant(argv, io, deps = defaultDeps) {
@@ -70610,9 +70675,10 @@ async function runGrant(argv, io, deps = defaultDeps) {
70610
70675
  }
70611
70676
  return usage2(io, `unknown subcommand "${sub}"`);
70612
70677
  }
70613
- var USAGE2 = "workit grant show [<workspace>] [--all] | grant set <workspace> <kind>=<true|false|verified>… [defaultEndpoint=commit|pr] [verification=self|independent] | grant unset <workspace> <kind>… (kinds: push, pr, merge, release, rerun, defaultEndpoint, verification)", AGENT_ENV, agentMarker = (env) => AGENT_ENV.find((name) => env[name]) ?? Object.keys(env).find((name) => name.startsWith("CODEX_") && env[name]), defaultDeps, parseValue = (raw) => raw === "true" || raw === "yes" || raw === "on" ? true : raw === "false" || raw === "no" || raw === "off" ? false : raw === "verified" ? "verified" : null, describe2 = (grants, configured, endpoint, verification) => [
70678
+ var USAGE2 = "workit grant show [<workspace>] [--all] | grant set <workspace> <kind>=<true|false|verified>… [defaultEndpoint=commit|pr|green|merged] [verification=self|independent] | grant unset <workspace> <kind>… (kinds: push, pr, merge, release, rerun, defaultEndpoint, verification)", AGENT_ENV, agentMarker = (env) => AGENT_ENV.find((name) => env[name]) ?? Object.keys(env).find((name) => name.startsWith("CODEX_") && env[name]), defaultDeps, parseValue = (raw) => raw === "true" || raw === "yes" || raw === "on" ? true : raw === "false" || raw === "no" || raw === "off" ? false : raw === "verified" ? "verified" : null, STOPS_AT, endpointLabel = (view) => view.effectiveEndpoint === view.defaultEndpoint ? view.defaultEndpoint : `${view.defaultEndpoint} (effective: ${view.effectiveEndpoint}${view.endpointReason ? `, ${view.endpointReason}` : ""})`, describe2 = (grants, configured, endpoint, verification) => [
70614
70679
  ...GRANT_KINDS.map((kind) => ` ${kind.padEnd(8)}${String(grants[kind]).padEnd(10)}${configured.includes(kind) ? "configured" : "default"}${kind === "release" ? " (no consumer yet: reserved, not enforced)" : ""}`),
70615
- ` default endpoint: ${endpoint} (an unnamed request stops at ${endpoint === "pr" ? "an opened PR" : "a local commit"})`,
70680
+ ` default endpoint: ${endpointLabel(endpoint)} (an unnamed request stops at ${STOPS_AT[endpoint.effectiveEndpoint]})`,
70681
+ ...endpoint.endpointIssue ? [` note: ${endpoint.endpointIssue}`] : [],
70616
70682
  ` verification: ${verification} (${verification === "independent" ? "a normal-risk behavior change needs a non-author verdict" : "a normal-risk behavior change needs a passing check and the author's --self verdict, shown as self-reviewed"})`
70617
70683
  ], usage2 = (io, message) => emit(io, fail("invalid_input", `${message}
70618
70684
  usage: ${USAGE2}`)), run8 = (argv, io) => runGrant(argv, io);
@@ -70633,6 +70699,12 @@ var init_grant = __esm(() => {
70633
70699
  interactive: () => process.stdin.isTTY === true && process.stdout.isTTY === true,
70634
70700
  ask: (question) => askLine(question)
70635
70701
  };
70702
+ STOPS_AT = {
70703
+ commit: "a local commit",
70704
+ pr: "an opened PR",
70705
+ green: "a merge-ready PR: CI green, threads resolved, verified; never merged",
70706
+ merged: "a merged PR (workit pr merge)"
70707
+ };
70636
70708
  });
70637
70709
 
70638
70710
  // packages/workit-cli/src/verbs/check.ts
@@ -71494,6 +71566,32 @@ function blockersOf(input) {
71494
71566
  }
71495
71567
  return out;
71496
71568
  }
71569
+ function babysitAction(next, reason, draft) {
71570
+ switch (next) {
71571
+ case "MERGED":
71572
+ return "merged";
71573
+ case "CLOSED":
71574
+ return null;
71575
+ case "RESOLVE_CONFLICTS":
71576
+ case "REBASE":
71577
+ return "update-branch";
71578
+ case "RESOLVE_THREADS":
71579
+ case "ADDRESS_REVIEW":
71580
+ return "address-threads";
71581
+ case "FIX_CI":
71582
+ return "fix-ci";
71583
+ case "WAITING_CI":
71584
+ return "wait";
71585
+ case "IN_MERGE_QUEUE":
71586
+ return "wait-forge";
71587
+ default:
71588
+ if (draft)
71589
+ return "mark-ready";
71590
+ if (next === "NOT_MERGEABLE" && reason === "mergeability_unknown")
71591
+ return "wait-forge";
71592
+ return "ready";
71593
+ }
71594
+ }
71497
71595
  function computeBehindBase(cwd, remotes, baseSha, headSha_, options = {}) {
71498
71596
  const empty = { behind: null, ahead: null, upToDate: null };
71499
71597
  if (!baseSha)
@@ -71618,7 +71716,8 @@ function buildStatusDoc(cwd, resolved, status, options) {
71618
71716
  blockers: status.state === "open" ? blockers.map((blocker) => blocker.reason) : [],
71619
71717
  identity: options.identity ?? null,
71620
71718
  truncated: status.truncated,
71621
- next: blockers[0]?.next ?? "READY"
71719
+ next: blockers[0]?.next ?? "READY",
71720
+ babysit: babysitAction(blockers[0]?.next ?? "READY", blockers[0]?.reason ?? null, status.draft || blockers.some((blocker) => blocker.next === "MARK_READY"))
71622
71721
  });
71623
71722
  }
71624
71723
  function prStatusReport(cwd, resolved, options = {}) {
@@ -72918,6 +73017,20 @@ function parseDuration2(value) {
72918
73017
  const factor = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60000 : 3600000;
72919
73018
  return Math.round(Number(match[1]) * factor);
72920
73019
  }
73020
+ function releaseTrunk(io, track, branch) {
73021
+ const resolved = vcsConfig("resolve", io.cwd, {
73022
+ track,
73023
+ ...branch ? { branch } : {}
73024
+ });
73025
+ if (resolved.ok === false)
73026
+ return track !== null || String(resolved.configPath ?? "").endsWith("workspaces.json") ? { error: String(resolved.error) } : { trunk: null, tracked: false };
73027
+ if (resolved.releaseTrack?.blocking)
73028
+ return { error: String(resolved.releaseTrack.blocking) };
73029
+ return {
73030
+ trunk: String(resolved.defaultTargetBranch ?? "") || null,
73031
+ tracked: Boolean(resolved.releaseTrack)
73032
+ };
73033
+ }
72921
73034
  function connect(io, branch) {
72922
73035
  const resolved = resolveForge(io.cwd, {
72923
73036
  branch,
@@ -72977,12 +73090,15 @@ function renderStatus(doc) {
72977
73090
  lines.push(`blockers: ${doc.blockers.join(", ")}`);
72978
73091
  if (doc.identity?.note)
72979
73092
  lines.push(`identity: ${doc.identity.note}`);
73093
+ if (doc.babysit)
73094
+ lines.push(`babysit: ${doc.babysit}`);
72980
73095
  lines.push(`next: ${doc.next}`);
72981
73096
  return lines;
72982
73097
  }
72983
73098
  var forgeDeps, usage3 = (io, message, line) => emit(io, fail("invalid_input", message, { unblock: line })), forgeFail = (io, result, data) => emit(io, fail(result.code, result.error, { data, unblock: result.unblock })), short = (sha) => sha ? sha.slice(0, 7) : "?";
72984
73099
  var init_forge_common = __esm(() => {
72985
73100
  init_resolve();
73101
+ init_vcs_config();
72986
73102
  init_output();
72987
73103
  forgeDeps = {
72988
73104
  runner: undefined,
@@ -75106,7 +75222,7 @@ async function plan(argv, io) {
75106
75222
  return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE);
75107
75223
  let trunk = flags.values.trunk ?? existing?.trunk ?? null;
75108
75224
  if (trunk === null || track !== null) {
75109
- const derived = defaultTrunk(io, track, flags.positionals[0] ?? null);
75225
+ const derived = releaseTrunk(io, track, flags.positionals[0] ?? null);
75110
75226
  if ("error" in derived)
75111
75227
  return emit(io, fail("blocked", derived.error, {
75112
75228
  unblock: "workit stack plan --track <name> … # or --trunk <branch>"
@@ -75274,17 +75390,7 @@ async function run14(argv, io) {
75274
75390
  return land(rest, io);
75275
75391
  return usage3(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE7);
75276
75392
  }
75277
- var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--timeout 20m] [--interval 30s] [--json]", USAGE7 = "workit stack plan|status|sync|land ... (workit help stack)", short4 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), defaultTrunk = (io, track, branch) => {
75278
- const resolved = vcsConfig("resolve", io.cwd, {
75279
- track,
75280
- ...branch ? { branch } : {}
75281
- });
75282
- if (resolved.ok === false)
75283
- return track !== null || String(resolved.configPath ?? "").endsWith("workspaces.json") ? { error: String(resolved.error) } : { trunk: null };
75284
- if (resolved.releaseTrack?.blocking)
75285
- return { error: String(resolved.releaseTrack.blocking) };
75286
- return { trunk: String(resolved.defaultTargetBranch ?? "") || null };
75287
- }, label = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
75393
+ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--timeout 20m] [--interval 30s] [--json]", USAGE7 = "workit stack plan|status|sync|land ... (workit help stack)", short4 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), label = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
75288
75394
  `${data.dryRun ? "would sync" : "synced"} stack ${data.name}${data.merged.length ? `; merged: ${data.merged.map((item) => item.branch).join(", ")}` : ""}`,
75289
75395
  ...data.steps.map((step) => [
75290
75396
  ` ${step.branch} on ${step.parent}:`,
@@ -75296,16 +75402,993 @@ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<b
75296
75402
  var init_stack2 = __esm(() => {
75297
75403
  init_ledger();
75298
75404
  init_stack();
75299
- init_vcs_config();
75300
75405
  init_output();
75301
75406
  init_forge_common();
75302
75407
  METHODS3 = new Set(["squash", "merge", "rebase"]);
75303
75408
  });
75304
75409
 
75410
+ // packages/workit-core/src/fanout.ts
75411
+ import { spawnSync as spawnSync20 } from "node:child_process";
75412
+ import fs22 from "node:fs";
75413
+ import path42 from "node:path";
75414
+ function scopeSamples(pattern) {
75415
+ if (!hasWildcard(pattern))
75416
+ return [unescape2(pattern)];
75417
+ let out = "";
75418
+ for (let index = 0;index < pattern.length; index += 1) {
75419
+ const char = pattern[index];
75420
+ if (char === "\\" && index + 1 < pattern.length) {
75421
+ out += pattern[index + 1];
75422
+ index += 1;
75423
+ } else if (char === "*" && pattern[index + 1] === "*") {
75424
+ const atSegmentStart = index === 0 || pattern[index - 1] === "/";
75425
+ if (atSegmentStart && pattern[index + 2] === "/")
75426
+ index += 2;
75427
+ else {
75428
+ out += "<any>";
75429
+ index += 1;
75430
+ }
75431
+ } else if (char === "*")
75432
+ out += "<any>";
75433
+ else if (char === "?")
75434
+ out += "x";
75435
+ else if (char === "{" && pattern.indexOf("}", index) > index) {
75436
+ const close = pattern.indexOf("}", index);
75437
+ out += pattern.slice(index + 1, close).split(",")[0];
75438
+ index = close;
75439
+ } else
75440
+ out += char;
75441
+ }
75442
+ return [out];
75443
+ }
75444
+ function globSource(pattern) {
75445
+ let out = "";
75446
+ for (let index = 0;index < pattern.length; index += 1) {
75447
+ const char = pattern[index];
75448
+ if (char === "\\" && index + 1 < pattern.length) {
75449
+ out += escapeRegExp4(pattern[index + 1]);
75450
+ index += 1;
75451
+ } else if (char === "*" && pattern[index + 1] === "*") {
75452
+ const atSegmentStart = index === 0 || pattern[index - 1] === "/";
75453
+ if (atSegmentStart && pattern[index + 2] === "/") {
75454
+ out += "(?:[^/]*/)*";
75455
+ index += 2;
75456
+ } else {
75457
+ out += ".*";
75458
+ index += 1;
75459
+ }
75460
+ } else if (char === "*")
75461
+ out += "[^/]*";
75462
+ else if (char === "?")
75463
+ out += "[^/]";
75464
+ else if (char === "{" && pattern.indexOf("}", index) > index) {
75465
+ const close = pattern.indexOf("}", index);
75466
+ const alternatives = pattern.slice(index + 1, close).split(",").map(globSource);
75467
+ out += `(?:${alternatives.join("|")})`;
75468
+ index = close;
75469
+ } else
75470
+ out += escapeRegExp4(char);
75471
+ }
75472
+ return out;
75473
+ }
75474
+ function matchesScope(pattern, file) {
75475
+ let regex = compiled.get(pattern);
75476
+ if (!regex) {
75477
+ regex = new RegExp(`^${globSource(pattern)}$`, "u");
75478
+ compiled.set(pattern, regex);
75479
+ }
75480
+ return regex.test(file) || !hasWildcard(pattern) && file.startsWith(`${unescape2(pattern)}/`);
75481
+ }
75482
+ function normalizePattern(raw) {
75483
+ let pattern = raw.trim().replace(/\\(?![[\]{}*?])/gu, "/");
75484
+ while (pattern.startsWith("./"))
75485
+ pattern = pattern.slice(2);
75486
+ if (pattern === "." || pattern === "")
75487
+ pattern = raw.trim() ? "**" : "";
75488
+ if (pattern.endsWith("/"))
75489
+ pattern = `${pattern}**`;
75490
+ if (!pattern)
75491
+ return { error: "is empty" };
75492
+ if (pattern.startsWith("/") || /^[A-Za-z]:/u.test(pattern))
75493
+ return { error: "must be relative to the repository root" };
75494
+ if (pattern.split("/").includes(".."))
75495
+ return { error: "must not contain .." };
75496
+ const bare = pattern.replace(/\\./gu, "");
75497
+ if (/[[\]]/u.test(bare))
75498
+ return {
75499
+ error: 'has an unescaped [ or ]: write \\[ and \\] for literal brackets (app/\\[id\\]/page.tsx; in JSON "app/\\\\[id\\\\]/page.tsx")'
75500
+ };
75501
+ if (bare.startsWith("!"))
75502
+ return { error: "uses ! (negation is not supported)" };
75503
+ let depth = 0;
75504
+ for (const char of bare) {
75505
+ if (char === "{")
75506
+ depth += 1;
75507
+ if (char === "}")
75508
+ depth -= 1;
75509
+ if (depth < 0 || depth > 1)
75510
+ return { error: "has unbalanced or nested braces" };
75511
+ }
75512
+ if (depth !== 0)
75513
+ return { error: "has unbalanced or nested braces" };
75514
+ return { pattern };
75515
+ }
75516
+ function inScope(plan, slice, file) {
75517
+ if (matchesAny(slice.owns, file))
75518
+ return true;
75519
+ if (!matchesAny(slice.scope, file))
75520
+ return false;
75521
+ return !plan.slices.some((other) => other.id !== slice.id && matchesAny(other.owns, file));
75522
+ }
75523
+ function parsePlan(raw, defaults, name) {
75524
+ const problems = [];
75525
+ if (!isRecord8(raw))
75526
+ return { ok: false, problems: ["the plan must be a JSON object"] };
75527
+ const planName = name ?? (typeof raw.name === "string" ? raw.name.trim() : "");
75528
+ if (!planName || planName.length > 200 || /\p{Cc}/u.test(planName))
75529
+ problems.push('name: pass --name or set a 1-200 character "name"');
75530
+ const trunk = typeof raw.trunk === "string" && raw.trunk.trim() ? raw.trunk.trim() : defaults.trunk;
75531
+ const shared = [];
75532
+ for (const entry of textList(raw.shared ?? []) ?? ["<not a list>"]) {
75533
+ const normalized = normalizePattern(entry);
75534
+ if ("error" in normalized)
75535
+ problems.push(`shared "${entry}" ${normalized.error}`);
75536
+ else
75537
+ shared.push(normalized.pattern);
75538
+ }
75539
+ if (!Array.isArray(raw.slices) || raw.slices.length === 0)
75540
+ return { ok: false, problems: [...problems, "slices: list at least one slice"] };
75541
+ const slices = [];
75542
+ const seenIds = new Set;
75543
+ const seenBranches = new Map;
75544
+ raw.slices.forEach((item, index) => {
75545
+ const where = isRecord8(item) && typeof item.id === "string" ? `slice ${item.id}` : `slice #${index + 1}`;
75546
+ if (!isRecord8(item)) {
75547
+ problems.push(`${where}: must be an object`);
75548
+ return;
75549
+ }
75550
+ const id = typeof item.id === "string" ? item.id : "";
75551
+ if (!ID.test(id))
75552
+ problems.push(`${where}: id must match ${ID.source}`);
75553
+ else if (seenIds.has(id))
75554
+ problems.push(`${where}: duplicate id`);
75555
+ seenIds.add(id);
75556
+ const branch = typeof item.branch === "string" ? item.branch.trim() : "";
75557
+ if (!branch)
75558
+ problems.push(`${where}: branch is empty`);
75559
+ else if (seenBranches.has(branch))
75560
+ problems.push(`${where}: branch ${branch} is also slice ${seenBranches.get(branch)}'s`);
75561
+ else
75562
+ seenBranches.set(branch, id);
75563
+ if (branch === trunk)
75564
+ problems.push(`${where}: branch must not be the trunk ${trunk}`);
75565
+ const goal = typeof item.goal === "string" ? item.goal.trim() : "";
75566
+ if (!filled(goal))
75567
+ problems.push(`${where}: goal is empty`);
75568
+ const brief = (field) => {
75569
+ const list = textList(item[field]);
75570
+ const kept = (list ?? []).map((entry) => entry.trim()).filter(filled);
75571
+ if (kept.length === 0)
75572
+ problems.push(`${where}: ${field} is empty`);
75573
+ return kept;
75574
+ };
75575
+ const acceptance = brief("acceptance");
75576
+ const verify = brief("verify");
75577
+ const forbidden = brief("forbidden");
75578
+ const scope = [];
75579
+ const owns = [];
75580
+ for (const [field, out] of [
75581
+ ["scope", scope],
75582
+ ["owns", owns]
75583
+ ]) {
75584
+ const list = textList(item[field] ?? []);
75585
+ if (list === null) {
75586
+ problems.push(`${where}: ${field} must be a list of globs`);
75587
+ continue;
75588
+ }
75589
+ for (const entry of list.filter(filled)) {
75590
+ const normalized = normalizePattern(entry);
75591
+ if ("error" in normalized)
75592
+ problems.push(`${where}: ${field} "${entry}" ${normalized.error}`);
75593
+ else
75594
+ out.push(normalized.pattern);
75595
+ }
75596
+ }
75597
+ if (scope.length === 0 && textList(item.scope ?? []) !== null)
75598
+ problems.push(`${where}: scope is empty`);
75599
+ const tier = item.tier;
75600
+ if (typeof tier !== "string" || !TIERS.includes(tier))
75601
+ problems.push(`${where}: tier must be one of ${TIERS.join(", ")}`);
75602
+ const dependsOn = textList(item.dependsOn ?? []);
75603
+ if (dependsOn === null)
75604
+ problems.push(`${where}: dependsOn must be a list of slice ids`);
75605
+ const optional = (field) => typeof item[field] === "string" && filled(item[field]) ? item[field].trim() : null;
75606
+ slices.push({
75607
+ id,
75608
+ branch,
75609
+ base: optional("base") ?? "",
75610
+ worktree: optional("worktree") ?? path42.join(defaults.worktreeRoot, id),
75611
+ tier,
75612
+ dependsOn: [...new Set(dependsOn ?? [])],
75613
+ scope,
75614
+ owns,
75615
+ goal,
75616
+ acceptance,
75617
+ verify,
75618
+ forbidden,
75619
+ context: optional("context"),
75620
+ timebox: optional("timebox")
75621
+ });
75622
+ });
75623
+ const byId = new Map(slices.map((slice) => [slice.id, slice]));
75624
+ const byBranch = new Map(slices.map((slice) => [slice.branch, slice]));
75625
+ for (const slice of slices) {
75626
+ for (const dep of slice.dependsOn) {
75627
+ if (dep === slice.id)
75628
+ problems.push(`slice ${slice.id}: depends on itself`);
75629
+ else if (!byId.has(dep))
75630
+ problems.push(`slice ${slice.id}: depends on unknown slice ${dep}`);
75631
+ }
75632
+ const explicitBase = slice.base;
75633
+ if (explicitBase) {
75634
+ const parent = byBranch.get(explicitBase);
75635
+ if (parent && !slice.dependsOn.includes(parent.id))
75636
+ problems.push(`slice ${slice.id}: base ${explicitBase} is slice ${parent.id}'s branch; add "${parent.id}" to its dependsOn`);
75637
+ } else {
75638
+ const single = slice.dependsOn.length === 1 ? byId.get(slice.dependsOn[0]) : undefined;
75639
+ slice.base = single ? single.branch : trunk;
75640
+ }
75641
+ }
75642
+ const cycle = findCycle(slices);
75643
+ if (cycle)
75644
+ problems.push(`dependency cycle: ${cycle.join(" -> ")}`);
75645
+ if (problems.length)
75646
+ return { ok: false, problems };
75647
+ return {
75648
+ ok: true,
75649
+ plan: {
75650
+ v: FANOUT_VERSION,
75651
+ name: planName,
75652
+ trunk,
75653
+ shared,
75654
+ slices,
75655
+ createdAt: "",
75656
+ updatedAt: ""
75657
+ }
75658
+ };
75659
+ }
75660
+ function findCycle(slices) {
75661
+ const byId = new Map(slices.map((slice) => [slice.id, slice]));
75662
+ const state = new Map;
75663
+ const trail = [];
75664
+ const visit = (id) => {
75665
+ if (state.get(id) === "done")
75666
+ return null;
75667
+ if (state.get(id) === "open")
75668
+ return [...trail.slice(trail.indexOf(id)), id];
75669
+ state.set(id, "open");
75670
+ trail.push(id);
75671
+ for (const dep of byId.get(id)?.dependsOn ?? []) {
75672
+ if (!byId.has(dep) || dep === id)
75673
+ continue;
75674
+ const found = visit(dep);
75675
+ if (found)
75676
+ return found;
75677
+ }
75678
+ trail.pop();
75679
+ state.set(id, "done");
75680
+ return null;
75681
+ };
75682
+ for (const slice of slices) {
75683
+ const found = visit(slice.id);
75684
+ if (found)
75685
+ return found;
75686
+ }
75687
+ return null;
75688
+ }
75689
+ function landingOrder(slices) {
75690
+ const order = [];
75691
+ const placed = new Set;
75692
+ const known = new Set(slices.map((slice) => slice.id));
75693
+ while (order.length < slices.length) {
75694
+ const next = slices.find((slice) => !placed.has(slice.id) && slice.dependsOn.every((dep) => placed.has(dep) || !known.has(dep)));
75695
+ if (!next)
75696
+ break;
75697
+ order.push(next.id);
75698
+ placed.add(next.id);
75699
+ }
75700
+ return order;
75701
+ }
75702
+ function waves(slices) {
75703
+ const level = new Map;
75704
+ const byId = new Map(slices.map((slice) => [slice.id, slice]));
75705
+ for (const id of landingOrder(slices)) {
75706
+ const deps = byId.get(id)?.dependsOn ?? [];
75707
+ level.set(id, deps.length ? Math.max(...deps.map((dep) => level.get(dep) ?? 0)) + 1 : 0);
75708
+ }
75709
+ const out = [];
75710
+ for (const slice of slices)
75711
+ (out[level.get(slice.id) ?? 0] ??= []).push(slice.id);
75712
+ return out.filter(Boolean);
75713
+ }
75714
+ function ancestors(slices) {
75715
+ const byId = new Map(slices.map((slice) => [slice.id, slice]));
75716
+ const out = new Map;
75717
+ for (const id of landingOrder(slices)) {
75718
+ const set = new Set;
75719
+ for (const dep of byId.get(id)?.dependsOn ?? []) {
75720
+ set.add(dep);
75721
+ for (const deeper of out.get(dep) ?? [])
75722
+ set.add(deeper);
75723
+ }
75724
+ out.set(id, set);
75725
+ }
75726
+ return out;
75727
+ }
75728
+ function analyzeOverlap(plan, candidates) {
75729
+ const shared = [...DEFAULT_SHARED, ...plan.shared];
75730
+ const before = ancestors(plan.slices);
75731
+ const related = (a, b) => Boolean(before.get(a)?.has(b) || before.get(b)?.has(a));
75732
+ const overlaps = [];
75733
+ const groups = new Map;
75734
+ const byCase = new Map;
75735
+ for (const file of [...new Set(candidates)].toSorted())
75736
+ (byCase.get(file.toLowerCase()) ?? byCase.set(file.toLowerCase(), []).get(file.toLowerCase()))?.push(file);
75737
+ for (const spellings of byCase.values()) {
75738
+ const file = spellings.join(" ~ ");
75739
+ const any = (patterns) => spellings.some((spelling) => matchesAny(patterns, spelling));
75740
+ const matching = plan.slices.filter((slice) => any(slice.scope) || any(slice.owns));
75741
+ if (matching.length < 2)
75742
+ continue;
75743
+ const ids = matching.map((slice) => slice.id);
75744
+ const owners = matching.filter((slice) => any(slice.owns));
75745
+ if (owners.length === 1) {
75746
+ overlaps.push({ path: file, slices: ids, resolution: "owner", owner: owners[0].id });
75747
+ continue;
75748
+ }
75749
+ if (owners.length === 0 && matching.every((a, i) => matching.slice(i + 1).every((b) => related(a.id, b.id)))) {
75750
+ overlaps.push({ path: file, slices: ids, resolution: "serialized" });
75751
+ continue;
75752
+ }
75753
+ const members = owners.length > 1 ? owners : matching;
75754
+ const reason = owners.length > 1 ? "several_owners" : "overlap";
75755
+ const isShared = any(shared);
75756
+ const key = `${reason}|${isShared}|${members.map((slice) => slice.id).join(",")}`;
75757
+ const group = groups.get(key);
75758
+ if (group)
75759
+ group.paths.push(file);
75760
+ else
75761
+ groups.set(key, {
75762
+ slices: members.map((slice) => slice.id),
75763
+ paths: [file],
75764
+ shared: isShared,
75765
+ reason,
75766
+ members,
75767
+ suggestion: ownerSuggestion(members[0].id, [])
75768
+ });
75769
+ }
75770
+ const conflicts = [...groups.values()].map(({ members, ...group }) => {
75771
+ const literal = members.find((slice) => slice.scope.some((pattern) => group.paths.includes(pattern)));
75772
+ const owner = ownerSuggestion((literal ?? members[0]).id, group.paths);
75773
+ if (group.reason === "several_owners")
75774
+ return {
75775
+ ...group,
75776
+ suggestion: {
75777
+ ...owner,
75778
+ text: `keep ${quoteList(group.paths)} in "owns" of slice ${owner.slice} only`
75779
+ }
75780
+ };
75781
+ const serialize = serializeSuggestion(group.slices);
75782
+ return group.shared ? { ...group, suggestion: owner, alternative: serialize } : { ...group, suggestion: serialize, alternative: owner };
75783
+ });
75784
+ return { overlaps, conflicts };
75785
+ }
75786
+ function branchRef(cwd, branch) {
75787
+ if (!branch || branch.startsWith("-"))
75788
+ return null;
75789
+ const local = resolveRef(cwd, `refs/heads/${branch}`);
75790
+ if (local)
75791
+ return local;
75792
+ const remote = pushRemoteName(cwd) ?? "origin";
75793
+ return resolveRef(cwd, `refs/remotes/${remote}/${branch}`);
75794
+ }
75795
+ function trunkRef(cwd, trunk) {
75796
+ if (!trunk || trunk.startsWith("-"))
75797
+ return null;
75798
+ const remote = pushRemoteName(cwd) ?? "origin";
75799
+ for (const ref of [`refs/remotes/${remote}/${trunk}`, `refs/heads/${trunk}`, trunk])
75800
+ if (resolveRef(cwd, ref))
75801
+ return ref;
75802
+ return null;
75803
+ }
75804
+ function defaultTrunk(cwd) {
75805
+ const base = defaultBase(cwd);
75806
+ return base ? base.replace(/^origin\//u, "") : "main";
75807
+ }
75808
+ function worktreeRoot(cwd) {
75809
+ const common = gitCommonDir(cwd);
75810
+ const main = common && path42.basename(common) === ".git" ? path42.dirname(common) : cwd;
75811
+ return path42.join(path42.dirname(main), `${path42.basename(main)}-wt`);
75812
+ }
75813
+ function fanoutsDir(cwd) {
75814
+ const root = storeRoot(cwd);
75815
+ if (!root.ok)
75816
+ return fanoutFail(root.code, root.error, root.unblock);
75817
+ return { ok: true, data: path42.join(root.value.root, "fanouts") };
75818
+ }
75819
+ function parseStored(raw) {
75820
+ if (!isRecord8(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
75821
+ return null;
75822
+ if (!Array.isArray(raw.slices))
75823
+ return null;
75824
+ const list = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
75825
+ const text = (value) => typeof value === "string" ? value : "";
75826
+ let globsValid = true;
75827
+ const globs = (value) => list(value).map((entry) => {
75828
+ const normalized = normalizePattern(entry);
75829
+ if ("error" in normalized) {
75830
+ globsValid = false;
75831
+ return entry;
75832
+ }
75833
+ return normalized.pattern;
75834
+ });
75835
+ const slices = [];
75836
+ for (const item of raw.slices) {
75837
+ if (!isRecord8(item) || typeof item.id !== "string" || typeof item.branch !== "string")
75838
+ return null;
75839
+ slices.push({
75840
+ id: item.id,
75841
+ branch: item.branch,
75842
+ base: text(item.base) || raw.trunk,
75843
+ worktree: text(item.worktree),
75844
+ tier: TIERS.includes(item.tier) ? item.tier : "standard",
75845
+ dependsOn: list(item.dependsOn),
75846
+ scope: globs(item.scope),
75847
+ owns: globs(item.owns),
75848
+ goal: text(item.goal),
75849
+ acceptance: list(item.acceptance),
75850
+ verify: list(item.verify),
75851
+ forbidden: list(item.forbidden),
75852
+ context: typeof item.context === "string" ? item.context : null,
75853
+ timebox: typeof item.timebox === "string" ? item.timebox : null
75854
+ });
75855
+ }
75856
+ const shared = globs(raw.shared);
75857
+ const ids = new Set(slices.map((slice) => slice.id));
75858
+ if (!globsValid || ids.size !== slices.length || slices.some((slice) => slice.dependsOn.some((dep) => dep === slice.id || !ids.has(dep))) || findCycle(slices))
75859
+ return null;
75860
+ return {
75861
+ v: typeof raw.v === "number" ? raw.v : FANOUT_VERSION,
75862
+ name: raw.name,
75863
+ trunk: raw.trunk,
75864
+ shared,
75865
+ slices,
75866
+ createdAt: text(raw.createdAt),
75867
+ updatedAt: text(raw.updatedAt)
75868
+ };
75869
+ }
75870
+ function readFanout(cwd, name) {
75871
+ const dir = fanoutsDir(cwd);
75872
+ if (!dir.ok)
75873
+ return dir;
75874
+ const file = path42.join(dir.data, stackFileName(name));
75875
+ let parsed = null;
75876
+ try {
75877
+ parsed = parseStored(JSON.parse(fs22.readFileSync(file, "utf8")));
75878
+ } catch (error) {
75879
+ if (error.code === "ENOENT")
75880
+ return { ok: true, data: null };
75881
+ parsed = null;
75882
+ }
75883
+ return parsed ? { ok: true, data: parsed } : fanoutFail("failed", `fanout file ${file} is not a valid plan`, `workit fanout plan <plan.json> --name ${name} # rewrite it`);
75884
+ }
75885
+ function listFanouts(cwd) {
75886
+ const dir = fanoutsDir(cwd);
75887
+ if (!dir.ok)
75888
+ return dir;
75889
+ let names = [];
75890
+ try {
75891
+ names = fs22.readdirSync(dir.data).filter((entry) => entry.endsWith(".json"));
75892
+ } catch {
75893
+ return { ok: true, data: [] };
75894
+ }
75895
+ const out = [];
75896
+ for (const entry of names) {
75897
+ let parsed = null;
75898
+ try {
75899
+ parsed = parseStored(JSON.parse(fs22.readFileSync(path42.join(dir.data, entry), "utf8")));
75900
+ } catch {
75901
+ parsed = null;
75902
+ }
75903
+ if (parsed && stackFileName(parsed.name) === entry)
75904
+ out.push(parsed);
75905
+ }
75906
+ return { ok: true, data: out.toSorted((a, b) => a.name.localeCompare(b.name)) };
75907
+ }
75908
+ function selectFanout(cwd, name, current) {
75909
+ if (name) {
75910
+ const read = readFanout(cwd, name);
75911
+ if (!read.ok)
75912
+ return read;
75913
+ return read.data ? { ok: true, data: read.data } : fanoutFail("not_found", `no fanout plan named ${name}`, "workit fanout plan <plan.json>");
75914
+ }
75915
+ const all = listFanouts(cwd);
75916
+ if (!all.ok)
75917
+ return all;
75918
+ const holding = all.data.filter((plan) => plan.slices.some((slice) => slice.branch === current));
75919
+ if (holding.length === 1)
75920
+ return { ok: true, data: holding[0] };
75921
+ if (all.data.length === 1)
75922
+ return { ok: true, data: all.data[0] };
75923
+ return all.data.length === 0 ? fanoutFail("not_found", "no fanout plan is recorded here", "workit fanout plan <plan.json>") : fanoutFail("invalid_input", `several fanout plans match; pick one (${all.data.map((plan) => plan.name).join(", ")})`, "pass --name <plan>");
75924
+ }
75925
+ function writeFanout(cwd, plan) {
75926
+ const dir = fanoutsDir(cwd);
75927
+ if (!dir.ok)
75928
+ return dir;
75929
+ const file = path42.join(dir.data, stackFileName(plan.name));
75930
+ const temp = `${file}.${process.pid}.tmp`;
75931
+ try {
75932
+ fs22.mkdirSync(dir.data, { recursive: true });
75933
+ fs22.writeFileSync(temp, `${JSON.stringify(plan, null, 2)}
75934
+ `);
75935
+ fs22.renameSync(temp, file);
75936
+ return { ok: true, data: file };
75937
+ } catch (error) {
75938
+ fs22.rmSync(temp, { force: true });
75939
+ return fanoutFail("unavailable", `cannot write ${file}: ${error.message}`);
75940
+ }
75941
+ }
75942
+ function planFanout(cwd, input, now = new Date) {
75943
+ const dir = fanoutsDir(cwd);
75944
+ if (!dir.ok)
75945
+ return dir;
75946
+ const parsed = parsePlan(input.trunk && isRecord8(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, { trunk: input.trunk ?? defaultTrunk(cwd), worktreeRoot: worktreeRoot(cwd) }, input.name);
75947
+ if (!parsed.ok)
75948
+ return fanoutFail("invalid_input", `the plan is incomplete: ${parsed.problems[0]}${parsed.problems.length > 1 ? ` (+${parsed.problems.length - 1} more)` : ""}`, "fill every listed field, then run workit fanout plan again", { problems: parsed.problems });
75949
+ const plan = parsed.plan;
75950
+ const badBranches = plan.slices.filter((slice) => !gitRun4(cwd, ["check-ref-format", "--branch", slice.branch]).ok).map((slice) => `slice ${slice.id}: ${slice.branch} is not a valid branch name`);
75951
+ if (badBranches.length)
75952
+ return fanoutFail("invalid_input", badBranches[0], "rename the branch in the plan", {
75953
+ problems: badBranches
75954
+ });
75955
+ const ref = trunkRef(cwd, plan.trunk);
75956
+ const tracked = ref ? nulList2(gitRun4(cwd, ["ls-tree", "-r", "-z", "--name-only", ref]).stdout) : [];
75957
+ const samples = plan.slices.flatMap((slice) => [...slice.scope, ...slice.owns].flatMap(scopeSamples));
75958
+ const { overlaps, conflicts } = analyzeOverlap(plan, [...tracked, ...samples]);
75959
+ if (conflicts.length)
75960
+ return fanoutFail("blocked", `${conflicts.length} file-scope overlap${conflicts.length === 1 ? "" : "s"} between slices: ${conflicts[0].slices.join(" and ")} on ${conflicts[0].paths[0]}${conflicts[0].paths.length > 1 ? ` (+${conflicts[0].paths.length - 1})` : ""}`, conflicts[0].suggestion.text, { name: plan.name, conflicts, overlaps });
75961
+ const existing = readFanout(cwd, plan.name);
75962
+ const previous = existing.ok ? existing.data : null;
75963
+ plan.createdAt = previous?.createdAt || now.toISOString();
75964
+ plan.updatedAt = now.toISOString();
75965
+ const written = writeFanout(cwd, plan);
75966
+ if (!written.ok)
75967
+ return written;
75968
+ appendObserved(cwd, {
75969
+ type: "fanout.planned",
75970
+ actor: input.actor,
75971
+ fanout: plan.name,
75972
+ trunk: plan.trunk,
75973
+ slices: plan.slices.length,
75974
+ ids: plan.slices.slice(0, 20).map((slice) => slice.id)
75975
+ });
75976
+ const grouped = waves(plan.slices);
75977
+ const notes = [];
75978
+ if (!ref)
75979
+ notes.push(`trunk ${plan.trunk} does not resolve here; only sample paths of each glob were compared`);
75980
+ grouped.forEach((wave, index) => {
75981
+ if (wave.length > IN_FLIGHT_CAP)
75982
+ notes.push(`wave ${index + 1} has ${wave.length} slices; keep 4-${IN_FLIGHT_CAP} in flight and queue the rest`);
75983
+ });
75984
+ for (const slice of plan.slices)
75985
+ if (slice.dependsOn.length > 1 && slice.base === plan.trunk)
75986
+ notes.push(`slice ${slice.id} depends on ${slice.dependsOn.join(", ")}: start it from ${plan.trunk} after they land`);
75987
+ return {
75988
+ ok: true,
75989
+ data: {
75990
+ name: plan.name,
75991
+ trunk: plan.trunk,
75992
+ file: written.data,
75993
+ created: previous === null,
75994
+ slices: plan.slices.map(({ id, branch, base, worktree, tier, dependsOn, scope, owns }) => ({
75995
+ id,
75996
+ branch,
75997
+ base,
75998
+ worktree,
75999
+ tier,
76000
+ dependsOn,
76001
+ scope,
76002
+ owns
76003
+ })),
76004
+ overlaps,
76005
+ waves: grouped,
76006
+ landingOrder: landingOrder(plan.slices),
76007
+ notes
76008
+ }
76009
+ };
76010
+ }
76011
+ var FANOUT_VERSION = 1, TIERS, fanoutFail = (code, error, unblock, data) => ({
76012
+ ok: false,
76013
+ code,
76014
+ error,
76015
+ ...unblock ? { unblock } : {},
76016
+ ...data ? { data } : {}
76017
+ }), DEFAULT_SHARED, escapeRegExp4 = (text) => text.replace(/[.+^$()|[\]\\{}*?]/gu, "\\$&"), unescape2 = (pattern) => pattern.replace(/\\(.)/gu, "$1"), hasWildcard = (pattern) => /[*?{]/u.test(pattern.replace(/\\./gu, "")), compiled, matchesAny = (patterns, file) => patterns.some((pattern) => matchesScope(pattern, file)), isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), PLACEHOLDER2, filled = (text) => text.trim() !== "" && !PLACEHOLDER2.test(text.trim()), textList = (value) => {
76018
+ if (typeof value === "string")
76019
+ return [value];
76020
+ if (Array.isArray(value) && value.every((item) => typeof item === "string"))
76021
+ return value;
76022
+ return null;
76023
+ }, ID, quoteList = (items) => items.map((item) => `"${item}"`).join(", "), ownerSuggestion = (slice, paths) => ({
76024
+ kind: "owner",
76025
+ slice,
76026
+ paths,
76027
+ text: `add ${quoteList(paths)} to "owns" of slice ${slice}`
76028
+ }), serializeSuggestion = (ids) => {
76029
+ const edges = ids.slice(1).map((slice, index) => ({ slice, dependsOn: ids[index] }));
76030
+ return {
76031
+ kind: "serialize",
76032
+ edges,
76033
+ text: edges.map((edge) => `add "${edge.dependsOn}" to "dependsOn" of slice ${edge.slice}`).join("; ")
76034
+ };
76035
+ }, gitRun4 = (cwd, args) => {
76036
+ const run = spawnSync20("git", args, {
76037
+ cwd,
76038
+ encoding: "utf8",
76039
+ timeout: GIT_TIMEOUTS.local,
76040
+ killSignal: "SIGKILL",
76041
+ windowsHide: true,
76042
+ maxBuffer: 64 * 1024 * 1024,
76043
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", LC_ALL: "C", LANGUAGE: "C" }
76044
+ });
76045
+ return {
76046
+ ok: run.status === 0,
76047
+ status: run.status,
76048
+ stdout: run.stdout ?? "",
76049
+ stderr: run.error ? String(run.error.message) : run.stderr ?? ""
76050
+ };
76051
+ }, nulList2 = (text) => text.split("\x00").filter(Boolean), IN_FLIGHT_CAP = 6;
76052
+ var init_fanout = __esm(() => {
76053
+ init_rev();
76054
+ init_ledger();
76055
+ init_stack();
76056
+ TIERS = ["mundane", "standard", "hard"];
76057
+ DEFAULT_SHARED = [
76058
+ "**/package.json",
76059
+ "**/{package-lock.json,npm-shrinkwrap.json,bun.lock,bun.lockb,pnpm-lock.yaml,pnpm-workspace.yaml,yarn.lock}",
76060
+ "**/{Cargo.lock,go.mod,go.sum,poetry.lock,uv.lock,Gemfile.lock,composer.lock}",
76061
+ "**/index.{ts,tsx,js,jsx,mjs,cjs}",
76062
+ "**/tsconfig.json",
76063
+ ".github/**",
76064
+ ".gitlab-ci.yml",
76065
+ "CHANGELOG.md"
76066
+ ];
76067
+ compiled = new Map;
76068
+ PLACEHOLDER2 = /^(?:<[^>]*>|tbd|todo|fixme|\?+|\.\.\.|…)$/iu;
76069
+ ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
76070
+ });
76071
+
76072
+ // packages/workit-core/src/fanout-check.ts
76073
+ function mergeConflicts(cwd, a, b) {
76074
+ const run = gitRun4(cwd, [
76075
+ "merge-tree",
76076
+ "--write-tree",
76077
+ "--name-only",
76078
+ "--no-messages",
76079
+ "-z",
76080
+ a,
76081
+ b
76082
+ ]);
76083
+ if (run.status === 0)
76084
+ return [];
76085
+ if (run.status === 1)
76086
+ return [...new Set(nulList2(run.stdout).slice(1))];
76087
+ const first = run.stderr.trim().split(`
76088
+ `)[0] || `git merge-tree exited ${run.status}`;
76089
+ return {
76090
+ error: /write-tree|usage: git merge-tree/u.test(run.stderr) ? `${first} (needs git >= 2.38 for merge-tree --write-tree)` : first
76091
+ };
76092
+ }
76093
+ function checkFanout(cwd, plan, options = {}) {
76094
+ const unknown = (options.only ?? []).filter((id) => !plan.slices.some((s) => s.id === id));
76095
+ if (unknown.length)
76096
+ return fanoutFail("invalid_input", `${unknown.join(", ")}: not a slice of fanout ${plan.name}`, `slices: ${plan.slices.map((slice) => slice.id).join(", ")}`);
76097
+ const trunk = options.trunkRef ?? trunkRef(cwd, plan.trunk);
76098
+ if (!SAFE(trunk) || !resolves(cwd, trunk))
76099
+ return fanoutFail("unavailable", `trunk ${options.trunkRef ?? plan.trunk} does not resolve here`, `git fetch, or pass --base <ref>`);
76100
+ const order = landingOrder(plan.slices);
76101
+ const byId = new Map(plan.slices.map((slice) => [slice.id, slice]));
76102
+ const tips = new Map(plan.slices.map((slice) => [slice.id, branchRef(cwd, slice.branch)]));
76103
+ const ledger = readLedger(cwd);
76104
+ const rows = ledger.ok ? ledger.value.rows : [];
76105
+ const checks = new Map;
76106
+ const changedBy = new Map;
76107
+ for (const slice of plan.slices)
76108
+ checks.set(slice.id, inspect(cwd, plan, slice, trunk, tips, rows, changedBy));
76109
+ const conflicts = [];
76110
+ for (let i = 0;i < order.length; i += 1)
76111
+ for (let j = i + 1;j < order.length; j += 1) {
76112
+ const first = order[i];
76113
+ const second = order[j];
76114
+ const a = tips.get(first);
76115
+ const b = tips.get(second);
76116
+ if (!a || !b || isAncestor2(cwd, a, b) || isAncestor2(cwd, b, a))
76117
+ continue;
76118
+ const check = checks.get(second);
76119
+ const theirs = new Map((changedBy.get(first) ?? []).map((file) => [file.toLowerCase(), file]));
76120
+ const cased = (changedBy.get(second) ?? []).flatMap((file) => {
76121
+ const other = theirs.get(file.toLowerCase());
76122
+ return other !== undefined && other !== file ? [`${other} ~ ${file}`] : [];
76123
+ });
76124
+ if (cased.length) {
76125
+ conflicts.push({
76126
+ first,
76127
+ second,
76128
+ paths: cased,
76129
+ caseOnly: true,
76130
+ resolution: `use ${first}'s spelling on ${second} (git mv), then workit fanout check`
76131
+ });
76132
+ check.siblingConflicts.push({ with: first, paths: cased });
76133
+ check.reasons.push(`differs only in case from ${first}: ${cased.join(", ")}`);
76134
+ }
76135
+ const result = mergeConflicts(cwd, a, b);
76136
+ if (!Array.isArray(result)) {
76137
+ check.reasons.push(`cannot merge with ${first}: ${result.error}`);
76138
+ continue;
76139
+ }
76140
+ if (result.length === 0)
76141
+ continue;
76142
+ const related = byId.get(second)?.dependsOn.includes(first);
76143
+ conflicts.push({
76144
+ first,
76145
+ second,
76146
+ paths: result,
76147
+ resolution: related ? `rebase ${second} onto ${byId.get(first)?.branch} (its dependency), then workit fanout check` : `land ${first} first, then rebase ${second} onto ${plan.trunk} and run workit fanout check; or make ${second} depend on ${first}`
76148
+ });
76149
+ check.siblingConflicts.push({ with: first, paths: result });
76150
+ check.reasons.push(`conflicts with ${first} on ${result.join(", ")}`);
76151
+ }
76152
+ for (const id of order) {
76153
+ const check = checks.get(id);
76154
+ check.waitsFor = (byId.get(id)?.dependsOn ?? []).filter((dep) => checks.get(dep)?.status !== "ready");
76155
+ if (check.waitsFor.length)
76156
+ check.reasons.push(`waits for ${check.waitsFor.join(", ")}`);
76157
+ check.status = check.reasons.length ? "blocked" : "ready";
76158
+ }
76159
+ const selected = options.only?.length ? new Set(options.only) : null;
76160
+ const slices = order.filter((id) => !selected || selected.has(id)).map((id) => checks.get(id));
76161
+ const ready = slices.every((check) => check.status === "ready");
76162
+ const blocked = slices.find((check) => check.status === "blocked");
76163
+ const notes = plan.v === FANOUT_VERSION ? [] : [
76164
+ `plan file version ${plan.v} is not ${FANOUT_VERSION}; read leniently (re-plan to rewrite it)`
76165
+ ];
76166
+ const next = blocked ? unblockFor(blocked, plan) : `land in this order: ${slices.map((check) => check.id).join(", ")} (workit pr merge each once verified; stacked slices with workit stack land)`;
76167
+ return {
76168
+ ok: true,
76169
+ data: {
76170
+ name: plan.name,
76171
+ trunk: plan.trunk,
76172
+ trunkRef: trunk,
76173
+ ready,
76174
+ slices,
76175
+ conflicts: conflicts.filter((conflict) => !selected || selected.has(conflict.first) || selected.has(conflict.second)),
76176
+ landingOrder: order,
76177
+ notes,
76178
+ next
76179
+ }
76180
+ };
76181
+ }
76182
+ function inspect(cwd, plan, slice, trunk, tips, rows, changedBy) {
76183
+ const head = tips.get(slice.id) ?? null;
76184
+ const check = {
76185
+ id: slice.id,
76186
+ branch: slice.branch,
76187
+ head,
76188
+ base: slice.base,
76189
+ status: "blocked",
76190
+ changed: 0,
76191
+ outOfScope: [],
76192
+ trunkConflicts: [],
76193
+ siblingConflicts: [],
76194
+ waitsFor: [],
76195
+ verdict: { accepted: false, basis: "missing" },
76196
+ reasons: []
76197
+ };
76198
+ if (!head) {
76199
+ check.reasons.push(`branch ${slice.branch} not found (not started, landed, or not fetched)`);
76200
+ return check;
76201
+ }
76202
+ const parent = plan.slices.find((other) => other.branch === slice.base);
76203
+ const baseRef = slice.base === plan.trunk ? trunk : parent ? tips.get(parent.id) ?? null : branchRef(cwd, slice.base);
76204
+ if (!SAFE(baseRef)) {
76205
+ check.reasons.push(`base ${slice.base} not found`);
76206
+ return check;
76207
+ }
76208
+ const diff = gitRun4(cwd, ["diff", "--name-only", "--no-renames", "-z", `${baseRef}...${head}`]);
76209
+ if (!diff.ok) {
76210
+ check.reasons.push(`cannot diff ${slice.base}...${slice.branch}: ${diff.stderr.trim()}`);
76211
+ return check;
76212
+ }
76213
+ const changed = nulList2(diff.stdout);
76214
+ changedBy.set(slice.id, changed);
76215
+ check.changed = changed.length;
76216
+ if (changed.length === 0)
76217
+ check.reasons.push(`no changes beyond ${slice.base}`);
76218
+ check.outOfScope = changed.filter((file) => !inScope(plan, slice, file)).toSorted();
76219
+ if (check.outOfScope.length)
76220
+ check.reasons.push(`edits outside its scope: ${check.outOfScope.join(", ")}`);
76221
+ const trunkResult = mergeConflicts(cwd, trunk, head);
76222
+ if (Array.isArray(trunkResult)) {
76223
+ check.trunkConflicts = trunkResult;
76224
+ if (trunkResult.length)
76225
+ check.reasons.push(`conflicts with ${plan.trunk} on ${trunkResult.join(", ")}`);
76226
+ } else
76227
+ check.reasons.push(`cannot merge with ${plan.trunk}: ${trunkResult.error}`);
76228
+ const verdict = checkVerdicts(cwd, slice.branch, rows);
76229
+ check.verdict = { accepted: verdict.accepted.accepted, basis: verdict.current.basis };
76230
+ return check;
76231
+ }
76232
+ function unblockFor(check, plan) {
76233
+ if (!check.head)
76234
+ return `start or fetch ${check.branch}, then workit fanout check; if it landed, re-plan without slice ${check.id}`;
76235
+ if (check.outOfScope.length)
76236
+ return `move ${check.outOfScope.join(", ")} off ${check.branch} (a follow-up slice), or widen slice ${check.id}'s scope with workit fanout plan, then workit fanout check`;
76237
+ if (check.trunkConflicts.length)
76238
+ return `rebase ${check.branch} onto ${plan.trunk} and resolve ${check.trunkConflicts.join(", ")}, then workit fanout check`;
76239
+ if (check.siblingConflicts.length) {
76240
+ const first = check.siblingConflicts[0].with;
76241
+ return `land ${first} first, then rebase ${check.branch} onto ${plan.trunk} and run workit fanout check`;
76242
+ }
76243
+ if (check.waitsFor.length)
76244
+ return `unblock ${check.waitsFor.join(", ")} first`;
76245
+ return check.reasons[0] ?? "workit fanout check";
76246
+ }
76247
+ var SAFE = (value) => Boolean(value) && !value?.startsWith("-"), isAncestor2 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, resolves = (cwd, ref) => gitRun4(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]).ok;
76248
+ var init_fanout_check = __esm(() => {
76249
+ init_ledger();
76250
+ init_fanout();
76251
+ });
76252
+
76253
+ // packages/workit-cli/src/verbs/fanout.ts
76254
+ var exports_fanout = {};
76255
+ __export(exports_fanout, {
76256
+ run: () => run15
76257
+ });
76258
+ import fs23 from "node:fs";
76259
+ import path43 from "node:path";
76260
+ function renderBlockedPlan(io, result) {
76261
+ if (io.json || result.code !== "blocked" && result.code !== "invalid_input")
76262
+ return;
76263
+ const problems = result.data?.problems;
76264
+ if (Array.isArray(problems))
76265
+ for (const problem of problems)
76266
+ io.stderr(` - ${problem}
76267
+ `);
76268
+ const conflicts = result.data?.conflicts;
76269
+ if (Array.isArray(conflicts))
76270
+ for (const conflict of conflicts)
76271
+ io.stderr(` - ${conflict.slices.join(" + ")}: ${conflict.paths.join(", ")}
76272
+ fix: ${conflict.suggestion.text}${conflict.alternative ? `
76273
+ or: ${conflict.alternative.text}` : ""}
76274
+ `);
76275
+ }
76276
+ function resolveTrunk(io, raw, flag, track) {
76277
+ if (flag !== null)
76278
+ return { trunk: flag };
76279
+ const named = typeof raw === "object" && raw !== null && typeof raw.trunk === "string" ? raw.trunk.trim() || null : null;
76280
+ if (named !== null && track === null)
76281
+ return { trunk: null };
76282
+ const derived = releaseTrunk(io, track, null);
76283
+ if ("error" in derived)
76284
+ return {
76285
+ code: emit(io, fail("blocked", derived.error, {
76286
+ unblock: "workit fanout plan <plan.json> --track <name> # or --trunk <branch>"
76287
+ }))
76288
+ };
76289
+ if (named !== null && derived.trunk !== named)
76290
+ return {
76291
+ code: usage3(io, `the plan names trunk ${named}; --track ${track} targets ${derived.trunk ?? "(none)"}`, PLAN_USAGE2)
76292
+ };
76293
+ return { trunk: derived.tracked || track !== null ? derived.trunk : null };
76294
+ }
76295
+ function plan2(argv, io) {
76296
+ const flags = parseFlags(argv, { name: "value", trunk: "value", track: "value" });
76297
+ if (typeof flags === "string")
76298
+ return usage3(io, flags, PLAN_USAGE2);
76299
+ const track = flags.values.track ?? null;
76300
+ if (track !== null && flags.values.trunk !== undefined)
76301
+ return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE2);
76302
+ if (flags.positionals.length !== 1)
76303
+ return usage3(io, flags.positionals.length ? `unexpected argument ${flags.positionals[1]}` : "missing <plan.json>", PLAN_USAGE2);
76304
+ const file = path43.resolve(io.cwd, flags.positionals[0]);
76305
+ let raw;
76306
+ try {
76307
+ raw = JSON.parse(fs23.readFileSync(file, "utf8"));
76308
+ } catch (error) {
76309
+ return usage3(io, `cannot read ${file} as JSON: ${error.message}`, PLAN_USAGE2);
76310
+ }
76311
+ const trunk = resolveTrunk(io, raw, flags.values.trunk ?? null, track);
76312
+ if ("code" in trunk)
76313
+ return trunk.code;
76314
+ const result = planFanout(io.cwd, {
76315
+ raw,
76316
+ name: flags.values.name ?? null,
76317
+ trunk: trunk.trunk,
76318
+ actor: actorFromEnv(io.env)
76319
+ });
76320
+ if (!result.ok) {
76321
+ const code = failed(io, result);
76322
+ renderBlockedPlan(io, result);
76323
+ return code;
76324
+ }
76325
+ return emit(io, ok(result.data), (data) => [
76326
+ `${data.created ? "planned" : "re-planned"} fanout ${data.name} on ${data.trunk}: ${data.slices.length} slice${data.slices.length === 1 ? "" : "s"}`,
76327
+ ...data.slices.map((slice) => ` ${slice.id} [${slice.tier}] ${slice.branch} <- ${slice.base}${slice.dependsOn.length ? ` after ${slice.dependsOn.join(", ")}` : ""}
76328
+ worktree ${slice.worktree}`),
76329
+ ...data.overlaps.map((overlap) => ` shared ${overlap.path}: ${overlap.resolution === "owner" ? `owned by ${overlap.owner}` : `serialized (${overlap.slices.join(" -> ")})`}`),
76330
+ `waves: ${data.waves.map((wave) => wave.join(", ")).join(" | ")}`,
76331
+ `landing order: ${data.landingOrder.join(", ")}`,
76332
+ ...data.notes.map((note) => `note: ${note}`)
76333
+ ]);
76334
+ }
76335
+ function check2(argv, io) {
76336
+ const flags = parseFlags(argv, { name: "value", base: "value" });
76337
+ if (typeof flags === "string")
76338
+ return usage3(io, flags, CHECK_USAGE);
76339
+ const selected = selectFanout(io.cwd, flags.values.name ?? null, currentBranch(io.cwd));
76340
+ if (!selected.ok)
76341
+ return failed(io, selected);
76342
+ const result = checkFanout(io.cwd, selected.data, {
76343
+ only: flags.positionals,
76344
+ trunkRef: flags.values.base ?? null
76345
+ });
76346
+ if (!result.ok)
76347
+ return failed(io, result);
76348
+ if (result.data.ready)
76349
+ return emit(io, ok(result.data), renderCheck);
76350
+ if (!io.json)
76351
+ for (const line of renderCheck(result.data))
76352
+ io.stdout(`${line}
76353
+ `);
76354
+ const first = result.data.slices.find((slice) => slice.status === "blocked");
76355
+ return emit(io, fail("blocked", `slice ${first?.id ?? "?"} is not ready: ${first?.reasons[0] ?? "blocked"}`, {
76356
+ data: result.data,
76357
+ unblock: result.data.next
76358
+ }));
76359
+ }
76360
+ async function run15(argv, io) {
76361
+ const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
76362
+ if (sub === "plan")
76363
+ return plan2(rest, io);
76364
+ if (sub === "check")
76365
+ return check2(rest, io);
76366
+ return usage3(io, sub && !sub.startsWith("-") ? `unknown fanout subcommand "${sub}"` : "missing fanout subcommand", USAGE8);
76367
+ }
76368
+ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--json]", CHECK_USAGE = "workit fanout check [<slice>…] [--name <n>] [--base <ref>] [--json]", USAGE8 = "workit fanout plan|check ... (workit help fanout)", failed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), renderCheck = (data) => [
76369
+ `fanout ${data.name} against ${data.trunkRef}: ${data.ready ? "ready to land" : "blocked"}`,
76370
+ ...data.slices.map((slice) => ` ${slice.status === "ready" ? "ok " : "x "}${slice.id} (${slice.branch}) ${slice.changed} file${slice.changed === 1 ? "" : "s"} · verdict ${slice.verdict.accepted ? "accepted" : slice.verdict.basis}${slice.reasons.length ? `
76371
+ ${slice.reasons.join(`
76372
+ `)}` : ""}`),
76373
+ ...data.conflicts.map((conflict) => ` conflict ${conflict.first} x ${conflict.second}: ${conflict.paths.join(", ")}
76374
+ fix: ${conflict.resolution}`),
76375
+ `landing order: ${data.landingOrder.join(", ")}`,
76376
+ `next: ${data.next}`,
76377
+ ...data.notes.map((note) => `note: ${note}`)
76378
+ ];
76379
+ var init_fanout2 = __esm(() => {
76380
+ init_fanout_check();
76381
+ init_fanout();
76382
+ init_rev();
76383
+ init_ledger();
76384
+ init_output();
76385
+ init_forge_common();
76386
+ });
76387
+
75305
76388
  // packages/workit-cli/src/verbs/ledger.ts
75306
76389
  var exports_ledger = {};
75307
76390
  __export(exports_ledger, {
75308
- run: () => run15
76391
+ run: () => run16
75309
76392
  });
75310
76393
  import { randomBytes as randomBytes4 } from "node:crypto";
75311
76394
  import { parseArgs } from "node:util";
@@ -75329,7 +76412,7 @@ function targetBranch(io, rows, branch, pr) {
75329
76412
  }
75330
76413
  return { ok: true, value: branch ?? null };
75331
76414
  }
75332
- async function run15(argv, io) {
76415
+ async function run16(argv, io) {
75333
76416
  let parsed;
75334
76417
  try {
75335
76418
  parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
@@ -75357,7 +76440,7 @@ async function run15(argv, io) {
75357
76440
  return usage4(io, `unknown ledger subcommand "${sub}"`);
75358
76441
  const ledger = readLedger(io.cwd);
75359
76442
  if (!ledger.ok)
75360
- return failed(io, ledger);
76443
+ return failed2(io, ledger);
75361
76444
  const { rows } = ledger.value;
75362
76445
  if (sub === "list" || sub === "show") {
75363
76446
  if (rest.length)
@@ -75379,7 +76462,7 @@ async function run15(argv, io) {
75379
76462
  return usage4(io, `unexpected argument: ${rest.at(-1)}`);
75380
76463
  const target = targetBranch(io, rows, rest[0] ?? values.branch, pr);
75381
76464
  if (!target.ok)
75382
- return failed(io, target);
76465
+ return failed2(io, target);
75383
76466
  const branch = target.value ?? currentBranch(io.cwd);
75384
76467
  if (!branch)
75385
76468
  return emit(io, fail("invalid_input", "HEAD is detached; name the branch", {
@@ -75389,7 +76472,7 @@ async function run15(argv, io) {
75389
76472
  }
75390
76473
  const target = targetBranch(io, rows, values.branch, pr);
75391
76474
  if (!target.ok)
75392
- return failed(io, target);
76475
+ return failed2(io, target);
75393
76476
  const acting = actorFor(io, values);
75394
76477
  if (acting instanceof Error)
75395
76478
  return usage4(io, acting.message);
@@ -75428,7 +76511,7 @@ async function run15(argv, io) {
75428
76511
  ..."derivedFrom" in acting ? { derivedFrom: acting.derivedFrom } : {}
75429
76512
  })), (row) => `recorded verdict ${row.id}: ${row.result} [${row.kind}] for ${row.branch} @ ${(row.head ?? "").slice(0, 12)} as ${actor.session ?? "no session"}${row.self ? ` (self${row.selfReason === "no_session" ? ": WORKIT_SESSION_ID unset" : ""}; never accepted)` : ""}`);
75430
76513
  }
75431
- var USAGE8 = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS, SESSION_SAFE2, ROLE_SAFE, actorFor = (io, values) => {
76514
+ var USAGE9 = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS, SESSION_SAFE2, ROLE_SAFE, actorFor = (io, values) => {
75432
76515
  const actor = actorFromEnv(io.env);
75433
76516
  if (values.session !== undefined && values.as !== undefined)
75434
76517
  return new Error("pass --session or --as, not both");
@@ -75452,7 +76535,7 @@ var USAGE8 = "workit ledger decision|ruling|verdict|list|check ... (workit help
75452
76535
  return;
75453
76536
  const parsed = Number(value);
75454
76537
  return Number.isInteger(parsed) && parsed > 0 ? parsed : new Error(`${flag} must be a positive integer`);
75455
- }, failed = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE8 })), verdictLines = (check) => {
76538
+ }, failed2 = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed2(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE9 })), verdictLines = (check) => {
75456
76539
  const lines = [
75457
76540
  `${check.branch} @ ${(check.head ?? "no commit").slice(0, 12)}: current: ${check.current.basis}${typeof check.current.verdict?.result === "string" ? ` (${check.current.verdict.result})` : ""}; ${check.accepted.accepted ? "accepted" : `not accepted (${check.accepted.reasons.join(", ")})`}; review: ${check.review}`
75458
76541
  ];
@@ -90449,7 +91532,7 @@ var init_ast = __esm(() => {
90449
91532
 
90450
91533
  // packages/workit-cli/src/test-audit/rules.ts
90451
91534
  import { builtinModules } from "node:module";
90452
- import path42 from "node:path";
91535
+ import path44 from "node:path";
90453
91536
  function testCallee(call) {
90454
91537
  let callee = call.callee;
90455
91538
  if (callee.type === "CallExpression")
@@ -90942,12 +92025,12 @@ function checkConstantsOnly(ctx, emit) {
90942
92025
  }
90943
92026
  function checkOverMocking(ctx, emit) {
90944
92027
  const { program } = ctx.parsed;
90945
- const unit = path42.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
92028
+ const unit = path44.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
90946
92029
  const mocked = find2(program, (node) => node.type === "CallExpression" && MODULE_MOCK.test(calleeName(node.callee) ?? "") && node.arguments[0]?.type === "StringLiteral");
90947
92030
  const internal = mocked.filter((call) => /^(?:\.|@\/|~\/)/.test(call.arguments[0].value));
90948
92031
  for (const call of mocked) {
90949
92032
  const spec = call.arguments[0].value;
90950
- const base = path42.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
92033
+ const base = path44.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
90951
92034
  if (base === unit)
90952
92035
  emit({
90953
92036
  rule: "over-mocking",
@@ -91335,11 +92418,11 @@ var init_rules = __esm(() => {
91335
92418
  });
91336
92419
 
91337
92420
  // packages/workit-cli/src/test-audit/audit.ts
91338
- import { spawnSync as spawnSync20 } from "node:child_process";
92421
+ import { spawnSync as spawnSync21 } from "node:child_process";
91339
92422
  import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync19, statSync as statSync7 } from "node:fs";
91340
- import path43 from "node:path";
92423
+ import path45 from "node:path";
91341
92424
  function git6(cwd, args) {
91342
- const run = spawnSync20("git", ["-c", "core.quotepath=off", ...args], {
92425
+ const run = spawnSync21("git", ["-c", "core.quotepath=off", ...args], {
91343
92426
  cwd,
91344
92427
  encoding: "utf8",
91345
92428
  maxBuffer: 64 * 1024 * 1024
@@ -91350,7 +92433,7 @@ function walkTests(dir, out) {
91350
92433
  for (const entry of readdirSync8(dir, { withFileTypes: true })) {
91351
92434
  if (entry.name.startsWith(".") && entry.name !== "." && entry.isDirectory())
91352
92435
  continue;
91353
- const full = path43.join(dir, entry.name);
92436
+ const full = path45.join(dir, entry.name);
91354
92437
  if (entry.isDirectory()) {
91355
92438
  if (!SKIP_DIRS.has(entry.name))
91356
92439
  walkTests(full, out);
@@ -91362,7 +92445,7 @@ function expandPaths(cwd, inputs) {
91362
92445
  const files = [];
91363
92446
  const missing = [];
91364
92447
  for (const input of inputs.length > 0 ? inputs : ["."]) {
91365
- const full = path43.resolve(cwd, input);
92448
+ const full = path45.resolve(cwd, input);
91366
92449
  if (!existsSync18(full))
91367
92450
  missing.push(input);
91368
92451
  else if (statSync7(full).isDirectory())
@@ -91399,11 +92482,11 @@ function changedLines(cwd, base) {
91399
92482
  for (let offset = 0;offset < count; offset += 1)
91400
92483
  lines.add(start + offset);
91401
92484
  }
91402
- changed.set(path43.join(root, file), lines);
92485
+ changed.set(path45.join(root, file), lines);
91403
92486
  }
91404
92487
  const untracked = git6(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
91405
92488
  for (const file of untracked.stdout.split("\x00").filter(Boolean))
91406
- changed.set(path43.join(root, file), "all");
92489
+ changed.set(path45.join(root, file), "all");
91407
92490
  return changed;
91408
92491
  }
91409
92492
  function auditFiles(files, display) {
@@ -91465,7 +92548,7 @@ function auditFiles(files, display) {
91465
92548
  durationMs: Math.round(performance.now() - started)
91466
92549
  };
91467
92550
  }
91468
- var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path43.sep).join("/"), SEVERITY_ORDER, ruleIds;
92551
+ var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path45.sep).join("/"), SEVERITY_ORDER, ruleIds;
91469
92552
  var init_audit = __esm(() => {
91470
92553
  init_ast();
91471
92554
  init_rules();
@@ -91495,7 +92578,7 @@ import {
91495
92578
  copyFileSync as copyFileSync6
91496
92579
  } from "node:fs";
91497
92580
  import os14 from "node:os";
91498
- import path44 from "node:path";
92581
+ import path46 from "node:path";
91499
92582
  function mutantsFor(source, file, lines) {
91500
92583
  const parsed = parseSource(source, file);
91501
92584
  const out = [];
@@ -91568,7 +92651,7 @@ function relatedTests(source, testFiles) {
91568
92651
  for (const match of text.matchAll(SPECIFIER)) {
91569
92652
  const spec = match[1];
91570
92653
  if (spec.startsWith(".")) {
91571
- if (stripExt(path44.posix.join(path44.posix.dirname(file), spec)) === target)
92654
+ if (stripExt(path46.posix.join(path46.posix.dirname(file), spec)) === target)
91572
92655
  return true;
91573
92656
  } else if (spec.includes("/") && stripExt(spec).split("/").slice(-2).join("/") === tail)
91574
92657
  return true;
@@ -91588,12 +92671,12 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
91588
92671
  for (const name of names) {
91589
92672
  if (!name.startsWith(TEMP_PREFIX))
91590
92673
  continue;
91591
- const dir = path44.join(tmp, name);
92674
+ const dir = path46.join(tmp, name);
91592
92675
  try {
91593
92676
  const stat = lstatSync7(dir);
91594
92677
  if (!stat.isDirectory() || uid !== undefined && stat.uid !== uid)
91595
92678
  continue;
91596
- const pid = Number(readFileSync20(path44.join(dir, PID_FILE), "utf8"));
92679
+ const pid = Number(readFileSync20(path46.join(dir, PID_FILE), "utf8"));
91597
92680
  if (Number.isInteger(pid) && pid > 0 && alive(pid))
91598
92681
  continue;
91599
92682
  } catch (error) {
@@ -91610,8 +92693,8 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
91610
92693
  function linkNodeModules(origin, target, repo, copy) {
91611
92694
  mkdirSync9(target, { recursive: true });
91612
92695
  for (const entry of readdirSync9(origin, { withFileTypes: true })) {
91613
- const from = path44.join(origin, entry.name);
91614
- const to = path44.join(target, entry.name);
92696
+ const from = path46.join(origin, entry.name);
92697
+ const to = path46.join(target, entry.name);
91615
92698
  if (entry.name.startsWith("@") && entry.isDirectory()) {
91616
92699
  linkNodeModules(from, to, repo, copy);
91617
92700
  continue;
@@ -91627,9 +92710,9 @@ function linkNodeModules(origin, target, repo, copy) {
91627
92710
  } catch {
91628
92711
  continue;
91629
92712
  }
91630
- const relative = path44.relative(repo, real);
91631
- if (!relative.startsWith("..") && !path44.isAbsolute(relative) && !relative.split(path44.sep).includes("node_modules")) {
91632
- symlinkSync(path44.join(copy, relative), to, "junction");
92713
+ const relative = path46.relative(repo, real);
92714
+ if (!relative.startsWith("..") && !path46.isAbsolute(relative) && !relative.split(path46.sep).includes("node_modules")) {
92715
+ symlinkSync(path46.join(copy, relative), to, "junction");
91633
92716
  continue;
91634
92717
  }
91635
92718
  }
@@ -91645,7 +92728,7 @@ function copySymlink(repo, tree, from, to) {
91645
92728
  }
91646
92729
  const realRepo = realpathSync9(repo);
91647
92730
  if (real && inside2(realRepo, real)) {
91648
- symlinkSync(path44.relative(path44.dirname(to), path44.join(tree, path44.relative(realRepo, real))), to);
92731
+ symlinkSync(path46.relative(path46.dirname(to), path46.join(tree, path46.relative(realRepo, real))), to);
91649
92732
  return;
91650
92733
  }
91651
92734
  if (real && statSync8(real).isFile())
@@ -91654,10 +92737,10 @@ function copySymlink(repo, tree, from, to) {
91654
92737
  symlinkSync(readlinkSync3(from), to);
91655
92738
  }
91656
92739
  function writeInside(copy, file, content) {
91657
- const target = path44.join(copy, file);
92740
+ const target = path46.join(copy, file);
91658
92741
  try {
91659
92742
  const stat = lstatSync7(target);
91660
- if (!stat.isFile() || !inside2(realpathSync9(copy), realpathSync9(path44.dirname(target))))
92743
+ if (!stat.isFile() || !inside2(realpathSync9(copy), realpathSync9(path46.dirname(target))))
91661
92744
  return false;
91662
92745
  } catch {
91663
92746
  return false;
@@ -91668,9 +92751,9 @@ function writeInside(copy, file, content) {
91668
92751
  return true;
91669
92752
  }
91670
92753
  function copyWorkingTree(repo) {
91671
- const dir = mkdtempSync4(path44.join(os14.tmpdir(), TEMP_PREFIX));
91672
- writeFileSync9(path44.join(dir, PID_FILE), String(process.pid));
91673
- const tree = path44.join(dir, "tree");
92754
+ const dir = mkdtempSync4(path46.join(os14.tmpdir(), TEMP_PREFIX));
92755
+ writeFileSync9(path46.join(dir, PID_FILE), String(process.pid));
92756
+ const tree = path46.join(dir, "tree");
91674
92757
  const head = git6(repo, ["rev-parse", "--verify", "--quiet", "HEAD"]).stdout.trim();
91675
92758
  if (head && git6(dir, ["clone", "-q", "--shared", "--no-checkout", repo, tree]).ok) {
91676
92759
  const branch = git6(repo, ["symbolic-ref", "--quiet", "--short", "HEAD"]).stdout.trim();
@@ -91687,29 +92770,29 @@ function copyWorkingTree(repo) {
91687
92770
  for (const relative of listed) {
91688
92771
  if (!relative)
91689
92772
  continue;
91690
- const from = path44.join(repo, relative);
92773
+ const from = path46.join(repo, relative);
91691
92774
  let stat;
91692
92775
  try {
91693
92776
  stat = lstatSync7(from);
91694
92777
  } catch {
91695
92778
  continue;
91696
92779
  }
91697
- const to = path44.join(tree, relative);
91698
- mkdirSync9(path44.dirname(to), { recursive: true });
92780
+ const to = path46.join(tree, relative);
92781
+ mkdirSync9(path46.dirname(to), { recursive: true });
91699
92782
  if (stat.isSymbolicLink())
91700
92783
  copySymlink(repo, tree, from, to);
91701
92784
  else if (stat.isFile())
91702
92785
  copyFileSync6(from, to);
91703
- let parent = path44.dirname(relative);
92786
+ let parent = path46.dirname(relative);
91704
92787
  while (parent !== "." && !dirs.has(parent)) {
91705
92788
  dirs.add(parent);
91706
- parent = path44.dirname(parent);
92789
+ parent = path46.dirname(parent);
91707
92790
  }
91708
92791
  }
91709
92792
  for (const parent of dirs) {
91710
- const modules = path44.join(repo, parent, "node_modules");
91711
- if (existsSync19(modules) && !existsSync19(path44.join(tree, parent, "node_modules")))
91712
- linkNodeModules(modules, path44.join(tree, parent, "node_modules"), repo, tree);
92793
+ const modules = path46.join(repo, parent, "node_modules");
92794
+ if (existsSync19(modules) && !existsSync19(path46.join(tree, parent, "node_modules")))
92795
+ linkNodeModules(modules, path46.join(tree, parent, "node_modules"), repo, tree);
91713
92796
  }
91714
92797
  return { dir, tree };
91715
92798
  }
@@ -91748,14 +92831,14 @@ async function runMutation(options) {
91748
92831
  const tracked = git6(repo, ["ls-files", "-co", "--exclude-standard", "-z"]).stdout.split("\x00").filter(Boolean);
91749
92832
  const testFiles = tracked.filter((file) => TEST_FILE.test(file)).map((file) => {
91750
92833
  try {
91751
- return { file, text: readFileSync20(path44.join(repo, file), "utf8") };
92834
+ return { file, text: readFileSync20(path46.join(repo, file), "utf8") };
91752
92835
  } catch {
91753
92836
  return { file, text: "" };
91754
92837
  }
91755
92838
  });
91756
92839
  const candidates = [];
91757
92840
  for (const [absolute, lines] of [...options.changed].toSorted(([a], [b]) => a.localeCompare(b))) {
91758
- const relative = toPosix(path44.relative(repo, absolute));
92841
+ const relative = toPosix(path46.relative(repo, absolute));
91759
92842
  if (!CODE_FILE.test(relative) || TEST_FILE.test(relative) || relative.endsWith(".d.ts"))
91760
92843
  continue;
91761
92844
  if (NOT_SOURCE.test(relative) || !existsSync19(absolute))
@@ -91769,9 +92852,9 @@ async function runMutation(options) {
91769
92852
  for (const mutant of mutants)
91770
92853
  if (!related.has(mutant.file))
91771
92854
  related.set(mutant.file, relatedTests(mutant.file, testFiles));
91772
- const cwdRelative = path44.relative(repo, options.cwd);
92855
+ const cwdRelative = path46.relative(repo, options.cwd);
91773
92856
  const asArg = (file) => {
91774
- const relative = toPosix(path44.relative(cwdRelative, file));
92857
+ const relative = toPosix(path46.relative(cwdRelative, file));
91775
92858
  return shellQuote(relative.startsWith(".") ? relative : `./${relative}`);
91776
92859
  };
91777
92860
  const commandFor = (tests) => options.command.replaceAll("{files}", () => tests.map(asArg).join(" "));
@@ -91806,7 +92889,7 @@ async function runMutation(options) {
91806
92889
  return finish2(options, 0, results, counts, started);
91807
92890
  removeStaleCopies();
91808
92891
  const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
91809
- const copyCwd = path44.join(copy, cwdRelative);
92892
+ const copyCwd = path46.join(copy, cwdRelative);
91810
92893
  let current = null;
91811
92894
  const restore = () => {
91812
92895
  if (current)
@@ -91841,7 +92924,7 @@ async function runMutation(options) {
91841
92924
  record({ ...mutant, status: "skipped", durationMs: 0, tests, reason: "budget" });
91842
92925
  continue;
91843
92926
  }
91844
- const original = readFileSync20(path44.join(copy, mutant.file), "utf8");
92927
+ const original = readFileSync20(path46.join(copy, mutant.file), "utf8");
91845
92928
  const mutated = original.slice(0, mutant.start) + mutant.replacement + original.slice(mutant.end);
91846
92929
  current = { file: mutant.file, original };
91847
92930
  if (!writeInside(copy, mutant.file, mutated)) {
@@ -91898,8 +92981,8 @@ var NOT_SOURCE, FLIPS, isStringy = (node) => node.type === "StringLiteral" || no
91898
92981
  return error.code === "EPERM";
91899
92982
  }
91900
92983
  }, inside2 = (root, file) => {
91901
- const relative = path44.relative(root, file);
91902
- return relative === "" || !relative.startsWith("..") && !path44.isAbsolute(relative);
92984
+ const relative = path46.relative(root, file);
92985
+ return relative === "" || !relative.startsWith("..") && !path46.isAbsolute(relative);
91903
92986
  }, active, killTree = (child) => {
91904
92987
  try {
91905
92988
  if (child.pid && process.platform !== "win32")
@@ -91945,10 +93028,10 @@ var init_mutate = __esm(() => {
91945
93028
  // packages/workit-cli/src/verbs/test-audit.ts
91946
93029
  var exports_test_audit = {};
91947
93030
  __export(exports_test_audit, {
91948
- run: () => run16
93031
+ run: () => run17
91949
93032
  });
91950
93033
  import { existsSync as existsSync20 } from "node:fs";
91951
- import path45 from "node:path";
93034
+ import path47 from "node:path";
91952
93035
  function parse7(argv) {
91953
93036
  const options = {
91954
93037
  paths: [],
@@ -92042,14 +93125,14 @@ function human(data) {
92042
93125
  }
92043
93126
  return lines;
92044
93127
  }
92045
- async function run16(argv, io) {
93128
+ async function run17(argv, io) {
92046
93129
  const options = parse7(argv);
92047
93130
  if (typeof options === "string")
92048
93131
  return emit(io, fail("invalid_input", `${options}
92049
- usage: ${USAGE9}`));
93132
+ usage: ${USAGE10}`));
92050
93133
  const top = git6(io.cwd, ["rev-parse", "--show-toplevel"]);
92051
- const repo = top.ok ? path45.resolve(top.stdout.trim()) : null;
92052
- const display = (file) => toPosix(path45.relative(io.cwd, file) || file);
93134
+ const repo = top.ok ? path47.resolve(top.stdout.trim()) : null;
93135
+ const display = (file) => toPosix(path47.relative(io.cwd, file) || file);
92053
93136
  let base = null;
92054
93137
  let changed = null;
92055
93138
  if (options.diff || options.mutate) {
@@ -92132,7 +93215,7 @@ usage: ${USAGE9}`));
92132
93215
  }
92133
93216
  return emit(io, ok(data), human);
92134
93217
  }
92135
- var USAGE9 = "workit test-audit [paths…] [--diff [base]] [--rule <ids>] [--min-severity <level>] [--fail-on <level>] [--mutate [--test-cmd <cmd>] [--max-mutants <n>] [--budget <s>] [--timeout <s>]] [--json]", LEVELS, VALUE_FLAGS2, quote = (arg) => /^[\w./@:=+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
93218
+ var USAGE10 = "workit test-audit [paths…] [--diff [base]] [--rule <ids>] [--min-severity <level>] [--fail-on <level>] [--mutate [--test-cmd <cmd>] [--max-mutants <n>] [--budget <s>] [--timeout <s>]] [--json]", LEVELS, VALUE_FLAGS2, quote = (arg) => /^[\w./@:=+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
92136
93219
  var init_test_audit = __esm(() => {
92137
93220
  init_audit();
92138
93221
  init_mutate();
@@ -92257,14 +93340,14 @@ var init_youtrack_writes = __esm(() => {
92257
93340
  // packages/workit-cli/src/verbs/youtrack.ts
92258
93341
  var exports_youtrack = {};
92259
93342
  __export(exports_youtrack, {
92260
- run: () => run17
93343
+ run: () => run18
92261
93344
  });
92262
93345
  import { readFileSync as readFileSync21 } from "node:fs";
92263
- import path46 from "node:path";
92264
- async function run17(argv, io) {
93346
+ import path48 from "node:path";
93347
+ async function run18(argv, io) {
92265
93348
  const [sub, ...rest] = argv;
92266
93349
  if (sub !== "note" && sub !== "time" && sub !== "meeting")
92267
- return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE10);
93350
+ return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE11);
92268
93351
  const flags = parseFlags(rest, {
92269
93352
  markdown: "value",
92270
93353
  file: "value",
@@ -92273,18 +93356,18 @@ async function run17(argv, io) {
92273
93356
  date: "value"
92274
93357
  });
92275
93358
  if (typeof flags === "string")
92276
- return usage3(io, flags, USAGE10);
93359
+ return usage3(io, flags, USAGE11);
92277
93360
  const [issueId, extra] = flags.positionals;
92278
93361
  if (!issueId || !ISSUE_RE2.test(issueId))
92279
- return usage3(io, "an issue id like ABC-123 is required", USAGE10);
93362
+ return usage3(io, "an issue id like ABC-123 is required", USAGE11);
92280
93363
  if (extra !== undefined)
92281
- return usage3(io, `unexpected argument ${extra}`, USAGE10);
93364
+ return usage3(io, `unexpected argument ${extra}`, USAGE11);
92282
93365
  const minutes = positiveInt(flags.values.minutes, "--minutes");
92283
93366
  if (typeof minutes === "string")
92284
- return usage3(io, minutes, USAGE10);
93367
+ return usage3(io, minutes, USAGE11);
92285
93368
  const date = youTrackWorkDateMs(flags.values.date ?? "auto");
92286
93369
  if ("error" in date)
92287
- return usage3(io, date.error, USAGE10);
93370
+ return usage3(io, date.error, USAGE11);
92288
93371
  const dateMs = date.data.dateMs;
92289
93372
  const markedDay = flags.values.date !== undefined && flags.values.date !== "auto" ? { dateMs } : {};
92290
93373
  const root = io.cwd;
@@ -92302,24 +93385,24 @@ async function run17(argv, io) {
92302
93385
  };
92303
93386
  if (sub === "note") {
92304
93387
  if (Boolean(flags.values.markdown) === Boolean(flags.values.file))
92305
- return usage3(io, "pass exactly one of --markdown or --file", USAGE10);
93388
+ return usage3(io, "pass exactly one of --markdown or --file", USAGE11);
92306
93389
  let markdown = flags.values.markdown ?? "";
92307
93390
  if (flags.values.file)
92308
93391
  try {
92309
- markdown = readFileSync21(path46.resolve(io.cwd, flags.values.file), "utf8");
93392
+ markdown = readFileSync21(path48.resolve(io.cwd, flags.values.file), "utf8");
92310
93393
  } catch (error) {
92311
- return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE10);
93394
+ return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE11);
92312
93395
  }
92313
93396
  if (!markdown.trim())
92314
- return usage3(io, "the note is empty", USAGE10);
93397
+ return usage3(io, "the note is empty", USAGE11);
92315
93398
  const missing = step("comment", "comments", { operation: "note", issueId, markdown }, markdown, async (text) => commentResult(await postComment(issueId, text, root))) ?? (minutes ? step("time", "workItems", { operation: "note.time", issueId, markdown, minutes, ...markedDay }, "workit update", (text) => logTimeUpdate({ issueId, minutes, text, dateMs, workspace_root: root })) : null);
92316
93399
  if (missing)
92317
93400
  return emit(io, fail("unavailable", missing));
92318
93401
  } else {
92319
93402
  if (!minutes)
92320
- return usage3(io, `${sub} needs --minutes <n>`, USAGE10);
93403
+ return usage3(io, `${sub} needs --minutes <n>`, USAGE11);
92321
93404
  if (sub === "meeting" && !flags.values.text)
92322
- return usage3(io, "meeting needs --text <what the meeting was>", USAGE10);
93405
+ return usage3(io, "meeting needs --text <what the meeting was>", USAGE11);
92323
93406
  const text = flags.values.text ?? "";
92324
93407
  const missing = step(sub, "workItems", { operation: sub, issueId, minutes, text, ...markedDay }, text, (marked) => logTimeUpdate({ issueId, minutes, text: marked, dateMs, workspace_root: root }));
92325
93408
  if (missing)
@@ -92348,7 +93431,7 @@ async function run17(argv, io) {
92348
93431
  ...data.steps.flatMap((entry) => entry.note ? [` note (${entry.step}): ${entry.note}`] : [])
92349
93432
  ]);
92350
93433
  }
92351
- var USAGE10 = "workit youtrack note <ISSUE> (--markdown <t> | --file <p>) [--minutes <n>] [--date auto|YYYY-MM-DD] | youtrack time <ISSUE> --minutes <n> [--text <t>] [--date …] | youtrack meeting <ISSUE> --minutes <n> --text <t> [--date …]", commentResult = (value) => {
93434
+ var USAGE11 = "workit youtrack note <ISSUE> (--markdown <t> | --file <p>) [--minutes <n>] [--date auto|YYYY-MM-DD] | youtrack time <ISSUE> --minutes <n> [--text <t>] [--date …] | youtrack meeting <ISSUE> --minutes <n> --text <t> [--date …]", commentResult = (value) => {
92352
93435
  try {
92353
93436
  return { ok: true, data: unwrap2(value) };
92354
93437
  } catch (error) {
@@ -92372,14 +93455,14 @@ var init_youtrack2 = __esm(() => {
92372
93455
  // packages/workit-cli/src/verbs/changelog.ts
92373
93456
  var exports_changelog = {};
92374
93457
  __export(exports_changelog, {
92375
- run: () => run18
93458
+ run: () => run19
92376
93459
  });
92377
93460
  import { readFileSync as readFileSync22 } from "node:fs";
92378
- import path47 from "node:path";
92379
- async function run18(argv, io) {
93461
+ import path49 from "node:path";
93462
+ async function run19(argv, io) {
92380
93463
  const [sub, ...rest] = argv;
92381
93464
  if (sub !== "apply")
92382
- return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE11);
93465
+ return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE12);
92383
93466
  const flags = parseFlags(rest, {
92384
93467
  entries: "value",
92385
93468
  path: "value",
@@ -92387,18 +93470,18 @@ async function run18(argv, io) {
92387
93470
  preview: "boolean"
92388
93471
  });
92389
93472
  if (typeof flags === "string")
92390
- return usage3(io, flags, USAGE11);
93473
+ return usage3(io, flags, USAGE12);
92391
93474
  if (flags.positionals.length)
92392
- return usage3(io, `unexpected argument ${flags.positionals[0]}`, USAGE11);
93475
+ return usage3(io, `unexpected argument ${flags.positionals[0]}`, USAGE12);
92393
93476
  const normalizeOnly = flags.booleans.has("normalize-only");
92394
93477
  if (!normalizeOnly && !flags.values.entries)
92395
- return usage3(io, "pass --entries <JSON> or --normalize-only", USAGE11);
93478
+ return usage3(io, "pass --entries <JSON> or --normalize-only", USAGE12);
92396
93479
  let entries;
92397
93480
  if (flags.values.entries)
92398
93481
  try {
92399
93482
  entries = readEntries(flags.values.entries, io.cwd);
92400
93483
  } catch (error) {
92401
- return usage3(io, `--entries is not valid JSON: ${String(error)}`, USAGE11);
93484
+ return usage3(io, `--entries is not valid JSON: ${String(error)}`, USAGE12);
92402
93485
  }
92403
93486
  const input = {
92404
93487
  entries,
@@ -92420,8 +93503,8 @@ async function run18(argv, io) {
92420
93503
  `changelog updated: ${String(applied.path ?? "CHANGELOG.md")}`
92421
93504
  ]);
92422
93505
  }
92423
- var USAGE11 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
92424
- const text = raw === "-" ? readFileSync22(0, "utf8") : raw.startsWith("@") ? readFileSync22(path47.resolve(cwd, raw.slice(1)), "utf8") : raw;
93506
+ var USAGE12 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
93507
+ const text = raw === "-" ? readFileSync22(0, "utf8") : raw.startsWith("@") ? readFileSync22(path49.resolve(cwd, raw.slice(1)), "utf8") : raw;
92425
93508
  return JSON.parse(text);
92426
93509
  };
92427
93510
  var init_changelog2 = __esm(() => {
@@ -92433,7 +93516,7 @@ var init_changelog2 = __esm(() => {
92433
93516
  // packages/workit-cli/src/verbs/handoff.ts
92434
93517
  var exports_handoff = {};
92435
93518
  __export(exports_handoff, {
92436
- run: () => run19
93519
+ run: () => run20
92437
93520
  });
92438
93521
  import { parseArgs as parseArgs2 } from "node:util";
92439
93522
  function human2(brief) {
@@ -92481,11 +93564,11 @@ async function resumeBrief(argv, io) {
92481
93564
  strict: true
92482
93565
  }));
92483
93566
  } catch (error) {
92484
- return emit(io, fail("invalid_input", error.message, { unblock: USAGE12 }));
93567
+ return emit(io, fail("invalid_input", error.message, { unblock: USAGE13 }));
92485
93568
  }
92486
93569
  const last = values.last === undefined ? 10 : Number(values.last);
92487
93570
  if (!Number.isInteger(last) || last < 1)
92488
- return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock: USAGE12 }));
93571
+ return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock: USAGE13 }));
92489
93572
  const built = buildHandoff(io.cwd, {
92490
93573
  last,
92491
93574
  note: values.note ?? null,
@@ -92503,13 +93586,13 @@ async function resumeBrief(argv, io) {
92503
93586
  }
92504
93587
  return emit(io, ok({ ...data, recorded }), human2);
92505
93588
  }
92506
- async function run19(argv, io) {
93589
+ async function run20(argv, io) {
92507
93590
  if (!argv.includes("--task"))
92508
93591
  return resumeBrief(argv, io);
92509
93592
  await Promise.resolve().then(() => init_task());
92510
93593
  return runTaskCommand(["handoff", ...argv]);
92511
93594
  }
92512
- var USAGE12 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short5 = (sha) => sha ? sha.slice(0, 12) : "no commit";
93595
+ var USAGE13 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short5 = (sha) => sha ? sha.slice(0, 12) : "no commit";
92513
93596
  var init_handoff = __esm(() => {
92514
93597
  init_ledger();
92515
93598
  init_output();
@@ -92518,7 +93601,7 @@ var init_handoff = __esm(() => {
92518
93601
  // packages/workit-cli/src/main.ts
92519
93602
  init_package();
92520
93603
  init_output();
92521
- import path48 from "node:path";
93604
+ import path50 from "node:path";
92522
93605
 
92523
93606
  // packages/workit-cli/src/verbs/registry.ts
92524
93607
  var TASK_FAMILY_NAMES = [
@@ -92630,6 +93713,13 @@ var VERBS = [
92630
93713
  summary: "Plan, restack (lease push + retarget) and land a base-branch stack of PRs, root first",
92631
93714
  load: () => Promise.resolve().then(() => (init_stack2(), exports_stack))
92632
93715
  },
93716
+ {
93717
+ name: "fanout",
93718
+ group: "delivery",
93719
+ usage: "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] | fanout check [<slice>…] [--name <n>] [--base <ref>] [--json]",
93720
+ summary: "Register parallel slices (complete briefs, disjoint scopes, shared-file owners), then gate fan-in: out-of-scope edits, trunk and sibling merge conflicts, landing order",
93721
+ load: () => Promise.resolve().then(() => (init_fanout2(), exports_fanout))
93722
+ },
92633
93723
  {
92634
93724
  name: "ledger",
92635
93725
  group: "delivery",
@@ -92748,7 +93838,7 @@ var defaultIo = (json) => ({
92748
93838
  });
92749
93839
  async function main(argv, overrides = {}, options = {}) {
92750
93840
  const parsed = parseGlobals(argv);
92751
- const cwd = path48.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
93841
+ const cwd = path50.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
92752
93842
  const io = { ...defaultIo(parsed.json), ...overrides, json: parsed.json, cwd };
92753
93843
  if (parsed.error)
92754
93844
  return emit(io, fail("invalid_input", parsed.error));