@zhushanwen/pi-subagent-workflow 5.0.2 → 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": "
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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 {
|
|
@@ -314,7 +314,7 @@ function buildReviewCall(def, round, max, batchIndex, roundDir, scoped) {
|
|
|
314
314
|
schema: reviewerSchema,
|
|
315
315
|
description: def.name,
|
|
316
316
|
timeoutMs: 3_600_000, // 1h(只读审查 + retry 退避余量)
|
|
317
|
-
// returnMeta: true —
|
|
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,
|