@zhushanwen/pi-subagent-workflow 9.2.0 → 9.2.1

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": "9.2.0",
3
+ "version": "9.2.1",
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.",
@@ -58,12 +58,12 @@
58
58
  ]
59
59
  },
60
60
  "dependencies": {
61
- "@zhushanwen/session-delivery": "1.0.0",
62
61
  "@zhushanwen/extension-protocol": "0.17.0",
62
+ "@zhushanwen/session-delivery": "1.0.0",
63
63
  "@zhushanwen/pi-ext-guards": "0.4.2",
64
- "@zhushanwen/pi-notify-ledger-host": "0.2.3",
65
64
  "@zhushanwen/pi-extension-logger": "0.6.1",
66
- "@zhushanwen/subagent-core": "1.2.0",
65
+ "@zhushanwen/pi-notify-ledger-host": "0.2.4",
66
+ "@zhushanwen/subagent-core": "1.2.1",
67
67
  "@zhushanwen/pi-subagent-cli": "0.5.0",
68
68
  "@zhushanwen/zcode-subagent-cli": "0.4.4"
69
69
  },
@@ -0,0 +1,147 @@
1
+ // staged-discovery.test.ts — 打包态内置资产**发现结果**门禁(档 2,2026-10-03 B′)
2
+ //
3
+ // 为什么单独建这个文件:本次事故(打包版 `subagents` 批量工具报
4
+ // "Built-in workflow 'fan-out' is not available")的根因是**发现层面**——esbuild
5
+ // 自包含 bundle 下 `require.resolve("@zhushanwen/subagent-core/…")` 必败,staged 布局
6
+ // 又无 node_modules,于是孝在包里的 `workflows/` 无人能扫到。既有门禁全是“文件在不在”
7
+ // 级断言(dry-run import 不调工厂、TC8 只比字节),对本类失效天然失明。
8
+ //
9
+ // 本文件的断言对象是**发现结果**(6 内置 workflow + 10 内置 agent 真的被列出来),
10
+ // 而非“文件存在”:前者才能拦住“路径/产物层”回归。两条通路都被锁:
11
+ // ① 纯函数:bundle 形态模块路径 → scope 根;dev/异常形态 → undefined(不误触发);
12
+ // ② 集成:以合成 bundle 模块路径调 `corePackageNpmRoot()`(resolve 必败 → 回退分支),
13
+ // 再把回退根当 npm 槽根跑**真实** `discoverResources`,断言结果与源目录逐文件一致。
14
+ //
15
+ // 资产集不写死数量(口径:与 `packages/subagent-core/{workflows,agents}` 源目录集合比对,
16
+ // 源增删自动跟随)。
17
+
18
+ import { cpSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
19
+ import { createRequire } from "node:module";
20
+ import { tmpdir } from "node:os";
21
+ import { dirname, join } from "node:path";
22
+ import { pathToFileURL } from "node:url";
23
+
24
+ import { discoverResources, isTargetFile } from "@zhushanwen/subagent-core";
25
+ import { afterAll, describe, expect, it, vi } from "vitest";
26
+
27
+ // pi 宿主协作件 mock:本文件只跑发现链,日志面给可调用桩(回退分支会走 debug)。
28
+ vi.mock("@earendil-works/pi-coding-agent", () => ({
29
+ getAgentDir: () => process.env.PI_CODING_AGENT_DIR ?? "/mock/agent-dir",
30
+ }));
31
+ vi.mock("@zhushanwen/pi-extension-logger", () => ({
32
+ getLogger: () => ({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }),
33
+ }));
34
+
35
+ const { corePackageNpmRoot, stagedScopeRootFromModuleUrl } = await import("../pi-host.ts");
36
+
37
+ /** 本仓 `packages/subagent-core`(源单源:断言口径由此推导,不写死数量)。
38
+ * 路径经与生产同一锚点解析得出(禁止手数 `..` 层级)。 */
39
+ const CORE_PKG = dirname(
40
+ dirname(createRequire(import.meta.url).resolve("@zhushanwen/subagent-core/workflows/README.md")),
41
+ );
42
+
43
+ const tmpDirs: string[] = [];
44
+ function makeTmp(prefix: string): string {
45
+ const dir = mkdtempSync(join(tmpdir(), prefix));
46
+ tmpDirs.push(dir);
47
+ return dir;
48
+ }
49
+ afterAll(() => {
50
+ for (const d of tmpDirs) {
51
+ // teardown 递归删除补 maxRetries:与在途异步写竞争时 ENOTEMPTY 满载 flake
52
+ rmSync(d, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
53
+ }
54
+ });
55
+
56
+ /** 合成 staged 布局:<tmp>/@zhushanwen/pi-subagent-workflow/{index.js,workflows,agents}。 */
57
+ function makeStagedLayout(): { scopeDir: string; moduleUrl: string } {
58
+ const root = makeTmp("staged-discovery-");
59
+ const pkgDir = join(root, "@zhushanwen", "pi-subagent-workflow");
60
+ mkdirSync(pkgDir, { recursive: true });
61
+ const indexJs = join(pkgDir, "index.js");
62
+ writeFileSync(indexJs, "// staged bundle placeholder\n", "utf8");
63
+ for (const kind of ["workflows", "agents"] as const) {
64
+ cpSync(join(CORE_PKG, kind), join(pkgDir, kind), { recursive: true });
65
+ }
66
+ return { scopeDir: join(root, "@zhushanwen"), moduleUrl: pathToFileURL(indexJs).href };
67
+ }
68
+
69
+ /** 源目录内可发现文件的集合——判据直接复用发现层导出的 isTargetFile(同一份
70
+ * 函数,发现层口径变化时期望集自动跟随,无双源)。 */
71
+ function sourceExpected(kind: "workflows" | "agents"): string[] {
72
+ return readdirSync(join(CORE_PKG, kind))
73
+ .filter((f) => isTargetFile(f, kind))
74
+ .sort();
75
+ }
76
+
77
+ describe("B′:staged/bundle 形态的 core 资产发现根", () => {
78
+ it("stagedScopeRootFromModuleUrl:bundle 形态(<scope>/<pkg>/index.js)→ 返回 scope 目录", () => {
79
+ const { scopeDir, moduleUrl } = makeStagedLayout();
80
+ expect(stagedScopeRootFromModuleUrl(moduleUrl)).toBe(scopeDir);
81
+ });
82
+
83
+ it("stagedScopeRootFromModuleUrl:dev 形态(非 scope 布局)→ undefined(不误触发回退)", () => {
84
+ const devLike = pathToFileURL(join(CORE_PKG, "workflows", "fan-out.js")).href;
85
+ expect(stagedScopeRootFromModuleUrl(devLike)).toBeUndefined();
86
+ });
87
+
88
+ it("corePackageNpmRoot:dev/workspace 形态走 resolve 通路(不回退)", () => {
89
+ const root = corePackageNpmRoot();
90
+ expect(root).toBeDefined();
91
+ // workspace 形态:scope 根下必须有 subagent-core 包目录(resolve 命中锚点的上三级)
92
+ expect(readdirSync(root as string)).toContain("subagent-core");
93
+ });
94
+
95
+ it("corePackageNpmRoot:bundle 形态 resolve 必败 → 回退到 scope 根", () => {
96
+ const { scopeDir, moduleUrl } = makeStagedLayout();
97
+ expect(corePackageNpmRoot(moduleUrl)).toBe(scopeDir);
98
+ });
99
+ });
100
+
101
+ describe("B′:回退根下的**发现结果**(本批防复发的核心断言)", () => {
102
+ it("workflows:发现结果与源目录集合逐文件一致(内置模板全部可派发)", async () => {
103
+ const { scopeDir } = makeStagedLayout();
104
+ const found = await discoverResources({
105
+ kind: "workflows",
106
+ workspaceRoot: scopeDir,
107
+ hostRoots: [{ dir: scopeDir, source: "npm" }],
108
+ });
109
+ // 只断言**回退根下**的发现结果(发现面还包括真实用户目录 ~/.agents/workflows
110
+ // 等宿主/约定根——本用例的主题是回退根本身,故按路径范围收窄,噪声不介入)
111
+ const names = found
112
+ .filter((r) => r.available && r.path.startsWith(scopeDir))
113
+ .map((r) => r.path.split("/").pop() as string)
114
+ .sort();
115
+ expect(names).toEqual(sourceExpected("workflows"));
116
+ // 回归锚:批量入口的固定执行体必须在列
117
+ expect(names).toContain("fan-out.js");
118
+ });
119
+
120
+ it("agents:发现结果与源目录集合逐文件一致(10 内置角色全部可选)", async () => {
121
+ const { scopeDir } = makeStagedLayout();
122
+ const found = await discoverResources({
123
+ kind: "agents",
124
+ workspaceRoot: scopeDir,
125
+ hostRoots: [{ dir: scopeDir, source: "npm" }],
126
+ });
127
+ const names = found
128
+ .filter((r) => r.available && r.path.startsWith(scopeDir))
129
+ .map((r) => r.path.split("/").pop() as string)
130
+ .sort();
131
+ expect(names).toEqual(sourceExpected("agents"));
132
+ expect(names).toContain("general-purpose.md");
133
+ });
134
+
135
+ it("负向:源资产缺席时发现结果为空(门禁失败方向正确,不会假绿)", async () => {
136
+ const root = makeTmp("staged-discovery-empty-");
137
+ const pkgDir = join(root, "@zhushanwen", "pi-subagent-workflow");
138
+ mkdirSync(pkgDir, { recursive: true });
139
+ writeFileSync(join(pkgDir, "index.js"), "// no assets\n", "utf8");
140
+ const found = await discoverResources({
141
+ kind: "workflows",
142
+ workspaceRoot: join(root, "@zhushanwen"),
143
+ hostRoots: [{ dir: join(root, "@zhushanwen"), source: "npm" }],
144
+ });
145
+ expect(found.filter((r) => r.available)).toEqual([]);
146
+ });
147
+ });
@@ -62,20 +62,34 @@ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
62
62
  * realpath 去重吸收)。
63
63
  *
64
64
  * 每次调用现解析(不 memo):发现调用点稀疏(session_start + 缓存 miss),解析
65
- * 成本可忽略;失败(异常布局/解析器不可用)降级为不注入并 warn——绝不因资产
66
- * 接线失败阻断发现主链。
65
+ * 成本可忽略;两种形态各有通路,都不阻断发现主链:
66
+ * - **dev / npm 形态**:`require.resolve` 沿本包依赖解析链命中 core 包;
67
+ * - **bundle / staged 打包形态**(2026-10-03 B′ 修复):esbuild 自包含 bundle 把
68
+ * core inline、staged 布局无 node_modules ⇒ resolve 必败(实证:pi-host 日志全天
69
+ * 202 条 'Cannot find module … workflows/README.md',内置 6 workflow + 10 agent
70
+ * 整体不可发现、`subagents` 批量工具报 'Built-in workflow fan-out is not available')。
71
+ * 此时回退到 staged scope 根(见 stagedScopeRootFromModuleUrl)——仅当回退也不
72
+ * 成立(异常布局/解析器不可用)才降级为不注入并 warn。
67
73
  */
68
- function corePackageNpmRoot(): string | undefined {
74
+ export function corePackageNpmRoot(moduleUrl: string = import.meta.url): string | undefined {
69
75
  try {
70
76
  // createRequire 锚定本模块(jiti 加载器下 import.meta.url 可用;不可用则随
71
77
  // catch 降级)。require.resolve 沿 pi-sw 自身的依赖解析链——workspace 与发布态
72
78
  // 都从本包出发命中 core。
73
- const require = createRequire(import.meta.url);
79
+ const require = createRequire(moduleUrl);
74
80
  const anchor = require.resolve("@zhushanwen/subagent-core/workflows/README.md");
75
81
  return dirname(dirname(dirname(anchor)));
76
82
  } catch (err) {
77
- // getLogger 惰性调用(catch 是冷路径——测试环境对 pi-extension-logger 的
78
- // module-level mock 可能返回 undefined,模块级持有会在 import 期踩 undefined)
83
+ const stagedRoot = stagedScopeRootFromModuleUrl(moduleUrl);
84
+ if (stagedRoot !== undefined) {
85
+ // getLogger 惰性调用(冷路径:测试环境对 pi-extension-logger 的 module-level
86
+ // mock 可能返回 undefined,模块级持有会在 import 期踩 undefined)
87
+ getLogger("pi-host").debug(
88
+ "[pi-host] core 包经 staged scope 根注入(bundle 形态,无 node_modules 解析面)",
89
+ { root: stagedRoot },
90
+ );
91
+ return stagedRoot;
92
+ }
79
93
  getLogger("pi-host").warn(
80
94
  "[pi-host] core 包 agents/ 注入根解析失败——10 内置角色可能不可发现",
81
95
  { reason: toErrorMessage(err) },
@@ -84,6 +98,40 @@ function corePackageNpmRoot(): string | undefined {
84
98
  }
85
99
  }
86
100
 
101
+ /**
102
+ * bundle / staged 形态的 core 资产注入根(2026-10-03 B′)。
103
+ *
104
+ * 打包态扩展被 esbuild 打成**自包含 index.js**(core 被 inline),staged 布局内无
105
+ * node_modules ⇒ `require.resolve` 锚点必败。但此时模块位置是
106
+ * `<stagedScope>/<pkg>/index.js`(如
107
+ * `<Resources>/extensions/@zhushanwen/pi-subagent-workflow/index.js`),其**父目录**
108
+ * 正是 `@zhushanwen` scope 目录——npm 槽语义("一级子项 = 包目录")恰好成立,
109
+ * 于是可直接当 npm 槽根用:约定目录扫描即命中
110
+ * `pi-subagent-workflow/{workflows,agents}`(两目录由 `scripts/bundle-extensions.mjs`
111
+ * 随包拷贝;`pi.agents`/`pi.workflows` manifest 约定及其"声明路径不存在 → 整包失败
112
+ * 占位"语义是 subagent-core resource-discovery 的 taiji 自建规则,权威源 =
113
+ * resource-discovery.ts processPackage——pi 上游 manifest 只有
114
+ * extensions/skills/prompts/themes 四字段,无 agents/workflows 概念。已核仓内无任何
115
+ * 包声明该字段,故扫描面内所有包都走约定目录分支,不进 manifest 失败分支)。
116
+ *
117
+ * 与 dev/npm 形态的既有语义一致:npm 槽根扫的就是 scope 下全部包目录(因此本包自带
118
+ * 的 workflows/agents 在 dev/npm 形态同样入发现面),非新增语义。
119
+ *
120
+ * 形态判据 = 父目录名以 `@` 开头:dev(模块在 `extensions/universal/...`)与 npm
121
+ * 安装形态都先走 resolve 成功分支,本回退不可达;仅 bundle/staged 形态命中。
122
+ */
123
+ export function stagedScopeRootFromModuleUrl(moduleUrl: string): string | undefined {
124
+ try {
125
+ const pkgDir = dirname(fileURLToPath(moduleUrl));
126
+ const scopeDir = dirname(pkgDir);
127
+ if (scopeDir === pkgDir) return undefined;
128
+ const scopeName = scopeDir.split(/[\\/]/).pop() ?? "";
129
+ return scopeName.startsWith("@") ? scopeDir : undefined;
130
+ } catch {
131
+ return undefined;
132
+ }
133
+ }
134
+
87
135
  /** agents/workflows 共享的 agentDir 派生根(末级目录名由 kind 决定)。
88
136
  * 顺序与 source 标签逐项对齐 resource-discovery.ts buildScanTargets 的
89
137
  * user-pi → npm → npm-dev 段(根列表按优先级低→高排列,D2 语义边界)。