@tekmidian/pai 0.61.0 → 0.62.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 (67) hide show
  1. package/dist/{async-CLvuY7b-.mjs → async-DNVWtQl2.mjs} +2 -2
  2. package/dist/{async-CLvuY7b-.mjs.map → async-DNVWtQl2.mjs.map} +1 -1
  3. package/dist/{chain-C5oeyUg4.mjs → chain-CuaDawK9.mjs} +2 -2
  4. package/dist/{chain-C5oeyUg4.mjs.map → chain-CuaDawK9.mjs.map} +1 -1
  5. package/dist/cli/index.mjs +11 -11
  6. package/dist/cli/program.mjs +11 -11
  7. package/dist/{context-handover-cache-BdtJZpgv.mjs → context-handover-cache-DFNnlWv2.mjs} +2 -2
  8. package/dist/{context-handover-cache-BdtJZpgv.mjs.map → context-handover-cache-DFNnlWv2.mjs.map} +1 -1
  9. package/dist/daemon/index.mjs +8 -8
  10. package/dist/{daemon-DSsIEqzW.mjs → daemon-Bf7Eock4.mjs} +32 -18
  11. package/dist/{daemon-DSsIEqzW.mjs.map → daemon-Bf7Eock4.mjs.map} +1 -1
  12. package/dist/daemon-bJi1gDkG.mjs +19 -0
  13. package/dist/daemon-mcp/index.mjs +4 -4
  14. package/dist/{factory-EUUhSSoq.mjs → factory-BozZa81q.mjs} +3 -3
  15. package/dist/{factory-EUUhSSoq.mjs.map → factory-BozZa81q.mjs.map} +1 -1
  16. package/dist/{factory-DW7BCJJT.mjs → factory-UO9_DFMN.mjs} +1 -1
  17. package/dist/{fallback-DDbJVaR8.mjs → fallback-F0TWGVM2.mjs} +2 -2
  18. package/dist/{fallback-DDbJVaR8.mjs.map → fallback-F0TWGVM2.mjs.map} +1 -1
  19. package/dist/{helpers-D9WkIf9R.mjs → helpers-Dj-B314s.mjs} +3 -2
  20. package/dist/{helpers-D9WkIf9R.mjs.map → helpers-Dj-B314s.mjs.map} +1 -1
  21. package/dist/hooks/block-sleep-poll.mjs.map +2 -2
  22. package/dist/hooks/load-project-context.mjs +8513 -133
  23. package/dist/hooks/load-project-context.mjs.map +4 -4
  24. package/dist/hooks/security-validator.mjs.map +1 -1
  25. package/dist/hooks/worker-guard.mjs +7859 -0
  26. package/dist/hooks/worker-guard.mjs.map +7 -0
  27. package/dist/hooks/worker-status-line.mjs +101 -8
  28. package/dist/hooks/worker-status-line.mjs.map +4 -4
  29. package/dist/index.mjs +1 -1
  30. package/dist/{indexer-backend-DcDtin1O.mjs → indexer-backend-BN9rpp9O.mjs} +2 -2
  31. package/dist/{main-resolver-DIQ6m_RA.mjs → main-resolver-BIft_FVF.mjs} +3 -3
  32. package/dist/{main-resolver-DIQ6m_RA.mjs.map → main-resolver-BIft_FVF.mjs.map} +1 -1
  33. package/dist/{main-resolver-CxOsxtOr.mjs → main-resolver-C9-poMdi.mjs} +1 -1
  34. package/dist/{planner-D8jAr_sx.mjs → planner-BG9ivPj0.mjs} +4 -4
  35. package/dist/{planner-D8jAr_sx.mjs.map → planner-BG9ivPj0.mjs.map} +1 -1
  36. package/dist/{postgres-nvU5pShF.mjs → postgres-CxD6_Dca.mjs} +3 -3
  37. package/dist/{postgres-nvU5pShF.mjs.map → postgres-CxD6_Dca.mjs.map} +1 -1
  38. package/dist/{program-BTdspOOa.mjs → program-BRvCzhjx.mjs} +157 -40
  39. package/dist/program-BRvCzhjx.mjs.map +1 -0
  40. package/dist/{run-CxxZKEZw.mjs → run-BVQZI79D.mjs} +281 -85
  41. package/dist/run-BVQZI79D.mjs.map +1 -0
  42. package/dist/{session-keepalive-urk--pfa.mjs → session-keepalive-DJA97K2X.mjs} +2 -2
  43. package/dist/{session-keepalive-urk--pfa.mjs.map → session-keepalive-DJA97K2X.mjs.map} +1 -1
  44. package/dist/{sqlite-D9p5a4qe.mjs → sqlite-CfKfuggn.mjs} +2 -2
  45. package/dist/{sqlite-D9p5a4qe.mjs.map → sqlite-CfKfuggn.mjs.map} +1 -1
  46. package/dist/{tools-r-7ZG8B5.mjs → tools-B52IvaU6.mjs} +3 -3
  47. package/dist/{tools-qXyHFwNj.mjs → tools-DFdPbX-X.mjs} +2 -2
  48. package/dist/{tools-qXyHFwNj.mjs.map → tools-DFdPbX-X.mjs.map} +1 -1
  49. package/dist/{vault-indexer-DHYBpRZ5.mjs → vault-indexer-mCFus_AM.mjs} +2 -2
  50. package/dist/{vault-indexer-DHYBpRZ5.mjs.map → vault-indexer-mCFus_AM.mjs.map} +1 -1
  51. package/dist/{wakeup-C48hcN6O.mjs → wakeup-DO1O4udw.mjs} +2 -2
  52. package/dist/{wakeup-C48hcN6O.mjs.map → wakeup-DO1O4udw.mjs.map} +1 -1
  53. package/dist/{work-queue-worker-DUvPiwGO.mjs → work-queue-worker-BbrC3I_R.mjs} +2 -2
  54. package/dist/{work-queue-worker-DUvPiwGO.mjs.map → work-queue-worker-BbrC3I_R.mjs.map} +1 -1
  55. package/dist/{work-queue-worker-1_VBfI_k.mjs → work-queue-worker-z-AATZ2t.mjs} +3 -3
  56. package/docs/commands/README.md +2 -0
  57. package/docs/commands/worker.md +27 -0
  58. package/package.json +2 -2
  59. package/scripts/build-hooks.mjs +6 -5
  60. package/scripts/build-skill-stubs.mjs +6 -5
  61. package/src/hooks/ts/lib/worker-guard.test.ts +188 -0
  62. package/src/hooks/ts/lib/worker-guard.ts +315 -0
  63. package/src/hooks/ts/pre-tool-use/worker-guard.ts +75 -0
  64. package/src/hooks/ts/session-start/load-project-context.ts +14 -0
  65. package/dist/daemon-MoFF4oeg.mjs +0 -19
  66. package/dist/program-BTdspOOa.mjs.map +0 -1
  67. package/dist/run-CxxZKEZw.mjs.map +0 -1
@@ -1,8 +1,8 @@
1
- import { i as getRegistryBackend } from "./factory-EUUhSSoq.mjs";
1
+ import { i as getRegistryBackend } from "./factory-BozZa81q.mjs";
2
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
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
4
  import { appendFileSync, closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
5
- import { homedir, tmpdir } from "node:os";
5
+ import { homedir, platform, tmpdir } from "node:os";
6
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";
@@ -598,7 +598,7 @@ function worktreePath(logDir, id) {
598
598
  return join(worktreesDir(logDir), id);
599
599
  }
600
600
  /** Run git in `cwd` with an optional env override, trimmed stdout, stderr on failure. */
601
- function runGit(cwd, args, env) {
601
+ function runGit(cwd, args, env, timeout = 3e4) {
602
602
  try {
603
603
  return execFileSync("git", [
604
604
  "-C",
@@ -606,7 +606,7 @@ function runGit(cwd, args, env) {
606
606
  ...args
607
607
  ], {
608
608
  encoding: "utf8",
609
- timeout: 3e4,
609
+ timeout,
610
610
  stdio: [
611
611
  "ignore",
612
612
  "pipe",
@@ -621,8 +621,8 @@ function runGit(cwd, args, env) {
621
621
  }
622
622
  }
623
623
  /** Run git in `cwd`, returning trimmed stdout; throws with stderr on failure. */
624
- function git(cwd, args) {
625
- return runGit(cwd, args);
624
+ function git(cwd, args, timeoutMs) {
625
+ return runGit(cwd, args, void 0, timeoutMs);
626
626
  }
627
627
  /** Is `cwd` inside a git repository (a .git dir — worktrees: a .git file)? */
628
628
  function isGitRepo(cwd) {
@@ -632,30 +632,23 @@ function isGitRepo(cwd) {
632
632
  return false;
633
633
  }
634
634
  }
635
- /**
636
- * Does the prompt read as a read-only task? Writing classes default to a
637
- * worktree; a prompt that only asks to look at things should not pay for one.
638
- * First-word verbs plus the explicit markers people actually write.
639
- */
640
- function promptLooksReadonly(prompt) {
641
- const p = prompt.trim();
642
- if (!p) return true;
643
- const firstLine = p.split("\n", 1)[0];
644
- if (/\b(read[- ]only|do not (modify|change|edit|write)|don'?t (modify|change|edit|write)|no changes)\b/i.test(firstLine)) return true;
645
- return /^(review|read|analy[sz]e|research|summar[iy]|inspect|investigate|spotcheck|report|find|list|check|verify|describe|explain|show)\b/i.test(p);
646
- }
647
- /** The classes whose runs write files and therefore default to a worktree. */
648
- const WORKTREE_CLASSES = [
649
- "implement",
650
- "complex",
651
- "plan"
635
+ /** Tools that can write to the checkout (Bash can, via the shell). */
636
+ const WRITING_TOOLS = [
637
+ "Edit",
638
+ "Write",
639
+ "MultiEdit",
640
+ "NotebookEdit",
641
+ "Bash"
652
642
  ];
653
- /** Should this run get a worktree? Explicit flag first, then the default rule. */
643
+ /** Can a run with these --allowedTools write? No restriction at all means yes. */
644
+ function toolsCanWrite(allowedTools) {
645
+ const names = allowedTools.flatMap((e) => e.split(",")).map((s) => s.trim().replace(/\(.*$/, "")).filter(Boolean);
646
+ return !names.length || names.some((n) => WRITING_TOOLS.includes(n));
647
+ }
648
+ /** Should this run get a worktree? Explicit flag first, then: can it write? */
654
649
  function worktreeWanted(flag, opts) {
655
650
  if (flag !== void 0) return flag;
656
- if (!opts.className || !WORKTREE_CLASSES.includes(opts.className)) return false;
657
- if (!isGitRepo(opts.cwd)) return false;
658
- return !promptLooksReadonly(opts.prompt ?? "");
651
+ return toolsCanWrite(opts.allowedTools) && isGitRepo(opts.cwd);
659
652
  }
660
653
  /**
661
654
  * Commit `cwd`'s current index + working tree (including untracked files) as
@@ -699,11 +692,6 @@ function snapshotUncommitted(cwd, id, head) {
699
692
  * degrade to an in-place run.
700
693
  */
701
694
  function addWorktree(logDir, id, cwd) {
702
- const swept = sweepOrphanWorktrees(logDir);
703
- if (swept.length) appendLedger(ledgerPath(logDir), "WORKER-NOTE", {
704
- id,
705
- note: `swept orphan worktree(s): ${swept.join(", ")}`
706
- });
707
695
  const dir = worktreePath(logDir, id);
708
696
  const branch = worktreeBranch(id);
709
697
  const head = git(cwd, ["rev-parse", "HEAD"]);
@@ -717,6 +705,7 @@ function addWorktree(logDir, id, cwd) {
717
705
  branch,
718
706
  base
719
707
  ]);
708
+ provisionDeps(logDir, id, cwd, dir);
720
709
  return {
721
710
  dir,
722
711
  branch,
@@ -725,44 +714,60 @@ function addWorktree(logDir, id, cwd) {
725
714
  };
726
715
  }
727
716
  /**
728
- * Remove worktrees (and their `worker/<id>` branches) whose worker no longer
729
- * exists — no status file left in the log dir. Killed and failed runs clean
730
- * up after themselves, but a `kill -9` or a crash strands a directory and a
731
- * branch; every worktree creation sweeps first so they cannot accumulate.
732
- * Directories younger than `minAgeMin` minutes are left alone: a run that is
733
- * just starting owns its worktree a moment before its status file exists.
717
+ * Give the worktree its own dependencies so the worker never has to improvise
718
+ * (symlinking node_modules to the main checkout, building in a /tmp copy).
719
+ * A real node_modules directory in the main checkout is APFS-cloned
720
+ * (`cp -c`, copy-on-write, near instant); elsewhere the lockfile installs it.
721
+ * Never a symlink. Time-boxed and best effort: a failure is logged, the run
722
+ * goes on.
734
723
  */
735
- function sweepOrphanWorktrees(logDir, minAgeMin = 10) {
736
- const root = worktreesDir(logDir);
737
- if (!existsSync(root)) return [];
738
- const swept = [];
739
- for (const ent of readdirSync(root, { withFileTypes: true })) {
740
- if (!ent.isDirectory()) continue;
741
- const id = ent.name;
742
- const dir = join(root, id);
743
- if (existsSync(statusPath(logDir, id))) continue;
744
- try {
745
- if ((Date.now() - statSync(dir).mtimeMs) / 6e4 < minAgeMin) continue;
746
- } catch {}
747
- let gitDir = null;
724
+ function provisionDeps(logDir, id, cwd, wtDir) {
725
+ const note = (n) => appendLedger(ledgerPath(logDir), "WORKER-NOTE", {
726
+ id,
727
+ note: n
728
+ });
729
+ try {
730
+ const main = join(git(cwd, ["rev-parse", "--show-toplevel"]), "node_modules");
731
+ const dest = join(wtDir, "node_modules");
732
+ if (!existsSync(main) || lstatSync(main).isSymbolicLink() || existsSync(dest)) return;
748
733
  try {
749
- const raw = git(dir, ["rev-parse", "--git-common-dir"]);
750
- gitDir = isAbsolute(raw) ? raw : resolve(dir, raw);
734
+ git(wtDir, [
735
+ "check-ignore",
736
+ "-q",
737
+ "node_modules"
738
+ ]);
751
739
  } catch {
752
- gitDir = null;
740
+ return;
753
741
  }
754
- removeWorktree(gitDir ?? dir, dir, true);
755
- if (gitDir) try {
756
- git(gitDir, ["worktree", "prune"]);
757
- git(gitDir, [
758
- "branch",
759
- "-D",
760
- worktreeBranch(id)
761
- ]);
742
+ const t0 = Date.now();
743
+ const opts = {
744
+ cwd: wtDir,
745
+ stdio: "ignore",
746
+ timeout: 18e4
747
+ };
748
+ if (platform() === "darwin") try {
749
+ execFileSync("cp", [
750
+ "-cR",
751
+ main,
752
+ dest
753
+ ], opts);
754
+ note(`node_modules cloned (apfs) in ${Date.now() - t0}ms`);
755
+ return;
756
+ } catch {
757
+ rmSync(dest, {
758
+ recursive: true,
759
+ force: true
760
+ });
761
+ }
762
+ if (existsSync(join(wtDir, "bun.lock"))) execFileSync("bun", ["install", "--frozen-lockfile"], opts);
763
+ else if (existsSync(join(wtDir, "package-lock.json"))) execFileSync("npm", ["ci"], opts);
764
+ else return;
765
+ note(`node_modules installed from lockfile in ${Date.now() - t0}ms`);
766
+ } catch (e) {
767
+ try {
768
+ note(`node_modules provisioning failed: ${e.message}`);
762
769
  } catch {}
763
- swept.push(id);
764
770
  }
765
- return swept;
766
771
  }
767
772
  /** Commits the branch collected on top of its base. */
768
773
  function commitsSince(dir, base) {
@@ -788,6 +793,12 @@ function recordWorktree(logDir, status, info, ok) {
788
793
  s.worktreeDir = info.dir;
789
794
  s.worktreeBase = info.base;
790
795
  s.worktreeSnapshot = info.snapshot;
796
+ } else if (safeCommitsSince(info.dir, info.base) > 0) {
797
+ s.branch = info.branch;
798
+ s.commits = commitsSince(info.dir, info.base);
799
+ s.worktreeDir = info.dir;
800
+ s.worktreeBase = info.base;
801
+ s.worktreeSnapshot = info.snapshot;
791
802
  } else {
792
803
  removeWorktree(s.cwd, info.dir, true);
793
804
  try {
@@ -806,6 +817,37 @@ function recordWorktree(logDir, status, info, ok) {
806
817
  saveStatus(logDir, s);
807
818
  return s;
808
819
  }
820
+ function safeCommitsSince(dir, base) {
821
+ try {
822
+ return commitsSince(dir, base);
823
+ } catch {
824
+ return 0;
825
+ }
826
+ }
827
+ /**
828
+ * Salvage for an exit path (run finaliser, kill signal): commit whatever the
829
+ * worker left uncommitted, log WORKER-SALVAGE, never throw. Bounded by
830
+ * `timeoutMs` per git call. Returns the committed paths.
831
+ */
832
+ function salvageOnExit(ledger, id, label, wtDir, timeoutMs) {
833
+ try {
834
+ const { committed, skipped } = salvageUncommitted(wtDir, label, timeoutMs);
835
+ if (committed.length) appendLedger(ledger, "WORKER-SALVAGE", {
836
+ id,
837
+ paths: committed.join(","),
838
+ skipped: skipped.length
839
+ });
840
+ return committed;
841
+ } catch (e) {
842
+ try {
843
+ appendLedger(ledger, "WORKER-NOTE", {
844
+ id,
845
+ note: `salvage failed: ${e.message}`
846
+ });
847
+ } catch {}
848
+ return [];
849
+ }
850
+ }
809
851
  /** Remove a worktree directory from git's books and the filesystem. */
810
852
  function removeWorktree(cwd, dir, force) {
811
853
  try {
@@ -831,17 +873,17 @@ function removeWorktree(cwd, dir, force) {
831
873
  * carry used; name-only, not porcelain: a worktree-only change renders as
832
874
  * " M path" and the shared git() helper trims that leading space away.
833
875
  */
834
- function uncommittedPaths(wtDir) {
876
+ function uncommittedPaths(wtDir, timeoutMs) {
835
877
  const tracked = git(wtDir, [
836
878
  "diff",
837
879
  "--name-only",
838
880
  "HEAD"
839
- ]).split("\n").filter(Boolean);
881
+ ], timeoutMs).split("\n").filter(Boolean);
840
882
  const untracked = git(wtDir, [
841
883
  "ls-files",
842
884
  "--others",
843
885
  "--exclude-standard"
844
- ]).split("\n").filter(Boolean);
886
+ ], timeoutMs).split("\n").filter(Boolean);
845
887
  return [...tracked, ...untracked];
846
888
  }
847
889
  /**
@@ -877,8 +919,8 @@ function symlinkEscapesWorktree(wtDir, relPath) {
877
919
  * as skipped instead. A failed commit throws with the worktree untouched:
878
920
  * its edits are still on disk, so nothing is lost.
879
921
  */
880
- function salvageUncommitted(wtDir, label) {
881
- const paths = uncommittedPaths(wtDir);
922
+ function salvageUncommitted(wtDir, label, timeoutMs) {
923
+ const paths = uncommittedPaths(wtDir, timeoutMs);
882
924
  if (!paths.length) return {
883
925
  committed: [],
884
926
  skipped: []
@@ -894,14 +936,16 @@ function salvageUncommitted(wtDir, label) {
894
936
  try {
895
937
  git(wtDir, [
896
938
  "add",
939
+ "-A",
897
940
  "--",
898
- ...toStage
899
- ]);
941
+ ".",
942
+ ...paths.filter((p) => !toStage.includes(p)).map((p) => `:(exclude,literal)${p}`)
943
+ ], timeoutMs);
900
944
  git(wtDir, [
901
945
  "commit",
902
946
  "-m",
903
947
  `salvaged: ${label}`
904
- ]);
948
+ ], timeoutMs);
905
949
  } catch (e) {
906
950
  throw new Error(`cannot salvage the uncommitted changes in ${wtDir} — ${e.message}; nothing was lost: commit them there by hand, then re-run merge`);
907
951
  }
@@ -1215,6 +1259,139 @@ function inPlaceSystemPrompt() {
1215
1259
  "Read-only git (status, diff, log, show, stash list) is fine."
1216
1260
  ].join("\n");
1217
1261
  }
1262
+ const GC_STAMP = "gc-last";
1263
+ /**
1264
+ * Archive worktrees of finished workers older than `olderThanHours` and
1265
+ * status-less orphan directories: uncommitted state is salvaged onto the
1266
+ * branch, the branch is x, the worktree is
1267
+ * removed. Nothing is deleted without a salvage first; a failed salvage skips
1268
+ * that worktree. Running workers are never touched. `dryRun` reports only.
1269
+ */
1270
+ function gcWorktrees(logDir, olderThanHours = 24, dryRun = false, now = Date.now()) {
1271
+ const cutoff = now - olderThanHours * 36e5;
1272
+ const out = [];
1273
+ const root = worktreesDir(logDir);
1274
+ const known = /* @__PURE__ */ new Set();
1275
+ const archive = (id, dir, repo, st) => {
1276
+ const target = `refs/pai-archive/${id}`;
1277
+ if (dryRun) return void out.push({
1278
+ id,
1279
+ archive: target
1280
+ });
1281
+ try {
1282
+ if (!repo) throw new Error("not a git worktree");
1283
+ const label = `archive: leftover state of worker ${id}`;
1284
+ if (uncommittedPaths(dir).length) {
1285
+ const res = salvageUncommitted(dir, label);
1286
+ if (res.committed.length) git(dir, [
1287
+ "commit",
1288
+ "--amend",
1289
+ "-q",
1290
+ "-m",
1291
+ label
1292
+ ]);
1293
+ if (res.skipped.length && uncommittedPaths(dir).some((p) => !symlinkEscapesWorktree(dir, p))) throw new Error("uncommitted paths remain");
1294
+ }
1295
+ const src = worktreeBranch(id);
1296
+ let hasBranch = true;
1297
+ try {
1298
+ git(repo, [
1299
+ "rev-parse",
1300
+ "--verify",
1301
+ "-q",
1302
+ `refs/heads/${src}`
1303
+ ]);
1304
+ } catch {
1305
+ hasBranch = false;
1306
+ }
1307
+ const tip = git(dir, ["rev-parse", "HEAD"]);
1308
+ if (hasBranch) git(repo, [
1309
+ "merge-base",
1310
+ "--is-ancestor",
1311
+ `refs/heads/${src}`,
1312
+ tip
1313
+ ]);
1314
+ git(repo, [
1315
+ "update-ref",
1316
+ target,
1317
+ tip
1318
+ ]);
1319
+ git(repo, [
1320
+ "worktree",
1321
+ "remove",
1322
+ "--force",
1323
+ dir
1324
+ ]);
1325
+ if (hasBranch) git(repo, [
1326
+ "branch",
1327
+ "-D",
1328
+ src
1329
+ ]);
1330
+ if (st) saveStatus(logDir, {
1331
+ ...st,
1332
+ branch: null,
1333
+ worktreeDir: null,
1334
+ archived: target
1335
+ });
1336
+ appendLedger(ledgerPath(logDir), "WORKER-GC", {
1337
+ id,
1338
+ archive: target
1339
+ });
1340
+ out.push({
1341
+ id,
1342
+ archive: target
1343
+ });
1344
+ } catch (e) {
1345
+ out.push({
1346
+ id,
1347
+ skipped: e.message.split("\n")[0]
1348
+ });
1349
+ }
1350
+ };
1351
+ for (const st of loadStatuses(logDir)) {
1352
+ known.add(st.id);
1353
+ if (!st.worktreeDir || !existsSync(st.worktreeDir) || isLive(st)) continue;
1354
+ let end = 0;
1355
+ try {
1356
+ end = statSync(statusPath(logDir, st.id)).mtimeMs;
1357
+ } catch {
1358
+ continue;
1359
+ }
1360
+ if (end > cutoff) continue;
1361
+ archive(st.id, st.worktreeDir, st.cwd, st);
1362
+ }
1363
+ if (existsSync(root)) for (const ent of readdirSync(root, { withFileTypes: true })) {
1364
+ if (!ent.isDirectory() || known.has(ent.name) || existsSync(statusPath(logDir, ent.name))) continue;
1365
+ const dir = join(root, ent.name);
1366
+ try {
1367
+ if (statSync(dir).mtimeMs > cutoff) continue;
1368
+ } catch {
1369
+ continue;
1370
+ }
1371
+ let repo = null;
1372
+ try {
1373
+ const raw = git(dir, ["rev-parse", "--git-common-dir"]);
1374
+ repo = isAbsolute(raw) ? raw : resolve(dir, raw);
1375
+ } catch {
1376
+ repo = null;
1377
+ }
1378
+ archive(ent.name, dir, repo, null);
1379
+ }
1380
+ if (!dryRun) for (const repo of new Set(loadStatuses(logDir).map((s) => s.cwd))) try {
1381
+ git(repo, ["worktree", "prune"]);
1382
+ } catch {}
1383
+ return out;
1384
+ }
1385
+ /** Run `gcWorktrees` at most once per `everyMin` minutes (stamp file in the logDir). */
1386
+ function gcWorktreesThrottled(logDir, everyMin = 60, now = Date.now()) {
1387
+ const stamp = join(logDir, GC_STAMP);
1388
+ try {
1389
+ if (now - statSync(stamp).mtimeMs < everyMin * 6e4) return null;
1390
+ } catch {}
1391
+ mkdirSync(logDir, { recursive: true });
1392
+ writeFileSync(stamp, String(now), "utf8");
1393
+ return gcWorktrees(logDir, 24, false, now);
1394
+ }
1218
1395
 
1219
1396
  //#endregion
1220
1397
  //#region src/workers/scope.ts
@@ -3233,8 +3410,12 @@ async function runWorker(opts) {
3233
3410
  if (!config.enabled) throw new Error("workers are off. Turn them on with: pai worker on\n(then the Agent-tool hook stops denying Anthropic subagents only when you do)");
3234
3411
  const logDir = workersLogDir(config);
3235
3412
  mkdirSync(logDir, { recursive: true });
3413
+ try {
3414
+ const gc = gcWorktreesThrottled(logDir)?.filter((r) => r.archive);
3415
+ if (gc?.length) process.stderr.write(`pai worker: archived ${gc.length} leftover worktree(s) (pai worker gc)\n`);
3416
+ } catch {}
3236
3417
  if (opts.className === "plan" && !opts._planner) {
3237
- const { runPlanner } = await import("./planner-D8jAr_sx.mjs");
3418
+ const { runPlanner } = await import("./planner-BG9ivPj0.mjs");
3238
3419
  return runPlanner(opts);
3239
3420
  }
3240
3421
  const parent = launchParent(opts.parent);
@@ -3445,10 +3626,21 @@ function interactiveMcpTools(explicitMcp, callerTools, projectLaunch) {
3445
3626
  * backgrounded a sub-worker and exited without waiting, the run must not
3446
3627
  * report success, whatever the process returned.
3447
3628
  */
3629
+ /** Per-git-call bound for the salvage a kill signal runs before exiting. */
3630
+ const SIGNAL_SALVAGE_MS = 5e3;
3448
3631
  async function finaliseRun(f) {
3449
3632
  const { logDir, ledger, ok, rc } = f;
3450
3633
  let { status, report } = f;
3451
3634
  const wid = status.id;
3635
+ const salvaged = f.worktree ? salvageOnExit(ledger, wid, status.label || UNLABELED, f.worktree.dir) : [];
3636
+ if (salvaged.length) {
3637
+ const msg = `WORKER-SALVAGE: committed uncommitted edits (${salvaged.join(", ")})`;
3638
+ report = {
3639
+ ...report ?? {},
3640
+ notes: [report?.notes ?? f.fallbackText, msg].filter(Boolean).join("\n"),
3641
+ result: report?.result ?? "-"
3642
+ };
3643
+ }
3452
3644
  const orphanReason = checkOrphanedChildren(logDir, wid);
3453
3645
  const succeeded = ok && !orphanReason;
3454
3646
  status.state = succeeded ? "done" : "failed";
@@ -3631,8 +3823,7 @@ async function executeRun(a) {
3631
3823
  let worktree = null;
3632
3824
  if (headless && !a.printCmd && worktreeWanted(a.worktreeFlag, {
3633
3825
  cwd,
3634
- className: a.className,
3635
- prompt: parsed.prompt
3826
+ allowedTools: parsed.allowedTools
3636
3827
  })) try {
3637
3828
  worktree = addWorktree(logDir, wid, cwd);
3638
3829
  } catch (e) {
@@ -3816,9 +4007,12 @@ async function executeRun(a) {
3816
4007
  killed: 1,
3817
4008
  label
3818
4009
  });
3819
- if (worktree) try {
3820
- recordWorktree(logDir, status, worktree, false);
3821
- } catch {}
4010
+ if (worktree) {
4011
+ salvageOnExit(ledger, wid, label, worktree.dir, SIGNAL_SALVAGE_MS);
4012
+ try {
4013
+ recordWorktree(logDir, status, worktree, false);
4014
+ } catch {}
4015
+ }
3822
4016
  cleanup();
3823
4017
  try {
3824
4018
  proc.kill();
@@ -4059,8 +4253,7 @@ async function executeCodexRun(a) {
4059
4253
  let worktree = null;
4060
4254
  if (worktreeWanted(a.worktreeFlag, {
4061
4255
  cwd,
4062
- className: a.className,
4063
- prompt: parsed.prompt
4256
+ allowedTools: parsed.allowedTools
4064
4257
  })) try {
4065
4258
  worktree = addWorktree(logDir, wid, cwd);
4066
4259
  } catch (e) {
@@ -4165,9 +4358,12 @@ async function executeCodexRun(a) {
4165
4358
  status.secs = Math.floor((Date.now() - t0) / 1e3);
4166
4359
  status.last = `killed by signal ${sig}`;
4167
4360
  saveStatus(logDir, status);
4168
- if (worktree) try {
4169
- recordWorktree(logDir, status, worktree, false);
4170
- } catch {}
4361
+ if (worktree) {
4362
+ salvageOnExit(ledger, wid, label, worktree.dir, SIGNAL_SALVAGE_MS);
4363
+ try {
4364
+ recordWorktree(logDir, status, worktree, false);
4365
+ } catch {}
4366
+ }
4171
4367
  try {
4172
4368
  proc.kill();
4173
4369
  } catch {}
@@ -4374,5 +4570,5 @@ function resultFromOutput(out) {
4374
4570
  }
4375
4571
 
4376
4572
  //#endregion
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 };
4378
- //# sourceMappingURL=run-CxxZKEZw.mjs.map
4573
+ export { appendLedger as A, loadStatus as B, workerInScope as C, mergeWorker as D, git as E, contextLabel as F, saveStatus as G, newWorkerId as H, contextPercent as I, longInlinePromptHint as J, setWorkerLabel as K, elapsedOf as L, UNLABELED as M, ageOf as N, uncommittedPaths as O, alive as P, expandMcpNames as Q, isChatPane as R, sessionTag as S, gcWorktrees as T, nowStamp as U, loadStatuses as V, ownsPid as W, shortText as X, parseRunnerArgs as Y, describeMcp as Z, 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, ledgerSummary as j, worktreesDir as k, readInbox as l, openPaneForWorker as m, runWorker as n, parseWorkerReport as o, openFollowPane as p, waitForTerminalStatus 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, isLive as z };
4574
+ //# sourceMappingURL=run-BVQZI79D.mjs.map