@brainervirus/workit-claude-code 7.5.2 → 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.5.2",
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.5.2",
107
- "@brainervirus/workit-cursor": "^7.5.2",
108
- "@brainervirus/workit-opencode": "^7.5.2",
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: [] };
@@ -63145,7 +63215,8 @@ var init_skill_manifests = __esm(() => {
63145
63215
  "workit-deslop",
63146
63216
  "workit-fanout",
63147
63217
  "workit-verify-app",
63148
- "workit-retro"
63218
+ "workit-retro",
63219
+ "workit-architecture"
63149
63220
  ];
63150
63221
  if (false) {}
63151
63222
  });
@@ -75712,6 +75783,7 @@ var init_stack2 = __esm(() => {
75712
75783
 
75713
75784
  // packages/workit-core/src/fanout.ts
75714
75785
  import { spawnSync as spawnSync21 } from "node:child_process";
75786
+ import { createHash as createHash16 } from "node:crypto";
75715
75787
  import fs23 from "node:fs";
75716
75788
  import path43 from "node:path";
75717
75789
  function expandBraces(pattern, limit = 64) {
@@ -75873,6 +75945,9 @@ function parsePlan(raw, defaults, name) {
75873
75945
  else
75874
75946
  shared.push(normalized.pattern);
75875
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(", ")}`);
75876
75951
  if (!Array.isArray(raw.slices) || raw.slices.length === 0)
75877
75952
  return { ok: false, problems: [...problems, "slices: list at least one slice"] };
75878
75953
  const slices = [];
@@ -75954,7 +76029,9 @@ function parsePlan(raw, defaults, name) {
75954
76029
  verify,
75955
76030
  forbidden,
75956
76031
  context: optional("context"),
75957
- timebox: optional("timebox")
76032
+ timebox: optional("timebox"),
76033
+ hash: "",
76034
+ hashSince: ""
75958
76035
  });
75959
76036
  });
75960
76037
  const byId = new Map(slices.map((slice) => [slice.id, slice]));
@@ -75987,8 +76064,11 @@ function parsePlan(raw, defaults, name) {
75987
76064
  v: FANOUT_VERSION,
75988
76065
  name: planName,
75989
76066
  trunk,
76067
+ fanIn,
75990
76068
  shared,
75991
76069
  slices,
76070
+ leadSession: null,
76071
+ formerLeads: [],
75992
76072
  createdAt: "",
75993
76073
  updatedAt: ""
75994
76074
  }
@@ -76174,6 +76254,18 @@ function worktreeRoot(cwd) {
76174
76254
  const main = mainCheckout(cwd);
76175
76255
  return path43.join(path43.dirname(main), `${path43.basename(main)}-wt`);
76176
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
+ }
76177
76269
  function fanoutsDir(cwd) {
76178
76270
  const root = storeRoot(cwd);
76179
76271
  if (!root.ok)
@@ -76214,20 +76306,29 @@ function parseStored(raw) {
76214
76306
  verify: list(item.verify),
76215
76307
  forbidden: list(item.forbidden),
76216
76308
  context: typeof item.context === "string" ? item.context : null,
76217
- 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)
76218
76312
  });
76219
76313
  }
76314
+ for (const slice of slices)
76315
+ slice.hash ||= sliceHash(slice);
76220
76316
  const shared = globs(raw.shared);
76221
76317
  const ids = new Set(slices.map((slice) => slice.id));
76222
76318
  if (!globsValid || ids.size !== slices.length || slices.some((slice) => slice.dependsOn.some((dep) => dep === slice.id || !ids.has(dep))) || findCycle(slices))
76223
76319
  return null;
76320
+ const fanIn = raw.fanIn === "integration" ? "integration" : "prs";
76321
+ const createdAt = text(raw.createdAt);
76224
76322
  return {
76225
76323
  v: typeof raw.v === "number" ? raw.v : FANOUT_VERSION,
76226
76324
  name: raw.name,
76227
76325
  trunk: raw.trunk,
76326
+ fanIn,
76228
76327
  shared,
76229
76328
  slices,
76230
- createdAt: text(raw.createdAt),
76329
+ leadSession: typeof raw.leadSession === "string" && raw.leadSession ? raw.leadSession : null,
76330
+ formerLeads: list(raw.formerLeads),
76331
+ createdAt,
76231
76332
  updatedAt: text(raw.updatedAt)
76232
76333
  };
76233
76334
  }
@@ -76307,6 +76408,11 @@ function planFanout(cwd, input, now = new Date) {
76307
76408
  const dir = fanoutsDir(cwd);
76308
76409
  if (!dir.ok)
76309
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");
76310
76416
  const parsed = parsePlan(input.trunk && isRecord8(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, {
76311
76417
  trunk: input.trunk ?? defaultTrunk(cwd),
76312
76418
  root: mainCheckout(cwd),
@@ -76315,6 +76421,8 @@ function planFanout(cwd, input, now = new Date) {
76315
76421
  if (!parsed.ok)
76316
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 });
76317
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>`);
76318
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`);
76319
76427
  if (badBranches.length)
76320
76428
  return fanoutFail("invalid_input", badBranches[0], "rename the branch in the plan", {
@@ -76343,20 +76451,50 @@ function planFanout(cwd, input, now = new Date) {
76343
76451
  const existing = readFanout(cwd, plan.name);
76344
76452
  const previous = existing.ok ? existing.data : null;
76345
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);
76346
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
+ }
76347
76467
  const written = writeFanout(cwd, plan);
76348
76468
  if (!written.ok)
76349
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
+ });
76350
76478
  appendObserved(cwd, {
76351
76479
  type: "fanout.planned",
76352
76480
  actor: input.actor,
76353
76481
  fanout: plan.name,
76354
76482
  trunk: plan.trunk,
76483
+ fanIn: plan.fanIn,
76355
76484
  slices: plan.slices.length,
76356
76485
  ids: plan.slices.slice(0, 20).map((slice) => slice.id)
76357
76486
  });
76358
76487
  const grouped = waves(plan.slices);
76359
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
+ }
76360
76498
  if (!ref)
76361
76499
  notes.push(`trunk ${plan.trunk} does not resolve here; only sample paths of each glob were compared`);
76362
76500
  grouped.forEach((wave, index) => {
@@ -76371,6 +76509,7 @@ function planFanout(cwd, input, now = new Date) {
76371
76509
  data: {
76372
76510
  name: plan.name,
76373
76511
  trunk: plan.trunk,
76512
+ fanIn: plan.fanIn,
76374
76513
  file: written.data,
76375
76514
  created: previous === null,
76376
76515
  slices: plan.slices.map(({ id, branch, base, worktree, tier, dependsOn, scope, owns }) => ({
@@ -76386,11 +76525,12 @@ function planFanout(cwd, input, now = new Date) {
76386
76525
  overlaps,
76387
76526
  waves: grouped,
76388
76527
  landingOrder: landingOrder(plan.slices),
76389
- notes
76528
+ notes,
76529
+ warnings
76390
76530
  }
76391
76531
  };
76392
76532
  }
76393
- var FANOUT_VERSION = 1, TIERS, fanoutFail = (code, error, unblock, data) => ({
76533
+ var FANOUT_VERSION = 1, TIERS, FAN_IN_MODES, fanoutFail = (code, error, unblock, data) => ({
76394
76534
  ok: false,
76395
76535
  code,
76396
76536
  error,
@@ -76430,12 +76570,13 @@ var FANOUT_VERSION = 1, TIERS, fanoutFail = (code, error, unblock, data) => ({
76430
76570
  stdout: run.stdout ?? "",
76431
76571
  stderr: run.error ? String(run.error.message) : run.stderr ?? ""
76432
76572
  };
76433
- }, 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;
76434
76574
  var init_fanout = __esm(() => {
76435
76575
  init_rev();
76436
76576
  init_ledger();
76437
76577
  init_stack();
76438
76578
  TIERS = ["mundane", "standard", "hard"];
76579
+ FAN_IN_MODES = ["prs", "integration"];
76439
76580
  DEFAULT_SHARED = [
76440
76581
  "**/package.json",
76441
76582
  "**/{package-lock.json,npm-shrinkwrap.json,bun.lock,bun.lockb,pnpm-lock.yaml,pnpm-workspace.yaml,yarn.lock}",
@@ -76451,6 +76592,488 @@ var init_fanout = __esm(() => {
76451
76592
  ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
76452
76593
  });
76453
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
+
76454
77077
  // packages/workit-core/src/fanout-landed.ts
76455
77078
  import { spawnSync as spawnSync22 } from "node:child_process";
76456
77079
  function forgeBreaker(resolved) {
@@ -76561,9 +77184,20 @@ function trunkPatchIds(ctx, fork) {
76561
77184
  cache.set(fork, ids);
76562
77185
  return ids;
76563
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
+ }
76564
77193
  function linked(rows, input, slice, head) {
76565
77194
  const since = input.since ? Date.parse(input.since) : Number.NaN;
76566
- return rows.some((row) => (Number.isNaN(since) || Date.parse(String(row.at)) >= since) && (row.type === "fanout.worktree.created" && row.fanout === input.fanout && row.slice === slice.id || head !== null && row.branch === slice.branch && row.head === head));
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
+ });
76567
77201
  }
76568
77202
  function detectLanding(ctx, slice, input) {
76569
77203
  const out = {
@@ -76636,7 +77270,7 @@ function sliceStart(cwd, rows, fanout, slice) {
76636
77270
  const reflog = branchCreatedAt(cwd, slice.branch);
76637
77271
  if (reflog)
76638
77272
  return reflog;
76639
- 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));
76640
77274
  return row?.baseSha ?? null;
76641
77275
  }
76642
77276
  var isAncestor2 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
@@ -76785,7 +77419,8 @@ function sliceLandings(cwd, plan, trunk, tips, rows, forge) {
76785
77419
  createdFrom: sliceStart(cwd, rows, plan.name, slice),
76786
77420
  parentHead,
76787
77421
  fanout: plan.name,
76788
- since: plan.createdAt || null,
77422
+ since: slice.hashSince || plan.createdAt || null,
77423
+ sliceHash: slice.hash || null,
76789
77424
  rows
76790
77425
  }));
76791
77426
  }
@@ -76927,7 +77562,7 @@ function activity(cwd, plan, slice, rows) {
76927
77562
  head,
76928
77563
  commit,
76929
77564
  last: newest(commit, reflog, ...ledger),
76930
- 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))
76931
77566
  };
76932
77567
  }
76933
77568
  function fanoutStatus(cwd, plan, options) {
@@ -77037,7 +77672,7 @@ function fanoutStatus(cwd, plan, options) {
77037
77672
  else if (row.branchExists)
77038
77673
  row.state = "active";
77039
77674
  else if (row.waitsFor.length === 0 || parent && row.waitsFor.length === 1 && row.waitsFor[0] === parent.id && out.get(parent.id)?.verdict.accepted) {
77040
- const created = rows.some((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === plan.name && candidate.slice === id);
77675
+ const created = rows.some((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === plan.name && candidate.slice === id && inSliceRun(candidate, byId.get(id)));
77041
77676
  row.state = created ? "active" : "not_started";
77042
77677
  if (!created)
77043
77678
  spawnable.push(id);
@@ -77089,340 +77724,6 @@ var init_fanout_status = __esm(() => {
77089
77724
  DEFAULT_STUCK_MS = 30 * 60000;
77090
77725
  });
77091
77726
 
77092
- // packages/workit-core/src/fanout-worktree.ts
77093
- import fs24 from "node:fs";
77094
- import path44 from "node:path";
77095
- function rowSample(lines) {
77096
- const out = [];
77097
- let bytes = 0;
77098
- for (const line of lines) {
77099
- const entry = line.slice(0, 200);
77100
- bytes += Buffer.byteLength(entry) + 3;
77101
- if (bytes > DIRTY_ROW_BYTES)
77102
- break;
77103
- out.push(entry);
77104
- }
77105
- return out;
77106
- }
77107
- function listWorktrees(cwd) {
77108
- const out = [];
77109
- let current = null;
77110
- for (const field of gitRun4(cwd, ["worktree", "list", "--porcelain", "-z"]).stdout.split("\x00")) {
77111
- if (field.startsWith("worktree ")) {
77112
- current = { path: field.slice(9), head: null, branch: null, locked: false };
77113
- out.push(current);
77114
- } else if (!current)
77115
- continue;
77116
- else if (field.startsWith("HEAD "))
77117
- current.head = field.slice(5);
77118
- else if (field.startsWith("branch "))
77119
- current.branch = field.slice(7);
77120
- else if (field === "locked" || field.startsWith("locked "))
77121
- current.locked = true;
77122
- }
77123
- return out;
77124
- }
77125
- function createdRow(cwd, plan, slice, target) {
77126
- const ledger = readLedger(cwd);
77127
- if (!ledger.ok)
77128
- return null;
77129
- const atPath = (row) => row.fanout === plan.name && row.slice === slice.id && typeof row.path === "string" && samePath(row.path, target);
77130
- const rows = ledger.value.rows;
77131
- const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && atPath(candidate));
77132
- if (!row)
77133
- return null;
77134
- const removedSince = rows.some((candidate) => candidate.seq > row.seq && candidate.type === "fanout.worktree.released" && candidate.outcome === "removed" && atPath(candidate));
77135
- if (removedSince)
77136
- return null;
77137
- const made = adminDirTime(target);
77138
- if (made === null || made + CLOCK_SLACK_MS < Date.parse(row.at))
77139
- return null;
77140
- return row;
77141
- }
77142
- function adminDirTime(worktree) {
77143
- const dir = gitRun4(worktree, ["rev-parse", "--absolute-git-dir"]);
77144
- if (!dir.ok)
77145
- return null;
77146
- try {
77147
- const stat = fs24.statSync(dir.stdout.trim());
77148
- return stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs;
77149
- } catch {
77150
- return null;
77151
- }
77152
- }
77153
- function findSlice(plan, id) {
77154
- const slice = plan.slices.find((candidate) => candidate.id === id);
77155
- return slice ? { ok: true, data: slice } : fanoutFail("invalid_input", `${id}: not a slice of fanout ${plan.name}`, `slices: ${plan.slices.map((candidate) => candidate.id).join(", ")}`);
77156
- }
77157
- function excludeScratch(cwd) {
77158
- const common = gitCommonDir(cwd);
77159
- if (!common)
77160
- return;
77161
- const file = path44.join(common, "info", "exclude");
77162
- let text = "";
77163
- try {
77164
- text = fs24.readFileSync(file, "utf8");
77165
- } catch {
77166
- text = "";
77167
- }
77168
- if (text.split(/\r?\n/u).includes(EXCLUDE_LINE))
77169
- return;
77170
- fs24.mkdirSync(path44.dirname(file), { recursive: true });
77171
- fs24.appendFileSync(file, `${text && !text.endsWith(`
77172
- `) ? `
77173
- ` : ""}# workit fanout: per-worker scratch dirs
77174
- ${EXCLUDE_LINE}
77175
- `);
77176
- }
77177
- function ensureScratch(worktree) {
77178
- const scratch = path44.join(worktree, SCRATCH_DIR);
77179
- fs24.mkdirSync(scratch, { recursive: true });
77180
- return scratch;
77181
- }
77182
- function createSliceWorktree(cwd, plan, input) {
77183
- const found = findSlice(plan, input.slice);
77184
- if (!found.ok)
77185
- return found;
77186
- const slice = found.data;
77187
- if (!slice.worktree)
77188
- return fanoutFail("invalid_input", `slice ${slice.id} has no worktree path`, "re-plan it with workit fanout plan");
77189
- const target = slicePath(cwd, slice);
77190
- const worktrees = listWorktrees(cwd);
77191
- const localRef = `refs/heads/${slice.branch}`;
77192
- const existing = worktrees.find((entry) => samePath(entry.path, target));
77193
- const base = {
77194
- fanout: plan.name,
77195
- slice: slice.id,
77196
- branch: slice.branch,
77197
- base: slice.base,
77198
- path: target
77199
- };
77200
- if (existing) {
77201
- if (existing.branch !== null && existing.branch !== localRef)
77202
- return fanoutFail("blocked", `${target} is a worktree on ${existing.branch.replace(/^refs\/heads\//u, "")}, not ${slice.branch}`, `release it first, or set slice ${slice.id}'s worktree to another path`);
77203
- if (!createdRow(cwd, plan, slice, target))
77204
- return fanoutFail("blocked", `${target} is a worktree that workit fanout worktree create did not make for slice ${slice.id}`, `remove it yourself (git worktree remove ${target}) once you know what it holds`);
77205
- excludeScratch(cwd);
77206
- return {
77207
- ok: true,
77208
- data: {
77209
- ...base,
77210
- scratch: ensureScratch(target),
77211
- head: existing.head ?? "",
77212
- mode: resolveRef(cwd, localRef) ? "resume" : "new",
77213
- created: false,
77214
- notes: ["the worktree already exists; nothing was changed"]
77215
- }
77216
- };
77217
- }
77218
- const elsewhere = worktrees.find((entry) => entry.branch === localRef);
77219
- if (elsewhere)
77220
- return fanoutFail("blocked", `${slice.branch} is checked out at ${elsewhere.path}: never two live workers on one branch`, `stop that worker, then workit fanout worktree release ${slice.id} (or git worktree remove ${elsewhere.path})`);
77221
- let dirExisted = false;
77222
- try {
77223
- const stat = fs24.statSync(target);
77224
- if (!stat.isDirectory() || fs24.readdirSync(target).length > 0)
77225
- return fanoutFail("blocked", `${target} exists and is ${stat.isDirectory() ? "not empty" : "not a directory"} (not a worktree of this repository)`, `move it away, or set slice ${slice.id}'s worktree to another path`);
77226
- dirExisted = true;
77227
- } catch (error) {
77228
- if (error.code !== "ENOENT")
77229
- return fanoutFail("failed", `cannot read ${target}: ${error.message}`);
77230
- }
77231
- const notes = [];
77232
- let mode = "resume";
77233
- if (resolveRef(cwd, localRef)) {
77234
- const added = gitRun4(cwd, ["worktree", "add", target, slice.branch], SLOW);
77235
- if (!added.ok)
77236
- return worktreeFailed(added.stderr);
77237
- } else {
77238
- const remote = pushRemoteName(cwd) ?? "origin";
77239
- const remoteRef = `refs/remotes/${remote}/${slice.branch}`;
77240
- if (resolveRef(cwd, remoteRef)) {
77241
- const added = gitRun4(cwd, ["worktree", "add", "-b", slice.branch, target, remoteRef], SLOW);
77242
- if (!added.ok)
77243
- return worktreeFailed(added.stderr);
77244
- notes.push(`resumed ${slice.branch} from ${remote}/${slice.branch}`);
77245
- } else {
77246
- mode = "new";
77247
- const parent = plan.slices.find((other) => other.branch === slice.base);
77248
- const from = slice.base === plan.trunk ? trunkRef(cwd, plan.trunk) : parent ? branchRef(cwd, parent.branch) : branchRef(cwd, slice.base);
77249
- if (!from)
77250
- return fanoutFail("not_found", `base ${slice.base} of slice ${slice.id} does not resolve here`, parent ? `start slice ${parent.id} first (its branch ${parent.branch} is the base)` : `git fetch origin ${slice.base}`);
77251
- const added = gitRun4(cwd, ["worktree", "add", "--detach", target, from], SLOW);
77252
- if (!added.ok)
77253
- return worktreeFailed(added.stderr);
77254
- const branched = gitBranch(target, { name: slice.branch, base: slice.base });
77255
- if (!branched.ok) {
77256
- gitRun4(cwd, ["worktree", "remove", target], SLOW);
77257
- return fanoutFail(branched.code, `workit git branch ${slice.branch} refused: ${branched.error}`, branched.unblock ?? `fix the branch name in the plan, then workit fanout worktree create ${slice.id}`);
77258
- }
77259
- notes.push(...branched.data.notes);
77260
- }
77261
- }
77262
- excludeScratch(cwd);
77263
- const scratch = ensureScratch(target);
77264
- const head = gitRun4(target, ["rev-parse", "HEAD"]).stdout.trim();
77265
- const baseSha = mode === "new" ? head : null;
77266
- appendObserved(cwd, {
77267
- type: "fanout.worktree.created",
77268
- actor: input.actor,
77269
- fanout: plan.name,
77270
- slice: slice.id,
77271
- branch: slice.branch,
77272
- path: target,
77273
- scratch,
77274
- mode,
77275
- head,
77276
- dirExisted,
77277
- planCreatedAt: plan.createdAt,
77278
- ...baseSha ? { baseSha } : {}
77279
- });
77280
- return { ok: true, data: { ...base, scratch, head, mode, created: true, notes } };
77281
- }
77282
- function releaseSliceWorktree(cwd, plan, input) {
77283
- const found = findSlice(plan, input.slice);
77284
- if (!found.ok)
77285
- return found;
77286
- const slice = found.data;
77287
- const target = slicePath(cwd, slice);
77288
- const worktrees = listWorktrees(cwd);
77289
- const index = worktrees.findIndex((entry) => samePath(entry.path, target));
77290
- const out = {
77291
- fanout: plan.name,
77292
- slice: slice.id,
77293
- branch: slice.branch,
77294
- path: target,
77295
- head: null,
77296
- dirty: [],
77297
- forced: input.force,
77298
- removed: false,
77299
- unpushed: null,
77300
- notes: []
77301
- };
77302
- if (index < 0) {
77303
- if (fs24.existsSync(target))
77304
- return fanoutFail("blocked", `${target} is not a worktree of this repository; nothing was removed`, "remove it yourself once you know what it holds");
77305
- out.notes.push(`no worktree at ${target}; nothing to release`);
77306
- return { ok: true, data: out };
77307
- }
77308
- const entry = worktrees[index];
77309
- if (index === 0)
77310
- return fanoutFail("blocked", `${target} is the main checkout; it is never released`);
77311
- if (inside2(cwd, entry.path))
77312
- return fanoutFail("blocked", `this command runs inside ${entry.path}`, `run it from the main checkout: workit --cwd <main checkout> fanout worktree release ${slice.id}`);
77313
- if (entry.branch !== null && entry.branch !== `refs/heads/${slice.branch}`)
77314
- return fanoutFail("blocked", `${entry.path} is on ${entry.branch.replace(/^refs\/heads\//u, "")}, not slice ${slice.id}'s ${slice.branch}; nothing was removed`);
77315
- const made = createdRow(cwd, plan, slice, entry.path);
77316
- if (!made)
77317
- return fanoutFail("blocked", `${entry.path} was not made by workit fanout worktree create for slice ${slice.id} (no ledger row); nothing was removed`, `remove it yourself (git worktree remove ${entry.path}) once you know what it holds`);
77318
- out.head = entry.head;
77319
- const status = gitRun4(entry.path, [
77320
- "status",
77321
- "--porcelain=v1",
77322
- "-z",
77323
- "--untracked-files=all",
77324
- "--ignore-submodules=none"
77325
- ]);
77326
- if (!status.ok)
77327
- return fanoutFail("failed", `git status in ${entry.path} failed: ${status.stderr.trim().split(`
77328
- `)[0]}`, "nothing was removed; inspect the worktree by hand");
77329
- const entries = porcelainEntries(status.stdout);
77330
- const inScratch = (line) => line.slice(3).startsWith(`${SCRATCH_DIR}/`);
77331
- out.dirty = entries.filter((line) => !inScratch(line));
77332
- out.unpushed = unpushedCount(cwd, slice.branch);
77333
- const refused = out.dirty.length > 0 && !input.force;
77334
- const recorded = appendObserved(cwd, {
77335
- type: "fanout.worktree.released",
77336
- actor: input.actor,
77337
- fanout: plan.name,
77338
- slice: slice.id,
77339
- branch: slice.branch,
77340
- path: entry.path,
77341
- head: entry.head,
77342
- uncommittedCount: out.dirty.length,
77343
- uncommitted: rowSample(out.dirty),
77344
- forced: input.force,
77345
- outcome: refused ? "refused" : "removed"
77346
- });
77347
- if (!recorded.ok)
77348
- return fanoutFail("unavailable", `cannot record the worktree state in the ledger (${recorded.error}); nothing was removed`);
77349
- if (refused)
77350
- return fanoutFail("blocked", `${entry.path} has ${out.dirty.length} uncommitted change${out.dirty.length === 1 ? "" : "s"} (${out.dirty.slice(0, 3).join(", ")}${out.dirty.length > 3 ? ", …" : ""}); recorded in the ledger, nothing removed`, `commit them on ${slice.branch} (workit git commit), or pass --force to drop them`, { ...out });
77351
- const args = ["worktree", "remove", ...input.force ? ["--force"] : [], entry.path];
77352
- let removed = gitRun4(cwd, args, SLOW);
77353
- if (!removed.ok && entries.some(inScratch)) {
77354
- removeScratch(entry.path);
77355
- removed = gitRun4(cwd, args, SLOW);
77356
- }
77357
- if (!removed.ok)
77358
- return fanoutFail("failed", `git worktree remove failed: ${removed.stderr.trim().split(`
77359
- `)[0]}`, entry.locked ? `git worktree unlock ${entry.path}, then release again` : undefined, { ...out });
77360
- out.removed = true;
77361
- if (made.dirExisted === true)
77362
- fs24.mkdirSync(entry.path, { recursive: true });
77363
- if (out.unpushed)
77364
- out.notes.push(`${slice.branch} is kept with ${out.unpushed} commit(s) not on the remote`);
77365
- return { ok: true, data: out };
77366
- }
77367
- function porcelainEntries(stdout) {
77368
- const fields = nulList2(stdout);
77369
- const out = [];
77370
- for (let index = 0;index < fields.length; index += 1) {
77371
- const entry = fields[index];
77372
- out.push(entry);
77373
- if (entry[0] === "R" || entry[0] === "C")
77374
- index += 1;
77375
- }
77376
- return out;
77377
- }
77378
- function unpushedCount(cwd, branch) {
77379
- const remote = pushRemoteName(cwd) ?? "origin";
77380
- const local = resolveRef(cwd, `refs/heads/${branch}`);
77381
- if (!local)
77382
- return null;
77383
- const tracking = resolveRef(cwd, `refs/remotes/${remote}/${branch}`);
77384
- if (!tracking)
77385
- return null;
77386
- const count = gitRun4(cwd, ["rev-list", "--count", `${tracking}..${local}`]);
77387
- return count.ok ? Number(count.stdout.trim()) : null;
77388
- }
77389
- function removeScratch(worktree) {
77390
- const scratch = path44.join(worktree, SCRATCH_DIR);
77391
- let stat;
77392
- try {
77393
- stat = fs24.lstatSync(scratch);
77394
- } catch {
77395
- return;
77396
- }
77397
- if (stat.isSymbolicLink() || !stat.isDirectory())
77398
- fs24.unlinkSync(scratch);
77399
- else
77400
- fs24.rmSync(scratch, { recursive: true, force: true, maxRetries: 2 });
77401
- }
77402
- var SLOW, SCRATCH_DIR = ".workit-scratch", EXCLUDE_LINE, DIRTY_ROW_BYTES = 2000, canonical2 = (target) => {
77403
- try {
77404
- return fs24.realpathSync.native(target);
77405
- } catch {
77406
- return path44.resolve(target);
77407
- }
77408
- }, samePath = (a, b) => {
77409
- const left = canonical2(a);
77410
- const right = canonical2(b);
77411
- return process.platform === "win32" || process.platform === "darwin" ? left.toLowerCase() === right.toLowerCase() : left === right;
77412
- }, inside2 = (child, parent) => {
77413
- const relative = path44.relative(canonical2(parent), canonical2(child));
77414
- return relative === "" || !relative.startsWith("..") && !path44.isAbsolute(relative);
77415
- }, slicePath = (cwd, slice) => path44.isAbsolute(slice.worktree) ? slice.worktree : path44.resolve(mainCheckout(cwd), slice.worktree), CLOCK_SLACK_MS = 2000, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
77416
- `)[0] || "unknown error"}`);
77417
- var init_fanout_worktree = __esm(() => {
77418
- init_ops();
77419
- init_rev();
77420
- init_ledger();
77421
- init_fanout();
77422
- SLOW = GIT_TIMEOUTS.worktree;
77423
- EXCLUDE_LINE = `/${SCRATCH_DIR}/`;
77424
- });
77425
-
77426
77727
  // packages/workit-cli/src/verbs/fanout.ts
77427
77728
  var exports_fanout = {};
77428
77729
  __export(exports_fanout, {
@@ -77477,7 +77778,12 @@ function resolveTrunk(io, raw, flag, track) {
77477
77778
  return { trunk: derived.tracked || track !== null ? derived.trunk : null };
77478
77779
  }
77479
77780
  function plan2(argv, io) {
77480
- 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
+ });
77481
77787
  if (typeof flags === "string")
77482
77788
  return usage3(io, flags, PLAN_USAGE2);
77483
77789
  const track = flags.values.track ?? null;
@@ -77499,7 +77805,8 @@ function plan2(argv, io) {
77499
77805
  raw,
77500
77806
  name: flags.values.name ?? null,
77501
77807
  trunk: trunk.trunk,
77502
- actor: actorFromEnv(io.env)
77808
+ actor: actorFromEnv(io.env),
77809
+ takeLead: flags.booleans.has("take-lead")
77503
77810
  });
77504
77811
  if (!result.ok) {
77505
77812
  const code = failed(io, result);
@@ -77507,15 +77814,42 @@ function plan2(argv, io) {
77507
77814
  return code;
77508
77815
  }
77509
77816
  return emit(io, ok(result.data), (data) => [
77510
- `${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"}`,
77511
77818
  ...data.slices.map((slice) => ` ${slice.id} [${slice.tier}] ${slice.branch} <- ${slice.base}${slice.dependsOn.length ? ` after ${slice.dependsOn.join(", ")}` : ""}
77512
77819
  worktree ${slice.worktree}`),
77513
77820
  ...data.overlaps.map((overlap) => ` shared ${overlap.path}: ${overlap.resolution === "owner" ? `owned by ${overlap.owner}` : `serialized (${overlap.slices.join(" -> ")})`}`),
77514
77821
  `waves: ${data.waves.map((wave) => wave.join(", ")).join(" | ")}`,
77515
77822
  `landing order: ${data.landingOrder.join(", ")}`,
77516
- ...data.notes.map((note) => `note: ${note}`)
77823
+ ...data.notes.map((note) => `note: ${note}`),
77824
+ ...data.warnings.map((warning) => `warning: ${warning}`)
77517
77825
  ]);
77518
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
+ }
77519
77853
  function check2(argv, io) {
77520
77854
  const flags = parseFlags(argv, { name: "value", base: "value", offline: "boolean" });
77521
77855
  if (typeof flags === "string")
@@ -77617,6 +77951,8 @@ async function run15(argv, io) {
77617
77951
  const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
77618
77952
  if (sub === "plan")
77619
77953
  return plan2(rest, io);
77954
+ if (sub === "brief")
77955
+ return brief(rest, io);
77620
77956
  if (sub === "check")
77621
77957
  return check2(rest, io);
77622
77958
  if (sub === "status")
@@ -77625,7 +77961,7 @@ async function run15(argv, io) {
77625
77961
  return worktree(rest, io);
77626
77962
  return usage3(io, sub && !sub.startsWith("-") ? `unknown fanout subcommand "${sub}"` : "missing fanout subcommand", USAGE8);
77627
77963
  }
77628
- var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--json]", CHECK_USAGE = "workit fanout check [<slice>…] [--name <n>] [--base <ref>] [--offline] [--json]", STATUS_USAGE3 = "workit fanout status [--name <n>] [--stuck-after 30m] [--offline] [--json]", WORKTREE_USAGE = "workit fanout worktree create <slice> [--name <n>] | worktree release <slice> [--name <n>] [--force] [--json]", USAGE8 = "workit fanout plan|check|status|worktree ... (workit help fanout)", failed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), renderCheck = (data) => [
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) => [
77629
77965
  `fanout ${data.name} against ${data.trunkRef}: ${data.ready ? "ready to land" : "blocked"}`,
77630
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 ? `
77631
77967
  ${slice.reasons.join(`
@@ -77663,6 +77999,7 @@ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --
77663
77999
  ];
77664
78000
  };
77665
78001
  var init_fanout2 = __esm(() => {
78002
+ init_fanout_brief();
77666
78003
  init_fanout_check();
77667
78004
  init_fanout_status();
77668
78005
  init_fanout_worktree();
@@ -77678,7 +78015,7 @@ var exports_ledger = {};
77678
78015
  __export(exports_ledger, {
77679
78016
  run: () => run16
77680
78017
  });
77681
- import { randomBytes as randomBytes4 } from "node:crypto";
78018
+ import { randomBytes as randomBytes5 } from "node:crypto";
77682
78019
  import { parseArgs } from "node:util";
77683
78020
  function targetBranch(io, rows, branch, pr) {
77684
78021
  if (pr !== undefined) {
@@ -77724,8 +78061,10 @@ async function run16(argv, io) {
77724
78061
  const text = rest.join(" ");
77725
78062
  if (sub === undefined)
77726
78063
  return usage4(io, "missing subcommand");
77727
- if (!["decision", "ruling", "verdict", "list", "show", "check"].includes(sub))
78064
+ if (!["decision", "ruling", "verdict", "standing", "list", "show", "check"].includes(sub))
77728
78065
  return usage4(io, `unknown ledger subcommand "${sub}"`);
78066
+ if (sub === "standing")
78067
+ return standing(io, values, rest);
77729
78068
  const ledger = readLedger(io.cwd);
77730
78069
  if (!ledger.ok)
77731
78070
  return failed2(io, ledger);
@@ -77803,7 +78142,62 @@ async function run16(argv, io) {
77803
78142
  `);
77804
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)` : ""}`);
77805
78144
  }
77806
- 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) => {
77807
78201
  const actor = actorFromEnv(io.env);
77808
78202
  if (values.session !== undefined && values.as !== undefined)
77809
78203
  return new Error("pass --session or --as, not both");
@@ -77817,7 +78211,7 @@ var USAGE9 = "workit ledger decision|ruling|verdict|list|check ... (workit help
77817
78211
  return new Error("--as takes a lowercase role, e.g. verifier");
77818
78212
  const prefix = (actor.session ?? actor.host).slice(0, 96);
77819
78213
  return {
77820
- actor: { ...actor, session: `${prefix}:${values.as}:${randomBytes4(4).toString("hex")}` },
78214
+ actor: { ...actor, session: `${prefix}:${values.as}:${randomBytes5(4).toString("hex")}` },
77821
78215
  derivedFrom: actor.session
77822
78216
  };
77823
78217
  }
@@ -77840,6 +78234,7 @@ var USAGE9 = "workit ledger decision|ruling|verdict|list|check ... (workit help
77840
78234
  };
77841
78235
  var init_ledger2 = __esm(() => {
77842
78236
  init_ledger();
78237
+ init_fanout();
77843
78238
  init_rev();
77844
78239
  init_implicit_task();
77845
78240
  init_output();
@@ -77860,6 +78255,7 @@ var init_ledger2 = __esm(() => {
77860
78255
  supersedes: { type: "string" },
77861
78256
  session: { type: "string" },
77862
78257
  as: { type: "string" },
78258
+ fanout: { type: "string" },
77863
78259
  json: { type: "boolean" }
77864
78260
  };
77865
78261
  SESSION_SAFE2 = /^[A-Za-z0-9_.:@/+-]{1,128}$/;
@@ -95043,15 +95439,15 @@ var VERBS = [
95043
95439
  {
95044
95440
  name: "fanout",
95045
95441
  group: "delivery",
95046
- 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]",
95047
- 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",
95048
95444
  load: () => Promise.resolve().then(() => (init_fanout2(), exports_fanout))
95049
95445
  },
95050
95446
  {
95051
95447
  name: "ledger",
95052
95448
  group: "delivery",
95053
- usage: "workit ledger decision|ruling|verdict|list|check ...",
95054
- 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",
95055
95451
  load: () => Promise.resolve().then(() => (init_ledger2(), exports_ledger))
95056
95452
  },
95057
95453
  {