@tekmidian/pai 0.60.1 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/{chain-CGyAIHo4.mjs → chain-C5oeyUg4.mjs} +4 -4
  2. package/dist/{chain-CGyAIHo4.mjs.map → chain-C5oeyUg4.mjs.map} +1 -1
  3. package/dist/cli/index.mjs +9 -9
  4. package/dist/cli/program.mjs +9 -9
  5. package/dist/{context-handover-cache-BzM7Qudn.mjs → context-handover-cache-BdtJZpgv.mjs} +3 -3
  6. package/dist/{context-handover-cache-BzM7Qudn.mjs.map → context-handover-cache-BdtJZpgv.mjs.map} +1 -1
  7. package/dist/daemon/index.mjs +7 -7
  8. package/dist/{daemon-B_TodsIS.mjs → daemon-DSsIEqzW.mjs} +20 -13
  9. package/dist/{daemon-B_TodsIS.mjs.map → daemon-DSsIEqzW.mjs.map} +1 -1
  10. package/dist/{daemon-jviL3VVJ.mjs → daemon-MoFF4oeg.mjs} +7 -7
  11. package/dist/daemon-mcp/index.mjs +7 -9
  12. package/dist/daemon-mcp/index.mjs.map +1 -1
  13. package/dist/{fallback-CjdakQVu.mjs → fallback-DDbJVaR8.mjs} +11 -13
  14. package/dist/{fallback-CjdakQVu.mjs.map → fallback-DDbJVaR8.mjs.map} +1 -1
  15. package/dist/hooks/block-sleep-poll.mjs.map +2 -2
  16. package/dist/hooks/context-compression-hook.mjs.map +1 -1
  17. package/dist/hooks/post-compact-inject.mjs.map +1 -1
  18. package/dist/hooks/route-agents-to-worker.mjs +3 -3
  19. package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
  20. package/dist/hooks/security-validator.mjs +58 -58
  21. package/dist/hooks/security-validator.mjs.map +3 -3
  22. package/dist/hooks/stop-hook.mjs +15 -15
  23. package/dist/hooks/stop-hook.mjs.map +3 -3
  24. package/dist/hooks/update-tab-on-action.mjs +14 -12
  25. package/dist/hooks/update-tab-on-action.mjs.map +3 -3
  26. package/dist/hooks/update-tab-titles.mjs +13 -9
  27. package/dist/hooks/update-tab-titles.mjs.map +3 -3
  28. package/dist/hooks/worker-proxy.mjs +3 -3
  29. package/dist/hooks/worker-proxy.mjs.map +2 -2
  30. package/dist/hooks/worker-status-line.mjs +3 -3
  31. package/dist/hooks/worker-status-line.mjs.map +2 -2
  32. package/dist/hooks/worker-supervision.mjs +3 -3
  33. package/dist/hooks/worker-supervision.mjs.map +2 -2
  34. package/dist/{main-resolver-VMBENifT.mjs → main-resolver-CxOsxtOr.mjs} +2 -2
  35. package/dist/{main-resolver-BFQfU5xo.mjs → main-resolver-DIQ6m_RA.mjs} +2 -2
  36. package/dist/{main-resolver-BFQfU5xo.mjs.map → main-resolver-DIQ6m_RA.mjs.map} +1 -1
  37. package/dist/{planner-DN967Lvm.mjs → planner-D8jAr_sx.mjs} +5 -5
  38. package/dist/{planner-DN967Lvm.mjs.map → planner-D8jAr_sx.mjs.map} +1 -1
  39. package/dist/{program-BnsjB57Q.mjs → program-BTdspOOa.mjs} +102 -52
  40. package/dist/program-BTdspOOa.mjs.map +1 -0
  41. package/dist/{run-FeM1_UVa.mjs → run-CxxZKEZw.mjs} +333 -172
  42. package/dist/run-CxxZKEZw.mjs.map +1 -0
  43. package/dist/{run-env-CEHTeIUM.mjs → run-env-BE5xyCwy.mjs} +25 -6
  44. package/dist/run-env-BE5xyCwy.mjs.map +1 -0
  45. package/dist/{server-CekGOJcp.mjs → server-BY6AedmC.mjs} +2 -2
  46. package/dist/{server-CekGOJcp.mjs.map → server-BY6AedmC.mjs.map} +1 -1
  47. package/dist/{session-keepalive-Bc9aT678.mjs → session-keepalive-urk--pfa.mjs} +4 -4
  48. package/dist/{session-keepalive-Bc9aT678.mjs.map → session-keepalive-urk--pfa.mjs.map} +1 -1
  49. package/dist/{work-queue-worker-DVOVcEnO.mjs → work-queue-worker-1_VBfI_k.mjs} +4 -4
  50. package/dist/{work-queue-worker-BL4dq5P7.mjs → work-queue-worker-DUvPiwGO.mjs} +2 -2
  51. package/dist/{work-queue-worker-BL4dq5P7.mjs.map → work-queue-worker-DUvPiwGO.mjs.map} +1 -1
  52. package/docs/commands/worker.md +1 -0
  53. package/package.json +1 -1
  54. package/src/hooks/ts/lib/tab-title.ts +18 -0
  55. package/src/hooks/ts/post-tool-use/update-tab-on-action.ts +3 -20
  56. package/src/hooks/ts/stop/stop-hook.ts +4 -15
  57. package/src/hooks/ts/user-prompt/update-tab-titles.ts +2 -10
  58. package/dist/program-BnsjB57Q.mjs.map +0 -1
  59. package/dist/run-FeM1_UVa.mjs.map +0 -1
  60. package/dist/run-env-CEHTeIUM.mjs.map +0 -1
@@ -1,9 +1,9 @@
1
1
  import { i as getRegistryBackend } from "./factory-EUUhSSoq.mjs";
2
- import { D as resolveModelCapability, O as resolveProviderKey, T as readWorkersSection, h as classModelCapability, m as assertProviderRunnable, n as claudeCommand, p as WorkersConfigError, t as buildRunEnv, w as providerKeyPath, y as isModelCapability } from "./run-env-CEHTeIUM.mjs";
3
- import { _ as statusPath, d as resolveTarget, f as setCooldown, g as panesDir, h as ledgerPath, i as capabilityForRun, m as eventsPath, n as ensureProxyRunning, o as isQuotaFailure, p as ensureNoMcpConfig, s as nextAutoProvider, t as DEFAULT_PROXY_PORT, u as resolveCapabilityRun, v as workersLogDir } from "./server-CekGOJcp.mjs";
4
- import { appendFileSync, closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
2
+ import { E as readWorkersSection, O as resolveModelCapability, T as providerKeyPath, b as isModelCapability, d as NATIVE_ANTHROPIC_MODELS, g as classModelCapability, h as assertProviderRunnable, k as resolveProviderKey, m as WorkersConfigError, n as claudeCommand, t as buildRunEnv } from "./run-env-BE5xyCwy.mjs";
3
+ import { _ as statusPath, d as resolveTarget, f as setCooldown, g as panesDir, h as ledgerPath, i as capabilityForRun, m as eventsPath, n as ensureProxyRunning, o as isQuotaFailure, p as ensureNoMcpConfig, s as nextAutoProvider, t as DEFAULT_PROXY_PORT, u as resolveCapabilityRun, v as workersLogDir } from "./server-BY6AedmC.mjs";
4
+ import { appendFileSync, closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
5
5
  import { homedir, tmpdir } from "node:os";
6
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import { execFileSync, spawn } from "node:child_process";
8
8
  import { connect, createServer } from "node:net";
9
9
  import { createInterface } from "node:readline";
@@ -336,9 +336,10 @@ function saveStatus(logDir, status, d = /* @__PURE__ */ new Date()) {
336
336
  const path = statusPath(logDir, status.id);
337
337
  const baseline = firstWrittenLabel.get(status.id);
338
338
  if (baseline === void 0) firstWrittenLabel.set(status.id, status.label);
339
- else if (status.label === baseline) {
340
- const onDisk = loadStatus(logDir, status.id);
341
- if (onDisk && onDisk.label !== status.label) status.label = onDisk.label;
339
+ const onDisk = baseline === void 0 ? void 0 : loadStatus(logDir, status.id);
340
+ if (onDisk) {
341
+ if (status.label === baseline && onDisk.label !== status.label) status.label = onDisk.label;
342
+ if (onDisk.controlsHeld) status.controlsHeld = true;
342
343
  }
343
344
  const tmp = statusTmpPath(logDir, status.id);
344
345
  if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
@@ -844,18 +845,58 @@ function uncommittedPaths(wtDir) {
844
845
  return [...tracked, ...untracked];
845
846
  }
846
847
  /**
848
+ * True when `wtDir`/`relPath` is a symlink whose target resolves outside
849
+ * `wtDir` — e.g. a worker's `node_modules -> <main checkout>/node_modules`
850
+ * convenience link. A `.gitignore` using `dir/` (directories only) does not
851
+ * ignore such a link, so it turns up as untracked; staging and merging it
852
+ * lets git replace the real directory on the other side with a symlink to
853
+ * itself. Never a candidate for salvage.
854
+ */
855
+ function symlinkEscapesWorktree(wtDir, relPath) {
856
+ const full = join(wtDir, relPath);
857
+ let stat;
858
+ try {
859
+ stat = lstatSync(full);
860
+ } catch {
861
+ return false;
862
+ }
863
+ if (!stat.isSymbolicLink()) return false;
864
+ const target = readlinkSync(full);
865
+ const resolvedTarget = isAbsolute(target) ? target : resolve(dirname(full), target);
866
+ const realTarget = existsSync(resolvedTarget) ? realpathSync(resolvedTarget) : resolvedTarget;
867
+ const realWtDir = realpathSync(wtDir);
868
+ return realTarget !== realWtDir && !realTarget.startsWith(realWtDir + sep);
869
+ }
870
+ /**
847
871
  * Commit a worktree's uncommitted changes to its branch so the merge carries
848
872
  * them. `git merge` only moves committed work, so a worker that stopped
849
873
  * without committing would lose its edits to `worktree remove` — exactly
850
- * what happened live on 2026-09-17. Returns the salvaged paths, [] when the
851
- * worktree is clean. A failed commit throws with the worktree untouched:
874
+ * what happened live on 2026-09-17. Returns the salvaged paths, empty when
875
+ * the worktree is clean. A symlink pointing outside the worktree (see
876
+ * `symlinkEscapesWorktree`) is never staged; it is left on disk and reported
877
+ * as skipped instead. A failed commit throws with the worktree untouched:
852
878
  * its edits are still on disk, so nothing is lost.
853
879
  */
854
880
  function salvageUncommitted(wtDir, label) {
855
881
  const paths = uncommittedPaths(wtDir);
856
- if (!paths.length) return [];
882
+ if (!paths.length) return {
883
+ committed: [],
884
+ skipped: []
885
+ };
886
+ const skipped = [];
887
+ const toStage = [];
888
+ for (const p of paths) if (symlinkEscapesWorktree(wtDir, p)) skipped.push(`symlink ${p} -> ${readlinkSync(join(wtDir, p))} (points outside the worktree)`);
889
+ else toStage.push(p);
890
+ if (!toStage.length) return {
891
+ committed: [],
892
+ skipped
893
+ };
857
894
  try {
858
- git(wtDir, ["add", "-A"]);
895
+ git(wtDir, [
896
+ "add",
897
+ "--",
898
+ ...toStage
899
+ ]);
859
900
  git(wtDir, [
860
901
  "commit",
861
902
  "-m",
@@ -864,7 +905,10 @@ function salvageUncommitted(wtDir, label) {
864
905
  } catch (e) {
865
906
  throw new Error(`cannot salvage the uncommitted changes in ${wtDir} — ${e.message}; nothing was lost: commit them there by hand, then re-run merge`);
866
907
  }
867
- return paths;
908
+ return {
909
+ committed: toStage,
910
+ skipped
911
+ };
868
912
  }
869
913
  /**
870
914
  * The dirty paths of a checkout, parsed from `git status --porcelain -z`:
@@ -915,9 +959,14 @@ function assertNoDirtyOverlap(cwd, incoming, id, branch) {
915
959
  if (!overlap.length) return;
916
960
  throw new Error(`worker ${id}: the checkout ${cwd} has uncommitted changes in paths ${branch} touches: ${overlap.join(", ")}. Commit or stash them in the checkout, then re-run: pai worker merge ${id}. No merge was made; the worktree was kept.`);
917
961
  }
918
- /** Never remove a worktree that still holds uncommitted changes. */
962
+ /**
963
+ * Never remove a worktree that still holds uncommitted changes — except a
964
+ * symlink escaping the worktree (see `symlinkEscapesWorktree`): it was
965
+ * deliberately left uncommitted by `salvageUncommitted`, and deleting the
966
+ * link itself touches nothing outside the worktree.
967
+ */
919
968
  function assertWorktreeClean(wtDir, id) {
920
- const leftover = uncommittedPaths(wtDir);
969
+ const leftover = uncommittedPaths(wtDir).filter((p) => !symlinkEscapesWorktree(wtDir, p));
921
970
  if (leftover.length) throw new Error(`worker ${id}: the worktree ${wtDir} still holds uncommitted changes (${leftover.join(", ")}) — it was NOT removed; commit or copy them by hand, then re-run merge`);
922
971
  }
923
972
  /** Blob sha of `path` at `rev` in `cwd`, or null when it does not exist there. */
@@ -971,7 +1020,10 @@ function mergeSnapshotWorker(logDir, id, st) {
971
1020
  const snapshot = st.worktreeBase;
972
1021
  const branch = st.branch;
973
1022
  const worktreeDir = st.worktreeDir;
974
- const salvaged = existsSync(worktreeDir) ? salvageUncommitted(worktreeDir, st.label || UNLABELED) : [];
1023
+ const salvage = existsSync(worktreeDir) ? salvageUncommitted(worktreeDir, st.label || UNLABELED) : {
1024
+ committed: [],
1025
+ skipped: []
1026
+ };
975
1027
  if ((parseInt(git(st.cwd, [
976
1028
  "rev-list",
977
1029
  "--count",
@@ -1030,7 +1082,10 @@ function mergeSnapshotWorker(logDir, id, st) {
1030
1082
  merged: true
1031
1083
  });
1032
1084
  const base = `applied worker ${id}'s changes (branch ${branch}) to ${st.cwd} as uncommitted changes: ${changedPaths.join(", ")} (worktree removed, branch deleted)`;
1033
- return salvaged.length ? `${base}; salvaged ${salvaged.length} uncommitted change(s) onto the branch first: ${salvaged.join(", ")}` : base;
1085
+ const notes = [];
1086
+ if (salvage.committed.length) notes.push(`salvaged ${salvage.committed.length} uncommitted change(s) onto the branch first: ${salvage.committed.join(", ")}`);
1087
+ if (salvage.skipped.length) notes.push(`skipped ${salvage.skipped.join(", ")}`);
1088
+ return notes.length ? `${base}; ${notes.join("; ")}` : base;
1034
1089
  }
1035
1090
  /**
1036
1091
  * `pai worker merge <id>`: salvage whatever the worker left uncommitted onto
@@ -1045,7 +1100,10 @@ function mergeWorker(logDir, id) {
1045
1100
  const st = mustHaveBranch(logDir, id);
1046
1101
  if (st.merged) return `worker ${id}: branch ${st.branch} already merged`;
1047
1102
  if (st.worktreeSnapshot) return mergeSnapshotWorker(logDir, id, st);
1048
- const salvaged = existsSync(st.worktreeDir) ? salvageUncommitted(st.worktreeDir, st.label || UNLABELED) : [];
1103
+ const salvage = existsSync(st.worktreeDir) ? salvageUncommitted(st.worktreeDir, st.label || UNLABELED) : {
1104
+ committed: [],
1105
+ skipped: []
1106
+ };
1049
1107
  if ((parseInt(git(st.cwd, [
1050
1108
  "rev-list",
1051
1109
  "--count",
@@ -1075,7 +1133,7 @@ function mergeWorker(logDir, id) {
1075
1133
  throw new Error(`worker ${id}: git refused the merge of ${st.branch} — ${e.message}. The worktree ${st.worktreeDir} was kept: resolve the conflict, then re-run merge`);
1076
1134
  }
1077
1135
  if (existsSync(st.worktreeDir)) assertWorktreeClean(st.worktreeDir, id);
1078
- removeWorktree(st.cwd, st.worktreeDir, false);
1136
+ removeWorktree(st.cwd, st.worktreeDir, true);
1079
1137
  let branchGone = true;
1080
1138
  try {
1081
1139
  git(st.cwd, [
@@ -1091,7 +1149,10 @@ function mergeWorker(logDir, id) {
1091
1149
  merged: true
1092
1150
  });
1093
1151
  const base = `merged ${st.branch} into ${st.cwd} (worktree removed${branchGone ? ", branch deleted" : "; branch kept: git refused -d"})`;
1094
- return salvaged.length ? `${base}; salvaged ${salvaged.length} uncommitted change(s): ${salvaged.join(", ")}` : base;
1152
+ const notes = [];
1153
+ if (salvage.committed.length) notes.push(`salvaged ${salvage.committed.length} uncommitted change(s): ${salvage.committed.join(", ")}`);
1154
+ if (salvage.skipped.length) notes.push(`skipped ${salvage.skipped.join(", ")}`);
1155
+ return notes.length ? `${base}; ${notes.join("; ")}` : base;
1095
1156
  }
1096
1157
  /** `pai worker discard <id>`: drop worktree and branch, keep nothing. */
1097
1158
  function discardWorker(logDir, id) {
@@ -1173,9 +1234,10 @@ function inPlaceSystemPrompt() {
1173
1234
  * If neither is available, viewers fall back to "all workers".
1174
1235
  *
1175
1236
  * A third case: workers spawned from a Claude Code Bash tool (the
1176
- * orchestrator pattern) have neither — Claude Code exports no terminal or
1177
- * session identity to its Bash children. Those record `spawnerSession`, the
1178
- * orchestrator's claude session id bridged through the status line's
1237
+ * orchestrator pattern) inherit the orchestrator's `ITERM_SESSION_ID`, which
1238
+ * identifies the orchestrator's own pane, not a pane for the sub-worker —
1239
+ * so it does not key the sub-worker's tab. Those record `spawnerSession`,
1240
+ * the orchestrator's claude session id bridged through the status line's
1179
1241
  * session map (see below), so their tab still claims them.
1180
1242
  */
1181
1243
  const AIBROKER_REGISTRY = join(homedir(), ".aibroker", "session-names.json");
@@ -1897,7 +1959,10 @@ function absoluteCliPath() {
1897
1959
  }
1898
1960
  /** Open (or only report on) the stacked follow pane of one worker. */
1899
1961
  async function openPaneForWorker(logDir, config, wid, term) {
1900
- if (workerPaneOpen(wid)) return `pane for ${wid} already open`;
1962
+ if (workerPaneOpen(wid)) return {
1963
+ message: `pane for ${wid} already open`,
1964
+ session: null
1965
+ };
1901
1966
  const uid = itermUuid(term);
1902
1967
  const regPath = join(panesDir(logDir), `${scopeKey(term)}.json`);
1903
1968
  const reg = loadRegistry(regPath);
@@ -1921,7 +1986,10 @@ async function openPaneForWorker(logDir, config, wid, term) {
1921
1986
  opened: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
1922
1987
  });
1923
1988
  saveRegistry(regPath, pruned);
1924
- return `pane opened for ${wid}`;
1989
+ return {
1990
+ message: `pane opened for ${wid}`,
1991
+ session: out.slice(bar + 1)
1992
+ };
1925
1993
  }
1926
1994
  /**
1927
1995
  * One `--check` line with the bounds of the window hosting `term`'s iTerm
@@ -2041,6 +2109,34 @@ function launchParent(explicit, env = process.env) {
2041
2109
  function isWorkerId(logDir, id) {
2042
2110
  return loadStatus(logDir, id) !== null;
2043
2111
  }
2112
+ /**
2113
+ * A worker owns its children's results (the worker contract says so): if
2114
+ * `parent` exits leaving sub-workers unaccounted for, its own run must not
2115
+ * report success. Two shapes count, both read off `runningChildren` and the
2116
+ * raw statuses, never a second live-child lookup:
2117
+ *
2118
+ * - still live: left running untouched (killing another process's work
2119
+ * here would be its own bug) but named in the reason.
2120
+ * - marked "running" with a dead pid (the child never got to write its own
2121
+ * terminal state before its parent vanished from under it): finalised
2122
+ * here as failed, so `ps` stops showing a corpse as live.
2123
+ *
2124
+ * Returns the reason string for the parent's own report, or null when every
2125
+ * child it started is accounted for.
2126
+ */
2127
+ function checkOrphanedChildren(logDir, parent) {
2128
+ const statuses = loadStatuses(logDir);
2129
+ const live = runningChildren(statuses, parent);
2130
+ const abandoned = statuses.filter((s) => s.parent === parent && s.state === "running" && !isLive(s));
2131
+ for (const child of abandoned) saveStatus(logDir, {
2132
+ ...child,
2133
+ state: "failed",
2134
+ last: `parent ${parent} exited`
2135
+ });
2136
+ const ids = [...live, ...abandoned].map((s) => s.id);
2137
+ if (!ids.length) return null;
2138
+ return `exited with ${ids.length} sub-worker${ids.length === 1 ? "" : "s"} still running (${ids.join(", ")}); they were not waited for — run children in the foreground or wait for them before finishing`;
2139
+ }
2044
2140
 
2045
2141
  //#endregion
2046
2142
  //#region src/workers/operator.ts
@@ -2283,6 +2379,16 @@ function ag2Spec() {
2283
2379
  return cachedSpec;
2284
2380
  }
2285
2381
  /**
2382
+ * Strip what workers wrap around a report: one surrounding ``` fence (opening
2383
+ * line may carry a language tag) and a leading `[YYYY-MM-DD HH:MM]` stamp line.
2384
+ */
2385
+ function unwrapAg2(text) {
2386
+ let t = (text ?? "").trim();
2387
+ const fenced = t.match(/^```[^\n]*\n([\s\S]*?)\n?```$/);
2388
+ if (fenced) t = fenced[1].trim();
2389
+ return t.replace(/^\[\d{4}-\d{2}-\d{2}[^\]\n]*\][ \t]*\n/, "").trim();
2390
+ }
2391
+ /**
2286
2392
  * Parse one AG2 message: first non-empty line is the kind (T R S Q A X,
2287
2393
  * optionally followed by trailing text — only the first character is the
2288
2394
  * kind), later lines are `key=value` or `@n=path` symbol declarations.
@@ -2298,7 +2404,7 @@ function parseAg2Report(text) {
2298
2404
  "A",
2299
2405
  "X"
2300
2406
  ]);
2301
- const lines = (text ?? "").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("```"));
2407
+ const lines = unwrapAg2(text).split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("```"));
2302
2408
  if (!lines.length) return null;
2303
2409
  const kind = lines[0][0];
2304
2410
  if (!kinds.has(kind)) return null;
@@ -2342,11 +2448,17 @@ function ag2ToWorkerReport(parsed) {
2342
2448
  summary: entry.slice(cut + 1).trim()
2343
2449
  };
2344
2450
  });
2345
- if (f.t) out.checks = f.t.split(/\s+/).filter(Boolean).map((entry) => ({
2346
- name: entry.slice(0, -1),
2347
- ok: passed(entry),
2348
- detail: entry.slice(-1)
2349
- }));
2451
+ if (f.t) {
2452
+ const tokens = f.t.split(/\s+/).filter(Boolean);
2453
+ out.checks = tokens.length > 0 && tokens.every((tok) => /^\S+[+\-~?!]$/.test(tok)) ? tokens.map((entry) => ({
2454
+ name: entry.slice(0, -1),
2455
+ ok: passed(entry),
2456
+ detail: entry.slice(-1)
2457
+ })) : [{
2458
+ name: "tests",
2459
+ detail: f.t
2460
+ }];
2461
+ }
2350
2462
  if (f.p) out.commands = f.p.split("|").map((s) => s.trim()).filter(Boolean);
2351
2463
  const open = [];
2352
2464
  if (f.x) open.push(f.x);
@@ -2372,7 +2484,7 @@ function validateAg2(text) {
2372
2484
  "-",
2373
2485
  "--json"
2374
2486
  ], {
2375
- input: text,
2487
+ input: unwrapAg2(text),
2376
2488
  timeout: 5e3,
2377
2489
  stdio: [
2378
2490
  "pipe",
@@ -2600,7 +2712,7 @@ function renderReport(c, prefix, r, cwd = "") {
2600
2712
  if (has(r.checks)) {
2601
2713
  out.push(`${prefix}${c("bold", "checks")}`);
2602
2714
  for (const ck of r.checks) {
2603
- const mark = ck.ok === false ? c("red", "✗") : c("green", "✓");
2715
+ const mark = ck.ok === false ? c("red", "✗") : ck.ok === true ? c("green", "✓") : c("dim", "·");
2604
2716
  const detail = ck.detail ? c("dim", ` · ${shortText(ck.detail, 80)}`) : "";
2605
2717
  out.push(`${prefix} ${mark} ${ck.name ?? "?"}${detail}`);
2606
2718
  }
@@ -3012,16 +3124,22 @@ function toolResultFailed(block) {
3012
3124
  const m = toolResultText(block.content).match(/Exit code (\d+)/);
3013
3125
  return m !== null && Number(m[1]) !== 0;
3014
3126
  }
3127
+ /** CLI aliases the built-in provider hands to `--model` (NATIVE_ANTHROPIC_MODELS'
3128
+ * values): a bare name like "sonnet", not a concrete id, so the init event's
3129
+ * resolved id is always the more useful thing to display. */
3130
+ const NATIVE_MODEL_ALIASES = new Set(Object.values(NATIVE_ANTHROPIC_MODELS));
3015
3131
  /**
3016
3132
  * The model the init event announces, adopted into the status when the spawn
3017
3133
  * could not name one (a caller that passed `--model` itself in the claude
3018
- * args, or a provider whose table has no entry for the capability). The run's
3019
- * real model is then only knowable from the first event it sends. Never
3020
- * overwrites an explicit model — a resolved or pinned model stays the
3134
+ * args, or a provider whose table has no entry for the capability) or named
3135
+ * only a CLI alias (the built-in provider's "sonnet"/"haiku", which tracks
3136
+ * whatever release is current — the init event says which one actually ran).
3137
+ * The run's real model is then only knowable from the first event it sends.
3138
+ * Never overwrites a concrete resolved or pinned model — that stays the
3021
3139
  * recorded truth.
3022
3140
  */
3023
3141
  function adoptInitModel(status, e) {
3024
- if (status.model) return;
3142
+ if (status.model && !NATIVE_MODEL_ALIASES.has(status.model)) return;
3025
3143
  const m = (e.model ?? "").trim();
3026
3144
  if (m) status.model = m;
3027
3145
  }
@@ -3116,7 +3234,7 @@ async function runWorker(opts) {
3116
3234
  const logDir = workersLogDir(config);
3117
3235
  mkdirSync(logDir, { recursive: true });
3118
3236
  if (opts.className === "plan" && !opts._planner) {
3119
- const { runPlanner } = await import("./planner-DN967Lvm.mjs");
3237
+ const { runPlanner } = await import("./planner-D8jAr_sx.mjs");
3120
3238
  return runPlanner(opts);
3121
3239
  }
3122
3240
  const parent = launchParent(opts.parent);
@@ -3320,6 +3438,64 @@ function interactiveMcpTools(explicitMcp, callerTools, projectLaunch) {
3320
3438
  tools: !callerTools && projectLaunch?.tools?.length ? projectLaunch.tools : []
3321
3439
  };
3322
3440
  }
3441
+ /**
3442
+ * The finalisation every engine shares: orphaned-children check, state, the
3443
+ * WORKER-END ledger line, worktree outcome, parent handoff, final rc. A worker
3444
+ * owns its children's results (the worker contract says so): if it
3445
+ * backgrounded a sub-worker and exited without waiting, the run must not
3446
+ * report success, whatever the process returned.
3447
+ */
3448
+ async function finaliseRun(f) {
3449
+ const { logDir, ledger, ok, rc } = f;
3450
+ let { status, report } = f;
3451
+ const wid = status.id;
3452
+ const orphanReason = checkOrphanedChildren(logDir, wid);
3453
+ const succeeded = ok && !orphanReason;
3454
+ status.state = succeeded ? "done" : "failed";
3455
+ status.rc = rc;
3456
+ status.secs = f.secs;
3457
+ if (orphanReason) {
3458
+ status.last = shortText(orphanReason, 90);
3459
+ report = {
3460
+ ...report ?? {},
3461
+ notes: orphanReason,
3462
+ result: "-"
3463
+ };
3464
+ appendLedger(ledger, "WORKER-ORPHAN-CHILDREN", {
3465
+ id: wid,
3466
+ reason: orphanReason
3467
+ });
3468
+ }
3469
+ saveStatus(logDir, status);
3470
+ appendLedger(ledger, "WORKER-END", {
3471
+ id: wid,
3472
+ ...f.end,
3473
+ rc,
3474
+ secs: f.secs
3475
+ });
3476
+ if (f.worktree) status = recordWorktree(logDir, status, f.worktree, ok);
3477
+ const early = await f.beforeHandoff?.(status, report);
3478
+ if (typeof early === "number") return early;
3479
+ const finalRc = rc !== 0 ? rc : succeeded ? 0 : 1;
3480
+ if (status.parent && isWorkerId(logDir, status.parent)) try {
3481
+ await deliverHandoff(logDir, {
3482
+ from: wid,
3483
+ to: status.parent,
3484
+ kind: "result",
3485
+ text: shortText(report?.notes ?? f.fallbackText ?? (succeeded ? "done" : "failed"), 400),
3486
+ data: {
3487
+ rc: finalRc,
3488
+ ok: succeeded,
3489
+ ...status.branch ? {
3490
+ branch: status.branch,
3491
+ commits: status.commits ?? 0
3492
+ } : {},
3493
+ ...report ? { report } : {}
3494
+ }
3495
+ });
3496
+ } catch {}
3497
+ return finalRc;
3498
+ }
3323
3499
  async function executeImageRun(a) {
3324
3500
  const { logDir, target, model, label, parsed } = a;
3325
3501
  if (!parsed.headless || parsed.prompt === null) throw new Error(`provider "${target.providerName}" (engine image) supports headless runs only: pass the task with -p '<prompt>'`);
@@ -3366,6 +3542,14 @@ async function executeImageRun(a) {
3366
3542
  capability: a.capability,
3367
3543
  ...a.specPath ? { spec: a.specPath } : {}
3368
3544
  });
3545
+ const imageEnd = {
3546
+ provider: target.providerName,
3547
+ mode: "headless",
3548
+ engine: "image",
3549
+ model,
3550
+ label,
3551
+ capability: a.capability
3552
+ };
3369
3553
  const t0 = Date.now();
3370
3554
  try {
3371
3555
  const result = await runImageCapability({
@@ -3378,46 +3562,61 @@ async function executeImageRun(a) {
3378
3562
  timeoutMs: a.timeoutMs
3379
3563
  });
3380
3564
  const secs = Math.round((Date.now() - t0) / 1e3);
3381
- status.state = "done";
3382
- status.rc = 0;
3383
- status.secs = secs;
3384
3565
  status.last = `wrote ${result.path}`;
3385
- saveStatus(logDir, status);
3386
- appendLedger(ledger, "WORKER-END", {
3387
- id: wid,
3388
- provider: target.providerName,
3389
- mode: "headless",
3390
- engine: "image",
3391
- model,
3566
+ return await finaliseRun({
3567
+ logDir,
3568
+ ledger,
3569
+ status,
3570
+ ok: true,
3392
3571
  rc: 0,
3393
3572
  secs,
3394
- label,
3395
- capability: a.capability
3573
+ end: imageEnd,
3574
+ report: null,
3575
+ beforeHandoff: () => {
3576
+ if (!a.quiet) process.stdout.write(JSON.stringify(result) + "\n");
3577
+ }
3396
3578
  });
3397
- if (!a.quiet) process.stdout.write(JSON.stringify(result) + "\n");
3398
- return 0;
3399
3579
  } catch (e) {
3400
3580
  const secs = Math.round((Date.now() - t0) / 1e3);
3401
- const message = e instanceof Error ? e.message : String(e);
3402
- status.state = "failed";
3403
- status.rc = 1;
3404
- status.secs = secs;
3405
- status.last = message;
3406
- saveStatus(logDir, status);
3407
- appendLedger(ledger, "WORKER-END", {
3408
- id: wid,
3409
- provider: target.providerName,
3410
- mode: "headless",
3411
- engine: "image",
3412
- model,
3581
+ status.last = e instanceof Error ? e.message : String(e);
3582
+ await finaliseRun({
3583
+ logDir,
3584
+ ledger,
3585
+ status,
3586
+ ok: false,
3413
3587
  rc: 1,
3414
3588
  secs,
3415
- label,
3416
- capability: a.capability
3589
+ end: imageEnd,
3590
+ report: null
3417
3591
  });
3418
3592
  throw e;
3419
3593
  }
3420
3594
  }
3595
+ /**
3596
+ * Follow pane, made observable on stderr (stdout stays pure JSON for
3597
+ * `--output-format json`): one line naming the outcome — opened, skipped
3598
+ * (with reason), or failed (with reason, alongside the existing PANE-FAIL
3599
+ * ledger line). Fire-and-forget, never blocks the worker.
3600
+ */
3601
+ async function announcePane(logDir, config, wid, term, noPane, ledger) {
3602
+ const skip = !noPane ? !config.pane.enabled ? "workers.pane.enabled is false" : process.env.PAI_WORKER_AUTOPANE === "0" ? "PAI_WORKER_AUTOPANE=0" : !term ? "not in iTerm2 (no ITERM_SESSION_ID)" : null : "--no-pane";
3603
+ if (skip) {
3604
+ process.stderr.write(`[pai worker] no pane for ${wid}: ${skip}\n`);
3605
+ return;
3606
+ }
3607
+ try {
3608
+ const r = await openPaneForWorker(logDir, config, wid, term);
3609
+ const suffix = r.session ? ` (iTerm session ${r.session})` : "";
3610
+ process.stderr.write(`[pai worker] pane opened for ${wid}${suffix}\n`);
3611
+ } catch (e) {
3612
+ const message = e instanceof Error ? e.message : String(e);
3613
+ process.stderr.write(`[pai worker] no pane for ${wid}: ${message.slice(0, 200)}\n`);
3614
+ appendLedger(ledger, "PANE-FAIL", {
3615
+ id: wid,
3616
+ error: message.slice(0, 200)
3617
+ });
3618
+ }
3619
+ }
3421
3620
  async function executeRun(a) {
3422
3621
  const { config, logDir, target, model, label, parsed, noPane } = a;
3423
3622
  const headless = parsed.headless;
@@ -3549,12 +3748,7 @@ async function executeRun(a) {
3549
3748
  ...a.capability ? { capability: a.capability } : {},
3550
3749
  ...a.specPath ? { spec: a.specPath } : {}
3551
3750
  });
3552
- if (headless && !noPane && config.pane.enabled && term && process.env.PAI_WORKER_AUTOPANE !== "0") openPaneForWorker(logDir, config, wid, term).catch((e) => {
3553
- appendLedger(ledger, "PANE-FAIL", {
3554
- id: wid,
3555
- error: (e instanceof Error ? e.message : String(e)).slice(0, 200)
3556
- });
3557
- });
3751
+ if (headless) announcePane(logDir, config, wid, term, noPane, ledger);
3558
3752
  const t0 = Date.now();
3559
3753
  const proc = spawn(cmd[0], cmd.slice(1), {
3560
3754
  env,
@@ -3707,9 +3901,6 @@ async function executeRun(a) {
3707
3901
  const secs = Math.floor((Date.now() - t0) / 1e3);
3708
3902
  const resultEvent = ctx.resultEvent;
3709
3903
  const ok = runSucceeded(headless, rc, resultEvent);
3710
- status.state = ok ? "done" : "failed";
3711
- status.rc = rc;
3712
- status.secs = secs;
3713
3904
  if (resultEvent) status.last = shortText(resultEvent.result ?? "", 90);
3714
3905
  if (ctx.resultReport?.notes) status.last = shortText(ctx.resultReport.notes, 90);
3715
3906
  if (headless && a.reportFormat === "ag2" && resultEvent?.result) {
@@ -3761,66 +3952,55 @@ async function executeRun(a) {
3761
3952
  });
3762
3953
  }
3763
3954
  }
3764
- saveStatus(logDir, status);
3765
- appendLedger(ledger, "WORKER-END", {
3766
- id: wid,
3767
- provider: target.providerName,
3768
- mode: headless ? "headless" : "interactive",
3769
- model,
3955
+ return finaliseRun({
3956
+ logDir,
3957
+ ledger,
3958
+ status,
3959
+ ok,
3770
3960
  rc,
3771
3961
  secs,
3772
- turns: status.turns,
3773
- tools: status.tools,
3774
- label
3775
- });
3776
- if (worktree) status = recordWorktree(logDir, status, worktree, ok);
3777
- if (headless && !a.quiet) printResult(parsed.outputFormat, resultEvent, rc, logDir, wid, ctx.resultReport, worktreeExtras(status));
3778
- const resultText = resultEvent?.result ?? "";
3779
- if (!ok && headless && a.target.via === "auto" && config.routing.retryOnQuota && status.turns <= 1 && status.tools === 0 && isQuotaFailure(resultText)) {
3780
- setCooldown(logDir, target.providerName, config.routing.cooldownMinutes);
3781
- const next = nextAutoProvider(config, logDir, target.providerName);
3782
- if (next && a.reroutes < config.routing.order.length) {
3783
- appendLedger(ledger, "WORKER-REROUTE", {
3784
- from: target.providerName,
3785
- to: next,
3786
- reason: "quota"
3787
- });
3788
- return runWorker({
3789
- providerFlag: next,
3790
- label,
3791
- noPane: a.noPane,
3792
- mcpFlag: a.mcpFlag,
3793
- cwd: a.cwd,
3794
- specPath: a.specPath,
3795
- parent: a.parent,
3796
- stage: a.stage,
3797
- claudeArgs: a.claudeArgs,
3798
- onWorkerStart: a.onWorkerStart,
3799
- worktreeFlag: a.worktreeFlag,
3800
- className: a.className,
3801
- _reroutes: a.reroutes + 1
3802
- });
3803
- }
3804
- }
3805
- const finalRc = rc !== 0 ? rc : ok ? 0 : 1;
3806
- if (status.parent && isWorkerId(logDir, status.parent)) try {
3807
- await deliverHandoff(logDir, {
3808
- from: wid,
3809
- to: status.parent,
3810
- kind: "result",
3811
- text: shortText(ctx.resultReport?.notes ?? resultEvent?.result ?? (ok ? "done" : "failed"), 400),
3812
- data: {
3813
- rc: finalRc,
3814
- ok,
3815
- ...status.branch ? {
3816
- branch: status.branch,
3817
- commits: status.commits ?? 0
3818
- } : {},
3819
- ...ctx.resultReport ? { report: ctx.resultReport } : {}
3962
+ end: {
3963
+ provider: target.providerName,
3964
+ mode: headless ? "headless" : "interactive",
3965
+ model,
3966
+ turns: status.turns,
3967
+ tools: status.tools,
3968
+ label
3969
+ },
3970
+ report: ctx.resultReport,
3971
+ fallbackText: resultEvent?.result,
3972
+ worktree,
3973
+ beforeHandoff: async (st, rep) => {
3974
+ if (headless && !a.quiet) printResult(parsed.outputFormat, resultEvent, rc, logDir, wid, rep, worktreeExtras(st));
3975
+ const resultText = resultEvent?.result ?? "";
3976
+ if (!ok && headless && a.target.via === "auto" && config.routing.retryOnQuota && status.turns <= 1 && status.tools === 0 && isQuotaFailure(resultText)) {
3977
+ setCooldown(logDir, target.providerName, config.routing.cooldownMinutes);
3978
+ const next = nextAutoProvider(config, logDir, target.providerName);
3979
+ if (next && a.reroutes < config.routing.order.length) {
3980
+ appendLedger(ledger, "WORKER-REROUTE", {
3981
+ from: target.providerName,
3982
+ to: next,
3983
+ reason: "quota"
3984
+ });
3985
+ return runWorker({
3986
+ providerFlag: next,
3987
+ label,
3988
+ noPane: a.noPane,
3989
+ mcpFlag: a.mcpFlag,
3990
+ cwd: a.cwd,
3991
+ specPath: a.specPath,
3992
+ parent: a.parent,
3993
+ stage: a.stage,
3994
+ claudeArgs: a.claudeArgs,
3995
+ onWorkerStart: a.onWorkerStart,
3996
+ worktreeFlag: a.worktreeFlag,
3997
+ className: a.className,
3998
+ _reroutes: a.reroutes + 1
3999
+ });
4000
+ }
3820
4001
  }
3821
- });
3822
- } catch {}
3823
- return finalRc;
4002
+ }
4003
+ });
3824
4004
  }
3825
4005
  /** The worktree fields printResult adds to a json payload, when there is one. */
3826
4006
  function worktreeExtras(s) {
@@ -3945,12 +4125,7 @@ async function executeCodexRun(a) {
3945
4125
  id: wid,
3946
4126
  note: `dropped for codex: ${dropped.join(", ")}`
3947
4127
  });
3948
- if (!noPane && config.pane.enabled && term && process.env.PAI_WORKER_AUTOPANE !== "0") openPaneForWorker(logDir, config, wid, term).catch((e) => {
3949
- appendLedger(ledger, "PANE-FAIL", {
3950
- id: wid,
3951
- error: (e instanceof Error ? e.message : String(e)).slice(0, 200)
3952
- });
3953
- });
4128
+ announcePane(logDir, config, wid, term, noPane, ledger);
3954
4129
  const t0 = Date.now();
3955
4130
  const proc = spawn("codex", buildCodexArgs(prompt, parsed.callerModel ? void 0 : model), {
3956
4131
  env,
@@ -4029,9 +4204,6 @@ async function executeCodexRun(a) {
4029
4204
  };
4030
4205
  writeEvent(resultEvent);
4031
4206
  const ok = rc === 0 && !fold.isError;
4032
- status.state = ok ? "done" : "failed";
4033
- status.rc = rc;
4034
- status.secs = secs;
4035
4207
  status.last = shortText(report?.notes ?? finalText, 90) || (ok ? "done" : "failed");
4036
4208
  if (report?.changed?.length) {
4037
4209
  const verify = verifyReportChanges(report.changed, worktree?.dir ?? cwd, t0);
@@ -4047,40 +4219,29 @@ async function executeCodexRun(a) {
4047
4219
  });
4048
4220
  }
4049
4221
  }
4050
- saveStatus(logDir, status);
4051
- appendLedger(ledger, "WORKER-END", {
4052
- id: wid,
4053
- provider: target.providerName,
4054
- mode: "headless",
4055
- engine: "codex",
4056
- model,
4222
+ return finaliseRun({
4223
+ logDir,
4224
+ ledger,
4225
+ status,
4226
+ ok,
4057
4227
  rc,
4058
4228
  secs,
4059
- turns: status.turns,
4060
- tools: status.tools,
4061
- label
4229
+ end: {
4230
+ provider: target.providerName,
4231
+ mode: "headless",
4232
+ engine: "codex",
4233
+ model,
4234
+ turns: status.turns,
4235
+ tools: status.tools,
4236
+ label
4237
+ },
4238
+ report,
4239
+ fallbackText: finalText,
4240
+ worktree,
4241
+ beforeHandoff: (st, rep) => {
4242
+ if (!a.quiet) printResult(parsed.outputFormat, resultEvent, rc, logDir, wid, rep, worktreeExtras(st));
4243
+ }
4062
4244
  });
4063
- if (worktree) status = recordWorktree(logDir, status, worktree, ok);
4064
- if (!a.quiet) printResult(parsed.outputFormat, resultEvent, rc, logDir, wid, report, worktreeExtras(status));
4065
- const finalRc = rc !== 0 ? rc : ok ? 0 : 1;
4066
- if (status.parent && isWorkerId(logDir, status.parent)) try {
4067
- await deliverHandoff(logDir, {
4068
- from: wid,
4069
- to: status.parent,
4070
- kind: "result",
4071
- text: shortText(report?.notes ?? finalText ?? (ok ? "done" : "failed"), 400),
4072
- data: {
4073
- rc: finalRc,
4074
- ok,
4075
- ...status.branch ? {
4076
- branch: status.branch,
4077
- commits: status.commits ?? 0
4078
- } : {},
4079
- ...report ? { report } : {}
4080
- }
4081
- });
4082
- } catch {}
4083
- return finalRc;
4084
4245
  }
4085
4246
  function printResult(fmt, resultEvent, rc, logDir, wid, report, extras) {
4086
4247
  if (fmt === "stream-json") return;
@@ -4214,4 +4375,4 @@ function resultFromOutput(out) {
4214
4375
 
4215
4376
  //#endregion
4216
4377
  export { UNLABELED as A, newWorkerId as B, workerInScope as C, worktreesDir as D, mergeWorker as E, elapsedOf as F, waitForTerminalStatus as G, ownsPid as H, isChatPane as I, shortText as J, longInlinePromptHint as K, isLive as L, alive as M, contextLabel as N, appendLedger as O, contextPercent as P, loadStatus as R, sessionTag as S, git as T, saveStatus as U, nowStamp as V, setWorkerLabel as W, expandMcpNames as X, describeMcp as Y, currentTabKey as _, projectLaunchConfig as a, resolveSession as b, handoffFromInside as c, workerDepth as d, checkPaneForWorker as f, markControlsHeld as g, clickrControlsArgv as h, usageContextTokens as i, ageOf as j, ledgerSummary as k, readInbox as l, openPaneForWorker as m, runWorker as n, parseWorkerReport as o, openFollowPane as p, parseRunnerArgs as q, testProvider as r, renderReport as s, printResult as t, sayToWorker as u, itermForClaudeSession as v, discardWorker as w, sessionMapPath as x, itermUuid as y, loadStatuses as z };
4217
- //# sourceMappingURL=run-FeM1_UVa.mjs.map
4378
+ //# sourceMappingURL=run-CxxZKEZw.mjs.map