@brainervirus/workit-cli 8.11.0 → 8.12.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.
Files changed (2) hide show
  1. package/dist/index.js +1156 -257
  2. package/package.json +4 -4
package/dist/index.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: "8.11.0",
68
+ version: "8.12.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": "^8.11.0",
107
- "@brainervirus/workit-cursor": "^8.11.0",
108
- "@brainervirus/workit-opencode": "^8.11.0",
106
+ "@brainervirus/workit-core": "^8.12.0",
107
+ "@brainervirus/workit-cursor": "^8.12.0",
108
+ "@brainervirus/workit-opencode": "^8.12.0",
109
109
  "@inkjs/ui": "2.0.0",
110
110
  ink: "7.1.1",
111
111
  react: "19.2.8"
@@ -298,7 +298,7 @@ function loadCheckConfig(cwd) {
298
298
  gates[gate] = names;
299
299
  }
300
300
  }
301
- return { source: CHECKS_FILE, root, checks, gates, error: null };
301
+ return { source: CHECKS_FILE, root, checks, gates, error: null, resources: parsed.resources };
302
302
  }
303
303
  function matchesNamedCheck(config, name, argv, cwd) {
304
304
  if (config.error || name === null || cwd !== ".")
@@ -13690,8 +13690,8 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
13690
13690
  profiles: record(nonBlank, profileSchema).optional()
13691
13691
  }).passthrough();
13692
13692
  return entrySchema;
13693
- }, resolveRuntimeWorkspaceVcs = (cwd) => resolveRuntimeWorkspaceCandidate(cwd, "vcs"), runtimeWorkspaceIndexSchema, matchRuntimeWorkspace = (cwd) => {
13694
- const file = path12.join(configDir(), "workspaces.json");
13693
+ }, resolveRuntimeWorkspaceVcs = (cwd) => resolveRuntimeWorkspaceCandidate(cwd, "vcs"), runtimeWorkspaceIndexSchema, matchRuntimeWorkspace = (cwd, dir = configDir()) => {
13694
+ const file = path12.join(dir, "workspaces.json");
13695
13695
  let raw;
13696
13696
  try {
13697
13697
  raw = readFileSync3(file, "utf8");
@@ -13720,10 +13720,27 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
13720
13720
  const matches = matchingEntries(cwd, entries ?? []);
13721
13721
  const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
13722
13722
  const selected = selectWorkspaceMatch(matches, cwd, workspaceName);
13723
- return selected ? { file, selected } : null;
13723
+ return selected ? { file, selected, matches } : null;
13724
13724
  }, resolveRuntimeReleaseTracks = (cwd) => {
13725
13725
  const matched = matchRuntimeWorkspace(cwd);
13726
13726
  return matched ? { workspace: matched.selected.name, releaseTracks: matched.selected.releaseTracks } : null;
13727
+ }, workspaceTrailers = (cwd, dir) => {
13728
+ let matched;
13729
+ try {
13730
+ matched = matchRuntimeWorkspace(cwd, dir);
13731
+ } catch {
13732
+ return { enabled: true, workspace: null, from: null };
13733
+ }
13734
+ if (!matched)
13735
+ return { enabled: true, workspace: null, from: null };
13736
+ const { selected, matches } = matched;
13737
+ const enclosing = matches.filter((entry) => entry !== selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
13738
+ const source = [selected, ...enclosing].find((entry) => typeof entry.trailers === "boolean");
13739
+ return {
13740
+ enabled: source?.trailers !== false,
13741
+ workspace: selected.name,
13742
+ from: source?.name ?? null
13743
+ };
13727
13744
  }, resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
13728
13745
  const matched = matchRuntimeWorkspace(cwd);
13729
13746
  if (!matched)
@@ -13874,6 +13891,7 @@ var init_workspaces = __esm(() => {
13874
13891
  autoApprove: union([boolean2(), array(nonBlank)]).optional(),
13875
13892
  defaultEndpoint: unknown().optional(),
13876
13893
  verification: _enum(["self", "independent"]).optional(),
13894
+ trailers: boolean2().optional(),
13877
13895
  autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
13878
13896
  kind,
13879
13897
  union([boolean2(), literal("verified")]).optional()
@@ -64447,6 +64465,26 @@ function installSessionHook(cwd, env = process.env) {
64447
64465
  detail: `${verb} the Workit ${SESSION_HOOK_NAME} hook at ${status.hookPath}${helper}`
64448
64466
  };
64449
64467
  }
64468
+ function removeSessionHook(cwd, env = process.env) {
64469
+ const status = inspectSessionHook(cwd, env);
64470
+ if (status.state === "not_git")
64471
+ return { action: "skipped", removed: [], status, detail: "not inside a git work tree" };
64472
+ const removed = [];
64473
+ if (workitHookIn(status)) {
64474
+ rmSync2(status.hookPath, { force: true });
64475
+ removed.push(status.hookPath);
64476
+ }
64477
+ if (helperIn(status)) {
64478
+ rmSync2(status.helperPath, { force: true });
64479
+ removed.push(status.helperPath);
64480
+ }
64481
+ return {
64482
+ action: removed.length ? "removed" : "unchanged",
64483
+ removed,
64484
+ status: inspectSessionHook(cwd, env),
64485
+ detail: removed.length ? `removed ${removed.join(" and ")}` : "no Workit session hook or helper to remove"
64486
+ };
64487
+ }
64450
64488
  var SESSION_HOOK_NAME = "prepare-commit-msg", SESSION_HOOK_MARKER = "# workit-session-trailer-hook", HELPER, LEGACY_CHAINED = "commit-msg.workit-chained", SESSION_TRAILER_SCRIPT = `#!/bin/sh
64451
64489
  # Workit (https://github.com/BrainerVirus/workit) wrote this file with
64452
64490
  # \`workit doctor --fix\`. A prepare-commit-msg hook runs it with its own
@@ -64474,6 +64512,19 @@ fi
64474
64512
  [ -n "$session" ] || exit 0
64475
64513
  [ \${#session} -le 128 ] || exit 0
64476
64514
  case "$session" in *[!A-Za-z0-9_.:@/+-]*) exit 0 ;; esac
64515
+ # A workspace with \`trailers: false\` keeps authorship in the ledger only.
64516
+ # \`workit git commit\` decides it already (WORKIT_TRAILERS); otherwise ask
64517
+ # workit, bounded, and add the trailer when it cannot answer.
64518
+ case "\${WORKIT_TRAILERS-}" in
64519
+ off) exit 0 ;;
64520
+ on) ;;
64521
+ *)
64522
+ if command -v workit >/dev/null 2>&1; then
64523
+ t=; ! command -v timeout >/dev/null 2>&1 || t="timeout 5"
64524
+ [ "$($t workit git trailers 2>/dev/null)" != off ] || exit 0
64525
+ fi
64526
+ ;;
64527
+ esac
64477
64528
  add_trailer() {
64478
64529
  [ -f "$1" ] && [ -r "$1" ] || return 0
64479
64530
  body=$(sed '/^# -\\{24\\} >8 -\\{24\\}$/,$d' "$1" | git stripspace --strip-comments)
@@ -64617,7 +64668,7 @@ exit 0
64617
64668
  } finally {
64618
64669
  rmSync2(staged, { force: true });
64619
64670
  }
64620
- };
64671
+ }, workitHookIn = (status) => status.hookPath !== null && (readText2(status.hookPath)?.includes(SESSION_HOOK_MARKER) ?? false), helperIn = (status) => status.helperPath !== null && exists(status.helperPath);
64621
64672
  var init_session_hook = __esm(() => {
64622
64673
  init_rev();
64623
64674
  HELPER = ["workit", "session-trailer.sh"];
@@ -66052,12 +66103,34 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
66052
66103
  };
66053
66104
  }
66054
66105
  }, checkSessionHook = (res) => {
66055
- if (!matchedWorkspace(res))
66106
+ const workspace = matchedWorkspace(res);
66107
+ if (!workspace)
66056
66108
  return {
66057
66109
  id: "session_hook",
66058
66110
  status: "pass",
66059
66111
  detail: `not in a Workit workspace — skipping the ${SESSION_HOOK_NAME} session hook`
66060
66112
  };
66113
+ const trailers = workspaceTrailers(res.cwd, res.configDir);
66114
+ if (!trailers.enabled) {
66115
+ const setting = `workspace ${trailers.from ?? workspace.name} sets trailers: false`;
66116
+ const left = inspectSessionHook(res.cwd, res.env);
66117
+ const files = [
66118
+ ...workitHookIn(left) ? [left.hookPath] : [],
66119
+ ...helperIn(left) ? [left.helperPath] : []
66120
+ ];
66121
+ if (files.length)
66122
+ return {
66123
+ id: "session_hook",
66124
+ status: "warn",
66125
+ detail: `${setting}, but Workit's ${files.join(" and ")} ${files.length > 1 ? "are" : "is"} still installed: plain git commits still get a Workit-Session trailer whenever the helper cannot ask workit (it fails open)`,
66126
+ fix: SESSION_HOOK_FIX
66127
+ };
66128
+ return {
66129
+ id: "session_hook",
66130
+ status: "pass",
66131
+ detail: `${setting} (authorship in the ledger only) — no ${SESSION_HOOK_NAME} session hook needed`
66132
+ };
66133
+ }
66061
66134
  const hook = inspectSessionHook(res.cwd, res.env);
66062
66135
  const finding = sessionHookFinding(hook);
66063
66136
  if (!hook.leftover)
@@ -66077,6 +66150,14 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
66077
66150
  detail: `${res.cwd} is not in a Workit workspace; no hook installed`,
66078
66151
  workspace: null
66079
66152
  };
66153
+ if (!workspaceTrailers(res.cwd, res.configDir).enabled) {
66154
+ const removal = removeSessionHook(res.cwd, res.env);
66155
+ return {
66156
+ ...removal,
66157
+ detail: `trailers: false; no hook installed (${removal.detail})`,
66158
+ workspace: workspace.name
66159
+ };
66160
+ }
66080
66161
  return { ...installSessionHook(res.cwd, res.env), workspace: workspace.name };
66081
66162
  }, isPlaceholder = (p) => {
66082
66163
  try {
@@ -72515,7 +72596,10 @@ function runCheckCommand(argv, options) {
72515
72596
  const buffer = new TailBuffer(MAX_LOG_BYTES);
72516
72597
  const redactor = new StreamRedactor((text) => buffer.push(text));
72517
72598
  const env = options.env ?? process.env;
72518
- const plan = planSpawn(argv, { platform, env, cwd: options.cwd, shell: options.shell });
72599
+ const wrapped = Boolean(options.wrapper?.length) && platform !== "win32";
72600
+ const command = wrapped ? [...options.wrapper, ...options.shell ? ["/bin/sh", "-c", argv[0]] : argv] : argv;
72601
+ const shell = wrapped ? false : options.shell;
72602
+ const plan = planSpawn(command, { platform, env, cwd: options.cwd, shell });
72519
72603
  return new Promise((resolve) => {
72520
72604
  let settled = false;
72521
72605
  let timedOut = false;
@@ -72578,6 +72662,8 @@ function runCheckCommand(argv, options) {
72578
72662
  finish(null, null, error.code ?? error.message);
72579
72663
  return;
72580
72664
  }
72665
+ if (child.pid)
72666
+ options.onSpawn?.(child.pid);
72581
72667
  if (!windows)
72582
72668
  for (const name of FORWARDED_SIGNALS) {
72583
72669
  const handler = () => {
@@ -72915,18 +73001,503 @@ var init_grant = __esm(() => {
72915
73001
  };
72916
73002
  });
72917
73003
 
73004
+ // packages/workit-core/src/core/resources.ts
73005
+ import { randomUUID as randomUUID5 } from "node:crypto";
73006
+ import fs20 from "node:fs";
73007
+ import os14 from "node:os";
73008
+ import path40 from "node:path";
73009
+ function memoryValue(value, host) {
73010
+ const text = typeof value === "number" ? String(value) : typeof value === "string" ? value : "";
73011
+ const percent = PERCENT.exec(text.trim());
73012
+ if (percent) {
73013
+ const share = Number(percent[1]);
73014
+ if (share <= 0 || share > 100)
73015
+ return new Error(`memoryMax ${text} must be 1-100%`);
73016
+ return Math.floor(host.totalMemBytes * share / 100);
73017
+ }
73018
+ const size = SIZE.exec(text.trim());
73019
+ if (size && Number(size[1]) > 0)
73020
+ return Math.floor(Number(size[1]) * UNITS[size[2].toUpperCase()]);
73021
+ return new Error(`memoryMax must be a percent of RAM ("40%") or a size ("8G"), got ${text}`);
73022
+ }
73023
+ function cpuValue(value, host) {
73024
+ if (typeof value === "number" && value > 0)
73025
+ return Math.round(value * 100);
73026
+ const percent = typeof value === "string" ? PERCENT.exec(value.trim()) : null;
73027
+ if (percent) {
73028
+ const share = Number(percent[1]);
73029
+ if (share <= 0 || share > 100)
73030
+ return new Error(`cpuQuota ${percent[0]} must be 1-100%`);
73031
+ return Math.max(1, Math.round(host.cpus * share));
73032
+ }
73033
+ return new Error(`cpuQuota must be a percent of the machine ("70%") or a CPU count, got ${String(value)}`);
73034
+ }
73035
+ function resolveResourceSettings(sources, host = hostShape()) {
73036
+ let memory = Math.floor(host.totalMemBytes * DEFAULT_MEMORY_SHARE);
73037
+ let cpu = Math.max(1, Math.round(host.cpus * DEFAULT_CPU_SHARE * 100));
73038
+ const settings = {
73039
+ slots: DEFAULT_SLOTS,
73040
+ heavy: [...DEFAULT_HEAVY_CHECKS],
73041
+ memoryMax: "",
73042
+ cpuQuota: "",
73043
+ waitMs: DEFAULT_WAIT_S * 1000,
73044
+ scope: true
73045
+ };
73046
+ const warnings = [];
73047
+ const ordered = [
73048
+ ...sources.filter((source) => source.trust === "machine"),
73049
+ ...sources.filter((source) => source.trust === "repo")
73050
+ ];
73051
+ for (const { label, trust, value } of ordered) {
73052
+ if (value === undefined || value === null)
73053
+ continue;
73054
+ if (!isRecord7(value)) {
73055
+ warnings.push(`${label}: resources must be an object`);
73056
+ continue;
73057
+ }
73058
+ const repo = trust === "repo";
73059
+ const warn = (field, message) => warnings.push(`${label}: ${field}: ${message}`);
73060
+ const machineOnly = (field) => warn(field, "is machine-wide; set it in your workspaces.json, not in the repo");
73061
+ if (value.slots !== undefined) {
73062
+ const slots = value.slots;
73063
+ if (repo)
73064
+ machineOnly("slots");
73065
+ else if (typeof slots === "number" && Number.isInteger(slots) && slots >= 1 && slots <= MAX_SLOTS)
73066
+ settings.slots = slots;
73067
+ else
73068
+ warn("slots", `must be an integer 1-${MAX_SLOTS}`);
73069
+ }
73070
+ if (value.scope !== undefined) {
73071
+ if (repo)
73072
+ machineOnly("scope");
73073
+ else if (typeof value.scope === "boolean")
73074
+ settings.scope = value.scope;
73075
+ else
73076
+ warn("scope", "must be true or false");
73077
+ }
73078
+ if (value.heavy !== undefined) {
73079
+ const heavy = isRecord7(value.heavy) ? Object.keys(value.heavy) : value.heavy;
73080
+ if (!Array.isArray(heavy) || !heavy.every((item) => typeof item === "string"))
73081
+ warn("heavy", "must be a list of check names");
73082
+ else
73083
+ settings.heavy = repo ? [...new Set([...settings.heavy, ...heavy])] : heavy;
73084
+ }
73085
+ if (value.memoryMax !== undefined) {
73086
+ const next = memoryValue(value.memoryMax, host);
73087
+ if (next instanceof Error)
73088
+ warn("memoryMax", next.message);
73089
+ else if (repo && next > memory)
73090
+ warn("memoryMax", "a repo can only lower it");
73091
+ else
73092
+ memory = next;
73093
+ }
73094
+ if (value.cpuQuota !== undefined) {
73095
+ const next = cpuValue(value.cpuQuota, host);
73096
+ if (next instanceof Error)
73097
+ warn("cpuQuota", next.message);
73098
+ else if (repo && next > cpu)
73099
+ warn("cpuQuota", "a repo can only lower it");
73100
+ else
73101
+ cpu = next;
73102
+ }
73103
+ if (value.wait !== undefined) {
73104
+ if (typeof value.wait === "number" && value.wait >= 0)
73105
+ settings.waitMs = value.wait * 1000;
73106
+ else
73107
+ warn("wait", "must be a number of seconds");
73108
+ }
73109
+ }
73110
+ settings.memoryMax = String(memory);
73111
+ settings.cpuQuota = `${cpu}%`;
73112
+ return { settings, warnings };
73113
+ }
73114
+ function stateDir(env, platform = process.platform) {
73115
+ if (env.WORKFLOW_TOOLKIT_STATE)
73116
+ return env.WORKFLOW_TOOLKIT_STATE;
73117
+ if (env.XDG_STATE_HOME)
73118
+ return path40.join(env.XDG_STATE_HOME, "workit");
73119
+ const home = env.HOME || os14.homedir();
73120
+ if (platform === "darwin")
73121
+ return path40.join(home, "Library", "Application Support", "workit");
73122
+ if (platform === "win32")
73123
+ return path40.join(env.LOCALAPPDATA ?? path40.join(home, "AppData", "Local"), "workit");
73124
+ return path40.join(home, ".local", "state", "workit");
73125
+ }
73126
+ function scopeBusy(unit) {
73127
+ if (process.platform !== "linux" || !SCOPE_UNIT.test(unit))
73128
+ return false;
73129
+ const uid = process.getuid?.() ?? 0;
73130
+ const manager = `/sys/fs/cgroup/user.slice/user-${uid}.slice/user@${uid}.service`;
73131
+ return [`${manager}/app.slice/${unit}`, `${manager}/${unit}`].some((dir) => (readText3(`${dir}/cgroup.procs`) ?? "").trim() !== "");
73132
+ }
73133
+ function isAncestor(pid) {
73134
+ if (process.platform !== "linux")
73135
+ return true;
73136
+ let current = process.pid;
73137
+ for (let step = 0;step < 128 && current > 1; step += 1) {
73138
+ if (current === pid)
73139
+ return true;
73140
+ const ppid = Number(statFields(current)?.[1]);
73141
+ if (!Number.isInteger(ppid) || ppid <= 0)
73142
+ return false;
73143
+ current = ppid;
73144
+ }
73145
+ return current === pid;
73146
+ }
73147
+ function staleReason(lease, probe = {}) {
73148
+ const boot = probe.boot === undefined ? bootId() : probe.boot;
73149
+ if (lease.bootId && boot) {
73150
+ if (lease.bootId !== boot)
73151
+ return "taken before a reboot";
73152
+ } else if (typeof lease.bootTime === "number") {
73153
+ const now = probe.bootTime === undefined ? bootTime() : probe.bootTime;
73154
+ if (now !== null && Math.abs(now - lease.bootTime) > BOOT_SLACK_MS)
73155
+ return "taken before a reboot";
73156
+ }
73157
+ const start = lease.processStart ? (probe.start ?? processStart)(lease.pid) : null;
73158
+ const age = (probe.now ?? Date.now()) - Date.parse(lease.startedAt);
73159
+ if (!start && typeof lease.maxAgeMs === "number" && age > lease.maxAgeMs)
73160
+ return `older than its ${Math.round(lease.maxAgeMs / 1000)} s bound`;
73161
+ const holder = !(probe.alive ?? pidAlive2)(lease.pid) ? `holder pid ${lease.pid} is gone` : start && start !== lease.processStart ? `pid ${lease.pid} now belongs to another process` : null;
73162
+ if (!holder)
73163
+ return null;
73164
+ if (typeof lease.pgid === "number" && lease.pgid > 0 && (probe.group ?? groupAlive)(lease.pgid))
73165
+ return null;
73166
+ if (lease.scope && (probe.scope ?? scopeBusy)(lease.scope))
73167
+ return null;
73168
+ return holder;
73169
+ }
73170
+ function reclaim(file, stale) {
73171
+ const guard = `${file}.reclaim`;
73172
+ try {
73173
+ fs20.mkdirSync(guard);
73174
+ } catch {
73175
+ try {
73176
+ if (Date.now() - fs20.statSync(guard).mtimeMs > GUARD_TTL_MS)
73177
+ fs20.rmSync(guard, { force: true, recursive: true });
73178
+ } catch {}
73179
+ return false;
73180
+ }
73181
+ try {
73182
+ let ageMs = 0;
73183
+ try {
73184
+ ageMs = Date.now() - fs20.statSync(file).mtimeMs;
73185
+ } catch {
73186
+ return true;
73187
+ }
73188
+ if (!stale(parseLease(readText3(file)), ageMs))
73189
+ return false;
73190
+ fs20.rmSync(file, { force: true });
73191
+ return true;
73192
+ } finally {
73193
+ fs20.rmSync(guard, { force: true, recursive: true });
73194
+ }
73195
+ }
73196
+ function tryOnce(options, record, reclaimed) {
73197
+ const holders = [];
73198
+ const text = `${JSON.stringify(record)}
73199
+ `;
73200
+ for (let slot = 0;slot < options.slots; slot += 1) {
73201
+ const file = path40.join(options.dir, `slot-${slot}.json`);
73202
+ for (let attempt = 0;attempt < 2; attempt += 1) {
73203
+ try {
73204
+ const fd = fs20.openSync(file, "wx", 384);
73205
+ try {
73206
+ fs20.writeSync(fd, text);
73207
+ } finally {
73208
+ fs20.closeSync(fd);
73209
+ }
73210
+ return { slot, file };
73211
+ } catch (error) {
73212
+ if (error.code !== "EEXIST")
73213
+ throw error;
73214
+ }
73215
+ const lease = parseLease(readText3(file));
73216
+ let reason = null;
73217
+ const removed = reclaim(file, (current, ageMs) => {
73218
+ reason = current ? staleReason(current, options.probe) : ageMs > UNREADABLE_TTL_MS ? "unreadable" : null;
73219
+ return reason !== null;
73220
+ });
73221
+ if (removed) {
73222
+ if (lease && reason)
73223
+ reclaimed.push({ ...holderOf(lease), reason });
73224
+ continue;
73225
+ }
73226
+ if (lease)
73227
+ holders.push(holderOf(lease));
73228
+ break;
73229
+ }
73230
+ }
73231
+ return { holders };
73232
+ }
73233
+ function removeOwn(file, id) {
73234
+ if (parseLease(readText3(file))?.id === id)
73235
+ fs20.rmSync(file, { force: true });
73236
+ }
73237
+ async function acquireLease(options) {
73238
+ const pid = options.pid ?? process.pid;
73239
+ const boot = bootId();
73240
+ const record = {
73241
+ id: randomUUID5(),
73242
+ pid,
73243
+ processStart: processStart(pid),
73244
+ bootId: boot,
73245
+ bootTime: boot ? null : bootTime(),
73246
+ pgid: null,
73247
+ maxAgeMs: options.maxAgeMs ?? null,
73248
+ check: options.check,
73249
+ cwd: options.cwd,
73250
+ session: options.session,
73251
+ startedAt: new Date().toISOString(),
73252
+ scope: null
73253
+ };
73254
+ try {
73255
+ fs20.mkdirSync(options.dir, { recursive: true, mode: 448 });
73256
+ } catch (error) {
73257
+ return { ok: false, reason: "unavailable", error: error.message };
73258
+ }
73259
+ const started = Date.now();
73260
+ const reclaimed = [];
73261
+ let waitedOn = [];
73262
+ for (;; ) {
73263
+ let outcome;
73264
+ try {
73265
+ outcome = tryOnce(options, record, reclaimed);
73266
+ } catch (error) {
73267
+ return { ok: false, reason: "unavailable", error: error.message };
73268
+ }
73269
+ if ("slot" in outcome) {
73270
+ const { file, slot } = outcome;
73271
+ let released = false;
73272
+ const release = () => {
73273
+ if (released)
73274
+ return;
73275
+ released = true;
73276
+ process.off("exit", release);
73277
+ removeOwn(file, record.id);
73278
+ };
73279
+ process.on("exit", release);
73280
+ return {
73281
+ ok: true,
73282
+ lease: {
73283
+ record,
73284
+ slot,
73285
+ file,
73286
+ waitedOn,
73287
+ waitedMs: waitedOn.length ? Date.now() - started : 0,
73288
+ reclaimed,
73289
+ release
73290
+ }
73291
+ };
73292
+ }
73293
+ const waited = Date.now() - started;
73294
+ if (!waitedOn.length) {
73295
+ waitedOn = outcome.holders;
73296
+ options.onWait?.(outcome.holders);
73297
+ }
73298
+ if (waited >= options.waitMs)
73299
+ return { ok: false, reason: "timeout", holders: outcome.holders, waitedMs: waited };
73300
+ await sleep(Math.min(options.pollMs ?? 2000, Math.max(1, options.waitMs - waited)));
73301
+ }
73302
+ }
73303
+ function updateLease(lease, patch) {
73304
+ Object.assign(lease.record, patch);
73305
+ try {
73306
+ if (parseLease(readText3(lease.file))?.id !== lease.record.id)
73307
+ return;
73308
+ const temp = `${lease.file}.${process.pid}.tmp`;
73309
+ fs20.writeFileSync(temp, `${JSON.stringify(lease.record)}
73310
+ `, { mode: 384 });
73311
+ fs20.renameSync(temp, lease.file);
73312
+ } catch {}
73313
+ }
73314
+ function heldLease(dir, id, options = {}) {
73315
+ if (!id || !/^[0-9a-f-]{36}$/u.test(id))
73316
+ return null;
73317
+ let names;
73318
+ try {
73319
+ names = fs20.readdirSync(dir);
73320
+ } catch {
73321
+ return null;
73322
+ }
73323
+ for (const name of names) {
73324
+ if (!/^slot-\d+\.json$/u.test(name))
73325
+ continue;
73326
+ const lease = parseLease(readText3(path40.join(dir, name)));
73327
+ if (lease?.id === id && !staleReason(lease, options.probe) && (options.ancestor ?? isAncestor)(lease.pid))
73328
+ return lease;
73329
+ }
73330
+ return null;
73331
+ }
73332
+ function describeHolder(holder, now = Date.now()) {
73333
+ const seconds = Math.max(0, Math.round((now - Date.parse(holder.startedAt)) / 1000));
73334
+ const age = seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`;
73335
+ return `${holder.check}, pid ${holder.pid}${holder.session ? `, session ${holder.session}` : ""}, ${age}, ${holder.cwd}`;
73336
+ }
73337
+ function scopeSupport(env, platform = process.platform, read = readText3) {
73338
+ if (platform !== "linux")
73339
+ return { ok: false, reason: `no systemd scopes on ${platform}` };
73340
+ if (!onPath("systemd-run", env))
73341
+ return { ok: false, reason: "systemd-run is not on PATH" };
73342
+ const uid = process.getuid?.() ?? 0;
73343
+ const runtime = env.XDG_RUNTIME_DIR || `/run/user/${uid}`;
73344
+ if (!fs20.existsSync(path40.join(runtime, "systemd", "private")))
73345
+ return { ok: false, reason: "no user systemd manager is running" };
73346
+ const controllers = read(`/sys/fs/cgroup/user.slice/user-${uid}.slice/user@${uid}.service/cgroup.controllers`);
73347
+ if (controllers === null)
73348
+ return { ok: false, reason: "cgroup v2 is not mounted for the user manager" };
73349
+ const missing = ["memory", "cpu"].filter((name) => !controllers.split(/\s+/u).includes(name));
73350
+ if (missing.length)
73351
+ return { ok: false, reason: `the user manager is not delegated ${missing.join(" and ")}` };
73352
+ return { ok: true };
73353
+ }
73354
+ function scopePrefix(unit, settings) {
73355
+ return [
73356
+ "systemd-run",
73357
+ "--user",
73358
+ "--scope",
73359
+ "--quiet",
73360
+ "--collect",
73361
+ `--unit=${unit}`,
73362
+ "-p",
73363
+ `MemoryMax=${settings.memoryMax}`,
73364
+ "-p",
73365
+ `CPUQuota=${settings.cpuQuota}`,
73366
+ "--"
73367
+ ];
73368
+ }
73369
+ async function runScoped(run, wrapper, sinks) {
73370
+ if (!wrapper)
73371
+ return { result: await run(null, sinks), fallback: null };
73372
+ let state = "deciding";
73373
+ let firstLine = "";
73374
+ const held = [];
73375
+ const flush = () => {
73376
+ state = "passing";
73377
+ for (const [sink, text] of held.splice(0))
73378
+ sinks[sink](text);
73379
+ };
73380
+ const result = await run(wrapper, {
73381
+ onStdout: (text) => {
73382
+ if (state === "passing")
73383
+ return sinks.onStdout(text);
73384
+ held.push(["onStdout", text]);
73385
+ if (state === "deciding")
73386
+ flush();
73387
+ },
73388
+ onStderr: (text) => {
73389
+ if (state === "passing")
73390
+ return sinks.onStderr(text);
73391
+ held.push(["onStderr", text]);
73392
+ if (state === "held")
73393
+ return;
73394
+ firstLine += text;
73395
+ const newline = firstLine.indexOf(`
73396
+ `);
73397
+ if (newline < 0 && firstLine.length < 4096)
73398
+ return;
73399
+ if (SCOPE_START_FAILURE.test(firstLine.slice(0, newline < 0 ? undefined : newline)))
73400
+ state = "held";
73401
+ else
73402
+ flush();
73403
+ }
73404
+ });
73405
+ const lines = tailLines(result.log, 2);
73406
+ if (state !== "passing" && result.exitCode !== 0 && lines.length === 1 && SCOPE_START_FAILURE.test(lines[0]))
73407
+ return { result: await run(null, sinks), fallback: lines[0] };
73408
+ flush();
73409
+ return { result, fallback: null };
73410
+ }
73411
+ function firstNotice(env, session, key, now = Date.now()) {
73412
+ const id = (session ?? `ppid-${process.ppid}`).replace(/[^A-Za-z0-9_.-]/gu, "_").slice(0, 120);
73413
+ const dir = path40.join(stateDir(env), "notices");
73414
+ try {
73415
+ fs20.mkdirSync(dir, { recursive: true, mode: 448 });
73416
+ fs20.writeFileSync(path40.join(dir, `${key}-${id}`), `${new Date(now).toISOString()}
73417
+ `, {
73418
+ flag: "wx",
73419
+ mode: 384
73420
+ });
73421
+ } catch (error) {
73422
+ return error.code !== "EEXIST";
73423
+ }
73424
+ try {
73425
+ for (const name of fs20.readdirSync(dir)) {
73426
+ const file = path40.join(dir, name);
73427
+ if (now - fs20.statSync(file).mtimeMs > NOTICE_TTL_MS)
73428
+ fs20.rmSync(file, { force: true });
73429
+ }
73430
+ } catch {}
73431
+ return true;
73432
+ }
73433
+ var DEFAULT_HEAVY_CHECKS, DEFAULT_SLOTS = 1, DEFAULT_WAIT_S = 600, DEFAULT_MEMORY_SHARE = 0.4, DEFAULT_CPU_SHARE = 0.7, MAX_SLOTS = 64, UNREADABLE_TTL_MS = 30000, GUARD_TTL_MS = 30000, hostShape = () => ({
73434
+ totalMemBytes: os14.totalmem(),
73435
+ cpus: Math.max(1, os14.availableParallelism?.() ?? os14.cpus().length)
73436
+ }), isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), SIZE, PERCENT, UNITS, leaseDir = (env) => path40.join(stateDir(env), "leases"), readText3 = (file) => {
73437
+ try {
73438
+ return fs20.readFileSync(file, "utf8");
73439
+ } catch {
73440
+ return null;
73441
+ }
73442
+ }, bootId = () => readText3("/proc/sys/kernel/random/boot_id")?.trim() || null, bootTime = () => Math.round((Date.now() - os14.uptime() * 1000) / 1000) * 1000, BOOT_SLACK_MS = 120000, statFields = (pid) => {
73443
+ const stat = readText3(`/proc/${pid}/stat`);
73444
+ return stat ? stat.slice(stat.lastIndexOf(")") + 2).split(" ") : null;
73445
+ }, processStart = (pid) => statFields(pid)?.[19] ?? null, pidAlive2 = (pid) => {
73446
+ try {
73447
+ process.kill(pid, 0);
73448
+ return true;
73449
+ } catch (error) {
73450
+ return error.code === "EPERM";
73451
+ }
73452
+ }, groupAlive = (pgid) => process.platform === "win32" ? pidAlive2(pgid) : pidAlive2(-pgid), SCOPE_UNIT, parseLease = (text) => {
73453
+ if (!text)
73454
+ return null;
73455
+ try {
73456
+ const value = JSON.parse(text);
73457
+ return isRecord7(value) && typeof value.id === "string" && typeof value.pid === "number" ? value : null;
73458
+ } catch {
73459
+ return null;
73460
+ }
73461
+ }, holderOf = (lease) => ({
73462
+ pid: lease.pid,
73463
+ check: lease.check,
73464
+ cwd: lease.cwd,
73465
+ session: lease.session,
73466
+ startedAt: lease.startedAt
73467
+ }), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), onPath = (command, env) => (env.PATH ?? "").split(path40.delimiter).filter(Boolean).some((dir) => {
73468
+ try {
73469
+ fs20.accessSync(path40.join(dir, command), fs20.constants.X_OK);
73470
+ return true;
73471
+ } catch {
73472
+ return false;
73473
+ }
73474
+ }), SCOPE_START_FAILURE, NOTICE_TTL_MS;
73475
+ var init_resources = __esm(() => {
73476
+ init_checks3();
73477
+ DEFAULT_HEAVY_CHECKS = ["test", "build", "typecheck"];
73478
+ SIZE = /^(\d+(?:\.\d+)?)([KMGT]?)$/iu;
73479
+ PERCENT = /^(\d+(?:\.\d+)?)%$/u;
73480
+ UNITS = { "": 1, K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4 };
73481
+ SCOPE_UNIT = /^[\w.@-]+\.scope$/u;
73482
+ SCOPE_START_FAILURE = /^Failed to (?:connect to bus|create bus connection|start transient scope unit|parse \S+?=)/u;
73483
+ NOTICE_TTL_MS = 7 * 86400000;
73484
+ });
73485
+
72918
73486
  // packages/workit-cli/src/verbs/check.ts
72919
73487
  var exports_check = {};
72920
73488
  __export(exports_check, {
72921
73489
  run: () => run10
72922
73490
  });
72923
- import path40 from "node:path";
73491
+ import os15 from "node:os";
73492
+ import path41 from "node:path";
72924
73493
  function parse5(argv) {
72925
73494
  const split = argv.indexOf("--");
72926
73495
  const head = split >= 0 ? argv.slice(0, split) : [...argv];
72927
73496
  const options = {
72928
73497
  name: null,
72929
73498
  shell: false,
73499
+ heavy: false,
73500
+ noLease: false,
72930
73501
  timeoutMs: 0,
72931
73502
  task: null,
72932
73503
  base: null,
@@ -72946,6 +73517,13 @@ function parse5(argv) {
72946
73517
  options.shell = true;
72947
73518
  continue;
72948
73519
  }
73520
+ if (flag === "--heavy" || flag === "--no-lease") {
73521
+ if (flag === "--heavy")
73522
+ options.heavy = true;
73523
+ else
73524
+ options.noLease = true;
73525
+ continue;
73526
+ }
72949
73527
  if (flag === "--name" || flag === "--task" || flag === "--base" || flag === "--timeout") {
72950
73528
  const got = value();
72951
73529
  if (got instanceof Error)
@@ -73069,6 +73647,90 @@ function boundedArgv(argv) {
73069
73647
  const truncated = cut.length !== argv.length || cut.some((item, index) => item !== argv[index]);
73070
73648
  return { argv: cut, truncated };
73071
73649
  }
73650
+ async function workspaceResources(io) {
73651
+ const env = io.env;
73652
+ const dir = env.WORKFLOW_TOOLKIT_CONFIG ?? env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path41.join(env.XDG_CONFIG_HOME || path41.join(env.HOME || os15.homedir(), ".config"), "workit");
73653
+ try {
73654
+ await Promise.resolve().then(() => init_workspaces());
73655
+ const workspace = resolveWorkspaceFrom(io.cwd, dir, env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined);
73656
+ return { value: workspace?.resources, warning: null };
73657
+ } catch (error) {
73658
+ return { value: undefined, warning: `workspaces.json: ${error.message}` };
73659
+ }
73660
+ }
73661
+ async function prepareHeavy(io, settings, label, runCwd, timeoutMs) {
73662
+ const session = hostSessionFromEnv(io.env).session;
73663
+ const dir = leaseDir(io.env);
73664
+ const outer = heldLease(dir, io.env.WORKIT_LEASE);
73665
+ if (outer)
73666
+ return {
73667
+ lease: null,
73668
+ leaseReport: { slot: null, waitedMs: 0, holder: null, reclaimed: [], inherited: true },
73669
+ scope: { unit: outer.scope, reason: "inherited from the outer heavy check" },
73670
+ wrapper: null,
73671
+ env: io.env
73672
+ };
73673
+ const acquired = await acquireLease({
73674
+ dir,
73675
+ slots: settings.slots,
73676
+ check: label,
73677
+ cwd: runCwd,
73678
+ session,
73679
+ waitMs: settings.waitMs,
73680
+ pollMs: 1000,
73681
+ maxAgeMs: (timeoutMs || MAX_UNVERIFIED_RUN_MS) + 60000,
73682
+ onWait: (holders) => io.stderr(`workit: waiting for the heavy-check lease (${settings.slots} slot${settings.slots === 1 ? "" : "s"}): held by ${holders.map((holder) => describeHolder(holder)).join("; ")}
73683
+ `)
73684
+ });
73685
+ if (!acquired.ok && acquired.reason === "timeout") {
73686
+ const holders = acquired.holders.map((holder) => describeHolder(holder)).join("; ");
73687
+ emit(io, fail("busy", `the heavy-check lease stayed busy for ${Math.round(acquired.waitedMs / 1000)} s`, {
73688
+ data: { lease: { waitedMs: acquired.waitedMs, holders: acquired.holders } },
73689
+ unblock: `wait for ${holders || "the holders"} to finish, then run it again`
73690
+ }));
73691
+ return 4;
73692
+ }
73693
+ let lease = null;
73694
+ if (acquired.ok)
73695
+ lease = acquired.lease;
73696
+ else
73697
+ io.stderr(`workit: running without the heavy-check lease: ${acquired.error}
73698
+ `);
73699
+ for (const stale of lease?.reclaimed ?? [])
73700
+ io.stderr(`workit: reclaimed a stale heavy-check lease (${stale.reason}): ${describeHolder(stale)}
73701
+ `);
73702
+ const leaseReport = lease ? {
73703
+ slot: lease.slot,
73704
+ waitedMs: lease.waitedMs,
73705
+ holder: lease.waitedOn[0] ?? null,
73706
+ reclaimed: lease.reclaimed,
73707
+ inherited: false
73708
+ } : null;
73709
+ const env = lease ? { ...io.env, WORKIT_LEASE: lease.record.id } : io.env;
73710
+ const support = settings.scope ? scopeSupport(io.env) : { ok: false, reason: "resources.scope is false" };
73711
+ if (!support.ok) {
73712
+ if (settings.scope && firstNotice(io.env, session, "no-scope"))
73713
+ io.stderr(`workit: heavy checks run without a memory/CPU limit here: ${support.reason} (said once per session)
73714
+ `);
73715
+ return {
73716
+ lease,
73717
+ leaseReport,
73718
+ scope: { unit: null, reason: support.reason },
73719
+ wrapper: null,
73720
+ env
73721
+ };
73722
+ }
73723
+ const unit = `workit-check-${(lease?.record.id ?? "nolease0").slice(0, 8)}-${process.pid}`;
73724
+ if (lease)
73725
+ updateLease(lease, { scope: `${unit}.scope` });
73726
+ return {
73727
+ lease,
73728
+ leaseReport,
73729
+ scope: { unit: `${unit}.scope`, memoryMax: settings.memoryMax, cpuQuota: settings.cpuQuota },
73730
+ wrapper: scopePrefix(unit, settings),
73731
+ env
73732
+ };
73733
+ }
73072
73734
  async function run10(argv, io) {
73073
73735
  const options = parse5(argv);
73074
73736
  if (options instanceof Error)
@@ -73103,21 +73765,52 @@ async function run10(argv, io) {
73103
73765
  if (options.shell && command.length !== 1)
73104
73766
  return emit(io, fail("invalid_input", '--shell takes the command as one string: workit check --shell -- "<cmd>"'));
73105
73767
  }
73106
- const relCwd = posix(path40.relative(top, runCwd));
73768
+ const relCwd = posix(path41.relative(top, runCwd));
73107
73769
  const configured = !options.shell && matchesNamedCheck(config, name, command, relCwd);
73108
73770
  const actor = actorFromEnv(io.env);
73109
73771
  const branchKey = codeKey(io.cwd, { base: options.base });
73110
73772
  const detached = branchKey.tree === null ? worktreeTree(io.cwd) : null;
73111
73773
  const key = detached ? { ...branchKey, tree: detached.key, dirty: detached.dirty } : branchKey;
73112
73774
  const signal = key.tree === null ? null : worktreeSignal(io.cwd);
73113
- const result = await runCheckCommand(command, {
73114
- cwd: runCwd,
73115
- shell: options.shell,
73116
- timeoutMs: options.timeoutMs,
73117
- env: io.env,
73118
- onStdout: io.json ? io.stderr : io.stdout,
73119
- onStderr: io.stderr
73120
- });
73775
+ let heavy = null;
73776
+ if (!options.noLease && (options.heavy || options.command === null)) {
73777
+ const workspace = await workspaceResources(io);
73778
+ const { settings, warnings } = resolveResourceSettings([
73779
+ { label: "workspaces.json", trust: "machine", value: workspace.value },
73780
+ { label: CHECKS_FILE, trust: "repo", value: config.resources }
73781
+ ]);
73782
+ for (const warning of [workspace.warning, ...warnings])
73783
+ if (warning)
73784
+ io.stderr(`workit: resources: ${warning}
73785
+ `);
73786
+ if (options.heavy || name !== null && settings.heavy.includes(name)) {
73787
+ const prepared = await prepareHeavy(io, settings, name ?? command.join(" "), runCwd, options.timeoutMs);
73788
+ if (typeof prepared === "number")
73789
+ return prepared;
73790
+ heavy = prepared;
73791
+ }
73792
+ }
73793
+ const lease = heavy?.lease ?? null;
73794
+ let result;
73795
+ try {
73796
+ const scoped = await runScoped((wrapper, sinks) => runCheckCommand(command, {
73797
+ cwd: runCwd,
73798
+ shell: options.shell,
73799
+ timeoutMs: options.timeoutMs,
73800
+ env: heavy?.env ?? io.env,
73801
+ ...sinks,
73802
+ ...wrapper ? { wrapper } : {},
73803
+ ...lease ? { onSpawn: (pid) => updateLease(lease, { pgid: pid }) } : {}
73804
+ }), heavy?.wrapper ?? null, { onStdout: io.json ? io.stderr : io.stdout, onStderr: io.stderr });
73805
+ result = scoped.result;
73806
+ if (heavy && scoped.fallback) {
73807
+ io.stderr(`workit: the memory/CPU scope could not start (${scoped.fallback}); ran without it
73808
+ `);
73809
+ heavy.scope = { unit: null, reason: `systemd-run failed: ${scoped.fallback}` };
73810
+ }
73811
+ } finally {
73812
+ lease?.release();
73813
+ }
73121
73814
  const treeAfter = key.tree === null ? null : worktreeTree(io.cwd)?.key ?? null;
73122
73815
  const modifiedWorktree = key.tree !== null && treeAfter !== key.tree;
73123
73816
  const store = storeRoot(io.cwd);
@@ -73198,6 +73891,8 @@ async function run10(argv, io) {
73198
73891
  outputBytes: result.outputBytes,
73199
73892
  logTruncated: result.truncated,
73200
73893
  task: task.task,
73894
+ lease: heavy?.leaseReport ?? null,
73895
+ scope: heavy?.scope ?? null,
73201
73896
  ...task.note ? { taskNote: task.note } : {},
73202
73897
  satisfies: task.satisfies,
73203
73898
  stillUnsatisfied: task.stillUnsatisfied,
@@ -73227,7 +73922,7 @@ async function run10(argv, io) {
73227
73922
  return recordError.code === "busy" ? 4 : recordError.code === "invalid_input" ? 2 : 5;
73228
73923
  return 0;
73229
73924
  }
73230
- 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(path40.sep).join("/") || ".", ENGINE_CODES2, FINGERPRINT_VARS;
73925
+ var USAGE3 = "workit check <name> [--no-lease] | workit check [--name <n>] [--shell] [--heavy] [--timeout <s>] [--task <id>] [--json] -- <cmd…>", MAX_UNVERIFIED_RUN_MS, RECORD_TAIL_LINES = 20, RECORD_TAIL_CHARS = 200, posix = (value) => value.split(path41.sep).join("/") || ".", ENGINE_CODES2, FINGERPRINT_VARS;
73231
73926
  var init_check = __esm(() => {
73232
73927
  init_check_config();
73233
73928
  init_checks3();
@@ -73236,6 +73931,8 @@ var init_check = __esm(() => {
73236
73931
  init_ledger();
73237
73932
  init_output();
73238
73933
  init_host_session();
73934
+ init_resources();
73935
+ MAX_UNVERIFIED_RUN_MS = 6 * 3600000;
73239
73936
  ENGINE_CODES2 = {
73240
73937
  busy: "busy",
73241
73938
  not_found: "not_found",
@@ -73251,7 +73948,7 @@ var success2 = (data) => ({ ok: true, data }), failure3 = (code, error, unblock)
73251
73948
  // packages/workit-core/src/git/ops.ts
73252
73949
  import { spawnSync as spawnSync18 } from "node:child_process";
73253
73950
  import { existsSync as existsSync19, readFileSync as readFileSync19 } from "node:fs";
73254
- import path41 from "node:path";
73951
+ import path42 from "node:path";
73255
73952
  function operationInProgress(cwd) {
73256
73953
  const markers = [
73257
73954
  ["rebase-merge", "rebase"],
@@ -73264,18 +73961,18 @@ function operationInProgress(cwd) {
73264
73961
  if (!run.ok)
73265
73962
  return null;
73266
73963
  const found = run.stdout.split(`
73267
- `).map((line) => path41.resolve(cwd, line.trim()));
73964
+ `).map((line) => path42.resolve(cwd, line.trim()));
73268
73965
  const conflicted = () => git5(cwd, ["ls-files", "-u"]).stdout.trim() !== "";
73269
73966
  for (const [index, [, operation]] of markers.entries()) {
73270
73967
  const marker = found[index];
73271
73968
  if (!marker || !existsSync19(marker))
73272
73969
  continue;
73273
- if (index === 1 && existsSync19(path41.join(marker, "applying")))
73970
+ if (index === 1 && existsSync19(path42.join(marker, "applying")))
73274
73971
  return { operation: "am", commitAllowed: false, branch: null };
73275
- if (index === 0 && existsSync19(path41.join(marker, "amend")) && !conflicted()) {
73972
+ if (index === 0 && existsSync19(path42.join(marker, "amend")) && !conflicted()) {
73276
73973
  let headName = "";
73277
73974
  try {
73278
- headName = readFileSync19(path41.join(marker, "head-name"), "utf8").trim();
73975
+ headName = readFileSync19(path42.join(marker, "head-name"), "utf8").trim();
73279
73976
  } catch {}
73280
73977
  const branch = headName.startsWith("refs/heads/") ? headName.slice("refs/heads/".length) : null;
73281
73978
  return { operation, commitAllowed: branch !== null, branch };
@@ -73425,6 +74122,40 @@ function lintCommitMessage(cwd, message) {
73425
74122
  correction: `use a ${detected} commit subject`
73426
74123
  };
73427
74124
  }
74125
+ function stripSessionTrailers(text, options) {
74126
+ const end = text.length - (/\s*$/u.exec(text)?.[0].length ?? 0);
74127
+ const lines = text.slice(0, end).split(`
74128
+ `);
74129
+ let start = lines.length;
74130
+ while (start > 0 && lines[start - 1]?.trim() !== "")
74131
+ start -= 1;
74132
+ if (start === 0 && options.subject)
74133
+ return text;
74134
+ const block = lines.slice(start);
74135
+ if (!block.length || !TRAILER_LINE.test(block[0] ?? ""))
74136
+ return text;
74137
+ if (!block.every((line) => TRAILER_LINE.test(line) || /^\s+\S/u.test(line)))
74138
+ return text;
74139
+ const kept = [];
74140
+ let dropping = false;
74141
+ for (const line of block) {
74142
+ if (TRAILER_LINE.test(line))
74143
+ dropping = SESSION_KEY.test(line);
74144
+ if (!dropping)
74145
+ kept.push(line);
74146
+ }
74147
+ if (kept.length === block.length)
74148
+ return text;
74149
+ let head = lines.slice(0, start);
74150
+ if (!kept.length) {
74151
+ let cut = head.length;
74152
+ while (cut > 0 && head[cut - 1]?.trim() === "")
74153
+ cut -= 1;
74154
+ head = head.slice(0, cut);
74155
+ }
74156
+ return [...head, ...kept].join(`
74157
+ `) + text.slice(end);
74158
+ }
73428
74159
  function messageTrailers(cwd, message) {
73429
74160
  const run = git5(cwd, [...TRAILER_CONFIG, "interpret-trailers", "--parse", "--no-divider"], {
73430
74161
  input: `${message}
@@ -73484,6 +74215,12 @@ function gitCommit(cwd, input) {
73484
74215
  const given = input.message?.trim() ?? "";
73485
74216
  if (!given && !input.amend)
73486
74217
  return failure3("invalid_input", "a commit message is required (-m <msg> or -F <file>)");
74218
+ const extra = (input.trailers ?? []).map((trailer) => trailer.trim());
74219
+ const malformed = extra.find((trailer) => !EXTRA_TRAILER.test(trailer));
74220
+ if (malformed !== undefined)
74221
+ return failure3("invalid_input", `--trailer ${JSON.stringify(malformed)} is not one "Key: value" line`, `workit git commit -m '<msg>' --trailer "Co-Authored-By: Name <email>"`);
74222
+ if (extra.some((trailer) => /^workit-session:/iu.test(trailer)))
74223
+ return failure3("invalid_input", "--trailer cannot set Workit-Session: workit writes it from the acting session");
73487
74224
  const paths = input.paths ?? [];
73488
74225
  if (input.all && paths.length)
73489
74226
  return failure3("invalid_input", "pass --all or paths, not both");
@@ -73505,6 +74242,7 @@ function gitCommit(cwd, input) {
73505
74242
  }
73506
74243
  let message = given;
73507
74244
  let carried = [];
74245
+ let previousTrailers = [];
73508
74246
  if (given) {
73509
74247
  const lint = lintCommitMessage(cwd, given);
73510
74248
  if (!lint.ok)
@@ -73514,8 +74252,9 @@ function gitCommit(cwd, input) {
73514
74252
  const kept = git5(cwd, ["log", "-1", "--format=%B", previous]);
73515
74253
  if (!kept.ok)
73516
74254
  return failure3("failed", `could not read the message of ${branch}: ${gitError(kept)}`);
74255
+ previousTrailers = messageTrailers(cwd, kept.stdout.trim());
73517
74256
  if (given)
73518
- carried = messageTrailers(cwd, kept.stdout.trim());
74257
+ carried = previousTrailers;
73519
74258
  else
73520
74259
  message = kept.stdout.trim();
73521
74260
  }
@@ -73529,12 +74268,31 @@ function gitCommit(cwd, input) {
73529
74268
  args.push("--amend");
73530
74269
  if (input.allowEmpty || emptyAmend)
73531
74270
  args.push("--allow-empty");
73532
- args.push("-m", withTrailers(cwd, message, [...carried, ...session ? [`Workit-Session: ${session}`] : []]));
74271
+ const trailersOn = workspaceTrailers(cwd).enabled;
74272
+ const sessionTrailer = Boolean(session) && trailersOn;
74273
+ if (!trailersOn) {
74274
+ carried = carried.filter((trailer) => !SESSION_KEY.test(trailer));
74275
+ if (!given)
74276
+ message = stripSessionTrailers(message, { subject: true });
74277
+ }
74278
+ const finalMessage = withTrailers(cwd, message, [
74279
+ ...carried,
74280
+ ...extra,
74281
+ ...sessionTrailer ? [`Workit-Session: ${session}`] : []
74282
+ ]);
74283
+ const allowed = new Set([
74284
+ ...trailersOn ? sessionValues(previousTrailers) : [],
74285
+ ...sessionTrailer && session ? [session] : []
74286
+ ]);
74287
+ const forged = sessionValues(messageTrailers(cwd, finalMessage)).find((value) => !allowed.has(value));
74288
+ if (forged !== undefined)
74289
+ return failure3("invalid_input", `forged_session_trailer: the message would carry "Workit-Session: ${forged}", which workit did not add; workit writes that trailer itself`, "drop the Workit-Session line (or the trailer.<alias>.key config that expands into it)");
74290
+ args.push("-m", finalMessage);
73533
74291
  if (paths.length) {
73534
74292
  if (paths.some((value) => value.startsWith("-") || !value.trim()))
73535
74293
  return failure3("invalid_input", "paths must not be empty or start with -");
73536
74294
  const tracked = new Set(nulList(git5(cwd, ["ls-files", "-z", "--", ...paths]).stdout));
73537
- const addable = paths.filter((value) => existsSync19(path41.resolve(cwd, value)) || [...tracked].some((file) => file === value || file.startsWith(`${value}/`)));
74295
+ const addable = paths.filter((value) => existsSync19(path42.resolve(cwd, value)) || [...tracked].some((file) => file === value || file.startsWith(`${value}/`)));
73538
74296
  if (addable.length) {
73539
74297
  const added = git5(cwd, ["add", "-A", "--", ...addable]);
73540
74298
  if (!added.ok)
@@ -73560,7 +74318,7 @@ function gitCommit(cwd, input) {
73560
74318
  }
73561
74319
  const committed = git5(cwd, args, {
73562
74320
  timeoutMs: COMMIT_TIMEOUT_MS,
73563
- env: input.env
74321
+ env: { ...input.env ?? process.env, WORKIT_TRAILERS: trailersOn ? "on" : "off" }
73564
74322
  });
73565
74323
  if (!committed.ok)
73566
74324
  return failure3("failed", `git commit failed (a hook or git refused it):
@@ -73604,6 +74362,7 @@ ${gitOutput(committed)}`);
73604
74362
  files,
73605
74363
  leftDirty,
73606
74364
  session,
74365
+ sessionTrailer,
73607
74366
  amended: previous,
73608
74367
  notes,
73609
74368
  recorded: row.ok ? { id: String(row.value.id) } : { error: row.error }
@@ -73967,18 +74726,22 @@ ${run.stdout ?? ""}`).split(/\r?\n/u).map((value) => value.trimEnd()).filter((va
73967
74726
  ...lines.slice(-half)
73968
74727
  ].join(`
73969
74728
  `);
73970
- }, validRefName = (cwd, name) => !name.startsWith("-") && git5(cwd, ["check-ref-format", "--branch", name]).ok, nulList = (text) => text.split("\x00").filter(Boolean), SESSION_SAFE, TRAILER_CONFIG, nothingToCommit = (what) => failure3("failed", `nothing_to_commit: ${what}`, "workit git commit --allow-empty -m '<msg>' # only to record an empty commit on purpose"), SHA, SHA_PREFIX, RELEASE_TAG;
74729
+ }, validRefName = (cwd, name) => !name.startsWith("-") && git5(cwd, ["check-ref-format", "--branch", name]).ok, nulList = (text) => text.split("\x00").filter(Boolean), SESSION_SAFE, EXTRA_TRAILER, SESSION_KEY, TRAILER_LINE, sessionValues = (trailers) => trailers.filter((trailer) => SESSION_KEY.test(trailer)).map((trailer) => trailer.slice(trailer.indexOf(":") + 1).trim()), TRAILER_CONFIG, nothingToCommit = (what) => failure3("failed", `nothing_to_commit: ${what}`, "workit git commit --allow-empty -m '<msg>' # only to record an empty commit on purpose"), SHA, SHA_PREFIX, RELEASE_TAG;
73971
74730
  var init_ops = __esm(() => {
73972
74731
  init_branch();
73973
74732
  init_commit_flavors();
73974
74733
  init_pr_create();
73975
74734
  init_release_tracks();
73976
74735
  init_vcs_config();
74736
+ init_workspaces();
73977
74737
  init_redact();
73978
74738
  init_ledger();
73979
74739
  init_rev();
73980
74740
  COMMIT_TIMEOUT_MS = 10 * 60000;
73981
74741
  SESSION_SAFE = /^[\w.:@/+-]{1,128}$/u;
74742
+ EXTRA_TRAILER = /^[A-Za-z0-9][A-Za-z0-9-]*: *\S[^\n\r]*$/u;
74743
+ SESSION_KEY = /^workit-session\s*:/iu;
74744
+ TRAILER_LINE = /^[A-Za-z0-9][A-Za-z0-9-]*\s*:/u;
73982
74745
  TRAILER_CONFIG = ["-c", "trailer.separators=:", "-c", "trailer.where=end"];
73983
74746
  SHA = /^[0-9a-f]{40,64}$/u;
73984
74747
  SHA_PREFIX = /^[0-9a-f]{7,39}$/u;
@@ -73986,11 +74749,11 @@ var init_ops = __esm(() => {
73986
74749
  });
73987
74750
 
73988
74751
  // packages/workit-core/src/forge/reruns.ts
73989
- import fs20 from "node:fs";
73990
- import path42 from "node:path";
74752
+ import fs21 from "node:fs";
74753
+ import path43 from "node:path";
73991
74754
  function rerunLogPath(cwd) {
73992
74755
  const common = gitCommonDir(cwd);
73993
- return common ? path42.join(common, "workit", "ci-reruns.jsonl") : null;
74756
+ return common ? path43.join(common, "workit", "ci-reruns.jsonl") : null;
73994
74757
  }
73995
74758
  function rerunCounts(cwd, key) {
73996
74759
  const counts = new Map;
@@ -73999,7 +74762,7 @@ function rerunCounts(cwd, key) {
73999
74762
  return counts;
74000
74763
  let text;
74001
74764
  try {
74002
- text = fs20.readFileSync(file, "utf8");
74765
+ text = fs21.readFileSync(file, "utf8");
74003
74766
  } catch {
74004
74767
  return counts;
74005
74768
  }
@@ -74025,8 +74788,8 @@ function recordReruns(cwd, rows) {
74025
74788
  if (!file)
74026
74789
  return false;
74027
74790
  try {
74028
- fs20.mkdirSync(path42.dirname(file), { recursive: true });
74029
- fs20.appendFileSync(file, rows.map((row) => `${JSON.stringify(row)}
74791
+ fs21.mkdirSync(path43.dirname(file), { recursive: true });
74792
+ fs21.appendFileSync(file, rows.map((row) => `${JSON.stringify(row)}
74030
74793
  `).join(""));
74031
74794
  return true;
74032
74795
  } catch {
@@ -74038,18 +74801,18 @@ function withRerunLock(cwd, fn) {
74038
74801
  if (!file)
74039
74802
  return fn();
74040
74803
  const lock = `${file.slice(0, -".jsonl".length)}.lock`;
74041
- fs20.mkdirSync(path42.dirname(lock), { recursive: true });
74804
+ fs21.mkdirSync(path43.dirname(lock), { recursive: true });
74042
74805
  const until = Date.now() + LOCK_WAIT_MS2;
74043
74806
  for (;; ) {
74044
74807
  try {
74045
- fs20.closeSync(fs20.openSync(lock, "wx"));
74808
+ fs21.closeSync(fs21.openSync(lock, "wx"));
74046
74809
  break;
74047
74810
  } catch (error) {
74048
74811
  if (error.code !== "EEXIST")
74049
74812
  throw error;
74050
74813
  try {
74051
- if (Date.now() - fs20.statSync(lock).mtimeMs > LOCK_STALE_MS2) {
74052
- fs20.rmSync(lock, { force: true });
74814
+ if (Date.now() - fs21.statSync(lock).mtimeMs > LOCK_STALE_MS2) {
74815
+ fs21.rmSync(lock, { force: true });
74053
74816
  continue;
74054
74817
  }
74055
74818
  } catch {
@@ -74063,7 +74826,7 @@ function withRerunLock(cwd, fn) {
74063
74826
  try {
74064
74827
  return fn();
74065
74828
  } finally {
74066
- fs20.rmSync(lock, { force: true });
74829
+ fs21.rmSync(lock, { force: true });
74067
74830
  }
74068
74831
  }
74069
74832
  var isRow = (value) => {
@@ -74647,6 +75410,56 @@ function nextHint(next, number) {
74647
75410
  return null;
74648
75411
  }
74649
75412
  }
75413
+ function squashMessageFor(cwd, resolved, input, doc) {
75414
+ if (input.method !== "squash")
75415
+ return null;
75416
+ if (input.subject === undefined && input.body === undefined && workspaceTrailers(cwd).enabled)
75417
+ return null;
75418
+ let subject = input.subject?.trim() ?? "";
75419
+ if (!subject) {
75420
+ const meta = resolved.forge.prMeta(doc.number);
75421
+ if (!meta.ok)
75422
+ return meta;
75423
+ subject = resolved.forge.kind === "github" ? `${meta.data.title} (#${doc.number})` : meta.data.title;
75424
+ }
75425
+ const body = input.body === undefined ? squashCommitList(cwd, resolved, doc.base, doc.head.sha) : stripSessionTrailers(input.body, { subject: false });
75426
+ return success2({ subject, body });
75427
+ }
75428
+ function squashCommitList(cwd, resolved, base, head) {
75429
+ const from = resolveRef(cwd, `refs/remotes/${resolved.baseRemote}/${base}`) ?? resolveRef(cwd, `refs/heads/${base}`);
75430
+ if (!from)
75431
+ return "";
75432
+ const log = spawnSync19("git", ["log", "--reverse", "--no-merges", "--format=%B%x1e", `${from}..${head}`], {
75433
+ cwd,
75434
+ encoding: "utf8",
75435
+ timeout: GIT_TIMEOUTS.local,
75436
+ killSignal: "SIGKILL",
75437
+ maxBuffer: 64 * 1024 * 1024
75438
+ });
75439
+ if (log.status !== 0)
75440
+ return "";
75441
+ const entries = log.stdout.split("\x1E").map((message) => stripSessionTrailers(message.replace(/^\n+/u, ""), { subject: true })).filter((message) => message.trim()).map((message) => {
75442
+ const [subject = "", ...rest] = message.trimEnd().split(`
75443
+ `);
75444
+ const body = rest.join(`
75445
+ `).replace(/^\n+/u, "");
75446
+ return body ? `* ${subject}
75447
+
75448
+ ${body}` : `* ${subject}`;
75449
+ });
75450
+ const listed = [];
75451
+ let size = 0;
75452
+ for (const entry of entries) {
75453
+ if (listed.length >= SQUASH_LIST_MAX_COMMITS || size + entry.length > SQUASH_LIST_MAX_CHARS)
75454
+ break;
75455
+ listed.push(entry);
75456
+ size += entry.length + 2;
75457
+ }
75458
+ const more = entries.length - listed.length;
75459
+ return [...listed, ...more ? [`… and ${more} more commit${more === 1 ? "" : "s"}`] : []].join(`
75460
+
75461
+ `);
75462
+ }
74650
75463
  async function mergePullRequest(cwd, resolved, input, sleep) {
74651
75464
  const grant = requireGrant(cwd, "merge");
74652
75465
  if (!grant.allowed)
@@ -74723,7 +75536,14 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
74723
75536
  return refuse(`head_mismatch: the verdict check reads ${branch} at ${check.head?.slice(0, 12) ?? "(missing)"} but ${label} is at ${head.slice(0, 12)}`, `git fetch && git switch ${branch} && git merge --ff-only @{u} # then re-verify that head`, { reason: "head_mismatch", verdict: summary });
74724
75537
  else if (!covered)
74725
75538
  return refuse(`NEEDS_VERDICT: ${label} has no accepted independent verdict for ${head.slice(0, 12)} (${check.accepted.reasons.join(", ")})`, `an independent session verifies the head and runs: workit ledger verdict verified --how "<what was exercised>" --branch ${branch}${grant.allowUnverified ? `; or, only if the user asks to merge unverified: workit pr merge --unverified --reason "<why>"` : ""}`, { reason: "needs_verdict", verdict: summary });
74726
- const merged = resolved.forge.merge(doc.number, { sha: head, method: input.method });
75539
+ const squashMessage = squashMessageFor(cwd, resolved, input, doc);
75540
+ if (squashMessage && !squashMessage.ok)
75541
+ return squashMessage;
75542
+ const merged = resolved.forge.merge(doc.number, {
75543
+ sha: head,
75544
+ method: input.method,
75545
+ ...squashMessage ? { squashMessage: squashMessage.data } : {}
75546
+ });
74727
75547
  if (!merged.ok)
74728
75548
  return merged;
74729
75549
  let state = "open";
@@ -74813,11 +75633,12 @@ var VERIFY_DELAYS_MS, noun = (resolved) => resolved.forge.kind === "github" ? "p
74813
75633
  error,
74814
75634
  unblock,
74815
75635
  refusal
74816
- });
75636
+ }), SQUASH_LIST_MAX_COMMITS = 100, SQUASH_LIST_MAX_CHARS = 60000;
74817
75637
  var init_pr_ops = __esm(() => {
74818
75638
  init_autonomy();
74819
75639
  init_branch();
74820
75640
  init_vcs_config();
75641
+ init_workspaces();
74821
75642
  init_release_tracks();
74822
75643
  init_ops();
74823
75644
  init_rev();
@@ -74836,8 +75657,8 @@ var init_pr_ops = __esm(() => {
74836
75657
  // packages/workit-core/src/stack.ts
74837
75658
  import { spawnSync as spawnSync20 } from "node:child_process";
74838
75659
  import { createHash as createHash17, randomBytes as randomBytes3 } from "node:crypto";
74839
- import fs21 from "node:fs";
74840
- import path43 from "node:path";
75660
+ import fs22 from "node:fs";
75661
+ import path44 from "node:path";
74841
75662
  function worktreeOf(cwd, branch) {
74842
75663
  const list = gitRun3(cwd, ["worktree", "list", "--porcelain"]);
74843
75664
  if (!list.ok)
@@ -74860,7 +75681,7 @@ function rebasingBranch2(dir) {
74860
75681
  if (!found.ok)
74861
75682
  continue;
74862
75683
  try {
74863
- return fs21.readFileSync(found.stdout.trim(), "utf8").trim();
75684
+ return fs22.readFileSync(found.stdout.trim(), "utf8").trim();
74864
75685
  } catch {}
74865
75686
  }
74866
75687
  return null;
@@ -74868,7 +75689,7 @@ function rebasingBranch2(dir) {
74868
75689
  function rebaseInProgress(dir) {
74869
75690
  for (const name of ["rebase-merge", "rebase-apply"]) {
74870
75691
  const found = gitRun3(dir, ["rev-parse", "--path-format=absolute", "--git-path", name]);
74871
- if (found.ok && fs21.existsSync(found.stdout.trim()))
75692
+ if (found.ok && fs22.existsSync(found.stdout.trim()))
74872
75693
  return true;
74873
75694
  }
74874
75695
  return false;
@@ -74880,8 +75701,8 @@ function gitRestack(cwd, input) {
74880
75701
  const scratch = scratchDir(cwd);
74881
75702
  if (!scratch)
74882
75703
  return { ok: false, conflict: false, worktree: cwd, error: "no store root" };
74883
- fs21.mkdirSync(scratch, { recursive: true });
74884
- dir = path43.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes3(4).toString("hex")}`);
75704
+ fs22.mkdirSync(scratch, { recursive: true });
75705
+ dir = path44.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes3(4).toString("hex")}`);
74885
75706
  const added = gitRun3(cwd, ["worktree", "add", "-q", dir, input.branch], GIT_TIMEOUTS.worktree);
74886
75707
  if (!added.ok)
74887
75708
  return { ok: false, conflict: false, worktree: cwd, error: firstLine(added) };
@@ -74919,10 +75740,10 @@ function gitRestack(cwd, input) {
74919
75740
  }
74920
75741
  function pruneScratch(cwd) {
74921
75742
  const scratch = scratchDir(cwd);
74922
- if (!scratch || !fs21.existsSync(scratch))
75743
+ if (!scratch || !fs22.existsSync(scratch))
74923
75744
  return;
74924
- for (const name of fs21.readdirSync(scratch)) {
74925
- const dir = path43.join(scratch, name);
75745
+ for (const name of fs22.readdirSync(scratch)) {
75746
+ const dir = path44.join(scratch, name);
74926
75747
  if (!rebaseInProgress(dir) && !trackedDirt(dir))
74927
75748
  gitRun3(cwd, ["worktree", "remove", "--force", dir]);
74928
75749
  }
@@ -74932,10 +75753,10 @@ function stacksDir(cwd) {
74932
75753
  const root = storeRoot(cwd);
74933
75754
  if (!root.ok)
74934
75755
  return stackFail(root.code, root.error, root.unblock);
74935
- return { ok: true, data: path43.join(root.value.root, "stacks") };
75756
+ return { ok: true, data: path44.join(root.value.root, "stacks") };
74936
75757
  }
74937
75758
  function parseStack(raw) {
74938
- if (!isRecord7(raw) || !Array.isArray(raw.branches))
75759
+ if (!isRecord8(raw) || !Array.isArray(raw.branches))
74939
75760
  return null;
74940
75761
  const name = str3(raw.name);
74941
75762
  const trunk = str3(raw.trunk);
@@ -74943,12 +75764,12 @@ function parseStack(raw) {
74943
75764
  return null;
74944
75765
  const branches = [];
74945
75766
  for (const item of raw.branches) {
74946
- if (!isRecord7(item))
75767
+ if (!isRecord8(item))
74947
75768
  return null;
74948
75769
  const branch = str3(item.branch);
74949
75770
  if (!branch)
74950
75771
  return null;
74951
- const merged = isRecord7(item.merged) ? {
75772
+ const merged = isRecord8(item.merged) ? {
74952
75773
  pr: typeof item.merged.pr === "number" ? item.merged.pr : null,
74953
75774
  mergeSha: str3(item.merged.mergeSha),
74954
75775
  at: str3(item.merged.at) ?? ""
@@ -74961,7 +75782,7 @@ function parseStack(raw) {
74961
75782
  lastParentHead: str3(item.lastParentHead),
74962
75783
  patchId: str3(item.patchId),
74963
75784
  merged,
74964
- ...isRecord7(item.pendingRestack) && str3(item.pendingRestack.from) && str3(item.pendingRestack.tip) && str3(item.pendingRestack.onto) ? {
75785
+ ...isRecord8(item.pendingRestack) && str3(item.pendingRestack.from) && str3(item.pendingRestack.tip) && str3(item.pendingRestack.onto) ? {
74965
75786
  pendingRestack: {
74966
75787
  from: str3(item.pendingRestack.from),
74967
75788
  tip: str3(item.pendingRestack.tip),
@@ -74984,10 +75805,10 @@ function readStack(cwd, name) {
74984
75805
  const dir = stacksDir(cwd);
74985
75806
  if (!dir.ok)
74986
75807
  return dir;
74987
- const file = path43.join(dir.data, fileFor(name));
75808
+ const file = path44.join(dir.data, fileFor(name));
74988
75809
  let text;
74989
75810
  try {
74990
- text = fs21.readFileSync(file, "utf8");
75811
+ text = fs22.readFileSync(file, "utf8");
74991
75812
  } catch (error) {
74992
75813
  if (error.code === "ENOENT")
74993
75814
  return { ok: true, data: null };
@@ -75005,17 +75826,17 @@ function writeStack(cwd, stack, now = new Date) {
75005
75826
  const dir = stacksDir(cwd);
75006
75827
  if (!dir.ok)
75007
75828
  return dir;
75008
- const file = path43.join(dir.data, fileFor(stack.name));
75829
+ const file = path44.join(dir.data, fileFor(stack.name));
75009
75830
  const temp = `${file}.${process.pid}.tmp`;
75010
75831
  try {
75011
- fs21.mkdirSync(dir.data, { recursive: true });
75832
+ fs22.mkdirSync(dir.data, { recursive: true });
75012
75833
  stack.updatedAt = now.toISOString();
75013
- fs21.writeFileSync(temp, `${JSON.stringify(stack, null, 2)}
75834
+ fs22.writeFileSync(temp, `${JSON.stringify(stack, null, 2)}
75014
75835
  `);
75015
- fs21.renameSync(temp, file);
75836
+ fs22.renameSync(temp, file);
75016
75837
  return { ok: true, data: undefined };
75017
75838
  } catch (error) {
75018
- fs21.rmSync(temp, { force: true });
75839
+ fs22.rmSync(temp, { force: true });
75019
75840
  return stackFail("unavailable", `cannot write ${file}: ${error.message}`);
75020
75841
  }
75021
75842
  }
@@ -75025,7 +75846,7 @@ function listStacks(cwd) {
75025
75846
  return dir;
75026
75847
  let names = [];
75027
75848
  try {
75028
- names = fs21.readdirSync(dir.data).filter((name) => name.endsWith(".json"));
75849
+ names = fs22.readdirSync(dir.data).filter((name) => name.endsWith(".json"));
75029
75850
  } catch {
75030
75851
  return { ok: true, data: [] };
75031
75852
  }
@@ -75033,7 +75854,7 @@ function listStacks(cwd) {
75033
75854
  for (const name of names) {
75034
75855
  let parsed = null;
75035
75856
  try {
75036
- parsed = parseStack(JSON.parse(fs21.readFileSync(path43.join(dir.data, name), "utf8")));
75857
+ parsed = parseStack(JSON.parse(fs22.readFileSync(path44.join(dir.data, name), "utf8")));
75037
75858
  } catch {
75038
75859
  parsed = null;
75039
75860
  }
@@ -75080,7 +75901,7 @@ function stackLockStale(raw, age) {
75080
75901
  }
75081
75902
  function takeLock(lock) {
75082
75903
  try {
75083
- fs21.mkdirSync(lock);
75904
+ fs22.mkdirSync(lock);
75084
75905
  } catch (error) {
75085
75906
  if (error.code === "EEXIST")
75086
75907
  return null;
@@ -75092,24 +75913,24 @@ function takeLock(lock) {
75092
75913
  host: localLockHost(),
75093
75914
  nonce: randomBytes3(8).toString("hex")
75094
75915
  };
75095
- fs21.writeFileSync(path43.join(lock, OWNER), JSON.stringify(owner));
75916
+ fs22.writeFileSync(path44.join(lock, OWNER), JSON.stringify(owner));
75096
75917
  return owner;
75097
75918
  }
75098
75919
  function reapLock(lock, observed) {
75099
75920
  const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
75100
75921
  try {
75101
- fs21.renameSync(lock, tomb);
75922
+ fs22.renameSync(lock, tomb);
75102
75923
  } catch {
75103
75924
  return false;
75104
75925
  }
75105
75926
  if (readOwner(tomb).raw === observed) {
75106
- fs21.rmSync(tomb, { recursive: true, force: true });
75927
+ fs22.rmSync(tomb, { recursive: true, force: true });
75107
75928
  return true;
75108
75929
  }
75109
75930
  try {
75110
- fs21.renameSync(tomb, lock);
75931
+ fs22.renameSync(tomb, lock);
75111
75932
  } catch {
75112
- fs21.rmSync(tomb, { recursive: true, force: true });
75933
+ fs22.rmSync(tomb, { recursive: true, force: true });
75113
75934
  }
75114
75935
  return false;
75115
75936
  }
@@ -75119,8 +75940,8 @@ function releaseLock(lock, owner) {
75119
75940
  return;
75120
75941
  const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
75121
75942
  try {
75122
- fs21.renameSync(lock, tomb);
75123
- fs21.rmSync(tomb, { recursive: true, force: true });
75943
+ fs22.renameSync(lock, tomb);
75944
+ fs22.rmSync(tomb, { recursive: true, force: true });
75124
75945
  } catch {}
75125
75946
  }
75126
75947
  async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
@@ -75129,9 +75950,9 @@ async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
75129
75950
  return dir;
75130
75951
  if (!validName(name))
75131
75952
  return stackFail("invalid_input", "a stack name must be 1-200 printable characters");
75132
- const lock = path43.join(dir.data, `${fileFor(name)}.lock`);
75953
+ const lock = path44.join(dir.data, `${fileFor(name)}.lock`);
75133
75954
  try {
75134
- fs21.mkdirSync(dir.data, { recursive: true });
75955
+ fs22.mkdirSync(dir.data, { recursive: true });
75135
75956
  } catch (error) {
75136
75957
  return stackFail("unavailable", `cannot create ${dir.data}: ${error.message}`);
75137
75958
  }
@@ -75159,7 +75980,7 @@ async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
75159
75980
  const touch = () => {
75160
75981
  try {
75161
75982
  const now = new Date;
75162
- fs21.utimesSync(path43.join(lock, OWNER), now, now);
75983
+ fs22.utimesSync(path44.join(lock, OWNER), now, now);
75163
75984
  } catch {}
75164
75985
  };
75165
75986
  const beat = setInterval(touch, HEARTBEAT_MS);
@@ -75192,7 +76013,7 @@ function deriveChain(cwd, trunk, base) {
75192
76013
  const [name, sha] = line.trim().split(" ");
75193
76014
  if (!name || !sha || name === branch || name === trunk)
75194
76015
  continue;
75195
- if (sha === head || isAncestor(cwd, sha, base) || !isAncestor(cwd, sha, head))
76016
+ if (sha === head || isAncestor2(cwd, sha, base) || !isAncestor2(cwd, sha, head))
75196
76017
  continue;
75197
76018
  const count = gitRun3(cwd, ["rev-list", "--count", `${base}..${sha}`]);
75198
76019
  below.push({ branch: name, depth: Number(count.stdout.trim()) || 0 });
@@ -75238,17 +76059,17 @@ function planStack(cwd, resolved, input, existing) {
75238
76059
  const status = resolved.forge.prStatus(before.pr);
75239
76060
  still = status.ok && status.data.state === "merged";
75240
76061
  } else
75241
- still = hasOwnCommits(cwd, tip, before.lastParentHead) && isAncestor(cwd, tip, trunkSha);
76062
+ still = hasOwnCommits(cwd, tip, before.lastParentHead) && isAncestor2(cwd, tip, trunkSha);
75242
76063
  if (still) {
75243
76064
  entries.push(before);
75244
76065
  continue;
75245
76066
  }
75246
76067
  notes.push(`${branch} was recorded as merged but is not; it is back in the stack`);
75247
76068
  }
75248
- const onParent = isAncestor(cwd, parentSha, tip);
76069
+ const onParent = isAncestor2(cwd, parentSha, tip);
75249
76070
  let builtOn = parentSha;
75250
76071
  if (!onParent) {
75251
- if (before?.lastParentHead && isAncestor(cwd, before.lastParentHead, tip))
76072
+ if (before?.lastParentHead && isAncestor2(cwd, before.lastParentHead, tip))
75252
76073
  builtOn = before.lastParentHead;
75253
76074
  else {
75254
76075
  const old = oldParentVersion(cwd, parent, parentSha, tip) ?? oldParentLookalike(cwd, parentSha, tip);
@@ -75323,7 +76144,7 @@ function planStack(cwd, resolved, input, existing) {
75323
76144
  function hasOwnCommits(cwd, tip, base) {
75324
76145
  if (!base)
75325
76146
  return false;
75326
- return tip !== base && !isAncestor(cwd, tip, base);
76147
+ return tip !== base && !isAncestor2(cwd, tip, base);
75327
76148
  }
75328
76149
  function oldParentVersion(cwd, parent, parentSha, child) {
75329
76150
  if (!safeRef2(parent))
@@ -75335,7 +76156,7 @@ function oldParentVersion(cwd, parent, parentSha, child) {
75335
76156
  `).map((line) => line.trim())) {
75336
76157
  if (!sha || sha === parentSha)
75337
76158
  continue;
75338
- if (isAncestor(cwd, sha, child) && !isAncestor(cwd, sha, parentSha))
76159
+ if (isAncestor2(cwd, sha, child) && !isAncestor2(cwd, sha, parentSha))
75339
76160
  return sha;
75340
76161
  }
75341
76162
  return null;
@@ -75469,7 +76290,7 @@ function stackStatus(ctx, stack) {
75469
76290
  pr: entry.pr,
75470
76291
  merged,
75471
76292
  head,
75472
- onParent: merged || !head || !parentSha ? null : isAncestor(ctx.cwd, parentSha, head),
76293
+ onParent: merged || !head || !parentSha ? null : isAncestor2(ctx.cwd, parentSha, head),
75473
76294
  prState: doc?.state ?? null,
75474
76295
  prBase: doc?.base ?? null,
75475
76296
  baseMatches: doc && !merged ? doc.base === parentName : null,
@@ -75532,7 +76353,7 @@ function detectMerged(ctx, cwd, stack, trunkSha, state) {
75532
76353
  merged = read.data.state === "merged";
75533
76354
  } else {
75534
76355
  const tip = branchTip(cwd, entry.branch);
75535
- merged = tip !== null && hasOwnCommits(cwd, tip, entry.lastParentHead) && isAncestor(cwd, tip, trunkSha);
76356
+ merged = tip !== null && hasOwnCommits(cwd, tip, entry.lastParentHead) && isAncestor2(cwd, tip, trunkSha);
75536
76357
  }
75537
76358
  if (merged) {
75538
76359
  entry.merged = { pr: entry.pr, mergeSha: null, at: new Date().toISOString() };
@@ -75553,7 +76374,7 @@ function pushBranch(cwd, actor, branch, priorTip) {
75553
76374
  const grant = requireGrant(cwd, "push", { forge: !plan.data.local });
75554
76375
  if (!grant.allowed)
75555
76376
  return stackFail("blocked", grant.error, grant.unblock, { reason: grant.reason });
75556
- const force = remote.sha !== null && !isAncestor(cwd, remote.sha, plan.data.sha);
76377
+ const force = remote.sha !== null && !isAncestor2(cwd, remote.sha, plan.data.sha);
75557
76378
  const expect = !force ? null : remote.sha === priorTip ? priorTip : recordedRemoteTip(cwd, plan.data) ?? null;
75558
76379
  const pushed = executePush(cwd, plan.data, { forceWithLease: force, expect, actor });
75559
76380
  if (!pushed.ok)
@@ -75630,9 +76451,9 @@ function syncStack(cwd, ctx, stack, options, actor) {
75630
76451
  carried: null
75631
76452
  };
75632
76453
  newTip = null;
75633
- } else if (!isAncestor(cwd, parentTip, tip)) {
76454
+ } else if (!isAncestor2(cwd, parentTip, tip)) {
75634
76455
  const pending = entry.pendingRestack ?? null;
75635
- const from = pending && isAncestor(cwd, pending.onto, tip) ? pending.onto : entry.lastParentHead && isAncestor(cwd, entry.lastParentHead, tip) ? entry.lastParentHead : mergeBase(cwd, entry.lastParentHead ?? parentTip, tip) ?? parentTip;
76456
+ const from = pending && isAncestor2(cwd, pending.onto, tip) ? pending.onto : entry.lastParentHead && isAncestor2(cwd, entry.lastParentHead, tip) ? entry.lastParentHead : mergeBase(cwd, entry.lastParentHead ?? parentTip, tip) ?? parentTip;
75636
76457
  if (options.dryRun) {
75637
76458
  step.restack = { from, onto: parentTip, head: null, carried: null };
75638
76459
  newTip = null;
@@ -75680,7 +76501,7 @@ function syncStack(cwd, ctx, stack, options, actor) {
75680
76501
  }
75681
76502
  }
75682
76503
  if (!options.dryRun && newTip) {
75683
- const check = entry.pendingRestack && parentTip && isAncestor(cwd, parentTip, newTip) ? entry.pendingRestack : null;
76504
+ const check = entry.pendingRestack && parentTip && isAncestor2(cwd, parentTip, newTip) ? entry.pendingRestack : null;
75684
76505
  let forced = false;
75685
76506
  if (check && parentTip) {
75686
76507
  const before = {
@@ -75994,12 +76815,12 @@ var STACK_VERSION = 1, stackFail = (code, error, unblock, data) => ({
75994
76815
  stderr: run.error ? String(run.error.message) : run.stderr ?? "",
75995
76816
  timedOut: failure?.code === "ETIMEDOUT"
75996
76817
  };
75997
- }, safeRef2 = (value) => value.length > 0 && !value.startsWith("-"), isAncestor = (cwd, ancestor, descendant) => safeRef2(ancestor) && safeRef2(descendant) && gitRun3(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok, branchTip = (cwd, branch) => resolveRef(cwd, `refs/heads/${branch}`), trackedDirt = (dir) => {
76818
+ }, safeRef2 = (value) => value.length > 0 && !value.startsWith("-"), isAncestor2 = (cwd, ancestor, descendant) => safeRef2(ancestor) && safeRef2(descendant) && gitRun3(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok, branchTip = (cwd, branch) => resolveRef(cwd, `refs/heads/${branch}`), trackedDirt = (dir) => {
75998
76819
  const status = gitRun3(dir, ["status", "--porcelain", "--untracked-files=no"]);
75999
76820
  return !status.ok || status.stdout.trim() !== "";
76000
76821
  }, scratchDir = (cwd) => {
76001
76822
  const dir = stacksDir(cwd);
76002
- return dir.ok ? path43.join(dir.data, "worktrees") : null;
76823
+ return dir.ok ? path44.join(dir.data, "worktrees") : null;
76003
76824
  }, firstLine = (run) => (`${run.stderr}
76004
76825
  ${run.stdout}`.split(`
76005
76826
  `).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()}-${createHash17("sha256").update(name).digest("hex").slice(0, 12)}.json`, fileFor, validName = (name) => {
@@ -76011,17 +76832,17 @@ ${run.stdout}`.split(`
76011
76832
  return false;
76012
76833
  }
76013
76834
  return true;
76014
- }, 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) => {
76835
+ }, isRecord8 = (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) => {
76015
76836
  let raw = null;
76016
76837
  try {
76017
- raw = fs21.readFileSync(path43.join(lock, OWNER), "utf8");
76838
+ raw = fs22.readFileSync(path44.join(lock, OWNER), "utf8");
76018
76839
  } catch {
76019
76840
  raw = null;
76020
76841
  }
76021
76842
  let age = null;
76022
- for (const target of [path43.join(lock, OWNER), lock])
76843
+ for (const target of [path44.join(lock, OWNER), lock])
76023
76844
  try {
76024
- age = Date.now() - fs21.statSync(target).mtimeMs;
76845
+ age = Date.now() - fs22.statSync(target).mtimeMs;
76025
76846
  break;
76026
76847
  } catch {}
76027
76848
  return { raw, age };
@@ -76238,16 +77059,16 @@ var init_pr_edit = __esm(() => {
76238
77059
 
76239
77060
  // packages/workit-core/src/forge/exec.ts
76240
77061
  import { spawnSync as spawnSync22 } from "node:child_process";
76241
- import fs22 from "node:fs";
76242
- import path44 from "node:path";
77062
+ import fs23 from "node:fs";
77063
+ import path45 from "node:path";
76243
77064
  function findExecutable(bin, env) {
76244
- const dirs = (env.PATH ?? env.Path ?? "").split(path44.delimiter).filter(Boolean);
77065
+ const dirs = (env.PATH ?? env.Path ?? "").split(path45.delimiter).filter(Boolean);
76245
77066
  const exts = process.platform === "win32" ? [".exe"] : [""];
76246
77067
  for (const dir of dirs)
76247
77068
  for (const ext of exts) {
76248
- const candidate = path44.join(dir, `${bin}${ext}`);
77069
+ const candidate = path45.join(dir, `${bin}${ext}`);
76249
77070
  try {
76250
- const stat = fs22.statSync(candidate);
77071
+ const stat = fs23.statSync(candidate);
76251
77072
  if (stat.isFile())
76252
77073
  return candidate;
76253
77074
  } catch {}
@@ -76279,7 +77100,7 @@ function systemRunner(kind, apiHost, env = process.env) {
76279
77100
  timeout: options.timeoutMs,
76280
77101
  killSignal: "SIGKILL",
76281
77102
  windowsHide: true,
76282
- stdio: ["ignore", "pipe", "pipe"]
77103
+ ...options.input === undefined ? { stdio: ["ignore", "pipe", "pipe"] } : { input: options.input, stdio: ["pipe", "pipe", "pipe"] }
76283
77104
  });
76284
77105
  const error = result.error;
76285
77106
  return {
@@ -76355,8 +77176,8 @@ function apiText(runner, bin, apiHost, args, what, timeoutMs = FORGE_TIMEOUTS.lo
76355
77176
  return cliFailure(bin, apiHost, run, what, timeoutMs);
76356
77177
  return { ok: true, data: run.stdout };
76357
77178
  }
76358
- function apiWrite(runner, bin, apiHost, args, what, timeoutMs = FORGE_TIMEOUTS.api) {
76359
- const run = runner(bin, args, { timeoutMs });
77179
+ function apiWrite(runner, bin, apiHost, args, what, timeoutMs = FORGE_TIMEOUTS.api, input) {
77180
+ const run = runner(bin, args, input === undefined ? { timeoutMs } : { timeoutMs, input });
76360
77181
  if (run.status !== 0) {
76361
77182
  const text = redactText(`${run.stderr}
76362
77183
  ${run.stdout}`).trim();
@@ -76658,16 +77479,19 @@ function createGitHubForge(options) {
76658
77479
  });
76659
77480
  },
76660
77481
  merge(number, mergeOptions) {
77482
+ const squash = mergeOptions.method === "squash" ? mergeOptions.squashMessage : undefined;
76661
77483
  const merged = apiWrite(runner, "gh", apiHost, [
76662
77484
  "api",
76663
77485
  "-X",
76664
77486
  "PUT",
76665
77487
  `repos/${repo}/pulls/${number}/merge`,
76666
- "-f",
76667
- `sha=${mergeOptions.sha}`,
76668
- "-f",
76669
- `merge_method=${mergeOptions.method}`
76670
- ], `merge of pull request #${number}`);
77488
+ ...squash ? ["--input", "-"] : ["-f", `sha=${mergeOptions.sha}`, "-f", `merge_method=${mergeOptions.method}`]
77489
+ ], `merge of pull request #${number}`, undefined, squash ? JSON.stringify({
77490
+ sha: mergeOptions.sha,
77491
+ merge_method: mergeOptions.method,
77492
+ commit_title: squash.subject,
77493
+ commit_message: squash.body
77494
+ }) : undefined);
76671
77495
  if (!merged.ok)
76672
77496
  return merged;
76673
77497
  if (merged.data.merged === false)
@@ -77025,6 +77849,7 @@ function createGitLabForge(options) {
77025
77849
  });
77026
77850
  },
77027
77851
  merge(iid, mergeOptions) {
77852
+ const squash = mergeOptions.method === "squash" ? mergeOptions.squashMessage : undefined;
77028
77853
  if (mergeOptions.method === "rebase")
77029
77854
  return failure3("invalid_input", "GitLab merges with the project's merge method; pass --method squash or merge");
77030
77855
  const merged = apiWrite(runner, "glab", apiHost, [
@@ -77032,11 +77857,19 @@ function createGitLabForge(options) {
77032
77857
  "-X",
77033
77858
  "PUT",
77034
77859
  `${project}/merge_requests/${iid}/merge`,
77035
- "-f",
77036
- `sha=${mergeOptions.sha}`,
77037
- "-F",
77038
- `squash=${mergeOptions.method === "squash" ? "true" : "false"}`
77039
- ], `merge of merge request !${iid}`);
77860
+ ...squash ? ["--input", "-"] : [
77861
+ "-f",
77862
+ `sha=${mergeOptions.sha}`,
77863
+ "-F",
77864
+ `squash=${mergeOptions.method === "squash" ? "true" : "false"}`
77865
+ ]
77866
+ ], `merge of merge request !${iid}`, undefined, squash ? JSON.stringify({
77867
+ sha: mergeOptions.sha,
77868
+ squash: true,
77869
+ squash_commit_message: squash.body ? `${squash.subject}
77870
+
77871
+ ${squash.body}` : squash.subject
77872
+ }) : undefined);
77040
77873
  if (!merged.ok)
77041
77874
  return merged;
77042
77875
  const value = merged.data;
@@ -77181,7 +78014,7 @@ var init_gitlab = __esm(() => {
77181
78014
  });
77182
78015
 
77183
78016
  // packages/workit-core/src/forge/resolve.ts
77184
- import fs23 from "node:fs";
78017
+ import fs24 from "node:fs";
77185
78018
  function memo4(scope, key, compute, now = Date.now) {
77186
78019
  let entries = memoScopes.get(scope.owner);
77187
78020
  if (!entries)
@@ -77226,7 +78059,7 @@ function resolveCredential(workspace, derived, run, scope) {
77226
78059
  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`);
77227
78060
  let token = "";
77228
78061
  try {
77229
- token = fs23.readFileSync(tokenFile.trim(), "utf8").trim();
78062
+ token = fs24.readFileSync(tokenFile.trim(), "utf8").trim();
77230
78063
  } catch {
77231
78064
  return failure3("blocked", `workspace ${workspace?.name ?? "?"} vcs.tokenFile cannot be read`, "fix vcs.tokenFile in ~/.config/workit/workspaces.json");
77232
78065
  }
@@ -77424,16 +78257,16 @@ function parseDuration2(value) {
77424
78257
  return null;
77425
78258
  let total = 0;
77426
78259
  for (const [, amount, unit] of parts) {
77427
- const factor = UNITS.find(([pattern]) => pattern.test(unit))?.[1];
78260
+ const factor = UNITS2.find(([pattern]) => pattern.test(unit))?.[1];
77428
78261
  if (factor === undefined)
77429
78262
  return null;
77430
78263
  total += Number(amount) * factor;
77431
78264
  }
77432
78265
  return Math.round(total);
77433
78266
  }
77434
- var UNITS;
78267
+ var UNITS2;
77435
78268
  var init_duration = __esm(() => {
77436
- UNITS = [
78269
+ UNITS2 = [
77437
78270
  [/^(?:ms|milliseconds?)$/u, 1],
77438
78271
  [/^(?:s|secs?|seconds?)$/u, 1000],
77439
78272
  [/^(?:m|mins?|minutes?)$/u, 60000],
@@ -77598,11 +78431,11 @@ __export(exports_pr, {
77598
78431
  run: () => run11
77599
78432
  });
77600
78433
  import { readFileSync as readFileSync21 } from "node:fs";
77601
- import path45 from "node:path";
78434
+ import path46 from "node:path";
77602
78435
  function readBodyFile(io, file) {
77603
78436
  try {
77604
78437
  return {
77605
- body: file === "-" ? forgeDeps.readStdin() : readFileSync21(path45.resolve(io.cwd, file), "utf8")
78438
+ body: file === "-" ? forgeDeps.readStdin() : readFileSync21(path46.resolve(io.cwd, file), "utf8")
77606
78439
  };
77607
78440
  } catch {
77608
78441
  return `cannot read --body-file ${file}`;
@@ -77780,7 +78613,10 @@ async function merge3(argv, io) {
77780
78613
  method: "value",
77781
78614
  "delete-branch": "boolean",
77782
78615
  unverified: "boolean",
77783
- reason: "value"
78616
+ reason: "value",
78617
+ subject: "value",
78618
+ body: "value",
78619
+ "body-file": "value"
77784
78620
  });
77785
78621
  if (typeof flags === "string")
77786
78622
  return usage3(io, flags, MERGE_USAGE);
@@ -77795,6 +78631,21 @@ async function merge3(argv, io) {
77795
78631
  const method = flags.values.method ?? "squash";
77796
78632
  if (!METHODS2.has(method))
77797
78633
  return usage3(io, "--method must be squash, merge or rebase", MERGE_USAGE);
78634
+ const { subject } = flags.values;
78635
+ if (flags.values.body !== undefined && flags.values["body-file"] !== undefined)
78636
+ return usage3(io, "pass --body or --body-file, not both", MERGE_USAGE);
78637
+ let body = flags.values.body;
78638
+ if (flags.values["body-file"] !== undefined) {
78639
+ const read = readBodyFile(io, flags.values["body-file"]);
78640
+ if (typeof read === "string")
78641
+ return usage3(io, read, MERGE_USAGE);
78642
+ body = read.body;
78643
+ }
78644
+ if ((subject !== undefined || body !== undefined) && method !== "squash")
78645
+ return usage3(io, "--subject and --body set a squash message; use --method squash", MERGE_USAGE);
78646
+ if (subject !== undefined && (!subject.trim() || subject.includes(`
78647
+ `)))
78648
+ return usage3(io, "--subject must be one non-empty line", MERGE_USAGE);
77798
78649
  const connected = connect(io);
77799
78650
  if (!connected.ok)
77800
78651
  return forgeFail(io, connected);
@@ -77803,7 +78654,9 @@ async function merge3(argv, io) {
77803
78654
  method,
77804
78655
  deleteBranch: flags.booleans.has("delete-branch"),
77805
78656
  actor: actorFromEnv(io.env),
77806
- ...unverified ? { unverified } : {}
78657
+ ...unverified ? { unverified } : {},
78658
+ ...subject !== undefined ? { subject } : {},
78659
+ ...body !== undefined ? { body } : {}
77807
78660
  }, forgeDeps.sleep);
77808
78661
  if (!result.ok)
77809
78662
  return emit(io, fail(result.code, result.error, {
@@ -77967,7 +78820,7 @@ async function run11(argv, io) {
77967
78820
  return reply(rest, io);
77968
78821
  return usage3(io, sub && !sub.startsWith("-") ? `unknown pr subcommand "${sub}"` : "missing pr subcommand", USAGE4);
77969
78822
  }
77970
- var STATUS_USAGE = "workit pr status [--pr <n> | --branch <b>] [--log-lines 60] [--json]", CREATE_USAGE = "workit pr create [--base <b> | --track <t>] (--title <t> [--body <text> | --body-file <f|->] | --fill) [--label <l>]… [--reviewer <login>]… [--draft] [--json]", MERGE_USAGE = "workit pr merge [--pr <n>] [--method squash|merge|rebase] [--delete-branch] [--unverified --reason <why>] [--json]", READY_USAGE = "workit pr ready [--pr <n>] [--undo] [--json]", EDIT_USAGE = "workit pr edit [--pr <n>] [--title <t>] [--body-file <f|->] [--add-label <l>]… [--remove-label <l>]… [--add-reviewer <login>]… [--base <b>] [--json]", THREADS_USAGE = "workit pr threads [--pr <n>] [--json]", REPLY_USAGE = "workit pr reply [--pr <n>] --thread <id> [--body-file <f|->] [--resolve] [--json]", USAGE4 = "workit pr status|create|merge|ready|edit|threads|reply ... (workit help pr)", prLabel = (kind, number) => `${kind === "github" ? "PR #" : "MR !"}${number}`, mergedVerdictLine = (landed) => landed === "untracked" ? "merged outside workit (no merge record)" : landed.unverified ? `merged unverified at ${(landed.head ?? "?").slice(0, 12)} (recorded bypass)` : `merged with accepted verdict ${landed.verdictId ?? "?"} at ${(landed.head ?? "?").slice(0, 12)}`, METHODS2;
78823
+ var STATUS_USAGE = "workit pr status [--pr <n> | --branch <b>] [--log-lines 60] [--json]", CREATE_USAGE = "workit pr create [--base <b> | --track <t>] (--title <t> [--body <text> | --body-file <f|->] | --fill) [--label <l>]… [--reviewer <login>]… [--draft] [--json]", MERGE_USAGE = "workit pr merge [--pr <n>] [--method squash|merge|rebase] [--subject <s>] [--body <b> | --body-file <f|->] [--delete-branch] [--unverified --reason <why>] [--json]", READY_USAGE = "workit pr ready [--pr <n>] [--undo] [--json]", EDIT_USAGE = "workit pr edit [--pr <n>] [--title <t>] [--body-file <f|->] [--add-label <l>]… [--remove-label <l>]… [--add-reviewer <login>]… [--base <b>] [--json]", THREADS_USAGE = "workit pr threads [--pr <n>] [--json]", REPLY_USAGE = "workit pr reply [--pr <n>] --thread <id> [--body-file <f|->] [--resolve] [--json]", USAGE4 = "workit pr status|create|merge|ready|edit|threads|reply ... (workit help pr)", prLabel = (kind, number) => `${kind === "github" ? "PR #" : "MR !"}${number}`, mergedVerdictLine = (landed) => landed === "untracked" ? "merged outside workit (no merge record)" : landed.unverified ? `merged unverified at ${(landed.head ?? "?").slice(0, 12)} (recorded bypass)` : `merged with accepted verdict ${landed.verdictId ?? "?"} at ${(landed.head ?? "?").slice(0, 12)}`, METHODS2;
77971
78824
  var init_pr = __esm(() => {
77972
78825
  init_pr_body();
77973
78826
  init_pr_edit();
@@ -78205,7 +79058,7 @@ __export(exports_git, {
78205
79058
  run: () => run13
78206
79059
  });
78207
79060
  import { readFileSync as readFileSync22 } from "node:fs";
78208
- import path46 from "node:path";
79061
+ import path47 from "node:path";
78209
79062
  function kindHint(cwd, kind) {
78210
79063
  let kinds = [...BRANCH_KINDS];
78211
79064
  try {
@@ -78256,6 +79109,7 @@ function parseCommit(argv) {
78256
79109
  const out = {
78257
79110
  messages: [],
78258
79111
  files: [],
79112
+ trailers: [],
78259
79113
  all: false,
78260
79114
  amend: false,
78261
79115
  noEdit: false,
@@ -78277,14 +79131,15 @@ function parseCommit(argv) {
78277
79131
  }
78278
79132
  const valued = [
78279
79133
  ["-m", "--message", out.messages],
78280
- ["-F", "--file", out.files]
78281
- ].find(([shortName, longName]) => arg === shortName || arg === longName || arg.startsWith(`${longName}=`) || arg.startsWith(shortName) && arg.length > 2);
79134
+ ["-F", "--file", out.files],
79135
+ [null, "--trailer", out.trailers]
79136
+ ].find(([shortName, longName]) => arg === shortName || arg === longName || arg.startsWith(`${longName}=`) || shortName !== null && arg.startsWith(shortName) && arg.length > 2);
78282
79137
  if (valued) {
78283
79138
  const [shortName, longName, into] = valued;
78284
79139
  if (arg === shortName || arg === longName) {
78285
79140
  const value = argv[++index];
78286
79141
  if (value === undefined)
78287
- return `${arg} requires a ${shortName === "-m" ? "message" : "file"}`;
79142
+ return `${arg} requires a ${shortName === "-m" ? "message" : shortName === "-F" ? "file" : "Key: value trailer"}`;
78288
79143
  into.push(value);
78289
79144
  } else
78290
79145
  into.push(arg.startsWith(`${longName}=`) ? arg.slice(longName.length + 1) : arg.slice(2));
@@ -78307,7 +79162,7 @@ function commitMessage(parsed, io) {
78307
79162
 
78308
79163
  `);
78309
79164
  try {
78310
- return file === "-" ? readFileSync22(0, "utf8") : readFileSync22(path46.resolve(io.cwd, file), "utf8");
79165
+ return file === "-" ? readFileSync22(0, "utf8") : readFileSync22(path47.resolve(io.cwd, file), "utf8");
78311
79166
  } catch (error) {
78312
79167
  return {
78313
79168
  error: `cannot read -F ${file}: ${error instanceof Error ? error.message : String(error)}`
@@ -78338,6 +79193,7 @@ async function commit(argv, io) {
78338
79193
  paths: parsed.paths,
78339
79194
  amend: parsed.amend,
78340
79195
  allowEmpty: parsed.allowEmpty,
79196
+ trailers: parsed.trailers,
78341
79197
  actor: actorFromEnv(io.env),
78342
79198
  env: io.env
78343
79199
  });
@@ -78450,6 +79306,13 @@ async function releasePush(names, io) {
78450
79306
  ..."error" in data.recorded ? [`ledger: not recorded (${data.recorded.error})`] : []
78451
79307
  ]);
78452
79308
  }
79309
+ function trailers(argv, io) {
79310
+ const extra = argv.find((arg) => arg !== "--json");
79311
+ if (extra !== undefined)
79312
+ return usage3(io, `unexpected argument ${extra}`, "workit git trailers");
79313
+ const state = workspaceTrailers(io.cwd);
79314
+ return emit(io, ok(state), (data) => data.enabled ? "on" : "off");
79315
+ }
78453
79316
  async function run13(argv, io) {
78454
79317
  const [sub, ...rest] = argv;
78455
79318
  if (sub === "branch")
@@ -78458,13 +79321,16 @@ async function run13(argv, io) {
78458
79321
  return commit(rest, io);
78459
79322
  if (sub === "push")
78460
79323
  return push(rest, io);
79324
+ if (sub === "trailers")
79325
+ return trailers(rest, io);
78461
79326
  return usage3(io, sub && !sub.startsWith("-") ? `unknown git subcommand "${sub}"` : "missing git subcommand", USAGE5);
78462
79327
  }
78463
- var BRANCH_USAGE = "workit git branch <name> | --kind feature|bugfix|release|hotfix --slug <s> [--base <b>] [--track <t>] [--carry] [--json]", COMMIT_USAGE = "workit git commit -m|--message <msg>… | -F|--file <file|-> [--amend [--no-edit]] [--allow-empty] [-a|--all | [--] <paths…>] [--json]", PUSH_USAGE = "workit git push [--set-upstream] [--force-with-lease [--expect <sha>] [--overwrite-unintegrated]] [--release <main|develop|v-tag>…] [--json]", USAGE5 = "workit git branch|commit|push ... (workit help git)", short2 = (sha) => sha ? sha.slice(0, 12) : "(none)", COMMIT_SWITCHES;
79328
+ var BRANCH_USAGE = "workit git branch <name> | --kind feature|bugfix|release|hotfix --slug <s> [--base <b>] [--track <t>] [--carry] [--json]", COMMIT_USAGE = "workit git commit -m|--message <msg>… | -F|--file <file|-> [--trailer <key: value>]… [--amend [--no-edit]] [--allow-empty] [-a|--all | [--] <paths…>] [--json]", PUSH_USAGE = "workit git push [--set-upstream] [--force-with-lease [--expect <sha>] [--overwrite-unintegrated]] [--release <main|develop|v-tag>…] [--json]", USAGE5 = "workit git branch|commit|push|trailers ... (workit help git)", short2 = (sha) => sha ? sha.slice(0, 12) : "(none)", COMMIT_SWITCHES;
78464
79329
  var init_git2 = __esm(() => {
78465
79330
  init_autonomy();
78466
79331
  init_branch();
78467
79332
  init_ops();
79333
+ init_workspaces();
78468
79334
  init_ledger();
78469
79335
  init_implicit_task();
78470
79336
  init_output();
@@ -79015,8 +79881,8 @@ var init_stack2 = __esm(() => {
79015
79881
  // packages/workit-core/src/fanout.ts
79016
79882
  import { spawnSync as spawnSync24 } from "node:child_process";
79017
79883
  import { createHash as createHash18 } from "node:crypto";
79018
- import fs24 from "node:fs";
79019
- import path47 from "node:path";
79884
+ import fs25 from "node:fs";
79885
+ import path48 from "node:path";
79020
79886
  function expandBraces(pattern, limit = 64) {
79021
79887
  for (let index = 0;index < pattern.length; index += 1) {
79022
79888
  if (pattern[index] === "\\") {
@@ -79162,7 +80028,7 @@ function inScope(plan, slice, file) {
79162
80028
  }
79163
80029
  function parsePlan(raw, defaults, name) {
79164
80030
  const problems = [];
79165
- if (!isRecord8(raw))
80031
+ if (!isRecord9(raw))
79166
80032
  return { ok: false, problems: ["the plan must be a JSON object"] };
79167
80033
  const planName = name ?? (typeof raw.name === "string" ? raw.name.trim() : "");
79168
80034
  if (!planName || planName.length > 200 || /\p{Cc}/u.test(planName))
@@ -79185,8 +80051,8 @@ function parsePlan(raw, defaults, name) {
79185
80051
  const seenIds = new Set;
79186
80052
  const seenBranches = new Map;
79187
80053
  raw.slices.forEach((item, index) => {
79188
- const where = isRecord8(item) && typeof item.id === "string" ? `slice ${item.id}` : `slice #${index + 1}`;
79189
- if (!isRecord8(item)) {
80054
+ const where = isRecord9(item) && typeof item.id === "string" ? `slice ${item.id}` : `slice #${index + 1}`;
80055
+ if (!isRecord9(item)) {
79190
80056
  problems.push(`${where}: must be an object`);
79191
80057
  return;
79192
80058
  }
@@ -79250,7 +80116,7 @@ function parsePlan(raw, defaults, name) {
79250
80116
  id,
79251
80117
  branch,
79252
80118
  base: optional("base") ?? "",
79253
- worktree: path47.resolve(defaults.root, optional("worktree") ?? path47.join(defaults.worktreeRoot, id)),
80119
+ worktree: path48.resolve(defaults.root, optional("worktree") ?? path48.join(defaults.worktreeRoot, id)),
79254
80120
  tier,
79255
80121
  dependsOn: [...new Set(dependsOn ?? [])],
79256
80122
  scope,
@@ -79479,11 +80345,11 @@ function defaultTrunk(cwd) {
79479
80345
  }
79480
80346
  function mainCheckout(cwd) {
79481
80347
  const common = gitCommonDir(cwd);
79482
- return common && path47.basename(common) === ".git" ? path47.dirname(common) : path47.resolve(cwd);
80348
+ return common && path48.basename(common) === ".git" ? path48.dirname(common) : path48.resolve(cwd);
79483
80349
  }
79484
80350
  function worktreeRoot(cwd) {
79485
80351
  const main = mainCheckout(cwd);
79486
- return path47.join(path47.dirname(main), `${path47.basename(main)}-wt`);
80352
+ return path48.join(path48.dirname(main), `${path48.basename(main)}-wt`);
79487
80353
  }
79488
80354
  function sliceHash(slice) {
79489
80355
  const {
@@ -79501,10 +80367,10 @@ function fanoutsDir(cwd) {
79501
80367
  const root = storeRoot(cwd);
79502
80368
  if (!root.ok)
79503
80369
  return fanoutFail(root.code, root.error, root.unblock);
79504
- return { ok: true, data: path47.join(root.value.root, "fanouts") };
80370
+ return { ok: true, data: path48.join(root.value.root, "fanouts") };
79505
80371
  }
79506
80372
  function parseStored(raw) {
79507
- if (!isRecord8(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
80373
+ if (!isRecord9(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
79508
80374
  return null;
79509
80375
  if (!Array.isArray(raw.slices))
79510
80376
  return null;
@@ -79521,7 +80387,7 @@ function parseStored(raw) {
79521
80387
  });
79522
80388
  const slices = [];
79523
80389
  for (const item of raw.slices) {
79524
- if (!isRecord8(item) || typeof item.id !== "string" || typeof item.branch !== "string")
80390
+ if (!isRecord9(item) || typeof item.id !== "string" || typeof item.branch !== "string")
79525
80391
  return null;
79526
80392
  slices.push({
79527
80393
  id: item.id,
@@ -79567,10 +80433,10 @@ function readFanout(cwd, name) {
79567
80433
  const dir = fanoutsDir(cwd);
79568
80434
  if (!dir.ok)
79569
80435
  return dir;
79570
- const file = path47.join(dir.data, stackFileName(name));
80436
+ const file = path48.join(dir.data, stackFileName(name));
79571
80437
  let parsed = null;
79572
80438
  try {
79573
- parsed = parseStored(JSON.parse(fs24.readFileSync(file, "utf8")));
80439
+ parsed = parseStored(JSON.parse(fs25.readFileSync(file, "utf8")));
79574
80440
  } catch (error) {
79575
80441
  if (error.code === "ENOENT")
79576
80442
  return { ok: true, data: null };
@@ -79584,7 +80450,7 @@ function listFanouts(cwd) {
79584
80450
  return dir;
79585
80451
  let names = [];
79586
80452
  try {
79587
- names = fs24.readdirSync(dir.data).filter((entry) => entry.endsWith(".json"));
80453
+ names = fs25.readdirSync(dir.data).filter((entry) => entry.endsWith(".json"));
79588
80454
  } catch {
79589
80455
  return { ok: true, data: [] };
79590
80456
  }
@@ -79592,7 +80458,7 @@ function listFanouts(cwd) {
79592
80458
  for (const entry of names) {
79593
80459
  let parsed = null;
79594
80460
  try {
79595
- parsed = parseStored(JSON.parse(fs24.readFileSync(path47.join(dir.data, entry), "utf8")));
80461
+ parsed = parseStored(JSON.parse(fs25.readFileSync(path48.join(dir.data, entry), "utf8")));
79596
80462
  } catch {
79597
80463
  parsed = null;
79598
80464
  }
@@ -79622,16 +80488,16 @@ function writeFanout(cwd, plan) {
79622
80488
  const dir = fanoutsDir(cwd);
79623
80489
  if (!dir.ok)
79624
80490
  return dir;
79625
- const file = path47.join(dir.data, stackFileName(plan.name));
80491
+ const file = path48.join(dir.data, stackFileName(plan.name));
79626
80492
  const temp = `${file}.${process.pid}.tmp`;
79627
80493
  try {
79628
- fs24.mkdirSync(dir.data, { recursive: true });
79629
- fs24.writeFileSync(temp, `${JSON.stringify(plan, null, 2)}
80494
+ fs25.mkdirSync(dir.data, { recursive: true });
80495
+ fs25.writeFileSync(temp, `${JSON.stringify(plan, null, 2)}
79630
80496
  `);
79631
- fs24.renameSync(temp, file);
80497
+ fs25.renameSync(temp, file);
79632
80498
  return { ok: true, data: file };
79633
80499
  } catch (error) {
79634
- fs24.rmSync(temp, { force: true });
80500
+ fs25.rmSync(temp, { force: true });
79635
80501
  return fanoutFail("unavailable", `cannot write ${file}: ${error.message}`);
79636
80502
  }
79637
80503
  }
@@ -79644,7 +80510,7 @@ function planFanout(cwd, input, now = new Date) {
79644
80510
  return fanoutFail("blocked", "--take-lead needs a session: WORKIT_SESSION_ID is not set", "export WORKIT_SESSION_ID=<your session id>, then plan again with --take-lead");
79645
80511
  if (input.takeLead && isDelegateSession(session))
79646
80512
  return fanoutFail("blocked", `session ${session} is a worker or verifier id; only a lead takes the lead`, "run --take-lead from the lead's own session");
79647
- const parsed = parsePlan(input.trunk && isRecord8(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, {
80513
+ const parsed = parsePlan(input.trunk && isRecord9(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, {
79648
80514
  trunk: input.trunk ?? defaultTrunk(cwd),
79649
80515
  root: mainCheckout(cwd),
79650
80516
  worktreeRoot: worktreeRoot(cwd)
@@ -79767,7 +80633,7 @@ var FANOUT_VERSION = 1, TIERS, FAN_IN_MODES, fanoutFail = (code, error, unblock,
79767
80633
  error,
79768
80634
  ...unblock ? { unblock } : {},
79769
80635
  ...data ? { data } : {}
79770
- }), 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) => {
80636
+ }), 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)), isRecord9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), PLACEHOLDER3, filled = (text) => text.trim() !== "" && !PLACEHOLDER3.test(text.trim()), textList = (value) => {
79771
80637
  if (typeof value === "string")
79772
80638
  return [value];
79773
80639
  if (Array.isArray(value) && value.every((item) => typeof item === "string"))
@@ -79825,8 +80691,8 @@ var init_fanout = __esm(() => {
79825
80691
 
79826
80692
  // packages/workit-core/src/fanout-worktree.ts
79827
80693
  import { randomBytes as randomBytes4 } from "node:crypto";
79828
- import fs25 from "node:fs";
79829
- import path48 from "node:path";
80694
+ import fs26 from "node:fs";
80695
+ import path49 from "node:path";
79830
80696
  function rowSample(lines) {
79831
80697
  const out = [];
79832
80698
  let bytes = 0;
@@ -79886,7 +80752,7 @@ function adminDir(worktree) {
79886
80752
  const file = dir.stdout.trim();
79887
80753
  let token = null;
79888
80754
  try {
79889
- token = fs25.readFileSync(path48.join(file, TOKEN_FILE), "utf8").trim() || null;
80755
+ token = fs26.readFileSync(path49.join(file, TOKEN_FILE), "utf8").trim() || null;
79890
80756
  } catch {
79891
80757
  token = null;
79892
80758
  }
@@ -79894,7 +80760,7 @@ function adminDir(worktree) {
79894
80760
  const stat = worktreeDeps.stat(file);
79895
80761
  return {
79896
80762
  path: file,
79897
- id: path48.basename(file),
80763
+ id: path49.basename(file),
79898
80764
  ino: stat.ino.toString(),
79899
80765
  token,
79900
80766
  birthMs: stat.birthtimeNs > 0n ? Number(stat.birthtimeMs) : null,
@@ -79910,7 +80776,7 @@ function markAdminDir(worktree) {
79910
80776
  return null;
79911
80777
  const token = randomBytes4(16).toString("hex");
79912
80778
  try {
79913
- fs25.writeFileSync(path48.join(admin.path, TOKEN_FILE), `${token}
80779
+ fs26.writeFileSync(path49.join(admin.path, TOKEN_FILE), `${token}
79914
80780
  `);
79915
80781
  } catch {
79916
80782
  return null;
@@ -79925,25 +80791,25 @@ function excludeScratch(cwd) {
79925
80791
  const common = gitCommonDir(cwd);
79926
80792
  if (!common)
79927
80793
  return;
79928
- const file = path48.join(common, "info", "exclude");
80794
+ const file = path49.join(common, "info", "exclude");
79929
80795
  let text = "";
79930
80796
  try {
79931
- text = fs25.readFileSync(file, "utf8");
80797
+ text = fs26.readFileSync(file, "utf8");
79932
80798
  } catch {
79933
80799
  text = "";
79934
80800
  }
79935
80801
  if (text.split(/\r?\n/u).includes(EXCLUDE_LINE))
79936
80802
  return;
79937
- fs25.mkdirSync(path48.dirname(file), { recursive: true });
79938
- fs25.appendFileSync(file, `${text && !text.endsWith(`
80803
+ fs26.mkdirSync(path49.dirname(file), { recursive: true });
80804
+ fs26.appendFileSync(file, `${text && !text.endsWith(`
79939
80805
  `) ? `
79940
80806
  ` : ""}# workit fanout: per-worker scratch dirs
79941
80807
  ${EXCLUDE_LINE}
79942
80808
  `);
79943
80809
  }
79944
80810
  function ensureScratch(worktree) {
79945
- const scratch = path48.join(worktree, SCRATCH_DIR);
79946
- fs25.mkdirSync(scratch, { recursive: true });
80811
+ const scratch = path49.join(worktree, SCRATCH_DIR);
80812
+ fs26.mkdirSync(scratch, { recursive: true });
79947
80813
  return scratch;
79948
80814
  }
79949
80815
  function createSliceWorktree(cwd, plan, input) {
@@ -79987,8 +80853,8 @@ function createSliceWorktree(cwd, plan, input) {
79987
80853
  return fanoutFail("blocked", `${slice.branch} is checked out at ${elsewhere.path}: never two live workers on one branch`, `stop that worker, then workit fanout worktree release ${slice.id} (or git worktree remove ${elsewhere.path})`);
79988
80854
  let dirExisted = false;
79989
80855
  try {
79990
- const stat = fs25.statSync(target);
79991
- if (!stat.isDirectory() || fs25.readdirSync(target).length > 0)
80856
+ const stat = fs26.statSync(target);
80857
+ if (!stat.isDirectory() || fs26.readdirSync(target).length > 0)
79992
80858
  return fanoutFail("blocked", `${target} exists and is ${stat.isDirectory() ? "not empty" : "not a directory"} (not a worktree of this repository)`, `move it away, or set slice ${slice.id}'s worktree to another path`);
79993
80859
  dirExisted = true;
79994
80860
  } catch (error) {
@@ -80070,7 +80936,7 @@ function releaseSliceWorktree(cwd, plan, input) {
80070
80936
  notes: []
80071
80937
  };
80072
80938
  if (index < 0) {
80073
- if (fs25.existsSync(target))
80939
+ if (fs26.existsSync(target))
80074
80940
  return fanoutFail("blocked", `${target} is not a worktree of this repository; nothing was removed`, "remove it yourself once you know what it holds");
80075
80941
  out.notes.push(`no worktree at ${target}; nothing to release`);
80076
80942
  return { ok: true, data: out };
@@ -80132,7 +80998,7 @@ function releaseSliceWorktree(cwd, plan, input) {
80132
80998
  `)[0]}`, entry.locked ? `git worktree unlock ${entry.path}, then release again` : undefined, { ...out });
80133
80999
  out.removed = true;
80134
81000
  if (made.dirExisted === true)
80135
- fs25.mkdirSync(entry.path, { recursive: true });
81001
+ fs26.mkdirSync(entry.path, { recursive: true });
80136
81002
  if (out.unpushed)
80137
81003
  out.notes.push(`${slice.branch} is kept with ${out.unpushed} commit(s) not on the remote`);
80138
81004
  return { ok: true, data: out };
@@ -80160,32 +81026,32 @@ function unpushedCount(cwd, branch) {
80160
81026
  return count.ok ? Number(count.stdout.trim()) : null;
80161
81027
  }
80162
81028
  function removeScratch(worktree) {
80163
- const scratch = path48.join(worktree, SCRATCH_DIR);
81029
+ const scratch = path49.join(worktree, SCRATCH_DIR);
80164
81030
  let stat;
80165
81031
  try {
80166
- stat = fs25.lstatSync(scratch);
81032
+ stat = fs26.lstatSync(scratch);
80167
81033
  } catch {
80168
81034
  return;
80169
81035
  }
80170
81036
  if (stat.isSymbolicLink() || !stat.isDirectory())
80171
- fs25.unlinkSync(scratch);
81037
+ fs26.unlinkSync(scratch);
80172
81038
  else
80173
- fs25.rmSync(scratch, { recursive: true, force: true, maxRetries: 2 });
81039
+ fs26.rmSync(scratch, { recursive: true, force: true, maxRetries: 2 });
80174
81040
  }
80175
81041
  var SLOW, SCRATCH_DIR = ".workit-scratch", EXCLUDE_LINE, DIRTY_ROW_BYTES = 2000, canonical2 = (target) => {
80176
81042
  try {
80177
- return fs25.realpathSync.native(target);
81043
+ return fs26.realpathSync.native(target);
80178
81044
  } catch {
80179
- return path48.resolve(target);
81045
+ return path49.resolve(target);
80180
81046
  }
80181
81047
  }, samePath = (a, b) => {
80182
81048
  const left = canonical2(a);
80183
81049
  const right = canonical2(b);
80184
81050
  return process.platform === "win32" || process.platform === "darwin" ? left.toLowerCase() === right.toLowerCase() : left === right;
80185
81051
  }, inside3 = (child, parent) => {
80186
- const relative = path48.relative(canonical2(parent), canonical2(child));
80187
- return relative === "" || !relative.startsWith("..") && !path48.isAbsolute(relative);
80188
- }, slicePath = (cwd, slice) => path48.isAbsolute(slice.worktree) ? slice.worktree : path48.resolve(mainCheckout(cwd), slice.worktree), CLOCK_SLACK_MS = 2000, TOKEN_FILE = "workit-fanout-token", worktreeDeps, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
81052
+ const relative = path49.relative(canonical2(parent), canonical2(child));
81053
+ return relative === "" || !relative.startsWith("..") && !path49.isAbsolute(relative);
81054
+ }, slicePath = (cwd, slice) => path49.isAbsolute(slice.worktree) ? slice.worktree : path49.resolve(mainCheckout(cwd), slice.worktree), CLOCK_SLACK_MS = 2000, TOKEN_FILE = "workit-fanout-token", worktreeDeps, worktreeFailed = (stderr) => fanoutFail("failed", `git worktree add failed: ${stderr.trim().split(`
80189
81055
  `)[0] || "unknown error"}`);
80190
81056
  var init_fanout_worktree = __esm(() => {
80191
81057
  init_ops();
@@ -80195,7 +81061,7 @@ var init_fanout_worktree = __esm(() => {
80195
81061
  SLOW = GIT_TIMEOUTS.worktree;
80196
81062
  EXCLUDE_LINE = `/${SCRATCH_DIR}/`;
80197
81063
  worktreeDeps = {
80198
- stat: (file) => fs25.statSync(file, { bigint: true })
81064
+ stat: (file) => fs26.statSync(file, { bigint: true })
80199
81065
  };
80200
81066
  });
80201
81067
 
@@ -80236,7 +81102,9 @@ function renderBrief(cwd, plan, input) {
80236
81102
  id: row.id,
80237
81103
  what: String(row.what)
80238
81104
  }));
80239
- const session = workerSession(sessionSafe(input.actor.session), slice.id, input.attempt);
81105
+ const identity = identityOf(input.actor.host);
81106
+ const minted = workerSession(sessionSafe(input.actor.session), slice.id, input.attempt);
81107
+ const session = identity === "declared" ? minted : null;
80240
81108
  const fanIn = plan.fanIn === "integration" ? [
80241
81109
  `FAN-IN: integration branch ${plan.trunk}. Before you report, merge its tip into your branch`,
80242
81110
  ` (\`git merge --no-edit ${plan.trunk}\`), resolve conflicts inside SCOPE and re-run VERIFY, so the`,
@@ -80265,8 +81133,8 @@ function renderBrief(cwd, plan, input) {
80265
81133
  ...REPORT,
80266
81134
  "STANDING:",
80267
81135
  ...standing.length ? standing.map((order) => ` - ${order.what}`) : [" - none recorded beyond this brief"],
80268
- ` export WORKIT_SESSION_ID=${session}`,
80269
- ...rules(mode, branch, slice.base)
81136
+ ...identityLines(identity, minted),
81137
+ ...rules(identity, mode, branch, slice.base)
80270
81138
  ];
80271
81139
  return {
80272
81140
  ok: true,
@@ -80287,13 +81155,38 @@ function renderBrief(cwd, plan, input) {
80287
81155
  }
80288
81156
  };
80289
81157
  }
80290
- var DEFAULT_TIMEBOX = "30 minutes", REPORT, rules = (mode, branch, base) => [
80291
- "RULES:",
80292
- mode === "new" ? ` 1. First command: \`workit git branch ${branch} --base ${base}\` (your worktree may start on another name).` : ` 1. First command: \`git switch ${branch}\`, then continue from its head.`,
80293
- ' 2. Decide ambiguities yourself: `workit ledger ruling "<what>" --why "<why>" --cost-if-wrong "<cost>"`; stop only for an irreversible, security-sensitive or out-of-worktree action.',
80294
- " 3. Commit with `workit git commit -m <msg> -- <paths in SCOPE>`; push only if this brief says so.",
80295
- " 4. Never record a verdict on your own work."
80296
- ], sessionSafe = (text) => text.replace(/[^A-Za-z0-9_.:@/+-]/gu, "-"), SESSION_MAX = 128;
81158
+ var DEFAULT_TIMEBOX = "30 minutes", REPORT, identityOf = (host) => {
81159
+ const family = host?.replace(/_(?:cli|desktop)$/, "") ?? "";
81160
+ if (family === "claude_code")
81161
+ return "issued";
81162
+ if (family === "opencode" || family === "pi" || family === "codex")
81163
+ return "derived";
81164
+ return "declared";
81165
+ }, identityLines = (identity, session) => {
81166
+ if (identity === "issued")
81167
+ return [
81168
+ " Session: your env already has WORKIT_SESSION_ID (the lead's id; SubagentStart issues ids only to verifiers and reviewers). Use it as is, in your Workit-Session trailer and workit commands; never export or hand-mint one."
81169
+ ];
81170
+ if (identity === "derived")
81171
+ return [
81172
+ " Session: your host already sets WORKIT_SESSION_ID in your shell; export nothing and never hand-mint one."
81173
+ ];
81174
+ return [` export WORKIT_SESSION_ID=${session}`];
81175
+ }, rules = (identity, mode, branch, base) => {
81176
+ const plain = identity === "issued";
81177
+ let first = ` 1. First command: \`workit git branch ${branch} --base ${base}\` (your worktree may start on another name).`;
81178
+ if (mode === "resume")
81179
+ first = ` 1. First command: \`git switch ${branch}\`, then continue from its head.`;
81180
+ else if (plain)
81181
+ first = ` 1. First command: \`git fetch origin && git switch -c ${branch} origin/${base}\` (plain git: worktree isolation refuses every \`workit git\` form).`;
81182
+ return [
81183
+ "RULES:",
81184
+ first,
81185
+ ' 2. Decide ambiguities yourself: `workit ledger ruling "<what>" --why "<why>" --cost-if-wrong "<cost>"`; stop only for an irreversible, security-sensitive or out-of-worktree action.',
81186
+ plain ? ' 3. Commit with plain `git commit -m <msg> --trailer "Workit-Session: <the WORKIT_SESSION_ID in your env>" --trailer "<your host attribution trailer>" -- <paths in SCOPE>`; push only if this brief says so.' : " 3. Commit with `workit git commit -m <msg> -- <paths in SCOPE>`; push only if this brief says so.",
81187
+ " 4. Never record a verdict on your own work."
81188
+ ];
81189
+ }, sessionSafe = (text) => text.replace(/[^A-Za-z0-9_.:@/+-]/gu, "-"), SESSION_MAX = 128;
80297
81190
  var init_fanout_brief = __esm(() => {
80298
81191
  init_ledger();
80299
81192
  init_fanout();
@@ -80349,9 +81242,9 @@ function hasOwnCommits2(cwd, branch, start, head) {
80349
81242
  const onto = /^rebase(?: -i)? \(finish\): \S+ onto ([0-9a-f]+)/u.exec(subject)?.[1];
80350
81243
  if (!made && !(onto && !sha.startsWith(onto) && !onto.startsWith(sha)))
80351
81244
  continue;
80352
- if (sha !== head && !isAncestor2(cwd, sha, head))
81245
+ if (sha !== head && !isAncestor3(cwd, sha, head))
80353
81246
  continue;
80354
- if (start && (sha === start || isAncestor2(cwd, sha, start)))
81247
+ if (start && (sha === start || isAncestor3(cwd, sha, start)))
80355
81248
  continue;
80356
81249
  return true;
80357
81250
  }
@@ -80361,7 +81254,7 @@ function reverted(cwd, trunk, landedHead, start) {
80361
81254
  const fork = mergeBase(cwd, landedHead, trunk);
80362
81255
  if (fork && fork !== landedHead)
80363
81256
  return unchangedSince(cwd, fork, landedHead, trunk);
80364
- const before = start && start !== landedHead && isAncestor2(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
81257
+ const before = start && start !== landedHead && isAncestor3(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
80365
81258
  return before ? unchangedSince(cwd, before, landedHead, trunk) : false;
80366
81259
  }
80367
81260
  function unchangedSince(cwd, before, after, trunk) {
@@ -80469,9 +81362,9 @@ function detectLanding(ctx, slice, input) {
80469
81362
  }
80470
81363
  return merged ? landed("pr_merged", merged) : { ...out, landed: true, how: "pr_merged" };
80471
81364
  }
80472
- if (!merged || head === merged || isAncestor2(ctx.cwd, head, merged))
81365
+ if (!merged || head === merged || isAncestor3(ctx.cwd, head, merged))
80473
81366
  return landed("pr_merged", merged ?? head);
80474
- out.note = isAncestor2(ctx.cwd, merged, head) ? `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} is ahead of it with newer commits` : `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} has diverged from it (rewritten after the merge)`;
81367
+ out.note = isAncestor3(ctx.cwd, merged, head) ? `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} is ahead of it with newer commits` : `${label} merged at ${merged.slice(0, 12)}; ${slice.branch} has diverged from it (rewritten after the merge)`;
80475
81368
  return out;
80476
81369
  }
80477
81370
  }
@@ -80488,7 +81381,7 @@ function detectLanding(ctx, slice, input) {
80488
81381
  if (trunkIds.size === 0)
80489
81382
  return out;
80490
81383
  const froms = [fork];
80491
- if (input.parentHead && input.parentHead !== head && isAncestor2(ctx.cwd, input.parentHead, head))
81384
+ if (input.parentHead && input.parentHead !== head && isAncestor3(ctx.cwd, input.parentHead, head))
80492
81385
  froms.push(input.parentHead);
80493
81386
  for (const from of froms) {
80494
81387
  const id = patchId(ctx.cwd, from, head);
@@ -80504,7 +81397,7 @@ function sliceStart(cwd, rows, fanout, slice) {
80504
81397
  const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string" && inSliceRun(candidate, slice));
80505
81398
  return row?.baseSha ?? null;
80506
81399
  }
80507
- var isAncestor2 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
81400
+ var isAncestor3 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
80508
81401
  var init_fanout_landed = __esm(() => {
80509
81402
  init_rev();
80510
81403
  init_fanout();
@@ -80561,7 +81454,7 @@ function checkFanout(cwd, plan, options = {}) {
80561
81454
  const b = tips.get(second);
80562
81455
  if (landings.get(first)?.landed || landings.get(second)?.landed)
80563
81456
  continue;
80564
- if (!a || !b || isAncestor3(cwd, a, b) || isAncestor3(cwd, b, a))
81457
+ if (!a || !b || isAncestor4(cwd, a, b) || isAncestor4(cwd, b, a))
80565
81458
  continue;
80566
81459
  const check = checks.get(second);
80567
81460
  const theirs = new Map((changedBy.get(first) ?? []).map((file) => [file.toLowerCase(), file]));
@@ -80741,7 +81634,7 @@ function unblockFor(check, plan) {
80741
81634
  return `unblock ${check.waitsFor.join(", ")} first`;
80742
81635
  return check.reasons[0] ?? "workit fanout check";
80743
81636
  }
80744
- var SAFE = (value) => Boolean(value) && !value?.startsWith("-"), isAncestor3 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, resolves = (cwd, ref) => gitRun4(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]).ok, blank2 = (slice, head) => ({
81637
+ var SAFE = (value) => Boolean(value) && !value?.startsWith("-"), isAncestor4 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, resolves = (cwd, ref) => gitRun4(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]).ok, blank2 = (slice, head) => ({
80745
81638
  id: slice.id,
80746
81639
  branch: slice.branch,
80747
81640
  head,
@@ -80960,8 +81853,8 @@ var exports_fanout = {};
80960
81853
  __export(exports_fanout, {
80961
81854
  run: () => run16
80962
81855
  });
80963
- import fs26 from "node:fs";
80964
- import path49 from "node:path";
81856
+ import fs27 from "node:fs";
81857
+ import path50 from "node:path";
80965
81858
  function forgeFor(io, offline) {
80966
81859
  if (offline)
80967
81860
  return { forge: null, error: "--offline" };
@@ -81022,10 +81915,10 @@ function plan2(argv, io) {
81022
81915
  return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE2);
81023
81916
  if (flags.positionals.length !== 1)
81024
81917
  return usage3(io, flags.positionals.length ? `unexpected argument ${flags.positionals[1]}` : "missing <plan.json>", PLAN_USAGE2);
81025
- const file = path49.resolve(io.cwd, flags.positionals[0]);
81918
+ const file = path50.resolve(io.cwd, flags.positionals[0]);
81026
81919
  let raw;
81027
81920
  try {
81028
- raw = JSON.parse(fs26.readFileSync(file, "utf8"));
81921
+ raw = JSON.parse(fs27.readFileSync(file, "utf8"));
81029
81922
  } catch (error) {
81030
81923
  return usage3(io, `cannot read ${file} as JSON: ${error.message}`, PLAN_USAGE2);
81031
81924
  }
@@ -96510,7 +97403,7 @@ var init_ast = __esm(() => {
96510
97403
 
96511
97404
  // packages/workit-cli/src/test-audit/rules.ts
96512
97405
  import { builtinModules } from "node:module";
96513
- import path50 from "node:path";
97406
+ import path51 from "node:path";
96514
97407
  function testCallee(call) {
96515
97408
  let callee = call.callee;
96516
97409
  if (callee.type === "CallExpression")
@@ -97003,12 +97896,12 @@ function checkConstantsOnly(ctx, emit) {
97003
97896
  }
97004
97897
  function checkOverMocking(ctx, emit) {
97005
97898
  const { program } = ctx.parsed;
97006
- const unit = path50.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
97899
+ const unit = path51.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
97007
97900
  const mocked = find2(program, (node) => node.type === "CallExpression" && MODULE_MOCK.test(calleeName(node.callee) ?? "") && node.arguments[0]?.type === "StringLiteral");
97008
97901
  const internal = mocked.filter((call) => /^(?:\.|@\/|~\/)/.test(call.arguments[0].value));
97009
97902
  for (const call of mocked) {
97010
97903
  const spec = call.arguments[0].value;
97011
- const base = path50.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
97904
+ const base = path51.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
97012
97905
  if (base === unit)
97013
97906
  emit({
97014
97907
  rule: "over-mocking",
@@ -97398,7 +98291,7 @@ var init_rules = __esm(() => {
97398
98291
  // packages/workit-cli/src/test-audit/audit.ts
97399
98292
  import { spawnSync as spawnSync26 } from "node:child_process";
97400
98293
  import { existsSync as existsSync20, readdirSync as readdirSync9, readFileSync as readFileSync23, statSync as statSync8 } from "node:fs";
97401
- import path51 from "node:path";
98294
+ import path52 from "node:path";
97402
98295
  function git7(cwd, args) {
97403
98296
  const run = spawnSync26("git", ["-c", "core.quotepath=off", ...args], {
97404
98297
  cwd,
@@ -97411,7 +98304,7 @@ function walkTests(dir, out) {
97411
98304
  for (const entry of readdirSync9(dir, { withFileTypes: true })) {
97412
98305
  if (entry.name.startsWith(".") && entry.name !== "." && entry.isDirectory())
97413
98306
  continue;
97414
- const full = path51.join(dir, entry.name);
98307
+ const full = path52.join(dir, entry.name);
97415
98308
  if (entry.isDirectory()) {
97416
98309
  if (!SKIP_DIRS.has(entry.name))
97417
98310
  walkTests(full, out);
@@ -97423,7 +98316,7 @@ function expandPaths(cwd, inputs) {
97423
98316
  const files = [];
97424
98317
  const missing = [];
97425
98318
  for (const input of inputs.length > 0 ? inputs : ["."]) {
97426
- const full = path51.resolve(cwd, input);
98319
+ const full = path52.resolve(cwd, input);
97427
98320
  if (!existsSync20(full))
97428
98321
  missing.push(input);
97429
98322
  else if (statSync8(full).isDirectory())
@@ -97460,11 +98353,11 @@ function changedLines(cwd, base) {
97460
98353
  for (let offset = 0;offset < count; offset += 1)
97461
98354
  lines.add(start + offset);
97462
98355
  }
97463
- changed.set(path51.join(root, file), lines);
98356
+ changed.set(path52.join(root, file), lines);
97464
98357
  }
97465
98358
  const untracked = git7(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
97466
98359
  for (const file of untracked.stdout.split("\x00").filter(Boolean))
97467
- changed.set(path51.join(root, file), "all");
98360
+ changed.set(path52.join(root, file), "all");
97468
98361
  return changed;
97469
98362
  }
97470
98363
  function auditFiles(files, display) {
@@ -97526,7 +98419,7 @@ function auditFiles(files, display) {
97526
98419
  durationMs: Math.round(performance.now() - started)
97527
98420
  };
97528
98421
  }
97529
- var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path51.sep).join("/"), SEVERITY_ORDER, ruleIds;
98422
+ var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path52.sep).join("/"), SEVERITY_ORDER, ruleIds;
97530
98423
  var init_audit = __esm(() => {
97531
98424
  init_ast();
97532
98425
  init_rules();
@@ -97555,8 +98448,8 @@ import {
97555
98448
  writeFileSync as writeFileSync11,
97556
98449
  copyFileSync as copyFileSync6
97557
98450
  } from "node:fs";
97558
- import os14 from "node:os";
97559
- import path52 from "node:path";
98451
+ import os16 from "node:os";
98452
+ import path53 from "node:path";
97560
98453
  function mutantsFor(source, file, lines) {
97561
98454
  const parsed = parseSource(source, file);
97562
98455
  const out = [];
@@ -97629,7 +98522,7 @@ function relatedTests(source, testFiles) {
97629
98522
  for (const match of text.matchAll(SPECIFIER)) {
97630
98523
  const spec = match[1];
97631
98524
  if (spec.startsWith(".")) {
97632
- if (stripExt(path52.posix.join(path52.posix.dirname(file), spec)) === target)
98525
+ if (stripExt(path53.posix.join(path53.posix.dirname(file), spec)) === target)
97633
98526
  return true;
97634
98527
  } else if (spec.includes("/") && stripExt(spec).split("/").slice(-2).join("/") === tail)
97635
98528
  return true;
@@ -97637,7 +98530,7 @@ function relatedTests(source, testFiles) {
97637
98530
  return false;
97638
98531
  }).map((entry) => entry.file);
97639
98532
  }
97640
- function removeStaleCopies(tmp = os14.tmpdir()) {
98533
+ function removeStaleCopies(tmp = os16.tmpdir()) {
97641
98534
  let removed = 0;
97642
98535
  let names = [];
97643
98536
  try {
@@ -97649,12 +98542,12 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
97649
98542
  for (const name of names) {
97650
98543
  if (!name.startsWith(TEMP_PREFIX))
97651
98544
  continue;
97652
- const dir = path52.join(tmp, name);
98545
+ const dir = path53.join(tmp, name);
97653
98546
  try {
97654
98547
  const stat = lstatSync8(dir);
97655
98548
  if (!stat.isDirectory() || uid !== undefined && stat.uid !== uid)
97656
98549
  continue;
97657
- const pid = Number(readFileSync24(path52.join(dir, PID_FILE), "utf8"));
98550
+ const pid = Number(readFileSync24(path53.join(dir, PID_FILE), "utf8"));
97658
98551
  if (Number.isInteger(pid) && pid > 0 && alive(pid))
97659
98552
  continue;
97660
98553
  } catch (error) {
@@ -97671,8 +98564,8 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
97671
98564
  function linkNodeModules(origin, target, repo, copy) {
97672
98565
  mkdirSync10(target, { recursive: true });
97673
98566
  for (const entry of readdirSync10(origin, { withFileTypes: true })) {
97674
- const from = path52.join(origin, entry.name);
97675
- const to = path52.join(target, entry.name);
98567
+ const from = path53.join(origin, entry.name);
98568
+ const to = path53.join(target, entry.name);
97676
98569
  if (entry.name.startsWith("@") && entry.isDirectory()) {
97677
98570
  linkNodeModules(from, to, repo, copy);
97678
98571
  continue;
@@ -97688,9 +98581,9 @@ function linkNodeModules(origin, target, repo, copy) {
97688
98581
  } catch {
97689
98582
  continue;
97690
98583
  }
97691
- const relative = path52.relative(repo, real);
97692
- if (!relative.startsWith("..") && !path52.isAbsolute(relative) && !relative.split(path52.sep).includes("node_modules")) {
97693
- symlinkSync(path52.join(copy, relative), to, "junction");
98584
+ const relative = path53.relative(repo, real);
98585
+ if (!relative.startsWith("..") && !path53.isAbsolute(relative) && !relative.split(path53.sep).includes("node_modules")) {
98586
+ symlinkSync(path53.join(copy, relative), to, "junction");
97694
98587
  continue;
97695
98588
  }
97696
98589
  }
@@ -97706,7 +98599,7 @@ function copySymlink(repo, tree, from, to) {
97706
98599
  }
97707
98600
  const realRepo = realpathSync9(repo);
97708
98601
  if (real && inside4(realRepo, real)) {
97709
- symlinkSync(path52.relative(path52.dirname(to), path52.join(tree, path52.relative(realRepo, real))), to);
98602
+ symlinkSync(path53.relative(path53.dirname(to), path53.join(tree, path53.relative(realRepo, real))), to);
97710
98603
  return;
97711
98604
  }
97712
98605
  if (real && statSync9(real).isFile())
@@ -97715,10 +98608,10 @@ function copySymlink(repo, tree, from, to) {
97715
98608
  symlinkSync(readlinkSync3(from), to);
97716
98609
  }
97717
98610
  function writeInside(copy, file, content) {
97718
- const target = path52.join(copy, file);
98611
+ const target = path53.join(copy, file);
97719
98612
  try {
97720
98613
  const stat = lstatSync8(target);
97721
- if (!stat.isFile() || !inside4(realpathSync9(copy), realpathSync9(path52.dirname(target))))
98614
+ if (!stat.isFile() || !inside4(realpathSync9(copy), realpathSync9(path53.dirname(target))))
97722
98615
  return false;
97723
98616
  } catch {
97724
98617
  return false;
@@ -97729,9 +98622,9 @@ function writeInside(copy, file, content) {
97729
98622
  return true;
97730
98623
  }
97731
98624
  function copyWorkingTree(repo) {
97732
- const dir = mkdtempSync4(path52.join(os14.tmpdir(), TEMP_PREFIX));
97733
- writeFileSync11(path52.join(dir, PID_FILE), String(process.pid));
97734
- const tree = path52.join(dir, "tree");
98625
+ const dir = mkdtempSync4(path53.join(os16.tmpdir(), TEMP_PREFIX));
98626
+ writeFileSync11(path53.join(dir, PID_FILE), String(process.pid));
98627
+ const tree = path53.join(dir, "tree");
97735
98628
  const head = git7(repo, ["rev-parse", "--verify", "--quiet", "HEAD"]).stdout.trim();
97736
98629
  if (head && git7(dir, ["clone", "-q", "--shared", "--no-checkout", repo, tree]).ok) {
97737
98630
  const branch = git7(repo, ["symbolic-ref", "--quiet", "--short", "HEAD"]).stdout.trim();
@@ -97748,29 +98641,29 @@ function copyWorkingTree(repo) {
97748
98641
  for (const relative of listed) {
97749
98642
  if (!relative)
97750
98643
  continue;
97751
- const from = path52.join(repo, relative);
98644
+ const from = path53.join(repo, relative);
97752
98645
  let stat;
97753
98646
  try {
97754
98647
  stat = lstatSync8(from);
97755
98648
  } catch {
97756
98649
  continue;
97757
98650
  }
97758
- const to = path52.join(tree, relative);
97759
- mkdirSync10(path52.dirname(to), { recursive: true });
98651
+ const to = path53.join(tree, relative);
98652
+ mkdirSync10(path53.dirname(to), { recursive: true });
97760
98653
  if (stat.isSymbolicLink())
97761
98654
  copySymlink(repo, tree, from, to);
97762
98655
  else if (stat.isFile())
97763
98656
  copyFileSync6(from, to);
97764
- let parent = path52.dirname(relative);
98657
+ let parent = path53.dirname(relative);
97765
98658
  while (parent !== "." && !dirs.has(parent)) {
97766
98659
  dirs.add(parent);
97767
- parent = path52.dirname(parent);
98660
+ parent = path53.dirname(parent);
97768
98661
  }
97769
98662
  }
97770
98663
  for (const parent of dirs) {
97771
- const modules = path52.join(repo, parent, "node_modules");
97772
- if (existsSync21(modules) && !existsSync21(path52.join(tree, parent, "node_modules")))
97773
- linkNodeModules(modules, path52.join(tree, parent, "node_modules"), repo, tree);
98664
+ const modules = path53.join(repo, parent, "node_modules");
98665
+ if (existsSync21(modules) && !existsSync21(path53.join(tree, parent, "node_modules")))
98666
+ linkNodeModules(modules, path53.join(tree, parent, "node_modules"), repo, tree);
97774
98667
  }
97775
98668
  return { dir, tree };
97776
98669
  }
@@ -97809,14 +98702,14 @@ async function runMutation(options) {
97809
98702
  const tracked = git7(repo, ["ls-files", "-co", "--exclude-standard", "-z"]).stdout.split("\x00").filter(Boolean);
97810
98703
  const testFiles = tracked.filter((file) => TEST_FILE.test(file)).map((file) => {
97811
98704
  try {
97812
- return { file, text: readFileSync24(path52.join(repo, file), "utf8") };
98705
+ return { file, text: readFileSync24(path53.join(repo, file), "utf8") };
97813
98706
  } catch {
97814
98707
  return { file, text: "" };
97815
98708
  }
97816
98709
  });
97817
98710
  const candidates = [];
97818
98711
  for (const [absolute, lines] of [...options.changed].toSorted(([a], [b]) => a.localeCompare(b))) {
97819
- const relative = toPosix(path52.relative(repo, absolute));
98712
+ const relative = toPosix(path53.relative(repo, absolute));
97820
98713
  if (!CODE_FILE.test(relative) || TEST_FILE.test(relative) || relative.endsWith(".d.ts"))
97821
98714
  continue;
97822
98715
  if (NOT_SOURCE.test(relative) || !existsSync21(absolute))
@@ -97830,9 +98723,9 @@ async function runMutation(options) {
97830
98723
  for (const mutant of mutants)
97831
98724
  if (!related.has(mutant.file))
97832
98725
  related.set(mutant.file, relatedTests(mutant.file, testFiles));
97833
- const cwdRelative = path52.relative(repo, options.cwd);
98726
+ const cwdRelative = path53.relative(repo, options.cwd);
97834
98727
  const asArg = (file) => {
97835
- const relative = toPosix(path52.relative(cwdRelative, file));
98728
+ const relative = toPosix(path53.relative(cwdRelative, file));
97836
98729
  return shellQuote(relative.startsWith(".") ? relative : `./${relative}`);
97837
98730
  };
97838
98731
  const commandFor = (tests) => options.command.replaceAll("{files}", () => tests.map(asArg).join(" "));
@@ -97867,7 +98760,7 @@ async function runMutation(options) {
97867
98760
  return finish2(options, 0, results, counts, started);
97868
98761
  removeStaleCopies();
97869
98762
  const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
97870
- const copyCwd = path52.join(copy, cwdRelative);
98763
+ const copyCwd = path53.join(copy, cwdRelative);
97871
98764
  let current = null;
97872
98765
  const restore = () => {
97873
98766
  if (current)
@@ -97902,7 +98795,7 @@ async function runMutation(options) {
97902
98795
  record({ ...mutant, status: "skipped", durationMs: 0, tests, reason: "budget" });
97903
98796
  continue;
97904
98797
  }
97905
- const original = readFileSync24(path52.join(copy, mutant.file), "utf8");
98798
+ const original = readFileSync24(path53.join(copy, mutant.file), "utf8");
97906
98799
  const mutated = original.slice(0, mutant.start) + mutant.replacement + original.slice(mutant.end);
97907
98800
  current = { file: mutant.file, original };
97908
98801
  if (!writeInside(copy, mutant.file, mutated)) {
@@ -97959,8 +98852,8 @@ var NOT_SOURCE, FLIPS, isStringy = (node) => node.type === "StringLiteral" || no
97959
98852
  return error.code === "EPERM";
97960
98853
  }
97961
98854
  }, inside4 = (root, file) => {
97962
- const relative = path52.relative(root, file);
97963
- return relative === "" || !relative.startsWith("..") && !path52.isAbsolute(relative);
98855
+ const relative = path53.relative(root, file);
98856
+ return relative === "" || !relative.startsWith("..") && !path53.isAbsolute(relative);
97964
98857
  }, active, killTree = (child) => {
97965
98858
  try {
97966
98859
  if (child.pid && process.platform !== "win32")
@@ -98009,7 +98902,7 @@ __export(exports_test_audit, {
98009
98902
  run: () => run18
98010
98903
  });
98011
98904
  import { existsSync as existsSync22 } from "node:fs";
98012
- import path53 from "node:path";
98905
+ import path54 from "node:path";
98013
98906
  function parse7(argv) {
98014
98907
  const options = {
98015
98908
  paths: [],
@@ -98113,8 +99006,8 @@ async function run18(argv, io) {
98113
99006
  return emit(io, fail("invalid_input", `${options}
98114
99007
  usage: ${USAGE10}`));
98115
99008
  const top = git7(io.cwd, ["rev-parse", "--show-toplevel"]);
98116
- const repo = top.ok ? path53.resolve(top.stdout.trim()) : null;
98117
- const display = (file) => toPosix(path53.relative(io.cwd, file) || file);
99009
+ const repo = top.ok ? path54.resolve(top.stdout.trim()) : null;
99010
+ const display = (file) => toPosix(path54.relative(io.cwd, file) || file);
98118
99011
  let base = null;
98119
99012
  let changed = null;
98120
99013
  if (options.diff || options.mutate) {
@@ -98357,7 +99250,7 @@ __export(exports_youtrack, {
98357
99250
  run: () => run20
98358
99251
  });
98359
99252
  import { readFileSync as readFileSync25 } from "node:fs";
98360
- import path54 from "node:path";
99253
+ import path55 from "node:path";
98361
99254
  async function run20(argv, io) {
98362
99255
  const [sub, ...rest] = argv;
98363
99256
  if (sub !== "note" && sub !== "time" && sub !== "meeting")
@@ -98403,7 +99296,7 @@ async function run20(argv, io) {
98403
99296
  let markdown = flags.values.markdown ?? "";
98404
99297
  if (flags.values.file)
98405
99298
  try {
98406
- markdown = readFileSync25(path54.resolve(io.cwd, flags.values.file), "utf8");
99299
+ markdown = readFileSync25(path55.resolve(io.cwd, flags.values.file), "utf8");
98407
99300
  } catch (error) {
98408
99301
  return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE12);
98409
99302
  }
@@ -98472,7 +99365,7 @@ __export(exports_changelog, {
98472
99365
  run: () => run21
98473
99366
  });
98474
99367
  import { readFileSync as readFileSync26 } from "node:fs";
98475
- import path55 from "node:path";
99368
+ import path56 from "node:path";
98476
99369
  async function run21(argv, io) {
98477
99370
  const [sub, ...rest] = argv;
98478
99371
  if (sub !== "apply")
@@ -98518,7 +99411,7 @@ async function run21(argv, io) {
98518
99411
  ]);
98519
99412
  }
98520
99413
  var USAGE13 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
98521
- const text = raw === "-" ? readFileSync26(0, "utf8") : raw.startsWith("@") ? readFileSync26(path55.resolve(cwd, raw.slice(1)), "utf8") : raw;
99414
+ const text = raw === "-" ? readFileSync26(0, "utf8") : raw.startsWith("@") ? readFileSync26(path56.resolve(cwd, raw.slice(1)), "utf8") : raw;
98522
99415
  return JSON.parse(text);
98523
99416
  };
98524
99417
  var init_changelog2 = __esm(() => {
@@ -98614,7 +99507,7 @@ var init_handoff = __esm(() => {
98614
99507
 
98615
99508
  // packages/workit-cli/src/main.ts
98616
99509
  init_package();
98617
- import path56 from "node:path";
99510
+ import path57 from "node:path";
98618
99511
 
98619
99512
  // packages/workit-cli/src/help.ts
98620
99513
  var HELP_FLAGS = new Set(["--help", "-h"]);
@@ -98879,7 +99772,7 @@ var VERBS = [
98879
99772
  {
98880
99773
  name: "check",
98881
99774
  group: "delivery",
98882
- usage: "workit check <name> [--task <id>] [--base <ref>] | workit check [--name <n>] [--shell] [--timeout <s>] [--task <id>] [--base <ref>] -- <cmd…>",
99775
+ usage: "workit check <name> [--task <id>] [--base <ref>] [--no-lease] | workit check [--name <n>] [--shell] [--timeout <s>] [--task <id>] [--base <ref>] [--heavy] [--no-lease] -- <cmd…>",
98883
99776
  summary: "Run a configured or ad-hoc check and record CLI-observed evidence (exit = the command's)",
98884
99777
  examples: ["workit check test", "workit check --name unit -- bun test test/unit"],
98885
99778
  load: () => Promise.resolve().then(() => (init_check(), exports_check))
@@ -98921,8 +99814,8 @@ var VERBS = [
98921
99814
  },
98922
99815
  {
98923
99816
  name: "merge",
98924
- usage: "workit pr merge [--pr <n>] [--method squash|merge|rebase] [--delete-branch] [--unverified --reason <why>]",
98925
- summary: "Merge when READY, verified by a non-author and granted"
99817
+ usage: "workit pr merge [--pr <n>] [--method squash|merge|rebase] [--subject <s>] [--body <b> | --body-file <f|->] [--delete-branch] [--unverified --reason <why>]",
99818
+ summary: "Merge when READY, verified by a non-author and granted; --subject/--body set the squash message"
98926
99819
  }
98927
99820
  ],
98928
99821
  examples: [
@@ -98962,18 +99855,24 @@ var VERBS = [
98962
99855
  },
98963
99856
  {
98964
99857
  name: "commit",
98965
- usage: "workit git commit -m|--message <msg>… | -F|--file <file|-> [--amend [--no-edit]] [--allow-empty] [-a|--all | [--] <paths…>]",
98966
- summary: "Commit (or --amend) with a linted Conventional Commit message and the session trailer"
99858
+ usage: "workit git commit -m|--message <msg>… | -F|--file <file|-> [--trailer <key: value>]… [--amend [--no-edit]] [--allow-empty] [-a|--all | [--] <paths…>]",
99859
+ summary: "Commit (or --amend) with a linted Conventional Commit message, the session trailer (unless the workspace sets trailers: false) and any --trailer"
98967
99860
  },
98968
99861
  {
98969
99862
  name: "push",
98970
99863
  usage: "workit git push [-u|--set-upstream] [--force-with-lease [--expect <sha>] [--overwrite-unintegrated]] [--release <main|develop|v-tag>…]",
98971
99864
  summary: "Push the current branch and verify the remote tip (never a blind --force); --release finishes a git-flow release"
99865
+ },
99866
+ {
99867
+ name: "trailers",
99868
+ usage: "workit git trailers",
99869
+ summary: "Print on or off: whether commits in this workspace get the Workit-Session trailer"
98972
99870
  }
98973
99871
  ],
98974
99872
  examples: [
98975
99873
  "workit git branch --kind feature --slug help-everywhere",
98976
99874
  'workit git commit -m "fix(cli): answer help flags first" --all',
99875
+ 'workit git commit -m "feat: x" --trailer "Co-Authored-By: Name <email>" --all',
98977
99876
  "workit git commit --amend --no-edit -- src/forgotten.ts",
98978
99877
  "workit git push --set-upstream"
98979
99878
  ],
@@ -99260,7 +100159,7 @@ var defaultIo = (json) => ({
99260
100159
  });
99261
100160
  async function main(argv, overrides = {}, options = {}) {
99262
100161
  const parsed = parseGlobals(argv);
99263
- const cwd = path56.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
100162
+ const cwd = path57.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
99264
100163
  const io = { ...defaultIo(parsed.json), ...overrides, json: parsed.json, cwd };
99265
100164
  if (parsed.error)
99266
100165
  return emit(io, fail("invalid_input", parsed.error));