@zhushanwen/pi-subagent-workflow 5.0.1 → 6.0.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": "5.0.1",
3
+ "version": "6.0.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.",
@@ -599,7 +599,7 @@ export class SubagentService {
599
599
  // - **不可作为后续 agent 的 cwd**——目录已删,复用会 ENOENT。
600
600
  // - wave 内 worktree 复用(spec-w §2 "wave 内 8 action 共享 worktree")在 pi 当前架构下
601
601
  // 不可行:worktree 绑定单次 agent() record,每次 executeAndAwait 结束 finalizeRecord
602
- // 无条件 cleanup,worktree 无法跨 action 存活。wave 改用主 cwd(见 recursive-split.js)。
602
+ // 无条件 cleanup,worktree 无法跨 action 存活。wave 改用主 cwd
603
603
  wfResult.worktreePath = record.worktreeHandle?.path;
604
604
  return wfResult;
605
605
  }
@@ -224,3 +224,94 @@ describe("discoverResources (async)", () => {
224
224
  expect(result).toHaveLength(2);
225
225
  });
226
226
  });
227
+
228
+ // ============================================================
229
+ // user-extension-paths (XYZ_EXTENSION_PATHS) — dev-link 扩展发现
230
+ // ============================================================
231
+
232
+ describe("user-extension-paths (XYZ_EXTENSION_PATHS)", () => {
233
+ let ws: string;
234
+ let agentDir: string;
235
+ let savedEnv: string | undefined;
236
+
237
+ beforeEach(() => {
238
+ ws = tmpWorkspace();
239
+ agentDir = path.join(ws, ".fake-agent");
240
+ savedEnv = process.env.XYZ_EXTENSION_PATHS;
241
+ });
242
+ afterEach(() => {
243
+ if (savedEnv === undefined) delete process.env.XYZ_EXTENSION_PATHS;
244
+ else process.env.XYZ_EXTENSION_PATHS = savedEnv;
245
+ fs.rmSync(ws, { recursive: true, force: true });
246
+ });
247
+
248
+ it("discovers agents from XYZ_EXTENSION_PATHS via pi.agents manifest", () => {
249
+ const pkgDir = path.join(ws, "my-ext");
250
+ writePackageJson(pkgDir, { agents: ["./agents"] });
251
+ writeFile(path.join(pkgDir, "agents"), "custom.md", "body");
252
+ process.env.XYZ_EXTENSION_PATHS = pkgDir;
253
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, agentDir });
254
+ expect(result.map((r) => path.basename(r.path))).toEqual(["custom.md"]);
255
+ expect(result[0]?.source).toBe("user-extension-paths");
256
+ });
257
+
258
+ it("discovers agents via convention dir (no manifest)", () => {
259
+ const pkgDir = path.join(ws, "my-ext");
260
+ writeFile(path.join(pkgDir, "agents"), "conv.md", "body");
261
+ process.env.XYZ_EXTENSION_PATHS = pkgDir;
262
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, agentDir });
263
+ expect(result.map((r) => path.basename(r.path))).toEqual(["conv.md"]);
264
+ expect(result[0]?.source).toBe("user-extension-paths");
265
+ });
266
+
267
+ it("multiple paths separated by delimiter", () => {
268
+ const pkg1 = path.join(ws, "ext1");
269
+ const pkg2 = path.join(ws, "ext2");
270
+ writeFile(path.join(pkg1, "agents"), "a1.md", "body");
271
+ writeFile(path.join(pkg2, "agents"), "a2.md", "body");
272
+ process.env.XYZ_EXTENSION_PATHS = `${pkg1}${path.delimiter}${pkg2}`;
273
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, agentDir });
274
+ expect(result.map((r) => path.basename(r.path)).sort()).toEqual(["a1.md", "a2.md"]);
275
+ expect(result.every((r) => r.source === "user-extension-paths")).toBe(true);
276
+ });
277
+
278
+ it("overrides npm on name clash (priority: user-extension-paths > npm)", () => {
279
+ const npmPkg = path.join(agentDir, "npm", "node_modules", "test-pkg");
280
+ writePackageJson(npmPkg, { agents: ["./agents"] });
281
+ writeFile(path.join(npmPkg, "agents"), "shared.md", "npm-body");
282
+ const devPkg = path.join(ws, "dev-ext");
283
+ writePackageJson(devPkg, { agents: ["./agents"] });
284
+ writeFile(path.join(devPkg, "agents"), "shared.md", "dev-body");
285
+ process.env.XYZ_EXTENSION_PATHS = devPkg;
286
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, agentDir });
287
+ const shared = result.find((r) => path.basename(r.path) === "shared.md");
288
+ expect(shared?.source).toBe("user-extension-paths");
289
+ });
290
+
291
+ it("project-agents overrides user-extension-paths (project wins)", () => {
292
+ const devPkg = path.join(ws, "dev-ext");
293
+ writePackageJson(devPkg, { agents: ["./agents"] });
294
+ writeFile(path.join(devPkg, "agents"), "x.md", "dev-body");
295
+ writeFile(path.join(ws, ".agents", "agents"), "x.md", "project-body");
296
+ process.env.XYZ_EXTENSION_PATHS = devPkg;
297
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, agentDir });
298
+ const x = result.find((r) => path.basename(r.path) === "x.md");
299
+ expect(x?.source).toBe("project-agents");
300
+ });
301
+
302
+ it("empty/unset env → no user-extension-paths source", () => {
303
+ delete process.env.XYZ_EXTENSION_PATHS;
304
+ const result = discoverResourcesSync({ kind: "agents", workspaceRoot: ws, agentDir });
305
+ expect(result.filter((r) => r.source === "user-extension-paths")).toEqual([]);
306
+ });
307
+
308
+ it("async discoverResources also scans user-extension-paths", async () => {
309
+ const pkgDir = path.join(ws, "my-ext");
310
+ writePackageJson(pkgDir, { agents: ["./agents"] });
311
+ writeFile(path.join(pkgDir, "agents"), "async.md", "body");
312
+ process.env.XYZ_EXTENSION_PATHS = pkgDir;
313
+ const result = await discoverResources({ kind: "agents", workspaceRoot: ws, agentDir });
314
+ expect(result.map((r) => path.basename(r.path))).toEqual(["async.md"]);
315
+ expect(result[0]?.source).toBe("user-extension-paths");
316
+ });
317
+ });
@@ -14,7 +14,7 @@
14
14
  import * as fsSync from "node:fs";
15
15
  import { access, readdir, readFile, stat } from "node:fs/promises";
16
16
  import { homedir } from "node:os";
17
- import { join,resolve } from "node:path";
17
+ import { delimiter, join, resolve } from "node:path";
18
18
 
19
19
  // ── 类型 ─────────────────────────────────────────────────────
20
20
 
@@ -32,7 +32,7 @@ export interface DiscoveredResource {
32
32
  }
33
33
 
34
34
  /** 资源来源层级 */
35
- export type ResourceSource = "user-pi" | "user-agents" | "npm" | "npm-dev" | "project-pi" | "project-pi-tmp" | "project-agents";
35
+ export type ResourceSource = "user-pi" | "user-agents" | "npm" | "npm-dev" | "user-extension-paths" | "project-pi" | "project-pi-tmp" | "project-agents";
36
36
 
37
37
  /** 扫描配置 */
38
38
  export interface ScanConfig {
@@ -288,6 +288,24 @@ interface ScanTarget {
288
288
  enabled: boolean;
289
289
  }
290
290
 
291
+ /**
292
+ * 读取 XYZ_EXTENSION_PATHS 环境变量(dev-link 写入的扩展源码路径)。
293
+ *
294
+ * delimiter 分隔(POSIX ':' / Windows ';'),trim + 过滤空 + ~ 展开。
295
+ * 每个路径是一个 extension 包目录(dev-link 指向源码),走 processPackage 发现其
296
+ * agents/workflows。解析逻辑与 extension-service.getUserExtensionPaths() 一致。
297
+ */
298
+ function readExtensionPaths(): string[] {
299
+ const raw = process.env.XYZ_EXTENSION_PATHS;
300
+ if (!raw) return [];
301
+ const paths = raw
302
+ .split(delimiter)
303
+ .map((p) => p.trim())
304
+ .filter((p) => p.length > 0)
305
+ .map((p) => (p.startsWith("~") ? join(homedir(), p.slice(1)) : p));
306
+ return [...new Set(paths)];
307
+ }
308
+
291
309
  /**
292
310
  * 构建所有扫描源(按优先级低→高排列)。
293
311
  *
@@ -306,6 +324,10 @@ function buildScanTargets(config: ScanConfig): ScanTarget[] {
306
324
  { dir: join(agentDir, "npm", "node_modules"), source: "npm", enabled: true },
307
325
  // 4. npm dev symlink: agentDir/extensions/*/<pkg>/
308
326
  { dir: join(agentDir, "extensions"), source: "npm-dev", enabled: true },
327
+ // user extension paths (XYZ_EXTENSION_PATHS, dev-link): each path is a package dir,
328
+ // 走 processPackage 读 pi.{kind} manifest 或扫 {kind}/ 目录。优先级高于 npm/npm-dev
329
+ // (dev-link 是开发版 override),低于 project(项目正式资源优先)。
330
+ ...readExtensionPaths().map((dir) => ({ dir, source: "user-extension-paths" as const, enabled: true })),
309
331
  // 5. project .pi/{kind}/
310
332
  { dir: join(workspaceRoot, ".pi", kind), source: "project-pi", enabled: true },
311
333
  ];
@@ -354,6 +376,11 @@ export async function discoverResources(config: ScanConfig): Promise<DiscoveredR
354
376
  // 覆盖 source 标签(scanNpmDir 内部统一标 "npm",这里修正为实际源)
355
377
  const tagged = resources.map((r) => ({ ...r, source: target.source }));
356
378
  allBySource.push({ source: target.source, resources: tagged });
379
+ } else if (target.source === "user-extension-paths") {
380
+ // XYZ_EXTENSION_PATHS(dev-link):每个 dir 是单个包目录,走 processPackage
381
+ const resources = await processPackage(target.dir, config.kind);
382
+ const tagged = resources.map((r) => ({ ...r, source: target.source }));
383
+ allBySource.push({ source: target.source, resources: tagged });
357
384
  } else {
358
385
  // 普通目录:直接扫
359
386
  const files = await scanDirectory(target.dir, config.kind);
@@ -517,6 +544,10 @@ export function discoverResourcesSync(config: ScanConfig): DiscoveredResource[]
517
544
  if (target.source === "npm" || target.source === "npm-dev") {
518
545
  const resources = scanNpmDirSync(target.dir, config.kind);
519
546
  all.push(...resources.map((r) => ({ ...r, source: target.source })));
547
+ } else if (target.source === "user-extension-paths") {
548
+ // XYZ_EXTENSION_PATHS(dev-link):每个 dir 是单个包目录
549
+ const resources = processPackageSync(target.dir, config.kind);
550
+ all.push(...resources.map((r) => ({ ...r, source: target.source })));
520
551
  } else {
521
552
  const files = scanDirectorySync(target.dir, config.kind);
522
553
  all.push(...files.map((f) => ({ path: f, source: target.source, available: true })));
@@ -1,6 +1,6 @@
1
1
  // review-fix-loop-utils.cjs — review-fix-loop.js 的可测纯函数模块
2
2
  //
3
- // 与 recursive-split-utils.cjs 同款模式:workflow 编排逻辑的纯函数抽到独立 .cjs,
3
+ // workflow 编排逻辑的纯函数抽到独立 .cjs,
4
4
  // 供 vitest 单测直接 require(extensions/subagent-workflow/src/__tests__/review-fix-loop-utils.test.ts)
5
5
  // 与 worker 运行时共用(review-fix-loop.js 经 workerData.scriptPath 定位本文件)。
6
6
  //
@@ -34,8 +34,8 @@ function fail(msg) {
34
34
 
35
35
  // ── 可测纯函数模块 ────────────────────────────────────────────────
36
36
  // 参数校验(normalizeBool/normalizeInt/白名单)/批次解析/聚合结果解析/审查指令构建
37
- // 的纯函数在 review-fix-loop-utils.cjs(与 recursive-split-utils.cjs 同款模式,
38
- // vitest 单测见 src/__tests__/review-fix-loop-utils.test.ts)。
37
+ // 的纯函数在 review-fix-loop-utils.cjs
38
+ // vitest 单测见 src/__tests__/review-fix-loop-utils.test.ts
39
39
  // worker 运行时经 workerData.scriptPath 定位自身目录——内置 workflow 在 npm 包内,
40
40
  // process.cwd() 是用户项目目录,不能作为锚点;其他引擎无 workerData 时回退 cwd。
41
41
  const {
@@ -313,8 +313,8 @@ function buildReviewCall(def, round, max, batchIndex, roundDir, scoped) {
313
313
  model: MODEL || def.model,
314
314
  schema: reviewerSchema,
315
315
  description: def.name,
316
- timeoutMs: 1_800_000,
317
- // returnMeta: true — recursive-split 脚本的 executeActionAgent 对齐:失败时 resolve
316
+ timeoutMs: 3_600_000, // 1h(只读审查 + retry 退避余量)
317
+ // returnMeta: true — 失败时 resolve
318
318
  // {value, error},raw.error 可检测(review- 前缀兜底/结构化终止可达);成功时
319
319
  // value = parsedOutput ?? content,parseResult 作用于 raw.value(MF-1)。
320
320
  returnMeta: true,
@@ -558,7 +558,7 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
558
558
  model: MODEL,
559
559
  schema: aggregatorSchema,
560
560
  description: "aggregate",
561
- timeoutMs: 1_800_000,
561
+ timeoutMs: 3_600_000, // 1h
562
562
  returnMeta: true,
563
563
  });
564
564
 
@@ -788,9 +788,8 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
788
788
  // frontmatter model 字段同样生效(之前丢弃了 FIX_DEF.model,只在 review 阶段消费)
789
789
  model: MODEL || (FIX_DEF && FIX_DEF.model),
790
790
  description: (FIX_DEF && FIX_DEF.name) || "fix",
791
- // info #15: 显式 timeoutMs review/aggregator 档位一致(fix 是写操作中最长阶段,
792
- // 不依赖引擎默认值——引擎默认值变化不会悄然缩短 fix 预算)
793
- timeoutMs: 1_800_000,
791
+ // fix 不设 timeoutMs = 不限时(execute-options-mapper: undefined/<=0 → 不设超时)。
792
+ // 带写操作(改项目代码)可能很久(大重构/多文件),不应被墙钟超时打断。
794
793
  returnMeta: true,
795
794
  ...(FIX_DEF && !FIX_DEF.isCustom ? { agent: FIX_DEF.name } : {}),
796
795
  });