@zhushanwen/pi-subagent-workflow 0.3.1 → 0.3.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-subagent-workflow",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
|
|
@@ -828,4 +828,84 @@ describe("runSpawn", () => {
|
|
|
828
828
|
|
|
829
829
|
// 注:C1(orphan 进程兜底)与 M8(stdout 边界)describe 块已移至 run-spawn-edges.test.ts,
|
|
830
830
|
// 拆分以保持本文件 < 1000 行(pre-commit hook 限制)。两文件各自独立声明文件级 mock。
|
|
831
|
+
|
|
832
|
+
// ============================================================
|
|
833
|
+
// E2E:runSpawn 从主进程 process.argv 镜像 extension/approve flag 到子进程
|
|
834
|
+
// 验证完整链路:runSpawn 真实执行 → 读 process.argv → buildSpawnArgs 拼参 →
|
|
835
|
+
// spawn 收到的 args 含镜像 flag。spawn 被 mock(FakeChild),但 runSpawn 体内逻辑
|
|
836
|
+
// 全真实跑,process.argv 用真实进程变量(测试中临时覆写 + 还原)。
|
|
837
|
+
// ============================================================
|
|
838
|
+
describe("E2E: 镜像主进程 argv flag 到子进程 spawn args", () => {
|
|
839
|
+
const originalArgv = process.argv;
|
|
840
|
+
|
|
841
|
+
afterEach(() => {
|
|
842
|
+
process.argv = originalArgv;
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it("主进程 argv 含 --extension/--no-extensions/--approve → 子进程 spawn args 全部镜像", async () => {
|
|
846
|
+
// 模拟主 pi 进程启动参数(xyz-agent runtime 启动 pi 的真实形态)
|
|
847
|
+
process.argv = [
|
|
848
|
+
"bun", "/path/to/pi",
|
|
849
|
+
"--mode", "rpc",
|
|
850
|
+
"--no-extensions",
|
|
851
|
+
"--approve",
|
|
852
|
+
"--extension", "/exts/goal",
|
|
853
|
+
"--extension", "/exts/todo",
|
|
854
|
+
"--session-dir", "/sessions",
|
|
855
|
+
];
|
|
856
|
+
|
|
857
|
+
const record = makeRecord();
|
|
858
|
+
const promise = runSpawn(record, "Task", makeOpts(), makeCtx());
|
|
859
|
+
await waitForSpawn();
|
|
860
|
+
const child = lastSpawnedChild();
|
|
861
|
+
mockSessionFileExists(
|
|
862
|
+
"/tmp/test/agents/subagents/--tmp-test--/sessions/2026-07-03T12-00-00-000Z_sess-mirror.jsonl",
|
|
863
|
+
);
|
|
864
|
+
emitStdoutLine(child, sessionHeader("sess-mirror"));
|
|
865
|
+
child.stdout.end();
|
|
866
|
+
child.stderr.end();
|
|
867
|
+
child.emit("close", 0);
|
|
868
|
+
await promise;
|
|
869
|
+
|
|
870
|
+
// 断言 spawn 收到的调用参数含全部镜像 flag
|
|
871
|
+
const spawnCall = mockSpawn.mock.calls[0];
|
|
872
|
+
// spawn(command, args, options) → args 是第二个参数
|
|
873
|
+
const spawnArgs = spawnCall[1] as string[];
|
|
874
|
+
|
|
875
|
+
expect(spawnArgs).toContain("--no-extensions");
|
|
876
|
+
expect(spawnArgs).toContain("--approve");
|
|
877
|
+
// 两个 extension 路径都镜像,顺序保留
|
|
878
|
+
const extIdxs = spawnArgs
|
|
879
|
+
.map((a, i) => (a === "--extension" ? i : -1))
|
|
880
|
+
.filter((i) => i >= 0);
|
|
881
|
+
expect(extIdxs).toHaveLength(2);
|
|
882
|
+
expect(spawnArgs[extIdxs[0] + 1]).toBe("/exts/goal");
|
|
883
|
+
expect(spawnArgs[extIdxs[1] + 1]).toBe("/exts/todo");
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it("主进程 argv 无目标 flag → 子进程 spawn args 不含镜像 flag(向后兼容)", async () => {
|
|
887
|
+
// 模拟纯 pi CLI 直接跑(无 extension/approve 配置)
|
|
888
|
+
process.argv = ["bun", "/path/to/pi", "--mode", "rpc"];
|
|
889
|
+
|
|
890
|
+
const record = makeRecord();
|
|
891
|
+
const promise = runSpawn(record, "Task", makeOpts(), makeCtx());
|
|
892
|
+
await waitForSpawn();
|
|
893
|
+
const child = lastSpawnedChild();
|
|
894
|
+
mockSessionFileExists(
|
|
895
|
+
"/tmp/test/agents/subagents/--tmp-test--/sessions/2026-07-03T12-00-00-000Zsess-noflag.jsonl",
|
|
896
|
+
);
|
|
897
|
+
emitStdoutLine(child, sessionHeader("sess-noflag"));
|
|
898
|
+
child.stdout.end();
|
|
899
|
+
child.stderr.end();
|
|
900
|
+
child.emit("close", 0);
|
|
901
|
+
await promise;
|
|
902
|
+
|
|
903
|
+
const spawnCall = mockSpawn.mock.calls[0];
|
|
904
|
+
const spawnArgs = spawnCall[1] as string[];
|
|
905
|
+
|
|
906
|
+
expect(spawnArgs).not.toContain("--no-extensions");
|
|
907
|
+
expect(spawnArgs).not.toContain("--approve");
|
|
908
|
+
expect(spawnArgs).not.toContain("--extension");
|
|
909
|
+
});
|
|
910
|
+
});
|
|
831
911
|
});
|
|
@@ -7,6 +7,7 @@ import * as path from "node:path";
|
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
8
|
|
|
9
9
|
import { MAX_FORK_DEPTH } from "../session-context-resolver.ts";
|
|
10
|
+
import { mirrorMainProcessFlags } from "../argv-mirror.ts";
|
|
10
11
|
import { buildEnvBlock, buildSpawnArgs } from "../session-runner.ts";
|
|
11
12
|
|
|
12
13
|
describe("buildSpawnArgs", () => {
|
|
@@ -131,6 +132,105 @@ describe("buildSpawnArgs", () => {
|
|
|
131
132
|
);
|
|
132
133
|
expect(args).not.toContain("--tools");
|
|
133
134
|
});
|
|
135
|
+
|
|
136
|
+
// ============================================================
|
|
137
|
+
// mirrorFlags 透传:子进程镜像主进程 extension/approve flag
|
|
138
|
+
// ============================================================
|
|
139
|
+
|
|
140
|
+
it("mirrorFlags 透传:noExtensions+approve+extensionPaths 全量 push(TC5)", () => {
|
|
141
|
+
const args = buildSpawnArgs({
|
|
142
|
+
...baseParams,
|
|
143
|
+
mirrorFlags: { noExtensions: true, approve: true, extensionPaths: ["/e1", "/e2"] },
|
|
144
|
+
});
|
|
145
|
+
expect(args).toContain("--no-extensions");
|
|
146
|
+
expect(args).toContain("--approve");
|
|
147
|
+
// 每个 extension 独立 token,顺序保留
|
|
148
|
+
const extIdxs = args.map((a, i) => (a === "--extension" ? i : -1)).filter((i) => i >= 0);
|
|
149
|
+
expect(extIdxs).toHaveLength(2);
|
|
150
|
+
expect(args[extIdxs[0] + 1]).toBe("/e1");
|
|
151
|
+
expect(args[extIdxs[1] + 1]).toBe("/e2");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("mirrorFlags 全 false/空 → 不追加任何目标 flag(TC6)", () => {
|
|
155
|
+
const args = buildSpawnArgs({
|
|
156
|
+
...baseParams,
|
|
157
|
+
mirrorFlags: { noExtensions: false, approve: false, extensionPaths: [] },
|
|
158
|
+
});
|
|
159
|
+
expect(args).not.toContain("--no-extensions");
|
|
160
|
+
expect(args).not.toContain("--approve");
|
|
161
|
+
expect(args).not.toContain("--extension");
|
|
162
|
+
// 仅基础参数
|
|
163
|
+
expect(args).toEqual(["--mode", "rpc", "--session-dir", "/sessions/dir"]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("mirrorFlags undefined → 行为等同旧版(TC7)", () => {
|
|
167
|
+
const args = buildSpawnArgs(baseParams);
|
|
168
|
+
expect(args).toEqual(["--mode", "rpc", "--session-dir", "/sessions/dir"]);
|
|
169
|
+
expect(args).not.toContain("--extension");
|
|
170
|
+
expect(args).not.toContain("--no-extensions");
|
|
171
|
+
expect(args).not.toContain("--approve");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ============================================================
|
|
176
|
+
// mirrorMainProcessFlags:从主进程 argv 解析可镜像的 flag
|
|
177
|
+
// ============================================================
|
|
178
|
+
|
|
179
|
+
describe("mirrorMainProcessFlags", () => {
|
|
180
|
+
it("--extension 多次出现(空格分隔)+ 布尔 flag(TC1)", () => {
|
|
181
|
+
const r = mirrorMainProcessFlags([
|
|
182
|
+
"bun", "/pi", "--mode", "rpc", "--no-extensions", "--approve",
|
|
183
|
+
"--extension", "/a", "--extension", "/b",
|
|
184
|
+
]);
|
|
185
|
+
expect(r).toEqual({ noExtensions: true, approve: true, extensionPaths: ["/a", "/b"] });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("--extension=path 等号形式 + 短形式 -e/-ne/-a(TC2)", () => {
|
|
189
|
+
const r = mirrorMainProcessFlags([
|
|
190
|
+
"bun", "/pi", "--extension=/x", "-ne", "-a",
|
|
191
|
+
]);
|
|
192
|
+
expect(r).toEqual({ noExtensions: true, approve: true, extensionPaths: ["/x"] });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("混合形式(空格 + 等号),顺序保留(TC3)", () => {
|
|
196
|
+
const r = mirrorMainProcessFlags([
|
|
197
|
+
"bun", "/pi", "--extension", "/a", "--extension=/b", "--extension", "/c",
|
|
198
|
+
]);
|
|
199
|
+
expect(r.extensionPaths).toEqual(["/a", "/b", "/c"]);
|
|
200
|
+
expect(r.noExtensions).toBe(false);
|
|
201
|
+
expect(r.approve).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("无目标 flag → 全空/全 false(向后兼容,TC4)", () => {
|
|
205
|
+
const r = mirrorMainProcessFlags(["bun", "/pi", "--mode", "rpc"]);
|
|
206
|
+
expect(r).toEqual({ noExtensions: false, approve: false, extensionPaths: [] });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("不误吃其他 flag 值与 positional 参数(TC8)", () => {
|
|
210
|
+
const r = mirrorMainProcessFlags([
|
|
211
|
+
"bun", "/pi", "--no-extensions", "--skill", "/sk", "some prompt text",
|
|
212
|
+
]);
|
|
213
|
+
expect(r.noExtensions).toBe(true);
|
|
214
|
+
expect(r.extensionPaths).toEqual([]);
|
|
215
|
+
// --skill 的 /sk 不混入 extensionPaths;positional prompt 被忽略
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("空 argv / 仅前导两项 → 全空", () => {
|
|
219
|
+
expect(mirrorMainProcessFlags([])).toEqual({ noExtensions: false, approve: false, extensionPaths: [] });
|
|
220
|
+
expect(mirrorMainProcessFlags(["bun", "/pi"])).toEqual({
|
|
221
|
+
noExtensions: false, approve: false, extensionPaths: [],
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("--extension 末尾无值 → 跳过(不越界、不误吃下一个 token)", () => {
|
|
226
|
+
const r = mirrorMainProcessFlags(["bun", "/pi", "--extension"]);
|
|
227
|
+
expect(r.extensionPaths).toEqual([]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("-e 短形式多次 + 等号混用", () => {
|
|
231
|
+
const r = mirrorMainProcessFlags(["bun", "/pi", "-e", "/a", "-e=/b", "-e", "/c"]);
|
|
232
|
+
expect(r.extensionPaths).toEqual(["/a", "/b", "/c"]);
|
|
233
|
+
});
|
|
134
234
|
});
|
|
135
235
|
|
|
136
236
|
// ============================================================
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从主进程 argv 解析可镜像给子进程的 flag。
|
|
3
|
+
*
|
|
4
|
+
* 独立模块:argv 解析是独立职责,从 session-runner.ts 拆出避免该文件超行数上限。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 镜像 flag 集合:从主进程 argv 解析出的可透传给子进程的 flag。
|
|
9
|
+
* 面向 buildSpawnArgs 的入参形态(解析后)。undefined 字段语义与解析为空一致。
|
|
10
|
+
*/
|
|
11
|
+
export interface MirrorFlags {
|
|
12
|
+
noExtensions: boolean;
|
|
13
|
+
approve: boolean;
|
|
14
|
+
extensionPaths: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 有值 flag 的解析规则表:成员是「后跟一个值」的 flag 名(含长短形式)。
|
|
19
|
+
* 用于 mirrorMainProcessFlags 跳过其他 flag 的值时不误吃。
|
|
20
|
+
*/
|
|
21
|
+
const VALUED_FLAGS = new Set<string>([
|
|
22
|
+
"--extension", "-e",
|
|
23
|
+
"--skill",
|
|
24
|
+
"--model", "--system-prompt", "--append-system-prompt",
|
|
25
|
+
"--tools", "-t", "--exclude-tools", "-xt",
|
|
26
|
+
"--fork", "--session-dir", "--mode",
|
|
27
|
+
"--thinking", "--models",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/** argv 中 flag 的起始索引:argv[0]=runtime, argv[1]=binary 路径。 */
|
|
31
|
+
const ARGV_FLAG_START = 2;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 从主进程 argv 解析可镜像的 flag(--no-extensions/--approve/--extension)。
|
|
35
|
+
*
|
|
36
|
+
* 数据源是主 pi 进程的 process.argv:子进程 spawn 的父就是主进程,
|
|
37
|
+
* 主进程 argv 完整保留启动时收到的全部 flag(已运行时验证)。这让子进程
|
|
38
|
+
* extension/approve 加载行为与主进程一致,且对任意 pi 宿主通用(不止 xyz-agent)。
|
|
39
|
+
*
|
|
40
|
+
* 解析规则:
|
|
41
|
+
* - --no-extensions / -ne、--approve / -a:布尔 flag
|
|
42
|
+
* - --extension / -e:支持 `--extension <path>`(空格)与 `--extension=<path>`(等号)
|
|
43
|
+
* - 其他 flag(在 VALUED_FLAGS 中)的值不被误当 extension 路径
|
|
44
|
+
* - positional 参数被忽略
|
|
45
|
+
* - argv[0]/argv[1] 是 bun/pi binary 路径,从 argv[2:] 开始扫
|
|
46
|
+
*/
|
|
47
|
+
export function mirrorMainProcessFlags(argv: readonly string[]): MirrorFlags {
|
|
48
|
+
let hasNoExtensions = false;
|
|
49
|
+
let hasApprove = false;
|
|
50
|
+
const extensionPaths: string[] = [];
|
|
51
|
+
|
|
52
|
+
// argv[0]=runtime(bun), argv[1]=pi binary 路径;flag 从 argv[2] 起
|
|
53
|
+
const flagArgs = argv.length > ARGV_FLAG_START ? argv.slice(ARGV_FLAG_START) : [];
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < flagArgs.length; i++) {
|
|
56
|
+
const tok = flagArgs[i];
|
|
57
|
+
if (tok === "--no-extensions" || tok === "-ne") {
|
|
58
|
+
hasNoExtensions = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (tok === "--approve" || tok === "-a") {
|
|
62
|
+
hasApprove = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
// 等号形式 --extension=path / -e=path
|
|
66
|
+
const eqMatch = /^(--extension|-e)=(.*)$/.exec(tok);
|
|
67
|
+
if (eqMatch) {
|
|
68
|
+
const val = eqMatch[2];
|
|
69
|
+
if (val) extensionPaths.push(val);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// 空格形式 --extension <path> / -e <path>:值在下一个 token
|
|
73
|
+
if (tok === "--extension" || tok === "-e") {
|
|
74
|
+
const next = flagArgs[i + 1];
|
|
75
|
+
if (next !== undefined && !next.startsWith("-") && next.length > 0) {
|
|
76
|
+
extensionPaths.push(next);
|
|
77
|
+
i++; // 跳过值
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// 其他有值 flag:跳过其值,避免误吃(如 --skill /a 的 /a)
|
|
82
|
+
if (VALUED_FLAGS.has(tok)) {
|
|
83
|
+
i++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// 其他情况(未知 flag、--flag=val 形式、positional)忽略
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { noExtensions: hasNoExtensions, approve: hasApprove, extensionPaths };
|
|
90
|
+
}
|
|
@@ -11,6 +11,7 @@ import * as fs from "node:fs";
|
|
|
11
11
|
|
|
12
12
|
import type { ExtensionMode } from "@mariozechner/pi-coding-agent";
|
|
13
13
|
|
|
14
|
+
import { type MirrorFlags, mirrorMainProcessFlags } from "./argv-mirror.ts";
|
|
14
15
|
import { writeAliveMarker } from "./alive-store.ts";
|
|
15
16
|
import { type DialogGlobalQueue, type UiRequestHandler } from "./dialog-queue.ts";
|
|
16
17
|
import { updateFromEvent } from "./execution-record.ts";
|
|
@@ -422,6 +423,11 @@ export function buildSpawnArgs(
|
|
|
422
423
|
sessionDir: string;
|
|
423
424
|
forkSource: string | undefined;
|
|
424
425
|
skillPaths: string[] | undefined;
|
|
426
|
+
/**
|
|
427
|
+
* 镜像自主进程 argv 的 flag(--no-extensions/--approve/--extension)。
|
|
428
|
+
* undefined 或全空/全 false 时行为不变(向后兼容)。
|
|
429
|
+
*/
|
|
430
|
+
mirrorFlags?: MirrorFlags;
|
|
425
431
|
},
|
|
426
432
|
): string[] {
|
|
427
433
|
// task 不通过命令行传——pi 的 runRpcMode 只消费 stdin RpcCommand,
|
|
@@ -451,6 +457,16 @@ export function buildSpawnArgs(
|
|
|
451
457
|
args.push("--skill", sp);
|
|
452
458
|
}
|
|
453
459
|
}
|
|
460
|
+
// 镜像主进程的 extension/approve flag:让子进程 extension 加载行为与主进程一致。
|
|
461
|
+
// undefined/空值时不追加(向后兼容)。顺序紧跟 skill 之后,注入类 flag 集中。
|
|
462
|
+
const mf = params.mirrorFlags;
|
|
463
|
+
if (mf) {
|
|
464
|
+
if (mf.noExtensions) args.push("--no-extensions");
|
|
465
|
+
if (mf.approve) args.push("--approve");
|
|
466
|
+
for (const ep of mf.extensionPaths) {
|
|
467
|
+
args.push("--extension", ep);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
454
470
|
return args;
|
|
455
471
|
}
|
|
456
472
|
|
|
@@ -625,6 +641,8 @@ export async function runSpawn(
|
|
|
625
641
|
sessionDir,
|
|
626
642
|
forkSource,
|
|
627
643
|
skillPaths: skillPaths.length > 0 ? skillPaths : undefined,
|
|
644
|
+
// 镜像主进程 argv 的 extension/approve flag,让子进程加载行为对齐主进程
|
|
645
|
+
mirrorFlags: mirrorMainProcessFlags(process.argv),
|
|
628
646
|
},
|
|
629
647
|
);
|
|
630
648
|
const invocation = getPiInvocation(spawnArgs);
|