@tekmidian/pai 0.71.0 → 0.71.1

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.
@@ -0,0 +1,404 @@
1
+ import { Ot as loadStatuses, Pt as pidAlive, Tt as isLive, jt as nowStamp, on as ledgerPath, wt as isChatPane, zt as spawnTimeOf } from "./config-CH2Xj7BG.mjs";
2
+ import { u as paiHomePath } from "./runtime-paths-BKtuLoJm.mjs";
3
+ import { t as appendLedger } from "./ledger-D6xicYlV.mjs";
4
+ import { a as itermUuid } from "./scope-B_kKDJhY.mjs";
5
+ import { Mt as loadPlanStatuses, Z as osascript, kt as fmtElapsed, lt as eventLine, q as loadRegistry } from "./run-DwHfR1WP.mjs";
6
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { spawn } from "node:child_process";
9
+
10
+ //#region src/workers/pane-run.ts
11
+ /**
12
+ * pane-run.ts — `pai pane run`: a session's background command runs in a visible pane.
13
+ *
14
+ * The caller (the process Claude Code backgrounds) owns the command: it spawns it in its own
15
+ * process group, tees the output to <PAI home>/logs/panes/<id>.log, and blocks until it ends, so
16
+ * the harness's completion notification keeps working. The pane runs `pai worker follow <id>`,
17
+ * which shows that log live and the exit code after, and leaves a short while later. Placement is
18
+ * a worker pane's (split, else tab, never a window; an ended pane of the scope is reused).
19
+ * Status lives in <logDir>/<id>.panestatus for `pai worker ps` and the cockpit.
20
+ */
21
+ /** The env marker of a command running under `pai pane run`: nested pane/build runs then stay in it. */
22
+ const PANE_RUN_ENV = "PAI_PANE_RUN";
23
+ /** Seconds the pane stays on screen after the command ended. */
24
+ const PANE_LINGER_SECS = 10;
25
+ const TAIL_LINES = 40;
26
+ const paneRunStatusFile = (logDir, id) => join(logDir, `${id}.panestatus`);
27
+ const paneRunLogDir = () => paiHomePath("logs", "panes");
28
+ const isPaneRunId = (id) => /^pane-[A-Za-z0-9._-]+$/.test(id);
29
+ function writePaneRunStatus(logDir, s) {
30
+ mkdirSync(logDir, { recursive: true });
31
+ const path = paneRunStatusFile(logDir, s.id);
32
+ const tmp = `${path}.${process.pid}.tmp`;
33
+ writeFileSync(tmp, JSON.stringify(s), "utf8");
34
+ renameSync(tmp, path);
35
+ }
36
+ function loadPaneRunStatus(logDir, id) {
37
+ try {
38
+ const s = JSON.parse(readFileSync(paneRunStatusFile(logDir, id), "utf8"));
39
+ return s.state === "running" && !pidAlive(s.pid) ? {
40
+ ...s,
41
+ state: "lost"
42
+ } : s;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ function loadPaneRunStatuses(logDir) {
48
+ if (!existsSync(logDir)) return [];
49
+ return readdirSync(logDir).filter((n) => n.endsWith(".panestatus")).sort().map((n) => loadPaneRunStatus(logDir, n.slice(0, -11))).filter((s) => !!s);
50
+ }
51
+ /** ps lines of the running pane commands: label, cwd, running time, log path. */
52
+ function renderPaneRunLines(statuses, now = Date.now()) {
53
+ return statuses.filter((s) => s.state === "running").map((s) => `pane ${s.label} ${s.cwd} ${fmtElapsed(now - s.startedMs)} ${s.log}`);
54
+ }
55
+ /** Cockpit rows of the running pane commands. */
56
+ function paneRunRows(statuses, now = Date.now()) {
57
+ return statuses.filter((s) => s.state === "running").map((s) => ({
58
+ id: s.id,
59
+ label: s.label,
60
+ cwd: s.cwd,
61
+ ageSec: Math.max(0, Math.round((now - s.startedMs) / 1e3)),
62
+ log: s.log
63
+ }));
64
+ }
65
+ /** Show the log of a pane run live until it has ended, then `linger` more seconds. */
66
+ async function followPaneRun(logDir, id, write, lingerSecs = PANE_LINGER_SECS, pollMs = 250) {
67
+ let at = 0;
68
+ let endedAt = 0;
69
+ for (;;) {
70
+ const st = loadPaneRunStatus(logDir, id);
71
+ try {
72
+ const text = readFileSync(st?.log ?? join(paneRunLogDir(), `${id}.log`), "utf8");
73
+ if (text.length > at) {
74
+ write(text.slice(at));
75
+ at = text.length;
76
+ }
77
+ } catch {}
78
+ if (st && st.state !== "running") {
79
+ endedAt ||= Date.now();
80
+ if (Date.now() - endedAt >= lingerSecs * 1e3) return;
81
+ }
82
+ await new Promise((r) => setTimeout(r, pollMs));
83
+ }
84
+ }
85
+ const newId = () => `pane-${nowStamp().replace(/\D/g, "")}-${Math.random().toString(36).slice(2, 6)}`;
86
+ /** Run the command in the pane; resolves to its exit code (128+n for a signal). */
87
+ async function runInPane(o) {
88
+ const env = o.env ?? process.env;
89
+ const out = o.out ?? ((s) => process.stdout.write(s));
90
+ const err = o.err ?? ((s) => process.stderr.write(s + "\n"));
91
+ const proc = o.proc ?? process;
92
+ const cwd = o.cwd ?? process.cwd();
93
+ const id = newId();
94
+ const logFile = join(paneRunLogDir(), `${id}.log`);
95
+ mkdirSync(paneRunLogDir(), { recursive: true });
96
+ const status = {
97
+ kind: "pane",
98
+ id,
99
+ label: o.label || o.command.slice(0, 2).join(" "),
100
+ cwd,
101
+ pid: process.pid,
102
+ started: nowStamp(),
103
+ startedMs: Date.now(),
104
+ state: "running",
105
+ log: logFile
106
+ };
107
+ writeFileSync(logFile, `$ ${o.command.join(" ")}\n`, "utf8");
108
+ writePaneRunStatus(o.logDir, status);
109
+ try {
110
+ err(`[pai pane] ${id}: ${await o.openPane(id)}`);
111
+ } catch (e) {
112
+ err(`[pai pane] no visible pane (${e instanceof Error ? e.message : String(e)}); running without one, log ${logFile}`);
113
+ }
114
+ const argv = o.command.length === 1 ? [
115
+ "/bin/sh",
116
+ "-c",
117
+ o.command[0]
118
+ ] : o.command;
119
+ const child = spawn(argv[0], argv.slice(1), {
120
+ cwd,
121
+ env: {
122
+ ...env,
123
+ [PANE_RUN_ENV]: "1"
124
+ },
125
+ detached: true,
126
+ stdio: [
127
+ "ignore",
128
+ "pipe",
129
+ "pipe"
130
+ ]
131
+ });
132
+ let tail = "";
133
+ const feed = (c) => {
134
+ const s = c.toString("utf8");
135
+ try {
136
+ appendFileSync(logFile, s, "utf8");
137
+ } catch {}
138
+ tail = (tail + s).split("\n").slice(-(TAIL_LINES + 1)).join("\n");
139
+ };
140
+ child.stdout?.on("data", feed);
141
+ child.stderr?.on("data", feed);
142
+ let signalled = null;
143
+ const killGroup = (sig) => {
144
+ if (!child.pid) return;
145
+ try {
146
+ process.kill(-child.pid, sig);
147
+ } catch {}
148
+ };
149
+ const onSignal = (sig) => {
150
+ if (signalled) return;
151
+ signalled = sig;
152
+ killGroup("SIGTERM");
153
+ setTimeout(() => killGroup("SIGKILL"), o.killGraceMs ?? 3e3).unref();
154
+ };
155
+ const sigs = [
156
+ "SIGTERM",
157
+ "SIGINT",
158
+ "SIGHUP"
159
+ ];
160
+ const handlers = sigs.map((s) => {
161
+ const h = () => onSignal(s);
162
+ proc.on(s, h);
163
+ return h;
164
+ });
165
+ const code = await new Promise((resolve) => {
166
+ child.on("error", (e) => {
167
+ feed(Buffer.from(`${e.message}\n`));
168
+ resolve(127);
169
+ });
170
+ child.on("close", (c, sig) => resolve(c ?? (sig ? 128 + ({
171
+ SIGHUP: 1,
172
+ SIGINT: 2,
173
+ SIGKILL: 9,
174
+ SIGTERM: 15
175
+ }[sig] ?? 0) : 1)));
176
+ });
177
+ sigs.forEach((s, i) => proc.off(s, handlers[i]));
178
+ killGroup("SIGKILL");
179
+ const took = fmtElapsed(Date.now() - status.startedMs);
180
+ const footer = `[exit ${code}${signalled ? ` (${signalled})` : ""} after ${took}]`;
181
+ try {
182
+ appendFileSync(logFile, `${tail.endsWith("\n") || !tail ? "" : "\n"}${footer}\n`, "utf8");
183
+ } catch {}
184
+ writePaneRunStatus(o.logDir, {
185
+ ...status,
186
+ state: code === 0 ? "done" : "failed",
187
+ exit: code
188
+ });
189
+ out(tail.replace(/\n+$/, "").split("\n").slice(-TAIL_LINES).join("\n") + `\n${footer}\n`);
190
+ return signalled ? 128 + {
191
+ SIGHUP: 1,
192
+ SIGINT: 2,
193
+ SIGTERM: 15
194
+ }[signalled] : code;
195
+ }
196
+
197
+ //#endregion
198
+ //#region src/workers/pane-audit.ts
199
+ /**
200
+ * pane-audit.ts — every process PAI starts (worker, plan runner, `pai pane run`) owns a LIVE pane in the
201
+ * tab of the session that started it. The audit reads the pane registry and a listing of iTerm sessions
202
+ * with their tab, and judges each running process: ok, NO PANE (none recorded, or gone while the process
203
+ * runs) WRONG TAB (the pane is not in the starting session's tab) or HOST UNKNOWN (the starting session's tab cannot be
204
+ * determined, so the placement cannot be judged). The daemon runs it every 30 s,
205
+ * writes <logDir>/pane-audit.json for `ps` and the cockpit, and reports each violation once.
206
+ */
207
+ /** A process younger than this (since start or pane registration) is not judged: its pane is still being opened. */
208
+ const START_GRACE_MS = 3e4;
209
+ const SMALL_COLS = 40;
210
+ const SMALL_ROWS = 6;
211
+ const norm = (id) => itermUuid(id) || id;
212
+ const stampMs = (t, utc) => t ? Date.parse(t.replace(" ", "T") + (utc ? "Z" : "")) : NaN;
213
+ function auditPanes(procs, registry, tabs, nowMs, sizes) {
214
+ const tabOf = (id) => id ? tabs.get(norm(id)) : void 0;
215
+ return procs.map((p) => {
216
+ const e = registry.find((x) => x.worker === p.id);
217
+ const host = e?.host ?? p.host;
218
+ const v = {
219
+ id: p.id,
220
+ kind: p.kind,
221
+ mark: "ok",
222
+ session: e?.session,
223
+ host
224
+ };
225
+ if (nowMs !== void 0) {
226
+ if ([p.startedMs, stampMs(e?.opened, true)].some((t) => t !== void 0 && Math.abs(nowMs - t) < START_GRACE_MS)) return {
227
+ ...v,
228
+ why: "within the start grace"
229
+ };
230
+ }
231
+ if (!e) return {
232
+ ...v,
233
+ mark: "NO PANE",
234
+ why: "no pane recorded for a running process"
235
+ };
236
+ const tab = tabOf(e.session);
237
+ if (!tab) return {
238
+ ...v,
239
+ mark: "NO PANE",
240
+ why: `pane ${e.session} is gone while the process runs`
241
+ };
242
+ const hostTab = tabOf(host);
243
+ if (!hostTab) return {
244
+ ...v,
245
+ mark: "HOST UNKNOWN",
246
+ tab,
247
+ why: host ? `starting session ${host} is not in the iTerm listing` : `pane ${e.session} has no recorded starting session`
248
+ };
249
+ if (hostTab !== tab) return {
250
+ ...v,
251
+ mark: "WRONG TAB",
252
+ tab,
253
+ hostTab,
254
+ why: `pane ${e.session} sits in ${tab}, the starting session in ${hostTab}`
255
+ };
256
+ const size = sizes?.get(norm(e.session));
257
+ if (size && (size.cols < SMALL_COLS || size.rows < SMALL_ROWS)) return {
258
+ ...v,
259
+ mark: "SMALL",
260
+ tab,
261
+ hostTab,
262
+ why: `pane ${e.session} is ${size.cols}x${size.rows}, under ${SMALL_COLS}x${SMALL_ROWS}`
263
+ };
264
+ return {
265
+ ...v,
266
+ tab,
267
+ hostTab
268
+ };
269
+ });
270
+ }
271
+ /** Everything running now that must own a pane. A run that was refused/allowed hidden (pane.opened false) is exempt. */
272
+ function runningProcs(logDir) {
273
+ const procs = [];
274
+ for (const s of loadStatuses(logDir)) {
275
+ if (isChatPane(s) || !isLive(s) || s.state === "queued" || s.pane?.opened === false) continue;
276
+ procs.push({
277
+ id: s.id,
278
+ kind: "worker",
279
+ host: s.term || void 0,
280
+ startedMs: spawnTimeOf(s) ?? stampMs(s.started, false)
281
+ });
282
+ }
283
+ for (const p of loadPlanStatuses(logDir)) if (p.state === "running" && p.hasPane) procs.push({
284
+ id: p.id,
285
+ kind: "plan",
286
+ startedMs: stampMs(p.started, false)
287
+ });
288
+ for (const p of loadPaneRunStatuses(logDir)) if (p.state === "running") procs.push({
289
+ id: p.id,
290
+ kind: "pane",
291
+ startedMs: p.startedMs
292
+ });
293
+ return procs;
294
+ }
295
+ const TABS_SCRIPT = `function run(argv) {
296
+ var app = Application("iTerm2");
297
+ if (!app.running()) return "";
298
+ function arr(f) { try { return f() || []; } catch (e) { return []; } }
299
+ var out = "";
300
+ arr(function () { return app.windows(); }).forEach(function (w) {
301
+ if (!w) return;
302
+ var wid = ""; try { wid = String(w.id()); } catch (e) {}
303
+ arr(function () { return w.tabs(); }).forEach(function (t, ti) {
304
+ if (!t) return;
305
+ var tid = ""; try { tid = String(t.id()); } catch (e) { tid = "i" + ti; }
306
+ arr(function () { return t.sessions(); }).forEach(function (s) {
307
+ try { out += s.id() + "\\t" + wid + ":" + tid + "\\t" + s.columns() + "\\t" + s.rows() + "\\n"; } catch (e) {}
308
+ });
309
+ });
310
+ });
311
+ return out;
312
+ }`;
313
+ /** Every iTerm session with its window:tab. Rejects when iTerm cannot be listed (no verdict is then given). */
314
+ async function listItermTabs(run = osascript, sizes) {
315
+ const p = await run(TABS_SCRIPT, []);
316
+ const m = /* @__PURE__ */ new Map();
317
+ for (const l of p.stdout.split("\n")) {
318
+ const [id, tab, cols, rows] = l.split(" ");
319
+ if (id && tab) m.set(norm(id), tab);
320
+ if (id && cols && rows) sizes?.set(norm(id), {
321
+ cols: Number(cols),
322
+ rows: Number(rows)
323
+ });
324
+ }
325
+ return m;
326
+ }
327
+ const auditFile = (logDir) => join(logDir, "pane-audit.json");
328
+ const pendingFile = (logDir) => join(logDir, "pane-audit-pending.json");
329
+ const toldFile = (logDir) => join(logDir, "pane-audit-told.json");
330
+ function readAudit(logDir) {
331
+ try {
332
+ return JSON.parse(readFileSync(auditFile(logDir), "utf8")).verdicts;
333
+ } catch {
334
+ return [];
335
+ }
336
+ }
337
+ function readJson(file) {
338
+ try {
339
+ return JSON.parse(readFileSync(file, "utf8"));
340
+ } catch {
341
+ return {};
342
+ }
343
+ }
344
+ const readTold = (logDir) => readJson(toldFile(logDir));
345
+ /**
346
+ * One audit round: judge, persist, and report each new violation once (ledger PANE-VIOLATION, the
347
+ * starting session, PAILot). A violation that clears may be reported again if it returns.
348
+ */
349
+ async function auditTick(logDir, msg, deps = {}) {
350
+ let tabs;
351
+ const sizes = /* @__PURE__ */ new Map();
352
+ try {
353
+ tabs = await (deps.tabs ?? ((z) => listItermTabs(void 0, z)))(sizes);
354
+ } catch {
355
+ return [];
356
+ }
357
+ if (tabs.size === 0) return [];
358
+ const now = deps.now?.() ?? /* @__PURE__ */ new Date();
359
+ const verdicts = auditPanes((deps.procs ?? (() => runningProcs(logDir)))(), (deps.registry ?? (() => loadRegistry(logDir)))(), tabs, now.getTime(), sizes);
360
+ const seen = readJson(pendingFile(logDir));
361
+ const failing = {};
362
+ for (const v of verdicts) {
363
+ if (v.mark === "ok" || v.mark === "SMALL") continue;
364
+ failing[v.id] = v.mark;
365
+ if (seen[v.id] !== v.mark) Object.assign(v, {
366
+ mark: "ok",
367
+ tab: void 0,
368
+ hostTab: void 0,
369
+ why: "first failed check, confirming"
370
+ });
371
+ }
372
+ writeFileSync(pendingFile(logDir), JSON.stringify(failing), "utf8");
373
+ writeFileSync(auditFile(logDir), JSON.stringify({
374
+ at: now.toISOString(),
375
+ verdicts
376
+ }), "utf8");
377
+ const told = readTold(logDir);
378
+ const next = {};
379
+ for (const v of verdicts) {
380
+ if (v.mark === "ok" || v.mark === "SMALL") continue;
381
+ next[v.id] = v.mark;
382
+ if (told[v.id] === v.mark) continue;
383
+ appendLedger(ledgerPath(logDir), "PANE-VIOLATION", {
384
+ id: v.id,
385
+ kind: v.kind,
386
+ mark: v.mark,
387
+ why: v.why
388
+ }, now);
389
+ const what = `${v.mark}: ${v.kind} ${v.id} runs without its pane in this session's tab (${v.why}).`;
390
+ if (v.host) await msg.toSession({ iterm: v.host }, eventLine(v.id, "pane-violation", { mark: v.mark }, what)).catch(() => false);
391
+ await msg.toPailot(what).catch(() => false);
392
+ }
393
+ writeFileSync(toldFile(logDir), JSON.stringify(next), "utf8");
394
+ return verdicts;
395
+ }
396
+ /** `pai worker ps --panes`: process -> pane -> tab for everything running. */
397
+ function renderPaneAudit(verdicts) {
398
+ if (!verdicts.length) return "no running process owns a pane to list";
399
+ return verdicts.map((v) => `${v.mark === "ok" ? "ok " : v.mark.padEnd(9)} ${v.kind.padEnd(6)} ${v.id} pane ${v.session ?? "-"} tab ${v.tab ?? "-"} host tab ${v.hostTab ?? "-"}`).join("\n");
400
+ }
401
+
402
+ //#endregion
403
+ export { renderPaneAudit as a, followPaneRun as c, loadPaneRunStatuses as d, paneRunRows as f, readAudit as i, isPaneRunId as l, runInPane as m, auditTick as n, runningProcs as o, renderPaneRunLines as p, listItermTabs as r, PANE_RUN_ENV as s, auditPanes as t, loadPaneRunStatus as u };
404
+ //# sourceMappingURL=pane-audit-C1QnR1vx.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pane-audit-C1QnR1vx.mjs","names":["osascriptJxa"],"sources":["../src/workers/pane-run.ts","../src/workers/pane-audit.ts"],"sourcesContent":["/**\n * pane-run.ts — `pai pane run`: a session's background command runs in a visible pane.\n *\n * The caller (the process Claude Code backgrounds) owns the command: it spawns it in its own\n * process group, tees the output to <PAI home>/logs/panes/<id>.log, and blocks until it ends, so\n * the harness's completion notification keeps working. The pane runs `pai worker follow <id>`,\n * which shows that log live and the exit code after, and leaves a short while later. Placement is\n * a worker pane's (split, else tab, never a window; an ended pane of the scope is reused).\n * Status lives in <logDir>/<id>.panestatus for `pai worker ps` and the cockpit.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { paiHomePath } from \"../config/pai-home.js\";\nimport { nowStamp, pidAlive } from \"./status.js\";\nimport { fmtElapsed } from \"./plan-status.js\";\n\nexport interface PaneRunStatus {\n kind: \"pane\";\n id: string;\n label: string;\n cwd: string;\n pid: number;\n started: string;\n startedMs: number;\n state: \"running\" | \"done\" | \"failed\" | \"lost\";\n exit?: number;\n log: string;\n}\n\n/** The env marker of a command running under `pai pane run`: nested pane/build runs then stay in it. */\nexport const PANE_RUN_ENV = \"PAI_PANE_RUN\";\n/** Seconds the pane stays on screen after the command ended. */\nexport const PANE_LINGER_SECS = 10;\nconst TAIL_LINES = 40;\n\nexport const paneRunStatusFile = (logDir: string, id: string): string => join(logDir, `${id}.panestatus`);\nexport const paneRunLogDir = (): string => paiHomePath(\"logs\", \"panes\");\nexport const isPaneRunId = (id: string): boolean => /^pane-[A-Za-z0-9._-]+$/.test(id);\n\nexport function writePaneRunStatus(logDir: string, s: PaneRunStatus): void {\n mkdirSync(logDir, { recursive: true });\n const path = paneRunStatusFile(logDir, s.id);\n const tmp = `${path}.${process.pid}.tmp`;\n writeFileSync(tmp, JSON.stringify(s), \"utf8\");\n renameSync(tmp, path);\n}\n\nexport function loadPaneRunStatus(logDir: string, id: string): PaneRunStatus | null {\n try {\n const s = JSON.parse(readFileSync(paneRunStatusFile(logDir, id), \"utf8\")) as PaneRunStatus;\n return s.state === \"running\" && !pidAlive(s.pid) ? { ...s, state: \"lost\" } : s;\n } catch {\n return null;\n }\n}\n\nexport function loadPaneRunStatuses(logDir: string): PaneRunStatus[] {\n if (!existsSync(logDir)) return [];\n return readdirSync(logDir)\n .filter((n) => n.endsWith(\".panestatus\"))\n .sort()\n .map((n) => loadPaneRunStatus(logDir, n.slice(0, -\".panestatus\".length)))\n .filter((s): s is PaneRunStatus => !!s);\n}\n\n/** ps lines of the running pane commands: label, cwd, running time, log path. */\nexport function renderPaneRunLines(statuses: PaneRunStatus[], now = Date.now()): string[] {\n return statuses\n .filter((s) => s.state === \"running\")\n .map((s) => `pane ${s.label} ${s.cwd} ${fmtElapsed(now - s.startedMs)} ${s.log}`);\n}\n\n/** Cockpit rows of the running pane commands. */\nexport function paneRunRows(statuses: PaneRunStatus[], now = Date.now()): Array<{ id: string; label: string; cwd: string; ageSec: number; log: string }> {\n return statuses\n .filter((s) => s.state === \"running\")\n .map((s) => ({ id: s.id, label: s.label, cwd: s.cwd, ageSec: Math.max(0, Math.round((now - s.startedMs) / 1000)), log: s.log }));\n}\n\n/** Show the log of a pane run live until it has ended, then `linger` more seconds. */\nexport async function followPaneRun(\n logDir: string,\n id: string,\n write: (s: string) => void,\n lingerSecs = PANE_LINGER_SECS,\n pollMs = 250\n): Promise<void> {\n let at = 0;\n let endedAt = 0;\n for (;;) {\n const st = loadPaneRunStatus(logDir, id);\n try {\n const text = readFileSync(st?.log ?? join(paneRunLogDir(), `${id}.log`), \"utf8\");\n if (text.length > at) {\n write(text.slice(at));\n at = text.length;\n }\n } catch {\n /* not written yet */\n }\n if (st && st.state !== \"running\") {\n endedAt ||= Date.now();\n if (Date.now() - endedAt >= lingerSecs * 1000) return;\n }\n await new Promise((r) => setTimeout(r, pollMs));\n }\n}\n\nexport interface PaneRunOptions {\n command: string[];\n label?: string;\n cwd?: string;\n logDir: string;\n /** Opens the pane following `id`; a rejection is reported and the command runs anyway. */\n openPane: (id: string) => Promise<string>;\n env?: NodeJS.ProcessEnv;\n out?: (s: string) => void;\n err?: (s: string) => void;\n /** Process the signal handlers attach to (tests pass their own emitter). */\n proc?: NodeJS.Process;\n killGraceMs?: number;\n}\n\nconst newId = (): string => `pane-${nowStamp().replace(/\\D/g, \"\")}-${Math.random().toString(36).slice(2, 6)}`;\n\n/** Run the command in the pane; resolves to its exit code (128+n for a signal). */\nexport async function runInPane(o: PaneRunOptions): Promise<number> {\n const env = o.env ?? process.env;\n const out = o.out ?? ((s: string) => process.stdout.write(s));\n const err = o.err ?? ((s: string) => process.stderr.write(s + \"\\n\"));\n const proc = o.proc ?? process;\n const cwd = o.cwd ?? process.cwd();\n const id = newId();\n const logFile = join(paneRunLogDir(), `${id}.log`);\n mkdirSync(paneRunLogDir(), { recursive: true });\n const status: PaneRunStatus = {\n kind: \"pane\",\n id,\n label: o.label || o.command.slice(0, 2).join(\" \"),\n cwd,\n pid: process.pid,\n started: nowStamp(),\n startedMs: Date.now(),\n state: \"running\",\n log: logFile,\n };\n writeFileSync(logFile, `$ ${o.command.join(\" \")}\\n`, \"utf8\");\n writePaneRunStatus(o.logDir, status);\n try {\n err(`[pai pane] ${id}: ${await o.openPane(id)}`);\n } catch (e) {\n err(`[pai pane] no visible pane (${e instanceof Error ? e.message : String(e)}); running without one, log ${logFile}`);\n }\n\n const argv = o.command.length === 1 ? [\"/bin/sh\", \"-c\", o.command[0]] : o.command;\n const child = spawn(argv[0], argv.slice(1), { cwd, env: { ...env, [PANE_RUN_ENV]: \"1\" }, detached: true, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n let tail = \"\";\n const feed = (c: Buffer): void => {\n const s = c.toString(\"utf8\");\n try {\n appendFileSync(logFile, s, \"utf8\");\n } catch {\n /* the log is a view */\n }\n tail = (tail + s).split(\"\\n\").slice(-(TAIL_LINES + 1)).join(\"\\n\");\n };\n child.stdout?.on(\"data\", feed);\n child.stderr?.on(\"data\", feed);\n\n let signalled: NodeJS.Signals | null = null;\n const killGroup = (sig: NodeJS.Signals): void => {\n if (!child.pid) return;\n try {\n process.kill(-child.pid, sig);\n } catch {\n /* gone */\n }\n };\n const onSignal = (sig: NodeJS.Signals): void => {\n if (signalled) return;\n signalled = sig;\n killGroup(\"SIGTERM\");\n setTimeout(() => killGroup(\"SIGKILL\"), o.killGraceMs ?? 3000).unref();\n };\n const sigs: NodeJS.Signals[] = [\"SIGTERM\", \"SIGINT\", \"SIGHUP\"];\n const handlers = sigs.map((s) => {\n const h = (): void => onSignal(s);\n proc.on(s, h);\n return h;\n });\n\n const code = await new Promise<number>((resolve) => {\n child.on(\"error\", (e) => {\n feed(Buffer.from(`${e.message}\\n`));\n resolve(127);\n });\n child.on(\"close\", (c, sig) => resolve(c ?? (sig ? 128 + (({ SIGHUP: 1, SIGINT: 2, SIGKILL: 9, SIGTERM: 15 } as Record<string, number>)[sig] ?? 0) : 1)));\n });\n sigs.forEach((s, i) => proc.off(s, handlers[i]));\n killGroup(\"SIGKILL\"); // stragglers of the group\n\n const took = fmtElapsed(Date.now() - status.startedMs);\n const footer = `[exit ${code}${signalled ? ` (${signalled})` : \"\"} after ${took}]`;\n try {\n appendFileSync(logFile, `${tail.endsWith(\"\\n\") || !tail ? \"\" : \"\\n\"}${footer}\\n`, \"utf8\");\n } catch {\n /* as above */\n }\n writePaneRunStatus(o.logDir, { ...status, state: code === 0 ? \"done\" : \"failed\", exit: code });\n out(tail.replace(/\\n+$/, \"\").split(\"\\n\").slice(-TAIL_LINES).join(\"\\n\") + `\\n${footer}\\n`);\n return signalled ? 128 + ({ SIGHUP: 1, SIGINT: 2, SIGTERM: 15 } as Record<string, number>)[signalled] : code;\n}\n","/**\n * pane-audit.ts — every process PAI starts (worker, plan runner, `pai pane run`) owns a LIVE pane in the\n * tab of the session that started it. The audit reads the pane registry and a listing of iTerm sessions\n * with their tab, and judges each running process: ok, NO PANE (none recorded, or gone while the process\n * runs) WRONG TAB (the pane is not in the starting session's tab) or HOST UNKNOWN (the starting session's tab cannot be\n * determined, so the placement cannot be judged). The daemon runs it every 30 s,\n * writes <logDir>/pane-audit.json for `ps` and the cockpit, and reports each violation once.\n */\n\nimport { readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { loadRegistry, osascriptJxa, type PaneEntry } from \"./pane.js\";\nimport { itermUuid } from \"./scope.js\";\nimport { appendLedger } from \"./ledger.js\";\nimport { ledgerPath } from \"./paths.js\";\nimport { loadStatuses, isLive, isChatPane, spawnTimeOf } from \"./status.js\";\nimport { loadPlanStatuses } from \"./plan-status.js\";\nimport { loadPaneRunStatuses } from \"./pane-run.js\";\nimport { eventLine, type Messenger } from \"./run-events.js\";\n\nexport type PaneMark = \"ok\" | \"NO PANE\" | \"WRONG TAB\" | \"HOST UNKNOWN\" | \"SMALL\";\n\nexport interface AuditProc {\n id: string;\n kind: \"worker\" | \"plan\" | \"pane\";\n /** The starting session's iTerm id (status.term), when known. */\n host?: string;\n /** When the process started (ms); a young process gets the start grace. */\n startedMs?: number;\n}\n\n/** A process younger than this (since start or pane registration) is not judged: its pane is still being opened. */\nexport const START_GRACE_MS = 30_000;\n\nexport interface PaneVerdict {\n id: string;\n kind: AuditProc[\"kind\"];\n mark: PaneMark;\n session?: string;\n host?: string;\n tab?: string;\n hostTab?: string;\n why?: string;\n}\n\n/** iTerm session id -> \"window:tab\" key. */\nexport type TabMap = Map<string, string>;\n/** iTerm session id -> its size in cells. */\nexport type SizeMap = Map<string, { cols: number; rows: number }>;\nconst SMALL_COLS = 40;\nconst SMALL_ROWS = 6;\n\nconst norm = (id: string): string => itermUuid(id) || id;\n\nconst stampMs = (t: string | undefined, utc: boolean): number => (t ? Date.parse(t.replace(\" \", \"T\") + (utc ? \"Z\" : \"\")) : NaN);\n\nexport function auditPanes(procs: AuditProc[], registry: PaneEntry[], tabs: TabMap, nowMs?: number, sizes?: SizeMap): PaneVerdict[] {\n const tabOf = (id: string | undefined): string | undefined => (id ? tabs.get(norm(id)) : undefined);\n return procs.map((p) => {\n const e = registry.find((x) => x.worker === p.id);\n const host = e?.host ?? p.host;\n const v: PaneVerdict = { id: p.id, kind: p.kind, mark: \"ok\", session: e?.session, host };\n if (nowMs !== undefined) {\n // registry stamps are UTC (toISOString); a recent start or a recent registration opens the grace\n const young = [p.startedMs, stampMs(e?.opened, true)].some((t) => t !== undefined && Math.abs(nowMs - t) < START_GRACE_MS);\n if (young) return { ...v, why: \"within the start grace\" };\n }\n if (!e) return { ...v, mark: \"NO PANE\", why: \"no pane recorded for a running process\" };\n const tab = tabOf(e.session);\n if (!tab) return { ...v, mark: \"NO PANE\", why: `pane ${e.session} is gone while the process runs` };\n const hostTab = tabOf(host);\n if (!hostTab) return { ...v, mark: \"HOST UNKNOWN\", tab, why: host ? `starting session ${host} is not in the iTerm listing` : `pane ${e.session} has no recorded starting session` };\n if (hostTab !== tab) return { ...v, mark: \"WRONG TAB\", tab, hostTab, why: `pane ${e.session} sits in ${tab}, the starting session in ${hostTab}` };\n const size = sizes?.get(norm(e.session));\n if (size && (size.cols < SMALL_COLS || size.rows < SMALL_ROWS)) {\n return { ...v, mark: \"SMALL\", tab, hostTab, why: `pane ${e.session} is ${size.cols}x${size.rows}, under ${SMALL_COLS}x${SMALL_ROWS}` };\n }\n return { ...v, tab, hostTab };\n });\n}\n\n/** Everything running now that must own a pane. A run that was refused/allowed hidden (pane.opened false) is exempt. */\nexport function runningProcs(logDir: string): AuditProc[] {\n const procs: AuditProc[] = [];\n for (const s of loadStatuses(logDir)) {\n if (isChatPane(s) || !isLive(s) || s.state === \"queued\" || s.pane?.opened === false) continue;\n procs.push({ id: s.id, kind: \"worker\", host: s.term || undefined, startedMs: spawnTimeOf(s) ?? stampMs(s.started, false) });\n }\n for (const p of loadPlanStatuses(logDir)) if (p.state === \"running\" && p.hasPane) procs.push({ id: p.id, kind: \"plan\", startedMs: stampMs(p.started, false) });\n for (const p of loadPaneRunStatuses(logDir)) if (p.state === \"running\") procs.push({ id: p.id, kind: \"pane\", startedMs: p.startedMs });\n return procs;\n}\n\nconst TABS_SCRIPT = `function run(argv) {\n var app = Application(\"iTerm2\");\n if (!app.running()) return \"\";\n function arr(f) { try { return f() || []; } catch (e) { return []; } }\n var out = \"\";\n arr(function () { return app.windows(); }).forEach(function (w) {\n if (!w) return;\n var wid = \"\"; try { wid = String(w.id()); } catch (e) {}\n arr(function () { return w.tabs(); }).forEach(function (t, ti) {\n if (!t) return;\n var tid = \"\"; try { tid = String(t.id()); } catch (e) { tid = \"i\" + ti; }\n arr(function () { return t.sessions(); }).forEach(function (s) {\n try { out += s.id() + \"\\\\t\" + wid + \":\" + tid + \"\\\\t\" + s.columns() + \"\\\\t\" + s.rows() + \"\\\\n\"; } catch (e) {}\n });\n });\n });\n return out;\n}`;\n\n/** Every iTerm session with its window:tab. Rejects when iTerm cannot be listed (no verdict is then given). */\nexport async function listItermTabs(run: (script: string, args: string[]) => Promise<{ stdout: string }> = osascriptJxa, sizes?: SizeMap): Promise<TabMap> {\n const p = await run(TABS_SCRIPT, []);\n const m: TabMap = new Map();\n for (const l of p.stdout.split(\"\\n\")) {\n const [id, tab, cols, rows] = l.split(\"\\t\");\n if (id && tab) m.set(norm(id), tab);\n if (id && cols && rows) sizes?.set(norm(id), { cols: Number(cols), rows: Number(rows) });\n }\n return m;\n}\n\nexport interface AuditFile {\n at: string;\n verdicts: PaneVerdict[];\n}\n\nconst auditFile = (logDir: string): string => join(logDir, \"pane-audit.json\");\nconst pendingFile = (logDir: string): string => join(logDir, \"pane-audit-pending.json\");\nconst toldFile = (logDir: string): string => join(logDir, \"pane-audit-told.json\");\n\nexport function readAudit(logDir: string): PaneVerdict[] {\n try {\n return (JSON.parse(readFileSync(auditFile(logDir), \"utf8\")) as AuditFile).verdicts;\n } catch {\n return [];\n }\n}\n\n/** The mark of one process from the last daemon audit, \"\" when ok or unaudited. */\nexport function markOf(verdicts: PaneVerdict[], id: string): string {\n const v = verdicts.find((x) => x.id === id);\n return v && v.mark !== \"ok\" ? v.mark : \"\";\n}\n\nfunction readJson(file: string): Record<string, string> {\n try {\n return JSON.parse(readFileSync(file, \"utf8\")) as Record<string, string>;\n } catch {\n return {};\n }\n}\n\nconst readTold = (logDir: string): Record<string, string> => readJson(toldFile(logDir));\n\n/**\n * One audit round: judge, persist, and report each new violation once (ledger PANE-VIOLATION, the\n * starting session, PAILot). A violation that clears may be reported again if it returns.\n */\nexport async function auditTick(\n logDir: string,\n msg: Messenger,\n deps: { tabs?: (sizes: SizeMap) => Promise<TabMap>; procs?: () => AuditProc[]; registry?: () => PaneEntry[]; now?: () => Date } = {}\n): Promise<PaneVerdict[]> {\n let tabs: TabMap;\n const sizes: SizeMap = new Map();\n try {\n tabs = await (deps.tabs ?? ((z) => listItermTabs(undefined, z)))(sizes);\n } catch {\n return []; // iTerm unreadable: no verdict, no false alarm\n }\n if (tabs.size === 0) return []; // no iTerm (tmux, other platform): nothing to compare against\n const now = deps.now?.() ?? new Date();\n const verdicts = auditPanes((deps.procs ?? (() => runningProcs(logDir)))(), (deps.registry ?? (() => loadRegistry(logDir)))(), tabs, now.getTime(), sizes);\n // a violation counts only when the previous check failed the same way: one bad listing is not a verdict\n const seen = readJson(pendingFile(logDir));\n const failing: Record<string, string> = {};\n for (const v of verdicts) {\n if (v.mark === \"ok\" || v.mark === \"SMALL\") continue;\n failing[v.id] = v.mark;\n if (seen[v.id] !== v.mark) Object.assign(v, { mark: \"ok\", tab: undefined, hostTab: undefined, why: \"first failed check, confirming\" });\n }\n writeFileSync(pendingFile(logDir), JSON.stringify(failing), \"utf8\");\n writeFileSync(auditFile(logDir), JSON.stringify({ at: now.toISOString(), verdicts } satisfies AuditFile), \"utf8\");\n const told = readTold(logDir);\n const next: Record<string, string> = {};\n for (const v of verdicts) {\n if (v.mark === \"ok\" || v.mark === \"SMALL\") continue; // SMALL is shown, never alarmed\n next[v.id] = v.mark;\n if (told[v.id] === v.mark) continue;\n appendLedger(ledgerPath(logDir), \"PANE-VIOLATION\", { id: v.id, kind: v.kind, mark: v.mark, why: v.why }, now);\n const what = `${v.mark}: ${v.kind} ${v.id} runs without its pane in this session's tab (${v.why}).`;\n if (v.host) await msg.toSession({ iterm: v.host }, eventLine(v.id, \"pane-violation\", { mark: v.mark }, what)).catch(() => false);\n await msg.toPailot(what).catch(() => false);\n }\n writeFileSync(toldFile(logDir), JSON.stringify(next), \"utf8\");\n return verdicts;\n}\n\n/** `pai worker ps --panes`: process -> pane -> tab for everything running. */\nexport function renderPaneAudit(verdicts: PaneVerdict[]): string {\n if (!verdicts.length) return \"no running process owns a pane to list\";\n return verdicts.map((v) => `${v.mark === \"ok\" ? \"ok \" : v.mark.padEnd(9)} ${v.kind.padEnd(6)} ${v.id} pane ${v.session ?? \"-\"} tab ${v.tab ?? \"-\"} host tab ${v.hostTab ?? \"-\"}`).join(\"\\n\");\n}\n"],"mappings":"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"}