pi-subagents 0.13.2 → 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 +20 -0
- package/README.md +23 -12
- package/async-execution.ts +2 -0
- package/execution.ts +76 -15
- package/index.ts +1 -1
- package/intercom-bridge.ts +71 -17
- package/package.json +1 -1
- package/render.ts +5 -3
- package/skills.ts +117 -25
- package/subagent-executor.ts +19 -5
- package/subagent-runner.ts +10 -2
- package/types.ts +21 -2
- package/worktree.ts +27 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
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
|
+
|
|
15
|
+
## [0.13.3] - 2026-04-13
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Added `intercomBridge.instructionFile` so subagent intercom guidance can be overridden from a Markdown template with `{orchestratorTarget}` interpolation.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Intercom-enabled delegated runs now detach only after the child actually starts the `intercom` tool, preserving clean sync behavior until coordination is needed.
|
|
22
|
+
- Graceful intercom coordination no longer leaves detached child runs vulnerable to later parent abort listeners, and reply confirmation follow-ups avoid unnecessary orchestrator aborts.
|
|
23
|
+
- Child process spawn failures now preserve the original error message instead of collapsing to a generic failure.
|
|
24
|
+
|
|
5
25
|
## [0.13.2] - 2026-04-13
|
|
6
26
|
|
|
7
27
|
### Changed
|
package/README.md
CHANGED
|
@@ -14,12 +14,6 @@ https://github.com/user-attachments/assets/702554ec-faaf-4635-80aa-fb5d6e292fd1
|
|
|
14
14
|
pi install npm:pi-subagents
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
To remove:
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
npx pi-subagents --remove
|
|
21
|
-
```
|
|
22
|
-
|
|
23
17
|
If you use [pi-prompt-template-model](https://github.com/nicobailon/pi-prompt-template-model), you can wrap subagent delegation in a slash command:
|
|
24
18
|
|
|
25
19
|
```markdown
|
|
@@ -787,21 +781,38 @@ Controls whether subagents receive runtime intercom coordination instructions (a
|
|
|
787
781
|
|
|
788
782
|
```json
|
|
789
783
|
{
|
|
790
|
-
|
|
784
|
+
"intercomBridge": {
|
|
785
|
+
"mode": "always",
|
|
786
|
+
"instructionFile": "./intercom-bridge.md"
|
|
787
|
+
}
|
|
791
788
|
}
|
|
792
789
|
```
|
|
793
790
|
|
|
794
|
-
|
|
795
|
-
- `"
|
|
796
|
-
- `"
|
|
797
|
-
|
|
791
|
+
Fields:
|
|
792
|
+
- `"mode"` (default: `"always"`): inject bridge in both `fresh` and `fork`, only in `fork`, or disable it entirely
|
|
793
|
+
- `"instructionFile"` (optional): path to a Markdown template that replaces the default injected subagent intercom instructions. Supports `{orchestratorTarget}` placeholder interpolation. Relative paths resolve from `~/.pi/agent/extensions/subagent/`.
|
|
794
|
+
|
|
795
|
+
Example `instructionFile`:
|
|
796
|
+
|
|
797
|
+
```md
|
|
798
|
+
Intercom orchestration channel:
|
|
799
|
+
|
|
800
|
+
Use `intercom` only to coordinate with the orchestrator session `{orchestratorTarget}`.
|
|
801
|
+
|
|
802
|
+
- Need a decision or you're blocked: `intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })`
|
|
803
|
+
- Need to report progress or completion: `intercom({ action: "send", to: "{orchestratorTarget}", message: "DONE: <summary>" })`
|
|
804
|
+
|
|
805
|
+
If intercom is unavailable in this run, continue the task normally.
|
|
806
|
+
```
|
|
798
807
|
|
|
799
808
|
Bridge activation also requires all of the following:
|
|
800
809
|
- [pi-intercom](https://github.com/nicobailon/pi-intercom) is installed (`pi install npm:pi-intercom`)
|
|
801
810
|
- `~/.pi/agent/intercom/config.json` is not set to `"enabled": false`
|
|
802
|
-
- 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)
|
|
803
812
|
- if agent `extensions` is an explicit allowlist, it must include `pi-intercom`
|
|
804
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
|
+
|
|
805
816
|
### `worktreeSetupHook`
|
|
806
817
|
|
|
807
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
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
type SingleResult,
|
|
20
20
|
type Usage,
|
|
21
21
|
DEFAULT_MAX_OUTPUT,
|
|
22
|
+
INTERCOM_DETACH_REQUEST_EVENT,
|
|
23
|
+
INTERCOM_DETACH_RESPONSE_EVENT,
|
|
22
24
|
truncateOutput,
|
|
23
25
|
getSubagentDepthEnv,
|
|
24
26
|
} from "./types.ts";
|
|
@@ -123,7 +125,6 @@ async function runSingleAttempt(
|
|
|
123
125
|
const startTime = Date.now();
|
|
124
126
|
const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
|
|
125
127
|
|
|
126
|
-
let closeJsonlWriter: (() => Promise<void>) | undefined;
|
|
127
128
|
const exitCode = await new Promise<number>((resolve) => {
|
|
128
129
|
const spawnSpec = getPiSpawnCommand(args);
|
|
129
130
|
const proc = spawn(spawnSpec.command, spawnSpec.args, {
|
|
@@ -132,9 +133,46 @@ async function runSingleAttempt(
|
|
|
132
133
|
stdio: ["ignore", "pipe", "pipe"],
|
|
133
134
|
});
|
|
134
135
|
const jsonlWriter = createJsonlWriter(shared.jsonlPath, proc.stdout);
|
|
135
|
-
closeJsonlWriter = () => jsonlWriter.close();
|
|
136
136
|
let buf = "";
|
|
137
137
|
let processClosed = false;
|
|
138
|
+
let settled = false;
|
|
139
|
+
let detached = false;
|
|
140
|
+
let intercomStarted = false;
|
|
141
|
+
let removeAbortListener: (() => void) | undefined;
|
|
142
|
+
|
|
143
|
+
const detachForIntercom = () => {
|
|
144
|
+
detached = true;
|
|
145
|
+
processClosed = true;
|
|
146
|
+
result.detached = true;
|
|
147
|
+
result.detachedReason = "intercom coordination";
|
|
148
|
+
progress.status = "detached";
|
|
149
|
+
progress.durationMs = Date.now() - startTime;
|
|
150
|
+
result.progressSummary = {
|
|
151
|
+
toolCount: progress.toolCount,
|
|
152
|
+
tokens: progress.tokens,
|
|
153
|
+
durationMs: progress.durationMs,
|
|
154
|
+
};
|
|
155
|
+
finish(-2);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
|
|
159
|
+
if (!options.allowIntercomDetach || detached || processClosed) return;
|
|
160
|
+
if (!payload || typeof payload !== "object") return;
|
|
161
|
+
const requestId = (payload as { requestId?: unknown }).requestId;
|
|
162
|
+
if (typeof requestId !== "string" || requestId.length === 0) return;
|
|
163
|
+
const accepted = intercomStarted;
|
|
164
|
+
options.intercomEvents?.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted });
|
|
165
|
+
if (!accepted) return;
|
|
166
|
+
detachForIntercom();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const finish = (code: number) => {
|
|
170
|
+
if (settled) return;
|
|
171
|
+
settled = true;
|
|
172
|
+
unsubscribeIntercomDetach?.();
|
|
173
|
+
removeAbortListener?.();
|
|
174
|
+
resolve(code);
|
|
175
|
+
};
|
|
138
176
|
|
|
139
177
|
const fireUpdate = () => {
|
|
140
178
|
if (!options.onUpdate || processClosed) return;
|
|
@@ -154,6 +192,9 @@ async function runSingleAttempt(
|
|
|
154
192
|
progress.durationMs = now - startTime;
|
|
155
193
|
|
|
156
194
|
if (evt.type === "tool_execution_start") {
|
|
195
|
+
if (options.allowIntercomDetach && evt.toolName === "intercom") {
|
|
196
|
+
intercomStarted = true;
|
|
197
|
+
}
|
|
157
198
|
progress.toolCount++;
|
|
158
199
|
progress.currentTool = evt.toolName;
|
|
159
200
|
progress.currentToolArgs = extractToolArgsPreview((evt.args || {}) as Record<string, unknown>);
|
|
@@ -215,35 +256,55 @@ async function runSingleAttempt(
|
|
|
215
256
|
stderrBuf += d.toString();
|
|
216
257
|
});
|
|
217
258
|
proc.on("close", (code) => {
|
|
259
|
+
void jsonlWriter.close().catch(() => {
|
|
260
|
+
// JSONL artifact flush is best effort.
|
|
261
|
+
});
|
|
262
|
+
cleanupTempDir(tempDir);
|
|
263
|
+
if (detached) {
|
|
264
|
+
finish(-2);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
218
267
|
processClosed = true;
|
|
219
268
|
if (buf.trim()) processLine(buf);
|
|
220
269
|
if (code !== 0 && stderrBuf.trim() && !result.error) {
|
|
221
270
|
result.error = stderrBuf.trim();
|
|
222
271
|
}
|
|
223
|
-
|
|
272
|
+
finish(code ?? 0);
|
|
273
|
+
});
|
|
274
|
+
proc.on("error", (error) => {
|
|
275
|
+
void jsonlWriter.close().catch(() => {
|
|
276
|
+
// JSONL artifact flush is best effort.
|
|
277
|
+
});
|
|
278
|
+
cleanupTempDir(tempDir);
|
|
279
|
+
if (!result.error) {
|
|
280
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
281
|
+
}
|
|
282
|
+
finish(1);
|
|
224
283
|
});
|
|
225
|
-
proc.on("error", () => resolve(1));
|
|
226
284
|
|
|
227
285
|
if (options.signal) {
|
|
228
286
|
const kill = () => {
|
|
287
|
+
if (processClosed || detached) return;
|
|
288
|
+
if (options.allowIntercomDetach && intercomStarted && !detached) {
|
|
289
|
+
detachForIntercom();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
229
292
|
proc.kill("SIGTERM");
|
|
230
293
|
setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
|
|
231
294
|
};
|
|
232
295
|
if (options.signal.aborted) kill();
|
|
233
|
-
else
|
|
296
|
+
else {
|
|
297
|
+
options.signal.addEventListener("abort", kill, { once: true });
|
|
298
|
+
removeAbortListener = () => options.signal?.removeEventListener("abort", kill);
|
|
299
|
+
}
|
|
234
300
|
}
|
|
235
301
|
});
|
|
236
|
-
|
|
237
|
-
if (closeJsonlWriter) {
|
|
238
|
-
try {
|
|
239
|
-
await closeJsonlWriter();
|
|
240
|
-
} catch {
|
|
241
|
-
// JSONL artifact flush is best effort.
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
cleanupTempDir(tempDir);
|
|
246
302
|
result.exitCode = exitCode;
|
|
303
|
+
if (result.detached) {
|
|
304
|
+
result.exitCode = 0;
|
|
305
|
+
result.finalOutput = "Detached for intercom coordination.";
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
247
308
|
|
|
248
309
|
if (exitCode === 0 && !result.error) {
|
|
249
310
|
const errInfo = detectSubagentError(result.messages);
|
package/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Toggle: async parameter (default: false, configurable via config.json)
|
|
10
10
|
*
|
|
11
11
|
* Config file: ~/.pi/agent/extensions/subagent/config.json
|
|
12
|
-
* { "asyncByDefault": true, "maxSubagentDepth": 1, "intercomBridge": "always", "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
|
|
12
|
+
* { "asyncByDefault": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import * as fs from "node:fs";
|
package/intercom-bridge.ts
CHANGED
|
@@ -2,27 +2,40 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentConfig } from "./agents.ts";
|
|
5
|
-
import type { ExtensionConfig } from "./types.ts";
|
|
5
|
+
import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "./types.ts";
|
|
6
6
|
|
|
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
|
+
const DEFAULT_SUBAGENT_CONFIG_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent");
|
|
10
|
+
const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
|
|
9
11
|
const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `Use intercom only for coordination with the orchestrator session:
|
|
13
|
+
- Need a decision or blocked: intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })
|
|
14
|
+
- Completion/update: intercom({ action: "send", to: "{orchestratorTarget}", message: "DONE: <summary>" })
|
|
15
|
+
If intercom is unavailable in this run, continue the task normally.`;
|
|
12
16
|
|
|
13
17
|
export interface IntercomBridgeState {
|
|
14
18
|
active: boolean;
|
|
15
19
|
mode: IntercomBridgeMode;
|
|
16
20
|
orchestratorTarget?: string;
|
|
17
21
|
extensionDir: string;
|
|
22
|
+
instruction: string;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
interface ResolveIntercomBridgeInput {
|
|
21
|
-
|
|
26
|
+
config: ExtensionConfig["intercomBridge"];
|
|
22
27
|
context: "fresh" | "fork" | undefined;
|
|
23
28
|
orchestratorTarget?: string;
|
|
24
29
|
extensionDir?: string;
|
|
25
30
|
configPath?: string;
|
|
31
|
+
settingsDir?: string;
|
|
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)}`;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
export function resolveIntercomBridgeMode(value: unknown): IntercomBridgeMode {
|
|
@@ -30,6 +43,19 @@ export function resolveIntercomBridgeMode(value: unknown): IntercomBridgeMode {
|
|
|
30
43
|
return "always";
|
|
31
44
|
}
|
|
32
45
|
|
|
46
|
+
function resolveIntercomBridgeConfig(value: ExtensionConfig["intercomBridge"]): Required<IntercomBridgeConfig> {
|
|
47
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
48
|
+
return {
|
|
49
|
+
mode: "always",
|
|
50
|
+
instructionFile: "",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
mode: resolveIntercomBridgeMode(value.mode),
|
|
55
|
+
instructionFile: typeof value.instructionFile === "string" ? value.instructionFile : "",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
33
59
|
function intercomEnabled(configPath: string): boolean {
|
|
34
60
|
if (!fs.existsSync(configPath)) return true;
|
|
35
61
|
try {
|
|
@@ -56,43 +82,71 @@ function extensionSandboxAllowsIntercom(extensions: string[] | undefined, extens
|
|
|
56
82
|
return false;
|
|
57
83
|
}
|
|
58
84
|
|
|
59
|
-
function
|
|
60
|
-
|
|
85
|
+
function expandTilde(filePath: string): string {
|
|
86
|
+
return filePath.startsWith("~/") ? path.join(os.homedir(), filePath.slice(2)) : filePath;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveInstructionTemplate(instructionFile: string, settingsDir: string): string {
|
|
90
|
+
if (!instructionFile) return DEFAULT_INTERCOM_BRIDGE_TEMPLATE;
|
|
91
|
+
const expandedPath = expandTilde(instructionFile);
|
|
92
|
+
const resolvedPath = path.isAbsolute(expandedPath)
|
|
93
|
+
? expandedPath
|
|
94
|
+
: path.resolve(settingsDir, expandedPath);
|
|
95
|
+
try {
|
|
96
|
+
return fs.readFileSync(resolvedPath, "utf-8");
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.warn(`Failed to read intercom bridge instructionFile at '${resolvedPath}'. Using default instructions.`, error);
|
|
99
|
+
return DEFAULT_INTERCOM_BRIDGE_TEMPLATE;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildIntercomBridgeInstruction(orchestratorTarget: string, template: string): string {
|
|
104
|
+
const instruction = template.replaceAll("{orchestratorTarget}", orchestratorTarget).trim();
|
|
105
|
+
if (instruction.startsWith(INTERCOM_BRIDGE_MARKER)) return instruction;
|
|
61
106
|
return `${INTERCOM_BRIDGE_MARKER}
|
|
62
|
-
|
|
63
|
-
- Need a decision or blocked: intercom({ action: "ask", to: ${escapedTarget}, message: "<question>" })
|
|
64
|
-
- Completion/update: intercom({ action: "send", to: ${escapedTarget}, message: "DONE: <summary>" })
|
|
65
|
-
If intercom is unavailable in this run, continue the task normally.`;
|
|
107
|
+
${instruction}`;
|
|
66
108
|
}
|
|
67
109
|
|
|
68
110
|
export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeState {
|
|
69
|
-
const
|
|
111
|
+
const config = resolveIntercomBridgeConfig(input.config);
|
|
112
|
+
const mode = config.mode;
|
|
70
113
|
const extensionDir = path.resolve(input.extensionDir ?? DEFAULT_INTERCOM_EXTENSION_DIR);
|
|
71
114
|
const orchestratorTarget = input.orchestratorTarget?.trim();
|
|
115
|
+
const settingsDir = path.resolve(input.settingsDir ?? DEFAULT_SUBAGENT_CONFIG_DIR);
|
|
116
|
+
const defaultInstruction = buildIntercomBridgeInstruction(
|
|
117
|
+
orchestratorTarget || "{orchestratorTarget}",
|
|
118
|
+
DEFAULT_INTERCOM_BRIDGE_TEMPLATE,
|
|
119
|
+
);
|
|
72
120
|
|
|
73
121
|
if (mode === "off") {
|
|
74
|
-
return { active: false, mode, extensionDir };
|
|
122
|
+
return { active: false, mode, extensionDir, instruction: defaultInstruction };
|
|
75
123
|
}
|
|
76
124
|
if (mode === "fork-only" && input.context !== "fork") {
|
|
77
|
-
return { active: false, mode, extensionDir };
|
|
125
|
+
return { active: false, mode, extensionDir, instruction: defaultInstruction };
|
|
78
126
|
}
|
|
79
127
|
if (!orchestratorTarget) {
|
|
80
|
-
return { active: false, mode, extensionDir };
|
|
128
|
+
return { active: false, mode, extensionDir, instruction: defaultInstruction };
|
|
81
129
|
}
|
|
82
130
|
if (!fs.existsSync(extensionDir)) {
|
|
83
|
-
return { active: false, mode, extensionDir };
|
|
131
|
+
return { active: false, mode, extensionDir, instruction: defaultInstruction };
|
|
84
132
|
}
|
|
85
133
|
|
|
86
134
|
const configPath = path.resolve(input.configPath ?? DEFAULT_INTERCOM_CONFIG_PATH);
|
|
87
135
|
if (!intercomEnabled(configPath)) {
|
|
88
|
-
return { active: false, mode, extensionDir };
|
|
136
|
+
return { active: false, mode, extensionDir, instruction: defaultInstruction };
|
|
89
137
|
}
|
|
90
138
|
|
|
139
|
+
const instruction = buildIntercomBridgeInstruction(
|
|
140
|
+
orchestratorTarget,
|
|
141
|
+
resolveInstructionTemplate(config.instructionFile, settingsDir),
|
|
142
|
+
);
|
|
143
|
+
|
|
91
144
|
return {
|
|
92
145
|
active: true,
|
|
93
146
|
mode,
|
|
94
147
|
orchestratorTarget,
|
|
95
148
|
extensionDir,
|
|
149
|
+
instruction,
|
|
96
150
|
};
|
|
97
151
|
}
|
|
98
152
|
|
|
@@ -103,7 +157,7 @@ export function applyIntercomBridgeToAgent(agent: AgentConfig, bridge: IntercomB
|
|
|
103
157
|
const tools = agent.tools && !agent.tools.includes("intercom")
|
|
104
158
|
? [...agent.tools, "intercom"]
|
|
105
159
|
: agent.tools;
|
|
106
|
-
const instruction =
|
|
160
|
+
const instruction = bridge.instruction;
|
|
107
161
|
const trimmedPrompt = agent.systemPrompt?.trim() || "";
|
|
108
162
|
const systemPrompt = trimmedPrompt.includes(INTERCOM_BRIDGE_MARKER)
|
|
109
163
|
? trimmedPrompt
|
package/package.json
CHANGED
package/render.ts
CHANGED
|
@@ -195,9 +195,11 @@ export function renderSubagentResult(
|
|
|
195
195
|
const isRunning = r.progress?.status === "running";
|
|
196
196
|
const icon = isRunning
|
|
197
197
|
? theme.fg("warning", "...")
|
|
198
|
-
: r.
|
|
199
|
-
? theme.fg("
|
|
200
|
-
:
|
|
198
|
+
: r.detached
|
|
199
|
+
? theme.fg("warning", "↗")
|
|
200
|
+
: r.exitCode === 0
|
|
201
|
+
? theme.fg("success", "ok")
|
|
202
|
+
: theme.fg("error", "X");
|
|
201
203
|
const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
|
|
202
204
|
const output = r.truncation?.text || getSingleResultOutput(r);
|
|
203
205
|
|
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 {
|
|
@@ -995,6 +995,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
995
995
|
const r = await runSync(ctx.cwd, agents, params.agent!, task, {
|
|
996
996
|
cwd: params.cwd,
|
|
997
997
|
signal,
|
|
998
|
+
allowIntercomDetach: agentConfig.systemPrompt?.includes("Intercom orchestration channel:") === true,
|
|
999
|
+
intercomEvents: deps.pi.events,
|
|
998
1000
|
runId,
|
|
999
1001
|
sessionDir: sessionDirForIndex(0),
|
|
1000
1002
|
sessionFile: sessionFileForIndex(0),
|
|
@@ -1024,6 +1026,19 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1024
1026
|
saveError: r.outputSaveError,
|
|
1025
1027
|
});
|
|
1026
1028
|
|
|
1029
|
+
if (r.detached) {
|
|
1030
|
+
return {
|
|
1031
|
+
content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}` }],
|
|
1032
|
+
details: {
|
|
1033
|
+
mode: "single",
|
|
1034
|
+
results: [r],
|
|
1035
|
+
progress: params.includeProgress ? allProgress : undefined,
|
|
1036
|
+
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1037
|
+
truncation: r.truncation,
|
|
1038
|
+
},
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1027
1042
|
if (r.exitCode !== 0)
|
|
1028
1043
|
return {
|
|
1029
1044
|
content: [{ type: "text", text: r.error || "Failed" }],
|
|
@@ -1102,12 +1117,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1102
1117
|
const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
1103
1118
|
deps.state.currentSessionId = parentSessionFile ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1104
1119
|
const discoveredAgents = deps.discoverAgents(ctx.cwd, scope).agents;
|
|
1105
|
-
const
|
|
1106
|
-
const orchestratorTarget = piWithSessionName.getSessionName?.();
|
|
1120
|
+
const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
|
|
1107
1121
|
const intercomBridge = resolveIntercomBridge({
|
|
1108
|
-
|
|
1122
|
+
config: deps.config.intercomBridge,
|
|
1109
1123
|
context: normalizedParams.context,
|
|
1110
|
-
orchestratorTarget,
|
|
1124
|
+
orchestratorTarget: sessionName,
|
|
1111
1125
|
});
|
|
1112
1126
|
const agents = intercomBridge.active
|
|
1113
1127
|
? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
|
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/types.ts
CHANGED
|
@@ -58,7 +58,7 @@ export interface ResolvedSkill {
|
|
|
58
58
|
export interface AgentProgress {
|
|
59
59
|
index: number;
|
|
60
60
|
agent: string;
|
|
61
|
-
status: "pending" | "running" | "completed" | "failed";
|
|
61
|
+
status: "pending" | "running" | "completed" | "failed" | "detached";
|
|
62
62
|
task: string;
|
|
63
63
|
skills?: string[];
|
|
64
64
|
currentTool?: string;
|
|
@@ -94,6 +94,8 @@ export interface SingleResult {
|
|
|
94
94
|
agent: string;
|
|
95
95
|
task: string;
|
|
96
96
|
exitCode: number;
|
|
97
|
+
detached?: boolean;
|
|
98
|
+
detachedReason?: string;
|
|
97
99
|
messages: Message[];
|
|
98
100
|
usage: Usage;
|
|
99
101
|
model?: string;
|
|
@@ -237,6 +239,14 @@ export interface ErrorInfo {
|
|
|
237
239
|
details?: string;
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
export interface IntercomEventBus {
|
|
243
|
+
on(channel: string, handler: (data: unknown) => void): () => void;
|
|
244
|
+
emit(channel: string, data: unknown): void;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const INTERCOM_DETACH_REQUEST_EVENT = "pi-intercom:detach-request";
|
|
248
|
+
export const INTERCOM_DETACH_RESPONSE_EVENT = "pi-intercom:detach-response";
|
|
249
|
+
|
|
240
250
|
// ============================================================================
|
|
241
251
|
// Execution Options
|
|
242
252
|
// ============================================================================
|
|
@@ -244,6 +254,8 @@ export interface ErrorInfo {
|
|
|
244
254
|
export interface RunSyncOptions {
|
|
245
255
|
cwd?: string;
|
|
246
256
|
signal?: AbortSignal;
|
|
257
|
+
allowIntercomDetach?: boolean;
|
|
258
|
+
intercomEvents?: IntercomEventBus;
|
|
247
259
|
onUpdate?: (r: import("@mariozechner/pi-agent-core").AgentToolResult<Details>) => void;
|
|
248
260
|
maxOutput?: MaxOutputConfig;
|
|
249
261
|
artifactsDir?: string;
|
|
@@ -263,13 +275,20 @@ export interface RunSyncOptions {
|
|
|
263
275
|
skills?: string[];
|
|
264
276
|
}
|
|
265
277
|
|
|
278
|
+
export type IntercomBridgeMode = "off" | "fork-only" | "always";
|
|
279
|
+
|
|
280
|
+
export interface IntercomBridgeConfig {
|
|
281
|
+
mode?: IntercomBridgeMode;
|
|
282
|
+
instructionFile?: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
266
285
|
export interface ExtensionConfig {
|
|
267
286
|
asyncByDefault?: boolean;
|
|
268
287
|
defaultSessionDir?: string;
|
|
269
288
|
maxSubagentDepth?: number;
|
|
270
289
|
worktreeSetupHook?: string;
|
|
271
290
|
worktreeSetupHookTimeoutMs?: number;
|
|
272
|
-
intercomBridge?:
|
|
291
|
+
intercomBridge?: IntercomBridgeConfig;
|
|
273
292
|
}
|
|
274
293
|
|
|
275
294
|
// ============================================================================
|
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 {
|