@zhushanwen/pi-subagent-workflow 0.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.
Files changed (143) hide show
  1. package/agents/context-builder.md +17 -0
  2. package/agents/general-purpose.md +16 -0
  3. package/agents/oracle.md +17 -0
  4. package/agents/planner.md +17 -0
  5. package/agents/researcher.md +17 -0
  6. package/agents/reviewer.md +17 -0
  7. package/agents/scout.md +17 -0
  8. package/agents/worker.md +16 -0
  9. package/examples/README.md +43 -0
  10. package/examples/chain.example.js +92 -0
  11. package/examples/map-reduce.example.js +99 -0
  12. package/examples/parallel.example.js +82 -0
  13. package/examples/scatter-gather.example.js +106 -0
  14. package/index.ts +1 -0
  15. package/package.json +66 -0
  16. package/skills/workflow-script-format/SKILL.md +328 -0
  17. package/src/execution/__tests__/agent-registry.test.ts +164 -0
  18. package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
  19. package/src/execution/__tests__/alive-store.test.ts +147 -0
  20. package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
  21. package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
  22. package/src/execution/__tests__/config.test.ts +110 -0
  23. package/src/execution/__tests__/crash-recovery.test.ts +311 -0
  24. package/src/execution/__tests__/execute-nesting.test.ts +359 -0
  25. package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
  26. package/src/execution/__tests__/execution-record.test.ts +959 -0
  27. package/src/execution/__tests__/finalized-marker.test.ts +82 -0
  28. package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
  29. package/src/execution/__tests__/format.test.ts +320 -0
  30. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  31. package/src/execution/__tests__/list-component.test.ts +347 -0
  32. package/src/execution/__tests__/model-resolver.test.ts +356 -0
  33. package/src/execution/__tests__/output-collector.test.ts +61 -0
  34. package/src/execution/__tests__/path-encoding.test.ts +75 -0
  35. package/src/execution/__tests__/pi-invocation.test.ts +73 -0
  36. package/src/execution/__tests__/record-store.test.ts +545 -0
  37. package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
  38. package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
  39. package/src/execution/__tests__/sdk-contract.test.ts +272 -0
  40. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  41. package/src/execution/__tests__/session-file-gc.test.ts +247 -0
  42. package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
  43. package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
  44. package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
  45. package/src/execution/__tests__/spawn-args.test.ts +244 -0
  46. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  47. package/src/execution/__tests__/subagent-service.test.ts +678 -0
  48. package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
  49. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  50. package/src/execution/__tests__/timeout-integration.test.ts +381 -0
  51. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  52. package/src/execution/__tests__/tool-action.test.ts +330 -0
  53. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  54. package/src/execution/__tests__/worktree-manager.test.ts +423 -0
  55. package/src/execution/__tests__/worktree-registry.test.ts +161 -0
  56. package/src/execution/agent-registry.ts +252 -0
  57. package/src/execution/agent-result-mapper.ts +84 -0
  58. package/src/execution/alive-store.ts +92 -0
  59. package/src/execution/best-effort.ts +30 -0
  60. package/src/execution/concurrency-pool.ts +84 -0
  61. package/src/execution/config.ts +73 -0
  62. package/src/execution/execute-options-mapper.ts +86 -0
  63. package/src/execution/execution-record.ts +778 -0
  64. package/src/execution/finalized-marker.ts +51 -0
  65. package/src/execution/model-config-service.ts +225 -0
  66. package/src/execution/model-resolver.ts +247 -0
  67. package/src/execution/notifier.ts +168 -0
  68. package/src/execution/output-collector.ts +88 -0
  69. package/src/execution/path-encoding.ts +34 -0
  70. package/src/execution/pi-invocation.ts +70 -0
  71. package/src/execution/record-store.ts +350 -0
  72. package/src/execution/session-context-resolver.ts +64 -0
  73. package/src/execution/session-file-gc.ts +98 -0
  74. package/src/execution/session-reconstructor.ts +450 -0
  75. package/src/execution/session-runner.ts +725 -0
  76. package/src/execution/spawn-event-adapter.ts +150 -0
  77. package/src/execution/subagent-service.ts +973 -0
  78. package/src/execution/subprocess-agent-runner.ts +108 -0
  79. package/src/execution/temp-prompt.ts +57 -0
  80. package/src/execution/tombstone-store.ts +72 -0
  81. package/src/execution/turn-limiter.ts +88 -0
  82. package/src/execution/types.ts +634 -0
  83. package/src/execution/worktree-manager.ts +285 -0
  84. package/src/execution/worktree-registry.ts +144 -0
  85. package/src/index.ts +454 -0
  86. package/src/interface/bg-notify-render.ts +286 -0
  87. package/src/interface/commands.ts +157 -0
  88. package/src/interface/format.ts +501 -0
  89. package/src/interface/gui-adapter.ts +136 -0
  90. package/src/interface/helpers.ts +110 -0
  91. package/src/interface/list-component.ts +643 -0
  92. package/src/interface/list-shared.ts +84 -0
  93. package/src/interface/list-view.ts +373 -0
  94. package/src/interface/reentry-guard.ts +30 -0
  95. package/src/interface/subagent-actions.ts +294 -0
  96. package/src/interface/subagent-tool.ts +294 -0
  97. package/src/interface/subagents.ts +30 -0
  98. package/src/interface/tool-render.ts +333 -0
  99. package/src/interface/tool-workflow-script.ts +351 -0
  100. package/src/interface/tool-workflow.ts +485 -0
  101. package/src/interface/views/WorkflowsView.ts +944 -0
  102. package/src/interface/views/detail-content.ts +298 -0
  103. package/src/interface/views/format.ts +320 -0
  104. package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
  105. package/src/orchestration/__tests__/config-loader.test.ts +381 -0
  106. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
  107. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  108. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
  109. package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
  110. package/src/orchestration/__tests__/script-lint.test.ts +347 -0
  111. package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
  112. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
  113. package/src/orchestration/agent-opts-resolver.ts +128 -0
  114. package/src/orchestration/concurrency-gate.ts +69 -0
  115. package/src/orchestration/config-loader.ts +313 -0
  116. package/src/orchestration/error-recovery.ts +578 -0
  117. package/src/orchestration/execute-agent-call.ts +174 -0
  118. package/src/orchestration/jsonl-run-store.ts +292 -0
  119. package/src/orchestration/launcher.ts +368 -0
  120. package/src/orchestration/lifecycle.ts +373 -0
  121. package/src/orchestration/models/__tests__/budget.test.ts +367 -0
  122. package/src/orchestration/models/agent-call.ts +76 -0
  123. package/src/orchestration/models/budget.ts +148 -0
  124. package/src/orchestration/models/ports.ts +165 -0
  125. package/src/orchestration/models/run-runtime.ts +91 -0
  126. package/src/orchestration/models/run-spec.ts +54 -0
  127. package/src/orchestration/models/run-state.ts +44 -0
  128. package/src/orchestration/models/trace.ts +102 -0
  129. package/src/orchestration/models/types.ts +242 -0
  130. package/src/orchestration/models/workflow-run.ts +275 -0
  131. package/src/orchestration/models/workflow-script-registry.ts +32 -0
  132. package/src/orchestration/models/workflow-script.ts +90 -0
  133. package/src/orchestration/node-ops.ts +192 -0
  134. package/src/orchestration/script-lint.ts +387 -0
  135. package/src/orchestration/skill-discovery.ts +60 -0
  136. package/src/orchestration/worker-handle.ts +115 -0
  137. package/src/orchestration/worker-host.ts +93 -0
  138. package/src/orchestration/worker-script-builder.ts +281 -0
  139. package/src/orchestration/workflow-files.ts +85 -0
  140. package/src/orchestration/workflow-script-registry-impl.ts +128 -0
  141. package/src/shared/__tests__/resource-discovery.test.ts +226 -0
  142. package/src/shared/agent-event.ts +13 -0
  143. package/src/shared/resource-discovery.ts +535 -0
@@ -0,0 +1,535 @@
1
+ // src/shared/resource-discovery.ts
2
+ //
3
+ // 统一资源发现模块——agent .md 与 workflow .js/.mjs 共享同一套扫描逻辑。
4
+ //
5
+ // 设计原则(ADR-031 统一资源发现):
6
+ // 1. 扫描源前缀统一:user/project 级目录用相同前缀,末级目录名(agents/workflows)参数化
7
+ // 2. 路径动态获取:user 级用 getAgentDir()(尊重 PI_CODING_AGENT_DIR),project 级用 findWorkspaceRoot(cwd)
8
+ // 3. npm/dev 包内发现:有 manifest(pi.agents/pi.workflows)只走 manifest,无 manifest 扫约定目录
9
+ // 4. manifest 路径存在性校验:声明的路径不存在 → 该包发现失败,不 fallback
10
+ // 5. 废弃 discovery.json:扫描路径完全由代码内推导,无外部依赖
11
+ //
12
+ // 优先级(低→高):user .pi/agent → user .agents → npm global → npm dev → project .pi → project .pi/.tmp(仅workflow) → project .agents
13
+
14
+ import * as fsSync from "node:fs";
15
+ import { access, readdir, readFile, stat } from "node:fs/promises";
16
+ import { homedir } from "node:os";
17
+ import { join,resolve } from "node:path";
18
+
19
+ // ── 类型 ─────────────────────────────────────────────────────
20
+
21
+ /** 资源种类:agent 或 workflow */
22
+ export type ResourceKind = "agents" | "workflows";
23
+
24
+ /** 发现到的单个资源文件(原始数据,由调用方解析 frontmatter/meta) */
25
+ export interface DiscoveredResource {
26
+ /** 绝对路径 */
27
+ path: string;
28
+ /** 来源层级 */
29
+ source: ResourceSource;
30
+ /** 是否可用(manifest 校验失败的包整体标 false) */
31
+ available: boolean;
32
+ }
33
+
34
+ /** 资源来源层级 */
35
+ export type ResourceSource = "user-pi" | "user-agents" | "npm" | "npm-dev" | "project-pi" | "project-pi-tmp" | "project-agents";
36
+
37
+ /** 扫描配置 */
38
+ export interface ScanConfig {
39
+ /** 资源种类 */
40
+ kind: ResourceKind;
41
+ /** 项目根目录(findWorkspaceRoot 推导结果) */
42
+ workspaceRoot: string;
43
+ /** agent 配置目录(getAgentDir() 结果) */
44
+ agentDir: string;
45
+ /** 是否包含 tmp 源(仅 workflow 用 .pi/workflows/.tmp/) */
46
+ includeTmp?: boolean;
47
+ }
48
+
49
+ // ── 常量 ─────────────────────────────────────────────────────
50
+
51
+ /** workspace root 向上查找的最大深度 */
52
+ const WORKSPACE_ROOT_MAX_DEPTH = 20;
53
+
54
+ // ── workspace root 推导(从 config-loader 提取,agent/workflow 共用) ──
55
+
56
+ /**
57
+ * 判断 dir 是否是 workspaceRoot 的直接子目录(一层深度)。
58
+ * 用于 bare+worktree 结构里识别 worktree 根。
59
+ */
60
+ function isDirectChildOfWorkspaceRoot(dir: string, workspaceRoot: string): boolean {
61
+ return resolve(dir, "..") === workspaceRoot;
62
+ }
63
+
64
+ /**
65
+ * 从 cwd 向上查找 workspace root。
66
+ *
67
+ * bare+worktree 优先找 .bare;普通 repo 找最顶层 .git;fallback 找 .pi。
68
+ * 与 config-loader 原有逻辑一致(合并后提取为共享函数)。
69
+ */
70
+ export function findWorkspaceRoot(cwd?: string): string {
71
+ const dir = cwd ?? process.cwd();
72
+ const root = resolve("/");
73
+
74
+ // Phase 1: bare repo 优先——先全路径扫一遍找 .bare
75
+ let probe = dir;
76
+ for (let i = 0; i < WORKSPACE_ROOT_MAX_DEPTH; i++) {
77
+ if (fsSync.existsSync(resolve(probe, ".bare"))) {
78
+ // worktree 是 workspace 根的直接子目录。若 cwd 自身有 .pi/,优先用 cwd
79
+ if (probe !== dir && isDirectChildOfWorkspaceRoot(dir, probe) && fsSync.existsSync(resolve(dir, ".pi"))) {
80
+ return dir;
81
+ }
82
+ return probe;
83
+ }
84
+ if (probe === root) break;
85
+ probe = resolve(probe, "..");
86
+ }
87
+
88
+ // Phase 2: 无 .bare 时,找最顶层的 .git
89
+ let topLevel = dir;
90
+ probe = dir;
91
+ for (let i = 0; i < WORKSPACE_ROOT_MAX_DEPTH; i++) {
92
+ if (fsSync.existsSync(resolve(probe, ".git"))) {
93
+ topLevel = probe;
94
+ }
95
+ if (probe === root) break;
96
+ probe = resolve(probe, "..");
97
+ }
98
+ if (topLevel !== dir) {
99
+ return topLevel;
100
+ }
101
+
102
+ // Phase 3: fallback——用第一个遇到的 .pi
103
+ probe = dir;
104
+ for (let i = 0; i < WORKSPACE_ROOT_MAX_DEPTH; i++) {
105
+ if (fsSync.existsSync(resolve(probe, ".pi"))) {
106
+ return probe;
107
+ }
108
+ if (probe === root) break;
109
+ probe = resolve(probe, "..");
110
+ }
111
+
112
+ return dir;
113
+ }
114
+
115
+ // ── 文件扩展名判定 ───────────────────────────────────────────
116
+
117
+ /** 根据资源种类判定脚本文件扩展名 */
118
+ function isTargetFile(name: string, kind: ResourceKind): boolean {
119
+ // _ 前缀 = draft/示例,不参与发现(与原 agent-registry/workflow 约定一致)
120
+ if (name.startsWith("_")) return false;
121
+ if (kind === "agents") {
122
+ return name.endsWith(".md") && !name.endsWith(".chain.md");
123
+ }
124
+ // workflows
125
+ return name.endsWith(".js") || name.endsWith(".mjs");
126
+ }
127
+
128
+ /** 提取文件名 stem(去目录去扩展名) */
129
+ function stem(filePath: string): string {
130
+ const base = filePath.split("/").pop() ?? filePath;
131
+ const dot = base.lastIndexOf(".");
132
+ return dot > 0 ? base.slice(0, dot) : base;
133
+ }
134
+
135
+ // ── 目录扫描 ─────────────────────────────────────────────────
136
+
137
+ /**
138
+ * 扫描单个目录下的资源文件。
139
+ * 返回文件绝对路径列表。目录不存在时返回空数组。
140
+ */
141
+ async function scanDirectory(dirPath: string, kind: ResourceKind): Promise<string[]> {
142
+ try {
143
+ await access(dirPath);
144
+ } catch {
145
+ return [];
146
+ }
147
+
148
+ const entries = await readdir(dirPath, { withFileTypes: true });
149
+ const files: string[] = [];
150
+ for (const e of entries) {
151
+ if (!isTargetFile(e.name, kind)) continue;
152
+ const absPath = resolve(dirPath, e.name);
153
+ // symlink 单独处理:Dirent.isFile() 对 symlink 返回 false
154
+ if (e.isFile()) {
155
+ files.push(absPath);
156
+ } else if (e.isSymbolicLink()) {
157
+ const targetStat = await stat(absPath).catch(() => null);
158
+ if (targetStat?.isFile()) files.push(absPath);
159
+ }
160
+ }
161
+ return files;
162
+ }
163
+
164
+ // ── npm/dev 包内 manifest 发现 ───────────────────────────────
165
+
166
+ /**
167
+ * 读取 package.json 的 pi.{kind} manifest(pi.agents / pi.workflows)。
168
+ * 返回 undefined 表示无 manifest 声明。
169
+ */
170
+ async function readPackageManifest(pkgDir: string, kind: ResourceKind): Promise<string[] | undefined> {
171
+ const pkgJsonPath = resolve(pkgDir, "package.json");
172
+ try {
173
+ const content = await readFile(pkgJsonPath, "utf-8");
174
+ const pkg = JSON.parse(content) as Record<string, unknown>;
175
+ const pi = pkg.pi as Record<string, unknown> | undefined;
176
+ if (!pi) return undefined;
177
+ const entries = pi[kind];
178
+ if (!Array.isArray(entries)) return undefined;
179
+ // 过滤非字符串元素
180
+ return entries.filter((p): p is string => typeof p === "string");
181
+ } catch {
182
+ return undefined;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * 处理单个 npm/dev 包:按 manifest 或约定目录发现资源。
188
+ *
189
+ * 规则:
190
+ * - 有 manifest → 只按 manifest 声明路径加载。路径不存在 → 整包失败(返回 available=false 占位)
191
+ * - 无 manifest → 扫约定目录 {kind}/(agents/ 或 workflows/)
192
+ */
193
+ async function processPackage(
194
+ pkgDir: string,
195
+ kind: ResourceKind,
196
+ ): Promise<DiscoveredResource[]> {
197
+ const manifestPaths = await readPackageManifest(pkgDir, kind);
198
+
199
+ // manifest 模式:只按声明路径加载,路径不存在则整包失败
200
+ if (manifestPaths && manifestPaths.length > 0) {
201
+ const results: DiscoveredResource[] = [];
202
+ let allFailed = true;
203
+
204
+ for (const relPath of manifestPaths) {
205
+ const absPath = resolve(pkgDir, relPath);
206
+ const fileStat = await stat(absPath).catch(() => null);
207
+ if (!fileStat) {
208
+ // manifest 声明的路径不存在 → 记录失败占位(路径存在性校验)
209
+ results.push({ path: absPath, source: "npm", available: false });
210
+ continue;
211
+ }
212
+
213
+ if (fileStat.isDirectory()) {
214
+ const files = await scanDirectory(absPath, kind);
215
+ for (const f of files) {
216
+ results.push({ path: f, source: "npm", available: true });
217
+ allFailed = false;
218
+ }
219
+ } else if (fileStat.isFile()) {
220
+ results.push({ path: absPath, source: "npm", available: true });
221
+ allFailed = false;
222
+ }
223
+ }
224
+
225
+ // manifest 全失败:返回 available=false 占位,不 fallback 到约定目录
226
+ if (allFailed) {
227
+ return results;
228
+ }
229
+ return results;
230
+ }
231
+
232
+ // 无 manifest:扫约定目录 {kind}/
233
+ const conventionDir = resolve(pkgDir, kind);
234
+ const files = await scanDirectory(conventionDir, kind);
235
+ return files.map((f) => ({ path: f, source: "npm", available: true }));
236
+ }
237
+
238
+ /**
239
+ * 扫描 npm node_modules 目录下所有包的资源。
240
+ * 支持 scoped(@scope/pkg)和 unscoped(pkg)包。
241
+ */
242
+ async function scanNpmDir(
243
+ nodeModulesDir: string,
244
+ kind: ResourceKind,
245
+ ): Promise<DiscoveredResource[]> {
246
+ let entries: string[];
247
+ try {
248
+ entries = await readdir(nodeModulesDir);
249
+ } catch {
250
+ return [];
251
+ }
252
+
253
+ const results: DiscoveredResource[] = [];
254
+
255
+ for (const entry of entries) {
256
+ const entryPath = resolve(nodeModulesDir, entry);
257
+
258
+ if (entry.startsWith("@")) {
259
+ // scoped 包——迭代子包
260
+ let scopedEntries: string[];
261
+ try {
262
+ scopedEntries = await readdir(entryPath);
263
+ } catch {
264
+ continue;
265
+ }
266
+ for (const scopedPkg of scopedEntries) {
267
+ const scopedPkgDir = resolve(entryPath, scopedPkg);
268
+ const pkgResults = await processPackage(scopedPkgDir, kind);
269
+ results.push(...pkgResults);
270
+ }
271
+ } else {
272
+ // unscoped 包
273
+ const pkgResults = await processPackage(entryPath, kind);
274
+ results.push(...pkgResults);
275
+ }
276
+ }
277
+
278
+ return results;
279
+ }
280
+
281
+ // ── 扫描源构建 ───────────────────────────────────────────────
282
+
283
+ /** 扫描源定义:路径 + source 标签 */
284
+ interface ScanTarget {
285
+ dir: string;
286
+ source: ResourceSource;
287
+ /** 该源是否参与本次扫描(如 tmp 仅 workflow 启用) */
288
+ enabled: boolean;
289
+ }
290
+
291
+ /**
292
+ * 构建所有扫描源(按优先级低→高排列)。
293
+ *
294
+ * agent 和 workflow 共享相同的前缀体系,末级目录名由 kind 决定。
295
+ */
296
+ function buildScanTargets(config: ScanConfig): ScanTarget[] {
297
+ const { kind, workspaceRoot, agentDir, includeTmp } = config;
298
+ const home = homedir();
299
+
300
+ const targets: ScanTarget[] = [
301
+ // 1. user .pi/agent/{kind}/
302
+ { dir: join(agentDir, kind), source: "user-pi", enabled: true },
303
+ // 2. user .agents/{kind}/
304
+ { dir: join(home, ".agents", kind), source: "user-agents", enabled: true },
305
+ // 3. npm global: agentDir/npm/node_modules/*/<pkg>/
306
+ { dir: join(agentDir, "npm", "node_modules"), source: "npm", enabled: true },
307
+ // 4. npm dev symlink: agentDir/extensions/*/<pkg>/
308
+ { dir: join(agentDir, "extensions"), source: "npm-dev", enabled: true },
309
+ // 5. project .pi/{kind}/
310
+ { dir: join(workspaceRoot, ".pi", kind), source: "project-pi", enabled: true },
311
+ ];
312
+
313
+ // 6. project .pi/{kind}/.tmp/(仅 workflow)
314
+ if (includeTmp) {
315
+ targets.push({
316
+ dir: join(workspaceRoot, ".pi", kind, ".tmp"),
317
+ source: "project-pi-tmp",
318
+ enabled: true,
319
+ });
320
+ }
321
+
322
+ // 7. project .agents/{kind}/
323
+ targets.push({
324
+ dir: join(workspaceRoot, ".agents", kind),
325
+ source: "project-agents",
326
+ enabled: true,
327
+ });
328
+
329
+ return targets.filter((t) => t.enabled);
330
+ }
331
+
332
+ // ── 公共 API ─────────────────────────────────────────────────
333
+
334
+ /**
335
+ * 发现所有资源文件(agent .md 或 workflow .js/.mjs)。
336
+ *
337
+ * 按优先级低→高扫描所有源,同名资源靠后覆盖靠前(last-writer-wins)。
338
+ * npm/dev 包内:有 manifest 只走 manifest(路径不存在则失败),无 manifest 扫约定目录。
339
+ *
340
+ * Never throws. 解析失败/不可读的资源以 available=false 返回。
341
+ *
342
+ * @returns 去重后的资源列表(按优先级合并,高优先级覆盖低优先级同名)
343
+ */
344
+ export async function discoverResources(config: ScanConfig): Promise<DiscoveredResource[]> {
345
+ const targets = buildScanTargets(config);
346
+
347
+ // 逐源扫描,收集结果(保留 source 标签用于优先级合并)
348
+ const allBySource: Array<{ source: ResourceSource; resources: DiscoveredResource[] }> = [];
349
+
350
+ for (const target of targets) {
351
+ if (target.source === "npm" || target.source === "npm-dev") {
352
+ // npm/dev 目录:迭代包,走 manifest 或约定目录
353
+ const resources = await scanNpmDir(target.dir, config.kind);
354
+ // 覆盖 source 标签(scanNpmDir 内部统一标 "npm",这里修正为实际源)
355
+ const tagged = resources.map((r) => ({ ...r, source: target.source }));
356
+ allBySource.push({ source: target.source, resources: tagged });
357
+ } else {
358
+ // 普通目录:直接扫
359
+ const files = await scanDirectory(target.dir, config.kind);
360
+ const resources = files.map((f) => ({ path: f, source: target.source, available: true }));
361
+ allBySource.push({ source: target.source, resources });
362
+ }
363
+ }
364
+
365
+ // 按优先级合并:targets 数组顺序即优先级(低→高),高优先级后写覆盖
366
+ // 用文件名 stem 作为去重 key(与旧逻辑一致:同名资源高优先级覆盖)
367
+ const merged = new Map<string, DiscoveredResource>();
368
+
369
+ for (const { resources } of allBySource) {
370
+ for (const r of resources) {
371
+ const key = stem(r.path);
372
+ // available=false 的占位不覆盖已有的 available=true
373
+ if (!r.available && merged.has(key)) {
374
+ continue;
375
+ }
376
+ merged.set(key, r);
377
+ }
378
+ }
379
+
380
+ return Array.from(merged.values());
381
+ }
382
+
383
+ /**
384
+ * 同步版:扫描单个目录下的资源文件路径(供 agent-registry 的 mtime 缓存模式使用)。
385
+ *
386
+ * agent .md 发现需要 mtime 缓存(hot-reload),不能走 async 全量扫描。
387
+ * 此函数提供目录级同步扫描,npm/dev 包内发现仍需 async(agent-registry 用 builtin 兜底)。
388
+ */
389
+ export function scanDirectorySync(dirPath: string, kind: ResourceKind): string[] {
390
+ try {
391
+ fsSync.accessSync(dirPath);
392
+ } catch {
393
+ return [];
394
+ }
395
+
396
+ let entries: string[];
397
+ try {
398
+ entries = fsSync.readdirSync(dirPath);
399
+ } catch {
400
+ return [];
401
+ }
402
+
403
+ const files: string[] = [];
404
+ for (const entry of entries) {
405
+ if (!isTargetFile(entry, kind)) continue;
406
+ files.push(resolve(dirPath, entry));
407
+ }
408
+ return files;
409
+ }
410
+
411
+ /**
412
+ * 同步版:读取 package.json 的 pi.{kind} manifest。
413
+ * 供 agent-registry 同步路径使用。
414
+ */
415
+ export function readPackageManifestSync(pkgDir: string, kind: ResourceKind): string[] | undefined {
416
+ const pkgJsonPath = resolve(pkgDir, "package.json");
417
+ try {
418
+ const content = fsSync.readFileSync(pkgJsonPath, "utf-8");
419
+ const pkg = JSON.parse(content) as Record<string, unknown>;
420
+ const pi = pkg.pi as Record<string, unknown> | undefined;
421
+ if (!pi) return undefined;
422
+ const entries = pi[kind];
423
+ if (!Array.isArray(entries)) return undefined;
424
+ return entries.filter((p): p is string => typeof p === "string");
425
+ } catch {
426
+ return undefined;
427
+ }
428
+ }
429
+
430
+ /**
431
+ * 同步版:处理单个 npm/dev 包。
432
+ * 供 agent-registry 同步路径使用。
433
+ */
434
+ export function processPackageSync(pkgDir: string, kind: ResourceKind): DiscoveredResource[] {
435
+ const manifestPaths = readPackageManifestSync(pkgDir, kind);
436
+
437
+ if (manifestPaths && manifestPaths.length > 0) {
438
+ const results: DiscoveredResource[] = [];
439
+
440
+ for (const relPath of manifestPaths) {
441
+ const absPath = resolve(pkgDir, relPath);
442
+ let fileStat: fsSync.Stats | null;
443
+ try {
444
+ fileStat = fsSync.statSync(absPath);
445
+ } catch {
446
+ fileStat = null;
447
+ }
448
+ if (!fileStat) {
449
+ results.push({ path: absPath, source: "npm", available: false });
450
+ continue;
451
+ }
452
+
453
+ if (fileStat.isDirectory()) {
454
+ const files = scanDirectorySync(absPath, kind);
455
+ for (const f of files) {
456
+ results.push({ path: f, source: "npm", available: true });
457
+ }
458
+ } else if (fileStat.isFile()) {
459
+ results.push({ path: absPath, source: "npm", available: true });
460
+ }
461
+ }
462
+
463
+ return results;
464
+ }
465
+
466
+ // 无 manifest:扫约定目录
467
+ const conventionDir = resolve(pkgDir, kind);
468
+ const files = scanDirectorySync(conventionDir, kind);
469
+ return files.map((f) => ({ path: f, source: "npm", available: true }));
470
+ }
471
+
472
+ /**
473
+ * 同步版:扫描 npm node_modules 目录。
474
+ * 供 agent-registry 同步路径使用。
475
+ */
476
+ export function scanNpmDirSync(nodeModulesDir: string, kind: ResourceKind): DiscoveredResource[] {
477
+ let entries: string[];
478
+ try {
479
+ entries = fsSync.readdirSync(nodeModulesDir);
480
+ } catch {
481
+ return [];
482
+ }
483
+
484
+ const results: DiscoveredResource[] = [];
485
+ for (const entry of entries) {
486
+ const entryPath = resolve(nodeModulesDir, entry);
487
+
488
+ if (entry.startsWith("@")) {
489
+ let scopedEntries: string[];
490
+ try {
491
+ scopedEntries = fsSync.readdirSync(entryPath);
492
+ } catch {
493
+ continue;
494
+ }
495
+ for (const scopedPkg of scopedEntries) {
496
+ const scopedPkgDir = resolve(entryPath, scopedPkg);
497
+ results.push(...processPackageSync(scopedPkgDir, kind));
498
+ }
499
+ } else {
500
+ results.push(...processPackageSync(entryPath, kind));
501
+ }
502
+ }
503
+ return results;
504
+ }
505
+
506
+ /**
507
+ * 同步版:发现所有资源(agent-registry 专用,支持 mtime 缓存的 hot-reload)。
508
+ *
509
+ * 与 discoverResources 对应的同步实现,扫描相同的源。
510
+ * 返回所有源的资源(未去重,调用方按需处理优先级)。
511
+ */
512
+ export function discoverResourcesSync(config: ScanConfig): DiscoveredResource[] {
513
+ const targets = buildScanTargets(config);
514
+ const all: DiscoveredResource[] = [];
515
+
516
+ for (const target of targets) {
517
+ if (target.source === "npm" || target.source === "npm-dev") {
518
+ const resources = scanNpmDirSync(target.dir, config.kind);
519
+ all.push(...resources.map((r) => ({ ...r, source: target.source })));
520
+ } else {
521
+ const files = scanDirectorySync(target.dir, config.kind);
522
+ all.push(...files.map((f) => ({ path: f, source: target.source, available: true })));
523
+ }
524
+ }
525
+
526
+ // 按优先级合并(targets 顺序 = 优先级低→高)
527
+ const merged = new Map<string, DiscoveredResource>();
528
+ for (const r of all) {
529
+ const key = stem(r.path);
530
+ if (!r.available && merged.has(key)) continue;
531
+ merged.set(key, r);
532
+ }
533
+
534
+ return Array.from(merged.values());
535
+ }