@tekmidian/pai 0.61.0 → 0.63.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-C5oeyUg4.mjs → chain-CUfE6WYK.mjs} +2 -2
- package/dist/{chain-C5oeyUg4.mjs.map → chain-CUfE6WYK.mjs.map} +1 -1
- package/dist/cli/index.mjs +11 -11
- package/dist/cli/program.mjs +11 -11
- package/dist/{context-handover-cache-BdtJZpgv.mjs → context-handover-cache-DFNnlWv2.mjs} +2 -2
- package/dist/{context-handover-cache-BdtJZpgv.mjs.map → context-handover-cache-DFNnlWv2.mjs.map} +1 -1
- package/dist/daemon/index.mjs +8 -8
- package/dist/daemon-C8-uzOuV.mjs +19 -0
- package/dist/{daemon-DSsIEqzW.mjs → daemon-Cex3dT9f.mjs} +32 -18
- package/dist/{daemon-DSsIEqzW.mjs.map → daemon-Cex3dT9f.mjs.map} +1 -1
- package/dist/daemon-mcp/index.mjs +4 -4
- 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-DDbJVaR8.mjs → fallback-ktbR0aR7.mjs} +2 -2
- package/dist/{fallback-DDbJVaR8.mjs.map → fallback-ktbR0aR7.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/load-project-context.mjs +8519 -133
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/security-validator.mjs.map +1 -1
- package/dist/hooks/worker-guard.mjs +7859 -0
- package/dist/hooks/worker-guard.mjs.map +7 -0
- package/dist/hooks/worker-status-line.mjs +101 -8
- package/dist/hooks/worker-status-line.mjs.map +4 -4
- package/dist/index.mjs +1 -1
- package/dist/{indexer-backend-DcDtin1O.mjs → indexer-backend-BN9rpp9O.mjs} +2 -2
- package/dist/{main-resolver-DIQ6m_RA.mjs → main-resolver-BIft_FVF.mjs} +3 -3
- package/dist/{main-resolver-DIQ6m_RA.mjs.map → main-resolver-BIft_FVF.mjs.map} +1 -1
- package/dist/{main-resolver-CxOsxtOr.mjs → main-resolver-C9-poMdi.mjs} +1 -1
- package/dist/{planner-D8jAr_sx.mjs → planner-DAQMTMjP.mjs} +4 -4
- package/dist/{planner-D8jAr_sx.mjs.map → planner-DAQMTMjP.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-BTdspOOa.mjs → program-De1GVVM4.mjs} +178 -48
- package/dist/program-De1GVVM4.mjs.map +1 -0
- package/dist/{run-CxxZKEZw.mjs → run-CZVCCvXE.mjs} +353 -133
- package/dist/run-CZVCCvXE.mjs.map +1 -0
- package/dist/{session-keepalive-urk--pfa.mjs → session-keepalive-CWGzLBVU.mjs} +2 -2
- package/dist/{session-keepalive-urk--pfa.mjs.map → session-keepalive-CWGzLBVU.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-DUvPiwGO.mjs → work-queue-worker-BbrC3I_R.mjs} +2 -2
- package/dist/{work-queue-worker-DUvPiwGO.mjs.map → work-queue-worker-BbrC3I_R.mjs.map} +1 -1
- package/dist/{work-queue-worker-1_VBfI_k.mjs → work-queue-worker-z-AATZ2t.mjs} +3 -3
- package/docs/commands/README.md +4 -0
- package/docs/commands/project.md +14 -5
- package/docs/commands/projects.md +14 -5
- package/docs/commands/worker.md +27 -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/worker-guard.test.ts +188 -0
- package/src/hooks/ts/lib/worker-guard.ts +315 -0
- package/src/hooks/ts/pre-tool-use/worker-guard.ts +75 -0
- package/src/hooks/ts/session-start/load-project-context.ts +23 -0
- package/dist/daemon-MoFF4oeg.mjs +0 -19
- package/dist/program-BTdspOOa.mjs.map +0 -1
- package/dist/run-CxxZKEZw.mjs.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { i as getRegistryBackend } from "./factory-
|
|
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";
|
|
@@ -157,6 +157,72 @@ function describeMcp(config, claudeJson = CLAUDE_JSON) {
|
|
|
157
157
|
return lines;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/workers/project-config.ts
|
|
162
|
+
/**
|
|
163
|
+
* project-config.ts — the project registry's optional launch pin, read for
|
|
164
|
+
* the interactive branch of run.ts only: which MCP servers and built-in
|
|
165
|
+
* tools a project's supervisor session (`pai worker run`, no -p) loads. Set
|
|
166
|
+
* with `pai project mcp` / `pai project tools` (src/cli/commands/project/
|
|
167
|
+
* session-config.ts), stored in the same `projects.session_config` JSON
|
|
168
|
+
* column as the rest of a project's launch config. Unset = today's
|
|
169
|
+
* behavior: every server, every tool.
|
|
170
|
+
*
|
|
171
|
+
* A standalone SQL lookup rather than importing the CLI's detectProject:
|
|
172
|
+
* workers/ is a dependency of cli/, not the other way around.
|
|
173
|
+
*/
|
|
174
|
+
function asStringArray(v) {
|
|
175
|
+
if (!Array.isArray(v)) return void 0;
|
|
176
|
+
const out = v.filter((x) => typeof x === "string");
|
|
177
|
+
return out.length ? out : void 0;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The launch config of the project whose root_path is `cwd` or an ancestor
|
|
181
|
+
* of it (longest match wins) — null when no registered project covers it,
|
|
182
|
+
* or when the matching project has no mcp/tools pin set. `registry` is
|
|
183
|
+
* injectable for tests; production callers take the process-wide backend.
|
|
184
|
+
*/
|
|
185
|
+
async function projectLaunchConfig(cwd, registry) {
|
|
186
|
+
const target = resolve(cwd);
|
|
187
|
+
const rows = await (registry ?? await getRegistryBackend()).listProjectsByPathLengthDesc({ excludeArchived: true });
|
|
188
|
+
for (const row of rows) {
|
|
189
|
+
const root = resolve(row.root_path);
|
|
190
|
+
if (target !== root && !target.startsWith(root + "/")) continue;
|
|
191
|
+
if (!row.session_config) return null;
|
|
192
|
+
let parsed;
|
|
193
|
+
try {
|
|
194
|
+
parsed = JSON.parse(row.session_config);
|
|
195
|
+
} catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const mcp = asStringArray(parsed.mcp);
|
|
199
|
+
const tools = asStringArray(parsed.tools);
|
|
200
|
+
return mcp || tools ? {
|
|
201
|
+
slug: row.slug,
|
|
202
|
+
mcp,
|
|
203
|
+
tools
|
|
204
|
+
} : null;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* One line per active pin, so a pin never silently removes a server or tool:
|
|
210
|
+
* shown by the SessionStart hook and by `pai worker run`'s interactive launch.
|
|
211
|
+
* `claudeJson` is injectable for tests; read-only.
|
|
212
|
+
*/
|
|
213
|
+
function pinNotices(launch, claudeJson) {
|
|
214
|
+
if (!launch) return [];
|
|
215
|
+
const lines = [];
|
|
216
|
+
if (launch.mcp) {
|
|
217
|
+
const all = Object.keys(readMcpServers(claudeJson));
|
|
218
|
+
const loaded = all.filter((n) => launch.mcp.includes(n));
|
|
219
|
+
const missing = all.filter((n) => !launch.mcp.includes(n));
|
|
220
|
+
lines.push(`MCP pinned for ${launch.slug}: loading ${loaded.length} of ${all.length} servers; not loaded: ${missing.join(", ") || "(none)"} — \`pai project mcp --clear\` to load all`);
|
|
221
|
+
}
|
|
222
|
+
if (launch.tools) lines.push(`Tools pinned for ${launch.slug}: ${launch.tools.join(",")} — \`pai project tools --clear\``);
|
|
223
|
+
return lines;
|
|
224
|
+
}
|
|
225
|
+
|
|
160
226
|
//#endregion
|
|
161
227
|
//#region src/workers/args.ts
|
|
162
228
|
function parseRunnerArgs(argv) {
|
|
@@ -598,7 +664,7 @@ function worktreePath(logDir, id) {
|
|
|
598
664
|
return join(worktreesDir(logDir), id);
|
|
599
665
|
}
|
|
600
666
|
/** Run git in `cwd` with an optional env override, trimmed stdout, stderr on failure. */
|
|
601
|
-
function runGit(cwd, args, env) {
|
|
667
|
+
function runGit(cwd, args, env, timeout = 3e4) {
|
|
602
668
|
try {
|
|
603
669
|
return execFileSync("git", [
|
|
604
670
|
"-C",
|
|
@@ -606,7 +672,7 @@ function runGit(cwd, args, env) {
|
|
|
606
672
|
...args
|
|
607
673
|
], {
|
|
608
674
|
encoding: "utf8",
|
|
609
|
-
timeout
|
|
675
|
+
timeout,
|
|
610
676
|
stdio: [
|
|
611
677
|
"ignore",
|
|
612
678
|
"pipe",
|
|
@@ -621,8 +687,8 @@ function runGit(cwd, args, env) {
|
|
|
621
687
|
}
|
|
622
688
|
}
|
|
623
689
|
/** Run git in `cwd`, returning trimmed stdout; throws with stderr on failure. */
|
|
624
|
-
function git(cwd, args) {
|
|
625
|
-
return runGit(cwd, args);
|
|
690
|
+
function git(cwd, args, timeoutMs) {
|
|
691
|
+
return runGit(cwd, args, void 0, timeoutMs);
|
|
626
692
|
}
|
|
627
693
|
/** Is `cwd` inside a git repository (a .git dir — worktrees: a .git file)? */
|
|
628
694
|
function isGitRepo(cwd) {
|
|
@@ -632,30 +698,23 @@ function isGitRepo(cwd) {
|
|
|
632
698
|
return false;
|
|
633
699
|
}
|
|
634
700
|
}
|
|
635
|
-
/**
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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"
|
|
701
|
+
/** Tools that can write to the checkout (Bash can, via the shell). */
|
|
702
|
+
const WRITING_TOOLS = [
|
|
703
|
+
"Edit",
|
|
704
|
+
"Write",
|
|
705
|
+
"MultiEdit",
|
|
706
|
+
"NotebookEdit",
|
|
707
|
+
"Bash"
|
|
652
708
|
];
|
|
653
|
-
/**
|
|
709
|
+
/** Can a run with these --allowedTools write? No restriction at all means yes. */
|
|
710
|
+
function toolsCanWrite(allowedTools) {
|
|
711
|
+
const names = allowedTools.flatMap((e) => e.split(",")).map((s) => s.trim().replace(/\(.*$/, "")).filter(Boolean);
|
|
712
|
+
return !names.length || names.some((n) => WRITING_TOOLS.includes(n));
|
|
713
|
+
}
|
|
714
|
+
/** Should this run get a worktree? Explicit flag first, then: can it write? */
|
|
654
715
|
function worktreeWanted(flag, opts) {
|
|
655
716
|
if (flag !== void 0) return flag;
|
|
656
|
-
|
|
657
|
-
if (!isGitRepo(opts.cwd)) return false;
|
|
658
|
-
return !promptLooksReadonly(opts.prompt ?? "");
|
|
717
|
+
return toolsCanWrite(opts.allowedTools) && isGitRepo(opts.cwd);
|
|
659
718
|
}
|
|
660
719
|
/**
|
|
661
720
|
* Commit `cwd`'s current index + working tree (including untracked files) as
|
|
@@ -699,11 +758,6 @@ function snapshotUncommitted(cwd, id, head) {
|
|
|
699
758
|
* degrade to an in-place run.
|
|
700
759
|
*/
|
|
701
760
|
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
761
|
const dir = worktreePath(logDir, id);
|
|
708
762
|
const branch = worktreeBranch(id);
|
|
709
763
|
const head = git(cwd, ["rev-parse", "HEAD"]);
|
|
@@ -717,6 +771,7 @@ function addWorktree(logDir, id, cwd) {
|
|
|
717
771
|
branch,
|
|
718
772
|
base
|
|
719
773
|
]);
|
|
774
|
+
provisionDeps(logDir, id, cwd, dir);
|
|
720
775
|
return {
|
|
721
776
|
dir,
|
|
722
777
|
branch,
|
|
@@ -725,44 +780,60 @@ function addWorktree(logDir, id, cwd) {
|
|
|
725
780
|
};
|
|
726
781
|
}
|
|
727
782
|
/**
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
783
|
+
* Give the worktree its own dependencies so the worker never has to improvise
|
|
784
|
+
* (symlinking node_modules to the main checkout, building in a /tmp copy).
|
|
785
|
+
* A real node_modules directory in the main checkout is APFS-cloned
|
|
786
|
+
* (`cp -c`, copy-on-write, near instant); elsewhere the lockfile installs it.
|
|
787
|
+
* Never a symlink. Time-boxed and best effort: a failure is logged, the run
|
|
788
|
+
* goes on.
|
|
734
789
|
*/
|
|
735
|
-
function
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
const
|
|
743
|
-
if (existsSync(
|
|
744
|
-
try {
|
|
745
|
-
if ((Date.now() - statSync(dir).mtimeMs) / 6e4 < minAgeMin) continue;
|
|
746
|
-
} catch {}
|
|
747
|
-
let gitDir = null;
|
|
790
|
+
function provisionDeps(logDir, id, cwd, wtDir) {
|
|
791
|
+
const note = (n) => appendLedger(ledgerPath(logDir), "WORKER-NOTE", {
|
|
792
|
+
id,
|
|
793
|
+
note: n
|
|
794
|
+
});
|
|
795
|
+
try {
|
|
796
|
+
const main = join(git(cwd, ["rev-parse", "--show-toplevel"]), "node_modules");
|
|
797
|
+
const dest = join(wtDir, "node_modules");
|
|
798
|
+
if (!existsSync(main) || lstatSync(main).isSymbolicLink() || existsSync(dest)) return;
|
|
748
799
|
try {
|
|
749
|
-
|
|
750
|
-
|
|
800
|
+
git(wtDir, [
|
|
801
|
+
"check-ignore",
|
|
802
|
+
"-q",
|
|
803
|
+
"node_modules"
|
|
804
|
+
]);
|
|
751
805
|
} catch {
|
|
752
|
-
|
|
806
|
+
return;
|
|
753
807
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
808
|
+
const t0 = Date.now();
|
|
809
|
+
const opts = {
|
|
810
|
+
cwd: wtDir,
|
|
811
|
+
stdio: "ignore",
|
|
812
|
+
timeout: 18e4
|
|
813
|
+
};
|
|
814
|
+
if (platform() === "darwin") try {
|
|
815
|
+
execFileSync("cp", [
|
|
816
|
+
"-cR",
|
|
817
|
+
main,
|
|
818
|
+
dest
|
|
819
|
+
], opts);
|
|
820
|
+
note(`node_modules cloned (apfs) in ${Date.now() - t0}ms`);
|
|
821
|
+
return;
|
|
822
|
+
} catch {
|
|
823
|
+
rmSync(dest, {
|
|
824
|
+
recursive: true,
|
|
825
|
+
force: true
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
if (existsSync(join(wtDir, "bun.lock"))) execFileSync("bun", ["install", "--frozen-lockfile"], opts);
|
|
829
|
+
else if (existsSync(join(wtDir, "package-lock.json"))) execFileSync("npm", ["ci"], opts);
|
|
830
|
+
else return;
|
|
831
|
+
note(`node_modules installed from lockfile in ${Date.now() - t0}ms`);
|
|
832
|
+
} catch (e) {
|
|
833
|
+
try {
|
|
834
|
+
note(`node_modules provisioning failed: ${e.message}`);
|
|
762
835
|
} catch {}
|
|
763
|
-
swept.push(id);
|
|
764
836
|
}
|
|
765
|
-
return swept;
|
|
766
837
|
}
|
|
767
838
|
/** Commits the branch collected on top of its base. */
|
|
768
839
|
function commitsSince(dir, base) {
|
|
@@ -788,6 +859,12 @@ function recordWorktree(logDir, status, info, ok) {
|
|
|
788
859
|
s.worktreeDir = info.dir;
|
|
789
860
|
s.worktreeBase = info.base;
|
|
790
861
|
s.worktreeSnapshot = info.snapshot;
|
|
862
|
+
} else if (safeCommitsSince(info.dir, info.base) > 0) {
|
|
863
|
+
s.branch = info.branch;
|
|
864
|
+
s.commits = commitsSince(info.dir, info.base);
|
|
865
|
+
s.worktreeDir = info.dir;
|
|
866
|
+
s.worktreeBase = info.base;
|
|
867
|
+
s.worktreeSnapshot = info.snapshot;
|
|
791
868
|
} else {
|
|
792
869
|
removeWorktree(s.cwd, info.dir, true);
|
|
793
870
|
try {
|
|
@@ -806,6 +883,37 @@ function recordWorktree(logDir, status, info, ok) {
|
|
|
806
883
|
saveStatus(logDir, s);
|
|
807
884
|
return s;
|
|
808
885
|
}
|
|
886
|
+
function safeCommitsSince(dir, base) {
|
|
887
|
+
try {
|
|
888
|
+
return commitsSince(dir, base);
|
|
889
|
+
} catch {
|
|
890
|
+
return 0;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Salvage for an exit path (run finaliser, kill signal): commit whatever the
|
|
895
|
+
* worker left uncommitted, log WORKER-SALVAGE, never throw. Bounded by
|
|
896
|
+
* `timeoutMs` per git call. Returns the committed paths.
|
|
897
|
+
*/
|
|
898
|
+
function salvageOnExit(ledger, id, label, wtDir, timeoutMs) {
|
|
899
|
+
try {
|
|
900
|
+
const { committed, skipped } = salvageUncommitted(wtDir, label, timeoutMs);
|
|
901
|
+
if (committed.length) appendLedger(ledger, "WORKER-SALVAGE", {
|
|
902
|
+
id,
|
|
903
|
+
paths: committed.join(","),
|
|
904
|
+
skipped: skipped.length
|
|
905
|
+
});
|
|
906
|
+
return committed;
|
|
907
|
+
} catch (e) {
|
|
908
|
+
try {
|
|
909
|
+
appendLedger(ledger, "WORKER-NOTE", {
|
|
910
|
+
id,
|
|
911
|
+
note: `salvage failed: ${e.message}`
|
|
912
|
+
});
|
|
913
|
+
} catch {}
|
|
914
|
+
return [];
|
|
915
|
+
}
|
|
916
|
+
}
|
|
809
917
|
/** Remove a worktree directory from git's books and the filesystem. */
|
|
810
918
|
function removeWorktree(cwd, dir, force) {
|
|
811
919
|
try {
|
|
@@ -831,17 +939,17 @@ function removeWorktree(cwd, dir, force) {
|
|
|
831
939
|
* carry used; name-only, not porcelain: a worktree-only change renders as
|
|
832
940
|
* " M path" and the shared git() helper trims that leading space away.
|
|
833
941
|
*/
|
|
834
|
-
function uncommittedPaths(wtDir) {
|
|
942
|
+
function uncommittedPaths(wtDir, timeoutMs) {
|
|
835
943
|
const tracked = git(wtDir, [
|
|
836
944
|
"diff",
|
|
837
945
|
"--name-only",
|
|
838
946
|
"HEAD"
|
|
839
|
-
]).split("\n").filter(Boolean);
|
|
947
|
+
], timeoutMs).split("\n").filter(Boolean);
|
|
840
948
|
const untracked = git(wtDir, [
|
|
841
949
|
"ls-files",
|
|
842
950
|
"--others",
|
|
843
951
|
"--exclude-standard"
|
|
844
|
-
]).split("\n").filter(Boolean);
|
|
952
|
+
], timeoutMs).split("\n").filter(Boolean);
|
|
845
953
|
return [...tracked, ...untracked];
|
|
846
954
|
}
|
|
847
955
|
/**
|
|
@@ -877,8 +985,8 @@ function symlinkEscapesWorktree(wtDir, relPath) {
|
|
|
877
985
|
* as skipped instead. A failed commit throws with the worktree untouched:
|
|
878
986
|
* its edits are still on disk, so nothing is lost.
|
|
879
987
|
*/
|
|
880
|
-
function salvageUncommitted(wtDir, label) {
|
|
881
|
-
const paths = uncommittedPaths(wtDir);
|
|
988
|
+
function salvageUncommitted(wtDir, label, timeoutMs) {
|
|
989
|
+
const paths = uncommittedPaths(wtDir, timeoutMs);
|
|
882
990
|
if (!paths.length) return {
|
|
883
991
|
committed: [],
|
|
884
992
|
skipped: []
|
|
@@ -894,14 +1002,16 @@ function salvageUncommitted(wtDir, label) {
|
|
|
894
1002
|
try {
|
|
895
1003
|
git(wtDir, [
|
|
896
1004
|
"add",
|
|
1005
|
+
"-A",
|
|
897
1006
|
"--",
|
|
898
|
-
|
|
899
|
-
|
|
1007
|
+
".",
|
|
1008
|
+
...paths.filter((p) => !toStage.includes(p)).map((p) => `:(exclude,literal)${p}`)
|
|
1009
|
+
], timeoutMs);
|
|
900
1010
|
git(wtDir, [
|
|
901
1011
|
"commit",
|
|
902
1012
|
"-m",
|
|
903
1013
|
`salvaged: ${label}`
|
|
904
|
-
]);
|
|
1014
|
+
], timeoutMs);
|
|
905
1015
|
} catch (e) {
|
|
906
1016
|
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
1017
|
}
|
|
@@ -1215,6 +1325,139 @@ function inPlaceSystemPrompt() {
|
|
|
1215
1325
|
"Read-only git (status, diff, log, show, stash list) is fine."
|
|
1216
1326
|
].join("\n");
|
|
1217
1327
|
}
|
|
1328
|
+
const GC_STAMP = "gc-last";
|
|
1329
|
+
/**
|
|
1330
|
+
* Archive worktrees of finished workers older than `olderThanHours` and
|
|
1331
|
+
* status-less orphan directories: uncommitted state is salvaged onto the
|
|
1332
|
+
* branch, the branch is x, the worktree is
|
|
1333
|
+
* removed. Nothing is deleted without a salvage first; a failed salvage skips
|
|
1334
|
+
* that worktree. Running workers are never touched. `dryRun` reports only.
|
|
1335
|
+
*/
|
|
1336
|
+
function gcWorktrees(logDir, olderThanHours = 24, dryRun = false, now = Date.now()) {
|
|
1337
|
+
const cutoff = now - olderThanHours * 36e5;
|
|
1338
|
+
const out = [];
|
|
1339
|
+
const root = worktreesDir(logDir);
|
|
1340
|
+
const known = /* @__PURE__ */ new Set();
|
|
1341
|
+
const archive = (id, dir, repo, st) => {
|
|
1342
|
+
const target = `refs/pai-archive/${id}`;
|
|
1343
|
+
if (dryRun) return void out.push({
|
|
1344
|
+
id,
|
|
1345
|
+
archive: target
|
|
1346
|
+
});
|
|
1347
|
+
try {
|
|
1348
|
+
if (!repo) throw new Error("not a git worktree");
|
|
1349
|
+
const label = `archive: leftover state of worker ${id}`;
|
|
1350
|
+
if (uncommittedPaths(dir).length) {
|
|
1351
|
+
const res = salvageUncommitted(dir, label);
|
|
1352
|
+
if (res.committed.length) git(dir, [
|
|
1353
|
+
"commit",
|
|
1354
|
+
"--amend",
|
|
1355
|
+
"-q",
|
|
1356
|
+
"-m",
|
|
1357
|
+
label
|
|
1358
|
+
]);
|
|
1359
|
+
if (res.skipped.length && uncommittedPaths(dir).some((p) => !symlinkEscapesWorktree(dir, p))) throw new Error("uncommitted paths remain");
|
|
1360
|
+
}
|
|
1361
|
+
const src = worktreeBranch(id);
|
|
1362
|
+
let hasBranch = true;
|
|
1363
|
+
try {
|
|
1364
|
+
git(repo, [
|
|
1365
|
+
"rev-parse",
|
|
1366
|
+
"--verify",
|
|
1367
|
+
"-q",
|
|
1368
|
+
`refs/heads/${src}`
|
|
1369
|
+
]);
|
|
1370
|
+
} catch {
|
|
1371
|
+
hasBranch = false;
|
|
1372
|
+
}
|
|
1373
|
+
const tip = git(dir, ["rev-parse", "HEAD"]);
|
|
1374
|
+
if (hasBranch) git(repo, [
|
|
1375
|
+
"merge-base",
|
|
1376
|
+
"--is-ancestor",
|
|
1377
|
+
`refs/heads/${src}`,
|
|
1378
|
+
tip
|
|
1379
|
+
]);
|
|
1380
|
+
git(repo, [
|
|
1381
|
+
"update-ref",
|
|
1382
|
+
target,
|
|
1383
|
+
tip
|
|
1384
|
+
]);
|
|
1385
|
+
git(repo, [
|
|
1386
|
+
"worktree",
|
|
1387
|
+
"remove",
|
|
1388
|
+
"--force",
|
|
1389
|
+
dir
|
|
1390
|
+
]);
|
|
1391
|
+
if (hasBranch) git(repo, [
|
|
1392
|
+
"branch",
|
|
1393
|
+
"-D",
|
|
1394
|
+
src
|
|
1395
|
+
]);
|
|
1396
|
+
if (st) saveStatus(logDir, {
|
|
1397
|
+
...st,
|
|
1398
|
+
branch: null,
|
|
1399
|
+
worktreeDir: null,
|
|
1400
|
+
archived: target
|
|
1401
|
+
});
|
|
1402
|
+
appendLedger(ledgerPath(logDir), "WORKER-GC", {
|
|
1403
|
+
id,
|
|
1404
|
+
archive: target
|
|
1405
|
+
});
|
|
1406
|
+
out.push({
|
|
1407
|
+
id,
|
|
1408
|
+
archive: target
|
|
1409
|
+
});
|
|
1410
|
+
} catch (e) {
|
|
1411
|
+
out.push({
|
|
1412
|
+
id,
|
|
1413
|
+
skipped: e.message.split("\n")[0]
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
for (const st of loadStatuses(logDir)) {
|
|
1418
|
+
known.add(st.id);
|
|
1419
|
+
if (!st.worktreeDir || !existsSync(st.worktreeDir) || isLive(st)) continue;
|
|
1420
|
+
let end = 0;
|
|
1421
|
+
try {
|
|
1422
|
+
end = statSync(statusPath(logDir, st.id)).mtimeMs;
|
|
1423
|
+
} catch {
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
if (end > cutoff) continue;
|
|
1427
|
+
archive(st.id, st.worktreeDir, st.cwd, st);
|
|
1428
|
+
}
|
|
1429
|
+
if (existsSync(root)) for (const ent of readdirSync(root, { withFileTypes: true })) {
|
|
1430
|
+
if (!ent.isDirectory() || known.has(ent.name) || existsSync(statusPath(logDir, ent.name))) continue;
|
|
1431
|
+
const dir = join(root, ent.name);
|
|
1432
|
+
try {
|
|
1433
|
+
if (statSync(dir).mtimeMs > cutoff) continue;
|
|
1434
|
+
} catch {
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
let repo = null;
|
|
1438
|
+
try {
|
|
1439
|
+
const raw = git(dir, ["rev-parse", "--git-common-dir"]);
|
|
1440
|
+
repo = isAbsolute(raw) ? raw : resolve(dir, raw);
|
|
1441
|
+
} catch {
|
|
1442
|
+
repo = null;
|
|
1443
|
+
}
|
|
1444
|
+
archive(ent.name, dir, repo, null);
|
|
1445
|
+
}
|
|
1446
|
+
if (!dryRun) for (const repo of new Set(loadStatuses(logDir).map((s) => s.cwd))) try {
|
|
1447
|
+
git(repo, ["worktree", "prune"]);
|
|
1448
|
+
} catch {}
|
|
1449
|
+
return out;
|
|
1450
|
+
}
|
|
1451
|
+
/** Run `gcWorktrees` at most once per `everyMin` minutes (stamp file in the logDir). */
|
|
1452
|
+
function gcWorktreesThrottled(logDir, everyMin = 60, now = Date.now()) {
|
|
1453
|
+
const stamp = join(logDir, GC_STAMP);
|
|
1454
|
+
try {
|
|
1455
|
+
if (now - statSync(stamp).mtimeMs < everyMin * 6e4) return null;
|
|
1456
|
+
} catch {}
|
|
1457
|
+
mkdirSync(logDir, { recursive: true });
|
|
1458
|
+
writeFileSync(stamp, String(now), "utf8");
|
|
1459
|
+
return gcWorktrees(logDir, 24, false, now);
|
|
1460
|
+
}
|
|
1218
1461
|
|
|
1219
1462
|
//#endregion
|
|
1220
1463
|
//#region src/workers/scope.ts
|
|
@@ -2782,54 +3025,6 @@ function relShort(p, cwd) {
|
|
|
2782
3025
|
return r && !r.startsWith("..") ? r : p;
|
|
2783
3026
|
}
|
|
2784
3027
|
|
|
2785
|
-
//#endregion
|
|
2786
|
-
//#region src/workers/project-config.ts
|
|
2787
|
-
/**
|
|
2788
|
-
* project-config.ts — the project registry's optional launch pin, read for
|
|
2789
|
-
* the interactive branch of run.ts only: which MCP servers and built-in
|
|
2790
|
-
* tools a project's supervisor session (`pai worker run`, no -p) loads. Set
|
|
2791
|
-
* with `pai project mcp` / `pai project tools` (src/cli/commands/project/
|
|
2792
|
-
* session-config.ts), stored in the same `projects.session_config` JSON
|
|
2793
|
-
* column as the rest of a project's launch config. Unset = today's
|
|
2794
|
-
* behavior: every server, every tool.
|
|
2795
|
-
*
|
|
2796
|
-
* A standalone SQL lookup rather than importing the CLI's detectProject:
|
|
2797
|
-
* workers/ is a dependency of cli/, not the other way around.
|
|
2798
|
-
*/
|
|
2799
|
-
function asStringArray(v) {
|
|
2800
|
-
if (!Array.isArray(v)) return void 0;
|
|
2801
|
-
const out = v.filter((x) => typeof x === "string");
|
|
2802
|
-
return out.length ? out : void 0;
|
|
2803
|
-
}
|
|
2804
|
-
/**
|
|
2805
|
-
* The launch config of the project whose root_path is `cwd` or an ancestor
|
|
2806
|
-
* of it (longest match wins) — null when no registered project covers it,
|
|
2807
|
-
* or when the matching project has no mcp/tools pin set. `registry` is
|
|
2808
|
-
* injectable for tests; production callers take the process-wide backend.
|
|
2809
|
-
*/
|
|
2810
|
-
async function projectLaunchConfig(cwd, registry) {
|
|
2811
|
-
const target = resolve(cwd);
|
|
2812
|
-
const rows = await (registry ?? await getRegistryBackend()).listProjectsByPathLengthDesc({ excludeArchived: true });
|
|
2813
|
-
for (const row of rows) {
|
|
2814
|
-
const root = resolve(row.root_path);
|
|
2815
|
-
if (target !== root && !target.startsWith(root + "/")) continue;
|
|
2816
|
-
if (!row.session_config) return null;
|
|
2817
|
-
let parsed;
|
|
2818
|
-
try {
|
|
2819
|
-
parsed = JSON.parse(row.session_config);
|
|
2820
|
-
} catch {
|
|
2821
|
-
return null;
|
|
2822
|
-
}
|
|
2823
|
-
const mcp = asStringArray(parsed.mcp);
|
|
2824
|
-
const tools = asStringArray(parsed.tools);
|
|
2825
|
-
return mcp || tools ? {
|
|
2826
|
-
mcp,
|
|
2827
|
-
tools
|
|
2828
|
-
} : null;
|
|
2829
|
-
}
|
|
2830
|
-
return null;
|
|
2831
|
-
}
|
|
2832
|
-
|
|
2833
3028
|
//#endregion
|
|
2834
3029
|
//#region src/workers/codex.ts
|
|
2835
3030
|
/**
|
|
@@ -3233,8 +3428,12 @@ async function runWorker(opts) {
|
|
|
3233
3428
|
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
3429
|
const logDir = workersLogDir(config);
|
|
3235
3430
|
mkdirSync(logDir, { recursive: true });
|
|
3431
|
+
try {
|
|
3432
|
+
const gc = gcWorktreesThrottled(logDir)?.filter((r) => r.archive);
|
|
3433
|
+
if (gc?.length) process.stderr.write(`pai worker: archived ${gc.length} leftover worktree(s) (pai worker gc)\n`);
|
|
3434
|
+
} catch {}
|
|
3236
3435
|
if (opts.className === "plan" && !opts._planner) {
|
|
3237
|
-
const { runPlanner } = await import("./planner-
|
|
3436
|
+
const { runPlanner } = await import("./planner-DAQMTMjP.mjs");
|
|
3238
3437
|
return runPlanner(opts);
|
|
3239
3438
|
}
|
|
3240
3439
|
const parent = launchParent(opts.parent);
|
|
@@ -3445,10 +3644,21 @@ function interactiveMcpTools(explicitMcp, callerTools, projectLaunch) {
|
|
|
3445
3644
|
* backgrounded a sub-worker and exited without waiting, the run must not
|
|
3446
3645
|
* report success, whatever the process returned.
|
|
3447
3646
|
*/
|
|
3647
|
+
/** Per-git-call bound for the salvage a kill signal runs before exiting. */
|
|
3648
|
+
const SIGNAL_SALVAGE_MS = 5e3;
|
|
3448
3649
|
async function finaliseRun(f) {
|
|
3449
3650
|
const { logDir, ledger, ok, rc } = f;
|
|
3450
3651
|
let { status, report } = f;
|
|
3451
3652
|
const wid = status.id;
|
|
3653
|
+
const salvaged = f.worktree ? salvageOnExit(ledger, wid, status.label || UNLABELED, f.worktree.dir) : [];
|
|
3654
|
+
if (salvaged.length) {
|
|
3655
|
+
const msg = `WORKER-SALVAGE: committed uncommitted edits (${salvaged.join(", ")})`;
|
|
3656
|
+
report = {
|
|
3657
|
+
...report ?? {},
|
|
3658
|
+
notes: [report?.notes ?? f.fallbackText, msg].filter(Boolean).join("\n"),
|
|
3659
|
+
result: report?.result ?? "-"
|
|
3660
|
+
};
|
|
3661
|
+
}
|
|
3452
3662
|
const orphanReason = checkOrphanedChildren(logDir, wid);
|
|
3453
3663
|
const succeeded = ok && !orphanReason;
|
|
3454
3664
|
status.state = succeeded ? "done" : "failed";
|
|
@@ -3631,8 +3841,7 @@ async function executeRun(a) {
|
|
|
3631
3841
|
let worktree = null;
|
|
3632
3842
|
if (headless && !a.printCmd && worktreeWanted(a.worktreeFlag, {
|
|
3633
3843
|
cwd,
|
|
3634
|
-
|
|
3635
|
-
prompt: parsed.prompt
|
|
3844
|
+
allowedTools: parsed.allowedTools
|
|
3636
3845
|
})) try {
|
|
3637
3846
|
worktree = addWorktree(logDir, wid, cwd);
|
|
3638
3847
|
} catch (e) {
|
|
@@ -3673,6 +3882,12 @@ async function executeRun(a) {
|
|
|
3673
3882
|
const explicitMcp = [...a.mcpFlag ? [a.mcpFlag] : [], ...parsed.mcp];
|
|
3674
3883
|
const projectLaunch = await projectLaunchConfig(cwd);
|
|
3675
3884
|
const picked = interactiveMcpTools(explicitMcp, parsed.callerTools, projectLaunch);
|
|
3885
|
+
const applied = projectLaunch && {
|
|
3886
|
+
...projectLaunch,
|
|
3887
|
+
mcp: explicitMcp.length ? void 0 : projectLaunch.mcp,
|
|
3888
|
+
tools: picked.tools.length ? projectLaunch.tools : void 0
|
|
3889
|
+
};
|
|
3890
|
+
for (const line of pinNotices(applied)) console.error(line);
|
|
3676
3891
|
if (picked.mcpNames.length) mcpArgs = [
|
|
3677
3892
|
"--strict-mcp-config",
|
|
3678
3893
|
"--mcp-config",
|
|
@@ -3816,9 +4031,12 @@ async function executeRun(a) {
|
|
|
3816
4031
|
killed: 1,
|
|
3817
4032
|
label
|
|
3818
4033
|
});
|
|
3819
|
-
if (worktree)
|
|
3820
|
-
|
|
3821
|
-
|
|
4034
|
+
if (worktree) {
|
|
4035
|
+
salvageOnExit(ledger, wid, label, worktree.dir, SIGNAL_SALVAGE_MS);
|
|
4036
|
+
try {
|
|
4037
|
+
recordWorktree(logDir, status, worktree, false);
|
|
4038
|
+
} catch {}
|
|
4039
|
+
}
|
|
3822
4040
|
cleanup();
|
|
3823
4041
|
try {
|
|
3824
4042
|
proc.kill();
|
|
@@ -4059,8 +4277,7 @@ async function executeCodexRun(a) {
|
|
|
4059
4277
|
let worktree = null;
|
|
4060
4278
|
if (worktreeWanted(a.worktreeFlag, {
|
|
4061
4279
|
cwd,
|
|
4062
|
-
|
|
4063
|
-
prompt: parsed.prompt
|
|
4280
|
+
allowedTools: parsed.allowedTools
|
|
4064
4281
|
})) try {
|
|
4065
4282
|
worktree = addWorktree(logDir, wid, cwd);
|
|
4066
4283
|
} catch (e) {
|
|
@@ -4165,9 +4382,12 @@ async function executeCodexRun(a) {
|
|
|
4165
4382
|
status.secs = Math.floor((Date.now() - t0) / 1e3);
|
|
4166
4383
|
status.last = `killed by signal ${sig}`;
|
|
4167
4384
|
saveStatus(logDir, status);
|
|
4168
|
-
if (worktree)
|
|
4169
|
-
|
|
4170
|
-
|
|
4385
|
+
if (worktree) {
|
|
4386
|
+
salvageOnExit(ledger, wid, label, worktree.dir, SIGNAL_SALVAGE_MS);
|
|
4387
|
+
try {
|
|
4388
|
+
recordWorktree(logDir, status, worktree, false);
|
|
4389
|
+
} catch {}
|
|
4390
|
+
}
|
|
4171
4391
|
try {
|
|
4172
4392
|
proc.kill();
|
|
4173
4393
|
} catch {}
|
|
@@ -4374,5 +4594,5 @@ function resultFromOutput(out) {
|
|
|
4374
4594
|
}
|
|
4375
4595
|
|
|
4376
4596
|
//#endregion
|
|
4377
|
-
export {
|
|
4378
|
-
//# sourceMappingURL=run-
|
|
4597
|
+
export { expandMcpNames as $, ledgerSummary as A, loadStatuses as B, discardWorker as C, uncommittedPaths as D, mergeWorker as E, contextPercent as F, setWorkerLabel as G, nowStamp as H, elapsedOf as I, parseRunnerArgs as J, waitForTerminalStatus as K, isChatPane as L, ageOf as M, alive as N, worktreesDir as O, contextLabel as P, describeMcp as Q, isLive as R, workerInScope as S, git as T, ownsPid as U, newWorkerId as V, saveStatus as W, pinNotices as X, shortText as Y, projectLaunchConfig as Z, itermForClaudeSession as _, parseWorkerReport as a, sessionMapPath as b, readInbox as c, checkPaneForWorker as d, openFollowPane as f, currentTabKey as g, markControlsHeld as h, usageContextTokens as i, UNLABELED as j, appendLedger as k, sayToWorker as l, clickrControlsArgv as m, runWorker as n, renderReport as o, openPaneForWorker as p, longInlinePromptHint as q, testProvider as r, handoffFromInside as s, printResult as t, workerDepth as u, itermUuid as v, gcWorktrees as w, sessionTag as x, resolveSession as y, loadStatus as z };
|
|
4598
|
+
//# sourceMappingURL=run-CZVCCvXE.mjs.map
|