@joenandez/academy 0.4.0 → 0.5.12

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 (121) hide show
  1. package/.claude-plugin/marketplace.json +5 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +390 -0
  4. package/README.md +67 -122
  5. package/hooks/backstory/.claude-plugin/plugin.json +11 -0
  6. package/hooks/backstory/SOURCE +2 -0
  7. package/hooks/backstory/bin/backstory +9 -0
  8. package/hooks/backstory/hooks/hooks.json +45 -0
  9. package/hooks/backstory/scripts/knowledge.mjs +87 -0
  10. package/hooks/backstory/scripts/memory.mjs +685 -0
  11. package/hooks/backstory/scripts/tool-tick.mjs +1 -0
  12. package/hooks/backstory/skills/capture/SKILL.md +71 -0
  13. package/hooks/backstory/skills/capture/references/knowledge-capture-input.json +16 -0
  14. package/hooks/backstory/skills/capture/references/tagging-policy.md +29 -0
  15. package/hooks/backstory/skills/memory/SKILL.md +70 -0
  16. package/hooks/backstory/skills/setup/SKILL.md +85 -0
  17. package/hooks/context_metrics.mjs +67 -0
  18. package/hooks/hook_runtime.mjs +43 -2
  19. package/hooks/memory_bridge.mjs +286 -52
  20. package/hooks/memory_store.mjs +78 -26
  21. package/hooks/register_session.mjs +76 -14
  22. package/hooks/roque/.claude-plugin/plugin.json +6 -0
  23. package/hooks/roque/LICENSE +21 -0
  24. package/hooks/roque/SOURCE +1 -0
  25. package/hooks/roque/hooks.json +38 -0
  26. package/hooks/roque/package.json +3 -0
  27. package/hooks/roque/roque.js +1690 -0
  28. package/hooks/sync_memory.mjs +11 -7
  29. package/package.json +7 -3
  30. package/scripts/cli/agent-gate.mjs +70 -0
  31. package/scripts/cli/agent-id.mjs +37 -0
  32. package/scripts/cli/archive.mjs +39 -38
  33. package/scripts/cli/archived.mjs +4 -2
  34. package/scripts/cli/args-kinds.mjs +33 -0
  35. package/scripts/cli/args-notes.mjs +42 -0
  36. package/scripts/cli/args-owner.mjs +37 -0
  37. package/scripts/cli/args-run.mjs +38 -0
  38. package/scripts/cli/args.mjs +47 -104
  39. package/scripts/cli/beads.mjs +57 -0
  40. package/scripts/cli/codex.mjs +66 -12
  41. package/scripts/cli/core.mjs +117 -41
  42. package/scripts/cli/create.mjs +224 -76
  43. package/scripts/cli/curator-feed-codex.mjs +81 -0
  44. package/scripts/cli/curator-feed-observations.mjs +97 -0
  45. package/scripts/cli/curator-feed-scope.mjs +124 -0
  46. package/scripts/cli/curator-feed-sources.mjs +172 -0
  47. package/scripts/cli/curator-feed.mjs +318 -0
  48. package/scripts/cli/default-name.mjs +73 -0
  49. package/scripts/cli/doctor.mjs +67 -2
  50. package/scripts/cli/eventlog.mjs +6 -2
  51. package/scripts/cli/exit-code.mjs +9 -0
  52. package/scripts/cli/first-run.mjs +115 -0
  53. package/scripts/cli/heartbeat.mjs +135 -0
  54. package/scripts/cli/helm-skip.mjs +20 -0
  55. package/scripts/cli/hire-headless.mjs +37 -22
  56. package/scripts/cli/hire.mjs +8 -2
  57. package/scripts/cli/inspect.mjs +36 -18
  58. package/scripts/cli/lifecycle.mjs +95 -49
  59. package/scripts/cli/main.mjs +32 -2
  60. package/scripts/cli/memory-env.mjs +124 -0
  61. package/scripts/cli/migrate.mjs +13 -7
  62. package/scripts/cli/next-steps.mjs +13 -0
  63. package/scripts/cli/nightly-launch.mjs +118 -0
  64. package/scripts/cli/nightly.mjs +228 -0
  65. package/scripts/cli/notes.mjs +58 -8
  66. package/scripts/cli/owner-config.mjs +181 -0
  67. package/scripts/cli/owner-jobs.mjs +201 -0
  68. package/scripts/cli/prompt-append.mjs +135 -0
  69. package/scripts/cli/rename.mjs +90 -109
  70. package/scripts/cli/run-context.mjs +50 -0
  71. package/scripts/cli/run.mjs +236 -79
  72. package/scripts/cli/scaffold.mjs +119 -126
  73. package/scripts/cli/schedule-errors.mjs +70 -0
  74. package/scripts/cli/sessions.mjs +10 -3
  75. package/scripts/cli/skills-check.mjs +147 -0
  76. package/scripts/cli/skills.mjs +170 -0
  77. package/scripts/cli/start.mjs +201 -0
  78. package/scripts/cli/templates.mjs +119 -45
  79. package/scripts/cli/tldr.mjs +184 -0
  80. package/scripts/cli/toml-string.mjs +76 -0
  81. package/scripts/cli/user-work-view.mjs +48 -0
  82. package/scripts/cli/writable-dirs.mjs +23 -0
  83. package/scripts/cli/yaml.mjs +1 -1
  84. package/skills/hire/SKILL.md +112 -100
  85. package/skills/hire/references/owner.md +68 -0
  86. package/templates/agents/claude-code/knowledge-curator.md +5 -2
  87. package/templates/agents/codex/knowledge-curator.toml +2 -1
  88. package/templates/operating-model/base.md +78 -0
  89. package/templates/operating-model/owner.md +11 -0
  90. package/templates/operating-model/principal.md +9 -0
  91. package/templates/operating-model/specialist.md +9 -0
  92. package/templates/packs/owner/craft.md +32 -0
  93. package/templates/packs/owner/identity.md +13 -0
  94. package/templates/packs/owner/mandate.md +26 -0
  95. package/templates/packs/owner/owner/SKILL.md +118 -0
  96. package/templates/packs/owner/owner-feedback/SKILL.md +49 -0
  97. package/templates/packs/owner/owner-heartbeat/SKILL.md +35 -0
  98. package/templates/packs/owner/owner-nightly/SKILL.md +114 -0
  99. package/templates/packs/owner/owner-review/SKILL.md +70 -0
  100. package/templates/packs/owner/owner-work/SKILL.md +53 -0
  101. package/templates/packs/principal/craft.md +27 -0
  102. package/templates/packs/principal/identity.md +13 -0
  103. package/templates/packs/principal/mandate.md +23 -0
  104. package/templates/packs/principal/principal-brief/SKILL.md +44 -0
  105. package/templates/packs/principal/principal-intro/SKILL.md +95 -0
  106. package/templates/packs/principal/principal-nightly/SKILL.md +152 -0
  107. package/templates/skills/check-in/SKILL.md +34 -24
  108. package/templates/skills/knowledge-curation/SKILL.md +89 -48
  109. package/templates/skills/local-rules/SKILL.md +86 -0
  110. package/templates/skills/nightly-consolidation/SKILL.md +131 -100
  111. package/templates/skills/skill-method/SKILL.md +79 -0
  112. package/templates/skills/user-nightly/SKILL.md +143 -0
  113. package/templates/skills/work-graph/SKILL.md +75 -0
  114. package/conformance/README.md +0 -60
  115. package/conformance/discovery.test.mjs +0 -140
  116. package/conformance/envelope.test.mjs +0 -185
  117. package/conformance/error-codes.test.mjs +0 -125
  118. package/conformance/harness.mjs +0 -180
  119. package/conformance/identity.test.mjs +0 -125
  120. package/docs/integration-guide.md +0 -1042
  121. package/templates/skills/self-update/SKILL.md +0 -121
@@ -0,0 +1,1690 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/main.ts
26
+ var import_node_fs10 = __toESM(require("node:fs"));
27
+ var import_node_path10 = __toESM(require("node:path"));
28
+
29
+ // src/capture.ts
30
+ var import_node_child_process = require("node:child_process");
31
+ var import_node_fs = __toESM(require("node:fs"));
32
+ var import_node_path = __toESM(require("node:path"));
33
+ var VALUE_FIELDS = [
34
+ "session_id",
35
+ "transcript_path",
36
+ "cwd",
37
+ "hook_event_name",
38
+ "source",
39
+ "trigger",
40
+ "model",
41
+ "tool_name",
42
+ "permission_mode",
43
+ "turn_id"
44
+ ];
45
+ var OPAQUE_FIELDS = /* @__PURE__ */ new Set(["tool_input", "tool_response", "tool_output"]);
46
+ var ENV_VALUE_FIELDS = ["CLAUDE_PROJECT_DIR", "PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT", "PLUGIN_DATA", "CLAUDE_PLUGIN_DATA"];
47
+ var ENV_NAME_PATTERN = /^(CLAUDE|CODEX|PLUGIN|ANTHROPIC|OPENAI)/;
48
+ var COMPACT_ENV_PATTERN = /COMPACT/i;
49
+ var MAX_ANCESTORS = 6;
50
+ function shapeOf(value, depth) {
51
+ if (Array.isArray(value)) return value.length && depth > 0 ? [shapeOf(value[0], depth - 1)] : "array";
52
+ if (value === null) return "null";
53
+ if (typeof value !== "object") return typeof value;
54
+ if (depth <= 0) return "object";
55
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, shapeOf(v, depth - 1)]));
56
+ }
57
+ function payloadShape(payload) {
58
+ return Object.fromEntries(
59
+ Object.entries(payload).map(([k, v]) => [k, shapeOf(v, OPAQUE_FIELDS.has(k) ? 1 : 4)])
60
+ );
61
+ }
62
+ function ancestors() {
63
+ const chain = [];
64
+ let pid = process.ppid;
65
+ for (let i = 0; i < MAX_ANCESTORS && pid > 1; i++) {
66
+ let line = "";
67
+ try {
68
+ line = (0, import_node_child_process.execFileSync)("ps", ["-o", "ppid=,args=", "-p", String(pid)], { encoding: "utf8" }).trim();
69
+ } catch {
70
+ break;
71
+ }
72
+ const [, ppid, args] = line.match(/^(\d+)\s+(.*)$/) ?? [];
73
+ if (!args) break;
74
+ const autocompact = args.match(/--autocompact[= ](\S+)/)?.[1];
75
+ chain.push({ pid, exe: import_node_path.default.basename(args.split(" ")[0]), ...autocompact ? { autocompact } : {} });
76
+ pid = Number(ppid);
77
+ }
78
+ return chain;
79
+ }
80
+ function captureRecord({ tool, event, payload, env }) {
81
+ const summary = payload.compact_summary;
82
+ return {
83
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
84
+ tool,
85
+ event,
86
+ values: Object.fromEntries(VALUE_FIELDS.filter((k) => k in payload).map((k) => [k, payload[k]])),
87
+ shape: payloadShape(payload),
88
+ ...typeof summary === "string" ? { compact_summary_chars: summary.length } : {},
89
+ env: Object.fromEntries(
90
+ Object.entries(env).filter(([k]) => ENV_VALUE_FIELDS.includes(k) || COMPACT_ENV_PATTERN.test(k))
91
+ ),
92
+ env_names: Object.keys(env).filter((k) => ENV_NAME_PATTERN.test(k)).sort(),
93
+ ancestors: ancestors()
94
+ };
95
+ }
96
+ function canaryFile(dir, { tool, event, payload }) {
97
+ const source = typeof payload.source === "string" ? `-${payload.source}` : "";
98
+ return import_node_path.default.join(dir, `emit-${tool}-${event}${source}.txt`);
99
+ }
100
+ function capture(dir, ctx) {
101
+ import_node_fs.default.appendFileSync(import_node_path.default.join(dir, "capture.jsonl"), JSON.stringify(captureRecord(ctx)) + "\n");
102
+ const canary = canaryFile(dir, ctx);
103
+ if (!import_node_fs.default.existsSync(canary)) return void 0;
104
+ const text = import_node_fs.default.readFileSync(canary, "utf8").replaceAll("{session_id}", String(ctx.payload.session_id));
105
+ return { hookSpecificOutput: { hookEventName: ctx.event, additionalContext: text } };
106
+ }
107
+ function captureHandlers(dir) {
108
+ const handler = (ctx) => capture(dir, ctx);
109
+ return { PostToolUse: handler, SessionStart: handler, PostCompact: handler };
110
+ }
111
+
112
+ // src/home.ts
113
+ var import_node_os = __toESM(require("node:os"));
114
+ var import_node_path2 = __toESM(require("node:path"));
115
+
116
+ // src/name.ts
117
+ var NAME = "roque";
118
+ var ENV_PREFIX = NAME.toUpperCase();
119
+ var PROJECT_DIR = `.${NAME}`;
120
+ var PROJECT_STATE_DIR = `${PROJECT_DIR}/state`;
121
+ var PROJECT_OFF_FILE = `${PROJECT_DIR}/off`;
122
+ var PROJECT_GITIGNORE_FILE = `${PROJECT_DIR}/.gitignore`;
123
+ var HOME_DIR = `.${NAME}`;
124
+ var HOME_LOGS_DIR = "logs";
125
+ var HOME_SETTINGS_FLAG_FILE = "settings-applied.json";
126
+ var HOME_PROJECTS_DIR = "projects";
127
+ function homeCompactionsDir(projectKey, contextKey) {
128
+ return `${HOME_PROJECTS_DIR}/${projectKey}/contexts/${contextKey}/compactions`;
129
+ }
130
+ function sessionFileStem(session) {
131
+ return session.replace(/[^A-Za-z0-9_-]/g, "_") || "_";
132
+ }
133
+ var HOME_ENV = `${ENV_PREFIX}_HOME`;
134
+ var CAPTURE_ENV = `${ENV_PREFIX}_CAPTURE`;
135
+
136
+ // src/home.ts
137
+ function claudeConfigDir(env, userHome = import_node_os.default.homedir()) {
138
+ return env.CLAUDE_CONFIG_DIR || import_node_path2.default.join(userHome, ".claude");
139
+ }
140
+ function roqueHome(env) {
141
+ return env[HOME_ENV] || import_node_path2.default.join(import_node_os.default.homedir(), HOME_DIR);
142
+ }
143
+
144
+ // src/hook.ts
145
+ var TOOLS = ["claude", "codex"];
146
+ function isPlainObject(value) {
147
+ return typeof value === "object" && value !== null && !Array.isArray(value);
148
+ }
149
+ function parseTool(argv) {
150
+ const value = argv[argv.indexOf("--tool") + 1];
151
+ if (!argv.includes("--tool") || !TOOLS.includes(value)) {
152
+ throw new Error(`expected --tool claude|codex, got ${JSON.stringify(argv)}`);
153
+ }
154
+ return value;
155
+ }
156
+ function parsePayload(stdin) {
157
+ const payload = JSON.parse(stdin);
158
+ if (!isPlainObject(payload)) throw new Error("hook stdin is not a JSON object");
159
+ return payload;
160
+ }
161
+ function serialise(result) {
162
+ if (result === void 0) return "";
163
+ if (!isPlainObject(result)) throw new Error(`handler result is not a JSON object: ${typeof result}`);
164
+ return JSON.stringify(result);
165
+ }
166
+ async function runHook(stdin, argv, env, handlers, onError) {
167
+ let tool;
168
+ let event;
169
+ try {
170
+ tool = parseTool(argv);
171
+ const payload = parsePayload(stdin);
172
+ event = String(payload.hook_event_name ?? "");
173
+ const handler = handlers[event];
174
+ if (!handler) return "";
175
+ const projectDir = env.CLAUDE_PROJECT_DIR || String(payload.cwd ?? "");
176
+ return serialise(await handler({ tool, event, projectDir, payload, env }));
177
+ } catch (error) {
178
+ try {
179
+ onError({ tool, event, error });
180
+ } catch {
181
+ }
182
+ return "";
183
+ }
184
+ }
185
+
186
+ // src/limit.ts
187
+ var import_node_child_process3 = require("node:child_process");
188
+ var import_node_fs4 = __toESM(require("node:fs"));
189
+ var import_node_os2 = __toESM(require("node:os"));
190
+ var import_node_path5 = __toESM(require("node:path"));
191
+
192
+ // src/log.ts
193
+ var import_node_fs3 = __toESM(require("node:fs"));
194
+ var import_node_path4 = __toESM(require("node:path"));
195
+
196
+ // src/scope.ts
197
+ var import_node_child_process2 = require("node:child_process");
198
+ var import_node_crypto = __toESM(require("node:crypto"));
199
+ var import_node_fs2 = __toESM(require("node:fs"));
200
+ var import_node_path3 = __toESM(require("node:path"));
201
+ var GIT_BUDGET_MS = 2e3;
202
+ var FIXED_GITS = ["/usr/bin/git", "/opt/homebrew/bin/git", "/usr/local/bin/git"];
203
+ var hashIdentity = (scope, value) => import_node_crypto.default.createHash("sha256").update(`spectre-memory/v1/${scope}\0${value}`).digest("hex");
204
+ function cwdScope(dir, source) {
205
+ const projectKey = hashIdentity("cwd", dir);
206
+ return { projectKey, contextKey: hashIdentity("context", projectKey), source };
207
+ }
208
+ function gitEnv(env) {
209
+ const kept = Object.fromEntries(Object.entries(env).filter(([key]) => !key.startsWith("GIT_")));
210
+ return { ...kept, GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_NOSYSTEM: "1" };
211
+ }
212
+ function findGit(env, fixedGits) {
213
+ const onPath = (env.PATH ?? "").split(import_node_path3.default.delimiter).filter((dir) => import_node_path3.default.isAbsolute(dir)).map((dir) => import_node_path3.default.join(dir, "git"));
214
+ return [...onPath, ...fixedGits].find((file) => {
215
+ try {
216
+ return import_node_fs2.default.statSync(file).isFile();
217
+ } catch {
218
+ return false;
219
+ }
220
+ });
221
+ }
222
+ function runGit(git, dir, args, env, deadline) {
223
+ const result = (0, import_node_child_process2.spawnSync)(git, args, {
224
+ cwd: dir,
225
+ env: gitEnv(env),
226
+ encoding: "utf8",
227
+ stdio: ["ignore", "pipe", "ignore"],
228
+ timeout: Math.max(1, deadline - Date.now())
229
+ });
230
+ return { ok: result.status === 0, lines: (result.stdout ?? "").split("\n"), error: result.error };
231
+ }
232
+ function gitScope(dir, env, fixedGits) {
233
+ const git = findGit(env, fixedGits);
234
+ if (!git) return void 0;
235
+ const deadline = Date.now() + GIT_BUDGET_MS;
236
+ const first = runGit(git, dir, ["rev-parse", "--show-toplevel", "--git-common-dir", "HEAD", "--abbrev-ref=loose", "HEAD"], env, deadline);
237
+ if (first.error) throw first.error;
238
+ const [toplevel, commonDir, head, abbrev] = first.lines;
239
+ if (!toplevel) return void 0;
240
+ const projectKey = hashIdentity("project", `local:${import_node_fs2.default.realpathSync(import_node_path3.default.resolve(dir, commonDir))}`);
241
+ let branch;
242
+ if (first.ok && abbrev !== "HEAD") branch = abbrev;
243
+ else if (!first.ok) {
244
+ const ref = runGit(git, dir, ["symbolic-ref", "--quiet", "--short", "HEAD"], env, deadline);
245
+ if (!ref.ok) throw ref.error ?? new Error("git HEAD is unresolvable");
246
+ branch = ref.lines[0];
247
+ }
248
+ if (!(branch || head)) throw new Error("git HEAD is unresolvable");
249
+ const context = branch ? `branch:${branch}` : `detached:${head}`;
250
+ return { projectKey, contextKey: hashIdentity("context", `${projectKey}:${context}`), ...branch ? { branch } : {}, source: "git" };
251
+ }
252
+ function resolveScope(projectDir, env = process.env, fixedGits = FIXED_GITS) {
253
+ let dir;
254
+ try {
255
+ dir = import_node_fs2.default.realpathSync(projectDir);
256
+ } catch {
257
+ return cwdScope(import_node_path3.default.resolve(projectDir), "cwd_fallback");
258
+ }
259
+ try {
260
+ return gitScope(dir, env, fixedGits) ?? cwdScope(dir, "cwd");
261
+ } catch {
262
+ return cwdScope(dir, "cwd_fallback");
263
+ }
264
+ }
265
+ var memo = /* @__PURE__ */ new Map();
266
+ function scopeOf(projectDir) {
267
+ let scope = memo.get(projectDir);
268
+ if (!scope) memo.set(projectDir, scope = resolveScope(projectDir));
269
+ return scope;
270
+ }
271
+
272
+ // src/log.ts
273
+ var UNAVAILABLE = "unavailable";
274
+ var RETRY_MS = 10;
275
+ function logsDir(home) {
276
+ return import_node_path4.default.join(home, HOME_LOGS_DIR);
277
+ }
278
+ function logPath(home, session) {
279
+ return import_node_path4.default.join(logsDir(home), `${sessionFileStem(session)}.jsonl`);
280
+ }
281
+ function lockPath(home, session) {
282
+ return import_node_path4.default.join(logsDir(home), `${sessionFileStem(session)}.lock`);
283
+ }
284
+ function breakerPath(home, session) {
285
+ return import_node_path4.default.join(logsDir(home), `${sessionFileStem(session)}.break.lock`);
286
+ }
287
+ function scopeFields(project) {
288
+ if (!project) return {};
289
+ const { projectKey, contextKey, branch, source } = scopeOf(project);
290
+ return {
291
+ project_key: projectKey,
292
+ context_key: contextKey,
293
+ ...branch ? { branch } : {},
294
+ ...source === "cwd_fallback" ? { scope_source: source } : {}
295
+ };
296
+ }
297
+ function prepareScope(project) {
298
+ if (project) scopeOf(project);
299
+ }
300
+ function appendEvent(home, input, now = /* @__PURE__ */ new Date()) {
301
+ const event = {
302
+ ts: now.toISOString(),
303
+ event: input.event,
304
+ tool: input.tool,
305
+ session: input.session,
306
+ project: input.project,
307
+ ...scopeFields(input.project),
308
+ observed_tokens: input.observed_tokens ?? UNAVAILABLE,
309
+ params: input.params ?? {},
310
+ result: input.result ?? null,
311
+ status: input.status ?? "ok",
312
+ latency_ms: input.latency_ms ?? null,
313
+ ...input.stack === void 0 ? {} : { stack: input.stack }
314
+ };
315
+ import_node_fs3.default.mkdirSync(logsDir(home), { recursive: true });
316
+ import_node_fs3.default.appendFileSync(logPath(home, input.session), JSON.stringify(event) + "\n");
317
+ return event;
318
+ }
319
+ function readEvents(home, session) {
320
+ let text;
321
+ try {
322
+ text = import_node_fs3.default.readFileSync(logPath(home, session), "utf8");
323
+ } catch {
324
+ return [];
325
+ }
326
+ const events = [];
327
+ for (const line of text.split("\n")) {
328
+ if (!line) continue;
329
+ try {
330
+ const parsed = JSON.parse(line);
331
+ if (parsed && typeof parsed === "object" && typeof parsed.event === "string") {
332
+ events.push(parsed);
333
+ }
334
+ } catch {
335
+ }
336
+ }
337
+ return events;
338
+ }
339
+ function marks(home, session) {
340
+ const current = { nudge: false, warning: false };
341
+ for (const { event } of readEvents(home, session)) {
342
+ if (event === "handback") {
343
+ current.nudge = false;
344
+ current.warning = false;
345
+ } else if (event === "nudge" || event === "warning") {
346
+ current[event] = true;
347
+ }
348
+ }
349
+ return current;
350
+ }
351
+ function sleepSync(ms) {
352
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
353
+ }
354
+ function createExclusive(file, token) {
355
+ let fd;
356
+ try {
357
+ fd = import_node_fs3.default.openSync(file, "wx");
358
+ } catch (error) {
359
+ if (error.code === "EEXIST") return false;
360
+ throw error;
361
+ }
362
+ try {
363
+ import_node_fs3.default.writeSync(fd, token);
364
+ } catch (error) {
365
+ import_node_fs3.default.closeSync(fd);
366
+ import_node_fs3.default.rmSync(file, { force: true });
367
+ throw error;
368
+ }
369
+ import_node_fs3.default.closeSync(fd);
370
+ return true;
371
+ }
372
+ function lockIdentity(lock) {
373
+ let stats;
374
+ try {
375
+ stats = import_node_fs3.default.statSync(lock);
376
+ } catch {
377
+ return void 0;
378
+ }
379
+ let token;
380
+ try {
381
+ token = import_node_fs3.default.readFileSync(lock, "utf8");
382
+ } catch {
383
+ token = null;
384
+ }
385
+ return { ino: stats.ino, mtimeMs: stats.mtimeMs, token };
386
+ }
387
+ var sameLock = (a, b) => a.ino === b.ino && a.mtimeMs === b.mtimeMs && a.token === b.token;
388
+ function breakIfStale(lock, breaker, staleMs, token) {
389
+ const seen = lockIdentity(lock);
390
+ if (!seen || Date.now() - seen.mtimeMs <= staleMs) return;
391
+ try {
392
+ if (!createExclusive(breaker, token)) {
393
+ if (Date.now() - import_node_fs3.default.statSync(breaker).mtimeMs > staleMs) import_node_fs3.default.unlinkSync(breaker);
394
+ return;
395
+ }
396
+ } catch {
397
+ return;
398
+ }
399
+ try {
400
+ const current = lockIdentity(lock);
401
+ if (current && sameLock(current, seen)) import_node_fs3.default.unlinkSync(lock);
402
+ } catch {
403
+ } finally {
404
+ releaseClaim(breaker, token);
405
+ }
406
+ }
407
+ function claimSession(home, session, options = {}) {
408
+ return claimLock(lockPath(home, session), breakerPath(home, session), options);
409
+ }
410
+ function claimLock(lock, breaker, options) {
411
+ const { timeoutMs = 2e3, staleMs = 15e3 } = options;
412
+ const token = `${process.pid}:${Date.now()}:${Math.random()}`;
413
+ const deadline = Date.now() + timeoutMs;
414
+ import_node_fs3.default.mkdirSync(import_node_path4.default.dirname(lock), { recursive: true });
415
+ for (; ; ) {
416
+ if (createExclusive(lock, token)) return () => releaseClaim(lock, token);
417
+ breakIfStale(lock, breaker, staleMs, token);
418
+ if (Date.now() >= deadline) return void 0;
419
+ sleepSync(RETRY_MS + Math.floor(Math.random() * RETRY_MS));
420
+ }
421
+ }
422
+ function releaseClaim(lock, token) {
423
+ try {
424
+ if (import_node_fs3.default.readFileSync(lock, "utf8") === token) import_node_fs3.default.unlinkSync(lock);
425
+ } catch {
426
+ }
427
+ }
428
+
429
+ // src/limit.ts
430
+ var CLAUDE_BUFFER = 33e3;
431
+ var CLAUDE_PCT_BASE_OFFSET = 2e4;
432
+ var WINDOW_1M = 1e6;
433
+ var WINDOW_DEFAULT = 2e5;
434
+ var NATIVE_1M_MODEL = /claude-(opus|sonnet|fable)-[5-9]/;
435
+ var CODEX_KEY = "model_auto_compact_token_limit";
436
+ var PS_TIMEOUT_MS = 1e3;
437
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
438
+ var PS_LINE = /^\s*\w{3}\s+(\w{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})\s+(.*)$/;
439
+ var unavailable = (source) => ({ limit: UNAVAILABLE, source });
440
+ function parentProcessProbe(pid = process.ppid) {
441
+ let cached;
442
+ const read = () => {
443
+ if (cached) return cached;
444
+ cached = {};
445
+ try {
446
+ const line = (0, import_node_child_process3.execFileSync)("ps", ["-o", "lstart=,args=", "-p", String(pid)], {
447
+ encoding: "utf8",
448
+ env: { ...process.env, LC_ALL: "C" },
449
+ timeout: PS_TIMEOUT_MS,
450
+ stdio: ["ignore", "pipe", "ignore"]
451
+ }).trim();
452
+ const match = line.match(PS_LINE);
453
+ if (match) {
454
+ const [, month, day, hh, mm, ss, year, args] = match;
455
+ const started = new Date(Number(year), MONTHS.indexOf(month), Number(day), Number(hh), Number(mm), Number(ss));
456
+ cached = { args, startedAt: Number.isNaN(started.getTime()) ? void 0 : started };
457
+ }
458
+ } catch {
459
+ }
460
+ return cached;
461
+ };
462
+ return { args: () => read().args, startedAt: () => read().startedAt };
463
+ }
464
+ function tokenCount(value) {
465
+ if (typeof value === "number") return Number.isFinite(value) && value > 0 ? Math.round(value) : void 0;
466
+ if (typeof value !== "string") return void 0;
467
+ const match = value.trim().match(/^(\d+(?:\.\d+)?)([kKmM]?)$/);
468
+ if (!match) return void 0;
469
+ const scale = { "": 1, k: 1e3, m: 1e6 }[match[2].toLowerCase()];
470
+ const count = Math.round(Number(match[1]) * scale);
471
+ return count > 0 ? count : void 0;
472
+ }
473
+ var isAuto = (value) => typeof value === "string" && value.trim().toLowerCase() === "auto";
474
+ function lastMatch(text, pattern) {
475
+ let found;
476
+ for (const match of text.matchAll(pattern)) found = match[1];
477
+ return found;
478
+ }
479
+ function hostArgs(probe, host) {
480
+ const args = probe.args();
481
+ if (!args) return void 0;
482
+ return args.split(/\s+/).slice(0, 2).some((token) => token.includes(host)) ? args : void 0;
483
+ }
484
+ function readJson(file) {
485
+ let text;
486
+ try {
487
+ text = import_node_fs4.default.readFileSync(file, "utf8");
488
+ } catch {
489
+ return {};
490
+ }
491
+ try {
492
+ const data = JSON.parse(text);
493
+ if (data && typeof data === "object" && !Array.isArray(data)) return { data };
494
+ } catch {
495
+ }
496
+ return { unreadable: true };
497
+ }
498
+ function claudeSettingsLayers(input) {
499
+ const configDir = claudeConfigDir(input.env, input.userHome);
500
+ const layers = [];
501
+ if (input.projectDir) {
502
+ layers.push({ layer: "local", file: import_node_path5.default.join(input.projectDir, ".claude", "settings.local.json") });
503
+ layers.push({ layer: "project", file: import_node_path5.default.join(input.projectDir, ".claude", "settings.json") });
504
+ }
505
+ layers.push({ layer: "user", file: import_node_path5.default.join(configDir, "settings.json") });
506
+ return layers;
507
+ }
508
+ function writtenAfterLaunch(input, userValue) {
509
+ const { data: flag } = readJson(import_node_path5.default.join(input.home, HOME_SETTINGS_FLAG_FILE));
510
+ if (flag?.wrote !== true || typeof flag.value !== "number" || userValue !== flag.value) return false;
511
+ const appliedAt = Date.parse(String(flag.appliedAt));
512
+ const started = input.probe.startedAt();
513
+ return !Number.isNaN(appliedAt) && started !== void 0 && started.getTime() < appliedAt;
514
+ }
515
+ function claudeConfiguredWindow(input, notes) {
516
+ const envValue = input.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW;
517
+ if (envValue) return { base: "env", raw: envValue };
518
+ const args = hostArgs(input.probe, "claude");
519
+ if (args === void 0) notes.push("argv_unreadable");
520
+ const flagValue = args && lastMatch(args, /(?:^|\s)--autocompact(?:=|\s+)(\S+)/g);
521
+ if (flagValue) return { base: "argv", raw: flagValue };
522
+ const settings = claudeSettingsLayers(input).flatMap(({ layer, file }) => {
523
+ const { data, unreadable } = readJson(file);
524
+ if (unreadable) notes.push(`unreadable_settings:${layer}`);
525
+ return data ? [{ layer, data }] : [];
526
+ });
527
+ for (const { layer, data } of settings) {
528
+ const perModel = input.model ? data.modelSettings?.[input.model]?.autoCompactWindow : void 0;
529
+ if (perModel !== void 0) return { base: `model_setting:${layer}`, raw: perModel };
530
+ }
531
+ for (const { layer, data } of settings) {
532
+ if (data.autoCompactWindow === void 0) continue;
533
+ if (layer === "user" && writtenAfterLaunch(input, data.autoCompactWindow)) {
534
+ notes.push("unapplied_setting_ignored");
535
+ continue;
536
+ }
537
+ return { base: `setting:${layer}`, raw: data.autoCompactWindow };
538
+ }
539
+ return void 0;
540
+ }
541
+ function claudeModelWindow(model) {
542
+ if (!model) return void 0;
543
+ return NATIVE_1M_MODEL.test(model) || model.includes("[1m]") ? WINDOW_1M : WINDOW_DEFAULT;
544
+ }
545
+ function claudeLimit(input) {
546
+ const notes = [];
547
+ const configured = claudeConfiguredWindow(input, notes);
548
+ const modelWindow = claudeModelWindow(input.model);
549
+ let window;
550
+ let source;
551
+ if (configured && isAuto(configured.raw)) {
552
+ if (modelWindow === void 0) return unavailable("model_unknown");
553
+ window = modelWindow;
554
+ source = `${configured.base}:auto`;
555
+ } else if (configured) {
556
+ const count = tokenCount(configured.raw);
557
+ if (count === void 0) return unavailable(`invalid:${configured.base}`);
558
+ window = count;
559
+ source = configured.base;
560
+ } else {
561
+ if (modelWindow === void 0) return unavailable("model_unknown");
562
+ window = modelWindow;
563
+ source = "default";
564
+ }
565
+ if (modelWindow === void 0 && window > WINDOW_DEFAULT) return unavailable("model_unknown");
566
+ if (modelWindow !== void 0 && window > modelWindow) {
567
+ window = modelWindow;
568
+ source += "+cap";
569
+ }
570
+ let point = window - CLAUDE_BUFFER;
571
+ const pct = input.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE;
572
+ if (pct && /^\d+(\.\d+)?$/.test(pct) && Number(pct) > 0) {
573
+ point = Math.min(Number(pct) / 100 * (window - CLAUDE_PCT_BASE_OFFSET), point);
574
+ source += "+pct";
575
+ }
576
+ point = Math.round(point);
577
+ if (point <= 0) return unavailable(`invalid:${source}`);
578
+ return { limit: point, source: [source, ...notes].join(";") };
579
+ }
580
+ function tomlTopLevelLimit(file) {
581
+ let text;
582
+ try {
583
+ text = import_node_fs4.default.readFileSync(file, "utf8");
584
+ } catch {
585
+ return void 0;
586
+ }
587
+ for (const line of text.split("\n")) {
588
+ if (line.trim().startsWith("[")) return void 0;
589
+ const match = line.match(/^\s*"?model_auto_compact_token_limit"?\s*=\s*(.*?)\s*(?:#.*)?$/);
590
+ if (match) return match[1];
591
+ }
592
+ return void 0;
593
+ }
594
+ function tomlInteger(raw) {
595
+ if (!/^\+?\d[\d_]*$/.test(raw)) return void 0;
596
+ const value = Number(raw.replace(/[+_]/g, ""));
597
+ return value > 0 ? value : void 0;
598
+ }
599
+ function codexLimit(input) {
600
+ if (!input.contextWindow) return unavailable("window_unknown");
601
+ const derived = Math.round(0.9 * Math.round(input.contextWindow / 0.95));
602
+ const codexHome = input.env.CODEX_HOME || import_node_path5.default.join(input.userHome ?? import_node_os2.default.homedir(), ".codex");
603
+ const notes = [];
604
+ const args = hostArgs(input.probe, "codex");
605
+ if (args === void 0) notes.push("argv_unreadable");
606
+ const cli = args && lastMatch(args, new RegExp(`(?:^|\\s)(?:-c|--config)(?:=|\\s+)${CODEX_KEY}=(\\S+)`, "g"));
607
+ const profile = args && lastMatch(args, /(?:^|\s)(?:-p|--profile)(?:=|\s+)([A-Za-z0-9_.-]+)(?=\s|$)/g);
608
+ const layers = [
609
+ ["cli", () => cli || void 0],
610
+ ["project", () => input.projectDir ? tomlTopLevelLimit(import_node_path5.default.join(input.projectDir, ".codex", "config.toml")) : void 0],
611
+ ["profile", () => profile && !profile.includes("..") ? tomlTopLevelLimit(import_node_path5.default.join(codexHome, `${profile}.config.toml`)) : void 0],
612
+ ["global", () => tomlTopLevelLimit(import_node_path5.default.join(codexHome, "config.toml"))]
613
+ ];
614
+ for (const [base, read] of layers) {
615
+ const raw = read();
616
+ if (raw === void 0) continue;
617
+ const configured = tomlInteger(raw);
618
+ if (configured === void 0) return unavailable(`invalid:${base}`);
619
+ const source = configured > derived ? `${base}+cap` : base;
620
+ return { limit: Math.min(configured, derived), source: [source, ...notes].join(";") };
621
+ }
622
+ return { limit: derived, source: ["window", ...notes].join(";") };
623
+ }
624
+
625
+ // src/meter.ts
626
+ var import_node_fs5 = __toESM(require("node:fs"));
627
+ var DEFAULT_CHUNK_BYTES = 64 * 1024;
628
+ var DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
629
+ var NEWLINE = 10;
630
+ var OPEN_BRACE = 123;
631
+ var CLOSE_BRACE = 125;
632
+ var SYNTHETIC_MODEL = "<synthetic>";
633
+ var CLAUDE_MARKERS = ['"usage"', '"compact_boundary"'];
634
+ function claudeUsageSum(line) {
635
+ const message = line.message;
636
+ if (line.type !== "assistant" || !message?.usage || message.model === SYNTHETIC_MODEL) return "none";
637
+ const { input_tokens, cache_creation_input_tokens = 0, cache_read_input_tokens = 0 } = message.usage;
638
+ const parts = [input_tokens, cache_creation_input_tokens, cache_read_input_tokens];
639
+ if (!parts.every((n) => typeof n === "number")) return "unknown_shape";
640
+ return input_tokens + cache_creation_input_tokens + cache_read_input_tokens;
641
+ }
642
+ function claudeScanner() {
643
+ let tokens;
644
+ let model;
645
+ return {
646
+ candidateMarkers: CLAUDE_MARKERS,
647
+ take(line) {
648
+ if (line.isSidechain === true) return true;
649
+ if (line.subtype === "compact_boundary") {
650
+ const post = line.compactMetadata?.postTokens;
651
+ tokens ??= typeof post === "number" ? post : 0;
652
+ return true;
653
+ }
654
+ const sum = claudeUsageSum(line);
655
+ if (sum === "none") return true;
656
+ if (sum === "unknown_shape") return false;
657
+ tokens ??= sum;
658
+ if (typeof line.message.model === "string") model ??= line.message.model;
659
+ return true;
660
+ },
661
+ done: () => tokens !== void 0 && model !== void 0,
662
+ result: () => tokens === void 0 ? void 0 : { tokens, ...model ? { model } : {} }
663
+ };
664
+ }
665
+ function isCodexTurnBoundary(line) {
666
+ const item = line.payload;
667
+ if (line.type !== "response_item" || !item) return false;
668
+ if (item.type === "agent_message") return true;
669
+ if (item.type !== "message" || item.role !== "user") return false;
670
+ const text = item.content?.find?.((part) => typeof part?.text === "string")?.text ?? "";
671
+ return !text.startsWith("<") && !text.startsWith("# AGENTS.md");
672
+ }
673
+ function codexReasoningTokens(line) {
674
+ const encrypted = line.payload?.encrypted_content;
675
+ if (typeof encrypted !== "string") return 0;
676
+ const bytes = Math.floor(encrypted.length * 3 / 4) - 650;
677
+ return bytes > 0 ? Math.ceil(bytes / 4) : 0;
678
+ }
679
+ function codexScanner() {
680
+ let tokens;
681
+ let contextWindow;
682
+ let pastBoundary = false;
683
+ let reasoning = 0;
684
+ let compacted = false;
685
+ return {
686
+ candidateMarkers: ['"token_count"', '"compacted"', '"type":"reasoning"', '"type":"agent_message"', '"role":"user"'],
687
+ take(line) {
688
+ if (line.type === "compacted") {
689
+ tokens ??= 0;
690
+ compacted = true;
691
+ return true;
692
+ }
693
+ if (line.type === "response_item") {
694
+ if (line.payload?.type === "reasoning" && pastBoundary) reasoning += codexReasoningTokens(line);
695
+ else if (isCodexTurnBoundary(line)) pastBoundary = true;
696
+ return true;
697
+ }
698
+ if (line.type !== "event_msg" || line.payload?.type !== "token_count") return true;
699
+ const info = line.payload.info;
700
+ if (info === null) return true;
701
+ const input = info?.last_token_usage?.input_tokens;
702
+ if (typeof input !== "number") return false;
703
+ if (typeof info.model_context_window === "number") contextWindow ??= info.model_context_window;
704
+ if (input > 0) tokens ??= input;
705
+ return true;
706
+ },
707
+ done: () => compacted && contextWindow !== void 0,
708
+ result: () => tokens === void 0 ? void 0 : { tokens: tokens + reasoning, ...contextWindow ? { contextWindow } : {} }
709
+ };
710
+ }
711
+ function claudePreCompactionScanner() {
712
+ let reading;
713
+ return {
714
+ candidateMarkers: CLAUDE_MARKERS,
715
+ take(line) {
716
+ if (line.isSidechain === true) return true;
717
+ if (line.subtype === "compact_boundary") {
718
+ const pre = line.compactMetadata?.preTokens;
719
+ if (typeof pre !== "number") return false;
720
+ reading = { tokens: pre, tokensSource: "boundary" };
721
+ return true;
722
+ }
723
+ const sum = claudeUsageSum(line);
724
+ if (sum === "none") return true;
725
+ if (sum === "unknown_shape") return false;
726
+ reading = { tokens: sum, tokensSource: "last_usage" };
727
+ return true;
728
+ },
729
+ done: () => reading !== void 0,
730
+ result: () => reading
731
+ };
732
+ }
733
+ function codexPreCompactionScanner() {
734
+ let compactions = 0;
735
+ let tokens;
736
+ return {
737
+ candidateMarkers: ['"token_count"', '"compacted"'],
738
+ take(line) {
739
+ if (line.type === "compacted") {
740
+ compactions++;
741
+ return true;
742
+ }
743
+ if (compactions === 0 || line.type !== "event_msg" || line.payload?.type !== "token_count") return true;
744
+ const info = line.payload.info;
745
+ if (info === null) return true;
746
+ const input = info?.last_token_usage?.input_tokens;
747
+ if (typeof input !== "number") return false;
748
+ if (input > 0) tokens = input;
749
+ return true;
750
+ },
751
+ done: () => tokens !== void 0 || compactions > 1,
752
+ result: () => tokens === void 0 ? void 0 : { tokens }
753
+ };
754
+ }
755
+ function* linesFromEnd(fd, size, chunkBytes, maxBytes) {
756
+ let position = size;
757
+ let scanned = 0;
758
+ let partial = [];
759
+ while (position > 0 && scanned < maxBytes) {
760
+ const length = Math.min(chunkBytes, position);
761
+ position -= length;
762
+ scanned += length;
763
+ const chunk = Buffer.allocUnsafe(length);
764
+ import_node_fs5.default.readSync(fd, chunk, 0, length, position);
765
+ let end = length;
766
+ let newline = chunk.lastIndexOf(NEWLINE, end - 1);
767
+ while (newline !== -1) {
768
+ const head = chunk.subarray(newline + 1, end);
769
+ const line = partial.length ? Buffer.concat([head, ...partial]) : head;
770
+ partial = [];
771
+ if (line.length) yield line;
772
+ end = newline;
773
+ newline = end > 0 ? chunk.lastIndexOf(NEWLINE, end - 1) : -1;
774
+ }
775
+ if (end > 0) partial.unshift(chunk.subarray(0, end));
776
+ }
777
+ if (position === 0 && partial.length) yield Buffer.concat(partial);
778
+ }
779
+ function looksLikeObject(line) {
780
+ return line[0] === OPEN_BRACE && line[line.length - 1] === CLOSE_BRACE;
781
+ }
782
+ function parseObject(line) {
783
+ try {
784
+ const parsed = JSON.parse(line.toString("utf8"));
785
+ return parsed && typeof parsed === "object" ? parsed : void 0;
786
+ } catch {
787
+ return void 0;
788
+ }
789
+ }
790
+ function parseLine(line, markers) {
791
+ const isCandidate = markers.some((marker) => line.includes(marker));
792
+ if (!looksLikeObject(line)) return isCandidate ? "broken_candidate" : "garbage";
793
+ if (!isCandidate) return "skip";
794
+ return parseObject(line) ?? "broken_candidate";
795
+ }
796
+ function endsWithNewline(fd, size) {
797
+ if (size === 0) return true;
798
+ const last = Buffer.alloc(1);
799
+ import_node_fs5.default.readSync(fd, last, 0, 1, size - 1);
800
+ return last[0] === NEWLINE;
801
+ }
802
+ function scanFile(file, scanner, chunkBytes, maxBytes) {
803
+ let fd;
804
+ try {
805
+ fd = import_node_fs5.default.openSync(file, "r");
806
+ } catch {
807
+ return { tokens: UNAVAILABLE, reason: "unreadable" };
808
+ }
809
+ let sawUnknownShape = false;
810
+ try {
811
+ const { size } = import_node_fs5.default.fstatSync(fd);
812
+ let newestInProgress = !endsWithNewline(fd, size);
813
+ for (const line of linesFromEnd(fd, size, chunkBytes, maxBytes)) {
814
+ if (newestInProgress && !parseObject(line)) return { tokens: UNAVAILABLE, reason: "no_reading" };
815
+ newestInProgress = false;
816
+ const parsed = parseLine(line, scanner.candidateMarkers);
817
+ if (parsed === "skip") continue;
818
+ if (parsed === "garbage") {
819
+ sawUnknownShape = true;
820
+ continue;
821
+ }
822
+ if (parsed === "broken_candidate") {
823
+ sawUnknownShape = true;
824
+ break;
825
+ }
826
+ if (!scanner.take(parsed)) {
827
+ sawUnknownShape = true;
828
+ break;
829
+ }
830
+ if (scanner.done()) break;
831
+ }
832
+ } catch {
833
+ return { tokens: UNAVAILABLE, reason: "unreadable" };
834
+ } finally {
835
+ import_node_fs5.default.closeSync(fd);
836
+ }
837
+ return scanner.result() ?? { tokens: UNAVAILABLE, reason: sawUnknownShape ? "unparsed" : "no_reading" };
838
+ }
839
+ function scanTranscript(transcriptPath, scanner, options) {
840
+ if (typeof transcriptPath !== "string" || !transcriptPath) return { tokens: UNAVAILABLE, reason: "no_transcript" };
841
+ const { chunkBytes = DEFAULT_CHUNK_BYTES, maxBytes = DEFAULT_MAX_BYTES } = options;
842
+ return scanFile(transcriptPath, scanner, chunkBytes, maxBytes);
843
+ }
844
+ function contextTokens(transcriptPath, tool, options = {}) {
845
+ return scanTranscript(transcriptPath, tool === "claude" ? claudeScanner() : codexScanner(), options);
846
+ }
847
+ function preCompactionTokens(transcriptPath, tool, options = {}) {
848
+ return scanTranscript(transcriptPath, tool === "claude" ? claudePreCompactionScanner() : codexPreCompactionScanner(), options);
849
+ }
850
+ function recordUnparsedOnce(home, ids, reading) {
851
+ if (reading.reason !== "unparsed") return false;
852
+ const logged = () => readEvents(home, ids.session).some((e) => e.event === "meter_unparsed");
853
+ if (logged()) return false;
854
+ prepareScope(ids.project);
855
+ const release = claimSession(home, ids.session, { timeoutMs: 500 });
856
+ if (!release) return false;
857
+ try {
858
+ if (logged()) return false;
859
+ appendEvent(home, { event: "meter_unparsed", ...ids, status: "warn", params: { reason: reading.reason } });
860
+ return true;
861
+ } finally {
862
+ release();
863
+ }
864
+ }
865
+
866
+ // src/prune.ts
867
+ var import_node_fs6 = __toESM(require("node:fs"));
868
+ var import_node_path6 = __toESM(require("node:path"));
869
+ var SUMMARY_FILE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z\.md$/;
870
+ var DAY_MS = 864e5;
871
+ var LOG_RETENTION_MS = 14 * DAY_MS;
872
+ var SUMMARY_RETENTION_MS = 30 * DAY_MS;
873
+ var LOG_FILE_RE = /\.(jsonl|lock)$/;
874
+ var ANY = "*";
875
+ var SUMMARY_DIR_PATTERN = homeCompactionsDir(ANY, ANY).split("/");
876
+ function errorReason(error) {
877
+ return error.code ?? String(error);
878
+ }
879
+ function deleteOldFiles(dir, nameRe, cutoffMs, skipped2) {
880
+ let names;
881
+ try {
882
+ names = import_node_fs6.default.readdirSync(dir);
883
+ } catch (error) {
884
+ skipped2.push({ path: dir, reason: errorReason(error) });
885
+ return 0;
886
+ }
887
+ let deleted = 0;
888
+ for (const name of names) {
889
+ if (!nameRe.test(name)) continue;
890
+ const file = import_node_path6.default.join(dir, name);
891
+ try {
892
+ const stat = import_node_fs6.default.lstatSync(file);
893
+ if (!stat.isFile() || stat.mtimeMs >= cutoffMs) continue;
894
+ import_node_fs6.default.unlinkSync(file);
895
+ deleted++;
896
+ } catch (error) {
897
+ skipped2.push({ path: file, reason: errorReason(error) });
898
+ }
899
+ }
900
+ return deleted;
901
+ }
902
+ function subdirs(dir, name, skipped2) {
903
+ let entries;
904
+ try {
905
+ entries = import_node_fs6.default.readdirSync(dir, { withFileTypes: true });
906
+ } catch (error) {
907
+ if (error.code !== "ENOENT") skipped2.push({ path: dir, reason: errorReason(error) });
908
+ return [];
909
+ }
910
+ return entries.filter((entry) => entry.isDirectory() && (name === ANY || entry.name === name)).map((entry) => import_node_path6.default.join(dir, entry.name));
911
+ }
912
+ function prune(home, options = {}) {
913
+ const nowMs = (options.now ?? /* @__PURE__ */ new Date()).getTime();
914
+ const skipped2 = [];
915
+ let logsDeleted = 0;
916
+ let summariesDeleted = 0;
917
+ try {
918
+ const logs = import_node_path6.default.join(home, HOME_LOGS_DIR);
919
+ if (import_node_fs6.default.existsSync(logs)) logsDeleted = deleteOldFiles(logs, LOG_FILE_RE, nowMs - LOG_RETENTION_MS, skipped2);
920
+ const summaryDirs = SUMMARY_DIR_PATTERN.reduce((dirs, name) => dirs.flatMap((dir) => subdirs(dir, name, skipped2)), [home]);
921
+ for (const dir of summaryDirs) summariesDeleted += deleteOldFiles(dir, SUMMARY_FILE_RE, nowMs - SUMMARY_RETENTION_MS, skipped2);
922
+ } catch (error) {
923
+ skipped2.push({ path: home, reason: errorReason(error) });
924
+ }
925
+ return { logsDeleted, summariesDeleted, skipped: skipped2 };
926
+ }
927
+
928
+ // src/report.ts
929
+ var import_node_fs7 = __toESM(require("node:fs"));
930
+ var import_node_path7 = __toESM(require("node:path"));
931
+ var DEFAULT_DAYS = 14;
932
+ var DAY_MS2 = 864e5;
933
+ var LOG_SUFFIX = ".jsonl";
934
+ var LOOP_EVENTS = /* @__PURE__ */ new Set(["nudge", "warning", "compaction", "handback"]);
935
+ var EVENT_COLUMN = 16;
936
+ var MAX_VALUE_CHARS = 120;
937
+ var MAX_GENERIC_PARAMS = 6;
938
+ function isRecord(value) {
939
+ return typeof value === "object" && value !== null && !Array.isArray(value);
940
+ }
941
+ function scalar(value) {
942
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
943
+ if (typeof value === "string") return value.split("\n")[0].slice(0, MAX_VALUE_CHARS);
944
+ return void 0;
945
+ }
946
+ function timeOf(ts) {
947
+ const ms = typeof ts === "string" ? Date.parse(ts) : NaN;
948
+ return Number.isNaN(ms) ? "unknown time".padEnd(19) : new Date(ms).toISOString().slice(0, 19).replace("T", " ");
949
+ }
950
+ function tokensOf(event) {
951
+ const tokens = event.observed_tokens;
952
+ if (typeof tokens === "number") return `${tokens} tokens`;
953
+ if (!LOOP_EVENTS.has(event.event)) return void 0;
954
+ return tokens === UNAVAILABLE ? `tokens ${UNAVAILABLE}` : "tokens unknown";
955
+ }
956
+ function labelled(params, keys) {
957
+ return keys.flatMap((key) => {
958
+ const value = scalar(params[key]);
959
+ return value === void 0 ? [] : [`${key} ${value}`];
960
+ });
961
+ }
962
+ function errorMessage(event, params) {
963
+ const result = event.result;
964
+ return (isRecord(result) ? scalar(result.message) : void 0) ?? scalar(params.reason) ?? scalar(event.stack) ?? "no message";
965
+ }
966
+ function detailsOf(event) {
967
+ const params = isRecord(event.params) ? event.params : {};
968
+ switch (event.event) {
969
+ case "nudge":
970
+ case "warning": {
971
+ const limit = scalar(params.limit);
972
+ const source = scalar(params.limit_source);
973
+ if (limit === void 0) return [];
974
+ return [source === void 0 ? `limit ${limit}` : `limit ${limit} (${source})`];
975
+ }
976
+ case "state_written":
977
+ return [`state file changed after ${scalar(params.after) ?? "unknown step"}`, ...labelled(params, ["bytes"])];
978
+ case "compaction":
979
+ return labelled(params, ["trigger"]);
980
+ case "handback":
981
+ return labelled(params, ["chars", "bytes", "state_bytes", "truncated"]);
982
+ case "summary_saved":
983
+ return labelled(params, ["file"]);
984
+ case "settings_applied":
985
+ case "settings_skipped":
986
+ return labelled(params, ["reason", "prior", "value"]);
987
+ case "error":
988
+ return [errorMessage(event, params)];
989
+ default:
990
+ return labelled(params, Object.keys(params).slice(0, MAX_GENERIC_PARAMS));
991
+ }
992
+ }
993
+ function eventLine(event) {
994
+ const parts = [tokensOf(event), ...detailsOf(event)].filter((part) => part !== void 0);
995
+ return ` ${timeOf(event.ts)} ${String(event.event).padEnd(EVENT_COLUMN)} ${parts.join(", ")}`.trimEnd();
996
+ }
997
+ function newCycle() {
998
+ return { nudge: false, warning: false, changedAfterNudge: false, changedAfterWarning: false, compacted: false };
999
+ }
1000
+ function verdictLine(cycle, number) {
1001
+ const parts = [];
1002
+ if (cycle.nudge) parts.push(`after nudge: ${cycle.changedAfterNudge ? "changed" : "not changed"}`);
1003
+ if (cycle.warning) parts.push(`after warning: ${cycle.changedAfterWarning ? "changed" : "not changed"}`);
1004
+ if (parts.length === 0) return void 0;
1005
+ const open = cycle.compacted ? "" : " (no compaction yet)";
1006
+ return ` cycle ${number} state file ${parts.join("; ")}${open}`;
1007
+ }
1008
+ function trackCycle(cycle, event) {
1009
+ if (event.event === "nudge" || event.event === "warning") cycle[event.event] = true;
1010
+ if (event.event === "compaction") cycle.compacted = true;
1011
+ if (event.event !== "state_written" || cycle.compacted) return;
1012
+ const after = isRecord(event.params) ? event.params.after : void 0;
1013
+ if (after === "nudge") cycle.changedAfterNudge = true;
1014
+ if (after === "warning") cycle.changedAfterWarning = true;
1015
+ }
1016
+ function renderSession(log) {
1017
+ const first = (key) => log.events.map((event) => scalar(event[key])).find(Boolean);
1018
+ const lines = [`session ${log.id} ${first("tool") ?? "unknown tool"} ${first("project") ?? ""}`.trimEnd()];
1019
+ let cycle = newCycle();
1020
+ let cycles = 0;
1021
+ const closeCycle = () => {
1022
+ const verdict = verdictLine(cycle, cycles + 1);
1023
+ if (verdict) {
1024
+ lines.push(verdict);
1025
+ cycles++;
1026
+ }
1027
+ cycle = newCycle();
1028
+ };
1029
+ for (const event of log.events) {
1030
+ lines.push(eventLine(event));
1031
+ trackCycle(cycle, event);
1032
+ if (event.event === "handback") closeCycle();
1033
+ }
1034
+ closeCycle();
1035
+ return lines;
1036
+ }
1037
+ function loadSession(home, stem) {
1038
+ const events = readEvents(home, stem);
1039
+ const times = events.map((event) => Date.parse(String(event.ts))).filter((ms) => !Number.isNaN(ms));
1040
+ let newestMs = times.length > 0 ? Math.max(...times) : NaN;
1041
+ if (Number.isNaN(newestMs)) {
1042
+ try {
1043
+ newestMs = import_node_fs7.default.statSync(import_node_path7.default.join(home, HOME_LOGS_DIR, stem + LOG_SUFFIX)).mtimeMs;
1044
+ } catch {
1045
+ newestMs = 0;
1046
+ }
1047
+ }
1048
+ const id = events.map((event) => scalar(event.session)).find(Boolean) ?? stem;
1049
+ return { id, events, newestMs };
1050
+ }
1051
+ function listSessions(home) {
1052
+ let names;
1053
+ try {
1054
+ names = import_node_fs7.default.readdirSync(import_node_path7.default.join(home, HOME_LOGS_DIR));
1055
+ } catch {
1056
+ return [];
1057
+ }
1058
+ return names.filter((name) => name.endsWith(LOG_SUFFIX)).map((name) => loadSession(home, name.slice(0, -LOG_SUFFIX.length))).filter((log) => log.events.length > 0);
1059
+ }
1060
+ function renderReport(home, options = {}) {
1061
+ const days = options.days ?? DEFAULT_DAYS;
1062
+ const cutoffMs = (options.now ?? /* @__PURE__ */ new Date()).getTime() - days * DAY_MS2;
1063
+ let sessions;
1064
+ let scope;
1065
+ if (options.session !== void 0) {
1066
+ const log = loadSession(home, options.session);
1067
+ sessions = log.events.length > 0 ? [log] : [];
1068
+ scope = `session ${options.session}`;
1069
+ } else {
1070
+ sessions = listSessions(home).filter((log) => log.newestMs >= cutoffMs).sort((a, b) => a.newestMs - b.newestMs);
1071
+ scope = `the last ${days} days`;
1072
+ }
1073
+ if (sessions.length === 0) return `${NAME} report: no sessions for ${scope}.
1074
+ `;
1075
+ const header = `${NAME} report for ${scope} (times UTC): ${sessions.length} session${sessions.length === 1 ? "" : "s"}`;
1076
+ return [header, ...sessions.flatMap((log) => ["", ...renderSession(log)])].join("\n") + "\n";
1077
+ }
1078
+
1079
+ // src/settings.ts
1080
+ var import_node_fs8 = __toESM(require("node:fs"));
1081
+ var import_node_path8 = __toESM(require("node:path"));
1082
+ var AUTO_COMPACT_WINDOW = 45e4;
1083
+ var ENV_KEY = "CLAUDE_CODE_AUTO_COMPACT_WINDOW";
1084
+ var SETTINGS_KEY = "autoCompactWindow";
1085
+ var NEW_FILE_MODE = 384;
1086
+ function ensureAutoCompactWindow(options) {
1087
+ const { home, settingsPath, env } = options;
1088
+ try {
1089
+ if (import_node_fs8.default.existsSync(import_node_path8.default.join(home, HOME_SETTINGS_FLAG_FILE))) return skipped("flag_present");
1090
+ if (!import_node_fs8.default.existsSync(import_node_path8.default.dirname(settingsPath))) return skipped("no_claude_dir");
1091
+ const baseline = lstat(settingsPath);
1092
+ const text = readText(settingsPath);
1093
+ const user = findUserValue(env, text);
1094
+ if (user) {
1095
+ writeFlag(options, { wrote: false, prior: user.prior });
1096
+ return { event: "settings_skipped", params: { reason: "user_value", prior: user.prior } };
1097
+ }
1098
+ if (baseline?.isSymbolicLink()) return skipped("symlinked_settings");
1099
+ writeFlag(options, { wrote: true, value: AUTO_COMPACT_WINDOW });
1100
+ return replaceSettings(options, text, baseline);
1101
+ } catch (error) {
1102
+ return { event: "error", params: { reason: "io_error" }, error: toError(error) };
1103
+ }
1104
+ }
1105
+ function replaceSettings(options, text, baseline) {
1106
+ const { settingsPath } = options;
1107
+ let data;
1108
+ try {
1109
+ data = text === void 0 ? {} : parseSettings(text);
1110
+ } catch (error) {
1111
+ return { event: "error", params: { reason: "parse_error" }, error: toError(error) };
1112
+ }
1113
+ const trailingNewline = text === void 0 || text.endsWith("\n") ? "\n" : "";
1114
+ const output = JSON.stringify({ ...data, [SETTINGS_KEY]: AUTO_COMPACT_WINDOW }, null, 2) + trailingNewline;
1115
+ const mode = baseline ? Number(baseline.mode) & 511 : NEW_FILE_MODE;
1116
+ const tempPath = `${settingsPath}.${NAME}.tmp`;
1117
+ import_node_fs8.default.rmSync(tempPath, { force: true });
1118
+ import_node_fs8.default.writeFileSync(tempPath, output, { mode });
1119
+ import_node_fs8.default.chmodSync(tempPath, mode);
1120
+ options.beforeRename?.();
1121
+ if (lstat(settingsPath)?.mtimeNs !== baseline?.mtimeNs) {
1122
+ import_node_fs8.default.rmSync(tempPath, { force: true });
1123
+ return {
1124
+ event: "error",
1125
+ params: { reason: "mtime_changed" },
1126
+ error: new Error("settings.json changed during update; left untouched")
1127
+ };
1128
+ }
1129
+ import_node_fs8.default.renameSync(tempPath, settingsPath);
1130
+ return {
1131
+ event: "settings_applied",
1132
+ params: { reason: text === void 0 ? "file_missing" : "key_absent", value: AUTO_COMPACT_WINDOW }
1133
+ };
1134
+ }
1135
+ function findUserValue(env, text) {
1136
+ if (env[ENV_KEY]) return { prior: env[ENV_KEY] };
1137
+ if (text === void 0) return void 0;
1138
+ let data;
1139
+ try {
1140
+ data = parseSettings(text);
1141
+ } catch {
1142
+ return void 0;
1143
+ }
1144
+ if (Object.hasOwn(data, SETTINGS_KEY)) return { prior: data[SETTINGS_KEY] };
1145
+ const models = data.modelSettings;
1146
+ if (!isObject(models)) return void 0;
1147
+ for (const model of Object.values(models)) {
1148
+ if (isObject(model) && Object.hasOwn(model, SETTINGS_KEY)) return { prior: model[SETTINGS_KEY] };
1149
+ }
1150
+ return void 0;
1151
+ }
1152
+ function parseSettings(text) {
1153
+ const data = JSON.parse(text);
1154
+ if (!isObject(data)) throw new SyntaxError("settings.json is not a JSON object");
1155
+ return data;
1156
+ }
1157
+ function isObject(value) {
1158
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1159
+ }
1160
+ function readText(filePath) {
1161
+ try {
1162
+ return import_node_fs8.default.readFileSync(filePath, "utf8");
1163
+ } catch (error) {
1164
+ if (error.code === "ENOENT") return void 0;
1165
+ throw error;
1166
+ }
1167
+ }
1168
+ function lstat(filePath) {
1169
+ return import_node_fs8.default.lstatSync(filePath, { bigint: true, throwIfNoEntry: false });
1170
+ }
1171
+ function writeFlag(options, fields) {
1172
+ const flag = { ...fields, appliedAt: options.now.toISOString(), session: options.session };
1173
+ import_node_fs8.default.mkdirSync(options.home, { recursive: true });
1174
+ import_node_fs8.default.writeFileSync(import_node_path8.default.join(options.home, HOME_SETTINGS_FLAG_FILE), JSON.stringify(flag) + "\n");
1175
+ }
1176
+ function skipped(reason) {
1177
+ return { event: "settings_skipped", params: { reason } };
1178
+ }
1179
+ function toError(error) {
1180
+ return error instanceof Error ? error : new Error(String(error));
1181
+ }
1182
+
1183
+ // src/state.ts
1184
+ var import_node_crypto2 = __toESM(require("node:crypto"));
1185
+ var import_node_fs9 = __toESM(require("node:fs"));
1186
+ var import_node_path9 = __toESM(require("node:path"));
1187
+ var STATE_TEMPLATE = `# State (rewrite in place, never append; keep only these sections)
1188
+
1189
+ Keep this file under 7,000 characters and 6,000 bytes. It is handed back to you word for word after compaction.
1190
+ "Told" means the user has heard this from you in plain words. Reading it in a log does not count.
1191
+
1192
+ ## Status
1193
+ - Checkpoint done:
1194
+ - Exact next step:
1195
+ - Branch / pushed:
1196
+ - Rules in force (the user's own words):
1197
+
1198
+ ## Tests
1199
+ - Baseline (count, commit): update this whenever main moves
1200
+ - Latest run:
1201
+ - Failing now (test: cause, real or environment):
1202
+ - Now passing and what fixed it:
1203
+
1204
+ ## Findings
1205
+ - (finding: current state; told the user yes/no)
1206
+
1207
+ ## U-turns
1208
+ - (from -> to: why; told the user yes/no)
1209
+
1210
+ ## Dead ends
1211
+ - (approach: why it failed; do not retry)
1212
+ `;
1213
+ function stateHash(content) {
1214
+ return import_node_crypto2.default.createHash("sha256").update(content).digest("hex");
1215
+ }
1216
+ function isOptedOut(projectDir) {
1217
+ return import_node_fs9.default.existsSync(import_node_path9.default.join(projectDir, PROJECT_OFF_FILE));
1218
+ }
1219
+ function mkdirIfAbsent(dir) {
1220
+ try {
1221
+ import_node_fs9.default.mkdirSync(dir);
1222
+ } catch (error) {
1223
+ if (error.code !== "EEXIST") throw error;
1224
+ }
1225
+ }
1226
+ function writeIfAbsent(file, content) {
1227
+ try {
1228
+ import_node_fs9.default.writeFileSync(file, content, { flag: "wx" });
1229
+ return true;
1230
+ } catch (error) {
1231
+ if (error.code !== "EEXIST") throw error;
1232
+ return false;
1233
+ }
1234
+ }
1235
+ function stateFile(projectDir, session) {
1236
+ const relPath = `${PROJECT_STATE_DIR}/${sessionFileStem(session)}.md`;
1237
+ return { path: import_node_path9.default.join(projectDir, relPath), relPath };
1238
+ }
1239
+ function stateFingerprint(file) {
1240
+ let stat;
1241
+ let content;
1242
+ try {
1243
+ stat = import_node_fs9.default.statSync(file);
1244
+ content = import_node_fs9.default.readFileSync(file);
1245
+ } catch (error) {
1246
+ if (error.code === "ENOENT") return void 0;
1247
+ throw error;
1248
+ }
1249
+ return { bytes: content.length, mtimeMs: stat.mtimeMs, hash: stateHash(content) };
1250
+ }
1251
+ function saveSummary(home, scope, summary, now) {
1252
+ const relPath = `${homeCompactionsDir(scope.projectKey, scope.contextKey)}/${now.toISOString().replace(/[:.]/g, "-")}.md`;
1253
+ import_node_fs9.default.mkdirSync(import_node_path9.default.dirname(import_node_path9.default.join(home, relPath)), { recursive: true });
1254
+ import_node_fs9.default.writeFileSync(import_node_path9.default.join(home, relPath), summary, { flag: "wx" });
1255
+ return relPath;
1256
+ }
1257
+ function ensureStateFile(projectDir, session) {
1258
+ const { path: file, relPath } = stateFile(projectDir, session);
1259
+ mkdirIfAbsent(import_node_path9.default.join(projectDir, PROJECT_DIR));
1260
+ writeIfAbsent(import_node_path9.default.join(projectDir, PROJECT_GITIGNORE_FILE), "*\n");
1261
+ mkdirIfAbsent(import_node_path9.default.join(projectDir, PROJECT_STATE_DIR));
1262
+ const created = writeIfAbsent(file, STATE_TEMPLATE);
1263
+ return { path: file, relPath, ...created ? { templateHash: stateHash(STATE_TEMPLATE) } : {} };
1264
+ }
1265
+ var kTokens = (n) => `${Math.round(n / 1e3)}k`;
1266
+ function meterLine({ tokens, limit }, note) {
1267
+ return `${NAME} context meter: ${kTokens(tokens)} of ${kTokens(limit)} tokens before compaction. ${note}`;
1268
+ }
1269
+ var NUDGE_UPDATE_NOW = "Update the state file now, before your next tool call, with where you are and your exact next step. Then keep working.";
1270
+ function rewriteNow(statePath) {
1271
+ return `Your next action is to rewrite the state file ${statePath} in full, before any other tool call: replace the whole file in one write, not an edit. Then carry on.`;
1272
+ }
1273
+ function stateRules(statePath) {
1274
+ return [
1275
+ `Keep the state file ${statePath} current: it is handed back to you word for word after compaction.`,
1276
+ "- Rewrite it in place at every checkpoint: a test run, a decision, a change of plan, a finding, a dead end. Never append.",
1277
+ "- Record each change as a change: old, new and why. Mark whether the user has been told.",
1278
+ "- Keep its sections and keep it under 7,000 characters and 6,000 bytes.",
1279
+ `The user can switch ${NAME} off for this project by creating ${PROJECT_OFF_FILE}.`
1280
+ ].join("\n");
1281
+ }
1282
+ function thresholdMessage(kind, facts) {
1283
+ switch (kind) {
1284
+ case "nudge":
1285
+ return [meterLine(facts, NUDGE_UPDATE_NOW), stateRules(facts.statePath)].join("\n");
1286
+ case "warning":
1287
+ return meterLine(facts, `Compaction is close. ${rewriteNow(facts.statePath)}`);
1288
+ case "both":
1289
+ return [meterLine(facts, `Compaction is close. ${rewriteNow(facts.statePath)}`), stateRules(facts.statePath)].join("\n");
1290
+ }
1291
+ }
1292
+ var HANDBACK_BUDGET = { claude: 9500, codex: 8e3 };
1293
+ var measure = (tool, text) => tool === "claude" ? text.length : Buffer.byteLength(text, "utf8");
1294
+ function handbackRules(statePath) {
1295
+ return [
1296
+ `${NAME} hand-back: this is your state file ${statePath} from before compaction.`,
1297
+ "Trust it over the compaction summary where they differ, and carry on with its next step."
1298
+ ].join("\n");
1299
+ }
1300
+ var isHighSurrogate = (code) => code >= 55296 && code <= 56319;
1301
+ function prefixWithin(tool, content, room) {
1302
+ if (room <= 0) return "";
1303
+ if (tool === "claude") {
1304
+ let end2 = Math.min(room, content.length);
1305
+ if (end2 < content.length && isHighSurrogate(content.charCodeAt(end2 - 1))) end2--;
1306
+ return content.slice(0, end2);
1307
+ }
1308
+ const bytes = Buffer.from(content, "utf8");
1309
+ let end = Math.min(room, bytes.length);
1310
+ while (end > 0 && end < bytes.length && (bytes[end] & 192) === 128) end--;
1311
+ return bytes.subarray(0, end).toString("utf8");
1312
+ }
1313
+ function handbackText(tool, statePath, content) {
1314
+ const budget = HANDBACK_BUDGET[tool];
1315
+ const rules = handbackRules(statePath);
1316
+ const whole = `${rules}
1317
+
1318
+ ${content}`;
1319
+ if (measure(tool, whole) <= budget) return { text: whole, truncated: false };
1320
+ const head = [
1321
+ `${NAME} hand-back: your state file was truncated to fit this message.`,
1322
+ `Read the full file ${statePath} before your next step.`,
1323
+ rules,
1324
+ "",
1325
+ ""
1326
+ ].join("\n");
1327
+ return { text: head + prefixWithin(tool, content, budget - measure(tool, head)), truncated: true };
1328
+ }
1329
+
1330
+ // src/main.ts
1331
+ var UNKNOWN_SESSION = "unknown";
1332
+ var NUDGE_FRACTION = 0.75;
1333
+ var WARNING_FRACTION = 0.9;
1334
+ var TRANSCRIPT_UNREADABLE = "transcript_unreadable";
1335
+ function readStdin() {
1336
+ try {
1337
+ return import_node_fs10.default.readFileSync(0, "utf8");
1338
+ } catch {
1339
+ return "";
1340
+ }
1341
+ }
1342
+ function stringField(record, key) {
1343
+ if (!record || typeof record !== "object") return void 0;
1344
+ const value = record[key];
1345
+ return typeof value === "string" && value ? value : void 0;
1346
+ }
1347
+ function payloadIds(stdin) {
1348
+ let payload;
1349
+ try {
1350
+ payload = JSON.parse(stdin);
1351
+ } catch {
1352
+ payload = void 0;
1353
+ }
1354
+ return {
1355
+ session: stringField(payload, "session_id") ?? UNKNOWN_SESSION,
1356
+ project: process.env.CLAUDE_PROJECT_DIR || stringField(payload, "cwd") || null,
1357
+ hookEvent: stringField(payload, "hook_event_name")
1358
+ };
1359
+ }
1360
+ function redacted(error, safeMessage) {
1361
+ const err = error instanceof Error ? error : new Error(String(error));
1362
+ if (!(err instanceof SyntaxError)) return err;
1363
+ const safe = new SyntaxError(safeMessage);
1364
+ safe.stack = [`SyntaxError: ${safe.message}`, ...String(err.stack).split("\n").filter((l) => /^\s+at /.test(l))].join("\n");
1365
+ return safe;
1366
+ }
1367
+ function logError(stdin) {
1368
+ return ({ tool, event, error }) => {
1369
+ const err = redacted(error, "hook stdin is not valid JSON");
1370
+ const { session, project, hookEvent } = payloadIds(stdin);
1371
+ appendEvent(roqueHome(process.env), {
1372
+ event: "error",
1373
+ tool: tool ?? null,
1374
+ session,
1375
+ project,
1376
+ params: { hook_event: event || hookEvent || null },
1377
+ result: { message: err.message },
1378
+ status: "error",
1379
+ stack: err.stack
1380
+ });
1381
+ };
1382
+ }
1383
+ function sessionId(payload) {
1384
+ const id = stringField(payload, "session_id");
1385
+ if (!id) throw new Error("hook payload has no session_id");
1386
+ return id;
1387
+ }
1388
+ function recordUnreadableOnce(home, ids) {
1389
+ const logged = readEvents(home, ids.session).some((e) => e.event === "error" && e.params?.reason === TRANSCRIPT_UNREADABLE);
1390
+ if (logged) return;
1391
+ const error = new Error("session transcript is missing or unreadable");
1392
+ appendEvent(home, { event: "error", ...ids, params: { reason: TRANSCRIPT_UNREADABLE }, status: "error", stack: error.stack });
1393
+ }
1394
+ function recordUnavailable(home, ids, reading) {
1395
+ if (reading.reason === "unparsed") recordUnparsedOnce(home, ids, reading);
1396
+ else if (reading.reason === "unreadable") recordUnreadableOnce(home, ids);
1397
+ }
1398
+ var hasMeterWarning = (home, session) => readEvents(home, session).some((e) => e.event === "meter_unparsed");
1399
+ function recordLimitWarningOnce(home, ids, tokens, reason, source) {
1400
+ if (hasMeterWarning(home, ids.session)) return;
1401
+ prepareScope(ids.project);
1402
+ const release = claimSession(home, ids.session, { timeoutMs: 500 });
1403
+ if (!release) return;
1404
+ try {
1405
+ if (hasMeterWarning(home, ids.session)) return;
1406
+ const params = { reason, limit_source: source };
1407
+ appendEvent(home, { event: "meter_unparsed", ...ids, observed_tokens: tokens, status: "warn", params });
1408
+ } finally {
1409
+ release();
1410
+ }
1411
+ }
1412
+ function loggedFingerprint(hash, bytes, mtimeMs) {
1413
+ if (typeof hash !== "string") return void 0;
1414
+ return { hash, bytes: typeof bytes === "number" ? bytes : -1, mtimeMs: typeof mtimeMs === "number" ? mtimeMs : -1 };
1415
+ }
1416
+ function stateCycle(events) {
1417
+ let cycle = {};
1418
+ for (const { event, params } of events) {
1419
+ const p = params ?? {};
1420
+ if (event === "handback") cycle = {};
1421
+ else if (event === "nudge" || event === "warning") {
1422
+ cycle = { after: event, baseline: loggedFingerprint(p.state_hash, p.state_bytes, p.state_mtime_ms) };
1423
+ } else if (event === "state_written" && cycle.after) {
1424
+ cycle.baseline = loggedFingerprint(p.hash, p.bytes, p.mtime_ms);
1425
+ }
1426
+ }
1427
+ return cycle;
1428
+ }
1429
+ var sameStat = (a, b) => a.size === b.bytes && a.mtimeMs === b.mtimeMs;
1430
+ function detectStateWrite(home, ids) {
1431
+ const file = stateFile(ids.project, ids.session).path;
1432
+ let stat;
1433
+ try {
1434
+ stat = import_node_fs10.default.statSync(file);
1435
+ } catch {
1436
+ return;
1437
+ }
1438
+ const { after, baseline } = stateCycle(readEvents(home, ids.session));
1439
+ if (!after || !baseline || sameStat(stat, baseline)) return;
1440
+ const current = stateFingerprint(file);
1441
+ if (!current || current.hash === baseline.hash) return;
1442
+ prepareScope(ids.project);
1443
+ const release = claimSession(home, ids.session, { timeoutMs: 500 });
1444
+ if (!release) return;
1445
+ try {
1446
+ const cycle = stateCycle(readEvents(home, ids.session));
1447
+ const latest = stateFingerprint(file);
1448
+ if (!cycle.after || !latest || latest.hash === cycle.baseline?.hash) return;
1449
+ const params = { after: cycle.after, bytes: latest.bytes, hash: latest.hash, mtime_ms: latest.mtimeMs };
1450
+ appendEvent(home, { event: "state_written", ...ids, params });
1451
+ } finally {
1452
+ release();
1453
+ }
1454
+ }
1455
+ function dueThreshold(current, warningReached) {
1456
+ const nudge = !current.nudge;
1457
+ const warning = warningReached && !current.warning;
1458
+ if (nudge && warning) return "both";
1459
+ if (warning) return "warning";
1460
+ return nudge ? "nudge" : void 0;
1461
+ }
1462
+ var compactedSince = (home, session, sinceMs) => readEvents(home, session).some((e) => (e.event === "compaction" || e.event === "handback") && Date.parse(e.ts) >= sinceMs);
1463
+ function effectiveLimit({ tool, projectDir, env }, home, reading) {
1464
+ const probe = parentProcessProbe();
1465
+ return tool === "claude" ? claudeLimit({ env, model: reading.model, projectDir, home, probe }) : codexLimit({ env, contextWindow: reading.contextWindow, projectDir, probe });
1466
+ }
1467
+ function onPostToolUse(ctx) {
1468
+ const started = Date.now();
1469
+ const { tool, projectDir, payload, env } = ctx;
1470
+ if (!import_node_path10.default.isAbsolute(projectDir) || isOptedOut(projectDir)) return void 0;
1471
+ if (payload.agent_id !== void 0) return void 0;
1472
+ const ids = { tool, session: sessionId(payload), project: projectDir };
1473
+ const home = roqueHome(env);
1474
+ detectStateWrite(home, ids);
1475
+ const reading = contextTokens(payload.transcript_path, tool);
1476
+ if (reading.tokens === UNAVAILABLE) {
1477
+ recordUnavailable(home, ids, reading);
1478
+ return void 0;
1479
+ }
1480
+ const { limit, source } = effectiveLimit(ctx, home, reading);
1481
+ if (limit === UNAVAILABLE) {
1482
+ recordLimitWarningOnce(home, ids, reading.tokens, "limit_unavailable", source);
1483
+ return void 0;
1484
+ }
1485
+ const fraction = reading.tokens / limit;
1486
+ if (fraction < NUDGE_FRACTION) return void 0;
1487
+ if (source.split(";").includes("argv_unreadable")) {
1488
+ recordLimitWarningOnce(home, ids, reading.tokens, "argv_unreadable", source);
1489
+ }
1490
+ const warningReached = fraction >= WARNING_FRACTION;
1491
+ if (!dueThreshold(marks(home, ids.session), warningReached)) return void 0;
1492
+ prepareScope(projectDir);
1493
+ const release = claimSession(home, ids.session);
1494
+ if (!release) throw new Error("session claim timed out; threshold message not sent");
1495
+ try {
1496
+ if (compactedSince(home, ids.session, started)) return void 0;
1497
+ const kind = dueThreshold(marks(home, ids.session), warningReached);
1498
+ if (!kind) return void 0;
1499
+ const state = ensureStateFile(projectDir, ids.session);
1500
+ const baseline = stateFingerprint(state.path);
1501
+ const params = {
1502
+ limit,
1503
+ limit_source: source,
1504
+ fraction: Math.round(fraction * 1e4) / 1e4,
1505
+ state_path: state.relPath,
1506
+ ...state.templateHash ? { template_hash: state.templateHash } : {},
1507
+ ...baseline ? { state_hash: baseline.hash, state_bytes: baseline.bytes, state_mtime_ms: baseline.mtimeMs } : {}
1508
+ };
1509
+ const events = kind === "both" ? ["nudge", "warning"] : [kind];
1510
+ for (const event of events) {
1511
+ appendEvent(home, { event, ...ids, observed_tokens: reading.tokens, params, latency_ms: Date.now() - started });
1512
+ }
1513
+ const additionalContext = thresholdMessage(kind, { statePath: state.path, tokens: reading.tokens, limit });
1514
+ return { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext } };
1515
+ } finally {
1516
+ release();
1517
+ }
1518
+ }
1519
+ var globalEventProject = (projectDir) => import_node_path10.default.isAbsolute(projectDir) && !isOptedOut(projectDir) ? projectDir : null;
1520
+ function applyClaudeSetting({ tool, projectDir, payload, env }) {
1521
+ const started = Date.now();
1522
+ const home = roqueHome(env);
1523
+ const session = stringField(payload, "session_id") ?? UNKNOWN_SESSION;
1524
+ const settingsPath = import_node_path10.default.join(claudeConfigDir(env), "settings.json");
1525
+ const outcome = ensureAutoCompactWindow({ home, settingsPath, env, now: /* @__PURE__ */ new Date(), session });
1526
+ if (outcome.params.reason === "flag_present") return;
1527
+ const error = outcome.error && redacted(outcome.error, "settings.json is not valid JSON");
1528
+ appendEvent(home, {
1529
+ event: outcome.event,
1530
+ tool,
1531
+ session,
1532
+ project: globalEventProject(projectDir),
1533
+ params: outcome.params,
1534
+ status: error ? "error" : "ok",
1535
+ latency_ms: Date.now() - started,
1536
+ ...error ? { result: { message: error.message }, stack: error.stack } : {}
1537
+ });
1538
+ }
1539
+ function readIfPresent(file) {
1540
+ try {
1541
+ return import_node_fs10.default.readFileSync(file);
1542
+ } catch (error) {
1543
+ if (error.code === "ENOENT") return void 0;
1544
+ throw error;
1545
+ }
1546
+ }
1547
+ function logHandbackError(home, ids, reason, error) {
1548
+ const err = error instanceof Error ? error : new Error(String(error));
1549
+ try {
1550
+ appendEvent(home, { event: "error", ...ids, params: { hook_event: "SessionStart", reason }, result: { message: err.message }, status: "error", stack: err.stack });
1551
+ } catch {
1552
+ }
1553
+ }
1554
+ function bestEffort(home, ids, reason, step) {
1555
+ try {
1556
+ return step();
1557
+ } catch (error) {
1558
+ logHandbackError(home, ids, reason, error);
1559
+ return void 0;
1560
+ }
1561
+ }
1562
+ function handBack({ tool, projectDir, payload, env }) {
1563
+ const started = Date.now();
1564
+ if (!import_node_path10.default.isAbsolute(projectDir) || isOptedOut(projectDir)) return void 0;
1565
+ const ids = { tool, session: sessionId(payload), project: projectDir };
1566
+ const home = roqueHome(env);
1567
+ prepareScope(projectDir);
1568
+ const claim = bestEffort(home, ids, "claim_failed", () => claimSession(home, ids.session) ?? "timed_out");
1569
+ if (claim === "timed_out") logHandbackError(home, ids, "claim_timeout", new Error("session claim timed out; hand-back sent without it"));
1570
+ const release = typeof claim === "function" ? claim : void 0;
1571
+ try {
1572
+ const trigger = stringField(payload, "trigger");
1573
+ const pre = preCompactionTokens(payload.transcript_path, tool);
1574
+ const compactionParams = {
1575
+ ...trigger ? { trigger } : {},
1576
+ ...pre.reason ? { tokens_reason: pre.reason } : {},
1577
+ ...pre.tokensSource ? { tokens_source: pre.tokensSource } : {}
1578
+ };
1579
+ bestEffort(home, ids, "log_failed", () => appendEvent(home, { event: "compaction", ...ids, observed_tokens: pre.tokens, params: compactionParams }));
1580
+ const state = stateFile(projectDir, ids.session);
1581
+ const content = readIfPresent(state.path);
1582
+ if (!content) return void 0;
1583
+ const { text, truncated } = handbackText(tool, state.path, content.toString("utf8"));
1584
+ const params = { truncated, chars: text.length, bytes: Buffer.byteLength(text, "utf8"), state_bytes: content.length };
1585
+ bestEffort(home, ids, "log_failed", () => appendEvent(home, { event: "handback", ...ids, observed_tokens: pre.tokens, params, latency_ms: Date.now() - started }));
1586
+ return { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text } };
1587
+ } finally {
1588
+ release?.();
1589
+ }
1590
+ }
1591
+ var pruneProblems = (result) => result.skipped.filter((skip) => skip.reason !== "ENOENT");
1592
+ var MAX_LOGGED_SKIPS = 5;
1593
+ function pruneRetention({ tool, projectDir, payload, env }) {
1594
+ try {
1595
+ const started = Date.now();
1596
+ const home = roqueHome(env);
1597
+ const result = prune(home);
1598
+ const problems = pruneProblems(result);
1599
+ if (result.logsDeleted === 0 && result.summariesDeleted === 0 && problems.length === 0) return;
1600
+ appendEvent(home, {
1601
+ event: "prune",
1602
+ tool,
1603
+ session: stringField(payload, "session_id") ?? UNKNOWN_SESSION,
1604
+ project: globalEventProject(projectDir),
1605
+ params: {
1606
+ logs_deleted: result.logsDeleted,
1607
+ summaries_deleted: result.summariesDeleted,
1608
+ skipped: problems.length,
1609
+ ...problems.length > 0 ? { skipped_paths: problems.slice(0, MAX_LOGGED_SKIPS) } : {}
1610
+ },
1611
+ status: problems.length > 0 ? "warn" : "ok",
1612
+ latency_ms: Date.now() - started
1613
+ });
1614
+ } catch {
1615
+ }
1616
+ }
1617
+ function onSessionStart(ctx) {
1618
+ if (ctx.tool === "claude" && ctx.payload.source === "startup") applyClaudeSetting(ctx);
1619
+ const result = ctx.payload.source === "compact" ? handBack(ctx) : void 0;
1620
+ pruneRetention(ctx);
1621
+ return result;
1622
+ }
1623
+ function onPostCompact({ tool, projectDir, payload, env }) {
1624
+ if (tool !== "claude") return void 0;
1625
+ const started = Date.now();
1626
+ if (!import_node_path10.default.isAbsolute(projectDir) || isOptedOut(projectDir)) return void 0;
1627
+ const session = sessionId(payload);
1628
+ if (!import_node_fs10.default.existsSync(stateFile(projectDir, session).path)) return void 0;
1629
+ const summary = payload.compact_summary;
1630
+ if (typeof summary !== "string") throw new Error("PostCompact payload has no compact_summary string");
1631
+ const home = roqueHome(env);
1632
+ const scope = scopeOf(projectDir);
1633
+ const file = saveSummary(home, scope, summary, /* @__PURE__ */ new Date());
1634
+ const params = { file, chars: summary.length };
1635
+ const status = scope.source === "cwd_fallback" ? "warn" : "ok";
1636
+ appendEvent(home, { event: "summary_saved", tool, session, project: projectDir, params, status, latency_ms: Date.now() - started });
1637
+ return void 0;
1638
+ }
1639
+ var roqueHandlers = { PostToolUse: onPostToolUse, SessionStart: onSessionStart, PostCompact: onPostCompact };
1640
+ var REPORT_USAGE = `usage: ${NAME} report [--days N] [--session id] (node dist/${NAME}.js report ...)`;
1641
+ function parseReportArgs(args) {
1642
+ const options = {};
1643
+ for (let i = 0; i < args.length; i += 2) {
1644
+ const [flag, value] = [args[i], args[i + 1]];
1645
+ if (value === void 0) return void 0;
1646
+ if (flag === "--days" && /^[1-9]\d*$/.test(value)) options.days = Number(value);
1647
+ else if (flag === "--session" && value) options.session = value;
1648
+ else return void 0;
1649
+ }
1650
+ return options;
1651
+ }
1652
+ function retentionNote(result) {
1653
+ const problems = pruneProblems(result);
1654
+ if (result.logsDeleted === 0 && result.summariesDeleted === 0 && problems.length === 0) return "";
1655
+ const skipped2 = problems.map((skip) => `
1656
+ skipped ${skip.path}: ${skip.reason}`).join("");
1657
+ return `
1658
+ retention: deleted ${result.logsDeleted} old logs and ${result.summariesDeleted} old summaries${skipped2}
1659
+ `;
1660
+ }
1661
+ function runReport(args) {
1662
+ const options = parseReportArgs(args);
1663
+ if (!options) {
1664
+ process.stderr.write(REPORT_USAGE + "\n");
1665
+ process.exitCode = 2;
1666
+ return;
1667
+ }
1668
+ try {
1669
+ const home = roqueHome(process.env);
1670
+ const pruned = prune(home);
1671
+ process.stdout.write(renderReport(home, options) + retentionNote(pruned));
1672
+ } catch (error) {
1673
+ process.stderr.write(`${NAME} report failed: ${error.message}
1674
+ `);
1675
+ process.exitCode = 1;
1676
+ }
1677
+ }
1678
+ function selectHandlers() {
1679
+ const captureDir = process.env[CAPTURE_ENV];
1680
+ return captureDir ? captureHandlers(captureDir) : roqueHandlers;
1681
+ }
1682
+ async function main() {
1683
+ const argv = process.argv.slice(2);
1684
+ if (argv[0] === "report") return runReport(argv.slice(1));
1685
+ const stdin = readStdin();
1686
+ const out = await runHook(stdin, argv, process.env, selectHandlers(), logError(stdin));
1687
+ if (out) process.stdout.write(out);
1688
+ }
1689
+ process.exitCode = 0;
1690
+ main().catch(() => void 0);