@brainervirus/workit-cli 8.12.3 → 8.13.1

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 +1549 -319
  2. package/package.json +5 -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.12.3",
68
+ version: "8.13.1",
69
69
  private: false,
70
70
  description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
71
71
  keywords: [
@@ -103,13 +103,14 @@ var init_package = __esm(() => {
103
103
  },
104
104
  dependencies: {
105
105
  "@babel/parser": "7.29.9",
106
- "@brainervirus/workit-core": "^8.12.3",
107
- "@brainervirus/workit-cursor": "^8.12.3",
108
- "@brainervirus/workit-opencode": "^8.12.3",
106
+ "@brainervirus/workit-cursor": "^8.13.1",
109
107
  "@inkjs/ui": "2.0.0",
110
108
  ink: "7.1.1",
111
109
  react: "19.2.8"
112
110
  },
111
+ devDependencies: {
112
+ "@brainervirus/workit-core": "^8.13.1"
113
+ },
113
114
  engines: {
114
115
  node: ">=24"
115
116
  }
@@ -10429,7 +10430,7 @@ var init_paths = __esm(() => {
10429
10430
  });
10430
10431
 
10431
10432
  // packages/workit-core/src/host-session.ts
10432
- import { createHash as createHash3 } from "node:crypto";
10433
+ import { createHash as createHash3, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
10433
10434
  import fs6 from "node:fs";
10434
10435
  import path6 from "node:path";
10435
10436
  function hostSessionFromEnv(env) {
@@ -10482,7 +10483,12 @@ function migrateRepoIssued(cwd) {
10482
10483
  const to = path6.join(userIssuedRoot(), lead);
10483
10484
  fs6.mkdirSync(to, { recursive: true, mode: 448 });
10484
10485
  for (const id of fs6.readdirSync(from)) {
10485
- fs6.copyFileSync(path6.join(from, id), path6.join(to, id));
10486
+ try {
10487
+ fs6.copyFileSync(path6.join(from, id), path6.join(to, id), fs6.constants.COPYFILE_EXCL);
10488
+ } catch (error) {
10489
+ if (error.code !== "EEXIST")
10490
+ throw error;
10491
+ }
10486
10492
  fs6.rmSync(path6.join(from, id), { force: true });
10487
10493
  }
10488
10494
  removeIfEmpty(from);
@@ -10490,7 +10496,25 @@ function migrateRepoIssued(cwd) {
10490
10496
  }
10491
10497
  removeIfEmpty(old);
10492
10498
  }
10493
- function sessionClaimRefusal(env, claimed, cwd) {
10499
+ function issuedBinding(cwd, claimed) {
10500
+ const record = issuedRecord(cwd, claimed);
10501
+ const { stored, leadBound } = issuedToken(claimed);
10502
+ if (stored)
10503
+ return "token";
10504
+ if (record === null)
10505
+ return null;
10506
+ return leadBound ? "token" : "legacy";
10507
+ }
10508
+ function issuedTokenMatches(cwd, claimed, token) {
10509
+ migrateRepoIssued(cwd);
10510
+ const { stored } = issuedToken(claimed);
10511
+ if (!stored || !token)
10512
+ return false;
10513
+ const given = Buffer.from(tokenHash(token.trim()));
10514
+ const want = Buffer.from(stored);
10515
+ return given.length === want.length && timingSafeEqual(given, want);
10516
+ }
10517
+ function sessionClaimRefusal(env, claimed, cwd, options = {}) {
10494
10518
  const real = realHostSession(env);
10495
10519
  const host = family(real.host);
10496
10520
  if (real.kind !== "derived" && host !== null && HOST_SET_VARIABLE[host])
@@ -10500,18 +10524,22 @@ function sessionClaimRefusal(env, claimed, cwd) {
10500
10524
  };
10501
10525
  if (real.kind === "none" || !real.session)
10502
10526
  return null;
10503
- let issuedClaim = false;
10504
- if (claimed === real.session) {
10505
- const declaredChild = real.kind === "declared" && host === "claude_code" && claimed.includes(":");
10506
- if (!declaredChild || wasIssued(cwd, leadOf(claimed), claimed))
10507
- return null;
10508
- issuedClaim = true;
10509
- } else if (real.kind === "derived" && claimed.endsWith(`:${real.session}`))
10510
- return null;
10511
- else if (claimed.startsWith(`${real.session}:`)) {
10512
- if (wasIssued(cwd, real.session, claimed))
10527
+ const ownShell = claimed === real.session && !(real.kind === "declared" && claimed.includes(":")) || real.kind === "derived" && claimed.endsWith(`:${real.session}`);
10528
+ if (ownShell)
10529
+ return options.flag ? {
10530
+ error: `session_flag: --session ${claimed} names this shell's own ${real.host} session; on a host only an issued subagent id is passed as --session`,
10531
+ unblock: "drop --session: the shell's own session is already the acting one"
10532
+ } : null;
10533
+ const issuedClaim = claimed === real.session || claimed.startsWith(`${real.session}:`);
10534
+ if (issuedClaim) {
10535
+ const binding = issuedBinding(cwd, claimed);
10536
+ if (binding === "legacy")
10513
10537
  return null;
10514
- issuedClaim = true;
10538
+ if (binding === "token")
10539
+ return issuedTokenMatches(cwd, claimed, options.token) ? null : {
10540
+ error: `unbound_session: ${claimed} was issued to a subagent, and this call did not present the token its SubagentStart hook gave it`,
10541
+ unblock: `only that subagent acts as ${claimed}: it passes --token exactly as its SubagentStart context names it (or WORKIT_SESSION_TOKEN); anyone else spawns their own verifier`
10542
+ };
10515
10543
  }
10516
10544
  const error = `forged_session: ${claimed} is neither this shell's session (${real.session}) nor an id its host issued to a child of it`;
10517
10545
  if (issuedClaim && noIssuedRecord(cwd, leadOf(claimed)) && host !== null && PLUGIN_UPDATE[host])
@@ -10522,10 +10550,10 @@ function sessionClaimRefusal(env, claimed, cwd) {
10522
10550
  const ownChildShell = host !== null && OWN_CHILD_SHELL.has(host);
10523
10551
  return {
10524
10552
  error,
10525
- unblock: ownChildShell ? `record the verdict from the verifier's own ${host === "pi" ? "Pi worker" : "OpenCode child session"} shell, where WORKIT_SESSION_ID is already set; never set WORKIT_SESSION_ID or --session by hand` : "never name a session yourself: spawn a verifier or reviewer subagent and pass exactly the --session its SubagentStart context names, or pass --as verifier"
10553
+ unblock: ownChildShell ? `record the verdict from the verifier's own ${host === "pi" ? "Pi worker" : "OpenCode child session"} shell, where WORKIT_SESSION_ID is already set; never set WORKIT_SESSION_ID or --session by hand` : "never name a session yourself: spawn a verifier or reviewer subagent and pass exactly the --session and --token its SubagentStart context names, or pass --as verifier"
10526
10554
  };
10527
10555
  }
10528
- var HOST_SESSION_VARS, family = (host) => host?.replace(/_(?:cli|desktop)$/, "") ?? null, hash = (value) => createHash3("sha256").update(value).digest("hex").slice(0, 32), userIssuedRoot = () => path6.join(resolveStateDir(), "issued"), safeList = (dir) => {
10556
+ var HOST_SESSION_VARS, family = (host) => host?.replace(/_(?:cli|desktop)$/, "") ?? null, hash = (value) => createHash3("sha256").update(value).digest("hex").slice(0, 32), userIssuedRoot = () => path6.join(resolveStateDir(), "issued"), userTokenRoot = () => path6.join(resolveStateDir(), "issued-tokens"), safeList = (dir) => {
10529
10557
  try {
10530
10558
  return fs6.readdirSync(dir);
10531
10559
  } catch {
@@ -10544,12 +10572,26 @@ var HOST_SESSION_VARS, family = (host) => host?.replace(/_(?:cli|desktop)$/, "")
10544
10572
  } catch {
10545
10573
  return false;
10546
10574
  }
10547
- }, PLUGIN_UPDATE, wasIssued = (cwd, lead, claimed) => {
10575
+ }, PLUGIN_UPDATE, tokenHash = (token) => createHash3("sha256").update(token).digest("hex"), issuedRecord = (cwd, claimed) => {
10576
+ const cut = claimed.lastIndexOf(":");
10577
+ if (cut <= 0)
10578
+ return null;
10548
10579
  migrateRepoIssued(cwd);
10549
10580
  try {
10550
- return fs6.existsSync(path6.join(userIssuedRoot(), hash(lead), hash(claimed)));
10581
+ return fs6.readFileSync(path6.join(userIssuedRoot(), hash(claimed.slice(0, cut)), hash(claimed)), "utf8");
10551
10582
  } catch {
10552
- return false;
10583
+ return null;
10584
+ }
10585
+ }, TOKEN_HASH, issuedToken = (claimed) => {
10586
+ const cut = claimed.lastIndexOf(":");
10587
+ if (cut <= 0)
10588
+ return { stored: null, leadBound: false };
10589
+ const dir = path6.join(userTokenRoot(), hash(claimed.slice(0, cut)));
10590
+ try {
10591
+ const text = fs6.readFileSync(path6.join(dir, hash(claimed)), "utf8");
10592
+ return { stored: TOKEN_HASH.exec(text)?.[1] ?? null, leadBound: true };
10593
+ } catch {
10594
+ return { stored: null, leadBound: safeList(dir).length > 0 };
10553
10595
  }
10554
10596
  }, leadOf = (session) => session.slice(0, session.lastIndexOf(":")), OWN_CHILD_SHELL, HOST_SET_VARIABLE;
10555
10597
  var init_host_session = __esm(() => {
@@ -10567,6 +10609,7 @@ var init_host_session = __esm(() => {
10567
10609
  claude_code: "claude plugin marketplace update workit && claude plugin update workit@workit",
10568
10610
  codex: "codex plugin remove workit@workit && codex plugin add workit@workit"
10569
10611
  };
10612
+ TOKEN_HASH = /^([0-9a-f]{64})$/mu;
10570
10613
  OWN_CHILD_SHELL = new Set(["opencode", "pi"]);
10571
10614
  HOST_SET_VARIABLE = {
10572
10615
  opencode: "OPENCODE_SESSION_ID",
@@ -10599,7 +10642,7 @@ import { spawnSync as spawnSync3 } from "node:child_process";
10599
10642
  import fs7 from "node:fs";
10600
10643
  import { hostname } from "node:os";
10601
10644
  import path8 from "node:path";
10602
- var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, RECLAIM_GUARD_TTL_MS = 30000, defaultLockTimeoutMs = 250, defaultLockTimeout = () => defaultLockTimeoutMs, setDefaultLockTimeout = (ms) => {
10645
+ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, RECLAIM_GUARD_TTL_MS = 30000, IN_PROCESS_LOCK_TIMEOUT_MS = 250, defaultLockTimeoutMs, defaultLockTimeout = () => defaultLockTimeoutMs, hostAwareLockTimeout = (cliMs) => defaultLockTimeoutMs > IN_PROCESS_LOCK_TIMEOUT_MS ? cliMs : IN_PROCESS_LOCK_TIMEOUT_MS, setDefaultLockTimeout = (ms) => {
10603
10646
  defaultLockTimeoutMs = ms;
10604
10647
  }, parseMetadataLock = (raw) => {
10605
10648
  let value;
@@ -10789,6 +10832,7 @@ var init_store_lock = __esm(() => {
10789
10832
  externalAction: literal(true).optional()
10790
10833
  }).strict();
10791
10834
  FOREIGN_LOCK_TTL_MS = 10 * 60000;
10835
+ defaultLockTimeoutMs = IN_PROCESS_LOCK_TIMEOUT_MS;
10792
10836
  TASK_STORE_LOCK = { reclaimUnverifiedAfterTtl: true };
10793
10837
  });
10794
10838
 
@@ -13697,6 +13741,14 @@ var PREFIXES, branchNames = (workspaceRoot) => {
13697
13741
  }, repoBranchPreset = (workspaceRoot) => {
13698
13742
  const detected = detectBranchPolicy(workspaceRoot);
13699
13743
  return detected.developBranch || detected.allowed.length ? detected.preset : null;
13744
+ }, releaseTagPrefixOf = (workspaceRoot, policyPrefix) => {
13745
+ if (typeof policyPrefix === "string")
13746
+ return policyPrefix.trim();
13747
+ const r = spawnSync5("git", ["config", "--get", "gitflow.prefix.versiontag"], {
13748
+ cwd: workspaceRoot,
13749
+ encoding: "utf8"
13750
+ });
13751
+ return r.status === 0 ? (r.stdout ?? "").trim() : "v";
13700
13752
  };
13701
13753
  var init_branch_policy = __esm(() => {
13702
13754
  init_config();
@@ -14781,7 +14833,7 @@ var init_vcs_config = __esm(() => {
14781
14833
 
14782
14834
  // packages/workit-core/src/ledger.ts
14783
14835
  import { spawnSync as spawnSync9 } from "node:child_process";
14784
- import { createHash as createHash9, randomBytes as randomBytes2 } from "node:crypto";
14836
+ import { createHash as createHash9, randomBytes as randomBytes3 } from "node:crypto";
14785
14837
  import fs12 from "node:fs";
14786
14838
  import path15 from "node:path";
14787
14839
  function resolveCommit(cwd, ref) {
@@ -14870,7 +14922,7 @@ function actorFromEnv(env) {
14870
14922
  agentId: value("WORKIT_AGENT_ID")
14871
14923
  };
14872
14924
  }
14873
- function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
14925
+ function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
14874
14926
  const lock = `${file}.lock`;
14875
14927
  const deadline = Date.now() + waitMs;
14876
14928
  let token = null;
@@ -14894,8 +14946,10 @@ function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
14894
14946
  }
14895
14947
  function appendLine(file, build, options) {
14896
14948
  const write = () => {
14897
- const fd = fs12.openSync(file, "a+");
14949
+ const fd = fs12.openSync(file, "a+", 384);
14898
14950
  try {
14951
+ if (process.platform !== "win32")
14952
+ tighten(fd);
14899
14953
  const size = fs12.fstatSync(fd).size;
14900
14954
  let fence = "";
14901
14955
  if (size > 0) {
@@ -14974,7 +15028,9 @@ function readLedger(cwd, options = {}) {
14974
15028
  if (!file.ok)
14975
15029
  return file;
14976
15030
  let text = "";
15031
+ let stat = null;
14977
15032
  try {
15033
+ stat = fs12.statSync(file.value);
14978
15034
  text = fs12.readFileSync(file.value, "utf8");
14979
15035
  } catch (error) {
14980
15036
  if (error.code === "ENOENT")
@@ -14994,6 +15050,7 @@ function readLedger(cwd, options = {}) {
14994
15050
  const integrity = { chained: 0, legacy: 0, unverified: [] };
14995
15051
  const earlier = new Set([null]);
14996
15052
  let chainStarted = false;
15053
+ const ids = new Set;
14997
15054
  for (const raw of text.split(`
14998
15055
  `)) {
14999
15056
  const trimmed = raw.trim();
@@ -15027,6 +15084,10 @@ function readLedger(cwd, options = {}) {
15027
15084
  reason = "unsigned";
15028
15085
  else
15029
15086
  rowIntegrity = "legacy";
15087
+ if (options.integrity && !reason && ids.has(parsed.id))
15088
+ reason = "duplicate_id";
15089
+ if (options.integrity)
15090
+ ids.add(parsed.id);
15030
15091
  if (reason)
15031
15092
  rowIntegrity = "unverified";
15032
15093
  const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
@@ -15050,7 +15111,8 @@ function readLedger(cwd, options = {}) {
15050
15111
  seq: rows.length + 1,
15051
15112
  superseded: false,
15052
15113
  supersedeIgnored: false,
15053
- ...rowIntegrity ? { integrity: rowIntegrity } : {}
15114
+ ...rowIntegrity ? { integrity: rowIntegrity } : {},
15115
+ ...reason ? { integrityReason: reason } : {}
15054
15116
  };
15055
15117
  rows.push(row);
15056
15118
  if (!options.integrity)
@@ -15078,18 +15140,44 @@ function readLedger(cwd, options = {}) {
15078
15140
  const byId = new Map;
15079
15141
  for (const row of rows) {
15080
15142
  const target = row.supersedes ? byId.get(row.supersedes) : undefined;
15081
- if (target && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
15143
+ if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
15082
15144
  target.superseded = true;
15083
15145
  else if (row.supersedes)
15084
15146
  row.supersedeIgnored = true;
15085
15147
  if (!byId.has(row.id))
15086
15148
  byId.set(row.id, row);
15087
15149
  }
15150
+ if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
15151
+ tamperCache.set(file.value, {
15152
+ size: stat.size,
15153
+ stamp: fileStamp(stat),
15154
+ breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
15155
+ });
15088
15156
  return {
15089
15157
  ok: true,
15090
15158
  value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
15091
15159
  };
15092
15160
  }
15161
+ function ledgerTampering(cwd) {
15162
+ const file = ledgerPath(cwd);
15163
+ if (!file.ok)
15164
+ return [];
15165
+ try {
15166
+ const stat = fs12.statSync(file.value);
15167
+ const cached = tamperCache.get(file.value);
15168
+ if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
15169
+ return cached.breaks;
15170
+ } catch {
15171
+ return [];
15172
+ }
15173
+ const read = readLedger(cwd, { integrity: true });
15174
+ return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
15175
+ }
15176
+ function tamperedUnblock(cwd) {
15177
+ const file = ledgerPath(cwd);
15178
+ const where = file.ok ? file.value : "the ledger file";
15179
+ return `workit ledger verify-integrity # lists the edited or missing rows; if the edit is understood, move ${where} aside (mv ${where} ${where}.tampered-<date>) and record the verdicts again`;
15180
+ }
15093
15181
  function filterRows(rows, filter) {
15094
15182
  const matched = rows.filter((row) => (filter.branch === undefined || row.branch === filter.branch) && (filter.pr === undefined || row.pr === filter.pr) && (filter.type === undefined || row.type === filter.type));
15095
15183
  return filter.last === undefined ? matched : matched.slice(Math.max(0, matched.length - filter.last));
@@ -15205,15 +15293,51 @@ function authorSessions(cwd, rows, branch, range = { base: null, head: null }, s
15205
15293
  sessions.add(session);
15206
15294
  return sessions;
15207
15295
  }
15208
- function effectiveOf(rows, authors, sameCode, isCurrent) {
15296
+ function sessionTrust(cwd, row) {
15297
+ const session = row.actor.session;
15298
+ if (!session || row.self === true)
15299
+ return null;
15300
+ const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
15301
+ const host = row.actor.host;
15302
+ const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
15303
+ if (!issuedOnly)
15304
+ return null;
15305
+ const binding = issuedBinding(cwd, session);
15306
+ if (binding === null)
15307
+ return "unissued_session";
15308
+ return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
15309
+ }
15310
+ function trustReasons(cwd, row) {
15311
+ if (row.integrity === "unverified")
15312
+ return [row.integrityReason ?? "unsigned"];
15313
+ if (row.integrity !== "verified")
15314
+ return ["unsigned"];
15315
+ const session = sessionTrust(cwd, row);
15316
+ return session ? [session] : [];
15317
+ }
15318
+ function withIntegrity(cwd, rows) {
15319
+ if (rows.every((row) => row.integrity !== undefined))
15320
+ return rows;
15321
+ const read = readLedger(cwd, { integrity: true });
15322
+ const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
15323
+ return rows.map((row) => {
15324
+ if (row.integrity !== undefined)
15325
+ return row;
15326
+ const fresh = bySeq.get(row.seq);
15327
+ return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
15328
+ });
15329
+ }
15330
+ function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
15209
15331
  let effective = null;
15210
15332
  for (const row of rows) {
15211
15333
  if (!effective) {
15212
15334
  effective = row;
15213
15335
  continue;
15214
15336
  }
15215
- const rowIndependent = independenceReasons(row, authors).length === 0;
15216
- const effIndependent = independenceReasons(effective, authors).length === 0;
15337
+ if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
15338
+ continue;
15339
+ const rowIndependent = reasonsOf(row).length === 0;
15340
+ const effIndependent = reasonsOf(effective).length === 0;
15217
15341
  if (effIndependent && !rowIndependent && isCurrent(effective))
15218
15342
  continue;
15219
15343
  if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
@@ -15227,7 +15351,7 @@ function restackCarries(rows, branch, from, to) {
15227
15351
  return false;
15228
15352
  const edges = new Map;
15229
15353
  for (const row of rows)
15230
- if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
15354
+ if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.integrity === "verified" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
15231
15355
  edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
15232
15356
  const seen = new Set([from]);
15233
15357
  const queue = [from];
@@ -15243,7 +15367,8 @@ function restackCarries(rows, branch, from, to) {
15243
15367
  }
15244
15368
  return false;
15245
15369
  }
15246
- function checkVerdicts(cwd, branch, rows) {
15370
+ function checkVerdicts(cwd, branch, ledgerRows) {
15371
+ const rows = withIntegrity(cwd, ledgerRows);
15247
15372
  const head = branchHead(cwd, branch);
15248
15373
  const fallbackBase = defaultBase(cwd);
15249
15374
  const scope = branchScope(cwd, branch);
@@ -15273,21 +15398,47 @@ function checkVerdicts(cwd, branch, rows) {
15273
15398
  return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(rows, branch, failed.head, row.head);
15274
15399
  };
15275
15400
  const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
15401
+ const failureAuthorCache = new Map;
15402
+ const failureAuthors = (row) => {
15403
+ let set = failureAuthorCache.get(row.seq);
15404
+ if (!set) {
15405
+ set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
15406
+ failureAuthorCache.set(row.seq, set);
15407
+ }
15408
+ return set;
15409
+ };
15410
+ const reasonCache = new Map;
15411
+ const reasonsOf = (row) => {
15412
+ let reasons = reasonCache.get(row);
15413
+ if (!reasons) {
15414
+ const failing = FAILING.has(String(row.result));
15415
+ reasons = [
15416
+ ...trustReasons(cwd, row),
15417
+ ...independenceReasons(row, failing ? failureAuthors(row) : authors)
15418
+ ];
15419
+ reasonCache.set(row, reasons);
15420
+ }
15421
+ return reasons;
15422
+ };
15423
+ const retired = new Set;
15424
+ for (const row of rows)
15425
+ if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
15426
+ retired.add(row.supersedes);
15276
15427
  const byKind = new Map;
15277
15428
  for (const row of rows)
15278
- if (isLiveVerdict(row) && scope(row)) {
15429
+ if (isLiveVerdict(row, retired) && scope(row)) {
15279
15430
  const kind = typeof row.kind === "string" ? row.kind : "review";
15280
15431
  byKind.set(kind, [...byKind.get(kind) ?? [], row]);
15281
15432
  }
15282
15433
  const verdicts = [];
15283
15434
  const warnings = [];
15284
15435
  for (const [kind, list] of byKind) {
15285
- const row = effectiveOf(list, authors, sameCode, (candidate) => basisOf(candidate) !== "stale");
15436
+ const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
15286
15437
  if (!row)
15287
15438
  continue;
15288
15439
  const basis = basisOf(row);
15289
15440
  const current = basis !== "stale";
15290
- const independence = independenceReasons(row, authors);
15441
+ const independence = reasonsOf(row);
15291
15442
  const reasons = [
15292
15443
  ...current ? [] : ["stale"],
15293
15444
  ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
@@ -15304,23 +15455,31 @@ function checkVerdicts(cwd, branch, rows) {
15304
15455
  });
15305
15456
  if (reasons.length === 0) {
15306
15457
  for (const later of list)
15307
- if (later.seq > row.seq && independenceReasons(later, authors).length > 0 && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
15458
+ if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
15308
15459
  warnings.push(`the author's own (self) ${String(later.result)} ${kind} verdict ${later.id} by ${later.actor.session ?? "no session"} on the current code is newer than the accepted pass ${row.id}: read it before merging`);
15309
15460
  }
15310
15461
  }
15311
15462
  for (const row of rows)
15312
15463
  if (row.integrity === "unverified" && scope(row))
15313
15464
  warnings.push(`ledger row ${row.id} (${row.type}) on ${row.branch ?? branch} is unverified: the workit CLI did not write it or it changed afterwards (workit ledger verify-integrity)`);
15465
+ const tampered = ledgerTampering(cwd);
15466
+ if (tampered.length) {
15467
+ warnings.push(tamperedWarning(cwd, tampered));
15468
+ for (const entry of verdicts) {
15469
+ entry.accepted = false;
15470
+ entry.reasons = [...entry.reasons, "ledger_tampered"];
15471
+ }
15472
+ }
15314
15473
  verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
15315
15474
  const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
15316
15475
  const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
15317
15476
  const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
15318
- const accepted = newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
15477
+ const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
15319
15478
  accepted: false,
15320
15479
  verdict: failing?.verdict ?? null,
15321
15480
  reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
15322
15481
  };
15323
- const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && STRONG_RESULTS.has(String(entry.verdict.result)));
15482
+ const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && !untrusted(entry.reasons) && STRONG_RESULTS.has(String(entry.verdict.result)));
15324
15483
  return {
15325
15484
  branch,
15326
15485
  head,
@@ -15515,6 +15674,7 @@ function recordVerdict(context, input) {
15515
15674
  self: self2,
15516
15675
  selfReason,
15517
15676
  sessionSource: context.sessionSource ?? "env",
15677
+ ...context.sessionBound === true ? { sessionBound: true } : {},
15518
15678
  evidenceRefs: input.evidenceRefs ?? []
15519
15679
  }, { now: context.now });
15520
15680
  }
@@ -15743,7 +15903,7 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, VERDICT_RESULTS, PASSING, FAILING
15743
15903
  } catch {
15744
15904
  return null;
15745
15905
  }
15746
- }, newId2 = () => `${Date.now().toString(36)}-${randomBytes2(6).toString("hex")}`, NETWORK_FS_TYPES, isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0), onNetworkFs = (dir) => {
15906
+ }, newId2 = () => `${Date.now().toString(36)}-${randomBytes3(6).toString("hex")}`, NETWORK_FS_TYPES, isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0), onNetworkFs = (dir) => {
15747
15907
  if (process.platform !== "linux")
15748
15908
  return false;
15749
15909
  try {
@@ -15796,6 +15956,11 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
15796
15956
  return text || null;
15797
15957
  }
15798
15958
  return null;
15959
+ }, tighten = (fd) => {
15960
+ try {
15961
+ if ((fs12.fstatSync(fd).mode & 63) !== 0)
15962
+ fs12.fchmodSync(fd, 384);
15963
+ } catch {}
15799
15964
  }, sealed = (row, id, at, previous) => {
15800
15965
  const { prevHash: _prev, rowHash: _row, ...fields } = row;
15801
15966
  const prevHash = previous === null ? null : sha2562(previous);
@@ -15810,7 +15975,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
15810
15975
  session: str2(actor.session),
15811
15976
  agentId: str2(actor.agentId)
15812
15977
  };
15813
- }, isLiveVerdict = (row) => row.type === "verdict" && typeof row.result === "string" && !row.superseded, RENAMED, renameAwareBranch = (scopeOf, branch) => {
15978
+ }, TAMPER_REASONS, tamperCache, fileStamp = (stat) => `${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}`, tamperedWarning = (cwd, breaks) => `the ledger was edited, or a row was removed or moved (${breaks.slice(0, 3).map((row) => `${row.reason} at row ${row.seq}`).join(", ")}${breaks.length > 3 ? ", …" : ""}): no verdict is accepted until it is repaired. ${tamperedUnblock(cwd)}`, isLiveVerdict = (row, retired) => row.type === "verdict" && typeof row.result === "string" && !retired.has(row.id), RENAMED, renameAwareBranch = (scopeOf, branch) => {
15814
15979
  return (target) => {
15815
15980
  const own = typeof branch === "string" ? branch : null;
15816
15981
  const theirs = typeof target.branch === "string" ? target.branch : null;
@@ -15827,7 +15992,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
15827
15992
  if (derivesFrom(author, session) || derivesFrom(session, author))
15828
15993
  return true;
15829
15994
  return false;
15830
- }, independenceReasons = (row, authors) => {
15995
+ }, TRUST_REASONS, SHELL_VOUCHED_HOSTS, untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason)), independenceReasons = (row, authors) => {
15831
15996
  const reasons = [];
15832
15997
  if (!row.actor.session)
15833
15998
  reasons.push("no_session");
@@ -15889,6 +16054,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
15889
16054
  return brief.pr === null ? "workit pr create" : `workit pr status --pr ${brief.pr}`;
15890
16055
  };
15891
16056
  var init_ledger = __esm(() => {
16057
+ init_store_lock();
15892
16058
  init_vcs_config();
15893
16059
  init_rev();
15894
16060
  init_host_session();
@@ -15927,14 +16093,14 @@ var init_ledger = __esm(() => {
15927
16093
  return null;
15928
16094
  throw error;
15929
16095
  }
15930
- const token = `${process.pid}-${randomBytes2(8).toString("hex")}`;
16096
+ const token = `${process.pid}-${randomBytes3(8).toString("hex")}`;
15931
16097
  fs12.writeFileSync(path15.join(lock, LOCK_OWNER), token);
15932
16098
  return token;
15933
16099
  },
15934
16100
  release(lock, token) {
15935
16101
  if (lockToken(lock) !== token)
15936
16102
  return false;
15937
- const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
16103
+ const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
15938
16104
  try {
15939
16105
  fs12.renameSync(lock, tomb);
15940
16106
  } catch {
@@ -15951,7 +16117,7 @@ var init_ledger = __esm(() => {
15951
16117
  },
15952
16118
  token: lockToken,
15953
16119
  reap(lock, observed) {
15954
- const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
16120
+ const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
15955
16121
  try {
15956
16122
  fs12.renameSync(lock, tomb);
15957
16123
  } catch {
@@ -15970,7 +16136,24 @@ var init_ledger = __esm(() => {
15970
16136
  }
15971
16137
  };
15972
16138
  TAIL_BYTES = 64 * 1024;
16139
+ TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
16140
+ tamperCache = new Map;
15973
16141
  RENAMED = /^branch: renamed refs\/heads\/(.+) to refs\/heads\/(.+)$/iu;
16142
+ TRUST_REASONS = new Set([
16143
+ "unsigned",
16144
+ "hash_mismatch",
16145
+ "broken_link",
16146
+ "duplicate_id",
16147
+ "unissued_session",
16148
+ "unbound_session"
16149
+ ]);
16150
+ SHELL_VOUCHED_HOSTS = new Set([
16151
+ "cli",
16152
+ "opencode",
16153
+ "pi",
16154
+ "codex_cli",
16155
+ "codex_desktop"
16156
+ ]);
15974
16157
  OBSERVED_ONLY = new Set([
15975
16158
  "check",
15976
16159
  "merge.unverified",
@@ -17311,6 +17494,7 @@ var init_shell_words = () => {};
17311
17494
  var SHELLS, POWERSHELLS, EVALS, WRAPPERS, WRAPPER_VALUES, KEYWORDS, DECLARATIONS, CD, PUSHD, POPD, GIT_VALUE_OPTIONS, GIT_CONFIG_OPTIONS, XARGS_VALUES, PARALLEL_VALUES, RECENT, LAUNCHERS, PUSH_VALUE_OPTIONS, VALUE_OPTIONS, API_VALUES, API_FIELDS, GIT_COMMANDS;
17312
17495
  var init_raw_git = __esm(() => {
17313
17496
  init_branch();
17497
+ init_branch_policy();
17314
17498
  init_workspaces();
17315
17499
  init_ledger();
17316
17500
  init_paths();
@@ -63704,7 +63888,8 @@ var init_changelog = __esm(() => {
63704
63888
  });
63705
63889
 
63706
63890
  // packages/workit-core/src/core/hygiene.ts
63707
- import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
63891
+ import { spawnSync as spawnSync12 } from "node:child_process";
63892
+ import { existsSync as existsSync9, readFileSync as readFileSync9, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
63708
63893
  import path26 from "node:path";
63709
63894
  var repoRoot2, templatesDir = () => path26.join(repoRoot2, "templates", "hygiene"), packageJson = (root) => {
63710
63895
  if (!existsSync9(path26.join(root, "package.json")))
@@ -63734,6 +63919,23 @@ var repoRoot2, templatesDir = () => path26.join(repoRoot2, "templates", "hygiene
63734
63919
  }
63735
63920
  }
63736
63921
  return "";
63922
+ }, projectTarget = (dir) => {
63923
+ const start = path26.resolve(dir);
63924
+ const top = spawnSync12("git", ["rev-parse", "--show-toplevel"], { cwd: start, encoding: "utf8" });
63925
+ const root = top.status === 0 ? (top.stdout ?? "").trim() : "";
63926
+ if (root)
63927
+ return { ok: true, root: path26.resolve(root) };
63928
+ let children = [];
63929
+ try {
63930
+ children = readdirSync5(start, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync9(path26.join(start, entry.name, ".git"))).map((entry) => path26.join(start, entry.name)).toSorted();
63931
+ } catch {
63932
+ children = [];
63933
+ }
63934
+ const where = children.length > 0 ? `run it inside one of its repositories: ${children.slice(0, 5).map((child) => `cd ${child}`).join(", ")}${children.length > 5 ? ` (+${children.length - 5} more)` : ""}` : "cd <repo> (or git init here) and run workit init again";
63935
+ return {
63936
+ ok: false,
63937
+ refused: `${start} is not a git repository, so no project files were added there; ${where}`
63938
+ };
63737
63939
  }, planHygieneFiles = (root, opts = {}) => {
63738
63940
  const openSource = opts.includeOpenSource ?? isOpenSource(root);
63739
63941
  const files = ["CHANGELOG.md", "README.md", ".editorconfig", ".gitattributes"];
@@ -63958,13 +64160,13 @@ var init_support_matrix = __esm(() => {
63958
64160
  });
63959
64161
 
63960
64162
  // packages/workit-cli/src/admin/host-install.ts
63961
- import { spawnSync as spawnSync12 } from "node:child_process";
64163
+ import { spawnSync as spawnSync13 } from "node:child_process";
63962
64164
  import {
63963
64165
  accessSync,
63964
64166
  constants as constants2,
63965
64167
  existsSync as existsSync11,
63966
64168
  readFileSync as readFileSync11,
63967
- readdirSync as readdirSync5,
64169
+ readdirSync as readdirSync6,
63968
64170
  realpathSync as realpathSync6,
63969
64171
  statSync as statSync2
63970
64172
  } from "node:fs";
@@ -64007,7 +64209,7 @@ function findHostExecutable(name, options = {}) {
64007
64209
  [path29.join(home, ".nvm", "versions", "node"), "bin"]
64008
64210
  ]) {
64009
64211
  try {
64010
- for (const entry of readdirSync5(base, { withFileTypes: true })) {
64212
+ for (const entry of readdirSync6(base, { withFileTypes: true })) {
64011
64213
  if (entry.isDirectory())
64012
64214
  dirs.push(path29.join(base, entry.name, tail));
64013
64215
  }
@@ -64229,7 +64431,7 @@ function runHostCommand(step, context = {}) {
64229
64431
  stderr: `Cannot safely execute batch shim ${step.command}; resolve its Node.js CLI entrypoint and retry`
64230
64432
  };
64231
64433
  }
64232
- const result = spawnSync12(step.command, step.args, {
64434
+ const result = spawnSync13(step.command, step.args, {
64233
64435
  cwd: step.cwd,
64234
64436
  env: (() => {
64235
64437
  const home = context.home ?? context.env?.HOME ?? os7.homedir();
@@ -64283,6 +64485,40 @@ var resolveWindowsShimScript = (shim) => {
64283
64485
  } catch {
64284
64486
  return false;
64285
64487
  }
64488
+ }, OPENCODE_PLUGIN_SCOPE = "@brainervirus", OPENCODE_PLUGIN_NAME = "workit-opencode", packageVersionAt = (root, name) => {
64489
+ try {
64490
+ const pkg = JSON.parse(readFileSync11(path29.join(root, "package.json"), "utf8"));
64491
+ return pkg.name === name && typeof pkg.version === "string" ? pkg.version : null;
64492
+ } catch {
64493
+ return null;
64494
+ }
64495
+ }, compareVersions = (a, b) => {
64496
+ const pa = a.split(/[.+-]/).map((n) => Number(n) || 0);
64497
+ const pb = b.split(/[.+-]/).map((n) => Number(n) || 0);
64498
+ for (let i = 0;i < Math.max(pa.length, pb.length); i++) {
64499
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
64500
+ if (d !== 0)
64501
+ return d;
64502
+ }
64503
+ return 0;
64504
+ }, openCodeCacheDir = (home, env = process.env) => path29.join(env.XDG_CACHE_HOME || path29.join(home, ".cache"), "opencode"), subdirs = (dir) => {
64505
+ try {
64506
+ return readdirSync6(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path29.join(dir, entry.name));
64507
+ } catch {
64508
+ return [];
64509
+ }
64510
+ }, openCodeInstalledPlugin = (home, env = process.env) => {
64511
+ const name = `${OPENCODE_PLUGIN_SCOPE}/${OPENCODE_PLUGIN_NAME}`;
64512
+ const cache = openCodeCacheDir(home, env);
64513
+ const specDirs = (layout) => subdirs(path29.join(cache, layout, OPENCODE_PLUGIN_SCOPE)).filter((dir) => path29.basename(dir) === `${OPENCODE_PLUGIN_NAME}@latest`);
64514
+ const installs = [...specDirs("npm").flatMap(subdirs), ...specDirs("packages")].map((dir) => path29.join(dir, "node_modules", OPENCODE_PLUGIN_SCOPE, OPENCODE_PLUGIN_NAME));
64515
+ let best = null;
64516
+ for (const root of installs) {
64517
+ const version = packageVersionAt(root, name);
64518
+ if (version && (!best || compareVersions(version, best.version) > 0))
64519
+ best = { root, version };
64520
+ }
64521
+ return best;
64286
64522
  }, isCanonicalCodexPackage = (root) => {
64287
64523
  for (const file of [
64288
64524
  path29.join(root, ".codex-plugin", "plugin.json"),
@@ -64302,7 +64538,7 @@ var resolveWindowsShimScript = (shim) => {
64302
64538
  if (isCanonicalCodexPackage(root))
64303
64539
  return true;
64304
64540
  try {
64305
- return readdirSync5(root, { withFileTypes: true }).some((entry) => entry.isDirectory() && isCanonicalCodexPackage(path29.join(root, entry.name)));
64541
+ return readdirSync6(root, { withFileTypes: true }).some((entry) => entry.isDirectory() && isCanonicalCodexPackage(path29.join(root, entry.name)));
64306
64542
  } catch {
64307
64543
  return false;
64308
64544
  }
@@ -64311,7 +64547,7 @@ var resolveWindowsShimScript = (shim) => {
64311
64547
  return true;
64312
64548
  const root = path29.join(codexHome, "plugins", "cache");
64313
64549
  try {
64314
- for (const marketplace of readdirSync5(root, { withFileTypes: true })) {
64550
+ for (const marketplace of readdirSync6(root, { withFileTypes: true })) {
64315
64551
  if (!marketplace.isDirectory())
64316
64552
  continue;
64317
64553
  if (isPackageOrVersionDirectory(path29.join(root, marketplace.name, "workit")))
@@ -64389,9 +64625,9 @@ var init_host_install = __esm(() => {
64389
64625
  });
64390
64626
 
64391
64627
  // packages/workit-core/src/core/skill-manifests.ts
64392
- import { cpSync as cpSync3, existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync6, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
64628
+ import { cpSync as cpSync3, existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync7, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
64393
64629
  import path30 from "node:path";
64394
- var WORKIT_METHOD_SKILLS, skillManifestNames = (root) => existsSync12(root) ? readdirSync6(root).filter((name) => existsSync12(path30.join(root, name, "SKILL.md"))).toSorted() : [], validateSkillManifests = (root, expected, label) => {
64630
+ var WORKIT_METHOD_SKILLS, skillManifestNames = (root) => existsSync12(root) ? readdirSync7(root).filter((name) => existsSync12(path30.join(root, name, "SKILL.md"))).toSorted() : [], validateSkillManifests = (root, expected, label) => {
64395
64631
  const actual = skillManifestNames(root);
64396
64632
  const missing = expected.filter((name) => !actual.includes(name));
64397
64633
  const extra = actual.filter((name) => !expected.includes(name));
@@ -64403,7 +64639,7 @@ var WORKIT_METHOD_SKILLS, skillManifestNames = (root) => existsSync12(root) ? re
64403
64639
  const pending = [path30.join(pluginDir, "skills")];
64404
64640
  while (pending.length > 0) {
64405
64641
  const dir = pending.pop();
64406
- for (const entry of readdirSync6(dir, { withFileTypes: true })) {
64642
+ for (const entry of readdirSync7(dir, { withFileTypes: true })) {
64407
64643
  const file = path30.join(dir, entry.name);
64408
64644
  if (entry.isDirectory())
64409
64645
  pending.push(file);
@@ -64434,7 +64670,7 @@ var init_skill_manifests = __esm(() => {
64434
64670
  });
64435
64671
 
64436
64672
  // packages/workit-core/src/git/session-hook.ts
64437
- import { spawnSync as spawnSync13 } from "node:child_process";
64673
+ import { spawnSync as spawnSync14 } from "node:child_process";
64438
64674
  import {
64439
64675
  chmodSync,
64440
64676
  existsSync as existsSync13,
@@ -64629,7 +64865,7 @@ add_trailer "$(git rev-parse --git-path COMMIT_EDITMSG)"
64629
64865
  [ "$2" != merge ] || add_trailer "$(git rev-parse --git-path MERGE_MSG)"
64630
64866
  exit 0
64631
64867
  `, manualTrailerLine = () => `h="$(git rev-parse --git-common-dir 2>/dev/null)/${HELPER.join("/")}"; [ ! -f "$h" ] || sh "$h" "$@" || true`, lefthookTrailerRun = () => `set -f; IFS=; h=$(git rev-parse --git-common-dir 2>/dev/null)/${HELPER.join("/")}; [ ! -f $h ] || sh $h '' {2} {3} || true`, HELPER_REF, SESSION_HOOK_SCRIPT, run2 = (cwd, args, env) => {
64632
- const r = spawnSync13("git", args, {
64868
+ const r = spawnSync14("git", args, {
64633
64869
  cwd,
64634
64870
  encoding: "utf8",
64635
64871
  timeout: GIT_TIMEOUTS.local,
@@ -64790,14 +65026,14 @@ ${manualTrailerLine()}
64790
65026
  });
64791
65027
 
64792
65028
  // packages/workit-cli/src/admin/doctor.ts
64793
- import { spawnSync as spawnSync14 } from "node:child_process";
65029
+ import { spawnSync as spawnSync15 } from "node:child_process";
64794
65030
  import { createHash as createHash14 } from "node:crypto";
64795
65031
  import {
64796
65032
  existsSync as existsSync14,
64797
65033
  lstatSync as lstatSync5,
64798
65034
  mkdirSync as mkdirSync6,
64799
65035
  mkdtempSync,
64800
- readdirSync as readdirSync7,
65036
+ readdirSync as readdirSync8,
64801
65037
  readFileSync as readFileSync14,
64802
65038
  readlinkSync as readlinkSync2,
64803
65039
  rmSync as rmSync3,
@@ -64823,7 +65059,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
64823
65059
  const configDir = options.configDir ?? env.WORKFLOW_TOOLKIT_CONFIG ?? env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path32.join(home, ".config", "workit");
64824
65060
  const stateDir = options.stateDir ?? env.WORKFLOW_TOOLKIT_STATE ?? path32.join(home, ".local", "state", "workit");
64825
65061
  const cwd = options.cwd ?? process.cwd();
64826
- const dev = options.dev ?? env.WORKFLOW_TOOLKIT_DEV ?? findDevFromCwd(cwd);
65062
+ const dev = options.dev?.trim() || env.WORKFLOW_TOOLKIT_DEV?.trim() || findDevFromCwd(cwd);
64827
65063
  return {
64828
65064
  host: options.host ?? "cli",
64829
65065
  home,
@@ -64833,7 +65069,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
64833
65069
  workspaceRoot: options.workspaceRoot ?? env.WORKFLOW_WORKSPACE_ROOT ?? cwd,
64834
65070
  dev,
64835
65071
  opencodeConfig: options.opencodeConfig ?? path32.join(home, ".config", "opencode", "opencode.json"),
64836
- opencodePackageCacheDir: options.opencodePackageCacheDir ?? path32.join(home, ".cache", "opencode", "packages", "@brainervirus", "workit-opencode@latest"),
65072
+ opencodePackageCacheDir: options.opencodePackageCacheDir ?? null,
64837
65073
  cursorSettings: options.cursorSettings ?? path32.join(home, ".cursor", "settings.json"),
64838
65074
  cursorMcp: options.cursorMcp ?? path32.join(home, ".cursor", "mcp.json"),
64839
65075
  cursorPluginDir: options.cursorPluginDir ?? path32.join(home, ".cursor", "plugins", "local", "workit"),
@@ -64879,7 +65115,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
64879
65115
  }
64880
65116
  return null;
64881
65117
  }, versionOf = (bin, env, timeout) => {
64882
- const r = spawnSync14(bin, ["--version"], { encoding: "utf8", env, timeout });
65118
+ const r = spawnSync15(bin, ["--version"], { encoding: "utf8", env, timeout });
64883
65119
  if (r.error)
64884
65120
  return null;
64885
65121
  return (r.stdout ?? "").trim();
@@ -64897,7 +65133,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
64897
65133
  if (env.BUN && existsSync14(env.BUN))
64898
65134
  return env.BUN;
64899
65135
  for (const candidate of [
64900
- path32.join(os8.homedir(), ".bun/bin/bun"),
65136
+ path32.join(env.HOME || os8.homedir(), ".bun/bin/bun"),
64901
65137
  "/usr/local/bin/bun",
64902
65138
  "/usr/bin/bun"
64903
65139
  ]) {
@@ -64906,47 +65142,50 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
64906
65142
  }
64907
65143
  return commandOnPath("bun", env) ? "bun" : null;
64908
65144
  }, checkRuntime = (res) => {
64909
- const node = versionOf("node", res.env);
64910
- const nodeOk = node !== null && semverAtLeast(node, SUPPORT_MATRIX.node.minimum);
65145
+ const running = `v${process.versions.node}`;
65146
+ const runningOk = semverAtLeast(running, SUPPORT_MATRIX.node.minimum);
65147
+ const pathNode = versionOf("node", res.env);
64911
65148
  const bun = resolveBun(res.env);
64912
65149
  const bunVersion = bun ? versionOf(bun, res.env) : null;
64913
65150
  const bunOk = bunVersion !== null;
64914
65151
  const needsBun = res.dev !== null;
64915
- if (!nodeOk) {
65152
+ if (!runningOk) {
64916
65153
  return {
64917
65154
  id: "runtime",
64918
65155
  status: "fail",
64919
- detail: `node ${SUPPORT_MATRIX.node.minimum}+ required (found ${node ?? "none on PATH"})`,
65156
+ detail: `node ${SUPPORT_MATRIX.node.minimum}+ required (workit runs under ${running} at ${process.execPath})`,
64920
65157
  fix: `Install Node ${SUPPORT_MATRIX.node.minimum}+ (declared support: ${SUPPORT_MATRIX.node.minimum}/current)`
64921
65158
  };
64922
65159
  }
64923
- if (!bunOk && needsBun) {
65160
+ const pathNote = pathNode !== null && pathNode !== running ? `; node on PATH is ${pathNode}${semverAtLeast(pathNode, SUPPORT_MATRIX.node.minimum) ? "" : ` (below ${SUPPORT_MATRIX.node.minimum}; it does not run this CLI)`}` : "";
65161
+ if (pathNode === null) {
64924
65162
  return {
64925
65163
  id: "runtime",
64926
- status: "fail",
64927
- detail: `bun not found on PATH (dev checkout requires the pinned toolchain ${SUPPORT_MATRIX.bun})`,
64928
- fix: `Install bun ${SUPPORT_MATRIX.bun} (curl -fsSL https://bun.sh/install | bash)`
65164
+ status: "warn",
65165
+ detail: `workit runs under node ${running}, but no node is on PATH for host hooks`,
65166
+ fix: `Put Node ${SUPPORT_MATRIX.node.minimum}+ on PATH (host hooks launch \`node\`)`
64929
65167
  };
64930
65168
  }
64931
- if (!bunOk) {
65169
+ if (!bunOk && needsBun) {
64932
65170
  return {
64933
65171
  id: "runtime",
64934
- status: "warn",
64935
- detail: "bun not found on PATH — not required for published artifacts",
64936
- fix: "Install bun for development/installer use"
65172
+ status: "fail",
65173
+ detail: `bun not found on PATH (dev checkout requires the pinned toolchain ${SUPPORT_MATRIX.bun})`,
65174
+ fix: `Install bun ${SUPPORT_MATRIX.bun} (curl -fsSL https://bun.sh/install | bash)`
64937
65175
  };
64938
65176
  }
65177
+ const pathOld = !semverAtLeast(pathNode, SUPPORT_MATRIX.node.minimum);
64939
65178
  return {
64940
65179
  id: "runtime",
64941
- status: "pass",
64942
- detail: `node ${node} (>=${SUPPORT_MATRIX.node.minimum}), bun ${bunVersion ?? "n/a"}`
65180
+ status: bunOk && !pathOld ? "pass" : "info",
65181
+ detail: `node ${running} (>=${SUPPORT_MATRIX.node.minimum})${pathNote}, ${bunOk ? `bun ${bunVersion}` : "no bun (needed only for a dev checkout)"}`
64943
65182
  };
64944
65183
  }, checkVersions = (res) => {
64945
65184
  if (!res.dev) {
64946
65185
  return {
64947
65186
  id: "versions",
64948
- status: "warn",
64949
- detail: "no dev checkout found (WORKFLOW_TOOLKIT_DEV) — skipping version parity"
65187
+ status: "info",
65188
+ detail: "installed package, not a dev checkout — version parity applies to checkouts only"
64950
65189
  };
64951
65190
  }
64952
65191
  const corePkg = readJson(path32.join(res.dev, "packages/workit-core/package.json"));
@@ -64960,7 +65199,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
64960
65199
  const refs = new Set;
64961
65200
  for (const name of ["workit-opencode", "workit-cursor", "workit-cli"]) {
64962
65201
  const pkg = readJson(path32.join(res.dev, "packages", name, "package.json"));
64963
- const dep = pkg?.dependencies?.["@brainervirus/workit-core"];
65202
+ const dep = pkg?.dependencies?.["@brainervirus/workit-core"] ?? pkg?.devDependencies?.["@brainervirus/workit-core"];
64964
65203
  if (typeof dep === "string")
64965
65204
  refs.add(dep);
64966
65205
  }
@@ -65057,10 +65296,16 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
65057
65296
  status: "pass",
65058
65297
  detail: `codex CLI ${installed} matches the qualified pin`
65059
65298
  };
65299
+ if (semverAtLeast(installed, qualified))
65300
+ return {
65301
+ id: "codex_pin",
65302
+ status: "info",
65303
+ detail: `codex CLI ${installed} is newer than the qualified ${qualified}`
65304
+ };
65060
65305
  return {
65061
65306
  id: "codex_pin",
65062
65307
  status: "warn",
65063
- detail: `codex CLI ${installed} differs from the qualified ${qualified}`,
65308
+ detail: `codex CLI ${installed} is older than the qualified ${qualified}`,
65064
65309
  fix: `Reinstall the qualified Codex CLI ${qualified} or record fresh qualification evidence`
65065
65310
  };
65066
65311
  }, readTomlTables = (file) => {
@@ -65117,7 +65362,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
65117
65362
  const dir = path32.join(codexHome, "plugins", "cache", marketplace, "workit");
65118
65363
  let versions = [];
65119
65364
  try {
65120
- versions = readdirSync7(dir).filter((v) => existsSync14(path32.join(dir, v, ".codex-plugin")));
65365
+ versions = readdirSync8(dir).filter((v) => existsSync14(path32.join(dir, v, ".codex-plugin")));
65121
65366
  } catch {
65122
65367
  versions = [];
65123
65368
  }
@@ -65226,7 +65471,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
65226
65471
  const installed = [];
65227
65472
  for (const install of installs) {
65228
65473
  const source = install.root ? path32.join(install.root, "agents") : null;
65229
- const files = source && existsSync14(source) ? readdirSync7(source).filter((f) => f.endsWith(".toml")).toSorted() : [];
65474
+ const files = source && existsSync14(source) ? readdirSync8(source).filter((f) => f.endsWith(".toml")).toSorted() : [];
65230
65475
  if (files.length === 0) {
65231
65476
  problems.push(`${install.id} ${install.version ?? "?"} bundles no agents`);
65232
65477
  fixes.push(`codex plugin remove ${install.id} && codex plugin add ${install.id}`);
@@ -65454,8 +65699,8 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
65454
65699
  if (!dev && res.host === "opencode") {
65455
65700
  return {
65456
65701
  id: "assets",
65457
- status: "warn",
65458
- detail: "no dev checkout found (WORKFLOW_TOOLKIT_DEV) — skipping asset check"
65702
+ status: "info",
65703
+ detail: "installed package — OpenCode loads the published assets, no checkout to compare"
65459
65704
  };
65460
65705
  }
65461
65706
  return {
@@ -65490,7 +65735,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
65490
65735
  `)) {
65491
65736
  return false;
65492
65737
  }
65493
- return spawnSync14(runtime, ["--check", entry], { encoding: "utf8", env }).status === 0;
65738
+ return spawnSync15(runtime, ["--check", entry], { encoding: "utf8", env }).status === 0;
65494
65739
  } catch {
65495
65740
  return false;
65496
65741
  }
@@ -65580,8 +65825,8 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
65580
65825
  if (!dev && res.host !== "cursor") {
65581
65826
  return {
65582
65827
  id: "launcher",
65583
- status: "warn",
65584
- detail: "no dev checkout found (WORKFLOW_TOOLKIT_DEV) — skipping non-Cursor launcher checks"
65828
+ status: "info",
65829
+ detail: "installed package — non-Cursor launchers ship inside each host plugin, no checkout to check"
65585
65830
  };
65586
65831
  }
65587
65832
  return {
@@ -65621,7 +65866,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
65621
65866
  fix: "Re-run install-cursor-plugin.sh (or `workit init` with Cursor) to rewrite the hook entries"
65622
65867
  };
65623
65868
  const started = performance.now();
65624
- const probe = spawnSync14("node", [launcher, "--probe", "workit-cursor-hook"], {
65869
+ const probe = spawnSync15("node", [launcher, "--probe", "workit-cursor-hook"], {
65625
65870
  encoding: "utf8",
65626
65871
  env: { ...res.env, WORKIT_CURSOR_HOOK_NPX_OFFLINE: "1" },
65627
65872
  timeout: 40000,
@@ -65690,18 +65935,18 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
65690
65935
  fix: "Install git and ensure it is on PATH"
65691
65936
  };
65692
65937
  }
65693
- if (process.platform !== "win32" && !commandOnPath("flock", res.env)) {
65938
+ if (res.dev && process.platform !== "win32" && !commandOnPath("flock", res.env)) {
65694
65939
  return {
65695
65940
  id: "utility",
65696
65941
  status: "warn",
65697
- detail: "flock (util-linux) not found — sync-runtime requires it for locking",
65698
- fix: "Install util-linux (flock)"
65942
+ detail: "flock not found — the dev checkout's sync-runtime requires it for locking",
65943
+ fix: process.platform === "darwin" ? "brew install flock (Homebrew's util-linux is keg-only and leaves flock off PATH)" : "Install util-linux (flock)"
65699
65944
  };
65700
65945
  }
65701
65946
  return {
65702
65947
  id: "utility",
65703
65948
  status: "pass",
65704
- detail: "git (and flock where required) on PATH"
65949
+ detail: res.dev ? "git and flock on PATH" : "git on PATH"
65705
65950
  };
65706
65951
  }, isFragileCachePin = (entry) => {
65707
65952
  const n = entry.replaceAll("\\", "/").toLowerCase();
@@ -65795,7 +66040,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
65795
66040
  if (!cmd)
65796
66041
  return null;
65797
66042
  try {
65798
- const r = spawnSync14(cmd, ["view", pkgName, "version"], {
66043
+ const r = spawnSync15(cmd, ["view", pkgName, "version"], {
65799
66044
  encoding: "utf8",
65800
66045
  timeout: 20000,
65801
66046
  env: res.env
@@ -65808,9 +66053,18 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
65808
66053
  }, installedPluginVersion = (res) => {
65809
66054
  const pkg = readJson(path32.join(res.cursorPluginDir, "package.json"));
65810
66055
  return typeof pkg?.version === "string" && pkg.version ? pkg.version : null;
65811
- }, installedOpenCodeCacheVersion = (res) => {
65812
- const pkg = readJson(path32.join(res.opencodePackageCacheDir, "node_modules", "@brainervirus", "workit-opencode", "package.json"));
65813
- return typeof pkg?.version === "string" && pkg.version ? pkg.version : null;
66056
+ }, installedOpenCodeCache = (res) => {
66057
+ if (res.opencodePackageCacheDir !== null) {
66058
+ const pkg = readJson(path32.join(res.opencodePackageCacheDir, "node_modules", "@brainervirus", "workit-opencode", "package.json"));
66059
+ return typeof pkg?.version === "string" && pkg.version ? { version: pkg.version, dir: res.opencodePackageCacheDir } : null;
66060
+ }
66061
+ const found = openCodeInstalledPlugin(res.home, res.env);
66062
+ if (!found)
66063
+ return null;
66064
+ let dir = path32.dirname(path32.dirname(path32.dirname(found.root)));
66065
+ if (path32.basename(dir) !== "workit-opencode@latest")
66066
+ dir = path32.dirname(dir);
66067
+ return { version: found.version, dir };
65814
66068
  }, checkOpenCodePackageCache = (res) => {
65815
66069
  if (!existsSync14(res.opencodeConfig)) {
65816
66070
  return {
@@ -65842,8 +66096,9 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
65842
66096
  detail: `opencode pin ${pin} is an exact version — not compared to registry latest`
65843
66097
  };
65844
66098
  }
65845
- const installed = installedOpenCodeCacheVersion(res);
65846
- if (installed === null) {
66099
+ const cache = installedOpenCodeCache(res);
66100
+ const installed = cache?.version ?? null;
66101
+ if (cache === null || installed === null) {
65847
66102
  return {
65848
66103
  id: "stale_install",
65849
66104
  status: "pass",
@@ -65857,7 +66112,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
65857
66112
  status: "warn",
65858
66113
  registryProbed: true,
65859
66114
  detail: "registry_unreachable: cannot compare OpenCode @latest cache against published workit-opencode",
65860
- fix: `Retry when npm is reachable, or remove ${res.opencodePackageCacheDir} and restart OpenCode to re-resolve`
66115
+ fix: `Retry when npm is reachable, or remove ${cache.dir} and restart OpenCode to re-resolve`
65861
66116
  };
65862
66117
  }
65863
66118
  if (!semverAtLeast(installed, expected)) {
@@ -65866,7 +66121,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
65866
66121
  status: "fail",
65867
66122
  registryProbed: true,
65868
66123
  detail: `stale_install: OpenCode @latest cache has workit-opencode ${installed} behind published ${expected}`,
65869
- fix: `Remove ${res.opencodePackageCacheDir} and restart OpenCode — it re-resolves @latest on the next launch`
66124
+ fix: `Remove ${cache.dir} and restart OpenCode — it re-resolves @latest on the next launch`
65870
66125
  };
65871
66126
  }
65872
66127
  return {
@@ -66335,7 +66590,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
66335
66590
  }, defaultIdentityProbes = (env) => ({
66336
66591
  origin: (cwd) => {
66337
66592
  try {
66338
- const out = spawnSync14("git", ["remote", "get-url", "origin"], {
66593
+ const out = spawnSync15("git", ["remote", "get-url", "origin"], {
66339
66594
  cwd,
66340
66595
  encoding: "utf8",
66341
66596
  env
@@ -66347,7 +66602,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
66347
66602
  },
66348
66603
  ghLogin: (e) => {
66349
66604
  try {
66350
- const out = spawnSync14("gh", ["api", "user", "--jq", ".login"], { encoding: "utf8", env: e });
66605
+ const out = spawnSync15("gh", ["api", "user", "--jq", ".login"], { encoding: "utf8", env: e });
66351
66606
  const login = (out.stdout ?? "").trim();
66352
66607
  return out.status === 0 && login ? login : null;
66353
66608
  } catch {
@@ -66356,7 +66611,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
66356
66611
  },
66357
66612
  glabLogin: (e) => {
66358
66613
  try {
66359
- const out = spawnSync14("glab", ["api", "user", "--jq", ".username"], {
66614
+ const out = spawnSync15("glab", ["api", "user", "--jq", ".username"], {
66360
66615
  encoding: "utf8",
66361
66616
  env: e
66362
66617
  });
@@ -66368,7 +66623,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
66368
66623
  },
66369
66624
  sshLogin: (host) => {
66370
66625
  try {
66371
- const out = spawnSync14("ssh", ["-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "-T", `git@${host}`], {
66626
+ const out = spawnSync15("ssh", ["-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "-T", `git@${host}`], {
66372
66627
  encoding: "utf8",
66373
66628
  env
66374
66629
  });
@@ -66525,10 +66780,10 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
66525
66780
  fix: repair,
66526
66781
  registryProbed
66527
66782
  };
66528
- }, HOOK_ISSUED_MARKER = "workit-issued-sessions/2", hookRecordsIssued = (root) => {
66783
+ }, HOOK_ISSUED_MARKER = "workit-issued-sessions/3", hookRecordsIssued = (root) => {
66529
66784
  let files;
66530
66785
  try {
66531
- files = readdirSync7(path32.join(root, "dist")).filter((f) => f.endsWith(".js"));
66786
+ files = readdirSync8(path32.join(root, "dist")).filter((f) => f.endsWith(".js"));
66532
66787
  } catch {
66533
66788
  return null;
66534
66789
  }
@@ -66587,12 +66842,12 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
66587
66842
  if (cached)
66588
66843
  return cached;
66589
66844
  const viaShell = process.platform === "win32" && /\.(?:cmd|bat)$/i.test(bin);
66590
- const r = viaShell ? spawnSync14(`"${bin}" --version`, {
66845
+ const r = viaShell ? spawnSync15(`"${bin}" --version`, {
66591
66846
  encoding: "utf8",
66592
66847
  env,
66593
66848
  timeout: WORKIT_PROBE_TIMEOUT_MS,
66594
66849
  shell: true
66595
- }) : spawnSync14(bin, ["--version"], { encoding: "utf8", env, timeout: WORKIT_PROBE_TIMEOUT_MS });
66850
+ }) : spawnSync15(bin, ["--version"], { encoding: "utf8", env, timeout: WORKIT_PROBE_TIMEOUT_MS });
66596
66851
  const timedOut = r.error !== undefined && "code" in r.error && r.error.code === "ETIMEDOUT";
66597
66852
  const version = r.status === 0 ? (r.stdout ?? "").match(SEMVER)?.[0] ?? null : null;
66598
66853
  const probe = { version: timedOut ? null : version, timedOut };
@@ -66605,7 +66860,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
66605
66860
  version: i.version
66606
66861
  })),
66607
66862
  { label: "Cursor plugin", version: installedPluginVersion(res) },
66608
- { label: "OpenCode plugin", version: installedOpenCodeCacheVersion(res) }
66863
+ { label: "OpenCode plugin", version: installedOpenCodeCache(res)?.version ?? null }
66609
66864
  ].filter((p) => p.version !== null);
66610
66865
  const newest = plugins.reduce((best, p) => best && semverAtLeast(best.version, p.version) ? best : p, null);
66611
66866
  const target = newest?.version ?? "latest";
@@ -66646,7 +66901,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
66646
66901
  } : c) : raw;
66647
66902
  const failed = checks.filter((c) => c.status === "fail").length;
66648
66903
  const warned = checks.filter((c) => c.status === "warn").length;
66649
- const passed = checks.filter((c) => c.status === "pass").length;
66904
+ const passed = checks.filter((c) => c.status === "pass" || c.status === "info").length;
66650
66905
  const exitCode = failed > 0 ? 1 : 0;
66651
66906
  const offline = !raw.some((c) => ("registryProbed" in c) && c.registryProbed);
66652
66907
  const report = {
@@ -66737,13 +66992,14 @@ import {
66737
66992
  mkdirSync as mkdirSync7,
66738
66993
  mkdtempSync as mkdtempSync2,
66739
66994
  readFileSync as readFileSync15,
66740
- readdirSync as readdirSync8,
66995
+ readdirSync as readdirSync9,
66741
66996
  realpathSync as realpathSync7,
66742
66997
  renameSync as renameSync4,
66743
66998
  rmSync as rmSync4,
66744
66999
  statSync as statSync6,
66745
67000
  writeFileSync as writeFileSync8
66746
67001
  } from "node:fs";
67002
+ import { createRequire as createRequire2 } from "node:module";
66747
67003
  import os9 from "node:os";
66748
67004
  import path33 from "node:path";
66749
67005
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
@@ -66780,6 +67036,7 @@ function buildSetupPreview(values, opts = {}) {
66780
67036
  const blocked = [];
66781
67037
  const mutations = [];
66782
67038
  const preserved = [];
67039
+ let projectRefused;
66783
67040
  const overrides = activeSetupOverrides(env);
66784
67041
  for (const entry of [state.config, state.youtrack, state.vcs, state.workspaces]) {
66785
67042
  if (entry.status === "malformed")
@@ -66902,8 +67159,11 @@ function buildSetupPreview(values, opts = {}) {
66902
67159
  }
66903
67160
  }
66904
67161
  }
66905
- if (values.applyProject) {
66906
- const root = path33.resolve(opts.cwd ?? process.cwd());
67162
+ const target = values.applyProject ? projectTarget(opts.cwd ?? process.cwd()) : null;
67163
+ if (target && !target.ok)
67164
+ projectRefused = target.refused;
67165
+ if (target?.ok) {
67166
+ const root = target.root;
66907
67167
  const gitignorePath = path33.join(root, ".gitignore");
66908
67168
  const existing = existsSync15(gitignorePath) ? readFileSync15(gitignorePath, "utf8") : "";
66909
67169
  const existingLines = new Set(existing.split(`
@@ -66922,7 +67182,8 @@ function buildSetupPreview(values, opts = {}) {
66922
67182
  overrides,
66923
67183
  preserved,
66924
67184
  platforms: values.platforms ?? [],
66925
- state
67185
+ state,
67186
+ ...projectRefused === undefined ? {} : { projectRefused }
66926
67187
  };
66927
67188
  }
66928
67189
  function resolveApply(preview, options) {
@@ -67105,7 +67366,14 @@ function adapterRoot(platform, res, preferInstalledAdapter = false) {
67105
67366
  if (res.dev) {
67106
67367
  candidates.push(path33.join(res.dev, "packages", `workit-${platform}`));
67107
67368
  } else {
67108
- candidates.push(path33.join(packageRoot(), "..", `workit-${platform}`));
67369
+ candidates.push(...nodeResolvedAdapters(platform));
67370
+ if (platform === "cursor")
67371
+ candidates.push(stagedCursor);
67372
+ if (platform === "opencode") {
67373
+ const cached = openCodeInstalledPlugin(res.home, res.env);
67374
+ if (cached)
67375
+ candidates.push(cached.root);
67376
+ }
67109
67377
  let dir = path33.resolve(res.cwd);
67110
67378
  while (true) {
67111
67379
  candidates.push(path33.join(dir, "packages", `workit-${platform}`));
@@ -67115,17 +67383,14 @@ function adapterRoot(platform, res, preferInstalledAdapter = false) {
67115
67383
  dir = parent;
67116
67384
  }
67117
67385
  }
67118
- candidates.push(path33.join(res.home, ".local", "share", "workit", "packages", `workit-${platform}`));
67119
- if (platform === "cursor")
67120
- candidates.push(stagedCursor);
67121
67386
  for (const candidate of candidates) {
67122
67387
  if (isAdapter(candidate, platform))
67123
- return candidate;
67388
+ return realRoot(candidate);
67124
67389
  }
67125
67390
  return null;
67126
67391
  }
67127
67392
  function applyOpenCode(root, res) {
67128
- const resolvedPin = opencodePin(root, res);
67393
+ const resolvedPin = root === null ? OPENCODE_NPM_PIN : opencodePin(root, res);
67129
67394
  if (!resolvedPin) {
67130
67395
  return {
67131
67396
  platform: "opencode",
@@ -67424,6 +67689,10 @@ function applySetupPreview(preview, options = {}) {
67424
67689
  root = adapterRoot(platform, res, options.preferInstalledAdapter);
67425
67690
  rootFor.set(platform, root);
67426
67691
  }
67692
+ if (root === null && registersNpmPin(platform, res)) {
67693
+ entries.push(applyOpenCode(null, res));
67694
+ continue;
67695
+ }
67427
67696
  if (root === null) {
67428
67697
  entries.push({
67429
67698
  platform,
@@ -67465,12 +67734,20 @@ function applySetupPreview(preview, options = {}) {
67465
67734
  detail: "existing credential preserved"
67466
67735
  });
67467
67736
  }
67737
+ if (preview.projectRefused) {
67738
+ entries.push({
67739
+ platform: "core",
67740
+ file: path33.resolve(res.cwd),
67741
+ status: "Skipped",
67742
+ detail: preview.projectRefused
67743
+ });
67744
+ }
67468
67745
  const doctor = [];
67469
67746
  for (const platform of preview.platforms) {
67470
67747
  if (platform !== "opencode" && platform !== "cursor")
67471
67748
  continue;
67472
67749
  const root = rootFor.get(platform);
67473
- if (root === null || root === undefined)
67750
+ if (root === undefined || root === null && !registersNpmPin(platform, res))
67474
67751
  continue;
67475
67752
  if (!entries.some((e) => e.platform === platform && e.status !== "Failed"))
67476
67753
  continue;
@@ -67599,6 +67876,16 @@ var TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", YT_OVERRIDES, VCS_OVERRIDES, SETUP_O
67599
67876
  } catch {
67600
67877
  return false;
67601
67878
  }
67879
+ }, nodeResolvedAdapters = (platform) => {
67880
+ const name = `@brainervirus/workit-${platform}`;
67881
+ const require2 = createRequire2(path33.join(packageRoot(), "package.json"));
67882
+ return (require2.resolve.paths(name) ?? []).map((dir) => path33.join(dir, name));
67883
+ }, realRoot = (root) => {
67884
+ try {
67885
+ return realpathSync7(root);
67886
+ } catch {
67887
+ return root;
67888
+ }
67602
67889
  }, isDevOpenCodeAdapterRoot = (root, dev) => {
67603
67890
  if (dev)
67604
67891
  return true;
@@ -67613,7 +67900,7 @@ var TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", YT_OVERRIDES, VCS_OVERRIDES, SETUP_O
67613
67900
  return `file://${entry}`;
67614
67901
  }
67615
67902
  return null;
67616
- }, opencodePin = (root, res) => resolveOpenCodePin(root, { dev: res.dev }), CURSOR_HOOKS_FILE, localizedCursorHooks = (shipped, pluginDir) => {
67903
+ }, opencodePin = (root, res) => resolveOpenCodePin(root, { dev: res.dev }), registersNpmPin = (platform, res) => platform === "opencode" && !res.dev, CURSOR_HOOKS_FILE, localizedCursorHooks = (shipped, pluginDir) => {
67617
67904
  try {
67618
67905
  const merged = mergeCursorHooks(JSON.parse(shipped.toString("utf8")), { command: cursorHooksEntry(pluginDir).command }, pluginDir);
67619
67906
  return Buffer.from(`${JSON.stringify(merged.config, null, 2)}
@@ -67623,7 +67910,7 @@ var TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", YT_OVERRIDES, VCS_OVERRIDES, SETUP_O
67623
67910
  }
67624
67911
  }, samePluginContent = (src, dest, pluginDir, relative = "") => {
67625
67912
  try {
67626
- for (const entry of readdirSync8(src, { withFileTypes: true })) {
67913
+ for (const entry of readdirSync9(src, { withFileTypes: true })) {
67627
67914
  if (entry.name === "node_modules")
67628
67915
  continue;
67629
67916
  const source = path33.join(src, entry.name);
@@ -67638,7 +67925,7 @@ var TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", YT_OVERRIDES, VCS_OVERRIDES, SETUP_O
67638
67925
  return false;
67639
67926
  }
67640
67927
  }
67641
- for (const entry of readdirSync8(dest, { withFileTypes: true })) {
67928
+ for (const entry of readdirSync9(dest, { withFileTypes: true })) {
67642
67929
  if (existsSync15(path33.join(src, entry.name)))
67643
67930
  continue;
67644
67931
  const rel = path33.join(relative, entry.name);
@@ -67657,7 +67944,7 @@ var TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", YT_OVERRIDES, VCS_OVERRIDES, SETUP_O
67657
67944
  const sourceRules = path33.join(src, "rules");
67658
67945
  const installedRules = path33.join(dest, "rules");
67659
67946
  try {
67660
- for (const entry of readdirSync8(installedRules, { withFileTypes: true })) {
67947
+ for (const entry of readdirSync9(installedRules, { withFileTypes: true })) {
67661
67948
  if (!entry.isFile() || !entry.name.endsWith(".mdc"))
67662
67949
  continue;
67663
67950
  if (existsSync15(path33.join(sourceRules, entry.name)))
@@ -67674,7 +67961,7 @@ var TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", YT_OVERRIDES, VCS_OVERRIDES, SETUP_O
67674
67961
  if (existsSync15(legacyRules)) {
67675
67962
  try {
67676
67963
  mkdirSync7(path33.join(res.cursorPluginDir, "rules"), { recursive: true });
67677
- for (const entry of readdirSync8(legacyRules, { withFileTypes: true })) {
67964
+ for (const entry of readdirSync9(legacyRules, { withFileTypes: true })) {
67678
67965
  if (!entry.isFile() || !entry.name.endsWith(".mdc"))
67679
67966
  continue;
67680
67967
  const target = path33.join(res.cursorPluginDir, "rules", entry.name);
@@ -67684,7 +67971,7 @@ var TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", YT_OVERRIDES, VCS_OVERRIDES, SETUP_O
67684
67971
  } catch {}
67685
67972
  }
67686
67973
  rmSync4(legacy, { recursive: true, force: true });
67687
- }, failedDetail = (report) => report.checks.filter((c) => c.status === "fail").map((c) => `${c.id}: ${c.detail}`).join("; "), setupCompletionGuidance = () => [
67974
+ }, failedDetail = (report) => report.checks.filter((c) => c.status === "fail").map((c) => c.detail.startsWith(`${c.id}:`) ? c.detail : `${c.id}: ${c.detail}`).join("; "), setupCompletionGuidance = () => [
67688
67975
  "Run `workit doctor` to verify your installation.",
67689
67976
  "Run /wk-status in OpenCode to review token setup steps."
67690
67977
  ];
@@ -70753,7 +71040,43 @@ function Screen({
70753
71040
  }, undefined, false, undefined, this)
70754
71041
  ]
70755
71042
  }, undefined, true, undefined, this);
70756
- case "project":
71043
+ case "project": {
71044
+ const target = projectTargetFor(resolveBasePath(draft.values));
71045
+ if (!target.ok) {
71046
+ return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
71047
+ flexDirection: "column",
71048
+ gap: 1,
71049
+ children: [
71050
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
71051
+ bold: true,
71052
+ children: "Step 6 — Project setup"
71053
+ }, undefined, false, undefined, this),
71054
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
71055
+ color: "yellow",
71056
+ children: [
71057
+ "Skipped: ",
71058
+ target.refused
71059
+ ]
71060
+ }, undefined, true, undefined, this),
71061
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(ConfirmInput, {
71062
+ defaultChoice: "confirm",
71063
+ submitOnEnter: false,
71064
+ onConfirm: () => {
71065
+ dispatch({ type: "set", field: "applyProject", value: true });
71066
+ dispatch({ type: "next" });
71067
+ },
71068
+ onCancel: () => {
71069
+ dispatch({ type: "set", field: "applyProject", value: false });
71070
+ dispatch({ type: "next" });
71071
+ }
71072
+ }, undefined, false, undefined, this),
71073
+ /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
71074
+ dimColor: true,
71075
+ children: "y to keep this note in the summary · n to skip it · either adds no project files · b Back · Esc Cancel"
71076
+ }, undefined, false, undefined, this)
71077
+ ]
71078
+ }, undefined, true, undefined, this);
71079
+ }
70757
71080
  return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
70758
71081
  flexDirection: "column",
70759
71082
  gap: 1,
@@ -70766,7 +71089,7 @@ function Screen({
70766
71089
  dimColor: true,
70767
71090
  children: [
70768
71091
  "Apply gitignore + hygiene in ",
70769
- resolveBasePath(draft.values),
71092
+ target.root,
70770
71093
  "? (Existing files are never overwritten.)"
70771
71094
  ]
70772
71095
  }, undefined, true, undefined, this),
@@ -70788,6 +71111,7 @@ function Screen({
70788
71111
  }, undefined, false, undefined, this)
70789
71112
  ]
70790
71113
  }, undefined, true, undefined, this);
71114
+ }
70791
71115
  case "summary": {
70792
71116
  const policy = effectivePolicy(draft.values);
70793
71117
  const preview = buildSetupPreview(draft.values, { cwd: resolveBasePath(draft.values) });
@@ -70929,8 +71253,15 @@ function Screen({
70929
71253
  }, undefined, true, undefined, this),
70930
71254
  /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
70931
71255
  children: [
70932
- "Project hygiene: ",
70933
- /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
71256
+ "Project hygiene:",
71257
+ " ",
71258
+ preview.projectRefused ? /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
71259
+ color: "yellow",
71260
+ children: [
71261
+ "skipped — ",
71262
+ preview.projectRefused
71263
+ ]
71264
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
70934
71265
  color: "green",
70935
71266
  children: draft.values.applyProject ? "yes" : "no"
70936
71267
  }, undefined, false, undefined, this)
@@ -71019,7 +71350,11 @@ function Screen({
71019
71350
  }, undefined, false, undefined, this);
71020
71351
  }
71021
71352
  }
71022
- var import_react66, jsx_dev_runtime2, PLATFORM_LABELS, BRANCH_PRESETS, BRANCH_PRESET_DESCRIPTIONS, SCREEN_PLACEHOLDERS, VCS_PROVIDERS, LOCALE_PICKER_OPTIONS, ISSUE_TRACKERS, WORKSPACE_ADVANCED_FIELDS, PROFILE_FIELDS, TRACK_FIELDS, enumFieldOptions = (field) => {
71353
+ var import_react66, jsx_dev_runtime2, lastProjectTarget = null, projectTargetFor = (base) => {
71354
+ if (lastProjectTarget?.base !== base)
71355
+ lastProjectTarget = { base, target: projectTarget(base) };
71356
+ return lastProjectTarget.target;
71357
+ }, PLATFORM_LABELS, BRANCH_PRESETS, BRANCH_PRESET_DESCRIPTIONS, SCREEN_PLACEHOLDERS, VCS_PROVIDERS, LOCALE_PICKER_OPTIONS, ISSUE_TRACKERS, WORKSPACE_ADVANCED_FIELDS, PROFILE_FIELDS, TRACK_FIELDS, enumFieldOptions = (field) => {
71023
71358
  if (field === "commitPolicy.preset")
71024
71359
  return [{ label: "Inherit global", value: "inherit" }, ...commitPresetOptions];
71025
71360
  if (field === "branchPolicy.preset")
@@ -71061,6 +71396,7 @@ var init_steps = __esm(async () => {
71061
71396
  init_logic();
71062
71397
  init_workspaces();
71063
71398
  init_branch_policy();
71399
+ init_hygiene();
71064
71400
  init_wizard_state();
71065
71401
  init_detect_hosts();
71066
71402
  await __promiseAll([
@@ -71452,7 +71788,7 @@ var init_init = __esm(() => {
71452
71788
  });
71453
71789
 
71454
71790
  // packages/workit-cli/src/upgrade.ts
71455
- import { spawnSync as spawnSync15 } from "node:child_process";
71791
+ import { spawnSync as spawnSync16 } from "node:child_process";
71456
71792
  import {
71457
71793
  existsSync as existsSync18,
71458
71794
  mkdtempSync as mkdtempSync3,
@@ -71808,7 +72144,7 @@ async function runLaunchCommand(argv, deps = {}) {
71808
72144
  }
71809
72145
  if (deps.run)
71810
72146
  return deps.run(host, hostArgs).status ?? 1;
71811
- return spawnSync15(launch.command, launch.args, {
72147
+ return spawnSync16(launch.command, launch.args, {
71812
72148
  stdio: "inherit",
71813
72149
  env
71814
72150
  }).status ?? 1;
@@ -71990,7 +72326,7 @@ var init_launch = __esm(() => {
71990
72326
  });
71991
72327
 
71992
72328
  // packages/workit-core/src/knowledge.ts
71993
- import { spawnSync as spawnSync16 } from "node:child_process";
72329
+ import { spawnSync as spawnSync17 } from "node:child_process";
71994
72330
  import fs18 from "node:fs";
71995
72331
  import path38 from "node:path";
71996
72332
  function lintKnowledge(root) {
@@ -72111,7 +72447,7 @@ var AGENTS_BYTE_BUDGET, STEERING_FILES, NEED_BASED_FILES, STANDARDS_FILE = "CODI
72111
72447
  return "unreadable";
72112
72448
  }
72113
72449
  }, trackedTopLevel = (root) => {
72114
- const run = spawnSync16("git", ["ls-files", "-z", "--cached"], {
72450
+ const run = spawnSync17("git", ["ls-files", "-z", "--cached"], {
72115
72451
  cwd: root,
72116
72452
  encoding: "utf8",
72117
72453
  maxBuffer: 256 * 1024 * 1024,
@@ -72479,7 +72815,8 @@ var R = "[REDACTED]", TOKEN_PATTERNS, WEBHOOK, URL_USERINFO, CURL_USER, KEY, ASS
72479
72815
  var init_redact = __esm(() => {
72480
72816
  TOKEN_PATTERNS = [
72481
72817
  /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})/gu,
72482
- /\bgl[a-z]{1,8}-[A-Za-z0-9_-]{16,}/gu,
72818
+ /\b(?:glpat|gloas|gldt|glrtr|glrt|glcbt|glptt|glft|glimt|glagent|glwt|glsoat|glffct)-[A-Za-z0-9_-]{20,}/gu,
72819
+ /\b_gitlab_session=[A-Za-z0-9%_-]{16,}/gu,
72483
72820
  /\bnpm_[A-Za-z0-9]{30,}/gu,
72484
72821
  /\bxox[abposr]-[A-Za-z0-9-]{10,}/gu,
72485
72822
  /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/gu,
@@ -72513,7 +72850,7 @@ var init_redact = __esm(() => {
72513
72850
  });
72514
72851
 
72515
72852
  // packages/workit-core/src/checks.ts
72516
- import { spawn, spawnSync as spawnSync17 } from "node:child_process";
72853
+ import { spawn, spawnSync as spawnSync18 } from "node:child_process";
72517
72854
  import { createHash as createHash16 } from "node:crypto";
72518
72855
  import fs19 from "node:fs";
72519
72856
  import os13 from "node:os";
@@ -72695,7 +73032,7 @@ function runCheckCommand(argv, options) {
72695
73032
  if (!child?.pid)
72696
73033
  return;
72697
73034
  if (windows)
72698
- spawnSync17("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
73035
+ spawnSync18("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
72699
73036
  stdio: "ignore",
72700
73037
  windowsHide: true
72701
73038
  });
@@ -74031,7 +74368,7 @@ var init_check = __esm(() => {
74031
74368
  var success2 = (data) => ({ ok: true, data }), failure3 = (code, error, unblock) => ({ ok: false, code, error, ...unblock ? { unblock } : {} });
74032
74369
 
74033
74370
  // packages/workit-core/src/git/ops.ts
74034
- import { spawnSync as spawnSync18 } from "node:child_process";
74371
+ import { spawnSync as spawnSync19 } from "node:child_process";
74035
74372
  import { existsSync as existsSync19, readFileSync as readFileSync19 } from "node:fs";
74036
74373
  import path42 from "node:path";
74037
74374
  function operationInProgress(cwd) {
@@ -74346,6 +74683,14 @@ function gitCommit(cwd, input) {
74346
74683
  const session = input.actor.session;
74347
74684
  if (session && !SESSION_SAFE.test(session))
74348
74685
  return failure3("invalid_input", "WORKIT_SESSION_ID must be 1-128 characters of [A-Za-z0-9_.:@/+-]");
74686
+ if (session) {
74687
+ const env = input.env ?? process.env;
74688
+ if (issuedBinding(cwd, session) !== null)
74689
+ return failure3("blocked", `judge_session: ${session} was issued to a verifier or reviewer subagent; it records verdicts and never authors commits`, "commit from the implementer's or the lead's own session (unset WORKIT_SESSION_ID back to the shell's)");
74690
+ const refusal = sessionClaimRefusal(env, session, cwd);
74691
+ if (refusal)
74692
+ return failure3("blocked", refusal.error, "commit from the session this shell really is: never set WORKIT_SESSION_ID by hand for a commit (unset it, or use the id the host put in this shell)");
74693
+ }
74349
74694
  const changeRequired = !input.amend && !input.allowEmpty;
74350
74695
  const emptyAmend = previous !== null && git5(cwd, ["rev-parse", "--verify", "-q", `${previous}^{tree}`]).stdout.trim() === git5(cwd, ["rev-parse", "--verify", "-q", `${previous}~1^{tree}`]).stdout.trim();
74351
74696
  const args = ["commit", "--no-edit"];
@@ -74628,6 +74973,51 @@ ${gitOutput(run)}`);
74628
74973
  recorded: row.ok ? { id: String(row.value.id) } : { error: row.error }
74629
74974
  });
74630
74975
  }
74976
+ function releaseLinesOf(cwd) {
74977
+ const { preset, branches, production } = releaseBranchesOf(cwd);
74978
+ let policyPrefix;
74979
+ try {
74980
+ const selected = resolveRuntimeWorkspacePolicy(cwd, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined).policy;
74981
+ policyPrefix = selected?.releaseTagPrefix;
74982
+ } catch {
74983
+ policyPrefix = undefined;
74984
+ }
74985
+ const prefix = releaseTagPrefixOf(cwd, policyPrefix);
74986
+ const trunk = production ?? "main";
74987
+ const lines = [
74988
+ {
74989
+ track: null,
74990
+ production: branches[0] ?? trunk,
74991
+ integration: branches[1] ?? trunk,
74992
+ mergeBack: branches[1] ? [branches[1]] : [],
74993
+ tagPrefix: prefix,
74994
+ gitflow: branches.length > 0,
74995
+ naming: {}
74996
+ }
74997
+ ];
74998
+ let raw;
74999
+ try {
75000
+ raw = resolveRuntimeReleaseTracks(cwd)?.releaseTracks;
75001
+ } catch {
75002
+ return lines;
75003
+ }
75004
+ for (const track of readReleaseTracks(raw).tracks) {
75005
+ const value = isRecord8(raw) ? raw[track.name] : undefined;
75006
+ const entry = isRecord8(value) ? value : {};
75007
+ const strategy = typeof entry.strategy === "string" ? entry.strategy : preset;
75008
+ const namespace = typeof entry.tagNamespace === "string" ? entry.tagNamespace.trim() : "";
75009
+ lines.push({
75010
+ track: track.name,
75011
+ production: track.productionBranch,
75012
+ integration: track.integrationBranch,
75013
+ mergeBack: track.mergeBackBranches.length ? track.mergeBackBranches : [track.integrationBranch],
75014
+ tagPrefix: `${namespace}${prefix}`,
75015
+ gitflow: strategy === "gitflow",
75016
+ naming: { release: track.naming.release, hotfix: track.naming.hotfix }
75017
+ });
75018
+ }
75019
+ return lines;
75020
+ }
74631
75021
  function releasePushPreflight(cwd, names) {
74632
75022
  const progress = operationInProgress(cwd);
74633
75023
  if (progress)
@@ -74635,7 +75025,16 @@ function releasePushPreflight(cwd, names) {
74635
75025
  const wanted = [...new Set(names)];
74636
75026
  if (!wanted.length)
74637
75027
  return failure3("invalid_input", "name the release refs to push");
74638
- const { preset, branches, production } = releaseBranchesOf(cwd);
75028
+ const { preset, production } = releaseBranchesOf(cwd);
75029
+ const lines = releaseLinesOf(cwd);
75030
+ const tracks = lines.filter((line) => line.track !== null && line.gitflow);
75031
+ const branches = [
75032
+ ...new Set([
75033
+ ...releaseBranchesOf(cwd).branches,
75034
+ ...tracks.flatMap((line) => [line.production, line.integration, ...line.mergeBack])
75035
+ ])
75036
+ ];
75037
+ const tagShape = `${lines[0].tagPrefix}<version>`;
74639
75038
  const refs = [];
74640
75039
  for (const raw of wanted) {
74641
75040
  if (raw.startsWith("-") || raw.includes(":"))
@@ -74647,16 +75046,22 @@ function releasePushPreflight(cwd, names) {
74647
75046
  if (tag && branch)
74648
75047
  return failure3("invalid_input", `${raw} names both a branch and a tag`, `workit git push --release refs/heads/${raw} # or refs/tags/${raw}`);
74649
75048
  if (tag) {
74650
- const prefix = releaseTagPrefix(cwd);
74651
- if (!isReleaseTag(tagName, prefix))
74652
- return failure3("blocked", `release_push_refused: tag ${tagName} is not a release tag (${prefix}<version>, from ${prefix === "v" ? "the default" : "gitflow.prefix.versiontag"})`, `git tag -a ${prefix}<version> -m <message> # then: workit git push --release ${prefix}<version>`);
74653
- refs.push({ kind: "tag", name: tagName, ref: `refs/tags/${tagName}`, sha: tag });
75049
+ const line = lineOfTag(tagName, lines);
75050
+ if (!line)
75051
+ return failure3("blocked", `release_push_refused: tag ${tagName} is not a release tag (${tagShape})`, `git tag -a ${tagShape} -m <message> # then: workit git push --release ${tagShape}`);
75052
+ refs.push({
75053
+ kind: "tag",
75054
+ name: tagName,
75055
+ ref: `refs/tags/${tagName}`,
75056
+ sha: tag,
75057
+ production: line.track === null ? production : line.production
75058
+ });
74654
75059
  continue;
74655
75060
  }
74656
75061
  if (!branch)
74657
75062
  return failure3("not_found", `${raw} is not a local branch or tag`);
74658
75063
  if (!branches.some((name) => name.toLowerCase() === branchName.toLowerCase()))
74659
- return failure3("blocked", branches.length ? `release_push_refused: ${branchName} is not a git-flow release branch (${branches.join(", ")})` : `release_push_refused: the ${preset} policy lands ${branchName} only through a reviewed PR; workit git push --release pushes v<version> tags there, not branches`, branches.length ? "workit git push # a feature, bugfix, release or hotfix branch, then: workit pr create" : `workit pr create --base ${branchName} # then tag the merged commit and: workit git push --release v<version>`);
75064
+ return failure3("blocked", branches.length ? `release_push_refused: ${branchName} is not a git-flow release branch (${branches.join(", ")})` : `release_push_refused: the ${preset} policy lands ${branchName} only through a reviewed PR; workit git push --release pushes ${tagShape} tags there, not branches`, branches.length ? "workit git push # a feature, bugfix, release or hotfix branch, then: workit pr create" : `workit pr create --base ${branchName} # then tag the merged commit and: workit git push --release ${tagShape}`);
74660
75065
  refs.push({ kind: "branch", name: branchName, ref: `refs/heads/${branchName}`, sha: branch });
74661
75066
  }
74662
75067
  const remote = pushRemoteName(cwd, refs.find((ref) => ref.kind === "branch")?.name ?? null);
@@ -74668,7 +75073,14 @@ function releasePushPreflight(cwd, names) {
74668
75073
  const local = parseRemoteUrl(raw)?.protocol === "file";
74669
75074
  if (!local && !pushTargetIsStable(cwd, raw))
74670
75075
  return failure3("blocked", `push_target_rewritten: the push URL of "${remote}" is rewritten by a url.*.insteadOf rule, so the destination is not the one checked`, "git config --get-regexp '^url\\.' # remove the rewrite for this remote");
74671
- return success2({ refs, remote, url: redactRemote(raw), rawUrl: raw, local, production });
75076
+ return success2({
75077
+ refs,
75078
+ remote,
75079
+ url: redactRemote(raw),
75080
+ rawUrl: raw,
75081
+ local,
75082
+ production
75083
+ });
74672
75084
  }
74673
75085
  function executeReleasePush(cwd, plan, input) {
74674
75086
  const previous = new Map;
@@ -74685,20 +75097,20 @@ function executeReleasePush(cwd, plan, input) {
74685
75097
  return failure3("failed", `non_fast_forward: ${plan.remote}/${ref.name} (${before.sha.slice(0, 12)}) is not part of local ${ref.name}; a release push never rewrites it`, `git fetch ${plan.remote} ${ref.name} && git switch ${ref.name} && git merge --ff-only ${plan.remote}/${ref.name} # then: workit git push --release ${ref.name}`);
74686
75098
  }
74687
75099
  const tags = plan.refs.filter((ref) => ref.kind === "tag");
74688
- if (tags.length) {
74689
- let productionTip = null;
74690
- if (plan.production) {
74691
- const tip = remoteRefTip(cwd, plan.rawUrl, `refs/heads/${plan.production}`);
75100
+ const contains = (tip, sha) => git5(cwd, ["merge-base", "--is-ancestor", sha, tip]).ok;
75101
+ for (const tag of tags) {
75102
+ const production = tag.production ?? plan.production;
75103
+ let remoteTip = null;
75104
+ if (production) {
75105
+ const tip = remoteRefTip(cwd, plan.rawUrl, `refs/heads/${production}`);
74692
75106
  if (!tip.ok)
74693
75107
  return failure3(tip.code, tip.error);
74694
- productionTip = tip.sha;
74695
- }
74696
- const contains = (tip, sha) => git5(cwd, ["merge-base", "--is-ancestor", sha, tip]).ok;
74697
- for (const tag of tags) {
74698
- const released = productionTip !== null && contains(productionTip, tag.sha) || plan.refs.some((ref) => ref.kind === "branch" && contains(ref.sha, tag.sha));
74699
- if (!released)
74700
- return failure3("blocked", `tag_unreleased: ${tag.name} (${tag.sha.slice(0, 12)}) is not on ${plan.remote}/${plan.production ?? "the production branch"}${plan.refs.some((ref) => ref.kind === "branch") ? " or a branch in this push" : ""}; a release tag marks a commit that already landed`, `land it through a PR first (workit pr create), git fetch ${plan.remote}, then tag the merged commit`);
75108
+ remoteTip = tip.sha;
74701
75109
  }
75110
+ const pushed = plan.refs.find((ref) => ref.kind === "branch" && ref.name.toLowerCase() === production?.toLowerCase());
75111
+ const released = remoteTip !== null && contains(remoteTip, tag.sha) || pushed !== undefined && contains(pushed.sha, tag.sha);
75112
+ if (!released)
75113
+ return failure3("blocked", `tag_unreleased: ${tag.name} (${tag.sha.slice(0, 12)}) is not on ${plan.remote}/${production ?? "the production branch"}${pushed ? ` or the ${production} in this push` : ""}; a release tag marks a commit that already landed on its line's production branch`, `land it through a PR first (workit pr create), git fetch ${plan.remote}, then tag the merged commit`);
74702
75114
  }
74703
75115
  const pending = plan.refs.filter((ref) => previous.get(ref.ref) !== ref.sha);
74704
75116
  if (pending.length) {
@@ -74777,7 +75189,7 @@ ${run.stdout}`;
74777
75189
  } : { ok: false, lease: false, error: gitError(run) };
74778
75190
  }
74779
75191
  var COMMIT_TIMEOUT_MS, PUSH_TIMEOUT_MS = 120000, git5 = (cwd, args, options = {}) => {
74780
- const result = spawnSync18("git", args, {
75192
+ const result = spawnSync19("git", args, {
74781
75193
  cwd,
74782
75194
  encoding: "utf8",
74783
75195
  env: { ...options.env ?? process.env, GIT_TERMINAL_PROMPT: "0" },
@@ -74812,18 +75224,17 @@ ${run.stdout ?? ""}`).split(/\r?\n/u).map((value) => value.trimEnd()).filter((va
74812
75224
  ...lines.slice(-half)
74813
75225
  ].join(`
74814
75226
  `);
74815
- }, 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, releaseTagPrefix = (cwd) => {
74816
- const configured = git5(cwd, ["config", "--get", "gitflow.prefix.versiontag"]);
74817
- return configured.ok ? configured.stdout.trim() : "v";
74818
- }, isReleaseTag = (name, prefix) => name.startsWith(prefix) && /^\d/u.test(name.slice(prefix.length));
75227
+ }, 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, isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), lineOfTag = (tag, lines) => lines.filter((line) => tag.startsWith(line.tagPrefix) && /^\d/u.test(tag.slice(line.tagPrefix.length))).toSorted((a, b) => b.tagPrefix.length - a.tagPrefix.length)[0] ?? null;
74819
75228
  var init_ops = __esm(() => {
74820
75229
  init_branch();
75230
+ init_branch_policy();
74821
75231
  init_commit_flavors();
74822
75232
  init_pr_create();
74823
75233
  init_release_tracks();
74824
75234
  init_vcs_config();
74825
75235
  init_workspaces();
74826
75236
  init_redact();
75237
+ init_host_session();
74827
75238
  init_ledger();
74828
75239
  init_rev();
74829
75240
  COMMIT_TIMEOUT_MS = 10 * 60000;
@@ -75312,7 +75723,7 @@ var init_report = __esm(() => {
75312
75723
  });
75313
75724
 
75314
75725
  // packages/workit-core/src/forge/pr-ops.ts
75315
- import { spawnSync as spawnSync19 } from "node:child_process";
75726
+ import { spawnSync as spawnSync20 } from "node:child_process";
75316
75727
  function fillFromCommits(cwd, resolved, base) {
75317
75728
  const tracking = `refs/remotes/${resolved.baseRemote}/${base}`;
75318
75729
  if (!base.startsWith("-"))
@@ -75320,7 +75731,7 @@ function fillFromCommits(cwd, resolved, base) {
75320
75731
  const from = resolveRef(cwd, tracking) ?? resolveRef(cwd, `refs/heads/${base}`);
75321
75732
  if (!from)
75322
75733
  return failure3("not_found", `base ${base} is not available locally`, `git fetch ${resolved.baseRemote} ${base}`);
75323
- const log = spawnSync19("git", ["log", "--reverse", "--no-merges", "--format=%s%x1f%b%x1e", `${from}..HEAD`], {
75734
+ const log = spawnSync20("git", ["log", "--reverse", "--no-merges", "--format=%s%x1f%b%x1e", `${from}..HEAD`], {
75324
75735
  cwd,
75325
75736
  encoding: "utf8",
75326
75737
  timeout: GIT_TIMEOUTS.local,
@@ -75517,7 +75928,7 @@ function squashCommitList(cwd, resolved, base, head) {
75517
75928
  const from = resolveRef(cwd, `refs/remotes/${resolved.baseRemote}/${base}`) ?? resolveRef(cwd, `refs/heads/${base}`);
75518
75929
  if (!from)
75519
75930
  return "";
75520
- const log = spawnSync19("git", ["log", "--reverse", "--no-merges", "--format=%B%x1e", `${from}..${head}`], {
75931
+ const log = spawnSync20("git", ["log", "--reverse", "--no-merges", "--format=%B%x1e", `${from}..${head}`], {
75521
75932
  cwd,
75522
75933
  encoding: "utf8",
75523
75934
  timeout: GIT_TIMEOUTS.local,
@@ -75590,7 +76001,7 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
75590
76001
  if (reason)
75591
76002
  return refuse(`protected_branch: --delete-branch would delete ${branch}, which ${reason}; workit never deletes it`, `workit pr merge${input.pr ? ` --pr ${doc.number}` : ""} --method ${input.method} # without --delete-branch`, { reason: "protected_branch" });
75592
76003
  }
75593
- const ledger = readLedger(cwd);
76004
+ const ledger = readLedger(cwd, { integrity: true });
75594
76005
  if (!ledger.ok)
75595
76006
  return ledger;
75596
76007
  const check = checkVerdicts(cwd, branch, ledger.value.rows);
@@ -75622,18 +76033,54 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
75622
76033
  unverified = { reason, recorded: String(recorded.value.id) };
75623
76034
  } else if (check.head !== head)
75624
76035
  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 });
76036
+ else if (!covered && check.accepted.reasons.includes("ledger_tampered"))
76037
+ return refuse(`ledger_tampered: a ledger row was edited, removed or moved, so no verdict for ${label} is accepted until it is repaired`, tamperedUnblock(cwd), { reason: "ledger_tampered", verdict: summary });
75625
76038
  else if (!covered)
75626
76039
  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 });
75627
76040
  const squashMessage = squashMessageFor(cwd, resolved, input, doc);
75628
76041
  if (squashMessage && !squashMessage.ok)
75629
76042
  return squashMessage;
76043
+ const intent = appendObserved(cwd, {
76044
+ type: "pr.merge.intent",
76045
+ actor: input.actor,
76046
+ branch,
76047
+ head,
76048
+ pr: doc.number,
76049
+ base: doc.base,
76050
+ method: input.method,
76051
+ verdictId: verdict.verdictId,
76052
+ ...unverified ? { unverified: unverified.recorded } : {},
76053
+ grant: grant.source
76054
+ });
76055
+ if (!intent.ok)
76056
+ return refuse(`intent_unrecorded: the merge intent for ${label} could not be recorded (${intent.error}); nothing was merged`, "fix the ledger (workit ledger check), then merge again", { reason: "intent_unrecorded" });
76057
+ const intentId = String(intent.value.id);
75630
76058
  const merged = resolved.forge.merge(doc.number, {
75631
76059
  sha: head,
75632
76060
  method: input.method,
75633
76061
  ...squashMessage ? { squashMessage: squashMessage.data } : {}
75634
76062
  });
75635
76063
  if (!merged.ok)
75636
- return merged;
76064
+ return {
76065
+ ...merged,
76066
+ error: `${merged.error} (merge intent ${intentId} is recorded: if the forge merged anyway, workit pr status reads it)`
76067
+ };
76068
+ const row = appendObserved(cwd, {
76069
+ type: "pr.merged",
76070
+ actor: input.actor,
76071
+ branch,
76072
+ head,
76073
+ pr: doc.number,
76074
+ base: doc.base,
76075
+ mergeSha: merged.data.mergeSha,
76076
+ method: input.method,
76077
+ verdictId: verdict.verdictId,
76078
+ ...unverified ? { unverified: unverified.recorded } : {},
76079
+ grant: grant.source,
76080
+ intent: intentId,
76081
+ verified: false
76082
+ });
76083
+ const recorded = row.ok ? { id: String(row.value.id) } : { error: row.error };
75637
76084
  let state = "open";
75638
76085
  for (const delay of [0, ...VERIFY_DELAYS_MS]) {
75639
76086
  if (delay)
@@ -75648,9 +76095,21 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
75648
76095
  return {
75649
76096
  ok: false,
75650
76097
  code: "failed",
75651
- error: `merge_unverified: the forge accepted the merge of ${label} but still reports it ${state}`,
75652
- unblock: `workit verify-delivery merged --pr ${doc.number}`
76098
+ error: `merge_unverified: the forge accepted the merge of ${label} but still reports it ${state}${"id" in recorded ? `; recorded as pr.merged ${recorded.id} (verified: false)` : `; the pr.merged row could not be written (${recorded.error})`}`,
76099
+ unblock: `workit verify-delivery merged --pr ${doc.number}`,
76100
+ recorded
75653
76101
  };
76102
+ const confirmation = "id" in recorded ? appendObserved(cwd, {
76103
+ type: "pr.merge.verified",
76104
+ actor: input.actor,
76105
+ branch,
76106
+ head,
76107
+ pr: doc.number,
76108
+ base: doc.base,
76109
+ merged: recorded.id,
76110
+ mergeSha: merged.data.mergeSha
76111
+ }) : null;
76112
+ const confirmed = confirmation === null ? { error: "no pr.merged row to confirm" } : confirmation.ok ? { id: String(confirmation.value.id) } : { error: confirmation.error };
75654
76113
  let deletedBranch = false;
75655
76114
  if (input.deleteBranch) {
75656
76115
  const deleted = deleteRemoteBranch(cwd, resolved.remote, branch, head);
@@ -75658,19 +76117,6 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
75658
76117
  deletedBranch = deleted.ok || gone && gone.ok && gone.sha === null ? true : { error: deleted.error };
75659
76118
  }
75660
76119
  const mergeBack = mergeBackFor(cwd, doc.base, branch);
75661
- const row = appendObserved(cwd, {
75662
- type: "pr.merged",
75663
- actor: input.actor,
75664
- branch,
75665
- head,
75666
- pr: doc.number,
75667
- base: doc.base,
75668
- mergeSha: merged.data.mergeSha,
75669
- method: input.method,
75670
- verdictId: verdict.verdictId,
75671
- ...unverified ? { unverified: unverified.recorded } : {},
75672
- grant: grant.source
75673
- });
75674
76120
  return {
75675
76121
  ok: true,
75676
76122
  data: {
@@ -75685,7 +76131,8 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
75685
76131
  unverified,
75686
76132
  grant: { source: grant.source },
75687
76133
  deletedBranch,
75688
- recorded: row.ok ? { id: String(row.value.id) } : { error: row.error },
76134
+ recorded,
76135
+ confirmed,
75689
76136
  mergeBack: mergeBack.branches,
75690
76137
  mergeBackTrack: mergeBack.track
75691
76138
  }
@@ -75743,8 +76190,8 @@ var init_pr_ops = __esm(() => {
75743
76190
  });
75744
76191
 
75745
76192
  // packages/workit-core/src/stack.ts
75746
- import { spawnSync as spawnSync20 } from "node:child_process";
75747
- import { createHash as createHash17, randomBytes as randomBytes3 } from "node:crypto";
76193
+ import { spawnSync as spawnSync21 } from "node:child_process";
76194
+ import { createHash as createHash17, randomBytes as randomBytes4 } from "node:crypto";
75748
76195
  import fs22 from "node:fs";
75749
76196
  import path44 from "node:path";
75750
76197
  function worktreeOf(cwd, branch) {
@@ -75790,7 +76237,7 @@ function gitRestack(cwd, input) {
75790
76237
  if (!scratch)
75791
76238
  return { ok: false, conflict: false, worktree: cwd, error: "no store root" };
75792
76239
  fs22.mkdirSync(scratch, { recursive: true });
75793
- dir = path44.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes3(4).toString("hex")}`);
76240
+ dir = path44.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes4(4).toString("hex")}`);
75794
76241
  const added = gitRun3(cwd, ["worktree", "add", "-q", dir, input.branch], GIT_TIMEOUTS.worktree);
75795
76242
  if (!added.ok)
75796
76243
  return { ok: false, conflict: false, worktree: cwd, error: firstLine(added) };
@@ -75844,7 +76291,7 @@ function stacksDir(cwd) {
75844
76291
  return { ok: true, data: path44.join(root.value.root, "stacks") };
75845
76292
  }
75846
76293
  function parseStack(raw) {
75847
- if (!isRecord8(raw) || !Array.isArray(raw.branches))
76294
+ if (!isRecord9(raw) || !Array.isArray(raw.branches))
75848
76295
  return null;
75849
76296
  const name = str3(raw.name);
75850
76297
  const trunk = str3(raw.trunk);
@@ -75852,12 +76299,12 @@ function parseStack(raw) {
75852
76299
  return null;
75853
76300
  const branches = [];
75854
76301
  for (const item of raw.branches) {
75855
- if (!isRecord8(item))
76302
+ if (!isRecord9(item))
75856
76303
  return null;
75857
76304
  const branch = str3(item.branch);
75858
76305
  if (!branch)
75859
76306
  return null;
75860
- const merged = isRecord8(item.merged) ? {
76307
+ const merged = isRecord9(item.merged) ? {
75861
76308
  pr: typeof item.merged.pr === "number" ? item.merged.pr : null,
75862
76309
  mergeSha: str3(item.merged.mergeSha),
75863
76310
  at: str3(item.merged.at) ?? ""
@@ -75870,7 +76317,7 @@ function parseStack(raw) {
75870
76317
  lastParentHead: str3(item.lastParentHead),
75871
76318
  patchId: str3(item.patchId),
75872
76319
  merged,
75873
- ...isRecord8(item.pendingRestack) && str3(item.pendingRestack.from) && str3(item.pendingRestack.tip) && str3(item.pendingRestack.onto) ? {
76320
+ ...isRecord9(item.pendingRestack) && str3(item.pendingRestack.from) && str3(item.pendingRestack.tip) && str3(item.pendingRestack.onto) ? {
75874
76321
  pendingRestack: {
75875
76322
  from: str3(item.pendingRestack.from),
75876
76323
  tip: str3(item.pendingRestack.tip),
@@ -75999,13 +76446,13 @@ function takeLock(lock) {
75999
76446
  pid: process.pid,
76000
76447
  processStart: processStartOf(process.pid),
76001
76448
  host: localLockHost(),
76002
- nonce: randomBytes3(8).toString("hex")
76449
+ nonce: randomBytes4(8).toString("hex")
76003
76450
  };
76004
76451
  fs22.writeFileSync(path44.join(lock, OWNER), JSON.stringify(owner));
76005
76452
  return owner;
76006
76453
  }
76007
76454
  function reapLock(lock, observed) {
76008
- const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
76455
+ const tomb = `${lock}.reap-${randomBytes4(6).toString("hex")}`;
76009
76456
  try {
76010
76457
  fs22.renameSync(lock, tomb);
76011
76458
  } catch {
@@ -76026,7 +76473,7 @@ function releaseLock(lock, owner) {
76026
76473
  const { raw } = readOwner(lock);
76027
76474
  if (raw !== JSON.stringify(owner))
76028
76475
  return;
76029
- const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
76476
+ const tomb = `${lock}.release-${randomBytes4(6).toString("hex")}`;
76030
76477
  try {
76031
76478
  fs22.renameSync(lock, tomb);
76032
76479
  fs22.rmSync(tomb, { recursive: true, force: true });
@@ -76256,7 +76703,7 @@ function commitKeys(cwd, range) {
76256
76703
  const patches = new Map;
76257
76704
  const diff = gitRun3(cwd, ["log", "--no-merges", "-p", "--format=commit %H", range]);
76258
76705
  if (diff.ok && diff.stdout.trim()) {
76259
- const ids = spawnSync20("git", ["patch-id", "--stable"], {
76706
+ const ids = spawnSync21("git", ["patch-id", "--stable"], {
76260
76707
  cwd,
76261
76708
  input: diff.stdout,
76262
76709
  encoding: "utf8",
@@ -76349,7 +76796,7 @@ function stackStatus(ctx, stack) {
76349
76796
  const mismatch = repoMismatch(ctx, stack);
76350
76797
  if (mismatch)
76351
76798
  return mismatch;
76352
- const ledger = readLedger(ctx.cwd);
76799
+ const ledger = readLedger(ctx.cwd, { integrity: true });
76353
76800
  if (!ledger.ok)
76354
76801
  return stackFail(ledger.code, ledger.error, ledger.unblock);
76355
76802
  const remote = remoteFor(ctx.cwd, ctx.resolved, stack.trunk);
@@ -76773,7 +77220,7 @@ async function landStack(ctx, stack, options) {
76773
77220
  absorb(synced.data);
76774
77221
  }
76775
77222
  const ledgerRows = () => {
76776
- const ledger = readLedger(ctx.cwd);
77223
+ const ledger = readLedger(ctx.cwd, { integrity: true });
76777
77224
  return ledger.ok ? ledger.value.rows : [];
76778
77225
  };
76779
77226
  const simulated = new Set;
@@ -76880,7 +77327,7 @@ var STACK_VERSION = 1, stackFail = (code, error, unblock, data) => ({
76880
77327
  ...unblock ? { unblock } : {},
76881
77328
  ...data ? { data } : {}
76882
77329
  }), fromForge = (result) => stackFail(result.code, result.error, result.unblock), stackDeps, gitRun3 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
76883
- const run = spawnSync20("git", args, {
77330
+ const run = spawnSync21("git", args, {
76884
77331
  cwd,
76885
77332
  encoding: "utf8",
76886
77333
  timeout: timeoutMs,
@@ -76920,7 +77367,7 @@ ${run.stdout}`.split(`
76920
77367
  return false;
76921
77368
  }
76922
77369
  return true;
76923
- }, 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) => {
77370
+ }, isRecord9 = (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) => {
76924
77371
  let raw = null;
76925
77372
  try {
76926
77373
  raw = fs22.readFileSync(path44.join(lock, OWNER), "utf8");
@@ -76961,7 +77408,7 @@ var init_stack = __esm(() => {
76961
77408
  });
76962
77409
 
76963
77410
  // packages/workit-core/src/forge/pr-edit.ts
76964
- import { spawnSync as spawnSync21 } from "node:child_process";
77411
+ import { spawnSync as spawnSync22 } from "node:child_process";
76965
77412
  function openPr(cwd, resolved, pr) {
76966
77413
  const grant = requireGrant(cwd, "pr");
76967
77414
  if (!grant.allowed)
@@ -77126,7 +77573,7 @@ function replyToThread(cwd, resolved, input) {
77126
77573
  });
77127
77574
  }
77128
77575
  var label = (resolved, number) => `${resolved.forge.kind === "github" ? "PR #" : "MR !"}${number}`, git6 = (cwd, args) => {
77129
- const run = spawnSync21("git", args, {
77576
+ const run = spawnSync22("git", args, {
77130
77577
  cwd,
77131
77578
  encoding: "utf8",
77132
77579
  timeout: GIT_TIMEOUTS.local,
@@ -77146,7 +77593,7 @@ var init_pr_edit = __esm(() => {
77146
77593
  });
77147
77594
 
77148
77595
  // packages/workit-core/src/forge/exec.ts
77149
- import { spawnSync as spawnSync22 } from "node:child_process";
77596
+ import { spawnSync as spawnSync23 } from "node:child_process";
77150
77597
  import fs23 from "node:fs";
77151
77598
  import path45 from "node:path";
77152
77599
  function findExecutable(bin, env) {
@@ -77181,7 +77628,7 @@ function systemRunner(kind, apiHost, env = process.env) {
77181
77628
  if (!exe)
77182
77629
  return { status: null, stdout: "", stderr: "", timedOut: false, missing: true };
77183
77630
  const token = options.token ? kind === "github" ? { GH_TOKEN: options.token, GH_ENTERPRISE_TOKEN: options.token } : { GITLAB_TOKEN: options.token } : {};
77184
- const result = spawnSync22(exe, [...args], {
77631
+ const result = spawnSync23(exe, [...args], {
77185
77632
  encoding: "utf8",
77186
77633
  env: { ...childEnv, ...token },
77187
77634
  maxBuffer: MAX_BUFFER2,
@@ -77678,6 +78125,24 @@ function createGitHubForge(options) {
77678
78125
  if (node?.pullRequest?.number !== number)
77679
78126
  return success2(null);
77680
78127
  return success2(node.isResolved ? "resolved" : "open");
78128
+ },
78129
+ findRelease(tag) {
78130
+ const found = rest(`repos/${repo}/releases/tags/${encodeRef(tag)}`, `release ${tag}`);
78131
+ if (!found.ok)
78132
+ return found.code === "not_found" ? success2(null) : found;
78133
+ return success2({
78134
+ tag: found.data.tag_name ?? tag,
78135
+ url: safeUrl(found.data.html_url ?? null)
78136
+ });
78137
+ },
78138
+ createRelease(input) {
78139
+ const created = apiWrite(runner, "gh", apiHost, ["api", "-X", "POST", `repos/${repo}/releases`, "--input", "-"], `release ${input.tag}`, undefined, JSON.stringify({ tag_name: input.tag, name: input.name, body: input.body }));
78140
+ if (!created.ok)
78141
+ return created;
78142
+ return success2({
78143
+ tag: created.data.tag_name ?? input.tag,
78144
+ url: safeUrl(created.data.html_url ?? null)
78145
+ });
77681
78146
  }
77682
78147
  };
77683
78148
  }
@@ -78063,6 +78528,24 @@ ${squash.body}` : squash.subject
78063
78528
  if (!notes.some((note) => note.resolvable === true))
78064
78529
  return success2(null);
78065
78530
  return success2(notes.some((note) => note.resolvable === true && note.resolved !== true) ? "open" : "resolved");
78531
+ },
78532
+ findRelease(tag) {
78533
+ const found = apiJson(runner, "glab", apiHost, api(`${project}/releases/${encodeURIComponent(tag)}`), `release ${tag}`);
78534
+ if (!found.ok)
78535
+ return found.code === "not_found" ? success2(null) : found;
78536
+ return success2({
78537
+ tag: found.data.tag_name ?? tag,
78538
+ url: safeUrl2(found.data["_links"]?.self ?? null)
78539
+ });
78540
+ },
78541
+ createRelease(input) {
78542
+ const created = apiWrite(runner, "glab", apiHost, ["api", "-X", "POST", `${project}/releases`, "--input", "-"], `release ${input.tag}`, undefined, JSON.stringify({ tag_name: input.tag, name: input.name, description: input.body }));
78543
+ if (!created.ok)
78544
+ return created;
78545
+ return success2({
78546
+ tag: created.data.tag_name ?? input.tag,
78547
+ url: safeUrl2(created.data["_links"]?.self ?? null)
78548
+ });
78066
78549
  }
78067
78550
  };
78068
78551
  }
@@ -78145,6 +78628,14 @@ function resolveCredential(workspace, derived, run, scope) {
78145
78628
  const explicit = typeof workspaceHost === "string" && workspaceHost.trim().toLowerCase().replace(/^https?:\/\//u, "").replace(/\/.*$/u, "") === apiHost;
78146
78629
  if (derived.via === "host_name" && !explicit)
78147
78630
  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`);
78631
+ if (process.platform !== "win32") {
78632
+ let mode = 0;
78633
+ try {
78634
+ mode = fs24.statSync(tokenFile.trim()).mode;
78635
+ } catch {}
78636
+ if ((mode & 63) !== 0)
78637
+ return failure3("blocked", `workspace ${workspace?.name ?? "?"} vcs.tokenFile is readable by group or other (mode ${(mode & 511).toString(8).padStart(4, "0")})`, `chmod 600 ${tokenFile.trim()}`);
78638
+ }
78148
78639
  let token = "";
78149
78640
  try {
78150
78641
  token = fs24.readFileSync(tokenFile.trim(), "utf8").trim();
@@ -78568,17 +79059,20 @@ async function status(argv, io) {
78568
79059
  });
78569
79060
  }
78570
79061
  function verdictBlock(cwd, branch, doc) {
78571
- const ledger = readLedger(cwd);
79062
+ const ledger = readLedger(cwd, { integrity: true });
78572
79063
  if (!ledger.ok)
78573
79064
  return null;
78574
79065
  const check = checkVerdicts(cwd, branch, ledger.value.rows);
78575
79066
  let landed = null;
78576
79067
  if (doc.state === "merged") {
78577
- const row = ledger.value.rows.findLast((candidate) => candidate.type === "pr.merged" && candidate.observer === "workit_cli" && candidate.pr === doc.number && candidate.base === doc.base);
79068
+ const mine = (type) => ledger.value.rows.findLast((candidate) => candidate.type === type && candidate.observer === "workit_cli" && candidate.integrity !== "unverified" && candidate.pr === doc.number && candidate.base === doc.base);
79069
+ const merged = mine("pr.merged");
79070
+ const row = merged ?? mine("pr.merge.intent");
78578
79071
  landed = row ? {
78579
79072
  head: typeof row.head === "string" ? row.head : null,
78580
79073
  verdictId: typeof row.verdictId === "string" ? row.verdictId : null,
78581
- unverified: row.unverified !== undefined && row.unverified !== null
79074
+ unverified: row.unverified !== undefined && row.unverified !== null,
79075
+ ...merged ? {} : { intentOnly: true }
78582
79076
  } : "untracked";
78583
79077
  }
78584
79078
  return {
@@ -78749,7 +79243,7 @@ async function merge3(argv, io) {
78749
79243
  if (!result.ok)
78750
79244
  return emit(io, fail(result.code, result.error, {
78751
79245
  unblock: result.unblock,
78752
- data: "refusal" in result && result.refusal ? result.refusal : {}
79246
+ data: "refusal" in result && result.refusal ? result.refusal : ("recorded" in result) && result.recorded ? { recorded: result.recorded } : {}
78753
79247
  }));
78754
79248
  return emit(io, ok(result.data), (data) => [
78755
79249
  `merged ${connected.data.forge.kind === "github" ? "PR #" : "MR !"}${data.number} (${data.method}) at head ${data.head.slice(0, 12)}${data.mergeSha ? ` -> ${data.mergeSha.slice(0, 12)}` : ""}`,
@@ -78758,7 +79252,8 @@ async function merge3(argv, io) {
78758
79252
  `merge back: ${data.base} is the ${data.mergeBackTrack} production branch; bring it into ${data.mergeBack.join(", ")}`
78759
79253
  ] : [],
78760
79254
  ...data.deletedBranch === true ? [`deleted ${data.branch}`] : typeof data.deletedBranch === "object" ? [`branch not deleted: ${data.deletedBranch.error}`] : [],
78761
- ..."error" in data.recorded ? [`ledger: not recorded (${data.recorded.error})`] : []
79255
+ ..."error" in data.recorded ? [`ledger: not recorded (${data.recorded.error})`] : [],
79256
+ ..."error" in data.confirmed && !("error" in data.recorded) ? [`ledger: merge not confirmed in the ledger (${data.confirmed.error})`] : []
78762
79257
  ]);
78763
79258
  }
78764
79259
  async function ready(argv, io) {
@@ -78908,7 +79403,7 @@ async function run11(argv, io) {
78908
79403
  return reply(rest, io);
78909
79404
  return usage3(io, sub && !sub.startsWith("-") ? `unknown pr subcommand "${sub}"` : "missing pr subcommand", USAGE4);
78910
79405
  }
78911
- 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;
79406
+ 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)}${landed.intentOnly ? " (merge intent recorded; the merge call itself failed)" : ""}`, METHODS2;
78912
79407
  var init_pr = __esm(() => {
78913
79408
  init_pr_body();
78914
79409
  init_pr_edit();
@@ -79433,7 +79928,7 @@ var init_git2 = __esm(() => {
79433
79928
  });
79434
79929
 
79435
79930
  // packages/workit-core/src/forge/verify.ts
79436
- import { spawnSync as spawnSync23 } from "node:child_process";
79931
+ import { spawnSync as spawnSync24 } from "node:child_process";
79437
79932
  function finish(cwd, report, actor) {
79438
79933
  const delivered = report.observations.length > 0 && report.observations.every((entry) => entry.ok);
79439
79934
  const out = { ...report, delivered };
@@ -79646,7 +80141,7 @@ var NPM_NAME, SEMVER2, SAFE_ARG, systemNpm = (args) => {
79646
80141
  };
79647
80142
  const found = process.platform === "win32" ? findExecutable("npm.cmd", process.env) : null;
79648
80143
  const npmCmd = found && !/["%!^]/u.test(found) ? `"${found}"` : "npm.cmd";
79649
- const result = process.platform === "win32" ? spawnSync23(`${npmCmd} ${args.join(" ")}`, { ...options, shell: true }) : spawnSync23("npm", [...args], options);
80144
+ const result = process.platform === "win32" ? spawnSync24(`${npmCmd} ${args.join(" ")}`, { ...options, shell: true }) : spawnSync24("npm", [...args], options);
79650
80145
  return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
79651
80146
  }, short3 = (sha) => sha ? sha.slice(0, 12) : "(absent)", npmView = (stdout) => {
79652
80147
  try {
@@ -79668,12 +80163,700 @@ var init_verify = __esm(() => {
79668
80163
  SAFE_ARG = /^[0-9A-Za-z@/._~+-]+$/u;
79669
80164
  });
79670
80165
 
80166
+ // packages/workit-core/src/git/release.ts
80167
+ function lineFor(cwd, track) {
80168
+ const lines = releaseLinesOf(cwd);
80169
+ let name = track;
80170
+ if (name === null && lines.length > 1) {
80171
+ const resolved = vcsConfig("resolve", cwd);
80172
+ if (resolved.ok === false)
80173
+ return failure3("blocked", String(resolved.error));
80174
+ if (resolved.releaseTrack?.blocking)
80175
+ return failure3("blocked", String(resolved.releaseTrack.blocking), "pass --track <name>");
80176
+ name = resolved.releaseTrack?.track?.name ?? null;
80177
+ }
80178
+ const line = lines.find((entry) => entry.track === name);
80179
+ if (!line)
80180
+ return failure3("invalid_input", `no release track ${JSON.stringify(name)} (configured: ${lines.map((entry) => entry.track).filter(Boolean).join(", ") || "none"})`);
80181
+ return success2(line);
80182
+ }
80183
+ function releaseStart(cwd, input) {
80184
+ const version = input.version.trim();
80185
+ if (!validVersion(version))
80186
+ return failure3("invalid_input", `${JSON.stringify(version)} is not a version (e.g. 1.4.0)`);
80187
+ const kind = input.hotfix ? "hotfix" : "release";
80188
+ const lineResult = lineFor(cwd, input.track ?? null);
80189
+ if (!lineResult.ok)
80190
+ return lineResult;
80191
+ const line = lineResult.data;
80192
+ const template = templateOf(line, kind);
80193
+ const existing = git5(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]).stdout.split(`
80194
+ `).filter((name) => name && versionIn(template, name) !== null);
80195
+ if (existing.length)
80196
+ return failure3("blocked", `release_exists: ${existing[0]} is still open; git-flow finishes one ${kind} before starting the next`, `workit release finish # on ${existing[0]}`);
80197
+ const tag = `${line.tagPrefix}${version}`;
80198
+ if (resolveRef(cwd, `refs/tags/${tag}`))
80199
+ return failure3("blocked", `tag_exists: ${tag} already exists`, "start the next version instead");
80200
+ const base = kind === "hotfix" ? line.production : line.integration;
80201
+ const remote = pushRemoteName(cwd, base);
80202
+ if (remote) {
80203
+ fetchRefs(cwd, remote, [`+refs/heads/${base}:refs/remotes/${remote}/${base}`]);
80204
+ const local = resolveRef(cwd, `refs/heads/${base}`);
80205
+ const tracked = resolveRef(cwd, `refs/remotes/${remote}/${base}`);
80206
+ if (local && tracked && local !== tracked && !git5(cwd, ["merge-base", "--is-ancestor", local, tracked]).ok)
80207
+ return failure3("blocked", `base_unpushed: local ${base} has commits ${remote}/${base} lacks; the ${kind} would start without them`, `land them on ${remote}/${base} first, or: git switch ${base} && git reset --keep ${remote}/${base}`);
80208
+ }
80209
+ const branched = gitBranch(cwd, {
80210
+ name: branchFor(line, kind, version),
80211
+ base,
80212
+ ...line.track ? { track: line.track } : {}
80213
+ });
80214
+ if (!branched.ok)
80215
+ return branched;
80216
+ record2(cwd, input.actor, {
80217
+ type: "release.started",
80218
+ version,
80219
+ kind,
80220
+ branch: branched.data.branch,
80221
+ head: branched.data.baseSha,
80222
+ base,
80223
+ track: line.track,
80224
+ tag
80225
+ });
80226
+ return success2({
80227
+ version,
80228
+ kind,
80229
+ branch: branched.data.branch,
80230
+ base,
80231
+ baseSha: branched.data.baseSha,
80232
+ track: line.track,
80233
+ tag,
80234
+ notes: branched.data.notes
80235
+ });
80236
+ }
80237
+ function locate(cwd, version, track) {
80238
+ const lines = releaseLinesOf(cwd).filter((line) => track === null || line.track === track);
80239
+ if (!lines.length)
80240
+ return failure3("invalid_input", `no release track ${JSON.stringify(track)}`);
80241
+ const rows = releaseRows(cwd);
80242
+ const started = rows.filter((row) => row.type === "release.started");
80243
+ const finished = new Set(rows.filter((row) => row.type === "release.finished").map((row) => String(row.branch)));
80244
+ const startedRow = (branch) => started.findLast((row) => row.branch === branch && !finished.has(branch));
80245
+ const lineOf = (branch, candidates) => {
80246
+ const row = startedRow(branch);
80247
+ const byRow = row ? candidates.find((line) => line.track === (row.track ?? null)) : undefined;
80248
+ if (byRow)
80249
+ return byRow;
80250
+ const resolved = vcsConfig("resolve", cwd, { branch });
80251
+ const name = resolved.ok === false ? null : resolved.releaseTrack?.track?.name ?? null;
80252
+ return candidates.find((line) => line.track === name) ?? candidates[0];
80253
+ };
80254
+ const match = (branch) => {
80255
+ const hits = [];
80256
+ for (const line of lines)
80257
+ for (const kind of KINDS) {
80258
+ const found = versionIn(templateOf(line, kind), branch);
80259
+ if (found)
80260
+ hits.push({ line, kind, version: found });
80261
+ }
80262
+ if (!hits.length)
80263
+ return null;
80264
+ const line = lineOf(branch, hits.map((hit) => hit.line));
80265
+ const hit = hits.find((entry) => entry.line === line) ?? hits[0];
80266
+ return { ...hit, branch };
80267
+ };
80268
+ if (version !== null) {
80269
+ if (!validVersion(version))
80270
+ return failure3("invalid_input", `${JSON.stringify(version)} is not a version`);
80271
+ for (const line of lines)
80272
+ for (const kind of KINDS) {
80273
+ const branch = branchFor(line, kind, version);
80274
+ if (resolveRef(cwd, `refs/heads/${branch}`) || startedRow(branch)) {
80275
+ const located = match(branch);
80276
+ if (located)
80277
+ return success2(located);
80278
+ }
80279
+ }
80280
+ return failure3("not_found", `no release or hotfix branch for ${version} here, and none this ledger started`);
80281
+ }
80282
+ const current = currentBranch(cwd);
80283
+ const here = current ? match(current) : null;
80284
+ if (here)
80285
+ return success2(here);
80286
+ const open = started.findLast((row) => !finished.has(String(row.branch)));
80287
+ if (open) {
80288
+ const located = match(String(open.branch));
80289
+ if (located)
80290
+ return success2(located);
80291
+ }
80292
+ const branches = git5(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]).stdout.split(`
80293
+ `).map((name) => name ? match(name) : null).filter((entry) => entry !== null);
80294
+ if (branches.length === 1)
80295
+ return success2(branches[0]);
80296
+ return failure3(branches.length ? "invalid_input" : "not_found", branches.length ? `several releases are open (${branches.map((entry) => entry.branch).join(", ")}); name one` : "no release or hotfix branch to finish", branches.length ? `workit release finish ${branches[0].version}` : "workit release start <version>");
80297
+ }
80298
+ function changelogSection(cwd, ref, version) {
80299
+ const shown = git5(cwd, ["show", `${ref}:CHANGELOG.md`]);
80300
+ if (!shown.ok)
80301
+ return null;
80302
+ const lines = shown.stdout.split(/\r?\n/u);
80303
+ const escaped = version.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
80304
+ const heading = new RegExp(`^(#{1,3})\\s+\\[?v?${escaped}\\]?(?:[\\s(]|$)`, "u");
80305
+ const start = lines.findIndex((line) => heading.test(line));
80306
+ if (start < 0)
80307
+ return null;
80308
+ const level = heading.exec(lines[start])?.[1].length ?? 2;
80309
+ let end = lines.length;
80310
+ for (let index = start + 1;index < lines.length; index += 1) {
80311
+ const hashes = /^(#+)\s/u.exec(lines[index])?.[1].length;
80312
+ if (hashes !== undefined && hashes <= level) {
80313
+ end = index;
80314
+ break;
80315
+ }
80316
+ }
80317
+ const body = lines.slice(start + 1, end).join(`
80318
+ `).trim();
80319
+ return body || null;
80320
+ }
80321
+ function planReleaseFinish(cwd, input) {
80322
+ const progress = operationInProgress(cwd);
80323
+ if (progress)
80324
+ return inProgressFailure(progress.operation, "finish the release");
80325
+ const located = locate(cwd, input.version?.trim() || null, input.track ?? null);
80326
+ if (!located.ok)
80327
+ return located;
80328
+ const { line, kind, version, branch } = located.data;
80329
+ const notes = [];
80330
+ const dirty = git5(cwd, ["status", "--porcelain", "--untracked-files=no"]).stdout.trim();
80331
+ if (dirty)
80332
+ return failure3("blocked", "dirty_worktree: tracked files have uncommitted changes; a release finish merges on a clean tree", "workit git commit -m '<msg>' -- <paths> # on the release branch, or stash them");
80333
+ const tag = `${line.tagPrefix}${version}`;
80334
+ const rows = releaseRows(cwd).filter((row) => row.branch === branch || row.release === branch);
80335
+ const recordedOpen = rows.findLast((row) => row.type === "release.hotfix_target");
80336
+ const openRelease = kind !== "hotfix" ? null : typeof recordedOpen?.target === "string" ? recordedOpen.target : git5(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]).stdout.split(`
80337
+ `).find((name) => name && versionIn(templateOf(line, "release"), name) !== null) ?? null;
80338
+ const mergeBack = openRelease ? [...new Set(line.mergeBack.map((name) => name === line.integration ? openRelease : name))] : line.mergeBack;
80339
+ if (openRelease)
80340
+ notes.push(`${openRelease} is open: the hotfix merges back into it instead of ${line.integration}`);
80341
+ const remote = pushRemoteName(cwd, line.production);
80342
+ if (remote) {
80343
+ const wanted = [line.production, ...mergeBack, branch];
80344
+ for (const name of wanted)
80345
+ fetchRefs(cwd, remote, [`+refs/heads/${name}:refs/remotes/${remote}/${name}`]);
80346
+ }
80347
+ const branchTip = resolveRef(cwd, `refs/heads/${branch}`);
80348
+ const remoteBranch = remote ? resolveRef(cwd, `refs/remotes/${remote}/${branch}`) : null;
80349
+ const mergedRow = rows.findLast((row) => row.type === "release.merged");
80350
+ const deletedRow = rows.findLast((row) => row.type === "release.branch_deleted");
80351
+ const recordedTip = [mergedRow?.releaseTip, deletedRow?.head].find((value) => typeof value === "string");
80352
+ const releaseTip = branchTip ?? recordedTip ?? null;
80353
+ if (!releaseTip)
80354
+ return failure3("not_found", `${branch} is gone and this ledger did not record its merge; nothing to resume`);
80355
+ for (const name of [line.production, ...mergeBack]) {
80356
+ const local = resolveRef(cwd, `refs/heads/${name}`);
80357
+ const tracked = remote ? resolveRef(cwd, `refs/remotes/${remote}/${name}`) : null;
80358
+ if (!local && tracked) {
80359
+ const created = git5(cwd, ["branch", "--no-track", name, tracked]);
80360
+ if (!created.ok)
80361
+ return failure3("failed", `could not create local ${name}: ${gitError(created)}`);
80362
+ notes.push(`created local ${name} from ${remote}/${name}`);
80363
+ continue;
80364
+ }
80365
+ if (!local)
80366
+ return failure3("not_found", `${name} exists neither locally nor on ${remote ?? "a remote"}`);
80367
+ if (!tracked || name === openRelease)
80368
+ continue;
80369
+ if (!isAncestor3(cwd, tracked, local))
80370
+ return failure3("blocked", `branch_behind: local ${name} is behind or diverged from ${remote}/${name}; git-flow finishes only onto up-to-date branches`, `git switch ${name} && git merge --ff-only ${remote}/${name} # then: workit release finish ${version}`);
80371
+ const productionTracked = remote ? resolveRef(cwd, `refs/remotes/${remote}/${line.production}`) : null;
80372
+ const foreign = git5(cwd, [
80373
+ "rev-list",
80374
+ local,
80375
+ `^${tracked}`,
80376
+ `^${releaseTip}`,
80377
+ ...productionTracked ? [`^${productionTracked}`] : []
80378
+ ]).stdout.split(`
80379
+ `).filter(Boolean).filter((sha) => !(git5(cwd, ["rev-parse", "-q", "--verify", `${sha}^2`]).ok && isAncestor3(cwd, releaseTip, sha)));
80380
+ if (foreign.length)
80381
+ return failure3("blocked", `branch_ahead: local ${name} has ${foreign.length} commit(s) ${remote}/${name} lacks that are not part of ${branch}; the finish push would publish them`, `land them through review first, or: git switch ${name} && git reset --keep ${remote}/${name} # then: workit release finish ${version}`);
80382
+ }
80383
+ if (branchTip && remoteBranch && !isAncestor3(cwd, remoteBranch, branchTip))
80384
+ return failure3("blocked", `release_behind: ${remote}/${branch} has commits local ${branch} lacks`, `git switch ${branch} && git merge --ff-only ${remote}/${branch}`);
80385
+ const production = resolveRef(cwd, `refs/heads/${line.production}`);
80386
+ const merged = isAncestor3(cwd, releaseTip, production);
80387
+ const localTag = resolveRef(cwd, `refs/tags/${tag}`);
80388
+ if (localTag && !(isAncestor3(cwd, localTag, production) && isAncestor3(cwd, releaseTip, localTag)))
80389
+ return failure3("blocked", `tag_exists: ${tag} (${localTag.slice(0, 12)}) is not this release's merge on ${line.production}`, "a release tag never moves; finish under the next version");
80390
+ if (remote) {
80391
+ const remoteTag = remoteRefTip(cwd, remote, `refs/tags/${tag}`);
80392
+ if (remoteTag.ok && remoteTag.sha && remoteTag.sha !== localTag)
80393
+ return failure3("blocked", `tag_exists: ${remote} already has ${tag} at ${remoteTag.sha.slice(0, 12)}`, "a release tag never moves; finish under the next version");
80394
+ }
80395
+ const recordedMerge = typeof mergedRow?.merge === "string" ? mergedRow.merge : null;
80396
+ const tagTarget = localTag ?? (merged ? recordedMerge ?? production : null);
80397
+ const state = {
80398
+ merged,
80399
+ tagged: localTag !== null,
80400
+ mergedBack: Object.fromEntries(mergeBack.map((name) => [
80401
+ name,
80402
+ localTag !== null && isAncestor3(cwd, localTag, resolveRef(cwd, `refs/heads/${name}`))
80403
+ ])),
80404
+ branchDeleted: branchTip === null
80405
+ };
80406
+ return success2({
80407
+ version,
80408
+ kind,
80409
+ branch,
80410
+ track: line.track,
80411
+ tag,
80412
+ production: line.production,
80413
+ mergeBack,
80414
+ pushBranches: [line.production, ...mergeBack.filter((name) => name !== openRelease)],
80415
+ openRelease,
80416
+ gitflow: line.gitflow,
80417
+ remote,
80418
+ releaseTip,
80419
+ published: remoteBranch,
80420
+ tagTarget,
80421
+ mergeRecorded: mergedRow !== undefined,
80422
+ state,
80423
+ changelog: changelogSection(cwd, branchTip ?? production, version),
80424
+ notes,
80425
+ warnings: integrationPrWarning(cwd, line.production)
80426
+ });
80427
+ }
80428
+ function integrationPrWarning(cwd, production) {
80429
+ try {
80430
+ const selected = resolveRuntimeWorkspacePolicy(cwd, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined).policy;
80431
+ return selected?.integration === "pr" ? [
80432
+ `the workspace integration mode is pr: the remote may require pull requests on ${production}, which would reject the finish push`
80433
+ ] : [];
80434
+ } catch {
80435
+ return [];
80436
+ }
80437
+ }
80438
+ function finishLocally(cwd, plan, actor) {
80439
+ const ran = [];
80440
+ const base = { version: plan.version, release: plan.branch, tag: plan.tag, track: plan.track };
80441
+ let tagTarget = plan.tagTarget;
80442
+ if (plan.openRelease && !releaseRows(cwd).some((row) => row.type === "release.hotfix_target" && row.release === plan.branch))
80443
+ record2(cwd, actor, {
80444
+ type: "release.hotfix_target",
80445
+ ...base,
80446
+ branch: plan.branch,
80447
+ target: plan.openRelease
80448
+ });
80449
+ if (plan.state.merged && !plan.mergeRecorded && tagTarget)
80450
+ record2(cwd, actor, {
80451
+ type: "release.merged",
80452
+ ...base,
80453
+ branch: plan.production,
80454
+ head: tagTarget,
80455
+ merge: tagTarget,
80456
+ releaseTip: plan.releaseTip,
80457
+ observed: true
80458
+ });
80459
+ if (!plan.state.merged) {
80460
+ const on = switchTo(cwd, plan.production);
80461
+ if (!on.ok)
80462
+ return on;
80463
+ const merged = mergeNoFf(cwd, plan.branch, plan.production, plan.version);
80464
+ if (!merged.ok)
80465
+ return merged;
80466
+ tagTarget = merged.data;
80467
+ ran.push("merge");
80468
+ record2(cwd, actor, {
80469
+ type: "release.merged",
80470
+ ...base,
80471
+ branch: plan.production,
80472
+ head: merged.data,
80473
+ merge: merged.data,
80474
+ releaseTip: plan.releaseTip
80475
+ });
80476
+ }
80477
+ if (!tagTarget)
80478
+ return failure3("failed", `cannot tell which ${plan.production} commit to tag`);
80479
+ if (!plan.state.tagged) {
80480
+ const message = plan.changelog ?? `Release ${plan.version}`;
80481
+ const tagged = git5(cwd, [
80482
+ "tag",
80483
+ "-a",
80484
+ "--cleanup=verbatim",
80485
+ plan.tag,
80486
+ "-m",
80487
+ message,
80488
+ tagTarget
80489
+ ]);
80490
+ if (!tagged.ok)
80491
+ return failure3("failed", `git tag ${plan.tag} failed: ${gitError(tagged)}`);
80492
+ ran.push("tag");
80493
+ record2(cwd, actor, {
80494
+ type: "release.tagged",
80495
+ ...base,
80496
+ branch: plan.production,
80497
+ head: tagTarget
80498
+ });
80499
+ }
80500
+ for (const name of plan.mergeBack) {
80501
+ if (plan.state.mergedBack[name])
80502
+ continue;
80503
+ const on = switchTo(cwd, name);
80504
+ if (!on.ok)
80505
+ return on;
80506
+ const merged = mergeNoFf(cwd, plan.tag, name, plan.version);
80507
+ if (!merged.ok)
80508
+ return merged;
80509
+ ran.push(`merge-back ${name}`);
80510
+ record2(cwd, actor, { type: "release.merged_back", ...base, branch: name, head: merged.data });
80511
+ }
80512
+ if (!plan.state.branchDeleted) {
80513
+ if (currentBranch(cwd) === plan.branch) {
80514
+ const on = switchTo(cwd, plan.mergeBack.at(-1) ?? plan.production);
80515
+ if (!on.ok)
80516
+ return on;
80517
+ }
80518
+ const deleted = git5(cwd, ["branch", "-d", plan.branch]);
80519
+ if (!deleted.ok)
80520
+ return failure3("failed", `git branch -d ${plan.branch} failed: ${gitError(deleted)}`, plan.published ? `workit release publish ${plan.version} # its remote copy lacks commits; then: workit release finish ${plan.version}` : undefined);
80521
+ ran.push("delete-branch");
80522
+ record2(cwd, actor, {
80523
+ type: "release.branch_deleted",
80524
+ ...base,
80525
+ branch: plan.branch,
80526
+ head: plan.releaseTip
80527
+ });
80528
+ }
80529
+ return success2({ ran, tagTarget });
80530
+ }
80531
+ function recordReleaseStep(cwd, actor, plan, row) {
80532
+ record2(cwd, actor, {
80533
+ version: plan.version,
80534
+ release: plan.branch,
80535
+ tag: plan.tag,
80536
+ track: plan.track,
80537
+ ...row
80538
+ });
80539
+ }
80540
+ function releaseToPublish(cwd, version) {
80541
+ const located = locate(cwd, version, null);
80542
+ if (!located.ok)
80543
+ return located;
80544
+ if (!resolveRef(cwd, `refs/heads/${located.data.branch}`))
80545
+ return failure3("not_found", `${located.data.branch} is not a local branch`);
80546
+ return located;
80547
+ }
80548
+ var VERSION, PLACEHOLDER3, validVersion = (version) => VERSION.test(version) && !version.includes("..") && !version.endsWith(".lock"), templateOf = (line, kind) => {
80549
+ const template = line.naming[kind];
80550
+ if (!template)
80551
+ return `${kind}/{version}`;
80552
+ return /\{[^}]*\}/u.test(template) ? template : `${template.replace(/\/?\*?$/u, "")}/{version}`;
80553
+ }, branchFor = (line, kind, version) => trackBranchName({ naming: line.naming }, kind, version) ?? `${kind}/${version}`, versionIn = (template, branch) => {
80554
+ const pattern = template.split(PLACEHOLDER3).map((part) => part.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join("(.+)");
80555
+ const match = new RegExp(`^${pattern}$`, "u").exec(branch);
80556
+ return match?.[1] && validVersion(match[1]) ? match[1] : null;
80557
+ }, releaseRows = (cwd) => {
80558
+ const ledger = readLedger(cwd);
80559
+ if (!ledger.ok)
80560
+ return [];
80561
+ return ledger.value.rows.filter((row) => row.type.startsWith("release."));
80562
+ }, record2 = (cwd, actor, row) => {
80563
+ appendObserved(cwd, { ...row, actor });
80564
+ }, KINDS, isAncestor3 = (cwd, ancestor, descendant) => git5(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok, switchTo = (cwd, branch) => {
80565
+ if (currentBranch(cwd) === branch)
80566
+ return success2(undefined);
80567
+ const switched = git5(cwd, ["switch", "-q", branch]);
80568
+ return switched.ok ? success2(undefined) : failure3("failed", `git switch ${branch} failed: ${gitError(switched)}`);
80569
+ }, mergeNoFf = (cwd, what, into, version) => {
80570
+ const merged = git5(cwd, ["merge", "--no-ff", "--no-edit", what], { timeoutMs: 10 * 60000 });
80571
+ if (!merged.ok) {
80572
+ const conflicted = git5(cwd, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]).ok;
80573
+ return failure3("failed", conflicted ? `merge_conflict: merging ${what} into ${into} conflicts` : `git merge ${what} into ${into} failed: ${gitError(merged)}`, conflicted ? `resolve the conflicts, git add <files>, git merge --continue, then: workit release finish ${version}` : undefined);
80574
+ }
80575
+ return success2(git5(cwd, ["rev-parse", "HEAD"]).stdout.trim());
80576
+ }, switchToRelease = (cwd, plan) => switchTo(cwd, plan.branch);
80577
+ var init_release = __esm(() => {
80578
+ init_ledger();
80579
+ init_vcs_config();
80580
+ init_release_tracks();
80581
+ init_workspaces();
80582
+ init_ops();
80583
+ init_rev();
80584
+ VERSION = /^[0-9A-Za-z][0-9A-Za-z._+-]{0,99}$/u;
80585
+ PLACEHOLDER3 = /\{[^}]*\}/gu;
80586
+ KINDS = ["release", "hotfix"];
80587
+ });
80588
+
80589
+ // packages/workit-cli/src/verbs/release.ts
80590
+ var exports_release = {};
80591
+ __export(exports_release, {
80592
+ run: () => run14
80593
+ });
80594
+ async function subVerb(load, argv, io) {
80595
+ let out = "";
80596
+ let err = "";
80597
+ const verb = await load();
80598
+ await verb.run(argv, {
80599
+ ...io,
80600
+ json: true,
80601
+ stdout: (text) => void (out += text),
80602
+ stderr: (text) => void (err += text)
80603
+ });
80604
+ try {
80605
+ return JSON.parse(out);
80606
+ } catch {
80607
+ return fail("failed", (out + err).trim() || `${argv.join(" ")} printed nothing`);
80608
+ }
80609
+ }
80610
+ async function start(argv, io) {
80611
+ const flags = parseFlags(argv, { hotfix: "boolean", track: "value" });
80612
+ if (typeof flags === "string")
80613
+ return usage3(io, flags, START_USAGE);
80614
+ if (flags.positionals.length !== 1)
80615
+ return usage3(io, flags.positionals.length ? "pass one version" : "missing <version>", START_USAGE);
80616
+ const result = releaseStart(io.cwd, {
80617
+ version: flags.positionals[0],
80618
+ hotfix: flags.booleans.has("hotfix"),
80619
+ track: flags.values.track ?? null,
80620
+ actor: actorFromEnv(io.env)
80621
+ });
80622
+ if (!result.ok)
80623
+ return forgeFail(io, result);
80624
+ return emit(io, ok(result.data), (data) => [
80625
+ `started ${data.branch} from ${data.base} (${short4(data.baseSha)}), local only; it will be tagged ${data.tag}`,
80626
+ `next: commit the version bump, changelog and docs on ${data.branch} (workit git commit), then: workit release finish`,
80627
+ ...data.notes.map((note) => `note: ${note}`)
80628
+ ]);
80629
+ }
80630
+ async function releasePr(io, plan, dryRun) {
80631
+ const title = `chore(release): ${plan.version}`;
80632
+ if (dryRun)
80633
+ return emit(io, ok({
80634
+ ...plan,
80635
+ steps: [`push ${plan.branch}`, `open a PR ${plan.branch} -> ${plan.production}: ${title}`]
80636
+ }), () => [
80637
+ `would push ${plan.branch} and open a release PR into ${plan.production} (not git-flow: the trunk moves only through review)`
80638
+ ]);
80639
+ const switched = switchToRelease(io.cwd, plan);
80640
+ if (!switched.ok)
80641
+ return forgeFail(io, switched);
80642
+ const pushed = await subVerb(() => Promise.resolve().then(() => (init_git2(), exports_git)), ["push", "--set-upstream"], io);
80643
+ if (!pushed.ok)
80644
+ return emit(io, pushed);
80645
+ const pr = await subVerb(() => Promise.resolve().then(() => (init_pr(), exports_pr)), [
80646
+ "create",
80647
+ "--base",
80648
+ plan.production,
80649
+ "--title",
80650
+ title,
80651
+ `--body=${plan.changelog ?? `Release ${plan.version}`}`
80652
+ ], io);
80653
+ if (!pr.ok)
80654
+ return emit(io, pr);
80655
+ recordReleaseStep(io.cwd, actorFromEnv(io.env), plan, {
80656
+ type: "release.pr_opened",
80657
+ branch: plan.branch,
80658
+ pr: pr.data.number ?? null
80659
+ });
80660
+ return emit(io, ok({ ...plan, pr: pr.data }), () => [
80661
+ `opened the release PR ${String(pr.data.url ?? "")} into ${plan.production}`,
80662
+ `next: once it merges, tag the merged commit ${plan.tag} and: workit git push --release ${plan.tag}`
80663
+ ]);
80664
+ }
80665
+ async function finish2(argv, io) {
80666
+ const flags = parseFlags(argv, { track: "value", "dry-run": "boolean" });
80667
+ if (typeof flags === "string")
80668
+ return usage3(io, flags, FINISH_USAGE);
80669
+ if (flags.positionals.length > 1)
80670
+ return usage3(io, `unexpected argument ${flags.positionals[1]}`, FINISH_USAGE);
80671
+ const dryRun = flags.booleans.has("dry-run");
80672
+ const planned = planReleaseFinish(io.cwd, {
80673
+ version: flags.positionals[0] ?? null,
80674
+ track: flags.values.track ?? null
80675
+ });
80676
+ if (!planned.ok)
80677
+ return forgeFail(io, planned);
80678
+ const plan = planned.data;
80679
+ if (!plan.gitflow) {
80680
+ if (!dryRun && plan.state.branchDeleted)
80681
+ return emit(io, fail("not_found", `${plan.branch} is gone; nothing to open a PR from`));
80682
+ return releasePr(io, plan, dryRun);
80683
+ }
80684
+ const actor = actorFromEnv(io.env);
80685
+ const local = isLocalRemote(io.cwd, plan.remote);
80686
+ const release = requireGrant(io.cwd, "release");
80687
+ const push = requireGrant(io.cwd, "push", { forge: !local });
80688
+ const steps = stepsOf(plan);
80689
+ for (const warning of plan.warnings)
80690
+ io.stderr(`warning: ${warning}
80691
+ `);
80692
+ const warned = (envelope) => plan.warnings.length ? { ...envelope, warnings: plan.warnings } : envelope;
80693
+ if (dryRun) {
80694
+ const missing = [release, push].find((grant) => !grant.allowed);
80695
+ const data = { ...plan, steps, grants: { release: release.allowed, push: push.allowed } };
80696
+ if (missing && !missing.allowed)
80697
+ return emit(io, warned(fail("blocked", missing.error, { unblock: missing.unblock, data })));
80698
+ return emit(io, warned(ok(data)), () => [
80699
+ `finish ${plan.branch} -> ${plan.production} as ${plan.tag}:`,
80700
+ ...steps.map((entry) => ` ${entry.done ? "done" : "todo"} ${entry.step}`)
80701
+ ]);
80702
+ }
80703
+ if (!release.allowed)
80704
+ return blocked(io, release);
80705
+ if (!push.allowed)
80706
+ return blocked(io, push);
80707
+ const resume = `workit release finish ${plan.version}${plan.track ? ` --track ${plan.track}` : ""}`;
80708
+ const merged = finishLocally(io.cwd, plan, actor);
80709
+ if (!merged.ok)
80710
+ return forgeFail(io, merged, { step: "merge", resume });
80711
+ const notes = [...plan.notes];
80712
+ const pushed = await subVerb(() => Promise.resolve().then(() => (init_git2(), exports_git)), ["push", ...[...plan.pushBranches, plan.tag].flatMap((ref) => ["--release", ref])], io);
80713
+ if (!pushed.ok)
80714
+ return emit(io, { ...pushed, data: { ...pushed.data, step: "push", resume } });
80715
+ if (plan.published && plan.remote) {
80716
+ const removed = deleteRemoteBranch(io.cwd, plan.remote, plan.branch, plan.published);
80717
+ notes.push(removed.ok ? `deleted ${plan.remote}/${plan.branch}` : `kept ${plan.remote}/${plan.branch}: ${removed.error}`);
80718
+ if (removed.ok)
80719
+ recordReleaseStep(io.cwd, actor, plan, {
80720
+ type: "release.remote_branch_deleted",
80721
+ branch: plan.branch,
80722
+ head: plan.published
80723
+ });
80724
+ }
80725
+ let forgeRelease = null;
80726
+ if (local)
80727
+ notes.push(`${plan.remote ?? "the remote"} is a local path: no forge release`);
80728
+ else {
80729
+ const connected = connect(io);
80730
+ if (!connected.ok)
80731
+ return forgeFail(io, connected, { step: "forge-release", resume });
80732
+ const forge = connected.data.forge;
80733
+ const found = forge.findRelease(plan.tag);
80734
+ if (!found.ok)
80735
+ return forgeFail(io, found, { step: "forge-release", resume });
80736
+ if (found.data)
80737
+ forgeRelease = { url: found.data.url, created: false };
80738
+ else {
80739
+ const created = forge.createRelease({
80740
+ tag: plan.tag,
80741
+ name: plan.tag,
80742
+ body: plan.changelog ?? `Release ${plan.version}`
80743
+ });
80744
+ if (!created.ok)
80745
+ return forgeFail(io, created, { step: "forge-release", resume });
80746
+ forgeRelease = { url: created.data.url, created: true };
80747
+ }
80748
+ recordReleaseStep(io.cwd, actor, plan, {
80749
+ type: "release.forge_released",
80750
+ branch: plan.production,
80751
+ url: forgeRelease.url,
80752
+ created: forgeRelease.created
80753
+ });
80754
+ }
80755
+ const verified = verifyReleased(io.cwd, { tag: plan.tag, actor }, forgeDeps.npm);
80756
+ if (!verified.ok)
80757
+ return forgeFail(io, verified, { step: "verify", resume });
80758
+ if (!verified.data.delivered)
80759
+ return emit(io, fail("failed", `release_unverified: ${plan.tag} is not on ${plan.remote ?? "the remote"}`, {
80760
+ data: { step: "verify", resume, report: verified.data },
80761
+ unblock: resume
80762
+ }));
80763
+ recordReleaseStep(io.cwd, actor, plan, {
80764
+ type: "release.finished",
80765
+ branch: plan.branch,
80766
+ head: merged.data.tagTarget
80767
+ });
80768
+ return emit(io, warned(ok({ ...plan, ran: merged.data.ran, push: pushed.data, forgeRelease, notes })), () => [
80769
+ `finished ${plan.branch}: ${plan.production} and ${plan.mergeBack.join(", ")} merged, ${plan.tag} tagged on ${short4(merged.data.tagTarget)}`,
80770
+ `pushed ${[...plan.pushBranches, plan.tag].join(", ")} to ${plan.remote ?? "the remote"}; remote tips verified`,
80771
+ ...forgeRelease ? [
80772
+ `forge release ${plan.tag}${forgeRelease.created ? " created" : " already there"}${forgeRelease.url ? `: ${forgeRelease.url}` : ""}`
80773
+ ] : [],
80774
+ ...notes.map((note) => `note: ${note}`)
80775
+ ]);
80776
+ }
80777
+ async function publish(argv, io) {
80778
+ const flags = parseFlags(argv, {});
80779
+ if (typeof flags === "string")
80780
+ return usage3(io, flags, PUBLISH_USAGE);
80781
+ if (flags.positionals.length > 1)
80782
+ return usage3(io, `unexpected argument ${flags.positionals[1]}`, PUBLISH_USAGE);
80783
+ const located = releaseToPublish(io.cwd, flags.positionals[0] ?? null);
80784
+ if (!located.ok)
80785
+ return forgeFail(io, located);
80786
+ const plan = pushPreflight(io.cwd, { branch: located.data.branch });
80787
+ if (!plan.ok)
80788
+ return forgeFail(io, plan);
80789
+ const identity = plan.data.local ? null : pushIdentity(io, plan.data.branch);
80790
+ if (identity && !identity.ok)
80791
+ return forgeFail(io, identity);
80792
+ const checked = identity?.data ?? null;
80793
+ const grant = requireGrant(io.cwd, "push", { forge: checked?.forge != null });
80794
+ if (!grant.allowed)
80795
+ return blocked(io, grant);
80796
+ const actor = actorFromEnv(io.env);
80797
+ const result = executePush(io.cwd, plan.data, {
80798
+ setUpstream: true,
80799
+ actor,
80800
+ ...checked ? { identity: { ...checked } } : {}
80801
+ });
80802
+ if (!result.ok)
80803
+ return forgeFail(io, result);
80804
+ appendObserved(io.cwd, {
80805
+ type: "release.published",
80806
+ actor,
80807
+ version: located.data.version,
80808
+ branch: located.data.branch,
80809
+ head: result.data.sha
80810
+ });
80811
+ return emit(io, ok({ ...result.data, version: located.data.version }), (data) => [
80812
+ `published ${data.branch} (${short4(data.sha)}) to ${data.remote}; remote tip verified`
80813
+ ]);
80814
+ }
80815
+ async function run14(argv, io) {
80816
+ const [sub, ...rest] = argv;
80817
+ if (sub === "start")
80818
+ return start(rest, io);
80819
+ if (sub === "finish")
80820
+ return finish2(rest, io);
80821
+ if (sub === "publish")
80822
+ return publish(rest, io);
80823
+ return usage3(io, sub && !sub.startsWith("-") ? `unknown release subcommand "${sub}"` : "missing release subcommand", USAGE6);
80824
+ }
80825
+ var START_USAGE = "workit release start <version> [--hotfix] [--track <t>] [--json]", FINISH_USAGE = "workit release finish [<version>] [--track <t>] [--dry-run] [--json]", PUBLISH_USAGE = "workit release publish [<version>] [--json]", USAGE6 = "workit release start|finish|publish ... (workit help release)", short4 = (sha) => sha ? sha.slice(0, 12) : "(none)", blocked = (io, grant) => emit(io, fail("blocked", grant.error, { unblock: grant.unblock, data: { reason: grant.reason } })), isLocalRemote = (cwd, remote) => {
80826
+ const url = remote ? pushUrl(cwd, remote) : null;
80827
+ return !url || parseRemoteUrl(url)?.protocol === "file";
80828
+ }, stepsOf = (plan) => [
80829
+ { step: `merge ${plan.branch} into ${plan.production} (--no-ff)`, done: plan.state.merged },
80830
+ { step: `tag ${plan.tag} (annotated)`, done: plan.state.tagged },
80831
+ ...plan.mergeBack.map((name) => ({
80832
+ step: `merge ${plan.tag} into ${name} (--no-ff)`,
80833
+ done: plan.state.mergedBack[name] === true
80834
+ })),
80835
+ { step: `delete local ${plan.branch}`, done: plan.state.branchDeleted },
80836
+ {
80837
+ step: `push ${[...plan.pushBranches, plan.tag].join(", ")} (atomic, fast-forward only)`,
80838
+ done: false
80839
+ },
80840
+ { step: `forge release ${plan.tag}`, done: false },
80841
+ { step: `verify ${plan.tag} on the remote`, done: false }
80842
+ ];
80843
+ var init_release2 = __esm(() => {
80844
+ init_autonomy();
80845
+ init_verify();
80846
+ init_ops();
80847
+ init_release();
80848
+ init_rev();
80849
+ init_ledger();
80850
+ init_output();
80851
+ init_forge_common();
80852
+ });
80853
+
79671
80854
  // packages/workit-cli/src/verbs/verify-delivery.ts
79672
80855
  var exports_verify_delivery = {};
79673
80856
  __export(exports_verify_delivery, {
79674
- run: () => run14
80857
+ run: () => run15
79675
80858
  });
79676
- async function run14(argv, io) {
80859
+ async function run15(argv, io) {
79677
80860
  const flags = parseFlags(argv, {
79678
80861
  expect: "value",
79679
80862
  branch: "value",
@@ -79683,31 +80866,31 @@ async function run14(argv, io) {
79683
80866
  package: "value"
79684
80867
  });
79685
80868
  if (typeof flags === "string")
79686
- return usage3(io, flags, USAGE6);
80869
+ return usage3(io, flags, USAGE7);
79687
80870
  if (flags.positionals.length > 1)
79688
- return usage3(io, `unexpected argument ${flags.positionals[1]}`, USAGE6);
80871
+ return usage3(io, `unexpected argument ${flags.positionals[1]}`, USAGE7);
79689
80872
  const named = flags.positionals[0];
79690
80873
  if (named !== undefined && !ENDPOINTS[named])
79691
- return usage3(io, `unknown endpoint "${named}"`, USAGE6);
80874
+ return usage3(io, `unknown endpoint "${named}"`, USAGE7);
79692
80875
  if (flags.values.expect !== undefined && !ENDPOINTS[flags.values.expect])
79693
- return usage3(io, "--expect must be pushed, pr, merged or released", USAGE6);
80876
+ return usage3(io, "--expect must be pushed, pr, merged or released", USAGE7);
79694
80877
  if (named && flags.values.expect && ENDPOINTS[named] !== ENDPOINTS[flags.values.expect])
79695
- return usage3(io, `${named} and --expect ${flags.values.expect} disagree`, USAGE6);
80878
+ return usage3(io, `${named} and --expect ${flags.values.expect} disagree`, USAGE7);
79696
80879
  const expect = ENDPOINTS[named ?? ""] ?? ENDPOINTS[flags.values.expect ?? ""] ?? (flags.values.tag || flags.values.package ? "released" : flags.values.pr ? "pr" : "pushed");
79697
80880
  const pr = positiveInt(flags.values.pr, "--pr");
79698
80881
  if (typeof pr === "string")
79699
- return usage3(io, pr, USAGE6);
80882
+ return usage3(io, pr, USAGE7);
79700
80883
  if (pr && flags.values.branch)
79701
- return usage3(io, "pass --pr or --branch, not both", USAGE6);
80884
+ return usage3(io, "pass --pr or --branch, not both", USAGE7);
79702
80885
  const sha = flags.values.sha ?? null;
79703
80886
  if (sha !== null && !/^[0-9a-f]{7,64}$/u.test(sha))
79704
- return usage3(io, "--sha must be a commit sha (7-64 hex chars)", USAGE6);
80887
+ return usage3(io, "--sha must be a commit sha (7-64 hex chars)", USAGE7);
79705
80888
  const actor = actorFromEnv(io.env);
79706
80889
  const branch = flags.values.branch ?? null;
79707
80890
  let result;
79708
80891
  if (expect === "pushed") {
79709
80892
  if (pr)
79710
- return usage3(io, "push is checked by --branch, not --pr", USAGE6);
80893
+ return usage3(io, "push is checked by --branch, not --pr", USAGE7);
79711
80894
  result = verifyPushed(io.cwd, { branch, sha, actor });
79712
80895
  } else if (expect === "released") {
79713
80896
  result = verifyReleased(io.cwd, { tag: flags.values.tag ?? null, pkg: flags.values.package ?? null, sha, actor }, forgeDeps.npm);
@@ -79732,7 +80915,7 @@ async function run14(argv, io) {
79732
80915
  unblock: expect === "pushed" ? "workit git push" : expect === "pr" ? "workit git push && workit pr create --fill" : expect === "merged" ? "workit pr status # then workit pr merge when READY and granted" : "check the release workflow; the tag/package is not published"
79733
80916
  }));
79734
80917
  }
79735
- var USAGE6 = "workit verify-delivery [push|pr|merge|release] [--expect pushed|pr|merged|released] [--branch <b>] [--pr <n>] [--sha <sha>] [--tag <t>] [--package <name[@version]>] [--json]", ENDPOINTS, render2 = (report) => [
80918
+ var USAGE7 = "workit verify-delivery [push|pr|merge|release] [--expect pushed|pr|merged|released] [--branch <b>] [--pr <n>] [--sha <sha>] [--tag <t>] [--package <name[@version]>] [--json]", ENDPOINTS, render2 = (report) => [
79736
80919
  `${report.expect}: ${report.delivered ? "delivered" : "NOT delivered"}${report.branch ? ` (${report.branch}${report.sha ? ` @ ${report.sha.slice(0, 12)}` : ""})` : ""}${report.pr ? ` #${report.pr}` : ""}`,
79737
80920
  ...report.observations.map((entry) => ` ${entry.ok ? "ok" : "x "} ${entry.kind}: expected ${entry.expected ?? "-"}, observed ${entry.observed ?? "(none)"}${entry.note ? ` — ${entry.note}` : ""}`)
79738
80921
  ];
@@ -79755,7 +80938,7 @@ var init_verify_delivery = __esm(() => {
79755
80938
  // packages/workit-cli/src/verbs/stack.ts
79756
80939
  var exports_stack = {};
79757
80940
  __export(exports_stack, {
79758
- run: () => run15
80941
+ run: () => run16
79759
80942
  });
79760
80943
  async function plan(argv, io) {
79761
80944
  const flags = parseFlags(argv, { name: "value", trunk: "value", track: "value" });
@@ -79929,7 +81112,7 @@ async function land(argv, io) {
79929
81112
  ...data.dryRun ? [
79930
81113
  data.wouldLand.length ? `would land: ${data.wouldLand.map((item) => `${label2(kind, item.pr)} (${item.branch})`).join(", ")}` : "would land nothing"
79931
81114
  ] : [
79932
- data.landed.length ? `landed: ${data.landed.map((item) => `${label2(kind, item.pr)} (${item.branch}) -> ${short4(item.mergeSha)}${item.unverified ? ` UNVERIFIED (ledger ${item.unverified})` : ""}`).join(", ")}` : "landed nothing"
81115
+ data.landed.length ? `landed: ${data.landed.map((item) => `${label2(kind, item.pr)} (${item.branch}) -> ${short5(item.mergeSha)}${item.unverified ? ` UNVERIFIED (ledger ${item.unverified})` : ""}`).join(", ")}` : "landed nothing"
79933
81116
  ],
79934
81117
  ...data.retargeted.length ? [`retargeted: ${data.retargeted.map((pr) => label2(kind, pr)).join(", ")}`] : [],
79935
81118
  ...data.restacked.length ? [`restacked: ${data.restacked.join(", ")}`] : [],
@@ -79937,7 +81120,7 @@ async function land(argv, io) {
79937
81120
  unblock: ${data.stoppedAt.unblock}` : data.complete ? "stack complete" : ""
79938
81121
  ].filter(Boolean));
79939
81122
  }
79940
- async function run15(argv, io) {
81123
+ async function run16(argv, io) {
79941
81124
  const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
79942
81125
  if (sub === "plan")
79943
81126
  return plan(rest, io);
@@ -79947,13 +81130,13 @@ async function run15(argv, io) {
79947
81130
  return sync(rest, io);
79948
81131
  if (sub === "land")
79949
81132
  return land(rest, io);
79950
- return usage3(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE7);
81133
+ return usage3(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE8);
79951
81134
  }
79952
- var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--unverified --reason <why>] [--timeout 20m] [--interval 30s] [--json]", USAGE7 = "workit stack plan|status|sync|land ... (workit help stack)", short4 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), label2 = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
81135
+ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--unverified --reason <why>] [--timeout 20m] [--interval 30s] [--json]", USAGE8 = "workit stack plan|status|sync|land ... (workit help stack)", short5 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), label2 = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
79953
81136
  `${data.dryRun ? "would sync" : "synced"} stack ${data.name}${data.merged.length ? `; merged: ${data.merged.map((item) => item.branch).join(", ")}` : ""}`,
79954
81137
  ...data.steps.map((step) => [
79955
81138
  ` ${step.branch} on ${step.parent}:`,
79956
- step.restack ? `restacked ${short4(step.restack.from)} -> ${short4(step.restack.onto)}${step.restack.head ? ` = ${short4(step.restack.head)}` : ""}${step.restack.carried === true ? " (same change; verdict carries)" : step.restack.carried === false ? " (change differs; verdict stale)" : ""}` : "up to date",
81139
+ step.restack ? `restacked ${short5(step.restack.from)} -> ${short5(step.restack.onto)}${step.restack.head ? ` = ${short5(step.restack.head)}` : ""}${step.restack.carried === true ? " (same change; verdict carries)" : step.restack.carried === false ? " (change differs; verdict stale)" : ""}` : "up to date",
79957
81140
  step.push ? step.push.pushed ? `pushed${step.push.forced ? " (lease)" : ""}` : "remote current" : "",
79958
81141
  step.retarget ? `retargeted ${step.retarget.from} -> ${step.retarget.to}` : ""
79959
81142
  ].filter(Boolean).join(" "))
@@ -79967,7 +81150,7 @@ var init_stack2 = __esm(() => {
79967
81150
  });
79968
81151
 
79969
81152
  // packages/workit-core/src/fanout.ts
79970
- import { spawnSync as spawnSync24 } from "node:child_process";
81153
+ import { spawnSync as spawnSync25 } from "node:child_process";
79971
81154
  import { createHash as createHash18 } from "node:crypto";
79972
81155
  import fs25 from "node:fs";
79973
81156
  import path48 from "node:path";
@@ -80116,7 +81299,7 @@ function inScope(plan, slice, file) {
80116
81299
  }
80117
81300
  function parsePlan(raw, defaults, name) {
80118
81301
  const problems = [];
80119
- if (!isRecord9(raw))
81302
+ if (!isRecord10(raw))
80120
81303
  return { ok: false, problems: ["the plan must be a JSON object"] };
80121
81304
  const planName = name ?? (typeof raw.name === "string" ? raw.name.trim() : "");
80122
81305
  if (!planName || planName.length > 200 || /\p{Cc}/u.test(planName))
@@ -80139,8 +81322,8 @@ function parsePlan(raw, defaults, name) {
80139
81322
  const seenIds = new Set;
80140
81323
  const seenBranches = new Map;
80141
81324
  raw.slices.forEach((item, index) => {
80142
- const where = isRecord9(item) && typeof item.id === "string" ? `slice ${item.id}` : `slice #${index + 1}`;
80143
- if (!isRecord9(item)) {
81325
+ const where = isRecord10(item) && typeof item.id === "string" ? `slice ${item.id}` : `slice #${index + 1}`;
81326
+ if (!isRecord10(item)) {
80144
81327
  problems.push(`${where}: must be an object`);
80145
81328
  return;
80146
81329
  }
@@ -80458,7 +81641,7 @@ function fanoutsDir(cwd) {
80458
81641
  return { ok: true, data: path48.join(root.value.root, "fanouts") };
80459
81642
  }
80460
81643
  function parseStored(raw) {
80461
- if (!isRecord9(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
81644
+ if (!isRecord10(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
80462
81645
  return null;
80463
81646
  if (!Array.isArray(raw.slices))
80464
81647
  return null;
@@ -80475,7 +81658,7 @@ function parseStored(raw) {
80475
81658
  });
80476
81659
  const slices = [];
80477
81660
  for (const item of raw.slices) {
80478
- if (!isRecord9(item) || typeof item.id !== "string" || typeof item.branch !== "string")
81661
+ if (!isRecord10(item) || typeof item.id !== "string" || typeof item.branch !== "string")
80479
81662
  return null;
80480
81663
  slices.push({
80481
81664
  id: item.id,
@@ -80598,7 +81781,7 @@ function planFanout(cwd, input, now = new Date) {
80598
81781
  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");
80599
81782
  if (input.takeLead && isDelegateSession(session))
80600
81783
  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");
80601
- const parsed = parsePlan(input.trunk && isRecord9(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, {
81784
+ const parsed = parsePlan(input.trunk && isRecord10(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, {
80602
81785
  trunk: input.trunk ?? defaultTrunk(cwd),
80603
81786
  root: mainCheckout(cwd),
80604
81787
  worktreeRoot: worktreeRoot(cwd)
@@ -80721,7 +81904,7 @@ var FANOUT_VERSION = 1, TIERS, FAN_IN_MODES, fanoutFail = (code, error, unblock,
80721
81904
  error,
80722
81905
  ...unblock ? { unblock } : {},
80723
81906
  ...data ? { data } : {}
80724
- }), 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) => {
81907
+ }), 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)), isRecord10 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), PLACEHOLDER4, filled = (text) => text.trim() !== "" && !PLACEHOLDER4.test(text.trim()), textList = (value) => {
80725
81908
  if (typeof value === "string")
80726
81909
  return [value];
80727
81910
  if (Array.isArray(value) && value.every((item) => typeof item === "string"))
@@ -80740,7 +81923,7 @@ var FANOUT_VERSION = 1, TIERS, FAN_IN_MODES, fanoutFail = (code, error, unblock,
80740
81923
  text: edges.map((edge) => `add "${edge.dependsOn}" to "dependsOn" of slice ${edge.slice}`).join("; ")
80741
81924
  };
80742
81925
  }, gitRun4 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
80743
- const run = spawnSync24("git", args, {
81926
+ const run = spawnSync25("git", args, {
80744
81927
  cwd,
80745
81928
  encoding: "utf8",
80746
81929
  timeout: timeoutMs,
@@ -80773,12 +81956,12 @@ var init_fanout = __esm(() => {
80773
81956
  "CHANGELOG.md"
80774
81957
  ];
80775
81958
  compiled = new Map;
80776
- PLACEHOLDER3 = /^(?:<[^>]*>|tbd|todo|fixme|\?+|\.\.\.|…)$/iu;
81959
+ PLACEHOLDER4 = /^(?:<[^>]*>|tbd|todo|fixme|\?+|\.\.\.|…)$/iu;
80777
81960
  ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
80778
81961
  });
80779
81962
 
80780
81963
  // packages/workit-core/src/fanout-worktree.ts
80781
- import { randomBytes as randomBytes4 } from "node:crypto";
81964
+ import { randomBytes as randomBytes5 } from "node:crypto";
80782
81965
  import fs26 from "node:fs";
80783
81966
  import path49 from "node:path";
80784
81967
  function rowSample(lines) {
@@ -80862,7 +82045,7 @@ function markAdminDir(worktree) {
80862
82045
  const admin = adminDir(worktree);
80863
82046
  if (!admin)
80864
82047
  return null;
80865
- const token = randomBytes4(16).toString("hex");
82048
+ const token = randomBytes5(16).toString("hex");
80866
82049
  try {
80867
82050
  fs26.writeFileSync(path49.join(admin.path, TOKEN_FILE), `${token}
80868
82051
  `);
@@ -81287,7 +82470,7 @@ var init_fanout_brief = __esm(() => {
81287
82470
  });
81288
82471
 
81289
82472
  // packages/workit-core/src/fanout-landed.ts
81290
- import { spawnSync as spawnSync25 } from "node:child_process";
82473
+ import { spawnSync as spawnSync26 } from "node:child_process";
81291
82474
  function forgeBreaker(resolved) {
81292
82475
  if (!resolved)
81293
82476
  return { forge: null, down: () => null };
@@ -81330,9 +82513,9 @@ function hasOwnCommits2(cwd, branch, start, head) {
81330
82513
  const onto = /^rebase(?: -i)? \(finish\): \S+ onto ([0-9a-f]+)/u.exec(subject)?.[1];
81331
82514
  if (!made && !(onto && !sha.startsWith(onto) && !onto.startsWith(sha)))
81332
82515
  continue;
81333
- if (sha !== head && !isAncestor3(cwd, sha, head))
82516
+ if (sha !== head && !isAncestor4(cwd, sha, head))
81334
82517
  continue;
81335
- if (start && (sha === start || isAncestor3(cwd, sha, start)))
82518
+ if (start && (sha === start || isAncestor4(cwd, sha, start)))
81336
82519
  continue;
81337
82520
  return true;
81338
82521
  }
@@ -81342,7 +82525,7 @@ function reverted(cwd, trunk, landedHead, start) {
81342
82525
  const fork = mergeBase(cwd, landedHead, trunk);
81343
82526
  if (fork && fork !== landedHead)
81344
82527
  return unchangedSince(cwd, fork, landedHead, trunk);
81345
- const before = start && start !== landedHead && isAncestor3(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
82528
+ const before = start && start !== landedHead && isAncestor4(cwd, start, landedHead) ? start : gitRun4(cwd, ["rev-parse", "--verify", "-q", `${landedHead}^1`]).stdout.trim();
81346
82529
  return before ? unchangedSince(cwd, before, landedHead, trunk) : false;
81347
82530
  }
81348
82531
  function unchangedSince(cwd, before, after, trunk) {
@@ -81378,7 +82561,7 @@ function trunkPatchIds(ctx, fork) {
81378
82561
  "--"
81379
82562
  ]);
81380
82563
  if (log.ok && log.stdout.trim()) {
81381
- const run = spawnSync25("git", ["patch-id", "--stable"], {
82564
+ const run = spawnSync26("git", ["patch-id", "--stable"], {
81382
82565
  cwd: ctx.cwd,
81383
82566
  input: log.stdout,
81384
82567
  encoding: "utf8",
@@ -81450,9 +82633,9 @@ function detectLanding(ctx, slice, input) {
81450
82633
  }
81451
82634
  return merged ? landed("pr_merged", merged) : { ...out, landed: true, how: "pr_merged" };
81452
82635
  }
81453
- if (!merged || head === merged || isAncestor3(ctx.cwd, head, merged))
82636
+ if (!merged || head === merged || isAncestor4(ctx.cwd, head, merged))
81454
82637
  return landed("pr_merged", merged ?? head);
81455
- 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)`;
82638
+ out.note = isAncestor4(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)`;
81456
82639
  return out;
81457
82640
  }
81458
82641
  }
@@ -81469,7 +82652,7 @@ function detectLanding(ctx, slice, input) {
81469
82652
  if (trunkIds.size === 0)
81470
82653
  return out;
81471
82654
  const froms = [fork];
81472
- if (input.parentHead && input.parentHead !== head && isAncestor3(ctx.cwd, input.parentHead, head))
82655
+ if (input.parentHead && input.parentHead !== head && isAncestor4(ctx.cwd, input.parentHead, head))
81473
82656
  froms.push(input.parentHead);
81474
82657
  for (const from of froms) {
81475
82658
  const id = patchId(ctx.cwd, from, head);
@@ -81485,7 +82668,7 @@ function sliceStart(cwd, rows, fanout, slice) {
81485
82668
  const row = rows.findLast((candidate) => candidate.type === "fanout.worktree.created" && candidate.fanout === fanout && candidate.slice === slice.id && typeof candidate.baseSha === "string" && inSliceRun(candidate, slice));
81486
82669
  return row?.baseSha ?? null;
81487
82670
  }
81488
- var isAncestor3 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
82671
+ var isAncestor4 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, TRUNK_SCAN = 500;
81489
82672
  var init_fanout_landed = __esm(() => {
81490
82673
  init_rev();
81491
82674
  init_fanout();
@@ -81542,7 +82725,7 @@ function checkFanout(cwd, plan, options = {}) {
81542
82725
  const b = tips.get(second);
81543
82726
  if (landings.get(first)?.landed || landings.get(second)?.landed)
81544
82727
  continue;
81545
- if (!a || !b || isAncestor4(cwd, a, b) || isAncestor4(cwd, b, a))
82728
+ if (!a || !b || isAncestor5(cwd, a, b) || isAncestor5(cwd, b, a))
81546
82729
  continue;
81547
82730
  const check = checks.get(second);
81548
82731
  const theirs = new Map((changedBy.get(first) ?? []).map((file) => [file.toLowerCase(), file]));
@@ -81722,7 +82905,7 @@ function unblockFor(check, plan) {
81722
82905
  return `unblock ${check.waitsFor.join(", ")} first`;
81723
82906
  return check.reasons[0] ?? "workit fanout check";
81724
82907
  }
81725
- 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) => ({
82908
+ var SAFE = (value) => Boolean(value) && !value?.startsWith("-"), isAncestor5 = (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) => ({
81726
82909
  id: slice.id,
81727
82910
  branch: slice.branch,
81728
82911
  head,
@@ -81939,7 +83122,7 @@ var init_fanout_status = __esm(() => {
81939
83122
  // packages/workit-cli/src/verbs/fanout.ts
81940
83123
  var exports_fanout = {};
81941
83124
  __export(exports_fanout, {
81942
- run: () => run16
83125
+ run: () => run17
81943
83126
  });
81944
83127
  import fs27 from "node:fs";
81945
83128
  import path50 from "node:path";
@@ -82135,7 +83318,7 @@ function worktree(argv, io) {
82135
83318
  return failed(io, result);
82136
83319
  return emit(io, ok(result.data), (data) => [
82137
83320
  `${data.created ? "created" : "kept"} worktree for slice ${data.slice} (${data.mode}): ${data.path}`,
82138
- ` branch ${data.branch} <- ${data.base} at ${short5(data.head)}`,
83321
+ ` branch ${data.branch} <- ${data.base} at ${short6(data.head)}`,
82139
83322
  ` scratch ${data.scratch} (put it in the worker brief: temp files go here)`,
82140
83323
  ...data.notes.map((note) => `note: ${note}`)
82141
83324
  ]);
@@ -82159,7 +83342,7 @@ function worktree(argv, io) {
82159
83342
  ...data.notes.map((note) => `note: ${note}`)
82160
83343
  ]);
82161
83344
  }
82162
- async function run16(argv, io) {
83345
+ async function run17(argv, io) {
82163
83346
  const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
82164
83347
  if (sub === "plan")
82165
83348
  return plan2(rest, io);
@@ -82171,9 +83354,9 @@ async function run16(argv, io) {
82171
83354
  return status3(rest, io);
82172
83355
  if (sub === "worktree")
82173
83356
  return worktree(rest, io);
82174
- return usage3(io, sub && !sub.startsWith("-") ? `unknown fanout subcommand "${sub}"` : "missing fanout subcommand", USAGE8);
83357
+ return usage3(io, sub && !sub.startsWith("-") ? `unknown fanout subcommand "${sub}"` : "missing fanout subcommand", USAGE9);
82175
83358
  }
82176
- var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--take-lead] [--json]", BRIEF_USAGE = "workit fanout brief <slice> [--name <n>] [--mode new|resume] [--attempt <n>] [--json]", CHECK_USAGE = "workit fanout check [<slice>…] [--name <n>] [--base <ref>] [--offline] [--json]", STATUS_USAGE3 = "workit fanout status [--name <n>] [--stuck-after 30m] [--offline] [--json]", WORKTREE_USAGE = "workit fanout worktree create <slice> [--name <n>] | worktree release <slice> [--name <n>] [--force] [--json]", USAGE8 = "workit fanout plan|brief|check|status|worktree ... (workit help fanout)", failed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), renderCheck = (data) => [
83359
+ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--take-lead] [--json]", BRIEF_USAGE = "workit fanout brief <slice> [--name <n>] [--mode new|resume] [--attempt <n>] [--json]", CHECK_USAGE = "workit fanout check [<slice>…] [--name <n>] [--base <ref>] [--offline] [--json]", STATUS_USAGE3 = "workit fanout status [--name <n>] [--stuck-after 30m] [--offline] [--json]", WORKTREE_USAGE = "workit fanout worktree create <slice> [--name <n>] | worktree release <slice> [--name <n>] [--force] [--json]", USAGE9 = "workit fanout plan|brief|check|status|worktree ... (workit help fanout)", failed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), renderCheck = (data) => [
82177
83360
  `fanout ${data.name} against ${data.trunkRef}: ${data.ready ? "ready to land" : "blocked"}`,
82178
83361
  ...data.slices.map((slice) => slice.status === "landed" ? ` -- ${slice.id} (${slice.branch}) landed (${slice.landed?.how}${slice.landed?.pr ? ` #${slice.landed.pr}` : ""})` : ` ${slice.status === "ready" ? "ok " : "x "}${slice.id} (${slice.branch}) ${slice.changed} file${slice.changed === 1 ? "" : "s"} · verdict ${slice.verdict.accepted ? "accepted" : slice.verdict.basis}${slice.reasons.length ? `
82179
83362
  ${slice.reasons.join(`
@@ -82183,7 +83366,7 @@ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --
82183
83366
  `landing order: ${data.landingOrder.join(", ")}`,
82184
83367
  `next: ${data.next}`,
82185
83368
  ...data.notes.map((note) => `note: ${note}`)
82186
- ], short5 = (sha) => sha ? sha.slice(0, 7) : "-", renderStatus2 = (data) => {
83369
+ ], short6 = (sha) => sha ? sha.slice(0, 7) : "-", renderStatus2 = (data) => {
82187
83370
  const counts = new Map;
82188
83371
  for (const slice of data.slices)
82189
83372
  counts.set(slice.state, (counts.get(slice.state) ?? 0) + 1);
@@ -82191,7 +83374,7 @@ var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --
82191
83374
  `fanout ${data.name} on ${data.trunk}: ${[...counts].map(([state, count]) => `${count} ${state.replace("_", " ")}`).join(", ")} · forge ${data.forge.available ? "on" : "off"}`,
82192
83375
  ...data.slices.map((slice) => {
82193
83376
  const parts = [
82194
- slice.branchExists ? short5(slice.head) : "no branch",
83377
+ slice.branchExists ? short6(slice.head) : "no branch",
82195
83378
  slice.lastCommitAt && slice.idleMinutes !== null ? `idle ${slice.idleMinutes} min` : null,
82196
83379
  slice.pr ? `PR #${slice.pr.number} ${slice.pr.state}` : null,
82197
83380
  slice.ci === "unknown" ? null : `CI ${slice.ci}`,
@@ -82225,9 +83408,9 @@ var init_fanout2 = __esm(() => {
82225
83408
  // packages/workit-cli/src/verbs/ledger.ts
82226
83409
  var exports_ledger = {};
82227
83410
  __export(exports_ledger, {
82228
- run: () => run17
83411
+ run: () => run18
82229
83412
  });
82230
- import { randomBytes as randomBytes5 } from "node:crypto";
83413
+ import { randomBytes as randomBytes6 } from "node:crypto";
82231
83414
  import { parseArgs } from "node:util";
82232
83415
  function targetBranch(io, rows, branch, pr) {
82233
83416
  if (pr !== undefined) {
@@ -82249,7 +83432,7 @@ function targetBranch(io, rows, branch, pr) {
82249
83432
  }
82250
83433
  return { ok: true, value: branch ?? null };
82251
83434
  }
82252
- async function run17(argv, io) {
83435
+ async function run18(argv, io) {
82253
83436
  let parsed;
82254
83437
  try {
82255
83438
  parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
@@ -82365,10 +83548,12 @@ async function run17(argv, io) {
82365
83548
  if (rest.length !== 1)
82366
83549
  return usage4(io, `ledger verdict takes one result: ${VERDICT_RESULTS.join("|")}`);
82367
83550
  const claimed = values.self === true ? null : claimedSession(acting);
82368
- const refusal = claimed === null ? null : sessionClaimRefusal(io.env, claimed, io.cwd);
83551
+ const token = values.token ?? io.env.WORKIT_SESSION_TOKEN ?? null;
83552
+ const refusal = claimed === null ? null : sessionClaimRefusal(io.env, claimed, io.cwd, { token, flag: acting.source === "flag" });
82369
83553
  if (refusal)
82370
83554
  return emit(io, fail("blocked", refusal.error, { unblock: refusal.unblock }));
82371
- const verdict = await recorded(recordVerdict(context, {
83555
+ const bound = claimed !== null && issuedTokenMatches(io.cwd, claimed, token);
83556
+ const verdict = await recorded(recordVerdict({ ...context, ...bound ? { sessionBound: true } : {} }, {
82372
83557
  result: rest[0],
82373
83558
  kind: values.kind,
82374
83559
  how: values.how,
@@ -82384,7 +83569,12 @@ async function run17(argv, io) {
82384
83569
  if (shadowed)
82385
83570
  io.stderr(`warning: ${shadowed}
82386
83571
  `);
82387
- return fromResult(io, verdict, (row) => `recorded verdict ${row.id}: ${row.result} [${row.kind}] for ${row.branch} @ ${(row.head ?? "").slice(0, 12)} as ${actor.session ?? "no session"}${row.self ? ` (self${row.selfReason === "no_session" ? ": WORKIT_SESSION_ID unset" : ""}; never accepted)` : ""}`);
83572
+ const withGate = verdict.ok ? { ok: true, value: { ...verdict.value, gate: gateReading(io.cwd, verdict.value) } } : verdict;
83573
+ return fromResult(io, withGate, (row) => [
83574
+ `recorded verdict ${row.id}: ${row.result} [${row.kind}] for ${row.branch} @ ${(row.head ?? "").slice(0, 12)} as ${actor.session ?? "no session"}${row.self ? ` (self${row.selfReason === "no_session" ? ": WORKIT_SESSION_ID unset" : ""}; never accepted)` : ""}`,
83575
+ row.gate ? `gate: ${row.branch} ${row.gate.accepted ? `accepted (verdict ${row.gate.verdictId ?? "?"})` : `not accepted (${row.gate.reasons.join(", ")})`}` : "gate: unknown (ledger unreadable)"
83576
+ ].join(`
83577
+ `));
82388
83578
  }
82389
83579
  function standingFanout(io, values) {
82390
83580
  if (values.fanout !== undefined) {
@@ -82441,7 +83631,7 @@ async function standing(io, values, args) {
82441
83631
  return usage4(io, `unexpected argument: ${rest[1]}`);
82442
83632
  return fromResult(io, clearStanding(context, { fanout, lead, leads, target: rest[0] ?? null }), (row) => row.target ? `cleared standing order ${row.target} for fanout ${fanout}` : `cleared every standing order for fanout ${fanout}`);
82443
83633
  }
82444
- var USAGE9 = "workit ledger decision|ruling|verdict|standing|list|check ... (workit help ledger for the grammar)", STANDING_USAGE = 'workit ledger standing add "<order>" | list | clear [<id>] [--fanout <name>] [--json]', OPTIONS, SESSION_SAFE2, ROLE_SAFE, actorFor = (io, values) => {
83634
+ var USAGE10 = "workit ledger decision|ruling|verdict|standing|list|check ... (workit help ledger for the grammar)", STANDING_USAGE = 'workit ledger standing add "<order>" | list | clear [<id>] [--fanout <name>] [--json]', OPTIONS, SESSION_SAFE2, ROLE_SAFE, actorFor = (io, values) => {
82445
83635
  const actor = actorFromEnv(io.env);
82446
83636
  if (values.session !== undefined && values.as !== undefined)
82447
83637
  return new Error("pass --session or --as, not both");
@@ -82455,7 +83645,7 @@ var USAGE9 = "workit ledger decision|ruling|verdict|standing|list|check ... (wor
82455
83645
  return new Error("--as takes a lowercase role, e.g. verifier");
82456
83646
  const prefix = (actor.session ?? actor.host).slice(0, 96);
82457
83647
  return {
82458
- actor: { ...actor, session: `${prefix}:${values.as}:${randomBytes5(4).toString("hex")}` },
83648
+ actor: { ...actor, session: `${prefix}:${values.as}:${randomBytes6(4).toString("hex")}` },
82459
83649
  source: "as",
82460
83650
  derivedFrom: actor.session
82461
83651
  };
@@ -82466,7 +83656,7 @@ var USAGE9 = "workit ledger decision|ruling|verdict|standing|list|check ... (wor
82466
83656
  return;
82467
83657
  const parsed = Number(value);
82468
83658
  return Number.isInteger(parsed) && parsed > 0 ? parsed : new Error(`${flag} must be a positive integer`);
82469
- }, failed2 = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed2(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE9 })), passedTestOn = (cwd, verdict) => {
83659
+ }, failed2 = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed2(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE10 })), passedTestOn = (cwd, verdict) => {
82470
83660
  const ledger = readLedger(cwd);
82471
83661
  return ledger.ok && ledger.value.rows.some((row) => row.type === "check" && row.observer === "workit_cli" && row.name === "test" && row.result === "passed" && row.dirty !== true && row.branch === verdict.branch && row.head === verdict.head);
82472
83662
  }, verdictLines = (check) => {
@@ -82498,7 +83688,17 @@ var USAGE9 = "workit ledger decision|ruling|verdict|standing|list|check ... (wor
82498
83688
  ] : [
82499
83689
  `${data.unverified_rows.length} unverified row(s) (not written by the workit CLI, or changed afterwards):`,
82500
83690
  ...data.unverified_rows.map((row) => ` #${row.seq} ${row.id} ${row.type}${row.branch ? ` [${row.branch}]` : ""}: ${row.reason}`)
82501
- ];
83691
+ ], gateReading = (cwd, row) => {
83692
+ const ledger = readLedger(cwd, { integrity: true });
83693
+ if (!ledger.ok || !row.branch)
83694
+ return null;
83695
+ const { accepted } = checkVerdicts(cwd, row.branch, ledger.value.rows);
83696
+ return {
83697
+ accepted: accepted.accepted,
83698
+ reasons: accepted.reasons,
83699
+ verdictId: typeof accepted.verdict?.id === "string" ? accepted.verdict.id : null
83700
+ };
83701
+ };
82502
83702
  var init_ledger2 = __esm(() => {
82503
83703
  init_ledger();
82504
83704
  init_fanout();
@@ -82522,6 +83722,7 @@ var init_ledger2 = __esm(() => {
82522
83722
  last: { type: "string" },
82523
83723
  supersedes: { type: "string" },
82524
83724
  session: { type: "string" },
83725
+ token: { type: "string" },
82525
83726
  as: { type: "string" },
82526
83727
  fanout: { type: "string" },
82527
83728
  json: { type: "boolean" }
@@ -98377,11 +99578,11 @@ var init_rules = __esm(() => {
98377
99578
  });
98378
99579
 
98379
99580
  // packages/workit-cli/src/test-audit/audit.ts
98380
- import { spawnSync as spawnSync26 } from "node:child_process";
98381
- import { existsSync as existsSync20, readdirSync as readdirSync9, readFileSync as readFileSync23, statSync as statSync8 } from "node:fs";
99581
+ import { spawnSync as spawnSync27 } from "node:child_process";
99582
+ import { existsSync as existsSync20, readdirSync as readdirSync10, readFileSync as readFileSync23, statSync as statSync8 } from "node:fs";
98382
99583
  import path52 from "node:path";
98383
99584
  function git7(cwd, args) {
98384
- const run = spawnSync26("git", ["-c", "core.quotepath=off", ...args], {
99585
+ const run = spawnSync27("git", ["-c", "core.quotepath=off", ...args], {
98385
99586
  cwd,
98386
99587
  encoding: "utf8",
98387
99588
  maxBuffer: 64 * 1024 * 1024
@@ -98389,7 +99590,7 @@ function git7(cwd, args) {
98389
99590
  return { ok: run.status === 0, stdout: run.stdout ?? "" };
98390
99591
  }
98391
99592
  function walkTests(dir, out) {
98392
- for (const entry of readdirSync9(dir, { withFileTypes: true })) {
99593
+ for (const entry of readdirSync10(dir, { withFileTypes: true })) {
98393
99594
  if (entry.name.startsWith(".") && entry.name !== "." && entry.isDirectory())
98394
99595
  continue;
98395
99596
  const full = path52.join(dir, entry.name);
@@ -98525,7 +99726,7 @@ import {
98525
99726
  lstatSync as lstatSync8,
98526
99727
  mkdirSync as mkdirSync10,
98527
99728
  mkdtempSync as mkdtempSync4,
98528
- readdirSync as readdirSync10,
99729
+ readdirSync as readdirSync11,
98529
99730
  readFileSync as readFileSync24,
98530
99731
  readlinkSync as readlinkSync3,
98531
99732
  realpathSync as realpathSync9,
@@ -98622,7 +99823,7 @@ function removeStaleCopies(tmp = os16.tmpdir()) {
98622
99823
  let removed = 0;
98623
99824
  let names = [];
98624
99825
  try {
98625
- names = readdirSync10(tmp);
99826
+ names = readdirSync11(tmp);
98626
99827
  } catch {
98627
99828
  return 0;
98628
99829
  }
@@ -98651,7 +99852,7 @@ function removeStaleCopies(tmp = os16.tmpdir()) {
98651
99852
  }
98652
99853
  function linkNodeModules(origin, target, repo, copy) {
98653
99854
  mkdirSync10(target, { recursive: true });
98654
- for (const entry of readdirSync10(origin, { withFileTypes: true })) {
99855
+ for (const entry of readdirSync11(origin, { withFileTypes: true })) {
98655
99856
  const from = path53.join(origin, entry.name);
98656
99857
  const to = path53.join(target, entry.name);
98657
99858
  if (entry.name.startsWith("@") && entry.isDirectory()) {
@@ -98845,7 +100046,7 @@ async function runMutation(options) {
98845
100046
  if (!runnable.includes(mutant))
98846
100047
  record({ ...mutant, status: "no-tests", durationMs: 0, tests: [] });
98847
100048
  if (runnable.length === 0)
98848
- return finish2(options, 0, results, counts, started);
100049
+ return finish3(options, 0, results, counts, started);
98849
100050
  removeStaleCopies();
98850
100051
  const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
98851
100052
  const copyCwd = path53.join(copy, cwdRelative);
@@ -98909,7 +100110,7 @@ async function runMutation(options) {
98909
100110
  restore();
98910
100111
  }
98911
100112
  }
98912
- return finish2(options, baseline.durationMs, results, counts, started);
100113
+ return finish3(options, baseline.durationMs, results, counts, started);
98913
100114
  } finally {
98914
100115
  process.off("SIGINT", onSignal);
98915
100116
  process.off("SIGTERM", onSignal);
@@ -98917,7 +100118,7 @@ async function runMutation(options) {
98917
100118
  cleanup();
98918
100119
  }
98919
100120
  }
98920
- function finish2(options, baselineMs, results, counts, started) {
100121
+ function finish3(options, baselineMs, results, counts, started) {
98921
100122
  const decided = counts.killed + counts.timeout + counts.survived;
98922
100123
  return {
98923
100124
  base: options.base,
@@ -98987,7 +100188,7 @@ var init_mutate = __esm(() => {
98987
100188
  // packages/workit-cli/src/verbs/test-audit.ts
98988
100189
  var exports_test_audit = {};
98989
100190
  __export(exports_test_audit, {
98990
- run: () => run18
100191
+ run: () => run19
98991
100192
  });
98992
100193
  import { existsSync as existsSync22 } from "node:fs";
98993
100194
  import path54 from "node:path";
@@ -99088,11 +100289,11 @@ function human(data) {
99088
100289
  }
99089
100290
  return lines;
99090
100291
  }
99091
- async function run18(argv, io) {
100292
+ async function run19(argv, io) {
99092
100293
  const options = parse7(argv);
99093
100294
  if (typeof options === "string")
99094
100295
  return emit(io, fail("invalid_input", `${options}
99095
- usage: ${USAGE10}`));
100296
+ usage: ${USAGE11}`));
99096
100297
  const top = git7(io.cwd, ["rev-parse", "--show-toplevel"]);
99097
100298
  const repo = top.ok ? path54.resolve(top.stdout.trim()) : null;
99098
100299
  const display = (file) => toPosix(path54.relative(io.cwd, file) || file);
@@ -99178,7 +100379,7 @@ usage: ${USAGE10}`));
99178
100379
  }
99179
100380
  return emit(io, ok(data), human);
99180
100381
  }
99181
- var USAGE10 = "workit test-audit [paths…] [--diff [base]] [--rule <ids>] [--min-severity <level>] [--fail-on <level>] [--mutate [--test-cmd <cmd>] [--max-mutants <n>] [--budget <s>] [--timeout <s>]] [--json]", LEVELS, VALUE_FLAGS2, quote = (arg) => /^[\w./@:=+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
100382
+ var USAGE11 = "workit test-audit [paths…] [--diff [base]] [--rule <ids>] [--min-severity <level>] [--fail-on <level>] [--mutate [--test-cmd <cmd>] [--max-mutants <n>] [--budget <s>] [--timeout <s>]] [--json]", LEVELS, VALUE_FLAGS2, quote = (arg) => /^[\w./@:=+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
99182
100383
  var init_test_audit = __esm(() => {
99183
100384
  init_audit();
99184
100385
  init_mutate();
@@ -99199,12 +100400,12 @@ var init_test_audit = __esm(() => {
99199
100400
  // packages/workit-cli/src/verbs/knowledge.ts
99200
100401
  var exports_knowledge = {};
99201
100402
  __export(exports_knowledge, {
99202
- run: () => run19
100403
+ run: () => run20
99203
100404
  });
99204
- async function run19(argv, io) {
100405
+ async function run20(argv, io) {
99205
100406
  const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
99206
100407
  if (sub !== "lint" || rest.length > 0)
99207
- return emit(io, fail("invalid_input", `usage: ${USAGE11}`));
100408
+ return emit(io, fail("invalid_input", `usage: ${USAGE12}`));
99208
100409
  const report = lintKnowledge(checkRoot(io.cwd));
99209
100410
  if (report.unreadable.length > 0)
99210
100411
  return emit(io, fail("unavailable", `knowledge lint: cannot read ${report.unreadable.join(", ")}`, {
@@ -99221,7 +100422,7 @@ async function run19(argv, io) {
99221
100422
  `);
99222
100423
  return report.findings.length === 0 ? 0 : 1;
99223
100424
  }
99224
- var USAGE11 = "workit knowledge lint [--json]";
100425
+ var USAGE12 = "workit knowledge lint [--json]";
99225
100426
  var init_knowledge2 = __esm(() => {
99226
100427
  init_check_config();
99227
100428
  init_knowledge();
@@ -99335,14 +100536,14 @@ var init_youtrack_writes = __esm(() => {
99335
100536
  // packages/workit-cli/src/verbs/youtrack.ts
99336
100537
  var exports_youtrack = {};
99337
100538
  __export(exports_youtrack, {
99338
- run: () => run20
100539
+ run: () => run21
99339
100540
  });
99340
100541
  import { readFileSync as readFileSync25 } from "node:fs";
99341
100542
  import path55 from "node:path";
99342
- async function run20(argv, io) {
100543
+ async function run21(argv, io) {
99343
100544
  const [sub, ...rest] = argv;
99344
100545
  if (sub !== "note" && sub !== "time" && sub !== "meeting")
99345
- return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE12);
100546
+ return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE13);
99346
100547
  const flags = parseFlags(rest, {
99347
100548
  markdown: "value",
99348
100549
  file: "value",
@@ -99351,18 +100552,18 @@ async function run20(argv, io) {
99351
100552
  date: "value"
99352
100553
  });
99353
100554
  if (typeof flags === "string")
99354
- return usage3(io, flags, USAGE12);
100555
+ return usage3(io, flags, USAGE13);
99355
100556
  const [issueId, extra] = flags.positionals;
99356
100557
  if (!issueId || !ISSUE_RE2.test(issueId))
99357
- return usage3(io, "an issue id like ABC-123 is required", USAGE12);
100558
+ return usage3(io, "an issue id like ABC-123 is required", USAGE13);
99358
100559
  if (extra !== undefined)
99359
- return usage3(io, `unexpected argument ${extra}`, USAGE12);
100560
+ return usage3(io, `unexpected argument ${extra}`, USAGE13);
99360
100561
  const minutes = positiveInt(flags.values.minutes, "--minutes");
99361
100562
  if (typeof minutes === "string")
99362
- return usage3(io, minutes, USAGE12);
100563
+ return usage3(io, minutes, USAGE13);
99363
100564
  const date = youTrackWorkDateMs(flags.values.date ?? "auto");
99364
100565
  if ("error" in date)
99365
- return usage3(io, date.error, USAGE12);
100566
+ return usage3(io, date.error, USAGE13);
99366
100567
  const dateMs = date.data.dateMs;
99367
100568
  const markedDay = flags.values.date !== undefined && flags.values.date !== "auto" ? { dateMs } : {};
99368
100569
  const root = io.cwd;
@@ -99380,24 +100581,24 @@ async function run20(argv, io) {
99380
100581
  };
99381
100582
  if (sub === "note") {
99382
100583
  if (Boolean(flags.values.markdown) === Boolean(flags.values.file))
99383
- return usage3(io, "pass exactly one of --markdown or --file", USAGE12);
100584
+ return usage3(io, "pass exactly one of --markdown or --file", USAGE13);
99384
100585
  let markdown = flags.values.markdown ?? "";
99385
100586
  if (flags.values.file)
99386
100587
  try {
99387
100588
  markdown = readFileSync25(path55.resolve(io.cwd, flags.values.file), "utf8");
99388
100589
  } catch (error) {
99389
- return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE12);
100590
+ return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE13);
99390
100591
  }
99391
100592
  if (!markdown.trim())
99392
- return usage3(io, "the note is empty", USAGE12);
100593
+ return usage3(io, "the note is empty", USAGE13);
99393
100594
  const missing = step("comment", "comments", { operation: "note", issueId, markdown }, markdown, async (text) => commentResult(await postComment(issueId, text, root))) ?? (minutes ? step("time", "workItems", { operation: "note.time", issueId, markdown, minutes, ...markedDay }, "workit update", (text) => logTimeUpdate({ issueId, minutes, text, dateMs, workspace_root: root })) : null);
99394
100595
  if (missing)
99395
100596
  return emit(io, fail("unavailable", missing));
99396
100597
  } else {
99397
100598
  if (!minutes)
99398
- return usage3(io, `${sub} needs --minutes <n>`, USAGE12);
100599
+ return usage3(io, `${sub} needs --minutes <n>`, USAGE13);
99399
100600
  if (sub === "meeting" && !flags.values.text)
99400
- return usage3(io, "meeting needs --text <what the meeting was>", USAGE12);
100601
+ return usage3(io, "meeting needs --text <what the meeting was>", USAGE13);
99401
100602
  const text = flags.values.text ?? "";
99402
100603
  const missing = step(sub, "workItems", { operation: sub, issueId, minutes, text, ...markedDay }, text, (marked) => logTimeUpdate({ issueId, minutes, text: marked, dateMs, workspace_root: root }));
99403
100604
  if (missing)
@@ -99426,7 +100627,7 @@ async function run20(argv, io) {
99426
100627
  ...data.steps.flatMap((entry) => entry.note ? [` note (${entry.step}): ${entry.note}`] : [])
99427
100628
  ]);
99428
100629
  }
99429
- var USAGE12 = "workit youtrack note <ISSUE> (--markdown <t> | --file <p>) [--minutes <n>] [--date auto|YYYY-MM-DD] | youtrack time <ISSUE> --minutes <n> [--text <t>] [--date …] | youtrack meeting <ISSUE> --minutes <n> --text <t> [--date …]", commentResult = (value) => {
100630
+ var USAGE13 = "workit youtrack note <ISSUE> (--markdown <t> | --file <p>) [--minutes <n>] [--date auto|YYYY-MM-DD] | youtrack time <ISSUE> --minutes <n> [--text <t>] [--date …] | youtrack meeting <ISSUE> --minutes <n> --text <t> [--date …]", commentResult = (value) => {
99430
100631
  try {
99431
100632
  return { ok: true, data: unwrap2(value) };
99432
100633
  } catch (error) {
@@ -99450,14 +100651,14 @@ var init_youtrack2 = __esm(() => {
99450
100651
  // packages/workit-cli/src/verbs/changelog.ts
99451
100652
  var exports_changelog = {};
99452
100653
  __export(exports_changelog, {
99453
- run: () => run21
100654
+ run: () => run22
99454
100655
  });
99455
100656
  import { readFileSync as readFileSync26 } from "node:fs";
99456
100657
  import path56 from "node:path";
99457
- async function run21(argv, io) {
100658
+ async function run22(argv, io) {
99458
100659
  const [sub, ...rest] = argv;
99459
100660
  if (sub !== "apply")
99460
- return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE13);
100661
+ return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE14);
99461
100662
  const flags = parseFlags(rest, {
99462
100663
  entries: "value",
99463
100664
  path: "value",
@@ -99465,18 +100666,18 @@ async function run21(argv, io) {
99465
100666
  preview: "boolean"
99466
100667
  });
99467
100668
  if (typeof flags === "string")
99468
- return usage3(io, flags, USAGE13);
100669
+ return usage3(io, flags, USAGE14);
99469
100670
  if (flags.positionals.length)
99470
- return usage3(io, `unexpected argument ${flags.positionals[0]}`, USAGE13);
100671
+ return usage3(io, `unexpected argument ${flags.positionals[0]}`, USAGE14);
99471
100672
  const normalizeOnly = flags.booleans.has("normalize-only");
99472
100673
  if (!normalizeOnly && !flags.values.entries)
99473
- return usage3(io, "pass --entries <JSON> or --normalize-only", USAGE13);
100674
+ return usage3(io, "pass --entries <JSON> or --normalize-only", USAGE14);
99474
100675
  let entries;
99475
100676
  if (flags.values.entries)
99476
100677
  try {
99477
100678
  entries = readEntries(flags.values.entries, io.cwd);
99478
100679
  } catch (error) {
99479
- return usage3(io, `--entries is not valid JSON: ${String(error)}`, USAGE13);
100680
+ return usage3(io, `--entries is not valid JSON: ${String(error)}`, USAGE14);
99480
100681
  }
99481
100682
  const input = {
99482
100683
  entries,
@@ -99498,7 +100699,7 @@ async function run21(argv, io) {
99498
100699
  `changelog updated: ${String(applied.path ?? "CHANGELOG.md")}`
99499
100700
  ]);
99500
100701
  }
99501
- var USAGE13 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
100702
+ var USAGE14 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
99502
100703
  const text = raw === "-" ? readFileSync26(0, "utf8") : raw.startsWith("@") ? readFileSync26(path56.resolve(cwd, raw.slice(1)), "utf8") : raw;
99503
100704
  return JSON.parse(text);
99504
100705
  };
@@ -99511,12 +100712,12 @@ var init_changelog2 = __esm(() => {
99511
100712
  // packages/workit-cli/src/verbs/handoff.ts
99512
100713
  var exports_handoff = {};
99513
100714
  __export(exports_handoff, {
99514
- run: () => run22
100715
+ run: () => run23
99515
100716
  });
99516
100717
  import { parseArgs as parseArgs2 } from "node:util";
99517
100718
  function human2(brief) {
99518
100719
  const lines = [
99519
- `branch: ${brief.branch ?? "(detached)"} @ ${short6(brief.head)}${brief.subject ? ` ${brief.subject}` : ""}`
100720
+ `branch: ${brief.branch ?? "(detached)"} @ ${short7(brief.head)}${brief.subject ? ` ${brief.subject}` : ""}`
99520
100721
  ];
99521
100722
  if (brief.base)
99522
100723
  lines.push(`base: ${brief.base}${brief.ahead === null ? "" : ` (ahead ${brief.ahead}, behind ${brief.behind})`}`);
@@ -99559,11 +100760,11 @@ async function resumeBrief(argv, io) {
99559
100760
  strict: true
99560
100761
  }));
99561
100762
  } catch (error) {
99562
- return emit(io, fail("invalid_input", error.message, { unblock: USAGE14 }));
100763
+ return emit(io, fail("invalid_input", error.message, { unblock: USAGE15 }));
99563
100764
  }
99564
100765
  const last = values.last === undefined ? 10 : Number(values.last);
99565
100766
  if (!Number.isInteger(last) || last < 1)
99566
- return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock: USAGE14 }));
100767
+ return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock: USAGE15 }));
99567
100768
  const built = buildHandoff(io.cwd, {
99568
100769
  last,
99569
100770
  note: values.note ?? null,
@@ -99581,13 +100782,13 @@ async function resumeBrief(argv, io) {
99581
100782
  }
99582
100783
  return emit(io, ok({ ...data, recorded }), human2);
99583
100784
  }
99584
- async function run22(argv, io) {
100785
+ async function run23(argv, io) {
99585
100786
  if (!argv.some((arg) => arg === "--task" || arg.startsWith("--task=")))
99586
100787
  return resumeBrief(argv, io);
99587
100788
  await Promise.resolve().then(() => init_task());
99588
100789
  return runTaskCommand(["handoff", ...argv]);
99589
100790
  }
99590
- var USAGE14 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short6 = (sha) => sha ? sha.slice(0, 12) : "no commit";
100791
+ var USAGE15 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short7 = (sha) => sha ? sha.slice(0, 12) : "no commit";
99591
100792
  var init_handoff = __esm(() => {
99592
100793
  init_ledger();
99593
100794
  init_output();
@@ -99788,7 +100989,7 @@ function withSubcommands(entry, options = {}) {
99788
100989
  };
99789
100990
  }
99790
100991
  var VERIFY_OPTIONS = "[--expect pushed|pr|merged|released] [--pr <n> | --branch <b>] [--sha <sha>] [--tag <t>] [--package <name[@version]>]";
99791
- var LEDGER_ACTOR = "[--branch <b> | --pr <n>] [--supersedes <id>] [--session <id> | --as <role>]";
100992
+ var LEDGER_ACTOR = "[--branch <b> | --pr <n>] [--supersedes <id>] [--session <id> [--token <t>] | --as <role>]";
99792
100993
  var VERBS = [
99793
100994
  {
99794
100995
  name: "init",
@@ -99966,6 +101167,35 @@ var VERBS = [
99966
101167
  ],
99967
101168
  load: () => Promise.resolve().then(() => (init_git2(), exports_git))
99968
101169
  }),
101170
+ withSubcommands({
101171
+ name: "release",
101172
+ group: "delivery",
101173
+ summary: "git-flow releases without the git-flow binary: start a local release branch, finish it (merge, tag, merge back, push, forge release), publish it only when asked",
101174
+ subcommands: [
101175
+ {
101176
+ name: "start",
101177
+ usage: "workit release start <version> [--hotfix] [--track <t>]",
101178
+ summary: "Cut release/<version> (hotfix/<version> from production) locally; never pushes it"
101179
+ },
101180
+ {
101181
+ name: "finish",
101182
+ usage: "workit release finish [<version>] [--track <t>] [--dry-run]",
101183
+ summary: "Merge into production, tag, merge back, delete the branch, push production, merge-back branches and the tag, publish the forge release; resumes an interrupted finish"
101184
+ },
101185
+ {
101186
+ name: "publish",
101187
+ usage: "workit release publish [<version>]",
101188
+ summary: "Push the release branch (opt-in: others commit to it or CI runs on it)"
101189
+ }
101190
+ ],
101191
+ examples: [
101192
+ "workit release start 1.4.0",
101193
+ 'workit git commit -m "chore(release): 1.4.0" -- package.json CHANGELOG.md',
101194
+ "workit release finish --dry-run",
101195
+ "workit release finish"
101196
+ ],
101197
+ load: () => Promise.resolve().then(() => (init_release2(), exports_release))
101198
+ }),
99969
101199
  withSubcommands({
99970
101200
  name: "verify-delivery",
99971
101201
  group: "delivery",
@@ -100088,7 +101318,7 @@ var VERBS = [
100088
101318
  },
100089
101319
  {
100090
101320
  name: "standing",
100091
- usage: `workit ledger standing add "<order>" | standing list | standing clear [<id>] [--fanout <name>] [--supersedes <id>] [--session <id> | --as <role>]`,
101321
+ usage: `workit ledger standing add "<order>" | standing list | standing clear [<id>] [--fanout <name>] [--supersedes <id>] [--session <id> [--token <t>] | --as <role>]`,
100092
101322
  summary: "The lead's standing orders for a fanout's workers"
100093
101323
  },
100094
101324
  {