agent-theatre 0.1.0 → 0.1.2

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/dist/cli.js CHANGED
@@ -11,11 +11,1123 @@ import pc from "picocolors";
11
11
  import { createServer } from "http";
12
12
  import { existsSync, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
13
13
  import { homedir as homedir2 } from "os";
14
- import { join as join2, basename } from "path";
14
+ import { join as join2, basename as basename2 } from "path";
15
15
  import { Hono } from "hono";
16
16
  import { getRequestListener } from "@hono/node-server";
17
17
  import { WebSocketServer } from "ws";
18
- import { Director, direct, filesFromEvents, generateWorld, hookPayloadToEvents, parseTranscript } from "@agent-theatre/core";
18
+
19
+ // ../core/dist/index.js
20
+ var last = 0;
21
+ var counter = 0;
22
+ var ALPHA = "0123456789abcdefghjkmnpqrstvwxyz";
23
+ function mintId(now = Date.now()) {
24
+ if (now === last) counter++;
25
+ else {
26
+ last = now;
27
+ counter = 0;
28
+ }
29
+ let t = now;
30
+ let s = "";
31
+ for (let i = 0; i < 10; i++) {
32
+ s = ALPHA[t % 32] + s;
33
+ t = Math.floor(t / 32);
34
+ }
35
+ return `ev_${s}${counter.toString(32).padStart(3, "0")}`;
36
+ }
37
+ function norm(p) {
38
+ return p.replace(/\\/g, "/").replace(/\/+$/, "");
39
+ }
40
+ function relativizePath(abs, ctx) {
41
+ const p = norm(abs);
42
+ const cwd = norm(ctx.cwd);
43
+ if (cwd && (p === cwd || p.startsWith(cwd + "/"))) return p.slice(cwd.length + 1) || ".";
44
+ if (ctx.home) {
45
+ const home = norm(ctx.home);
46
+ if (home && (p === home || p.startsWith(home + "/"))) return "~/" + p.slice(home.length + 1);
47
+ }
48
+ if (!p.startsWith("/")) return p;
49
+ const base2 = p.slice(p.lastIndexOf("/") + 1);
50
+ return `//ext/${base2}`;
51
+ }
52
+ function basename(p) {
53
+ const n = norm(p);
54
+ return n.slice(n.lastIndexOf("/") + 1);
55
+ }
56
+ function isTestPath(p) {
57
+ return /(^|\/)(tests?|__tests__|spec|specs)\//i.test(p) || /\.(test|spec)\.[cm]?[jt]sx?$/i.test(p) || /(^|\/)test_[^/]+\.py$/i.test(p) || /_test\.(go|rs|py)$/i.test(p);
58
+ }
59
+ var TEST_RUNNERS = /\b(vitest|jest|mocha|pytest|py\.test|go\s+test|cargo\s+test|npm\s+test|pnpm\s+test|yarn\s+test|bun\s+test|npx\s+(vitest|jest|playwright)|rspec|phpunit|dotnet\s+test|mvn\s+test|gradle\s+test|make\s+test)\b/;
60
+ var BUILD = /\b(tsc|tsup|vite\s+build|webpack|esbuild|rollup|next\s+build|cargo\s+build|go\s+build|make\b(?!\s+test)|npm\s+run\s+build|pnpm\s+(run\s+)?build|yarn\s+build|gradle\s+build|mvn\s+package|docker\s+build)\b/;
61
+ var INSTALL = /\b(npm\s+(i|install|ci)|pnpm\s+(i|install|add)|yarn(\s+add|\s+install)?|pip3?\s+install|poetry\s+(add|install)|cargo\s+add|go\s+get|brew\s+install|apt(-get)?\s+install|bun\s+(i|install|add))\b/;
62
+ var SEARCH = /\b(grep|rg|ag|ack|find|fd|locate)\b/;
63
+ var READ = /^\s*(cat|head|tail|less|more|bat|wc|stat|jq)\b/;
64
+ function argv0Of(command) {
65
+ const first = command.split(/\s*(?:&&|\|\||;|\|)\s*/)[0] ?? "";
66
+ const toks = first.trim().split(/\s+/).map((t) => t.replace(/^[({]+/, "")).filter(Boolean);
67
+ let i = 0;
68
+ while (i < toks.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(toks[i])) i++;
69
+ if (toks[i] === "cd" && toks.length > i + 1) {
70
+ const rest = command.split(/\s*&&\s*/)[1];
71
+ if (rest) return argv0Of(rest);
72
+ }
73
+ const a = toks[i] ?? "";
74
+ return a.replace(/^.*\//, "").slice(0, 40) || "sh";
75
+ }
76
+ function cleanTarget(t) {
77
+ const s = t.replace(/^["']|["']$/g, "").trim();
78
+ if (!s || s.startsWith("-") || s === "/dev/null" || s.includes("$(") || s.startsWith("$")) return null;
79
+ if (/^\/dev\//.test(s) || /^&\d/.test(s)) return null;
80
+ return s;
81
+ }
82
+ function extractWrites(command) {
83
+ const out = /* @__PURE__ */ new Set();
84
+ for (const m of command.matchAll(/(?:^|[\s)])\d?>{1,2}\s*([^\s|&;<>]+)/g)) {
85
+ const t = cleanTarget(m[1] ?? "");
86
+ if (t) out.add(t);
87
+ }
88
+ for (const m of command.matchAll(/\btee\s+(?:-[a-z]+\s+)*((?:[^\s|&;<>]+\s*)+)/g)) {
89
+ for (const f of (m[1] ?? "").split(/\s+/)) {
90
+ const t = cleanTarget(f);
91
+ if (t) out.add(t);
92
+ }
93
+ }
94
+ for (const m of command.matchAll(/\bsed\s+-i\S*\s+(.+?)(?=\s*(?:&&|\|\||;|\||$))/g)) {
95
+ const toks = (m[1] ?? "").match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
96
+ const files = toks.filter((t) => !t.startsWith("-") && !t.startsWith("'") && !t.startsWith('"') && !/^s[/|#]/.test(t) && !/^-e$/.test(t));
97
+ for (const f of files) {
98
+ const t = cleanTarget(f);
99
+ if (t) out.add(t);
100
+ }
101
+ }
102
+ for (const m of command.matchAll(/\b(?:cp|mv)\s+(?:-[a-zA-Z]+\s+)*((?:[^\s|&;<>]+\s+)+)([^\s|&;<>]+)/g)) {
103
+ const t = cleanTarget(m[2] ?? "");
104
+ if (t) out.add(t);
105
+ }
106
+ for (const m of command.matchAll(/\btouch\s+((?:[^\s|&;<>]+\s*)+)/g)) {
107
+ for (const f of (m[1] ?? "").split(/\s+/)) {
108
+ const t = cleanTarget(f);
109
+ if (t) out.add(t);
110
+ }
111
+ }
112
+ return [...out];
113
+ }
114
+ function extractReads(command) {
115
+ const out = /* @__PURE__ */ new Set();
116
+ for (const seg of command.split(/\s*(?:&&|\|\||;|\|)\s*/)) {
117
+ const m = seg.match(/^\s*(?:cat|head|tail|less|more|bat|wc)\s+(?:-[^\s]+\s+)*((?:[^\s|&;<>]+\s*)+)/);
118
+ if (!m) continue;
119
+ for (const f of (m[1] ?? "").split(/\s+/)) {
120
+ const t = cleanTarget(f);
121
+ if (t && !/^\d+$/.test(t)) out.add(t);
122
+ }
123
+ }
124
+ return [...out];
125
+ }
126
+ function classifyBash(command) {
127
+ const argv0 = argv0Of(command);
128
+ const writes = extractWrites(command);
129
+ const reads = extractReads(command);
130
+ let intent = "other";
131
+ if (TEST_RUNNERS.test(command)) intent = "test";
132
+ else if (BUILD.test(command)) intent = "build";
133
+ else if (INSTALL.test(command)) intent = "install";
134
+ else if (/^\s*git\b/.test(command) || argv0 === "git" || argv0 === "gh") intent = "git";
135
+ else if (writes.length) intent = "write_file";
136
+ else if (SEARCH.test(command) && argv0 !== "cd") intent = "search";
137
+ else if (READ.test(command) && reads.length) intent = "read_file";
138
+ return { argv0, intent, writes, reads };
139
+ }
140
+ var num = (s) => s ? parseInt(s, 10) : 0;
141
+ function parseTestSummary(output) {
142
+ const tail = output.length > 2e4 ? output.slice(-2e4) : output;
143
+ let m = tail.match(/Tests\s+(?:(\d+)\s+failed\s*\|\s*)?(\d+)\s+passed(?:\s*\|\s*(\d+)\s+skipped)?/);
144
+ if (m) return { failed: num(m[1]), passed: num(m[2]), ...m[3] ? { skipped: num(m[3]) } : {}, framework: "vitest" };
145
+ m = tail.match(/Tests\s+(\d+)\s+failed\s*\((\d+)\)/);
146
+ if (m) return { failed: num(m[1]), passed: Math.max(0, num(m[2]) - num(m[1])), framework: "vitest" };
147
+ m = tail.match(/Tests:\s+(?:(\d+)\s+failed,\s*)?(?:(\d+)\s+skipped,\s*)?(\d+)\s+passed,\s*(?:(\d+)\s+skipped,\s*)?\d+\s+total/);
148
+ if (m) return { failed: num(m[1]), passed: num(m[3]), ...m[2] ?? m[4] ? { skipped: num(m[2] ?? m[4]) } : {}, framework: "jest" };
149
+ m = tail.match(/Tests:\s+(\d+)\s+failed,\s*\d+\s+total/);
150
+ if (m) return { failed: num(m[1]), passed: 0, framework: "jest" };
151
+ m = tail.match(/=+\s*(?:(\d+)\s+failed,?\s*)?(?:(\d+)\s+passed,?\s*)?(?:(\d+)\s+skipped,?\s*)?(?:\d+\s+(?:error|errors|warnings?|deselected|xfailed|xpassed),?\s*)*in\s+[\d.]+s/);
152
+ if (m && (m[1] || m[2])) return { failed: num(m[1]), passed: num(m[2]), ...m[3] ? { skipped: num(m[3]) } : {}, framework: "pytest" };
153
+ if (/^--- (?:PASS|FAIL):/m.test(tail) || /^(?:ok|FAIL)\s+\S+\s+(?:[\d.]+s|\(cached\))/m.test(tail)) {
154
+ const failed = (tail.match(/^--- FAIL:/gm) ?? []).length || (tail.match(/^FAIL\s+\S+\s+[\d.]+s/gm) ?? []).length;
155
+ const passed = (tail.match(/^--- PASS:/gm) ?? []).length || (tail.match(/^ok\s+\S+/gm) ?? []).length;
156
+ return { failed, passed, framework: "go" };
157
+ }
158
+ m = tail.match(/test result:\s*\w+\.\s*(\d+)\s+passed;\s*(\d+)\s+failed;\s*(\d+)\s+ignored/);
159
+ if (m) return { passed: num(m[1]), failed: num(m[2]), skipped: num(m[3]), framework: "cargo" };
160
+ const mp = tail.match(/^\s*(\d+)\s+passing\b/m);
161
+ const mf = tail.match(/^\s*(\d+)\s+failing\b/m);
162
+ if (mp || mf) {
163
+ const ms = tail.match(/^\s*(\d+)\s+pending\b/m);
164
+ return { passed: num(mp?.[1]), failed: num(mf?.[1]), ...ms ? { skipped: num(ms[1]) } : {}, framework: "mocha" };
165
+ }
166
+ const pp = tail.match(/^\s*(\d+)\s+passed\s*\(/m);
167
+ const pf = tail.match(/^\s*(\d+)\s+failed\b/m);
168
+ if (pp || pf) return { passed: num(pp?.[1]), failed: num(pf?.[1]), framework: "playwright" };
169
+ return null;
170
+ }
171
+ var obj = (x) => x && typeof x === "object" ? x : {};
172
+ var str = (x) => typeof x === "string" ? x : void 0;
173
+ var lineCount = (s) => s ? (s.match(/\n/g)?.length ?? 0) + (s.endsWith("\n") ? 0 : 1) : 0;
174
+ function patchCounts(response) {
175
+ const patch = obj(response).structuredPatch;
176
+ if (!Array.isArray(patch)) return null;
177
+ let added = 0, removed = 0;
178
+ for (const h of patch) {
179
+ const lines = obj(h).lines;
180
+ if (!Array.isArray(lines)) continue;
181
+ for (const l of lines) {
182
+ if (typeof l !== "string") continue;
183
+ if (l.startsWith("+")) added++;
184
+ else if (l.startsWith("-")) removed++;
185
+ }
186
+ }
187
+ return { added, removed, hunks: patch.length };
188
+ }
189
+ function responseText(response) {
190
+ if (typeof response === "string") return response;
191
+ const r = obj(response);
192
+ const parts = [str(r.stdout), str(r.stderr), str(r.output)].filter((x) => !!x);
193
+ if (parts.length) return parts.join("\n");
194
+ if (Array.isArray(response)) return response.map((b) => str(obj(b).text) ?? "").join("\n");
195
+ return "";
196
+ }
197
+ function heredocLines(command) {
198
+ let n = 0;
199
+ for (const m of command.matchAll(/<<-?\s*(['"]?)(\w+)\1[^\n]*\n([\s\S]*?)\n\2(?=\n|$)/g)) n += lineCount(m[3]);
200
+ if (n === 0 && /(?:^|[\s)])\d?>{1,2}\s*[^\s|&;<>]+/.test(command) && !/>\s*\/dev\/null/.test(command)) n = 1;
201
+ return n;
202
+ }
203
+ var SEARCH_TOOLS_REPO = /* @__PURE__ */ new Set(["Grep", "Glob", "LS", "ToolSearch"]);
204
+ var SEARCH_TOOLS_WEB = /* @__PURE__ */ new Set(["WebSearch", "WebFetch"]);
205
+ var SILENT_TOOLS = /* @__PURE__ */ new Set(["TodoWrite", "TodoRead", "Skill", "EnterPlanMode", "ExitPlanMode", "EnterWorktree", "ExitWorktree", "NotebookRead", "ListAgents", "SendMessage", "TaskStop", "Monitor", "ScheduleWakeup", "ReportFindings"]);
206
+ function toolCallToOutcome(call, ctx) {
207
+ const input = obj(call.input);
208
+ const resp = call.response;
209
+ const ok = !call.isError && obj(resp).interrupted !== true;
210
+ const rel = (p) => relativizePath(p, ctx);
211
+ const dur = call.durationMs !== void 0 ? { durationMs: call.durationMs } : {};
212
+ switch (call.name) {
213
+ case "Read": {
214
+ const path = str(input.file_path) ?? str(input.path);
215
+ if (!path) return { bodies: [], warning: { code: "missing_field", detail: "Read without file_path" } };
216
+ const content = str(obj(obj(resp).file).content);
217
+ return { bodies: [{ kind: "read_file", path: rel(path), ...content !== void 0 ? { bytes: content.length } : {} }] };
218
+ }
219
+ case "Edit":
220
+ case "MultiEdit":
221
+ case "NotebookEdit": {
222
+ const path = str(input.file_path) ?? str(input.notebook_path);
223
+ if (!path) return { bodies: [], warning: { code: "missing_field", detail: `${call.name} without file_path` } };
224
+ const counts = patchCounts(resp) ?? {
225
+ added: lineCount(str(input.new_string) ?? str(input.new_source)),
226
+ removed: lineCount(str(input.old_string)),
227
+ hunks: 1
228
+ };
229
+ return { bodies: [{ kind: "edit_file", path: rel(path), ...counts }] };
230
+ }
231
+ case "Write": {
232
+ const path = str(input.file_path);
233
+ if (!path) return { bodies: [], warning: { code: "missing_field", detail: "Write without file_path" } };
234
+ const r = obj(resp);
235
+ const created = r.type === "create" || r.type === void 0 && r.originalFile === null;
236
+ return { bodies: [{ kind: "write_file", path: rel(path), added: lineCount(str(input.content)), created }] };
237
+ }
238
+ case "Bash": {
239
+ const command = str(input.command) ?? "";
240
+ const c = classifyBash(command);
241
+ const touched = [...new Set([...c.writes, ...c.reads].map(rel))];
242
+ const bodies = [{ kind: "run_command", argv0: c.argv0, intent: c.intent, touched, ok, ...dur }];
243
+ const heredoc = heredocLines(command);
244
+ c.writes.forEach((w, i) => bodies.push({ kind: "write_file", path: rel(w), added: i === 0 ? heredoc : 0, created: false }));
245
+ for (const r of c.reads) bodies.push({ kind: "read_file", path: rel(r) });
246
+ if (c.intent === "test") {
247
+ const sum = parseTestSummary(responseText(resp));
248
+ if (sum) bodies.push({ kind: "test_result", ok: sum.failed === 0 && ok, passed: sum.passed, failed: sum.failed, ...sum.skipped !== void 0 ? { skipped: sum.skipped } : {}, framework: sum.framework, ...dur });
249
+ else bodies.push({ kind: "test_result", ok, passed: 0, failed: 0, framework: "unknown", ...dur });
250
+ }
251
+ return { bodies };
252
+ }
253
+ case "Agent":
254
+ case "Task": {
255
+ const r = obj(resp);
256
+ const agentId = str(r.agentId) ?? str(r.agent_id) ?? "";
257
+ const label = str(input.description) ?? str(input.subagent_type);
258
+ const start = { kind: "subagent_start", childId: agentId, ...label ? { label } : {} };
259
+ if (r.status === "async_launched" || r.isAsync === true) return { bodies: [start], pendingSubagent: { childId: agentId } };
260
+ return { bodies: [start, { kind: "subagent_stop", childId: agentId, ok }] };
261
+ }
262
+ case "AskUserQuestion":
263
+ return { bodies: [{ kind: "waiting_for_user", reason: "question" }] };
264
+ }
265
+ if (SEARCH_TOOLS_REPO.has(call.name)) {
266
+ const n = obj(resp).numFiles ?? obj(resp).numLines ?? obj(resp).count;
267
+ return { bodies: [{ kind: "search", scope: "repo", ...typeof n === "number" ? { hits: n } : {} }] };
268
+ }
269
+ if (SEARCH_TOOLS_WEB.has(call.name)) return { bodies: [{ kind: "search", scope: "web" }] };
270
+ if (SILENT_TOOLS.has(call.name)) return { bodies: [] };
271
+ return {
272
+ bodies: [{ kind: "run_command", argv0: call.name.slice(0, 40), intent: "other", touched: [], ok, ...dur }],
273
+ warning: { code: "unknown_tool", detail: call.name.slice(0, 80) }
274
+ };
275
+ }
276
+ var SUCCESS_LEXICON = [
277
+ /\ball (?:\d+ )?tests? (?:are )?pass(?:ing|ed)?\b/i,
278
+ /\btests? (?:are |is )?(?:now )?(?:passing|green)\b/i,
279
+ /\b(?:is|are|it's|that's|everything(?:'s| is)) (?:now )?(?:fixed|working|done|complete|resolved)\b/i,
280
+ /\bshould (?:now )?work\b/i,
281
+ /\bworks now\b/i,
282
+ /\b(?:done|complete|completed|fixed|resolved|shipped|implemented)[.!]/i,
283
+ /✅/
284
+ ];
285
+ var HEDGE_LEXICON = [
286
+ /\bnot (?:yet )?(?:done|fixed|working|tested|verified|complete)\b/i,
287
+ /\bstill (?:fail|broken|not)/i,
288
+ /\b(?:could ?n[’']?t|cannot|can ?not|unable to|wasn[’']?t able)\b/i,
289
+ /\bfail(?:ed|ing|s)\b/i,
290
+ /\bTODO\b/,
291
+ /\bhaven[’']?t (?:run|tested|verified|checked)/i,
292
+ /\buntested\b/i,
293
+ /\bI did not (?:run|test|verify)/i
294
+ ];
295
+ var TAIL = 600;
296
+ function claimsSuccess(text) {
297
+ if (!text) return false;
298
+ const tail = text.length > TAIL ? text.slice(-TAIL) : text;
299
+ if (HEDGE_LEXICON.some((r) => r.test(tail))) return false;
300
+ return SUCCESS_LEXICON.some((r) => r.test(tail));
301
+ }
302
+ var ROW_TYPES = /* @__PURE__ */ new Set(["user", "assistant", "system"]);
303
+ var obj2 = (x) => x && typeof x === "object" ? x : {};
304
+ var str2 = (x) => typeof x === "string" ? x : void 0;
305
+ function parseTranscript(text, opts = {}) {
306
+ const rows = [];
307
+ const warnings = [];
308
+ const lines = text.split(/\r?\n/);
309
+ for (let i = 0; i < lines.length; i++) {
310
+ const line = lines[i];
311
+ if (!line.trim()) continue;
312
+ try {
313
+ rows.push(JSON.parse(line));
314
+ } catch {
315
+ warnings.push({ code: "bad_json", detail: `line ${i + 1}`, line: i + 1 });
316
+ }
317
+ }
318
+ const res = parseTranscriptRows(rows, opts);
319
+ return { events: res.events, warnings: [...warnings, ...res.warnings] };
320
+ }
321
+ function parseTranscriptRows(rows, opts = {}) {
322
+ const events = [];
323
+ const warnings = [];
324
+ const unknownTypes = /* @__PURE__ */ new Set();
325
+ const pending = /* @__PURE__ */ new Map();
326
+ const openSubagents = /* @__PURE__ */ new Map();
327
+ let sessionId = "";
328
+ let cwd = "";
329
+ let branch;
330
+ let firstTs = Number.NaN;
331
+ let lastTs = Number.NaN;
332
+ let lastAssistantText = "";
333
+ let seq = 0;
334
+ let sawEnd = false;
335
+ const ctx = () => ({ cwd, ...opts.home ? { home: opts.home } : {} });
336
+ const push = (ts, id, body, extra = {}) => {
337
+ events.push({ id, seq: seq++, ts, sessionId, source: "transcript", ...extra, ...body });
338
+ };
339
+ const tsOf = (row) => {
340
+ const t = str2(row.timestamp);
341
+ const n = t ? Date.parse(t) : Number.NaN;
342
+ return Number.isFinite(n) ? n : Number.isFinite(lastTs) ? lastTs : 0;
343
+ };
344
+ const agentOf = (row) => str2(row.agentId) ?? (row.isSidechain === true ? opts.sidechainAgentId ?? "sidechain" : void 0);
345
+ for (const raw of rows) {
346
+ const row = obj2(raw);
347
+ const type = str2(row.type) ?? "";
348
+ if (!ROW_TYPES.has(type)) {
349
+ if (type) unknownTypes.add(type);
350
+ continue;
351
+ }
352
+ if (!sessionId && str2(row.sessionId)) sessionId = str2(row.sessionId);
353
+ if (!cwd && str2(row.cwd)) cwd = str2(row.cwd);
354
+ if (!branch && str2(row.gitBranch)) branch = str2(row.gitBranch);
355
+ const ts = tsOf(row);
356
+ if (!Number.isFinite(firstTs)) {
357
+ firstTs = ts;
358
+ push(ts, `${sessionId || "session"}:start`, { kind: "session_start", repoId: basename(cwd) || "repo", ...branch ? { branch } : {} });
359
+ }
360
+ lastTs = ts;
361
+ const agentId = agentOf(row);
362
+ const message = obj2(row.message);
363
+ const content = message.content;
364
+ if (type === "system") {
365
+ if (row.subtype === "api_error") {
366
+ const attempt = typeof row.retryAttempt === "number" ? row.retryAttempt : 0;
367
+ const max = typeof row.maxRetries === "number" ? row.maxRetries : 0;
368
+ push(ts, str2(row.uuid) ?? `sys:${seq}`, { kind: "error", where: "api", retrying: attempt < max }, agentId ? { agentId } : {});
369
+ }
370
+ continue;
371
+ }
372
+ if (type === "assistant") {
373
+ const blocks2 = Array.isArray(content) ? content : [];
374
+ for (const b of blocks2) {
375
+ const block = obj2(b);
376
+ if (block.type === "tool_use" && str2(block.id) && str2(block.name)) {
377
+ pending.set(str2(block.id), { name: str2(block.name), input: block.input, ts, ...agentId ? { agentId } : {} });
378
+ } else if (block.type === "text" && !agentId) {
379
+ const t = str2(block.text);
380
+ if (t) lastAssistantText = t;
381
+ }
382
+ }
383
+ if (typeof content === "string" && !agentId) lastAssistantText = content;
384
+ if (message.stop_reason === "end_turn" && !agentId) {
385
+ push(ts, `${str2(row.uuid) ?? seq}:turn`, { kind: "turn_end", claimedSuccess: claimsSuccess(lastAssistantText) });
386
+ }
387
+ continue;
388
+ }
389
+ if (row.toolDenialKind !== void 0) push(ts, `${str2(row.uuid) ?? seq}:deny`, { kind: "waiting_for_user", reason: "permission" });
390
+ const blocks = Array.isArray(content) ? content : [];
391
+ for (const b of blocks) {
392
+ const block = obj2(b);
393
+ if (block.type !== "tool_result") continue;
394
+ const useId = str2(block.tool_use_id);
395
+ if (!useId) continue;
396
+ const p = pending.get(useId);
397
+ if (!p) {
398
+ warnings.push({ code: "unpaired_result", detail: useId });
399
+ continue;
400
+ }
401
+ pending.delete(useId);
402
+ const response = row.toolUseResult ?? block.content;
403
+ const out = toolCallToOutcome(
404
+ { name: p.name, input: p.input, response, isError: block.is_error === true, durationMs: Math.max(0, ts - p.ts) },
405
+ ctx()
406
+ );
407
+ if (out.warning) warnings.push(out.warning);
408
+ const rowAgent = p.agentId ?? agentId;
409
+ const parentId = rowAgent ? openSubagents.get(rowAgent) : void 0;
410
+ const extra = { ...rowAgent ? { agentId: rowAgent } : {}, ...parentId ? { parentId } : {} };
411
+ out.bodies.forEach((body, i) => push(p.ts, i === 0 ? useId : `${useId}:${i}`, body, extra));
412
+ if (out.pendingSubagent) openSubagents.set(out.pendingSubagent.childId, useId);
413
+ }
414
+ if (blocks.length === 0 && typeof content === "string" && /\[Request interrupted by user/i.test(content)) {
415
+ push(ts, `${str2(row.uuid) ?? seq}:interrupt`, { kind: "waiting_for_user", reason: "idle" });
416
+ }
417
+ }
418
+ for (const [useId, p] of pending) {
419
+ warnings.push({ code: "unpaired_result", detail: `tool_use ${useId} (${p.name}) has no result` });
420
+ const out = toolCallToOutcome({ name: p.name, input: p.input, isError: true }, ctx());
421
+ if (out.warning) warnings.push(out.warning);
422
+ out.bodies.forEach((body, i) => push(p.ts, i === 0 ? useId : `${useId}:${i}`, body, p.agentId ? { agentId: p.agentId } : {}));
423
+ }
424
+ for (const [childId, spawnId] of openSubagents) {
425
+ push(lastTs, `${spawnId}:stop`, { kind: "subagent_stop", childId, ok: true }, { parentId: spawnId });
426
+ }
427
+ for (const t of unknownTypes) warnings.push({ code: "unknown_row_type", detail: t });
428
+ if (Number.isFinite(firstTs) && !sawEnd) {
429
+ sawEnd = true;
430
+ push(lastTs, `${sessionId || "session"}:end`, { kind: "session_end", reason: "stop", claimedSuccess: claimsSuccess(lastAssistantText) });
431
+ }
432
+ return { events, warnings };
433
+ }
434
+ var obj3 = (x) => x && typeof x === "object" ? x : {};
435
+ var str3 = (x) => typeof x === "string" ? x : void 0;
436
+ var TOOL_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse", "PostToolUseFailure"]);
437
+ function endReason(reason) {
438
+ switch (reason) {
439
+ case "clear":
440
+ case "logout":
441
+ case "prompt_input_exit":
442
+ return "user";
443
+ default:
444
+ return "stop";
445
+ }
446
+ }
447
+ function hookPayloadToEvents(payload, opts = {}) {
448
+ const p = obj3(payload);
449
+ const name = str3(p.hook_event_name);
450
+ const sessionId = str3(p.session_id) ?? "";
451
+ const ts = opts.now ?? Date.now();
452
+ if (!name) return { events: [], warnings: [{ code: "missing_field", detail: "hook_event_name" }] };
453
+ const cwd = str3(p.cwd) ?? "";
454
+ const ctx = { cwd, ...opts.home ? { home: opts.home } : {} };
455
+ const agentId = TOOL_EVENTS.has(name) ? str3(p.agent_id) : void 0;
456
+ const extra = agentId ? { agentId } : {};
457
+ let seq = 0;
458
+ const mk = (body, id = mintId(ts)) => ({ id, seq: seq++, ts, sessionId, source: "hook", ...extra, ...body });
459
+ const one = (body) => ({ events: [mk(body)], warnings: [] });
460
+ switch (name) {
461
+ case "SessionStart":
462
+ return one({ kind: "session_start", repoId: basename(cwd) || "repo" });
463
+ case "SessionEnd":
464
+ return one({ kind: "session_end", reason: endReason(str3(p.reason)), claimedSuccess: false });
465
+ case "Stop":
466
+ return one({ kind: "turn_end", claimedSuccess: claimsSuccess(str3(p.last_assistant_message)) });
467
+ case "SubagentStart": {
468
+ const id = str3(p.agent_id) ?? "";
469
+ const label = str3(p.agent_type);
470
+ return { events: [mk({ kind: "subagent_start", childId: id, ...label ? { label } : {} })], warnings: [] };
471
+ }
472
+ case "SubagentStop":
473
+ return one({ kind: "subagent_stop", childId: str3(p.agent_id) ?? "", ok: true });
474
+ case "Notification": {
475
+ const t = str3(p.notification_type);
476
+ if (t === "permission_prompt") return one({ kind: "waiting_for_user", reason: "permission" });
477
+ if (t === "idle_prompt") return one({ kind: "waiting_for_user", reason: "idle" });
478
+ if (t === "elicitation_dialog") return one({ kind: "waiting_for_user", reason: "question" });
479
+ return { events: [], warnings: [] };
480
+ }
481
+ case "PostToolUseFailure":
482
+ return one({ kind: "error", where: str3(p.tool_name) ?? "tool", retrying: false });
483
+ case "PostToolUse": {
484
+ const tool = str3(p.tool_name);
485
+ if (!tool) return { events: [], warnings: [{ code: "missing_field", detail: "tool_name" }] };
486
+ const dur = typeof p.duration_ms === "number" ? { durationMs: p.duration_ms } : {};
487
+ const out = toolCallToOutcome({ name: tool, input: p.tool_input, response: p.tool_response, ...dur }, ctx);
488
+ const baseId = str3(p.tool_use_id) ?? mintId(ts);
489
+ const events = out.bodies.map((body, i) => mk(body, i === 0 ? baseId : `${baseId}:${i}`));
490
+ return { events, warnings: out.warning ? [out.warning] : [] };
491
+ }
492
+ case "PreToolUse":
493
+ case "UserPromptSubmit":
494
+ case "PreCompact":
495
+ case "PostCompact":
496
+ return { events: [], warnings: [] };
497
+ }
498
+ return { events: [], warnings: [{ code: "unknown_row_type", detail: name }] };
499
+ }
500
+ function hashString(s) {
501
+ let h = 2166136261;
502
+ for (let i = 0; i < s.length; i++) {
503
+ h ^= s.charCodeAt(i);
504
+ h = Math.imul(h, 16777619);
505
+ }
506
+ return h >>> 0;
507
+ }
508
+ function mulberry32(seed) {
509
+ let a = seed >>> 0;
510
+ return () => {
511
+ a = a + 1831565813 >>> 0;
512
+ let t = a;
513
+ t = Math.imul(t ^ t >>> 15, t | 1);
514
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
515
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
516
+ };
517
+ }
518
+ var ARCHIVE_RE = /(^|\/)(schema\.(sql|prisma|graphql|gql)|.*\.(sql|prisma|db|sqlite)|migrations?\/|drizzle\/|models?\/.*\.(ts|js|py|rb)$)/i;
519
+ var IGNORE_RE = /(^|\/)(node_modules|\.git|dist|build|out|coverage|_site|\.next|\.turbo|target|vendor|__pycache__|\.venv|venv)(\/|$)|(^|\/)\.[^/]+$|\.(lock|map|min\.js|png|jpg|jpeg|gif|webp|svg|ico|woff2?|ttf|mp4|mp3|pdf|zip)$/i;
520
+ function isIgnoredPath(p) {
521
+ return IGNORE_RE.test(p);
522
+ }
523
+ function kindOf(path) {
524
+ if (isTestPath(path)) return "beacon";
525
+ if (ARCHIVE_RE.test(path)) return "archive";
526
+ return "building";
527
+ }
528
+ function locOf(f) {
529
+ if (typeof f.loc === "number") return f.loc;
530
+ if (typeof f.size === "number") return Math.max(1, Math.round(f.size / 40));
531
+ return 1;
532
+ }
533
+ function scoreFiles(files, touched) {
534
+ const kept = files.filter((f) => !isIgnoredPath(f.path));
535
+ const maxLoc = Math.max(1, ...kept.map(locOf));
536
+ const maxChurn = Math.max(1, ...kept.map((f) => f.churn ?? 0));
537
+ return kept.map((file) => {
538
+ const loc = locOf(file);
539
+ const t = touched.has(file.path);
540
+ const score = 0.5 * Math.sqrt(loc / maxLoc) + 0.3 * ((file.churn ?? 0) / maxChurn) + (t ? 0.2 : 0);
541
+ return { file, loc, score, kind: kindOf(file.path), touched: t };
542
+ });
543
+ }
544
+ function heightClass(loc, maxLoc) {
545
+ const r = Math.sqrt(loc / Math.max(1, maxLoc));
546
+ return Math.max(1, Math.min(5, Math.ceil(r * 5)));
547
+ }
548
+ function footprintOf(height) {
549
+ return height >= 5 ? 3 : height >= 3 ? 2 : 1;
550
+ }
551
+ var EXT_LANG = { ts: "TypeScript", tsx: "TypeScript", js: "JavaScript", jsx: "JavaScript", mjs: "JavaScript", py: "Python", go: "Go", rs: "Rust", java: "Java", kt: "Kotlin", swift: "Swift", rb: "Ruby", php: "PHP", cs: "C#", cpp: "C++", cc: "C++", c: "C", h: "C", m: "Objective-C", scala: "Scala", ex: "Elixir", exs: "Elixir", dart: "Dart", lua: "Lua", zig: "Zig", sh: "Shell", sql: "SQL", md: "Markdown", mdx: "Markdown", html: "HTML", css: "CSS", scss: "CSS", vue: "Vue", svelte: "Svelte", tf: "Terraform", yaml: "YAML", yml: "YAML", ipynb: "Notebook" };
552
+ function profileRepo(files, manifests = {}) {
553
+ const paths = files.map((f) => f.path.toLowerCase());
554
+ const has = (re) => paths.some((p) => re.test(p));
555
+ const count = (re) => paths.filter((p) => re.test(p)).length;
556
+ const langCount = /* @__PURE__ */ new Map();
557
+ for (const p of paths) {
558
+ const ext = p.slice(p.lastIndexOf(".") + 1);
559
+ const l = EXT_LANG[ext];
560
+ if (l && !/^(markdown|yaml|html|css|shell|sql)$/i.test(l)) langCount.set(l, (langCount.get(l) ?? 0) + 1);
561
+ }
562
+ const languages = [...langCount.entries()].sort((a, b) => b[1] - a[1]).map(([l]) => l).slice(0, 4);
563
+ const pkg2 = (manifests["package.json"] ?? "").toLowerCase();
564
+ const py = ((manifests["pyproject.toml"] ?? "") + (manifests["requirements.txt"] ?? "")).toLowerCase();
565
+ const dep = (re) => re.test(pkg2) || re.test(py);
566
+ const scores = /* @__PURE__ */ new Map();
567
+ const add = (k, n) => scores.set(k, (scores.get(k) ?? 0) + n);
568
+ if (dep(/"(react|react-dom|next|vue|nuxt|svelte|@sveltejs\/kit|@angular\/core|solid-js|astro|remix|vite)"/)) add("web-frontend", 3);
569
+ if (count(/\.(tsx|jsx|vue|svelte)$/) > 3) add("web-frontend", 2);
570
+ if (has(/(^|\/)(index|app)\.html$/) || count(/\.(css|scss)$/) > 3) add("web-frontend", 1);
571
+ if (dep(/"(express|fastify|hono|koa|@nestjs\/core|apollo-server|prisma|drizzle-orm|django|flask|fastapi|sqlalchemy|gin|fiber|actix|axum|rails|spring)"/)) add("backend-api", 3);
572
+ if (has(/(^|\/)(routes?|controllers?|api|handlers?|migrations?)\//) || has(/\.(sql|prisma)$/)) add("backend-api", 2);
573
+ if (pkg2.includes('"bin"') || has(/(^|\/)(cli|cmd|bin)\//) || dep(/"(commander|yargs|clap|cobra|click|typer)"/)) add("cli-tool", 3);
574
+ if (dep(/"(torch|tensorflow|jax|pandas|numpy|scikit-learn|sklearn|transformers|xgboost|lightgbm|polars)"/) || count(/\.ipynb$/) > 0 || has(/(^|\/)(notebooks?|models?|datasets?|training)\//)) add("data-ml", 3);
575
+ if (has(/\.(swift|kt)$/) || has(/(^|\/)(ios|android)\//) || has(/package\.swift$/) || has(/build\.gradle(\.kts)?$/) || dep(/"(react-native|expo|flutter)"/) || has(/\.dart$/)) add("mobile", 3);
576
+ if (has(/\.(unity|unitypackage|godot|tscn|gd)$/) || has(/(^|\/)assets\/.*\.(prefab|fbx)$/) || dep(/"(phaser|pixi\.js|three|babylonjs|@react-three\/fiber|excalibur|kaplay|bevy|godot)"/)) add("game", 3);
577
+ if (has(/\.tf$/) || has(/(^|\/)(dockerfile|docker-compose\.ya?ml|helm|k8s|kubernetes|\.github\/workflows|ansible|pulumi)/)) add("infra-devops", has(/\.tf$/) || has(/(^|\/)(helm|k8s|kubernetes|ansible|pulumi)\//) ? 3 : 1);
578
+ const mdShare = count(/\.(md|mdx|rst)$/) / Math.max(1, paths.length);
579
+ if (mdShare > 0.4 || has(/(^|\/)(mkdocs|docusaurus)\.config|(^|\/)docs\/.*\.mdx?$/) && mdShare > 0.25) add("docs-content", 3);
580
+ if (has(/(^|\/)pnpm-workspace\.ya?ml$/) || has(/(^|\/)packages\/[^/]+\/package\.json$/) || has(/(^|\/)(lerna|nx|turbo)\.json$/) || has(/(^|\/)cargo\.toml$/) && count(/(^|\/)crates\/[^/]+\//) > 0) add("monorepo", 2);
581
+ if (count(/\.(rs|go|c|cpp|cc|h|zig)$/) > 5 && !scores.has("web-frontend")) add("systems", 2);
582
+ if (pkg2.includes('"main"') && !pkg2.includes('"bin"') && !scores.has("web-frontend") && !scores.has("backend-api")) add("library", 2);
583
+ if (has(/(^|\/)(lib|src\/lib)\//) && !scores.has("web-frontend")) add("library", 1);
584
+ const kinds = [...scores.entries()].filter(([, n]) => n >= 2).sort((a, b) => b[1] - a[1]).map(([k]) => k);
585
+ return { kinds: kinds.length ? kinds : ["unknown"], languages, files: files.length };
586
+ }
587
+ function generateWorld(opts) {
588
+ const cap = opts.cap ?? 60;
589
+ const beaconCap = opts.beaconCap ?? 8;
590
+ const touchedCap = opts.touchedCap ?? 48;
591
+ const touched = new Set((opts.touched ?? []).map(norm2).slice(0, touchedCap));
592
+ const seed = hashString(opts.repoId);
593
+ const rnd = mulberry32(seed);
594
+ const files = opts.files.map((f) => ({ ...f, path: norm2(f.path) }));
595
+ const scored = scoreFiles(files, touched);
596
+ const maxLoc = Math.max(1, ...scored.map((s) => s.loc));
597
+ const byScore = [...scored].sort((a, b) => b.score - a.score || a.file.path.localeCompare(b.file.path));
598
+ const beacons = pick(byScore.filter((s) => s.kind === "beacon"), beaconCap);
599
+ const others = pick(byScore.filter((s) => s.kind !== "beacon"), cap);
600
+ const selected = [...others, ...beacons];
601
+ const capped = selected.length < scored.length;
602
+ const groups = /* @__PURE__ */ new Map();
603
+ for (const s of others) {
604
+ const d = topDir(s.file.path);
605
+ (groups.get(d) ?? groups.set(d, []).get(d)).push(s);
606
+ }
607
+ const names = [...groups.keys()].sort();
608
+ const occupied = /* @__PURE__ */ new Set();
609
+ const places = [];
610
+ const districts = [];
611
+ const forge = { id: "forge", kind: "forge", path: "", label: "forge", district: "", gx: -1, gy: -1, footprint: 2, height: 2, score: 0, touched: false };
612
+ places.push(forge);
613
+ const layout = opts.layout ?? LAYOUTS[seed % LAYOUTS.length];
614
+ const jitter = rnd() * Math.PI * 2;
615
+ const sized = names.map((name) => {
616
+ const items = groups.get(name).sort((a, b) => b.score - a.score || a.file.path.localeCompare(b.file.path));
617
+ const cells = items.reduce((n, s) => n + footprintOf(heightClass(s.loc, maxLoc)) ** 2, 0);
618
+ const side = Math.max(3, Math.ceil(Math.sqrt(cells * 1.45)));
619
+ return { name, items, side };
620
+ });
621
+ const placedRects = [];
622
+ const hubRect = { x: -2, y: -2, w: 4, h: 4 };
623
+ const rects = layoutRects(layout, sized.map((d) => d.side), hubRect, jitter, rnd);
624
+ placedRects.push(hubRect, ...rects);
625
+ const hub = hubFor(layout, rects);
626
+ forge.gx = hub[0];
627
+ forge.gy = hub[1];
628
+ occupied.clear();
629
+ claim(occupied, forge);
630
+ sized.forEach((d0, i) => {
631
+ const rect = rects[i];
632
+ const d = { id: `d:${d0.name}`, label: d0.name === "" ? "/" : `${d0.name}/`, bounds: rect, cx: rect.x + Math.floor(rect.w / 2), cy: rect.y + Math.floor(rect.h / 2), placeIds: [] };
633
+ for (const s of d0.items) {
634
+ const h = heightClass(s.loc, maxLoc);
635
+ const fp = footprintOf(h);
636
+ const spot = spiralSpot(occupied, d.cx, d.cy, fp, rect);
637
+ const p = { id: `p:${s.file.path}`, kind: s.kind, path: s.file.path, label: base(s.file.path), district: d.id, gx: spot[0], gy: spot[1], footprint: fp, height: h, score: round3(s.score), touched: s.touched };
638
+ claim(occupied, p);
639
+ places.push(p);
640
+ d.placeIds.push(p.id);
641
+ }
642
+ districts.push(d);
643
+ });
644
+ let ring = Math.ceil(maxExtent(placedRects));
645
+ void ring;
646
+ for (const r of placedRects) for (let x = r.x - 1; x < r.x + r.w + 1; x++) for (let y = r.y - 1; y < r.y + r.h + 1; y++) occupied.add(`${x},${y}`);
647
+ for (const s of beacons) {
648
+ const home = districts.find((d) => d.id === `d:${topDir(s.file.path)}`);
649
+ const spot = home ? edgeSpot(occupied, home, forge, 1) : nearSpot(occupied, forge.gx + 1, forge.gy + 1, rnd() * Math.PI * 2, 4, 1);
650
+ const p = { id: `p:${s.file.path}`, kind: "beacon", path: s.file.path, label: base(s.file.path), district: home?.id ?? "", gx: spot[0], gy: spot[1], footprint: 1, height: 4, score: round3(s.score), touched: s.touched };
651
+ claim(occupied, p);
652
+ places.push(p);
653
+ home?.placeIds.push(p.id);
654
+ }
655
+ const dockSpot = nearSpot(occupied, forge.gx + 1, forge.gy + 1, jitter + Math.PI, 5, 2);
656
+ const dock = { id: "dock", kind: "dock", path: "", label: "dock", district: "", gx: dockSpot[0], gy: dockSpot[1], footprint: 2, height: 1, score: 0, touched: false };
657
+ claim(occupied, dock);
658
+ places.push(dock);
659
+ const paths = layoutPaths(layout, districts, forge, dock);
660
+ if (capped) {
661
+ for (const d of districts) {
662
+ const top = d.id.slice(2);
663
+ const total = scored.filter((s) => s.kind !== "beacon" && topDir(s.file.path) === top).length;
664
+ if (total <= d.placeIds.length) continue;
665
+ const spot = spiralSpot(occupied, d.cx, d.cy, 1, d.bounds);
666
+ const p = { id: `p:${top ? top + "/" : ""}\u2026`, kind: "overflow", path: `${top ? top + "/" : ""}\u2026`, label: `+${total - d.placeIds.length} more`, district: d.id, gx: spot[0], gy: spot[1], footprint: 1, height: 2, score: 0, touched: false };
667
+ claim(occupied, p);
668
+ places.push(p);
669
+ d.placeIds.push(p.id);
670
+ }
671
+ }
672
+ const bounds = boundsOf(places, 2);
673
+ const profile = profileRepo(files, opts.manifests ?? {});
674
+ return { repoId: opts.repoId, seed, layout, profile, districts, places, paths, bounds, stats: { files: scored.length, placed: selected.length, capped } };
675
+ }
676
+ var norm2 = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
677
+ var base = (p) => p.slice(p.lastIndexOf("/") + 1);
678
+ var topDir = (p) => p.includes("/") ? p.slice(0, p.indexOf("/")) : "";
679
+ var round3 = (n) => Math.round(n * 1e3) / 1e3;
680
+ function pick(sorted, cap) {
681
+ const forced = sorted.filter((s) => s.touched);
682
+ const rest = sorted.filter((s) => !s.touched);
683
+ return [...forced, ...rest.slice(0, Math.max(0, cap - forced.length))];
684
+ }
685
+ function overlaps(a, b, gap) {
686
+ return a.x < b.x + b.w + gap && b.x < a.x + a.w + gap && a.y < b.y + b.h + gap && b.y < a.y + a.h + gap;
687
+ }
688
+ function cellsOf(gx, gy, fp) {
689
+ const out = [];
690
+ for (let dx = 0; dx < fp; dx++) for (let dy = 0; dy < fp; dy++) out.push(`${gx + dx},${gy + dy}`);
691
+ return out;
692
+ }
693
+ function free(occ, gx, gy, fp) {
694
+ return cellsOf(gx, gy, fp).every((c) => !occ.has(c));
695
+ }
696
+ function claim(occ, p) {
697
+ for (const c of cellsOf(p.gx, p.gy, p.footprint)) occ.add(c);
698
+ }
699
+ function spiralSpot(occ, cx, cy, fp, rect) {
700
+ let x = 0, y = 0, dx = 0, dy = -1;
701
+ let fallback = null;
702
+ for (let i = 0; i < 4096; i++) {
703
+ const gx = cx + x - Math.floor(fp / 2), gy = cy + y - Math.floor(fp / 2);
704
+ if (free(occ, gx, gy, fp)) {
705
+ const inside = gx >= rect.x - 1 && gy >= rect.y - 1 && gx + fp <= rect.x + rect.w + 1 && gy + fp <= rect.y + rect.h + 1;
706
+ if (inside) return [gx, gy];
707
+ fallback ??= [gx, gy];
708
+ if (i > 600) return fallback;
709
+ }
710
+ if (x === y || x < 0 && x === -y || x > 0 && x === 1 - y) [dx, dy] = [-dy, dx];
711
+ x += dx;
712
+ y += dy;
713
+ }
714
+ return fallback ?? [cx, cy];
715
+ }
716
+ function maxExtent(rects) {
717
+ return Math.max(...rects.map((r) => Math.max(Math.abs(r.x), Math.abs(r.x + r.w), Math.abs(r.y), Math.abs(r.y + r.h))));
718
+ }
719
+ function boundsOf(places, pad) {
720
+ const xs = places.flatMap((p) => [p.gx, p.gx + p.footprint]);
721
+ const ys = places.flatMap((p) => [p.gy, p.gy + p.footprint]);
722
+ const x = Math.min(...xs) - pad, y = Math.min(...ys) - pad;
723
+ return { x, y, w: Math.max(...xs) + pad - x, h: Math.max(...ys) + pad - y };
724
+ }
725
+ var LAYOUTS = ["ring", "grid", "strip", "scatter", "terrace", "spine"];
726
+ function layoutRects(style, sides, hub, jitter, rnd) {
727
+ const n = sides.length, out = [];
728
+ const maxSide = Math.max(3, ...sides);
729
+ const fits = (r) => !overlaps(hub, r, 1) && !out.some((o) => overlaps(o, r, 1));
730
+ const push = (x, y, side) => {
731
+ const r = { x, y, w: side, h: side };
732
+ out.push(r);
733
+ return r;
734
+ };
735
+ switch (style) {
736
+ case "grid": {
737
+ const cols = Math.max(2, Math.ceil(Math.sqrt(n + 1)));
738
+ const cells = [];
739
+ let y = 0, i = 0;
740
+ while (i < n) {
741
+ const row = sides.slice(i, i + cols), rowH = Math.max(...row) + 2;
742
+ let x = 0;
743
+ for (const w of row) {
744
+ cells.push({ x, y });
745
+ x += w + 2;
746
+ }
747
+ y += rowH;
748
+ i += cols;
749
+ }
750
+ const totalW = Math.max(...cells.map((c, j) => c.x + sides[j])), totalH = y;
751
+ for (let j = 0; j < n; j++) {
752
+ let rect = { x: cells[j].x - Math.round(totalW / 2), y: cells[j].y - Math.round(totalH / 2), w: sides[j], h: sides[j] };
753
+ while (!fits(rect)) rect = { ...rect, x: rect.x + 1 };
754
+ out.push(rect);
755
+ }
756
+ return out;
757
+ }
758
+ case "strip": {
759
+ const total = sides.reduce((a, b) => a + b + 2, 0);
760
+ let x = Math.round(-total / 2) + 6;
761
+ for (let i = 0; i < n; i++) {
762
+ const wobble = Math.round((rnd() - 0.5) * 3);
763
+ push(x, wobble - Math.floor(sides[i] / 2), sides[i]);
764
+ x += sides[i] + 2;
765
+ }
766
+ return out;
767
+ }
768
+ case "terrace": {
769
+ let x = -Math.round(n * (maxSide + 1) / 2), y = -Math.round(n * (maxSide + 1) * 0.3);
770
+ for (let i = 0; i < n; i++) {
771
+ push(x, y, sides[i]);
772
+ x += sides[i] + 1;
773
+ y += Math.round(sides[i] * 0.6) + 1;
774
+ }
775
+ return out;
776
+ }
777
+ case "spine": {
778
+ let y = -Math.round(n * (maxSide + 2) / 4);
779
+ for (let i = 0; i < n; i++) {
780
+ const left = i % 2 === 0;
781
+ push(left ? -sides[i] - 3 : 3, y, sides[i]);
782
+ if (!left) y += maxSide + 2;
783
+ }
784
+ return out;
785
+ }
786
+ case "scatter": {
787
+ for (let i = 0; i < n; i++) {
788
+ let R = 5 + maxSide, placed = false;
789
+ for (let tries = 0; tries < 400 && !placed; tries++) {
790
+ const a = jitter + rnd() * Math.PI * 2, r = R * (0.5 + rnd() * 0.7);
791
+ const rect = { x: Math.round(Math.cos(a) * r), y: Math.round(Math.sin(a) * r * 0.8), w: sides[i], h: sides[i] };
792
+ if (fits(rect)) {
793
+ out.push(rect);
794
+ placed = true;
795
+ }
796
+ if (tries % 40 === 39) R += 2;
797
+ }
798
+ if (!placed) push(R + i * 2, 0, sides[i]);
799
+ }
800
+ return out;
801
+ }
802
+ default: {
803
+ let ring = 3;
804
+ for (let i = 0; i < n; i++) {
805
+ const side = sides[i], angle = jitter + i / Math.max(1, n) * Math.PI * 2;
806
+ let r = ring + side / 2, rect;
807
+ for (; ; ) {
808
+ const cx = Math.round(Math.cos(angle) * r), cy = Math.round(Math.sin(angle) * r * 0.8);
809
+ rect = { x: cx - Math.floor(side / 2), y: cy - Math.floor(side / 2), w: side, h: side };
810
+ if (fits(rect)) break;
811
+ r += 1;
812
+ }
813
+ out.push(rect);
814
+ ring = Math.max(ring, r - side / 2);
815
+ }
816
+ return out;
817
+ }
818
+ }
819
+ }
820
+ function hubFor(style, rects) {
821
+ if (!rects.length) return [-1, -1];
822
+ const minX = Math.min(...rects.map((r) => r.x)), minY = Math.min(...rects.map((r) => r.y));
823
+ switch (style) {
824
+ case "strip":
825
+ return [minX - 5, -1];
826
+ case "terrace":
827
+ return [minX - 5, minY - 1];
828
+ case "spine":
829
+ return [-1, minY - 5];
830
+ default:
831
+ return [-1, -1];
832
+ }
833
+ }
834
+ function layoutPaths(style, districts, forge, dock) {
835
+ const hub = [forge.gx + 1, forge.gy + 1];
836
+ const paths = [];
837
+ if (style === "strip" || style === "terrace") {
838
+ const ordered = [...districts].sort((a, b) => a.cx - b.cx || a.cy - b.cy);
839
+ let prev = hub, prevId = "forge";
840
+ for (const d of ordered) {
841
+ paths.push({ from: prevId, to: d.id, points: [prev, [d.cx, d.cy]] });
842
+ prev = [d.cx, d.cy];
843
+ prevId = d.id;
844
+ }
845
+ } else if (style === "grid" || style === "spine") {
846
+ for (const d of districts) paths.push({ from: d.id, to: "forge", points: [[d.cx, d.cy], [d.cx, hub[1]], hub] });
847
+ } else {
848
+ for (const d of districts) paths.push({ from: d.id, to: "forge", points: [[d.cx, d.cy], [Math.round((d.cx + hub[0]) / 2), Math.round((d.cy + hub[1]) / 2)], hub] });
849
+ }
850
+ paths.push({ from: "dock", to: "forge", points: [[dock.gx, dock.gy], hub] });
851
+ return paths;
852
+ }
853
+ function nearSpot(occ, cx, cy, ang, radius, fp) {
854
+ for (let r = radius; r < radius + 40; r++) {
855
+ for (let k = 0; k < 64; k++) {
856
+ const a = ang + (k % 2 ? 1 : -1) * Math.ceil(k / 2) * (Math.PI / 32);
857
+ const gx = Math.round(cx + Math.cos(a) * r), gy = Math.round(cy + Math.sin(a) * r * 0.85);
858
+ if (free(occ, gx, gy, fp)) return [gx, gy];
859
+ }
860
+ }
861
+ return [cx + radius, cy];
862
+ }
863
+ function edgeSpot(occ, d, hub, fp) {
864
+ const ang = Math.atan2(d.cy - hub.gy, d.cx - hub.gx);
865
+ const r0 = Math.ceil(Math.max(d.bounds.w, d.bounds.h) / 2) + 2;
866
+ return nearSpot(occ, d.cx, d.cy, ang, r0, fp);
867
+ }
868
+ var MoodTracker = class {
869
+ constructor(windowMs) {
870
+ this.windowMs = windowMs;
871
+ }
872
+ windowMs;
873
+ mood = "curious";
874
+ trouble = [];
875
+ recordTrouble(ts) {
876
+ this.trouble = this.trouble.filter((t) => ts - t <= this.windowMs);
877
+ this.trouble.push(ts);
878
+ return this.trouble.length >= 2;
879
+ }
880
+ onRead() {
881
+ return this.mood = this.mood === "lost" ? "lost" : "curious";
882
+ }
883
+ onBuild() {
884
+ return this.mood = this.mood === "lost" ? "lost" : "focused";
885
+ }
886
+ onFail(ts) {
887
+ return this.mood = this.recordTrouble(ts) ? "lost" : "uneasy";
888
+ }
889
+ onError(ts) {
890
+ return this.mood = this.recordTrouble(ts) ? "lost" : "uneasy";
891
+ }
892
+ onPass() {
893
+ this.trouble = [];
894
+ return this.mood = "happy";
895
+ }
896
+ onClaimUnverified() {
897
+ return this.mood = "uneasy";
898
+ }
899
+ };
900
+ var Director = class {
901
+ constructor(world, opts = {}) {
902
+ this.world = world;
903
+ for (const p of world.places) {
904
+ this.places.set(p.id, p);
905
+ if (p.path) this.byPath.set(p.path, p);
906
+ }
907
+ this.beacons = world.places.filter((p) => p.kind === "beacon");
908
+ this.forge = world.places.find((p) => p.kind === "forge") ?? world.places[0];
909
+ this.dock = world.places.find((p) => p.kind === "dock") ?? this.forge;
910
+ this.maxGap = opts.maxGapMs ?? 2500;
911
+ this.msPerCell = opts.walkMsPerCell ?? 90;
912
+ this.mood = new MoodTracker(opts.lostWindowMs ?? 9e4);
913
+ this.actorAt.set("pip", this.forge.id);
914
+ }
915
+ world;
916
+ places = /* @__PURE__ */ new Map();
917
+ byPath = /* @__PURE__ */ new Map();
918
+ beacons;
919
+ forge;
920
+ dock;
921
+ maxGap;
922
+ msPerCell;
923
+ mood;
924
+ beats = [];
925
+ sceneMs = 0;
926
+ lastRealTs = null;
927
+ seq = 0;
928
+ actorAt = /* @__PURE__ */ new Map();
929
+ helpers = /* @__PURE__ */ new Map();
930
+ gremlins = /* @__PURE__ */ new Map();
931
+ // gremlinId -> place
932
+ lastEdited = null;
933
+ pendingBeacon = null;
934
+ editedSinceTurn = false;
935
+ verifiedSinceEdit = false;
936
+ everVerified = false;
937
+ claimGremlin = null;
938
+ get timeline() {
939
+ return this.beats;
940
+ }
941
+ feed(e) {
942
+ const start = this.beats.length;
943
+ this.advanceClock(e.ts, e.id);
944
+ const actor = e.agentId && this.helpers.has(e.agentId) ? e.agentId : "pip";
945
+ switch (e.kind) {
946
+ case "session_start":
947
+ this.emit(e, actor, { kind: "curtain_up", repoId: e.repoId }, 1200);
948
+ break;
949
+ case "read_file": {
950
+ const p = this.placeFor(e.path, e, actor);
951
+ this.walk(e, actor, p);
952
+ this.emit(e, actor, { kind: "read", target: p.id }, clamp(700 + (e.bytes ?? 400) / 4, 900, 1800), this.mood.onRead());
953
+ break;
954
+ }
955
+ case "edit_file":
956
+ case "write_file": {
957
+ const p = this.placeFor(e.path, e, actor);
958
+ this.walk(e, actor, p);
959
+ const lines = e.kind === "edit_file" ? e.added + e.removed : e.added;
960
+ const intensity = clamp(Math.sqrt(lines / 50), 0.15, 1);
961
+ const g = [...this.gremlins].find(([, at]) => at === p.id);
962
+ if (g) this.emit(e, actor, { kind: "chase", gremlinId: g[0], target: p.id }, 900, this.mood.mood);
963
+ if (e.kind === "write_file" && e.created) this.emit(e, actor, { kind: "raise", target: p.id }, 900, this.mood.onBuild());
964
+ this.emit(e, actor, { kind: "build", target: p.id, intensity }, clamp(600 + intensity * 1900, 800, 2500), this.mood.onBuild());
965
+ this.lastEdited = p;
966
+ this.editedSinceTurn = true;
967
+ this.verifiedSinceEdit = false;
968
+ break;
969
+ }
970
+ case "run_command": {
971
+ if (e.intent === "test") {
972
+ const beacon = this.beaconFor(e.touched);
973
+ this.walk(e, actor, beacon);
974
+ this.pendingBeacon = beacon;
975
+ this.emit(e, actor, { kind: "run_tests", target: beacon.id }, clamp(e.durationMs ?? 1200, 900, 3e3), this.mood.mood);
976
+ } else if (e.intent === "search") {
977
+ this.emit(e, actor, { kind: "search", scope: "repo" }, 700, this.mood.onRead());
978
+ } else if (e.touched.length === 0 || e.intent === "git" || e.intent === "install" || e.intent === "build") {
979
+ this.walk(e, actor, this.forge);
980
+ const intensity = e.intent === "build" || e.intent === "install" ? 0.8 : e.intent === "git" ? 0.4 : 0.5;
981
+ this.emit(e, actor, { kind: "forge", intensity }, clamp(e.durationMs ?? 800, 700, 2e3), this.mood.mood);
982
+ }
983
+ if (!e.ok && e.intent !== "test") this.emit(e, actor, { kind: "stumble", retrying: false }, 600, this.mood.onError(e.ts));
984
+ break;
985
+ }
986
+ case "test_result": {
987
+ const beacon = this.pendingBeacon ?? this.beaconFor([]);
988
+ this.pendingBeacon = null;
989
+ const outcome = e.framework === "unknown" && e.passed === 0 && e.failed === 0 ? e.ok ? "pass" : "fail" : e.ok ? "pass" : "fail";
990
+ if (outcome === "pass") {
991
+ this.emit(e, actor, { kind: "test_outcome", target: beacon.id, outcome, passed: e.passed, failed: e.failed }, 800, this.mood.onPass());
992
+ if (e.passed > 0 || e.framework === "unknown") {
993
+ this.verifiedSinceEdit = true;
994
+ this.everVerified = true;
995
+ }
996
+ for (const [gid] of this.gremlins) this.emit(e, actor, { kind: "banish", gremlinId: gid }, 700, "happy");
997
+ this.gremlins.clear();
998
+ if (this.claimGremlin) {
999
+ this.emit(e, actor, { kind: "banish", gremlinId: this.claimGremlin }, 700, "happy");
1000
+ this.claimGremlin = null;
1001
+ }
1002
+ this.emit(e, actor, { kind: "celebrate", target: beacon.id }, 1600, "happy");
1003
+ } else {
1004
+ this.emit(e, actor, { kind: "test_outcome", target: beacon.id, outcome, passed: e.passed, failed: e.failed }, 800, this.mood.onFail(e.ts));
1005
+ const at = this.lastEdited ?? beacon;
1006
+ const gid = `g:${e.id}`;
1007
+ this.gremlins.set(gid, at.id);
1008
+ this.emit(e, actor, { kind: "spawn_gremlin", gremlinId: gid, target: at.id }, 900, this.mood.mood);
1009
+ }
1010
+ break;
1011
+ }
1012
+ case "search":
1013
+ this.emit(e, actor, { kind: "search", scope: e.scope }, 700, this.mood.onRead());
1014
+ break;
1015
+ case "subagent_start":
1016
+ this.helpers.set(e.childId, e.label ? { label: e.label } : {});
1017
+ this.actorAt.set(e.childId, this.dock.id);
1018
+ this.emit(e, "pip", { kind: "summon_helper", helperId: e.childId, ...e.label ? { label: e.label } : {} }, 1400, this.mood.mood);
1019
+ break;
1020
+ case "subagent_stop":
1021
+ if (this.helpers.has(e.childId)) {
1022
+ this.emit(e, e.childId, { kind: "dismiss_helper", helperId: e.childId, ok: e.ok }, 1e3, this.mood.mood);
1023
+ this.helpers.delete(e.childId);
1024
+ this.actorAt.delete(e.childId);
1025
+ }
1026
+ break;
1027
+ case "waiting_for_user":
1028
+ if (e.reason === "idle") this.emit(e, actor, { kind: "your_turn" }, 1e3, this.mood.mood);
1029
+ else this.emit(e, actor, { kind: "blocked_on_you", reason: e.reason }, 1200, this.mood.mood);
1030
+ break;
1031
+ case "error":
1032
+ this.emit(e, actor, { kind: "stumble", retrying: e.retrying }, 700, this.mood.onError(e.ts));
1033
+ break;
1034
+ case "turn_end": {
1035
+ if (e.claimedSuccess && this.editedSinceTurn && !this.verifiedSinceEdit) {
1036
+ const gid = `claim:${e.id}`;
1037
+ this.claimGremlin = gid;
1038
+ this.emit(e, actor, { kind: "unverified_claim", gremlinId: gid }, 1400, this.mood.onClaimUnverified());
1039
+ }
1040
+ this.editedSinceTurn = false;
1041
+ this.emit(e, actor, { kind: "your_turn" }, 1e3, this.mood.mood);
1042
+ break;
1043
+ }
1044
+ case "session_end":
1045
+ this.emit(e, actor, { kind: "curtain_down", verified: this.everVerified && !this.claimGremlin }, 1500, this.mood.mood);
1046
+ break;
1047
+ }
1048
+ return this.beats.slice(start);
1049
+ }
1050
+ // ---- internals ----
1051
+ advanceClock(ts, eventId) {
1052
+ if (this.lastRealTs !== null) {
1053
+ const lastEnd = this.beats.length ? Math.max(...this.beats.slice(-3).map((b) => b.atMs + b.durationMs)) : this.sceneMs;
1054
+ const gap = Math.min(Math.max(0, ts - this.lastRealTs), this.maxGap);
1055
+ const target = Math.max(lastEnd, this.sceneMs) + gap;
1056
+ if (target - lastEnd >= 1e3) {
1057
+ const idle = { id: `b${this.seq++}`, atMs: lastEnd, durationMs: target - lastEnd, realTs: this.lastRealTs, mood: this.mood.mood, actor: "pip", sourceEventIds: [eventId], kind: "idle" };
1058
+ this.beats.push(idle);
1059
+ }
1060
+ this.sceneMs = Math.max(this.sceneMs, target);
1061
+ }
1062
+ this.lastRealTs = ts;
1063
+ }
1064
+ emit(e, actor, body, durationMs, mood = this.mood.mood) {
1065
+ durationMs = Math.round(durationMs);
1066
+ if ("intensity" in body) body = { ...body, intensity: Math.round(body.intensity * 1e3) / 1e3 };
1067
+ const b = { id: `b${this.seq++}`, atMs: Math.round(this.sceneMs), durationMs, realTs: e.ts, mood, actor, sourceEventIds: [e.id], ...body };
1068
+ this.beats.push(b);
1069
+ this.sceneMs += durationMs;
1070
+ return b;
1071
+ }
1072
+ walk(e, actor, to) {
1073
+ const fromId = this.actorAt.get(actor) ?? this.forge.id;
1074
+ if (fromId === to.id) return;
1075
+ const from = this.places.get(fromId) ?? this.forge;
1076
+ const cells = Math.hypot(to.gx - from.gx, to.gy - from.gy);
1077
+ this.emit(e, actor, { kind: "walk_to", target: to.id }, clamp(Math.round(cells * this.msPerCell), 300, 2500));
1078
+ this.actorAt.set(actor, to.id);
1079
+ }
1080
+ raised = 0;
1081
+ /** Known place; else the district's overflow building; else (bounded) a new one raised for a file created mid-session. */
1082
+ placeFor(path, e, actor) {
1083
+ const known = this.byPath.get(path);
1084
+ if (known) return known;
1085
+ const top = path.includes("/") ? path.slice(0, path.indexOf("/")) : "";
1086
+ const overflow = this.byPath.get(`${top ? top + "/" : ""}\u2026`);
1087
+ if (overflow && (this.raised >= 24 || !this.world.stats.capped === false)) return overflow;
1088
+ if (this.raised >= 24) return overflow ?? this.forge;
1089
+ this.raised++;
1090
+ const district = this.world.districts.find((d) => d.id === `d:${top}`) ?? this.world.districts[0];
1091
+ const anchor = district ? { gx: district.cx, gy: district.cy } : { gx: this.forge.gx + 3, gy: this.forge.gy };
1092
+ const place = { id: `p:${path}`, kind: kindOf(path), path, label: path.slice(path.lastIndexOf("/") + 1), district: district?.id ?? "", gx: anchor.gx, gy: anchor.gy, footprint: 1, height: 1, score: 0, touched: true };
1093
+ this.places.set(place.id, place);
1094
+ this.byPath.set(path, place);
1095
+ if (place.kind === "beacon") this.beacons.push(place);
1096
+ this.emit(e, actor, { kind: "raise", target: place.id, newPlace: { path, label: place.label, kind: place.kind, district: place.district } }, 900);
1097
+ return place;
1098
+ }
1099
+ /** The beacon a test run belongs to: an explicitly touched test file, else the last-edited district's, else the first, else the forge. */
1100
+ beaconFor(touched) {
1101
+ const t = touched.find((p) => isTestPath(p));
1102
+ if (t) {
1103
+ const b = this.byPath.get(t);
1104
+ if (b) return b;
1105
+ }
1106
+ if (this.lastEdited) {
1107
+ const same = this.beacons.find((b) => b.district === this.lastEdited.district);
1108
+ if (same) return same;
1109
+ }
1110
+ return this.beacons[0] ?? this.forge;
1111
+ }
1112
+ };
1113
+ function direct(events, world, opts) {
1114
+ const d = new Director(world, opts);
1115
+ for (const e of events) d.feed(e);
1116
+ return [...d.timeline];
1117
+ }
1118
+ var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
1119
+ function filesFromEvents(events) {
1120
+ const loc = /* @__PURE__ */ new Map();
1121
+ const bump = (p, n) => loc.set(p, (loc.get(p) ?? 0) + n);
1122
+ for (const e of events) {
1123
+ if (e.kind === "read_file") bump(e.path, Math.max(5, Math.round((e.bytes ?? 400) / 40)));
1124
+ else if (e.kind === "edit_file") bump(e.path, Math.max(5, (e.added + e.removed) * 3));
1125
+ else if (e.kind === "write_file") bump(e.path, Math.max(5, e.added * 2));
1126
+ else if (e.kind === "run_command") for (const p of e.touched) bump(p, 5);
1127
+ }
1128
+ const touched = [...loc.keys()].sort((a, b) => loc.get(b) - loc.get(a));
1129
+ return { files: touched.map((path) => ({ path, loc: loc.get(path) })), touched };
1130
+ }
19
1131
 
20
1132
  // src/sessions.ts
21
1133
  import { readdirSync, readFileSync, statSync } from "fs";
@@ -191,14 +1303,13 @@ var Theatre = class {
191
1303
  return `http://127.0.0.1:${this.port}/`;
192
1304
  }
193
1305
  get name() {
194
- return basename(this.opts.cwd);
1306
+ return basename2(this.opts.cwd);
195
1307
  }
196
1308
  };
197
1309
 
198
1310
  // src/scan.ts
199
1311
  import { readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
200
1312
  import { join as join3, relative } from "path";
201
- import { isIgnoredPath } from "@agent-theatre/core";
202
1313
  function scanRepo(root, opts = {}) {
203
1314
  const maxFiles = opts.maxFiles ?? 2e4, maxBytes = opts.maxBytesForLoc ?? 512e3;
204
1315
  const files = [];
@@ -269,9 +1380,9 @@ var HOOK_TIMEOUT_S = 5;
269
1380
  function parse(existing) {
270
1381
  if (existing === null || existing.trim() === "") return {};
271
1382
  try {
272
- const obj = JSON.parse(existing);
273
- if (!obj || typeof obj !== "object" || Array.isArray(obj)) throw new Error("not an object");
274
- return obj;
1383
+ const obj4 = JSON.parse(existing);
1384
+ if (!obj4 || typeof obj4 !== "object" || Array.isArray(obj4)) throw new Error("not an object");
1385
+ return obj4;
275
1386
  } catch {
276
1387
  throw new Error("existing settings.json is not valid JSON; refusing to overwrite it");
277
1388
  }
@@ -281,8 +1392,8 @@ var isOurs = (group) => {
281
1392
  return Array.isArray(g?.hooks) && g.hooks.some((h) => typeof h?.command === "string" && h.command.includes(HOOK_MARKER));
282
1393
  };
283
1394
  function patchSettings(existing, scriptPath) {
284
- const obj = parse(existing);
285
- const hooks2 = obj.hooks ??= {};
1395
+ const obj4 = parse(existing);
1396
+ const hooks2 = obj4.hooks ??= {};
286
1397
  const cmd = `node "${scriptPath}"`;
287
1398
  let changed = false;
288
1399
  for (const { event, matcher } of HOOK_EVENTS) {
@@ -298,12 +1409,12 @@ function patchSettings(existing, scriptPath) {
298
1409
  list.push({ ...matcher ? { matcher } : {}, hooks: [{ type: "command", command: cmd, timeout: HOOK_TIMEOUT_S }] });
299
1410
  changed = true;
300
1411
  }
301
- return { next: JSON.stringify(obj, null, 2) + "\n", changed };
1412
+ return { next: JSON.stringify(obj4, null, 2) + "\n", changed };
302
1413
  }
303
1414
  function unpatchSettings(existing) {
304
- const obj = parse(existing);
1415
+ const obj4 = parse(existing);
305
1416
  let changed = false;
306
- const hooks2 = obj.hooks;
1417
+ const hooks2 = obj4.hooks;
307
1418
  if (hooks2) {
308
1419
  for (const event of Object.keys(hooks2)) {
309
1420
  const list = hooks2[event];
@@ -313,14 +1424,14 @@ function unpatchSettings(existing) {
313
1424
  if (kept.length) hooks2[event] = kept;
314
1425
  else delete hooks2[event];
315
1426
  }
316
- if (Object.keys(hooks2).length === 0) delete obj.hooks;
1427
+ if (Object.keys(hooks2).length === 0) delete obj4.hooks;
317
1428
  }
318
- return { next: JSON.stringify(obj, null, 2) + "\n", changed };
1429
+ return { next: JSON.stringify(obj4, null, 2) + "\n", changed };
319
1430
  }
320
1431
  function hasOurHooks(existing) {
321
1432
  try {
322
- const obj = parse(existing);
323
- return Object.values(obj.hooks ?? {}).some((l) => Array.isArray(l) && l.some(isOurs));
1433
+ const obj4 = parse(existing);
1434
+ return Object.values(obj4.hooks ?? {}).some((l) => Array.isArray(l) && l.some(isOurs));
324
1435
  } catch {
325
1436
  return false;
326
1437
  }
@@ -435,8 +1546,8 @@ program.command("replay [session]").description("replay a past Claude Code sessi
435
1546
  const ans = (await rl.question(`
436
1547
  Which one? [1] `)).trim() || "1";
437
1548
  rl.close();
438
- const pick = sessions[Math.max(0, Math.min(sessions.length - 1, (parseInt(ans, 10) || 1) - 1))];
439
- id = pick.id;
1549
+ const pick2 = sessions[Math.max(0, Math.min(sessions.length - 1, (parseInt(ans, 10) || 1) - 1))];
1550
+ id = pick2.id;
440
1551
  }
441
1552
  const found = sessions.find((s) => s.id === id || s.id.startsWith(id));
442
1553
  if (!found) {