@delorenj/pjangler 1.5.39 → 1.5.41

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/index.js CHANGED
@@ -6064,8 +6064,8 @@ var init_capture = __esm({
6064
6064
 
6065
6065
  // src/index.ts
6066
6066
  import { spawnSync as spawnSync18 } from "node:child_process";
6067
- import { existsSync as existsSync27, readdirSync as readdirSync13, readFileSync as readFileSync30, statSync as statSync9 } from "node:fs";
6068
- import { basename as basename11, join as join35, resolve as resolve25 } from "node:path";
6067
+ import { existsSync as existsSync26, readdirSync as readdirSync14, readFileSync as readFileSync32, statSync as statSync9 } from "node:fs";
6068
+ import { basename as basename12, join as join37, resolve as resolve25 } from "node:path";
6069
6069
  import { Command as Command3, CommanderError, Option } from "commander";
6070
6070
 
6071
6071
  // src/recipes/Recipe.ts
@@ -20943,8 +20943,204 @@ async function loadScreen(relativeToDist, base) {
20943
20943
  init_boardQuery();
20944
20944
  init_style();
20945
20945
  import { spawnSync as spawnSync17 } from "node:child_process";
20946
- import { readFileSync as readFileSync29 } from "node:fs";
20946
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync30, realpathSync as realpathSync12, renameSync as renameSync10, writeFileSync as writeFileSync15 } from "node:fs";
20947
+ import { homedir as homedir15 } from "node:os";
20948
+ import { dirname as dirname22, join as join35 } from "node:path";
20949
+
20950
+ // src/crank/engine.ts
20951
+ import { closeSync as closeSync9, constants as constants7, fstatSync as fstatSync5, openSync as openSync9, readFileSync as readFileSync29, readSync as readSync5 } from "node:fs";
20952
+ import { homedir as homedir14 } from "node:os";
20947
20953
  import { join as join34 } from "node:path";
20954
+ var ENGINE_DEFAULT_JOB_ID = "777c511a009c";
20955
+ var WINDOW_MS = 24 * 60 * 60 * 1e3;
20956
+ var TICK_MS = 5 * 60 * 1e3;
20957
+ var STALL_MS = 3 * TICK_MS;
20958
+ var ENGINE_AUDIT_TAIL_BYTES = 1024 * 1024;
20959
+ function unknownEngineStatus(nowMs, jobId = ENGINE_DEFAULT_JOB_ID) {
20960
+ return { known: false, jobId, readAtMs: nowMs, outcome: "unknown", beats24h: 0, tokens24h: 0, tokenBlocked: false };
20961
+ }
20962
+ var FUTURE_SKEW_MS = 5 * 6e4;
20963
+ function parseAuditRows(text2, jobId) {
20964
+ const rows = [];
20965
+ for (const line of text2.split("\n")) {
20966
+ const trimmed2 = line.trim();
20967
+ if (!trimmed2) continue;
20968
+ let parsed;
20969
+ try {
20970
+ parsed = JSON.parse(trimmed2);
20971
+ } catch {
20972
+ continue;
20973
+ }
20974
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) continue;
20975
+ const record3 = parsed;
20976
+ if (String(record3.job_id ?? "") !== jobId) continue;
20977
+ const ts = typeof record3.ts === "string" ? Date.parse(record3.ts) : Number.NaN;
20978
+ if (!Number.isFinite(ts)) continue;
20979
+ const tokens = Number(record3.total_tokens ?? 0);
20980
+ const reasonRaw = record3.economics_reason ?? record3.error;
20981
+ rows.push({
20982
+ tsMs: ts,
20983
+ status: typeof record3.status === "string" ? record3.status : "",
20984
+ tokens: Number.isFinite(tokens) && tokens > 0 ? tokens : 0,
20985
+ reason: typeof reasonRaw === "string" && reasonRaw.trim() ? reasonRaw.trim() : void 0
20986
+ });
20987
+ }
20988
+ return rows.sort((a, b) => a.tsMs - b.tsMs);
20989
+ }
20990
+ function capFromReason(reason) {
20991
+ const match = /daily token limit reached \(([\d,]+)\)/i.exec(reason ?? "");
20992
+ if (!match) return void 0;
20993
+ const n = Number.parseInt((match[1] ?? "").replace(/,/g, ""), 10);
20994
+ return Number.isFinite(n) && n > 0 ? n : void 0;
20995
+ }
20996
+ function parseEngineStatus(auditText, input) {
20997
+ const jobId = input.jobId ?? ENGINE_DEFAULT_JOB_ID;
20998
+ try {
20999
+ const rows = parseAuditRows(auditText, jobId).filter((r) => r.tsMs <= input.nowMs + FUTURE_SKEW_MS);
21000
+ const last = rows.at(-1);
21001
+ if (!last) return unknownEngineStatus(input.nowMs, jobId);
21002
+ const windowStart = input.nowMs - WINDOW_MS;
21003
+ const counted = rows.filter((r) => (r.status === "completed" || r.status === "started") && r.tsMs >= windowStart);
21004
+ const tokens24h = counted.reduce((sum, r) => sum + r.tokens, 0);
21005
+ const ran = last.status === "completed" || last.status === "started";
21006
+ const refused = !ran && last.status !== "failed" && (last.status === "skipped" || last.status === "blocked" || !!last.reason);
21007
+ const outcome = ran ? "cranked" : refused ? "refused" : "other";
21008
+ const tokenBlocked = outcome === "refused" && /daily token limit/i.test(last.reason ?? "");
21009
+ let refusedSinceMs;
21010
+ if (outcome === "refused") {
21011
+ refusedSinceMs = last.tsMs;
21012
+ for (let i = rows.length - 2; i >= 0; i--) {
21013
+ const r = rows[i];
21014
+ if (!r) break;
21015
+ if (r.status === "completed" || r.status === "started" || r.status === "failed") break;
21016
+ refusedSinceMs = r.tsMs;
21017
+ }
21018
+ }
21019
+ const cap = input.cap && input.cap > 0 ? input.cap : capFromReason(last.reason);
21020
+ let resumeMs;
21021
+ if (tokenBlocked && cap) {
21022
+ let remaining = tokens24h;
21023
+ resumeMs = remaining < cap ? input.nowMs : void 0;
21024
+ for (const row of counted) {
21025
+ if (remaining < cap) break;
21026
+ remaining -= row.tokens;
21027
+ if (remaining < cap) {
21028
+ resumeMs = row.tsMs + WINDOW_MS;
21029
+ break;
21030
+ }
21031
+ }
21032
+ }
21033
+ return {
21034
+ known: true,
21035
+ jobId,
21036
+ readAtMs: input.nowMs,
21037
+ outcome,
21038
+ lastStatus: last.status || void 0,
21039
+ lastTickMs: last.tsMs,
21040
+ reason: outcome === "cranked" ? void 0 : last.reason,
21041
+ refusedSinceMs,
21042
+ beats24h: counted.length,
21043
+ tokens24h,
21044
+ cap,
21045
+ tokenBlocked,
21046
+ resumeMs
21047
+ };
21048
+ } catch {
21049
+ return unknownEngineStatus(input.nowMs, jobId);
21050
+ }
21051
+ }
21052
+ function readTail(path, maxBytes) {
21053
+ const fd = openSync9(path, constants7.O_RDONLY | constants7.O_NONBLOCK);
21054
+ try {
21055
+ const stat = fstatSync5(fd);
21056
+ if (!stat.isFile()) throw new Error(`${path} is not a regular file`);
21057
+ const size = stat.size;
21058
+ const length = Math.min(size, maxBytes);
21059
+ const buffer = Buffer.alloc(length);
21060
+ let read = 0;
21061
+ while (read < length) {
21062
+ const n = readSync5(fd, buffer, read, length - read, size - length + read);
21063
+ if (n <= 0) break;
21064
+ read += n;
21065
+ }
21066
+ const text2 = buffer.subarray(0, read).toString("utf8");
21067
+ if (size <= maxBytes) return text2;
21068
+ const newline = text2.indexOf("\n");
21069
+ return newline === -1 ? "" : text2.slice(newline + 1);
21070
+ } finally {
21071
+ closeSync9(fd);
21072
+ }
21073
+ }
21074
+ function engineSources(env2 = process.env) {
21075
+ const cronDir = join34(homedir14(), ".hermes", "profiles", "33god-pm", "cron");
21076
+ return {
21077
+ auditPath: env2.PJ_CRANK_USAGE_AUDIT?.trim() || join34(cronDir, "usage_audit.jsonl"),
21078
+ jobsPath: join34(cronDir, "jobs.json"),
21079
+ jobId: env2.PJ_CRANK_JOB_ID?.trim() || ENGINE_DEFAULT_JOB_ID
21080
+ };
21081
+ }
21082
+ function readCap(jobsPath, jobId) {
21083
+ try {
21084
+ const parsed = JSON.parse(readFileSync29(jobsPath, "utf8"));
21085
+ const jobs = Array.isArray(parsed.jobs) ? parsed.jobs : [];
21086
+ const job = jobs.find((j) => typeof j === "object" && j !== null && String(j.id ?? "") === jobId);
21087
+ const economics = job?.economics;
21088
+ if (typeof economics !== "object" || economics === null) return void 0;
21089
+ const cap = Number(economics.max_daily_tokens);
21090
+ return Number.isFinite(cap) && cap > 0 ? cap : void 0;
21091
+ } catch {
21092
+ return void 0;
21093
+ }
21094
+ }
21095
+ function readEngineStatus(options = {}) {
21096
+ const nowMs = options.nowMs ?? Date.now();
21097
+ const base = engineSources(options.env);
21098
+ const sources = { ...base, ...options.sources };
21099
+ try {
21100
+ const text2 = readTail(sources.auditPath, options.tailBytes ?? ENGINE_AUDIT_TAIL_BYTES);
21101
+ return parseEngineStatus(text2, { jobId: sources.jobId, cap: readCap(sources.jobsPath, sources.jobId), nowMs });
21102
+ } catch {
21103
+ return unknownEngineStatus(nowMs, sources.jobId);
21104
+ }
21105
+ }
21106
+ function clockTime(ms) {
21107
+ const d = new Date(ms);
21108
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
21109
+ }
21110
+ function ago(thenMs, nowMs) {
21111
+ const s = Math.max(0, Math.round((nowMs - thenMs) / 1e3));
21112
+ if (s < 60) return `${s}s ago`;
21113
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
21114
+ if (s < 86400) return `${Math.floor(s / 3600)}h ${String(Math.floor(s % 3600 / 60)).padStart(2, "0")}m ago`;
21115
+ return `${Math.floor(s / 86400)}d ${Math.floor(s % 86400 / 3600)}h ago`;
21116
+ }
21117
+ function compactTokens(n) {
21118
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
21119
+ if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
21120
+ return String(n);
21121
+ }
21122
+ function nextTickAtOrAfter(ms) {
21123
+ return Math.ceil(ms / TICK_MS) * TICK_MS;
21124
+ }
21125
+ function engineStalled(status) {
21126
+ return status.known && status.lastTickMs !== void 0 && status.readAtMs - status.lastTickMs > STALL_MS;
21127
+ }
21128
+ function engineSummary(status) {
21129
+ if (!status.known || status.lastTickMs === void 0) return { text: "engine status unknown", tone: "unknown" };
21130
+ const now = status.readAtMs;
21131
+ const usage = `${status.beats24h} beat${status.beats24h === 1 ? "" : "s"} / ${compactTokens(status.tokens24h)} tokens in 24h${status.cap ? ` of ${compactTokens(status.cap)}` : ""}`;
21132
+ const tick = `last tick ${ago(status.lastTickMs, now)}`;
21133
+ const stalled = engineStalled(status) ? " - not ticking" : "";
21134
+ if (status.outcome === "cranked") return { text: `${tick} (cranked)${stalled} \xB7 ${usage}`, tone: stalled ? "warn" : "ok" };
21135
+ if (status.outcome === "refused") {
21136
+ const since = status.refusedSinceMs !== void 0 && status.refusedSinceMs < status.lastTickMs ? ` since ${ago(status.refusedSinceMs, now)}` : "";
21137
+ const resume = status.tokenBlocked && status.resumeMs !== void 0 ? ` \xB7 resumes \u2248 ${clockTime(nextTickAtOrAfter(status.resumeMs))}` : "";
21138
+ return { text: `REFUSING${since}: ${status.reason ?? "refused"} \xB7 ${tick}${stalled} \xB7 ${usage}${resume}`, tone: "warn" };
21139
+ }
21140
+ return { text: `${tick} (${status.lastStatus ?? "other"}${status.reason ? `: ${status.reason}` : ""})${stalled} \xB7 ${usage}`, tone: "warn" };
21141
+ }
21142
+
21143
+ // src/crank/index.ts
20948
21144
  var ZCRANK_REDIS_HOST = "127.0.0.1";
20949
21145
  var ZCRANK_REDIS_PORT = 6379;
20950
21146
  function redisEndpoint(env2 = process.env) {
@@ -20959,7 +21155,7 @@ var ZCRANK_LOG_CAP = 500;
20959
21155
  var TAB_STATUS_SUFFIX = /[\s🟢🟡🔴🔔⚫✳✻●·─]+$/u;
20960
21156
  var defaultShell = (command, args) => {
20961
21157
  const result2 = spawnSync17(command, args, { encoding: "utf8", timeout: 1e4 });
20962
- if (result2.error) return { stdout: "", status: 127 };
21158
+ if (result2.error) return { stdout: "", status: result2.error.code === "ETIMEDOUT" ? 124 : 127 };
20963
21159
  return { stdout: result2.stdout ?? "", status: result2.status ?? 1, stderr: result2.stderr ?? "" };
20964
21160
  };
20965
21161
  function tabDisplay(name) {
@@ -21095,6 +21291,15 @@ function redis(deps = {}) {
21095
21291
  setTabEnabled: (tab, enabled2) => {
21096
21292
  run("HSET", `zcrank:tab:${tab}`, "enabled", enabled2 ? "1" : "0");
21097
21293
  },
21294
+ /** HGET zcrank:tab:<tab> root -> the owning project identity, or undefined (legacy key / absent). */
21295
+ tabRoot: (tab) => {
21296
+ const v = run("HGET", `zcrank:tab:${tab}`, "root");
21297
+ return v === "" || v == null ? void 0 : v;
21298
+ },
21299
+ /** HSET zcrank:tab:<tab> root <project identity> (PJAN-201). */
21300
+ setTabRoot: (tab, root) => {
21301
+ run("HSET", `zcrank:tab:${tab}`, "root", root);
21302
+ },
21098
21303
  /** HGET zcrank:tab:<tab> interval -> a valid interval, else undefined (absent or garbage). */
21099
21304
  tabInterval: (tab) => parseInterval(run("HGET", `zcrank:tab:${tab}`, "interval")),
21100
21305
  /** HSET zcrank:tab:<tab> interval <5m|1h|6h|1d>. */
@@ -21165,11 +21370,188 @@ function liveTabs(deps = {}) {
21165
21370
  try {
21166
21371
  const parsed = JSON.parse(result2.stdout);
21167
21372
  if (!Array.isArray(parsed)) return [];
21168
- return parsed.filter((entry) => typeof entry === "object" && entry !== null).map((entry) => ({ name: String(entry.name ?? ""), position: Number(entry.position ?? 0) }));
21373
+ return parsed.filter((entry) => typeof entry === "object" && entry !== null).map((entry) => {
21374
+ const id = Number(entry.tab_id);
21375
+ return {
21376
+ name: String(entry.name ?? ""),
21377
+ position: Number(entry.position ?? 0),
21378
+ ...entry.tab_id !== void 0 && entry.tab_id !== null && Number.isInteger(id) ? { id } : {}
21379
+ };
21380
+ });
21169
21381
  } catch {
21170
21382
  return [];
21171
21383
  }
21172
21384
  }
21385
+ function parseListPanes(stdout) {
21386
+ if (!stdout.trim()) return null;
21387
+ let parsed;
21388
+ try {
21389
+ parsed = JSON.parse(stdout);
21390
+ } catch {
21391
+ return null;
21392
+ }
21393
+ if (!Array.isArray(parsed)) return null;
21394
+ const panes = [];
21395
+ for (const entry of parsed) {
21396
+ if (typeof entry !== "object" || entry === null) continue;
21397
+ const e = entry;
21398
+ const id = Number(e.id);
21399
+ if (!Number.isFinite(id)) continue;
21400
+ const tabId = Number(e.tab_id);
21401
+ const tabPosition = Number(e.tab_position);
21402
+ panes.push({
21403
+ id,
21404
+ tabId: e.tab_id !== void 0 && e.tab_id !== null && Number.isInteger(tabId) ? tabId : void 0,
21405
+ tabPosition: e.tab_position !== void 0 && e.tab_position !== null && Number.isInteger(tabPosition) ? tabPosition : void 0,
21406
+ tabName: String(e.tab_name ?? ""),
21407
+ isPlugin: e.is_plugin === true,
21408
+ cwd: typeof e.pane_cwd === "string" && e.pane_cwd.trim() ? e.pane_cwd : void 0,
21409
+ title: typeof e.title === "string" ? e.title : null
21410
+ });
21411
+ }
21412
+ return panes;
21413
+ }
21414
+ function runListPanes(session, shell) {
21415
+ return shell("env", ["-u", "ZELLIJ", "-u", "ZELLIJ_SESSION_NAME", "timeout", "8s", "zellij", "-s", session, "action", "list-panes", "--json", "--all"]);
21416
+ }
21417
+ function livePanes(deps = {}) {
21418
+ const shell = deps.shell ?? defaultShell;
21419
+ const result2 = runListPanes(deps.session ?? ZCRANK_SESSION, shell);
21420
+ if (result2.status !== 0) return [];
21421
+ return (parseListPanes(result2.stdout) ?? []).map(({ title: _title, ...pane }) => pane);
21422
+ }
21423
+ function projectIdentity(dir, deps = {}) {
21424
+ const target = (dir ?? "").trim();
21425
+ if (!target) return void 0;
21426
+ const cached = deps.cache?.get(target);
21427
+ if (cached !== void 0) return cached ?? void 0;
21428
+ if (deps.budget) {
21429
+ deps.budget.untilMs ??= Date.now() + deps.budget.totalMs;
21430
+ if (Date.now() > deps.budget.untilMs) return void 0;
21431
+ }
21432
+ const shell = deps.shell ?? defaultShell;
21433
+ let identity2;
21434
+ try {
21435
+ const result2 = shell("timeout", ["2s", "git", "-C", target, "worktree", "list", "--porcelain"]);
21436
+ if (result2.status === 0) {
21437
+ const first = result2.stdout.split("\n").find((line) => line.startsWith("worktree "));
21438
+ const path = first?.slice("worktree ".length).replace(/\r$/, "").trim();
21439
+ if (path) identity2 = realpathSync12(path);
21440
+ }
21441
+ } catch {
21442
+ identity2 = void 0;
21443
+ }
21444
+ deps.cache?.set(target, identity2 ?? null);
21445
+ return identity2;
21446
+ }
21447
+ function withTabOwners(tabs, panes, identify, claimed = []) {
21448
+ const claimedSet = new Set(claimed);
21449
+ return tabs.map((tab) => {
21450
+ const counts = /* @__PURE__ */ new Map();
21451
+ for (const pane of panes) {
21452
+ if (pane.isPlugin || !pane.cwd) continue;
21453
+ const inTab = tab.id !== void 0 && pane.tabId !== void 0 ? pane.tabId === tab.id : pane.tabPosition === tab.position;
21454
+ if (!inTab) continue;
21455
+ const identity2 = identify(pane.cwd);
21456
+ if (identity2) counts.set(identity2, (counts.get(identity2) ?? 0) + 1);
21457
+ }
21458
+ const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]);
21459
+ const tied = ranked.filter(([, n]) => n === ranked[0]?.[1]).map(([identity2]) => identity2);
21460
+ const claimedTied = tied.filter((identity2) => claimedSet.has(identity2));
21461
+ const owner = tied.length === 1 ? tied[0] : claimedTied.length === 1 ? claimedTied[0] : void 0;
21462
+ const enriched = { ...tab };
21463
+ if (owner) enriched.owner = owner;
21464
+ if (ranked.length) enriched.ownedPanes = Object.fromEntries(ranked);
21465
+ return enriched;
21466
+ });
21467
+ }
21468
+ var PANE_CWD_TTL_SECONDS = 6 * 3600;
21469
+ function paneCwdCachePath(env2 = process.env) {
21470
+ return env2.ZCRANK_CWD_CACHE?.trim() || join35(homedir15(), ".local/state/zcrank/pane-cwds.json");
21471
+ }
21472
+ function loadCwdCache(path) {
21473
+ try {
21474
+ const parsed = JSON.parse(readFileSync30(path, "utf8"));
21475
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
21476
+ } catch {
21477
+ return {};
21478
+ }
21479
+ }
21480
+ function saveCwdCache(path, cache) {
21481
+ try {
21482
+ mkdirSync12(dirname22(path), { recursive: true });
21483
+ const tmp = `${path}.${process.pid}.tmp`;
21484
+ writeFileSync15(tmp, JSON.stringify(cache));
21485
+ renameSync10(tmp, path);
21486
+ } catch {
21487
+ }
21488
+ }
21489
+ function cwdEntryFresh(entry, nowSeconds) {
21490
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return false;
21491
+ const ts = Number(entry.ts ?? 0);
21492
+ return Number.isFinite(ts) && nowSeconds - ts < PANE_CWD_TTL_SECONDS;
21493
+ }
21494
+ function resolvePaneCwds(fetch2, opts = {}) {
21495
+ const now = (opts.nowMs ?? Date.now()) / 1e3;
21496
+ const path = opts.cachePath ?? paneCwdCachePath();
21497
+ const get = () => {
21498
+ try {
21499
+ const rows = fetch2();
21500
+ return Array.isArray(rows) ? rows : [];
21501
+ } catch {
21502
+ return [];
21503
+ }
21504
+ };
21505
+ const panes = get();
21506
+ if (!panes.length) return panes;
21507
+ const cache = loadCwdCache(path);
21508
+ const sameTab = (entry, pane) => (entry.tab_id ?? null) === (pane.tabId ?? null);
21509
+ const fill = () => {
21510
+ let unknown2 = 0;
21511
+ for (const pane of panes) {
21512
+ if (pane.isPlugin) continue;
21513
+ const key = String(pane.id);
21514
+ if (pane.cwd) {
21515
+ cache[key] = { cwd: pane.cwd, tab_id: pane.tabId ?? null, ts: now };
21516
+ continue;
21517
+ }
21518
+ const entry = cache[key];
21519
+ if (cwdEntryFresh(entry, now) && sameTab(entry, pane) && typeof entry.cwd === "string" && entry.cwd) {
21520
+ pane.cwd = entry.cwd;
21521
+ } else {
21522
+ unknown2++;
21523
+ }
21524
+ }
21525
+ return unknown2;
21526
+ };
21527
+ let unknown = fill();
21528
+ for (let attempt = 0; attempt < (opts.retries ?? 0) && unknown; attempt++) {
21529
+ const again = new Map(get().filter((pane) => pane.cwd).map((pane) => [pane.id, pane.cwd]));
21530
+ for (const pane of panes) {
21531
+ const cwd = again.get(pane.id);
21532
+ if (!pane.isPlugin && !pane.cwd && cwd) pane.cwd = cwd;
21533
+ }
21534
+ unknown = fill();
21535
+ }
21536
+ const live = new Set(panes.filter((pane) => !pane.isPlugin).map((pane) => String(pane.id)));
21537
+ for (const key of Object.keys(cache)) {
21538
+ if (!live.has(key) || !cwdEntryFresh(cache[key], now)) delete cache[key];
21539
+ }
21540
+ saveCwdCache(path, cache);
21541
+ return panes;
21542
+ }
21543
+ function liveTabsWithOwners(deps = {}) {
21544
+ const tabs = liveTabs(deps);
21545
+ if (!tabs.length) return tabs;
21546
+ const cache = /* @__PURE__ */ new Map();
21547
+ const budget = { totalMs: IDENTITY_BUDGET_MS };
21548
+ const panes = resolvePaneCwds(() => livePanes(deps), { retries: deps.retries ?? 1, nowMs: deps.nowMs, cachePath: deps.cachePath });
21549
+ return withTabOwners(tabs, panes, (cwd) => projectIdentity(cwd, { shell: deps.shell, cache, budget }), deps.claimed ?? []);
21550
+ }
21551
+ function claimedRoots(store, keys = storedTabKeys(store)) {
21552
+ return [...new Set(Object.values(storedTabRoots(store, keys)))].sort();
21553
+ }
21554
+ var IDENTITY_BUDGET_MS = 2e4;
21173
21555
  function candidateNames(project) {
21174
21556
  const out = [project.name.trim()].filter(Boolean);
21175
21557
  for (const agent of project.agents) {
@@ -21182,13 +21564,41 @@ function candidateNames(project) {
21182
21564
  }
21183
21565
  return out;
21184
21566
  }
21185
- function resolveTabForProject(project, live, storedTabs) {
21567
+ function keyForLiveTab(tab, storedTabs) {
21568
+ const display = tabDisplay(tab.name);
21569
+ if (storedTabs.includes(display)) return display;
21570
+ return storedTabs.find((s) => canon(s) && canon(s) === canon(display)) ?? display;
21571
+ }
21572
+ function ownerOfLiveTab(live, liveTab) {
21573
+ if (liveTab === void 0) return void 0;
21574
+ return live.find((tab) => tabDisplay(tab.name) === liveTab)?.owner;
21575
+ }
21576
+ function resolveTabForProject(project, live, storedTabs, scope = {}) {
21577
+ const byCwd = resolveByCwd(project, live, storedTabs, scope);
21578
+ if (byCwd) return byCwd;
21579
+ const resolution = resolveByName(project, live, storedTabs);
21580
+ const tabOwner = ownerOfLiveTab(live, resolution.liveTab);
21581
+ return tabOwner ? { ...resolution, tabOwner } : resolution;
21582
+ }
21583
+ function resolveByCwd(project, live, storedTabs, scope) {
21584
+ const identity2 = scope.identity;
21585
+ if (!identity2) return void 0;
21586
+ const owned = live.filter((tab2) => tab2.owner === identity2);
21587
+ if (!owned.length) return void 0;
21588
+ const candidateCanons = new Set(candidateNames(project).map((name) => canon(name)).filter(Boolean));
21589
+ const ranked = [...owned].sort((a, b) => Number(candidateCanons.has(canon(b.name))) - Number(candidateCanons.has(canon(a.name))) || (b.ownedPanes?.[identity2] ?? 0) - (a.ownedPanes?.[identity2] ?? 0) || a.position - b.position);
21590
+ const tab = ranked[0];
21591
+ if (!tab) return void 0;
21592
+ let key = keyForLiveTab(tab, storedTabs);
21593
+ if (!storedTabs.includes(key)) {
21594
+ const rooted = storedTabs.find((stored) => scope.roots?.[stored] === identity2);
21595
+ if (rooted !== void 0) key = rooted;
21596
+ }
21597
+ return { tab: key, source: "live-cwd", matched: identity2, storedAsIs: false, liveTab: tabDisplay(tab.name), tabOwner: identity2 };
21598
+ }
21599
+ function resolveByName(project, live, storedTabs) {
21186
21600
  const candidates = candidateNames(project);
21187
- const keyFor = (tab) => {
21188
- const display = tabDisplay(tab.name);
21189
- if (storedTabs.includes(display)) return display;
21190
- return storedTabs.find((s) => canon(s) && canon(s) === canon(display)) ?? display;
21191
- };
21601
+ const keyFor = (tab) => keyForLiveTab(tab, storedTabs);
21192
21602
  const sideSets = [
21193
21603
  (tab) => [tab.name],
21194
21604
  (tab) => tabDisplay(tab.name).split(":")
@@ -21224,7 +21634,7 @@ function resolveTabForProject(project, live, storedTabs) {
21224
21634
  function projectNameFor(handle, registryName) {
21225
21635
  if (registryName && registryName.trim()) return registryName.trim();
21226
21636
  try {
21227
- const manifest = JSON.parse(readFileSync29(join34(handle.root, ".project.json"), "utf8"));
21637
+ const manifest = JSON.parse(readFileSync30(join35(handle.root, ".project.json"), "utf8"));
21228
21638
  const name = manifest.project_name;
21229
21639
  if (typeof name === "string" && name.trim()) return name.trim();
21230
21640
  } catch {
@@ -21235,7 +21645,7 @@ function projectAgentsFor(handle, registryAgents) {
21235
21645
  const fromRecord = Object.entries(registryAgents ?? {}).map(([agentHandle, value]) => ({ handle: agentHandle, role: String(value?.role ?? "") }));
21236
21646
  if (fromRecord.length) return fromRecord;
21237
21647
  try {
21238
- const manifest = JSON.parse(readFileSync29(join34(handle.root, ".project.json"), "utf8"));
21648
+ const manifest = JSON.parse(readFileSync30(join35(handle.root, ".project.json"), "utf8"));
21239
21649
  const agents = manifest.agents;
21240
21650
  if (agents && typeof agents === "object" && !Array.isArray(agents)) {
21241
21651
  return Object.entries(agents).map(([agentHandle, value]) => ({ handle: agentHandle, role: typeof value?.role === "string" ? value.role : "" }));
@@ -21244,17 +21654,22 @@ function projectAgentsFor(handle, registryAgents) {
21244
21654
  }
21245
21655
  return [];
21246
21656
  }
21247
- function keysNamingTab(tab, storedKeys) {
21657
+ function keysNamingTab(tab, storedKeys, scope = {}) {
21248
21658
  const c = canon(tab);
21249
21659
  const out = [];
21250
21660
  if (storedKeys.includes(tab)) out.push(tab);
21251
21661
  for (const key of storedKeys) {
21252
21662
  if (key !== tab && c && canon(key) === c) out.push(key);
21253
21663
  }
21664
+ if (scope.owner) {
21665
+ for (const key of storedKeys) {
21666
+ if (scope.roots?.[key] === scope.owner && !out.includes(key)) out.push(key);
21667
+ }
21668
+ }
21254
21669
  return out;
21255
21670
  }
21256
- function snapshotCrank(store, tab, storedKeys, nowMs, beatLimit = 5) {
21257
- const keys = keysNamingTab(tab, storedKeys);
21671
+ function snapshotCrank(store, tab, storedKeys, nowMs, beatLimit = 5, scope = {}) {
21672
+ const keys = keysNamingTab(tab, storedKeys, scope);
21258
21673
  const readKeys = keys.length ? keys : [tab];
21259
21674
  const enabledValues = keys.map((key) => store.tabEnabled(key)).filter((v) => v !== void 0);
21260
21675
  const enabled2 = enabledValues.length ? keys.length > 0 && keys.every((key) => store.tabEnabled(key) === "1") : void 0;
@@ -21280,12 +21695,13 @@ function snapshotCrank(store, tab, storedKeys, nowMs, beatLimit = 5) {
21280
21695
  }
21281
21696
  function planRenameMigration(input) {
21282
21697
  const target = input.resolution.liveTab;
21283
- if (!target || input.resolution.source !== "live-exact" && input.resolution.source !== "live-prefix") return void 0;
21698
+ if (!target || input.resolution.source !== "live-exact" && input.resolution.source !== "live-prefix" && input.resolution.source !== "live-cwd") return void 0;
21284
21699
  const targetCanon = canon(target);
21285
21700
  const wanted = new Set([targetCanon, ...input.candidates.map((name) => canon(name))].filter(Boolean));
21286
21701
  const otherLive = new Set(input.live.map((tab) => canon(tab.name)).filter((c) => c && c !== targetCanon));
21287
21702
  const oldKeys = [...new Set(input.storedKeys)].filter((key) => {
21288
21703
  if (key === target) return false;
21704
+ if (input.identity && input.roots?.[key] === input.identity) return true;
21289
21705
  const c = canon(key);
21290
21706
  return c !== "" && wanted.has(c) && !otherLive.has(c);
21291
21707
  });
@@ -21316,12 +21732,36 @@ function applyRenameMigration(store, plan) {
21316
21732
  store.deleteKeys(`zcrank:tab:${old}`, ...oldPanes, `zcrank:log:${old}`);
21317
21733
  }
21318
21734
  }
21319
- function buildCrankContext(project, live, storedKeys) {
21320
- return { resolution: resolveTabForProject(project, live, storedKeys), candidates: candidateNames(project), live, storedKeys };
21735
+ function buildCrankContext(project, live, storedKeys, scope = {}) {
21736
+ const roots = scope.roots ?? {};
21737
+ return {
21738
+ resolution: resolveTabForProject(project, live, storedKeys, { identity: scope.identity, roots }),
21739
+ candidates: candidateNames(project),
21740
+ live,
21741
+ storedKeys,
21742
+ identity: scope.identity,
21743
+ roots
21744
+ };
21745
+ }
21746
+ function keyScopeOf(ctx) {
21747
+ return { owner: ctx.resolution.tabOwner, roots: ctx.roots };
21321
21748
  }
21322
21749
  function storedTabKeys(store) {
21323
21750
  return store.tabKeys().map((key) => key.split(":").slice(2).join(":"));
21324
21751
  }
21752
+ function storedTabRoots(store, keys) {
21753
+ const roots = {};
21754
+ for (const key of keys) {
21755
+ let root;
21756
+ try {
21757
+ root = store.tabRoot(key);
21758
+ } catch (error) {
21759
+ if (!(error instanceof Error) || !error.message.startsWith("Redis error reply")) throw error;
21760
+ }
21761
+ if (root) roots[key] = root;
21762
+ }
21763
+ return roots;
21764
+ }
21325
21765
  function enableCrank(store, ctx, options = {}) {
21326
21766
  const now = options.now ?? /* @__PURE__ */ new Date();
21327
21767
  const ts = crankTimestamp(now);
@@ -21331,16 +21771,20 @@ function enableCrank(store, ctx, options = {}) {
21331
21771
  storedKeys: ctx.storedKeys,
21332
21772
  live: ctx.live,
21333
21773
  intervals: (key) => store.tabInterval(key),
21334
- chosenInterval: options.interval
21774
+ chosenInterval: options.interval,
21775
+ identity: ctx.identity,
21776
+ roots: ctx.roots
21335
21777
  });
21336
21778
  const tab = plan?.target ?? ctx.resolution.tab;
21337
21779
  store.setTabEnabled(tab, true);
21780
+ writeRoot(store, tab, ctx.identity);
21338
21781
  if (options.interval) store.setTabInterval(tab, options.interval);
21339
21782
  if (plan) applyRenameMigration(store, plan);
21340
21783
  else {
21341
- for (const sibling of keysNamingTab(tab, ctx.storedKeys)) {
21784
+ for (const sibling of keysNamingTab(tab, ctx.storedKeys, keyScopeOf(ctx))) {
21342
21785
  if (sibling === tab) continue;
21343
21786
  store.setTabEnabled(sibling, true);
21787
+ writeRoot(store, sibling, ctx.identity);
21344
21788
  if (options.interval) store.clearTabInterval(sibling);
21345
21789
  }
21346
21790
  }
@@ -21350,42 +21794,54 @@ function enableCrank(store, ctx, options = {}) {
21350
21794
  for (const { from, to } of renamed) store.pushLog(tab, "renamed", `pj crank enable: ${from} -> ${to}`, ts);
21351
21795
  return { tab, renamed, interval: options.interval ?? plan?.carryInterval };
21352
21796
  }
21353
- function disableCrank(store, tab, now = /* @__PURE__ */ new Date()) {
21797
+ function writeRoot(store, tab, root) {
21798
+ if (root) store.setTabRoot(tab, root);
21799
+ }
21800
+ function disableCrank(store, tab, now = /* @__PURE__ */ new Date(), root) {
21354
21801
  store.setTabEnabled(tab, false);
21802
+ writeRoot(store, tab, root);
21355
21803
  store.pushLog(tab, "disabled", "pj crank disable", crankTimestamp(now));
21356
21804
  }
21357
- function setCrankInterval(store, tab, storedKeys, interval, now = /* @__PURE__ */ new Date()) {
21358
- for (const key of keysNamingTab(tab, storedKeys)) {
21805
+ function setCrankInterval(store, tab, storedKeys, interval, now = /* @__PURE__ */ new Date(), scope = {}) {
21806
+ for (const key of keysNamingTab(tab, storedKeys, scope)) {
21359
21807
  if (key !== tab) store.clearTabInterval(key);
21360
21808
  }
21361
21809
  if (interval) store.setTabInterval(tab, interval);
21362
21810
  else store.clearTabInterval(tab);
21811
+ writeRoot(store, tab, scope.root);
21363
21812
  const action = interval ? `pj crank tui: interval ${interval}` : `pj crank tui: interval reset to global (${store.globalInterval()})`;
21364
21813
  store.pushLog(tab, "interval", action, crankTimestamp(now));
21365
21814
  }
21366
21815
  function buildCrankList(input) {
21367
21816
  const global = input.globalInterval ?? ZCRANK_DEFAULT_INTERVAL;
21368
- const intervalOf = (tab) => effectiveInterval(
21369
- (keysNamingTab(tab, input.storedTabs).length ? keysNamingTab(tab, input.storedTabs) : [tab]).map((key) => input.tabInterval?.(key)),
21370
- global
21371
- );
21817
+ const roots = input.roots ?? {};
21818
+ const intervalOf = (tab, owner) => {
21819
+ const keys = keysNamingTab(tab, input.storedTabs, { owner, roots });
21820
+ return effectiveInterval((keys.length ? keys : [tab]).map((key) => input.tabInterval?.(key)), global);
21821
+ };
21372
21822
  const rows = [];
21373
21823
  const consumed = /* @__PURE__ */ new Set();
21824
+ const consumedKeys = /* @__PURE__ */ new Set();
21374
21825
  for (const project of input.projects) {
21375
- const resolution = resolveTabForProject(project, input.live, input.storedTabs);
21826
+ const identity2 = project.repoPath ? input.identityOf?.(project.repoPath) : void 0;
21827
+ const resolution = resolveTabForProject(project, input.live, input.storedTabs, { identity: identity2, roots });
21376
21828
  consumed.add(canon(resolution.tab));
21377
- const enabled2 = input.tabEnabled(resolution.tab);
21829
+ const keys = keysNamingTab(resolution.tab, input.storedTabs, { owner: resolution.tabOwner, roots });
21830
+ for (const key of keys) consumedKeys.add(key);
21831
+ const values = (keys.length ? keys : [resolution.tab]).map((key) => input.tabEnabled(key)).filter((v) => v !== void 0);
21832
+ const enabled2 = values.length ? values.every((v) => v === "1") ? "1" : "0" : void 0;
21378
21833
  rows.push({
21379
21834
  slug: project.slug,
21380
21835
  name: project.name,
21381
21836
  tab: resolution.tab,
21382
21837
  crank: enabled2 === "1" ? "ON" : enabled2 === "0" ? "OFF" : "\u2014",
21383
21838
  source: resolution.source,
21384
- ...intervalOf(resolution.tab)
21839
+ ...resolution.liveTab !== void 0 ? { liveTab: resolution.liveTab } : {},
21840
+ ...intervalOf(resolution.tab, resolution.tabOwner)
21385
21841
  });
21386
21842
  }
21387
- const orphans = input.storedTabs.filter((stored) => !consumed.has(canon(stored))).map((stored) => ({ tab: stored, enabled: input.tabEnabled(stored) === "1", ...intervalOf(stored) }));
21388
- return { rows, orphans };
21843
+ const orphans = input.storedTabs.filter((stored) => !consumed.has(canon(stored)) && !consumedKeys.has(stored)).map((stored) => ({ tab: stored, enabled: input.tabEnabled(stored) === "1", ...intervalOf(stored) }));
21844
+ return { engine: input.engine, rows, orphans };
21389
21845
  }
21390
21846
  function crankTimestamp(now = /* @__PURE__ */ new Date()) {
21391
21847
  return now.toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -21401,17 +21857,25 @@ function renderLogRow(raw) {
21401
21857
  return null;
21402
21858
  }
21403
21859
  }
21860
+ function tabCell(row) {
21861
+ if (row.source !== "live-cwd" || row.liveTab === void 0) return row.tab;
21862
+ return canon(row.tab) === canon(row.liveTab) ? row.liveTab : `${row.liveTab} (key ${row.tab})`;
21863
+ }
21404
21864
  function formatCrankList(report) {
21405
21865
  const lines = [""];
21406
21866
  lines.push(` ${heading("Crank by project")}`);
21867
+ if (report.engine) {
21868
+ const summary = engineSummary(report.engine);
21869
+ lines.push(summary.tone === "unknown" ? ` ${dim(summary.text)}` : ` ${summary.tone === "ok" ? green("Engine") : yellow("Engine")} ${summary.tone === "warn" ? yellow(summary.text) : dim(summary.text)}`);
21870
+ }
21407
21871
  lines.push("");
21408
21872
  const slugWidth = Math.max("PROJECT".length, ...report.rows.map((row) => row.slug.length));
21409
- const tabWidth = Math.max("TAB".length, ...report.rows.map((row) => row.tab.length), ...report.orphans.map((orphan) => orphan.tab.length));
21873
+ const tabWidth = Math.max("TAB".length, ...report.rows.map((row) => tabCell(row).length), ...report.orphans.map((orphan) => orphan.tab.length));
21410
21874
  const intervalCell = (row) => row.inherited ? dim(`${row.interval} (global)`) : row.interval;
21411
21875
  lines.push(` ${"PROJECT".padEnd(slugWidth)} ${"TAB".padEnd(tabWidth)} ${"CRANK".padEnd(5)} INTERVAL`);
21412
21876
  for (const row of report.rows) {
21413
21877
  const state = row.crank === "ON" ? green("ON") : row.crank === "OFF" ? dim("OFF") : dim("\u2014");
21414
- lines.push(` ${cyan(row.slug.padEnd(slugWidth))} ${row.tab.padEnd(tabWidth)} ${state}${" ".repeat(Math.max(0, 5 - (row.crank === "ON" ? 2 : row.crank === "OFF" ? 3 : 1)))} ${intervalCell(row)}`);
21878
+ lines.push(` ${cyan(row.slug.padEnd(slugWidth))} ${tabCell(row).padEnd(tabWidth)} ${state}${" ".repeat(Math.max(0, 5 - (row.crank === "ON" ? 2 : row.crank === "OFF" ? 3 : 1)))} ${intervalCell(row)}`);
21415
21879
  }
21416
21880
  if (report.orphans.length) {
21417
21881
  lines.push("");
@@ -21426,34 +21890,492 @@ function formatCrankList(report) {
21426
21890
 
21427
21891
  // src/crank/controller.ts
21428
21892
  var LIVE_TABS_TTL_MS = 3e4;
21893
+ var LIVE_OWNERS_TTL_MS = 3e5;
21894
+ function ownerMemoReader(deps = {}) {
21895
+ const clock = deps.clock ?? Date.now;
21896
+ const read = deps.read ?? ((claimed) => liveTabsWithOwners({ claimed }));
21897
+ const listTabs = deps.tabs ?? (() => liveTabs());
21898
+ const readClaimed = () => {
21899
+ try {
21900
+ return deps.claimed?.() ?? [];
21901
+ } catch {
21902
+ return [];
21903
+ }
21904
+ };
21905
+ let memo;
21906
+ return () => {
21907
+ const now = clock();
21908
+ const tabs = listTabs();
21909
+ const claimed = readClaimed();
21910
+ const claimedSig = claimed.join("\n");
21911
+ if (memo && memo.claimed === claimedSig && now - memo.at <= LIVE_OWNERS_TTL_MS && tabs.length && tabs.every((tab) => tab.id !== void 0 && memo?.byId.has(tab.id))) {
21912
+ const known = memo.byId;
21913
+ return tabs.map((tab) => ({ ...tab, ...known.get(tab.id) }));
21914
+ }
21915
+ const full = read(claimed);
21916
+ memo = full.length && full.every((tab) => tab.id !== void 0) ? { at: now, claimed: claimedSig, byId: new Map(full.map((tab) => [tab.id, { owner: tab.owner, ownedPanes: tab.ownedPanes }])) } : void 0;
21917
+ return full;
21918
+ };
21919
+ }
21429
21920
  function createCrankController(deps) {
21430
21921
  const clock = deps.clock ?? Date.now;
21431
- const readLive = deps.live ?? (() => liveTabs());
21922
+ const readLive = deps.live ?? ownerMemoReader({ clock, claimed: () => claimedRoots(deps.store) });
21923
+ const readEngine = (nowMs) => {
21924
+ try {
21925
+ return (deps.engine ?? ((now) => readEngineStatus({ nowMs: now })))(nowMs);
21926
+ } catch {
21927
+ return unknownEngineStatus(nowMs);
21928
+ }
21929
+ };
21432
21930
  let cachedLive;
21433
21931
  const live = (fresh) => {
21434
21932
  const now = clock();
21435
21933
  if (fresh || !cachedLive || now - cachedLive.at > LIVE_TABS_TTL_MS) cachedLive = { at: now, tabs: readLive() };
21436
21934
  return cachedLive.tabs;
21437
21935
  };
21438
- const context = (fresh) => buildCrankContext(deps.project, live(fresh), storedTabKeys(deps.store));
21936
+ const context = (fresh) => {
21937
+ const keys = storedTabKeys(deps.store);
21938
+ return buildCrankContext(deps.project, live(fresh), keys, { identity: deps.identity, roots: storedTabRoots(deps.store, keys) });
21939
+ };
21439
21940
  return {
21440
21941
  slug: deps.slug,
21441
21942
  name: deps.project.name,
21442
21943
  read(nowMs = clock()) {
21443
21944
  const ctx = context(false);
21444
- return { slug: deps.slug, name: deps.project.name, resolution: ctx.resolution, snapshot: snapshotCrank(deps.store, ctx.resolution.tab, ctx.storedKeys, nowMs) };
21945
+ return {
21946
+ slug: deps.slug,
21947
+ name: deps.project.name,
21948
+ resolution: ctx.resolution,
21949
+ snapshot: snapshotCrank(deps.store, ctx.resolution.tab, ctx.storedKeys, nowMs, 5, keyScopeOf(ctx)),
21950
+ engine: readEngine(nowMs)
21951
+ };
21445
21952
  },
21446
21953
  enable: () => enableCrank(deps.store, context(true), { now: new Date(clock()) }),
21447
21954
  disable() {
21448
- disableCrank(deps.store, context(true).resolution.tab, new Date(clock()));
21955
+ const ctx = context(true);
21956
+ disableCrank(deps.store, ctx.resolution.tab, new Date(clock()), ctx.identity);
21449
21957
  },
21450
21958
  setInterval(interval) {
21451
21959
  const ctx = context(true);
21452
- setCrankInterval(deps.store, ctx.resolution.tab, ctx.storedKeys, interval, new Date(clock()));
21960
+ setCrankInterval(deps.store, ctx.resolution.tab, ctx.storedKeys, interval, new Date(clock()), { ...keyScopeOf(ctx), root: ctx.identity });
21453
21961
  }
21454
21962
  };
21455
21963
  }
21456
21964
 
21965
+ // src/crank/panes.ts
21966
+ import { readFileSync as readFileSync31, readdirSync as readdirSync13, readlinkSync as readlinkSync9 } from "node:fs";
21967
+ import { homedir as homedir16 } from "node:os";
21968
+ import { basename as basename11, join as join36 } from "node:path";
21969
+ var AGENT_CLIS = [
21970
+ "claude",
21971
+ "codex",
21972
+ "copilot",
21973
+ "opencode",
21974
+ "aider",
21975
+ "hermes",
21976
+ "agy",
21977
+ "kimi-code",
21978
+ "kimi-co",
21979
+ "gemini",
21980
+ "openclaw",
21981
+ // Not in Deckard's list: the Kimi binary on the operator box is `~/.kimi-code/bin/kimi` (argv[0]
21982
+ // basename `kimi`), which neither `kimi-code` nor `kimi-co` prefixes. Listed after them so the
21983
+ // reported name stays `kimi-code` where Deckard would say it.
21984
+ "kimi"
21985
+ ];
21986
+ function agentCli(exe, argv) {
21987
+ if (argv.includes("mcp-server") || argv.includes(" mcp ")) return null;
21988
+ const byName = AGENT_CLIS.find((pat) => exe === pat || exe.startsWith(pat));
21989
+ if (byName) return byName;
21990
+ const tokens = argv.split(" ").filter(Boolean);
21991
+ if (INTERPRETER.test(exe)) {
21992
+ const program2 = interpretedProgram(exe, tokens);
21993
+ if (!program2) return null;
21994
+ if (program2.module) {
21995
+ const top = (program2.module.split(".")[0] ?? "").replace(/_/g, "-");
21996
+ return AGENT_CLIS.find((pat) => top === pat || PATH_SUFFIXES.some((suffix) => top === `${pat}-${suffix}`)) ?? null;
21997
+ }
21998
+ return PATH_PASS.find(([, re]) => re.test(program2.script))?.[0] ?? null;
21999
+ }
22000
+ const versioned = /\/([^/]+)\/versions\/[^/]+$/.exec(tokens[0] ?? "");
22001
+ return versioned ? AGENT_CLIS.find((pat) => versioned[1] === pat) ?? null : null;
22002
+ }
22003
+ var INTERPRETER = /^(?:node(?:js)?|bun|deno|tsx|python[\d.]*|ruby|perl|bash|sh|zsh|dash|fish|ksh)$/;
22004
+ var SHELL = /^(?:bash|sh|zsh|dash|fish|ksh)$/;
22005
+ var PYTHON = /^python[\d.]*$/;
22006
+ var VALUE_OPTIONS = [
22007
+ [/^(?:node(?:js)?|bun|deno|tsx)$/, /* @__PURE__ */ new Set(["-r", "--require", "--import", "--loader", "--experimental-loader", "-C", "--conditions", "--inspect-port", "--debug-port", "--input-type", "--title", "--env-file", "--disable-warning", "--watch-path", "--stack-size", "--max-old-space-size", "--redirect-warnings", "--openssl-config", "--icu-data-dir", "--tls-cipher-list", "--secure-heap", "--diagnostic-dir", "--report-dir", "--cpu-prof-dir", "--heap-prof-dir"])],
22008
+ [PYTHON, /* @__PURE__ */ new Set(["-W", "-X", "-Q", "--check-hash-based-pycs"])],
22009
+ [SHELL, /* @__PURE__ */ new Set(["-o", "+o", "-O", "+O", "--rcfile", "--init-file"])]
22010
+ ];
22011
+ function interpretedProgram(exe, tokens) {
22012
+ const takesValue = VALUE_OPTIONS.find(([re]) => re.test(exe))?.[1] ?? /* @__PURE__ */ new Set();
22013
+ for (let i = 1; i < tokens.length; i++) {
22014
+ const token = tokens[i];
22015
+ if (token === "-m" || PYTHON.test(exe) && /^-[BEIOPRSbdhiqsuvx]*m$/.test(token)) return tokens[i + 1] ? { module: tokens[i + 1], script: "" } : null;
22016
+ if (token === "-e" || token === "-p" || token === "-pe" || token === "-ep" || token === "--eval" || token === "--print") return null;
22017
+ if (SHELL.test(exe) && /^-[a-zA-Z]*c[a-zA-Z]*$/.test(token)) return null;
22018
+ if (PYTHON.test(exe) && /^-[BEIOPRSbdhiqsuvx]*c$/.test(token)) return null;
22019
+ if (token.startsWith("-") || token.startsWith("+")) {
22020
+ if (takesValue.has(token)) i++;
22021
+ continue;
22022
+ }
22023
+ return { script: token };
22024
+ }
22025
+ return null;
22026
+ }
22027
+ var PATH_SUFFIXES = ["code", "cli", "agent"];
22028
+ var PATH_PASS = AGENT_CLIS.map((pat) => {
22029
+ const name = pat.replace(/[-]/g, "\\-");
22030
+ const end = "(?=$|[ /.])";
22031
+ return [pat, new RegExp(`/${name}(?:${end}|-(?:${PATH_SUFFIXES.join("|")})${end})`)];
22032
+ });
22033
+ function parseProcStat(text2) {
22034
+ const close = text2.lastIndexOf(")");
22035
+ const open = text2.indexOf("(");
22036
+ if (close < 0 || open < 0 || open > close) return null;
22037
+ const comm = text2.slice(open + 1, close);
22038
+ const fields = text2.slice(close + 1).trim().split(/\s+/);
22039
+ const state = fields[0];
22040
+ const ppid = Number(fields[1]);
22041
+ const pgrp = Number(fields[2]);
22042
+ const ttyNr = Number(fields[4]);
22043
+ const tpgid = Number(fields[5]);
22044
+ const startTime = fields[19];
22045
+ if (!state || !startTime || ![ppid, pgrp, ttyNr, tpgid].every(Number.isInteger)) return null;
22046
+ return { comm, state, ppid, pgrp, ttyNr, tpgid, startTime };
22047
+ }
22048
+ function readProcStat(pid) {
22049
+ try {
22050
+ return parseProcStat(readFileSync31(`/proc/${pid}/stat`, "latin1"));
22051
+ } catch {
22052
+ return null;
22053
+ }
22054
+ }
22055
+ function readTag(pid) {
22056
+ let raw;
22057
+ try {
22058
+ raw = readFileSync31(`/proc/${pid}/environ`);
22059
+ } catch {
22060
+ return null;
22061
+ }
22062
+ let paneId = null;
22063
+ let session = null;
22064
+ let start = 0;
22065
+ while (start <= raw.length) {
22066
+ let end = raw.indexOf(0, start);
22067
+ if (end < 0) end = raw.length;
22068
+ const eq = raw.indexOf(61, start);
22069
+ if (eq >= 0 && eq < end) {
22070
+ const key = raw.toString("latin1", start, eq);
22071
+ if (key === "ZELLIJ_PANE_ID") {
22072
+ const value = raw.toString("utf8", eq + 1, end);
22073
+ paneId = /^\d+$/.test(value) ? Number(value) : null;
22074
+ } else if (key === "ZELLIJ_SESSION_NAME") {
22075
+ session = raw.toString("utf8", eq + 1, end);
22076
+ }
22077
+ }
22078
+ start = end + 1;
22079
+ }
22080
+ return paneId === null ? null : { paneId, session };
22081
+ }
22082
+ function readArgv(pid) {
22083
+ try {
22084
+ const parts = readFileSync31(`/proc/${pid}/cmdline`, "utf8").split("\0").filter(Boolean);
22085
+ return { argv: parts.join(" "), exe: parts[0] ? basename11(parts[0]) : "" };
22086
+ } catch {
22087
+ return { argv: "", exe: "" };
22088
+ }
22089
+ }
22090
+ var ProcScanner = class {
22091
+ known = /* @__PURE__ */ new Map();
22092
+ enumerate;
22093
+ /**
22094
+ * `pids` replaces the enumeration of /proc: the scanner then looks at exactly those pids and no
22095
+ * others. Production never passes it (the default reads every pid of the box, which is the
22096
+ * point). A test that spawns its own processes passes its own pids, so that processes OTHER
22097
+ * tests or agents have on the box (session-less tagged ones are visible to every scan by
22098
+ * design) cannot change its answer.
22099
+ */
22100
+ constructor(options = {}) {
22101
+ this.enumerate = options.pids;
22102
+ }
22103
+ /** How many pids the cache holds (negatives included). For the cost report and tests. */
22104
+ cached() {
22105
+ return this.known.size;
22106
+ }
22107
+ /** Is `pid` in the cache, as a tagged process or a remembered negative? */
22108
+ has(pid) {
22109
+ return this.known.has(pid);
22110
+ }
22111
+ /**
22112
+ * Every process in `session` that carries a pane tag. A process whose ZELLIJ_SESSION_NAME names
22113
+ * a DIFFERENT session is dropped; one with no session name at all is kept (`session: null`),
22114
+ * because zellij has not always set it and dropping it would lose real agents (Deckard's rule).
22115
+ * That also keeps processes some other tool started under `env -u ZELLIJ_SESSION_NAME` (e.g.
22116
+ * a `zellij action list-panes` run from a pane): they carry a pane id and no session, and are
22117
+ * not agents, so classification discards them.
22118
+ */
22119
+ scan(session) {
22120
+ let pids;
22121
+ if (this.enumerate) {
22122
+ pids = [...this.enumerate()];
22123
+ } else {
22124
+ try {
22125
+ pids = readdirSync13("/proc").filter((name) => /^\d+$/.test(name)).map(Number);
22126
+ } catch {
22127
+ return [];
22128
+ }
22129
+ }
22130
+ const alive = /* @__PURE__ */ new Set();
22131
+ const out = [];
22132
+ for (const pid of pids) {
22133
+ alive.add(pid);
22134
+ const stat = readProcStat(pid);
22135
+ if (!stat) continue;
22136
+ let slot = this.known.get(pid);
22137
+ if (!slot || slot.startTime !== stat.startTime || slot.comm !== stat.comm) {
22138
+ const tag2 = readTag(pid);
22139
+ slot = { startTime: stat.startTime, comm: stat.comm, tag: tag2, ...tag2 ? readArgv(pid) : { argv: "", exe: "" } };
22140
+ this.known.set(pid, slot);
22141
+ } else if (slot.tag && stat.state !== "Z") {
22142
+ Object.assign(slot, readArgv(pid));
22143
+ }
22144
+ const tag = slot.tag;
22145
+ if (!tag) continue;
22146
+ if (tag.session !== null && tag.session !== session) continue;
22147
+ let cwd = null;
22148
+ try {
22149
+ cwd = readlinkSync9(`/proc/${pid}/cwd`);
22150
+ } catch {
22151
+ }
22152
+ out.push({ pid, paneId: tag.paneId, session: tag.session, argv: slot.argv, exe: slot.exe, cwd, stat });
22153
+ }
22154
+ for (const pid of this.known.keys()) if (!alive.has(pid)) this.known.delete(pid);
22155
+ return out.sort((a, b) => a.pid - b.pid);
22156
+ }
22157
+ };
22158
+ var PANE_STATUSES = ["busy", "idle", "shell"];
22159
+ function claudeSessionsDir(env2 = process.env) {
22160
+ const override = env2.PJ_CLAUDE_SESSIONS_DIR?.trim();
22161
+ return override || join36(homedir16(), ".claude", "sessions");
22162
+ }
22163
+ function readClaudeSession(pid, stat, dir = claudeSessionsDir()) {
22164
+ const none = (reason) => ({ kind: null, sessionId: null, reason, status: "unknown", statusUpdatedAt: null });
22165
+ let raw;
22166
+ try {
22167
+ raw = readFileSync31(join36(dir, `${pid}.json`), "utf8");
22168
+ } catch {
22169
+ return none("no-session-file");
22170
+ }
22171
+ let data;
22172
+ try {
22173
+ data = JSON.parse(raw);
22174
+ } catch {
22175
+ return none("unreadable-session-file");
22176
+ }
22177
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return none("unreadable-session-file");
22178
+ const file = data;
22179
+ const procStart = file.procStart === void 0 || file.procStart === null ? "" : String(file.procStart);
22180
+ if (!stat || !procStart || procStart !== stat.startTime) return none("stale-session-file");
22181
+ const sessionId = typeof file.sessionId === "string" && file.sessionId ? file.sessionId : null;
22182
+ const status = typeof file.status === "string" && PANE_STATUSES.includes(file.status) ? file.status : "unknown";
22183
+ const at = Number(file.statusUpdatedAt);
22184
+ return {
22185
+ kind: typeof file.kind === "string" ? file.kind : null,
22186
+ sessionId,
22187
+ reason: sessionId ? null : "no-session-id",
22188
+ status,
22189
+ statusUpdatedAt: file.statusUpdatedAt !== void 0 && file.statusUpdatedAt !== null && Number.isFinite(at) ? at : null
22190
+ };
22191
+ }
22192
+ function listPanesFailure(session, result2) {
22193
+ const said = (result2.stderr ?? "").trim().split("\n")[0]?.trim();
22194
+ if (result2.status === 127) return `zellij is not installed or not on PATH (exit 127)${said ? `: ${said}` : ""}`;
22195
+ if (result2.status === 124) return `zellij list-panes for session "${session}" timed out (exit 124): the zellij server may be wedged`;
22196
+ if (said && /not found|no active session|no session/i.test(said)) return `zellij has no session "${session}" (exit ${result2.status}): ${said}`;
22197
+ return `zellij list-panes for session "${session}" failed (exit ${result2.status})${said ? `: ${said}` : ""}`;
22198
+ }
22199
+ function readPaneTable(session, shell = defaultShell) {
22200
+ const panes = /* @__PURE__ */ new Map();
22201
+ const result2 = runListPanes(session, shell);
22202
+ if (result2.status !== 0) return { ok: false, error: listPanesFailure(session, result2), panes };
22203
+ const listed = parseListPanes(result2.stdout);
22204
+ if (!listed) return { ok: false, error: `zellij list-panes for session "${session}" printed no pane list`, panes };
22205
+ if (!listed.length) return { ok: false, error: `zellij list-panes for session "${session}" listed no panes at all: not a live session`, panes };
22206
+ for (const pane of listed) {
22207
+ if (pane.isPlugin || !Number.isInteger(pane.id)) continue;
22208
+ panes.set(pane.id, { tabId: pane.tabId ?? null, tabPosition: pane.tabPosition ?? null, tabName: pane.tabName, title: pane.title });
22209
+ }
22210
+ return { ok: true, error: null, panes };
22211
+ }
22212
+ var IDENTITY_BUDGET_MS2 = 2e4;
22213
+ function paneTerminal(inPane, serverParent) {
22214
+ const live = inPane.filter((p) => p.stat.state !== "Z");
22215
+ const pidsInPane = new Set(live.map((p) => p.pid));
22216
+ const roots = live.filter((p) => !pidsInPane.has(p.stat.ppid));
22217
+ const underServer = roots.filter((p) => serverParent(p.stat.ppid));
22218
+ const ttys = new Set((underServer.length ? underServer : roots).map((p) => p.stat.ttyNr).filter((tty) => tty > 0));
22219
+ return ttys.size === 1 ? [...ttys][0] : null;
22220
+ }
22221
+ function chooseCandidate(candidates) {
22222
+ const start = (c) => Number(c.proc.stat.startTime);
22223
+ const trusted = (c) => c.claude?.sessionId ? 1 : 0;
22224
+ const background = (c) => c.claude?.kind === "bg" ? 1 : 0;
22225
+ return [...candidates].sort(
22226
+ (a, b) => Number(b.foreground) - Number(a.foreground) || background(a) - background(b) || (a.foreground && b.foreground ? trusted(b) - trusted(a) : 0) || start(b) - start(a) || b.proc.pid - a.proc.pid
22227
+ )[0];
22228
+ }
22229
+ function inventoryPanes(options = {}) {
22230
+ const fromOption = options.session?.trim();
22231
+ const fromEnv = process.env.ZELLIJ_SESSION_NAME?.trim();
22232
+ const session = fromOption || fromEnv || ZCRANK_SESSION;
22233
+ const sessionSource = fromOption ? "option" : fromEnv ? "ZELLIJ_SESSION_NAME" : "default";
22234
+ const shell = options.shell ?? defaultShell;
22235
+ const sessionsDir = options.sessionsDir ?? claudeSessionsDir();
22236
+ const scanner = options.scanner ?? new ProcScanner();
22237
+ const identityCache = options.identityCache ?? /* @__PURE__ */ new Map();
22238
+ const budget = { totalMs: IDENTITY_BUDGET_MS2 };
22239
+ const procs = scanner.scan(session);
22240
+ const table = readPaneTable(session, shell);
22241
+ if (!table.ok) {
22242
+ return { session, sessionSource, zellijReadable: false, error: table.error, unmappedPaneIds: [], panes: [] };
22243
+ }
22244
+ const taggedByPane = /* @__PURE__ */ new Map();
22245
+ for (const proc of procs) {
22246
+ const list = taggedByPane.get(proc.paneId);
22247
+ if (list) list.push(proc);
22248
+ else taggedByPane.set(proc.paneId, [proc]);
22249
+ }
22250
+ const serverMemo = /* @__PURE__ */ new Map();
22251
+ const isZellijServer = (pid) => {
22252
+ let known = serverMemo.get(pid);
22253
+ if (known === void 0) {
22254
+ const { exe, argv } = readArgv(pid);
22255
+ known = exe === "zellij" && argv.includes(" --server");
22256
+ serverMemo.set(pid, known);
22257
+ }
22258
+ return known;
22259
+ };
22260
+ const byPane = /* @__PURE__ */ new Map();
22261
+ for (const [paneId, inPane] of taggedByPane) {
22262
+ const tty = paneTerminal(inPane, isZellijServer);
22263
+ for (const proc of inPane) {
22264
+ if (proc.stat.state === "Z") continue;
22265
+ const agent = agentCli(proc.exe, proc.argv);
22266
+ if (!agent) continue;
22267
+ const claude = agent === "claude" ? readClaudeSession(proc.pid, proc.stat, sessionsDir) : null;
22268
+ const foreground = tty !== null && proc.stat.ttyNr === tty && proc.stat.tpgid > 0 && proc.stat.pgrp === proc.stat.tpgid;
22269
+ const candidate = { proc, agent, foreground, claude };
22270
+ const list = byPane.get(paneId);
22271
+ if (list) list.push(candidate);
22272
+ else byPane.set(paneId, [candidate]);
22273
+ }
22274
+ }
22275
+ const rows = [];
22276
+ const unmappedPaneIds = [];
22277
+ for (const [paneId, candidates] of byPane) {
22278
+ if (!table.panes.has(paneId)) {
22279
+ unmappedPaneIds.push(paneId);
22280
+ continue;
22281
+ }
22282
+ const chosen = chooseCandidate(candidates);
22283
+ const entry = table.panes.get(paneId);
22284
+ const cwd = chosen.proc.cwd;
22285
+ const info = chosen.claude ?? { kind: null, sessionId: null, reason: "unresolved", status: "unknown", statusUpdatedAt: null };
22286
+ rows.push({
22287
+ position: entry?.tabPosition ?? Number.MAX_SAFE_INTEGER,
22288
+ row: {
22289
+ session,
22290
+ tabId: entry?.tabId ?? null,
22291
+ tabName: entry?.tabName ?? null,
22292
+ paneId,
22293
+ paneTitle: entry?.title ?? null,
22294
+ pid: chosen.proc.pid,
22295
+ agent: chosen.agent,
22296
+ cwd,
22297
+ project: (cwd ? projectIdentity(cwd, { shell, cache: identityCache, budget }) : void 0) ?? null,
22298
+ sessionId: info.sessionId,
22299
+ sessionReason: info.sessionId ? null : info.reason,
22300
+ status: info.status,
22301
+ statusUpdatedAt: info.statusUpdatedAt,
22302
+ nested: candidates.filter((c) => c !== chosen).map((c) => ({ pid: c.proc.pid, agent: c.agent })).sort((a, b) => a.pid - b.pid)
22303
+ }
22304
+ });
22305
+ }
22306
+ if (options.all) {
22307
+ for (const [paneId, entry] of table.panes) {
22308
+ if (byPane.has(paneId)) continue;
22309
+ rows.push({
22310
+ position: entry.tabPosition ?? Number.MAX_SAFE_INTEGER,
22311
+ row: {
22312
+ session,
22313
+ tabId: entry.tabId,
22314
+ tabName: entry.tabName,
22315
+ paneId,
22316
+ paneTitle: entry.title,
22317
+ pid: null,
22318
+ agent: null,
22319
+ cwd: null,
22320
+ project: null,
22321
+ sessionId: null,
22322
+ sessionReason: null,
22323
+ status: "unknown",
22324
+ statusUpdatedAt: null,
22325
+ nested: []
22326
+ }
22327
+ });
22328
+ }
22329
+ }
22330
+ rows.sort((a, b) => a.position - b.position || a.row.paneId - b.row.paneId);
22331
+ return { session, sessionSource, zellijReadable: true, error: null, unmappedPaneIds: unmappedPaneIds.sort((a, b) => a - b), panes: rows.map((r) => r.row) };
22332
+ }
22333
+ function tilde(path) {
22334
+ if (!path) return "-";
22335
+ const home = homedir16();
22336
+ return path === home ? "~" : path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
22337
+ }
22338
+ function displayWidth(text2) {
22339
+ let width = 0;
22340
+ for (const { segment } of new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(text2)) width += clusterWidth(segment);
22341
+ return width;
22342
+ }
22343
+ var EMOJI_PRESENTATION = /^\p{Emoji_Presentation}/u;
22344
+ var EMOJI_BASE = /^\p{Emoji}/u;
22345
+ var REGIONAL_INDICATOR = /^\p{Regional_Indicator}/u;
22346
+ var ZERO_WIDTH_ONLY = /^[\p{Mn}\p{Me}\p{Cf}\p{Cc}]+$/u;
22347
+ function clusterWidth(cluster) {
22348
+ if (ZERO_WIDTH_ONLY.test(cluster)) return 0;
22349
+ const cp = cluster.codePointAt(0);
22350
+ if (REGIONAL_INDICATOR.test(cluster)) return [...cluster].length >= 2 ? 2 : 1;
22351
+ if (EMOJI_PRESENTATION.test(cluster)) return 2;
22352
+ if (cluster.includes("\uFE0F") && EMOJI_BASE.test(cluster)) return 2;
22353
+ const wide = cp >= 4352 && cp <= 4447 || // Hangul Jamo
22354
+ cp >= 11904 && cp <= 42191 && cp !== 12351 || // CJK, kana, Yi
22355
+ cp >= 44032 && cp <= 55203 || // Hangul syllables
22356
+ cp >= 63744 && cp <= 64255 || // CJK compatibility
22357
+ cp >= 65072 && cp <= 65135 || // CJK compatibility forms
22358
+ cp >= 65280 && cp <= 65376 || // fullwidth forms
22359
+ cp >= 65504 && cp <= 65510 || cp >= 131072 && cp <= 262141;
22360
+ return wide ? 2 : 1;
22361
+ }
22362
+ function formatPaneTable(inventory) {
22363
+ const header = ["TAB", "PANE", "PID", "AGENT", "PROJECT", "STATUS", "SESSION", "NESTED"];
22364
+ const rows = inventory.panes.map((p) => [
22365
+ p.tabName ?? "-",
22366
+ String(p.paneId),
22367
+ p.pid === null ? "-" : String(p.pid),
22368
+ p.agent ?? "(no agent)",
22369
+ tilde(p.project ?? p.cwd),
22370
+ p.agent ? p.status : "-",
22371
+ p.sessionId ?? (p.agent ? `- (${p.sessionReason})` : "-"),
22372
+ p.nested.length ? p.nested.map((n) => `${n.agent}:${n.pid}`).join(",") : "-"
22373
+ ]);
22374
+ const widths = header.map((h, i) => Math.max(displayWidth(h), ...rows.map((r) => displayWidth(r[i] ?? ""))));
22375
+ const line = (cells) => cells.map((c, i) => c + " ".repeat(Math.max(0, (widths[i] ?? 0) - displayWidth(c)))).join(" ").trimEnd();
22376
+ return [line(header), ...rows.map(line)].join("\n");
22377
+ }
22378
+
21457
22379
  // src/crank/cli.ts
21458
22380
  var loadCrankScreens = () => loadScreen("./crank/tui.js", import.meta.url);
21459
22381
  var xmark = `${red(glyph.fail)}`;
@@ -21470,13 +22392,20 @@ function currentProjectNames() {
21470
22392
  names: {
21471
22393
  name: projectNameFor(handle, record3?.name),
21472
22394
  agents: projectAgentsFor(handle, record3?.agents)
21473
- }
22395
+ },
22396
+ // PJAN-201: where the project lives (its main worktree), independent of any tab name.
22397
+ identity: projectIdentity(handle.root)
21474
22398
  };
21475
22399
  }
22400
+ function contextFor(project, store) {
22401
+ const keys = storedTabKeys(store);
22402
+ const roots = storedTabRoots(store, keys);
22403
+ return buildCrankContext(project.names, liveTabsWithOwners({ claimed: Object.values(roots) }), keys, { identity: project.identity, roots });
22404
+ }
21476
22405
  function resolveCurrentTab() {
21477
22406
  const project = currentProjectNames();
21478
22407
  const store = redis();
21479
- const ctx = buildCrankContext(project.names, liveTabs(), storedTabKeys(store));
22408
+ const ctx = contextFor(project, store);
21480
22409
  return { slug: project.slug, tab: ctx.resolution.tab, storedAsIs: ctx.resolution.storedAsIs };
21481
22410
  }
21482
22411
  function registerCrankCli(program2) {
@@ -21490,6 +22419,9 @@ function registerCrankCli(program2) {
21490
22419
  crankCmd.command("tui").description("Full-screen crank dashboard: toggle, interval, last runs, countdown to the next run, log").action(async () => {
21491
22420
  await runTui();
21492
22421
  });
22422
+ crankCmd.command("panes").description("Read-only inventory of the live agent panes in this zellij session: pane -> pid -> agent CLI -> session id/status (from /proc, not tab names)").option("--json", "Emit the inventory as JSON").option("--all", "Also list terminal panes that run no agent").action((options) => {
22423
+ runPanes(options);
22424
+ });
21493
22425
  crankCmd.command("list").description("Every registry project with crank ON/OFF and its resolved tab, plus orphan zcrank keys").action(() => {
21494
22426
  try {
21495
22427
  const store = redis();
@@ -21507,12 +22439,16 @@ function registerCrankCli(program2) {
21507
22439
  role: String(value?.role ?? "")
21508
22440
  }))
21509
22441
  }));
21510
- const live = liveTabs();
21511
22442
  const storedTabs = storedTabKeys(store);
22443
+ const live = liveTabsWithOwners({ claimed: Object.values(storedTabRoots(store, storedTabs)) });
22444
+ const identityCache = /* @__PURE__ */ new Map();
21512
22445
  const report = buildCrankList({
21513
22446
  projects,
21514
22447
  live,
21515
22448
  storedTabs,
22449
+ identityOf: (repoPath) => projectIdentity(repoPath, { cache: identityCache }),
22450
+ roots: storedTabRoots(store, storedTabs),
22451
+ engine: readEngineStatus(),
21516
22452
  tabEnabled: (tab) => store.tabEnabled(tab),
21517
22453
  tabInterval: (tab) => store.tabInterval(tab),
21518
22454
  globalInterval: store.globalInterval()
@@ -21576,9 +22512,9 @@ async function runEnable(options) {
21576
22512
  }
21577
22513
  const project = currentProjectNames();
21578
22514
  const store = liveStore();
21579
- const ctx = buildCrankContext(project.names, liveTabs(), storedTabKeys(store));
22515
+ const ctx = contextFor(project, store);
21580
22516
  if (!chosen && isInteractive()) {
21581
- const snap = snapshotCrank(store, ctx.resolution.tab, ctx.storedKeys, Date.now());
22517
+ const snap = snapshotCrank(store, ctx.resolution.tab, ctx.storedKeys, Date.now(), 5, keyScopeOf(ctx));
21582
22518
  const screens = await loadCrankScreens();
21583
22519
  const picked = await screens.pickInterval({ slug: project.slug, tab: ctx.resolution.liveTab ?? ctx.resolution.tab, effective: snap.interval, inherited: snap.inherited, global: snap.global });
21584
22520
  if (!picked) {
@@ -21592,7 +22528,8 @@ async function runEnable(options) {
21592
22528
  for (const { from, to } of result2.renamed) {
21593
22529
  console.log(` ${yellow(glyph.warn)} tab renamed ${dim(glyph.dot)} ${from} ${dim(glyph.pointer)} ${bold(to)} ${dim("(old key merged into the new one and removed)")}`);
21594
22530
  }
21595
- const live = snapshotCrank(store, result2.tab, storedTabKeys(store), Date.now());
22531
+ const afterKeys = storedTabKeys(store);
22532
+ const live = snapshotCrank(store, result2.tab, afterKeys, Date.now(), 5, { owner: ctx.resolution.tabOwner, roots: storedTabRoots(store, afterKeys) });
21596
22533
  console.log(` ${dim("interval")} ${live.interval}${live.inherited ? dim(` (inherits global ${live.global})`) : ""}`);
21597
22534
  if (ctx.resolution.storedAsIs) {
21598
22535
  console.log(` ${yellow(glyph.warn)} ${dim(`no live zellij tab or zcrank key matches "${project.names.name}" \u2014 stored under the project name as-is`)}`);
@@ -21605,8 +22542,8 @@ function runDisable() {
21605
22542
  try {
21606
22543
  const project = currentProjectNames();
21607
22544
  const store = liveStore();
21608
- const ctx = buildCrankContext(project.names, liveTabs(), storedTabKeys(store));
21609
- disableCrank(store, ctx.resolution.tab);
22545
+ const ctx = contextFor(project, store);
22546
+ disableCrank(store, ctx.resolution.tab, /* @__PURE__ */ new Date(), ctx.identity);
21610
22547
  console.log(` ${dim("OFF")} ${cyan(project.slug)} ${dim(glyph.pointer)} ${bold(ctx.resolution.tab)}`);
21611
22548
  if (ctx.resolution.storedAsIs) {
21612
22549
  console.log(` ${yellow(glyph.warn)} ${dim(`no live zellij tab or zcrank key matches "${project.names.name}" \u2014 stored under the project name as-is`)}`);
@@ -21615,6 +22552,34 @@ function runDisable() {
21615
22552
  fail(error, "crank");
21616
22553
  }
21617
22554
  }
22555
+ function runPanes(options) {
22556
+ try {
22557
+ const inventory = inventoryPanes({ all: options.all === true });
22558
+ const sessionNote = inventory.sessionSource === "default" ? `not inside zellij ($ZELLIJ_SESSION_NAME is unset): using the default session "${inventory.session}"` : inventory.sessionSource === "ZELLIJ_SESSION_NAME" ? `session "${inventory.session}" (from $ZELLIJ_SESSION_NAME)` : `session "${inventory.session}"`;
22559
+ if (options.json) {
22560
+ console.log(JSON.stringify(inventory, null, 2));
22561
+ }
22562
+ if (!inventory.zellijReadable) {
22563
+ console.error(`${xmark} ${inventory.error ?? "zellij is unreadable"} \u2014 ${sessionNote}`);
22564
+ process.exit(1);
22565
+ }
22566
+ if (options.json) {
22567
+ if (inventory.unmappedPaneIds.length) console.error(` ${dim(`${inventory.unmappedPaneIds.length} pane id(s) with agent processes are not listed by zellij: ${inventory.unmappedPaneIds.join(", ")}`)}`);
22568
+ return;
22569
+ }
22570
+ if (inventory.sessionSource === "default") console.error(` ${yellow(glyph.warn)} ${dim(sessionNote)}`);
22571
+ if (inventory.unmappedPaneIds.length) {
22572
+ console.error(` ${dim(`ignored ${inventory.unmappedPaneIds.length} pane id(s) with agent processes that zellij does not list (a closed pane, a pane that closed during the scan, or a stale daemon environment): ${inventory.unmappedPaneIds.join(", ")}`)}`);
22573
+ }
22574
+ if (!inventory.panes.length) {
22575
+ console.log(` ${dim(`no live agent panes in session ${inventory.session}`)}`);
22576
+ return;
22577
+ }
22578
+ console.log(formatPaneTable(inventory));
22579
+ } catch (error) {
22580
+ fail(error, "crank panes");
22581
+ }
22582
+ }
21618
22583
  async function runTui() {
21619
22584
  try {
21620
22585
  if (!isInteractive()) {
@@ -21623,7 +22588,7 @@ async function runTui() {
21623
22588
  }
21624
22589
  const project = currentProjectNames();
21625
22590
  const store = liveStore();
21626
- const controller = createCrankController({ slug: project.slug, project: project.names, store });
22591
+ const controller = createCrankController({ slug: project.slug, project: project.names, store, identity: project.identity });
21627
22592
  const screens = await loadCrankScreens();
21628
22593
  await screens.runCrankDashboard(controller);
21629
22594
  } catch (error) {
@@ -21658,9 +22623,9 @@ async function promptForRuleIds(rules) {
21658
22623
  return selected;
21659
22624
  }
21660
22625
  function readJson2(path) {
21661
- if (!existsSync27(path)) return void 0;
22626
+ if (!existsSync26(path)) return void 0;
21662
22627
  try {
21663
- const parsed = JSON.parse(readFileSync30(path, "utf8"));
22628
+ const parsed = JSON.parse(readFileSync32(path, "utf8"));
21664
22629
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
21665
22630
  } catch {
21666
22631
  return void 0;
@@ -21677,9 +22642,9 @@ function packageNameToProjectName(value) {
21677
22642
  return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
21678
22643
  }
21679
22644
  function deriveProjectDefaults(targetDir) {
21680
- const manifest = readJson2(join35(targetDir, ".project.json"));
21681
- const pkg = readJson2(join35(targetDir, "package.json"));
21682
- const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename11(targetDir)) || "Project";
22645
+ const manifest = readJson2(join37(targetDir, ".project.json"));
22646
+ const pkg = readJson2(join37(targetDir, "package.json"));
22647
+ const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename12(targetDir)) || "Project";
21683
22648
  const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
21684
22649
  return {
21685
22650
  name,
@@ -21753,7 +22718,7 @@ function actionNeedsRun(plan, kind, syncMode) {
21753
22718
  if (!action || action.kind !== "project.write-manifest") return false;
21754
22719
  const next = `${JSON.stringify(action.manifest, null, 2)}
21755
22720
  `;
21756
- return !existsSync27(action.path) || readFileSync30(action.path, "utf8") !== next;
22721
+ return !existsSync26(action.path) || readFileSync32(action.path, "utf8") !== next;
21757
22722
  }
21758
22723
  if (kind === "copier.copy.commonproject") return true;
21759
22724
  if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
@@ -21810,28 +22775,28 @@ async function resolveProjectInitTarget(name, options) {
21810
22775
  if (!targetDir && cwdGitRoot) {
21811
22776
  targetDir = cwdGitRoot;
21812
22777
  }
21813
- if (!targetDir && !name && existsSync27(join35(cwd, ".project.json"))) {
22778
+ if (!targetDir && !name && existsSync26(join37(cwd, ".project.json"))) {
21814
22779
  targetDir = cwd;
21815
22780
  }
21816
22781
  if (!targetDir && interactive) {
21817
- const promptedName = await promptTextValue("Project name", packageNameToProjectName(basename11(cwd)));
22782
+ const promptedName = await promptTextValue("Project name", packageNameToProjectName(basename12(cwd)));
21818
22783
  targetDir = resolve25(await promptTextValue("Project directory", projectTargetDirUnder(promptedName, cwd)));
21819
22784
  name = promptedName;
21820
22785
  }
21821
22786
  if (!targetDir) {
21822
22787
  throw new Error("Project name or --target-dir is required when project init is not run inside a git repo or a directory carrying .project.json");
21823
22788
  }
21824
- const targetExists = existsSync27(targetDir);
22789
+ const targetExists = existsSync26(targetDir);
21825
22790
  if (targetExists && !statSync9(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
21826
22791
  const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
21827
- const alreadyScaffolded = targetExists && (existsSync27(join35(targetDir, ".project.json")) || existsSync27(join35(targetDir, ".copier-answers.yml")));
22792
+ const alreadyScaffolded = targetExists && (existsSync26(join37(targetDir, ".project.json")) || existsSync26(join37(targetDir, ".copier-answers.yml")));
21828
22793
  const syncMode = Boolean(
21829
22794
  targetGitRoot && resolve25(targetGitRoot) === resolve25(targetDir) || alreadyScaffolded
21830
22795
  );
21831
- if (targetExists && !syncMode && !options.force && readdirSync13(targetDir).length > 0) {
22796
+ if (targetExists && !syncMode && !options.force && readdirSync14(targetDir).length > 0) {
21832
22797
  throw new Error(`Target already exists and is not empty: ${targetDir} (pass --force to render over it)`);
21833
22798
  }
21834
- const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(basename11(targetDir)) ?? "Project", description: "" };
22799
+ const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(basename12(targetDir)) ?? "Project", description: "" };
21835
22800
  if (!name && interactive && !syncMode) {
21836
22801
  name = await promptTextValue("Project name", defaults.name);
21837
22802
  }
@@ -22346,7 +23311,7 @@ program.command("reindex").argument("[project-id]", "Canonical project ID (defau
22346
23311
  else if (options.all) result2 = registryRequest(location, "POST", "/v1/reindex", {});
22347
23312
  else {
22348
23313
  const repo = projectId ? getProject(loadProjectRegistry(location), projectId).repo_path : resolveProject(process.cwd()).root;
22349
- result2 = registryRequest(location, "POST", "/v1/index", { manifest_path: join35(repo, ".project.json") });
23314
+ result2 = registryRequest(location, "POST", "/v1/index", { manifest_path: join37(repo, ".project.json") });
22350
23315
  }
22351
23316
  const statuses = result2.__registry_status;
22352
23317
  const stale = Object.entries(statuses ?? {}).filter(([, item]) => item.status !== "ok");