@brainervirus/workit-claude-code 8.35.2 → 8.37.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "workit",
3
3
  "displayName": "Workit",
4
- "version": "8.35.2",
4
+ "version": "8.37.0",
5
5
  "description": "Workflow rails for agentic coding: task context on every session, branch policy on shell commands, and the workit method skills",
6
6
  "author": {
7
7
  "name": "BrainerVirus"
@@ -13,7 +13,7 @@
13
13
  // policy): a broken hook must never brick the host.
14
14
  import { spawnSync } from "node:child_process";
15
15
  import { createHash } from "node:crypto";
16
- import { existsSync, readFileSync, statSync } from "node:fs";
16
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
17
17
  import os from "node:os";
18
18
  import path from "node:path";
19
19
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -162,6 +162,7 @@ const maybeHeavy = (command, cwd) => {
162
162
  }
163
163
  };
164
164
  const EDITS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
165
+ const READS = new Set(["Read", "Grep", "Glob"]);
165
166
 
166
167
  /** `<git-common-dir>/workit` (or `<dir>/.workit` outside git) holds tasks. */
167
168
  const hasTaskStore = (cwd) => {
@@ -240,6 +241,66 @@ const standingPending = (cwd, session, agentId) => {
240
241
  return readJson(path.join(store, "standing", "seen", `${key}.json`))?.rev !== rev;
241
242
  };
242
243
 
244
+ // The worker tool-call budget: a copy of spawnBudget, noteSpawnBudget and
245
+ // tickBudget in workit-core/src/hooks/budget-signal.ts (a test holds them
246
+ // equal). A spawn whose brief names `N tool calls` queues N for the next
247
+ // subagent of its type; each pre-tool call of a budgeted agent appends one
248
+ // byte to its counter, and only the call that reaches the next multiple of N
249
+ // loads the runtime, which tells the agent once.
250
+ const budgetHash = (text) => createHash("sha256").update(text).digest("hex").slice(0, 32);
251
+ const spawnBudget = (prompt) => {
252
+ const found = /^TIMEBOX:[^\n]*?\b(\d+)\s+tool calls\b/mu.exec(prompt);
253
+ const n = found ? Number(found[1]) : NaN;
254
+ return Number.isInteger(n) && n > 0 ? n : null;
255
+ };
256
+ const noteSpawnBudget = (cwd, session, type, prompt) => {
257
+ try {
258
+ const store = session ? gitStoreDir(cwd) : null;
259
+ if (!store) return;
260
+ mkdirSync(path.join(store, "budget"), { recursive: true });
261
+ appendFileSync(
262
+ path.join(store, "budget", `spawns-${budgetHash(session)}.jsonl`),
263
+ `${JSON.stringify({ type: type ?? "", n: spawnBudget(prompt) })}\n`,
264
+ );
265
+ } catch {
266
+ // Without the note the worker runs unbudgeted.
267
+ }
268
+ };
269
+ const tickBudget = (cwd, session, agentId) => {
270
+ try {
271
+ const store = session ? gitStoreDir(cwd) : null;
272
+ if (!store) return false;
273
+ const base = path.join(store, "budget", budgetHash(`${session}\0${agentId ?? ""}`));
274
+ const budget = readJson(`${base}.json`);
275
+ if (typeof budget?.limit !== "number" || budget.limit <= 0) return false;
276
+ const told = typeof budget.told === "number" ? budget.told : 0;
277
+ appendFileSync(`${base}.calls`, ".");
278
+ return statSync(`${base}.calls`).size >= budget.limit * (told + 1);
279
+ } catch {
280
+ return false;
281
+ }
282
+ };
283
+ const parsed = (() => {
284
+ try {
285
+ return JSON.parse(payload);
286
+ } catch {
287
+ return undefined;
288
+ }
289
+ })();
290
+ // Counted before any fast path, so a call the fast path answers counts too.
291
+ const budgetDue = (() => {
292
+ try {
293
+ const value = parsed;
294
+ if (value?.hook_event_name !== "PreToolUse" || typeof value.session_id !== "string")
295
+ return false;
296
+ return typeof value.agent_id === "string" && value.agent_id !== ""
297
+ ? tickBudget(String(value.cwd ?? "."), value.session_id, value.agent_id)
298
+ : false;
299
+ } catch {
300
+ return false;
301
+ }
302
+ })();
303
+
243
304
  // The plugin dir this hook runs from (`<config>/plugins/cache/<mkt>/workit/<version>`)
244
305
  // against the newest version the registry lists beside it: while the line has not
245
306
  // been given this session, the runtime runs (src/stale-plugin.ts holds the same test).
@@ -277,7 +338,8 @@ const markerFor = (session, installed) =>
277
338
 
278
339
  const fastAllow = (() => {
279
340
  try {
280
- const value = JSON.parse(payload);
341
+ if (parsed === undefined) return false;
342
+ const value = parsed;
281
343
  const command = value?.tool_input?.command;
282
344
  if (
283
345
  value?.hook_event_name === "PreToolUse" &&
@@ -288,6 +350,7 @@ const fastAllow = (() => {
288
350
  if (value?.hook_event_name === "PostToolUse")
289
351
  return typeof command === "string" && !/\bcommit\b/.test(command);
290
352
  if (value?.hook_event_name !== "PreToolUse") return false;
353
+ if (budgetDue) return false;
291
354
  if (mentionsIssued(JSON.stringify(value.tool_input ?? {}))) return false;
292
355
  if (standingPending(String(value.cwd ?? "."), value.session_id, value.agent_id)) return false;
293
356
  // An implementer's writes are confined to its Workit worktree with or
@@ -315,14 +378,49 @@ const fastAllow = (() => {
315
378
  return false;
316
379
  return typeof spawn.model === "string" && spawn.model.trim() !== "";
317
380
  }
381
+ // Read, Grep and Glob reach this hook only to be counted against a budget.
382
+ if (READS.has(value.tool_name)) return true;
318
383
  return EDITS.has(value.tool_name) && !implementer && !hasTaskStore(String(value.cwd ?? "."));
319
384
  } catch {
320
385
  return false;
321
386
  }
322
387
  })();
323
388
 
389
+ // A lead's spawn the fast path allows queues its budget or none (the runtime
390
+ // queues one its gate lets through); a stopped subagent's budget files go.
391
+ // Copies of handle.ts noteClaudeSpawn and budget-signal.ts dropBudget.
392
+ const settleBudget = () => {
393
+ try {
394
+ const value = parsed;
395
+ const session = value?.session_id;
396
+ if (typeof session !== "string" || !session) return;
397
+ const cwd = String(value.cwd ?? ".");
398
+ if (value.hook_event_name === "SubagentStop") {
399
+ const store = gitStoreDir(cwd);
400
+ if (!store || typeof value.agent_id !== "string" || !value.agent_id) return;
401
+ const base = path.join(store, "budget", budgetHash(`${session}\0${value.agent_id}`));
402
+ rmSync(`${base}.json`, { force: true });
403
+ rmSync(`${base}.calls`, { force: true });
404
+ return;
405
+ }
406
+ const spawn = value.tool_input;
407
+ if (fastAllow && !value.agent_id && (value.tool_name === "Agent" || value.tool_name === "Task"))
408
+ noteSpawnBudget(
409
+ cwd,
410
+ session,
411
+ typeof spawn?.subagent_type === "string" && spawn.subagent_type
412
+ ? spawn.subagent_type
413
+ : "general-purpose",
414
+ typeof spawn?.prompt === "string" ? spawn.prompt : "",
415
+ );
416
+ } catch {
417
+ // Budgets fail open.
418
+ }
419
+ };
420
+ settleBudget();
421
+
324
422
  process.env.WORKIT_PLUGIN_ROOT = root;
325
- if (fastAllow) {
423
+ if (parsed?.hook_event_name === "SubagentStop" || fastAllow) {
326
424
  process.stdout.write("{}\n");
327
425
  } else if (fromSource) {
328
426
  const run = spawnSync(process.env.WORKIT_BUN ?? "bun", [source], {