@brainervirus/workit-core 8.33.0 → 8.34.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "8.33.0",
3
+ "version": "8.34.0",
4
4
  "private": false,
5
5
  "description": "Workit shared core — task, policy, evidence, review, decision, worker, and writer state for agentic coding workflows",
6
6
  "keywords": [
@@ -0,0 +1,96 @@
1
+ // A tiny record of which hook events each host ran (#433): without it "zero
2
+ // hook events" cannot tell a host that never fires its hooks from one that
3
+ // fires them silently. One small file per host and native event in the state
4
+ // dir (`hook-runs/<host>__<event>.json`: last seen time and a count), written
5
+ // best effort: one read and one atomic-enough write, no spawn, and a
6
+ // read-only state dir never changes the hook's answer. `workit doctor`
7
+ // (`hook_runs`) reads it. Light imports only: the Codex launcher's quiet path
8
+ // calls it before the runtime loads.
9
+ import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import path from "node:path";
11
+ import { stateDir } from "../core/heavy-words";
12
+
13
+ export const HOOK_RUNS_DIR = "hook-runs";
14
+
15
+ export type HookRun = {
16
+ host: string;
17
+ event: string;
18
+ at: string;
19
+ count: number;
20
+ };
21
+
22
+ const safe = (part: string): string => part.replace(/[^A-Za-z0-9_.-]/g, "-").slice(0, 64);
23
+
24
+ const runFile = (dir: string, host: string, event: string): string =>
25
+ path.join(dir, HOOK_RUNS_DIR, `${safe(host)}__${safe(event)}.json`);
26
+
27
+ /** Counts one run of the native `event` on `host`; never throws. */
28
+ export const recordHookRun = (
29
+ host: string,
30
+ event: string | null | undefined,
31
+ env: NodeJS.ProcessEnv = process.env,
32
+ now: () => number = Date.now,
33
+ ): void => {
34
+ if (!event) return;
35
+ try {
36
+ const dir = stateDir(env);
37
+ const file = runFile(dir, host, event);
38
+ let count = 0;
39
+ try {
40
+ const previous = JSON.parse(readFileSync(file, "utf8")) as {
41
+ count?: unknown;
42
+ };
43
+ if (typeof previous.count === "number" && Number.isFinite(previous.count))
44
+ count = previous.count;
45
+ } catch {
46
+ // First run, or a torn file: start again.
47
+ }
48
+ const row: HookRun = {
49
+ host,
50
+ event,
51
+ at: new Date(now()).toISOString(),
52
+ count: count + 1,
53
+ };
54
+ try {
55
+ writeFileSync(file, JSON.stringify(row));
56
+ } catch {
57
+ mkdirSync(path.dirname(file), { recursive: true });
58
+ writeFileSync(file, JSON.stringify(row));
59
+ }
60
+ } catch {
61
+ // The hook still answers the host; only the doctor line is lost.
62
+ }
63
+ };
64
+
65
+ /** Every recorded run under `stateDir`; unreadable or malformed files are skipped. */
66
+ export const readHookRuns = (dir: string): HookRun[] => {
67
+ const runs: HookRun[] = [];
68
+ let names: string[] = [];
69
+ try {
70
+ names = readdirSync(path.join(dir, HOOK_RUNS_DIR));
71
+ } catch {
72
+ return runs;
73
+ }
74
+ for (const name of names) {
75
+ try {
76
+ const row = JSON.parse(
77
+ readFileSync(path.join(dir, HOOK_RUNS_DIR, name), "utf8"),
78
+ ) as Partial<HookRun>;
79
+ if (
80
+ typeof row.host === "string" &&
81
+ typeof row.event === "string" &&
82
+ typeof row.at === "string" &&
83
+ typeof row.count === "number"
84
+ )
85
+ runs.push({
86
+ host: row.host,
87
+ event: row.event,
88
+ at: row.at,
89
+ count: row.count,
90
+ });
91
+ } catch {
92
+ // Not a run record.
93
+ }
94
+ }
95
+ return runs;
96
+ };
package/src/hooks/run.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  import { claudeCodeAdapter } from "./hosts/claude-code";
12
12
  import { codexAdapter } from "./hosts/codex";
13
13
  import { cursorAdapter } from "./hosts/cursor";
14
+ import { recordHookRun } from "./hook-runs";
14
15
  import type { HostAdapter, HostId } from "./protocol";
15
16
 
16
17
  type ProcessHost = Exclude<HostId, "opencode" | "pi">;
@@ -44,6 +45,12 @@ export async function runHookProcess(
44
45
  } catch {
45
46
  raw = undefined;
46
47
  }
48
+ const native = (raw as { hook_event_name?: unknown } | null | undefined)?.hook_event_name;
49
+ recordHookRun(
50
+ typeof host === "string" ? host : adapter.descriptor.host,
51
+ typeof native === "string" ? native : null,
52
+ env,
53
+ );
47
54
  const result =
48
55
  text === null
49
56
  ? skippedHook(adapter, undefined, env, budget, "input")