@zhushanwen/pi-subagent-workflow 5.0.2 → 7.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.
Files changed (77) hide show
  1. package/agents/{reviewer.md → code-reviewer.md} +20 -3
  2. package/agents/context-builder.md +5 -0
  3. package/agents/doc-reviewer.md +9 -2
  4. package/agents/explorer.md +5 -0
  5. package/agents/general-purpose.md +5 -0
  6. package/agents/oracle.md +20 -4
  7. package/agents/orchestrator.md +6 -1
  8. package/agents/planner.md +5 -0
  9. package/agents/researcher.md +5 -0
  10. package/agents/worker.md +5 -0
  11. package/package.json +6 -4
  12. package/src/execution/__tests__/agent-registry.test.ts +189 -119
  13. package/src/execution/__tests__/crash-recovery.test.ts +0 -1
  14. package/src/execution/__tests__/execute-options-mapper.test.ts +4 -4
  15. package/src/execution/__tests__/index-session-start.test.ts +0 -1
  16. package/src/execution/__tests__/model-resolver.test.ts +20 -0
  17. package/src/execution/__tests__/session-start-reaper.test.ts +0 -2
  18. package/src/execution/__tests__/subprocess-agent-runner.test.ts +1 -1
  19. package/src/execution/agent-registry.ts +92 -169
  20. package/src/execution/execute-options-mapper.ts +2 -2
  21. package/src/execution/model-config-service.ts +13 -34
  22. package/src/execution/model-resolver.ts +5 -3
  23. package/src/execution/subagent-service.ts +9 -6
  24. package/src/execution/subprocess-agent-runner.ts +3 -2
  25. package/src/index.ts +4 -25
  26. package/src/injectors/__tests__/subagent-list-injector.test.ts +266 -14
  27. package/src/injectors/__tests__/workflow-list-injector.test.ts +236 -32
  28. package/src/injectors/subagent-list-injector.ts +99 -48
  29. package/src/injectors/workflow-list-injector.ts +65 -50
  30. package/src/interface/__tests__/detectors.test.ts +100 -43
  31. package/src/interface/__tests__/subagent-tool-prompt.test.ts +8 -12
  32. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +163 -0
  33. package/src/interface/__tests__/workflow-tool-prompt.test.ts +55 -9
  34. package/src/interface/subagent-tool.ts +7 -4
  35. package/src/interface/tool-workflow-script.ts +27 -10
  36. package/src/interface/tool-workflow.ts +174 -81
  37. package/src/orchestration/__tests__/args-validator.test.ts +143 -0
  38. package/src/orchestration/__tests__/config-loader.test.ts +124 -40
  39. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +33 -2
  40. package/src/orchestration/__tests__/lifecycle.test.ts +59 -2
  41. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +116 -51
  42. package/src/orchestration/__tests__/script-lint.test.ts +167 -1
  43. package/src/orchestration/__tests__/worker-host.test.ts +120 -0
  44. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +69 -0
  45. package/src/orchestration/__tests__/worker-script-builder.test.ts +51 -1
  46. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +2 -2
  47. package/src/orchestration/__tests__/workflows-e2e.test.ts +177 -24
  48. package/src/orchestration/agent-opts-resolver.ts +51 -101
  49. package/src/orchestration/args-validator.ts +127 -0
  50. package/src/orchestration/config-loader.ts +63 -94
  51. package/src/orchestration/error-recovery.ts +6 -9
  52. package/src/orchestration/launcher.ts +77 -41
  53. package/src/orchestration/lifecycle.ts +7 -0
  54. package/src/orchestration/models/ports.ts +0 -14
  55. package/src/orchestration/models/run-spec.ts +24 -1
  56. package/src/orchestration/models/types.ts +15 -8
  57. package/src/orchestration/models/workflow-script-registry.ts +3 -0
  58. package/src/orchestration/models/workflow-script.ts +10 -14
  59. package/src/orchestration/script-lint.ts +159 -0
  60. package/src/orchestration/worker-host.ts +5 -0
  61. package/src/orchestration/worker-script-builder.ts +15 -4
  62. package/src/orchestration/workflow-script-registry-impl.ts +34 -29
  63. package/src/shared/__tests__/meta-parser.test.ts +304 -0
  64. package/src/shared/__tests__/resource-discovery.test.ts +167 -7
  65. package/src/shared/__tests__/resource-meta.test.ts +51 -0
  66. package/src/shared/agent-ref.ts +36 -0
  67. package/src/shared/meta-parser.ts +257 -0
  68. package/src/shared/resource-discovery.ts +88 -2
  69. package/src/shared/resource-meta.ts +60 -0
  70. package/workflows/README.md +2 -2
  71. package/workflows/_shared/agent-refs.cjs +40 -0
  72. package/workflows/chain.js +30 -6
  73. package/workflows/map-reduce.js +33 -5
  74. package/workflows/parallel.js +34 -7
  75. package/workflows/review-fix-loop-utils.cjs +23 -103
  76. package/workflows/review-fix-loop.js +121 -58
  77. package/workflows/scatter-gather.js +30 -6
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Meta Parser — 资源元数据统一解析器(v5 §4.2 / IF1 + IF2)
3
+ *
4
+ * 两个变体,职责分离(R8-F1:discovery 的 fail-safe null 与 generate 的 linePos 需求互斥,
5
+ * 单一函数无法兼顾):
6
+ * - parseResourceMeta(IF1,discovery 用):fail-safe,任何失败返 null,不抛。
7
+ * 供 config-loader / registry / 两 injector / agent-registry 调用(4 parser 收敛为 1)。
8
+ * - parseResourceMetaDetailed(IF2,generate 闭环用):失败返 {ok:false, error, linePos},
9
+ * linePos 取自 eemeli/yaml YAMLParseError.linePos[0]([P-yaml] 探针实测:
10
+ * e.linePos 是 [start,end] 数组,取 [0] 作起止点),供 actionGenerate 报行列给 LLM 自纠正。
11
+ *
12
+ * 格式(v5 §7 / DM4):
13
+ * - workflow (.js):块注释 `/* @pi-meta <YAML> * /`(单星,非 JSDoc),WORKFLOW_META_RE 提取。
14
+ * - agent (.md):frontmatter `--- <YAML> ---`,FRONTMATTER_RE 提取。
15
+ * - 无 legacy fallback(D1):const meta 旧格式 → extractBlock 取不到块 → null。
16
+ *
17
+ * [P-yaml] 探针已验证:eemeli/yaml 2.9.0 的 YAMLParseError.linePos = [{line,col},{line,col}]。
18
+ *
19
+ * exec-review 修复(major-1 + minor-2..8):
20
+ * - 正则闭合符(星斜杠)必须独占行首,防止 YAML 正文里中途出现的星斜杠(如 usage 块标量
21
+ * 或 patternProperties 正则)截断块致 parameters 等字段静默丢失(§2.3 failure-A 同形态)。
22
+ * - typecheckMeta 严格化:kind 专属字段不可串类(workflow 不许 examples、agent 不许 phases),
23
+ * description 必填,phase detail 非字符串/parameters 非对象均 reject(消除「静默丢弃非法字段」)。
24
+ * - 区分「未找到块」(undefined) 与「块为空」(""),IF2 给可操作错误。
25
+ * - FRONTMATTER_RE 兼容 CRLF。
26
+ *
27
+ * 层归属:shared(L2 统一解析器)。
28
+ */
29
+
30
+ import { parse as parseYaml } from "yaml";
31
+
32
+ import type {
33
+ AgentMeta,
34
+ ResourceKind,
35
+ ResourceMeta,
36
+ RoutingExample,
37
+ WorkflowMeta,
38
+ } from "./resource-meta.ts";
39
+
40
+ // ── 格式提取正则 ──────────────────────────────────────────────
41
+
42
+ /**
43
+ * workflow @pi-meta 块注释:单星块注释(非 JSDoc `/**`),内容为 YAML。
44
+ * 闭合符(星斜杠)必须独占行首、列 0——防止 YAML 正文里中途出现的星斜杠(如 usage 块标量
45
+ * 内的 see-星斜杠-for、或 patternProperties 正则含星后接斜杠)截断块致后续字段静默丢失(major-1)。
46
+ * 格式规范要求闭合符在列 0(v5 §7),故列 0 闭合不损失合法用例。
47
+ */
48
+ const WORKFLOW_META_RE = /\/\*\s*@pi-meta\s*\n([\s\S]*?)\n\*\//;
49
+
50
+ /** agent frontmatter:标准 YAML frontmatter,兼容 CRLF(minor-7)。 */
51
+ const FRONTMATTER_RE = /---\r?\n([\s\S]*?)\r?\n---/;
52
+
53
+ /**
54
+ * 按资源种类取 meta 块文本(YAML 体)。
55
+ * @returns 未匹配返 undefined;匹配返字符串(可能为空 "")——调用方据 undefined 区分「未找到」。
56
+ */
57
+ function extractBlock(content: string, kind: ResourceKind): string | undefined {
58
+ const re = kind === "workflow" ? WORKFLOW_META_RE : FRONTMATTER_RE;
59
+ return re.exec(content)?.[1];
60
+ }
61
+
62
+ // ── 类型校验(手写,非 ajv;meta 结构简单)──────────────────
63
+
64
+ function isNonEmptyString(v: unknown): v is string {
65
+ return typeof v === "string" && v.length > 0;
66
+ }
67
+ function isString(v: unknown): v is string {
68
+ return typeof v === "string";
69
+ }
70
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
71
+ return typeof v === "object" && v !== null && !Array.isArray(v);
72
+ }
73
+
74
+ /**
75
+ * 把 parseYaml 结果(unknown)校验为类型化 ResourceMeta,失败返 null(语义非法,非语法错)。
76
+ * 严格化(exec-review minor-2..5):kind 专属字段不可串类、description 必填、
77
+ * phase detail 非字符串/parameters 非对象均 reject(消除「静默丢弃非法字段」)。
78
+ */
79
+ function typecheckMeta(raw: unknown, kind: ResourceKind): ResourceMeta | null {
80
+ if (!isPlainObject(raw)) return null;
81
+ const o = raw;
82
+
83
+ // 公共必填:name 非空字符串、description 必须是字符串(minor-3:缺 description reject)
84
+ if (!isNonEmptyString(o.name)) return null;
85
+ if (!isString(o.description)) return null;
86
+ const when = isString(o.when) ? o.when : undefined;
87
+ const notFor = isString(o.notFor) ? o.notFor : undefined;
88
+
89
+ if (kind === "workflow") {
90
+ // minor-2:agent 专属字段不可出现在 workflow(串类 reject)
91
+ if (o.examples !== undefined || o.tools !== undefined || o.model !== undefined) return null;
92
+ // phases 必填数组,元素为 string | {title:string, detail?:string}
93
+ if (!Array.isArray(o.phases)) return null;
94
+ const phases: WorkflowMeta["phases"] = [];
95
+ for (const p of o.phases) {
96
+ if (isString(p)) {
97
+ phases.push(p);
98
+ } else if (isPlainObject(p) && isNonEmptyString(p.title)) {
99
+ // minor-4:detail 存在但非字符串 → reject(不再静默丢弃)
100
+ if (p.detail !== undefined && !isString(p.detail)) return null;
101
+ phases.push(isString(p.detail) ? { title: p.title, detail: p.detail } : { title: p.title });
102
+ } else {
103
+ return null;
104
+ }
105
+ }
106
+ // minor-5:parameters 存在但非 plain object → reject(不再静默当 undefined)
107
+ const parameters = o.parameters;
108
+ if (parameters !== undefined && !isPlainObject(parameters)) return null;
109
+ const usage = isString(o.usage) ? o.usage : undefined;
110
+
111
+ const meta: WorkflowMeta = {
112
+ kind: "workflow",
113
+ name: o.name,
114
+ description: o.description,
115
+ phases,
116
+ ...(parameters !== undefined ? { parameters: parameters as Record<string, unknown> } : {}),
117
+ ...(usage !== undefined ? { usage } : {}),
118
+ ...(when !== undefined ? { when } : {}),
119
+ ...(notFor !== undefined ? { notFor } : {}),
120
+ };
121
+ return meta;
122
+ }
123
+
124
+ // kind === "agent"
125
+ // minor-2:workflow 专属字段不可出现在 agent(串类 reject)
126
+ if (o.phases !== undefined || o.parameters !== undefined || o.usage !== undefined) return null;
127
+ let examples: RoutingExample[] | undefined;
128
+ if (o.examples !== undefined) {
129
+ if (!Array.isArray(o.examples)) return null;
130
+ const exs: RoutingExample[] = [];
131
+ for (const e of o.examples) {
132
+ if (
133
+ isPlainObject(e) &&
134
+ isString(e.match) &&
135
+ isString(e.action) &&
136
+ typeof e.positive === "boolean"
137
+ ) {
138
+ exs.push({ match: e.match, action: e.action, positive: e.positive });
139
+ } else {
140
+ return null;
141
+ }
142
+ }
143
+ examples = exs;
144
+ }
145
+ let tools: string[] | undefined;
146
+ if (o.tools !== undefined) {
147
+ if (Array.isArray(o.tools)) {
148
+ if (!o.tools.every(isString)) return null;
149
+ tools = o.tools as string[];
150
+ } else if (isString(o.tools)) {
151
+ // 兼容 agent .md 的逗号分隔字符串约定(如 `tools: read, bash, grep`)
152
+ const parts = o.tools.split(",").map((s) => s.trim()).filter(Boolean);
153
+ tools = parts.length > 0 ? parts : undefined;
154
+ } else {
155
+ return null;
156
+ }
157
+ }
158
+ const model = isString(o.model) ? o.model : undefined;
159
+
160
+ const meta: AgentMeta = {
161
+ kind: "agent",
162
+ name: o.name,
163
+ description: o.description,
164
+ ...(examples !== undefined ? { examples } : {}),
165
+ ...(tools !== undefined ? { tools } : {}),
166
+ ...(model !== undefined ? { model } : {}),
167
+ ...(when !== undefined ? { when } : {}),
168
+ ...(notFor !== undefined ? { notFor } : {}),
169
+ };
170
+ return meta;
171
+ }
172
+
173
+ // ── IF1: parseResourceMeta(discovery,fail-safe null)─────────────────
174
+
175
+ /**
176
+ * 统一 meta 解析入口(discovery 用)。仅认新格式,无 legacy fallback。
177
+ * 任何失败(缺块 / YAML 语法错 / 类型校验失败)→ return null(不抛)。
178
+ * discovery fail-safe:单文件解析失败仅让该资源 available=false,不阻塞其他资源。
179
+ */
180
+ export function parseResourceMeta(
181
+ content: string,
182
+ kind: ResourceKind,
183
+ ): ResourceMeta | null {
184
+ const block = extractBlock(content, kind);
185
+ if (block === undefined) return null;
186
+ try {
187
+ const raw = parseYaml(block);
188
+ return typecheckMeta(raw, kind);
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
193
+
194
+ /** 从 eemeli/yaml 拋错提取 linePos 起点(类型守卫,避免 unsafe cast)。[P-yaml] 实测 e.linePos = [{line,col},{line,col}]。*/
195
+ function getYamlLinePos(e: unknown): { line: number; col: number } | undefined {
196
+ if (e !== null && typeof e === "object" && "linePos" in e) {
197
+ const lp = (e as Record<string, unknown>).linePos;
198
+ if (Array.isArray(lp) && lp.length > 0) {
199
+ const first = lp[0];
200
+ if (first !== null && first !== undefined && typeof first === "object"
201
+ && "line" in first && "col" in first) {
202
+ const f = first as Record<string, unknown>;
203
+ if (typeof f["line"] === "number" && typeof f["col"] === "number") {
204
+ return { line: f["line"], col: f["col"] };
205
+ }
206
+ }
207
+ }
208
+ }
209
+ return undefined;
210
+ }
211
+
212
+ // ── IF2: parseResourceMetaDetailed(generate 闭环,返 linePos)─────────
213
+
214
+ export type DetailedResult =
215
+ | { ok: true; meta: ResourceMeta }
216
+ | { ok: false; error: string; linePos?: { line: number; col: number } };
217
+
218
+ /**
219
+ * generate 闭环专用。失败时返回 error + linePos(取自 eemeli/yaml YAMLParseError.linePos[0]),
220
+ * 供 actionGenerate 报「生成 YAML 错在 line X col Y」给 LLM 自纠正(ERR4)。
221
+ * discovery 不用此(保持 fail-safe null)。
222
+ *
223
+ * [P-yaml] 探针实测:e.linePos 是 [{line,col},{line,col}](start+end),取 [0] 作起点。
224
+ *
225
+ * minor-6:区分「未找到块」(undefined) 与「块为空/非法」(""),给可操作错误。
226
+ * minor-8:typecheckMeta 包进 try(与 IF1 对称,防御 typecheck 未来抛错)。
227
+ */
228
+ export function parseResourceMetaDetailed(
229
+ content: string,
230
+ kind: ResourceKind,
231
+ ): DetailedResult {
232
+ const block = extractBlock(content, kind);
233
+ if (block === undefined) {
234
+ return { ok: false, error: "未找到 meta 块(缺少 /* @pi-meta */ 或 frontmatter,或闭合 */ 不在行首)" };
235
+ }
236
+ let raw: unknown;
237
+ try {
238
+ raw = parseYaml(block);
239
+ } catch (e) {
240
+ // eemeli/yaml YAMLParseError:e.linePos = [{line,col},{line,col}]([P-yaml] 实测)
241
+ const linePos = getYamlLinePos(e);
242
+ return {
243
+ ok: false,
244
+ error: e instanceof Error ? e.message.split("\n")[0] : String(e),
245
+ ...(linePos !== undefined ? { linePos } : {}),
246
+ };
247
+ }
248
+ try {
249
+ const meta = typecheckMeta(raw, kind);
250
+ if (!meta) {
251
+ return { ok: false, error: "meta 类型校验失败(缺 name/description、phases 非法、kind 字段串类或可选字段类型错)" };
252
+ }
253
+ return { ok: true, meta };
254
+ } catch (e) {
255
+ return { ok: false, error: `meta 类型校验异常: ${e instanceof Error ? e.message : String(e)}` };
256
+ }
257
+ }
@@ -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 {
@@ -67,6 +67,61 @@ function isDirectChildOfWorkspaceRoot(dir: string, workspaceRoot: string): boole
67
67
  * bare+worktree 优先找 .bare;普通 repo 找最顶层 .git;fallback 找 .pi。
68
68
  * 与 config-loader 原有逻辑一致(合并后提取为共享函数)。
69
69
  */
70
+ // ── 统一 mtime 缓存层(m5 IF10)────────────────────────────────────
71
+ //
72
+ // 模块级 Map<path, { mtimeMs, content }>:mtime 判变缓存文件内容。
73
+ // - sync 实现(statSync/readFileSync):agent-registry discoverAll 是同步路径,
74
+ // async 缓存无法被 await(m5 design-review A1 探针实证约束)
75
+ // - stat 失败/ENOENT → 驱逐条目(mtime 缓存下文件删除不自愈——A3 修复)
76
+ // - 已知局限(C3 记录):内容变 mtime 未变(cp -p/rsync -t 保留源 mtime、
77
+ // 2s 粒度文件系统)→ 漏判,invalidateCache/clearFileCache 兜底;
78
+ // APFS mtimeMs 微秒级浮点 === 判变可靠(探针 P-mtime-精度)
79
+
80
+ interface MtimeCacheEntry {
81
+ mtimeMs: number;
82
+ content: string;
83
+ }
84
+
85
+ const mtimeCache = new Map<string, MtimeCacheEntry>();
86
+
87
+ /**
88
+ * stat + content 统一缓存。文件不存在/不可读 → null(并驱逐条目)。
89
+ * mtime 未变 → 返回缓存 content(不重 read);变 → readFileSync + 缓存。
90
+ */
91
+ export function getCachedFile(filePath: string): { mtimeMs: number; content: string } | null {
92
+ let mtimeMs: number;
93
+ try {
94
+ mtimeMs = fsSync.statSync(filePath).mtimeMs;
95
+ } catch {
96
+ mtimeCache.delete(filePath);
97
+ return null;
98
+ }
99
+ const entry = mtimeCache.get(filePath);
100
+ if (entry && entry.mtimeMs === mtimeMs) return entry;
101
+ let content: string;
102
+ try {
103
+ content = fsSync.readFileSync(filePath, "utf-8");
104
+ } catch {
105
+ // stat 与 read 之间的删除/EACCES 竞态 → 驱逐并返回 null(exec-review major-2:
106
+ // docstring 承诺「不可读 → null」——readFileSync 也必须入守卫)
107
+ mtimeCache.delete(filePath);
108
+ return null;
109
+ }
110
+ const cached = { mtimeMs, content };
111
+ mtimeCache.set(filePath, cached);
112
+ return cached;
113
+ }
114
+
115
+ /** 便捷封装:只取 content(不存在 → null)。 */
116
+ export function getCachedFileContent(filePath: string): string | null {
117
+ return getCachedFile(filePath)?.content ?? null;
118
+ }
119
+
120
+ /** 清空(invalidateCache 语义——测试隔离 + mtime 漏判场景手动刷新兜底)。 */
121
+ export function clearFileCache(): void {
122
+ mtimeCache.clear();
123
+ }
124
+
70
125
  export function findWorkspaceRoot(cwd?: string): string {
71
126
  const dir = cwd ?? process.cwd();
72
127
  const root = resolve("/");
@@ -288,6 +343,24 @@ interface ScanTarget {
288
343
  enabled: boolean;
289
344
  }
290
345
 
346
+ /**
347
+ * 读取 XYZ_EXTENSION_PATHS 环境变量(dev-link 写入的扩展源码路径)。
348
+ *
349
+ * delimiter 分隔(POSIX ':' / Windows ';'),trim + 过滤空 + ~ 展开。
350
+ * 每个路径是一个 extension 包目录(dev-link 指向源码),走 processPackage 发现其
351
+ * agents/workflows。解析逻辑与 extension-service.getUserExtensionPaths() 一致。
352
+ */
353
+ function readExtensionPaths(): string[] {
354
+ const raw = process.env.XYZ_EXTENSION_PATHS;
355
+ if (!raw) return [];
356
+ const paths = raw
357
+ .split(delimiter)
358
+ .map((p) => p.trim())
359
+ .filter((p) => p.length > 0)
360
+ .map((p) => (p.startsWith("~") ? join(homedir(), p.slice(1)) : p));
361
+ return [...new Set(paths)];
362
+ }
363
+
291
364
  /**
292
365
  * 构建所有扫描源(按优先级低→高排列)。
293
366
  *
@@ -306,6 +379,10 @@ function buildScanTargets(config: ScanConfig): ScanTarget[] {
306
379
  { dir: join(agentDir, "npm", "node_modules"), source: "npm", enabled: true },
307
380
  // 4. npm dev symlink: agentDir/extensions/*/<pkg>/
308
381
  { dir: join(agentDir, "extensions"), source: "npm-dev", enabled: true },
382
+ // user extension paths (XYZ_EXTENSION_PATHS, dev-link): each path is a package dir,
383
+ // 走 processPackage 读 pi.{kind} manifest 或扫 {kind}/ 目录。优先级高于 npm/npm-dev
384
+ // (dev-link 是开发版 override),低于 project(项目正式资源优先)。
385
+ ...readExtensionPaths().map((dir) => ({ dir, source: "user-extension-paths" as const, enabled: true })),
309
386
  // 5. project .pi/{kind}/
310
387
  { dir: join(workspaceRoot, ".pi", kind), source: "project-pi", enabled: true },
311
388
  ];
@@ -354,6 +431,11 @@ export async function discoverResources(config: ScanConfig): Promise<DiscoveredR
354
431
  // 覆盖 source 标签(scanNpmDir 内部统一标 "npm",这里修正为实际源)
355
432
  const tagged = resources.map((r) => ({ ...r, source: target.source }));
356
433
  allBySource.push({ source: target.source, resources: tagged });
434
+ } else if (target.source === "user-extension-paths") {
435
+ // XYZ_EXTENSION_PATHS(dev-link):每个 dir 是单个包目录,走 processPackage
436
+ const resources = await processPackage(target.dir, config.kind);
437
+ const tagged = resources.map((r) => ({ ...r, source: target.source }));
438
+ allBySource.push({ source: target.source, resources: tagged });
357
439
  } else {
358
440
  // 普通目录:直接扫
359
441
  const files = await scanDirectory(target.dir, config.kind);
@@ -517,6 +599,10 @@ export function discoverResourcesSync(config: ScanConfig): DiscoveredResource[]
517
599
  if (target.source === "npm" || target.source === "npm-dev") {
518
600
  const resources = scanNpmDirSync(target.dir, config.kind);
519
601
  all.push(...resources.map((r) => ({ ...r, source: target.source })));
602
+ } else if (target.source === "user-extension-paths") {
603
+ // XYZ_EXTENSION_PATHS(dev-link):每个 dir 是单个包目录
604
+ const resources = processPackageSync(target.dir, config.kind);
605
+ all.push(...resources.map((r) => ({ ...r, source: target.source })));
520
606
  } else {
521
607
  const files = scanDirectorySync(target.dir, config.kind);
522
608
  all.push(...files.map((f) => ({ path: f, source: target.source, available: true })));
@@ -0,0 +1,60 @@
1
+ /**
2
+ * ResourceMeta — 资源元数据统一类型族(v5 §4.1 / DM1)
3
+ *
4
+ * workflow 与 agent 两类资源共用同一数据格式(YAML),仅 kind 判别 + 专属字段不同。
5
+ * skills 显式 out-of-scope(归 pi core)。
6
+ *
7
+ * 设计:
8
+ * - 整对象透传——config-loader.toCachedMeta / registry-impl.toScript 改为 meta: parsedMeta
9
+ * 不再 {name,description,phases} 解构重建(消灭 3 重映射丢字段 bug)。
10
+ * - workflow 的 parameters(JSON Schema)由 args-validator(m3)按 schema 校验 args;
11
+ * usage(markdown)覆盖 schema 表达不了的语义约束 + 真实合法示例命令。
12
+ * - agent 的 tools/model 供 AgentRegistry 执行侧 spawn 子进程用,不进 system prompt 注入段。
13
+ *
14
+ * 层归属:shared(L1 统一资源模型)。
15
+ */
16
+
17
+ /** 资源种类。skills 归 pi core,本 extension 仅 workflow + agent。 */
18
+ export type ResourceKind = "workflow" | "agent";
19
+
20
+ /** 两类资源共有的路由字段。 */
21
+ export interface ResourceMetaBase {
22
+ kind: ResourceKind;
23
+ /** 路由用一句话(受 SSOT lint W1/W2 约束:≤200 字符、不含参数引用语法)。 */
24
+ name: string;
25
+ description: string;
26
+ /** "Use when ..." 正向路由提示(PromptBudget 最后保)。 */
27
+ when?: string;
28
+ /** "Not for ..." 负向路由提示。 */
29
+ notFor?: string;
30
+ }
31
+
32
+ /** workflow 专属:phases + 参数契约 + 语义说明。 */
33
+ export interface WorkflowMeta extends ResourceMetaBase {
34
+ kind: "workflow";
35
+ phases: (string | { title: string; detail?: string })[];
36
+ /** 参数契约(JSON Schema draft-07)。未声明则 $ARGS 透传不校验(向后兼容)。 */
37
+ parameters?: Record<string, unknown>;
38
+ /** markdown,覆盖 schema 表达不了的语义约束 + 真实合法示例命令。 */
39
+ usage?: string;
40
+ }
41
+
42
+ /** agent 路由样本(结构化,非嵌入 description 字符串)。 */
43
+ export interface RoutingExample {
44
+ match: string;
45
+ action: string;
46
+ /** true=应触发,false=不应触发(正反各一)。 */
47
+ positive: boolean;
48
+ }
49
+
50
+ /** agent 专属:路由样本 + 执行配置。 */
51
+ export interface AgentMeta extends ResourceMetaBase {
52
+ kind: "agent";
53
+ examples?: RoutingExample[];
54
+ /** 供 AgentRegistry 执行侧 spawn 时注入,不进 system prompt 注入段。 */
55
+ tools?: string[];
56
+ model?: string;
57
+ }
58
+
59
+ /** 判别联合(kind 判别)。 */
60
+ export type ResourceMeta = WorkflowMeta | AgentMeta;
@@ -56,7 +56,7 @@ workflow run map-reduce --args itemsJson=/path/to/items.json --args operation=".
56
56
 
57
57
  ```
58
58
  workflow run review-fix-loop --args targetType=git-diff target=main \
59
- --args batch1=fallow-scan --args batch2=reviewer --args autoCommit=true
59
+ --args batch1=fallow-scan --args batch2=code-reviewer --args autoCommit=true
60
60
  workflow run review-fix-loop --args targetType=file target=/path/to/doc.md \
61
61
  --args batch1=doc-reviewer
62
62
  ```
@@ -64,7 +64,7 @@ workflow run review-fix-loop --args targetType=file target=/path/to/doc.md \
64
64
  - `targetType` 枚举:`git-diff`(target=base ref)/ `file`(target=路径)/ `dir`(target=目录)/ `text`(target=自由描述)
65
65
  - `batch1..batchN`:批串行,批内并行 review → aggregate → fix → 重审直到 clean;批次用于前置依赖(如 `fallow-scan` 静态分析先行,后续审查才有意义)
66
66
  - 批内某 agent 无 must-fix 后后续轮跳过(`skipCleanAgents` 默认 true + `recheckAfterFix` 默认 false):clean agent 下轮跳过不重派;显式传 `recheckAfterFix=true` 启用强回归模式——fix 后重派全批,clean agent 走限定 prompt(只审 modifiedFiles ∪ 自检关联点,不诱导全量重扫)
67
- - agent 项支持:AgentRegistry 名(如 `reviewer`)/ 自定义 .md 文件路径(如 `batch1=/path/to/reviewer.md`)/ 内置 `fallow-scan` / **内置 `doc-reviewer`**(文档场景推荐:`targetType=file/dir` + `batch1=doc-reviewer`,四遍审查方法论:事实锚点核实/逻辑断言验证/落地清单完备性/边界与迁移;无 write 工具,报告经 schema 返回由 workflow 落盘)
67
+ - agent 项支持:AgentRegistry 名(如 `code-reviewer`)/ 自定义 .md 文件路径(如 `batch1=/path/to/code-reviewer.md`)/ 内置 `fallow-scan` / **内置 `doc-reviewer`**(文档场景推荐:`targetType=file/dir` + `batch1=doc-reviewer`,四遍审查方法论:事实锚点核实/逻辑断言验证/落地清单完备性/边界与迁移;无 write 工具,报告经 schema 返回由 workflow 落盘)
68
68
  - `fixAgent`(可选):fix 阶段加载指定 agent(内置名或 .md 路径);代码场景可在该 agent.md 内写 verify 命令(typecheck/test 实测)当轮拦截编译类回归。⚠️ agent.md 内写的 verify 命令**必须确认能在目标项目可运行**(target 的包管理器/目录结构未知),否则命令失败会误报 fix 状态
69
69
  - `maxFixAttempts`(可选,默认 2):needs-redesign 阈值。问题经 maxFixAttempts 次修复仍未收敛(regressed)→ 终止该批,terminated="needs-redesign"(结构性问题需人工介入,非继续补丁能解决)
70
70
  - `convergeNewIssues`(可选,默认 1)+ `convergeRounds`(可选,默认 2):新发现率收敛阈值。连续 convergeRounds 轮新发现问题 ≤ convergeNewIssues **且**无 open/regressed 活跃条目 → terminated="converged"(推进下一批)。收敛不等于问题全清——需同时满足无活跃条目才终止
@@ -0,0 +1,40 @@
1
+ // workflows/_shared/agent-refs.cjs
2
+ //
3
+ // 编排 workflow 共用的 agentRef 参数解析(S4 路径统一)。
4
+ //
5
+ // agentRef = .md 绝对路径(注入段 <available_subagents> 的 <location>)。
6
+ // agents 参数 = 逗号分隔的 agentRef 列表,按阶段顺序一一映射。
7
+ // 缺省(未传/空)= 不指定 agent(默认执行者)——编排骨架不强制指定执行者。
8
+ //
9
+ // 注意:本模块只做「字符串 → 路径数组」的纯解析,不读文件。agent 内容的加载
10
+ // 由主线程 resolveAgentOpts(agent-call 时按路径读 + systemPrompt 注入)统一完成。
11
+ "use strict";
12
+
13
+ /**
14
+ * 解析 agents 参数为 agentRef 路径数组。
15
+ *
16
+ * @param raw 逗号分隔字符串(如 "/a/x.md,/b/y.md");undefined/null/空 → []
17
+ * @returns 路径数组(trim + 去空项)
18
+ */
19
+ function parseAgentRefs(raw) {
20
+ if (raw === undefined || raw === null) return [];
21
+ if (typeof raw !== "string") return [];
22
+ const refs = raw
23
+ .split(",")
24
+ .map((s) => s.trim())
25
+ .filter(Boolean);
26
+ return refs;
27
+ }
28
+
29
+ /**
30
+ * 按阶段索引取 agentRef(缺省 undefined = 不指定 agent)。
31
+ *
32
+ * @param refs parseAgentRefs 结果
33
+ * @param index 阶段索引(0-based)
34
+ * @returns agentRef 或 undefined
35
+ */
36
+ function agentRefAt(refs, index) {
37
+ return refs.length > index ? refs[index] : undefined;
38
+ }
39
+
40
+ module.exports = { parseAgentRefs, agentRefAt };
@@ -11,11 +11,22 @@
11
11
  // - 禁止 bare IIFE(用 top-level await)
12
12
  // - 禁止用 result 作变量名
13
13
 
14
- const meta = {
15
- name: "chain",
16
- description: "通用编排:analyze → transform → synthesize 顺序三步链",
17
- phases: ["analyze", "transform", "synthesize"],
18
- };
14
+ /* @pi-meta
15
+ name: chain
16
+ description: 通用编排:analyze → transform → synthesize 顺序三步链
17
+ phases: [analyze, transform, synthesize]
18
+ parameters:
19
+ type: object
20
+ properties:
21
+ task: { type: string, minLength: 1 }
22
+ agents: { type: string }
23
+ required: [task]
24
+ usage: |
25
+ ## 使用说明
26
+ - 顺序三步链:analyze(提取要点)→ transform(产出方案)→ synthesize(合成结论)
27
+ - agents:逗号分隔的 agent .md 绝对路径,按顺序对应三步(少于 3 个尾部用默认执行者)
28
+ - 示例:workflow run chain --args task="<任务描述>" agents="/path/analyzer.md,/path/planner.md"
29
+ */
19
30
 
20
31
  // ── 入参($ARGS)──────────────────────────────────────────────────
21
32
  const task = $ARGS.task;
@@ -23,7 +34,17 @@ if (typeof task !== "string" || task.trim() === "") {
23
34
  throw new Error("chain 缺少必需参数 task(非空字符串)。用法:workflow run chain --args task=\"<描述>\"");
24
35
  }
25
36
 
26
- log("chain 开始,task=" + task);
37
+ // S4:agents 参数 = 逗号分隔的 agentRef 路径数组,按阶段顺序对应三步
38
+ // worker 沙箱为 eval 模式:require 相对路径以 cwd 为基准(非脚本目录),
39
+ // 必须用 workerData.scriptPath 锚定脚本目录(review-fix-loop 同模式)。
40
+ const SCRIPT_DIR = workerData && workerData.scriptPath
41
+ ? require("path").dirname(workerData.scriptPath)
42
+ : process.cwd();
43
+ const { parseAgentRefs, agentRefAt } = require(SCRIPT_DIR + "/_shared/agent-refs.cjs");
44
+ const agentRefs = parseAgentRefs($ARGS.agents);
45
+ const stepAgent = (i) => { const ref = agentRefAt(agentRefs, i); return ref ? { agent: ref } : {}; };
46
+
47
+ log("chain 开始,task=" + task + (agentRefs.length ? ",agents=" + agentRefs.join(",") : ""));
27
48
 
28
49
  let currentPhase = "init";
29
50
  let outcome;
@@ -47,6 +68,7 @@ try {
47
68
  required: ["insights", "keyPoints"],
48
69
  },
49
70
  description: "chain-analyze",
71
+ ...stepAgent(0),
50
72
  });
51
73
 
52
74
  // ── 段 2:transform(基于分析产出方案)───────────────────────────
@@ -69,6 +91,7 @@ try {
69
91
  required: ["plan", "actions"],
70
92
  },
71
93
  description: "chain-transform",
94
+ ...stepAgent(1),
72
95
  });
73
96
 
74
97
  // ── 段 3:synthesize(综合方案输出最终结论)─────────────────────
@@ -87,6 +110,7 @@ try {
87
110
  required: ["summary", "recommendation"],
88
111
  },
89
112
  description: "chain-synthesize",
113
+ ...stepAgent(2),
90
114
  });
91
115
 
92
116
  outcome = {
@@ -13,11 +13,27 @@
13
13
  //
14
14
  // ⚠️ lintScript 约束(本脚本已遵守):含 parallel() 入口(兼 agent 嵌套),禁止 bare IIFE
15
15
 
16
- const meta = {
17
- name: "map-reduce",
18
- description: "通用编排:parallel map → reduce 两段,处理已知 items 数组",
19
- phases: ["map", "reduce"],
20
- };
16
+ /* @pi-meta
17
+ name: map-reduce
18
+ description: 通用编排:parallel map → reduce 两段,处理已知 items 数组
19
+ phases: [map, reduce]
20
+ parameters:
21
+ type: object
22
+ properties:
23
+ items: { type: array, items: { type: string } }
24
+ itemsJson: { type: string }
25
+ operation: { type: string }
26
+ agents: { type: string }
27
+ required: [operation]
28
+ oneOf:
29
+ - required: [items]
30
+ - required: [itemsJson]
31
+ usage: |
32
+ ## 使用说明
33
+ - items 与 itemsJson 至少一个:items 直接传字符串数组,itemsJson 传文件路径(内容为 JSON 数组)
34
+ - agents:逗号分隔的 agent .md 绝对路径,按顺序对应 map/reduce 两段
35
+ - 示例:workflow run map-reduce --args operation="<对每个 item 做什么>" itemsJson="/path/items.json" agents="/path/mapper.md,/path/reducer.md"
36
+ */
21
37
 
22
38
  const fs = require("fs");
23
39
 
@@ -50,6 +66,16 @@ if (!Array.isArray(items) || items.length === 0) {
50
66
 
51
67
  log("map-reduce 开始,items=" + items.length + " 个,operation=" + operation);
52
68
 
69
+ // S4:agents 参数 = 逗号分隔的 agentRef 路径数组,按顺序对应 map/reduce 两段
70
+ // worker 沙箱为 eval 模式:require 相对路径以 cwd 为基准(非脚本目录),
71
+ // 必须用 workerData.scriptPath 锚定脚本目录(review-fix-loop 同模式)。
72
+ const SCRIPT_DIR = workerData && workerData.scriptPath
73
+ ? require("path").dirname(workerData.scriptPath)
74
+ : process.cwd();
75
+ const { parseAgentRefs, agentRefAt } = require(SCRIPT_DIR + "/_shared/agent-refs.cjs");
76
+ const agentRefs = parseAgentRefs($ARGS.agents);
77
+ const stepAgent = (i) => { const ref = agentRefAt(agentRefs, i); return ref ? { agent: ref } : {}; };
78
+
53
79
  let currentPhase = "init";
54
80
  let outcome;
55
81
 
@@ -74,6 +100,7 @@ try {
74
100
  required: ["itemIndex", "mapped"],
75
101
  },
76
102
  description: "map-reduce-map-" + idx,
103
+ ...stepAgent(0),
77
104
  })
78
105
  ),
79
106
  );
@@ -121,6 +148,7 @@ try {
121
148
  required: ["reduced", "stats"],
122
149
  },
123
150
  description: "map-reduce-reduce",
151
+ ...stepAgent(1),
124
152
  });
125
153
 
126
154
  outcome = {