@brainervirus/workit-cursor 8.17.0 → 8.18.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.
@@ -2,7 +2,7 @@
2
2
  "name": "workit",
3
3
  "displayName": "Workit",
4
4
  "description": "Adaptive Workit workflow with shared MCP tools and native Cursor hooks",
5
- "version": "8.17.0",
5
+ "version": "8.18.0",
6
6
  "author": {
7
7
  "name": "Cristhofer Pincetti"
8
8
  },
@@ -11526,7 +11526,7 @@ var RULES, derived2 = (spec) => ({
11526
11526
  };
11527
11527
  const since = Date.parse(task.createdAt);
11528
11528
  const branch = ledger.branch();
11529
- const row = ledger.rows().findLast((item) => item.type === "decision" && !item.superseded && (item.branch ?? null) === branch && Date.parse(item.at) >= since);
11529
+ const row = ledger.rows().findLast((item) => item.type === "decision" && item.by !== "decider" && !item.superseded && (item.branch ?? null) === branch && Date.parse(item.at) >= since);
11530
11530
  return row ? { met: true, reason: `ledger decision ${row.id} records the choice`, decisionIds: [] } : {
11531
11531
  met: false,
11532
11532
  reason: 'open product choice: ask the user, then `workit ledger decision "<choice>" --why "<reason>"` (or, if it is not open, `workit policy assess --judge product-choice=no --why "<reason>"`)',
@@ -15033,6 +15033,11 @@ var init_workers = __esm(() => {
15033
15033
  import { spawnSync as spawnSync9 } from "node:child_process";
15034
15034
  import fs12 from "node:fs";
15035
15035
  import path16 from "node:path";
15036
+ function checksConfigFile(root) {
15037
+ const current = path16.join(root, CHECKS_FILE);
15038
+ const legacy = path16.join(root, LEGACY_CHECKS_FILE);
15039
+ return !fs12.existsSync(current) && fs12.existsSync(legacy) ? legacy : current;
15040
+ }
15036
15041
  function repoHeavyCommands(root) {
15037
15042
  try {
15038
15043
  const value = JSON.parse(fs12.readFileSync(checksConfigFile(root), "utf8"));
@@ -15124,19 +15129,25 @@ function detectedChecks(root) {
15124
15129
  function loadCheckConfig(cwd) {
15125
15130
  const root = checkRoot(cwd);
15126
15131
  const file = checksConfigFile(root);
15132
+ const legacy = path16.basename(file) === LEGACY_CHECKS_FILE;
15133
+ const label = legacy ? LEGACY_CHECKS_FILE : CHECKS_FILE;
15134
+ const note = legacy ? {
15135
+ note: `${LEGACY_CHECKS_FILE} is deprecated; move it to ${CHECKS_FILE} (workit doctor --fix)`
15136
+ } : {};
15127
15137
  const none = (error = null) => ({
15128
- source: error ? CHECKS_FILE : "none",
15138
+ source: error ? label : "none",
15129
15139
  root,
15130
15140
  checks: [],
15131
15141
  gates: {},
15132
- error
15142
+ error,
15143
+ ...note
15133
15144
  });
15134
15145
  let text = null;
15135
15146
  try {
15136
15147
  text = fs12.readFileSync(file, "utf8");
15137
15148
  } catch (error) {
15138
15149
  if (error.code !== "ENOENT")
15139
- return none(`cannot read ${CHECKS_FILE}: ${error.message}`);
15150
+ return none(`cannot read ${label}: ${error.message}`);
15140
15151
  }
15141
15152
  if (text === null) {
15142
15153
  const checks = detectedChecks(root);
@@ -15146,33 +15157,33 @@ function loadCheckConfig(cwd) {
15146
15157
  try {
15147
15158
  parsed = JSON.parse(text);
15148
15159
  } catch {
15149
- return none(`${CHECKS_FILE} is not valid JSON`);
15160
+ return none(`${label} is not valid JSON`);
15150
15161
  }
15151
15162
  if (!isRecord3(parsed) || !isRecord3(parsed.checks))
15152
- return none(`${CHECKS_FILE} must be {"checks":{"<name>":"<command>"|["argv",…]}}`);
15163
+ return none(`${label} must be {"checks":{"<name>":"<command>"|["argv",…]}}`);
15153
15164
  const checks = [];
15154
15165
  for (const [name, value] of Object.entries(parsed.checks)) {
15155
15166
  if (!CHECK_NAME.test(name))
15156
- return none(`${CHECKS_FILE}: check name "${name}" must match ${CHECK_NAME.source}`);
15167
+ return none(`${label}: check name "${name}" must match ${CHECK_NAME.source}`);
15157
15168
  const argv = typeof value === "string" ? splitCommand(value) : Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.length > 0) ? value : new Error("expected a command string or a non-empty argv array");
15158
15169
  if (argv instanceof Error)
15159
- return none(`${CHECKS_FILE}: check "${name}": ${argv.message}`);
15170
+ return none(`${label}: check "${name}": ${argv.message}`);
15160
15171
  checks.push({ name, argv, accepts: [argv] });
15161
15172
  }
15162
15173
  const gates = {};
15163
15174
  if (parsed.gates !== undefined) {
15164
15175
  if (!isRecord3(parsed.gates))
15165
- return none(`${CHECKS_FILE}: gates must be {"testing":"<name>"|["<name>",…]}`);
15176
+ return none(`${label}: gates must be {"testing":"<name>"|["<name>",…]}`);
15166
15177
  for (const [gate, value] of Object.entries(parsed.gates)) {
15167
15178
  if (!GATES.includes(gate))
15168
- return none(`${CHECKS_FILE}: unknown gate "${gate}" (use ${GATES.join(" or ")})`);
15179
+ return none(`${label}: unknown gate "${gate}" (use ${GATES.join(" or ")})`);
15169
15180
  const names = typeof value === "string" ? [value] : value;
15170
15181
  if (!Array.isArray(names) || names.length === 0 || !names.every((name) => typeof name === "string" && checks.some((c) => c.name === name)))
15171
- return none(`${CHECKS_FILE}: gates.${gate} must name configured checks`);
15182
+ return none(`${label}: gates.${gate} must name configured checks`);
15172
15183
  gates[gate] = names;
15173
15184
  }
15174
15185
  }
15175
- return { source: CHECKS_FILE, root, checks, gates, error: null, resources: parsed.resources };
15186
+ return { source: label, root, checks, gates, error: null, ...note, resources: parsed.resources };
15176
15187
  }
15177
15188
  function matchesNamedCheck(config, name, argv, cwd) {
15178
15189
  if (config.error || name === null || cwd !== ".")
@@ -15207,7 +15218,7 @@ function workitAvailable(env = process.env) {
15207
15218
  function checkCommand(args, version, onPath = workitAvailable()) {
15208
15219
  return onPath ? `workit check ${args}` : `npx -y @brainervirus/workit-cli@${version} check ${args}`;
15209
15220
  }
15210
- var SCRIPT_CHECKS, CHECKS_FILE = "workit.checks.json", checksConfigFile = (root) => path16.join(root, CHECKS_FILE), isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), SHELL_META, git4 = (cwd, args) => {
15221
+ var SCRIPT_CHECKS, CHECKS_FILE = ".workit/checks.json", LEGACY_CHECKS_FILE = "workit.checks.json", isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), SHELL_META, git4 = (cwd, args) => {
15211
15222
  const run = spawnSync9("git", args, {
15212
15223
  cwd,
15213
15224
  encoding: "utf8",
@@ -15423,7 +15434,7 @@ function evaluateObservedGate(task, requirement, statuses, config) {
15423
15434
  return {
15424
15435
  satisfied: [],
15425
15436
  considered,
15426
- reason: requirement.dimension === "testing" && config.checks.length ? `no \`test\` check is configured; add one (or gates.testing) to ${CHECKS_FILE}, then run ${checkCommand("test", runtimeVersion())}` : `no checks are configured or detected; add ${CHECKS_FILE} (committed, so it shows in the diff), e.g. {"checks":{"test":"<your test command>"}}, then run ${checkCommand("test", runtimeVersion())}`
15437
+ reason: requirement.dimension === "testing" && config.checks.length ? `no \`test\` check is configured; add one (or gates.testing) to ${CHECKS_FILE}, then run ${checkCommand("test", runtimeVersion())}` : `no checks are configured or detected; add ${CHECKS_FILE}, e.g. {"checks":{"test":"<your test command>"}}, then run ${checkCommand("test", runtimeVersion())}`
15427
15438
  };
15428
15439
  const satisfied = fresh.filter(named);
15429
15440
  if (satisfied.length)
@@ -22274,6 +22285,102 @@ var init_handle = __esm(() => {
22274
22285
  CLAUDE_WORKTREE = /[\\/]\.claude[\\/]worktrees[\\/]/;
22275
22286
  });
22276
22287
 
22288
+ // packages/workit-core/src/hooks/deadline.ts
22289
+ import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
22290
+ import path28 from "node:path";
22291
+ var HOOK_SKIP_TEXT = "workit context skipped: host under load", HOOK_DEADLINE_MS = 8000, HOST_DEADLINES, HOOK_SKIPS_FILE = "hook-skips.jsonl", override = (env) => {
22292
+ const value = Number(env.WORKIT_HOOK_DEADLINE_MS);
22293
+ return Number.isFinite(value) && value > 0 ? value : null;
22294
+ }, hookDeadlineMs = (env, host = "", native) => {
22295
+ const row = HOST_DEADLINES[host];
22296
+ return override(env) ?? (row ? native === undefined ? Math.min(...Object.values(row)) : row[native] ?? row[""] : HOOK_DEADLINE_MS);
22297
+ }, skipsFile = (stateDir) => path28.join(stateDir, HOOK_SKIPS_FILE), recordHookSkip = (skip, stateDir = resolveStateDir()) => {
22298
+ try {
22299
+ mkdirSync3(stateDir, { recursive: true });
22300
+ appendFileSync(skipsFile(stateDir), `${JSON.stringify(skip)}
22301
+ `);
22302
+ } catch {}
22303
+ }, QUIET, skipDecision = (event) => QUIET.has(event) ? { kind: "none" } : { kind: "context", text: HOOK_SKIP_TEXT }, TIMED_OUT, within = async (work, ms) => {
22304
+ let timer;
22305
+ const expired = new Promise((resolve) => {
22306
+ timer = setTimeout(() => resolve(TIMED_OUT), Math.max(0, ms));
22307
+ timer.unref?.();
22308
+ });
22309
+ try {
22310
+ return await Promise.race([work, expired]);
22311
+ } finally {
22312
+ clearTimeout(timer);
22313
+ }
22314
+ }, isTimedOut = (value) => value === TIMED_OUT, PROCESS_START, hookBudget = (env, options = {}, host = "") => {
22315
+ const now = options.now ?? Date.now;
22316
+ return {
22317
+ now,
22318
+ record: options.record ?? ((skip) => recordHookSkip(skip)),
22319
+ startedAt: options.startedAt ?? now(),
22320
+ deadlineMs: options.deadlineMs ?? hookDeadlineMs(env, host),
22321
+ host: options.deadlineMs === undefined ? host : null
22322
+ };
22323
+ }, readWithin = async (stdin, budget) => {
22324
+ const read = (async () => {
22325
+ let text = "";
22326
+ for await (const chunk of stdin)
22327
+ text += String(chunk);
22328
+ return text;
22329
+ })();
22330
+ const text = await within(read, budget.startedAt + budget.deadlineMs - budget.now());
22331
+ return isTimedOut(text) ? null : text;
22332
+ }, note = (adapter, raw, env, budget, outcome, stage) => {
22333
+ const parsed = raw === undefined ? null : adapter.parse(raw, env);
22334
+ const event = parsed ? parsed.ok ? parsed.input.event.kind : parsed.event : null;
22335
+ budget.record({
22336
+ at: new Date(budget.now()).toISOString(),
22337
+ host: adapter.descriptor.host,
22338
+ event,
22339
+ outcome,
22340
+ stage,
22341
+ elapsedMs: Math.round(budget.now() - budget.startedAt)
22342
+ });
22343
+ return { native: parsed?.native ?? null, event };
22344
+ }, skippedHook = (adapter, raw, env, budget, stage) => {
22345
+ const { native, event } = note(adapter, raw, env, budget, "skip", stage);
22346
+ return {
22347
+ ...adapter.render(skipDecision(event), native),
22348
+ error: `${HOOK_SKIP_TEXT} (past the ${budget.deadlineMs} ms hook deadline)`,
22349
+ skipped: true
22350
+ };
22351
+ }, dispatchWithin = async (adapter, raw, env, budget, dispatch = dispatchHook) => {
22352
+ const native = raw?.hook_event_name;
22353
+ if (budget.host !== null && typeof native === "string")
22354
+ budget.deadlineMs = hookDeadlineMs(env, budget.host, native);
22355
+ const remaining = () => budget.startedAt + budget.deadlineMs - budget.now();
22356
+ const result = remaining() > 0 ? await within(Promise.resolve().then(() => dispatch(adapter, raw, env)), remaining()) : TIMED_OUT;
22357
+ if (isTimedOut(result))
22358
+ return skippedHook(adapter, raw, env, budget, "dispatch");
22359
+ if (remaining() < 0)
22360
+ note(adapter, raw, env, budget, "late", "done");
22361
+ return { ...result, skipped: false };
22362
+ };
22363
+ var init_deadline = __esm(() => {
22364
+ init_logger();
22365
+ init_handle();
22366
+ HOST_DEADLINES = {
22367
+ claude_code: { "": 8000, SessionStart: 16000, Stop: 16000 },
22368
+ codex_cli: { "": 45000, Stop: 16000 },
22369
+ codex_desktop: { "": 45000, Stop: 16000 },
22370
+ cursor: { "": 45000 }
22371
+ };
22372
+ QUIET = new Set([
22373
+ "stop",
22374
+ "subagent.stop",
22375
+ "shell.post",
22376
+ "compact.pre",
22377
+ "prompt.submit",
22378
+ null
22379
+ ]);
22380
+ TIMED_OUT = Symbol("timed out");
22381
+ PROCESS_START = performance.timeOrigin;
22382
+ });
22383
+
22277
22384
  // packages/workit-core/src/hooks/hosts/claude-code.ts
22278
22385
  var CLAUDE_CODE_DESCRIPTOR, EVENTS, SOURCES, SHELL_TOOLS, dialectOf2 = (value) => value.tool_name === "PowerShell" ? "powershell" : "posix", toolCommand = (value) => isRecord6(value.tool_input) ? commandText(value.tool_input.command) : null, eventOf = (name, value) => {
22279
22386
  const toolUseId = optionalText(value.tool_use_id);
@@ -22832,7 +22939,7 @@ var init_codex = __esm(() => {
22832
22939
 
22833
22940
  // packages/workit-core/src/hooks/hosts/cursor.ts
22834
22941
  import fs21 from "node:fs";
22835
- import path28 from "node:path";
22942
+ import path29 from "node:path";
22836
22943
  var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (value) => {
22837
22944
  if (!isRecord6(value) || !Object.hasOwn(EVENTS3, String(value.hook_event_name)))
22838
22945
  return { ok: false, error: "hook_event_name is required" };
@@ -22913,7 +23020,7 @@ var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (valu
22913
23020
  };
22914
23021
  }
22915
23022
  }, TRANSCRIPT_TAIL_BYTES, lastAssistantText = (file) => {
22916
- if (!file || !path28.isAbsolute(file))
23023
+ if (!file || !path29.isAbsolute(file))
22917
23024
  return null;
22918
23025
  try {
22919
23026
  const size = fs21.statSync(file).size;
@@ -22949,14 +23056,14 @@ var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (valu
22949
23056
  } catch {
22950
23057
  return null;
22951
23058
  }
22952
- }, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path28.sep) ? root : `${root}${path28.sep}`), hookCwd = (input) => {
23059
+ }, within2 = (root, dir) => dir === root || dir.startsWith(root.endsWith(path29.sep) ? root : `${root}${path29.sep}`), hookCwd = (input) => {
22953
23060
  const cwd = existingDirectory(input.cwd);
22954
23061
  if (cwd && input.hook_event_name === "beforeShellExecution")
22955
23062
  return cwd;
22956
23063
  return cursorWorkspaceRoot(input);
22957
23064
  }, cursorWorkspaceRoot = (input) => {
22958
23065
  const cwd = existingDirectory(input.cwd);
22959
- return cwd && input.workspace_roots.find((root) => within(root, cwd)) || input.workspace_roots[0];
23066
+ return cwd && input.workspace_roots.find((root) => within2(root, cwd)) || input.workspace_roots[0];
22960
23067
  }, cursorDeny = (reason) => ({
22961
23068
  permission: "deny",
22962
23069
  user_message: "Workit blocked this action",
@@ -23130,19 +23237,21 @@ var init_cursor = __esm(() => {
23130
23237
  });
23131
23238
 
23132
23239
  // packages/workit-core/src/hooks/run.ts
23133
- async function runHookProcess(host, stdin, stdout, stderr = process.stderr, env = process.env) {
23240
+ async function runHookProcess(host, stdin, stdout, stderr = process.stderr, env = process.env, deadline = {}) {
23134
23241
  const adapter = typeof host === "string" ? HOOK_ADAPTERS[host] : host;
23135
- let text = "";
23136
- for await (const chunk of stdin)
23137
- text += String(chunk);
23242
+ const budget = hookBudget(env, deadline, adapter.descriptor.host);
23243
+ const text = await readWithin(stdin, budget);
23138
23244
  let raw;
23139
23245
  try {
23140
- raw = JSON.parse(text || "{}");
23246
+ raw = text === null ? undefined : JSON.parse(text || "{}");
23141
23247
  } catch {
23142
23248
  raw = undefined;
23143
23249
  }
23144
- const result = dispatchHook(adapter, raw, env);
23145
- if (raw === undefined || result.error)
23250
+ const result = text === null ? skippedHook(adapter, undefined, env, budget, "input") : await dispatchWithin(adapter, raw, env, budget, deadline.dispatch);
23251
+ if (result.skipped)
23252
+ stderr.write(`[workit] ${result.error}
23253
+ `);
23254
+ else if (raw === undefined || result.error)
23146
23255
  stderr.write(`[workit] hook input rejected: ${raw === undefined ? "invalid JSON hook input" : result.error}
23147
23256
  `);
23148
23257
  stdout.write(`${JSON.stringify(result.json)}
@@ -23151,7 +23260,7 @@ async function runHookProcess(host, stdin, stdout, stderr = process.stderr, env
23151
23260
  }
23152
23261
  var HOOK_ADAPTERS;
23153
23262
  var init_run = __esm(async () => {
23154
- init_handle();
23263
+ init_deadline();
23155
23264
  init_claude_code();
23156
23265
  init_codex();
23157
23266
  init_cursor();
@@ -23309,6 +23418,14 @@ var hostRef = (handle) => ({ kind: "host", host: "cursor", handle }), cursorCapa
23309
23418
  return { json: {} };
23310
23419
  const note = `[workit: ${gap}; Workit checks are skipped for this action]`;
23311
23420
  return { json: { permission: "allow", user_message: note, agent_message: note } };
23421
+ }, subagentStartStep = (_adapter, raw) => {
23422
+ let json;
23423
+ try {
23424
+ json = handleSubagentStartHook(raw);
23425
+ } catch (error) {
23426
+ json = cursorDeny(`hook failure: ${String(error)}`);
23427
+ }
23428
+ return { json, exitCode: json.permission === "deny" ? 2 : 0, error: null };
23312
23429
  }, runCursorHook = async () => {
23313
23430
  let text = "";
23314
23431
  for await (const chunk of process.stdin)
@@ -23326,24 +23443,16 @@ var hostRef = (handle) => ({ kind: "host", host: "cursor", handle }), cursorCapa
23326
23443
  process.exitCode = 0;
23327
23444
  return;
23328
23445
  }
23329
- if (isSubagentStart(raw)) {
23330
- let output;
23331
- try {
23332
- output = handleSubagentStartHook(raw);
23333
- } catch (error) {
23334
- output = cursorDeny(`hook failure: ${String(error)}`);
23335
- }
23336
- process.stdout.write(`${JSON.stringify(output)}
23337
- `);
23338
- process.exitCode = output.permission === "deny" ? 2 : 0;
23339
- return;
23340
- }
23341
- process.exitCode = await runHookProcess(cursorAdapter, [text], process.stdout);
23446
+ process.exitCode = await runHookProcess(cursorAdapter, [text], process.stdout, process.stderr, process.env, {
23447
+ startedAt: PROCESS_START,
23448
+ ...isSubagentStart(raw) ? { dispatch: subagentStartStep } : {}
23449
+ });
23342
23450
  };
23343
23451
  var init_workit_hook = __esm(async () => {
23344
23452
  init_task_contract();
23345
23453
  init_task_engine();
23346
23454
  init_task_store();
23455
+ init_deadline();
23347
23456
  await __promiseAll([
23348
23457
  init_hooks(),
23349
23458
  init_hooks()
@@ -21250,8 +21250,13 @@ import { spawnSync as spawnSync3 } from "node:child_process";
21250
21250
  import fs9 from "node:fs";
21251
21251
  import path9 from "node:path";
21252
21252
  var SCRIPT_CHECKS = ["test", "lint", "typecheck", "check"];
21253
- var CHECKS_FILE = "workit.checks.json";
21254
- var checksConfigFile = (root) => path9.join(root, CHECKS_FILE);
21253
+ var CHECKS_FILE = ".workit/checks.json";
21254
+ var LEGACY_CHECKS_FILE = "workit.checks.json";
21255
+ function checksConfigFile(root) {
21256
+ const current = path9.join(root, CHECKS_FILE);
21257
+ const legacy = path9.join(root, LEGACY_CHECKS_FILE);
21258
+ return !fs9.existsSync(current) && fs9.existsSync(legacy) ? legacy : current;
21259
+ }
21255
21260
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
21256
21261
  var SHELL_META = /[|&;<>()$`\\*?[\]{}~!#]/u;
21257
21262
  function splitCommand(command) {
@@ -21365,19 +21370,25 @@ var GATES = ["testing", "verification"];
21365
21370
  function loadCheckConfig(cwd) {
21366
21371
  const root = checkRoot(cwd);
21367
21372
  const file = checksConfigFile(root);
21373
+ const legacy = path9.basename(file) === LEGACY_CHECKS_FILE;
21374
+ const label = legacy ? LEGACY_CHECKS_FILE : CHECKS_FILE;
21375
+ const note = legacy ? {
21376
+ note: `${LEGACY_CHECKS_FILE} is deprecated; move it to ${CHECKS_FILE} (workit doctor --fix)`
21377
+ } : {};
21368
21378
  const none = (error = null) => ({
21369
- source: error ? CHECKS_FILE : "none",
21379
+ source: error ? label : "none",
21370
21380
  root,
21371
21381
  checks: [],
21372
21382
  gates: {},
21373
- error
21383
+ error,
21384
+ ...note
21374
21385
  });
21375
21386
  let text = null;
21376
21387
  try {
21377
21388
  text = fs9.readFileSync(file, "utf8");
21378
21389
  } catch (error) {
21379
21390
  if (error.code !== "ENOENT")
21380
- return none(`cannot read ${CHECKS_FILE}: ${error.message}`);
21391
+ return none(`cannot read ${label}: ${error.message}`);
21381
21392
  }
21382
21393
  if (text === null) {
21383
21394
  const checks = detectedChecks(root);
@@ -21387,33 +21398,33 @@ function loadCheckConfig(cwd) {
21387
21398
  try {
21388
21399
  parsed = JSON.parse(text);
21389
21400
  } catch {
21390
- return none(`${CHECKS_FILE} is not valid JSON`);
21401
+ return none(`${label} is not valid JSON`);
21391
21402
  }
21392
21403
  if (!isRecord(parsed) || !isRecord(parsed.checks))
21393
- return none(`${CHECKS_FILE} must be {"checks":{"<name>":"<command>"|["argv",…]}}`);
21404
+ return none(`${label} must be {"checks":{"<name>":"<command>"|["argv",…]}}`);
21394
21405
  const checks = [];
21395
21406
  for (const [name, value] of Object.entries(parsed.checks)) {
21396
21407
  if (!CHECK_NAME.test(name))
21397
- return none(`${CHECKS_FILE}: check name "${name}" must match ${CHECK_NAME.source}`);
21408
+ return none(`${label}: check name "${name}" must match ${CHECK_NAME.source}`);
21398
21409
  const argv = typeof value === "string" ? splitCommand(value) : Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.length > 0) ? value : new Error("expected a command string or a non-empty argv array");
21399
21410
  if (argv instanceof Error)
21400
- return none(`${CHECKS_FILE}: check "${name}": ${argv.message}`);
21411
+ return none(`${label}: check "${name}": ${argv.message}`);
21401
21412
  checks.push({ name, argv, accepts: [argv] });
21402
21413
  }
21403
21414
  const gates = {};
21404
21415
  if (parsed.gates !== undefined) {
21405
21416
  if (!isRecord(parsed.gates))
21406
- return none(`${CHECKS_FILE}: gates must be {"testing":"<name>"|["<name>",…]}`);
21417
+ return none(`${label}: gates must be {"testing":"<name>"|["<name>",…]}`);
21407
21418
  for (const [gate, value] of Object.entries(parsed.gates)) {
21408
21419
  if (!GATES.includes(gate))
21409
- return none(`${CHECKS_FILE}: unknown gate "${gate}" (use ${GATES.join(" or ")})`);
21420
+ return none(`${label}: unknown gate "${gate}" (use ${GATES.join(" or ")})`);
21410
21421
  const names = typeof value === "string" ? [value] : value;
21411
21422
  if (!Array.isArray(names) || names.length === 0 || !names.every((name) => typeof name === "string" && checks.some((c) => c.name === name)))
21412
- return none(`${CHECKS_FILE}: gates.${gate} must name configured checks`);
21423
+ return none(`${label}: gates.${gate} must name configured checks`);
21413
21424
  gates[gate] = names;
21414
21425
  }
21415
21426
  }
21416
- return { source: CHECKS_FILE, root, checks, gates, error: null, resources: parsed.resources };
21427
+ return { source: label, root, checks, gates, error: null, ...note, resources: parsed.resources };
21417
21428
  }
21418
21429
  var sameArgv = (left, right) => left.length === right.length && left.every((item, index) => item === right[index]);
21419
21430
  function matchesNamedCheck(config, name, argv, cwd) {
@@ -24072,7 +24083,7 @@ var productDecision = (task, ledger) => {
24072
24083
  };
24073
24084
  const since = Date.parse(task.createdAt);
24074
24085
  const branch = ledger.branch();
24075
- const row = ledger.rows().findLast((item) => item.type === "decision" && !item.superseded && (item.branch ?? null) === branch && Date.parse(item.at) >= since);
24086
+ const row = ledger.rows().findLast((item) => item.type === "decision" && item.by !== "decider" && !item.superseded && (item.branch ?? null) === branch && Date.parse(item.at) >= since);
24076
24087
  return row ? { met: true, reason: `ledger decision ${row.id} records the choice`, decisionIds: [] } : {
24077
24088
  met: false,
24078
24089
  reason: 'open product choice: ask the user, then `workit ledger decision "<choice>" --why "<reason>"` (or, if it is not open, `workit policy assess --judge product-choice=no --why "<reason>"`)',
@@ -24556,7 +24567,7 @@ function evaluateObservedGate(task, requirement, statuses, config) {
24556
24567
  return {
24557
24568
  satisfied: [],
24558
24569
  considered,
24559
- reason: requirement.dimension === "testing" && config.checks.length ? `no \`test\` check is configured; add one (or gates.testing) to ${CHECKS_FILE}, then run ${checkCommand("test", runtimeVersion())}` : `no checks are configured or detected; add ${CHECKS_FILE} (committed, so it shows in the diff), e.g. {"checks":{"test":"<your test command>"}}, then run ${checkCommand("test", runtimeVersion())}`
24570
+ reason: requirement.dimension === "testing" && config.checks.length ? `no \`test\` check is configured; add one (or gates.testing) to ${CHECKS_FILE}, then run ${checkCommand("test", runtimeVersion())}` : `no checks are configured or detected; add ${CHECKS_FILE}, e.g. {"checks":{"test":"<your test command>"}}, then run ${checkCommand("test", runtimeVersion())}`
24560
24571
  };
24561
24572
  const satisfied = fresh.filter(named);
24562
24573
  if (satisfied.length)
@@ -28828,6 +28839,18 @@ var WRITE_TOOLS = new Set([
28828
28839
  ]);
28829
28840
  // packages/workit-core/src/hooks/handle.ts
28830
28841
  var offered = new Set;
28842
+ // packages/workit-core/src/hooks/deadline.ts
28843
+ var QUIET = new Set([
28844
+ "stop",
28845
+ "subagent.stop",
28846
+ "shell.post",
28847
+ "compact.pre",
28848
+ "prompt.submit",
28849
+ null
28850
+ ]);
28851
+ var TIMED_OUT = Symbol("timed out");
28852
+ var PROCESS_START = performance.timeOrigin;
28853
+
28831
28854
  // packages/workit-core/src/hooks/hosts/claude-code.ts
28832
28855
  var SOURCES = new Set(["startup", "resume", "clear", "compact", "fork"]);
28833
28856
  var SHELL_TOOLS = new Set(["Bash", "PowerShell"]);
@@ -11528,7 +11528,7 @@ var productDecision = (task, ledger) => {
11528
11528
  };
11529
11529
  const since = Date.parse(task.createdAt);
11530
11530
  const branch = ledger.branch();
11531
- const row = ledger.rows().findLast((item) => item.type === "decision" && !item.superseded && (item.branch ?? null) === branch && Date.parse(item.at) >= since);
11531
+ const row = ledger.rows().findLast((item) => item.type === "decision" && item.by !== "decider" && !item.superseded && (item.branch ?? null) === branch && Date.parse(item.at) >= since);
11532
11532
  return row ? { met: true, reason: `ledger decision ${row.id} records the choice`, decisionIds: [] } : {
11533
11533
  met: false,
11534
11534
  reason: 'open product choice: ask the user, then `workit ledger decision "<choice>" --why "<reason>"` (or, if it is not open, `workit policy assess --judge product-choice=no --why "<reason>"`)',
@@ -15071,8 +15071,13 @@ import { spawnSync as spawnSync9 } from "node:child_process";
15071
15071
  import fs12 from "node:fs";
15072
15072
  import path16 from "node:path";
15073
15073
  var SCRIPT_CHECKS = ["test", "lint", "typecheck", "check"];
15074
- var CHECKS_FILE = "workit.checks.json";
15075
- var checksConfigFile = (root) => path16.join(root, CHECKS_FILE);
15074
+ var CHECKS_FILE = ".workit/checks.json";
15075
+ var LEGACY_CHECKS_FILE = "workit.checks.json";
15076
+ function checksConfigFile(root) {
15077
+ const current = path16.join(root, CHECKS_FILE);
15078
+ const legacy = path16.join(root, LEGACY_CHECKS_FILE);
15079
+ return !fs12.existsSync(current) && fs12.existsSync(legacy) ? legacy : current;
15080
+ }
15076
15081
  function repoHeavyCommands(root) {
15077
15082
  try {
15078
15083
  const value = JSON.parse(fs12.readFileSync(checksConfigFile(root), "utf8"));
@@ -15196,19 +15201,25 @@ var GATES = ["testing", "verification"];
15196
15201
  function loadCheckConfig(cwd) {
15197
15202
  const root = checkRoot(cwd);
15198
15203
  const file = checksConfigFile(root);
15204
+ const legacy = path16.basename(file) === LEGACY_CHECKS_FILE;
15205
+ const label = legacy ? LEGACY_CHECKS_FILE : CHECKS_FILE;
15206
+ const note = legacy ? {
15207
+ note: `${LEGACY_CHECKS_FILE} is deprecated; move it to ${CHECKS_FILE} (workit doctor --fix)`
15208
+ } : {};
15199
15209
  const none = (error = null) => ({
15200
- source: error ? CHECKS_FILE : "none",
15210
+ source: error ? label : "none",
15201
15211
  root,
15202
15212
  checks: [],
15203
15213
  gates: {},
15204
- error
15214
+ error,
15215
+ ...note
15205
15216
  });
15206
15217
  let text = null;
15207
15218
  try {
15208
15219
  text = fs12.readFileSync(file, "utf8");
15209
15220
  } catch (error) {
15210
15221
  if (error.code !== "ENOENT")
15211
- return none(`cannot read ${CHECKS_FILE}: ${error.message}`);
15222
+ return none(`cannot read ${label}: ${error.message}`);
15212
15223
  }
15213
15224
  if (text === null) {
15214
15225
  const checks = detectedChecks(root);
@@ -15218,33 +15229,33 @@ function loadCheckConfig(cwd) {
15218
15229
  try {
15219
15230
  parsed = JSON.parse(text);
15220
15231
  } catch {
15221
- return none(`${CHECKS_FILE} is not valid JSON`);
15232
+ return none(`${label} is not valid JSON`);
15222
15233
  }
15223
15234
  if (!isRecord3(parsed) || !isRecord3(parsed.checks))
15224
- return none(`${CHECKS_FILE} must be {"checks":{"<name>":"<command>"|["argv",…]}}`);
15235
+ return none(`${label} must be {"checks":{"<name>":"<command>"|["argv",…]}}`);
15225
15236
  const checks = [];
15226
15237
  for (const [name, value] of Object.entries(parsed.checks)) {
15227
15238
  if (!CHECK_NAME.test(name))
15228
- return none(`${CHECKS_FILE}: check name "${name}" must match ${CHECK_NAME.source}`);
15239
+ return none(`${label}: check name "${name}" must match ${CHECK_NAME.source}`);
15229
15240
  const argv = typeof value === "string" ? splitCommand(value) : Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.length > 0) ? value : new Error("expected a command string or a non-empty argv array");
15230
15241
  if (argv instanceof Error)
15231
- return none(`${CHECKS_FILE}: check "${name}": ${argv.message}`);
15242
+ return none(`${label}: check "${name}": ${argv.message}`);
15232
15243
  checks.push({ name, argv, accepts: [argv] });
15233
15244
  }
15234
15245
  const gates = {};
15235
15246
  if (parsed.gates !== undefined) {
15236
15247
  if (!isRecord3(parsed.gates))
15237
- return none(`${CHECKS_FILE}: gates must be {"testing":"<name>"|["<name>",…]}`);
15248
+ return none(`${label}: gates must be {"testing":"<name>"|["<name>",…]}`);
15238
15249
  for (const [gate, value] of Object.entries(parsed.gates)) {
15239
15250
  if (!GATES.includes(gate))
15240
- return none(`${CHECKS_FILE}: unknown gate "${gate}" (use ${GATES.join(" or ")})`);
15251
+ return none(`${label}: unknown gate "${gate}" (use ${GATES.join(" or ")})`);
15241
15252
  const names = typeof value === "string" ? [value] : value;
15242
15253
  if (!Array.isArray(names) || names.length === 0 || !names.every((name) => typeof name === "string" && checks.some((c) => c.name === name)))
15243
- return none(`${CHECKS_FILE}: gates.${gate} must name configured checks`);
15254
+ return none(`${label}: gates.${gate} must name configured checks`);
15244
15255
  gates[gate] = names;
15245
15256
  }
15246
15257
  }
15247
- return { source: CHECKS_FILE, root, checks, gates, error: null, resources: parsed.resources };
15258
+ return { source: label, root, checks, gates, error: null, ...note, resources: parsed.resources };
15248
15259
  }
15249
15260
  var sameArgv = (left, right) => left.length === right.length && left.every((item, index) => item === right[index]);
15250
15261
  function matchesNamedCheck(config, name, argv, cwd) {
@@ -15711,7 +15722,7 @@ function evaluateObservedGate(task, requirement, statuses, config) {
15711
15722
  return {
15712
15723
  satisfied: [],
15713
15724
  considered,
15714
- reason: requirement.dimension === "testing" && config.checks.length ? `no \`test\` check is configured; add one (or gates.testing) to ${CHECKS_FILE}, then run ${checkCommand("test", runtimeVersion())}` : `no checks are configured or detected; add ${CHECKS_FILE} (committed, so it shows in the diff), e.g. {"checks":{"test":"<your test command>"}}, then run ${checkCommand("test", runtimeVersion())}`
15725
+ reason: requirement.dimension === "testing" && config.checks.length ? `no \`test\` check is configured; add one (or gates.testing) to ${CHECKS_FILE}, then run ${checkCommand("test", runtimeVersion())}` : `no checks are configured or detected; add ${CHECKS_FILE}, e.g. {"checks":{"test":"<your test command>"}}, then run ${checkCommand("test", runtimeVersion())}`
15715
15726
  };
15716
15727
  const satisfied = fresh.filter(named);
15717
15728
  if (satisfied.length)
@@ -22498,6 +22509,112 @@ var dispatchHook = (adapter, raw, env = process.env) => {
22498
22509
  };
22499
22510
  }
22500
22511
  };
22512
+ // packages/workit-core/src/hooks/deadline.ts
22513
+ import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
22514
+ import path28 from "node:path";
22515
+ var HOOK_SKIP_TEXT = "workit context skipped: host under load";
22516
+ var HOOK_DEADLINE_MS = 8000;
22517
+ var HOST_DEADLINES = {
22518
+ claude_code: { "": 8000, SessionStart: 16000, Stop: 16000 },
22519
+ codex_cli: { "": 45000, Stop: 16000 },
22520
+ codex_desktop: { "": 45000, Stop: 16000 },
22521
+ cursor: { "": 45000 }
22522
+ };
22523
+ var HOOK_SKIPS_FILE = "hook-skips.jsonl";
22524
+ var override = (env) => {
22525
+ const value = Number(env.WORKIT_HOOK_DEADLINE_MS);
22526
+ return Number.isFinite(value) && value > 0 ? value : null;
22527
+ };
22528
+ var hookDeadlineMs = (env, host = "", native) => {
22529
+ const row = HOST_DEADLINES[host];
22530
+ return override(env) ?? (row ? native === undefined ? Math.min(...Object.values(row)) : row[native] ?? row[""] : HOOK_DEADLINE_MS);
22531
+ };
22532
+ var skipsFile = (stateDir) => path28.join(stateDir, HOOK_SKIPS_FILE);
22533
+ var recordHookSkip = (skip, stateDir = resolveStateDir()) => {
22534
+ try {
22535
+ mkdirSync3(stateDir, { recursive: true });
22536
+ appendFileSync(skipsFile(stateDir), `${JSON.stringify(skip)}
22537
+ `);
22538
+ } catch {}
22539
+ };
22540
+ var QUIET = new Set([
22541
+ "stop",
22542
+ "subagent.stop",
22543
+ "shell.post",
22544
+ "compact.pre",
22545
+ "prompt.submit",
22546
+ null
22547
+ ]);
22548
+ var skipDecision = (event) => QUIET.has(event) ? { kind: "none" } : { kind: "context", text: HOOK_SKIP_TEXT };
22549
+ var TIMED_OUT = Symbol("timed out");
22550
+ var within = async (work, ms) => {
22551
+ let timer;
22552
+ const expired = new Promise((resolve) => {
22553
+ timer = setTimeout(() => resolve(TIMED_OUT), Math.max(0, ms));
22554
+ timer.unref?.();
22555
+ });
22556
+ try {
22557
+ return await Promise.race([work, expired]);
22558
+ } finally {
22559
+ clearTimeout(timer);
22560
+ }
22561
+ };
22562
+ var isTimedOut = (value) => value === TIMED_OUT;
22563
+ var PROCESS_START = performance.timeOrigin;
22564
+ var hookBudget = (env, options = {}, host = "") => {
22565
+ const now = options.now ?? Date.now;
22566
+ return {
22567
+ now,
22568
+ record: options.record ?? ((skip) => recordHookSkip(skip)),
22569
+ startedAt: options.startedAt ?? now(),
22570
+ deadlineMs: options.deadlineMs ?? hookDeadlineMs(env, host),
22571
+ host: options.deadlineMs === undefined ? host : null
22572
+ };
22573
+ };
22574
+ var readWithin = async (stdin, budget) => {
22575
+ const read = (async () => {
22576
+ let text = "";
22577
+ for await (const chunk of stdin)
22578
+ text += String(chunk);
22579
+ return text;
22580
+ })();
22581
+ const text = await within(read, budget.startedAt + budget.deadlineMs - budget.now());
22582
+ return isTimedOut(text) ? null : text;
22583
+ };
22584
+ var note = (adapter, raw, env, budget, outcome, stage) => {
22585
+ const parsed = raw === undefined ? null : adapter.parse(raw, env);
22586
+ const event = parsed ? parsed.ok ? parsed.input.event.kind : parsed.event : null;
22587
+ budget.record({
22588
+ at: new Date(budget.now()).toISOString(),
22589
+ host: adapter.descriptor.host,
22590
+ event,
22591
+ outcome,
22592
+ stage,
22593
+ elapsedMs: Math.round(budget.now() - budget.startedAt)
22594
+ });
22595
+ return { native: parsed?.native ?? null, event };
22596
+ };
22597
+ var skippedHook = (adapter, raw, env, budget, stage) => {
22598
+ const { native, event } = note(adapter, raw, env, budget, "skip", stage);
22599
+ return {
22600
+ ...adapter.render(skipDecision(event), native),
22601
+ error: `${HOOK_SKIP_TEXT} (past the ${budget.deadlineMs} ms hook deadline)`,
22602
+ skipped: true
22603
+ };
22604
+ };
22605
+ var dispatchWithin = async (adapter, raw, env, budget, dispatch = dispatchHook) => {
22606
+ const native = raw?.hook_event_name;
22607
+ if (budget.host !== null && typeof native === "string")
22608
+ budget.deadlineMs = hookDeadlineMs(env, budget.host, native);
22609
+ const remaining = () => budget.startedAt + budget.deadlineMs - budget.now();
22610
+ const result = remaining() > 0 ? await within(Promise.resolve().then(() => dispatch(adapter, raw, env)), remaining()) : TIMED_OUT;
22611
+ if (isTimedOut(result))
22612
+ return skippedHook(adapter, raw, env, budget, "dispatch");
22613
+ if (remaining() < 0)
22614
+ note(adapter, raw, env, budget, "late", "done");
22615
+ return { ...result, skipped: false };
22616
+ };
22617
+
22501
22618
  // packages/workit-core/src/hooks/hosts/claude-code.ts
22502
22619
  var CLAUDE_CODE_DESCRIPTOR = {
22503
22620
  host: "claude_code",
@@ -23057,7 +23174,7 @@ var codexAdapter = {
23057
23174
 
23058
23175
  // packages/workit-core/src/hooks/hosts/cursor.ts
23059
23176
  import fs21 from "node:fs";
23060
- import path28 from "node:path";
23177
+ import path29 from "node:path";
23061
23178
  var undocumented = { support: "undocumented", native: null };
23062
23179
  var none = { support: "none", native: null };
23063
23180
  var CURSOR_DESCRIPTOR = {
@@ -23254,7 +23371,7 @@ var protocolEvent2 = (input) => {
23254
23371
  };
23255
23372
  var TRANSCRIPT_TAIL_BYTES = 256 * 1024;
23256
23373
  var lastAssistantText = (file) => {
23257
- if (!file || !path28.isAbsolute(file))
23374
+ if (!file || !path29.isAbsolute(file))
23258
23375
  return null;
23259
23376
  try {
23260
23377
  const size = fs21.statSync(file).size;
@@ -23291,7 +23408,7 @@ var lastAssistantText = (file) => {
23291
23408
  return null;
23292
23409
  }
23293
23410
  };
23294
- var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path28.sep) ? root : `${root}${path28.sep}`);
23411
+ var within2 = (root, dir) => dir === root || dir.startsWith(root.endsWith(path29.sep) ? root : `${root}${path29.sep}`);
23295
23412
  var hookCwd = (input) => {
23296
23413
  const cwd = existingDirectory(input.cwd);
23297
23414
  if (cwd && input.hook_event_name === "beforeShellExecution")
@@ -23300,7 +23417,7 @@ var hookCwd = (input) => {
23300
23417
  };
23301
23418
  var cursorWorkspaceRoot = (input) => {
23302
23419
  const cwd = existingDirectory(input.cwd);
23303
- return cwd && input.workspace_roots.find((root) => within(root, cwd)) || input.workspace_roots[0];
23420
+ return cwd && input.workspace_roots.find((root) => within2(root, cwd)) || input.workspace_roots[0];
23304
23421
  };
23305
23422
  var cursorDeny = (reason) => ({
23306
23423
  permission: "deny",
@@ -23365,19 +23482,21 @@ var HOOK_ADAPTERS = {
23365
23482
  codex_desktop: codexAdapter,
23366
23483
  cursor: cursorAdapter
23367
23484
  };
23368
- async function runHookProcess(host, stdin, stdout, stderr = process.stderr, env = process.env) {
23485
+ async function runHookProcess(host, stdin, stdout, stderr = process.stderr, env = process.env, deadline = {}) {
23369
23486
  const adapter = typeof host === "string" ? HOOK_ADAPTERS[host] : host;
23370
- let text = "";
23371
- for await (const chunk of stdin)
23372
- text += String(chunk);
23487
+ const budget = hookBudget(env, deadline, adapter.descriptor.host);
23488
+ const text = await readWithin(stdin, budget);
23373
23489
  let raw;
23374
23490
  try {
23375
- raw = JSON.parse(text || "{}");
23491
+ raw = text === null ? undefined : JSON.parse(text || "{}");
23376
23492
  } catch {
23377
23493
  raw = undefined;
23378
23494
  }
23379
- const result = dispatchHook(adapter, raw, env);
23380
- if (raw === undefined || result.error)
23495
+ const result = text === null ? skippedHook(adapter, undefined, env, budget, "input") : await dispatchWithin(adapter, raw, env, budget, deadline.dispatch);
23496
+ if (result.skipped)
23497
+ stderr.write(`[workit] ${result.error}
23498
+ `);
23499
+ else if (raw === undefined || result.error)
23381
23500
  stderr.write(`[workit] hook input rejected: ${raw === undefined ? "invalid JSON hook input" : result.error}
23382
23501
  `);
23383
23502
  stdout.write(`${JSON.stringify(result.json)}
@@ -23530,6 +23649,15 @@ var degradedCursorHook = (raw) => {
23530
23649
  return { json: { permission: "allow", user_message: note, agent_message: note } };
23531
23650
  };
23532
23651
  var handleCursorHook = (raw) => degradedCursorHook(raw)?.json ?? (isSubagentStart(raw) ? handleSubagentStartHook(raw) : dispatchHook(cursorAdapter, raw).json);
23652
+ var subagentStartStep = (_adapter, raw) => {
23653
+ let json;
23654
+ try {
23655
+ json = handleSubagentStartHook(raw);
23656
+ } catch (error) {
23657
+ json = cursorDeny(`hook failure: ${String(error)}`);
23658
+ }
23659
+ return { json, exitCode: json.permission === "deny" ? 2 : 0, error: null };
23660
+ };
23533
23661
  var runCursorHook = async () => {
23534
23662
  let text = "";
23535
23663
  for await (const chunk of process.stdin)
@@ -23547,19 +23675,10 @@ var runCursorHook = async () => {
23547
23675
  process.exitCode = 0;
23548
23676
  return;
23549
23677
  }
23550
- if (isSubagentStart(raw)) {
23551
- let output;
23552
- try {
23553
- output = handleSubagentStartHook(raw);
23554
- } catch (error) {
23555
- output = cursorDeny(`hook failure: ${String(error)}`);
23556
- }
23557
- process.stdout.write(`${JSON.stringify(output)}
23558
- `);
23559
- process.exitCode = output.permission === "deny" ? 2 : 0;
23560
- return;
23561
- }
23562
- process.exitCode = await runHookProcess(cursorAdapter, [text], process.stdout);
23678
+ process.exitCode = await runHookProcess(cursorAdapter, [text], process.stdout, process.stderr, process.env, {
23679
+ startedAt: PROCESS_START,
23680
+ ...isSubagentStart(raw) ? { dispatch: subagentStartStep } : {}
23681
+ });
23563
23682
  };
23564
23683
  if (__require.main == __require.module)
23565
23684
  await runCursorHook();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-cursor",
3
- "version": "8.17.0",
3
+ "version": "8.18.0",
4
4
  "private": false,
5
5
  "description": "Workit Cursor plugin — shared MCP transport, native hooks, and sixteen method skills",
6
6
  "keywords": [
@@ -46,8 +46,8 @@
46
46
  "build": "bun scripts/build.ts"
47
47
  },
48
48
  "devDependencies": {
49
- "@brainervirus/workit-core": "^8.17.0",
50
- "@brainervirus/workit-mcp": "^8.17.0"
49
+ "@brainervirus/workit-core": "^8.18.0",
50
+ "@brainervirus/workit-mcp": "^8.18.0"
51
51
  },
52
52
  "engines": {
53
53
  "node": ">=24"
@@ -22,7 +22,7 @@ description: Turn requirements into Given/When/Then scenarios, agree the test se
22
22
  then take the next scenario. Any edit makes the observation stale; re-run
23
23
  before you claim it. A recorded "tests pass" is a note, and an ad-hoc
24
24
  `workit check -- <cmd>` never satisfies the gate: only the configured
25
- `test` check does. No `test` detected? Create `workit.checks.json`, copying
25
+ `test` check does. No `test` detected? Create `.workit/checks.json`, copying
26
26
  in every check the repo already runs: once it exists it replaces the
27
27
  detected defaults.
28
28
  5. **Mock only at system boundaries:** network, clock, randomness, other
@@ -37,8 +37,8 @@ units of ~100+ lines each. Coupled work stays with one owner. Workit never spawn
37
37
  6. **Retry once, then escalate.** A stuck or failed slice gets one retry with
38
38
  a fresh brief (`workit fanout brief <slice> --mode resume`, same branch);
39
39
  if that fails too, re-slice it, take it over, or report the gap. Never
40
- chain resumes. A hard slice may race instead: N attempts, keep the best
41
- verdict (`references/orchestration.md`).
40
+ chain resumes. A fix round and recheck that both fail: a decider, not a
41
+ third round; product choices go to the user (`references/orchestration.md`).
42
42
  7. **Verify by the workspace `verification` setting** (`workit grant show`).
43
43
  A report is a pointer, never evidence. `self`: a verifier that wrote none
44
44
  of the slices, or you if you wrote none. `independent` or high risk: a
@@ -43,6 +43,27 @@ rulings). If the retry fails too, escalate instead of a third worker:
43
43
  re-slice it smaller (`workit fanout plan` again), take it over yourself, or
44
44
  report the gap to the user.
45
45
 
46
+ ## Stuck calls: the decider
47
+
48
+ A failed review gets one fix round and one recheck (the changed hunks only).
49
+ If the recheck fails, or a worker and a reviewer disagree, spawn a decider
50
+ instead of a third round. A decider is a fresh read-only agent, not the author
51
+ or the reviewer. It returns three things: the options, a pick by
52
+ reversibility (the option cheapest to undo wins a tie), and the exact change
53
+ list. The fixer applies that list once, then one focused recheck follows.
54
+
55
+ - Hard call (shared contract, security, migration): two deciders on different
56
+ models (Claude Code: a different `model` on the Agent call; OpenCode: a
57
+ child session on another configured agent; Codex, Cursor and Pi: one
58
+ decider, since the host picks no model). If they disagree, you take the
59
+ more reversible option.
60
+ - Record it: `workit ledger decision "<pick>" --by decider --why "<why it is
61
+ the more reversible>" --view "<decider A: view>" --view "<decider B: view>"`.
62
+ The row is listed as a decider call; it is never the user's answer to a
63
+ product choice and never a verdict.
64
+ - A product choice, or authority nobody here has, is not a decider's call:
65
+ ask the user with your recommendation.
66
+
46
67
  ## Race a hard slice
47
68
 
48
69
  When a hard slice has an uncertain approach and a cheap VERIFY, run 2-3
@@ -32,7 +32,7 @@ proposes and stops. Nothing changes until the user approves.
32
32
  more cited occurrences**; a one-off is not a learning.
33
33
  4. **Name the strongest enforcer that works**, strongest first: architecture
34
34
  or types (the mistake cannot be written) > lint rule > configured check
35
- (`workit.checks.json`, `workit check <name>`) or CI job > test >
35
+ (`.workit/checks.json`, `workit check <name>`) or CI job > test >
36
36
  `CODING_STANDARDS.md` (judgment the reviewer reads) > AGENTS.md pointer
37
37
  (navigation only). A mechanical rule gets a check, not prose: a check can
38
38
  fail, a sentence cannot. From types to test, the proof is that the new
@@ -60,7 +60,7 @@ Bad: "Agents seem lost in the build. Added 'read the build docs carefully' to
60
60
  AGENTS.md." One vague occurrence, no citation, a no-op line, auto-applied.
61
61
 
62
62
  Good: "Navigation, 3 occurrences (rulings 12, 19; PR #88 thread): agents sought
63
- check names in package.json, missing `workit.checks.json`. Enforcer: AGENTS.md
63
+ check names in package.json, missing `.workit/checks.json`. Enforcer: AGENTS.md
64
64
  pointer, +74 bytes, minus the stale Commands paragraph (-210). Unstable check,
65
65
  2 occurrences (ledger rows 31, 44: `e2e` red then green on one SHA): a debug
66
66
  slice to fix the check. Approve?"
@@ -36,7 +36,10 @@ independent; `type-check-only` never proves a behavior change.
36
36
  7. Record the verdict:
37
37
  `workit ledger verdict verified|failed|blocked --kind review --branch <b> --how "<what you ran and read>"`
38
38
  under your own session (the one the lead or the hook gave you).
39
- 8. **Harness note** (optional): friction caused by the repo or harness, not
39
+ 8. **Bound the loop.** A failed verdict gets one fix round and one recheck of
40
+ the changed hunks. Still failing, or stuck: no third round; the lead spawns
41
+ a decider (read the `workit-fanout` skill's SKILL.md and follow it). A product choice goes to the user.
42
+ 9. **Harness note** (optional): friction caused by the repo or harness, not
40
43
  the change (a flaky fixture, a missing check, a misleading doc or skill
41
44
  line), goes on its own `Harness note:` line in the report and any PR
42
45
  comment, never as a finding or in the verdict, so a later retro finds it.
@@ -30,7 +30,7 @@ once the verdict is accepted. `merged`: step 7. `null` (closed, not merged):
30
30
  stop and report. Stop early only for a new consequential choice, a host
31
31
  denial, a review comment that needs a product decision, a required update
32
32
  that repeats because the base keeps moving, or after 3 failed fix attempts on
33
- the same check.
33
+ the same check. Fix round and recheck both failed: a decider (read the `workit-fanout` skill's SKILL.md and follow it).
34
34
 
35
35
  1. **Open.** `workit git push`, then `workit pr create --title "<title>"
36
36
  --body-file <f|->` plus `--issue <#n|ID>` per issue (`--refs`: no close; idempotent;
@@ -35,7 +35,7 @@ way a user would.
35
35
  never run is a guess; do not hand it off.
36
36
  4. **Make the driver a named check** when it is cheap and deterministic. A
37
37
  named check is one command without shell syntax (put pipes and setup in a
38
- script). Add `"verify": "<driver>"` to `checks` in `workit.checks.json`; if
38
+ script). Add `"verify": "<driver>"` to `checks` in `.workit/checks.json`; if
39
39
  the file does not exist yet, first copy in every check the repo already runs
40
40
  (test, lint, typecheck), because the file replaces the detected defaults.
41
41
  `workit check verify` then records it, and it joins the verification gate