@brainervirus/workit-cli 8.38.4 → 8.38.5

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 +271 -92
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -9317,7 +9317,7 @@ var package_default;
9317
9317
  var init_package = __esm(() => {
9318
9318
  package_default = {
9319
9319
  name: "@brainervirus/workit-cli",
9320
- version: "8.38.4",
9320
+ version: "8.38.5",
9321
9321
  private: false,
9322
9322
  description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
9323
9323
  keywords: [
@@ -9355,13 +9355,13 @@ var init_package = __esm(() => {
9355
9355
  },
9356
9356
  dependencies: {
9357
9357
  "@babel/parser": "7.29.9",
9358
- "@brainervirus/workit-cursor": "^8.38.4",
9358
+ "@brainervirus/workit-cursor": "^8.38.5",
9359
9359
  "@inkjs/ui": "2.0.0",
9360
9360
  ink: "7.1.1",
9361
9361
  react: "19.2.8"
9362
9362
  },
9363
9363
  devDependencies: {
9364
- "@brainervirus/workit-core": "^8.38.4"
9364
+ "@brainervirus/workit-core": "^8.38.5"
9365
9365
  },
9366
9366
  engines: {
9367
9367
  node: ">=24"
@@ -77869,6 +77869,7 @@ function resolveResourceSettings(sources, host = hostShape()) {
77869
77869
  const settings = {
77870
77870
  slots: DEFAULT_SLOTS,
77871
77871
  heavy: [...DEFAULT_HEAVY_CHECKS],
77872
+ light: [...DEFAULT_LIGHT_CHECKS],
77872
77873
  memoryMax: "",
77873
77874
  cpuQuota: "",
77874
77875
  waitMs: DEFAULT_WAIT_S * 1000,
@@ -77910,8 +77911,21 @@ function resolveResourceSettings(sources, host = hostShape()) {
77910
77911
  const heavy = isRecord9(value.heavy) ? Object.keys(value.heavy) : value.heavy;
77911
77912
  if (!Array.isArray(heavy) || !heavy.every((item) => typeof item === "string"))
77912
77913
  warn("heavy", "must be a list of check names");
77913
- else
77914
+ else {
77914
77915
  settings.heavy = repo ? [...new Set([...settings.heavy, ...heavy])] : heavy;
77916
+ settings.light = settings.light.filter((name) => !heavy.includes(name));
77917
+ }
77918
+ }
77919
+ if (value.light !== undefined) {
77920
+ const light = value.light;
77921
+ if (repo)
77922
+ machineOnly("light");
77923
+ else if (!Array.isArray(light) || !light.every((item) => typeof item === "string"))
77924
+ warn("light", "must be a list of check names");
77925
+ else {
77926
+ settings.light = light;
77927
+ settings.heavy = settings.heavy.filter((name) => !light.includes(name));
77928
+ }
77915
77929
  }
77916
77930
  if (value.memoryMax !== undefined) {
77917
77931
  const next = memoryValue(value.memoryMax, host);
@@ -77932,10 +77946,11 @@ function resolveResourceSettings(sources, host = hostShape()) {
77932
77946
  cpu = next;
77933
77947
  }
77934
77948
  if (value.wait !== undefined) {
77935
- if (typeof value.wait === "number" && value.wait >= 0)
77936
- settings.waitMs = value.wait * 1000;
77949
+ const wait = typeof value.wait === "number" ? value.wait >= 0 ? value.wait * 1000 : null : typeof value.wait === "string" ? parseWait(value.wait) : null;
77950
+ if (wait !== null)
77951
+ settings.waitMs = wait;
77937
77952
  else
77938
- warn("wait", "must be a number of seconds");
77953
+ warn("wait", 'must be a number of seconds or a duration ("30m")');
77939
77954
  }
77940
77955
  }
77941
77956
  settings.memoryMax = String(memory);
@@ -78053,7 +78068,65 @@ function removeOwn(file, id) {
78053
78068
  if (parseLease(readText5(file))?.id === id)
78054
78069
  fs24.rmSync(file, { force: true });
78055
78070
  }
78071
+ function leaseQueue(dir, probe = {}) {
78072
+ let names;
78073
+ try {
78074
+ names = fs24.readdirSync(queueDir(dir));
78075
+ } catch {
78076
+ return [];
78077
+ }
78078
+ const entries = [];
78079
+ for (const name of names.filter((item) => item.endsWith(".json")).toSorted()) {
78080
+ const file = path52.join(queueDir(dir), name);
78081
+ const record = parseLease(readText5(file));
78082
+ if (!record)
78083
+ continue;
78084
+ entries.push({
78085
+ file,
78086
+ holder: holderOf(record),
78087
+ id: record.id,
78088
+ stale: staleReason(record, probe)
78089
+ });
78090
+ }
78091
+ return entries;
78092
+ }
78093
+ function waitersAhead(options, own) {
78094
+ let ahead = 0;
78095
+ for (const entry of leaseQueue(options.dir, options.probe)) {
78096
+ if (path52.basename(entry.file) >= own)
78097
+ break;
78098
+ if (!TICKET.test(path52.basename(entry.file)))
78099
+ continue;
78100
+ if (entry.stale && reclaim(entry.file, (current) => !current || staleReason(current, options.probe) !== null))
78101
+ continue;
78102
+ ahead += 1;
78103
+ }
78104
+ return ahead;
78105
+ }
78106
+ function liveHolders(options) {
78107
+ return leaseSlots(options.dir, options.slots, options.probe).filter((slot) => slot.holder && !slot.stale).map((slot) => slot.holder);
78108
+ }
78109
+ function writeTicket(file, record) {
78110
+ fs24.mkdirSync(path52.dirname(file), { recursive: true, mode: 448 });
78111
+ fs24.writeFileSync(file, `${JSON.stringify(record)}
78112
+ `, { mode: 384 });
78113
+ }
78114
+ function parseWait(text) {
78115
+ const match = /^(\d+(?:\.\d+)?)([smh]?)$/u.exec(text.trim());
78116
+ if (!match)
78117
+ return null;
78118
+ const unit = { "": 1000, s: 1000, m: 60000, h: 3600000 }[match[2]] ?? 1000;
78119
+ return Math.round(Number(match[1]) * unit);
78120
+ }
78121
+ function formatWait(ms) {
78122
+ const seconds = Math.max(0, Math.round(ms / 1000));
78123
+ if (seconds < 60)
78124
+ return `${seconds}s`;
78125
+ const minutes = Math.floor(seconds / 60);
78126
+ return seconds % 60 ? `${minutes}m ${seconds % 60}s` : `${minutes}m`;
78127
+ }
78056
78128
  async function acquireLease(options) {
78129
+ const clock = options.clock ?? realClock;
78057
78130
  const pid = options.pid ?? process.pid;
78058
78131
  const boot = bootId();
78059
78132
  const record = {
@@ -78067,7 +78140,7 @@ async function acquireLease(options) {
78067
78140
  check: options.check,
78068
78141
  cwd: options.cwd,
78069
78142
  session: options.session,
78070
- startedAt: new Date().toISOString(),
78143
+ startedAt: new Date(clock.now()).toISOString(),
78071
78144
  scope: null
78072
78145
  };
78073
78146
  try {
@@ -78075,48 +78148,104 @@ async function acquireLease(options) {
78075
78148
  } catch (error) {
78076
78149
  return { ok: false, reason: "unavailable", error: error.message };
78077
78150
  }
78078
- const started = Date.now();
78151
+ const started = clock.now();
78079
78152
  const reclaimed = [];
78080
78153
  let waitedOn = [];
78081
- for (;; ) {
78082
- let outcome;
78083
- try {
78084
- outcome = tryOnce(options, record, reclaimed);
78085
- } catch (error) {
78086
- return { ok: false, reason: "unavailable", error: error.message };
78087
- }
78088
- if ("slot" in outcome) {
78089
- const { file, slot } = outcome;
78090
- let released = false;
78091
- const release = () => {
78092
- if (released)
78093
- return;
78094
- released = true;
78095
- process.off("exit", release);
78096
- removeOwn(file, record.id);
78097
- };
78098
- process.on("exit", release);
78099
- return {
78100
- ok: true,
78101
- lease: {
78102
- record,
78103
- slot,
78104
- file,
78105
- waitedOn,
78106
- waitedMs: waitedOn.length ? Date.now() - started : 0,
78107
- reclaimed,
78108
- release
78154
+ const name = ticketName(started, record.id);
78155
+ const ticket = path52.join(queueDir(options.dir), name);
78156
+ const ticketRecord = {
78157
+ ...record,
78158
+ maxAgeMs: options.waitMs + 60000
78159
+ };
78160
+ let queued = false;
78161
+ const onSignal = (signal) => {
78162
+ leave();
78163
+ process.exit(signal === "SIGINT" ? 130 : 143);
78164
+ };
78165
+ const leave = () => {
78166
+ process.off("exit", leave);
78167
+ process.off("SIGINT", onSignal);
78168
+ process.off("SIGTERM", onSignal);
78169
+ if (queued)
78170
+ removeOwn(ticket, record.id);
78171
+ queued = false;
78172
+ };
78173
+ let nextStatus = 0;
78174
+ try {
78175
+ for (;; ) {
78176
+ const waited = clock.now() - started;
78177
+ const ahead = queued ? waitersAhead(options, name) : waitersAhead(options, "~");
78178
+ const free = options.slots - liveHolders(options).length;
78179
+ if (ahead < free) {
78180
+ const outcome = tryOnce(options, record, reclaimed);
78181
+ if ("slot" in outcome) {
78182
+ const waitedMs = queued ? waited : 0;
78183
+ leave();
78184
+ const { file, slot } = outcome;
78185
+ let released = false;
78186
+ const release = () => {
78187
+ if (released)
78188
+ return;
78189
+ released = true;
78190
+ process.off("exit", release);
78191
+ removeOwn(file, record.id);
78192
+ };
78193
+ process.on("exit", release);
78194
+ return {
78195
+ ok: true,
78196
+ lease: {
78197
+ record,
78198
+ slot,
78199
+ file,
78200
+ waitedOn,
78201
+ waitedMs,
78202
+ reclaimed,
78203
+ release
78204
+ }
78205
+ };
78109
78206
  }
78110
- };
78111
- }
78112
- const waited = Date.now() - started;
78113
- if (!waitedOn.length) {
78114
- waitedOn = outcome.holders;
78115
- options.onWait?.(outcome.holders);
78207
+ }
78208
+ const holders = liveHolders(options);
78209
+ if (!waitedOn.length) {
78210
+ waitedOn = holders;
78211
+ options.onWait?.(holders);
78212
+ }
78213
+ if (waited >= options.waitMs)
78214
+ return {
78215
+ ok: false,
78216
+ reason: "timeout",
78217
+ holders,
78218
+ waitedMs: waited,
78219
+ position: ahead + 1
78220
+ };
78221
+ if (!queued) {
78222
+ writeTicket(ticket, ticketRecord);
78223
+ queued = true;
78224
+ process.on("exit", leave);
78225
+ process.on("SIGINT", onSignal);
78226
+ process.on("SIGTERM", onSignal);
78227
+ } else if (!fs24.existsSync(ticket)) {
78228
+ writeTicket(ticket, ticketRecord);
78229
+ }
78230
+ if (waited >= nextStatus) {
78231
+ options.onStatus?.({
78232
+ position: ahead + 1,
78233
+ ahead,
78234
+ holders,
78235
+ waitedMs: waited
78236
+ });
78237
+ nextStatus = waited + (options.statusEveryMs ?? 30000);
78238
+ }
78239
+ await clock.sleep(Math.min(options.pollMs ?? 2000, Math.max(1, options.waitMs - waited)));
78116
78240
  }
78117
- if (waited >= options.waitMs)
78118
- return { ok: false, reason: "timeout", holders: outcome.holders, waitedMs: waited };
78119
- await sleep(Math.min(options.pollMs ?? 2000, Math.max(1, options.waitMs - waited)));
78241
+ } catch (error) {
78242
+ return {
78243
+ ok: false,
78244
+ reason: "unavailable",
78245
+ error: error.message
78246
+ };
78247
+ } finally {
78248
+ leave();
78120
78249
  }
78121
78250
  }
78122
78251
  function updateLease(lease, patch) {
@@ -78229,7 +78358,7 @@ function leaseSlots(dir, slots, probe = {}) {
78229
78358
  }
78230
78359
  return states;
78231
78360
  }
78232
- var DEFAULT_HEAVY_CHECKS, DEFAULT_SLOTS = 1, DEFAULT_WAIT_S = 600, DEFAULT_MEMORY_SHARE = 0.4, DEFAULT_CPU_SHARE = 0.7, MAX_SLOTS = 64, UNREADABLE_TTL_MS = 30000, GUARD_TTL_MS = 30000, hostShape = () => ({
78361
+ var DEFAULT_HEAVY_CHECKS, DEFAULT_LIGHT_CHECKS, DEFAULT_SLOTS = 1, DEFAULT_WAIT_S = 1800, DEFAULT_MEMORY_SHARE = 0.4, DEFAULT_CPU_SHARE = 0.7, MAX_SLOTS = 64, UNREADABLE_TTL_MS = 30000, GUARD_TTL_MS = 30000, hostShape = () => ({
78233
78362
  totalMemBytes: os20.totalmem(),
78234
78363
  cpus: Math.max(1, os20.availableParallelism?.() ?? os20.cpus().length)
78235
78364
  }), isRecord9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), SIZE, PERCENT, UNITS, leaseDir = (env) => path52.join(stateDir2(env), "leases"), readText5 = (file) => {
@@ -78263,16 +78392,22 @@ var DEFAULT_HEAVY_CHECKS, DEFAULT_SLOTS = 1, DEFAULT_WAIT_S = 600, DEFAULT_MEMOR
78263
78392
  cwd: lease.cwd,
78264
78393
  session: lease.session,
78265
78394
  startedAt: lease.startedAt
78266
- }), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), SCOPE_START_FAILURE, NOTICE_TTL_MS;
78395
+ }), realClock, queueDir = (dir) => path52.join(dir, "queue"), TICKET, ticketName = (now, id) => `${String(now).padStart(15, "0")}-${String(process.hrtime.bigint() % 10n ** 16n).padStart(16, "0")}-${id}.json`, SCOPE_START_FAILURE, NOTICE_TTL_MS;
78267
78396
  var init_resources = __esm(() => {
78268
78397
  init_checks3();
78269
78398
  init_heavy();
78270
78399
  init_heavy_scope();
78271
- DEFAULT_HEAVY_CHECKS = ["test", "build", "typecheck"];
78400
+ DEFAULT_HEAVY_CHECKS = ["test", "build"];
78401
+ DEFAULT_LIGHT_CHECKS = ["lint", "typecheck", "format"];
78272
78402
  SIZE = /^(\d+(?:\.\d+)?)([KMGT]?)$/iu;
78273
78403
  PERCENT = /^(\d+(?:\.\d+)?)%$/u;
78274
78404
  UNITS = { "": 1, K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4 };
78275
78405
  SCOPE_UNIT = /^[\w.@-]+\.scope$/u;
78406
+ realClock = {
78407
+ now: () => Date.now(),
78408
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
78409
+ };
78410
+ TICKET = /^\d{15}-\d{16}-[\w-]+\.json$/u;
78276
78411
  SCOPE_START_FAILURE = /^Failed to (?:connect to bus|create bus connection|start transient scope unit|parse \S+?=)/u;
78277
78412
  NOTICE_TTL_MS = 7 * 86400000;
78278
78413
  });
@@ -78283,7 +78418,6 @@ __export(exports_check, {
78283
78418
  OUTPUT_LINES: () => OUTPUT_LINES,
78284
78419
  boundedOutput: () => boundedOutput,
78285
78420
  countWarnings: () => countWarnings,
78286
- hostToolWaitCapMs: () => hostToolWaitCapMs,
78287
78421
  run: () => run12,
78288
78422
  workspaceResources: () => workspaceResources
78289
78423
  });
@@ -78306,6 +78440,7 @@ function parse6(argv) {
78306
78440
  positionals: [],
78307
78441
  command: split >= 0 ? argv.slice(split + 1) : null
78308
78442
  };
78443
+ let noWait = false;
78309
78444
  for (let index = 0;index < head.length; index += 1) {
78310
78445
  const arg = head[index];
78311
78446
  const [flag, inline] = arg.startsWith("--") && arg.includes("=") ? arg.split(/=(.*)/su) : [arg];
@@ -78329,6 +78464,13 @@ function parse6(argv) {
78329
78464
  options.noLease = true;
78330
78465
  continue;
78331
78466
  }
78467
+ if (flag === "--no-wait") {
78468
+ if (options.waitMs !== null && !noWait)
78469
+ return new Error("--no-wait goes without --wait");
78470
+ noWait = true;
78471
+ options.waitMs = 0;
78472
+ continue;
78473
+ }
78332
78474
  if (flag === "--name" || flag === "--task" || flag === "--base" || flag === "--timeout" || flag === "--wait" || flag === "--reason") {
78333
78475
  const got = value();
78334
78476
  if (got instanceof Error)
@@ -78338,10 +78480,12 @@ function parse6(argv) {
78338
78480
  else if (flag === "--reason")
78339
78481
  options.reason = got.trim() || null;
78340
78482
  else if (flag === "--wait") {
78341
- const seconds = Number(got);
78342
- if (!Number.isFinite(seconds) || seconds < 0)
78343
- return new Error("--wait must be a number of seconds");
78344
- options.waitMs = Math.round(seconds * 1000);
78483
+ if (noWait)
78484
+ return new Error("--no-wait goes without --wait");
78485
+ const waitMs = parseWait(got);
78486
+ if (waitMs === null)
78487
+ return new Error('--wait must be seconds or a duration ("90", "90s", "30m", "1h")');
78488
+ options.waitMs = waitMs;
78345
78489
  } else if (flag === "--task")
78346
78490
  options.task = got;
78347
78491
  else if (flag === "--base")
@@ -78471,22 +78615,30 @@ function countWarnings(log) {
78471
78615
  return redactLog(log).replace(ANSI_COLOR, "").split(`
78472
78616
  `).filter((line) => WARNING_LINE.test(line) && !NO_WARNINGS.test(line)).length;
78473
78617
  }
78474
- function boundedOutput(log, lines) {
78618
+ function boundedOutput(log, lines, stderr = "") {
78475
78619
  const all = tailLines(log, Number.MAX_SAFE_INTEGER);
78476
78620
  if (all.length === 0)
78477
78621
  return "";
78478
- const shown = all.slice(-lines);
78622
+ let shown = all.slice(-lines);
78623
+ const errors = tailLines(redactLog(stderr), STDERR_TAIL_LINES);
78624
+ let missing = [];
78625
+ if (errors.length) {
78626
+ const tail = new Set(shown);
78627
+ if (errors.some((line) => !tail.has(line))) {
78628
+ shown = all.slice(-Math.max(1, lines - errors.length - 2));
78629
+ const kept = new Set(shown);
78630
+ missing = errors.filter((line) => !kept.has(line));
78631
+ }
78632
+ }
78479
78633
  const omitted = all.length - shown.length;
78634
+ const footer = missing.length ? `… the last stderr lines, which the merged log holds earlier:
78635
+ ${missing.join(`
78636
+ `)}
78637
+ ` : "";
78480
78638
  return `${omitted ? `… ${omitted} earlier line${omitted === 1 ? "" : "s"} omitted (the full log is named below; --full streams it)
78481
78639
  ` : ""}${shown.join(`
78482
78640
  `)}
78483
- `;
78484
- }
78485
- function hostToolWaitCapMs(env) {
78486
- if (!env.CLAUDECODE && !env.OPENCODE && !env.OPENCODE_SESSION_ID)
78487
- return null;
78488
- const override = Number(env.WORKIT_HOST_WAIT_CAP_MS);
78489
- return Number.isFinite(override) && override >= 0 && env.WORKIT_HOST_WAIT_CAP_MS ? override : HOST_TOOL_WAIT_CAP_MS;
78641
+ ${footer}`;
78490
78642
  }
78491
78643
  async function workspaceResources(io) {
78492
78644
  const env = io.env;
@@ -78520,14 +78672,26 @@ async function prepareHeavy(io, settings, label, runCwd, timeoutMs) {
78520
78672
  waitMs: settings.waitMs,
78521
78673
  pollMs: 1000,
78522
78674
  maxAgeMs: (timeoutMs || MAX_UNVERIFIED_RUN_MS) + 60000,
78523
- onWait: (holders) => io.stderr(`workit: waiting for the heavy-check lease (${settings.slots} slot${settings.slots === 1 ? "" : "s"}): held by ${holders.map((holder) => describeHolder(holder)).join("; ")}
78524
- `)
78675
+ onStatus: (status) => {
78676
+ const slots = `${settings.slots} slot${settings.slots === 1 ? "" : "s"}`;
78677
+ const holders = status.holders.map((holder) => describeHolder(holder)).join("; ");
78678
+ io.stderr(status.waitedMs === 0 ? `workit: waiting for the heavy-check lease (${slots}): held by ${holders || "nobody; waiters ahead"}; queued at position ${status.position}, waiting up to ${formatWait(settings.waitMs)} (--wait <dur> sets it)
78679
+ ` : `workit: still queued for the heavy-check lease: position ${status.position}, ${formatWait(status.waitedMs)} of ${formatWait(settings.waitMs)} waited; held by ${holders || "nobody; waiters ahead"}
78680
+ `);
78681
+ }
78525
78682
  });
78526
78683
  if (!acquired.ok && acquired.reason === "timeout") {
78527
78684
  const holders = acquired.holders.map((holder) => describeHolder(holder)).join("; ");
78528
- emit(io, fail2("busy", `the heavy-check lease stayed busy for ${Math.round(acquired.waitedMs / 1000)} s`, {
78529
- data: { lease: { waitedMs: acquired.waitedMs, holders: acquired.holders } },
78530
- unblock: `wait for ${holders || "the holders"} to finish, then run it again`
78685
+ const waited = formatWait(acquired.waitedMs);
78686
+ emit(io, fail2("busy", settings.waitMs === 0 ? `the heavy-check lease is busy (--no-wait): held by ${holders || "nobody; waiters ahead"}` : `the heavy-check lease stayed busy for ${waited} (position ${acquired.position} in the queue)`, {
78687
+ data: {
78688
+ lease: {
78689
+ waitedMs: acquired.waitedMs,
78690
+ holders: acquired.holders,
78691
+ position: acquired.position
78692
+ }
78693
+ },
78694
+ unblock: `wait for ${holders || "the waiters ahead"} to finish (each holder's age is shown; a full test suite takes minutes), then run it again, or queue longer with --wait 30m`
78531
78695
  }));
78532
78696
  return 4;
78533
78697
  }
@@ -78627,11 +78791,8 @@ async function run12(argv, io) {
78627
78791
  { label: "workspaces.json", trust: "machine", value: workspace.value },
78628
78792
  { label: CHECKS_FILE, trust: "repo", value: config.resources }
78629
78793
  ]);
78630
- const cap = hostToolWaitCapMs(io.env);
78631
78794
  if (options.waitMs !== null)
78632
78795
  settings.waitMs = options.waitMs;
78633
- else if (cap !== null)
78634
- settings.waitMs = Math.min(settings.waitMs, cap);
78635
78796
  for (const warning of [workspace.warning, ...warnings])
78636
78797
  if (warning)
78637
78798
  io.stderr(`workit: resources: ${warning}
@@ -78648,6 +78809,7 @@ async function run12(argv, io) {
78648
78809
  `);
78649
78810
  const lease = heavy?.lease ?? null;
78650
78811
  let result;
78812
+ let stderrTail = "";
78651
78813
  try {
78652
78814
  const scoped = await runScoped((wrapper, sinks) => runCheckCommand(command, {
78653
78815
  cwd: runCwd,
@@ -78657,7 +78819,12 @@ async function run12(argv, io) {
78657
78819
  ...sinks,
78658
78820
  ...wrapper ? { wrapper } : {},
78659
78821
  ...lease ? { onSpawn: (pid) => updateLease(lease, { pgid: pid }) } : {}
78660
- }), heavy?.wrapper ?? null, options.full ? { onStdout: io.json ? io.stderr : io.stdout, onStderr: io.stderr } : { onStdout: () => {}, onStderr: () => {} });
78822
+ }), heavy?.wrapper ?? null, options.full ? { onStdout: io.json ? io.stderr : io.stdout, onStderr: io.stderr } : {
78823
+ onStdout: () => {},
78824
+ onStderr: (text) => {
78825
+ stderrTail = (stderrTail + text).slice(-STDERR_KEEP_CHARS);
78826
+ }
78827
+ });
78661
78828
  result = scoped.result;
78662
78829
  if (heavy && scoped.fallback) {
78663
78830
  io.stderr(`workit: the memory/CPU scope could not start (${scoped.fallback}); ran without it
@@ -78769,7 +78936,7 @@ async function run12(argv, io) {
78769
78936
  `);
78770
78937
  else {
78771
78938
  if (!options.full)
78772
- io.stdout(boundedOutput(result.log, OUTPUT_LINES));
78939
+ io.stdout(boundedOutput(result.log, OUTPUT_LINES, stderrTail));
78773
78940
  io.stderr(`${summary}
78774
78941
  `);
78775
78942
  if (recordError && passed)
@@ -78786,7 +78953,7 @@ async function run12(argv, io) {
78786
78953
  return recordError.code === "busy" ? 4 : recordError.code === "invalid_input" ? 2 : 5;
78787
78954
  return 0;
78788
78955
  }
78789
- var USAGE6 = '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(path53.sep).join("/") || ".", ENGINE_CODES2, FINGERPRINT_VARS, ANSI_COLOR, WARNING_LINE, NO_WARNINGS, HOST_TOOL_WAIT_CAP_MS = 1e5;
78956
+ var USAGE6 = 'workit check <name> [--full] [--wait <dur> | --no-wait] [--no-lease --reason "<why>"] | workit check [--name <n>] [--shell] [--heavy] [--full] [--wait <dur> | --no-wait] [--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(path53.sep).join("/") || ".", ENGINE_CODES2, FINGERPRINT_VARS, ANSI_COLOR, WARNING_LINE, NO_WARNINGS, STDERR_TAIL_LINES = 10, STDERR_KEEP_CHARS = 65536;
78790
78957
  var init_check = __esm(() => {
78791
78958
  init_check_config();
78792
78959
  init_checks3();
@@ -78824,25 +78991,35 @@ async function settingsFor(io) {
78824
78991
  { label: CHECKS_FILE, trust: "repo", value: config.resources }
78825
78992
  ]).settings;
78826
78993
  }
78827
- function slotLines(slots) {
78994
+ function slotLines(slots, queue = []) {
78828
78995
  const busy = held(slots).length;
78996
+ const waiting = queue.filter((entry) => !entry.stale);
78829
78997
  return [
78830
78998
  `heavy-check lease: ${busy} of ${slots.length} slot${slots.length === 1 ? "" : "s"} held`,
78831
- ...slots.map((slot) => slot.holder ? ` slot ${slot.slot}: ${describeHolder(slot.holder)}${slot.stale ? ` (stale: ${slot.stale}; the next heavy check reclaims it)` : ""}` : ` slot ${slot.slot}: free`)
78999
+ ...slots.map((slot) => slot.holder ? ` slot ${slot.slot}: ${describeHolder(slot.holder)}${slot.stale ? ` (stale: ${slot.stale}; the next heavy check reclaims it)` : ""}` : ` slot ${slot.slot}: free`),
79000
+ `queue: ${waiting.length ? `${waiting.length} waiting, first come, first served` : "empty"}`,
79001
+ ...waiting.map((entry, index) => ` ${index + 1}. ${describeHolder(entry.holder)}`)
78832
79002
  ];
78833
79003
  }
78834
79004
  async function status(io) {
78835
79005
  const settings = await settingsFor(io);
78836
79006
  const slots = leaseSlots(leaseDir(io.env), settings.slots);
78837
- return emit(io, ok2({ dir: leaseDir(io.env), slots, waitMs: settings.waitMs }), (data) => slotLines(data.slots));
79007
+ const queue = leaseQueue(leaseDir(io.env));
79008
+ const waiting = queue.filter((entry) => !entry.stale).map((entry) => entry.holder);
79009
+ return emit(io, ok2({
79010
+ dir: leaseDir(io.env),
79011
+ slots,
79012
+ queue: waiting,
79013
+ waitMs: settings.waitMs,
79014
+ light: settings.light
79015
+ }), () => slotLines(slots, queue));
78838
79016
  }
78839
79017
  function timeoutOf(args) {
78840
79018
  const at = args.findIndex((arg) => arg === "--timeout" || arg.startsWith("--timeout="));
78841
79019
  if (at < 0)
78842
79020
  return null;
78843
79021
  const raw = args[at].includes("=") ? args[at].split("=")[1] : args[at + 1];
78844
- const seconds = Number(raw);
78845
- return Number.isFinite(seconds) && seconds >= 0 ? Math.round(seconds * 1000) : new Error("--timeout must be a number of seconds");
79022
+ return parseWait(raw ?? "") ?? new Error('--timeout must be seconds or a duration ("90", "90s", "30m", "1h")');
78846
79023
  }
78847
79024
  async function wait(args, io) {
78848
79025
  const timeout = timeoutOf(args);
@@ -78859,10 +79036,10 @@ async function wait(args, io) {
78859
79036
  const waitedMs = Date.now() - started;
78860
79037
  if (busy.length < slots.length)
78861
79038
  return emit(io, ok2({ free: true, waitedMs, slots }), () => [
78862
- `workit: the heavy-check lease has a free slot${waitedMs ? ` after ${Math.round(waitedMs / 1000)} s` : ""}`
79039
+ `workit: the heavy-check lease has a free slot${waitedMs ? ` after ${formatWait(waitedMs)}` : ""}`
78863
79040
  ]);
78864
79041
  if (waitedMs >= limit)
78865
- return emit(io, fail2("busy", `the heavy-check lease stayed busy for ${Math.round(waitedMs / 1000)} s`, {
79042
+ return emit(io, fail2("busy", `the heavy-check lease stayed busy for ${formatWait(waitedMs)}`, {
78866
79043
  data: { free: false, waitedMs, slots },
78867
79044
  unblock: "workit lock status"
78868
79045
  }));
@@ -78871,16 +79048,16 @@ async function wait(args, io) {
78871
79048
  io.stderr(`workit: waiting for the heavy-check lease: held by ${busy.flatMap((slot) => slot.holder ? [describeHolder(slot.holder)] : []).join("; ")}
78872
79049
  `);
78873
79050
  }
78874
- await sleep2(Math.min(1000, Math.max(1, limit - waitedMs)));
79051
+ await sleep(Math.min(1000, Math.max(1, limit - waitedMs)));
78875
79052
  }
78876
79053
  }
78877
79054
  async function runHeavy(args, io) {
78878
79055
  const split = args.indexOf("--");
78879
79056
  const head = split < 0 ? args : args.slice(0, split);
78880
- const unknown = head.find((arg, index) => arg !== "--shell" && arg !== "--json" && !arg.startsWith("--timeout") && !arg.startsWith("--wait") && !((head[index - 1] === "--timeout" || head[index - 1] === "--wait") && !arg.startsWith("-")));
79057
+ const unknown = head.find((arg, index) => arg !== "--shell" && arg !== "--json" && arg !== "--no-wait" && !arg.startsWith("--timeout") && !arg.startsWith("--wait") && !((head[index - 1] === "--timeout" || head[index - 1] === "--wait") && !arg.startsWith("-")));
78881
79058
  if (split < 0 || unknown)
78882
79059
  return emit(io, fail2("invalid_input", unknown ? `unexpected ${unknown}` : "no command after --", {
78883
- unblock: "workit lock run [--shell] [--wait <s>] [--timeout <s>] -- <cmd…>"
79060
+ unblock: "workit lock run [--shell] [--wait <dur> | --no-wait] [--timeout <s>] -- <cmd…>"
78884
79061
  }));
78885
79062
  const check = await Promise.resolve().then(() => (init_check(), {}));
78886
79063
  return run12([`--heavy`, ...head, ...args.slice(split)], io);
@@ -78966,7 +79143,7 @@ async function run13(argv, io) {
78966
79143
  return heavy(rest, io);
78967
79144
  return emit(io, fail2("invalid_input", `unknown lock subcommand ${sub ?? "(none)"}`, { unblock: USAGE7 }));
78968
79145
  }
78969
- var USAGE7 = "workit lock status | wait [--timeout <s>] | run [--shell] [--wait <s>] [--timeout <s>] -- <cmd…> | heavy list|add|remove|test", held = (slots) => slots.filter((slot) => slot.holder && !slot.stale), sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), commandText = (args) => {
79146
+ var USAGE7 = "workit lock status | wait [--timeout <dur>] | run [--shell] [--wait <dur> | --no-wait] [--timeout <s>] -- <cmd…> | heavy list|add|remove|test", held = (slots) => slots.filter((slot) => slot.holder && !slot.stale), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), commandText = (args) => {
78970
79147
  const split = args.indexOf("--");
78971
79148
  return (split < 0 ? args : args.slice(split + 1)).join(" ").trim();
78972
79149
  };
@@ -107041,9 +107218,10 @@ function withSubcommands(entry, options = {}) {
107041
107218
  };
107042
107219
  }
107043
107220
  var LEASE_NOTES = [
107044
- "The heavy-check lease: heavy work (tests, builds, lint, typecheck, installs, image builds) shares one machine-wide lease, so parallel agents and worktrees never exhaust this machine's memory and CPU. `slots` (default 1) heavy checks run at once; on Linux each also runs in a systemd scope with MemoryMax and CPUQuota.",
107045
- "Waiting: a heavy check that finds every slot held prints one `waiting for the heavy-check lease` line naming the holder (check, pid, session, age, directory), polls once a second, and exits 4 (busy) after `--wait <s>`, else `resources.wait` seconds (default 600, capped at 100 inside a Claude Code or OpenCode shell). `workit lock status` shows the holders; `workit lock wait` waits for a free slot.",
107046
- "--heavy: an ad-hoc `workit check -- <cmd>` takes the lease only with --heavy; the named checks test, build and typecheck always do. The shell hook runs a raw heavy command (`npx jest`, `npm run test:ci`, `cargo test`, `docker build`) as `workit check --heavy -- <cmd>` on Claude Code, OpenCode and Pi, and denies it with that exact command on Codex and Cursor.",
107221
+ "The heavy-check lease: heavy work (tests, builds, installs, image builds) shares one machine-wide lease, so parallel agents and worktrees never exhaust this machine's memory and CPU. `slots` (default 1) heavy checks run at once; on Linux each also runs in a systemd scope with MemoryMax and CPUQuota.",
107222
+ "Queueing: a heavy check that finds every slot held joins a first-come, first-served queue and prints `waiting for the heavy-check lease` naming the holder (check, pid, session, age, directory) and its position, then a status line every 30 s. It waits `--wait <dur>` (90, 90s, 30m, 1h), else `resources.wait` (default 30 min), and exits 4 (busy) only past that wait or at once with --no-wait. A waiter that is killed gives up its place. A wait longer than your shell tool's timeout belongs in a background call. `workit lock status` shows the holders and the queue.",
107223
+ "Light checks: the named checks lint, typecheck and format (`resources.light`, set in workspaces.json) never take the lease, so they run at once beside a long suite.",
107224
+ "--heavy: an ad-hoc `workit check -- <cmd>` takes the lease only with --heavy; the named checks test and build always do. The shell hook runs a raw heavy command (`npx jest`, `npm run test:ci`, `cargo test`, `docker build`) as `workit check --heavy -- <cmd>` on Claude Code, OpenCode and Pi, and denies it with that exact command on Codex and Cursor.",
107047
107225
  '--no-lease skips the lease and needs --reason "<why>". Never hand-roll a flock: the lease already queues every agent on the machine.',
107048
107226
  "Output: a check prints the last 60 lines of its output and one summary line with `log: <path>` to the full log; --full streams all of it."
107049
107227
  ];
@@ -107160,7 +107338,7 @@ var VERBS = [
107160
107338
  {
107161
107339
  name: "check",
107162
107340
  group: "delivery",
107163
- usage: 'workit check <name> [--full] [--task <id>] [--base <ref>] [--no-lease --reason "<why>"] | workit check [--name <n>] [--shell] [--full] [--timeout <s>] [--task <id>] [--base <ref>] [--heavy [--wait <s>]] [--no-lease --reason "<why>"] -- <cmd…>',
107341
+ usage: 'workit check <name> [--full] [--task <id>] [--base <ref>] [--wait <dur> | --no-wait] [--no-lease --reason "<why>"] | workit check [--name <n>] [--shell] [--full] [--timeout <s>] [--task <id>] [--base <ref>] [--heavy [--wait <dur> | --no-wait]] [--no-lease --reason "<why>"] -- <cmd…>',
107164
107342
  flagHints: {
107165
107343
  list: "workit check <name> runs a configured check; a name that is not configured prints the valid ones (workit check test|lint|typecheck|check)"
107166
107344
  },
@@ -107168,6 +107346,7 @@ var VERBS = [
107168
107346
  examples: [
107169
107347
  "workit check test",
107170
107348
  "workit check --heavy -- npx jest",
107349
+ "workit check test --wait 45m",
107171
107350
  "workit check --name unit -- bun test test/unit",
107172
107351
  'workit check test --no-lease --reason "machine is idle, debugging a hang"'
107173
107352
  ],
@@ -107182,16 +107361,16 @@ var VERBS = [
107182
107361
  {
107183
107362
  name: "status",
107184
107363
  usage: "workit lock status",
107185
- summary: "Show each lease slot: free, held (check, pid, session, age, directory) or stale"
107364
+ summary: "Show each lease slot (free, held by check, pid, session, age, directory, or stale) and the queue of waiting checks in arrival order"
107186
107365
  },
107187
107366
  {
107188
107367
  name: "wait",
107189
- usage: "workit lock wait [--timeout <s>]",
107368
+ usage: "workit lock wait [--timeout <dur>]",
107190
107369
  summary: "Wait until a lease slot is free, without taking it (exit 0 free, 4 still busy at the timeout)"
107191
107370
  },
107192
107371
  {
107193
107372
  name: "run",
107194
- usage: "workit lock run [--shell] [--wait <s>] [--timeout <s>] -- <cmd…>",
107373
+ usage: "workit lock run [--shell] [--wait <dur> | --no-wait] [--timeout <s>] -- <cmd…>",
107195
107374
  summary: "Run a command under the lease: the same as workit check --heavy -- <cmd…>"
107196
107375
  },
107197
107376
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-cli",
3
- "version": "8.38.4",
3
+ "version": "8.38.5",
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.38.4",
41
+ "@brainervirus/workit-cursor": "^8.38.5",
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.38.4"
47
+ "@brainervirus/workit-core": "^8.38.5"
48
48
  },
49
49
  "engines": {
50
50
  "node": ">=24"