@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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/workit-hook.mjs +101 -3
- package/dist/workit-hook.js +95 -91
- package/dist/workit.js +23 -9
- package/hooks/hooks.json +24 -1
- package/package.json +3 -3
- package/skills/implement/SKILL.md +1 -1
- package/skills/ship/SKILL.md +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workit",
|
|
3
3
|
"displayName": "Workit",
|
|
4
|
-
"version": "8.
|
|
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"
|
package/bin/workit-hook.mjs
CHANGED
|
@@ -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
|
-
|
|
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], {
|