pi-subagents 0.13.3 → 0.13.4
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/CHANGELOG.md +10 -0
- package/README.md +3 -1
- package/async-execution.ts +2 -0
- package/execution.ts +1 -1
- package/intercom-bridge.ts +8 -0
- package/package.json +1 -1
- package/skills.ts +117 -25
- package/subagent-executor.ts +2 -6
- package/subagent-runner.ts +10 -2
- package/worktree.ts +27 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.13.4] - 2026-04-13
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Intercom orchestration now uses a runtime-only `subagent-chat-<id>` fallback target for unnamed sessions instead of persisting a generic session title, so `pi --resume` keeps showing transcript snippets while delegated intercom routing still works.
|
|
9
|
+
- GitHub Actions test workflow now uses `actions/checkout@v5` and `actions/setup-node@v5`, removing Node 20 action-runtime deprecation warnings ahead of the enforced Node 24 transition.
|
|
10
|
+
- Worktree cwd mapping now derives repo-relative prefixes from `git rev-parse --show-prefix` instead of `path.relative(realpath, realpath)`, fixing Windows 8.3/canonical-path mismatches that could map `agentCwd` back to the source repo instead of the created worktree.
|
|
11
|
+
- Async background runs now pass the parent process `argv[1]` through to the detached runner, so Windows child spawning keeps targeting the intended `pi` CLI entry point instead of accidentally treating the runner's `jiti` bootstrap script as `pi`.
|
|
12
|
+
- Intercom detach listeners now guard optional event-bus subscriptions with optional-call semantics, so delegated runs no longer fail when host event buses expose `emit` without `on`.
|
|
13
|
+
- Skill discovery no longer depends on runtime imports from `@mariozechner/pi-coding-agent`; it now resolves skills directly from configured filesystem paths, preventing `ERR_MODULE_NOT_FOUND` crashes in local/integration test environments.
|
|
14
|
+
|
|
5
15
|
## [0.13.3] - 2026-04-13
|
|
6
16
|
|
|
7
17
|
### Added
|
package/README.md
CHANGED
|
@@ -808,9 +808,11 @@ If intercom is unavailable in this run, continue the task normally.
|
|
|
808
808
|
Bridge activation also requires all of the following:
|
|
809
809
|
- [pi-intercom](https://github.com/nicobailon/pi-intercom) is installed (`pi install npm:pi-intercom`)
|
|
810
810
|
- `~/.pi/agent/intercom/config.json` is not set to `"enabled": false`
|
|
811
|
-
- the current session
|
|
811
|
+
- the current session can be targeted by intercom (existing `/name`, or the runtime-only fallback alias `subagent-chat-<id>` when unnamed)
|
|
812
812
|
- if agent `extensions` is an explicit allowlist, it must include `pi-intercom`
|
|
813
813
|
|
|
814
|
+
When an unnamed session falls back to `subagent-chat-<id>`, that alias is used only for the live intercom broker. It is not persisted as the Pi session title, so `pi --resume` can still show the transcript snippet.
|
|
815
|
+
|
|
814
816
|
### `worktreeSetupHook`
|
|
815
817
|
|
|
816
818
|
`worktreeSetupHook` configures an optional setup hook for worktree-isolated parallel runs. The hook runs once per created worktree, after `git worktree add` succeeds and before the agent starts.
|
package/async-execution.ts
CHANGED
|
@@ -264,6 +264,7 @@ export function executeAsyncChain(
|
|
|
264
264
|
asyncDir,
|
|
265
265
|
sessionId: ctx.currentSessionId,
|
|
266
266
|
piPackageRoot,
|
|
267
|
+
piArgv1: process.argv[1],
|
|
267
268
|
worktreeSetupHook,
|
|
268
269
|
worktreeSetupHookTimeoutMs,
|
|
269
270
|
},
|
|
@@ -384,6 +385,7 @@ export function executeAsyncSingle(
|
|
|
384
385
|
asyncDir,
|
|
385
386
|
sessionId: ctx.currentSessionId,
|
|
386
387
|
piPackageRoot,
|
|
388
|
+
piArgv1: process.argv[1],
|
|
387
389
|
worktreeSetupHook,
|
|
388
390
|
worktreeSetupHookTimeoutMs,
|
|
389
391
|
},
|
package/execution.ts
CHANGED
|
@@ -155,7 +155,7 @@ async function runSingleAttempt(
|
|
|
155
155
|
finish(-2);
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
-
const unsubscribeIntercomDetach = options.intercomEvents?.on(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
|
|
158
|
+
const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
|
|
159
159
|
if (!options.allowIntercomDetach || detached || processClosed) return;
|
|
160
160
|
if (!payload || typeof payload !== "object") return;
|
|
161
161
|
const requestId = (payload as { requestId?: unknown }).requestId;
|
package/intercom-bridge.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "
|
|
|
7
7
|
const DEFAULT_INTERCOM_EXTENSION_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "pi-intercom");
|
|
8
8
|
const DEFAULT_INTERCOM_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "intercom", "config.json");
|
|
9
9
|
const DEFAULT_SUBAGENT_CONFIG_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent");
|
|
10
|
+
const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
|
|
10
11
|
const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
|
|
11
12
|
const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `Use intercom only for coordination with the orchestrator session:
|
|
12
13
|
- Need a decision or blocked: intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })
|
|
@@ -30,6 +31,13 @@ interface ResolveIntercomBridgeInput {
|
|
|
30
31
|
settingsDir?: string;
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
export function resolveIntercomSessionTarget(sessionName: string | undefined, sessionId: string): string {
|
|
35
|
+
const trimmedName = sessionName?.trim();
|
|
36
|
+
if (trimmedName) return trimmedName;
|
|
37
|
+
const normalizedSessionId = sessionId.startsWith("session-") ? sessionId.slice("session-".length) : sessionId;
|
|
38
|
+
return `${DEFAULT_INTERCOM_TARGET_PREFIX}-${normalizedSessionId.slice(0, 8)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
export function resolveIntercomBridgeMode(value: unknown): IntercomBridgeMode {
|
|
34
42
|
if (value === "off" || value === "always" || value === "fork-only") return value;
|
|
35
43
|
return "always";
|
package/package.json
CHANGED
package/skills.ts
CHANGED
|
@@ -6,7 +6,6 @@ import { execSync } from "node:child_process";
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import { loadSkills, type Skill } from "@mariozechner/pi-coding-agent";
|
|
10
9
|
|
|
11
10
|
export type SkillSource =
|
|
12
11
|
| "project"
|
|
@@ -86,6 +85,7 @@ function getPackageSkillPaths(packageRoot: string): string[] {
|
|
|
86
85
|
.filter((s: unknown) => typeof s === "string")
|
|
87
86
|
.map((s: string) => path.resolve(packageRoot, s));
|
|
88
87
|
} catch {
|
|
88
|
+
// Package scanning is opportunistic; ignore malformed/missing package metadata.
|
|
89
89
|
return [];
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -98,6 +98,7 @@ function getGlobalNpmRoot(): string | null {
|
|
|
98
98
|
cachedGlobalNpmRoot = execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim();
|
|
99
99
|
return cachedGlobalNpmRoot;
|
|
100
100
|
} catch {
|
|
101
|
+
// Global npm root is optional in constrained environments.
|
|
101
102
|
cachedGlobalNpmRoot = ""; // Empty string means "tried but failed"
|
|
102
103
|
return null;
|
|
103
104
|
}
|
|
@@ -123,6 +124,7 @@ function collectPackageSkillPaths(cwd: string): string[] {
|
|
|
123
124
|
try {
|
|
124
125
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
125
126
|
} catch {
|
|
127
|
+
// Ignore unreadable package roots and continue scanning other roots.
|
|
126
128
|
continue;
|
|
127
129
|
}
|
|
128
130
|
|
|
@@ -136,6 +138,7 @@ function collectPackageSkillPaths(cwd: string): string[] {
|
|
|
136
138
|
try {
|
|
137
139
|
scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
|
|
138
140
|
} catch {
|
|
141
|
+
// Ignore unreadable scoped package directories and continue.
|
|
139
142
|
continue;
|
|
140
143
|
}
|
|
141
144
|
for (const scopeEntry of scopeEntries) {
|
|
@@ -178,7 +181,9 @@ function collectSettingsSkillPaths(cwd: string): string[] {
|
|
|
178
181
|
}
|
|
179
182
|
results.push(resolved);
|
|
180
183
|
}
|
|
181
|
-
} catch {
|
|
184
|
+
} catch {
|
|
185
|
+
// Settings-provided skills are optional; ignore malformed or missing settings files.
|
|
186
|
+
}
|
|
182
187
|
}
|
|
183
188
|
|
|
184
189
|
return results;
|
|
@@ -196,21 +201,22 @@ function buildSkillPaths(cwd: string): string[] {
|
|
|
196
201
|
return [...new Set([...defaultSkillPaths, ...packagePaths, ...settingsPaths])];
|
|
197
202
|
}
|
|
198
203
|
|
|
199
|
-
function inferSkillSource(
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
function inferSkillSource(filePath: string, cwd: string): SkillSource {
|
|
205
|
+
const projectConfigRoot = path.resolve(cwd, CONFIG_DIR);
|
|
206
|
+
const projectSkillsRoot = path.resolve(cwd, CONFIG_DIR, "skills");
|
|
207
|
+
const projectPackagesRoot = path.resolve(cwd, CONFIG_DIR, "npm", "node_modules");
|
|
208
|
+
const projectAgentsRoot = path.resolve(cwd, ".agents");
|
|
209
|
+
const userSkillsRoot = path.resolve(AGENT_DIR, "skills");
|
|
210
|
+
const userPackagesRoot = path.resolve(AGENT_DIR, "npm", "node_modules");
|
|
211
|
+
const userAgentsRoot = path.resolve(os.homedir(), ".agents");
|
|
204
212
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const altProjectRoot = path.resolve(cwd, ".agents");
|
|
209
|
-
const isProjectScoped = isWithinPath(filePath, projectRoot) || isWithinPath(filePath, altProjectRoot);
|
|
210
|
-
if (isProjectScoped) return "project";
|
|
213
|
+
if (isWithinPath(filePath, projectPackagesRoot)) return "project-package";
|
|
214
|
+
if (isWithinPath(filePath, projectSkillsRoot) || isWithinPath(filePath, projectAgentsRoot)) return "project";
|
|
215
|
+
if (isWithinPath(filePath, projectConfigRoot)) return "project-settings";
|
|
211
216
|
|
|
212
|
-
|
|
213
|
-
if (
|
|
217
|
+
if (isWithinPath(filePath, userPackagesRoot)) return "user-package";
|
|
218
|
+
if (isWithinPath(filePath, userSkillsRoot) || isWithinPath(filePath, userAgentsRoot)) return "user";
|
|
219
|
+
if (isWithinPath(filePath, AGENT_DIR)) return "user-settings";
|
|
214
220
|
|
|
215
221
|
const globalRoot = getGlobalNpmRoot();
|
|
216
222
|
if (globalRoot && isWithinPath(filePath, globalRoot)) return "user-package";
|
|
@@ -227,6 +233,99 @@ function chooseHigherPrioritySkill(existing: CachedSkillEntry | undefined, candi
|
|
|
227
233
|
return candidate.order < existing.order ? candidate : existing;
|
|
228
234
|
}
|
|
229
235
|
|
|
236
|
+
function maybeReadSkillDescription(filePath: string): string | undefined {
|
|
237
|
+
try {
|
|
238
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
239
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
240
|
+
if (!normalized.startsWith("---")) return undefined;
|
|
241
|
+
|
|
242
|
+
const endIndex = normalized.indexOf("\n---", 3);
|
|
243
|
+
if (endIndex === -1) return undefined;
|
|
244
|
+
|
|
245
|
+
const frontmatter = normalized.slice(3, endIndex).trim();
|
|
246
|
+
const match = frontmatter.match(/^description:\s*(.+)$/m);
|
|
247
|
+
if (!match) return undefined;
|
|
248
|
+
return match[1]?.trim().replace(/^['\"]|['\"]$/g, "");
|
|
249
|
+
} catch {
|
|
250
|
+
// Description parsing is best-effort metadata extraction.
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function collectFilesystemSkills(cwd: string, skillPaths: string[]): CachedSkillEntry[] {
|
|
256
|
+
const entries: CachedSkillEntry[] = [];
|
|
257
|
+
const seen = new Set<string>();
|
|
258
|
+
let order = 0;
|
|
259
|
+
|
|
260
|
+
const pushEntry = (name: string, filePath: string) => {
|
|
261
|
+
const resolvedFile = path.resolve(filePath);
|
|
262
|
+
if (seen.has(resolvedFile)) return;
|
|
263
|
+
if (!fs.existsSync(resolvedFile)) return;
|
|
264
|
+
seen.add(resolvedFile);
|
|
265
|
+
entries.push({
|
|
266
|
+
name,
|
|
267
|
+
filePath: resolvedFile,
|
|
268
|
+
source: inferSkillSource(resolvedFile, cwd),
|
|
269
|
+
description: maybeReadSkillDescription(resolvedFile),
|
|
270
|
+
order: order++,
|
|
271
|
+
});
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
for (const skillPath of skillPaths) {
|
|
275
|
+
if (!fs.existsSync(skillPath)) continue;
|
|
276
|
+
|
|
277
|
+
let stat: fs.Stats;
|
|
278
|
+
try {
|
|
279
|
+
stat = fs.statSync(skillPath);
|
|
280
|
+
} catch {
|
|
281
|
+
// Ignore paths that disappear or become unreadable during discovery.
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (stat.isFile()) {
|
|
286
|
+
const fileName = path.basename(skillPath);
|
|
287
|
+
if (!fileName.toLowerCase().endsWith(".md")) continue;
|
|
288
|
+
const skillName = fileName.toLowerCase() === "skill.md"
|
|
289
|
+
? path.basename(path.dirname(skillPath))
|
|
290
|
+
: path.basename(fileName, path.extname(fileName));
|
|
291
|
+
pushEntry(skillName, skillPath);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!stat.isDirectory()) continue;
|
|
296
|
+
|
|
297
|
+
const rootSkillFile = path.join(skillPath, "SKILL.md");
|
|
298
|
+
if (fs.existsSync(rootSkillFile)) {
|
|
299
|
+
pushEntry(path.basename(skillPath), rootSkillFile);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let childEntries: fs.Dirent[];
|
|
303
|
+
try {
|
|
304
|
+
childEntries = fs.readdirSync(skillPath, { withFileTypes: true });
|
|
305
|
+
} catch {
|
|
306
|
+
// Ignore unreadable skill directories and continue scanning.
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const child of childEntries) {
|
|
311
|
+
if (child.name.startsWith(".")) continue;
|
|
312
|
+
const childPath = path.join(skillPath, child.name);
|
|
313
|
+
if (child.isDirectory() || child.isSymbolicLink()) {
|
|
314
|
+
const nestedSkillPath = path.join(childPath, "SKILL.md");
|
|
315
|
+
if (fs.existsSync(nestedSkillPath)) {
|
|
316
|
+
pushEntry(child.name, nestedSkillPath);
|
|
317
|
+
}
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (child.isFile() && child.name.toLowerCase().endsWith(".md")) {
|
|
321
|
+
pushEntry(path.basename(child.name, path.extname(child.name)), childPath);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return entries;
|
|
327
|
+
}
|
|
328
|
+
|
|
230
329
|
function getCachedSkills(cwd: string): CachedSkillEntry[] {
|
|
231
330
|
const now = Date.now();
|
|
232
331
|
if (loadSkillsCache && loadSkillsCache.cwd === cwd && now - loadSkillsCache.timestamp < LOAD_SKILLS_CACHE_TTL_MS) {
|
|
@@ -234,18 +333,10 @@ function getCachedSkills(cwd: string): CachedSkillEntry[] {
|
|
|
234
333
|
}
|
|
235
334
|
|
|
236
335
|
const skillPaths = buildSkillPaths(cwd);
|
|
237
|
-
const loaded =
|
|
336
|
+
const loaded = collectFilesystemSkills(cwd, skillPaths);
|
|
238
337
|
const dedupedByName = new Map<string, CachedSkillEntry>();
|
|
239
338
|
|
|
240
|
-
for (
|
|
241
|
-
const skill = loaded.skills[i] as Skill;
|
|
242
|
-
const entry: CachedSkillEntry = {
|
|
243
|
-
name: skill.name,
|
|
244
|
-
filePath: skill.filePath,
|
|
245
|
-
source: inferSkillSource(skill.sourceInfo, skill.filePath, cwd),
|
|
246
|
-
description: skill.description,
|
|
247
|
-
order: i,
|
|
248
|
-
};
|
|
339
|
+
for (const entry of loaded) {
|
|
249
340
|
const current = dedupedByName.get(entry.name);
|
|
250
341
|
dedupedByName.set(entry.name, chooseHigherPrioritySkill(current, entry));
|
|
251
342
|
}
|
|
@@ -294,6 +385,7 @@ export function readSkill(
|
|
|
294
385
|
|
|
295
386
|
return skill;
|
|
296
387
|
} catch {
|
|
388
|
+
// Treat unreadable skill files as unresolved so callers can surface as missing.
|
|
297
389
|
return undefined;
|
|
298
390
|
}
|
|
299
391
|
}
|
package/subagent-executor.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import { discoverAvailableSkills, normalizeSkillInput } from "./skills.ts";
|
|
23
23
|
import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async-execution.ts";
|
|
24
24
|
import { createForkContextResolver } from "./fork-context.ts";
|
|
25
|
-
import { applyIntercomBridgeToAgent, resolveIntercomBridge } from "./intercom-bridge.ts";
|
|
25
|
+
import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget } from "./intercom-bridge.ts";
|
|
26
26
|
import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
|
|
27
27
|
import { getSingleResultOutput, mapConcurrent } from "./utils.ts";
|
|
28
28
|
import {
|
|
@@ -1117,11 +1117,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1117
1117
|
const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
1118
1118
|
deps.state.currentSessionId = parentSessionFile ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1119
1119
|
const discoveredAgents = deps.discoverAgents(ctx.cwd, scope).agents;
|
|
1120
|
-
|
|
1121
|
-
if (!sessionName) {
|
|
1122
|
-
sessionName = `session-${ctx.sessionManager.getSessionId().slice(0, 8)}`;
|
|
1123
|
-
deps.pi.setSessionName(sessionName);
|
|
1124
|
-
}
|
|
1120
|
+
const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
|
|
1125
1121
|
const intercomBridge = resolveIntercomBridge({
|
|
1126
1122
|
config: deps.config.intercomBridge,
|
|
1127
1123
|
context: normalizedParams.context,
|
package/subagent-runner.ts
CHANGED
|
@@ -53,6 +53,7 @@ interface SubagentRunConfig {
|
|
|
53
53
|
asyncDir: string;
|
|
54
54
|
sessionId?: string | null;
|
|
55
55
|
piPackageRoot?: string;
|
|
56
|
+
piArgv1?: string;
|
|
56
57
|
worktreeSetupHook?: string;
|
|
57
58
|
worktreeSetupHookTimeoutMs?: number;
|
|
58
59
|
}
|
|
@@ -156,12 +157,16 @@ function runPiStreaming(
|
|
|
156
157
|
outputFile: string,
|
|
157
158
|
env?: Record<string, string | undefined>,
|
|
158
159
|
piPackageRoot?: string,
|
|
160
|
+
piArgv1?: string,
|
|
159
161
|
maxSubagentDepth?: number,
|
|
160
162
|
): Promise<{ stdout: string; stderr: string; exitCode: number | null }> {
|
|
161
163
|
return new Promise((resolve) => {
|
|
162
164
|
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
163
165
|
const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
|
|
164
|
-
const spawnSpec = getPiSpawnCommand(args,
|
|
166
|
+
const spawnSpec = getPiSpawnCommand(args, {
|
|
167
|
+
...(piPackageRoot ? { piPackageRoot } : {}),
|
|
168
|
+
...(piArgv1 ? { argv1: piArgv1 } : {}),
|
|
169
|
+
});
|
|
165
170
|
const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
|
|
166
171
|
let stdout = "";
|
|
167
172
|
let stderr = "";
|
|
@@ -340,6 +345,7 @@ interface SingleStepContext {
|
|
|
340
345
|
flatStepCount: number;
|
|
341
346
|
outputFile: string;
|
|
342
347
|
piPackageRoot?: string;
|
|
348
|
+
piArgv1?: string;
|
|
343
349
|
}
|
|
344
350
|
|
|
345
351
|
/** Run a single pi agent step, returning output and metadata */
|
|
@@ -401,7 +407,7 @@ async function runSingleStep(
|
|
|
401
407
|
promptFileStem: step.agent,
|
|
402
408
|
});
|
|
403
409
|
const outputFile = index === 0 ? ctx.outputFile : `${ctx.outputFile}.attempt-${index + 1}`;
|
|
404
|
-
const run = await runPiStreaming(args, step.cwd ?? ctx.cwd, outputFile, env, ctx.piPackageRoot, step.maxSubagentDepth);
|
|
410
|
+
const run = await runPiStreaming(args, step.cwd ?? ctx.cwd, outputFile, env, ctx.piPackageRoot, ctx.piArgv1, step.maxSubagentDepth);
|
|
405
411
|
cleanupTempDir(tempDir);
|
|
406
412
|
|
|
407
413
|
const parsed = parseRunOutput(run.stdout);
|
|
@@ -759,6 +765,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
759
765
|
flatIndex: fi, flatStepCount: flatSteps.length,
|
|
760
766
|
outputFile: path.join(asyncDir, `output-${fi}.log`),
|
|
761
767
|
piPackageRoot: config.piPackageRoot,
|
|
768
|
+
piArgv1: config.piArgv1,
|
|
762
769
|
});
|
|
763
770
|
if (task.sessionFile) {
|
|
764
771
|
latestSessionFile = task.sessionFile;
|
|
@@ -879,6 +886,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
879
886
|
flatIndex, flatStepCount: flatSteps.length,
|
|
880
887
|
outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
|
|
881
888
|
piPackageRoot: config.piPackageRoot,
|
|
889
|
+
piArgv1: config.piArgv1,
|
|
882
890
|
});
|
|
883
891
|
if (seqStep.sessionFile) {
|
|
884
892
|
latestSessionFile = seqStep.sessionFile;
|
package/worktree.ts
CHANGED
|
@@ -106,9 +106,11 @@ function resolveRepoState(cwd: string): RepoState {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
const toplevel = runGitChecked(cwd, ["rev-parse", "--show-toplevel"]).trim();
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
109
|
+
const rawPrefix = runGitChecked(cwd, ["rev-parse", "--show-prefix"]).trim();
|
|
110
|
+
const normalizedPrefix = rawPrefix
|
|
111
|
+
? path.normalize(rawPrefix.replace(/[\\/]+$/, ""))
|
|
112
|
+
: "";
|
|
113
|
+
const cwdRelative = normalizedPrefix === "." ? "" : normalizedPrefix;
|
|
112
114
|
|
|
113
115
|
const status = runGitChecked(toplevel, ["status", "--porcelain"]);
|
|
114
116
|
if (status.trim().length > 0) {
|
|
@@ -124,6 +126,7 @@ function normalizeComparableCwd(cwd: string): string {
|
|
|
124
126
|
try {
|
|
125
127
|
return fs.realpathSync(resolved);
|
|
126
128
|
} catch {
|
|
129
|
+
// Use the unresolved absolute path when realpath resolution is unavailable.
|
|
127
130
|
return resolved;
|
|
128
131
|
}
|
|
129
132
|
}
|
|
@@ -169,6 +172,7 @@ function linkNodeModulesIfPresent(toplevel: string, worktreePath: string): boole
|
|
|
169
172
|
fs.symlinkSync(nodeModulesPath, nodeModulesLinkPath);
|
|
170
173
|
return true;
|
|
171
174
|
} catch {
|
|
175
|
+
// Symlink creation is optional (e.g., unsupported filesystems on CI runners).
|
|
172
176
|
return false;
|
|
173
177
|
}
|
|
174
178
|
}
|
|
@@ -344,8 +348,12 @@ function createSingleWorktree(
|
|
|
344
348
|
syntheticPaths,
|
|
345
349
|
};
|
|
346
350
|
} catch (error) {
|
|
347
|
-
try { runGitChecked(toplevel, ["worktree", "remove", "--force", worktreePath]); } catch {
|
|
348
|
-
|
|
351
|
+
try { runGitChecked(toplevel, ["worktree", "remove", "--force", worktreePath]); } catch {
|
|
352
|
+
// Best-effort rollback; preserve the original setup failure.
|
|
353
|
+
}
|
|
354
|
+
try { runGitChecked(toplevel, ["branch", "-D", branch]); } catch {
|
|
355
|
+
// Best-effort rollback; preserve the original setup failure.
|
|
356
|
+
}
|
|
349
357
|
throw error;
|
|
350
358
|
}
|
|
351
359
|
}
|
|
@@ -453,12 +461,18 @@ function captureWorktreeDiff(
|
|
|
453
461
|
function writeEmptyPatch(patchPath: string): void {
|
|
454
462
|
try {
|
|
455
463
|
fs.writeFileSync(patchPath, "", "utf-8");
|
|
456
|
-
} catch {
|
|
464
|
+
} catch {
|
|
465
|
+
// Diff artifact writing is best-effort in error paths.
|
|
466
|
+
}
|
|
457
467
|
}
|
|
458
468
|
|
|
459
469
|
function cleanupSingleWorktree(repoCwd: string, worktree: WorktreeInfo): void {
|
|
460
|
-
try { runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]); } catch {
|
|
461
|
-
|
|
470
|
+
try { runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]); } catch {
|
|
471
|
+
// Cleanup is best-effort to avoid masking caller errors.
|
|
472
|
+
}
|
|
473
|
+
try { runGitChecked(repoCwd, ["branch", "-D", worktree.branch]); } catch {
|
|
474
|
+
// Cleanup is best-effort to avoid masking caller errors.
|
|
475
|
+
}
|
|
462
476
|
}
|
|
463
477
|
|
|
464
478
|
function hasWorktreeChanges(diff: WorktreeDiff): boolean {
|
|
@@ -502,6 +516,7 @@ export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir:
|
|
|
502
516
|
try {
|
|
503
517
|
fs.mkdirSync(diffsDir, { recursive: true });
|
|
504
518
|
} catch {
|
|
519
|
+
// Returning no diffs is safer than failing the whole command on artifact-dir issues.
|
|
505
520
|
return [];
|
|
506
521
|
}
|
|
507
522
|
|
|
@@ -513,6 +528,7 @@ export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir:
|
|
|
513
528
|
try {
|
|
514
529
|
diffs.push(captureWorktreeDiff(setup, worktree, agent, patchPath));
|
|
515
530
|
} catch {
|
|
531
|
+
// Preserve execution flow; failed diff capture maps to an empty per-task patch.
|
|
516
532
|
writeEmptyPatch(patchPath);
|
|
517
533
|
diffs.push(emptyDiff(index, agent, worktree.branch, patchPath));
|
|
518
534
|
}
|
|
@@ -525,7 +541,9 @@ export function cleanupWorktrees(setup: WorktreeSetup): void {
|
|
|
525
541
|
for (let index = setup.worktrees.length - 1; index >= 0; index--) {
|
|
526
542
|
cleanupSingleWorktree(setup.cwd, setup.worktrees[index]!);
|
|
527
543
|
}
|
|
528
|
-
try { runGitChecked(setup.cwd, ["worktree", "prune"]); } catch {
|
|
544
|
+
try { runGitChecked(setup.cwd, ["worktree", "prune"]); } catch {
|
|
545
|
+
// Pruning is best-effort cleanup.
|
|
546
|
+
}
|
|
529
547
|
}
|
|
530
548
|
|
|
531
549
|
export function formatWorktreeDiffSummary(diffs: WorktreeDiff[]): string {
|