@zhushanwen/pi-subagent-workflow 7.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-subagent-workflow",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
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.",
@@ -41,7 +41,7 @@
41
41
  ]
42
42
  },
43
43
  "dependencies": {
44
- "@xyz-agent/extension-protocol": "^0.3.1",
44
+ "@xyz-agent/extension-protocol": "^0.4.0",
45
45
  "ajv": "^8.20.0",
46
46
  "yaml": "^2.9.0",
47
47
  "@zhushanwen/pi-extension-logger": "0.2.0"
@@ -52,7 +52,7 @@
52
52
  "@earendil-works/pi-tui": "*",
53
53
  "typebox": "*",
54
54
  "@zhushanwen/pi-pending-notifications": "0.3.1",
55
- "@zhushanwen/pi-structured-output": "5.0.0"
55
+ "@zhushanwen/pi-structured-output": "5.0.1"
56
56
  },
57
57
  "peerDependenciesMeta": {
58
58
  "@earendil-works/pi-coding-agent": {
@@ -1,10 +1,9 @@
1
1
  // src/__tests__/execute-and-await-worktree.test.ts
2
2
  //
3
- // executeAndAwait 的 worktree 前置守卫 + 失败收尾测试(W1 code review 修复回归锁)。
3
+ // executeAndAwait 的 worktree 失败收尾测试(W1 code review 修复回归锁)。
4
4
  //
5
- // 覆盖两点:
6
- // 1. [MF#7] worktree:true && !fork 在任何副作用之前 fail-fast 抛错
7
- // 2. worktreeManager.create 抛错时 record 被 finalizeFailed(status→failed)且原错外抛
5
+ // 覆盖:worktreeManager.create 抛错时 record 被 finalizeFailed(status→failed)且原错外抛。
6
+ // (worktree 与 fork 解耦后,worktree:true+fork:false 的解耦验证见 subagent-service.test.ts。)
8
7
  //
9
8
  // ── mock 策略 ──
10
9
  //
@@ -153,33 +152,11 @@ function getStore(service: SubagentService): RecordStore {
153
152
  return Reflect.get(service, "store") as RecordStore;
154
153
  }
155
154
 
156
- describe("executeAndAwait worktree 前置守卫 + 失败收尾", () => {
155
+ describe("executeAndAwait worktree 失败收尾", () => {
157
156
  afterEach(() => {
158
157
  vi.restoreAllMocks();
159
158
  });
160
159
 
161
- // ============================================================
162
- // [MF#7] worktree:true && !fork → fail-fast 抛错(任何副作用之前)
163
- // ============================================================
164
- it("[MF#7] worktree:true 且 fork 未设时抛 'worktree:true requires fork:true'", async () => {
165
- const { service } = setup();
166
-
167
- // guard 在 BC-12 深度检查之后、步骤 1 之前——无需 fork:true,worktree:true 即触发。
168
- // 传入完整 ExecuteOptions(补全 slug 必填字段),不再用 `as` 掩盖缺失字段——让缺字段在类型层可见。
169
- await expect(
170
- service.executeAndAwait({
171
- task: "needs worktree without fork",
172
- slug: "mf7-worktree-without-fork",
173
- worktree: true,
174
- fork: undefined,
175
- ctxModel,
176
- }),
177
- ).rejects.toThrow(/worktree:true requires fork:true/);
178
-
179
- // 无副作用:guard 在 createRecordForMode 之前 → store 无 running record。
180
- expect(getStore(service).listRunning()).toHaveLength(0);
181
- });
182
-
183
160
  // ============================================================
184
161
  // worktreeManager.create 抛错 → record 收尾为 failed + 原错外抛
185
162
  // ============================================================
@@ -385,24 +385,22 @@ describe("SubagentService", () => {
385
385
  });
386
386
 
387
387
  // ============================================================
388
- // execute() worktree fail-fast 校验 [MF#7](commit 8e8e75966)
388
+ // execute() worktree 路径(worktree fork 解耦后)
389
389
  // ============================================================
390
390
  //
391
- // [MF#7] execute 入口校验 `worktree:true && !fork` → fail-fast 抛错。
392
- // 否则下面三个 worktree 分支都不命中,worktreeHandle 恒 undefined → 子 agent
393
- // 零文件隔离且零报错(静默 no-op)。此组验证该校验的三种 fork/worktree 组合:
394
- // 1. worktree:true + fork:false → "requires fork"(fail-fast 命中)
395
- // 2. worktree:true + fork:true → 不命中校验(执行越过 guard,后续因副作用失败)
396
- // 3. worktree:false + fork:false → 不命中校验(默认路径,执行越过 guard
391
+ // worktree(文件隔离)与 fork(上下文继承)已解耦:worktree:true 可独立于 fork 工作
392
+ // (worktreeManager.create 只看 opts.worktree,不读 fork)。此组验证三种 fork/worktree
393
+ // 组合下 worktree 路径的行为(均不应抛 'requires fork'——该 guard 已移除):
394
+ // 1. worktree:true + fork:false → 解耦后正常(创建 worktree 路径,不抛 requires fork
395
+ // 2. worktree:true + fork:true → 创建 worktree 路径(测试环境 git 失败,抛非 requires fork 错)
396
+ // 3. worktree:false + fork:false → 默认路径(不创建 worktree
397
397
  //
398
- // 被测点是 execute() 入口的 guard(subagent-service.ts L277-282),在任何副作用
399
- // (record 创建 / worktree 创建 / spawn)之前。本文件不 mock spawn(保持与文件头
400
- // 声明一致——execute 集成测试在 execute-nesting / run-spawn-integration),
401
- // 因此 case 2/3 验证「guard 放行」而非「执行完成」:执行越过 guard 后在后续步骤
402
- // (worktreeManager.create 调 git / runSpawn 调 spawn)抛与 fork/worktree 无关的错。
403
- // try/catch 断言抛出的不是 guard 错误,精确锁住 guard 的触发条件。
404
-
405
- describe("execute() worktree fail-fast 校验 [MF#7]", () => {
398
+ // 本文件不 mock spawn(保持与文件头声明一致——execute 集成测试在 execute-nesting /
399
+ // run-spawn-integration),因此 case 验证「不抛 requires fork」而非「执行完成」:
400
+ // 执行越过 worktree 创建后在后续步骤(worktreeManager.create 调 git / runSpawn 调
401
+ // spawn)抛与 fork/worktree 无关的错。用 try/catch 断言抛出的不是 requires fork。
402
+
403
+ describe("execute() worktree 路径(worktree fork 解耦)", () => {
406
404
  /** 构造已就绪的 service(initSession + initModel 注入 ctxModel,使 resolveIdentity 不因 model 拗错)。 */
407
405
  function makeReadyService(): SubagentService {
408
406
  const service = new SubagentService({ cwd: agentDir, modelService });
@@ -422,24 +420,25 @@ describe("SubagentService", () => {
422
420
  return service;
423
421
  }
424
422
 
425
- it("worktree:true + fork:false → fail-fast 抛错含 'requires fork'(guard 命中)", async () => {
423
+ it("worktree:true + fork:false → 解耦后不抛 'requires fork'(worktree 独立于 fork)", async () => {
426
424
  const service = makeReadyService();
427
- // guard 在所有副作用之前:无 record 创建、无 spawn
428
- await expect(
429
- service.execute({
430
- task: "worktree without fork",
425
+ // 解耦后 worktree:true+fork:false 不再 throw requires fork(worktreeManager.create 只看 worktree)
426
+ try {
427
+ await service.execute({
428
+ task: "worktree without fork (decoupled)",
431
429
  worktree: true,
432
430
  fork: false,
433
431
  ctxModel: { id: "ctx-model", name: "Ctx", provider: "p", reasoning: false },
434
- }),
435
- ).rejects.toThrow(/requires fork/);
436
- // 无副作用:record 未创建(guard createRecordForMode 之前)
437
- expect(service.collectRecords(10)).toHaveLength(0);
432
+ });
433
+ } catch (err) {
434
+ // 解耦后绝不抛 requires fork(执行继续到 worktreeManager.create/spawn 才可能抛其他错)
435
+ expect((err as Error).message).not.toMatch(/requires fork/);
436
+ }
438
437
  });
439
438
 
440
- it("worktree:true + fork:true → 不命中 guard(执行越过 guard,不抛 'requires fork')", async () => {
439
+ it("worktree:true + fork:true → 创建 worktree 路径(不抛 'requires fork')", async () => {
441
440
  const service = makeReadyService();
442
- // guard 放行 → 执行继续:先创建 record,然后 worktreeManager.create 调 git(测试环境无 repo → 抛与 fork 无关的错)
441
+ // 执行继续:先创建 record,然后 worktreeManager.create 调 git(测试环境无 repo → 抛与 fork 无关的错)
443
442
  try {
444
443
  await service.execute({
445
444
  task: "worktree with fork",
@@ -454,9 +453,9 @@ describe("SubagentService", () => {
454
453
  }
455
454
  });
456
455
 
457
- it("worktree:false + fork:false → 不命中 guard(默认路径越过 guard,不抛 'requires fork')", async () => {
456
+ it("worktree:false + fork:false → 默认路径(不创建 worktree,不抛 'requires fork')", async () => {
458
457
  const service = makeReadyService();
459
- // guard 放行 → 执行继续:runSpawn 调 child_process.spawn(测试环境无真实 pi → 抛与 fork 无关的错)
458
+ // 默认路径:runSpawn 调 child_process.spawn(测试环境无真实 pi → 抛与 fork 无关的错)
460
459
  try {
461
460
  await service.execute({
462
461
  task: "default path",
@@ -19,7 +19,7 @@ export interface BgNotifyRecord {
19
19
  error?: string;
20
20
  startedAt: number;
21
21
  endedAt: number | undefined;
22
- /** [MF#1] fork+worktree 模式下子 agent 改动的 patch 路径(worktree 外,cleanup 后留存)。
22
+ /** [MF#1] worktree 模式下子 agent 改动的 patch 路径(worktree 外,cleanup 后留存)。
23
23
  * done 时通知文本显式提示 `git apply`,否则 background 子 agent 在隔离 worktree 的改动
24
24
  * 会静默丢失——父 LLM 不知 patch 路径,无法应用。 */
25
25
  patchFile?: string;
@@ -421,16 +421,6 @@ export class SubagentService {
421
421
  );
422
422
  }
423
423
 
424
- // [MF#7] worktree:true 需要 fork:true——否则下面三个 worktree 分支都不命中,
425
- // worktreeHandle 恒 undefined → 子 agent 零文件隔离且零报错(静默 no-op)。此处在
426
- // 任何副作用(record 创建 / worktree 创建)之前 fail-fast,不吞误用。
427
- if (opts.worktree === true && !opts.fork) {
428
- throw new Error(
429
- "worktree:true requires fork:true (worktree isolation only applies to forked sessions). " +
430
- "Set fork:true together with worktree:true.",
431
- );
432
- }
433
-
434
424
  // mode 固定 background(sync 模式已删除)
435
425
  const mode: ExecutionMode = "background";
436
426
  const ctx = this.buildSessionRunnerContext(opts.cwd);
@@ -451,7 +441,7 @@ export class SubagentService {
451
441
  // 传入的是已创建的 WorktreeHandle
452
442
  worktreeHandle = opts.worktree;
453
443
  } else if (opts.worktree === true) {
454
- // worktree===true(显式要求)——创建新 worktree。MF#7 已保证此处 fork 必为 true。
444
+ // worktree===true(显式要求)——创建新 worktree。与 fork 正交(worktree 文件隔离不依赖 fork 上下文继承)。
455
445
  try {
456
446
  worktreeHandle = this.worktreeManager.create(this.cwd, record.id);
457
447
  record.worktreeHandle = worktreeHandle;
@@ -524,15 +514,6 @@ export class SubagentService {
524
514
  );
525
515
  }
526
516
 
527
- // [MF#7] worktree:true requires fork:true — symmetric with execute() guard.
528
- // Fails fast before any side effect (record creation / worktree creation).
529
- if (opts.worktree === true && !opts.fork) {
530
- throw new Error(
531
- "worktree:true requires fork:true (worktree isolation only applies to forked sessions). " +
532
- "Set fork:true together with worktree:true.",
533
- );
534
- }
535
-
536
517
  // ── 步骤 1: IDENTITY 解析 ──
537
518
  const identity = await this.resolveIdentity(opts);
538
519
 
@@ -543,8 +524,8 @@ export class SubagentService {
543
524
  // ── 步骤 2.5: worktree creation (only worktree===true; handle injection is execute()'s path) ──
544
525
  // Workflow path receives boolean only (AgentCallOpts.worktree: boolean) — WorktreeHandle is a
545
526
  // main-thread non-serializable object that cannot cross worker postMessage, so no object branch
546
- // here (unlike execute() :445-447 which serves the subagent-tool path). MF#7 guard above ensures
547
- // fork===true when worktree===true. On create failure, finalizeFailed cleans up the record, then
527
+ // here (unlike execute() :445-447 which serves the subagent-tool path).
528
+ // On create failure, finalizeFailed cleans up the record, then
548
529
  // throw lets SAR.run() convert it to an AgentResult.error (not return-handle like execute()).
549
530
  let worktreeHandle: WorktreeHandle | undefined;
550
531
  if (opts.worktree === true) {
@@ -231,7 +231,7 @@ export interface AgentResult {
231
231
  */
232
232
  /**
233
233
  * worktree handle 值对象。仅 worktree:true 时持有——worktree 是独立维度,
234
- * 需显式开启(且要求 fork:true),fork alone 不创建 worktree。
234
+ * 需显式开启,fork alone 不创建 worktree。
235
235
  * Object.freeze 守卫保证不可变。
236
236
  */
237
237
  export interface WorktreeHandle {
@@ -339,7 +339,7 @@ export interface ExecutionRecord {
339
339
  /** session jsonl 文件名。session 创建成功后由 session-runner.run() 回填(窗口期内 undefined)。 */
340
340
  sessionFile?: string;
341
341
 
342
- /** [MF#3] fork+worktree 模式下子 agent 改动的 patch 文件路径(worktree 外,供调用方应用)。 */
342
+ /** [MF#3] worktree 模式下子 agent 改动的 patch 文件路径(worktree 外,供调用方应用)。 */
343
343
  patchFile?: string;
344
344
 
345
345
  /** worktree 隔离时的 handle(仅 worktree:true 时存在;fork alone 无此字段)。 */
@@ -383,7 +383,7 @@ export interface SubagentToolDetails {
383
383
  parsedOutput?: unknown;
384
384
  /** session jsonl 文件名(不含目录)。窗口期内可能 undefined(session 尚未创建成功)。 */
385
385
  sessionFile?: string;
386
- /** [MF#3] fork+worktree 模式下子 agent 改动的 patch 文件路径(worktree 外,供调用方应用)。 */
386
+ /** [MF#3] worktree 模式下子 agent 改动的 patch 文件路径(worktree 外,供调用方应用)。 */
387
387
  patchFile?: string;
388
388
  }
389
389
 
@@ -419,7 +419,7 @@ export interface ExecuteOptions {
419
419
  onComplete?: (record: RecordSnapshot) => void;
420
420
  /** 是否继承父会话上下文(fork 模式,只继承上下文)。 */
421
421
  fork?: boolean;
422
- /** 文件系统隔离:true=创建新 git worktree(要求 fork:true),WorktreeHandle=复用外部已创建的;undefined=不隔离(parent cwd)。 */
422
+ /** 文件系统隔离:true=创建新 git worktreeWorktreeHandle=复用外部已创建的;undefined=不隔离(parent cwd)。 */
423
423
  worktree?: boolean | WorktreeHandle;
424
424
  /** 覆盖执行 cwd(默认 mainCwd)。 */
425
425
  cwd?: string;
@@ -527,7 +527,7 @@ export interface SubagentRecord {
527
527
  result?: string;
528
528
  error?: string;
529
529
  sessionFile?: string;
530
- /** [MF#3] fork+worktree 模式下子 agent 改动的 patch 文件路径(worktree 外,供调用方应用)。 */
530
+ /** [MF#3] worktree 模式下子 agent 改动的 patch 文件路径(worktree 外,供调用方应用)。 */
531
531
  patchFile?: string;
532
532
  /** 外部 Pi 实例(进程隔离模式下由外部启动的子进程)。 */
533
533
  externalInstance?: AliveMarker;
@@ -57,7 +57,7 @@ interface BgNotifyRecord {
57
57
  model?: string;
58
58
  result?: string;
59
59
  error?: string;
60
- /** [MF#1] fork+worktree background 完成通知携带的 patch 文件路径。 */
60
+ /** [MF#1] worktree background 完成通知携带的 patch 文件路径。 */
61
61
  patchFile?: string;
62
62
  }
63
63
 
@@ -275,7 +275,7 @@ function extractBgNotifyRecord(details: unknown): BgNotifyRecord | undefined {
275
275
  model: typeof d.model === "string" ? d.model : undefined,
276
276
  result: typeof d.result === "string" ? d.result : undefined,
277
277
  error: typeof d.error === "string" ? d.error : undefined,
278
- // [MF#1] 提取 patchFile(fork+worktree background 完成通知携带)。
278
+ // [MF#1] 提取 patchFile(worktree background 完成通知携带)。
279
279
  patchFile: typeof d.patchFile === "string" ? d.patchFile : undefined,
280
280
  };
281
281
  }
@@ -105,10 +105,10 @@ const SubagentParams = Type.Object({
105
105
  description: "Extra turns allowed after maxTurns is reached before SIGTERM (default 2). Only meaningful when maxTurns is set.",
106
106
  })),
107
107
  fork: Type.Optional(Type.Boolean({
108
- description: "Fork mode: inherit the parent's conversation context. When true, the subagent receives the parent's session file via --fork and builds a branched conversation (it sees prior turns/messages). The subagent still runs in a separate spawned child process (process isolation) — fork is about context inheritance, not process sharing. Use worktree:true (requires fork:true) for file-system isolation.",
108
+ description: "Fork mode: inherit the parent's conversation context. When true, the subagent receives the parent's session file via --fork and builds a branched conversation (it sees prior turns/messages). The subagent still runs in a separate spawned child process (process isolation) — fork is about context inheritance, not process sharing; independent of worktree (file-system isolation, see worktree param). When to use: only when the task extends from the parent and genuinely needs key information from the parent's conversation history that a self-contained task prompt cannot carry — most tasks a plain prompt can describe do NOT need fork, so keep false by default and enable only when the user explicitly asks or the task truly depends on seeing prior turns. Caveat: fork drags in the parent's dispatch records and unrelated task context, polluting the subagent (it cannot tell 'context meant for me' from 'parent dispatching me'); when state lives in an external store the subagent can query (e.g., cw handoff), prefer that over fork.",
109
109
  })),
110
110
  worktree: Type.Optional(Type.Boolean({
111
- description: "Worktree isolation (requires fork:true): run the subagent in a dedicated git worktree, providing file-system level isolation from the parent session. Prevents concurrent file-write conflicts between parent and subagent. Only takes effect when fork:true; passing worktree:true without fork:true throws an error.",
111
+ description: "Worktree isolation: run the subagent in a dedicated git worktree, providing file-system level isolation from the parent session (prevents concurrent file-write conflicts). Independent of fork worktree may be combined with fork:false (file isolation does not require context inheritance). When to use: parallel development scenarios where multiple agents write files concurrently and need isolated working directories (each gets its own checkout; merge later); leave false for single-agent or read-only tasks.",
112
112
  })),
113
113
  cwd: Type.Optional(Type.String({
114
114
  description: 'Override the working directory for the subagent execution. Must be an absolute path. Defaults to the parent session\'s cwd.',
@@ -142,9 +142,9 @@ export interface AgentCallOpts {
142
142
  * undefined 时 spawn 继承 workflow 进程的 cwd(向后兼容)。
143
143
  */
144
144
  cwd?: string;
145
- /** Inherit parent session context (fork mode). Required when worktree isolation is enabled. */
145
+ /** Inherit parent session context (fork mode). Independent of worktree (file isolation). */
146
146
  fork?: boolean;
147
- /** Filesystem isolation: when true, creates a new git worktree for the agent (requires fork: true). */
147
+ /** Filesystem isolation: when true, creates a new git worktree for the agent. Independent of fork. */
148
148
  worktree?: boolean;
149
149
  /** When true, agent() resolves {value, sessionFile, worktreePath, error} instead of a bare value.
150
150
  * Worker-layer flag only — not forwarded to ExecuteOptions (mapToExecuteOptions drops it). */