oira666_pi-subagent 0.5.1 → 0.5.2
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/README.md +29 -1
- package/extension/contracts.ts +2 -2
- package/extension/execution.ts +7 -1
- package/extension/provider.ts +5 -1
- package/index.ts +2 -0
- package/package.json +9 -9
- package/runner/arguments.ts +17 -1
- package/runner/extension-policy.ts +174 -0
- package/runner/options.ts +2 -0
- package/runner/parallel.ts +2 -0
- package/runner/single.ts +3 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Delegate tasks to agents running in separate Pi processes. Each worker has its own context and saved session. The parent receives its final text and usage statistics, not its reasoning or tool transcript.
|
|
4
4
|
|
|
5
|
-
Requires Pi 0.
|
|
5
|
+
Requires Pi 0.87.1 or newer, using the `@earendil-works` SDK packages.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -18,6 +18,34 @@ To remove it:
|
|
|
18
18
|
pi remove npm:oira666_pi-subagent
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## Extension controls
|
|
22
|
+
|
|
23
|
+
Exclude extensions from child agents without changing which extensions the parent loads:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
PI_SUBAGENT_EXCLUDE_EXTENSIONS="oira666_pi-free-swarm,@scope/other-package,./local-extension.ts" pi
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The originally requested hyphenated name is also supported. Use `env` because most shells cannot assign hyphenated variable names directly:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
env 'PI-SUBAGENT-EXCLUDE-EXTENSIONS=oira666_pi-free-swarm' pi
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Both variables accept comma-separated lists; whitespace and empty entries are ignored, and the two lists are combined. Entries match a package source, package name (including `npm:@scope/name@version`, ignoring the version), or an extension file/directory path. Automatic and settings-listed extension paths use the nearest owning `package.json` after resolving symlinks; loose hooks do not inherit the current workspace's or Pi configuration directory's package name. Relative paths resolve from the worker's working directory; `~/` and symlinks are supported. Commas inside paths are not supported. These variables are inherited by nested workers.
|
|
36
|
+
|
|
37
|
+
When a list is nonempty, the runner discovers enabled extensions without executing them, then launches the child with `--no-extensions` and explicit paths for every remaining extension, including authentication providers. This covers automatic user/project extensions and forwarded `-e` sources. An original `--no-extensions` still suppresses automatic extensions. Project resources are included only when the parent's live context trusts that working directory; unknown trust fails closed. The same trust decision is passed to the child. With no exclusions, launch behavior is unchanged and no extra discovery runs.
|
|
38
|
+
|
|
39
|
+
Discovery does not install packages or edit settings. Remote explicit `-e` sources must already be installed in Pi's user package location (or its supported legacy global npm location); temporary-only and project-only remote installs must first be installed there or passed as local paths. If Pi leaves a directory unresolved, pass its entry-point file explicitly so file exclusions cannot be bypassed by directory loading. Missing or invalid sources fail the launch rather than falling back to unfiltered loading. Pi's offline mode skips missing automatic packages, as usual. Filtering controls extension entry points, not code imported by an allowed extension, and the allowlist is a launch-time snapshot until the next delegation.
|
|
40
|
+
|
|
41
|
+
Disable this entire extension, including its tools, commands, flags, providers, and event handlers:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
PI_SUBAGENT_DISABLED=1 pi
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`true` (case-insensitive) also disables it; `PI-SUBAGENT-DISABLED` is an equivalent alias. Other values leave it enabled. The setting applies to the current process and is inherited by children. It is separate from child-extension exclusions, which do not disable anything in the parent. Restart Pi or reload extensions after changing the process environment.
|
|
48
|
+
|
|
21
49
|
## Launch and resume
|
|
22
50
|
|
|
23
51
|
```json
|
package/extension/contracts.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TranscriptContext, SimpleStreamOptions } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { SubagentDetails } from "../types.js";
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@ export type ResumeModel = NonNullable<ExtensionContext["model"]>;
|
|
|
7
7
|
export type ResumeModelRegistry = ExtensionContext["modelRegistry"];
|
|
8
8
|
export type SessionContext = ExtensionContext;
|
|
9
9
|
export type StreamOptions = SimpleStreamOptions;
|
|
10
|
-
export type ProviderContext =
|
|
10
|
+
export type ProviderContext = TranscriptContext;
|
|
11
11
|
export type ProgressUpdate = AgentToolResult<SubagentDetails>;
|
|
12
12
|
|
|
13
13
|
export function isRecord(value: unknown): value is Record<string, unknown> {
|
package/extension/execution.ts
CHANGED
|
@@ -46,6 +46,7 @@ export async function executeSingle(
|
|
|
46
46
|
try {
|
|
47
47
|
result = await runAgentSubprocess({
|
|
48
48
|
cwd: defaultCwd,
|
|
49
|
+
projectTrusted: state.latestSessionCtx?.cwd === defaultCwd && state.latestSessionCtx.isProjectTrusted(),
|
|
49
50
|
agents,
|
|
50
51
|
agentName,
|
|
51
52
|
task,
|
|
@@ -149,7 +150,12 @@ export async function executeParallel(
|
|
|
149
150
|
updateLatestBroadcastTargets(state, undefined);
|
|
150
151
|
}
|
|
151
152
|
},
|
|
152
|
-
{
|
|
153
|
+
{
|
|
154
|
+
...extras,
|
|
155
|
+
namesFile: state.currentNamesFile || undefined,
|
|
156
|
+
smartDecision: state.smartDecision,
|
|
157
|
+
projectTrusted: state.latestSessionCtx?.cwd === defaultCwd && state.latestSessionCtx.isProjectTrusted(),
|
|
158
|
+
},
|
|
153
159
|
);
|
|
154
160
|
} finally {
|
|
155
161
|
for (const id of taskIds.values()) state.activeSubagents.delete(id);
|
package/extension/provider.ts
CHANGED
|
@@ -127,7 +127,11 @@ export function registerResumeProvider(state: ExtensionState): void {
|
|
|
127
127
|
if (plans.length > 0 && phase === "tool" && triggerMatches) {
|
|
128
128
|
resume.phase = "final";
|
|
129
129
|
const toolCalls = plans.map((plan, index) =>
|
|
130
|
-
fauxToolCall(
|
|
130
|
+
fauxToolCall(
|
|
131
|
+
SUBAGENT_TOOL_NAME,
|
|
132
|
+
{ tasks: plan.tasks.map((task) => ({ ...task })) },
|
|
133
|
+
{ id: `resume_subagent_${Date.now()}_${index}` },
|
|
134
|
+
),
|
|
131
135
|
);
|
|
132
136
|
resumeCore.setResponses([() => fauxAssistantMessage(toolCalls, { stopReason: "toolUse" })]);
|
|
133
137
|
const stream = resumeCore.streamSimple(model, context, options);
|
package/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { subagentDisabled } from "./runner/extension-policy.js";
|
|
2
3
|
import { registerSessionEvents } from "./extension/events.js";
|
|
3
4
|
import { registerSubagentExpandCommand } from "./extension/expand-command.js";
|
|
4
5
|
import { registerPromptHook } from "./extension/prompt-hook.js";
|
|
@@ -12,6 +13,7 @@ export { getSubagentsToolDescription } from "./extension/prompts.js";
|
|
|
12
13
|
export { collectCombinedUsageStatusLine, collectLiveUsageSummary } from "./extension/usage.js";
|
|
13
14
|
|
|
14
15
|
export default function (pi: ExtensionAPI): void {
|
|
16
|
+
if (subagentDisabled()) return;
|
|
15
17
|
pi.registerFlag("subagent-max-depth", {
|
|
16
18
|
description: "Maximum allowed subagent delegation depth (default: 3).",
|
|
17
19
|
type: "string",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oira666_pi-subagent",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -61,20 +61,20 @@
|
|
|
61
61
|
"check": "npm run typecheck && npm run format:check && npm run check:size && npm test"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
65
|
-
"@earendil-works/pi-ai": "^0.
|
|
66
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
67
|
-
"@earendil-works/pi-tui": "^0.
|
|
64
|
+
"@earendil-works/pi-agent-core": "^0.87.1",
|
|
65
|
+
"@earendil-works/pi-ai": "^0.87.1",
|
|
66
|
+
"@earendil-works/pi-coding-agent": "^0.87.1",
|
|
67
|
+
"@earendil-works/pi-tui": "^0.87.1",
|
|
68
68
|
"@types/node": "^25.2.3",
|
|
69
69
|
"prettier": "^3.9.6",
|
|
70
70
|
"tsx": "^4.23.13",
|
|
71
71
|
"typescript": "^5.9.3"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
|
-
"@earendil-works/pi-agent-core": ">=0.
|
|
75
|
-
"@earendil-works/pi-ai": ">=0.
|
|
76
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
77
|
-
"@earendil-works/pi-tui": ">=0.
|
|
74
|
+
"@earendil-works/pi-agent-core": ">=0.87.1",
|
|
75
|
+
"@earendil-works/pi-ai": ">=0.87.1",
|
|
76
|
+
"@earendil-works/pi-coding-agent": ">=0.87.1",
|
|
77
|
+
"@earendil-works/pi-tui": ">=0.87.1"
|
|
78
78
|
},
|
|
79
79
|
"peerDependenciesMeta": {
|
|
80
80
|
"@earendil-works/pi-agent-core": {
|
package/runner/arguments.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentConfig } from "../agents.js";
|
|
5
5
|
import { SUBAGENT_FALLBACK_MODEL_ENV } from "./constants.js";
|
|
6
|
+
import { childExtensionArgs, excludedExtensions } from "./extension-policy.js";
|
|
6
7
|
function resolveExtensionArg(value: string): string {
|
|
7
8
|
if (!value) return value;
|
|
8
9
|
if (value.startsWith("npm:") || value.startsWith("git:")) return value;
|
|
@@ -202,6 +203,12 @@ function parseInheritedCliArgs(argv: string[]): InheritedCliArgs {
|
|
|
202
203
|
/** Cached once — process.argv is immutable at runtime */
|
|
203
204
|
const _inheritedCliArgs = parseInheritedCliArgs(process.argv);
|
|
204
205
|
|
|
206
|
+
export async function resolveChildExtensionArgs(cwd: string, projectTrusted?: boolean): Promise<string[] | undefined> {
|
|
207
|
+
const excludes = excludedExtensions();
|
|
208
|
+
if (excludes.length === 0) return undefined;
|
|
209
|
+
return childExtensionArgs(_inheritedCliArgs.extensionArgs, cwd, projectTrusted, excludes);
|
|
210
|
+
}
|
|
211
|
+
|
|
205
212
|
export function resolveSubagentModel(agentModel?: string, currentParentModel?: string): string | undefined {
|
|
206
213
|
// The active parent model is authoritative. Agent frontmatter is retained as
|
|
207
214
|
// a compatibility fallback only for callers that cannot supply live context.
|
|
@@ -219,6 +226,7 @@ export function buildPiArgs(
|
|
|
219
226
|
fallbackModelOverride?: string,
|
|
220
227
|
rawPrompt = false,
|
|
221
228
|
selection?: { provider: string; model: string; thinking: string },
|
|
229
|
+
extensionArgsOverride?: string[],
|
|
222
230
|
): { args: string[]; prompt: string } {
|
|
223
231
|
// A Jev-selected provider must not inherit a conflicting provider or its CLI credential.
|
|
224
232
|
const proxyArgs = selection
|
|
@@ -227,7 +235,15 @@ export function buildPiArgs(
|
|
|
227
235
|
!["--provider", "--api-key"].includes(arg) && !["--provider", "--api-key"].includes(args[index - 1]),
|
|
228
236
|
)
|
|
229
237
|
: _inheritedCliArgs.alwaysProxy;
|
|
230
|
-
const args: string[] = [
|
|
238
|
+
const args: string[] = [
|
|
239
|
+
"--mode",
|
|
240
|
+
"rpc",
|
|
241
|
+
...(extensionArgsOverride ? [] : _inheritedCliArgs.extensionArgs),
|
|
242
|
+
...proxyArgs,
|
|
243
|
+
];
|
|
244
|
+
// Keep the resolved trust decision after forwarded startup flags. Extension filtering
|
|
245
|
+
// must never interpret another flag's value (such as a prompt or credential) as a flag.
|
|
246
|
+
if (extensionArgsOverride) args.push(...extensionArgsOverride);
|
|
231
247
|
|
|
232
248
|
if (sessionDir) args.push("--session-dir", sessionDir);
|
|
233
249
|
if (resumeSession) args.push("--continue");
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
DefaultPackageManager,
|
|
6
|
+
SettingsManager,
|
|
7
|
+
getAgentDir,
|
|
8
|
+
type ResolvedResource,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
export function excludedExtensions(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
12
|
+
return [
|
|
13
|
+
...new Set(
|
|
14
|
+
[env["PI-SUBAGENT-EXCLUDE-EXTENSIONS"], env.PI_SUBAGENT_EXCLUDE_EXTENSIONS]
|
|
15
|
+
.flatMap((value) => (value ?? "").split(","))
|
|
16
|
+
.map((value) => value.trim())
|
|
17
|
+
.filter(Boolean),
|
|
18
|
+
),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function subagentDisabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
23
|
+
return [env.PI_SUBAGENT_DISABLED, env["PI-SUBAGENT-DISABLED"]].some(
|
|
24
|
+
(value) => value === "1" || value?.toLowerCase() === "true",
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolvedPath(value: string, cwd: string): string {
|
|
29
|
+
const absolute = path.resolve(cwd, value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value);
|
|
30
|
+
return fs.existsSync(absolute) ? fs.realpathSync(absolute) : absolute;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function npmName(source: string): string | undefined {
|
|
34
|
+
const spec = source.replace(/^npm:/, "");
|
|
35
|
+
return /^(?:@[^/\s]+\/)?[^/@\s]+(?:@[^\s]+)?$/.test(spec)
|
|
36
|
+
? spec.slice(0, spec.indexOf("@", 1) === -1 ? undefined : spec.indexOf("@", 1))
|
|
37
|
+
: undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function manifestName(directory: string): string | undefined {
|
|
41
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf8"));
|
|
42
|
+
return typeof parsed === "object" && parsed !== null && "name" in parsed && typeof parsed.name === "string"
|
|
43
|
+
? parsed.name
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function packageName(resource: ResolvedResource, cwd: string, agentDir: string): string | undefined {
|
|
48
|
+
if (resource.metadata.source.startsWith("npm:")) return npmName(resource.metadata.source);
|
|
49
|
+
if (resource.metadata.origin === "package" && resource.metadata.baseDir) {
|
|
50
|
+
const base = resolvedPath(resource.metadata.baseDir, cwd);
|
|
51
|
+
if (fs.existsSync(path.join(base, "package.json"))) return manifestName(base);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const resourcePath = resolvedPath(resource.path, cwd);
|
|
55
|
+
let directory = fs.statSync(resourcePath).isDirectory() ? resourcePath : path.dirname(resourcePath);
|
|
56
|
+
const boundaries = new Set(
|
|
57
|
+
[
|
|
58
|
+
cwd,
|
|
59
|
+
agentDir,
|
|
60
|
+
path.join(cwd, ".pi"),
|
|
61
|
+
path.join(cwd, ".pi/extensions"),
|
|
62
|
+
path.join(agentDir, "extensions"),
|
|
63
|
+
os.homedir(),
|
|
64
|
+
].map((entry) => resolvedPath(entry, cwd)),
|
|
65
|
+
);
|
|
66
|
+
// A loose hook belongs to neither the surrounding workspace nor Pi's config
|
|
67
|
+
// directory. Resolve symlinks first, and stop at the nearest manifest even if unnamed.
|
|
68
|
+
while (!boundaries.has(directory) && directory !== path.dirname(directory)) {
|
|
69
|
+
if (fs.existsSync(path.join(directory, "package.json"))) return manifestName(directory);
|
|
70
|
+
directory = path.dirname(directory);
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isExcluded(resource: ResolvedResource, excludes: string[], cwd: string, agentDir: string): boolean {
|
|
76
|
+
const resourcePath = resolvedPath(resource.path, cwd);
|
|
77
|
+
const name = packageName(resource, cwd, agentDir);
|
|
78
|
+
return excludes.some((exclude) => {
|
|
79
|
+
if (
|
|
80
|
+
(resource.metadata.origin === "package" && exclude === resource.metadata.source) ||
|
|
81
|
+
(name !== undefined && npmName(exclude) === name)
|
|
82
|
+
)
|
|
83
|
+
return true;
|
|
84
|
+
const excludedPath = resolvedPath(exclude, cwd);
|
|
85
|
+
return resourcePath === excludedPath || resourcePath.startsWith(`${excludedPath}${path.sep}`);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isRemote(source: string): boolean {
|
|
90
|
+
return /^(?:npm:|git:|https?:\/\/|git@|ssh:)/.test(source);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Resolve before loading: filtering Extension objects would already have executed their factories. */
|
|
94
|
+
export async function childExtensionArgs(
|
|
95
|
+
args: string[],
|
|
96
|
+
cwd: string,
|
|
97
|
+
projectTrusted = false,
|
|
98
|
+
excludes = excludedExtensions(),
|
|
99
|
+
agentDir = getAgentDir(),
|
|
100
|
+
): Promise<string[]> {
|
|
101
|
+
if (excludes.length === 0) return args;
|
|
102
|
+
const rest: string[] = [];
|
|
103
|
+
const explicit: string[] = [];
|
|
104
|
+
let noExtensions = false;
|
|
105
|
+
for (let index = 0; index < args.length; index++) {
|
|
106
|
+
const arg = args[index];
|
|
107
|
+
if (arg === "-ne" || arg === "--no-extensions") noExtensions = true;
|
|
108
|
+
else if (arg === "-e" || arg === "--extension") {
|
|
109
|
+
const source = args[++index];
|
|
110
|
+
if (!source) throw new Error("Missing explicit extension source");
|
|
111
|
+
explicit.push(source);
|
|
112
|
+
} else if (["-a", "--approve", "-na", "--no-approve"].includes(arg)) {
|
|
113
|
+
// The live parent's decision is authoritative, not stale startup flags.
|
|
114
|
+
} else rest.push(arg);
|
|
115
|
+
}
|
|
116
|
+
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
|
|
117
|
+
if (settingsManager.drainErrors().length > 0) throw new Error("Cannot resolve child extensions: invalid Pi settings");
|
|
118
|
+
const manager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
|
|
119
|
+
const automatic = noExtensions ? [] : (await manager.resolve(async () => "error")).extensions;
|
|
120
|
+
const explicitResources: ResolvedResource[] = [];
|
|
121
|
+
for (const source of explicit) {
|
|
122
|
+
let resolved;
|
|
123
|
+
if (isRemote(source)) {
|
|
124
|
+
// resolveExtensionSources can install or refresh remote sources. Use the read-only
|
|
125
|
+
// missing-source policy instead; temporary-only remote installs must be installed normally first.
|
|
126
|
+
const explicitSettings = SettingsManager.inMemory(
|
|
127
|
+
{
|
|
128
|
+
npmCommand: settingsManager.getGlobalSettings().npmCommand,
|
|
129
|
+
packages: [source],
|
|
130
|
+
},
|
|
131
|
+
{ projectTrusted: false },
|
|
132
|
+
);
|
|
133
|
+
const explicitManager = new DefaultPackageManager({ cwd, agentDir, settingsManager: explicitSettings });
|
|
134
|
+
if (!explicitManager.getInstalledPath(source, "user")) {
|
|
135
|
+
throw new Error(`Explicit extension source must be installed before exclusion filtering: ${source}`);
|
|
136
|
+
}
|
|
137
|
+
resolved = await explicitManager.resolve(async () => "error");
|
|
138
|
+
for (const kind of ["extensions", "skills", "prompts", "themes"] as const) {
|
|
139
|
+
resolved[kind] = resolved[kind].filter(
|
|
140
|
+
(resource) => resource.metadata.origin === "package" && resource.metadata.source === source,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (Object.values(resolved).every((resources) => resources.length === 0)) {
|
|
144
|
+
throw new Error(`No installed resources match explicit extension source: ${source}`);
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
const local = resolvedPath(source, cwd);
|
|
148
|
+
if (!fs.existsSync(local)) throw new Error(`Missing explicit extension source: ${source}`);
|
|
149
|
+
resolved = await manager.resolveExtensionSources([local], { temporary: true });
|
|
150
|
+
}
|
|
151
|
+
explicitResources.push(...resolved.extensions);
|
|
152
|
+
for (const [kind, flag] of [
|
|
153
|
+
["skills", "--skill"],
|
|
154
|
+
["prompts", "--prompt-template"],
|
|
155
|
+
["themes", "--theme"],
|
|
156
|
+
] as const) {
|
|
157
|
+
for (const resource of resolved[kind]) if (resource.enabled) rest.push(flag, resource.path);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const kept = new Set<string>();
|
|
161
|
+
for (const resource of [...explicitResources, ...automatic]) {
|
|
162
|
+
if (!resource.enabled || isExcluded(resource, excludes, cwd, agentDir)) continue;
|
|
163
|
+
if (!fs.statSync(resource.path).isFile()) {
|
|
164
|
+
throw new Error(`Extension source did not resolve to files: ${resource.path}. Pass its entry point explicitly.`);
|
|
165
|
+
}
|
|
166
|
+
kept.add(resource.path);
|
|
167
|
+
}
|
|
168
|
+
return [
|
|
169
|
+
...rest,
|
|
170
|
+
projectTrusted ? "--approve" : "--no-approve",
|
|
171
|
+
"--no-extensions",
|
|
172
|
+
...[...kept].flatMap((entry) => ["-e", entry]),
|
|
173
|
+
];
|
|
174
|
+
}
|
package/runner/options.ts
CHANGED
|
@@ -17,6 +17,8 @@ export type RunningSubagentStartedCallback = (handle: RunningSubagentHandle) =>
|
|
|
17
17
|
export interface RunAgentOptions {
|
|
18
18
|
/** Working directory inherited by every subagent process. */
|
|
19
19
|
cwd: string;
|
|
20
|
+
/** Effective trust for this cwd; absent SDK callers fail closed. */
|
|
21
|
+
projectTrusted?: boolean;
|
|
20
22
|
/** All available agent configs. */
|
|
21
23
|
agents: AgentConfig[];
|
|
22
24
|
/** Name of the agent to run. */
|
package/runner/parallel.ts
CHANGED
|
@@ -40,6 +40,7 @@ export async function executeParallelSubprocess(
|
|
|
40
40
|
namesFile?: string;
|
|
41
41
|
budgets?: Array<SubagentBudget | undefined>;
|
|
42
42
|
smartDecision?: RunAgentOptions["smartDecision"];
|
|
43
|
+
projectTrusted?: boolean;
|
|
43
44
|
},
|
|
44
45
|
): Promise<{
|
|
45
46
|
content: Array<{ type: "text"; text: string }>;
|
|
@@ -123,6 +124,7 @@ export async function executeParallelSubprocess(
|
|
|
123
124
|
try {
|
|
124
125
|
result = await runAgentSubprocess({
|
|
125
126
|
cwd: defaultCwd,
|
|
127
|
+
projectTrusted: extras?.projectTrusted,
|
|
126
128
|
agents,
|
|
127
129
|
agentName: t.agent,
|
|
128
130
|
task: t.task,
|
package/runner/single.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
STARTUP_RETRY_BASE_BACKOFF_MS,
|
|
8
8
|
} from "./constants.js";
|
|
9
9
|
import type { RunAgentOptions } from "./options.js";
|
|
10
|
-
import { buildPiArgs } from "./arguments.js";
|
|
10
|
+
import { buildPiArgs, resolveChildExtensionArgs } from "./arguments.js";
|
|
11
11
|
import { writePromptToTempFile, cleanupTempDir, sessionDirExists } from "./files.js";
|
|
12
12
|
import { appendBoundedStderr, priorDescendantUsage, endedWithSyntheticResumeFailure } from "./result.js";
|
|
13
13
|
import { runAttempt } from "./attempt.js";
|
|
@@ -112,6 +112,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
try {
|
|
115
|
+
const extensionArgs = await resolveChildExtensionArgs(opts.cwd, opts.projectTrusted);
|
|
115
116
|
const selection = await selectSmartDecision(
|
|
116
117
|
opts.smartDecision,
|
|
117
118
|
{ systemPrompt: agent.systemPrompt, task },
|
|
@@ -137,6 +138,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
137
138
|
fallbackModel,
|
|
138
139
|
opts.rawPrompt === true,
|
|
139
140
|
selection,
|
|
141
|
+
extensionArgs,
|
|
140
142
|
);
|
|
141
143
|
const prompt =
|
|
142
144
|
result.budget && readBudget(result.budget).limit > 0
|