@brainervirus/workit-cli 8.34.0 → 8.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +351 -65
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -6401,9 +6401,16 @@ function workspaceDefaultTarget(cwd, globalDefault) {
6401
6401
  }
6402
6402
  var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspaceCommitPolicySchema, workspaceProfileSchema, versionSourceSchema, releaseTrackSchema, workspaceConfigSchema, workspacesFileSchema, workspacesRevision = (contents) => createHash("sha256").update(contents ?? "<missing>").digest("hex"), validateWorkspacesDocument = (parsed, file) => {
6403
6403
  if (!isConfigObject(parsed)) {
6404
- return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
6404
+ return {
6405
+ status: "malformed",
6406
+ path: file,
6407
+ entries: [],
6408
+ error: `${file} is not a JSON object`
6409
+ };
6405
6410
  }
6406
- const validated = workspacesFileSchema.safeParse(parsed, { reportInput: true });
6411
+ const validated = workspacesFileSchema.safeParse(parsed, {
6412
+ reportInput: true
6413
+ });
6407
6414
  if (!validated.success) {
6408
6415
  const detail = validated.error.issues.map((issue) => {
6409
6416
  const field = String(issue.path[issue.path.length - 1] ?? "");
@@ -6442,7 +6449,12 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
6442
6449
  raw = readFileSync2(file, "utf8");
6443
6450
  } catch (error) {
6444
6451
  if (error.code === "ENOENT")
6445
- return { status: "missing", path: file, entries: [], revision: workspacesRevision(null) };
6452
+ return {
6453
+ status: "missing",
6454
+ path: file,
6455
+ entries: [],
6456
+ revision: workspacesRevision(null)
6457
+ };
6446
6458
  return {
6447
6459
  status: "malformed",
6448
6460
  path: file,
@@ -6462,7 +6474,10 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
6462
6474
  error: `${file} is not valid JSON`
6463
6475
  };
6464
6476
  }
6465
- return { ...validateWorkspacesDocument(parsed, file), revision: workspacesRevision(raw) };
6477
+ return {
6478
+ ...validateWorkspacesDocument(parsed, file),
6479
+ revision: workspacesRevision(raw)
6480
+ };
6466
6481
  }, loadWorkspacesFrom = (dir) => readWorkspacesResult(dir).entries, globToRegExp = (glob) => {
6467
6482
  let out = "";
6468
6483
  for (let i = 0;i < glob.length; i++) {
@@ -6591,7 +6606,7 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
6591
6606
  for (const b of entries.slice(i + 1)) {
6592
6607
  const aInB = matchWorkspace(b.glob, globRoot(a.glob));
6593
6608
  const bInA = matchWorkspace(a.glob, globRoot(b.glob));
6594
- if (!aInB && !bInA)
6609
+ if (!aInB && !bInA && !globsMayOverlap(a.glob, b.glob))
6595
6610
  continue;
6596
6611
  const [inner, outer] = aInB && !bInA ? [a, b] : bInA && !aInB ? [b, a] : globSpecificity(a.glob) >= globSpecificity(b.glob) ? [a, b] : [b, a];
6597
6612
  const si = globSpecificity(inner.glob);
@@ -6681,7 +6696,10 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
6681
6696
  return selected ? { file, selected, matches } : null;
6682
6697
  }, resolveRuntimeReleaseTracks = (cwd) => {
6683
6698
  const matched = matchRuntimeWorkspace(cwd);
6684
- return matched ? { workspace: matched.selected.name, releaseTracks: matched.selected.releaseTracks } : null;
6699
+ return matched ? {
6700
+ workspace: matched.selected.name,
6701
+ releaseTracks: matched.selected.releaseTracks
6702
+ } : null;
6685
6703
  }, inheritedSetting = (cwd, dir, has) => {
6686
6704
  let matched;
6687
6705
  try {
@@ -6693,7 +6711,10 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
6693
6711
  return null;
6694
6712
  const { selected, matches } = matched;
6695
6713
  const enclosing = matches.filter((entry) => entry !== selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
6696
- return { workspace: selected.name, source: [selected, ...enclosing].find(has) ?? null };
6714
+ return {
6715
+ workspace: selected.name,
6716
+ source: [selected, ...enclosing].find(has) ?? null
6717
+ };
6697
6718
  }, workspaceDeleteBranch = (cwd, dir) => inheritedSetting(cwd, dir, (entry) => typeof entry.deleteBranch === "boolean")?.source?.deleteBranch !== false, workspaceTrailers = (cwd, dir) => {
6698
6719
  const found = inheritedSetting(cwd, dir, (entry) => typeof entry.trailers === "boolean");
6699
6720
  return {
@@ -6821,7 +6842,10 @@ var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspace
6821
6842
  status: "resolved",
6822
6843
  profileName: null,
6823
6844
  workspace: null,
6824
- provenance: { branchPolicy: "user-default", commitPolicy: "user-default" }
6845
+ provenance: {
6846
+ branchPolicy: "user-default",
6847
+ commitPolicy: "user-default"
6848
+ }
6825
6849
  };
6826
6850
  if (selected.status === "invalid")
6827
6851
  return selected;
@@ -6892,7 +6916,11 @@ var init_workspaces = __esm(() => {
6892
6916
  ["requiredChecks", track.requiredChecks ?? []]
6893
6917
  ]) {
6894
6918
  if (new Set(values).size !== values.length) {
6895
- context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
6919
+ context.addIssue({
6920
+ code: "custom",
6921
+ path: [field],
6922
+ message: "entries must be unique"
6923
+ });
6896
6924
  }
6897
6925
  }
6898
6926
  if (track.versionSource?.kind === "package-json" && (path2.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
@@ -6925,7 +6953,10 @@ var init_workspaces = __esm(() => {
6925
6953
  kind,
6926
6954
  union([boolean2(), literal("verified")]).optional()
6927
6955
  ]))).passthrough().optional(),
6928
- youtrack: object({ baseUrl: nonBlank.optional(), link_issues: boolean2().optional() }).passthrough().optional(),
6956
+ youtrack: object({
6957
+ baseUrl: nonBlank.optional(),
6958
+ link_issues: boolean2().optional()
6959
+ }).passthrough().optional(),
6929
6960
  issues: object({
6930
6961
  provider: _enum(["github", "gitlab", "youtrack"]).optional(),
6931
6962
  link: boolean2().optional(),
@@ -9286,7 +9317,7 @@ var package_default;
9286
9317
  var init_package = __esm(() => {
9287
9318
  package_default = {
9288
9319
  name: "@brainervirus/workit-cli",
9289
- version: "8.34.0",
9320
+ version: "8.36.0",
9290
9321
  private: false,
9291
9322
  description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
9292
9323
  keywords: [
@@ -9324,13 +9355,13 @@ var init_package = __esm(() => {
9324
9355
  },
9325
9356
  dependencies: {
9326
9357
  "@babel/parser": "7.29.9",
9327
- "@brainervirus/workit-cursor": "^8.34.0",
9358
+ "@brainervirus/workit-cursor": "^8.36.0",
9328
9359
  "@inkjs/ui": "2.0.0",
9329
9360
  ink: "7.1.1",
9330
9361
  react: "19.2.8"
9331
9362
  },
9332
9363
  devDependencies: {
9333
- "@brainervirus/workit-core": "^8.34.0"
9364
+ "@brainervirus/workit-core": "^8.36.0"
9334
9365
  },
9335
9366
  engines: {
9336
9367
  node: ">=24"
@@ -14889,7 +14920,12 @@ function readLedger(cwd, options = {}) {
14889
14920
  });
14890
14921
  return {
14891
14922
  ok: true,
14892
- value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
14923
+ value: {
14924
+ path: file.value,
14925
+ rows,
14926
+ skipped,
14927
+ integrity: options.integrity ? integrity : null
14928
+ }
14893
14929
  };
14894
14930
  }
14895
14931
  function ledgerTampering(cwd) {
@@ -15014,7 +15050,10 @@ function branchScope(cwd, branch) {
15014
15050
  return at >= since && at <= until;
15015
15051
  };
15016
15052
  }
15017
- function authorSessions(cwd, rows, branch, range = { base: null, head: null }, scope = branchScope(cwd, branch)) {
15053
+ function authorSessions(cwd, rows, branch, range = {
15054
+ base: null,
15055
+ head: null
15056
+ }, scope = branchScope(cwd, branch)) {
15018
15057
  const sessions = new Set;
15019
15058
  for (const row of rows) {
15020
15059
  if (!scope(row) || row.type !== "commit.recorded" && row.type !== "task.opened")
@@ -15089,12 +15128,13 @@ function restackCarries(exactIn, rows, branch, from, to) {
15089
15128
  const cwd = exactIn;
15090
15129
  let known = restackExact.get(row);
15091
15130
  if (known === undefined) {
15092
- known = typeof row.fromBase === "string" && typeof row.toBase === "string" && sameOwnChange(cwd, {
15131
+ const range = typeof row.fromBase === "string" && typeof row.toBase === "string" ? {
15093
15132
  from: fromHead,
15094
15133
  to: head,
15095
15134
  fromBase: row.fromBase,
15096
15135
  toBase: row.toBase
15097
- });
15136
+ } : null;
15137
+ known = range !== null && (sameOwnChange(cwd, range) || cleanRebase(cwd, range));
15098
15138
  restackExact.set(row, known);
15099
15139
  }
15100
15140
  return known;
@@ -15129,7 +15169,10 @@ function checkVerdicts(cwd, branch, ledgerRows) {
15129
15169
  const cacheKey = `${base}\x00${at}`;
15130
15170
  let key = keyCache.get(cacheKey);
15131
15171
  if (!key) {
15132
- key = { patchId: patchId(cwd, base, at), diffHash: diffHash(cwd, base, at) };
15172
+ key = {
15173
+ patchId: patchId(cwd, base, at),
15174
+ diffHash: diffHash(cwd, base, at)
15175
+ };
15133
15176
  keyCache.set(cacheKey, key);
15134
15177
  }
15135
15178
  return key;
@@ -15169,7 +15212,10 @@ function checkVerdicts(cwd, branch, ledgerRows) {
15169
15212
  };
15170
15213
  const exactCarry = (row) => FAILING.has(String(row.result)) || exactMatch(row) !== null;
15171
15214
  const keyBasis = (row) => {
15172
- const basis = verdictBasis(row, { head, ...keyFor(row.base ?? fallbackBase) });
15215
+ const basis = verdictBasis(row, {
15216
+ head,
15217
+ ...keyFor(row.base ?? fallbackBase)
15218
+ });
15173
15219
  return basis === "carried" && !exactCarry(row) ? "stale" : basis;
15174
15220
  };
15175
15221
  const rebaseNote = (row) => row.head && head && row.head !== head && !FAILING.has(String(row.result)) && keyBasis(row) === "carried" && exactMatch(row) === "rebase" ? { rule: "rebase", since: row.head, files: [] } : null;
@@ -15274,7 +15320,11 @@ function checkVerdicts(cwd, branch, ledgerRows) {
15274
15320
  const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
15275
15321
  const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
15276
15322
  const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
15277
- const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
15323
+ const accepted = tampered.length ? {
15324
+ accepted: false,
15325
+ verdict: null,
15326
+ reasons: ["ledger_tampered"]
15327
+ } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
15278
15328
  accepted: false,
15279
15329
  verdict: failing?.verdict ?? null,
15280
15330
  reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
@@ -15292,7 +15342,10 @@ function checkVerdicts(cwd, branch, ledgerRows) {
15292
15342
  selfVerdict: accepted.accepted ? null : selfEntry?.verdict ?? null,
15293
15343
  base: newestCurrent?.verdict.base ?? fallbackBase,
15294
15344
  patchId: keyFor(newestCurrent?.verdict.base ?? fallbackBase).patchId,
15295
- current: { basis: newestCurrent?.basis ?? "none", verdict: newestCurrent?.verdict ?? null },
15345
+ current: {
15346
+ basis: newestCurrent?.basis ?? "none",
15347
+ verdict: newestCurrent?.verdict ?? null
15348
+ },
15296
15349
  carry: carryEntry?.carry ?? null,
15297
15350
  reverify,
15298
15351
  accepted,
@@ -15531,10 +15584,18 @@ function recordVerdict(context, input) {
15531
15584
  }
15532
15585
  function untrackedField(cwd) {
15533
15586
  const files = untrackedFiles2(cwd);
15534
- return files.length ? { untracked: files.slice(0, MAX_UNTRACKED_LISTED), untrackedCount: files.length } : {};
15587
+ return files.length ? {
15588
+ untracked: files.slice(0, MAX_UNTRACKED_LISTED),
15589
+ untrackedCount: files.length
15590
+ } : {};
15535
15591
  }
15536
15592
  function recordHandoff(context, input) {
15537
- return appendRow(context.cwd, { ...common(context, noteKey(context)), type: "handoff", note: input.note, next: input.next }, { now: context.now });
15593
+ return appendRow(context.cwd, {
15594
+ ...common(context, noteKey(context)),
15595
+ type: "handoff",
15596
+ note: input.note,
15597
+ next: input.next
15598
+ }, { now: context.now });
15538
15599
  }
15539
15600
  function rowLabels(row) {
15540
15601
  const labels = [];
@@ -15689,7 +15750,11 @@ function buildHandoff(cwd, options = {}) {
15689
15750
  }));
15690
15751
  const lastHandoffRow = mine.findLast((row) => row.type === "handoff");
15691
15752
  const partial = {
15692
- store: { root: store.value.root, shared: store.value.shared, ledger: ledger.path },
15753
+ store: {
15754
+ root: store.value.root,
15755
+ shared: store.value.shared,
15756
+ ledger: ledger.path
15757
+ },
15693
15758
  branch,
15694
15759
  head,
15695
15760
  subject: head ? git4(cwd, ["log", "-1", "--format=%s", head]) : null,
@@ -15722,7 +15787,11 @@ function buildHandoff(cwd, options = {}) {
15722
15787
  const nextCommand = nextCommandFor(partial);
15723
15788
  return {
15724
15789
  ok: true,
15725
- value: { ...partial, nextCommand, next: options.next?.trim() || nextCommand }
15790
+ value: {
15791
+ ...partial,
15792
+ nextCommand,
15793
+ next: options.next?.trim() || nextCommand
15794
+ }
15726
15795
  };
15727
15796
  }
15728
15797
  var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, VERDICT_RESULTS, PASSING, FAILING, STRONG_RESULTS, VERDICT_KINDS, VERDICT_SURFACES, PR_ROW_TYPES, err = (code, error, unblock) => ({
@@ -15887,7 +15956,11 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
15887
15956
  if (entry[0] === "R" || entry[0] === "C")
15888
15957
  index += 1;
15889
15958
  }
15890
- return { dirty: paths.length > 0, files: paths.length, paths: paths.slice(0, MAX_DIRTY_PATHS) };
15959
+ return {
15960
+ dirty: paths.length > 0,
15961
+ files: paths.length,
15962
+ paths: paths.slice(0, MAX_DIRTY_PATHS)
15963
+ };
15891
15964
  }, nextCommandFor = (brief) => {
15892
15965
  const branchFlag = brief.branch ? ` --branch ${brief.branch}` : "";
15893
15966
  const verify = `workit ledger verdict verified${branchFlag} --how "<method and evidence>" (from a session that did not author the branch, with WORKIT_SESSION_ID set)`;
@@ -77566,6 +77639,7 @@ var exports_check = {};
77566
77639
  __export(exports_check, {
77567
77640
  OUTPUT_LINES: () => OUTPUT_LINES,
77568
77641
  boundedOutput: () => boundedOutput,
77642
+ countWarnings: () => countWarnings,
77569
77643
  hostToolWaitCapMs: () => hostToolWaitCapMs,
77570
77644
  run: () => run12,
77571
77645
  workspaceResources: () => workspaceResources
@@ -77750,6 +77824,10 @@ function boundedArgv(argv) {
77750
77824
  const truncated = cut.length !== argv.length || cut.some((item, index) => item !== argv[index]);
77751
77825
  return { argv: cut, truncated };
77752
77826
  }
77827
+ function countWarnings(log) {
77828
+ return redactLog(log).replace(ANSI_COLOR, "").split(`
77829
+ `).filter((line) => WARNING_LINE.test(line) && !NO_WARNINGS.test(line)).length;
77830
+ }
77753
77831
  function boundedOutput(log, lines) {
77754
77832
  const all = tailLines(log, Number.MAX_SAFE_INTEGER);
77755
77833
  if (all.length === 0)
@@ -78006,6 +78084,7 @@ async function run12(argv, io) {
78006
78084
  };
78007
78085
  const storeDir = io.env.WORKFLOW_WORKSPACE_ROOT || top;
78008
78086
  const task = await recordInTask(io, storeDir, options.task, observation, unblockFor);
78087
+ const warnings = countWarnings(result.log);
78009
78088
  const data = {
78010
78089
  evidenceId: task.evidenceId,
78011
78090
  ledgerRowId: observation.ledgerRowId,
@@ -78026,6 +78105,7 @@ async function run12(argv, io) {
78026
78105
  logPath,
78027
78106
  logTail: tail,
78028
78107
  outputBytes: result.outputBytes,
78108
+ warnings,
78029
78109
  logTruncated: result.truncated,
78030
78110
  task: task.task,
78031
78111
  lease: heavy?.leaseReport ?? null,
@@ -78040,7 +78120,7 @@ async function run12(argv, io) {
78040
78120
  const code = !passed ? "failed" : recordError ? recordError.code : "ok";
78041
78121
  const error = !passed ? result.timedOut ? `timed out after ${options.timeoutMs} ms` : result.spawnError ? `cannot start ${command[0]}: ${result.spawnError}` : `exited ${result.exitCode}` : recordError?.message;
78042
78122
  const label = name ?? command.join(" ");
78043
- const summary = `workit check ${label}: ${passed ? "passed, exit 0" : error ?? "failed"} in ${result.durationMs} ms (${configured ? "configured" : "ad-hoc"}${name && !configured && config.checks.some((item) => item.name === name) ? `; not the configured command for "${name}"` : ""}); ${task.evidenceId ? `evidence ${task.evidenceId} on task ${task.task?.id}` : task.note ?? (task.error ? `task: ${task.error.message}` : "ledger only")}; log: ${logPath ?? "not stored"}`;
78123
+ const summary = `workit check ${label}: ${passed ? "passed, exit 0" : error ?? "failed"} in ${result.durationMs} ms (${configured ? "configured" : "ad-hoc"}${warnings ? `; warnings: ${warnings}` : ""}${name && !configured && config.checks.some((item) => item.name === name) ? `; not the configured command for "${name}"` : ""}); ${task.evidenceId ? `evidence ${task.evidenceId} on task ${task.task?.id}` : task.note ?? (task.error ? `task: ${task.error.message}` : "ledger only")}; log: ${logPath ?? "not stored"}`;
78044
78124
  if (io.json)
78045
78125
  io.stdout(`${JSON.stringify({ ok: code === "ok", code, data, ...error ? { error } : {}, ...task.stillUnsatisfied[0] ? { unblock: task.stillUnsatisfied[0].unblock } : {} })}
78046
78126
  `);
@@ -78063,7 +78143,7 @@ async function run12(argv, io) {
78063
78143
  return recordError.code === "busy" ? 4 : recordError.code === "invalid_input" ? 2 : 5;
78064
78144
  return 0;
78065
78145
  }
78066
- var USAGE5 = 'workit check <name> [--full] [--no-lease --reason "<why>"] | workit check [--name <n>] [--shell] [--heavy] [--full] [--wait <s>] [--timeout <s>] [--task <id>] [--json] -- <cmd…>', OUTPUT_LINES = 60, MAX_UNVERIFIED_RUN_MS, RECORD_TAIL_LINES = 20, RECORD_TAIL_CHARS = 200, posix = (value) => value.split(path50.sep).join("/") || ".", ENGINE_CODES2, FINGERPRINT_VARS, HOST_TOOL_WAIT_CAP_MS = 1e5;
78146
+ var USAGE5 = 'workit check <name> [--full] [--no-lease --reason "<why>"] | workit check [--name <n>] [--shell] [--heavy] [--full] [--wait <s>] [--timeout <s>] [--task <id>] [--json] -- <cmd…>', OUTPUT_LINES = 60, MAX_UNVERIFIED_RUN_MS, RECORD_TAIL_LINES = 20, RECORD_TAIL_CHARS = 200, posix = (value) => value.split(path50.sep).join("/") || ".", ENGINE_CODES2, FINGERPRINT_VARS, ANSI_COLOR, WARNING_LINE, NO_WARNINGS, HOST_TOOL_WAIT_CAP_MS = 1e5;
78067
78147
  var init_check = __esm(() => {
78068
78148
  init_check_config();
78069
78149
  init_checks3();
@@ -78081,6 +78161,9 @@ var init_check = __esm(() => {
78081
78161
  permission_denied: "blocked"
78082
78162
  };
78083
78163
  FINGERPRINT_VARS = ["CI", "NODE_ENV", "npm_config_script_shell", "NODE_OPTIONS", "SHELL"];
78164
+ ANSI_COLOR = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
78165
+ WARNING_LINE = /^\s*(?:⚠|\[(?:WARN|WARNING|warn|warning)\]|(?:npm\s+)?(?:WARN\s|warn\s)|warn(?:ing)?(?:\[[^\]]*\])?:|\d+:\d+\s+warning\s|\S+\(\d+,\d+\):\s*warning\s|\S+:\d+:\d+:\s*warning\b)/;
78166
+ NO_WARNINGS = /\b(?:no|0|zero|none)\s+warnings?\b/i;
78084
78167
  });
78085
78168
 
78086
78169
  // packages/workit-cli/src/verbs/lock.ts
@@ -80235,6 +80318,22 @@ function implicitDeleteSkip(cwd, resolved, doc, branch) {
80235
80318
  return `open PR ${children.data.map((n) => `#${n}`).join(", ")} targets ${branch} (a stack)`;
80236
80319
  return null;
80237
80320
  }
80321
+ function baseCiState(cwd, resolved, base) {
80322
+ if (!isProtectedTarget(cwd, base))
80323
+ return { ok: true, note: null };
80324
+ const read = resolved.forge.branchChecks(base);
80325
+ if (!read.ok)
80326
+ return { ok: true, note: `base ${base} CI not read (${read.error}); merging anyway` };
80327
+ const { requiredKnown } = read.data;
80328
+ const failing = read.data.checks.filter((check) => check.state === "failing" && (requiredKnown ? check.required === true : check.required !== false && check.conclusion === "failure"));
80329
+ if (failing.length)
80330
+ return { ok: false, failing, sha: read.data.sha, requiredKnown };
80331
+ const pending = read.data.checks.filter((check) => check.state === "pending");
80332
+ return {
80333
+ ok: true,
80334
+ note: pending.length ? `base ${base} CI is still running (${pending.map((check) => check.name).join(", ")}); merging without waiting` : null
80335
+ };
80336
+ }
80238
80337
  async function mergePullRequest(cwd, resolved, input, sleep) {
80239
80338
  const grant = requireGrant(cwd, "merge");
80240
80339
  if (!grant.allowed)
@@ -80244,6 +80343,9 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
80244
80343
  return failure3("invalid_input", "--unverified needs --reason <why the user asked for it>");
80245
80344
  if (bypass && !grant.allowUnverified)
80246
80345
  return refuse(`unverified_refused: workspace ${grant.workspace ? `"${grant.workspace}"` : "(none)"} grants merge: "verified", which never merges without an accepted independent verdict`, "a non-author session verifies the head (workit-review), then merge without --unverified", { reason: "unverified_refused" });
80346
+ const baseRed = input.baseRedOk ?? null;
80347
+ if (baseRed && !baseRed.reason.trim())
80348
+ return failure3("invalid_input", "--base-red-ok needs --reason <why merging onto a red base is fine>");
80247
80349
  const number = selectPr(cwd, resolved, { pr: input.pr });
80248
80350
  if (!number.ok)
80249
80351
  return number;
@@ -80328,6 +80430,15 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
80328
80430
  const lane = gate.verification === "proportional" ? `, ${laneLabel(gate)}` : "";
80329
80431
  return refuse(gate.need === "self" ? `NEEDS_VERDICT: ${label} has no verdict for ${head.slice(0, 12)}${lane}: the author's own --self verdict is enough (${check.accepted.reasons.join(", ")})` : `NEEDS_VERDICT: ${label} has no accepted independent verdict for ${head.slice(0, 12)}${lane} (${check.accepted.reasons.join(", ")})`, gate.need === "self" ? `after an observed passing \`workit check test\`, the author runs: workit ledger verdict tests-verified --self --how "<what you ran>" --branch ${branch} (or a verifier records one)${bypassHint}` : `an independent session verifies the head and runs: workit ledger verdict verified --how "<what was exercised>" --branch ${branch}${bypassHint}`, { reason: "needs_verdict", verdict: summary });
80330
80432
  }
80433
+ const baseCi = baseCiState(cwd, resolved, doc.base);
80434
+ let baseRedOverride = null;
80435
+ if (!baseCi.ok) {
80436
+ const names = baseCi.failing.map((failed) => failed.name).join(", ");
80437
+ const url = baseCi.failing.find((failed) => failed.url)?.url ?? null;
80438
+ if (!baseRed)
80439
+ return refuse(`base_red: ${doc.base} at ${baseCi.sha?.slice(0, 12) ?? "its head"} has failing CI (${names}${baseCi.requiredKnown ? "" : "; the required checks could not be read, so every failed check counts"})${url ? `: ${url}` : ""}; merging ${label} onto it would land on a red base`, `fix ${doc.base} first, or, if the user accepts it: workit pr merge${input.pr ? ` --pr ${doc.number}` : ""} --base-red-ok --reason "<why>"`, { reason: "base_red", blockers: baseCi.failing.map((failed) => failed.name) });
80440
+ baseRedOverride = baseRed;
80441
+ }
80331
80442
  const squashMessage = squashMessageFor(cwd, resolved, input, doc);
80332
80443
  if (squashMessage && !squashMessage.ok)
80333
80444
  return squashMessage;
@@ -80343,6 +80454,7 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
80343
80454
  review: verdict.review,
80344
80455
  lane: verdict.lane,
80345
80456
  ...unverified ? { unverified: unverified.recorded } : {},
80457
+ ...baseRedOverride ? { baseRedOk: baseRedOverride.reason } : {},
80346
80458
  grant: grant.source
80347
80459
  });
80348
80460
  if (!intent.ok)
@@ -80371,6 +80483,7 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
80371
80483
  review: verdict.review,
80372
80484
  lane: verdict.lane,
80373
80485
  ...unverified ? { unverified: unverified.recorded } : {},
80486
+ ...baseRedOverride ? { baseRedOk: baseRedOverride.reason } : {},
80374
80487
  grant: grant.source,
80375
80488
  intent: intentId,
80376
80489
  verified: false
@@ -80427,6 +80540,10 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
80427
80540
  mergeSha: merged.data.mergeSha,
80428
80541
  verdict,
80429
80542
  unverified,
80543
+ baseCi: {
80544
+ note: baseRedOverride ? `base ${doc.base} CI is red; merged anyway (--base-red-ok: ${baseRedOverride.reason})` : baseCi.ok ? baseCi.note : null,
80545
+ override: baseRedOverride
80546
+ },
80430
80547
  grant: { source: grant.source },
80431
80548
  deletedBranch,
80432
80549
  recorded,
@@ -80535,12 +80652,22 @@ function gitRestack(cwd, input) {
80535
80652
  if (!dir) {
80536
80653
  const scratch = scratchDir(cwd);
80537
80654
  if (!scratch)
80538
- return { ok: false, conflict: false, worktree: cwd, error: "no store root" };
80655
+ return {
80656
+ ok: false,
80657
+ conflict: false,
80658
+ worktree: cwd,
80659
+ error: "no store root"
80660
+ };
80539
80661
  fs28.mkdirSync(scratch, { recursive: true });
80540
80662
  dir = path56.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes4(4).toString("hex")}`);
80541
80663
  const added = gitRun3(cwd, ["worktree", "add", "-q", dir, input.branch], GIT_TIMEOUTS.worktree);
80542
80664
  if (!added.ok)
80543
- return { ok: false, conflict: false, worktree: cwd, error: firstLine(added) };
80665
+ return {
80666
+ ok: false,
80667
+ conflict: false,
80668
+ worktree: cwd,
80669
+ error: firstLine(added)
80670
+ };
80544
80671
  }
80545
80672
  const run = gitRun3(dir, [
80546
80673
  "-c",
@@ -80563,7 +80690,12 @@ function gitRestack(cwd, input) {
80563
80690
  error: `git rebase did not finish within ${stackDeps.rebaseTimeoutMs} ms`
80564
80691
  };
80565
80692
  if (rebaseInProgress(dir))
80566
- return { ok: false, conflict: true, worktree: dir, error: firstLine(run) };
80693
+ return {
80694
+ ok: false,
80695
+ conflict: true,
80696
+ worktree: dir,
80697
+ error: firstLine(run)
80698
+ };
80567
80699
  if (!owner)
80568
80700
  gitRun3(cwd, ["worktree", "remove", "--force", dir]);
80569
80701
  return { ok: false, conflict: false, worktree: dir, error: firstLine(run) };
@@ -80571,7 +80703,12 @@ function gitRestack(cwd, input) {
80571
80703
  if (!owner)
80572
80704
  gitRun3(cwd, ["worktree", "remove", "--force", dir]);
80573
80705
  const head = branchTip(cwd, input.branch);
80574
- return head ? { ok: true, head } : { ok: false, conflict: false, worktree: dir, error: `${input.branch} vanished` };
80706
+ return head ? { ok: true, head } : {
80707
+ ok: false,
80708
+ conflict: false,
80709
+ worktree: dir,
80710
+ error: `${input.branch} vanished`
80711
+ };
80575
80712
  }
80576
80713
  function pruneScratch(cwd) {
80577
80714
  const scratch = scratchDir(cwd);
@@ -80696,7 +80833,10 @@ function listStacks(cwd) {
80696
80833
  if (parsed && fileFor(parsed.name) === name)
80697
80834
  out.push(parsed);
80698
80835
  }
80699
- return { ok: true, data: out.toSorted((a, b) => a.name.localeCompare(b.name)) };
80836
+ return {
80837
+ ok: true,
80838
+ data: out.toSorted((a, b) => a.name.localeCompare(b.name))
80839
+ };
80700
80840
  }
80701
80841
  function selectStack(cwd, name) {
80702
80842
  if (name !== null && !validName(name))
@@ -81041,7 +81181,10 @@ function statusDoc(ctx, pr) {
81041
81181
  const status = ctx.resolved.forge.prStatus(pr);
81042
81182
  if (!status.ok)
81043
81183
  return status;
81044
- return buildStatusDoc(ctx.cwd, ctx.resolved, status.data, { logLines: 0, behind: false });
81184
+ return buildStatusDoc(ctx.cwd, ctx.resolved, status.data, {
81185
+ logLines: 0,
81186
+ behind: false
81187
+ });
81045
81188
  }
81046
81189
  function prBinding(ctx, entry, doc) {
81047
81190
  const label = noun2(ctx.resolved, entry.pr);
@@ -81061,7 +81204,10 @@ function prBinding(ctx, entry, doc) {
81061
81204
  function repoMismatch(ctx, stack) {
81062
81205
  const { kind, repo } = ctx.resolved.forge;
81063
81206
  if (stack.repo && stack.repo !== repo || stack.forge && stack.forge !== kind)
81064
- return stackFail("blocked", `repo_mismatch: stack ${stack.name} was planned on ${stack.forge ?? "?"} ${stack.repo ?? "?"}, but this checkout resolves to ${kind} ${repo}`, `workit stack plan --name ${stack.name} <branch…> # re-plan it here, or run from the right checkout`, { planned: { forge: stack.forge, repo: stack.repo }, resolved: { forge: kind, repo } });
81207
+ return stackFail("blocked", `repo_mismatch: stack ${stack.name} was planned on ${stack.forge ?? "?"} ${stack.repo ?? "?"}, but this checkout resolves to ${kind} ${repo}`, `workit stack plan --name ${stack.name} <branch…> # re-plan it here, or run from the right checkout`, {
81208
+ planned: { forge: stack.forge, repo: stack.repo },
81209
+ resolved: { forge: kind, repo }
81210
+ });
81065
81211
  return null;
81066
81212
  }
81067
81213
  function qualify(ctx, stack, entry, doc, verdict, grant, binding = null, bypass = false) {
@@ -81202,7 +81348,11 @@ function detectMerged(ctx, cwd, stack, trunkSha, state) {
81202
81348
  merged = tip !== null && hasOwnCommits(cwd, tip, entry.lastParentHead) && isAncestor2(cwd, tip, trunkSha);
81203
81349
  }
81204
81350
  if (merged) {
81205
- entry.merged = { pr: entry.pr, mergeSha: null, at: new Date().toISOString() };
81351
+ entry.merged = {
81352
+ pr: entry.pr,
81353
+ mergeSha: null,
81354
+ at: new Date().toISOString()
81355
+ };
81206
81356
  newly.push({ branch: entry.branch, pr: entry.pr });
81207
81357
  }
81208
81358
  }
@@ -81216,18 +81366,31 @@ function pushBranch(cwd, actor, branch, priorTip) {
81216
81366
  if (!remote.ok)
81217
81367
  return stackFail(remote.code, remote.error);
81218
81368
  if (remote.sha === plan.data.sha)
81219
- return { ok: true, data: { pushed: false, forced: false, sha: plan.data.sha } };
81369
+ return {
81370
+ ok: true,
81371
+ data: { pushed: false, forced: false, sha: plan.data.sha }
81372
+ };
81220
81373
  const grant = requireGrant(cwd, "push", { forge: !plan.data.local });
81221
81374
  if (!grant.allowed)
81222
- return stackFail("blocked", grant.error, grant.unblock, { reason: grant.reason });
81375
+ return stackFail("blocked", grant.error, grant.unblock, {
81376
+ reason: grant.reason
81377
+ });
81223
81378
  const force = remote.sha !== null && !isAncestor2(cwd, remote.sha, plan.data.sha);
81224
81379
  const expect = !force ? null : remote.sha === priorTip ? priorTip : recordedRemoteTip(cwd, plan.data) ?? null;
81225
- const pushed = executePush(cwd, plan.data, { forceWithLease: force, expect, actor });
81380
+ const pushed = executePush(cwd, plan.data, {
81381
+ forceWithLease: force,
81382
+ expect,
81383
+ actor
81384
+ });
81226
81385
  if (!pushed.ok)
81227
81386
  return { ...fromForge(pushed), data: { branch } };
81228
81387
  return {
81229
81388
  ok: true,
81230
- data: { pushed: pushed.data.pushed, forced: pushed.data.forced, sha: plan.data.sha }
81389
+ data: {
81390
+ pushed: pushed.data.pushed,
81391
+ forced: pushed.data.forced,
81392
+ sha: plan.data.sha
81393
+ }
81231
81394
  };
81232
81395
  }
81233
81396
  function syncStack(cwd, ctx, stack, options, actor) {
@@ -81337,7 +81500,11 @@ function syncStack(cwd, ctx, stack, options, actor) {
81337
81500
  worktree: rebased.worktree,
81338
81501
  progress: outcome
81339
81502
  });
81340
- return rebased.conflict ? stackFail("blocked", `conflict restacking ${entry.branch} onto ${parent.name}: ${rebased.error}`, `resolve the conflicts in ${rebased.worktree}, git rebase --continue, then workit stack sync # or git rebase --abort`, { conflict: entry.branch, worktree: rebased.worktree, progress: outcome }) : stackFail("failed", `restacking ${entry.branch} failed: ${rebased.error}`, undefined, {
81503
+ return rebased.conflict ? stackFail("blocked", `conflict restacking ${entry.branch} onto ${parent.name}: ${rebased.error}`, `resolve the conflicts in ${rebased.worktree}, git rebase --continue, then workit stack sync # or git rebase --abort`, {
81504
+ conflict: entry.branch,
81505
+ worktree: rebased.worktree,
81506
+ progress: outcome
81507
+ }) : stackFail("failed", `restacking ${entry.branch} failed: ${rebased.error}`, undefined, {
81341
81508
  branch: entry.branch,
81342
81509
  progress: outcome
81343
81510
  });
@@ -81397,8 +81564,16 @@ function syncStack(cwd, ctx, stack, options, actor) {
81397
81564
  p: patchId(cwd, fromBase, fromHead),
81398
81565
  d: diffHash(cwd, fromBase, fromHead)
81399
81566
  };
81400
- const after = { p: patchId(cwd, parentTip, newTip), d: diffHash(cwd, parentTip, newTip) };
81401
- const equal = before.d !== null && before.d === after.d && before.p !== null && before.p === after.p && sameOwnChange(cwd, { from: fromHead, to: newTip, fromBase, toBase: parentTip });
81567
+ const after = {
81568
+ p: patchId(cwd, parentTip, newTip),
81569
+ d: diffHash(cwd, parentTip, newTip)
81570
+ };
81571
+ const equal = before.d !== null && before.d === after.d && before.p !== null && before.p === after.p && sameOwnChange(cwd, {
81572
+ from: fromHead,
81573
+ to: newTip,
81574
+ fromBase,
81575
+ toBase: parentTip
81576
+ });
81402
81577
  appendObserved(cwd, {
81403
81578
  type: "stack.restacked",
81404
81579
  actor,
@@ -81416,7 +81591,12 @@ function syncStack(cwd, ctx, stack, options, actor) {
81416
81591
  if (step.restack)
81417
81592
  step.restack.carried = equal;
81418
81593
  else
81419
- step.restack = { from: fromBase, onto: parentTip, head: newTip, carried: equal };
81594
+ step.restack = {
81595
+ from: fromBase,
81596
+ onto: parentTip,
81597
+ head: newTip,
81598
+ carried: equal
81599
+ };
81420
81600
  }
81421
81601
  if (options.publish) {
81422
81602
  touchStackLock();
@@ -81445,7 +81625,10 @@ function syncStack(cwd, ctx, stack, options, actor) {
81445
81625
  });
81446
81626
  const updated = ctx.resolved.forge.updateBase(entry.pr, parent.name);
81447
81627
  if (!updated.ok)
81448
- return { ...fromForge(updated), data: { branch: entry.branch, progress: outcome } };
81628
+ return {
81629
+ ...fromForge(updated),
81630
+ data: { branch: entry.branch, progress: outcome }
81631
+ };
81449
81632
  appendObserved(cwd, {
81450
81633
  type: "pr.retargeted",
81451
81634
  actor,
@@ -81520,7 +81703,9 @@ async function landStack(ctx, stack, options) {
81520
81703
  if (bypass) {
81521
81704
  const grant = requireGrant(ctx.cwd, "merge");
81522
81705
  if (!grant.allowed)
81523
- return failWith(stackFail("blocked", grant.error, grant.unblock, { reason: "grant_required" }));
81706
+ return failWith(stackFail("blocked", grant.error, grant.unblock, {
81707
+ reason: "grant_required"
81708
+ }));
81524
81709
  if (!grant.allowUnverified)
81525
81710
  return failWith(stackFail("blocked", `unverified_refused: workspace ${grant.workspace ? `"${grant.workspace}"` : "(none)"} grants merge: "verified", which never lands without an accepted independent verdict`, "a non-author session verifies each head (workit-review), then land without --unverified", { reason: "unverified_refused" }));
81526
81711
  }
@@ -81593,7 +81778,8 @@ async function landStack(ctx, stack, options) {
81593
81778
  deleteBranch: false,
81594
81779
  actor: ctx.actor,
81595
81780
  expect: { base: stack.trunk, branch: entry.branch },
81596
- ...bypass && !verdict.satisfied ? { unverified: bypass } : {}
81781
+ ...bypass && !verdict.satisfied ? { unverified: bypass } : {},
81782
+ ...options.baseRedOk ? { baseRedOk: options.baseRedOk } : {}
81597
81783
  }, ctx.sleep);
81598
81784
  if (!merged.ok) {
81599
81785
  if (merged.code === "blocked" && "refusal" in merged && merged.refusal) {
@@ -81615,7 +81801,11 @@ async function landStack(ctx, stack, options) {
81615
81801
  mergeSha: merged.data.mergeSha,
81616
81802
  unverified: merged.data.unverified?.recorded ?? null
81617
81803
  });
81618
- entry.merged = { pr, mergeSha: merged.data.mergeSha, at: new Date().toISOString() };
81804
+ entry.merged = {
81805
+ pr,
81806
+ mergeSha: merged.data.mergeSha,
81807
+ at: new Date().toISOString()
81808
+ };
81619
81809
  const written = writeStack(ctx.cwd, stack);
81620
81810
  if (!written.ok)
81621
81811
  return failWith(written);
@@ -82982,6 +83172,30 @@ function createGitHubForge(options) {
82982
83172
  };
82983
83173
  return success2(status);
82984
83174
  },
83175
+ branchChecks(branch) {
83176
+ const contexts = [];
83177
+ let sha = null;
83178
+ let cursor = null;
83179
+ for (let page = 1;page <= maxPages; page += 1) {
83180
+ const data = graphql(BRANCH_CHECKS_QUERY, { owner, name, ref: `refs/heads/${branch}`, contexts: cursor }, `checks on ${branch}`);
83181
+ if (!data.ok)
83182
+ return data;
83183
+ const commit = data.data.repository?.object ?? null;
83184
+ if (!commit)
83185
+ return failure3("not_found", `branch ${branch} was not found`);
83186
+ sha = commit.oid ?? sha;
83187
+ const rollup = commit.statusCheckRollup;
83188
+ if (!rollup)
83189
+ break;
83190
+ contexts.push(...rollup.contexts.nodes);
83191
+ if (!rollup.contexts.pageInfo.hasNextPage)
83192
+ break;
83193
+ cursor = rollup.contexts.pageInfo.endCursor;
83194
+ }
83195
+ const required = requiredContexts(branch);
83196
+ const checks = latestPerName(contexts.map(checkFromContext).filter((check) => check !== null)).map((check) => required ? { ...check, required: required.includes(check.context) } : check);
83197
+ return success2({ sha, checks, requiredKnown: required !== null });
83198
+ },
82985
83199
  jobLogTail(job, lines) {
82986
83200
  const text = apiText(runner, "gh", apiHost, ["api", `repos/${repo}/actions/jobs/${job.jobId}/logs`], `job ${job.jobId} log`, FORGE_TIMEOUTS.log);
82987
83201
  return text.ok ? success2(logTail(text.data, lines)) : text;
@@ -83179,7 +83393,7 @@ function createGitHubForge(options) {
83179
83393
  }
83180
83394
  };
83181
83395
  }
83182
- var GITHUB_MAX_PAGES = 10, PR_FIELDS = `number url state isDraft isInMergeQueue mergeable mergeStateStatus baseRefName baseRefOid baseRef { target { oid } } headRefName headRefOid reviewDecision`, THREADS = `reviewThreads(first: 100, after: $threads) { pageInfo { hasNextPage endCursor } nodes { id isResolved isOutdated path line comments(first: 1) { nodes { author { login __typename } body url } } } }`, CONTEXTS = `contexts(first: 100, after: $contexts) { pageInfo { hasNextPage endCursor } nodes { __typename ... on CheckRun { name status conclusion detailsUrl databaseId isRequired(pullRequestNumber: $number) checkSuite { workflowRun { databaseId workflow { name } } } } ... on StatusContext { context state targetUrl isRequired(pullRequestNumber: $number) } } }`, PR_STATUS_QUERY, FIND_PR_QUERY = `query($owner: String!, $name: String!, $head: String!) { repository(owner: $owner, name: $name) { pullRequests(headRefName: $head, first: 20, orderBy: { field: CREATED_AT, direction: DESC }) { nodes { number url state baseRefName headRefName headRefOid headRepositoryOwner { login } } } } }`, READY_MUTATION = `mutation($id: ID!) { markPullRequestReadyForReview(input: { pullRequestId: $id }) { pullRequest { isDraft } } }`, DRAFT_MUTATION = `mutation($id: ID!) { convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { isDraft } } }`, REPLY_MUTATION = `mutation($thread: ID!, $body: String!) { addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $thread, body: $body }) { comment { url } } }`, RESOLVE_MUTATION = `mutation($thread: ID!) { resolveReviewThread(input: { threadId: $thread }) { thread { isResolved } } }`, THREAD_QUERY = `query($thread: ID!) { node(id: $thread) { ... on PullRequestReviewThread { isResolved pullRequest { number } } } }`, prState = (value) => value === "MERGED" ? "merged" : value === "CLOSED" ? "closed" : "open", FAILING2, safeUrl = (value) => value ? redactText(value) : null, encodeRef = (ref) => ref.split("/").map(encodeURIComponent).join("/");
83396
+ var GITHUB_MAX_PAGES = 10, PR_FIELDS = `number url state isDraft isInMergeQueue mergeable mergeStateStatus baseRefName baseRefOid baseRef { target { oid } } headRefName headRefOid reviewDecision`, THREADS = `reviewThreads(first: 100, after: $threads) { pageInfo { hasNextPage endCursor } nodes { id isResolved isOutdated path line comments(first: 1) { nodes { author { login __typename } body url } } } }`, CONTEXTS = `contexts(first: 100, after: $contexts) { pageInfo { hasNextPage endCursor } nodes { __typename ... on CheckRun { name status conclusion detailsUrl databaseId isRequired(pullRequestNumber: $number) checkSuite { workflowRun { databaseId workflow { name } } } } ... on StatusContext { context state targetUrl isRequired(pullRequestNumber: $number) } } }`, PR_STATUS_QUERY, FIND_PR_QUERY = `query($owner: String!, $name: String!, $head: String!) { repository(owner: $owner, name: $name) { pullRequests(headRefName: $head, first: 20, orderBy: { field: CREATED_AT, direction: DESC }) { nodes { number url state baseRefName headRefName headRefOid headRepositoryOwner { login } } } } }`, BRANCH_CHECKS_QUERY = `query($owner: String!, $name: String!, $ref: String!, $contexts: String) { repository(owner: $owner, name: $name) { object(expression: $ref) { ... on Commit { oid statusCheckRollup { contexts(first: 100, after: $contexts) { pageInfo { hasNextPage endCursor } nodes { __typename ... on CheckRun { name status conclusion detailsUrl databaseId checkSuite { workflowRun { databaseId workflow { name } } } } ... on StatusContext { context state targetUrl } } } } } } } }`, READY_MUTATION = `mutation($id: ID!) { markPullRequestReadyForReview(input: { pullRequestId: $id }) { pullRequest { isDraft } } }`, DRAFT_MUTATION = `mutation($id: ID!) { convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { isDraft } } }`, REPLY_MUTATION = `mutation($thread: ID!, $body: String!) { addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $thread, body: $body }) { comment { url } } }`, RESOLVE_MUTATION = `mutation($thread: ID!) { resolveReviewThread(input: { threadId: $thread }) { thread { isResolved } } }`, THREAD_QUERY = `query($thread: ID!) { node(id: $thread) { ... on PullRequestReviewThread { isResolved pullRequest { number } } } }`, prState = (value) => value === "MERGED" ? "merged" : value === "CLOSED" ? "closed" : "open", FAILING2, safeUrl = (value) => value ? redactText(value) : null, encodeRef = (ref) => ref.split("/").map(encodeURIComponent).join("/");
83183
83397
  var init_github = __esm(() => {
83184
83398
  init_exec();
83185
83399
  init_redact();
@@ -83380,6 +83594,35 @@ function createGitLabForge(options) {
83380
83594
  };
83381
83595
  return success2(status);
83382
83596
  },
83597
+ branchChecks(branch) {
83598
+ const tip = baseTip(branch);
83599
+ if (!tip)
83600
+ return failure3("not_found", `branch ${branch} was not found`);
83601
+ const latest = apiJson(runner, "glab", apiHost, api(`${project}/pipelines?ref=${encodeURIComponent(branch)}&sha=${tip}&order_by=id&sort=desc&per_page=1`), `pipelines on ${branch}`);
83602
+ if (!latest.ok)
83603
+ return latest;
83604
+ const pipeline = Array.isArray(latest.data) ? latest.data[0] : undefined;
83605
+ if (!pipeline)
83606
+ return success2({ sha: tip, checks: [], requiredKnown: true });
83607
+ const scope = String(pipeline.project_id ?? encodeURIComponent(repo));
83608
+ const jobs = pages(`projects/${scope}/pipelines/${pipeline.id}/jobs`, `pipeline ${pipeline.id} jobs`);
83609
+ if (!jobs.ok)
83610
+ return jobs;
83611
+ const bridges = pages(`projects/${scope}/pipelines/${pipeline.id}/bridges`, `pipeline ${pipeline.id} bridges`);
83612
+ if (!bridges.ok)
83613
+ return bridges;
83614
+ const checks = [
83615
+ ...jobs.data.items.map((job) => checkFromJob(job, pipeline.id, scope)),
83616
+ ...bridges.data.items.map((bridge) => checkFromBridge(bridge, pipeline.id, scope))
83617
+ ];
83618
+ if (stateOf(pipeline.status, false) === "failing" && !checks.some((check) => check.state === "failing"))
83619
+ checks.push(pipelineCheck("failing", pipeline.status, {
83620
+ id: pipeline.id,
83621
+ project_id: pipeline.project_id ?? 0,
83622
+ web_url: pipeline.web_url
83623
+ }));
83624
+ return success2({ sha: tip, checks, requiredKnown: true });
83625
+ },
83383
83626
  jobLogTail(job, lines) {
83384
83627
  const scope = job.scope ?? encodeURIComponent(repo);
83385
83628
  const text = apiText(runner, "glab", apiHost, ["api", `projects/${scope}/jobs/${job.jobId}/trace`], `job ${job.jobId} trace`, FORGE_TIMEOUTS.log);
@@ -85487,6 +85730,7 @@ async function merge3(argv, io) {
85487
85730
  "delete-branch": "boolean",
85488
85731
  "keep-branch": "boolean",
85489
85732
  unverified: "boolean",
85733
+ "base-red-ok": "boolean",
85490
85734
  reason: "value",
85491
85735
  subject: "value",
85492
85736
  body: "value",
@@ -85494,7 +85738,11 @@ async function merge3(argv, io) {
85494
85738
  });
85495
85739
  if (typeof flags === "string")
85496
85740
  return usage4(io, flags, MERGE_USAGE);
85497
- const unverified = unverifiedFlag(flags);
85741
+ const baseRedOk = flags.booleans.has("base-red-ok");
85742
+ const baseRedReason = flags.values.reason?.trim();
85743
+ if (baseRedOk && !baseRedReason)
85744
+ return usage4(io, "--base-red-ok needs --reason <why merging onto a red base is fine>", MERGE_USAGE);
85745
+ const unverified = baseRedOk && !flags.booleans.has("unverified") ? null : unverifiedFlag(flags);
85498
85746
  if (typeof unverified === "string")
85499
85747
  return usage4(io, unverified, MERGE_USAGE);
85500
85748
  if (flags.positionals.length)
@@ -85536,6 +85784,7 @@ async function merge3(argv, io) {
85536
85784
  implicitDelete: !flags.booleans.has("keep-branch") && workspaceDeleteBranch(io.cwd),
85537
85785
  actor: actorFromEnv(io.env),
85538
85786
  ...unverified ? { unverified } : {},
85787
+ ...baseRedOk && baseRedReason ? { baseRedOk: { reason: baseRedReason } } : {},
85539
85788
  ...subject !== undefined ? { subject } : {},
85540
85789
  ...body !== undefined ? { body } : {}
85541
85790
  }, forgeDeps.sleep);
@@ -85548,6 +85797,7 @@ async function merge3(argv, io) {
85548
85797
  return emit(io, ok2({ ...result.data, localCleanup: cleanup }), (data) => [
85549
85798
  `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)}` : ""}`,
85550
85799
  data.unverified ? `UNVERIFIED: merged without a verdict (--unverified: ${data.unverified.reason}); ledger ${data.unverified.recorded}` : `verdict ${data.verdict.verdictId ?? "?"} accepted for that head${data.verdict.cover ? ` (stack verdict on ${data.verdict.cover.branch} @ ${data.verdict.cover.head.slice(0, 12)})` : data.verdict.carry ? ` (${carryLabel(data.verdict.carry)})` : ""}`,
85800
+ ...data.baseCi.note ? [data.baseCi.note] : [],
85551
85801
  ...data.mergeBack.length ? [
85552
85802
  `merge back: ${data.base} is the ${data.mergeBackTrack} production branch; bring it into ${data.mergeBack.join(", ")}`
85553
85803
  ] : [],
@@ -85714,7 +85964,7 @@ async function run15(argv, io) {
85714
85964
  return reply(rest, io);
85715
85965
  return usage4(io, sub && !sub.startsWith("-") ? `unknown pr subcommand "${sub}"` : "missing pr subcommand", USAGE8);
85716
85966
  }
85717
- 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) [--issue <#n|ID|url>]… [--refs] [--label <l>]… [--reviewer <login>]… [--draft] [--json]", MERGE_USAGE = "workit pr merge [--pr <n>] [--squash|--merge|--rebase | --method squash|merge|rebase] [--subject <s>] [--body <b> | --body-file <f|->] [--delete-branch | --keep-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]", USAGE8 = "workit pr status|create|merge|ready|edit|threads|reply ... (workit help pr)", prLabel = (kind, number) => `${kind === "github" ? "PR #" : "MR !"}${number}`, NEED_TEXT, mergedVerdictLine = (landed) => landed === "untracked" ? "merged outside workit (no merge record)" : landed.unverified ? `merged unverified at ${(landed.head ?? "?").slice(0, 12)} (recorded bypass)` : landed.lane === "none" && !landed.verdictId ? `merged at ${(landed.head ?? "?").slice(0, 12)} with no verdict (lane none)` : landed.review === "self-reviewed" ? `merged self-reviewed (lane self) with the author's verdict ${landed.verdictId ?? "?"} at ${(landed.head ?? "?").slice(0, 12)}` : `merged with accepted verdict ${landed.verdictId ?? "?"} at ${(landed.head ?? "?").slice(0, 12)}${landed.intentOnly ? " (merge intent recorded; the merge call itself failed)" : ""}`, acceptedHow = (verdict) => verdict.cover ? `stack verdict ${verdict.cover.verdictId} on ${verdict.cover.branch} @ ${verdict.cover.head.slice(0, 12)}` : verdict.carry ? carryLabel(verdict.carry) : verdict.basis, METHODS2;
85967
+ 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) [--issue <#n|ID|url>]… [--refs] [--label <l>]… [--reviewer <login>]… [--draft] [--json]", MERGE_USAGE = "workit pr merge [--pr <n>] [--squash|--merge|--rebase | --method squash|merge|rebase] [--subject <s>] [--body <b> | --body-file <f|->] [--delete-branch | --keep-branch] [--unverified --reason <why>] [--base-red-ok --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]", USAGE8 = "workit pr status|create|merge|ready|edit|threads|reply ... (workit help pr)", prLabel = (kind, number) => `${kind === "github" ? "PR #" : "MR !"}${number}`, NEED_TEXT, mergedVerdictLine = (landed) => landed === "untracked" ? "merged outside workit (no merge record)" : landed.unverified ? `merged unverified at ${(landed.head ?? "?").slice(0, 12)} (recorded bypass)` : landed.lane === "none" && !landed.verdictId ? `merged at ${(landed.head ?? "?").slice(0, 12)} with no verdict (lane none)` : landed.review === "self-reviewed" ? `merged self-reviewed (lane self) with the author's verdict ${landed.verdictId ?? "?"} at ${(landed.head ?? "?").slice(0, 12)}` : `merged with accepted verdict ${landed.verdictId ?? "?"} at ${(landed.head ?? "?").slice(0, 12)}${landed.intentOnly ? " (merge intent recorded; the merge call itself failed)" : ""}`, acceptedHow = (verdict) => verdict.cover ? `stack verdict ${verdict.cover.verdictId} on ${verdict.cover.branch} @ ${verdict.cover.head.slice(0, 12)}` : verdict.carry ? carryLabel(verdict.carry) : verdict.basis, METHODS2;
85718
85968
  var init_pr = __esm(() => {
85719
85969
  init_autonomy();
85720
85970
  init_issue_link();
@@ -87317,7 +87567,11 @@ __export(exports_stack, {
87317
87567
  run: () => run20
87318
87568
  });
87319
87569
  async function plan(argv, io) {
87320
- const flags = parseFlags(argv, { name: "value", trunk: "value", track: "value" });
87570
+ const flags = parseFlags(argv, {
87571
+ name: "value",
87572
+ trunk: "value",
87573
+ track: "value"
87574
+ });
87321
87575
  if (typeof flags === "string")
87322
87576
  return usage4(io, flags, PLAN_USAGE);
87323
87577
  const name = flags.values.name ?? null;
@@ -87378,7 +87632,12 @@ async function status3(argv, io) {
87378
87632
  const connected = connect(io, selected.data.branches[0]?.branch ?? null);
87379
87633
  if (!connected.ok)
87380
87634
  return forgeFail(io, connected);
87381
- const result = stackStatus({ cwd: io.cwd, resolved: connected.data, actor: actorFromEnv(io.env), sleep: forgeDeps.sleep }, selected.data);
87635
+ const result = stackStatus({
87636
+ cwd: io.cwd,
87637
+ resolved: connected.data,
87638
+ actor: actorFromEnv(io.env),
87639
+ sleep: forgeDeps.sleep
87640
+ }, selected.data);
87382
87641
  if (!result.ok)
87383
87642
  return stackFailed(io, result);
87384
87643
  return emit(io, ok2(result.data), (data) => [
@@ -87442,11 +87701,16 @@ async function land(argv, io) {
87442
87701
  timeout: "value",
87443
87702
  interval: "value",
87444
87703
  unverified: "boolean",
87704
+ "base-red-ok": "boolean",
87445
87705
  reason: "value"
87446
87706
  });
87447
87707
  if (typeof flags === "string")
87448
87708
  return usage4(io, flags, LAND_USAGE);
87449
- const unverified = unverifiedFlag(flags);
87709
+ const baseRedOk = flags.booleans.has("base-red-ok");
87710
+ const baseRedReason = flags.values.reason?.trim();
87711
+ if (baseRedOk && !baseRedReason)
87712
+ return usage4(io, "--base-red-ok needs --reason <why merging onto a red base is fine>", LAND_USAGE);
87713
+ const unverified = baseRedOk && !flags.booleans.has("unverified") ? null : unverifiedFlag(flags);
87450
87714
  if (typeof unverified === "string")
87451
87715
  return usage4(io, unverified, LAND_USAGE);
87452
87716
  if (flags.positionals.length)
@@ -87479,7 +87743,15 @@ async function land(argv, io) {
87479
87743
  resolved: connected.data,
87480
87744
  actor: actorFromEnv(io.env),
87481
87745
  sleep: forgeDeps.sleep
87482
- }, fresh.data ?? selected.data, { dryRun: flags.booleans.has("dry-run"), max, method, timeoutMs, intervalMs, unverified });
87746
+ }, fresh.data ?? selected.data, {
87747
+ dryRun: flags.booleans.has("dry-run"),
87748
+ max,
87749
+ method,
87750
+ timeoutMs,
87751
+ intervalMs,
87752
+ unverified,
87753
+ baseRedOk: baseRedOk && baseRedReason ? { reason: baseRedReason } : null
87754
+ });
87483
87755
  });
87484
87756
  if (!result.ok)
87485
87757
  return stackFailed(io, result);
@@ -87508,7 +87780,10 @@ async function run20(argv, io) {
87508
87780
  return land(rest, io);
87509
87781
  return usage4(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE12);
87510
87782
  }
87511
- 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]", USAGE12 = "workit stack plan|status|sync|land ... (workit help stack)", short5 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail2(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), label3 = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
87783
+ 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>] [--base-red-ok --reason <why>] [--timeout 20m] [--interval 30s] [--json]", USAGE12 = "workit stack plan|status|sync|land ... (workit help stack)", short5 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail2(result.code, result.error, {
87784
+ unblock: result.unblock,
87785
+ data: result.data ?? {}
87786
+ })), label3 = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
87512
87787
  `${data.dryRun ? "would sync" : "synced"} stack ${data.name}${data.merged.length ? `; merged: ${data.merged.map((item) => item.branch).join(", ")}` : ""}`,
87513
87788
  ...data.steps.map((step) => [
87514
87789
  ` ${step.branch} on ${step.parent}:`,
@@ -105920,19 +106195,22 @@ function bounded(brief, cwd) {
105920
106195
  file = path73.join(dir, `${(brief.branch ?? "detached").replaceAll(/[^\w.-]+/gu, "-")}.txt`);
105921
106196
  fs37.writeFileSync(file, `${[...body, ...tail].join(`
105922
106197
  `)}
105923
- `, { mode: 384 });
106198
+ `, {
106199
+ mode: 384
106200
+ });
105924
106201
  } catch {
105925
106202
  file = null;
105926
106203
  }
105927
106204
  const pointer = `… cut to ${MAX_BRIEF_BYTES} bytes; full brief: ${file ?? "not written (workit handoff --json has it all)"}`;
106205
+ const cappedTail = tail.map(capLine);
105928
106206
  const kept = [];
105929
106207
  for (const line of body) {
105930
106208
  const cut = line.length > 400 ? `${line.slice(0, 400)}…` : line;
105931
- if (bytes([...kept, cut, pointer, ...tail]) > MAX_BRIEF_BYTES)
106209
+ if (bytes([...kept, cut, pointer, ...cappedTail]) > MAX_BRIEF_BYTES)
105932
106210
  break;
105933
106211
  kept.push(cut);
105934
106212
  }
105935
- return [...kept, pointer, ...tail];
106213
+ return [...kept, pointer, ...cappedTail];
105936
106214
  }
105937
106215
  async function resumeBrief(argv, io) {
105938
106216
  let values;
@@ -105954,7 +106232,9 @@ async function resumeBrief(argv, io) {
105954
106232
  }
105955
106233
  const last = values.last === undefined ? 10 : Number(values.last);
105956
106234
  if (!Number.isInteger(last) || last < 1)
105957
- return emit(io, fail2("invalid_input", "--last must be a positive integer", { unblock: USAGE19 }));
106235
+ return emit(io, fail2("invalid_input", "--last must be a positive integer", {
106236
+ unblock: USAGE19
106237
+ }));
105958
106238
  const built = buildHandoff(io.cwd, {
105959
106239
  last,
105960
106240
  note: values.note ?? null,
@@ -105980,7 +106260,7 @@ async function run27(argv, io) {
105980
106260
  }
105981
106261
  var USAGE19 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", MAX_BRIEF_BYTES = 4096, short7 = (sha) => sha ? sha.slice(0, 12) : "no commit", bytes = (lines) => Buffer.byteLength(`${lines.join(`
105982
106262
  `)}
105983
- `);
106263
+ `), TAIL_LINE_BYTES = 480, capLine = (line) => Buffer.byteLength(line) <= TAIL_LINE_BYTES ? line : `${Buffer.from(line).subarray(0, TAIL_LINE_BYTES).toString().replace(/\uFFFD$/u, "")}… (cut)`;
105984
106264
  var init_handoff = __esm(() => {
105985
106265
  init_ledger();
105986
106266
  init_output();
@@ -106020,13 +106300,19 @@ var FAMILY_ACTIONS = {
106020
106300
  },
106021
106301
  evidence: { record: "Record evidence against a task's requirements" },
106022
106302
  finding: { record: "Record a finding", resolve: "Resolve a finding" },
106023
- decision: { record: "Record a task decision", revoke: "Revoke a task decision" },
106303
+ decision: {
106304
+ record: "Record a task decision",
106305
+ revoke: "Revoke a task decision"
106306
+ },
106024
106307
  worker: {
106025
106308
  assign: "Assign a worker to a task",
106026
106309
  report: "Report a worker's result",
106027
106310
  cancel: "Cancel a worker assignment"
106028
106311
  },
106029
- state: { export: "Export a task's state (v1)", import: "Import an exported task state" }
106312
+ state: {
106313
+ export: "Export a task's state (v1)",
106314
+ import: "Import an exported task state"
106315
+ }
106030
106316
  };
106031
106317
  var POLICY_JUDGE = "--judge risk=trivial|normal|high behavior=yes|no product-choice=yes|no plan=yes|no [--ref <path>]… [--why <reason>]";
106032
106318
  var TASK_IMPLICIT = [
@@ -106317,7 +106603,7 @@ var VERBS = [
106317
106603
  },
106318
106604
  {
106319
106605
  name: "merge",
106320
- usage: "workit pr merge [--pr <n>] [--squash|--merge|--rebase | --method squash|merge|rebase] [--subject <s>] [--body <b> | --body-file <f|->] [--delete-branch | --keep-branch] [--unverified --reason <why>]",
106606
+ usage: "workit pr merge [--pr <n>] [--squash|--merge|--rebase | --method squash|merge|rebase] [--subject <s>] [--body <b> | --body-file <f|->] [--delete-branch | --keep-branch] [--unverified --reason <why>] [--base-red-ok --reason <why>]",
106321
106607
  summary: "Merge when READY, verified by a non-author and granted; --subject/--body set the squash message"
106322
106608
  }
106323
106609
  ],
@@ -106460,7 +106746,7 @@ var VERBS = [
106460
106746
  },
106461
106747
  {
106462
106748
  name: "land",
106463
- usage: "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--unverified --reason <why>] [--timeout 20m] [--interval 30s]",
106749
+ usage: "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--unverified --reason <why>] [--base-red-ok --reason <why>] [--timeout 20m] [--interval 30s]",
106464
106750
  summary: "Merge the stack root first, restacking the rest as each lands"
106465
106751
  }
106466
106752
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-cli",
3
- "version": "8.34.0",
3
+ "version": "8.36.0",
4
4
  "private": false,
5
5
  "description": "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
6
6
  "keywords": [
@@ -38,13 +38,13 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@babel/parser": "7.29.9",
41
- "@brainervirus/workit-cursor": "^8.34.0",
41
+ "@brainervirus/workit-cursor": "^8.36.0",
42
42
  "@inkjs/ui": "2.0.0",
43
43
  "ink": "7.1.1",
44
44
  "react": "19.2.8"
45
45
  },
46
46
  "devDependencies": {
47
- "@brainervirus/workit-core": "^8.34.0"
47
+ "@brainervirus/workit-core": "^8.36.0"
48
48
  },
49
49
  "engines": {
50
50
  "node": ">=24"