larkin 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +7 -1
  3. package/artifacts/release/THIRD_PARTY_NOTICES.txt +1 -100
  4. package/dist/agent/context-prompt.cjs +19 -17
  5. package/dist/agent/context-prompt.mjs +18 -16
  6. package/dist/runtime/pi-state-dir.cjs +105 -0
  7. package/dist/runtime/pi-state-dir.mjs +94 -0
  8. package/dist/runtime/pi-tmux-extension.mjs +137 -0
  9. package/dist/runtime/pi-tmux-injection.mjs +49 -0
  10. package/dist/runtime/pi-tmux.bundle.js +600 -0
  11. package/dist/runtime/pi-tmux.mjs +408 -0
  12. package/dist/runtime/runtime-adapters.mjs +34 -114
  13. package/dist/runtime/runtime-host.cjs +3 -413
  14. package/dist/runtime/runtime-host.mjs +2 -412
  15. package/package.json +2 -8
  16. package/dist/runtime/pi-bash-timeout-extension.mjs +0 -60
  17. package/dist/runtime/pi-bash-timeout-injection.mjs +0 -54
  18. package/dist/runtime/pi-bash-timeout.bundle.js +0 -43
  19. package/dist/runtime/pi-extension-api.mjs +0 -1
  20. package/dist/runtime/pi-subagent-injection.mjs +0 -290
  21. package/dist/runtime/pi-subagent-ledger.cjs +0 -632
  22. package/dist/runtime/pi-subagent-ledger.mjs +0 -594
  23. package/dist/runtime/pi-subagent-record-watchdog-injection.mjs +0 -43
  24. package/dist/runtime/pi-subagent-record-watchdog.bundle.js +0 -183
  25. package/dist/runtime/pi-subagent-record-watchdog.mjs +0 -46
  26. package/dist/runtime/pi-subagents-notification.mjs +0 -175
  27. package/dist/runtime/pi-subagents.bundle.js +0 -9110
  28. package/dist/runtime/pi-supervised-command-extension.mjs +0 -72
  29. package/dist/runtime/pi-supervised-command.bundle.js +0 -4735
  30. package/dist/runtime/pi-supervised-command.mjs +0 -420
  31. package/patches/@tintinweb%2Fpi-subagents@0.14.3.patch +0 -945
@@ -1,60 +0,0 @@
1
- import { createBashToolDefinition } from "@earendil-works/pi-coding-agent";
2
- /**
3
- * pi bash 工具超时护栏扩展。
4
- *
5
- * 通过用户安装的 Pi 的 `pi --extension/-e <bundle>` 注入,覆盖内置 `bash` 工具:
6
- * - 无论模型是否传 `timeout`,都强制收窄到 <= MAX_BASH_SECONDS(默认 60s),
7
- * 避免单个 bash 调用无限期占住整个 agent 回合(issue #55)。
8
- * - 超时时 pi 会杀掉整个进程树(不残留卡死子进程,issue #56 的进程堆积),
9
- * 并在返回的错误里明确提示:长任务必须改用后台 subagent
10
- * (Agent(run_in_background: true),来自 pi-subagents 扩展)。
11
- *
12
- * 这是 pi 扩展,运行在 Pi RPC 子进程内部;build.mjs 把 `@earendil-works/pi-*`
13
- * 标为 external,由 host Pi 的 loader 在运行时提供模块。
14
- */
15
- /** 前台 bash 的硬性上限(秒)。issue #55 建议默认 60s。 */
16
- const MAX_BASH_SECONDS = 60;
17
- /** 解析生效超时:默认 60,允许用 LARKIN_PI_BASH_TIMEOUT_SECONDS 调低(用于快速 eval),硬上限 60。 */
18
- function effectiveBashTimeout() {
19
- const raw = Number.parseInt(process.env.LARKIN_PI_BASH_TIMEOUT_SECONDS || "", 10);
20
- if (Number.isInteger(raw) && raw > 0)
21
- return Math.min(raw, MAX_BASH_SECONDS);
22
- return MAX_BASH_SECONDS;
23
- }
24
- export default function (pi) {
25
- const cwd = process.cwd();
26
- // Host bash tool definition (keeps renderCall/renderResult/system-prompt metadata).
27
- const builtin = createBashToolDefinition(cwd);
28
- const MAX = effectiveBashTimeout();
29
- const SUBAGENT_GUIDANCE = `This command exceeded the ${MAX}s foreground hard limit. ` +
30
- "Long-running or deploy-style work MUST run in a background subagent instead: " +
31
- "call Agent({ prompt, description, run_in_background: true }), report the returned agent id, and end the turn; " +
32
- "a completion notification arrives automatically. " +
33
- "Do NOT retry this command in the foreground, and do not use nohup / '&' / disown shell background jobs " +
34
- "(they bypass subagent isolation and die with this run).";
35
- pi.registerTool({
36
- ...builtin,
37
- name: "bash",
38
- label: "bash",
39
- description: builtin.description,
40
- async execute(toolCallId, params, signal, onUpdate, ctx) {
41
- const requested = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout : MAX;
42
- // 模型显式设置 timeout > 上限 → 它已知这是长任务。立即报错引导改用后台 subagent,
43
- // 不真的跑(省掉白等 + 避免产生部分副作用)。
44
- if (requested > MAX) {
45
- throw new Error(`timeout:${requested} exceeds the ${MAX}s foreground hard limit. ${SUBAGENT_GUIDANCE}`);
46
- }
47
- const timeout = Math.min(requested, MAX);
48
- try {
49
- return await builtin.execute(toolCallId, { ...params, timeout }, signal, onUpdate, ctx);
50
- }
51
- catch (error) {
52
- const message = error instanceof Error ? error.message : String(error);
53
- if (/timed out after/i.test(message)) {
54
- throw new Error(`${message}\n\n${SUBAGENT_GUIDANCE}`);
55
- }
56
- throw error;
57
- }
58
- },
59
- });
60
- }
@@ -1,54 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { piVersionSupportsSubagents, probeExternalPiVersion } from "./pi-subagent-injection.mjs";
5
- /**
6
- * pi-bash-timeout 扩展注入(bash 60s 超时护栏,issue #55/#56)。
7
- *
8
- * 分发:构建期把 src/runtime/pi-bash-timeout-extension.ts bundle 成单文件
9
- * `dist/runtime/pi-bash-timeout.bundle.js`(pi-* 包 external),运行时仅向 external
10
- * (用户 pi CLI)通过 `pi --extension/-e` 显式注入,不碰用户 ~/.pi 配置。
11
- * Builtin Pi 直接接收静态 factory。与 pi-subagents 共享同一个 external 版本门槛。
12
- */
13
- /** 把 embedded bundle 落盘到 <configDir>/providers/pi/extensions/(0700/0600)。无 embedded 资产返回 null。 */
14
- export function materializeEmbeddedPiBashTimeoutBundle(configDir) {
15
- const embedded = globalThis.__LARKIN_EMBEDDED_PI_BASH_TIMEOUT_BUNDLE__;
16
- if (!embedded || !configDir)
17
- return null;
18
- const dir = path.join(path.resolve(configDir), "providers", "pi", "extensions");
19
- const target = path.join(dir, "pi-bash-timeout.bundle.js");
20
- try {
21
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
22
- fs.chmodSync(dir, 0o700);
23
- if (!fs.existsSync(target) || fs.readFileSync(target, "utf8") !== embedded) {
24
- const temporary = `${target}.${process.pid}.tmp`;
25
- fs.writeFileSync(temporary, embedded, { mode: 0o600, flag: "wx" });
26
- fs.renameSync(temporary, target);
27
- fs.chmodSync(target, 0o600);
28
- }
29
- return target;
30
- }
31
- catch {
32
- return null;
33
- }
34
- }
35
- /** 构建产物单文件路径;standalone 二进制回退到 embedded 资产落盘。 */
36
- export function bundledPiBashTimeoutExtensionPath(configDir) {
37
- try {
38
- const url = new URL("./pi-bash-timeout.bundle.js", import.meta.url);
39
- const resolved = fileURLToPath(url);
40
- if (fs.existsSync(resolved))
41
- return resolved;
42
- }
43
- catch {
44
- /* fall through to embedded */
45
- }
46
- return materializeEmbeddedPiBashTimeoutBundle(configDir);
47
- }
48
- /** External Pi 需满足 >= 0.80.0;返回 bundle 路径或 null。 */
49
- export function resolvePiBashTimeoutExtensionArg(input, probeVersion = () => probeExternalPiVersion(input.piCommand, input.env), resolveBundle = () => bundledPiBashTimeoutExtensionPath(input.env.LARKIN_CONFIG_DIR)) {
50
- const bundle = resolveBundle();
51
- if (!bundle)
52
- return null;
53
- return piVersionSupportsSubagents(probeVersion()) ? bundle : null;
54
- }
@@ -1,43 +0,0 @@
1
- // @bun
2
- // src/runtime/pi-bash-timeout-extension.ts
3
- import { createBashToolDefinition } from "@earendil-works/pi-coding-agent";
4
- var MAX_BASH_SECONDS = 60;
5
- function effectiveBashTimeout() {
6
- const raw = Number.parseInt(process.env.LARKIN_PI_BASH_TIMEOUT_SECONDS || "", 10);
7
- if (Number.isInteger(raw) && raw > 0)
8
- return Math.min(raw, MAX_BASH_SECONDS);
9
- return MAX_BASH_SECONDS;
10
- }
11
- function pi_bash_timeout_extension_default(pi) {
12
- const cwd = process.cwd();
13
- const builtin = createBashToolDefinition(cwd);
14
- const MAX = effectiveBashTimeout();
15
- const SUBAGENT_GUIDANCE = `This command exceeded the ${MAX}s foreground hard limit. ` + "Long-running or deploy-style work MUST run in a background subagent instead: " + "call Agent({ prompt, description, run_in_background: true }), report the returned agent id, and end the turn; " + "a completion notification arrives automatically. " + "Do NOT retry this command in the foreground, and do not use nohup / '&' / disown shell background jobs " + "(they bypass subagent isolation and die with this run).";
16
- pi.registerTool({
17
- ...builtin,
18
- name: "bash",
19
- label: "bash",
20
- description: builtin.description,
21
- async execute(toolCallId, params, signal, onUpdate, ctx) {
22
- const requested = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout : MAX;
23
- if (requested > MAX) {
24
- throw new Error(`timeout:${requested} exceeds the ${MAX}s foreground hard limit. ${SUBAGENT_GUIDANCE}`);
25
- }
26
- const timeout = Math.min(requested, MAX);
27
- try {
28
- return await builtin.execute(toolCallId, { ...params, timeout }, signal, onUpdate, ctx);
29
- } catch (error) {
30
- const message = error instanceof Error ? error.message : String(error);
31
- if (/timed out after/i.test(message)) {
32
- throw new Error(`${message}
33
-
34
- ${SUBAGENT_GUIDANCE}`);
35
- }
36
- throw error;
37
- }
38
- }
39
- });
40
- }
41
- export {
42
- pi_bash_timeout_extension_default as default
43
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,290 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { spawnSync } from "node:child_process";
4
- import { fileURLToPath } from "node:url";
5
- /**
6
- * pi-subagents 扩展注入。
7
- *
8
- * 分发:构建期把 @tintinweb/pi-subagents bundle 成单文件
9
- * `dist/runtime/pi-subagents.bundle.js`(pi-* 包 external),运行时仅向 external
10
- * (用户 pi CLI)通过 `pi --extension/-e` 显式注入。可只读用户 ~/.pi/agent
11
- * 的非密钥设置/扩展元数据以避免重复注册 FATAL;不得读、复制或写 auth.json,
12
- * 也不得写入 ~/.pi。
13
- *
14
- * 版本门槛:pi-subagents peerDependency 要求 @earendil-works/pi-* >= 0.80.0;
15
- * external 的 pi 版本低于 0.80 时不注入(降级为无 subagent 能力)。
16
- */
17
- /**
18
- * 把 embedded bundle 落盘到 <configDir>/providers/pi/extensions/pi-subagents.bundle.js(0700/0600)。
19
- * 无 embedded 资产或 configDir 缺失时返回 null。
20
- */
21
- function rejectSymlinkPath(target) {
22
- let current = path.resolve(target);
23
- for (;;) {
24
- if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
25
- throw new Error("bundle path must not contain a symlink");
26
- }
27
- const parent = path.dirname(current);
28
- if (parent === current)
29
- break;
30
- current = parent;
31
- }
32
- }
33
- function backupRegularFile(target) {
34
- if (!fs.existsSync(target))
35
- return null;
36
- const stat = fs.lstatSync(target);
37
- if (stat.isSymbolicLink())
38
- throw new Error("bundle target must not be a symlink");
39
- return { content: fs.readFileSync(target), mode: stat.mode & 0o777 };
40
- }
41
- function restoreRegularFile(target, backup) {
42
- if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) {
43
- throw new Error("bundle target must not be a symlink");
44
- }
45
- if (!backup) {
46
- if (fs.existsSync(target))
47
- fs.unlinkSync(target);
48
- return;
49
- }
50
- fs.writeFileSync(target, backup.content, { mode: backup.mode });
51
- fs.chmodSync(target, backup.mode);
52
- }
53
- export function materializeEmbeddedPiSubagentBundle(configDir) {
54
- const embedded = globalThis.__LARKIN_EMBEDDED_PI_SUBAGENTS_BUNDLE__;
55
- const supervised = globalThis.__LARKIN_EMBEDDED_PI_SUPERVISED_COMMAND_BUNDLE__;
56
- if (!embedded || !configDir)
57
- return null;
58
- if (!supervised)
59
- throw new Error("supervised command bundle is missing");
60
- const resolvedConfig = path.resolve(configDir);
61
- fs.mkdirSync(resolvedConfig, { recursive: true, mode: 0o700 });
62
- const canonical = fs.realpathSync(resolvedConfig);
63
- const dir = path.join(canonical, "providers", "pi", "extensions");
64
- const subTarget = path.join(dir, "pi-subagents.bundle.js");
65
- const supTarget = path.join(dir, "pi-supervised-command.bundle.js");
66
- const lockPath = path.join(dir, ".materialize.lock");
67
- let lock;
68
- try {
69
- rejectSymlinkPath(dir);
70
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
71
- rejectSymlinkPath(dir);
72
- fs.chmodSync(dir, 0o700);
73
- lock = fs.openSync(lockPath, "wx", 0o600);
74
- const oldSub = backupRegularFile(subTarget);
75
- const oldSup = backupRegularFile(supTarget);
76
- try {
77
- writePrivateBundle(dir, "pi-subagents.bundle.js", embedded);
78
- writePrivateBundle(dir, "pi-supervised-command.bundle.js", supervised);
79
- return subTarget;
80
- }
81
- catch (error) {
82
- restoreRegularFile(subTarget, oldSub);
83
- restoreRegularFile(supTarget, oldSup);
84
- throw error;
85
- }
86
- }
87
- catch (error) {
88
- if (error instanceof Error && error.message.includes("supervised command bundle is missing"))
89
- throw error;
90
- return null;
91
- }
92
- finally {
93
- if (lock !== undefined) {
94
- try {
95
- fs.closeSync(lock);
96
- }
97
- catch { /* ignore */ }
98
- try {
99
- fs.unlinkSync(lockPath);
100
- }
101
- catch { /* ignore */ }
102
- }
103
- }
104
- }
105
- function writePrivateBundle(dir, name, embedded) {
106
- const target = path.join(dir, name);
107
- rejectSymlinkPath(target);
108
- if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) {
109
- throw new Error("bundle target must not be a symlink");
110
- }
111
- if (!fs.existsSync(target) || fs.readFileSync(target, "utf8") !== embedded) {
112
- const temporary = `${target}.${process.pid}.tmp`;
113
- fs.writeFileSync(temporary, embedded, { mode: 0o600, flag: "wx" });
114
- fs.renameSync(temporary, target);
115
- }
116
- fs.chmodSync(target, 0o600);
117
- return target;
118
- }
119
- export function materializeEmbeddedPiSupervisedCommandBundle(dirOrConfig) {
120
- const embedded = globalThis.__LARKIN_EMBEDDED_PI_SUPERVISED_COMMAND_BUNDLE__;
121
- if (!embedded || !dirOrConfig)
122
- return null;
123
- const dir = dirOrConfig.endsWith("extensions") ? dirOrConfig : path.join(path.resolve(dirOrConfig), "providers", "pi", "extensions");
124
- try {
125
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
126
- fs.chmodSync(dir, 0o700);
127
- return writePrivateBundle(dir, "pi-supervised-command.bundle.js", embedded);
128
- }
129
- catch {
130
- return null;
131
- }
132
- }
133
- /**
134
- * 构建产物单文件路径。优先返回 dist/runtime/pi-subagents.bundle.js(源码/编译形态);
135
- * standalone 二进制没有该文件,回退到 embedded 资产落盘(<configDir>/providers/pi/extensions/)。
136
- */
137
- export function bundledPiSubagentExtensionPath(configDir) {
138
- try {
139
- const url = new URL("./pi-subagents.bundle.js", import.meta.url);
140
- const resolved = fileURLToPath(url);
141
- if (fs.existsSync(resolved))
142
- return resolved;
143
- }
144
- catch {
145
- /* fall through to embedded */
146
- }
147
- return materializeEmbeddedPiSubagentBundle(configDir);
148
- }
149
- /** 解析 `pi --version` 输出中的主版本号,无法解析返回 null。 */
150
- export function parsePiVersion(output) {
151
- if (!output)
152
- return null;
153
- const match = output.match(/(\d+)\.(\d+)\.\d+/);
154
- if (!match)
155
- return null;
156
- return { major: Number(match[1]), minor: Number(match[2]) };
157
- }
158
- /** 是否满足 pi-subagents 的 pi 版本门槛(>= 0.80.0)。 */
159
- export function piVersionSupportsSubagents(version) {
160
- if (!version)
161
- return false;
162
- return version.major > 0 || (version.major === 0 && version.minor >= 80);
163
- }
164
- const externalPiVersionProbeCache = new Map();
165
- function externalPiVersionProbeKey(piCommand, env) {
166
- return JSON.stringify([piCommand, env.PATH ?? ""]);
167
- }
168
- /** 测试用:清空进程内 `pi --version` 探测缓存。 */
169
- export function resetExternalPiVersionProbeCache() {
170
- externalPiVersionProbeCache.clear();
171
- }
172
- /** 解析 external `pi --version`;无法确认版本时视为不支持,不注入。按 piCommand+PATH 缓存。 */
173
- export function probeExternalPiVersion(piCommand, env) {
174
- const key = externalPiVersionProbeKey(piCommand, env);
175
- if (externalPiVersionProbeCache.has(key))
176
- return externalPiVersionProbeCache.get(key) ?? null;
177
- const result = spawnSync(piCommand, ["--version"], { env, encoding: "utf8", timeout: 5_000, maxBuffer: 64 * 1024 });
178
- const version = result.error || result.status !== 0
179
- ? null
180
- : parsePiVersion(String(result.stdout || result.stderr || ""));
181
- externalPiVersionProbeCache.set(key, version);
182
- return version;
183
- }
184
- /**
185
- * 用户 pi 是否已自行安装 pi-subagents(settings.json packages 或包目录)。
186
- * 已装时 Larkin 不再 -e 注入,避免同名工具(Agent/get_subagent_result/steer_subagent)
187
- * 重复注册导致 pi 扩展加载 FATAL(pi 对 duplicate tool registration 是硬失败)。
188
- *
189
- * Owner 边界:只读非密钥兼容信息(settings.json / package 清单 / 扩展源码标记)。
190
- * 不得打开或写入用户 Pi home 里的 auth.json,也不得往 ~/.pi 写任何文件。
191
- */
192
- const BOUNDED_WAIT_CAPABILITY = "larkin-pi-subagents-bounded-wait-v1";
193
- const SUPERVISED_COMMAND_CAPABILITY = "larkin-pi-supervised-command-v1";
194
- function assertRegularFile(file) {
195
- const stat = fs.lstatSync(file);
196
- if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) {
197
- throw new Error("user Pi path is not a regular file");
198
- }
199
- }
200
- function readRegularFile(file) {
201
- assertRegularFile(file);
202
- return fs.readFileSync(file, "utf8");
203
- }
204
- function isRealpathInside(root, candidate) {
205
- const rootReal = fs.realpathSync(root);
206
- const candidateReal = fs.realpathSync(candidate);
207
- const prefix = rootReal.endsWith(path.sep) ? rootReal : `${rootReal}${path.sep}`;
208
- return candidateReal.startsWith(prefix);
209
- }
210
- /** 只读包根内的单链接常规文件;绝对路径、逃逸、符号链接、硬链接一律视为不可验证。 */
211
- function readContainedPackageEntry(packageRoot, entry) {
212
- if (path.isAbsolute(entry))
213
- throw new Error("absolute Pi extension entry");
214
- const root = path.resolve(packageRoot);
215
- const resolved = path.resolve(root, entry);
216
- const lexicalPrefix = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
217
- if (resolved !== root && !resolved.startsWith(lexicalPrefix))
218
- throw new Error("Pi extension entry escaped package root");
219
- assertRegularFile(resolved);
220
- if (!isRealpathInside(root, resolved))
221
- throw new Error("Pi extension entry escaped package root");
222
- return fs.readFileSync(resolved, "utf8");
223
- }
224
- function userPiSubagentsWaitCapability(env) {
225
- const agentDir = env.PI_CODING_AGENT_DIR || path.join(env.HOME || process.env.HOME || "", ".pi", "agent");
226
- try {
227
- let configured = false;
228
- const settingsFile = path.join(agentDir, "settings.json");
229
- if (fs.existsSync(settingsFile)) {
230
- const settings = JSON.parse(readRegularFile(settingsFile));
231
- const packages = settings?.packages;
232
- if (Array.isArray(packages)) {
233
- configured = packages.some((entry) => typeof entry === "string" && /pi-subagents/i.test(entry));
234
- }
235
- }
236
- // settings.json 可能未登记但包目录已存在(残留/手动安装)。
237
- // Split token: repo contract forbids the literal package-manager word in sources.
238
- const packageRoots = [
239
- path.join(agentDir, "n" + "pm", "node_modules", "@tintinweb", "pi-subagents"),
240
- path.join(agentDir, "node_modules", "@tintinweb", "pi-subagents"),
241
- ];
242
- const installedRoot = packageRoots.find((root) => fs.existsSync(root));
243
- if (!configured && !installedRoot)
244
- return "absent";
245
- if (!installedRoot)
246
- return "unbounded";
247
- const packageManifest = JSON.parse(readRegularFile(path.join(installedRoot, "package.json")));
248
- const extensions = packageManifest.pi?.extensions;
249
- if (!Array.isArray(extensions) || extensions.length === 0
250
- || !extensions.every((entry) => typeof entry === "string" && entry.length > 0)) {
251
- return "unbounded";
252
- }
253
- // Pi executes the entries declared by the package manifest. Do not trust a
254
- // marker in a build artifact when the manifest points Pi at another file.
255
- const hasCapability = extensions.every((entry) => {
256
- const source = readContainedPackageEntry(installedRoot, entry);
257
- return source.includes(BOUNDED_WAIT_CAPABILITY) && source.includes(SUPERVISED_COMMAND_CAPABILITY);
258
- });
259
- return hasCapability ? "bounded" : "unbounded";
260
- }
261
- catch {
262
- // Unreadable or escaped user extension state is not safe to treat as bounded.
263
- return "unbounded";
264
- }
265
- }
266
- export function userPiAlreadyHasSubagentsExtension(env) {
267
- return userPiSubagentsWaitCapability(env) !== "absent";
268
- }
269
- export function userPiSubagentsHasBoundedWaitCapability(env) {
270
- return userPiSubagentsWaitCapability(env) === "bounded";
271
- }
272
- export function resolvePiSubagentExtensionArg(input, probeVersion = () => probeExternalPiVersion(input.piCommand, input.env), resolveBundle = () => bundledPiSubagentExtensionPath(input.env.LARKIN_CONFIG_DIR)) {
273
- // A user-installed extension wins only when it advertises the exact bounded
274
- // wait capability. Check this before the bundle path so a missing Larkin
275
- // asset cannot silently leave an unbounded user extension active.
276
- if (userPiAlreadyHasSubagentsExtension(input.env)) {
277
- if (!userPiSubagentsHasBoundedWaitCapability(input.env)) {
278
- throw new Error("[larkin] WARNING: refusing external Pi because the user-installed " +
279
- "pi-subagents extension is unbounded or unverifiable; remove it or install " +
280
- "a version advertising larkin-pi-subagents-bounded-wait-v1 and larkin-pi-supervised-command-v1.");
281
- }
282
- return null;
283
- }
284
- // resolveBundle is injectable so unit tests stay environment-independent
285
- // (no dependency on build artifacts or the filesystem).
286
- const bundle = resolveBundle();
287
- if (!bundle)
288
- return null;
289
- return piVersionSupportsSubagents(probeVersion()) ? bundle : null;
290
- }