@brainervirus/workit-claude-code 7.2.0 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/workit.js CHANGED
@@ -65,7 +65,7 @@ var package_default;
65
65
  var init_package = __esm(() => {
66
66
  package_default = {
67
67
  name: "@brainervirus/workit-cli",
68
- version: "7.2.0",
68
+ version: "7.4.0",
69
69
  private: false,
70
70
  description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
71
71
  keywords: [
@@ -103,9 +103,9 @@ var init_package = __esm(() => {
103
103
  },
104
104
  dependencies: {
105
105
  "@babel/parser": "7.29.9",
106
- "@brainervirus/workit-core": "^7.2.0",
107
- "@brainervirus/workit-cursor": "^7.2.0",
108
- "@brainervirus/workit-opencode": "^7.2.0",
106
+ "@brainervirus/workit-core": "^7.4.0",
107
+ "@brainervirus/workit-cursor": "^7.4.0",
108
+ "@brainervirus/workit-opencode": "^7.4.0",
109
109
  "@inkjs/ui": "2.0.0",
110
110
  ink: "7.1.1",
111
111
  react: "19.2.8"
@@ -63144,7 +63144,8 @@ var init_skill_manifests = __esm(() => {
63144
63144
  "workit-test-audit",
63145
63145
  "workit-deslop",
63146
63146
  "workit-fanout",
63147
- "workit-verify-app"
63147
+ "workit-verify-app",
63148
+ "workit-retro"
63148
63149
  ];
63149
63150
  if (false) {}
63150
63151
  });
@@ -69916,6 +69917,270 @@ var init_launch = __esm(() => {
69916
69917
  init_upgrade();
69917
69918
  });
69918
69919
 
69920
+ // packages/workit-core/src/knowledge.ts
69921
+ import { spawnSync as spawnSync14 } from "node:child_process";
69922
+ import fs17 from "node:fs";
69923
+ import path36 from "node:path";
69924
+ function lintKnowledge(root) {
69925
+ const seen = new Set;
69926
+ const unreadable = [];
69927
+ const load = (file) => {
69928
+ const doc = read(root, file);
69929
+ if (doc === "unreadable")
69930
+ unreadable.push(file);
69931
+ if (doc === null || doc === "unreadable" || seen.has(doc.real))
69932
+ return null;
69933
+ seen.add(doc.real);
69934
+ return doc;
69935
+ };
69936
+ const steering = STEERING_FILES.map(load).filter((doc) => doc !== null);
69937
+ const needBased = NEED_BASED_FILES.map(load).filter((doc) => doc !== null);
69938
+ const findings = [];
69939
+ for (const doc of steering) {
69940
+ const bytes = Buffer.byteLength(doc.text);
69941
+ if (bytes > AGENTS_BYTE_BUDGET)
69942
+ findings.push({
69943
+ rule: "agents-budget",
69944
+ file: doc.file,
69945
+ line: null,
69946
+ message: `${bytes} bytes, over the ${AGENTS_BYTE_BUDGET}-byte budget; move detail behind a pointer or into a check, and name what each addition removes`
69947
+ });
69948
+ }
69949
+ for (const doc of needBased)
69950
+ if (scaffoldOnly(doc))
69951
+ findings.push({
69952
+ rule: "scaffold-file",
69953
+ file: doc.file,
69954
+ line: null,
69955
+ message: "holds no entry (headings, comments or placeholders only); delete it, or create it in the same edit as its first real entry"
69956
+ });
69957
+ const tracked = trackedTopLevel(root);
69958
+ for (const doc of [...steering, ...needBased])
69959
+ findings.push(...brokenLinks(root, doc, tracked));
69960
+ const standards = needBased.find((doc) => doc.file === STANDARDS_FILE);
69961
+ if (standards)
69962
+ findings.push(...duplicateRules(steering, standards));
69963
+ return {
69964
+ root,
69965
+ budget: AGENTS_BYTE_BUDGET,
69966
+ files: [...steering, ...needBased].map((doc) => ({
69967
+ file: doc.file,
69968
+ bytes: Buffer.byteLength(doc.text)
69969
+ })),
69970
+ findings,
69971
+ unreadable
69972
+ };
69973
+ }
69974
+ var AGENTS_BYTE_BUDGET, STEERING_FILES, NEED_BASED_FILES, STANDARDS_FILE = "CODING_STANDARDS.md", MIN_RULE_WORDS = 6, FENCE, HEADING, TABLE_SEPARATOR, LIST_ITEM, PLACEHOLDER2, INDENTED_CODE, blank = (line) => (line ?? "").trim() === "", proseLines = (lines) => {
69975
+ const out = [];
69976
+ let fence = null;
69977
+ let comment = false;
69978
+ let indentedCode = false;
69979
+ let inList = false;
69980
+ lines.forEach((raw, index) => {
69981
+ if (fence) {
69982
+ const close = FENCE.exec(raw);
69983
+ if (close && close[1][0] === fence[0] && close[1].length >= fence.length && !close[2].trim())
69984
+ fence = null;
69985
+ return;
69986
+ }
69987
+ if (!comment) {
69988
+ const open = FENCE.exec(raw);
69989
+ if (open && !(open[1][0] === "`" && open[2].includes("`"))) {
69990
+ fence = open[1];
69991
+ indentedCode = false;
69992
+ return;
69993
+ }
69994
+ if (INDENTED_CODE.test(raw) && !blank(raw)) {
69995
+ if (indentedCode || !inList && blank(lines[index - 1])) {
69996
+ indentedCode = true;
69997
+ return;
69998
+ }
69999
+ } else if (!blank(raw)) {
70000
+ indentedCode = false;
70001
+ inList = LIST_ITEM.test(raw) || inList && /^\s/.test(raw);
70002
+ }
70003
+ }
70004
+ let text = raw;
70005
+ if (comment) {
70006
+ const end = text.indexOf("-->");
70007
+ if (end === -1)
70008
+ return;
70009
+ comment = false;
70010
+ text = text.slice(end + 3);
70011
+ }
70012
+ text = text.replace(/<!--[\s\S]*?-->/g, "");
70013
+ const open = text.indexOf("<!--");
70014
+ if (open !== -1) {
70015
+ comment = true;
70016
+ text = text.slice(0, open);
70017
+ }
70018
+ out.push({ index, text });
70019
+ });
70020
+ return out;
70021
+ }, withoutFrontMatter = (lines) => {
70022
+ if (lines[0]?.trim() !== "---")
70023
+ return lines;
70024
+ const end = lines.indexOf("---", 1);
70025
+ return end === -1 ? lines : [...lines.slice(0, end + 1).map(() => ""), ...lines.slice(end + 1)];
70026
+ }, read = (root, file) => {
70027
+ const full = path36.join(root, file);
70028
+ try {
70029
+ fs17.lstatSync(full);
70030
+ } catch {
70031
+ return null;
70032
+ }
70033
+ try {
70034
+ if (!fs17.statSync(full).isFile())
70035
+ return null;
70036
+ const text = fs17.readFileSync(full, "utf8");
70037
+ return { file, text, lines: text.split(/\r?\n/), real: fs17.realpathSync(full) };
70038
+ } catch {
70039
+ return "unreadable";
70040
+ }
70041
+ }, trackedTopLevel = (root) => {
70042
+ const run = spawnSync14("git", ["ls-files", "-z", "--cached"], {
70043
+ cwd: root,
70044
+ encoding: "utf8",
70045
+ maxBuffer: 256 * 1024 * 1024,
70046
+ timeout: 20000,
70047
+ windowsHide: true
70048
+ });
70049
+ if (run.status !== 0)
70050
+ return null;
70051
+ return new Set(run.stdout.split("\x00").filter(Boolean).map((file) => file.split("/")[0]));
70052
+ }, exists = (target) => {
70053
+ try {
70054
+ fs17.statSync(target);
70055
+ return true;
70056
+ } catch {
70057
+ return false;
70058
+ }
70059
+ }, LINK, REFERENCE_DEFINITION, CODE_SPAN, PATH_LIKE, isExternal = (target) => /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("#") || target.startsWith("//"), localTarget = (target) => {
70060
+ const bare = target.replace(/[#?].*$/, "");
70061
+ try {
70062
+ return decodeURIComponent(bare);
70063
+ } catch {
70064
+ return bare;
70065
+ }
70066
+ }, brokenLinks = (root, doc, tracked) => {
70067
+ const findings = [];
70068
+ const base = path36.dirname(path36.join(root, doc.file));
70069
+ const missing = (target, line, how) => findings.push({
70070
+ rule: "broken-link",
70071
+ file: doc.file,
70072
+ line,
70073
+ message: `${how} \`${target}\` points at a missing file`
70074
+ });
70075
+ for (const { index, text } of proseLines(doc.lines)) {
70076
+ const prose = text.replace(CODE_SPAN, (span) => " ".repeat(span.length));
70077
+ const targets = [...prose.matchAll(LINK)].map((match) => match[1] ?? match[2]);
70078
+ const definition = REFERENCE_DEFINITION.exec(prose)?.[1];
70079
+ if (definition)
70080
+ targets.push(definition);
70081
+ for (const target of targets) {
70082
+ if (isExternal(target))
70083
+ continue;
70084
+ const local = localTarget(target);
70085
+ if (!local)
70086
+ continue;
70087
+ const resolved = local.startsWith("/") ? path36.join(root, local) : path36.resolve(base, local);
70088
+ if (!exists(resolved))
70089
+ missing(target, index + 1, "link");
70090
+ }
70091
+ for (const [, span] of text.matchAll(CODE_SPAN)) {
70092
+ const candidate = span.replace(/:\d+(?::\d+)?$/, "");
70093
+ if (!PATH_LIKE.test(candidate))
70094
+ continue;
70095
+ const relative = /^\.\.?\//.test(candidate);
70096
+ const first = candidate.split("/")[0];
70097
+ if (!relative && !(tracked ? tracked.has(first) : exists(path36.join(root, first))))
70098
+ continue;
70099
+ const resolved = relative ? path36.resolve(base, candidate) : path36.join(root, candidate);
70100
+ if (!exists(resolved))
70101
+ missing(span, index + 1, "pointer");
70102
+ }
70103
+ }
70104
+ return findings;
70105
+ }, isPlaceholder2 = (text) => PLACEHOLDER2.test(text.replace(LIST_ITEM, "").replace(/[*_`]/g, "").replace(/^\[\s?\]\s*/, "").trim()), scaffoldOnly = (doc) => {
70106
+ const lines = proseLines(withoutFrontMatter(doc.lines));
70107
+ return lines.every(({ text }, position) => {
70108
+ const trimmed = text.trim();
70109
+ if (trimmed === "" || HEADING.test(text) || /^([-*_])(\s*\1){2,}$/.test(trimmed))
70110
+ return true;
70111
+ if (TABLE_SEPARATOR.test(text))
70112
+ return true;
70113
+ const next = lines[position + 1]?.text ?? "";
70114
+ if (trimmed.startsWith("|") && TABLE_SEPARATOR.test(next))
70115
+ return true;
70116
+ if (trimmed.startsWith("|"))
70117
+ return trimmed.split("|").map((cell) => cell.trim()).every((cell) => cell === "" || isPlaceholder2(cell));
70118
+ return isPlaceholder2(trimmed);
70119
+ });
70120
+ }, ruleUnits = (doc) => {
70121
+ const units = [];
70122
+ let current = null;
70123
+ const flush = () => {
70124
+ if (current)
70125
+ units.push(current);
70126
+ current = null;
70127
+ };
70128
+ for (const { index, text } of proseLines(doc.lines)) {
70129
+ const trimmed = text.trim();
70130
+ if (trimmed === "" || HEADING.test(text) || trimmed.startsWith("|")) {
70131
+ flush();
70132
+ continue;
70133
+ }
70134
+ if (LIST_ITEM.test(text) || !current) {
70135
+ flush();
70136
+ current = { line: index + 1, text: trimmed.replace(LIST_ITEM, "") };
70137
+ } else
70138
+ current.text += ` ${trimmed}`;
70139
+ }
70140
+ flush();
70141
+ return units;
70142
+ }, normalize = (sentence) => sentence.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_`]/g, "").toLowerCase().replace(/\s+/g, " ").replace(/[\s.,;:!?]+$/, "").trim(), sentences = (doc) => {
70143
+ const out = new Map;
70144
+ for (const unit of ruleUnits(doc))
70145
+ for (const sentence of unit.text.split(/(?<=[.!?])\s+/)) {
70146
+ const key = normalize(sentence);
70147
+ if (key.split(" ").length >= MIN_RULE_WORDS && !out.has(key))
70148
+ out.set(key, unit.line);
70149
+ }
70150
+ return out;
70151
+ }, duplicateRules = (steering, standards) => {
70152
+ const findings = [];
70153
+ const rules = sentences(standards);
70154
+ for (const doc of steering)
70155
+ for (const [key, line] of sentences(doc)) {
70156
+ const standardsLine = rules.get(key);
70157
+ if (standardsLine === undefined)
70158
+ continue;
70159
+ findings.push({
70160
+ rule: "duplicate-rule",
70161
+ file: doc.file,
70162
+ line,
70163
+ message: `repeats ${standards.file}:${standardsLine} ("${key.slice(0, 60)}${key.length > 60 ? "…" : ""}"); keep the rule in one file`
70164
+ });
70165
+ }
70166
+ return findings;
70167
+ };
70168
+ var init_knowledge = __esm(() => {
70169
+ AGENTS_BYTE_BUDGET = 8 * 1024;
70170
+ STEERING_FILES = ["AGENTS.md", "CLAUDE.md"];
70171
+ NEED_BASED_FILES = ["CODING_STANDARDS.md", "GLOSSARY.md"];
70172
+ FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
70173
+ HEADING = /^\s{0,3}#{1,6}(\s|$)/;
70174
+ TABLE_SEPARATOR = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/;
70175
+ LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s+/;
70176
+ PLACEHOLDER2 = /^(?:(?:tbd|todo|tba|fixme)\b.*|n\/a|none|none yet|coming soon|placeholder|\.\.\.|…|-)?[.:]?$/i;
70177
+ INDENTED_CODE = /^(?: {4}|\t)/;
70178
+ LINK = /!?\[[^\]]*\]\(\s*(?:<([^>\n]+)>|((?:[^()\s<>]|\([^()\s]*\))+))(?:\s+["'(][^)]*)?\)/g;
70179
+ REFERENCE_DEFINITION = /^\s{0,3}\[[^\]]+\]:\s*<?(\S+?)>?(?:\s|$)/;
70180
+ CODE_SPAN = /`([^`\n]+)`/g;
70181
+ PATH_LIKE = /^(?:\.{1,2}\/)?[\w@.-]+(?:\/[\w@.-]+)+\/?$/;
70182
+ });
70183
+
69919
70184
  // packages/workit-cli/src/prompt.ts
69920
70185
  import { createInterface } from "node:readline/promises";
69921
70186
  async function askLine(question, output = process.stdout) {
@@ -69999,8 +70264,23 @@ async function run5(argv, io) {
69999
70264
  fixLock = clearStaleMetadataLock(root, { force });
70000
70265
  }
70001
70266
  const report = runDoctor({ host: "cli", cwd: io.cwd, workspaceRoot: root });
70267
+ let knowledge = null;
70268
+ try {
70269
+ knowledge = lintKnowledge(root);
70270
+ if (knowledge.unreadable.length > 0)
70271
+ knowledge = null;
70272
+ } catch {
70273
+ knowledge = null;
70274
+ }
70275
+ const agents = knowledge?.files.find((file) => file.file === "AGENTS.md");
70276
+ const knowledgeLine = knowledge ? `info knowledge — ${agents ? `AGENTS.md ${agents.bytes} of ${knowledge.budget} bytes` : "no AGENTS.md"}; ${knowledge.findings.length} finding(s) (workit knowledge lint)` : "info knowledge — unavailable";
70002
70277
  if (argv.includes("--json")) {
70003
- io.stdout(`${JSON.stringify(fixLock ? { ...report, fixLock } : report, null, 2)}
70278
+ const summary = knowledge ? {
70279
+ agentsBytes: agents?.bytes ?? null,
70280
+ budget: knowledge.budget,
70281
+ findings: knowledge.findings.length
70282
+ } : { unavailable: true };
70283
+ io.stdout(`${JSON.stringify({ ...report, ...fixLock ? { fixLock } : {}, knowledge: summary }, null, 2)}
70004
70284
  `);
70005
70285
  return report.exitCode;
70006
70286
  }
@@ -70019,12 +70299,15 @@ async function run5(argv, io) {
70019
70299
  io.stdout(` fix: ${check.fix}
70020
70300
  `);
70021
70301
  }
70302
+ io.stdout(`${knowledgeLine}
70303
+ `);
70022
70304
  io.stdout(`passed ${report.summary.passed} / warned ${report.summary.warned} / failed ${report.summary.failed}
70023
70305
  `);
70024
70306
  return report.exitCode;
70025
70307
  }
70026
70308
  var init_doctor2 = __esm(() => {
70027
70309
  init_doctor();
70310
+ init_knowledge();
70028
70311
  init_store_lock();
70029
70312
  init_output();
70030
70313
  init_prompt();
@@ -70135,11 +70418,11 @@ var init_redact = __esm(() => {
70135
70418
  });
70136
70419
 
70137
70420
  // packages/workit-core/src/checks.ts
70138
- import { spawn, spawnSync as spawnSync14 } from "node:child_process";
70421
+ import { spawn, spawnSync as spawnSync15 } from "node:child_process";
70139
70422
  import { createHash as createHash14 } from "node:crypto";
70140
- import fs17 from "node:fs";
70423
+ import fs18 from "node:fs";
70141
70424
  import os13 from "node:os";
70142
- import path36 from "node:path";
70425
+ import path37 from "node:path";
70143
70426
  import { StringDecoder } from "node:string_decoder";
70144
70427
  function tailLines(text, lines = TAIL_LINES) {
70145
70428
  const all = text.replace(/\r\n?/gu, `
@@ -70259,10 +70542,10 @@ function planSpawn(argv, options) {
70259
70542
  return { file: argv[0], args: [], verbatim: false, shell: true };
70260
70543
  if (options.platform !== "win32")
70261
70544
  return { file: argv[0], args: argv.slice(1), verbatim: false, shell: false };
70262
- const win = path36.win32;
70545
+ const win = path37.win32;
70263
70546
  const isFile = options.isFile ?? ((file) => {
70264
70547
  try {
70265
- return fs17.statSync(file).isFile();
70548
+ return fs18.statSync(file).isFile();
70266
70549
  } catch {
70267
70550
  return false;
70268
70551
  }
@@ -70314,7 +70597,7 @@ function runCheckCommand(argv, options) {
70314
70597
  if (!child?.pid)
70315
70598
  return;
70316
70599
  if (windows)
70317
- spawnSync14("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
70600
+ spawnSync15("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
70318
70601
  stdio: "ignore",
70319
70602
  windowsHide: true
70320
70603
  });
@@ -70416,13 +70699,13 @@ function runCheckCommand(argv, options) {
70416
70699
  function storeLog(storeRoot, log) {
70417
70700
  const digest = logDigest(log);
70418
70701
  const ref = `blobs/logs/${digest.slice("sha256:".length)}.log`;
70419
- const file = path36.join(storeRoot, ref);
70702
+ const file = path37.join(storeRoot, ref);
70420
70703
  try {
70421
- if (!fs17.existsSync(file)) {
70422
- fs17.mkdirSync(path36.dirname(file), { recursive: true });
70704
+ if (!fs18.existsSync(file)) {
70705
+ fs18.mkdirSync(path37.dirname(file), { recursive: true });
70423
70706
  const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
70424
- fs17.writeFileSync(temp, log, { mode: 384 });
70425
- fs17.renameSync(temp, file);
70707
+ fs18.writeFileSync(temp, log, { mode: 384 });
70708
+ fs18.renameSync(temp, file);
70426
70709
  }
70427
70710
  return { digest, ref };
70428
70711
  } catch {
@@ -70432,17 +70715,17 @@ function storeLog(storeRoot, log) {
70432
70715
  function pruneCheckLogs(storeRoot, options = {}) {
70433
70716
  const limits = { ...LOG_RETENTION, ...options.retention };
70434
70717
  const now = options.now ?? Date.now();
70435
- const dir = path36.join(storeRoot, "blobs", "logs");
70718
+ const dir = path37.join(storeRoot, "blobs", "logs");
70436
70719
  const report = { removed: 0, removedBytes: 0, kept: 0 };
70437
70720
  let names;
70438
70721
  try {
70439
- names = fs17.readdirSync(dir);
70722
+ names = fs18.readdirSync(dir);
70440
70723
  } catch {
70441
70724
  return report;
70442
70725
  }
70443
70726
  const files = names.flatMap((name) => {
70444
70727
  try {
70445
- const stat = fs17.statSync(path36.join(dir, name));
70728
+ const stat = fs18.statSync(path37.join(dir, name));
70446
70729
  return stat.isFile() ? [{ name, size: stat.size, mtimeMs: stat.mtimeMs }] : [];
70447
70730
  } catch {
70448
70731
  return [];
@@ -70461,7 +70744,7 @@ function pruneCheckLogs(storeRoot, options = {}) {
70461
70744
  continue;
70462
70745
  if (!options.dryRun)
70463
70746
  try {
70464
- fs17.rmSync(path36.join(dir, file.name), { force: true });
70747
+ fs18.rmSync(path37.join(dir, file.name), { force: true });
70465
70748
  } catch {
70466
70749
  continue;
70467
70750
  }
@@ -70712,7 +70995,7 @@ var exports_check = {};
70712
70995
  __export(exports_check, {
70713
70996
  run: () => run9
70714
70997
  });
70715
- import path37 from "node:path";
70998
+ import path38 from "node:path";
70716
70999
  function parse5(argv) {
70717
71000
  const split = argv.indexOf("--");
70718
71001
  const head = split >= 0 ? argv.slice(0, split) : [...argv];
@@ -70895,7 +71178,7 @@ async function run9(argv, io) {
70895
71178
  if (options.shell && command.length !== 1)
70896
71179
  return emit(io, fail("invalid_input", '--shell takes the command as one string: workit check --shell -- "<cmd>"'));
70897
71180
  }
70898
- const relCwd = posix(path37.relative(top, runCwd));
71181
+ const relCwd = posix(path38.relative(top, runCwd));
70899
71182
  const configured = !options.shell && matchesNamedCheck(config, name, command, relCwd);
70900
71183
  const actor = actorFromEnv(io.env);
70901
71184
  const branchKey = codeKey(io.cwd, { base: options.base });
@@ -71019,7 +71302,7 @@ async function run9(argv, io) {
71019
71302
  return recordError.code === "busy" ? 4 : recordError.code === "invalid_input" ? 2 : 5;
71020
71303
  return 0;
71021
71304
  }
71022
- var USAGE3 = "workit check <name> | workit check [--name <n>] [--shell] [--timeout <s>] [--task <id>] [--json] -- <cmd…>", RECORD_TAIL_LINES = 20, RECORD_TAIL_CHARS = 200, posix = (value) => value.split(path37.sep).join("/") || ".", ENGINE_CODES2, FINGERPRINT_VARS;
71305
+ var USAGE3 = "workit check <name> | workit check [--name <n>] [--shell] [--timeout <s>] [--task <id>] [--json] -- <cmd…>", RECORD_TAIL_LINES = 20, RECORD_TAIL_CHARS = 200, posix = (value) => value.split(path38.sep).join("/") || ".", ENGINE_CODES2, FINGERPRINT_VARS;
71023
71306
  var init_check = __esm(() => {
71024
71307
  init_check_config();
71025
71308
  init_checks3();
@@ -71040,7 +71323,7 @@ var init_check = __esm(() => {
71040
71323
  var success2 = (data) => ({ ok: true, data }), failure3 = (code, error, unblock) => ({ ok: false, code, error, ...unblock ? { unblock } : {} });
71041
71324
 
71042
71325
  // packages/workit-core/src/git/ops.ts
71043
- import { spawnSync as spawnSync15 } from "node:child_process";
71326
+ import { spawnSync as spawnSync16 } from "node:child_process";
71044
71327
  function dirtEntries(cwd) {
71045
71328
  const run = git5(cwd, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
71046
71329
  if (!run.ok)
@@ -71384,7 +71667,7 @@ ${run.stdout}`;
71384
71667
  return /stale info|cannot lock ref|fetch first|non-fast-forward/iu.test(text) ? { ok: false, lease: true, error: "the remote branch tip changed before deletion" } : { ok: false, lease: false, error: gitError(run) };
71385
71668
  }
71386
71669
  var COMMIT_TIMEOUT_MS, PUSH_TIMEOUT_MS = 120000, git5 = (cwd, args, options = {}) => {
71387
- const result = spawnSync15("git", args, {
71670
+ const result = spawnSync16("git", args, {
71388
71671
  cwd,
71389
71672
  encoding: "utf8",
71390
71673
  env: { ...options.env ?? process.env, GIT_TERMINAL_PROMPT: "0" },
@@ -71420,11 +71703,11 @@ var init_ops = __esm(() => {
71420
71703
  });
71421
71704
 
71422
71705
  // packages/workit-core/src/forge/reruns.ts
71423
- import fs18 from "node:fs";
71424
- import path38 from "node:path";
71706
+ import fs19 from "node:fs";
71707
+ import path39 from "node:path";
71425
71708
  function rerunLogPath(cwd) {
71426
71709
  const common = gitCommonDir(cwd);
71427
- return common ? path38.join(common, "workit", "ci-reruns.jsonl") : null;
71710
+ return common ? path39.join(common, "workit", "ci-reruns.jsonl") : null;
71428
71711
  }
71429
71712
  function rerunCounts(cwd, key) {
71430
71713
  const counts = new Map;
@@ -71433,7 +71716,7 @@ function rerunCounts(cwd, key) {
71433
71716
  return counts;
71434
71717
  let text;
71435
71718
  try {
71436
- text = fs18.readFileSync(file, "utf8");
71719
+ text = fs19.readFileSync(file, "utf8");
71437
71720
  } catch {
71438
71721
  return counts;
71439
71722
  }
@@ -71459,8 +71742,8 @@ function recordReruns(cwd, rows) {
71459
71742
  if (!file)
71460
71743
  return false;
71461
71744
  try {
71462
- fs18.mkdirSync(path38.dirname(file), { recursive: true });
71463
- fs18.appendFileSync(file, rows.map((row) => `${JSON.stringify(row)}
71745
+ fs19.mkdirSync(path39.dirname(file), { recursive: true });
71746
+ fs19.appendFileSync(file, rows.map((row) => `${JSON.stringify(row)}
71464
71747
  `).join(""));
71465
71748
  return true;
71466
71749
  } catch {
@@ -71472,18 +71755,18 @@ function withRerunLock(cwd, fn) {
71472
71755
  if (!file)
71473
71756
  return fn();
71474
71757
  const lock = `${file.slice(0, -".jsonl".length)}.lock`;
71475
- fs18.mkdirSync(path38.dirname(lock), { recursive: true });
71758
+ fs19.mkdirSync(path39.dirname(lock), { recursive: true });
71476
71759
  const until = Date.now() + LOCK_WAIT_MS2;
71477
71760
  for (;; ) {
71478
71761
  try {
71479
- fs18.closeSync(fs18.openSync(lock, "wx"));
71762
+ fs19.closeSync(fs19.openSync(lock, "wx"));
71480
71763
  break;
71481
71764
  } catch (error) {
71482
71765
  if (error.code !== "EEXIST")
71483
71766
  throw error;
71484
71767
  try {
71485
- if (Date.now() - fs18.statSync(lock).mtimeMs > LOCK_STALE_MS2) {
71486
- fs18.rmSync(lock, { force: true });
71768
+ if (Date.now() - fs19.statSync(lock).mtimeMs > LOCK_STALE_MS2) {
71769
+ fs19.rmSync(lock, { force: true });
71487
71770
  continue;
71488
71771
  }
71489
71772
  } catch {
@@ -71497,7 +71780,7 @@ function withRerunLock(cwd, fn) {
71497
71780
  try {
71498
71781
  return fn();
71499
71782
  } finally {
71500
- fs18.rmSync(lock, { force: true });
71783
+ fs19.rmSync(lock, { force: true });
71501
71784
  }
71502
71785
  }
71503
71786
  var isRow = (value) => {
@@ -71865,7 +72148,7 @@ var init_report = __esm(() => {
71865
72148
  });
71866
72149
 
71867
72150
  // packages/workit-core/src/forge/pr-ops.ts
71868
- import { spawnSync as spawnSync16 } from "node:child_process";
72151
+ import { spawnSync as spawnSync17 } from "node:child_process";
71869
72152
  function fillFromCommits(cwd, resolved, base) {
71870
72153
  const tracking = `refs/remotes/${resolved.baseRemote}/${base}`;
71871
72154
  if (!base.startsWith("-"))
@@ -71873,7 +72156,7 @@ function fillFromCommits(cwd, resolved, base) {
71873
72156
  const from = resolveRef(cwd, tracking) ?? resolveRef(cwd, `refs/heads/${base}`);
71874
72157
  if (!from)
71875
72158
  return failure3("not_found", `base ${base} is not available locally`, `git fetch ${resolved.baseRemote} ${base}`);
71876
- const log = spawnSync16("git", ["log", "--reverse", "--no-merges", "--format=%s%x1f%b%x1e", `${from}..HEAD`], {
72159
+ const log = spawnSync17("git", ["log", "--reverse", "--no-merges", "--format=%s%x1f%b%x1e", `${from}..HEAD`], {
71877
72160
  cwd,
71878
72161
  encoding: "utf8",
71879
72162
  timeout: GIT_TIMEOUTS.local,
@@ -72148,17 +72431,17 @@ var init_pr_ops = __esm(() => {
72148
72431
  });
72149
72432
 
72150
72433
  // packages/workit-core/src/forge/exec.ts
72151
- import { spawnSync as spawnSync17 } from "node:child_process";
72152
- import fs19 from "node:fs";
72153
- import path39 from "node:path";
72434
+ import { spawnSync as spawnSync18 } from "node:child_process";
72435
+ import fs20 from "node:fs";
72436
+ import path40 from "node:path";
72154
72437
  function findExecutable(bin, env) {
72155
- const dirs = (env.PATH ?? env.Path ?? "").split(path39.delimiter).filter(Boolean);
72438
+ const dirs = (env.PATH ?? env.Path ?? "").split(path40.delimiter).filter(Boolean);
72156
72439
  const exts = process.platform === "win32" ? [".exe"] : [""];
72157
72440
  for (const dir of dirs)
72158
72441
  for (const ext of exts) {
72159
- const candidate = path39.join(dir, `${bin}${ext}`);
72442
+ const candidate = path40.join(dir, `${bin}${ext}`);
72160
72443
  try {
72161
- const stat = fs19.statSync(candidate);
72444
+ const stat = fs20.statSync(candidate);
72162
72445
  if (stat.isFile())
72163
72446
  return candidate;
72164
72447
  } catch {}
@@ -72183,7 +72466,7 @@ function systemRunner(kind, apiHost, env = process.env) {
72183
72466
  if (!exe)
72184
72467
  return { status: null, stdout: "", stderr: "", timedOut: false, missing: true };
72185
72468
  const token = options.token ? kind === "github" ? { GH_TOKEN: options.token, GH_ENTERPRISE_TOKEN: options.token } : { GITLAB_TOKEN: options.token } : {};
72186
- const result = spawnSync17(exe, [...args], {
72469
+ const result = spawnSync18(exe, [...args], {
72187
72470
  encoding: "utf8",
72188
72471
  env: { ...childEnv, ...token },
72189
72472
  maxBuffer: MAX_BUFFER2,
@@ -72853,7 +73136,7 @@ var init_gitlab = __esm(() => {
72853
73136
  });
72854
73137
 
72855
73138
  // packages/workit-core/src/forge/resolve.ts
72856
- import fs20 from "node:fs";
73139
+ import fs21 from "node:fs";
72857
73140
  function resolveForge(cwd, options = {}) {
72858
73141
  let workspace;
72859
73142
  try {
@@ -72882,7 +73165,7 @@ function resolveForge(cwd, options = {}) {
72882
73165
  if (derived.via === "host_name" && !explicit)
72883
73166
  return failure3("blocked", `token_host_unverified: push host ${derived.host} only looks like ${kind}; workspace ${workspace?.name ?? "?"} vcs.tokenFile is not sent to it`, `if ${apiHost} is your ${kind} server, set ${kind}.host in ~/.config/workit/vcs.json (or vcs.host for the workspace); otherwise fix the push remote`);
72884
73167
  try {
72885
- token = fs20.readFileSync(tokenFile.trim(), "utf8").trim();
73168
+ token = fs21.readFileSync(tokenFile.trim(), "utf8").trim();
72886
73169
  } catch {
72887
73170
  return failure3("blocked", `workspace ${workspace?.name ?? "?"} vcs.tokenFile cannot be read`, "fix vcs.tokenFile in ~/.config/workit/workspaces.json");
72888
73171
  }
@@ -73017,6 +73300,20 @@ function parseDuration2(value) {
73017
73300
  const factor = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60000 : 3600000;
73018
73301
  return Math.round(Number(match[1]) * factor);
73019
73302
  }
73303
+ function releaseTrunk(io, track, branch) {
73304
+ const resolved = vcsConfig("resolve", io.cwd, {
73305
+ track,
73306
+ ...branch ? { branch } : {}
73307
+ });
73308
+ if (resolved.ok === false)
73309
+ return track !== null || String(resolved.configPath ?? "").endsWith("workspaces.json") ? { error: String(resolved.error) } : { trunk: null, tracked: false };
73310
+ if (resolved.releaseTrack?.blocking)
73311
+ return { error: String(resolved.releaseTrack.blocking) };
73312
+ return {
73313
+ trunk: String(resolved.defaultTargetBranch ?? "") || null,
73314
+ tracked: Boolean(resolved.releaseTrack)
73315
+ };
73316
+ }
73020
73317
  function connect(io, branch) {
73021
73318
  const resolved = resolveForge(io.cwd, {
73022
73319
  branch,
@@ -73084,6 +73381,7 @@ function renderStatus(doc) {
73084
73381
  var forgeDeps, usage3 = (io, message, line) => emit(io, fail("invalid_input", message, { unblock: line })), forgeFail = (io, result, data) => emit(io, fail(result.code, result.error, { data, unblock: result.unblock })), short = (sha) => sha ? sha.slice(0, 7) : "?";
73085
73382
  var init_forge_common = __esm(() => {
73086
73383
  init_resolve();
73384
+ init_vcs_config();
73087
73385
  init_output();
73088
73386
  forgeDeps = {
73089
73387
  runner: undefined,
@@ -73099,7 +73397,7 @@ __export(exports_pr, {
73099
73397
  run: () => run10
73100
73398
  });
73101
73399
  import { readFileSync as readFileSync18 } from "node:fs";
73102
- import path40 from "node:path";
73400
+ import path41 from "node:path";
73103
73401
  async function status(argv, io) {
73104
73402
  const flags = parseFlags(argv, { pr: "value", branch: "value", "log-lines": "value" });
73105
73403
  if (typeof flags === "string")
@@ -73170,7 +73468,7 @@ async function create4(argv, io) {
73170
73468
  let body = flags.values.body ?? null;
73171
73469
  if (flags.values["body-file"] !== undefined) {
73172
73470
  try {
73173
- body = readFileSync18(path40.resolve(io.cwd, flags.values["body-file"]), "utf8");
73471
+ body = readFileSync18(path41.resolve(io.cwd, flags.values["body-file"]), "utf8");
73174
73472
  } catch {
73175
73473
  return usage3(io, `cannot read --body-file ${flags.values["body-file"]}`, CREATE_USAGE);
73176
73474
  }
@@ -73662,7 +73960,7 @@ var init_git2 = __esm(() => {
73662
73960
  });
73663
73961
 
73664
73962
  // packages/workit-core/src/forge/verify.ts
73665
- import { spawnSync as spawnSync18 } from "node:child_process";
73963
+ import { spawnSync as spawnSync19 } from "node:child_process";
73666
73964
  function finish(cwd, report, actor) {
73667
73965
  const delivered = report.observations.length > 0 && report.observations.every((entry) => entry.ok);
73668
73966
  const out = { ...report, delivered };
@@ -73875,7 +74173,7 @@ var NPM_NAME, SEMVER, SAFE_ARG, systemNpm = (args) => {
73875
74173
  };
73876
74174
  const found = process.platform === "win32" ? findExecutable("npm.cmd", process.env) : null;
73877
74175
  const npmCmd = found && !/["%!^]/u.test(found) ? `"${found}"` : "npm.cmd";
73878
- const result = process.platform === "win32" ? spawnSync18(`${npmCmd} ${args.join(" ")}`, { ...options, shell: true }) : spawnSync18("npm", [...args], options);
74176
+ const result = process.platform === "win32" ? spawnSync19(`${npmCmd} ${args.join(" ")}`, { ...options, shell: true }) : spawnSync19("npm", [...args], options);
73879
74177
  return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
73880
74178
  }, short3 = (sha) => sha ? sha.slice(0, 12) : "(absent)", npmView = (stdout) => {
73881
74179
  try {
@@ -73982,10 +74280,10 @@ var init_verify_delivery = __esm(() => {
73982
74280
  });
73983
74281
 
73984
74282
  // packages/workit-core/src/stack.ts
73985
- import { spawnSync as spawnSync19 } from "node:child_process";
74283
+ import { spawnSync as spawnSync20 } from "node:child_process";
73986
74284
  import { createHash as createHash15, randomBytes as randomBytes3 } from "node:crypto";
73987
- import fs21 from "node:fs";
73988
- import path41 from "node:path";
74285
+ import fs22 from "node:fs";
74286
+ import path42 from "node:path";
73989
74287
  function worktreeOf(cwd, branch) {
73990
74288
  const list = gitRun3(cwd, ["worktree", "list", "--porcelain"]);
73991
74289
  if (!list.ok)
@@ -74008,7 +74306,7 @@ function rebasingBranch2(dir) {
74008
74306
  if (!found.ok)
74009
74307
  continue;
74010
74308
  try {
74011
- return fs21.readFileSync(found.stdout.trim(), "utf8").trim();
74309
+ return fs22.readFileSync(found.stdout.trim(), "utf8").trim();
74012
74310
  } catch {}
74013
74311
  }
74014
74312
  return null;
@@ -74016,7 +74314,7 @@ function rebasingBranch2(dir) {
74016
74314
  function rebaseInProgress(dir) {
74017
74315
  for (const name of ["rebase-merge", "rebase-apply"]) {
74018
74316
  const found = gitRun3(dir, ["rev-parse", "--path-format=absolute", "--git-path", name]);
74019
- if (found.ok && fs21.existsSync(found.stdout.trim()))
74317
+ if (found.ok && fs22.existsSync(found.stdout.trim()))
74020
74318
  return true;
74021
74319
  }
74022
74320
  return false;
@@ -74028,8 +74326,8 @@ function gitRestack(cwd, input) {
74028
74326
  const scratch = scratchDir(cwd);
74029
74327
  if (!scratch)
74030
74328
  return { ok: false, conflict: false, worktree: cwd, error: "no store root" };
74031
- fs21.mkdirSync(scratch, { recursive: true });
74032
- dir = path41.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes3(4).toString("hex")}`);
74329
+ fs22.mkdirSync(scratch, { recursive: true });
74330
+ dir = path42.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes3(4).toString("hex")}`);
74033
74331
  const added = gitRun3(cwd, ["worktree", "add", "-q", dir, input.branch], GIT_TIMEOUTS.worktree);
74034
74332
  if (!added.ok)
74035
74333
  return { ok: false, conflict: false, worktree: cwd, error: firstLine(added) };
@@ -74067,10 +74365,10 @@ function gitRestack(cwd, input) {
74067
74365
  }
74068
74366
  function pruneScratch(cwd) {
74069
74367
  const scratch = scratchDir(cwd);
74070
- if (!scratch || !fs21.existsSync(scratch))
74368
+ if (!scratch || !fs22.existsSync(scratch))
74071
74369
  return;
74072
- for (const name of fs21.readdirSync(scratch)) {
74073
- const dir = path41.join(scratch, name);
74370
+ for (const name of fs22.readdirSync(scratch)) {
74371
+ const dir = path42.join(scratch, name);
74074
74372
  if (!rebaseInProgress(dir) && !trackedDirt(dir))
74075
74373
  gitRun3(cwd, ["worktree", "remove", "--force", dir]);
74076
74374
  }
@@ -74080,7 +74378,7 @@ function stacksDir(cwd) {
74080
74378
  const root = storeRoot(cwd);
74081
74379
  if (!root.ok)
74082
74380
  return stackFail(root.code, root.error, root.unblock);
74083
- return { ok: true, data: path41.join(root.value.root, "stacks") };
74381
+ return { ok: true, data: path42.join(root.value.root, "stacks") };
74084
74382
  }
74085
74383
  function parseStack(raw) {
74086
74384
  if (!isRecord7(raw) || !Array.isArray(raw.branches))
@@ -74132,10 +74430,10 @@ function readStack(cwd, name) {
74132
74430
  const dir = stacksDir(cwd);
74133
74431
  if (!dir.ok)
74134
74432
  return dir;
74135
- const file = path41.join(dir.data, fileFor(name));
74433
+ const file = path42.join(dir.data, fileFor(name));
74136
74434
  let text;
74137
74435
  try {
74138
- text = fs21.readFileSync(file, "utf8");
74436
+ text = fs22.readFileSync(file, "utf8");
74139
74437
  } catch (error) {
74140
74438
  if (error.code === "ENOENT")
74141
74439
  return { ok: true, data: null };
@@ -74153,17 +74451,17 @@ function writeStack(cwd, stack, now = new Date) {
74153
74451
  const dir = stacksDir(cwd);
74154
74452
  if (!dir.ok)
74155
74453
  return dir;
74156
- const file = path41.join(dir.data, fileFor(stack.name));
74454
+ const file = path42.join(dir.data, fileFor(stack.name));
74157
74455
  const temp = `${file}.${process.pid}.tmp`;
74158
74456
  try {
74159
- fs21.mkdirSync(dir.data, { recursive: true });
74457
+ fs22.mkdirSync(dir.data, { recursive: true });
74160
74458
  stack.updatedAt = now.toISOString();
74161
- fs21.writeFileSync(temp, `${JSON.stringify(stack, null, 2)}
74459
+ fs22.writeFileSync(temp, `${JSON.stringify(stack, null, 2)}
74162
74460
  `);
74163
- fs21.renameSync(temp, file);
74461
+ fs22.renameSync(temp, file);
74164
74462
  return { ok: true, data: undefined };
74165
74463
  } catch (error) {
74166
- fs21.rmSync(temp, { force: true });
74464
+ fs22.rmSync(temp, { force: true });
74167
74465
  return stackFail("unavailable", `cannot write ${file}: ${error.message}`);
74168
74466
  }
74169
74467
  }
@@ -74173,7 +74471,7 @@ function listStacks(cwd) {
74173
74471
  return dir;
74174
74472
  let names = [];
74175
74473
  try {
74176
- names = fs21.readdirSync(dir.data).filter((name) => name.endsWith(".json"));
74474
+ names = fs22.readdirSync(dir.data).filter((name) => name.endsWith(".json"));
74177
74475
  } catch {
74178
74476
  return { ok: true, data: [] };
74179
74477
  }
@@ -74181,7 +74479,7 @@ function listStacks(cwd) {
74181
74479
  for (const name of names) {
74182
74480
  let parsed = null;
74183
74481
  try {
74184
- parsed = parseStack(JSON.parse(fs21.readFileSync(path41.join(dir.data, name), "utf8")));
74482
+ parsed = parseStack(JSON.parse(fs22.readFileSync(path42.join(dir.data, name), "utf8")));
74185
74483
  } catch {
74186
74484
  parsed = null;
74187
74485
  }
@@ -74228,7 +74526,7 @@ function stackLockStale(raw, age) {
74228
74526
  }
74229
74527
  function takeLock(lock) {
74230
74528
  try {
74231
- fs21.mkdirSync(lock);
74529
+ fs22.mkdirSync(lock);
74232
74530
  } catch (error) {
74233
74531
  if (error.code === "EEXIST")
74234
74532
  return null;
@@ -74240,24 +74538,24 @@ function takeLock(lock) {
74240
74538
  host: localLockHost(),
74241
74539
  nonce: randomBytes3(8).toString("hex")
74242
74540
  };
74243
- fs21.writeFileSync(path41.join(lock, OWNER), JSON.stringify(owner));
74541
+ fs22.writeFileSync(path42.join(lock, OWNER), JSON.stringify(owner));
74244
74542
  return owner;
74245
74543
  }
74246
74544
  function reapLock(lock, observed) {
74247
74545
  const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
74248
74546
  try {
74249
- fs21.renameSync(lock, tomb);
74547
+ fs22.renameSync(lock, tomb);
74250
74548
  } catch {
74251
74549
  return false;
74252
74550
  }
74253
74551
  if (readOwner(tomb).raw === observed) {
74254
- fs21.rmSync(tomb, { recursive: true, force: true });
74552
+ fs22.rmSync(tomb, { recursive: true, force: true });
74255
74553
  return true;
74256
74554
  }
74257
74555
  try {
74258
- fs21.renameSync(tomb, lock);
74556
+ fs22.renameSync(tomb, lock);
74259
74557
  } catch {
74260
- fs21.rmSync(tomb, { recursive: true, force: true });
74558
+ fs22.rmSync(tomb, { recursive: true, force: true });
74261
74559
  }
74262
74560
  return false;
74263
74561
  }
@@ -74267,8 +74565,8 @@ function releaseLock(lock, owner) {
74267
74565
  return;
74268
74566
  const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
74269
74567
  try {
74270
- fs21.renameSync(lock, tomb);
74271
- fs21.rmSync(tomb, { recursive: true, force: true });
74568
+ fs22.renameSync(lock, tomb);
74569
+ fs22.rmSync(tomb, { recursive: true, force: true });
74272
74570
  } catch {}
74273
74571
  }
74274
74572
  async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
@@ -74277,9 +74575,9 @@ async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
74277
74575
  return dir;
74278
74576
  if (!validName(name))
74279
74577
  return stackFail("invalid_input", "a stack name must be 1-200 printable characters");
74280
- const lock = path41.join(dir.data, `${fileFor(name)}.lock`);
74578
+ const lock = path42.join(dir.data, `${fileFor(name)}.lock`);
74281
74579
  try {
74282
- fs21.mkdirSync(dir.data, { recursive: true });
74580
+ fs22.mkdirSync(dir.data, { recursive: true });
74283
74581
  } catch (error) {
74284
74582
  return stackFail("unavailable", `cannot create ${dir.data}: ${error.message}`);
74285
74583
  }
@@ -74307,7 +74605,7 @@ async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
74307
74605
  const touch = () => {
74308
74606
  try {
74309
74607
  const now = new Date;
74310
- fs21.utimesSync(path41.join(lock, OWNER), now, now);
74608
+ fs22.utimesSync(path42.join(lock, OWNER), now, now);
74311
74609
  } catch {}
74312
74610
  };
74313
74611
  const beat = setInterval(touch, HEARTBEAT_MS);
@@ -74495,7 +74793,7 @@ function commitKeys(cwd, range) {
74495
74793
  const patches = new Map;
74496
74794
  const diff = gitRun3(cwd, ["log", "--no-merges", "-p", "--format=commit %H", range]);
74497
74795
  if (diff.ok && diff.stdout.trim()) {
74498
- const ids = spawnSync19("git", ["patch-id", "--stable"], {
74796
+ const ids = spawnSync20("git", ["patch-id", "--stable"], {
74499
74797
  cwd,
74500
74798
  input: diff.stdout,
74501
74799
  encoding: "utf8",
@@ -75103,7 +75401,7 @@ var STACK_VERSION = 1, stackFail = (code, error, unblock, data) => ({
75103
75401
  ...unblock ? { unblock } : {},
75104
75402
  ...data ? { data } : {}
75105
75403
  }), fromForge = (result) => stackFail(result.code, result.error, result.unblock), stackDeps, gitRun3 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
75106
- const run = spawnSync19("git", args, {
75404
+ const run = spawnSync20("git", args, {
75107
75405
  cwd,
75108
75406
  encoding: "utf8",
75109
75407
  timeout: timeoutMs,
@@ -75131,7 +75429,7 @@ var STACK_VERSION = 1, stackFail = (code, error, unblock, data) => ({
75131
75429
  return !status.ok || status.stdout.trim() !== "";
75132
75430
  }, scratchDir = (cwd) => {
75133
75431
  const dir = stacksDir(cwd);
75134
- return dir.ok ? path41.join(dir.data, "worktrees") : null;
75432
+ return dir.ok ? path42.join(dir.data, "worktrees") : null;
75135
75433
  }, firstLine = (run) => (`${run.stderr}
75136
75434
  ${run.stdout}`.split(`
75137
75435
  `).find((line) => line.trim()) ?? "git failed").trim().slice(0, 300), stackFileName = (name) => `${name.replace(/[^A-Za-z0-9._-]+/gu, "_").replace(/^[._]+/u, "").slice(0, 48).toLowerCase()}-${createHash15("sha256").update(name).digest("hex").slice(0, 12)}.json`, fileFor, validName = (name) => {
@@ -75146,14 +75444,14 @@ ${run.stdout}`.split(`
75146
75444
  }, isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), str3 = (value) => typeof value === "string" && value ? value : null, LOCK_WAIT_MS3 = 2000, HEARTBEAT_MS = 20000, HEARTBEAT_STALE_MS, OWNERLESS_STALE_MS = 30000, OWNER = "owner", readOwner = (lock) => {
75147
75445
  let raw = null;
75148
75446
  try {
75149
- raw = fs21.readFileSync(path41.join(lock, OWNER), "utf8");
75447
+ raw = fs22.readFileSync(path42.join(lock, OWNER), "utf8");
75150
75448
  } catch {
75151
75449
  raw = null;
75152
75450
  }
75153
75451
  let age = null;
75154
- for (const target of [path41.join(lock, OWNER), lock])
75452
+ for (const target of [path42.join(lock, OWNER), lock])
75155
75453
  try {
75156
- age = Date.now() - fs21.statSync(target).mtimeMs;
75454
+ age = Date.now() - fs22.statSync(target).mtimeMs;
75157
75455
  break;
75158
75456
  } catch {}
75159
75457
  return { raw, age };
@@ -75207,7 +75505,7 @@ async function plan(argv, io) {
75207
75505
  return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE);
75208
75506
  let trunk = flags.values.trunk ?? existing?.trunk ?? null;
75209
75507
  if (trunk === null || track !== null) {
75210
- const derived = defaultTrunk(io, track, flags.positionals[0] ?? null);
75508
+ const derived = releaseTrunk(io, track, flags.positionals[0] ?? null);
75211
75509
  if ("error" in derived)
75212
75510
  return emit(io, fail("blocked", derived.error, {
75213
75511
  unblock: "workit stack plan --track <name> … # or --trunk <branch>"
@@ -75375,17 +75673,7 @@ async function run14(argv, io) {
75375
75673
  return land(rest, io);
75376
75674
  return usage3(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE7);
75377
75675
  }
75378
- var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--timeout 20m] [--interval 30s] [--json]", USAGE7 = "workit stack plan|status|sync|land ... (workit help stack)", short4 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), defaultTrunk = (io, track, branch) => {
75379
- const resolved = vcsConfig("resolve", io.cwd, {
75380
- track,
75381
- ...branch ? { branch } : {}
75382
- });
75383
- if (resolved.ok === false)
75384
- return track !== null || String(resolved.configPath ?? "").endsWith("workspaces.json") ? { error: String(resolved.error) } : { trunk: null };
75385
- if (resolved.releaseTrack?.blocking)
75386
- return { error: String(resolved.releaseTrack.blocking) };
75387
- return { trunk: String(resolved.defaultTargetBranch ?? "") || null };
75388
- }, label = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
75676
+ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--timeout 20m] [--interval 30s] [--json]", USAGE7 = "workit stack plan|status|sync|land ... (workit help stack)", short4 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), label = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
75389
75677
  `${data.dryRun ? "would sync" : "synced"} stack ${data.name}${data.merged.length ? `; merged: ${data.merged.map((item) => item.branch).join(", ")}` : ""}`,
75390
75678
  ...data.steps.map((step) => [
75391
75679
  ` ${step.branch} on ${step.parent}:`,
@@ -75397,16 +75685,993 @@ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<b
75397
75685
  var init_stack2 = __esm(() => {
75398
75686
  init_ledger();
75399
75687
  init_stack();
75400
- init_vcs_config();
75401
75688
  init_output();
75402
75689
  init_forge_common();
75403
75690
  METHODS3 = new Set(["squash", "merge", "rebase"]);
75404
75691
  });
75405
75692
 
75693
+ // packages/workit-core/src/fanout.ts
75694
+ import { spawnSync as spawnSync21 } from "node:child_process";
75695
+ import fs23 from "node:fs";
75696
+ import path43 from "node:path";
75697
+ function scopeSamples(pattern) {
75698
+ if (!hasWildcard(pattern))
75699
+ return [unescape2(pattern)];
75700
+ let out = "";
75701
+ for (let index = 0;index < pattern.length; index += 1) {
75702
+ const char = pattern[index];
75703
+ if (char === "\\" && index + 1 < pattern.length) {
75704
+ out += pattern[index + 1];
75705
+ index += 1;
75706
+ } else if (char === "*" && pattern[index + 1] === "*") {
75707
+ const atSegmentStart = index === 0 || pattern[index - 1] === "/";
75708
+ if (atSegmentStart && pattern[index + 2] === "/")
75709
+ index += 2;
75710
+ else {
75711
+ out += "<any>";
75712
+ index += 1;
75713
+ }
75714
+ } else if (char === "*")
75715
+ out += "<any>";
75716
+ else if (char === "?")
75717
+ out += "x";
75718
+ else if (char === "{" && pattern.indexOf("}", index) > index) {
75719
+ const close = pattern.indexOf("}", index);
75720
+ out += pattern.slice(index + 1, close).split(",")[0];
75721
+ index = close;
75722
+ } else
75723
+ out += char;
75724
+ }
75725
+ return [out];
75726
+ }
75727
+ function globSource(pattern) {
75728
+ let out = "";
75729
+ for (let index = 0;index < pattern.length; index += 1) {
75730
+ const char = pattern[index];
75731
+ if (char === "\\" && index + 1 < pattern.length) {
75732
+ out += escapeRegExp4(pattern[index + 1]);
75733
+ index += 1;
75734
+ } else if (char === "*" && pattern[index + 1] === "*") {
75735
+ const atSegmentStart = index === 0 || pattern[index - 1] === "/";
75736
+ if (atSegmentStart && pattern[index + 2] === "/") {
75737
+ out += "(?:[^/]*/)*";
75738
+ index += 2;
75739
+ } else {
75740
+ out += ".*";
75741
+ index += 1;
75742
+ }
75743
+ } else if (char === "*")
75744
+ out += "[^/]*";
75745
+ else if (char === "?")
75746
+ out += "[^/]";
75747
+ else if (char === "{" && pattern.indexOf("}", index) > index) {
75748
+ const close = pattern.indexOf("}", index);
75749
+ const alternatives = pattern.slice(index + 1, close).split(",").map(globSource);
75750
+ out += `(?:${alternatives.join("|")})`;
75751
+ index = close;
75752
+ } else
75753
+ out += escapeRegExp4(char);
75754
+ }
75755
+ return out;
75756
+ }
75757
+ function matchesScope(pattern, file) {
75758
+ let regex = compiled.get(pattern);
75759
+ if (!regex) {
75760
+ regex = new RegExp(`^${globSource(pattern)}$`, "u");
75761
+ compiled.set(pattern, regex);
75762
+ }
75763
+ return regex.test(file) || !hasWildcard(pattern) && file.startsWith(`${unescape2(pattern)}/`);
75764
+ }
75765
+ function normalizePattern(raw) {
75766
+ let pattern = raw.trim().replace(/\\(?![[\]{}*?])/gu, "/");
75767
+ while (pattern.startsWith("./"))
75768
+ pattern = pattern.slice(2);
75769
+ if (pattern === "." || pattern === "")
75770
+ pattern = raw.trim() ? "**" : "";
75771
+ if (pattern.endsWith("/"))
75772
+ pattern = `${pattern}**`;
75773
+ if (!pattern)
75774
+ return { error: "is empty" };
75775
+ if (pattern.startsWith("/") || /^[A-Za-z]:/u.test(pattern))
75776
+ return { error: "must be relative to the repository root" };
75777
+ if (pattern.split("/").includes(".."))
75778
+ return { error: "must not contain .." };
75779
+ const bare = pattern.replace(/\\./gu, "");
75780
+ if (/[[\]]/u.test(bare))
75781
+ return {
75782
+ error: 'has an unescaped [ or ]: write \\[ and \\] for literal brackets (app/\\[id\\]/page.tsx; in JSON "app/\\\\[id\\\\]/page.tsx")'
75783
+ };
75784
+ if (bare.startsWith("!"))
75785
+ return { error: "uses ! (negation is not supported)" };
75786
+ let depth = 0;
75787
+ for (const char of bare) {
75788
+ if (char === "{")
75789
+ depth += 1;
75790
+ if (char === "}")
75791
+ depth -= 1;
75792
+ if (depth < 0 || depth > 1)
75793
+ return { error: "has unbalanced or nested braces" };
75794
+ }
75795
+ if (depth !== 0)
75796
+ return { error: "has unbalanced or nested braces" };
75797
+ return { pattern };
75798
+ }
75799
+ function inScope(plan, slice, file) {
75800
+ if (matchesAny(slice.owns, file))
75801
+ return true;
75802
+ if (!matchesAny(slice.scope, file))
75803
+ return false;
75804
+ return !plan.slices.some((other) => other.id !== slice.id && matchesAny(other.owns, file));
75805
+ }
75806
+ function parsePlan(raw, defaults, name) {
75807
+ const problems = [];
75808
+ if (!isRecord8(raw))
75809
+ return { ok: false, problems: ["the plan must be a JSON object"] };
75810
+ const planName = name ?? (typeof raw.name === "string" ? raw.name.trim() : "");
75811
+ if (!planName || planName.length > 200 || /\p{Cc}/u.test(planName))
75812
+ problems.push('name: pass --name or set a 1-200 character "name"');
75813
+ const trunk = typeof raw.trunk === "string" && raw.trunk.trim() ? raw.trunk.trim() : defaults.trunk;
75814
+ const shared = [];
75815
+ for (const entry of textList(raw.shared ?? []) ?? ["<not a list>"]) {
75816
+ const normalized = normalizePattern(entry);
75817
+ if ("error" in normalized)
75818
+ problems.push(`shared "${entry}" ${normalized.error}`);
75819
+ else
75820
+ shared.push(normalized.pattern);
75821
+ }
75822
+ if (!Array.isArray(raw.slices) || raw.slices.length === 0)
75823
+ return { ok: false, problems: [...problems, "slices: list at least one slice"] };
75824
+ const slices = [];
75825
+ const seenIds = new Set;
75826
+ const seenBranches = new Map;
75827
+ raw.slices.forEach((item, index) => {
75828
+ const where = isRecord8(item) && typeof item.id === "string" ? `slice ${item.id}` : `slice #${index + 1}`;
75829
+ if (!isRecord8(item)) {
75830
+ problems.push(`${where}: must be an object`);
75831
+ return;
75832
+ }
75833
+ const id = typeof item.id === "string" ? item.id : "";
75834
+ if (!ID.test(id))
75835
+ problems.push(`${where}: id must match ${ID.source}`);
75836
+ else if (seenIds.has(id))
75837
+ problems.push(`${where}: duplicate id`);
75838
+ seenIds.add(id);
75839
+ const branch = typeof item.branch === "string" ? item.branch.trim() : "";
75840
+ if (!branch)
75841
+ problems.push(`${where}: branch is empty`);
75842
+ else if (seenBranches.has(branch))
75843
+ problems.push(`${where}: branch ${branch} is also slice ${seenBranches.get(branch)}'s`);
75844
+ else
75845
+ seenBranches.set(branch, id);
75846
+ if (branch === trunk)
75847
+ problems.push(`${where}: branch must not be the trunk ${trunk}`);
75848
+ const goal = typeof item.goal === "string" ? item.goal.trim() : "";
75849
+ if (!filled(goal))
75850
+ problems.push(`${where}: goal is empty`);
75851
+ const brief = (field) => {
75852
+ const list = textList(item[field]);
75853
+ const kept = (list ?? []).map((entry) => entry.trim()).filter(filled);
75854
+ if (kept.length === 0)
75855
+ problems.push(`${where}: ${field} is empty`);
75856
+ return kept;
75857
+ };
75858
+ const acceptance = brief("acceptance");
75859
+ const verify = brief("verify");
75860
+ const forbidden = brief("forbidden");
75861
+ const scope = [];
75862
+ const owns = [];
75863
+ for (const [field, out] of [
75864
+ ["scope", scope],
75865
+ ["owns", owns]
75866
+ ]) {
75867
+ const list = textList(item[field] ?? []);
75868
+ if (list === null) {
75869
+ problems.push(`${where}: ${field} must be a list of globs`);
75870
+ continue;
75871
+ }
75872
+ for (const entry of list.filter(filled)) {
75873
+ const normalized = normalizePattern(entry);
75874
+ if ("error" in normalized)
75875
+ problems.push(`${where}: ${field} "${entry}" ${normalized.error}`);
75876
+ else
75877
+ out.push(normalized.pattern);
75878
+ }
75879
+ }
75880
+ if (scope.length === 0 && textList(item.scope ?? []) !== null)
75881
+ problems.push(`${where}: scope is empty`);
75882
+ const tier = item.tier;
75883
+ if (typeof tier !== "string" || !TIERS.includes(tier))
75884
+ problems.push(`${where}: tier must be one of ${TIERS.join(", ")}`);
75885
+ const dependsOn = textList(item.dependsOn ?? []);
75886
+ if (dependsOn === null)
75887
+ problems.push(`${where}: dependsOn must be a list of slice ids`);
75888
+ const optional = (field) => typeof item[field] === "string" && filled(item[field]) ? item[field].trim() : null;
75889
+ slices.push({
75890
+ id,
75891
+ branch,
75892
+ base: optional("base") ?? "",
75893
+ worktree: optional("worktree") ?? path43.join(defaults.worktreeRoot, id),
75894
+ tier,
75895
+ dependsOn: [...new Set(dependsOn ?? [])],
75896
+ scope,
75897
+ owns,
75898
+ goal,
75899
+ acceptance,
75900
+ verify,
75901
+ forbidden,
75902
+ context: optional("context"),
75903
+ timebox: optional("timebox")
75904
+ });
75905
+ });
75906
+ const byId = new Map(slices.map((slice) => [slice.id, slice]));
75907
+ const byBranch = new Map(slices.map((slice) => [slice.branch, slice]));
75908
+ for (const slice of slices) {
75909
+ for (const dep of slice.dependsOn) {
75910
+ if (dep === slice.id)
75911
+ problems.push(`slice ${slice.id}: depends on itself`);
75912
+ else if (!byId.has(dep))
75913
+ problems.push(`slice ${slice.id}: depends on unknown slice ${dep}`);
75914
+ }
75915
+ const explicitBase = slice.base;
75916
+ if (explicitBase) {
75917
+ const parent = byBranch.get(explicitBase);
75918
+ if (parent && !slice.dependsOn.includes(parent.id))
75919
+ problems.push(`slice ${slice.id}: base ${explicitBase} is slice ${parent.id}'s branch; add "${parent.id}" to its dependsOn`);
75920
+ } else {
75921
+ const single = slice.dependsOn.length === 1 ? byId.get(slice.dependsOn[0]) : undefined;
75922
+ slice.base = single ? single.branch : trunk;
75923
+ }
75924
+ }
75925
+ const cycle = findCycle(slices);
75926
+ if (cycle)
75927
+ problems.push(`dependency cycle: ${cycle.join(" -> ")}`);
75928
+ if (problems.length)
75929
+ return { ok: false, problems };
75930
+ return {
75931
+ ok: true,
75932
+ plan: {
75933
+ v: FANOUT_VERSION,
75934
+ name: planName,
75935
+ trunk,
75936
+ shared,
75937
+ slices,
75938
+ createdAt: "",
75939
+ updatedAt: ""
75940
+ }
75941
+ };
75942
+ }
75943
+ function findCycle(slices) {
75944
+ const byId = new Map(slices.map((slice) => [slice.id, slice]));
75945
+ const state = new Map;
75946
+ const trail = [];
75947
+ const visit = (id) => {
75948
+ if (state.get(id) === "done")
75949
+ return null;
75950
+ if (state.get(id) === "open")
75951
+ return [...trail.slice(trail.indexOf(id)), id];
75952
+ state.set(id, "open");
75953
+ trail.push(id);
75954
+ for (const dep of byId.get(id)?.dependsOn ?? []) {
75955
+ if (!byId.has(dep) || dep === id)
75956
+ continue;
75957
+ const found = visit(dep);
75958
+ if (found)
75959
+ return found;
75960
+ }
75961
+ trail.pop();
75962
+ state.set(id, "done");
75963
+ return null;
75964
+ };
75965
+ for (const slice of slices) {
75966
+ const found = visit(slice.id);
75967
+ if (found)
75968
+ return found;
75969
+ }
75970
+ return null;
75971
+ }
75972
+ function landingOrder(slices) {
75973
+ const order = [];
75974
+ const placed = new Set;
75975
+ const known = new Set(slices.map((slice) => slice.id));
75976
+ while (order.length < slices.length) {
75977
+ const next = slices.find((slice) => !placed.has(slice.id) && slice.dependsOn.every((dep) => placed.has(dep) || !known.has(dep)));
75978
+ if (!next)
75979
+ break;
75980
+ order.push(next.id);
75981
+ placed.add(next.id);
75982
+ }
75983
+ return order;
75984
+ }
75985
+ function waves(slices) {
75986
+ const level = new Map;
75987
+ const byId = new Map(slices.map((slice) => [slice.id, slice]));
75988
+ for (const id of landingOrder(slices)) {
75989
+ const deps = byId.get(id)?.dependsOn ?? [];
75990
+ level.set(id, deps.length ? Math.max(...deps.map((dep) => level.get(dep) ?? 0)) + 1 : 0);
75991
+ }
75992
+ const out = [];
75993
+ for (const slice of slices)
75994
+ (out[level.get(slice.id) ?? 0] ??= []).push(slice.id);
75995
+ return out.filter(Boolean);
75996
+ }
75997
+ function ancestors(slices) {
75998
+ const byId = new Map(slices.map((slice) => [slice.id, slice]));
75999
+ const out = new Map;
76000
+ for (const id of landingOrder(slices)) {
76001
+ const set = new Set;
76002
+ for (const dep of byId.get(id)?.dependsOn ?? []) {
76003
+ set.add(dep);
76004
+ for (const deeper of out.get(dep) ?? [])
76005
+ set.add(deeper);
76006
+ }
76007
+ out.set(id, set);
76008
+ }
76009
+ return out;
76010
+ }
76011
+ function analyzeOverlap(plan, candidates) {
76012
+ const shared = [...DEFAULT_SHARED, ...plan.shared];
76013
+ const before = ancestors(plan.slices);
76014
+ const related = (a, b) => Boolean(before.get(a)?.has(b) || before.get(b)?.has(a));
76015
+ const overlaps = [];
76016
+ const groups = new Map;
76017
+ const byCase = new Map;
76018
+ for (const file of [...new Set(candidates)].toSorted())
76019
+ (byCase.get(file.toLowerCase()) ?? byCase.set(file.toLowerCase(), []).get(file.toLowerCase()))?.push(file);
76020
+ for (const spellings of byCase.values()) {
76021
+ const file = spellings.join(" ~ ");
76022
+ const any = (patterns) => spellings.some((spelling) => matchesAny(patterns, spelling));
76023
+ const matching = plan.slices.filter((slice) => any(slice.scope) || any(slice.owns));
76024
+ if (matching.length < 2)
76025
+ continue;
76026
+ const ids = matching.map((slice) => slice.id);
76027
+ const owners = matching.filter((slice) => any(slice.owns));
76028
+ if (owners.length === 1) {
76029
+ overlaps.push({ path: file, slices: ids, resolution: "owner", owner: owners[0].id });
76030
+ continue;
76031
+ }
76032
+ if (owners.length === 0 && matching.every((a, i) => matching.slice(i + 1).every((b) => related(a.id, b.id)))) {
76033
+ overlaps.push({ path: file, slices: ids, resolution: "serialized" });
76034
+ continue;
76035
+ }
76036
+ const members = owners.length > 1 ? owners : matching;
76037
+ const reason = owners.length > 1 ? "several_owners" : "overlap";
76038
+ const isShared = any(shared);
76039
+ const key = `${reason}|${isShared}|${members.map((slice) => slice.id).join(",")}`;
76040
+ const group = groups.get(key);
76041
+ if (group)
76042
+ group.paths.push(file);
76043
+ else
76044
+ groups.set(key, {
76045
+ slices: members.map((slice) => slice.id),
76046
+ paths: [file],
76047
+ shared: isShared,
76048
+ reason,
76049
+ members,
76050
+ suggestion: ownerSuggestion(members[0].id, [])
76051
+ });
76052
+ }
76053
+ const conflicts = [...groups.values()].map(({ members, ...group }) => {
76054
+ const literal = members.find((slice) => slice.scope.some((pattern) => group.paths.includes(pattern)));
76055
+ const owner = ownerSuggestion((literal ?? members[0]).id, group.paths);
76056
+ if (group.reason === "several_owners")
76057
+ return {
76058
+ ...group,
76059
+ suggestion: {
76060
+ ...owner,
76061
+ text: `keep ${quoteList(group.paths)} in "owns" of slice ${owner.slice} only`
76062
+ }
76063
+ };
76064
+ const serialize = serializeSuggestion(group.slices);
76065
+ return group.shared ? { ...group, suggestion: owner, alternative: serialize } : { ...group, suggestion: serialize, alternative: owner };
76066
+ });
76067
+ return { overlaps, conflicts };
76068
+ }
76069
+ function branchRef(cwd, branch) {
76070
+ if (!branch || branch.startsWith("-"))
76071
+ return null;
76072
+ const local = resolveRef(cwd, `refs/heads/${branch}`);
76073
+ if (local)
76074
+ return local;
76075
+ const remote = pushRemoteName(cwd) ?? "origin";
76076
+ return resolveRef(cwd, `refs/remotes/${remote}/${branch}`);
76077
+ }
76078
+ function trunkRef(cwd, trunk) {
76079
+ if (!trunk || trunk.startsWith("-"))
76080
+ return null;
76081
+ const remote = pushRemoteName(cwd) ?? "origin";
76082
+ for (const ref of [`refs/remotes/${remote}/${trunk}`, `refs/heads/${trunk}`, trunk])
76083
+ if (resolveRef(cwd, ref))
76084
+ return ref;
76085
+ return null;
76086
+ }
76087
+ function defaultTrunk(cwd) {
76088
+ const base = defaultBase(cwd);
76089
+ return base ? base.replace(/^origin\//u, "") : "main";
76090
+ }
76091
+ function worktreeRoot(cwd) {
76092
+ const common = gitCommonDir(cwd);
76093
+ const main = common && path43.basename(common) === ".git" ? path43.dirname(common) : cwd;
76094
+ return path43.join(path43.dirname(main), `${path43.basename(main)}-wt`);
76095
+ }
76096
+ function fanoutsDir(cwd) {
76097
+ const root = storeRoot(cwd);
76098
+ if (!root.ok)
76099
+ return fanoutFail(root.code, root.error, root.unblock);
76100
+ return { ok: true, data: path43.join(root.value.root, "fanouts") };
76101
+ }
76102
+ function parseStored(raw) {
76103
+ if (!isRecord8(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
76104
+ return null;
76105
+ if (!Array.isArray(raw.slices))
76106
+ return null;
76107
+ const list = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
76108
+ const text = (value) => typeof value === "string" ? value : "";
76109
+ let globsValid = true;
76110
+ const globs = (value) => list(value).map((entry) => {
76111
+ const normalized = normalizePattern(entry);
76112
+ if ("error" in normalized) {
76113
+ globsValid = false;
76114
+ return entry;
76115
+ }
76116
+ return normalized.pattern;
76117
+ });
76118
+ const slices = [];
76119
+ for (const item of raw.slices) {
76120
+ if (!isRecord8(item) || typeof item.id !== "string" || typeof item.branch !== "string")
76121
+ return null;
76122
+ slices.push({
76123
+ id: item.id,
76124
+ branch: item.branch,
76125
+ base: text(item.base) || raw.trunk,
76126
+ worktree: text(item.worktree),
76127
+ tier: TIERS.includes(item.tier) ? item.tier : "standard",
76128
+ dependsOn: list(item.dependsOn),
76129
+ scope: globs(item.scope),
76130
+ owns: globs(item.owns),
76131
+ goal: text(item.goal),
76132
+ acceptance: list(item.acceptance),
76133
+ verify: list(item.verify),
76134
+ forbidden: list(item.forbidden),
76135
+ context: typeof item.context === "string" ? item.context : null,
76136
+ timebox: typeof item.timebox === "string" ? item.timebox : null
76137
+ });
76138
+ }
76139
+ const shared = globs(raw.shared);
76140
+ const ids = new Set(slices.map((slice) => slice.id));
76141
+ if (!globsValid || ids.size !== slices.length || slices.some((slice) => slice.dependsOn.some((dep) => dep === slice.id || !ids.has(dep))) || findCycle(slices))
76142
+ return null;
76143
+ return {
76144
+ v: typeof raw.v === "number" ? raw.v : FANOUT_VERSION,
76145
+ name: raw.name,
76146
+ trunk: raw.trunk,
76147
+ shared,
76148
+ slices,
76149
+ createdAt: text(raw.createdAt),
76150
+ updatedAt: text(raw.updatedAt)
76151
+ };
76152
+ }
76153
+ function readFanout(cwd, name) {
76154
+ const dir = fanoutsDir(cwd);
76155
+ if (!dir.ok)
76156
+ return dir;
76157
+ const file = path43.join(dir.data, stackFileName(name));
76158
+ let parsed = null;
76159
+ try {
76160
+ parsed = parseStored(JSON.parse(fs23.readFileSync(file, "utf8")));
76161
+ } catch (error) {
76162
+ if (error.code === "ENOENT")
76163
+ return { ok: true, data: null };
76164
+ parsed = null;
76165
+ }
76166
+ return parsed ? { ok: true, data: parsed } : fanoutFail("failed", `fanout file ${file} is not a valid plan`, `workit fanout plan <plan.json> --name ${name} # rewrite it`);
76167
+ }
76168
+ function listFanouts(cwd) {
76169
+ const dir = fanoutsDir(cwd);
76170
+ if (!dir.ok)
76171
+ return dir;
76172
+ let names = [];
76173
+ try {
76174
+ names = fs23.readdirSync(dir.data).filter((entry) => entry.endsWith(".json"));
76175
+ } catch {
76176
+ return { ok: true, data: [] };
76177
+ }
76178
+ const out = [];
76179
+ for (const entry of names) {
76180
+ let parsed = null;
76181
+ try {
76182
+ parsed = parseStored(JSON.parse(fs23.readFileSync(path43.join(dir.data, entry), "utf8")));
76183
+ } catch {
76184
+ parsed = null;
76185
+ }
76186
+ if (parsed && stackFileName(parsed.name) === entry)
76187
+ out.push(parsed);
76188
+ }
76189
+ return { ok: true, data: out.toSorted((a, b) => a.name.localeCompare(b.name)) };
76190
+ }
76191
+ function selectFanout(cwd, name, current) {
76192
+ if (name) {
76193
+ const read = readFanout(cwd, name);
76194
+ if (!read.ok)
76195
+ return read;
76196
+ return read.data ? { ok: true, data: read.data } : fanoutFail("not_found", `no fanout plan named ${name}`, "workit fanout plan <plan.json>");
76197
+ }
76198
+ const all = listFanouts(cwd);
76199
+ if (!all.ok)
76200
+ return all;
76201
+ const holding = all.data.filter((plan) => plan.slices.some((slice) => slice.branch === current));
76202
+ if (holding.length === 1)
76203
+ return { ok: true, data: holding[0] };
76204
+ if (all.data.length === 1)
76205
+ return { ok: true, data: all.data[0] };
76206
+ return all.data.length === 0 ? fanoutFail("not_found", "no fanout plan is recorded here", "workit fanout plan <plan.json>") : fanoutFail("invalid_input", `several fanout plans match; pick one (${all.data.map((plan) => plan.name).join(", ")})`, "pass --name <plan>");
76207
+ }
76208
+ function writeFanout(cwd, plan) {
76209
+ const dir = fanoutsDir(cwd);
76210
+ if (!dir.ok)
76211
+ return dir;
76212
+ const file = path43.join(dir.data, stackFileName(plan.name));
76213
+ const temp = `${file}.${process.pid}.tmp`;
76214
+ try {
76215
+ fs23.mkdirSync(dir.data, { recursive: true });
76216
+ fs23.writeFileSync(temp, `${JSON.stringify(plan, null, 2)}
76217
+ `);
76218
+ fs23.renameSync(temp, file);
76219
+ return { ok: true, data: file };
76220
+ } catch (error) {
76221
+ fs23.rmSync(temp, { force: true });
76222
+ return fanoutFail("unavailable", `cannot write ${file}: ${error.message}`);
76223
+ }
76224
+ }
76225
+ function planFanout(cwd, input, now = new Date) {
76226
+ const dir = fanoutsDir(cwd);
76227
+ if (!dir.ok)
76228
+ return dir;
76229
+ const parsed = parsePlan(input.trunk && isRecord8(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, { trunk: input.trunk ?? defaultTrunk(cwd), worktreeRoot: worktreeRoot(cwd) }, input.name);
76230
+ if (!parsed.ok)
76231
+ return fanoutFail("invalid_input", `the plan is incomplete: ${parsed.problems[0]}${parsed.problems.length > 1 ? ` (+${parsed.problems.length - 1} more)` : ""}`, "fill every listed field, then run workit fanout plan again", { problems: parsed.problems });
76232
+ const plan = parsed.plan;
76233
+ 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`);
76234
+ if (badBranches.length)
76235
+ return fanoutFail("invalid_input", badBranches[0], "rename the branch in the plan", {
76236
+ problems: badBranches
76237
+ });
76238
+ const ref = trunkRef(cwd, plan.trunk);
76239
+ const tracked = ref ? nulList2(gitRun4(cwd, ["ls-tree", "-r", "-z", "--name-only", ref]).stdout) : [];
76240
+ const samples = plan.slices.flatMap((slice) => [...slice.scope, ...slice.owns].flatMap(scopeSamples));
76241
+ const { overlaps, conflicts } = analyzeOverlap(plan, [...tracked, ...samples]);
76242
+ if (conflicts.length)
76243
+ return fanoutFail("blocked", `${conflicts.length} file-scope overlap${conflicts.length === 1 ? "" : "s"} between slices: ${conflicts[0].slices.join(" and ")} on ${conflicts[0].paths[0]}${conflicts[0].paths.length > 1 ? ` (+${conflicts[0].paths.length - 1})` : ""}`, conflicts[0].suggestion.text, { name: plan.name, conflicts, overlaps });
76244
+ const existing = readFanout(cwd, plan.name);
76245
+ const previous = existing.ok ? existing.data : null;
76246
+ plan.createdAt = previous?.createdAt || now.toISOString();
76247
+ plan.updatedAt = now.toISOString();
76248
+ const written = writeFanout(cwd, plan);
76249
+ if (!written.ok)
76250
+ return written;
76251
+ appendObserved(cwd, {
76252
+ type: "fanout.planned",
76253
+ actor: input.actor,
76254
+ fanout: plan.name,
76255
+ trunk: plan.trunk,
76256
+ slices: plan.slices.length,
76257
+ ids: plan.slices.slice(0, 20).map((slice) => slice.id)
76258
+ });
76259
+ const grouped = waves(plan.slices);
76260
+ const notes = [];
76261
+ if (!ref)
76262
+ notes.push(`trunk ${plan.trunk} does not resolve here; only sample paths of each glob were compared`);
76263
+ grouped.forEach((wave, index) => {
76264
+ if (wave.length > IN_FLIGHT_CAP)
76265
+ notes.push(`wave ${index + 1} has ${wave.length} slices; keep 4-${IN_FLIGHT_CAP} in flight and queue the rest`);
76266
+ });
76267
+ for (const slice of plan.slices)
76268
+ if (slice.dependsOn.length > 1 && slice.base === plan.trunk)
76269
+ notes.push(`slice ${slice.id} depends on ${slice.dependsOn.join(", ")}: start it from ${plan.trunk} after they land`);
76270
+ return {
76271
+ ok: true,
76272
+ data: {
76273
+ name: plan.name,
76274
+ trunk: plan.trunk,
76275
+ file: written.data,
76276
+ created: previous === null,
76277
+ slices: plan.slices.map(({ id, branch, base, worktree, tier, dependsOn, scope, owns }) => ({
76278
+ id,
76279
+ branch,
76280
+ base,
76281
+ worktree,
76282
+ tier,
76283
+ dependsOn,
76284
+ scope,
76285
+ owns
76286
+ })),
76287
+ overlaps,
76288
+ waves: grouped,
76289
+ landingOrder: landingOrder(plan.slices),
76290
+ notes
76291
+ }
76292
+ };
76293
+ }
76294
+ var FANOUT_VERSION = 1, TIERS, fanoutFail = (code, error, unblock, data) => ({
76295
+ ok: false,
76296
+ code,
76297
+ error,
76298
+ ...unblock ? { unblock } : {},
76299
+ ...data ? { data } : {}
76300
+ }), DEFAULT_SHARED, escapeRegExp4 = (text) => text.replace(/[.+^$()|[\]\\{}*?]/gu, "\\$&"), unescape2 = (pattern) => pattern.replace(/\\(.)/gu, "$1"), hasWildcard = (pattern) => /[*?{]/u.test(pattern.replace(/\\./gu, "")), compiled, matchesAny = (patterns, file) => patterns.some((pattern) => matchesScope(pattern, file)), isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), PLACEHOLDER3, filled = (text) => text.trim() !== "" && !PLACEHOLDER3.test(text.trim()), textList = (value) => {
76301
+ if (typeof value === "string")
76302
+ return [value];
76303
+ if (Array.isArray(value) && value.every((item) => typeof item === "string"))
76304
+ return value;
76305
+ return null;
76306
+ }, ID, quoteList = (items) => items.map((item) => `"${item}"`).join(", "), ownerSuggestion = (slice, paths) => ({
76307
+ kind: "owner",
76308
+ slice,
76309
+ paths,
76310
+ text: `add ${quoteList(paths)} to "owns" of slice ${slice}`
76311
+ }), serializeSuggestion = (ids) => {
76312
+ const edges = ids.slice(1).map((slice, index) => ({ slice, dependsOn: ids[index] }));
76313
+ return {
76314
+ kind: "serialize",
76315
+ edges,
76316
+ text: edges.map((edge) => `add "${edge.dependsOn}" to "dependsOn" of slice ${edge.slice}`).join("; ")
76317
+ };
76318
+ }, gitRun4 = (cwd, args) => {
76319
+ const run = spawnSync21("git", args, {
76320
+ cwd,
76321
+ encoding: "utf8",
76322
+ timeout: GIT_TIMEOUTS.local,
76323
+ killSignal: "SIGKILL",
76324
+ windowsHide: true,
76325
+ maxBuffer: 64 * 1024 * 1024,
76326
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", LC_ALL: "C", LANGUAGE: "C" }
76327
+ });
76328
+ return {
76329
+ ok: run.status === 0,
76330
+ status: run.status,
76331
+ stdout: run.stdout ?? "",
76332
+ stderr: run.error ? String(run.error.message) : run.stderr ?? ""
76333
+ };
76334
+ }, nulList2 = (text) => text.split("\x00").filter(Boolean), IN_FLIGHT_CAP = 6;
76335
+ var init_fanout = __esm(() => {
76336
+ init_rev();
76337
+ init_ledger();
76338
+ init_stack();
76339
+ TIERS = ["mundane", "standard", "hard"];
76340
+ DEFAULT_SHARED = [
76341
+ "**/package.json",
76342
+ "**/{package-lock.json,npm-shrinkwrap.json,bun.lock,bun.lockb,pnpm-lock.yaml,pnpm-workspace.yaml,yarn.lock}",
76343
+ "**/{Cargo.lock,go.mod,go.sum,poetry.lock,uv.lock,Gemfile.lock,composer.lock}",
76344
+ "**/index.{ts,tsx,js,jsx,mjs,cjs}",
76345
+ "**/tsconfig.json",
76346
+ ".github/**",
76347
+ ".gitlab-ci.yml",
76348
+ "CHANGELOG.md"
76349
+ ];
76350
+ compiled = new Map;
76351
+ PLACEHOLDER3 = /^(?:<[^>]*>|tbd|todo|fixme|\?+|\.\.\.|…)$/iu;
76352
+ ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
76353
+ });
76354
+
76355
+ // packages/workit-core/src/fanout-check.ts
76356
+ function mergeConflicts(cwd, a, b) {
76357
+ const run = gitRun4(cwd, [
76358
+ "merge-tree",
76359
+ "--write-tree",
76360
+ "--name-only",
76361
+ "--no-messages",
76362
+ "-z",
76363
+ a,
76364
+ b
76365
+ ]);
76366
+ if (run.status === 0)
76367
+ return [];
76368
+ if (run.status === 1)
76369
+ return [...new Set(nulList2(run.stdout).slice(1))];
76370
+ const first = run.stderr.trim().split(`
76371
+ `)[0] || `git merge-tree exited ${run.status}`;
76372
+ return {
76373
+ error: /write-tree|usage: git merge-tree/u.test(run.stderr) ? `${first} (needs git >= 2.38 for merge-tree --write-tree)` : first
76374
+ };
76375
+ }
76376
+ function checkFanout(cwd, plan, options = {}) {
76377
+ const unknown = (options.only ?? []).filter((id) => !plan.slices.some((s) => s.id === id));
76378
+ if (unknown.length)
76379
+ return fanoutFail("invalid_input", `${unknown.join(", ")}: not a slice of fanout ${plan.name}`, `slices: ${plan.slices.map((slice) => slice.id).join(", ")}`);
76380
+ const trunk = options.trunkRef ?? trunkRef(cwd, plan.trunk);
76381
+ if (!SAFE(trunk) || !resolves(cwd, trunk))
76382
+ return fanoutFail("unavailable", `trunk ${options.trunkRef ?? plan.trunk} does not resolve here`, `git fetch, or pass --base <ref>`);
76383
+ const order = landingOrder(plan.slices);
76384
+ const byId = new Map(plan.slices.map((slice) => [slice.id, slice]));
76385
+ const tips = new Map(plan.slices.map((slice) => [slice.id, branchRef(cwd, slice.branch)]));
76386
+ const ledger = readLedger(cwd);
76387
+ const rows = ledger.ok ? ledger.value.rows : [];
76388
+ const checks = new Map;
76389
+ const changedBy = new Map;
76390
+ for (const slice of plan.slices)
76391
+ checks.set(slice.id, inspect(cwd, plan, slice, trunk, tips, rows, changedBy));
76392
+ const conflicts = [];
76393
+ for (let i = 0;i < order.length; i += 1)
76394
+ for (let j = i + 1;j < order.length; j += 1) {
76395
+ const first = order[i];
76396
+ const second = order[j];
76397
+ const a = tips.get(first);
76398
+ const b = tips.get(second);
76399
+ if (!a || !b || isAncestor2(cwd, a, b) || isAncestor2(cwd, b, a))
76400
+ continue;
76401
+ const check = checks.get(second);
76402
+ const theirs = new Map((changedBy.get(first) ?? []).map((file) => [file.toLowerCase(), file]));
76403
+ const cased = (changedBy.get(second) ?? []).flatMap((file) => {
76404
+ const other = theirs.get(file.toLowerCase());
76405
+ return other !== undefined && other !== file ? [`${other} ~ ${file}`] : [];
76406
+ });
76407
+ if (cased.length) {
76408
+ conflicts.push({
76409
+ first,
76410
+ second,
76411
+ paths: cased,
76412
+ caseOnly: true,
76413
+ resolution: `use ${first}'s spelling on ${second} (git mv), then workit fanout check`
76414
+ });
76415
+ check.siblingConflicts.push({ with: first, paths: cased });
76416
+ check.reasons.push(`differs only in case from ${first}: ${cased.join(", ")}`);
76417
+ }
76418
+ const result = mergeConflicts(cwd, a, b);
76419
+ if (!Array.isArray(result)) {
76420
+ check.reasons.push(`cannot merge with ${first}: ${result.error}`);
76421
+ continue;
76422
+ }
76423
+ if (result.length === 0)
76424
+ continue;
76425
+ const related = byId.get(second)?.dependsOn.includes(first);
76426
+ conflicts.push({
76427
+ first,
76428
+ second,
76429
+ paths: result,
76430
+ resolution: related ? `rebase ${second} onto ${byId.get(first)?.branch} (its dependency), then workit fanout check` : `land ${first} first, then rebase ${second} onto ${plan.trunk} and run workit fanout check; or make ${second} depend on ${first}`
76431
+ });
76432
+ check.siblingConflicts.push({ with: first, paths: result });
76433
+ check.reasons.push(`conflicts with ${first} on ${result.join(", ")}`);
76434
+ }
76435
+ for (const id of order) {
76436
+ const check = checks.get(id);
76437
+ check.waitsFor = (byId.get(id)?.dependsOn ?? []).filter((dep) => checks.get(dep)?.status !== "ready");
76438
+ if (check.waitsFor.length)
76439
+ check.reasons.push(`waits for ${check.waitsFor.join(", ")}`);
76440
+ check.status = check.reasons.length ? "blocked" : "ready";
76441
+ }
76442
+ const selected = options.only?.length ? new Set(options.only) : null;
76443
+ const slices = order.filter((id) => !selected || selected.has(id)).map((id) => checks.get(id));
76444
+ const ready = slices.every((check) => check.status === "ready");
76445
+ const blocked = slices.find((check) => check.status === "blocked");
76446
+ const notes = plan.v === FANOUT_VERSION ? [] : [
76447
+ `plan file version ${plan.v} is not ${FANOUT_VERSION}; read leniently (re-plan to rewrite it)`
76448
+ ];
76449
+ const next = blocked ? unblockFor(blocked, plan) : `land in this order: ${slices.map((check) => check.id).join(", ")} (workit pr merge each once verified; stacked slices with workit stack land)`;
76450
+ return {
76451
+ ok: true,
76452
+ data: {
76453
+ name: plan.name,
76454
+ trunk: plan.trunk,
76455
+ trunkRef: trunk,
76456
+ ready,
76457
+ slices,
76458
+ conflicts: conflicts.filter((conflict) => !selected || selected.has(conflict.first) || selected.has(conflict.second)),
76459
+ landingOrder: order,
76460
+ notes,
76461
+ next
76462
+ }
76463
+ };
76464
+ }
76465
+ function inspect(cwd, plan, slice, trunk, tips, rows, changedBy) {
76466
+ const head = tips.get(slice.id) ?? null;
76467
+ const check = {
76468
+ id: slice.id,
76469
+ branch: slice.branch,
76470
+ head,
76471
+ base: slice.base,
76472
+ status: "blocked",
76473
+ changed: 0,
76474
+ outOfScope: [],
76475
+ trunkConflicts: [],
76476
+ siblingConflicts: [],
76477
+ waitsFor: [],
76478
+ verdict: { accepted: false, basis: "missing" },
76479
+ reasons: []
76480
+ };
76481
+ if (!head) {
76482
+ check.reasons.push(`branch ${slice.branch} not found (not started, landed, or not fetched)`);
76483
+ return check;
76484
+ }
76485
+ const parent = plan.slices.find((other) => other.branch === slice.base);
76486
+ const baseRef = slice.base === plan.trunk ? trunk : parent ? tips.get(parent.id) ?? null : branchRef(cwd, slice.base);
76487
+ if (!SAFE(baseRef)) {
76488
+ check.reasons.push(`base ${slice.base} not found`);
76489
+ return check;
76490
+ }
76491
+ const diff = gitRun4(cwd, ["diff", "--name-only", "--no-renames", "-z", `${baseRef}...${head}`]);
76492
+ if (!diff.ok) {
76493
+ check.reasons.push(`cannot diff ${slice.base}...${slice.branch}: ${diff.stderr.trim()}`);
76494
+ return check;
76495
+ }
76496
+ const changed = nulList2(diff.stdout);
76497
+ changedBy.set(slice.id, changed);
76498
+ check.changed = changed.length;
76499
+ if (changed.length === 0)
76500
+ check.reasons.push(`no changes beyond ${slice.base}`);
76501
+ check.outOfScope = changed.filter((file) => !inScope(plan, slice, file)).toSorted();
76502
+ if (check.outOfScope.length)
76503
+ check.reasons.push(`edits outside its scope: ${check.outOfScope.join(", ")}`);
76504
+ const trunkResult = mergeConflicts(cwd, trunk, head);
76505
+ if (Array.isArray(trunkResult)) {
76506
+ check.trunkConflicts = trunkResult;
76507
+ if (trunkResult.length)
76508
+ check.reasons.push(`conflicts with ${plan.trunk} on ${trunkResult.join(", ")}`);
76509
+ } else
76510
+ check.reasons.push(`cannot merge with ${plan.trunk}: ${trunkResult.error}`);
76511
+ const verdict = checkVerdicts(cwd, slice.branch, rows);
76512
+ check.verdict = { accepted: verdict.accepted.accepted, basis: verdict.current.basis };
76513
+ return check;
76514
+ }
76515
+ function unblockFor(check, plan) {
76516
+ if (!check.head)
76517
+ return `start or fetch ${check.branch}, then workit fanout check; if it landed, re-plan without slice ${check.id}`;
76518
+ if (check.outOfScope.length)
76519
+ return `move ${check.outOfScope.join(", ")} off ${check.branch} (a follow-up slice), or widen slice ${check.id}'s scope with workit fanout plan, then workit fanout check`;
76520
+ if (check.trunkConflicts.length)
76521
+ return `rebase ${check.branch} onto ${plan.trunk} and resolve ${check.trunkConflicts.join(", ")}, then workit fanout check`;
76522
+ if (check.siblingConflicts.length) {
76523
+ const first = check.siblingConflicts[0].with;
76524
+ return `land ${first} first, then rebase ${check.branch} onto ${plan.trunk} and run workit fanout check`;
76525
+ }
76526
+ if (check.waitsFor.length)
76527
+ return `unblock ${check.waitsFor.join(", ")} first`;
76528
+ return check.reasons[0] ?? "workit fanout check";
76529
+ }
76530
+ var SAFE = (value) => Boolean(value) && !value?.startsWith("-"), isAncestor2 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, resolves = (cwd, ref) => gitRun4(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]).ok;
76531
+ var init_fanout_check = __esm(() => {
76532
+ init_ledger();
76533
+ init_fanout();
76534
+ });
76535
+
76536
+ // packages/workit-cli/src/verbs/fanout.ts
76537
+ var exports_fanout = {};
76538
+ __export(exports_fanout, {
76539
+ run: () => run15
76540
+ });
76541
+ import fs24 from "node:fs";
76542
+ import path44 from "node:path";
76543
+ function renderBlockedPlan(io, result) {
76544
+ if (io.json || result.code !== "blocked" && result.code !== "invalid_input")
76545
+ return;
76546
+ const problems = result.data?.problems;
76547
+ if (Array.isArray(problems))
76548
+ for (const problem of problems)
76549
+ io.stderr(` - ${problem}
76550
+ `);
76551
+ const conflicts = result.data?.conflicts;
76552
+ if (Array.isArray(conflicts))
76553
+ for (const conflict of conflicts)
76554
+ io.stderr(` - ${conflict.slices.join(" + ")}: ${conflict.paths.join(", ")}
76555
+ fix: ${conflict.suggestion.text}${conflict.alternative ? `
76556
+ or: ${conflict.alternative.text}` : ""}
76557
+ `);
76558
+ }
76559
+ function resolveTrunk(io, raw, flag, track) {
76560
+ if (flag !== null)
76561
+ return { trunk: flag };
76562
+ const named = typeof raw === "object" && raw !== null && typeof raw.trunk === "string" ? raw.trunk.trim() || null : null;
76563
+ if (named !== null && track === null)
76564
+ return { trunk: null };
76565
+ const derived = releaseTrunk(io, track, null);
76566
+ if ("error" in derived)
76567
+ return {
76568
+ code: emit(io, fail("blocked", derived.error, {
76569
+ unblock: "workit fanout plan <plan.json> --track <name> # or --trunk <branch>"
76570
+ }))
76571
+ };
76572
+ if (named !== null && derived.trunk !== named)
76573
+ return {
76574
+ code: usage3(io, `the plan names trunk ${named}; --track ${track} targets ${derived.trunk ?? "(none)"}`, PLAN_USAGE2)
76575
+ };
76576
+ return { trunk: derived.tracked || track !== null ? derived.trunk : null };
76577
+ }
76578
+ function plan2(argv, io) {
76579
+ const flags = parseFlags(argv, { name: "value", trunk: "value", track: "value" });
76580
+ if (typeof flags === "string")
76581
+ return usage3(io, flags, PLAN_USAGE2);
76582
+ const track = flags.values.track ?? null;
76583
+ if (track !== null && flags.values.trunk !== undefined)
76584
+ return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE2);
76585
+ if (flags.positionals.length !== 1)
76586
+ return usage3(io, flags.positionals.length ? `unexpected argument ${flags.positionals[1]}` : "missing <plan.json>", PLAN_USAGE2);
76587
+ const file = path44.resolve(io.cwd, flags.positionals[0]);
76588
+ let raw;
76589
+ try {
76590
+ raw = JSON.parse(fs24.readFileSync(file, "utf8"));
76591
+ } catch (error) {
76592
+ return usage3(io, `cannot read ${file} as JSON: ${error.message}`, PLAN_USAGE2);
76593
+ }
76594
+ const trunk = resolveTrunk(io, raw, flags.values.trunk ?? null, track);
76595
+ if ("code" in trunk)
76596
+ return trunk.code;
76597
+ const result = planFanout(io.cwd, {
76598
+ raw,
76599
+ name: flags.values.name ?? null,
76600
+ trunk: trunk.trunk,
76601
+ actor: actorFromEnv(io.env)
76602
+ });
76603
+ if (!result.ok) {
76604
+ const code = failed(io, result);
76605
+ renderBlockedPlan(io, result);
76606
+ return code;
76607
+ }
76608
+ return emit(io, ok(result.data), (data) => [
76609
+ `${data.created ? "planned" : "re-planned"} fanout ${data.name} on ${data.trunk}: ${data.slices.length} slice${data.slices.length === 1 ? "" : "s"}`,
76610
+ ...data.slices.map((slice) => ` ${slice.id} [${slice.tier}] ${slice.branch} <- ${slice.base}${slice.dependsOn.length ? ` after ${slice.dependsOn.join(", ")}` : ""}
76611
+ worktree ${slice.worktree}`),
76612
+ ...data.overlaps.map((overlap) => ` shared ${overlap.path}: ${overlap.resolution === "owner" ? `owned by ${overlap.owner}` : `serialized (${overlap.slices.join(" -> ")})`}`),
76613
+ `waves: ${data.waves.map((wave) => wave.join(", ")).join(" | ")}`,
76614
+ `landing order: ${data.landingOrder.join(", ")}`,
76615
+ ...data.notes.map((note) => `note: ${note}`)
76616
+ ]);
76617
+ }
76618
+ function check2(argv, io) {
76619
+ const flags = parseFlags(argv, { name: "value", base: "value" });
76620
+ if (typeof flags === "string")
76621
+ return usage3(io, flags, CHECK_USAGE);
76622
+ const selected = selectFanout(io.cwd, flags.values.name ?? null, currentBranch(io.cwd));
76623
+ if (!selected.ok)
76624
+ return failed(io, selected);
76625
+ const result = checkFanout(io.cwd, selected.data, {
76626
+ only: flags.positionals,
76627
+ trunkRef: flags.values.base ?? null
76628
+ });
76629
+ if (!result.ok)
76630
+ return failed(io, result);
76631
+ if (result.data.ready)
76632
+ return emit(io, ok(result.data), renderCheck);
76633
+ if (!io.json)
76634
+ for (const line of renderCheck(result.data))
76635
+ io.stdout(`${line}
76636
+ `);
76637
+ const first = result.data.slices.find((slice) => slice.status === "blocked");
76638
+ return emit(io, fail("blocked", `slice ${first?.id ?? "?"} is not ready: ${first?.reasons[0] ?? "blocked"}`, {
76639
+ data: result.data,
76640
+ unblock: result.data.next
76641
+ }));
76642
+ }
76643
+ async function run15(argv, io) {
76644
+ const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
76645
+ if (sub === "plan")
76646
+ return plan2(rest, io);
76647
+ if (sub === "check")
76648
+ return check2(rest, io);
76649
+ return usage3(io, sub && !sub.startsWith("-") ? `unknown fanout subcommand "${sub}"` : "missing fanout subcommand", USAGE8);
76650
+ }
76651
+ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--json]", CHECK_USAGE = "workit fanout check [<slice>…] [--name <n>] [--base <ref>] [--json]", USAGE8 = "workit fanout plan|check ... (workit help fanout)", failed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), renderCheck = (data) => [
76652
+ `fanout ${data.name} against ${data.trunkRef}: ${data.ready ? "ready to land" : "blocked"}`,
76653
+ ...data.slices.map((slice) => ` ${slice.status === "ready" ? "ok " : "x "}${slice.id} (${slice.branch}) ${slice.changed} file${slice.changed === 1 ? "" : "s"} · verdict ${slice.verdict.accepted ? "accepted" : slice.verdict.basis}${slice.reasons.length ? `
76654
+ ${slice.reasons.join(`
76655
+ `)}` : ""}`),
76656
+ ...data.conflicts.map((conflict) => ` conflict ${conflict.first} x ${conflict.second}: ${conflict.paths.join(", ")}
76657
+ fix: ${conflict.resolution}`),
76658
+ `landing order: ${data.landingOrder.join(", ")}`,
76659
+ `next: ${data.next}`,
76660
+ ...data.notes.map((note) => `note: ${note}`)
76661
+ ];
76662
+ var init_fanout2 = __esm(() => {
76663
+ init_fanout_check();
76664
+ init_fanout();
76665
+ init_rev();
76666
+ init_ledger();
76667
+ init_output();
76668
+ init_forge_common();
76669
+ });
76670
+
75406
76671
  // packages/workit-cli/src/verbs/ledger.ts
75407
76672
  var exports_ledger = {};
75408
76673
  __export(exports_ledger, {
75409
- run: () => run15
76674
+ run: () => run16
75410
76675
  });
75411
76676
  import { randomBytes as randomBytes4 } from "node:crypto";
75412
76677
  import { parseArgs } from "node:util";
@@ -75430,7 +76695,7 @@ function targetBranch(io, rows, branch, pr) {
75430
76695
  }
75431
76696
  return { ok: true, value: branch ?? null };
75432
76697
  }
75433
- async function run15(argv, io) {
76698
+ async function run16(argv, io) {
75434
76699
  let parsed;
75435
76700
  try {
75436
76701
  parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
@@ -75458,7 +76723,7 @@ async function run15(argv, io) {
75458
76723
  return usage4(io, `unknown ledger subcommand "${sub}"`);
75459
76724
  const ledger = readLedger(io.cwd);
75460
76725
  if (!ledger.ok)
75461
- return failed(io, ledger);
76726
+ return failed2(io, ledger);
75462
76727
  const { rows } = ledger.value;
75463
76728
  if (sub === "list" || sub === "show") {
75464
76729
  if (rest.length)
@@ -75480,7 +76745,7 @@ async function run15(argv, io) {
75480
76745
  return usage4(io, `unexpected argument: ${rest.at(-1)}`);
75481
76746
  const target = targetBranch(io, rows, rest[0] ?? values.branch, pr);
75482
76747
  if (!target.ok)
75483
- return failed(io, target);
76748
+ return failed2(io, target);
75484
76749
  const branch = target.value ?? currentBranch(io.cwd);
75485
76750
  if (!branch)
75486
76751
  return emit(io, fail("invalid_input", "HEAD is detached; name the branch", {
@@ -75490,7 +76755,7 @@ async function run15(argv, io) {
75490
76755
  }
75491
76756
  const target = targetBranch(io, rows, values.branch, pr);
75492
76757
  if (!target.ok)
75493
- return failed(io, target);
76758
+ return failed2(io, target);
75494
76759
  const acting = actorFor(io, values);
75495
76760
  if (acting instanceof Error)
75496
76761
  return usage4(io, acting.message);
@@ -75529,7 +76794,7 @@ async function run15(argv, io) {
75529
76794
  ..."derivedFrom" in acting ? { derivedFrom: acting.derivedFrom } : {}
75530
76795
  })), (row) => `recorded verdict ${row.id}: ${row.result} [${row.kind}] for ${row.branch} @ ${(row.head ?? "").slice(0, 12)} as ${actor.session ?? "no session"}${row.self ? ` (self${row.selfReason === "no_session" ? ": WORKIT_SESSION_ID unset" : ""}; never accepted)` : ""}`);
75531
76796
  }
75532
- var USAGE8 = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS, SESSION_SAFE2, ROLE_SAFE, actorFor = (io, values) => {
76797
+ var USAGE9 = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS, SESSION_SAFE2, ROLE_SAFE, actorFor = (io, values) => {
75533
76798
  const actor = actorFromEnv(io.env);
75534
76799
  if (values.session !== undefined && values.as !== undefined)
75535
76800
  return new Error("pass --session or --as, not both");
@@ -75553,7 +76818,7 @@ var USAGE8 = "workit ledger decision|ruling|verdict|list|check ... (workit help
75553
76818
  return;
75554
76819
  const parsed = Number(value);
75555
76820
  return Number.isInteger(parsed) && parsed > 0 ? parsed : new Error(`${flag} must be a positive integer`);
75556
- }, failed = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE8 })), verdictLines = (check) => {
76821
+ }, failed2 = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed2(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE9 })), verdictLines = (check) => {
75557
76822
  const lines = [
75558
76823
  `${check.branch} @ ${(check.head ?? "no commit").slice(0, 12)}: current: ${check.current.basis}${typeof check.current.verdict?.result === "string" ? ` (${check.current.verdict.result})` : ""}; ${check.accepted.accepted ? "accepted" : `not accepted (${check.accepted.reasons.join(", ")})`}; review: ${check.review}`
75559
76824
  ];
@@ -90550,7 +91815,7 @@ var init_ast = __esm(() => {
90550
91815
 
90551
91816
  // packages/workit-cli/src/test-audit/rules.ts
90552
91817
  import { builtinModules } from "node:module";
90553
- import path42 from "node:path";
91818
+ import path45 from "node:path";
90554
91819
  function testCallee(call) {
90555
91820
  let callee = call.callee;
90556
91821
  if (callee.type === "CallExpression")
@@ -91043,12 +92308,12 @@ function checkConstantsOnly(ctx, emit) {
91043
92308
  }
91044
92309
  function checkOverMocking(ctx, emit) {
91045
92310
  const { program } = ctx.parsed;
91046
- const unit = path42.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
92311
+ const unit = path45.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
91047
92312
  const mocked = find2(program, (node) => node.type === "CallExpression" && MODULE_MOCK.test(calleeName(node.callee) ?? "") && node.arguments[0]?.type === "StringLiteral");
91048
92313
  const internal = mocked.filter((call) => /^(?:\.|@\/|~\/)/.test(call.arguments[0].value));
91049
92314
  for (const call of mocked) {
91050
92315
  const spec = call.arguments[0].value;
91051
- const base = path42.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
92316
+ const base = path45.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
91052
92317
  if (base === unit)
91053
92318
  emit({
91054
92319
  rule: "over-mocking",
@@ -91436,11 +92701,11 @@ var init_rules = __esm(() => {
91436
92701
  });
91437
92702
 
91438
92703
  // packages/workit-cli/src/test-audit/audit.ts
91439
- import { spawnSync as spawnSync20 } from "node:child_process";
92704
+ import { spawnSync as spawnSync22 } from "node:child_process";
91440
92705
  import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync19, statSync as statSync7 } from "node:fs";
91441
- import path43 from "node:path";
92706
+ import path46 from "node:path";
91442
92707
  function git6(cwd, args) {
91443
- const run = spawnSync20("git", ["-c", "core.quotepath=off", ...args], {
92708
+ const run = spawnSync22("git", ["-c", "core.quotepath=off", ...args], {
91444
92709
  cwd,
91445
92710
  encoding: "utf8",
91446
92711
  maxBuffer: 64 * 1024 * 1024
@@ -91451,7 +92716,7 @@ function walkTests(dir, out) {
91451
92716
  for (const entry of readdirSync8(dir, { withFileTypes: true })) {
91452
92717
  if (entry.name.startsWith(".") && entry.name !== "." && entry.isDirectory())
91453
92718
  continue;
91454
- const full = path43.join(dir, entry.name);
92719
+ const full = path46.join(dir, entry.name);
91455
92720
  if (entry.isDirectory()) {
91456
92721
  if (!SKIP_DIRS.has(entry.name))
91457
92722
  walkTests(full, out);
@@ -91463,7 +92728,7 @@ function expandPaths(cwd, inputs) {
91463
92728
  const files = [];
91464
92729
  const missing = [];
91465
92730
  for (const input of inputs.length > 0 ? inputs : ["."]) {
91466
- const full = path43.resolve(cwd, input);
92731
+ const full = path46.resolve(cwd, input);
91467
92732
  if (!existsSync18(full))
91468
92733
  missing.push(input);
91469
92734
  else if (statSync7(full).isDirectory())
@@ -91500,11 +92765,11 @@ function changedLines(cwd, base) {
91500
92765
  for (let offset = 0;offset < count; offset += 1)
91501
92766
  lines.add(start + offset);
91502
92767
  }
91503
- changed.set(path43.join(root, file), lines);
92768
+ changed.set(path46.join(root, file), lines);
91504
92769
  }
91505
92770
  const untracked = git6(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
91506
92771
  for (const file of untracked.stdout.split("\x00").filter(Boolean))
91507
- changed.set(path43.join(root, file), "all");
92772
+ changed.set(path46.join(root, file), "all");
91508
92773
  return changed;
91509
92774
  }
91510
92775
  function auditFiles(files, display) {
@@ -91566,7 +92831,7 @@ function auditFiles(files, display) {
91566
92831
  durationMs: Math.round(performance.now() - started)
91567
92832
  };
91568
92833
  }
91569
- var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path43.sep).join("/"), SEVERITY_ORDER, ruleIds;
92834
+ var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path46.sep).join("/"), SEVERITY_ORDER, ruleIds;
91570
92835
  var init_audit = __esm(() => {
91571
92836
  init_ast();
91572
92837
  init_rules();
@@ -91596,7 +92861,7 @@ import {
91596
92861
  copyFileSync as copyFileSync6
91597
92862
  } from "node:fs";
91598
92863
  import os14 from "node:os";
91599
- import path44 from "node:path";
92864
+ import path47 from "node:path";
91600
92865
  function mutantsFor(source, file, lines) {
91601
92866
  const parsed = parseSource(source, file);
91602
92867
  const out = [];
@@ -91669,7 +92934,7 @@ function relatedTests(source, testFiles) {
91669
92934
  for (const match of text.matchAll(SPECIFIER)) {
91670
92935
  const spec = match[1];
91671
92936
  if (spec.startsWith(".")) {
91672
- if (stripExt(path44.posix.join(path44.posix.dirname(file), spec)) === target)
92937
+ if (stripExt(path47.posix.join(path47.posix.dirname(file), spec)) === target)
91673
92938
  return true;
91674
92939
  } else if (spec.includes("/") && stripExt(spec).split("/").slice(-2).join("/") === tail)
91675
92940
  return true;
@@ -91689,12 +92954,12 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
91689
92954
  for (const name of names) {
91690
92955
  if (!name.startsWith(TEMP_PREFIX))
91691
92956
  continue;
91692
- const dir = path44.join(tmp, name);
92957
+ const dir = path47.join(tmp, name);
91693
92958
  try {
91694
92959
  const stat = lstatSync7(dir);
91695
92960
  if (!stat.isDirectory() || uid !== undefined && stat.uid !== uid)
91696
92961
  continue;
91697
- const pid = Number(readFileSync20(path44.join(dir, PID_FILE), "utf8"));
92962
+ const pid = Number(readFileSync20(path47.join(dir, PID_FILE), "utf8"));
91698
92963
  if (Number.isInteger(pid) && pid > 0 && alive(pid))
91699
92964
  continue;
91700
92965
  } catch (error) {
@@ -91711,8 +92976,8 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
91711
92976
  function linkNodeModules(origin, target, repo, copy) {
91712
92977
  mkdirSync9(target, { recursive: true });
91713
92978
  for (const entry of readdirSync9(origin, { withFileTypes: true })) {
91714
- const from = path44.join(origin, entry.name);
91715
- const to = path44.join(target, entry.name);
92979
+ const from = path47.join(origin, entry.name);
92980
+ const to = path47.join(target, entry.name);
91716
92981
  if (entry.name.startsWith("@") && entry.isDirectory()) {
91717
92982
  linkNodeModules(from, to, repo, copy);
91718
92983
  continue;
@@ -91728,9 +92993,9 @@ function linkNodeModules(origin, target, repo, copy) {
91728
92993
  } catch {
91729
92994
  continue;
91730
92995
  }
91731
- const relative = path44.relative(repo, real);
91732
- if (!relative.startsWith("..") && !path44.isAbsolute(relative) && !relative.split(path44.sep).includes("node_modules")) {
91733
- symlinkSync(path44.join(copy, relative), to, "junction");
92996
+ const relative = path47.relative(repo, real);
92997
+ if (!relative.startsWith("..") && !path47.isAbsolute(relative) && !relative.split(path47.sep).includes("node_modules")) {
92998
+ symlinkSync(path47.join(copy, relative), to, "junction");
91734
92999
  continue;
91735
93000
  }
91736
93001
  }
@@ -91746,7 +93011,7 @@ function copySymlink(repo, tree, from, to) {
91746
93011
  }
91747
93012
  const realRepo = realpathSync9(repo);
91748
93013
  if (real && inside2(realRepo, real)) {
91749
- symlinkSync(path44.relative(path44.dirname(to), path44.join(tree, path44.relative(realRepo, real))), to);
93014
+ symlinkSync(path47.relative(path47.dirname(to), path47.join(tree, path47.relative(realRepo, real))), to);
91750
93015
  return;
91751
93016
  }
91752
93017
  if (real && statSync8(real).isFile())
@@ -91755,10 +93020,10 @@ function copySymlink(repo, tree, from, to) {
91755
93020
  symlinkSync(readlinkSync3(from), to);
91756
93021
  }
91757
93022
  function writeInside(copy, file, content) {
91758
- const target = path44.join(copy, file);
93023
+ const target = path47.join(copy, file);
91759
93024
  try {
91760
93025
  const stat = lstatSync7(target);
91761
- if (!stat.isFile() || !inside2(realpathSync9(copy), realpathSync9(path44.dirname(target))))
93026
+ if (!stat.isFile() || !inside2(realpathSync9(copy), realpathSync9(path47.dirname(target))))
91762
93027
  return false;
91763
93028
  } catch {
91764
93029
  return false;
@@ -91769,9 +93034,9 @@ function writeInside(copy, file, content) {
91769
93034
  return true;
91770
93035
  }
91771
93036
  function copyWorkingTree(repo) {
91772
- const dir = mkdtempSync4(path44.join(os14.tmpdir(), TEMP_PREFIX));
91773
- writeFileSync9(path44.join(dir, PID_FILE), String(process.pid));
91774
- const tree = path44.join(dir, "tree");
93037
+ const dir = mkdtempSync4(path47.join(os14.tmpdir(), TEMP_PREFIX));
93038
+ writeFileSync9(path47.join(dir, PID_FILE), String(process.pid));
93039
+ const tree = path47.join(dir, "tree");
91775
93040
  const head = git6(repo, ["rev-parse", "--verify", "--quiet", "HEAD"]).stdout.trim();
91776
93041
  if (head && git6(dir, ["clone", "-q", "--shared", "--no-checkout", repo, tree]).ok) {
91777
93042
  const branch = git6(repo, ["symbolic-ref", "--quiet", "--short", "HEAD"]).stdout.trim();
@@ -91788,29 +93053,29 @@ function copyWorkingTree(repo) {
91788
93053
  for (const relative of listed) {
91789
93054
  if (!relative)
91790
93055
  continue;
91791
- const from = path44.join(repo, relative);
93056
+ const from = path47.join(repo, relative);
91792
93057
  let stat;
91793
93058
  try {
91794
93059
  stat = lstatSync7(from);
91795
93060
  } catch {
91796
93061
  continue;
91797
93062
  }
91798
- const to = path44.join(tree, relative);
91799
- mkdirSync9(path44.dirname(to), { recursive: true });
93063
+ const to = path47.join(tree, relative);
93064
+ mkdirSync9(path47.dirname(to), { recursive: true });
91800
93065
  if (stat.isSymbolicLink())
91801
93066
  copySymlink(repo, tree, from, to);
91802
93067
  else if (stat.isFile())
91803
93068
  copyFileSync6(from, to);
91804
- let parent = path44.dirname(relative);
93069
+ let parent = path47.dirname(relative);
91805
93070
  while (parent !== "." && !dirs.has(parent)) {
91806
93071
  dirs.add(parent);
91807
- parent = path44.dirname(parent);
93072
+ parent = path47.dirname(parent);
91808
93073
  }
91809
93074
  }
91810
93075
  for (const parent of dirs) {
91811
- const modules = path44.join(repo, parent, "node_modules");
91812
- if (existsSync19(modules) && !existsSync19(path44.join(tree, parent, "node_modules")))
91813
- linkNodeModules(modules, path44.join(tree, parent, "node_modules"), repo, tree);
93076
+ const modules = path47.join(repo, parent, "node_modules");
93077
+ if (existsSync19(modules) && !existsSync19(path47.join(tree, parent, "node_modules")))
93078
+ linkNodeModules(modules, path47.join(tree, parent, "node_modules"), repo, tree);
91814
93079
  }
91815
93080
  return { dir, tree };
91816
93081
  }
@@ -91849,14 +93114,14 @@ async function runMutation(options) {
91849
93114
  const tracked = git6(repo, ["ls-files", "-co", "--exclude-standard", "-z"]).stdout.split("\x00").filter(Boolean);
91850
93115
  const testFiles = tracked.filter((file) => TEST_FILE.test(file)).map((file) => {
91851
93116
  try {
91852
- return { file, text: readFileSync20(path44.join(repo, file), "utf8") };
93117
+ return { file, text: readFileSync20(path47.join(repo, file), "utf8") };
91853
93118
  } catch {
91854
93119
  return { file, text: "" };
91855
93120
  }
91856
93121
  });
91857
93122
  const candidates = [];
91858
93123
  for (const [absolute, lines] of [...options.changed].toSorted(([a], [b]) => a.localeCompare(b))) {
91859
- const relative = toPosix(path44.relative(repo, absolute));
93124
+ const relative = toPosix(path47.relative(repo, absolute));
91860
93125
  if (!CODE_FILE.test(relative) || TEST_FILE.test(relative) || relative.endsWith(".d.ts"))
91861
93126
  continue;
91862
93127
  if (NOT_SOURCE.test(relative) || !existsSync19(absolute))
@@ -91870,9 +93135,9 @@ async function runMutation(options) {
91870
93135
  for (const mutant of mutants)
91871
93136
  if (!related.has(mutant.file))
91872
93137
  related.set(mutant.file, relatedTests(mutant.file, testFiles));
91873
- const cwdRelative = path44.relative(repo, options.cwd);
93138
+ const cwdRelative = path47.relative(repo, options.cwd);
91874
93139
  const asArg = (file) => {
91875
- const relative = toPosix(path44.relative(cwdRelative, file));
93140
+ const relative = toPosix(path47.relative(cwdRelative, file));
91876
93141
  return shellQuote(relative.startsWith(".") ? relative : `./${relative}`);
91877
93142
  };
91878
93143
  const commandFor = (tests) => options.command.replaceAll("{files}", () => tests.map(asArg).join(" "));
@@ -91907,7 +93172,7 @@ async function runMutation(options) {
91907
93172
  return finish2(options, 0, results, counts, started);
91908
93173
  removeStaleCopies();
91909
93174
  const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
91910
- const copyCwd = path44.join(copy, cwdRelative);
93175
+ const copyCwd = path47.join(copy, cwdRelative);
91911
93176
  let current = null;
91912
93177
  const restore = () => {
91913
93178
  if (current)
@@ -91942,7 +93207,7 @@ async function runMutation(options) {
91942
93207
  record({ ...mutant, status: "skipped", durationMs: 0, tests, reason: "budget" });
91943
93208
  continue;
91944
93209
  }
91945
- const original = readFileSync20(path44.join(copy, mutant.file), "utf8");
93210
+ const original = readFileSync20(path47.join(copy, mutant.file), "utf8");
91946
93211
  const mutated = original.slice(0, mutant.start) + mutant.replacement + original.slice(mutant.end);
91947
93212
  current = { file: mutant.file, original };
91948
93213
  if (!writeInside(copy, mutant.file, mutated)) {
@@ -91999,8 +93264,8 @@ var NOT_SOURCE, FLIPS, isStringy = (node) => node.type === "StringLiteral" || no
91999
93264
  return error.code === "EPERM";
92000
93265
  }
92001
93266
  }, inside2 = (root, file) => {
92002
- const relative = path44.relative(root, file);
92003
- return relative === "" || !relative.startsWith("..") && !path44.isAbsolute(relative);
93267
+ const relative = path47.relative(root, file);
93268
+ return relative === "" || !relative.startsWith("..") && !path47.isAbsolute(relative);
92004
93269
  }, active, killTree = (child) => {
92005
93270
  try {
92006
93271
  if (child.pid && process.platform !== "win32")
@@ -92046,10 +93311,10 @@ var init_mutate = __esm(() => {
92046
93311
  // packages/workit-cli/src/verbs/test-audit.ts
92047
93312
  var exports_test_audit = {};
92048
93313
  __export(exports_test_audit, {
92049
- run: () => run16
93314
+ run: () => run17
92050
93315
  });
92051
93316
  import { existsSync as existsSync20 } from "node:fs";
92052
- import path45 from "node:path";
93317
+ import path48 from "node:path";
92053
93318
  function parse7(argv) {
92054
93319
  const options = {
92055
93320
  paths: [],
@@ -92143,14 +93408,14 @@ function human(data) {
92143
93408
  }
92144
93409
  return lines;
92145
93410
  }
92146
- async function run16(argv, io) {
93411
+ async function run17(argv, io) {
92147
93412
  const options = parse7(argv);
92148
93413
  if (typeof options === "string")
92149
93414
  return emit(io, fail("invalid_input", `${options}
92150
- usage: ${USAGE9}`));
93415
+ usage: ${USAGE10}`));
92151
93416
  const top = git6(io.cwd, ["rev-parse", "--show-toplevel"]);
92152
- const repo = top.ok ? path45.resolve(top.stdout.trim()) : null;
92153
- const display = (file) => toPosix(path45.relative(io.cwd, file) || file);
93417
+ const repo = top.ok ? path48.resolve(top.stdout.trim()) : null;
93418
+ const display = (file) => toPosix(path48.relative(io.cwd, file) || file);
92154
93419
  let base = null;
92155
93420
  let changed = null;
92156
93421
  if (options.diff || options.mutate) {
@@ -92233,7 +93498,7 @@ usage: ${USAGE9}`));
92233
93498
  }
92234
93499
  return emit(io, ok(data), human);
92235
93500
  }
92236
- var USAGE9 = "workit test-audit [paths…] [--diff [base]] [--rule <ids>] [--min-severity <level>] [--fail-on <level>] [--mutate [--test-cmd <cmd>] [--max-mutants <n>] [--budget <s>] [--timeout <s>]] [--json]", LEVELS, VALUE_FLAGS2, quote = (arg) => /^[\w./@:=+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
93501
+ var USAGE10 = "workit test-audit [paths…] [--diff [base]] [--rule <ids>] [--min-severity <level>] [--fail-on <level>] [--mutate [--test-cmd <cmd>] [--max-mutants <n>] [--budget <s>] [--timeout <s>]] [--json]", LEVELS, VALUE_FLAGS2, quote = (arg) => /^[\w./@:=+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
92237
93502
  var init_test_audit = __esm(() => {
92238
93503
  init_audit();
92239
93504
  init_mutate();
@@ -92251,6 +93516,38 @@ var init_test_audit = __esm(() => {
92251
93516
  ]);
92252
93517
  });
92253
93518
 
93519
+ // packages/workit-cli/src/verbs/knowledge.ts
93520
+ var exports_knowledge = {};
93521
+ __export(exports_knowledge, {
93522
+ run: () => run18
93523
+ });
93524
+ async function run18(argv, io) {
93525
+ const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
93526
+ if (sub !== "lint" || rest.length > 0)
93527
+ return emit(io, fail("invalid_input", `usage: ${USAGE11}`));
93528
+ const report = lintKnowledge(checkRoot(io.cwd));
93529
+ if (report.unreadable.length > 0)
93530
+ return emit(io, fail("unavailable", `knowledge lint: cannot read ${report.unreadable.join(", ")}`, {
93531
+ data: report,
93532
+ unblock: `make ${report.unreadable.join(", ")} readable, then re-run workit knowledge lint`
93533
+ }));
93534
+ const summary = `knowledge lint: ${report.findings.length} finding${report.findings.length === 1 ? "" : "s"} in ${report.files.length} file${report.files.length === 1 ? "" : "s"} (${report.files.map((file) => `${file.file} ${file.bytes} B`).join(", ") || "no knowledge files"}; AGENTS.md budget ${report.budget} B)`;
93535
+ if (io.json)
93536
+ return emit(io, report.findings.length === 0 ? ok(report) : fail("failed", summary, { data: report }));
93537
+ for (const finding of report.findings)
93538
+ io.stdout(`${finding.file}${finding.line === null ? "" : `:${finding.line}`} [${finding.rule}] ${finding.message}
93539
+ `);
93540
+ io.stdout(`${summary}
93541
+ `);
93542
+ return report.findings.length === 0 ? 0 : 1;
93543
+ }
93544
+ var USAGE11 = "workit knowledge lint [--json]";
93545
+ var init_knowledge2 = __esm(() => {
93546
+ init_check_config();
93547
+ init_knowledge();
93548
+ init_output();
93549
+ });
93550
+
92254
93551
  // packages/workit-core/src/core/youtrack-tools.ts
92255
93552
  async function logTimeUpdate(input, operation = defaultOperations) {
92256
93553
  try {
@@ -92358,14 +93655,14 @@ var init_youtrack_writes = __esm(() => {
92358
93655
  // packages/workit-cli/src/verbs/youtrack.ts
92359
93656
  var exports_youtrack = {};
92360
93657
  __export(exports_youtrack, {
92361
- run: () => run17
93658
+ run: () => run19
92362
93659
  });
92363
93660
  import { readFileSync as readFileSync21 } from "node:fs";
92364
- import path46 from "node:path";
92365
- async function run17(argv, io) {
93661
+ import path49 from "node:path";
93662
+ async function run19(argv, io) {
92366
93663
  const [sub, ...rest] = argv;
92367
93664
  if (sub !== "note" && sub !== "time" && sub !== "meeting")
92368
- return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE10);
93665
+ return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE12);
92369
93666
  const flags = parseFlags(rest, {
92370
93667
  markdown: "value",
92371
93668
  file: "value",
@@ -92374,18 +93671,18 @@ async function run17(argv, io) {
92374
93671
  date: "value"
92375
93672
  });
92376
93673
  if (typeof flags === "string")
92377
- return usage3(io, flags, USAGE10);
93674
+ return usage3(io, flags, USAGE12);
92378
93675
  const [issueId, extra] = flags.positionals;
92379
93676
  if (!issueId || !ISSUE_RE2.test(issueId))
92380
- return usage3(io, "an issue id like ABC-123 is required", USAGE10);
93677
+ return usage3(io, "an issue id like ABC-123 is required", USAGE12);
92381
93678
  if (extra !== undefined)
92382
- return usage3(io, `unexpected argument ${extra}`, USAGE10);
93679
+ return usage3(io, `unexpected argument ${extra}`, USAGE12);
92383
93680
  const minutes = positiveInt(flags.values.minutes, "--minutes");
92384
93681
  if (typeof minutes === "string")
92385
- return usage3(io, minutes, USAGE10);
93682
+ return usage3(io, minutes, USAGE12);
92386
93683
  const date = youTrackWorkDateMs(flags.values.date ?? "auto");
92387
93684
  if ("error" in date)
92388
- return usage3(io, date.error, USAGE10);
93685
+ return usage3(io, date.error, USAGE12);
92389
93686
  const dateMs = date.data.dateMs;
92390
93687
  const markedDay = flags.values.date !== undefined && flags.values.date !== "auto" ? { dateMs } : {};
92391
93688
  const root = io.cwd;
@@ -92403,24 +93700,24 @@ async function run17(argv, io) {
92403
93700
  };
92404
93701
  if (sub === "note") {
92405
93702
  if (Boolean(flags.values.markdown) === Boolean(flags.values.file))
92406
- return usage3(io, "pass exactly one of --markdown or --file", USAGE10);
93703
+ return usage3(io, "pass exactly one of --markdown or --file", USAGE12);
92407
93704
  let markdown = flags.values.markdown ?? "";
92408
93705
  if (flags.values.file)
92409
93706
  try {
92410
- markdown = readFileSync21(path46.resolve(io.cwd, flags.values.file), "utf8");
93707
+ markdown = readFileSync21(path49.resolve(io.cwd, flags.values.file), "utf8");
92411
93708
  } catch (error) {
92412
- return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE10);
93709
+ return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE12);
92413
93710
  }
92414
93711
  if (!markdown.trim())
92415
- return usage3(io, "the note is empty", USAGE10);
93712
+ return usage3(io, "the note is empty", USAGE12);
92416
93713
  const missing = step("comment", "comments", { operation: "note", issueId, markdown }, markdown, async (text) => commentResult(await postComment(issueId, text, root))) ?? (minutes ? step("time", "workItems", { operation: "note.time", issueId, markdown, minutes, ...markedDay }, "workit update", (text) => logTimeUpdate({ issueId, minutes, text, dateMs, workspace_root: root })) : null);
92417
93714
  if (missing)
92418
93715
  return emit(io, fail("unavailable", missing));
92419
93716
  } else {
92420
93717
  if (!minutes)
92421
- return usage3(io, `${sub} needs --minutes <n>`, USAGE10);
93718
+ return usage3(io, `${sub} needs --minutes <n>`, USAGE12);
92422
93719
  if (sub === "meeting" && !flags.values.text)
92423
- return usage3(io, "meeting needs --text <what the meeting was>", USAGE10);
93720
+ return usage3(io, "meeting needs --text <what the meeting was>", USAGE12);
92424
93721
  const text = flags.values.text ?? "";
92425
93722
  const missing = step(sub, "workItems", { operation: sub, issueId, minutes, text, ...markedDay }, text, (marked) => logTimeUpdate({ issueId, minutes, text: marked, dateMs, workspace_root: root }));
92426
93723
  if (missing)
@@ -92449,7 +93746,7 @@ async function run17(argv, io) {
92449
93746
  ...data.steps.flatMap((entry) => entry.note ? [` note (${entry.step}): ${entry.note}`] : [])
92450
93747
  ]);
92451
93748
  }
92452
- var USAGE10 = "workit youtrack note <ISSUE> (--markdown <t> | --file <p>) [--minutes <n>] [--date auto|YYYY-MM-DD] | youtrack time <ISSUE> --minutes <n> [--text <t>] [--date …] | youtrack meeting <ISSUE> --minutes <n> --text <t> [--date …]", commentResult = (value) => {
93749
+ var USAGE12 = "workit youtrack note <ISSUE> (--markdown <t> | --file <p>) [--minutes <n>] [--date auto|YYYY-MM-DD] | youtrack time <ISSUE> --minutes <n> [--text <t>] [--date …] | youtrack meeting <ISSUE> --minutes <n> --text <t> [--date …]", commentResult = (value) => {
92453
93750
  try {
92454
93751
  return { ok: true, data: unwrap2(value) };
92455
93752
  } catch (error) {
@@ -92473,14 +93770,14 @@ var init_youtrack2 = __esm(() => {
92473
93770
  // packages/workit-cli/src/verbs/changelog.ts
92474
93771
  var exports_changelog = {};
92475
93772
  __export(exports_changelog, {
92476
- run: () => run18
93773
+ run: () => run20
92477
93774
  });
92478
93775
  import { readFileSync as readFileSync22 } from "node:fs";
92479
- import path47 from "node:path";
92480
- async function run18(argv, io) {
93776
+ import path50 from "node:path";
93777
+ async function run20(argv, io) {
92481
93778
  const [sub, ...rest] = argv;
92482
93779
  if (sub !== "apply")
92483
- return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE11);
93780
+ return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE13);
92484
93781
  const flags = parseFlags(rest, {
92485
93782
  entries: "value",
92486
93783
  path: "value",
@@ -92488,18 +93785,18 @@ async function run18(argv, io) {
92488
93785
  preview: "boolean"
92489
93786
  });
92490
93787
  if (typeof flags === "string")
92491
- return usage3(io, flags, USAGE11);
93788
+ return usage3(io, flags, USAGE13);
92492
93789
  if (flags.positionals.length)
92493
- return usage3(io, `unexpected argument ${flags.positionals[0]}`, USAGE11);
93790
+ return usage3(io, `unexpected argument ${flags.positionals[0]}`, USAGE13);
92494
93791
  const normalizeOnly = flags.booleans.has("normalize-only");
92495
93792
  if (!normalizeOnly && !flags.values.entries)
92496
- return usage3(io, "pass --entries <JSON> or --normalize-only", USAGE11);
93793
+ return usage3(io, "pass --entries <JSON> or --normalize-only", USAGE13);
92497
93794
  let entries;
92498
93795
  if (flags.values.entries)
92499
93796
  try {
92500
93797
  entries = readEntries(flags.values.entries, io.cwd);
92501
93798
  } catch (error) {
92502
- return usage3(io, `--entries is not valid JSON: ${String(error)}`, USAGE11);
93799
+ return usage3(io, `--entries is not valid JSON: ${String(error)}`, USAGE13);
92503
93800
  }
92504
93801
  const input = {
92505
93802
  entries,
@@ -92521,8 +93818,8 @@ async function run18(argv, io) {
92521
93818
  `changelog updated: ${String(applied.path ?? "CHANGELOG.md")}`
92522
93819
  ]);
92523
93820
  }
92524
- var USAGE11 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
92525
- const text = raw === "-" ? readFileSync22(0, "utf8") : raw.startsWith("@") ? readFileSync22(path47.resolve(cwd, raw.slice(1)), "utf8") : raw;
93821
+ var USAGE13 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
93822
+ const text = raw === "-" ? readFileSync22(0, "utf8") : raw.startsWith("@") ? readFileSync22(path50.resolve(cwd, raw.slice(1)), "utf8") : raw;
92526
93823
  return JSON.parse(text);
92527
93824
  };
92528
93825
  var init_changelog2 = __esm(() => {
@@ -92534,7 +93831,7 @@ var init_changelog2 = __esm(() => {
92534
93831
  // packages/workit-cli/src/verbs/handoff.ts
92535
93832
  var exports_handoff = {};
92536
93833
  __export(exports_handoff, {
92537
- run: () => run19
93834
+ run: () => run21
92538
93835
  });
92539
93836
  import { parseArgs as parseArgs2 } from "node:util";
92540
93837
  function human2(brief) {
@@ -92582,11 +93879,11 @@ async function resumeBrief(argv, io) {
92582
93879
  strict: true
92583
93880
  }));
92584
93881
  } catch (error) {
92585
- return emit(io, fail("invalid_input", error.message, { unblock: USAGE12 }));
93882
+ return emit(io, fail("invalid_input", error.message, { unblock: USAGE14 }));
92586
93883
  }
92587
93884
  const last = values.last === undefined ? 10 : Number(values.last);
92588
93885
  if (!Number.isInteger(last) || last < 1)
92589
- return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock: USAGE12 }));
93886
+ return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock: USAGE14 }));
92590
93887
  const built = buildHandoff(io.cwd, {
92591
93888
  last,
92592
93889
  note: values.note ?? null,
@@ -92604,13 +93901,13 @@ async function resumeBrief(argv, io) {
92604
93901
  }
92605
93902
  return emit(io, ok({ ...data, recorded }), human2);
92606
93903
  }
92607
- async function run19(argv, io) {
93904
+ async function run21(argv, io) {
92608
93905
  if (!argv.includes("--task"))
92609
93906
  return resumeBrief(argv, io);
92610
93907
  await Promise.resolve().then(() => init_task());
92611
93908
  return runTaskCommand(["handoff", ...argv]);
92612
93909
  }
92613
- var USAGE12 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short5 = (sha) => sha ? sha.slice(0, 12) : "no commit";
93910
+ var USAGE14 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short5 = (sha) => sha ? sha.slice(0, 12) : "no commit";
92614
93911
  var init_handoff = __esm(() => {
92615
93912
  init_ledger();
92616
93913
  init_output();
@@ -92619,7 +93916,7 @@ var init_handoff = __esm(() => {
92619
93916
  // packages/workit-cli/src/main.ts
92620
93917
  init_package();
92621
93918
  init_output();
92622
- import path48 from "node:path";
93919
+ import path51 from "node:path";
92623
93920
 
92624
93921
  // packages/workit-cli/src/verbs/registry.ts
92625
93922
  var TASK_FAMILY_NAMES = [
@@ -92731,6 +94028,13 @@ var VERBS = [
92731
94028
  summary: "Plan, restack (lease push + retarget) and land a base-branch stack of PRs, root first",
92732
94029
  load: () => Promise.resolve().then(() => (init_stack2(), exports_stack))
92733
94030
  },
94031
+ {
94032
+ name: "fanout",
94033
+ group: "delivery",
94034
+ usage: "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] | fanout check [<slice>…] [--name <n>] [--base <ref>] [--json]",
94035
+ summary: "Register parallel slices (complete briefs, disjoint scopes, shared-file owners), then gate fan-in: out-of-scope edits, trunk and sibling merge conflicts, landing order",
94036
+ load: () => Promise.resolve().then(() => (init_fanout2(), exports_fanout))
94037
+ },
92734
94038
  {
92735
94039
  name: "ledger",
92736
94040
  group: "delivery",
@@ -92745,6 +94049,13 @@ var VERBS = [
92745
94049
  summary: "Flag tautological and low-value tests with a suggested independent oracle; --mutate checks changed lines with diff-scoped mutation",
92746
94050
  load: () => Promise.resolve().then(() => (init_test_audit(), exports_test_audit))
92747
94051
  },
94052
+ {
94053
+ name: "knowledge",
94054
+ group: "delivery",
94055
+ usage: "workit knowledge lint [--json]",
94056
+ summary: "Lint agent knowledge files: AGENTS.md byte budget, broken local links, scaffold-only files, duplicated rules (exit 1 on findings)",
94057
+ load: () => Promise.resolve().then(() => (init_knowledge2(), exports_knowledge))
94058
+ },
92748
94059
  {
92749
94060
  name: "youtrack",
92750
94061
  group: "delivery",
@@ -92849,7 +94160,7 @@ var defaultIo = (json) => ({
92849
94160
  });
92850
94161
  async function main(argv, overrides = {}, options = {}) {
92851
94162
  const parsed = parseGlobals(argv);
92852
- const cwd = path48.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
94163
+ const cwd = path51.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
92853
94164
  const io = { ...defaultIo(parsed.json), ...overrides, json: parsed.json, cwd };
92854
94165
  if (parsed.error)
92855
94166
  return emit(io, fail("invalid_input", parsed.error));