@tekmidian/pai 0.60.1 → 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.
- package/dist/{async-CLvuY7b-.mjs → async-DNVWtQl2.mjs} +2 -2
- package/dist/{async-CLvuY7b-.mjs.map → async-DNVWtQl2.mjs.map} +1 -1
- package/dist/{chain-CGyAIHo4.mjs → chain-CuaDawK9.mjs} +4 -4
- package/dist/{chain-CGyAIHo4.mjs.map → chain-CuaDawK9.mjs.map} +1 -1
- package/dist/cli/index.mjs +13 -13
- package/dist/cli/program.mjs +13 -13
- package/dist/{context-handover-cache-BzM7Qudn.mjs → context-handover-cache-DFNnlWv2.mjs} +4 -4
- package/dist/{context-handover-cache-BzM7Qudn.mjs.map → context-handover-cache-DFNnlWv2.mjs.map} +1 -1
- package/dist/daemon/index.mjs +10 -10
- package/dist/{daemon-B_TodsIS.mjs → daemon-Bf7Eock4.mjs} +45 -24
- package/dist/{daemon-B_TodsIS.mjs.map → daemon-Bf7Eock4.mjs.map} +1 -1
- package/dist/daemon-bJi1gDkG.mjs +19 -0
- package/dist/daemon-mcp/index.mjs +8 -10
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{factory-EUUhSSoq.mjs → factory-BozZa81q.mjs} +3 -3
- package/dist/{factory-EUUhSSoq.mjs.map → factory-BozZa81q.mjs.map} +1 -1
- package/dist/{factory-DW7BCJJT.mjs → factory-UO9_DFMN.mjs} +1 -1
- package/dist/{fallback-CjdakQVu.mjs → fallback-F0TWGVM2.mjs} +11 -13
- package/dist/{fallback-CjdakQVu.mjs.map → fallback-F0TWGVM2.mjs.map} +1 -1
- package/dist/{helpers-D9WkIf9R.mjs → helpers-Dj-B314s.mjs} +3 -2
- package/dist/{helpers-D9WkIf9R.mjs.map → helpers-Dj-B314s.mjs.map} +1 -1
- package/dist/hooks/block-sleep-poll.mjs.map +2 -2
- package/dist/hooks/context-compression-hook.mjs.map +1 -1
- package/dist/hooks/load-project-context.mjs +8513 -133
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/post-compact-inject.mjs.map +1 -1
- package/dist/hooks/route-agents-to-worker.mjs +3 -3
- package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
- package/dist/hooks/security-validator.mjs +58 -58
- package/dist/hooks/security-validator.mjs.map +3 -3
- package/dist/hooks/stop-hook.mjs +15 -15
- package/dist/hooks/stop-hook.mjs.map +3 -3
- package/dist/hooks/update-tab-on-action.mjs +14 -12
- package/dist/hooks/update-tab-on-action.mjs.map +3 -3
- package/dist/hooks/update-tab-titles.mjs +13 -9
- package/dist/hooks/update-tab-titles.mjs.map +3 -3
- package/dist/hooks/worker-guard.mjs +7859 -0
- package/dist/hooks/worker-guard.mjs.map +7 -0
- package/dist/hooks/worker-proxy.mjs +3 -3
- package/dist/hooks/worker-proxy.mjs.map +2 -2
- package/dist/hooks/worker-status-line.mjs +104 -11
- package/dist/hooks/worker-status-line.mjs.map +4 -4
- package/dist/hooks/worker-supervision.mjs +3 -3
- package/dist/hooks/worker-supervision.mjs.map +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{indexer-backend-DcDtin1O.mjs → indexer-backend-BN9rpp9O.mjs} +2 -2
- package/dist/{main-resolver-BFQfU5xo.mjs → main-resolver-BIft_FVF.mjs} +4 -4
- package/dist/{main-resolver-BFQfU5xo.mjs.map → main-resolver-BIft_FVF.mjs.map} +1 -1
- package/dist/{main-resolver-VMBENifT.mjs → main-resolver-C9-poMdi.mjs} +2 -2
- package/dist/{planner-DN967Lvm.mjs → planner-BG9ivPj0.mjs} +6 -6
- package/dist/{planner-DN967Lvm.mjs.map → planner-BG9ivPj0.mjs.map} +1 -1
- package/dist/{postgres-nvU5pShF.mjs → postgres-CxD6_Dca.mjs} +3 -3
- package/dist/{postgres-nvU5pShF.mjs.map → postgres-CxD6_Dca.mjs.map} +1 -1
- package/dist/{program-BnsjB57Q.mjs → program-BRvCzhjx.mjs} +249 -82
- package/dist/program-BRvCzhjx.mjs.map +1 -0
- package/dist/{run-FeM1_UVa.mjs → run-BVQZI79D.mjs} +610 -253
- package/dist/run-BVQZI79D.mjs.map +1 -0
- package/dist/{run-env-CEHTeIUM.mjs → run-env-BE5xyCwy.mjs} +25 -6
- package/dist/run-env-BE5xyCwy.mjs.map +1 -0
- package/dist/{server-CekGOJcp.mjs → server-BY6AedmC.mjs} +2 -2
- package/dist/{server-CekGOJcp.mjs.map → server-BY6AedmC.mjs.map} +1 -1
- package/dist/{session-keepalive-Bc9aT678.mjs → session-keepalive-DJA97K2X.mjs} +4 -4
- package/dist/{session-keepalive-Bc9aT678.mjs.map → session-keepalive-DJA97K2X.mjs.map} +1 -1
- package/dist/{sqlite-D9p5a4qe.mjs → sqlite-CfKfuggn.mjs} +2 -2
- package/dist/{sqlite-D9p5a4qe.mjs.map → sqlite-CfKfuggn.mjs.map} +1 -1
- package/dist/{tools-r-7ZG8B5.mjs → tools-B52IvaU6.mjs} +3 -3
- package/dist/{tools-qXyHFwNj.mjs → tools-DFdPbX-X.mjs} +2 -2
- package/dist/{tools-qXyHFwNj.mjs.map → tools-DFdPbX-X.mjs.map} +1 -1
- package/dist/{vault-indexer-DHYBpRZ5.mjs → vault-indexer-mCFus_AM.mjs} +2 -2
- package/dist/{vault-indexer-DHYBpRZ5.mjs.map → vault-indexer-mCFus_AM.mjs.map} +1 -1
- package/dist/{wakeup-C48hcN6O.mjs → wakeup-DO1O4udw.mjs} +2 -2
- package/dist/{wakeup-C48hcN6O.mjs.map → wakeup-DO1O4udw.mjs.map} +1 -1
- package/dist/{work-queue-worker-BL4dq5P7.mjs → work-queue-worker-BbrC3I_R.mjs} +2 -2
- package/dist/{work-queue-worker-BL4dq5P7.mjs.map → work-queue-worker-BbrC3I_R.mjs.map} +1 -1
- package/dist/work-queue-worker-z-AATZ2t.mjs +11 -0
- package/docs/commands/README.md +2 -0
- package/docs/commands/worker.md +28 -0
- package/package.json +2 -2
- package/scripts/build-hooks.mjs +6 -5
- package/scripts/build-skill-stubs.mjs +6 -5
- package/src/hooks/ts/lib/tab-title.ts +18 -0
- package/src/hooks/ts/lib/worker-guard.test.ts +188 -0
- package/src/hooks/ts/lib/worker-guard.ts +315 -0
- package/src/hooks/ts/post-tool-use/update-tab-on-action.ts +3 -20
- package/src/hooks/ts/pre-tool-use/worker-guard.ts +75 -0
- package/src/hooks/ts/session-start/load-project-context.ts +14 -0
- package/src/hooks/ts/stop/stop-hook.ts +4 -15
- package/src/hooks/ts/user-prompt/update-tab-titles.ts +2 -10
- package/dist/daemon-jviL3VVJ.mjs +0 -19
- package/dist/program-BnsjB57Q.mjs.map +0 -1
- package/dist/run-FeM1_UVa.mjs.map +0 -1
- package/dist/run-env-CEHTeIUM.mjs.map +0 -1
- package/dist/work-queue-worker-DVOVcEnO.mjs +0 -11
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { i as getRegistryBackend } from "./factory-
|
|
2
|
-
import {
|
|
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-
|
|
4
|
-
import { appendFileSync, closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
5
|
-
import { homedir, tmpdir } from "node:os";
|
|
6
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
1
|
+
import { i as getRegistryBackend } from "./factory-BozZa81q.mjs";
|
|
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
|
+
import { homedir, platform, tmpdir } from "node:os";
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
if (
|
|
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 });
|
|
@@ -597,7 +598,7 @@ function worktreePath(logDir, id) {
|
|
|
597
598
|
return join(worktreesDir(logDir), id);
|
|
598
599
|
}
|
|
599
600
|
/** Run git in `cwd` with an optional env override, trimmed stdout, stderr on failure. */
|
|
600
|
-
function runGit(cwd, args, env) {
|
|
601
|
+
function runGit(cwd, args, env, timeout = 3e4) {
|
|
601
602
|
try {
|
|
602
603
|
return execFileSync("git", [
|
|
603
604
|
"-C",
|
|
@@ -605,7 +606,7 @@ function runGit(cwd, args, env) {
|
|
|
605
606
|
...args
|
|
606
607
|
], {
|
|
607
608
|
encoding: "utf8",
|
|
608
|
-
timeout
|
|
609
|
+
timeout,
|
|
609
610
|
stdio: [
|
|
610
611
|
"ignore",
|
|
611
612
|
"pipe",
|
|
@@ -620,8 +621,8 @@ function runGit(cwd, args, env) {
|
|
|
620
621
|
}
|
|
621
622
|
}
|
|
622
623
|
/** Run git in `cwd`, returning trimmed stdout; throws with stderr on failure. */
|
|
623
|
-
function git(cwd, args) {
|
|
624
|
-
return runGit(cwd, args);
|
|
624
|
+
function git(cwd, args, timeoutMs) {
|
|
625
|
+
return runGit(cwd, args, void 0, timeoutMs);
|
|
625
626
|
}
|
|
626
627
|
/** Is `cwd` inside a git repository (a .git dir — worktrees: a .git file)? */
|
|
627
628
|
function isGitRepo(cwd) {
|
|
@@ -631,30 +632,23 @@ function isGitRepo(cwd) {
|
|
|
631
632
|
return false;
|
|
632
633
|
}
|
|
633
634
|
}
|
|
634
|
-
/**
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if (!p) return true;
|
|
642
|
-
const firstLine = p.split("\n", 1)[0];
|
|
643
|
-
if (/\b(read[- ]only|do not (modify|change|edit|write)|don'?t (modify|change|edit|write)|no changes)\b/i.test(firstLine)) return true;
|
|
644
|
-
return /^(review|read|analy[sz]e|research|summar[iy]|inspect|investigate|spotcheck|report|find|list|check|verify|describe|explain|show)\b/i.test(p);
|
|
645
|
-
}
|
|
646
|
-
/** The classes whose runs write files and therefore default to a worktree. */
|
|
647
|
-
const WORKTREE_CLASSES = [
|
|
648
|
-
"implement",
|
|
649
|
-
"complex",
|
|
650
|
-
"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"
|
|
651
642
|
];
|
|
652
|
-
/**
|
|
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? */
|
|
653
649
|
function worktreeWanted(flag, opts) {
|
|
654
650
|
if (flag !== void 0) return flag;
|
|
655
|
-
|
|
656
|
-
if (!isGitRepo(opts.cwd)) return false;
|
|
657
|
-
return !promptLooksReadonly(opts.prompt ?? "");
|
|
651
|
+
return toolsCanWrite(opts.allowedTools) && isGitRepo(opts.cwd);
|
|
658
652
|
}
|
|
659
653
|
/**
|
|
660
654
|
* Commit `cwd`'s current index + working tree (including untracked files) as
|
|
@@ -698,11 +692,6 @@ function snapshotUncommitted(cwd, id, head) {
|
|
|
698
692
|
* degrade to an in-place run.
|
|
699
693
|
*/
|
|
700
694
|
function addWorktree(logDir, id, cwd) {
|
|
701
|
-
const swept = sweepOrphanWorktrees(logDir);
|
|
702
|
-
if (swept.length) appendLedger(ledgerPath(logDir), "WORKER-NOTE", {
|
|
703
|
-
id,
|
|
704
|
-
note: `swept orphan worktree(s): ${swept.join(", ")}`
|
|
705
|
-
});
|
|
706
695
|
const dir = worktreePath(logDir, id);
|
|
707
696
|
const branch = worktreeBranch(id);
|
|
708
697
|
const head = git(cwd, ["rev-parse", "HEAD"]);
|
|
@@ -716,6 +705,7 @@ function addWorktree(logDir, id, cwd) {
|
|
|
716
705
|
branch,
|
|
717
706
|
base
|
|
718
707
|
]);
|
|
708
|
+
provisionDeps(logDir, id, cwd, dir);
|
|
719
709
|
return {
|
|
720
710
|
dir,
|
|
721
711
|
branch,
|
|
@@ -724,44 +714,60 @@ function addWorktree(logDir, id, cwd) {
|
|
|
724
714
|
};
|
|
725
715
|
}
|
|
726
716
|
/**
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
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.
|
|
733
723
|
*/
|
|
734
|
-
function
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
const
|
|
741
|
-
const
|
|
742
|
-
if (existsSync(
|
|
743
|
-
try {
|
|
744
|
-
if ((Date.now() - statSync(dir).mtimeMs) / 6e4 < minAgeMin) continue;
|
|
745
|
-
} catch {}
|
|
746
|
-
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;
|
|
747
733
|
try {
|
|
748
|
-
|
|
749
|
-
|
|
734
|
+
git(wtDir, [
|
|
735
|
+
"check-ignore",
|
|
736
|
+
"-q",
|
|
737
|
+
"node_modules"
|
|
738
|
+
]);
|
|
750
739
|
} catch {
|
|
751
|
-
|
|
740
|
+
return;
|
|
752
741
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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}`);
|
|
761
769
|
} catch {}
|
|
762
|
-
swept.push(id);
|
|
763
770
|
}
|
|
764
|
-
return swept;
|
|
765
771
|
}
|
|
766
772
|
/** Commits the branch collected on top of its base. */
|
|
767
773
|
function commitsSince(dir, base) {
|
|
@@ -787,6 +793,12 @@ function recordWorktree(logDir, status, info, ok) {
|
|
|
787
793
|
s.worktreeDir = info.dir;
|
|
788
794
|
s.worktreeBase = info.base;
|
|
789
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;
|
|
790
802
|
} else {
|
|
791
803
|
removeWorktree(s.cwd, info.dir, true);
|
|
792
804
|
try {
|
|
@@ -805,6 +817,37 @@ function recordWorktree(logDir, status, info, ok) {
|
|
|
805
817
|
saveStatus(logDir, s);
|
|
806
818
|
return s;
|
|
807
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
|
+
}
|
|
808
851
|
/** Remove a worktree directory from git's books and the filesystem. */
|
|
809
852
|
function removeWorktree(cwd, dir, force) {
|
|
810
853
|
try {
|
|
@@ -830,41 +873,86 @@ function removeWorktree(cwd, dir, force) {
|
|
|
830
873
|
* carry used; name-only, not porcelain: a worktree-only change renders as
|
|
831
874
|
* " M path" and the shared git() helper trims that leading space away.
|
|
832
875
|
*/
|
|
833
|
-
function uncommittedPaths(wtDir) {
|
|
876
|
+
function uncommittedPaths(wtDir, timeoutMs) {
|
|
834
877
|
const tracked = git(wtDir, [
|
|
835
878
|
"diff",
|
|
836
879
|
"--name-only",
|
|
837
880
|
"HEAD"
|
|
838
|
-
]).split("\n").filter(Boolean);
|
|
881
|
+
], timeoutMs).split("\n").filter(Boolean);
|
|
839
882
|
const untracked = git(wtDir, [
|
|
840
883
|
"ls-files",
|
|
841
884
|
"--others",
|
|
842
885
|
"--exclude-standard"
|
|
843
|
-
]).split("\n").filter(Boolean);
|
|
886
|
+
], timeoutMs).split("\n").filter(Boolean);
|
|
844
887
|
return [...tracked, ...untracked];
|
|
845
888
|
}
|
|
846
889
|
/**
|
|
890
|
+
* True when `wtDir`/`relPath` is a symlink whose target resolves outside
|
|
891
|
+
* `wtDir` — e.g. a worker's `node_modules -> <main checkout>/node_modules`
|
|
892
|
+
* convenience link. A `.gitignore` using `dir/` (directories only) does not
|
|
893
|
+
* ignore such a link, so it turns up as untracked; staging and merging it
|
|
894
|
+
* lets git replace the real directory on the other side with a symlink to
|
|
895
|
+
* itself. Never a candidate for salvage.
|
|
896
|
+
*/
|
|
897
|
+
function symlinkEscapesWorktree(wtDir, relPath) {
|
|
898
|
+
const full = join(wtDir, relPath);
|
|
899
|
+
let stat;
|
|
900
|
+
try {
|
|
901
|
+
stat = lstatSync(full);
|
|
902
|
+
} catch {
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
if (!stat.isSymbolicLink()) return false;
|
|
906
|
+
const target = readlinkSync(full);
|
|
907
|
+
const resolvedTarget = isAbsolute(target) ? target : resolve(dirname(full), target);
|
|
908
|
+
const realTarget = existsSync(resolvedTarget) ? realpathSync(resolvedTarget) : resolvedTarget;
|
|
909
|
+
const realWtDir = realpathSync(wtDir);
|
|
910
|
+
return realTarget !== realWtDir && !realTarget.startsWith(realWtDir + sep);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
847
913
|
* Commit a worktree's uncommitted changes to its branch so the merge carries
|
|
848
914
|
* them. `git merge` only moves committed work, so a worker that stopped
|
|
849
915
|
* without committing would lose its edits to `worktree remove` — exactly
|
|
850
|
-
* what happened live on 2026-09-17. Returns the salvaged paths,
|
|
851
|
-
* worktree is clean. A
|
|
916
|
+
* what happened live on 2026-09-17. Returns the salvaged paths, empty when
|
|
917
|
+
* the worktree is clean. A symlink pointing outside the worktree (see
|
|
918
|
+
* `symlinkEscapesWorktree`) is never staged; it is left on disk and reported
|
|
919
|
+
* as skipped instead. A failed commit throws with the worktree untouched:
|
|
852
920
|
* its edits are still on disk, so nothing is lost.
|
|
853
921
|
*/
|
|
854
|
-
function salvageUncommitted(wtDir, label) {
|
|
855
|
-
const paths = uncommittedPaths(wtDir);
|
|
856
|
-
if (!paths.length) return
|
|
922
|
+
function salvageUncommitted(wtDir, label, timeoutMs) {
|
|
923
|
+
const paths = uncommittedPaths(wtDir, timeoutMs);
|
|
924
|
+
if (!paths.length) return {
|
|
925
|
+
committed: [],
|
|
926
|
+
skipped: []
|
|
927
|
+
};
|
|
928
|
+
const skipped = [];
|
|
929
|
+
const toStage = [];
|
|
930
|
+
for (const p of paths) if (symlinkEscapesWorktree(wtDir, p)) skipped.push(`symlink ${p} -> ${readlinkSync(join(wtDir, p))} (points outside the worktree)`);
|
|
931
|
+
else toStage.push(p);
|
|
932
|
+
if (!toStage.length) return {
|
|
933
|
+
committed: [],
|
|
934
|
+
skipped
|
|
935
|
+
};
|
|
857
936
|
try {
|
|
858
|
-
git(wtDir, [
|
|
937
|
+
git(wtDir, [
|
|
938
|
+
"add",
|
|
939
|
+
"-A",
|
|
940
|
+
"--",
|
|
941
|
+
".",
|
|
942
|
+
...paths.filter((p) => !toStage.includes(p)).map((p) => `:(exclude,literal)${p}`)
|
|
943
|
+
], timeoutMs);
|
|
859
944
|
git(wtDir, [
|
|
860
945
|
"commit",
|
|
861
946
|
"-m",
|
|
862
947
|
`salvaged: ${label}`
|
|
863
|
-
]);
|
|
948
|
+
], timeoutMs);
|
|
864
949
|
} catch (e) {
|
|
865
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`);
|
|
866
951
|
}
|
|
867
|
-
return
|
|
952
|
+
return {
|
|
953
|
+
committed: toStage,
|
|
954
|
+
skipped
|
|
955
|
+
};
|
|
868
956
|
}
|
|
869
957
|
/**
|
|
870
958
|
* The dirty paths of a checkout, parsed from `git status --porcelain -z`:
|
|
@@ -915,9 +1003,14 @@ function assertNoDirtyOverlap(cwd, incoming, id, branch) {
|
|
|
915
1003
|
if (!overlap.length) return;
|
|
916
1004
|
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
1005
|
}
|
|
918
|
-
/**
|
|
1006
|
+
/**
|
|
1007
|
+
* Never remove a worktree that still holds uncommitted changes — except a
|
|
1008
|
+
* symlink escaping the worktree (see `symlinkEscapesWorktree`): it was
|
|
1009
|
+
* deliberately left uncommitted by `salvageUncommitted`, and deleting the
|
|
1010
|
+
* link itself touches nothing outside the worktree.
|
|
1011
|
+
*/
|
|
919
1012
|
function assertWorktreeClean(wtDir, id) {
|
|
920
|
-
const leftover = uncommittedPaths(wtDir);
|
|
1013
|
+
const leftover = uncommittedPaths(wtDir).filter((p) => !symlinkEscapesWorktree(wtDir, p));
|
|
921
1014
|
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
1015
|
}
|
|
923
1016
|
/** Blob sha of `path` at `rev` in `cwd`, or null when it does not exist there. */
|
|
@@ -971,7 +1064,10 @@ function mergeSnapshotWorker(logDir, id, st) {
|
|
|
971
1064
|
const snapshot = st.worktreeBase;
|
|
972
1065
|
const branch = st.branch;
|
|
973
1066
|
const worktreeDir = st.worktreeDir;
|
|
974
|
-
const
|
|
1067
|
+
const salvage = existsSync(worktreeDir) ? salvageUncommitted(worktreeDir, st.label || UNLABELED) : {
|
|
1068
|
+
committed: [],
|
|
1069
|
+
skipped: []
|
|
1070
|
+
};
|
|
975
1071
|
if ((parseInt(git(st.cwd, [
|
|
976
1072
|
"rev-list",
|
|
977
1073
|
"--count",
|
|
@@ -1030,7 +1126,10 @@ function mergeSnapshotWorker(logDir, id, st) {
|
|
|
1030
1126
|
merged: true
|
|
1031
1127
|
});
|
|
1032
1128
|
const base = `applied worker ${id}'s changes (branch ${branch}) to ${st.cwd} as uncommitted changes: ${changedPaths.join(", ")} (worktree removed, branch deleted)`;
|
|
1033
|
-
|
|
1129
|
+
const notes = [];
|
|
1130
|
+
if (salvage.committed.length) notes.push(`salvaged ${salvage.committed.length} uncommitted change(s) onto the branch first: ${salvage.committed.join(", ")}`);
|
|
1131
|
+
if (salvage.skipped.length) notes.push(`skipped ${salvage.skipped.join(", ")}`);
|
|
1132
|
+
return notes.length ? `${base}; ${notes.join("; ")}` : base;
|
|
1034
1133
|
}
|
|
1035
1134
|
/**
|
|
1036
1135
|
* `pai worker merge <id>`: salvage whatever the worker left uncommitted onto
|
|
@@ -1045,7 +1144,10 @@ function mergeWorker(logDir, id) {
|
|
|
1045
1144
|
const st = mustHaveBranch(logDir, id);
|
|
1046
1145
|
if (st.merged) return `worker ${id}: branch ${st.branch} already merged`;
|
|
1047
1146
|
if (st.worktreeSnapshot) return mergeSnapshotWorker(logDir, id, st);
|
|
1048
|
-
const
|
|
1147
|
+
const salvage = existsSync(st.worktreeDir) ? salvageUncommitted(st.worktreeDir, st.label || UNLABELED) : {
|
|
1148
|
+
committed: [],
|
|
1149
|
+
skipped: []
|
|
1150
|
+
};
|
|
1049
1151
|
if ((parseInt(git(st.cwd, [
|
|
1050
1152
|
"rev-list",
|
|
1051
1153
|
"--count",
|
|
@@ -1075,7 +1177,7 @@ function mergeWorker(logDir, id) {
|
|
|
1075
1177
|
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
1178
|
}
|
|
1077
1179
|
if (existsSync(st.worktreeDir)) assertWorktreeClean(st.worktreeDir, id);
|
|
1078
|
-
removeWorktree(st.cwd, st.worktreeDir,
|
|
1180
|
+
removeWorktree(st.cwd, st.worktreeDir, true);
|
|
1079
1181
|
let branchGone = true;
|
|
1080
1182
|
try {
|
|
1081
1183
|
git(st.cwd, [
|
|
@@ -1091,7 +1193,10 @@ function mergeWorker(logDir, id) {
|
|
|
1091
1193
|
merged: true
|
|
1092
1194
|
});
|
|
1093
1195
|
const base = `merged ${st.branch} into ${st.cwd} (worktree removed${branchGone ? ", branch deleted" : "; branch kept: git refused -d"})`;
|
|
1094
|
-
|
|
1196
|
+
const notes = [];
|
|
1197
|
+
if (salvage.committed.length) notes.push(`salvaged ${salvage.committed.length} uncommitted change(s): ${salvage.committed.join(", ")}`);
|
|
1198
|
+
if (salvage.skipped.length) notes.push(`skipped ${salvage.skipped.join(", ")}`);
|
|
1199
|
+
return notes.length ? `${base}; ${notes.join("; ")}` : base;
|
|
1095
1200
|
}
|
|
1096
1201
|
/** `pai worker discard <id>`: drop worktree and branch, keep nothing. */
|
|
1097
1202
|
function discardWorker(logDir, id) {
|
|
@@ -1154,6 +1259,139 @@ function inPlaceSystemPrompt() {
|
|
|
1154
1259
|
"Read-only git (status, diff, log, show, stash list) is fine."
|
|
1155
1260
|
].join("\n");
|
|
1156
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
|
+
}
|
|
1157
1395
|
|
|
1158
1396
|
//#endregion
|
|
1159
1397
|
//#region src/workers/scope.ts
|
|
@@ -1173,9 +1411,10 @@ function inPlaceSystemPrompt() {
|
|
|
1173
1411
|
* If neither is available, viewers fall back to "all workers".
|
|
1174
1412
|
*
|
|
1175
1413
|
* A third case: workers spawned from a Claude Code Bash tool (the
|
|
1176
|
-
* orchestrator pattern)
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1414
|
+
* orchestrator pattern) inherit the orchestrator's `ITERM_SESSION_ID`, which
|
|
1415
|
+
* identifies the orchestrator's own pane, not a pane for the sub-worker —
|
|
1416
|
+
* so it does not key the sub-worker's tab. Those record `spawnerSession`,
|
|
1417
|
+
* the orchestrator's claude session id bridged through the status line's
|
|
1179
1418
|
* session map (see below), so their tab still claims them.
|
|
1180
1419
|
*/
|
|
1181
1420
|
const AIBROKER_REGISTRY = join(homedir(), ".aibroker", "session-names.json");
|
|
@@ -1897,7 +2136,10 @@ function absoluteCliPath() {
|
|
|
1897
2136
|
}
|
|
1898
2137
|
/** Open (or only report on) the stacked follow pane of one worker. */
|
|
1899
2138
|
async function openPaneForWorker(logDir, config, wid, term) {
|
|
1900
|
-
if (workerPaneOpen(wid)) return
|
|
2139
|
+
if (workerPaneOpen(wid)) return {
|
|
2140
|
+
message: `pane for ${wid} already open`,
|
|
2141
|
+
session: null
|
|
2142
|
+
};
|
|
1901
2143
|
const uid = itermUuid(term);
|
|
1902
2144
|
const regPath = join(panesDir(logDir), `${scopeKey(term)}.json`);
|
|
1903
2145
|
const reg = loadRegistry(regPath);
|
|
@@ -1921,7 +2163,10 @@ async function openPaneForWorker(logDir, config, wid, term) {
|
|
|
1921
2163
|
opened: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
|
|
1922
2164
|
});
|
|
1923
2165
|
saveRegistry(regPath, pruned);
|
|
1924
|
-
return
|
|
2166
|
+
return {
|
|
2167
|
+
message: `pane opened for ${wid}`,
|
|
2168
|
+
session: out.slice(bar + 1)
|
|
2169
|
+
};
|
|
1925
2170
|
}
|
|
1926
2171
|
/**
|
|
1927
2172
|
* One `--check` line with the bounds of the window hosting `term`'s iTerm
|
|
@@ -2041,6 +2286,34 @@ function launchParent(explicit, env = process.env) {
|
|
|
2041
2286
|
function isWorkerId(logDir, id) {
|
|
2042
2287
|
return loadStatus(logDir, id) !== null;
|
|
2043
2288
|
}
|
|
2289
|
+
/**
|
|
2290
|
+
* A worker owns its children's results (the worker contract says so): if
|
|
2291
|
+
* `parent` exits leaving sub-workers unaccounted for, its own run must not
|
|
2292
|
+
* report success. Two shapes count, both read off `runningChildren` and the
|
|
2293
|
+
* raw statuses, never a second live-child lookup:
|
|
2294
|
+
*
|
|
2295
|
+
* - still live: left running untouched (killing another process's work
|
|
2296
|
+
* here would be its own bug) but named in the reason.
|
|
2297
|
+
* - marked "running" with a dead pid (the child never got to write its own
|
|
2298
|
+
* terminal state before its parent vanished from under it): finalised
|
|
2299
|
+
* here as failed, so `ps` stops showing a corpse as live.
|
|
2300
|
+
*
|
|
2301
|
+
* Returns the reason string for the parent's own report, or null when every
|
|
2302
|
+
* child it started is accounted for.
|
|
2303
|
+
*/
|
|
2304
|
+
function checkOrphanedChildren(logDir, parent) {
|
|
2305
|
+
const statuses = loadStatuses(logDir);
|
|
2306
|
+
const live = runningChildren(statuses, parent);
|
|
2307
|
+
const abandoned = statuses.filter((s) => s.parent === parent && s.state === "running" && !isLive(s));
|
|
2308
|
+
for (const child of abandoned) saveStatus(logDir, {
|
|
2309
|
+
...child,
|
|
2310
|
+
state: "failed",
|
|
2311
|
+
last: `parent ${parent} exited`
|
|
2312
|
+
});
|
|
2313
|
+
const ids = [...live, ...abandoned].map((s) => s.id);
|
|
2314
|
+
if (!ids.length) return null;
|
|
2315
|
+
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`;
|
|
2316
|
+
}
|
|
2044
2317
|
|
|
2045
2318
|
//#endregion
|
|
2046
2319
|
//#region src/workers/operator.ts
|
|
@@ -2283,6 +2556,16 @@ function ag2Spec() {
|
|
|
2283
2556
|
return cachedSpec;
|
|
2284
2557
|
}
|
|
2285
2558
|
/**
|
|
2559
|
+
* Strip what workers wrap around a report: one surrounding ``` fence (opening
|
|
2560
|
+
* line may carry a language tag) and a leading `[YYYY-MM-DD HH:MM]` stamp line.
|
|
2561
|
+
*/
|
|
2562
|
+
function unwrapAg2(text) {
|
|
2563
|
+
let t = (text ?? "").trim();
|
|
2564
|
+
const fenced = t.match(/^```[^\n]*\n([\s\S]*?)\n?```$/);
|
|
2565
|
+
if (fenced) t = fenced[1].trim();
|
|
2566
|
+
return t.replace(/^\[\d{4}-\d{2}-\d{2}[^\]\n]*\][ \t]*\n/, "").trim();
|
|
2567
|
+
}
|
|
2568
|
+
/**
|
|
2286
2569
|
* Parse one AG2 message: first non-empty line is the kind (T R S Q A X,
|
|
2287
2570
|
* optionally followed by trailing text — only the first character is the
|
|
2288
2571
|
* kind), later lines are `key=value` or `@n=path` symbol declarations.
|
|
@@ -2298,7 +2581,7 @@ function parseAg2Report(text) {
|
|
|
2298
2581
|
"A",
|
|
2299
2582
|
"X"
|
|
2300
2583
|
]);
|
|
2301
|
-
const lines = (text
|
|
2584
|
+
const lines = unwrapAg2(text).split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("```"));
|
|
2302
2585
|
if (!lines.length) return null;
|
|
2303
2586
|
const kind = lines[0][0];
|
|
2304
2587
|
if (!kinds.has(kind)) return null;
|
|
@@ -2342,11 +2625,17 @@ function ag2ToWorkerReport(parsed) {
|
|
|
2342
2625
|
summary: entry.slice(cut + 1).trim()
|
|
2343
2626
|
};
|
|
2344
2627
|
});
|
|
2345
|
-
if (f.t)
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2628
|
+
if (f.t) {
|
|
2629
|
+
const tokens = f.t.split(/\s+/).filter(Boolean);
|
|
2630
|
+
out.checks = tokens.length > 0 && tokens.every((tok) => /^\S+[+\-~?!]$/.test(tok)) ? tokens.map((entry) => ({
|
|
2631
|
+
name: entry.slice(0, -1),
|
|
2632
|
+
ok: passed(entry),
|
|
2633
|
+
detail: entry.slice(-1)
|
|
2634
|
+
})) : [{
|
|
2635
|
+
name: "tests",
|
|
2636
|
+
detail: f.t
|
|
2637
|
+
}];
|
|
2638
|
+
}
|
|
2350
2639
|
if (f.p) out.commands = f.p.split("|").map((s) => s.trim()).filter(Boolean);
|
|
2351
2640
|
const open = [];
|
|
2352
2641
|
if (f.x) open.push(f.x);
|
|
@@ -2372,7 +2661,7 @@ function validateAg2(text) {
|
|
|
2372
2661
|
"-",
|
|
2373
2662
|
"--json"
|
|
2374
2663
|
], {
|
|
2375
|
-
input: text,
|
|
2664
|
+
input: unwrapAg2(text),
|
|
2376
2665
|
timeout: 5e3,
|
|
2377
2666
|
stdio: [
|
|
2378
2667
|
"pipe",
|
|
@@ -2600,7 +2889,7 @@ function renderReport(c, prefix, r, cwd = "") {
|
|
|
2600
2889
|
if (has(r.checks)) {
|
|
2601
2890
|
out.push(`${prefix}${c("bold", "checks")}`);
|
|
2602
2891
|
for (const ck of r.checks) {
|
|
2603
|
-
const mark = ck.ok === false ? c("red", "✗") : c("green", "✓");
|
|
2892
|
+
const mark = ck.ok === false ? c("red", "✗") : ck.ok === true ? c("green", "✓") : c("dim", "·");
|
|
2604
2893
|
const detail = ck.detail ? c("dim", ` · ${shortText(ck.detail, 80)}`) : "";
|
|
2605
2894
|
out.push(`${prefix} ${mark} ${ck.name ?? "?"}${detail}`);
|
|
2606
2895
|
}
|
|
@@ -3012,16 +3301,22 @@ function toolResultFailed(block) {
|
|
|
3012
3301
|
const m = toolResultText(block.content).match(/Exit code (\d+)/);
|
|
3013
3302
|
return m !== null && Number(m[1]) !== 0;
|
|
3014
3303
|
}
|
|
3304
|
+
/** CLI aliases the built-in provider hands to `--model` (NATIVE_ANTHROPIC_MODELS'
|
|
3305
|
+
* values): a bare name like "sonnet", not a concrete id, so the init event's
|
|
3306
|
+
* resolved id is always the more useful thing to display. */
|
|
3307
|
+
const NATIVE_MODEL_ALIASES = new Set(Object.values(NATIVE_ANTHROPIC_MODELS));
|
|
3015
3308
|
/**
|
|
3016
3309
|
* The model the init event announces, adopted into the status when the spawn
|
|
3017
3310
|
* 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)
|
|
3019
|
-
*
|
|
3020
|
-
*
|
|
3311
|
+
* args, or a provider whose table has no entry for the capability) or named
|
|
3312
|
+
* only a CLI alias (the built-in provider's "sonnet"/"haiku", which tracks
|
|
3313
|
+
* whatever release is current — the init event says which one actually ran).
|
|
3314
|
+
* The run's real model is then only knowable from the first event it sends.
|
|
3315
|
+
* Never overwrites a concrete resolved or pinned model — that stays the
|
|
3021
3316
|
* recorded truth.
|
|
3022
3317
|
*/
|
|
3023
3318
|
function adoptInitModel(status, e) {
|
|
3024
|
-
if (status.model) return;
|
|
3319
|
+
if (status.model && !NATIVE_MODEL_ALIASES.has(status.model)) return;
|
|
3025
3320
|
const m = (e.model ?? "").trim();
|
|
3026
3321
|
if (m) status.model = m;
|
|
3027
3322
|
}
|
|
@@ -3115,8 +3410,12 @@ async function runWorker(opts) {
|
|
|
3115
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)");
|
|
3116
3411
|
const logDir = workersLogDir(config);
|
|
3117
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 {}
|
|
3118
3417
|
if (opts.className === "plan" && !opts._planner) {
|
|
3119
|
-
const { runPlanner } = await import("./planner-
|
|
3418
|
+
const { runPlanner } = await import("./planner-BG9ivPj0.mjs");
|
|
3120
3419
|
return runPlanner(opts);
|
|
3121
3420
|
}
|
|
3122
3421
|
const parent = launchParent(opts.parent);
|
|
@@ -3320,6 +3619,75 @@ function interactiveMcpTools(explicitMcp, callerTools, projectLaunch) {
|
|
|
3320
3619
|
tools: !callerTools && projectLaunch?.tools?.length ? projectLaunch.tools : []
|
|
3321
3620
|
};
|
|
3322
3621
|
}
|
|
3622
|
+
/**
|
|
3623
|
+
* The finalisation every engine shares: orphaned-children check, state, the
|
|
3624
|
+
* WORKER-END ledger line, worktree outcome, parent handoff, final rc. A worker
|
|
3625
|
+
* owns its children's results (the worker contract says so): if it
|
|
3626
|
+
* backgrounded a sub-worker and exited without waiting, the run must not
|
|
3627
|
+
* report success, whatever the process returned.
|
|
3628
|
+
*/
|
|
3629
|
+
/** Per-git-call bound for the salvage a kill signal runs before exiting. */
|
|
3630
|
+
const SIGNAL_SALVAGE_MS = 5e3;
|
|
3631
|
+
async function finaliseRun(f) {
|
|
3632
|
+
const { logDir, ledger, ok, rc } = f;
|
|
3633
|
+
let { status, report } = f;
|
|
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
|
+
}
|
|
3644
|
+
const orphanReason = checkOrphanedChildren(logDir, wid);
|
|
3645
|
+
const succeeded = ok && !orphanReason;
|
|
3646
|
+
status.state = succeeded ? "done" : "failed";
|
|
3647
|
+
status.rc = rc;
|
|
3648
|
+
status.secs = f.secs;
|
|
3649
|
+
if (orphanReason) {
|
|
3650
|
+
status.last = shortText(orphanReason, 90);
|
|
3651
|
+
report = {
|
|
3652
|
+
...report ?? {},
|
|
3653
|
+
notes: orphanReason,
|
|
3654
|
+
result: "-"
|
|
3655
|
+
};
|
|
3656
|
+
appendLedger(ledger, "WORKER-ORPHAN-CHILDREN", {
|
|
3657
|
+
id: wid,
|
|
3658
|
+
reason: orphanReason
|
|
3659
|
+
});
|
|
3660
|
+
}
|
|
3661
|
+
saveStatus(logDir, status);
|
|
3662
|
+
appendLedger(ledger, "WORKER-END", {
|
|
3663
|
+
id: wid,
|
|
3664
|
+
...f.end,
|
|
3665
|
+
rc,
|
|
3666
|
+
secs: f.secs
|
|
3667
|
+
});
|
|
3668
|
+
if (f.worktree) status = recordWorktree(logDir, status, f.worktree, ok);
|
|
3669
|
+
const early = await f.beforeHandoff?.(status, report);
|
|
3670
|
+
if (typeof early === "number") return early;
|
|
3671
|
+
const finalRc = rc !== 0 ? rc : succeeded ? 0 : 1;
|
|
3672
|
+
if (status.parent && isWorkerId(logDir, status.parent)) try {
|
|
3673
|
+
await deliverHandoff(logDir, {
|
|
3674
|
+
from: wid,
|
|
3675
|
+
to: status.parent,
|
|
3676
|
+
kind: "result",
|
|
3677
|
+
text: shortText(report?.notes ?? f.fallbackText ?? (succeeded ? "done" : "failed"), 400),
|
|
3678
|
+
data: {
|
|
3679
|
+
rc: finalRc,
|
|
3680
|
+
ok: succeeded,
|
|
3681
|
+
...status.branch ? {
|
|
3682
|
+
branch: status.branch,
|
|
3683
|
+
commits: status.commits ?? 0
|
|
3684
|
+
} : {},
|
|
3685
|
+
...report ? { report } : {}
|
|
3686
|
+
}
|
|
3687
|
+
});
|
|
3688
|
+
} catch {}
|
|
3689
|
+
return finalRc;
|
|
3690
|
+
}
|
|
3323
3691
|
async function executeImageRun(a) {
|
|
3324
3692
|
const { logDir, target, model, label, parsed } = a;
|
|
3325
3693
|
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 +3734,14 @@ async function executeImageRun(a) {
|
|
|
3366
3734
|
capability: a.capability,
|
|
3367
3735
|
...a.specPath ? { spec: a.specPath } : {}
|
|
3368
3736
|
});
|
|
3737
|
+
const imageEnd = {
|
|
3738
|
+
provider: target.providerName,
|
|
3739
|
+
mode: "headless",
|
|
3740
|
+
engine: "image",
|
|
3741
|
+
model,
|
|
3742
|
+
label,
|
|
3743
|
+
capability: a.capability
|
|
3744
|
+
};
|
|
3369
3745
|
const t0 = Date.now();
|
|
3370
3746
|
try {
|
|
3371
3747
|
const result = await runImageCapability({
|
|
@@ -3378,46 +3754,61 @@ async function executeImageRun(a) {
|
|
|
3378
3754
|
timeoutMs: a.timeoutMs
|
|
3379
3755
|
});
|
|
3380
3756
|
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
3381
|
-
status.state = "done";
|
|
3382
|
-
status.rc = 0;
|
|
3383
|
-
status.secs = secs;
|
|
3384
3757
|
status.last = `wrote ${result.path}`;
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
engine: "image",
|
|
3391
|
-
model,
|
|
3758
|
+
return await finaliseRun({
|
|
3759
|
+
logDir,
|
|
3760
|
+
ledger,
|
|
3761
|
+
status,
|
|
3762
|
+
ok: true,
|
|
3392
3763
|
rc: 0,
|
|
3393
3764
|
secs,
|
|
3394
|
-
|
|
3395
|
-
|
|
3765
|
+
end: imageEnd,
|
|
3766
|
+
report: null,
|
|
3767
|
+
beforeHandoff: () => {
|
|
3768
|
+
if (!a.quiet) process.stdout.write(JSON.stringify(result) + "\n");
|
|
3769
|
+
}
|
|
3396
3770
|
});
|
|
3397
|
-
if (!a.quiet) process.stdout.write(JSON.stringify(result) + "\n");
|
|
3398
|
-
return 0;
|
|
3399
3771
|
} catch (e) {
|
|
3400
3772
|
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
appendLedger(ledger, "WORKER-END", {
|
|
3408
|
-
id: wid,
|
|
3409
|
-
provider: target.providerName,
|
|
3410
|
-
mode: "headless",
|
|
3411
|
-
engine: "image",
|
|
3412
|
-
model,
|
|
3773
|
+
status.last = e instanceof Error ? e.message : String(e);
|
|
3774
|
+
await finaliseRun({
|
|
3775
|
+
logDir,
|
|
3776
|
+
ledger,
|
|
3777
|
+
status,
|
|
3778
|
+
ok: false,
|
|
3413
3779
|
rc: 1,
|
|
3414
3780
|
secs,
|
|
3415
|
-
|
|
3416
|
-
|
|
3781
|
+
end: imageEnd,
|
|
3782
|
+
report: null
|
|
3417
3783
|
});
|
|
3418
3784
|
throw e;
|
|
3419
3785
|
}
|
|
3420
3786
|
}
|
|
3787
|
+
/**
|
|
3788
|
+
* Follow pane, made observable on stderr (stdout stays pure JSON for
|
|
3789
|
+
* `--output-format json`): one line naming the outcome — opened, skipped
|
|
3790
|
+
* (with reason), or failed (with reason, alongside the existing PANE-FAIL
|
|
3791
|
+
* ledger line). Fire-and-forget, never blocks the worker.
|
|
3792
|
+
*/
|
|
3793
|
+
async function announcePane(logDir, config, wid, term, noPane, ledger) {
|
|
3794
|
+
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";
|
|
3795
|
+
if (skip) {
|
|
3796
|
+
process.stderr.write(`[pai worker] no pane for ${wid}: ${skip}\n`);
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3799
|
+
try {
|
|
3800
|
+
const r = await openPaneForWorker(logDir, config, wid, term);
|
|
3801
|
+
const suffix = r.session ? ` (iTerm session ${r.session})` : "";
|
|
3802
|
+
process.stderr.write(`[pai worker] pane opened for ${wid}${suffix}\n`);
|
|
3803
|
+
} catch (e) {
|
|
3804
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3805
|
+
process.stderr.write(`[pai worker] no pane for ${wid}: ${message.slice(0, 200)}\n`);
|
|
3806
|
+
appendLedger(ledger, "PANE-FAIL", {
|
|
3807
|
+
id: wid,
|
|
3808
|
+
error: message.slice(0, 200)
|
|
3809
|
+
});
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3421
3812
|
async function executeRun(a) {
|
|
3422
3813
|
const { config, logDir, target, model, label, parsed, noPane } = a;
|
|
3423
3814
|
const headless = parsed.headless;
|
|
@@ -3432,8 +3823,7 @@ async function executeRun(a) {
|
|
|
3432
3823
|
let worktree = null;
|
|
3433
3824
|
if (headless && !a.printCmd && worktreeWanted(a.worktreeFlag, {
|
|
3434
3825
|
cwd,
|
|
3435
|
-
|
|
3436
|
-
prompt: parsed.prompt
|
|
3826
|
+
allowedTools: parsed.allowedTools
|
|
3437
3827
|
})) try {
|
|
3438
3828
|
worktree = addWorktree(logDir, wid, cwd);
|
|
3439
3829
|
} catch (e) {
|
|
@@ -3549,12 +3939,7 @@ async function executeRun(a) {
|
|
|
3549
3939
|
...a.capability ? { capability: a.capability } : {},
|
|
3550
3940
|
...a.specPath ? { spec: a.specPath } : {}
|
|
3551
3941
|
});
|
|
3552
|
-
if (headless
|
|
3553
|
-
appendLedger(ledger, "PANE-FAIL", {
|
|
3554
|
-
id: wid,
|
|
3555
|
-
error: (e instanceof Error ? e.message : String(e)).slice(0, 200)
|
|
3556
|
-
});
|
|
3557
|
-
});
|
|
3942
|
+
if (headless) announcePane(logDir, config, wid, term, noPane, ledger);
|
|
3558
3943
|
const t0 = Date.now();
|
|
3559
3944
|
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
3560
3945
|
env,
|
|
@@ -3622,9 +4007,12 @@ async function executeRun(a) {
|
|
|
3622
4007
|
killed: 1,
|
|
3623
4008
|
label
|
|
3624
4009
|
});
|
|
3625
|
-
if (worktree)
|
|
3626
|
-
|
|
3627
|
-
|
|
4010
|
+
if (worktree) {
|
|
4011
|
+
salvageOnExit(ledger, wid, label, worktree.dir, SIGNAL_SALVAGE_MS);
|
|
4012
|
+
try {
|
|
4013
|
+
recordWorktree(logDir, status, worktree, false);
|
|
4014
|
+
} catch {}
|
|
4015
|
+
}
|
|
3628
4016
|
cleanup();
|
|
3629
4017
|
try {
|
|
3630
4018
|
proc.kill();
|
|
@@ -3707,9 +4095,6 @@ async function executeRun(a) {
|
|
|
3707
4095
|
const secs = Math.floor((Date.now() - t0) / 1e3);
|
|
3708
4096
|
const resultEvent = ctx.resultEvent;
|
|
3709
4097
|
const ok = runSucceeded(headless, rc, resultEvent);
|
|
3710
|
-
status.state = ok ? "done" : "failed";
|
|
3711
|
-
status.rc = rc;
|
|
3712
|
-
status.secs = secs;
|
|
3713
4098
|
if (resultEvent) status.last = shortText(resultEvent.result ?? "", 90);
|
|
3714
4099
|
if (ctx.resultReport?.notes) status.last = shortText(ctx.resultReport.notes, 90);
|
|
3715
4100
|
if (headless && a.reportFormat === "ag2" && resultEvent?.result) {
|
|
@@ -3761,66 +4146,55 @@ async function executeRun(a) {
|
|
|
3761
4146
|
});
|
|
3762
4147
|
}
|
|
3763
4148
|
}
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
model,
|
|
4149
|
+
return finaliseRun({
|
|
4150
|
+
logDir,
|
|
4151
|
+
ledger,
|
|
4152
|
+
status,
|
|
4153
|
+
ok,
|
|
3770
4154
|
rc,
|
|
3771
4155
|
secs,
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
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 } : {}
|
|
4156
|
+
end: {
|
|
4157
|
+
provider: target.providerName,
|
|
4158
|
+
mode: headless ? "headless" : "interactive",
|
|
4159
|
+
model,
|
|
4160
|
+
turns: status.turns,
|
|
4161
|
+
tools: status.tools,
|
|
4162
|
+
label
|
|
4163
|
+
},
|
|
4164
|
+
report: ctx.resultReport,
|
|
4165
|
+
fallbackText: resultEvent?.result,
|
|
4166
|
+
worktree,
|
|
4167
|
+
beforeHandoff: async (st, rep) => {
|
|
4168
|
+
if (headless && !a.quiet) printResult(parsed.outputFormat, resultEvent, rc, logDir, wid, rep, worktreeExtras(st));
|
|
4169
|
+
const resultText = resultEvent?.result ?? "";
|
|
4170
|
+
if (!ok && headless && a.target.via === "auto" && config.routing.retryOnQuota && status.turns <= 1 && status.tools === 0 && isQuotaFailure(resultText)) {
|
|
4171
|
+
setCooldown(logDir, target.providerName, config.routing.cooldownMinutes);
|
|
4172
|
+
const next = nextAutoProvider(config, logDir, target.providerName);
|
|
4173
|
+
if (next && a.reroutes < config.routing.order.length) {
|
|
4174
|
+
appendLedger(ledger, "WORKER-REROUTE", {
|
|
4175
|
+
from: target.providerName,
|
|
4176
|
+
to: next,
|
|
4177
|
+
reason: "quota"
|
|
4178
|
+
});
|
|
4179
|
+
return runWorker({
|
|
4180
|
+
providerFlag: next,
|
|
4181
|
+
label,
|
|
4182
|
+
noPane: a.noPane,
|
|
4183
|
+
mcpFlag: a.mcpFlag,
|
|
4184
|
+
cwd: a.cwd,
|
|
4185
|
+
specPath: a.specPath,
|
|
4186
|
+
parent: a.parent,
|
|
4187
|
+
stage: a.stage,
|
|
4188
|
+
claudeArgs: a.claudeArgs,
|
|
4189
|
+
onWorkerStart: a.onWorkerStart,
|
|
4190
|
+
worktreeFlag: a.worktreeFlag,
|
|
4191
|
+
className: a.className,
|
|
4192
|
+
_reroutes: a.reroutes + 1
|
|
4193
|
+
});
|
|
4194
|
+
}
|
|
3820
4195
|
}
|
|
3821
|
-
}
|
|
3822
|
-
}
|
|
3823
|
-
return finalRc;
|
|
4196
|
+
}
|
|
4197
|
+
});
|
|
3824
4198
|
}
|
|
3825
4199
|
/** The worktree fields printResult adds to a json payload, when there is one. */
|
|
3826
4200
|
function worktreeExtras(s) {
|
|
@@ -3879,8 +4253,7 @@ async function executeCodexRun(a) {
|
|
|
3879
4253
|
let worktree = null;
|
|
3880
4254
|
if (worktreeWanted(a.worktreeFlag, {
|
|
3881
4255
|
cwd,
|
|
3882
|
-
|
|
3883
|
-
prompt: parsed.prompt
|
|
4256
|
+
allowedTools: parsed.allowedTools
|
|
3884
4257
|
})) try {
|
|
3885
4258
|
worktree = addWorktree(logDir, wid, cwd);
|
|
3886
4259
|
} catch (e) {
|
|
@@ -3945,12 +4318,7 @@ async function executeCodexRun(a) {
|
|
|
3945
4318
|
id: wid,
|
|
3946
4319
|
note: `dropped for codex: ${dropped.join(", ")}`
|
|
3947
4320
|
});
|
|
3948
|
-
|
|
3949
|
-
appendLedger(ledger, "PANE-FAIL", {
|
|
3950
|
-
id: wid,
|
|
3951
|
-
error: (e instanceof Error ? e.message : String(e)).slice(0, 200)
|
|
3952
|
-
});
|
|
3953
|
-
});
|
|
4321
|
+
announcePane(logDir, config, wid, term, noPane, ledger);
|
|
3954
4322
|
const t0 = Date.now();
|
|
3955
4323
|
const proc = spawn("codex", buildCodexArgs(prompt, parsed.callerModel ? void 0 : model), {
|
|
3956
4324
|
env,
|
|
@@ -3990,9 +4358,12 @@ async function executeCodexRun(a) {
|
|
|
3990
4358
|
status.secs = Math.floor((Date.now() - t0) / 1e3);
|
|
3991
4359
|
status.last = `killed by signal ${sig}`;
|
|
3992
4360
|
saveStatus(logDir, status);
|
|
3993
|
-
if (worktree)
|
|
3994
|
-
|
|
3995
|
-
|
|
4361
|
+
if (worktree) {
|
|
4362
|
+
salvageOnExit(ledger, wid, label, worktree.dir, SIGNAL_SALVAGE_MS);
|
|
4363
|
+
try {
|
|
4364
|
+
recordWorktree(logDir, status, worktree, false);
|
|
4365
|
+
} catch {}
|
|
4366
|
+
}
|
|
3996
4367
|
try {
|
|
3997
4368
|
proc.kill();
|
|
3998
4369
|
} catch {}
|
|
@@ -4029,9 +4400,6 @@ async function executeCodexRun(a) {
|
|
|
4029
4400
|
};
|
|
4030
4401
|
writeEvent(resultEvent);
|
|
4031
4402
|
const ok = rc === 0 && !fold.isError;
|
|
4032
|
-
status.state = ok ? "done" : "failed";
|
|
4033
|
-
status.rc = rc;
|
|
4034
|
-
status.secs = secs;
|
|
4035
4403
|
status.last = shortText(report?.notes ?? finalText, 90) || (ok ? "done" : "failed");
|
|
4036
4404
|
if (report?.changed?.length) {
|
|
4037
4405
|
const verify = verifyReportChanges(report.changed, worktree?.dir ?? cwd, t0);
|
|
@@ -4047,40 +4415,29 @@ async function executeCodexRun(a) {
|
|
|
4047
4415
|
});
|
|
4048
4416
|
}
|
|
4049
4417
|
}
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
engine: "codex",
|
|
4056
|
-
model,
|
|
4418
|
+
return finaliseRun({
|
|
4419
|
+
logDir,
|
|
4420
|
+
ledger,
|
|
4421
|
+
status,
|
|
4422
|
+
ok,
|
|
4057
4423
|
rc,
|
|
4058
4424
|
secs,
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4425
|
+
end: {
|
|
4426
|
+
provider: target.providerName,
|
|
4427
|
+
mode: "headless",
|
|
4428
|
+
engine: "codex",
|
|
4429
|
+
model,
|
|
4430
|
+
turns: status.turns,
|
|
4431
|
+
tools: status.tools,
|
|
4432
|
+
label
|
|
4433
|
+
},
|
|
4434
|
+
report,
|
|
4435
|
+
fallbackText: finalText,
|
|
4436
|
+
worktree,
|
|
4437
|
+
beforeHandoff: (st, rep) => {
|
|
4438
|
+
if (!a.quiet) printResult(parsed.outputFormat, resultEvent, rc, logDir, wid, rep, worktreeExtras(st));
|
|
4439
|
+
}
|
|
4062
4440
|
});
|
|
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
4441
|
}
|
|
4085
4442
|
function printResult(fmt, resultEvent, rc, logDir, wid, report, extras) {
|
|
4086
4443
|
if (fmt === "stream-json") return;
|
|
@@ -4213,5 +4570,5 @@ function resultFromOutput(out) {
|
|
|
4213
4570
|
}
|
|
4214
4571
|
|
|
4215
4572
|
//#endregion
|
|
4216
|
-
export {
|
|
4217
|
-
//# sourceMappingURL=run-
|
|
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
|