pi-better-harness 0.15.3 → 0.16.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/node_modules/pi-better-goal/README.md +1 -1
- package/node_modules/pi-better-goal/package.json +1 -1
- package/node_modules/pi-better-goal/src/index.ts +2 -2
- package/node_modules/pi-better-sandbox/package.json +1 -1
- package/node_modules/pi-better-sandbox/shared-task-sandbox.ts +43 -32
- package/node_modules/pi-better-subagents/README.md +7 -4
- package/node_modules/pi-better-subagents/config.ts +28 -1
- package/node_modules/pi-better-subagents/delegation.ts +2 -0
- package/node_modules/pi-better-subagents/docs/usage.md +33 -11
- package/node_modules/pi-better-subagents/index.ts +80 -22
- package/node_modules/pi-better-subagents/package.json +1 -1
- package/node_modules/pi-better-subagents/settings-page.ts +128 -0
- package/node_modules/pi-better-subagents/shared-task-sandbox.ts +43 -32
- package/package.json +4 -4
|
@@ -17,7 +17,7 @@ Use `pi-better-goal` when a Pi session should keep an explicit objective visible
|
|
|
17
17
|
- Background work that finishes while an `ask_user_question` is pending is handed to the agent right after the answer.
|
|
18
18
|
- A compact goal widget that does not replace Pi's footer.
|
|
19
19
|
- Background activity tracking for subagents and other registered providers.
|
|
20
|
-
- A progress-aware follow-up loop with ten identical no-progress retries and linear backoff (
|
|
20
|
+
- A progress-aware follow-up loop with ten identical no-progress retries and linear backoff (60s through 600s by default), followed by a hold recoverable with `/goal resume`. Delays saturate at Node's timer limit, and observed background drains reset progress before callback turns can cancel the wake. Pi's network retry policy remains independent.
|
|
21
21
|
- An observable-progress stall state for active goals.
|
|
22
22
|
|
|
23
23
|
## Skill-Owned Workflows
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
} from "./types.js";
|
|
51
51
|
|
|
52
52
|
const POLL_INTERVAL_MS = 2_000;
|
|
53
|
-
const DEFAULT_IDLE_CONTINUATION_DELAY_MS =
|
|
53
|
+
const DEFAULT_IDLE_CONTINUATION_DELAY_MS = 60_000;
|
|
54
54
|
const DEFAULT_MAX_NO_PROGRESS_RETRIES = 10;
|
|
55
55
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
56
56
|
const WAKE_DISABLED =
|
|
@@ -124,7 +124,7 @@ function parseDurationEnv(raw: string | undefined, fallback: number): number {
|
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
126
|
* Linear backoff for no-progress retries: each identical outcome waits one more
|
|
127
|
-
* grace period than the last (
|
|
127
|
+
* grace period than the last (60s, 120s, 180s, ... by default).
|
|
128
128
|
*/
|
|
129
129
|
export function idleContinuationDelay(noProgressRetries: number, baseMs = IDLE_CONTINUATION_DELAY_MS): number {
|
|
130
130
|
return Math.min(MAX_TIMER_DELAY_MS, baseMs * (1 + Math.max(0, noProgressRetries)));
|
|
@@ -6,6 +6,8 @@ const { createBashToolDefinition, createReadToolDefinition, createWriteToolDefin
|
|
|
6
6
|
createEditToolDefinition, createLocalBashOperations, getShellConfig } = PiCodingAgent;
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { unlink } from "node:fs/promises";
|
|
9
11
|
import { accessSync, constants, lstatSync, mkdirSync, mkdtempSync, readlinkSync, writeFileSync } from "node:fs";
|
|
10
12
|
import { homedir, tmpdir } from "node:os";
|
|
11
13
|
import { basename, dirname, isAbsolute, join, parse, resolve, sep } from "node:path";
|
|
@@ -117,42 +119,51 @@ export function createTaskBashOperations(
|
|
|
117
119
|
const scratch = plan.policy.runtimeWrite?.[0];
|
|
118
120
|
const taskEnv = { ...process.env, ...options.env,
|
|
119
121
|
...(scratch ? { TMPDIR: scratch, TMP: scratch, TEMP: scratch } : {}) };
|
|
120
|
-
const wrapped = maybeBuildSandboxCommand({
|
|
121
|
-
policy: plan.policy, profilePath: plan.profilePath,
|
|
122
|
-
execPath: "/usr/bin/env", execArgs: ["-i", "--", ...Object.entries(taskEnv)
|
|
123
|
-
.filter((entry): entry is [string, string] => entry[1] !== undefined)
|
|
124
|
-
.map(([key, value]) => `${key}=${value}`), shell.shell, ...shell.args, command],
|
|
125
|
-
}, { sandboxEnabled: true, explicitSandbox: true });
|
|
126
|
-
if (!wrapped) throw new Error("Sandbox: no task execution backend is available.");
|
|
127
|
-
for (const line of wrapped.notices ?? []) options.onData(Buffer.from(`${line}\n`));
|
|
128
122
|
if (options.signal?.aborted) throw new Error("aborted");
|
|
129
123
|
if (options.timeout !== undefined && (!Number.isFinite(options.timeout) || options.timeout <= 0 || options.timeout * 1000 > 2147483647)) {
|
|
130
124
|
throw new Error("Invalid timeout: must be a positive, supported number of seconds");
|
|
131
125
|
}
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
126
|
+
// A sibling launch must never rewrite a profile another process is reading.
|
|
127
|
+
const profilePath = `${plan.profilePath}.${randomUUID()}.bash.sb`;
|
|
128
|
+
try {
|
|
129
|
+
const wrapped = maybeBuildSandboxCommand({
|
|
130
|
+
policy: plan.policy, profilePath,
|
|
131
|
+
execPath: "/usr/bin/env", execArgs: ["-i", "--", ...Object.entries(taskEnv)
|
|
132
|
+
.filter((entry): entry is [string, string] => entry[1] !== undefined)
|
|
133
|
+
.map(([key, value]) => `${key}=${value}`), shell.shell, ...shell.args, command],
|
|
134
|
+
}, { sandboxEnabled: true, explicitSandbox: true });
|
|
135
|
+
if (!wrapped) throw new Error("Sandbox: no task execution backend is available.");
|
|
136
|
+
for (const line of wrapped.notices ?? []) options.onData(Buffer.from(`${line}\n`));
|
|
137
|
+
if (options.signal?.aborted) throw new Error("aborted");
|
|
138
|
+
// No shell or caller-controlled loader environment runs before the boundary.
|
|
139
|
+
return await new Promise<{ exitCode: number | null }>((resolve, reject) => {
|
|
140
|
+
const child = spawn(wrapped.file, wrapped.fileArgs, { cwd, detached: true,
|
|
141
|
+
env: { PATH: "/usr/bin:/bin", HOME: plan.policy.home }, stdio: ["ignore", "pipe", "pipe"] });
|
|
142
|
+
let timedOut = false;
|
|
143
|
+
const kill = () => { if (child.pid) { try { process.kill(-child.pid, "SIGKILL"); } catch { child.kill("SIGKILL"); } } };
|
|
144
|
+
const timer = options.timeout === undefined ? undefined : setTimeout(() => { timedOut = true; kill(); }, options.timeout * 1000);
|
|
145
|
+
if (child.pid) trackDetachedChildPid(child.pid);
|
|
146
|
+
const cleanup = () => {
|
|
147
|
+
if (child.pid) untrackDetachedChildPid(child.pid);
|
|
148
|
+
if (timer) clearTimeout(timer);
|
|
149
|
+
options.signal?.removeEventListener("abort", kill);
|
|
150
|
+
};
|
|
151
|
+
child.stdout.on("data", options.onData);
|
|
152
|
+
child.stderr.on("data", options.onData);
|
|
153
|
+
options.signal?.addEventListener("abort", kill, { once: true });
|
|
154
|
+
if (options.signal?.aborted) kill();
|
|
155
|
+
void waitForChildProcess(child).then((exitCode: number | null) => {
|
|
156
|
+
cleanup();
|
|
157
|
+
if (options.signal?.aborted) reject(new Error("aborted"));
|
|
158
|
+
else if (timedOut) reject(new Error(`timeout:${options.timeout}`));
|
|
159
|
+
else resolve({ exitCode });
|
|
160
|
+
}, (error: unknown) => { cleanup(); reject(error); });
|
|
161
|
+
});
|
|
162
|
+
} finally {
|
|
163
|
+
await unlink(profilePath).catch((error: NodeJS.ErrnoException) => {
|
|
164
|
+
if (error.code !== "ENOENT") throw error;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
156
167
|
},
|
|
157
168
|
};
|
|
158
169
|
}
|
|
@@ -36,10 +36,13 @@ Linux confinement requires a usable `bubblewrap` backend and Pi SDK 0.82.1 or ne
|
|
|
36
36
|
## Delegation Modes
|
|
37
37
|
|
|
38
38
|
`config.json` sets `delegationMode` to `manual`, `adaptive` (default), or
|
|
39
|
-
`coordinator`. `/subagents`
|
|
40
|
-
|
|
41
|
-
session
|
|
42
|
-
|
|
39
|
+
`coordinator`. `/subagents settings` (or `/subagents`) opens a settings page
|
|
40
|
+
for delegation mode and the concurrent-subagent cap (default 4). Edits apply to
|
|
41
|
+
this session; Save as defaults persists both, and Reset to defaults clears
|
|
42
|
+
session overrides. `/subagents mode manual|adaptive|coordinator` and
|
|
43
|
+
`/subagents cap <number>` are command equivalents; `/subagents save` persists
|
|
44
|
+
both. Lowering the cap blocks new single and batch launches without stopping
|
|
45
|
+
running work. The navigator's `main` row retains its mode-only controls:
|
|
43
46
|
`m` cycles the session mode, `ctrl+s` saves that default, and `x`
|
|
44
47
|
does not stop it. Manual delegates only on explicit user or workflow request, even with a
|
|
45
48
|
plan. Adaptive delegates substantial independent work when useful. Coordinator
|
|
@@ -21,7 +21,7 @@ export interface SubagentConfig extends TimingSettings {
|
|
|
21
21
|
delegationMode?: DelegationMode | null;
|
|
22
22
|
defaultModel?: string | null;
|
|
23
23
|
defaultTools?: string | null;
|
|
24
|
-
/**
|
|
24
|
+
/** Default concurrency cap; /subagents cap overrides it for the current session. */
|
|
25
25
|
maxConcurrent?: number | null;
|
|
26
26
|
/**
|
|
27
27
|
* Tool name → extension package(s) that provide it. Drives which extension
|
|
@@ -74,6 +74,14 @@ export interface SubagentConfig extends TimingSettings {
|
|
|
74
74
|
/** Concurrency cap when config.json sets none. */
|
|
75
75
|
export const DEFAULT_MAX_CONCURRENT = 4;
|
|
76
76
|
|
|
77
|
+
export function isConcurrencyCap(value: unknown): value is number {
|
|
78
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function normalizeConcurrencyCap(value: unknown): number {
|
|
82
|
+
return isConcurrencyCap(value) ? value : DEFAULT_MAX_CONCURRENT;
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
/** Built-in default tool set when config.json sets nothing. */
|
|
78
86
|
export const SAFE_DEFAULT_TOOLS = "read, bash, edit, write, web_search, web_fetch";
|
|
79
87
|
/** Safe default for a hermetic (clean) child where extension tools don't exist. */
|
|
@@ -123,6 +131,25 @@ export function writeDelegationMode(mode: DelegationMode, path = configPath()):
|
|
|
123
131
|
if (path === configPath()) cached = current;
|
|
124
132
|
}
|
|
125
133
|
|
|
134
|
+
/** Save human-facing defaults together, preserving unrelated configuration. */
|
|
135
|
+
export function writeSubagentSettings(settings: { delegationMode: DelegationMode; maxConcurrent: number }, path = configPath()): void {
|
|
136
|
+
if (!isDelegationMode(settings.delegationMode)) throw new Error("Invalid delegation mode.");
|
|
137
|
+
if (!isConcurrencyCap(settings.maxConcurrent)) throw new Error("Concurrent subagents must be a positive whole number.");
|
|
138
|
+
let current: Record<string, unknown> = {};
|
|
139
|
+
try {
|
|
140
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf-8"));
|
|
141
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Config must be a JSON object.");
|
|
142
|
+
current = parsed as Record<string, unknown>;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
145
|
+
}
|
|
146
|
+
Object.assign(current, settings);
|
|
147
|
+
const pending = `${path}.${process.pid}.tmp`;
|
|
148
|
+
writeFileSync(pending, `${JSON.stringify(current, null, 2)}\n`);
|
|
149
|
+
renameSync(pending, path);
|
|
150
|
+
if (path === configPath()) cached = current as SubagentConfig;
|
|
151
|
+
}
|
|
152
|
+
|
|
126
153
|
/** Normalize a comma/space tool list to pi's bare comma form: "a, b" → "a,b". */
|
|
127
154
|
export function normalizeTools(list: string): string {
|
|
128
155
|
return list.split(",").map((t) => t.trim()).filter(Boolean).join(",");
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type DelegationMode = "manual" | "adaptive" | "coordinator";
|
|
2
2
|
|
|
3
|
+
export const DELEGATION_MODES = ["manual", "adaptive", "coordinator"] as const;
|
|
4
|
+
|
|
3
5
|
export const DELEGATION_MODE_REQUEST = "pi-better-subagents:delegation-mode-request";
|
|
4
6
|
|
|
5
7
|
export function isDelegationMode(value: unknown): value is DelegationMode {
|
|
@@ -16,14 +16,33 @@ launch is the result · completion triggers fetch · the foreground never blocks
|
|
|
16
16
|
|
|
17
17
|
Set `delegationMode` in this package's `config.json` to `manual`, `adaptive`, or
|
|
18
18
|
`coordinator` (default `adaptive`). This controls foreground delegation guidance,
|
|
19
|
-
not child permissions or catalog definitions. `/subagents`
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
not child permissions or catalog definitions. `/subagents settings` (or simply
|
|
20
|
+
`/subagents`) opens the terminal settings page with mode and concurrent-subagent
|
|
21
|
+
cap controls, saved defaults, Save as defaults, and Reset to defaults. Changes
|
|
22
|
+
apply immediately to this session; Escape closes the page without undoing them.
|
|
23
|
+
Enter edits the cap as a positive whole number; Escape cancels an unfinished edit.
|
|
24
|
+
Ctrl+S saves both settings as defaults. Sandbox permissions remain in `/sandbox`.
|
|
25
|
+
|
|
26
|
+
Command equivalents also work outside the terminal UI:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
/subagents mode manual|adaptive|coordinator
|
|
30
|
+
/subagents cap 6
|
|
31
|
+
/subagents save
|
|
32
|
+
/subagents reset
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The cap defaults to 4 and applies to both single and batch launches. Lowering it
|
|
36
|
+
below the running count leaves existing work untouched and blocks new admissions
|
|
37
|
+
until capacity becomes available. Slots already reserved by an in-flight launch
|
|
38
|
+
remain admitted. Settings apply to this parent Pi process, not
|
|
39
|
+
all Pi sessions on the machine. Saving preserves unrelated config keys and
|
|
40
|
+
clears both session overrides. The command form asks for confirmation when a UI
|
|
41
|
+
is available. Reset restores saved defaults without changing config.
|
|
42
|
+
|
|
43
|
+
`/reload` retains session overrides; new sessions use config, while resuming or
|
|
44
|
+
navigating a branch restores its settings. On the navigator `main` sheet, `m`
|
|
45
|
+
cycles the session mode and `ctrl+s` saves only that mode default.
|
|
27
46
|
|
|
28
47
|
- **Manual:** no proactive delegation, including in plan mode. A user request or
|
|
29
48
|
explicit workflow requirement may still delegate.
|
|
@@ -383,9 +402,12 @@ top.
|
|
|
383
402
|
foreground model).
|
|
384
403
|
- `delegationMode` — foreground policy (`manual`, `adaptive`, or `coordinator`;
|
|
385
404
|
default `adaptive`). `/subagents mode ...` changes only the current session.
|
|
386
|
-
`/subagents save`
|
|
387
|
-
- `maxConcurrent` —
|
|
388
|
-
|
|
405
|
+
`/subagents save` saves mode and cap together as config defaults.
|
|
406
|
+
- `maxConcurrent` — positive whole-number concurrency cap (**default 4**).
|
|
407
|
+
`/subagents cap <number>` overrides it for this session. Both single and batch
|
|
408
|
+
admissions respect the cap; existing runs are never stopped by a cap change.
|
|
409
|
+
Use `/subagents settings` to edit both controls or `/subagents reset` to return
|
|
410
|
+
to saved defaults.
|
|
389
411
|
|
|
390
412
|
## The allowlist also decides what LOADS
|
|
391
413
|
|
|
@@ -35,7 +35,8 @@ import {
|
|
|
35
35
|
import { spawnDetached, type SpawnResult } from "./spawn.ts";
|
|
36
36
|
import { parseRun, readRunTranscript, resetParseRunCursor, type Usage } from "./parse.ts";
|
|
37
37
|
import { finalizeRun as finalizeRunCore } from "./finalization.ts";
|
|
38
|
-
import { loadConfig, writeDelegationMode, normalizeTools, resolveExtensionPath, selfDir, SAFE_DEFAULT_TOOLS, SAFE_CLEAN_TOOLS
|
|
38
|
+
import { loadConfig, writeDelegationMode, writeSubagentSettings, isConcurrencyCap, normalizeConcurrencyCap, normalizeTools, resolveExtensionPath, selfDir, SAFE_DEFAULT_TOOLS, SAFE_CLEAN_TOOLS } from "./config.ts";
|
|
39
|
+
import { createSubagentSettingsPage } from "./settings-page.ts";
|
|
39
40
|
import { STEER_FILE_ENV, readSteerReceipt } from "./child-steer.ts";
|
|
40
41
|
import {
|
|
41
42
|
decideTiming,
|
|
@@ -52,7 +53,7 @@ import {
|
|
|
52
53
|
type TimingStopReason,
|
|
53
54
|
} from "./timing.ts";
|
|
54
55
|
import { readAppendedLines, type LogCursor } from "./log-cursor.ts";
|
|
55
|
-
import { DELEGATION_MODE_REQUEST, delegationPrompt, isDelegationMode, normalizeDelegationMode, type DelegationMode } from "./delegation.ts";
|
|
56
|
+
import { DELEGATION_MODES, DELEGATION_MODE_REQUEST, delegationPrompt, isDelegationMode, normalizeDelegationMode, type DelegationMode } from "./delegation.ts";
|
|
56
57
|
import { resolveExtensions, extensionArgs } from "./extensions.ts";
|
|
57
58
|
import { prepareTaskRuntime } from "./task-policy.ts";
|
|
58
59
|
import { canonicalizePath, takeRecoverySnapshot } from "./shared-sandbox-core.ts";
|
|
@@ -162,10 +163,13 @@ import {
|
|
|
162
163
|
import { defaultUserRoot, type CatalogSnapshot } from "./catalog-store.ts";
|
|
163
164
|
|
|
164
165
|
const SUBAGENT_MODE_ACTIONS: readonly AutocompleteItem[] = [
|
|
166
|
+
{ value: "settings", label: "settings", description: "Open delegation mode and concurrency settings" },
|
|
167
|
+
{ value: "cap", label: "cap <number>", description: "Set a positive concurrent subagent limit for this session" },
|
|
168
|
+
{ value: "reset", label: "reset", description: "Reset session mode and cap to saved defaults" },
|
|
165
169
|
{ value: "mode manual", label: "mode manual", description: "Delegate only when explicitly requested" },
|
|
166
170
|
{ value: "mode adaptive", label: "mode adaptive", description: "Delegate substantial independent work" },
|
|
167
171
|
{ value: "mode coordinator", label: "mode coordinator", description: "Delegate role-owned work by default" },
|
|
168
|
-
{ value: "save", label: "save", description: "
|
|
172
|
+
{ value: "save", label: "save", description: "Save the current mode and cap as defaults" },
|
|
169
173
|
];
|
|
170
174
|
|
|
171
175
|
export function subagentsArgumentCompletions(argumentPrefix: string): AutocompleteItem[] | null {
|
|
@@ -1176,7 +1180,6 @@ function subagentWorkRows(now: number): BackgroundWorkRow[] {
|
|
|
1176
1180
|
});
|
|
1177
1181
|
}
|
|
1178
1182
|
|
|
1179
|
-
const DELEGATION_MODES = ["manual", "adaptive", "coordinator"] as const;
|
|
1180
1183
|
const DELEGATION_BLURB: Record<DelegationMode, string> = {
|
|
1181
1184
|
manual: "only when you or a workflow asks",
|
|
1182
1185
|
adaptive: "substantial independent work",
|
|
@@ -1496,7 +1499,33 @@ function catalogRoleSchema(purpose: string) {
|
|
|
1496
1499
|
|
|
1497
1500
|
export default function (pi: ExtensionAPI) {
|
|
1498
1501
|
let delegationOverride: DelegationMode | undefined;
|
|
1502
|
+
let capOverride: number | undefined;
|
|
1503
|
+
const activeConcurrencyCap = (): number => capOverride ?? normalizeConcurrencyCap(loadConfig().maxConcurrent);
|
|
1504
|
+
const changeCap = (cap: number): void => {
|
|
1505
|
+
if (!isConcurrencyCap(cap)) throw new Error("Concurrent subagents must be a positive whole number.");
|
|
1506
|
+
capOverride = cap;
|
|
1507
|
+
pi.appendEntry("pi-better-subagents-cap", { version: 1, maxConcurrent: cap });
|
|
1508
|
+
};
|
|
1509
|
+
const changeMode = (mode: DelegationMode): void => {
|
|
1510
|
+
delegationOverride = mode;
|
|
1511
|
+
pi.appendEntry("pi-better-subagents-delegation", { version: 1, mode });
|
|
1512
|
+
};
|
|
1513
|
+
const resetSettings = (): void => {
|
|
1514
|
+
capOverride = undefined;
|
|
1515
|
+
delegationOverride = undefined;
|
|
1516
|
+
pi.appendEntry("pi-better-subagents-cap", { version: 1, maxConcurrent: null });
|
|
1517
|
+
pi.appendEntry("pi-better-subagents-delegation", { version: 1, mode: null });
|
|
1518
|
+
};
|
|
1499
1519
|
const activeDelegationMode = (): DelegationMode => delegationOverride ?? normalizeDelegationMode(loadConfig().delegationMode);
|
|
1520
|
+
const saveSettings = (): { ok: boolean; message: string } => {
|
|
1521
|
+
try {
|
|
1522
|
+
writeSubagentSettings({ delegationMode: activeDelegationMode(), maxConcurrent: activeConcurrencyCap() });
|
|
1523
|
+
resetSettings();
|
|
1524
|
+
return { ok: true, message: "Subagent defaults saved." };
|
|
1525
|
+
} catch (error) {
|
|
1526
|
+
return { ok: false, message: `Could not save subagent defaults: ${error instanceof Error ? error.message : String(error)}` };
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1500
1529
|
readDelegation = () => ({
|
|
1501
1530
|
mode: activeDelegationMode(),
|
|
1502
1531
|
source: delegationOverride ? "session" : "config",
|
|
@@ -1504,8 +1533,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1504
1533
|
cycleDelegation = () => {
|
|
1505
1534
|
const current = activeDelegationMode();
|
|
1506
1535
|
const next = DELEGATION_MODES[(DELEGATION_MODES.indexOf(current) + 1) % DELEGATION_MODES.length]!;
|
|
1507
|
-
|
|
1508
|
-
pi.appendEntry("pi-better-subagents-delegation", { version: 1, mode: next });
|
|
1536
|
+
changeMode(next);
|
|
1509
1537
|
};
|
|
1510
1538
|
saveDelegationDefault = (mode) => {
|
|
1511
1539
|
try {
|
|
@@ -1519,9 +1547,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1519
1547
|
};
|
|
1520
1548
|
const restoreDelegationMode = (ctx: ExtensionContext): void => {
|
|
1521
1549
|
delegationOverride = undefined;
|
|
1550
|
+
capOverride = undefined;
|
|
1522
1551
|
const branch = ctx.sessionManager?.getBranch?.();
|
|
1523
1552
|
if (!Array.isArray(branch)) return;
|
|
1524
1553
|
for (const entry of branch) {
|
|
1554
|
+
if (entry.type === "custom" && entry.customType === "pi-better-subagents-cap") {
|
|
1555
|
+
const data = entry.data as { version?: unknown; maxConcurrent?: unknown } | null;
|
|
1556
|
+
if (data?.version === 1) {
|
|
1557
|
+
if (data.maxConcurrent === null) capOverride = undefined;
|
|
1558
|
+
else if (isConcurrencyCap(data.maxConcurrent)) capOverride = data.maxConcurrent;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1525
1561
|
if (entry.type !== "custom" || entry.customType !== "pi-better-subagents-delegation") continue;
|
|
1526
1562
|
const data = entry.data as { version?: unknown; mode?: unknown } | null;
|
|
1527
1563
|
if (data?.version !== 1) continue;
|
|
@@ -1905,8 +1941,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1905
1941
|
const blankSelector = blankCatalogSelector(p);
|
|
1906
1942
|
if (blankSelector) throw new Error(blankSelector);
|
|
1907
1943
|
|
|
1908
|
-
const
|
|
1909
|
-
const maxConcurrent = cfg.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
|
1944
|
+
const maxConcurrent = activeConcurrencyCap();
|
|
1910
1945
|
const countRunning = () =>
|
|
1911
1946
|
listActiveMetasForParent(process.pid).filter((m) => effectiveStatus(m) === "running").length;
|
|
1912
1947
|
// Shared with batch-spawn: reserve before any async work so an interleaved
|
|
@@ -1926,7 +1961,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1926
1961
|
}
|
|
1927
1962
|
if (catalog.jobs.length > 1) {
|
|
1928
1963
|
const extra = catalog.jobs.length - 1;
|
|
1929
|
-
if (extra > 0 && !gate.tryReserve(extra,
|
|
1964
|
+
if (extra > 0 && !gate.tryReserve(extra, activeConcurrencyCap())) {
|
|
1930
1965
|
gate.release(1);
|
|
1931
1966
|
reserved = 0;
|
|
1932
1967
|
throw new Error(`Choosing split needs ${catalog.jobs.length} subagent slots, but only one was free. Nothing was launched.`);
|
|
@@ -2044,7 +2079,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
2044
2079
|
};
|
|
2045
2080
|
|
|
2046
2081
|
const cfg = loadConfig();
|
|
2047
|
-
const maxConcurrent = cfg.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
|
2048
2082
|
const countRunning = () =>
|
|
2049
2083
|
listActiveMetasForParent(process.pid).filter((m) => effectiveStatus(m) === "running").length;
|
|
2050
2084
|
const launchAvailable = p.onCapacity === "launch-available";
|
|
@@ -2087,6 +2121,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2087
2121
|
// until each job commits (or the unused remainder is released) closes the
|
|
2088
2122
|
// interleaving oversubscribe class — a stale plan alone is not enough.
|
|
2089
2123
|
if (!launchAvailable) {
|
|
2124
|
+
const maxConcurrent = activeConcurrencyCap();
|
|
2090
2125
|
// planBatchLaunches still produces the public error text (incl. pending).
|
|
2091
2126
|
planBatchLaunches({
|
|
2092
2127
|
jobs: p.jobs,
|
|
@@ -2122,7 +2157,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2122
2157
|
const merged = mergeJobOptions(p.shared, job);
|
|
2123
2158
|
|
|
2124
2159
|
if (launchAvailable) {
|
|
2125
|
-
if (!gate.tryReserve(1,
|
|
2160
|
+
if (!gate.tryReserve(1, activeConcurrencyCap())) {
|
|
2126
2161
|
for (let j = i; j < p.jobs.length; j++) {
|
|
2127
2162
|
skipped.push({ job: j + 1, name: names[j] });
|
|
2128
2163
|
}
|
|
@@ -2235,30 +2270,53 @@ export default function (pi: ExtensionAPI) {
|
|
|
2235
2270
|
if (typeof pi.registerCommand === "function") {
|
|
2236
2271
|
agentOperations.registerCommands(pi);
|
|
2237
2272
|
pi.registerCommand("subagents", {
|
|
2238
|
-
description: "[mode manual|adaptive|coordinator|save] —
|
|
2273
|
+
description: "[settings|mode manual|adaptive|coordinator|cap <number>|save|reset] — Subagent settings",
|
|
2239
2274
|
getArgumentCompletions: subagentsArgumentCompletions,
|
|
2240
2275
|
async handler(args, ctx) {
|
|
2241
2276
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
2242
|
-
if (tokens.length === 0) {
|
|
2243
|
-
ctx.ui.
|
|
2277
|
+
if (tokens.length === 0 || (tokens.length === 1 && tokens[0] === "settings")) {
|
|
2278
|
+
if (ctx.mode === "tui" && ctx.hasUI && typeof ctx.ui.custom === "function") {
|
|
2279
|
+
await ctx.ui.custom<void>((tui, theme, _kb, done) => createSubagentSettingsPage(theme, {
|
|
2280
|
+
get: () => ({
|
|
2281
|
+
mode: activeDelegationMode(), maxConcurrent: activeConcurrencyCap(),
|
|
2282
|
+
modeSource: delegationOverride ? "session" : "config",
|
|
2283
|
+
capSource: capOverride !== undefined ? "session" : "config",
|
|
2284
|
+
defaultMode: normalizeDelegationMode(loadConfig().delegationMode),
|
|
2285
|
+
defaultCap: normalizeConcurrencyCap(loadConfig().maxConcurrent),
|
|
2286
|
+
}),
|
|
2287
|
+
changeMode, changeCap, save: saveSettings, reset: resetSettings,
|
|
2288
|
+
}, () => { refreshBackgroundWorkNavigator(ctx); tui.requestRender(); }, () => done()));
|
|
2289
|
+
} else {
|
|
2290
|
+
ctx.ui.notify(`Delegation mode: ${activeDelegationMode()}. Concurrent subagents: ${activeConcurrencyCap()}. Use /subagents mode <mode> or /subagents cap <number>; /subagents save persists both.`, "info");
|
|
2291
|
+
}
|
|
2244
2292
|
return;
|
|
2245
2293
|
}
|
|
2246
2294
|
const requested = tokens[1];
|
|
2247
2295
|
if (tokens.length === 1 && tokens[0] === "save") {
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
ctx.ui.notify("Delegation default unchanged.", "info");
|
|
2296
|
+
if (ctx.hasUI && !(await ctx.ui.confirm("Save subagent defaults?", `Future sessions will start in ${activeDelegationMode()} with a cap of ${activeConcurrencyCap()}.`))) {
|
|
2297
|
+
ctx.ui.notify("Subagent defaults unchanged.", "info");
|
|
2251
2298
|
return;
|
|
2252
2299
|
}
|
|
2253
|
-
const saved =
|
|
2300
|
+
const saved = saveSettings();
|
|
2254
2301
|
ctx.ui.notify(saved.message, saved.ok ? "info" : "warning");
|
|
2302
|
+
} else if (tokens.length === 1 && tokens[0] === "reset") {
|
|
2303
|
+
resetSettings();
|
|
2304
|
+
ctx.ui.notify("Session subagent settings reset to saved defaults.", "info");
|
|
2255
2305
|
} else if (tokens.length === 2 && tokens[0] === "mode" && isDelegationMode(requested)) {
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2306
|
+
changeMode(requested);
|
|
2307
|
+
ctx.ui.notify(`Delegation mode: ${activeDelegationMode()} (current session).`, "info");
|
|
2308
|
+
} else if (tokens.length === 2 && tokens[0] === "cap") {
|
|
2309
|
+
const cap = /^\d+$/.test(requested!) ? Number(requested) : NaN;
|
|
2310
|
+
if (!isConcurrencyCap(cap)) {
|
|
2311
|
+
ctx.ui.notify("Concurrent subagents must be a positive whole number. Nothing changed.", "warning");
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
changeCap(cap);
|
|
2315
|
+
ctx.ui.notify(`Concurrent subagents: ${cap} (current session). Running subagents are unchanged.`, "info");
|
|
2259
2316
|
} else {
|
|
2260
|
-
ctx.ui.notify("Usage: /subagents [mode manual|adaptive|coordinator|save]", "warning");
|
|
2317
|
+
ctx.ui.notify("Usage: /subagents [settings|mode manual|adaptive|coordinator|cap <number>|save|reset]", "warning");
|
|
2261
2318
|
}
|
|
2319
|
+
refreshBackgroundWorkNavigator(ctx);
|
|
2262
2320
|
},
|
|
2263
2321
|
});
|
|
2264
2322
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import * as Tui from "@earendil-works/pi-tui";
|
|
3
|
+
import { isConcurrencyCap } from "./config.ts";
|
|
4
|
+
import { DELEGATION_MODES, type DelegationMode } from "./delegation.ts";
|
|
5
|
+
|
|
6
|
+
export interface SubagentSettingsSnapshot {
|
|
7
|
+
mode: DelegationMode;
|
|
8
|
+
maxConcurrent: number;
|
|
9
|
+
modeSource: "session" | "config";
|
|
10
|
+
capSource: "session" | "config";
|
|
11
|
+
defaultMode: DelegationMode;
|
|
12
|
+
defaultCap: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SubagentSettingsHandlers {
|
|
16
|
+
get(): SubagentSettingsSnapshot;
|
|
17
|
+
changeMode(mode: DelegationMode): void;
|
|
18
|
+
changeCap(cap: number): void;
|
|
19
|
+
save(): { ok: boolean; message: string };
|
|
20
|
+
reset(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const labels = ["Delegation mode", "Concurrent subagents", "Save as defaults", "Reset to defaults"];
|
|
24
|
+
|
|
25
|
+
export function createSubagentSettingsPage(
|
|
26
|
+
theme: Theme,
|
|
27
|
+
handlers: SubagentSettingsHandlers,
|
|
28
|
+
requestRender: () => void,
|
|
29
|
+
close: () => void,
|
|
30
|
+
): Tui.Component & Tui.Focusable {
|
|
31
|
+
const input = new Tui.Input();
|
|
32
|
+
let focused = false;
|
|
33
|
+
let row = 0;
|
|
34
|
+
let editing = false;
|
|
35
|
+
let message = "";
|
|
36
|
+
let error = false;
|
|
37
|
+
function report(text: string, failed = false): void {
|
|
38
|
+
message = text;
|
|
39
|
+
error = failed;
|
|
40
|
+
requestRender();
|
|
41
|
+
}
|
|
42
|
+
function action(): void {
|
|
43
|
+
try {
|
|
44
|
+
const settings = handlers.get();
|
|
45
|
+
if (row === 0) {
|
|
46
|
+
const next = DELEGATION_MODES[(DELEGATION_MODES.indexOf(settings.mode) + 1) % DELEGATION_MODES.length]!;
|
|
47
|
+
handlers.changeMode(next);
|
|
48
|
+
report("Session mode updated.");
|
|
49
|
+
} else if (row === 1) {
|
|
50
|
+
editing = true;
|
|
51
|
+
input.setValue(String(settings.maxConcurrent));
|
|
52
|
+
input.handleInput("\x05");
|
|
53
|
+
input.focused = focused;
|
|
54
|
+
report("");
|
|
55
|
+
} else if (row === 2) {
|
|
56
|
+
const result = handlers.save();
|
|
57
|
+
report(result.message, !result.ok);
|
|
58
|
+
} else {
|
|
59
|
+
handlers.reset();
|
|
60
|
+
report("Session settings reset to saved defaults.");
|
|
61
|
+
}
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
report(cause instanceof Error ? cause.message : String(cause), true);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
input.onSubmit = (value) => {
|
|
67
|
+
const cap = /^\d+$/.test(value.trim()) ? Number(value.trim()) : NaN;
|
|
68
|
+
if (!isConcurrencyCap(cap)) {
|
|
69
|
+
report("Enter a positive whole number.", true);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
handlers.changeCap(cap);
|
|
74
|
+
editing = false;
|
|
75
|
+
input.focused = false;
|
|
76
|
+
report("Session cap updated.");
|
|
77
|
+
} catch (cause) {
|
|
78
|
+
report(cause instanceof Error ? cause.message : String(cause), true);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
input.onEscape = () => {
|
|
82
|
+
editing = false;
|
|
83
|
+
input.focused = false;
|
|
84
|
+
report("");
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
get focused() { return focused; },
|
|
88
|
+
set focused(value: boolean) { focused = value; input.focused = editing && value; },
|
|
89
|
+
invalidate() { input.invalidate(); },
|
|
90
|
+
handleInput(data: string) {
|
|
91
|
+
if (editing) {
|
|
92
|
+
input.handleInput(data);
|
|
93
|
+
requestRender();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (Tui.matchesKey(data, Tui.Key.escape)) close();
|
|
97
|
+
else if (Tui.matchesKey(data, Tui.Key.up)) row = Math.max(0, row - 1);
|
|
98
|
+
else if (Tui.matchesKey(data, Tui.Key.down)) row = Math.min(labels.length - 1, row + 1);
|
|
99
|
+
else if (Tui.matchesKey(data, Tui.Key.enter) || data === " ") action();
|
|
100
|
+
else if (Tui.matchesKey(data, "ctrl+s")) {
|
|
101
|
+
const selected = row;
|
|
102
|
+
row = 2;
|
|
103
|
+
action();
|
|
104
|
+
row = selected;
|
|
105
|
+
}
|
|
106
|
+
requestRender();
|
|
107
|
+
},
|
|
108
|
+
render(width: number) {
|
|
109
|
+
const settings = handlers.get();
|
|
110
|
+
const values = [
|
|
111
|
+
`${settings.mode} (${settings.modeSource})`,
|
|
112
|
+
`${settings.maxConcurrent} (${settings.capSource})`,
|
|
113
|
+
"",
|
|
114
|
+
"",
|
|
115
|
+
];
|
|
116
|
+
const lines = [theme.fg("accent", theme.bold("Subagent settings")), ""];
|
|
117
|
+
labels.forEach((label, index) => {
|
|
118
|
+
const text = `${index === row ? ">" : " "} ${label}${values[index] ? `: ${values[index]}` : ""}`;
|
|
119
|
+
lines.push(index === row ? theme.fg("accent", theme.bold(text)) : text);
|
|
120
|
+
});
|
|
121
|
+
if (editing) lines.push("", ...input.render(Math.max(1, width)));
|
|
122
|
+
lines.push("", theme.fg("muted", `Saved defaults: ${settings.defaultMode}, cap ${settings.defaultCap}`));
|
|
123
|
+
if (message) lines.push(theme.fg(error ? "error" : "success", message));
|
|
124
|
+
lines.push("", theme.fg("dim", editing ? "Enter apply Esc cancel" : "Up/Down select Enter change Ctrl+S save Esc close"));
|
|
125
|
+
return lines.map((line) => Tui.truncateToWidth(line, Math.max(1, width), ""));
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -6,6 +6,8 @@ const { createBashToolDefinition, createReadToolDefinition, createWriteToolDefin
|
|
|
6
6
|
createEditToolDefinition, createLocalBashOperations, getShellConfig } = PiCodingAgent;
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { unlink } from "node:fs/promises";
|
|
9
11
|
import { accessSync, constants, lstatSync, mkdirSync, mkdtempSync, readlinkSync, writeFileSync } from "node:fs";
|
|
10
12
|
import { homedir, tmpdir } from "node:os";
|
|
11
13
|
import { basename, dirname, isAbsolute, join, parse, resolve, sep } from "node:path";
|
|
@@ -117,42 +119,51 @@ export function createTaskBashOperations(
|
|
|
117
119
|
const scratch = plan.policy.runtimeWrite?.[0];
|
|
118
120
|
const taskEnv = { ...process.env, ...options.env,
|
|
119
121
|
...(scratch ? { TMPDIR: scratch, TMP: scratch, TEMP: scratch } : {}) };
|
|
120
|
-
const wrapped = maybeBuildSandboxCommand({
|
|
121
|
-
policy: plan.policy, profilePath: plan.profilePath,
|
|
122
|
-
execPath: "/usr/bin/env", execArgs: ["-i", "--", ...Object.entries(taskEnv)
|
|
123
|
-
.filter((entry): entry is [string, string] => entry[1] !== undefined)
|
|
124
|
-
.map(([key, value]) => `${key}=${value}`), shell.shell, ...shell.args, command],
|
|
125
|
-
}, { sandboxEnabled: true, explicitSandbox: true });
|
|
126
|
-
if (!wrapped) throw new Error("Sandbox: no task execution backend is available.");
|
|
127
|
-
for (const line of wrapped.notices ?? []) options.onData(Buffer.from(`${line}\n`));
|
|
128
122
|
if (options.signal?.aborted) throw new Error("aborted");
|
|
129
123
|
if (options.timeout !== undefined && (!Number.isFinite(options.timeout) || options.timeout <= 0 || options.timeout * 1000 > 2147483647)) {
|
|
130
124
|
throw new Error("Invalid timeout: must be a positive, supported number of seconds");
|
|
131
125
|
}
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
126
|
+
// A sibling launch must never rewrite a profile another process is reading.
|
|
127
|
+
const profilePath = `${plan.profilePath}.${randomUUID()}.bash.sb`;
|
|
128
|
+
try {
|
|
129
|
+
const wrapped = maybeBuildSandboxCommand({
|
|
130
|
+
policy: plan.policy, profilePath,
|
|
131
|
+
execPath: "/usr/bin/env", execArgs: ["-i", "--", ...Object.entries(taskEnv)
|
|
132
|
+
.filter((entry): entry is [string, string] => entry[1] !== undefined)
|
|
133
|
+
.map(([key, value]) => `${key}=${value}`), shell.shell, ...shell.args, command],
|
|
134
|
+
}, { sandboxEnabled: true, explicitSandbox: true });
|
|
135
|
+
if (!wrapped) throw new Error("Sandbox: no task execution backend is available.");
|
|
136
|
+
for (const line of wrapped.notices ?? []) options.onData(Buffer.from(`${line}\n`));
|
|
137
|
+
if (options.signal?.aborted) throw new Error("aborted");
|
|
138
|
+
// No shell or caller-controlled loader environment runs before the boundary.
|
|
139
|
+
return await new Promise<{ exitCode: number | null }>((resolve, reject) => {
|
|
140
|
+
const child = spawn(wrapped.file, wrapped.fileArgs, { cwd, detached: true,
|
|
141
|
+
env: { PATH: "/usr/bin:/bin", HOME: plan.policy.home }, stdio: ["ignore", "pipe", "pipe"] });
|
|
142
|
+
let timedOut = false;
|
|
143
|
+
const kill = () => { if (child.pid) { try { process.kill(-child.pid, "SIGKILL"); } catch { child.kill("SIGKILL"); } } };
|
|
144
|
+
const timer = options.timeout === undefined ? undefined : setTimeout(() => { timedOut = true; kill(); }, options.timeout * 1000);
|
|
145
|
+
if (child.pid) trackDetachedChildPid(child.pid);
|
|
146
|
+
const cleanup = () => {
|
|
147
|
+
if (child.pid) untrackDetachedChildPid(child.pid);
|
|
148
|
+
if (timer) clearTimeout(timer);
|
|
149
|
+
options.signal?.removeEventListener("abort", kill);
|
|
150
|
+
};
|
|
151
|
+
child.stdout.on("data", options.onData);
|
|
152
|
+
child.stderr.on("data", options.onData);
|
|
153
|
+
options.signal?.addEventListener("abort", kill, { once: true });
|
|
154
|
+
if (options.signal?.aborted) kill();
|
|
155
|
+
void waitForChildProcess(child).then((exitCode: number | null) => {
|
|
156
|
+
cleanup();
|
|
157
|
+
if (options.signal?.aborted) reject(new Error("aborted"));
|
|
158
|
+
else if (timedOut) reject(new Error(`timeout:${options.timeout}`));
|
|
159
|
+
else resolve({ exitCode });
|
|
160
|
+
}, (error: unknown) => { cleanup(); reject(error); });
|
|
161
|
+
});
|
|
162
|
+
} finally {
|
|
163
|
+
await unlink(profilePath).catch((error: NodeJS.ErrnoException) => {
|
|
164
|
+
if (error.code !== "ENOENT") throw error;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
156
167
|
},
|
|
157
168
|
};
|
|
158
169
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, and structured plans.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -53,11 +53,11 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"pi-better-background-tasks": "0.6.3",
|
|
56
|
-
"pi-better-goal": "0.6.
|
|
56
|
+
"pi-better-goal": "0.6.1",
|
|
57
57
|
"pi-better-plan": "0.5.2",
|
|
58
|
-
"pi-better-sandbox": "0.8.
|
|
58
|
+
"pi-better-sandbox": "0.8.2",
|
|
59
59
|
"pi-better-ssh": "0.1.1",
|
|
60
|
-
"pi-better-subagents": "0.
|
|
60
|
+
"pi-better-subagents": "0.12.0",
|
|
61
61
|
"smol-toml": "1.9.0",
|
|
62
62
|
"yaml": "^2.9.1"
|
|
63
63
|
},
|