@brainervirus/workit-claude-code 7.6.0 → 7.7.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.6.0",
68
+ version: "7.7.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.6.0",
107
- "@brainervirus/workit-cursor": "^7.6.0",
108
- "@brainervirus/workit-opencode": "^7.6.0",
106
+ "@brainervirus/workit-core": "^7.7.0",
107
+ "@brainervirus/workit-cursor": "^7.7.0",
108
+ "@brainervirus/workit-opencode": "^7.7.0",
109
109
  "@inkjs/ui": "2.0.0",
110
110
  ink: "7.1.1",
111
111
  react: "19.2.8"
@@ -13426,6 +13426,70 @@ function recordRuling(context, input) {
13426
13426
  refs: input.refs ?? []
13427
13427
  }, { now: context.now });
13428
13428
  }
13429
+ function activeStanding(rows, fanout, leads = null) {
13430
+ const active = [];
13431
+ for (const row of rows) {
13432
+ if (row.fanout !== fanout || !fromLead(row, leads))
13433
+ continue;
13434
+ if (row.type === "standing" && typeof row.what === "string" && row.what.trim()) {
13435
+ if (!row.superseded)
13436
+ active.push(row);
13437
+ } else if (row.type === "standing.cleared") {
13438
+ const target = str2(row.target);
13439
+ if (target === null)
13440
+ active.length = 0;
13441
+ else {
13442
+ const index = active.findIndex((order) => order.id === target);
13443
+ if (index >= 0)
13444
+ active.splice(index, 1);
13445
+ }
13446
+ }
13447
+ }
13448
+ return active;
13449
+ }
13450
+ function leadOnly(context, lead) {
13451
+ const session = context.actor.session;
13452
+ if (lead !== null ? session === lead : !isDelegateSession(session))
13453
+ return { ok: true, value: undefined };
13454
+ return err("blocked", lead !== null ? `session ${session ?? "(none)"} is not the fanout's lead (${lead}): standing orders come from the lead` : `session ${session} is a worker or verifier: standing orders come from the lead`, lead !== null ? `the lead in a new session? workit fanout plan <plan.json> --take-lead from this session; a worker or verifier reports the order to the lead instead` : "a worker or verifier reports the order to the lead instead");
13455
+ }
13456
+ function recordStanding(context, input) {
13457
+ const what = required2(input.what, "<order>");
13458
+ if (!what.ok)
13459
+ return what;
13460
+ if (/\p{Cc}/u.test(what.value))
13461
+ return err("invalid_input", "a standing order is one line: no newlines or control characters", 'add each order separately: workit ledger standing add "<order>"');
13462
+ const lead = leadOnly(context, input.lead);
13463
+ if (!lead.ok)
13464
+ return lead;
13465
+ const link = checkSupersede(context, "standing");
13466
+ if (!link.ok)
13467
+ return link;
13468
+ return appendRow(context.cwd, {
13469
+ ...common(context, noteKey(context)),
13470
+ type: "standing",
13471
+ fanout: input.fanout,
13472
+ what: what.value
13473
+ }, { now: context.now });
13474
+ }
13475
+ function clearStanding(context, input) {
13476
+ const lead = leadOnly(context, input.lead);
13477
+ if (!lead.ok)
13478
+ return lead;
13479
+ if (input.target !== null) {
13480
+ const ledger = readLedger(context.cwd);
13481
+ if (!ledger.ok)
13482
+ return ledger;
13483
+ if (!activeStanding(ledger.value.rows, input.fanout, input.leads ?? null).some((row) => row.id === input.target))
13484
+ return err("not_found", `no standing order ${input.target} in force for fanout ${input.fanout}`, `workit ledger standing list --fanout ${input.fanout}`);
13485
+ }
13486
+ return appendRow(context.cwd, {
13487
+ ...common(context, noteKey(context)),
13488
+ type: "standing.cleared",
13489
+ fanout: input.fanout,
13490
+ target: input.target
13491
+ }, { now: context.now });
13492
+ }
13429
13493
  function recordVerdict(context, input) {
13430
13494
  if (!VERDICT_RESULTS.includes(input.result))
13431
13495
  return err("invalid_input", `unknown verdict "${input.result}"; expected ${VERDICT_RESULTS.join("|")}`);
@@ -13504,6 +13568,12 @@ function summarizeRow(row) {
13504
13568
  case "handoff":
13505
13569
  summary = `next: ${text("next")}${text("note") ? ` (note: ${text("note")})` : ""}`;
13506
13570
  break;
13571
+ case "standing":
13572
+ summary = `${text("what")} (fanout ${text("fanout")})`;
13573
+ break;
13574
+ case "standing.cleared":
13575
+ summary = `cleared ${text("target") || "every standing order"} (fanout ${text("fanout")})`;
13576
+ break;
13507
13577
  case "commit.recorded":
13508
13578
  summary = `${(row.head ?? "?").slice(0, 12)} ${text("subject")}${row.actor.session ? "" : " (no session)"}`;
13509
13579
  break;
@@ -13730,7 +13800,7 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, VERDICT_RESULTS, PASSING, FAILING
13730
13800
  }, required2 = (value, flag) => {
13731
13801
  const text = value?.trim();
13732
13802
  return text ? { ok: true, value: text } : err("invalid_input", `${flag} is required`);
13733
- }, OBSERVED_ONLY, MAX_DIRTY_PATHS = 10, dirtyState = (cwd) => {
13803
+ }, fromLead = (row, leads) => leads === null || row.actor.session !== null && leads.includes(row.actor.session), isDelegateSession = (session) => session !== null && (/-w-[a-z0-9._-]+(?:\+try\d)?$/u.test(session) || /-v\d+$/u.test(session) || session.includes(":")), OBSERVED_ONLY, MAX_DIRTY_PATHS = 10, dirtyState = (cwd) => {
13734
13804
  const run = gitRun2(cwd, ["status", "--porcelain=v1", "-z", "--untracked-files=normal"], GIT_TIMEOUTS.worktree);
13735
13805
  if (!run.ok)
13736
13806
  return { dirty: false, files: 0, paths: [] };
@@ -75713,6 +75783,7 @@ var init_stack2 = __esm(() => {
75713
75783
 
75714
75784
  // packages/workit-core/src/fanout.ts
75715
75785
  import { spawnSync as spawnSync21 } from "node:child_process";
75786
+ import { createHash as createHash16 } from "node:crypto";
75716
75787
  import fs23 from "node:fs";
75717
75788
  import path43 from "node:path";
75718
75789
  function expandBraces(pattern, limit = 64) {
@@ -75874,6 +75945,9 @@ function parsePlan(raw, defaults, name) {
75874
75945
  else
75875
75946
  shared.push(normalized.pattern);
75876
75947
  }
75948
+ const fanIn = raw.fanIn ?? "prs";
75949
+ if (!FAN_IN_MODES.includes(fanIn))
75950
+ problems.push(`fanIn must be one of ${FAN_IN_MODES.join(", ")}`);
75877
75951
  if (!Array.isArray(raw.slices) || raw.slices.length === 0)
75878
75952
  return { ok: false, problems: [...problems, "slices: list at least one slice"] };
75879
75953
  const slices = [];
@@ -75955,7 +76029,9 @@ function parsePlan(raw, defaults, name) {
75955
76029
  verify,
75956
76030
  forbidden,
75957
76031
  context: optional("context"),
75958
- timebox: optional("timebox")
76032
+ timebox: optional("timebox"),
76033
+ hash: "",
76034
+ hashSince: ""
75959
76035
  });
75960
76036
  });
75961
76037
  const byId = new Map(slices.map((slice) => [slice.id, slice]));
@@ -75988,8 +76064,11 @@ function parsePlan(raw, defaults, name) {
75988
76064
  v: FANOUT_VERSION,
75989
76065
  name: planName,
75990
76066
  trunk,
76067
+ fanIn,
75991
76068
  shared,
75992
76069
  slices,
76070
+ leadSession: null,
76071
+ formerLeads: [],
75993
76072
  createdAt: "",
75994
76073
  updatedAt: ""
75995
76074
  }
@@ -76175,6 +76254,18 @@ function worktreeRoot(cwd) {
76175
76254
  const main = mainCheckout(cwd);
76176
76255
  return path43.join(path43.dirname(main), `${path43.basename(main)}-wt`);
76177
76256
  }
76257
+ function sliceHash(slice) {
76258
+ const {
76259
+ worktree: _worktree,
76260
+ hash: _hash,
76261
+ hashSince: _since,
76262
+ tier: _tier,
76263
+ timebox: _timebox,
76264
+ context: _context,
76265
+ ...definition
76266
+ } = slice;
76267
+ return createHash16("sha256").update(JSON.stringify(definition)).digest("hex").slice(0, 16);
76268
+ }
76178
76269
  function fanoutsDir(cwd) {
76179
76270
  const root = storeRoot(cwd);
76180
76271
  if (!root.ok)
@@ -76215,20 +76306,29 @@ function parseStored(raw) {
76215
76306
  verify: list(item.verify),
76216
76307
  forbidden: list(item.forbidden),
76217
76308
  context: typeof item.context === "string" ? item.context : null,
76218
- timebox: typeof item.timebox === "string" ? item.timebox : null
76309
+ timebox: typeof item.timebox === "string" ? item.timebox : null,
76310
+ hash: text(item.hash),
76311
+ hashSince: text(item.hashSince) || text(raw.createdAt)
76219
76312
  });
76220
76313
  }
76314
+ for (const slice of slices)
76315
+ slice.hash ||= sliceHash(slice);
76221
76316
  const shared = globs(raw.shared);
76222
76317
  const ids = new Set(slices.map((slice) => slice.id));
76223
76318
  if (!globsValid || ids.size !== slices.length || slices.some((slice) => slice.dependsOn.some((dep) => dep === slice.id || !ids.has(dep))) || findCycle(slices))
76224
76319
  return null;
76320
+ const fanIn = raw.fanIn === "integration" ? "integration" : "prs";
76321
+ const createdAt = text(raw.createdAt);
76225
76322
  return {
76226
76323
  v: typeof raw.v === "number" ? raw.v : FANOUT_VERSION,
76227
76324
  name: raw.name,
76228
76325
  trunk: raw.trunk,
76326
+ fanIn,
76229
76327
  shared,
76230
76328
  slices,
76231
- createdAt: text(raw.createdAt),
76329
+ leadSession: typeof raw.leadSession === "string" && raw.leadSession ? raw.leadSession : null,
76330
+ formerLeads: list(raw.formerLeads),
76331
+ createdAt,
76232
76332
  updatedAt: text(raw.updatedAt)
76233
76333
  };
76234
76334
  }
@@ -76308,6 +76408,11 @@ function planFanout(cwd, input, now = new Date) {
76308
76408
  const dir = fanoutsDir(cwd);
76309
76409
  if (!dir.ok)
76310
76410
  return dir;
76411
+ const session = input.actor.session;
76412
+ if (input.takeLead && !session)
76413
+ return fanoutFail("blocked", "--take-lead needs a session: WORKIT_SESSION_ID is not set", "export WORKIT_SESSION_ID=<your session id>, then plan again with --take-lead");
76414
+ if (input.takeLead && isDelegateSession(session))
76415
+ return fanoutFail("blocked", `session ${session} is a worker or verifier id; only a lead takes the lead`, "run --take-lead from the lead's own session");
76311
76416
  const parsed = parsePlan(input.trunk && isRecord8(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, {
76312
76417
  trunk: input.trunk ?? defaultTrunk(cwd),
76313
76418
  root: mainCheckout(cwd),
@@ -76316,6 +76421,8 @@ function planFanout(cwd, input, now = new Date) {
76316
76421
  if (!parsed.ok)
76317
76422
  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 });
76318
76423
  const plan = parsed.plan;
76424
+ if (plan.fanIn === "integration" && plan.trunk === defaultTrunk(cwd))
76425
+ return fanoutFail("invalid_input", `fanIn "integration" needs an integration branch as the trunk, not ${plan.trunk}`, `workit git branch <integration-branch> --base ${plan.trunk}, then plan with --trunk <integration-branch>`);
76319
76426
  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`);
76320
76427
  if (badBranches.length)
76321
76428
  return fanoutFail("invalid_input", badBranches[0], "rename the branch in the plan", {
@@ -76344,20 +76451,50 @@ function planFanout(cwd, input, now = new Date) {
76344
76451
  const existing = readFanout(cwd, plan.name);
76345
76452
  const previous = existing.ok ? existing.data : null;
76346
76453
  plan.createdAt = previous?.createdAt || now.toISOString();
76454
+ plan.leadSession = input.takeLead ? session : previous ? previous.leadSession : session;
76455
+ plan.formerLeads = [
76456
+ ...new Set([
76457
+ ...previous?.formerLeads ?? [],
76458
+ ...previous?.leadSession && previous.leadSession !== plan.leadSession ? [previous.leadSession] : []
76459
+ ])
76460
+ ].filter((former) => former !== plan.leadSession);
76347
76461
  plan.updatedAt = now.toISOString();
76462
+ for (const slice of plan.slices) {
76463
+ slice.hash = sliceHash(slice);
76464
+ const before = previous?.slices.find((old) => old.id === slice.id);
76465
+ slice.hashSince = before && before.hash === slice.hash ? before.hashSince || plan.updatedAt : plan.updatedAt;
76466
+ }
76348
76467
  const written = writeFanout(cwd, plan);
76349
76468
  if (!written.ok)
76350
76469
  return written;
76470
+ if (previous && plan.leadSession !== previous.leadSession)
76471
+ appendObserved(cwd, {
76472
+ type: "fanout.lead.changed",
76473
+ actor: input.actor,
76474
+ fanout: plan.name,
76475
+ from: previous.leadSession,
76476
+ to: plan.leadSession
76477
+ });
76351
76478
  appendObserved(cwd, {
76352
76479
  type: "fanout.planned",
76353
76480
  actor: input.actor,
76354
76481
  fanout: plan.name,
76355
76482
  trunk: plan.trunk,
76483
+ fanIn: plan.fanIn,
76356
76484
  slices: plan.slices.length,
76357
76485
  ids: plan.slices.slice(0, 20).map((slice) => slice.id)
76358
76486
  });
76359
76487
  const grouped = waves(plan.slices);
76360
76488
  const notes = [];
76489
+ const warnings = [];
76490
+ if (!input.takeLead && previous?.leadSession && session && session !== previous.leadSession)
76491
+ notes.push(`the lead stays ${previous.leadSession}; this session is ${session}. Leading from this session now? workit fanout plan <plan.json> --take-lead`);
76492
+ if (previous === null) {
76493
+ const ledger = readLedger(cwd);
76494
+ const inForce = ledger.ok ? activeStanding(ledger.value.rows, plan.name, planLeads(plan)) : [];
76495
+ if (inForce.length)
76496
+ warnings.push(`${inForce.length} standing order${inForce.length === 1 ? " is" : "s are"} already in force for fanout ${plan.name}, and every brief will carry ${inForce.length === 1 ? "it" : "them"}: ${inForce.map((row) => `"${String(row.what)}"`).join("; ")}. From an earlier run? workit ledger standing clear --fanout ${plan.name}`);
76497
+ }
76361
76498
  if (!ref)
76362
76499
  notes.push(`trunk ${plan.trunk} does not resolve here; only sample paths of each glob were compared`);
76363
76500
  grouped.forEach((wave, index) => {
@@ -76372,6 +76509,7 @@ function planFanout(cwd, input, now = new Date) {
76372
76509
  data: {
76373
76510
  name: plan.name,
76374
76511
  trunk: plan.trunk,
76512
+ fanIn: plan.fanIn,
76375
76513
  file: written.data,
76376
76514
  created: previous === null,
76377
76515
  slices: plan.slices.map(({ id, branch, base, worktree, tier, dependsOn, scope, owns }) => ({
@@ -76387,11 +76525,12 @@ function planFanout(cwd, input, now = new Date) {
76387
76525
  overlaps,
76388
76526
  waves: grouped,
76389
76527
  landingOrder: landingOrder(plan.slices),
76390
- notes
76528
+ notes,
76529
+ warnings
76391
76530
  }
76392
76531
  };
76393
76532
  }
76394
- var FANOUT_VERSION = 1, TIERS, fanoutFail = (code, error, unblock, data) => ({
76533
+ var FANOUT_VERSION = 1, TIERS, FAN_IN_MODES, fanoutFail = (code, error, unblock, data) => ({
76395
76534
  ok: false,
76396
76535
  code,
76397
76536
  error,
@@ -76431,12 +76570,13 @@ var FANOUT_VERSION = 1, TIERS, fanoutFail = (code, error, unblock, data) => ({
76431
76570
  stdout: run.stdout ?? "",
76432
76571
  stderr: run.error ? String(run.error.message) : run.stderr ?? ""
76433
76572
  };
76434
- }, nulList2 = (text) => text.split("\x00").filter(Boolean), IN_FLIGHT_CAP = 6;
76573
+ }, nulList2 = (text) => text.split("\x00").filter(Boolean), planLeads = (plan) => plan.leadSession ? [plan.leadSession, ...plan.formerLeads] : null, IN_FLIGHT_CAP = 6;
76435
76574
  var init_fanout = __esm(() => {
76436
76575
  init_rev();
76437
76576
  init_ledger();
76438
76577
  init_stack();
76439
76578
  TIERS = ["mundane", "standard", "hard"];
76579
+ FAN_IN_MODES = ["prs", "integration"];
76440
76580
  DEFAULT_SHARED = [
76441
76581
  "**/package.json",
76442
76582
  "**/{package-lock.json,npm-shrinkwrap.json,bun.lock,bun.lockb,pnpm-lock.yaml,pnpm-workspace.yaml,yarn.lock}",
@@ -76452,6 +76592,488 @@ var init_fanout = __esm(() => {
76452
76592
  ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
76453
76593
  });
76454
76594
 
76595
+ // packages/workit-core/src/fanout-worktree.ts
76596
+ import { randomBytes as randomBytes4 } from "node:crypto";
76597
+ import fs24 from "node:fs";
76598
+ import path44 from "node:path";
76599
+ function rowSample(lines) {
76600
+ const out = [];
76601
+ let bytes = 0;
76602
+ for (const line of lines) {
76603
+ const entry = line.slice(0, 200);
76604
+ bytes += Buffer.byteLength(entry) + 3;
76605
+ if (bytes > DIRTY_ROW_BYTES)
76606
+ break;
76607
+ out.push(entry);
76608
+ }
76609
+ return out;
76610
+ }
76611
+ function listWorktrees(cwd) {
76612
+ const out = [];
76613
+ let current = null;
76614
+ for (const field of gitRun4(cwd, ["worktree", "list", "--porcelain", "-z"]).stdout.split("\x00")) {
76615
+ if (field.startsWith("worktree ")) {
76616
+ current = { path: field.slice(9), head: null, branch: null, locked: false };
76617
+ out.push(current);
76618
+ } else if (!current)
76619
+ continue;
76620
+ else if (field.startsWith("HEAD "))
76621
+ current.head = field.slice(5);
76622
+ else if (field.startsWith("branch "))
76623
+ current.branch = field.slice(7);
76624
+ else if (field === "locked" || field.startsWith("locked "))
76625
+ current.locked = true;
76626
+ }
76627
+ return out;
76628
+ }
76629
+ function createdRow(cwd, plan, slice, target) {
76630
+ const ledger = readLedger(cwd);
76631
+ if (!ledger.ok)
76632
+ return null;
76633
+ const atPath = (row) => row.fanout === plan.name && row.slice === slice.id && typeof row.path === "string" && samePath(row.path, target);
76634
+ const rows = ledger.value.rows;
76635
+ const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && atPath(candidate));
76636
+ if (!row)
76637
+ return null;
76638
+ const removedSince = rows.some((candidate) => candidate.seq > row.seq && candidate.type === "fanout.worktree.released" && candidate.outcome === "removed" && atPath(candidate));
76639
+ if (removedSince)
76640
+ return null;
76641
+ const admin = adminDir(target);
76642
+ if (!admin)
76643
+ return null;
76644
+ if (typeof row.adminToken === "string")
76645
+ return row.adminToken === admin.token && row.adminId === admin.id && row.adminIno === admin.ino ? { row, exact: true } : null;
76646
+ const made = admin.birthMs ?? admin.mtimeMs;
76647
+ if (made + CLOCK_SLACK_MS < Date.parse(row.at))
76648
+ return null;
76649
+ return { row, exact: false };
76650
+ }
76651
+ function adminDir(worktree) {
76652
+ const dir = gitRun4(worktree, ["rev-parse", "--absolute-git-dir"]);
76653
+ if (!dir.ok)
76654
+ return null;
76655
+ const file = dir.stdout.trim();
76656
+ let token = null;
76657
+ try {
76658
+ token = fs24.readFileSync(path44.join(file, TOKEN_FILE), "utf8").trim() || null;
76659
+ } catch {
76660
+ token = null;
76661
+ }
76662
+ try {
76663
+ const stat = worktreeDeps.stat(file);
76664
+ return {
76665
+ path: file,
76666
+ id: path44.basename(file),
76667
+ ino: stat.ino.toString(),
76668
+ token,
76669
+ birthMs: stat.birthtimeNs > 0n ? Number(stat.birthtimeMs) : null,
76670
+ mtimeMs: Number(stat.mtimeMs)
76671
+ };
76672
+ } catch {
76673
+ return null;
76674
+ }
76675
+ }
76676
+ function markAdminDir(worktree) {
76677
+ const admin = adminDir(worktree);
76678
+ if (!admin)
76679
+ return null;
76680
+ const token = randomBytes4(16).toString("hex");
76681
+ try {
76682
+ fs24.writeFileSync(path44.join(admin.path, TOKEN_FILE), `${token}
76683
+ `);
76684
+ } catch {
76685
+ return null;
76686
+ }
76687
+ return { ...admin, token };
76688
+ }
76689
+ function findSlice(plan, id) {
76690
+ const slice = plan.slices.find((candidate) => candidate.id === id);
76691
+ return slice ? { ok: true, data: slice } : fanoutFail("invalid_input", `${id}: not a slice of fanout ${plan.name}`, `slices: ${plan.slices.map((candidate) => candidate.id).join(", ")}`);
76692
+ }
76693
+ function excludeScratch(cwd) {
76694
+ const common = gitCommonDir(cwd);
76695
+ if (!common)
76696
+ return;
76697
+ const file = path44.join(common, "info", "exclude");
76698
+ let text = "";
76699
+ try {
76700
+ text = fs24.readFileSync(file, "utf8");
76701
+ } catch {
76702
+ text = "";
76703
+ }
76704
+ if (text.split(/\r?\n/u).includes(EXCLUDE_LINE))
76705
+ return;
76706
+ fs24.mkdirSync(path44.dirname(file), { recursive: true });
76707
+ fs24.appendFileSync(file, `${text && !text.endsWith(`
76708
+ `) ? `
76709
+ ` : ""}# workit fanout: per-worker scratch dirs
76710
+ ${EXCLUDE_LINE}
76711
+ `);
76712
+ }
76713
+ function ensureScratch(worktree) {
76714
+ const scratch = path44.join(worktree, SCRATCH_DIR);
76715
+ fs24.mkdirSync(scratch, { recursive: true });
76716
+ return scratch;
76717
+ }
76718
+ function createSliceWorktree(cwd, plan, input) {
76719
+ const found = findSlice(plan, input.slice);
76720
+ if (!found.ok)
76721
+ return found;
76722
+ const slice = found.data;
76723
+ if (!slice.worktree)
76724
+ return fanoutFail("invalid_input", `slice ${slice.id} has no worktree path`, "re-plan it with workit fanout plan");
76725
+ const target = slicePath(cwd, slice);
76726
+ const worktrees = listWorktrees(cwd);
76727
+ const localRef = `refs/heads/${slice.branch}`;
76728
+ const existing = worktrees.find((entry) => samePath(entry.path, target));
76729
+ const base = {
76730
+ fanout: plan.name,
76731
+ slice: slice.id,
76732
+ branch: slice.branch,
76733
+ base: slice.base,
76734
+ path: target
76735
+ };
76736
+ if (existing) {
76737
+ if (existing.branch !== null && existing.branch !== localRef)
76738
+ return fanoutFail("blocked", `${target} is a worktree on ${existing.branch.replace(/^refs\/heads\//u, "")}, not ${slice.branch}`, `release it first, or set slice ${slice.id}'s worktree to another path`);
76739
+ if (!createdRow(cwd, plan, slice, target))
76740
+ return fanoutFail("blocked", `${target} is a worktree that workit fanout worktree create did not make for slice ${slice.id}`, `remove it yourself (git worktree remove ${target}) once you know what it holds`);
76741
+ excludeScratch(cwd);
76742
+ return {
76743
+ ok: true,
76744
+ data: {
76745
+ ...base,
76746
+ scratch: ensureScratch(target),
76747
+ head: existing.head ?? "",
76748
+ mode: resolveRef(cwd, localRef) ? "resume" : "new",
76749
+ created: false,
76750
+ notes: ["the worktree already exists; nothing was changed"]
76751
+ }
76752
+ };
76753
+ }
76754
+ const elsewhere = worktrees.find((entry) => entry.branch === localRef);
76755
+ if (elsewhere)
76756
+ return fanoutFail("blocked", `${slice.branch} is checked out at ${elsewhere.path}: never two live workers on one branch`, `stop that worker, then workit fanout worktree release ${slice.id} (or git worktree remove ${elsewhere.path})`);
76757
+ let dirExisted = false;
76758
+ try {
76759
+ const stat = fs24.statSync(target);
76760
+ if (!stat.isDirectory() || fs24.readdirSync(target).length > 0)
76761
+ return fanoutFail("blocked", `${target} exists and is ${stat.isDirectory() ? "not empty" : "not a directory"} (not a worktree of this repository)`, `move it away, or set slice ${slice.id}'s worktree to another path`);
76762
+ dirExisted = true;
76763
+ } catch (error) {
76764
+ if (error.code !== "ENOENT")
76765
+ return fanoutFail("failed", `cannot read ${target}: ${error.message}`);
76766
+ }
76767
+ const notes = [];
76768
+ let mode = "resume";
76769
+ if (resolveRef(cwd, localRef)) {
76770
+ const added = gitRun4(cwd, ["worktree", "add", target, slice.branch], SLOW);
76771
+ if (!added.ok)
76772
+ return worktreeFailed(added.stderr);
76773
+ } else {
76774
+ const remote = pushRemoteName(cwd) ?? "origin";
76775
+ const remoteRef = `refs/remotes/${remote}/${slice.branch}`;
76776
+ if (resolveRef(cwd, remoteRef)) {
76777
+ const added = gitRun4(cwd, ["worktree", "add", "-b", slice.branch, target, remoteRef], SLOW);
76778
+ if (!added.ok)
76779
+ return worktreeFailed(added.stderr);
76780
+ notes.push(`resumed ${slice.branch} from ${remote}/${slice.branch}`);
76781
+ } else {
76782
+ mode = "new";
76783
+ const parent = plan.slices.find((other) => other.branch === slice.base);
76784
+ const from = slice.base === plan.trunk ? trunkRef(cwd, plan.trunk) : parent ? branchRef(cwd, parent.branch) : branchRef(cwd, slice.base);
76785
+ if (!from)
76786
+ return fanoutFail("not_found", `base ${slice.base} of slice ${slice.id} does not resolve here`, parent ? `start slice ${parent.id} first (its branch ${parent.branch} is the base)` : `git fetch origin ${slice.base}`);
76787
+ const added = gitRun4(cwd, ["worktree", "add", "--detach", target, from], SLOW);
76788
+ if (!added.ok)
76789
+ return worktreeFailed(added.stderr);
76790
+ const branched = gitBranch(target, { name: slice.branch, base: slice.base });
76791
+ if (!branched.ok) {
76792
+ gitRun4(cwd, ["worktree", "remove", target], SLOW);
76793
+ return fanoutFail(branched.code, `workit git branch ${slice.branch} refused: ${branched.error}`, branched.unblock ?? `fix the branch name in the plan, then workit fanout worktree create ${slice.id}`);
76794
+ }
76795
+ notes.push(...branched.data.notes);
76796
+ }
76797
+ }
76798
+ excludeScratch(cwd);
76799
+ const scratch = ensureScratch(target);
76800
+ const head = gitRun4(target, ["rev-parse", "HEAD"]).stdout.trim();
76801
+ const baseSha = mode === "new" ? head : null;
76802
+ const admin = markAdminDir(target);
76803
+ appendObserved(cwd, {
76804
+ type: "fanout.worktree.created",
76805
+ actor: input.actor,
76806
+ fanout: plan.name,
76807
+ slice: slice.id,
76808
+ branch: slice.branch,
76809
+ path: target,
76810
+ scratch,
76811
+ mode,
76812
+ head,
76813
+ dirExisted,
76814
+ planCreatedAt: plan.createdAt,
76815
+ sliceHash: slice.hash,
76816
+ ...admin ? { adminId: admin.id, adminIno: admin.ino, adminToken: admin.token } : {},
76817
+ ...baseSha ? { baseSha } : {}
76818
+ });
76819
+ return { ok: true, data: { ...base, scratch, head, mode, created: true, notes } };
76820
+ }
76821
+ function releaseSliceWorktree(cwd, plan, input) {
76822
+ const found = findSlice(plan, input.slice);
76823
+ if (!found.ok)
76824
+ return found;
76825
+ const slice = found.data;
76826
+ const target = slicePath(cwd, slice);
76827
+ const worktrees = listWorktrees(cwd);
76828
+ const index = worktrees.findIndex((entry) => samePath(entry.path, target));
76829
+ const out = {
76830
+ fanout: plan.name,
76831
+ slice: slice.id,
76832
+ branch: slice.branch,
76833
+ path: target,
76834
+ head: null,
76835
+ dirty: [],
76836
+ forced: input.force,
76837
+ removed: false,
76838
+ unpushed: null,
76839
+ notes: []
76840
+ };
76841
+ if (index < 0) {
76842
+ if (fs24.existsSync(target))
76843
+ return fanoutFail("blocked", `${target} is not a worktree of this repository; nothing was removed`, "remove it yourself once you know what it holds");
76844
+ out.notes.push(`no worktree at ${target}; nothing to release`);
76845
+ return { ok: true, data: out };
76846
+ }
76847
+ const entry = worktrees[index];
76848
+ if (index === 0)
76849
+ return fanoutFail("blocked", `${target} is the main checkout; it is never released`);
76850
+ if (inside2(cwd, entry.path))
76851
+ return fanoutFail("blocked", `this command runs inside ${entry.path}`, `run it from the main checkout: workit --cwd <main checkout> fanout worktree release ${slice.id}`);
76852
+ if (entry.branch !== null && entry.branch !== `refs/heads/${slice.branch}`)
76853
+ return fanoutFail("blocked", `${entry.path} is on ${entry.branch.replace(/^refs\/heads\//u, "")}, not slice ${slice.id}'s ${slice.branch}; nothing was removed`);
76854
+ const owned = createdRow(cwd, plan, slice, entry.path);
76855
+ if (!owned)
76856
+ return fanoutFail("blocked", `${entry.path} was not made by workit fanout worktree create for slice ${slice.id} (no ledger row); nothing was removed`, `remove it yourself (git worktree remove ${entry.path}) once you know what it holds`);
76857
+ if (input.force && !owned.exact)
76858
+ return fanoutFail("blocked", `${entry.path} was created before workit recorded worktree identity, so it cannot be told from a worktree someone re-added there; --force is refused and nothing was removed`, `release it without --force, or remove it yourself (git worktree remove ${entry.path}) once you know what it holds`);
76859
+ const made = owned.row;
76860
+ out.head = entry.head;
76861
+ const status = gitRun4(entry.path, [
76862
+ "status",
76863
+ "--porcelain=v1",
76864
+ "-z",
76865
+ "--untracked-files=all",
76866
+ "--ignore-submodules=none"
76867
+ ]);
76868
+ if (!status.ok)
76869
+ return fanoutFail("failed", `git status in ${entry.path} failed: ${status.stderr.trim().split(`
76870
+ `)[0]}`, "nothing was removed; inspect the worktree by hand");
76871
+ const entries = porcelainEntries(status.stdout);
76872
+ const inScratch = (line) => line.slice(3).startsWith(`${SCRATCH_DIR}/`);
76873
+ out.dirty = entries.filter((line) => !inScratch(line));
76874
+ out.unpushed = unpushedCount(cwd, slice.branch);
76875
+ const refused = out.dirty.length > 0 && !input.force;
76876
+ const recorded = appendObserved(cwd, {
76877
+ type: "fanout.worktree.released",
76878
+ actor: input.actor,
76879
+ fanout: plan.name,
76880
+ slice: slice.id,
76881
+ branch: slice.branch,
76882
+ path: entry.path,
76883
+ head: entry.head,
76884
+ uncommittedCount: out.dirty.length,
76885
+ uncommitted: rowSample(out.dirty),
76886
+ forced: input.force,
76887
+ outcome: refused ? "refused" : "removed"
76888
+ });
76889
+ if (!recorded.ok)
76890
+ return fanoutFail("unavailable", `cannot record the worktree state in the ledger (${recorded.error}); nothing was removed`);
76891
+ if (refused)
76892
+ return fanoutFail("blocked", `${entry.path} has ${out.dirty.length} uncommitted change${out.dirty.length === 1 ? "" : "s"} (${out.dirty.slice(0, 3).join(", ")}${out.dirty.length > 3 ? ", …" : ""}); recorded in the ledger, nothing removed`, `commit them on ${slice.branch} (workit git commit), or pass --force to drop them`, { ...out });
76893
+ const args = ["worktree", "remove", ...input.force ? ["--force"] : [], entry.path];
76894
+ let removed = gitRun4(cwd, args, SLOW);
76895
+ if (!removed.ok && entries.some(inScratch)) {
76896
+ removeScratch(entry.path);
76897
+ removed = gitRun4(cwd, args, SLOW);
76898
+ }
76899
+ if (!removed.ok)
76900
+ return fanoutFail("failed", `git worktree remove failed: ${removed.stderr.trim().split(`
76901
+ `)[0]}`, entry.locked ? `git worktree unlock ${entry.path}, then release again` : undefined, { ...out });
76902
+ out.removed = true;
76903
+ if (made.dirExisted === true)
76904
+ fs24.mkdirSync(entry.path, { recursive: true });
76905
+ if (out.unpushed)
76906
+ out.notes.push(`${slice.branch} is kept with ${out.unpushed} commit(s) not on the remote`);
76907
+ return { ok: true, data: out };
76908
+ }
76909
+ function porcelainEntries(stdout) {
76910
+ const fields = nulList2(stdout);
76911
+ const out = [];
76912
+ for (let index = 0;index < fields.length; index += 1) {
76913
+ const entry = fields[index];
76914
+ out.push(entry);
76915
+ if (entry[0] === "R" || entry[0] === "C")
76916
+ index += 1;
76917
+ }
76918
+ return out;
76919
+ }
76920
+ function unpushedCount(cwd, branch) {
76921
+ const remote = pushRemoteName(cwd) ?? "origin";
76922
+ const local = resolveRef(cwd, `refs/heads/${branch}`);
76923
+ if (!local)
76924
+ return null;
76925
+ const tracking = resolveRef(cwd, `refs/remotes/${remote}/${branch}`);
76926
+ if (!tracking)
76927
+ return null;
76928
+ const count = gitRun4(cwd, ["rev-list", "--count", `${tracking}..${local}`]);
76929
+ return count.ok ? Number(count.stdout.trim()) : null;
76930
+ }
76931
+ function removeScratch(worktree) {
76932
+ const scratch = path44.join(worktree, SCRATCH_DIR);
76933
+ let stat;
76934
+ try {
76935
+ stat = fs24.lstatSync(scratch);
76936
+ } catch {
76937
+ return;
76938
+ }
76939
+ if (stat.isSymbolicLink() || !stat.isDirectory())
76940
+ fs24.unlinkSync(scratch);
76941
+ else
76942
+ fs24.rmSync(scratch, { recursive: true, force: true, maxRetries: 2 });
76943
+ }
76944
+ var SLOW, SCRATCH_DIR = ".workit-scratch", EXCLUDE_LINE, DIRTY_ROW_BYTES = 2000, canonical2 = (target) => {
76945
+ try {
76946
+ return fs24.realpathSync.native(target);
76947
+ } catch {
76948
+ return path44.resolve(target);
76949
+ }
76950
+ }, samePath = (a, b) => {
76951
+ const left = canonical2(a);
76952
+ const right = canonical2(b);
76953
+ return process.platform === "win32" || process.platform === "darwin" ? left.toLowerCase() === right.toLowerCase() : left === right;
76954
+ }, inside2 = (child, parent) => {
76955
+ const relative = path44.relative(canonical2(parent), canonical2(child));
76956
+ return relative === "" || !relative.startsWith("..") && !path44.isAbsolute(relative);
76957
+ }, slicePath = (cwd, slice) => path44.isAbsolute(slice.worktree) ? slice.worktree : path44.resolve(mainCheckout(cwd), slice.worktree), CLOCK_SLACK_MS = 2000, TOKEN_FILE = "workit-fanout-token", worktreeDeps, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
76958
+ `)[0] || "unknown error"}`);
76959
+ var init_fanout_worktree = __esm(() => {
76960
+ init_ops();
76961
+ init_rev();
76962
+ init_ledger();
76963
+ init_fanout();
76964
+ SLOW = GIT_TIMEOUTS.worktree;
76965
+ EXCLUDE_LINE = `/${SCRATCH_DIR}/`;
76966
+ worktreeDeps = {
76967
+ stat: (file) => fs24.statSync(file, { bigint: true })
76968
+ };
76969
+ });
76970
+
76971
+ // packages/workit-core/src/fanout-brief.ts
76972
+ import { createHash as createHash17 } from "node:crypto";
76973
+ function workerSession(lead, slice, attempt) {
76974
+ const tail = attempt === null ? "" : `+try${attempt}`;
76975
+ const full = `${lead}-w-${slice}${tail}`;
76976
+ if (full.length <= SESSION_MAX)
76977
+ return full;
76978
+ const digest = createHash17("sha256").update(slice).digest("hex").slice(0, 12);
76979
+ return `${lead.slice(0, SESSION_MAX - 3 - digest.length - tail.length)}-w-${digest}${tail}`;
76980
+ }
76981
+ function renderBrief(cwd, plan, input) {
76982
+ const found = findSlice(plan, input.slice);
76983
+ if (!found.ok)
76984
+ return found;
76985
+ const slice = found.data;
76986
+ const ledger = readLedger(cwd);
76987
+ if (!ledger.ok)
76988
+ return fanoutFail(ledger.code, ledger.error, ledger.unblock);
76989
+ if (!input.actor.session)
76990
+ return fanoutFail("blocked", "WORKIT_SESSION_ID is not set: worker session ids derive from the lead's", "export WORKIT_SESSION_ID=<your session id>, then render the brief again");
76991
+ const branch = input.attempt === null ? slice.branch : `${slice.branch}-try${input.attempt}`;
76992
+ const taken = plan.slices.find((other) => other.id !== slice.id && other.branch === branch);
76993
+ if (taken)
76994
+ return fanoutFail("invalid_input", `attempt branch ${branch} is slice ${taken.id}'s branch`, "pick another --attempt number, or rename one of the branches in the plan");
76995
+ const head = branchRef(cwd, branch);
76996
+ const mode = input.mode ?? (head ? "resume" : "new");
76997
+ if (mode === "resume" && !head)
76998
+ return fanoutFail("not_found", `MODE resume needs the branch ${branch}, which does not exist here`, `workit fanout brief ${slice.id} --mode new`);
76999
+ const planned = `${slicePath(cwd, slice)}${input.attempt === null ? "" : `+try${input.attempt}`}`;
77000
+ const exists = listWorktrees(cwd).some((entry) => samePath(entry.path, planned));
77001
+ excludeScratch(cwd);
77002
+ const scratch = exists ? `${planned}/${SCRATCH_DIR}` : SCRATCH_DIR;
77003
+ const scratchLine = exists ? `SCRATCH: ${scratch} (yours alone; temp files go here, never a shared path)` : `SCRATCH: ${SCRATCH_DIR}/ at your worktree root (mkdir -p it; git ignores it; never a shared path)`;
77004
+ const standing = activeStanding(ledger.value.rows, plan.name, planLeads(plan)).map((row) => ({
77005
+ id: row.id,
77006
+ what: String(row.what)
77007
+ }));
77008
+ const session = workerSession(sessionSafe(input.actor.session), slice.id, input.attempt);
77009
+ const fanIn = plan.fanIn === "integration" ? [
77010
+ `FAN-IN: integration branch ${plan.trunk}. Before you report, merge its tip into your branch`,
77011
+ ` (\`git merge --no-edit ${plan.trunk}\`), resolve conflicts inside SCOPE and re-run VERIFY, so the`,
77012
+ " lead's merge is a fast-forward. No other merge, and no rebase, retarget or force-push."
77013
+ ] : [
77014
+ "FAN-IN: one PR per slice. Never rebase, retarget, merge or force-push: the lead owns topology."
77015
+ ];
77016
+ const lines = [
77017
+ `MODE: ${mode}${input.attempt === null ? "" : ` (race attempt ${input.attempt}: the lead keeps the attempt with the best verdict)`}`,
77018
+ `GOAL: ${slice.goal}`,
77019
+ `SCOPE: ${slice.scope.join(", ")}`,
77020
+ ...slice.owns.length ? [` owns: ${slice.owns.join(", ")}`] : [],
77021
+ ` branch: ${branch} base: ${slice.base}`,
77022
+ `CONTEXT: ${slice.context ?? "none given: read the files in SCOPE and the neighbours they import"}`,
77023
+ ...mode === "resume" && head ? [
77024
+ ` resume: continue ${branch} from ${head.slice(0, 12)}; read \`git log ${slice.base}..${branch}\` and \`workit ledger list --branch ${branch}\` (the last report, verdicts, rulings)`
77025
+ ] : [],
77026
+ "ACCEPTANCE:",
77027
+ ...slice.acceptance.map((line) => ` - ${line}`),
77028
+ `VERIFY: ${slice.verify.join("; ")}`,
77029
+ `TIER: ${slice.tier}`,
77030
+ `TIMEBOX: ${slice.timebox ?? DEFAULT_TIMEBOX}; past it without a new commit you will be replaced`,
77031
+ scratchLine,
77032
+ `FORBIDDEN: ${slice.forbidden.join("; ")}`,
77033
+ ...fanIn,
77034
+ ...REPORT,
77035
+ "STANDING:",
77036
+ ...standing.length ? standing.map((order) => ` - ${order.what}`) : [" - none recorded beyond this brief"],
77037
+ ` export WORKIT_SESSION_ID=${session}`,
77038
+ ...rules(mode, branch, slice.base)
77039
+ ];
77040
+ return {
77041
+ ok: true,
77042
+ data: {
77043
+ fanout: plan.name,
77044
+ slice: slice.id,
77045
+ mode,
77046
+ attempt: input.attempt,
77047
+ branch,
77048
+ base: slice.base,
77049
+ tier: slice.tier,
77050
+ scratch,
77051
+ session,
77052
+ standing,
77053
+ text: `${lines.join(`
77054
+ `)}
77055
+ `
77056
+ }
77057
+ };
77058
+ }
77059
+ var DEFAULT_TIMEBOX = "30 minutes", REPORT, rules = (mode, branch, base) => [
77060
+ "RULES:",
77061
+ mode === "new" ? ` 1. First command: \`workit git branch ${branch} --base ${base}\` (your worktree may start on another name).` : ` 1. First command: \`git switch ${branch}\`, then continue from its head.`,
77062
+ ' 2. Decide ambiguities yourself: `workit ledger ruling "<what>" --why "<why>" --cost-if-wrong "<cost>"`; stop only for an irreversible, security-sensitive or out-of-worktree action.',
77063
+ " 3. Commit with `workit git commit -m <msg> -- <paths in SCOPE>`; push only if this brief says so.",
77064
+ " 4. Never record a verdict on your own work."
77065
+ ], sessionSafe = (text) => text.replace(/[^A-Za-z0-9_.:@/+-]/gu, "-"), SESSION_MAX = 128;
77066
+ var init_fanout_brief = __esm(() => {
77067
+ init_ledger();
77068
+ init_fanout();
77069
+ init_fanout_worktree();
77070
+ REPORT = [
77071
+ "REPORT: branch, head SHA, files changed, each VERIFY command with its exit code,",
77072
+ " each ACCEPTANCE line met / not met, rulings you made (`workit ledger ruling`),",
77073
+ " anything out of scope as a follow-up, not a diff."
77074
+ ];
77075
+ });
77076
+
76455
77077
  // packages/workit-core/src/fanout-landed.ts
76456
77078
  import { spawnSync as spawnSync22 } from "node:child_process";
76457
77079
  function forgeBreaker(resolved) {
@@ -76562,9 +77184,20 @@ function trunkPatchIds(ctx, fork) {
76562
77184
  cache.set(fork, ids);
76563
77185
  return ids;
76564
77186
  }
77187
+ function inSliceRun(row, slice) {
77188
+ if (row.type === "fanout.worktree.created" && typeof row.sliceHash === "string" && slice.hash)
77189
+ return row.sliceHash === slice.hash;
77190
+ const since = Date.parse(slice.hashSince);
77191
+ return Number.isNaN(since) || Date.parse(String(row.at)) >= since;
77192
+ }
76565
77193
  function linked(rows, input, slice, head) {
76566
77194
  const since = input.since ? Date.parse(input.since) : Number.NaN;
76567
- return rows.some((row) => (Number.isNaN(since) || Date.parse(String(row.at)) >= since) && (row.type === "fanout.worktree.created" && row.fanout === input.fanout && row.slice === slice.id || head !== null && row.branch === slice.branch && row.head === head));
77195
+ const recent = (row) => Number.isNaN(since) || Date.parse(String(row.at)) >= since;
77196
+ return rows.some((row) => {
77197
+ if (row.type === "fanout.worktree.created" && row.fanout === input.fanout && row.slice === slice.id)
77198
+ return typeof row.sliceHash === "string" && input.sliceHash ? row.sliceHash === input.sliceHash : recent(row);
77199
+ return recent(row) && head !== null && row.branch === slice.branch && row.head === head;
77200
+ });
76568
77201
  }
76569
77202
  function detectLanding(ctx, slice, input) {
76570
77203
  const out = {
@@ -76637,7 +77270,7 @@ function sliceStart(cwd, rows, fanout, slice) {
76637
77270
  const reflog = branchCreatedAt(cwd, slice.branch);
76638
77271
  if (reflog)
76639
77272
  return reflog;
76640
- const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string");
77273
+ const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string" && inSliceRun(candidate, slice));
76641
77274
  return row?.baseSha ?? null;
76642
77275
  }
76643
77276
  var isAncestor2 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
@@ -76786,7 +77419,8 @@ function sliceLandings(cwd, plan, trunk, tips, rows, forge) {
76786
77419
  createdFrom: sliceStart(cwd, rows, plan.name, slice),
76787
77420
  parentHead,
76788
77421
  fanout: plan.name,
76789
- since: plan.createdAt || null,
77422
+ since: slice.hashSince || plan.createdAt || null,
77423
+ sliceHash: slice.hash || null,
76790
77424
  rows
76791
77425
  }));
76792
77426
  }
@@ -76928,7 +77562,7 @@ function activity(cwd, plan, slice, rows) {
76928
77562
  head,
76929
77563
  commit,
76930
77564
  last: newest(commit, reflog, ...ledger),
76931
- worktreeCreated: rows.some((row) => row.type === "fanout.worktree.created" && row.fanout === plan.name && row.slice === slice.id)
77565
+ worktreeCreated: rows.some((row) => row.type === "fanout.worktree.created" && row.fanout === plan.name && row.slice === slice.id && inSliceRun(row, slice))
76932
77566
  };
76933
77567
  }
76934
77568
  function fanoutStatus(cwd, plan, options) {
@@ -77038,7 +77672,7 @@ function fanoutStatus(cwd, plan, options) {
77038
77672
  else if (row.branchExists)
77039
77673
  row.state = "active";
77040
77674
  else if (row.waitsFor.length === 0 || parent && row.waitsFor.length === 1 && row.waitsFor[0] === parent.id && out.get(parent.id)?.verdict.accepted) {
77041
- const created = rows.some((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === plan.name && candidate.slice === id);
77675
+ const created = rows.some((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === plan.name && candidate.slice === id && inSliceRun(candidate, byId.get(id)));
77042
77676
  row.state = created ? "active" : "not_started";
77043
77677
  if (!created)
77044
77678
  spawnable.push(id);
@@ -77090,340 +77724,6 @@ var init_fanout_status = __esm(() => {
77090
77724
  DEFAULT_STUCK_MS = 30 * 60000;
77091
77725
  });
77092
77726
 
77093
- // packages/workit-core/src/fanout-worktree.ts
77094
- import fs24 from "node:fs";
77095
- import path44 from "node:path";
77096
- function rowSample(lines) {
77097
- const out = [];
77098
- let bytes = 0;
77099
- for (const line of lines) {
77100
- const entry = line.slice(0, 200);
77101
- bytes += Buffer.byteLength(entry) + 3;
77102
- if (bytes > DIRTY_ROW_BYTES)
77103
- break;
77104
- out.push(entry);
77105
- }
77106
- return out;
77107
- }
77108
- function listWorktrees(cwd) {
77109
- const out = [];
77110
- let current = null;
77111
- for (const field of gitRun4(cwd, ["worktree", "list", "--porcelain", "-z"]).stdout.split("\x00")) {
77112
- if (field.startsWith("worktree ")) {
77113
- current = { path: field.slice(9), head: null, branch: null, locked: false };
77114
- out.push(current);
77115
- } else if (!current)
77116
- continue;
77117
- else if (field.startsWith("HEAD "))
77118
- current.head = field.slice(5);
77119
- else if (field.startsWith("branch "))
77120
- current.branch = field.slice(7);
77121
- else if (field === "locked" || field.startsWith("locked "))
77122
- current.locked = true;
77123
- }
77124
- return out;
77125
- }
77126
- function createdRow(cwd, plan, slice, target) {
77127
- const ledger = readLedger(cwd);
77128
- if (!ledger.ok)
77129
- return null;
77130
- const atPath = (row) => row.fanout === plan.name && row.slice === slice.id && typeof row.path === "string" && samePath(row.path, target);
77131
- const rows = ledger.value.rows;
77132
- const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && atPath(candidate));
77133
- if (!row)
77134
- return null;
77135
- const removedSince = rows.some((candidate) => candidate.seq > row.seq && candidate.type === "fanout.worktree.released" && candidate.outcome === "removed" && atPath(candidate));
77136
- if (removedSince)
77137
- return null;
77138
- const made = adminDirTime(target);
77139
- if (made === null || made + CLOCK_SLACK_MS < Date.parse(row.at))
77140
- return null;
77141
- return row;
77142
- }
77143
- function adminDirTime(worktree) {
77144
- const dir = gitRun4(worktree, ["rev-parse", "--absolute-git-dir"]);
77145
- if (!dir.ok)
77146
- return null;
77147
- try {
77148
- const stat = fs24.statSync(dir.stdout.trim());
77149
- return stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs;
77150
- } catch {
77151
- return null;
77152
- }
77153
- }
77154
- function findSlice(plan, id) {
77155
- const slice = plan.slices.find((candidate) => candidate.id === id);
77156
- return slice ? { ok: true, data: slice } : fanoutFail("invalid_input", `${id}: not a slice of fanout ${plan.name}`, `slices: ${plan.slices.map((candidate) => candidate.id).join(", ")}`);
77157
- }
77158
- function excludeScratch(cwd) {
77159
- const common = gitCommonDir(cwd);
77160
- if (!common)
77161
- return;
77162
- const file = path44.join(common, "info", "exclude");
77163
- let text = "";
77164
- try {
77165
- text = fs24.readFileSync(file, "utf8");
77166
- } catch {
77167
- text = "";
77168
- }
77169
- if (text.split(/\r?\n/u).includes(EXCLUDE_LINE))
77170
- return;
77171
- fs24.mkdirSync(path44.dirname(file), { recursive: true });
77172
- fs24.appendFileSync(file, `${text && !text.endsWith(`
77173
- `) ? `
77174
- ` : ""}# workit fanout: per-worker scratch dirs
77175
- ${EXCLUDE_LINE}
77176
- `);
77177
- }
77178
- function ensureScratch(worktree) {
77179
- const scratch = path44.join(worktree, SCRATCH_DIR);
77180
- fs24.mkdirSync(scratch, { recursive: true });
77181
- return scratch;
77182
- }
77183
- function createSliceWorktree(cwd, plan, input) {
77184
- const found = findSlice(plan, input.slice);
77185
- if (!found.ok)
77186
- return found;
77187
- const slice = found.data;
77188
- if (!slice.worktree)
77189
- return fanoutFail("invalid_input", `slice ${slice.id} has no worktree path`, "re-plan it with workit fanout plan");
77190
- const target = slicePath(cwd, slice);
77191
- const worktrees = listWorktrees(cwd);
77192
- const localRef = `refs/heads/${slice.branch}`;
77193
- const existing = worktrees.find((entry) => samePath(entry.path, target));
77194
- const base = {
77195
- fanout: plan.name,
77196
- slice: slice.id,
77197
- branch: slice.branch,
77198
- base: slice.base,
77199
- path: target
77200
- };
77201
- if (existing) {
77202
- if (existing.branch !== null && existing.branch !== localRef)
77203
- return fanoutFail("blocked", `${target} is a worktree on ${existing.branch.replace(/^refs\/heads\//u, "")}, not ${slice.branch}`, `release it first, or set slice ${slice.id}'s worktree to another path`);
77204
- if (!createdRow(cwd, plan, slice, target))
77205
- return fanoutFail("blocked", `${target} is a worktree that workit fanout worktree create did not make for slice ${slice.id}`, `remove it yourself (git worktree remove ${target}) once you know what it holds`);
77206
- excludeScratch(cwd);
77207
- return {
77208
- ok: true,
77209
- data: {
77210
- ...base,
77211
- scratch: ensureScratch(target),
77212
- head: existing.head ?? "",
77213
- mode: resolveRef(cwd, localRef) ? "resume" : "new",
77214
- created: false,
77215
- notes: ["the worktree already exists; nothing was changed"]
77216
- }
77217
- };
77218
- }
77219
- const elsewhere = worktrees.find((entry) => entry.branch === localRef);
77220
- if (elsewhere)
77221
- return fanoutFail("blocked", `${slice.branch} is checked out at ${elsewhere.path}: never two live workers on one branch`, `stop that worker, then workit fanout worktree release ${slice.id} (or git worktree remove ${elsewhere.path})`);
77222
- let dirExisted = false;
77223
- try {
77224
- const stat = fs24.statSync(target);
77225
- if (!stat.isDirectory() || fs24.readdirSync(target).length > 0)
77226
- return fanoutFail("blocked", `${target} exists and is ${stat.isDirectory() ? "not empty" : "not a directory"} (not a worktree of this repository)`, `move it away, or set slice ${slice.id}'s worktree to another path`);
77227
- dirExisted = true;
77228
- } catch (error) {
77229
- if (error.code !== "ENOENT")
77230
- return fanoutFail("failed", `cannot read ${target}: ${error.message}`);
77231
- }
77232
- const notes = [];
77233
- let mode = "resume";
77234
- if (resolveRef(cwd, localRef)) {
77235
- const added = gitRun4(cwd, ["worktree", "add", target, slice.branch], SLOW);
77236
- if (!added.ok)
77237
- return worktreeFailed(added.stderr);
77238
- } else {
77239
- const remote = pushRemoteName(cwd) ?? "origin";
77240
- const remoteRef = `refs/remotes/${remote}/${slice.branch}`;
77241
- if (resolveRef(cwd, remoteRef)) {
77242
- const added = gitRun4(cwd, ["worktree", "add", "-b", slice.branch, target, remoteRef], SLOW);
77243
- if (!added.ok)
77244
- return worktreeFailed(added.stderr);
77245
- notes.push(`resumed ${slice.branch} from ${remote}/${slice.branch}`);
77246
- } else {
77247
- mode = "new";
77248
- const parent = plan.slices.find((other) => other.branch === slice.base);
77249
- const from = slice.base === plan.trunk ? trunkRef(cwd, plan.trunk) : parent ? branchRef(cwd, parent.branch) : branchRef(cwd, slice.base);
77250
- if (!from)
77251
- return fanoutFail("not_found", `base ${slice.base} of slice ${slice.id} does not resolve here`, parent ? `start slice ${parent.id} first (its branch ${parent.branch} is the base)` : `git fetch origin ${slice.base}`);
77252
- const added = gitRun4(cwd, ["worktree", "add", "--detach", target, from], SLOW);
77253
- if (!added.ok)
77254
- return worktreeFailed(added.stderr);
77255
- const branched = gitBranch(target, { name: slice.branch, base: slice.base });
77256
- if (!branched.ok) {
77257
- gitRun4(cwd, ["worktree", "remove", target], SLOW);
77258
- return fanoutFail(branched.code, `workit git branch ${slice.branch} refused: ${branched.error}`, branched.unblock ?? `fix the branch name in the plan, then workit fanout worktree create ${slice.id}`);
77259
- }
77260
- notes.push(...branched.data.notes);
77261
- }
77262
- }
77263
- excludeScratch(cwd);
77264
- const scratch = ensureScratch(target);
77265
- const head = gitRun4(target, ["rev-parse", "HEAD"]).stdout.trim();
77266
- const baseSha = mode === "new" ? head : null;
77267
- appendObserved(cwd, {
77268
- type: "fanout.worktree.created",
77269
- actor: input.actor,
77270
- fanout: plan.name,
77271
- slice: slice.id,
77272
- branch: slice.branch,
77273
- path: target,
77274
- scratch,
77275
- mode,
77276
- head,
77277
- dirExisted,
77278
- planCreatedAt: plan.createdAt,
77279
- ...baseSha ? { baseSha } : {}
77280
- });
77281
- return { ok: true, data: { ...base, scratch, head, mode, created: true, notes } };
77282
- }
77283
- function releaseSliceWorktree(cwd, plan, input) {
77284
- const found = findSlice(plan, input.slice);
77285
- if (!found.ok)
77286
- return found;
77287
- const slice = found.data;
77288
- const target = slicePath(cwd, slice);
77289
- const worktrees = listWorktrees(cwd);
77290
- const index = worktrees.findIndex((entry) => samePath(entry.path, target));
77291
- const out = {
77292
- fanout: plan.name,
77293
- slice: slice.id,
77294
- branch: slice.branch,
77295
- path: target,
77296
- head: null,
77297
- dirty: [],
77298
- forced: input.force,
77299
- removed: false,
77300
- unpushed: null,
77301
- notes: []
77302
- };
77303
- if (index < 0) {
77304
- if (fs24.existsSync(target))
77305
- return fanoutFail("blocked", `${target} is not a worktree of this repository; nothing was removed`, "remove it yourself once you know what it holds");
77306
- out.notes.push(`no worktree at ${target}; nothing to release`);
77307
- return { ok: true, data: out };
77308
- }
77309
- const entry = worktrees[index];
77310
- if (index === 0)
77311
- return fanoutFail("blocked", `${target} is the main checkout; it is never released`);
77312
- if (inside2(cwd, entry.path))
77313
- return fanoutFail("blocked", `this command runs inside ${entry.path}`, `run it from the main checkout: workit --cwd <main checkout> fanout worktree release ${slice.id}`);
77314
- if (entry.branch !== null && entry.branch !== `refs/heads/${slice.branch}`)
77315
- return fanoutFail("blocked", `${entry.path} is on ${entry.branch.replace(/^refs\/heads\//u, "")}, not slice ${slice.id}'s ${slice.branch}; nothing was removed`);
77316
- const made = createdRow(cwd, plan, slice, entry.path);
77317
- if (!made)
77318
- return fanoutFail("blocked", `${entry.path} was not made by workit fanout worktree create for slice ${slice.id} (no ledger row); nothing was removed`, `remove it yourself (git worktree remove ${entry.path}) once you know what it holds`);
77319
- out.head = entry.head;
77320
- const status = gitRun4(entry.path, [
77321
- "status",
77322
- "--porcelain=v1",
77323
- "-z",
77324
- "--untracked-files=all",
77325
- "--ignore-submodules=none"
77326
- ]);
77327
- if (!status.ok)
77328
- return fanoutFail("failed", `git status in ${entry.path} failed: ${status.stderr.trim().split(`
77329
- `)[0]}`, "nothing was removed; inspect the worktree by hand");
77330
- const entries = porcelainEntries(status.stdout);
77331
- const inScratch = (line) => line.slice(3).startsWith(`${SCRATCH_DIR}/`);
77332
- out.dirty = entries.filter((line) => !inScratch(line));
77333
- out.unpushed = unpushedCount(cwd, slice.branch);
77334
- const refused = out.dirty.length > 0 && !input.force;
77335
- const recorded = appendObserved(cwd, {
77336
- type: "fanout.worktree.released",
77337
- actor: input.actor,
77338
- fanout: plan.name,
77339
- slice: slice.id,
77340
- branch: slice.branch,
77341
- path: entry.path,
77342
- head: entry.head,
77343
- uncommittedCount: out.dirty.length,
77344
- uncommitted: rowSample(out.dirty),
77345
- forced: input.force,
77346
- outcome: refused ? "refused" : "removed"
77347
- });
77348
- if (!recorded.ok)
77349
- return fanoutFail("unavailable", `cannot record the worktree state in the ledger (${recorded.error}); nothing was removed`);
77350
- if (refused)
77351
- return fanoutFail("blocked", `${entry.path} has ${out.dirty.length} uncommitted change${out.dirty.length === 1 ? "" : "s"} (${out.dirty.slice(0, 3).join(", ")}${out.dirty.length > 3 ? ", …" : ""}); recorded in the ledger, nothing removed`, `commit them on ${slice.branch} (workit git commit), or pass --force to drop them`, { ...out });
77352
- const args = ["worktree", "remove", ...input.force ? ["--force"] : [], entry.path];
77353
- let removed = gitRun4(cwd, args, SLOW);
77354
- if (!removed.ok && entries.some(inScratch)) {
77355
- removeScratch(entry.path);
77356
- removed = gitRun4(cwd, args, SLOW);
77357
- }
77358
- if (!removed.ok)
77359
- return fanoutFail("failed", `git worktree remove failed: ${removed.stderr.trim().split(`
77360
- `)[0]}`, entry.locked ? `git worktree unlock ${entry.path}, then release again` : undefined, { ...out });
77361
- out.removed = true;
77362
- if (made.dirExisted === true)
77363
- fs24.mkdirSync(entry.path, { recursive: true });
77364
- if (out.unpushed)
77365
- out.notes.push(`${slice.branch} is kept with ${out.unpushed} commit(s) not on the remote`);
77366
- return { ok: true, data: out };
77367
- }
77368
- function porcelainEntries(stdout) {
77369
- const fields = nulList2(stdout);
77370
- const out = [];
77371
- for (let index = 0;index < fields.length; index += 1) {
77372
- const entry = fields[index];
77373
- out.push(entry);
77374
- if (entry[0] === "R" || entry[0] === "C")
77375
- index += 1;
77376
- }
77377
- return out;
77378
- }
77379
- function unpushedCount(cwd, branch) {
77380
- const remote = pushRemoteName(cwd) ?? "origin";
77381
- const local = resolveRef(cwd, `refs/heads/${branch}`);
77382
- if (!local)
77383
- return null;
77384
- const tracking = resolveRef(cwd, `refs/remotes/${remote}/${branch}`);
77385
- if (!tracking)
77386
- return null;
77387
- const count = gitRun4(cwd, ["rev-list", "--count", `${tracking}..${local}`]);
77388
- return count.ok ? Number(count.stdout.trim()) : null;
77389
- }
77390
- function removeScratch(worktree) {
77391
- const scratch = path44.join(worktree, SCRATCH_DIR);
77392
- let stat;
77393
- try {
77394
- stat = fs24.lstatSync(scratch);
77395
- } catch {
77396
- return;
77397
- }
77398
- if (stat.isSymbolicLink() || !stat.isDirectory())
77399
- fs24.unlinkSync(scratch);
77400
- else
77401
- fs24.rmSync(scratch, { recursive: true, force: true, maxRetries: 2 });
77402
- }
77403
- var SLOW, SCRATCH_DIR = ".workit-scratch", EXCLUDE_LINE, DIRTY_ROW_BYTES = 2000, canonical2 = (target) => {
77404
- try {
77405
- return fs24.realpathSync.native(target);
77406
- } catch {
77407
- return path44.resolve(target);
77408
- }
77409
- }, samePath = (a, b) => {
77410
- const left = canonical2(a);
77411
- const right = canonical2(b);
77412
- return process.platform === "win32" || process.platform === "darwin" ? left.toLowerCase() === right.toLowerCase() : left === right;
77413
- }, inside2 = (child, parent) => {
77414
- const relative = path44.relative(canonical2(parent), canonical2(child));
77415
- return relative === "" || !relative.startsWith("..") && !path44.isAbsolute(relative);
77416
- }, slicePath = (cwd, slice) => path44.isAbsolute(slice.worktree) ? slice.worktree : path44.resolve(mainCheckout(cwd), slice.worktree), CLOCK_SLACK_MS = 2000, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
77417
- `)[0] || "unknown error"}`);
77418
- var init_fanout_worktree = __esm(() => {
77419
- init_ops();
77420
- init_rev();
77421
- init_ledger();
77422
- init_fanout();
77423
- SLOW = GIT_TIMEOUTS.worktree;
77424
- EXCLUDE_LINE = `/${SCRATCH_DIR}/`;
77425
- });
77426
-
77427
77727
  // packages/workit-cli/src/verbs/fanout.ts
77428
77728
  var exports_fanout = {};
77429
77729
  __export(exports_fanout, {
@@ -77478,7 +77778,12 @@ function resolveTrunk(io, raw, flag, track) {
77478
77778
  return { trunk: derived.tracked || track !== null ? derived.trunk : null };
77479
77779
  }
77480
77780
  function plan2(argv, io) {
77481
- const flags = parseFlags(argv, { name: "value", trunk: "value", track: "value" });
77781
+ const flags = parseFlags(argv, {
77782
+ name: "value",
77783
+ trunk: "value",
77784
+ track: "value",
77785
+ "take-lead": "boolean"
77786
+ });
77482
77787
  if (typeof flags === "string")
77483
77788
  return usage3(io, flags, PLAN_USAGE2);
77484
77789
  const track = flags.values.track ?? null;
@@ -77500,7 +77805,8 @@ function plan2(argv, io) {
77500
77805
  raw,
77501
77806
  name: flags.values.name ?? null,
77502
77807
  trunk: trunk.trunk,
77503
- actor: actorFromEnv(io.env)
77808
+ actor: actorFromEnv(io.env),
77809
+ takeLead: flags.booleans.has("take-lead")
77504
77810
  });
77505
77811
  if (!result.ok) {
77506
77812
  const code = failed(io, result);
@@ -77508,15 +77814,42 @@ function plan2(argv, io) {
77508
77814
  return code;
77509
77815
  }
77510
77816
  return emit(io, ok(result.data), (data) => [
77511
- `${data.created ? "planned" : "re-planned"} fanout ${data.name} on ${data.trunk}: ${data.slices.length} slice${data.slices.length === 1 ? "" : "s"}`,
77817
+ `${data.created ? "planned" : "re-planned"} fanout ${data.name} on ${data.trunk}${data.fanIn === "integration" ? " (integration branch: one PR)" : ""}: ${data.slices.length} slice${data.slices.length === 1 ? "" : "s"}`,
77512
77818
  ...data.slices.map((slice) => ` ${slice.id} [${slice.tier}] ${slice.branch} <- ${slice.base}${slice.dependsOn.length ? ` after ${slice.dependsOn.join(", ")}` : ""}
77513
77819
  worktree ${slice.worktree}`),
77514
77820
  ...data.overlaps.map((overlap) => ` shared ${overlap.path}: ${overlap.resolution === "owner" ? `owned by ${overlap.owner}` : `serialized (${overlap.slices.join(" -> ")})`}`),
77515
77821
  `waves: ${data.waves.map((wave) => wave.join(", ")).join(" | ")}`,
77516
77822
  `landing order: ${data.landingOrder.join(", ")}`,
77517
- ...data.notes.map((note) => `note: ${note}`)
77823
+ ...data.notes.map((note) => `note: ${note}`),
77824
+ ...data.warnings.map((warning) => `warning: ${warning}`)
77518
77825
  ]);
77519
77826
  }
77827
+ function brief(argv, io) {
77828
+ const flags = parseFlags(argv, { name: "value", mode: "value", attempt: "value" });
77829
+ if (typeof flags === "string")
77830
+ return usage3(io, flags, BRIEF_USAGE);
77831
+ if (flags.positionals.length !== 1)
77832
+ return usage3(io, flags.positionals.length ? `unexpected argument ${flags.positionals[1]}` : "missing <slice>", BRIEF_USAGE);
77833
+ const mode = flags.values.mode ?? null;
77834
+ if (mode !== null && mode !== "new" && mode !== "resume")
77835
+ return usage3(io, "--mode takes new or resume", BRIEF_USAGE);
77836
+ const rawAttempt = flags.values.attempt;
77837
+ const attempt = rawAttempt === undefined ? null : Number(rawAttempt);
77838
+ if (attempt !== null && !(Number.isInteger(attempt) && attempt >= 1 && attempt <= 9))
77839
+ return usage3(io, "--attempt takes a number from 1 to 9", BRIEF_USAGE);
77840
+ const selected = selectFanout(io.cwd, flags.values.name ?? null, currentBranch(io.cwd));
77841
+ if (!selected.ok)
77842
+ return failed(io, selected);
77843
+ const result = renderBrief(io.cwd, selected.data, {
77844
+ slice: flags.positionals[0],
77845
+ mode,
77846
+ attempt,
77847
+ actor: actorFromEnv(io.env)
77848
+ });
77849
+ if (!result.ok)
77850
+ return failed(io, result);
77851
+ return emit(io, ok(result.data), (data) => data.text.trimEnd());
77852
+ }
77520
77853
  function check2(argv, io) {
77521
77854
  const flags = parseFlags(argv, { name: "value", base: "value", offline: "boolean" });
77522
77855
  if (typeof flags === "string")
@@ -77618,6 +77951,8 @@ async function run15(argv, io) {
77618
77951
  const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
77619
77952
  if (sub === "plan")
77620
77953
  return plan2(rest, io);
77954
+ if (sub === "brief")
77955
+ return brief(rest, io);
77621
77956
  if (sub === "check")
77622
77957
  return check2(rest, io);
77623
77958
  if (sub === "status")
@@ -77626,7 +77961,7 @@ async function run15(argv, io) {
77626
77961
  return worktree(rest, io);
77627
77962
  return usage3(io, sub && !sub.startsWith("-") ? `unknown fanout subcommand "${sub}"` : "missing fanout subcommand", USAGE8);
77628
77963
  }
77629
- var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--json]", CHECK_USAGE = "workit fanout check [<slice>…] [--name <n>] [--base <ref>] [--offline] [--json]", STATUS_USAGE3 = "workit fanout status [--name <n>] [--stuck-after 30m] [--offline] [--json]", WORKTREE_USAGE = "workit fanout worktree create <slice> [--name <n>] | worktree release <slice> [--name <n>] [--force] [--json]", USAGE8 = "workit fanout plan|check|status|worktree ... (workit help fanout)", failed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), renderCheck = (data) => [
77964
+ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--take-lead] [--json]", BRIEF_USAGE = "workit fanout brief <slice> [--name <n>] [--mode new|resume] [--attempt <n>] [--json]", CHECK_USAGE = "workit fanout check [<slice>…] [--name <n>] [--base <ref>] [--offline] [--json]", STATUS_USAGE3 = "workit fanout status [--name <n>] [--stuck-after 30m] [--offline] [--json]", WORKTREE_USAGE = "workit fanout worktree create <slice> [--name <n>] | worktree release <slice> [--name <n>] [--force] [--json]", USAGE8 = "workit fanout plan|brief|check|status|worktree ... (workit help fanout)", failed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), renderCheck = (data) => [
77630
77965
  `fanout ${data.name} against ${data.trunkRef}: ${data.ready ? "ready to land" : "blocked"}`,
77631
77966
  ...data.slices.map((slice) => slice.status === "landed" ? ` -- ${slice.id} (${slice.branch}) landed (${slice.landed?.how}${slice.landed?.pr ? ` #${slice.landed.pr}` : ""})` : ` ${slice.status === "ready" ? "ok " : "x "}${slice.id} (${slice.branch}) ${slice.changed} file${slice.changed === 1 ? "" : "s"} · verdict ${slice.verdict.accepted ? "accepted" : slice.verdict.basis}${slice.reasons.length ? `
77632
77967
  ${slice.reasons.join(`
@@ -77664,6 +77999,7 @@ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --
77664
77999
  ];
77665
78000
  };
77666
78001
  var init_fanout2 = __esm(() => {
78002
+ init_fanout_brief();
77667
78003
  init_fanout_check();
77668
78004
  init_fanout_status();
77669
78005
  init_fanout_worktree();
@@ -77679,7 +78015,7 @@ var exports_ledger = {};
77679
78015
  __export(exports_ledger, {
77680
78016
  run: () => run16
77681
78017
  });
77682
- import { randomBytes as randomBytes4 } from "node:crypto";
78018
+ import { randomBytes as randomBytes5 } from "node:crypto";
77683
78019
  import { parseArgs } from "node:util";
77684
78020
  function targetBranch(io, rows, branch, pr) {
77685
78021
  if (pr !== undefined) {
@@ -77725,8 +78061,10 @@ async function run16(argv, io) {
77725
78061
  const text = rest.join(" ");
77726
78062
  if (sub === undefined)
77727
78063
  return usage4(io, "missing subcommand");
77728
- if (!["decision", "ruling", "verdict", "list", "show", "check"].includes(sub))
78064
+ if (!["decision", "ruling", "verdict", "standing", "list", "show", "check"].includes(sub))
77729
78065
  return usage4(io, `unknown ledger subcommand "${sub}"`);
78066
+ if (sub === "standing")
78067
+ return standing(io, values, rest);
77730
78068
  const ledger = readLedger(io.cwd);
77731
78069
  if (!ledger.ok)
77732
78070
  return failed2(io, ledger);
@@ -77804,7 +78142,62 @@ async function run16(argv, io) {
77804
78142
  `);
77805
78143
  return fromResult(io, verdict, (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)` : ""}`);
77806
78144
  }
77807
- var USAGE9 = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS, SESSION_SAFE2, ROLE_SAFE, actorFor = (io, values) => {
78145
+ function standingFanout(io, values) {
78146
+ if (values.fanout !== undefined) {
78147
+ const name = values.fanout.trim();
78148
+ if (!name)
78149
+ return new Error("--fanout needs a plan name");
78150
+ const read = readFanout(io.cwd, name);
78151
+ return { name, plan: read.ok ? read.data : null };
78152
+ }
78153
+ const selected = selectFanout(io.cwd, null, currentBranch(io.cwd));
78154
+ return selected.ok ? { name: selected.data.name, plan: selected.data } : new Error(`${selected.error}; pass --fanout <name>`);
78155
+ }
78156
+ async function standing(io, values, args) {
78157
+ const [action, ...rest] = args;
78158
+ if (action !== "add" && action !== "list" && action !== "clear")
78159
+ return emit(io, fail("invalid_input", action ? `unknown standing action "${action}"` : "missing add, list or clear", { unblock: STANDING_USAGE }));
78160
+ const target = standingFanout(io, values);
78161
+ if (target instanceof Error)
78162
+ return emit(io, fail("invalid_input", target.message, { unblock: STANDING_USAGE }));
78163
+ const fanout = target.name;
78164
+ const lead = target.plan?.leadSession ?? null;
78165
+ const leads = target.plan ? planLeads(target.plan) : null;
78166
+ if (action === "list") {
78167
+ if (rest.length)
78168
+ return usage4(io, `unexpected argument: ${rest[0]}`);
78169
+ const ledger = readLedger(io.cwd);
78170
+ if (!ledger.ok)
78171
+ return failed2(io, ledger);
78172
+ const orders = activeStanding(ledger.value.rows, fanout, leads).map((row) => ({
78173
+ id: row.id,
78174
+ at: row.at,
78175
+ what: row.what
78176
+ }));
78177
+ return emit(io, ok({ fanout, orders }), (data) => data.orders.length ? [
78178
+ `standing orders for fanout ${fanout}:`,
78179
+ ...data.orders.map((o) => ` ${o.id} ${o.what}`)
78180
+ ] : `no standing orders for fanout ${fanout}`);
78181
+ }
78182
+ const acting = actorFor(io, values);
78183
+ if (acting instanceof Error)
78184
+ return usage4(io, acting.message);
78185
+ const context = {
78186
+ cwd: io.cwd,
78187
+ actor: acting.actor,
78188
+ ...values.supersedes ? { supersedes: values.supersedes } : {}
78189
+ };
78190
+ if (action === "add" && target.plan === null)
78191
+ return emit(io, fail("not_found", `no fanout plan named ${fanout}`, {
78192
+ unblock: "workit fanout plan <plan.json> first, or name an existing plan with --fanout"
78193
+ }));
78194
+ if (action === "add")
78195
+ return fromResult(io, recordStanding(context, { fanout, lead, what: rest.join(" ") }), (row) => `recorded standing order ${row.id} for fanout ${fanout}`);
78196
+ if (rest.length > 1)
78197
+ return usage4(io, `unexpected argument: ${rest[1]}`);
78198
+ return fromResult(io, clearStanding(context, { fanout, lead, leads, target: rest[0] ?? null }), (row) => row.target ? `cleared standing order ${row.target} for fanout ${fanout}` : `cleared every standing order for fanout ${fanout}`);
78199
+ }
78200
+ var USAGE9 = "workit ledger decision|ruling|verdict|standing|list|check ... (workit help ledger for the grammar)", STANDING_USAGE = 'workit ledger standing add "<order>" | list | clear [<id>] [--fanout <name>] [--json]', OPTIONS, SESSION_SAFE2, ROLE_SAFE, actorFor = (io, values) => {
77808
78201
  const actor = actorFromEnv(io.env);
77809
78202
  if (values.session !== undefined && values.as !== undefined)
77810
78203
  return new Error("pass --session or --as, not both");
@@ -77818,7 +78211,7 @@ var USAGE9 = "workit ledger decision|ruling|verdict|list|check ... (workit help
77818
78211
  return new Error("--as takes a lowercase role, e.g. verifier");
77819
78212
  const prefix = (actor.session ?? actor.host).slice(0, 96);
77820
78213
  return {
77821
- actor: { ...actor, session: `${prefix}:${values.as}:${randomBytes4(4).toString("hex")}` },
78214
+ actor: { ...actor, session: `${prefix}:${values.as}:${randomBytes5(4).toString("hex")}` },
77822
78215
  derivedFrom: actor.session
77823
78216
  };
77824
78217
  }
@@ -77841,6 +78234,7 @@ var USAGE9 = "workit ledger decision|ruling|verdict|list|check ... (workit help
77841
78234
  };
77842
78235
  var init_ledger2 = __esm(() => {
77843
78236
  init_ledger();
78237
+ init_fanout();
77844
78238
  init_rev();
77845
78239
  init_implicit_task();
77846
78240
  init_output();
@@ -77861,6 +78255,7 @@ var init_ledger2 = __esm(() => {
77861
78255
  supersedes: { type: "string" },
77862
78256
  session: { type: "string" },
77863
78257
  as: { type: "string" },
78258
+ fanout: { type: "string" },
77864
78259
  json: { type: "boolean" }
77865
78260
  };
77866
78261
  SESSION_SAFE2 = /^[A-Za-z0-9_.:@/+-]{1,128}$/;
@@ -95044,15 +95439,15 @@ var VERBS = [
95044
95439
  {
95045
95440
  name: "fanout",
95046
95441
  group: "delivery",
95047
- usage: "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] | fanout check [<slice>…] [--name <n>] [--base <ref>] [--offline] | fanout status [--name <n>] [--stuck-after 30m] [--offline] | fanout worktree create|release <slice> [--force] [--json]",
95048
- summary: "Register parallel slices (complete briefs, disjoint scopes, shared-file owners), watch them (STUCK, PR, CI, verdict, landed, landing order), make and release slice worktrees, and gate fan-in: out-of-scope edits, trunk and sibling merge conflicts",
95442
+ usage: "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--take-lead] | fanout brief <slice> [--mode new|resume] [--attempt <n>] | fanout check [<slice>…] [--name <n>] [--base <ref>] [--offline] | fanout status [--name <n>] [--stuck-after 30m] [--offline] | fanout worktree create|release <slice> [--force] [--json]",
95443
+ summary: "Register parallel slices (complete briefs, disjoint scopes, shared-file owners), render each worker's brief with the standing orders, watch them (STUCK, PR, CI, verdict, landed, landing order), make and release slice worktrees, and gate fan-in: out-of-scope edits, trunk and sibling merge conflicts",
95049
95444
  load: () => Promise.resolve().then(() => (init_fanout2(), exports_fanout))
95050
95445
  },
95051
95446
  {
95052
95447
  name: "ledger",
95053
95448
  group: "delivery",
95054
- usage: "workit ledger decision|ruling|verdict|list|check ...",
95055
- summary: "Record and query decisions, rulings and SHA-keyed verdicts (patch-id carry-over across rebases)",
95449
+ usage: "workit ledger decision|ruling|verdict|standing|list|check ...",
95450
+ summary: "Record and query decisions, rulings, SHA-keyed verdicts (patch-id carry-over across rebases) and a fanout's standing orders",
95056
95451
  load: () => Promise.resolve().then(() => (init_ledger2(), exports_ledger))
95057
95452
  },
95058
95453
  {