@zhushanwen/subagent-core 0.2.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 (330) hide show
  1. package/README.md +114 -0
  2. package/dist/chunk-3VOERJPJ.js +22 -0
  3. package/dist/chunk-A4IVZWVX.js +31 -0
  4. package/dist/execution/engine/engines/zcode/constants.cjs +54 -0
  5. package/dist/execution/engine/engines/zcode/constants.d.cts +31 -0
  6. package/dist/execution/engine/engines/zcode/constants.d.ts +31 -0
  7. package/dist/execution/engine/engines/zcode/constants.js +22 -0
  8. package/dist/execution/engine/engines/zcode/reader.cjs +276 -0
  9. package/dist/execution/engine/engines/zcode/reader.d.cts +20 -0
  10. package/dist/execution/engine/engines/zcode/reader.d.ts +20 -0
  11. package/dist/execution/engine/engines/zcode/reader.js +239 -0
  12. package/dist/execution/engine/paths.cjs +55 -0
  13. package/dist/execution/engine/paths.d.cts +8 -0
  14. package/dist/execution/engine/paths.d.ts +8 -0
  15. package/dist/execution/engine/paths.js +26 -0
  16. package/dist/execution/relay-env.cjs +62 -0
  17. package/dist/execution/relay-env.d.cts +33 -0
  18. package/dist/execution/relay-env.d.ts +33 -0
  19. package/dist/execution/relay-env.js +20 -0
  20. package/dist/index.cjs +1852 -0
  21. package/dist/index.d.cts +1184 -0
  22. package/dist/index.d.ts +1184 -0
  23. package/dist/index.js +1810 -0
  24. package/dist/types-BxyAidGf.d.cts +875 -0
  25. package/dist/types-BxyAidGf.d.ts +875 -0
  26. package/package.json +99 -0
  27. package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +142 -0
  28. package/src/__tests__/append-system-prompt-assembly.test.ts +242 -0
  29. package/src/__tests__/builtin-workflows-structure.test.ts +180 -0
  30. package/src/__tests__/derive-closed-display-parity.test.ts +93 -0
  31. package/src/__tests__/fr4-get-state-handshake.test.ts +125 -0
  32. package/src/__tests__/fr8-orphan-recovery.test.ts +259 -0
  33. package/src/__tests__/m2-append-content-probe.test.ts +73 -0
  34. package/src/__tests__/manifest-store.test.ts +368 -0
  35. package/src/__tests__/record-store-cache.test.ts +335 -0
  36. package/src/__tests__/record-store-index.test.ts +560 -0
  37. package/src/__tests__/record-store-last-line.test.ts +129 -0
  38. package/src/__tests__/review-fix-loop-script.test.ts +288 -0
  39. package/src/__tests__/review-fix-loop-utils.test.ts +2377 -0
  40. package/src/__tests__/robustness-low-batch1.test.ts +286 -0
  41. package/src/__tests__/robustness-medium-batch1.test.ts +69 -0
  42. package/src/__tests__/robustness-medium-batch2.test.ts +65 -0
  43. package/src/__tests__/robustness-medium-batch3.test.ts +30 -0
  44. package/src/__tests__/robustness-medium-batch4.test.ts +358 -0
  45. package/src/__tests__/session-runner.test.ts +69 -0
  46. package/src/__tests__/smoke.test.ts +9 -0
  47. package/src/__tests__/stream-sink.test.ts +203 -0
  48. package/src/core/__tests__/host-services.test.ts +144 -0
  49. package/src/core/__tests__/logger.test.ts +78 -0
  50. package/src/core/__tests__/notify-ports.test.ts +104 -0
  51. package/src/core/host-services.ts +97 -0
  52. package/src/core/logger.ts +47 -0
  53. package/src/core/notify-ports.ts +126 -0
  54. package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +53 -0
  55. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  56. package/src/execution/__tests__/agent-result-mapper.test.ts +150 -0
  57. package/src/execution/__tests__/alive-store.test.ts +147 -0
  58. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +490 -0
  59. package/src/execution/__tests__/channel-registry-handshake.test.ts +242 -0
  60. package/src/execution/__tests__/chat-engine-routing.test.ts +610 -0
  61. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  62. package/src/execution/__tests__/concurrency-pool.test.ts +250 -0
  63. package/src/execution/__tests__/config.test.ts +302 -0
  64. package/src/execution/__tests__/delivery-methods.test.ts +424 -0
  65. package/src/execution/__tests__/dialog-queue.test.ts +299 -0
  66. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  67. package/src/execution/__tests__/execute-and-await-worktree.test.ts +243 -0
  68. package/src/execution/__tests__/execute-nesting.test.ts +323 -0
  69. package/src/execution/__tests__/execute-options-mapper.test.ts +200 -0
  70. package/src/execution/__tests__/execution-record.test.ts +1394 -0
  71. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
  72. package/src/execution/__tests__/finalize-record.test.ts +367 -0
  73. package/src/execution/__tests__/finalized-marker.test.ts +104 -0
  74. package/src/execution/__tests__/format-schema-instruction.test.ts +169 -0
  75. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  76. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  77. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  78. package/src/execution/__tests__/helpers/spawn-mock.ts +242 -0
  79. package/src/execution/__tests__/host-mode.test.ts +87 -0
  80. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  81. package/src/execution/__tests__/lifecycle-manager.test.ts +383 -0
  82. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  83. package/src/execution/__tests__/manifest-parentid.test.ts +117 -0
  84. package/src/execution/__tests__/model-resolver.test.ts +465 -0
  85. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  86. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  87. package/src/execution/__tests__/output-collector.test.ts +358 -0
  88. package/src/execution/__tests__/path-encoding.test.ts +104 -0
  89. package/src/execution/__tests__/pi-invocation.test.ts +134 -0
  90. package/src/execution/__tests__/record-store.test.ts +1057 -0
  91. package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
  92. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +339 -0
  93. package/src/execution/__tests__/recursive-visibility-env.test.ts +273 -0
  94. package/src/execution/__tests__/relay-env.test.ts +42 -0
  95. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  96. package/src/execution/__tests__/rpc-mode.test.ts +89 -0
  97. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  98. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  99. package/src/execution/__tests__/run-spawn-edges.test.ts +687 -0
  100. package/src/execution/__tests__/run-spawn-integration.test.ts +933 -0
  101. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  102. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +196 -0
  103. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
  104. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  105. package/src/execution/__tests__/session-file-gc.test.ts +293 -0
  106. package/src/execution/__tests__/session-reconstructor.test.ts +379 -0
  107. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  108. package/src/execution/__tests__/session-runner-schema-env.test.ts +350 -0
  109. package/src/execution/__tests__/spawn-args.test.ts +445 -0
  110. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
  111. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  112. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +207 -0
  113. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  114. package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
  115. package/src/execution/__tests__/stdin-writer.test.ts +462 -0
  116. package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
  117. package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
  118. package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
  119. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  120. package/src/execution/__tests__/subagent-service.test.ts +788 -0
  121. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
  122. package/src/execution/__tests__/subprocess-agent-runner.test.ts +612 -0
  123. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  124. package/src/execution/__tests__/timeout-integration.test.ts +615 -0
  125. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  126. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  127. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  128. package/src/execution/__tests__/ui-channels.test.ts +187 -0
  129. package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
  130. package/src/execution/__tests__/ui-request-handler-factory.test.ts +369 -0
  131. package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
  132. package/src/execution/__tests__/ui-request-observability.test.ts +113 -0
  133. package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
  134. package/src/execution/__tests__/worktree-manager.test.ts +633 -0
  135. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +229 -0
  136. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
  137. package/src/execution/__tests__/worktree-registry.test.ts +199 -0
  138. package/src/execution/agent-registry.ts +199 -0
  139. package/src/execution/agent-result-mapper.ts +90 -0
  140. package/src/execution/alive-store.ts +119 -0
  141. package/src/execution/argv-mirror.ts +113 -0
  142. package/src/execution/best-effort.ts +38 -0
  143. package/src/execution/channel-registry-access.ts +144 -0
  144. package/src/execution/concurrency-pool.ts +116 -0
  145. package/src/execution/config.ts +165 -0
  146. package/src/execution/dialog-queue.ts +330 -0
  147. package/src/execution/engine/__tests__/common/data-dir.test.ts +78 -0
  148. package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
  149. package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
  150. package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
  151. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
  152. package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
  153. package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
  154. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
  155. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
  156. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
  157. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
  158. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
  159. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
  160. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
  161. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
  162. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
  163. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
  164. package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
  165. package/src/execution/engine/__tests__/model-prompt.test.ts +197 -0
  166. package/src/execution/engine/__tests__/paths.test.ts +39 -0
  167. package/src/execution/engine/__tests__/registry.test.ts +120 -0
  168. package/src/execution/engine/__tests__/routing.test.ts +231 -0
  169. package/src/execution/engine/common/data-dir.ts +66 -0
  170. package/src/execution/engine/common/errors.ts +183 -0
  171. package/src/execution/engine/common/event-journal.ts +254 -0
  172. package/src/execution/engine/common/journal-replay.ts +62 -0
  173. package/src/execution/engine/common/kill-chain.ts +221 -0
  174. package/src/execution/engine/common/nesting-guard.ts +50 -0
  175. package/src/execution/engine/common/persona-router.ts +108 -0
  176. package/src/execution/engine/common/pool-manager.ts +226 -0
  177. package/src/execution/engine/common/schema-emulation.ts +189 -0
  178. package/src/execution/engine/common/session-view-projection.ts +51 -0
  179. package/src/execution/engine/engine-discovery.ts +65 -0
  180. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
  181. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
  182. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
  183. package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
  184. package/src/execution/engine/engines/pi/reader.ts +48 -0
  185. package/src/execution/engine/engines/pi/registration.ts +35 -0
  186. package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
  187. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
  188. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
  189. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
  190. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
  191. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
  192. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +71 -0
  193. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
  194. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
  195. package/src/execution/engine/engines/zcode/constants.ts +43 -0
  196. package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
  197. package/src/execution/engine/engines/zcode/launcher.ts +161 -0
  198. package/src/execution/engine/engines/zcode/parser.ts +436 -0
  199. package/src/execution/engine/engines/zcode/preparer.ts +363 -0
  200. package/src/execution/engine/engines/zcode/reader.ts +381 -0
  201. package/src/execution/engine/engines/zcode/registration.ts +37 -0
  202. package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
  203. package/src/execution/engine/host-task-spec.ts +47 -0
  204. package/src/execution/engine/model-prompt.ts +158 -0
  205. package/src/execution/engine/paths.ts +42 -0
  206. package/src/execution/engine/port.ts +153 -0
  207. package/src/execution/engine/registry.ts +133 -0
  208. package/src/execution/engine/routing.ts +218 -0
  209. package/src/execution/engine/types.ts +309 -0
  210. package/src/execution/execute-options-mapper.ts +115 -0
  211. package/src/execution/execution-record.ts +984 -0
  212. package/src/execution/finalize-record.ts +254 -0
  213. package/src/execution/finalized-marker.ts +70 -0
  214. package/src/execution/get-state-handshake.ts +104 -0
  215. package/src/execution/host-mode.ts +56 -0
  216. package/src/execution/idle-gc.ts +47 -0
  217. package/src/execution/lifecycle-manager.ts +513 -0
  218. package/src/execution/lifecycle-predicates.ts +65 -0
  219. package/src/execution/manifest-store.ts +256 -0
  220. package/src/execution/model-config-service.ts +250 -0
  221. package/src/execution/model-resolver.ts +248 -0
  222. package/src/execution/notifier.ts +372 -0
  223. package/src/execution/notify-ledger.ts +580 -0
  224. package/src/execution/output-collector.ts +228 -0
  225. package/src/execution/path-encoding.ts +60 -0
  226. package/src/execution/pi-invocation.ts +120 -0
  227. package/src/execution/record-entry.ts +132 -0
  228. package/src/execution/record-store.ts +1265 -0
  229. package/src/execution/relay-env.ts +37 -0
  230. package/src/execution/session-context-resolver.ts +64 -0
  231. package/src/execution/session-file-gc.ts +120 -0
  232. package/src/execution/session-pending.ts +170 -0
  233. package/src/execution/session-reconstructor.ts +678 -0
  234. package/src/execution/session-runner.ts +1789 -0
  235. package/src/execution/sessions-index.ts +304 -0
  236. package/src/execution/spawn-event-adapter.ts +363 -0
  237. package/src/execution/stdin-writer.ts +198 -0
  238. package/src/execution/stream-sink.ts +126 -0
  239. package/src/execution/subagent-service.ts +2268 -0
  240. package/src/execution/subprocess-agent-runner.ts +317 -0
  241. package/src/execution/temp-prompt.ts +62 -0
  242. package/src/execution/tombstone-store.ts +72 -0
  243. package/src/execution/turn-limiter.ts +102 -0
  244. package/src/execution/types.ts +1023 -0
  245. package/src/execution/ui-channels.ts +216 -0
  246. package/src/execution/ui-interaction-model.ts +48 -0
  247. package/src/execution/ui-request-handler-factory.ts +243 -0
  248. package/src/execution/ui-request-observability.ts +81 -0
  249. package/src/execution/ui-request-queue.ts +184 -0
  250. package/src/execution/worktree-manager.ts +688 -0
  251. package/src/execution/worktree-registry.ts +279 -0
  252. package/src/index.ts +87 -0
  253. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +349 -0
  254. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +202 -0
  255. package/src/orchestration/__tests__/agent-call-stream.test.ts +152 -0
  256. package/src/orchestration/__tests__/args-validator.test.ts +143 -0
  257. package/src/orchestration/__tests__/config-loader.test.ts +503 -0
  258. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +809 -0
  259. package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +404 -0
  260. package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +56 -0
  261. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  262. package/src/orchestration/__tests__/execute-agent-call.test.ts +451 -0
  263. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +600 -0
  264. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
  265. package/src/orchestration/__tests__/lifecycle.test.ts +659 -0
  266. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
  267. package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +183 -0
  268. package/src/orchestration/__tests__/script-lint.test.ts +831 -0
  269. package/src/orchestration/__tests__/skill-discovery.test.ts +210 -0
  270. package/src/orchestration/__tests__/test-mocks.ts +197 -0
  271. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
  272. package/src/orchestration/__tests__/worker-host.test.ts +120 -0
  273. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
  274. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +563 -0
  275. package/src/orchestration/__tests__/worker-script-builder.test.ts +309 -0
  276. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +129 -0
  277. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +317 -0
  278. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  279. package/src/orchestration/agent-opts-resolver.ts +174 -0
  280. package/src/orchestration/args-validator.ts +127 -0
  281. package/src/orchestration/config-loader.ts +316 -0
  282. package/src/orchestration/error-recovery.ts +1018 -0
  283. package/src/orchestration/execute-agent-call.ts +256 -0
  284. package/src/orchestration/launcher.ts +455 -0
  285. package/src/orchestration/lifecycle.ts +399 -0
  286. package/src/orchestration/models/__tests__/budget.test.ts +307 -0
  287. package/src/orchestration/models/agent-call.ts +83 -0
  288. package/src/orchestration/models/budget.ts +118 -0
  289. package/src/orchestration/models/ports.ts +164 -0
  290. package/src/orchestration/models/run-runtime.ts +104 -0
  291. package/src/orchestration/models/run-spec.ts +83 -0
  292. package/src/orchestration/models/run-state.ts +44 -0
  293. package/src/orchestration/models/trace.ts +183 -0
  294. package/src/orchestration/models/types.ts +301 -0
  295. package/src/orchestration/models/workflow-run.ts +254 -0
  296. package/src/orchestration/models/workflow-script-registry.ts +35 -0
  297. package/src/orchestration/models/workflow-script.ts +117 -0
  298. package/src/orchestration/script-lint.ts +766 -0
  299. package/src/orchestration/skill-discovery.ts +135 -0
  300. package/src/orchestration/worker-handle.ts +115 -0
  301. package/src/orchestration/worker-host.ts +98 -0
  302. package/src/orchestration/worker-script-builder.ts +421 -0
  303. package/src/orchestration/workflow-files.ts +85 -0
  304. package/src/orchestration/workflow-script-registry-impl.ts +133 -0
  305. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  306. package/src/shared/__tests__/meta-parser.test.ts +304 -0
  307. package/src/shared/__tests__/model-ref.test.ts +306 -0
  308. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +288 -0
  309. package/src/shared/__tests__/resource-discovery.test.ts +749 -0
  310. package/src/shared/__tests__/resource-meta.test.ts +51 -0
  311. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  312. package/src/shared/__tests__/timer-delay.test.ts +61 -0
  313. package/src/shared/agent-event.ts +13 -0
  314. package/src/shared/agent-ref.ts +57 -0
  315. package/src/shared/meta-parser.ts +261 -0
  316. package/src/shared/model-ref.ts +286 -0
  317. package/src/shared/resource-discovery.ts +835 -0
  318. package/src/shared/resource-meta.ts +65 -0
  319. package/src/shared/schema-env.ts +44 -0
  320. package/src/shared/schema-jsonify.ts +58 -0
  321. package/src/shared/timer-delay.ts +54 -0
  322. package/src/shared/xml-injection.ts +35 -0
  323. package/workflows/README.md +81 -0
  324. package/workflows/_shared/agent-refs.cjs +40 -0
  325. package/workflows/chain.js +137 -0
  326. package/workflows/map-reduce.js +180 -0
  327. package/workflows/parallel.js +154 -0
  328. package/workflows/review-fix-loop-utils.cjs +1542 -0
  329. package/workflows/review-fix-loop.js +1605 -0
  330. package/workflows/scatter-gather.js +184 -0
@@ -0,0 +1,835 @@
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. 路径动态获取:宿主级根经 ScanConfig.hostRoots 注入(u0-data-discovery,D2 语义
8
+ // 边界——宿主提供根列表,扫描/遮蔽语义归 core),project 级用 findWorkspaceRoot(cwd)
9
+ // 3. npm/dev 包内发现:有 manifest(pi.agents/pi.workflows)只走 manifest,无 manifest 扫约定目录
10
+ // 4. manifest 路径存在性校验:声明的路径不存在 → 该包发现失败,不 fallback
11
+ // 5. 废弃 discovery.json:扫描路径完全由代码内推导,无外部依赖
12
+ //
13
+ // 优先级(低→高):user .pi/agent → user .agents → npm global → npm dev → project .pi → project .pi/.tmp(仅workflow) → project .agents
14
+
15
+ import * as fsSync from "node:fs";
16
+ import { access, readdir, readFile, stat } from "node:fs/promises";
17
+ import { homedir } from "node:os";
18
+ import { delimiter, join, resolve } from "node:path";
19
+
20
+ import type { DiscoveryRoot } from "../core/host-services.ts";
21
+ import { getLogger } from "../core/logger.ts";
22
+
23
+ // 模块级 logger(facade:configureCore 前落缺省 console,配置后透明切换宿主实现——
24
+ // pi 壳经 pi-host 桥接到宿主结构化日志 appendEntry 通路,输出面不变)
25
+ const logger = getLogger("subagents");
26
+
27
+ // [D8d] warn 路径进程内去重集合:key=(kind, stem, shadowedPath, keptPath),
28
+ // cap 1024 超限先清空再 add(对齐 ui-request-observability 的 MAX_WARNED_SESSIONS 范式)。
29
+ // debug 路径不去重(默认 no-op,无成本)。
30
+ const shadowWarnDedup = new Set<string>();
31
+ const MAX_SHADOW_WARN_DEDUP = 1024;
32
+
33
+ /** @internal 测试辅助:重置 warn 去重集合(cap 测试用,生产代码不调用)。 */
34
+ export function __testResetShadowDedup(): void {
35
+ shadowWarnDedup.clear();
36
+ }
37
+
38
+ /** @internal 测试辅助:向 warn 去重集合注入 key(cap 测试用)。 */
39
+ export function __testInjectShadowDedupKey(key: string): void {
40
+ shadowWarnDedup.add(key);
41
+ }
42
+
43
+ // ── 类型 ─────────────────────────────────────────────────────
44
+
45
+ /** 资源种类:agent 或 workflow */
46
+ export type ResourceKind = "agents" | "workflows";
47
+
48
+ /** 发现到的单个资源文件(原始数据,由调用方解析 frontmatter/meta) */
49
+ export interface DiscoveredResource {
50
+ /** 绝对路径 */
51
+ path: string;
52
+ /** 来源层级 */
53
+ source: ResourceSource;
54
+ /** 是否可用(manifest 校验失败的包整体标 false) */
55
+ available: boolean;
56
+ }
57
+
58
+ /** 资源来源层级 */
59
+ export type ResourceSource = "user-pi" | "user-agents" | "npm" | "npm-dev" | "user-extension-paths" | "project-pi" | "project-pi-tmp" | "project-agents";
60
+
61
+ /** 扫描配置 */
62
+ export interface ScanConfig {
63
+ /** 资源种类 */
64
+ kind: ResourceKind;
65
+ /** 项目根目录(findWorkspaceRoot 推导结果) */
66
+ workspaceRoot: string;
67
+ /** 宿主注入的发现根(DiscoveryRoot.dir 已含 kind 末级目录与安装布局,
68
+ * source 为宿主语义标签——pi 壳 = user-pi/npm/npm-dev 三根)。buildScanTargets
69
+ * 按标签查表填充对应槽位,宿主未提供某标签根时该槽位条目整体缺席。 */
70
+ hostRoots: DiscoveryRoot[];
71
+ /** 是否包含 tmp 源(仅 workflow 用 .pi/workflows/.tmp/) */
72
+ includeTmp?: boolean;
73
+ }
74
+
75
+ // ── 常量 ─────────────────────────────────────────────────────
76
+
77
+ /** 机器源集合:包管理/工程配置产物,其同名重复是安装拓扑常态(非用户配置错误)。
78
+ * 用户个人源(user-pi / user-agents)不在此列——双个人源同名重复保留 warn。 */
79
+ const MACHINE_SOURCES: ReadonlySet<ResourceSource> = new Set<ResourceSource>([
80
+ "npm",
81
+ "npm-dev",
82
+ "user-extension-paths",
83
+ "project-pi",
84
+ "project-pi-tmp",
85
+ "project-agents",
86
+ ]);
87
+
88
+ /** 判断 source 是否属于机器源(安装拓扑常态,同名重复降 debug)。
89
+ * 导出仅为测试穷举断言用(封闭 8 值枚举 × 分级边界)。 */
90
+ export function isMachineSource(source: ResourceSource): boolean {
91
+ return MACHINE_SOURCES.has(source);
92
+ }
93
+
94
+ /** workspace root 向上查找的最大深度 */
95
+ const WORKSPACE_ROOT_MAX_DEPTH = 20;
96
+
97
+ // ── workspace root 推导(从 config-loader 提取,agent/workflow 共用) ──
98
+
99
+ /**
100
+ * 判断 dir 是否是 workspaceRoot 的直接子目录(一层深度)。
101
+ * 用于 bare+worktree 结构里识别 worktree 根。
102
+ */
103
+ function isDirectChildOfWorkspaceRoot(dir: string, workspaceRoot: string): boolean {
104
+ return resolve(dir, "..") === workspaceRoot;
105
+ }
106
+
107
+ /**
108
+ * 从 cwd 向上查找 workspace root。
109
+ *
110
+ * bare+worktree 优先找 .bare;普通 repo 找最顶层 .git;fallback 找 .pi。
111
+ * 与 config-loader 原有逻辑一致(合并后提取为共享函数)。
112
+ */
113
+ // ── 统一 mtime 缓存层(m5 IF10)────────────────────────────────────
114
+ //
115
+ // 模块级 Map<path, { mtimeMs, content }>:mtime 判变缓存文件内容。
116
+ // - sync 实现(statSync/readFileSync):agent-registry discoverAll 是同步路径,
117
+ // async 缓存无法被 await(m5 design-review A1 探针实证约束)
118
+ // - stat 失败/ENOENT → 驱逐条目(mtime 缓存下文件删除不自愈——A3 修复)
119
+ // - 已知局限(C3 记录):内容变 mtime 未变(cp -p/rsync -t 保留源 mtime、
120
+ // 2s 粒度文件系统)→ 漏判,invalidateCache/clearFileCache 兜底;
121
+ // APFS mtimeMs 微秒级浮点 === 判变可靠(探针 P-mtime-精度)
122
+
123
+ interface MtimeCacheEntry {
124
+ mtimeMs: number;
125
+ content: string;
126
+ }
127
+
128
+ const mtimeCache = new Map<string, MtimeCacheEntry>();
129
+
130
+ // [perf] findWorkspaceRoot 结果按 cwd memo——每次调用最多 3 phase × 20 层 existsSync
131
+ //(30-60 次 stat),调用点(session_start、discoverWorkflows、getWorkflow)在 spawn/
132
+ // tool 热路径上重复付。cwd 不变则祖先目录结构(.bare/.git/.pi)不变;变化场景由
133
+ // clearFileCache 兜底(invalidateCache 语义,测试隔离也走这里)。
134
+ const workspaceRootCache = new Map<string, string>();
135
+
136
+ // [perf] npm/dev 包 package.json manifest 缓存(swf-perf-impl cleanup TC1/IF1)。
137
+ // key = package.json 绝对路径;缓存 parse 后的整个 pi 对象(agents/workflows 两 kind
138
+ // 共享一次 parse,不按 kind 分裂条目)。失效 = mtimeMs 严格相等判定(与 mtimeCache 同构,
139
+ // 『内容变 mtime 未变』漏判局限共享 C3 声明,clearFileCache 兜底)。
140
+ // 失败语义(消歧,三方 TC1/DM1/IF1 同一口径):
141
+ // - stat 失败(文件不存在/不可 stat)→ 驱逐该 path 条目 + undefined(对齐 getCachedFile:
142
+ // 文件已删条目无意义)
143
+ // - read 失败(stat 与 read 间竞态删除/EACCES)或 JSON.parse 失败(坏 JSON)→
144
+ // 不缓存(不写新条目、不驱逐已有好条目)+ undefined,下次调用重试——毒条目永不入缓存,
145
+ // 坏文件被修复且 mtime 变化后正常覆写。
146
+ // pi === undefined 条目仅在『read+parse 成功且 manifest 无 pi 字段』时写入(合法解析结果)。
147
+ interface ManifestCacheEntry {
148
+ mtimeMs: number;
149
+ pi: Record<string, unknown> | undefined;
150
+ }
151
+
152
+ const manifestCache = new Map<string, ManifestCacheEntry>();
153
+
154
+ /** 从缓存的 pi 对象派生 kind manifest(廉价操作,每次调用新数组——语义与旧直读版一致)。 */
155
+ function piToManifest(pi: Record<string, unknown> | undefined, kind: ResourceKind): string[] | undefined {
156
+ if (!pi) return undefined;
157
+ const entries = pi[kind];
158
+ if (!Array.isArray(entries)) return undefined;
159
+ // 过滤非字符串元素
160
+ return entries.filter((p): p is string => typeof p === "string");
161
+ }
162
+
163
+ /**
164
+ * package.json 文本 → pi 字段对象(pi.agents / pi.workflows 的容器)。
165
+ * [review 修复] 结构守卫:JSON.parse 对 "42" / '"str"' / "null" 等合法 JSON 产出
166
+ * 非对象值,旧 `as Record<string, unknown>` 盲断言下 .pi 访问是「碰巧不抛」
167
+ * (primitive 装箱返 undefined / null 抛 TypeError 落入 catch)——显式判非对象,
168
+ * 按「无 manifest」(undefined)处理,后果与原先一致但语义不再依赖巧合。
169
+ * pi 字段本身非对象(如 {"pi":42})或为数组(如 {"pi":[]}——数组不是合法 pi 容器,
170
+ * typeof "object" 守卫会放行,Record 断言对数组是谎言)同样显式归 undefined。
171
+ * JSON SyntaxError 向上抛,由调用方 catch 承担「坏 JSON 不缓存、下次重试」语义。
172
+ */
173
+ function parsePiField(content: string): Record<string, unknown> | undefined {
174
+ const parsed: unknown = JSON.parse(content);
175
+ if (typeof parsed !== "object" || parsed === null) return undefined;
176
+ const pi: unknown = (parsed as Record<string, unknown>).pi;
177
+ return typeof pi === "object" && pi !== null && !Array.isArray(pi)
178
+ ? (pi as Record<string, unknown>)
179
+ : undefined;
180
+ }
181
+
182
+ /**
183
+ * stat + content 统一缓存。文件不存在/不可读 → null(并驱逐条目)。
184
+ * mtime 未变 → 返回缓存 content(不重 read);变 → readFileSync + 缓存。
185
+ */
186
+ export function getCachedFile(filePath: string): { mtimeMs: number; content: string } | null {
187
+ let mtimeMs: number;
188
+ try {
189
+ mtimeMs = fsSync.statSync(filePath).mtimeMs;
190
+ } catch {
191
+ mtimeCache.delete(filePath);
192
+ return null;
193
+ }
194
+ const entry = mtimeCache.get(filePath);
195
+ if (entry && entry.mtimeMs === mtimeMs) return entry;
196
+ let content: string;
197
+ try {
198
+ content = fsSync.readFileSync(filePath, "utf-8");
199
+ } catch {
200
+ // stat 与 read 之间的删除/EACCES 竞态 → 驱逐并返回 null(exec-review major-2:
201
+ // docstring 承诺「不可读 → null」——readFileSync 也必须入守卫)
202
+ mtimeCache.delete(filePath);
203
+ return null;
204
+ }
205
+ const cached = { mtimeMs, content };
206
+ mtimeCache.set(filePath, cached);
207
+ return cached;
208
+ }
209
+
210
+ /** 便捷封装:只取 content(不存在 → null)。 */
211
+ export function getCachedFileContent(filePath: string): string | null {
212
+ return getCachedFile(filePath)?.content ?? null;
213
+ }
214
+
215
+ // [perf] 解析结果缓存(KV-cache 稳定性改造):外层 key = parse 函数身份,内层 key =
216
+ // path,value = { mtimeMs, parsed }。key 含 parse 身份是正确性要求——同一 path 可能被
217
+ // 不同 parse(agent frontmatter vs workflow meta)解析,单层 path key 会跨 parse 类型
218
+ // 互相污染缓存(先 parse 的结果被 as T 断言返回)。用普通 Map 而非 WeakMap:
219
+ // clearFileCache 需全量清空(测试隔离),WeakMap 不可遍历;parse 函数均为模块级
220
+ // 常量,强引用无泄漏。复用 getCachedFile 的 mtime 判变——mtime 未变时跳过 parse
221
+ // (frontmatter YAML 解析是重建发现时最大的可省 CPU 项)。parse 的确定性结果(含
222
+ // null,如 frontmatter 非法)均可缓存:同一 content 必然解析出同一结果。失效与
223
+ // mtimeCache 同步(clearFileCache)。
224
+ const parsedCache = new Map<
225
+ (content: string) => unknown,
226
+ Map<string, { mtimeMs: number; parsed: unknown }>
227
+ >();
228
+
229
+ /**
230
+ * mtime 级解析结果缓存:mtime 未变返回缓存 parsed,变则经 getCachedFile 取 content
231
+ * 重新 parse 并缓存。文件不存在/不可读 → null(并驱逐条目)。缓存按 parse 函数隔离
232
+ * ——同一 path 的不同 parse 互不污染。
233
+ */
234
+ export function getCachedParsed<T>(filePath: string, parse: (content: string) => T): T | null {
235
+ const file = getCachedFile(filePath);
236
+ let perParse = parsedCache.get(parse);
237
+ if (!file) {
238
+ perParse?.delete(filePath);
239
+ return null;
240
+ }
241
+ if (!perParse) {
242
+ perParse = new Map();
243
+ parsedCache.set(parse, perParse);
244
+ }
245
+ const entry = perParse.get(filePath);
246
+ if (entry && entry.mtimeMs === file.mtimeMs) return entry.parsed as T;
247
+ const parsed = parse(file.content);
248
+ perParse.set(filePath, { mtimeMs: file.mtimeMs, parsed });
249
+ return parsed;
250
+ }
251
+
252
+ /** 清空(invalidateCache 语义——测试隔离 + mtime 漏判场景手动刷新兜底)。 */
253
+ export function clearFileCache(): void {
254
+ mtimeCache.clear();
255
+ workspaceRootCache.clear();
256
+ manifestCache.clear();
257
+ for (const perParse of parsedCache.values()) perParse.clear();
258
+ }
259
+
260
+ export function findWorkspaceRoot(cwd?: string): string {
261
+ const dir = cwd ?? process.cwd();
262
+ const memo = workspaceRootCache.get(dir);
263
+ if (memo !== undefined) return memo;
264
+ const root = computeWorkspaceRoot(dir);
265
+ workspaceRootCache.set(dir, root);
266
+ return root;
267
+ }
268
+
269
+ function computeWorkspaceRoot(dir: string): string {
270
+ const root = resolve("/");
271
+
272
+ // Phase 1: bare repo 优先——先全路径扫一遍找 .bare
273
+ let probe = dir;
274
+ for (let i = 0; i < WORKSPACE_ROOT_MAX_DEPTH; i++) {
275
+ if (fsSync.existsSync(resolve(probe, ".bare"))) {
276
+ // worktree 是 workspace 根的直接子目录。若 cwd 自身有 .pi/,优先用 cwd
277
+ if (probe !== dir && isDirectChildOfWorkspaceRoot(dir, probe) && fsSync.existsSync(resolve(dir, ".pi"))) {
278
+ return dir;
279
+ }
280
+ return probe;
281
+ }
282
+ if (probe === root) break;
283
+ probe = resolve(probe, "..");
284
+ }
285
+
286
+ // Phase 2: 无 .bare 时,找最顶层的 .git
287
+ let topLevel = dir;
288
+ probe = dir;
289
+ for (let i = 0; i < WORKSPACE_ROOT_MAX_DEPTH; i++) {
290
+ if (fsSync.existsSync(resolve(probe, ".git"))) {
291
+ topLevel = probe;
292
+ }
293
+ if (probe === root) break;
294
+ probe = resolve(probe, "..");
295
+ }
296
+ if (topLevel !== dir) {
297
+ return topLevel;
298
+ }
299
+
300
+ // Phase 3: fallback——用第一个遇到的 .pi
301
+ probe = dir;
302
+ for (let i = 0; i < WORKSPACE_ROOT_MAX_DEPTH; i++) {
303
+ if (fsSync.existsSync(resolve(probe, ".pi"))) {
304
+ return probe;
305
+ }
306
+ if (probe === root) break;
307
+ probe = resolve(probe, "..");
308
+ }
309
+
310
+ return dir;
311
+ }
312
+
313
+ // ── 文件扩展名判定 ───────────────────────────────────────────
314
+
315
+ /** 根据资源种类判定脚本文件扩展名 */
316
+ function isTargetFile(name: string, kind: ResourceKind): boolean {
317
+ // _ 前缀 = draft/示例,不参与发现(与原 agent-registry/workflow 约定一致)
318
+ if (name.startsWith("_")) return false;
319
+ if (kind === "agents") {
320
+ return name.endsWith(".md") && !name.endsWith(".chain.md");
321
+ }
322
+ // workflows
323
+ return name.endsWith(".js") || name.endsWith(".mjs");
324
+ }
325
+
326
+ /** 提取文件名 stem(去目录去扩展名) */
327
+ function stem(filePath: string): string {
328
+ const base = filePath.split("/").pop() ?? filePath;
329
+ const dot = base.lastIndexOf(".");
330
+ return dot > 0 ? base.slice(0, dot) : base;
331
+ }
332
+
333
+ // ── 目录扫描 ─────────────────────────────────────────────────
334
+
335
+ /**
336
+ * 扫描单个目录下的资源文件。
337
+ * 返回文件绝对路径列表。目录不存在时返回空数组。
338
+ */
339
+ async function scanDirectory(dirPath: string, kind: ResourceKind): Promise<string[]> {
340
+ try {
341
+ await access(dirPath);
342
+ } catch {
343
+ return [];
344
+ }
345
+
346
+ const entries = await readdir(dirPath, { withFileTypes: true });
347
+ const files: string[] = [];
348
+ for (const e of entries) {
349
+ if (!isTargetFile(e.name, kind)) continue;
350
+ const absPath = resolve(dirPath, e.name);
351
+ // symlink 单独处理:Dirent.isFile() 对 symlink 返回 false
352
+ if (e.isFile()) {
353
+ files.push(absPath);
354
+ } else if (e.isSymbolicLink()) {
355
+ const targetStat = await stat(absPath).catch(() => null);
356
+ if (targetStat?.isFile()) files.push(absPath);
357
+ }
358
+ }
359
+ return files;
360
+ }
361
+
362
+ // ── npm/dev 包内 manifest 发现 ───────────────────────────────
363
+
364
+ /**
365
+ * 读取 package.json 的 pi.{kind} manifest(pi.agents / pi.workflows)。
366
+ * 返回 undefined 表示无 manifest 声明(无 pi / pi[kind] 非数组 / 解析失败)。
367
+ * 内部走 manifestCache(与 readPackageManifestSync 共享同一 Map,失败语义见声明处)。
368
+ */
369
+ async function readPackageManifest(pkgDir: string, kind: ResourceKind): Promise<string[] | undefined> {
370
+ const pkgJsonPath = resolve(pkgDir, "package.json");
371
+ let mtimeMs: number;
372
+ try {
373
+ mtimeMs = (await stat(pkgJsonPath)).mtimeMs;
374
+ } catch {
375
+ // stat 失败:文件不存在/不可 stat → 驱逐条目(对齐 getCachedFile 语义)
376
+ manifestCache.delete(pkgJsonPath);
377
+ return undefined;
378
+ }
379
+ const entry = manifestCache.get(pkgJsonPath);
380
+ if (entry && entry.mtimeMs === mtimeMs) return piToManifest(entry.pi, kind);
381
+ let pi: Record<string, unknown> | undefined;
382
+ try {
383
+ const content = await readFile(pkgJsonPath, "utf-8");
384
+ pi = parsePiField(content);
385
+ } catch {
386
+ // read 失败或坏 JSON → 不缓存不驱逐已有好条目,下次调用重试
387
+ return undefined;
388
+ }
389
+ manifestCache.set(pkgJsonPath, { mtimeMs, pi });
390
+ return piToManifest(pi, kind);
391
+ }
392
+
393
+ /**
394
+ * 处理单个 npm/dev 包:按 manifest 或约定目录发现资源。
395
+ *
396
+ * 规则:
397
+ * - 有 manifest → 只按 manifest 声明路径加载。路径不存在 → 整包失败(返回 available=false 占位)
398
+ * - 无 manifest → 扫约定目录 {kind}/(agents/ 或 workflows/)
399
+ */
400
+ async function processPackage(
401
+ pkgDir: string,
402
+ kind: ResourceKind,
403
+ ): Promise<DiscoveredResource[]> {
404
+ const manifestPaths = await readPackageManifest(pkgDir, kind);
405
+
406
+ // manifest 模式:只按声明路径加载,路径不存在则整包失败
407
+ if (manifestPaths && manifestPaths.length > 0) {
408
+ const results: DiscoveredResource[] = [];
409
+ let allFailed = true;
410
+
411
+ for (const relPath of manifestPaths) {
412
+ const absPath = resolve(pkgDir, relPath);
413
+ const fileStat = await stat(absPath).catch(() => null);
414
+ if (!fileStat) {
415
+ // manifest 声明的路径不存在 → 记录失败占位(路径存在性校验)
416
+ results.push({ path: absPath, source: "npm", available: false });
417
+ continue;
418
+ }
419
+
420
+ if (fileStat.isDirectory()) {
421
+ const files = await scanDirectory(absPath, kind);
422
+ for (const f of files) {
423
+ results.push({ path: f, source: "npm", available: true });
424
+ allFailed = false;
425
+ }
426
+ } else if (fileStat.isFile()) {
427
+ results.push({ path: absPath, source: "npm", available: true });
428
+ allFailed = false;
429
+ }
430
+ }
431
+
432
+ // manifest 全失败:返回 available=false 占位,不 fallback 到约定目录
433
+ if (allFailed) {
434
+ return results;
435
+ }
436
+ return results;
437
+ }
438
+
439
+ // 无 manifest:扫约定目录 {kind}/
440
+ const conventionDir = resolve(pkgDir, kind);
441
+ const files = await scanDirectory(conventionDir, kind);
442
+ return files.map((f) => ({ path: f, source: "npm", available: true }));
443
+ }
444
+
445
+ /**
446
+ * 扫描 npm node_modules 目录下所有包的资源。
447
+ * 支持 scoped(@scope/pkg)和 unscoped(pkg)包。
448
+ */
449
+ async function scanNpmDir(
450
+ nodeModulesDir: string,
451
+ kind: ResourceKind,
452
+ ): Promise<DiscoveredResource[]> {
453
+ let entries: string[];
454
+ try {
455
+ entries = await readdir(nodeModulesDir);
456
+ } catch {
457
+ return [];
458
+ }
459
+
460
+ // [perf] 包级并行(swf-perf-impl cleanup TC2/IF2):entries.map + Promise.all,
461
+ // perEntry.flat() 按原 readdir 序 concat——输出序与串行逐包 push 等价。
462
+ // 不加新增 catch:每包失败面由 processPackage 内部既有 catch 承担,未捕获异常
463
+ // 传播语义与串行版一致(Promise.all 整体 reject ↔ 串行版向上抛)。
464
+ const perEntry = await Promise.all(
465
+ entries.map(async (entry): Promise<DiscoveredResource[]> => {
466
+ const entryPath = resolve(nodeModulesDir, entry);
467
+
468
+ if (entry.startsWith("@")) {
469
+ // scoped 包——迭代子包(子包级同样并行,flat 保序)
470
+ let scopedEntries: string[];
471
+ try {
472
+ scopedEntries = await readdir(entryPath);
473
+ } catch {
474
+ return [];
475
+ }
476
+ const perScoped = await Promise.all(
477
+ scopedEntries.map((scopedPkg) => processPackage(resolve(entryPath, scopedPkg), kind)),
478
+ );
479
+ return perScoped.flat();
480
+ }
481
+ // unscoped 包
482
+ return processPackage(entryPath, kind);
483
+ }),
484
+ );
485
+
486
+ return perEntry.flat();
487
+ }
488
+
489
+ // ── 扫描源构建 ───────────────────────────────────────────────
490
+
491
+ /** 扫描源定义:路径 + source 标签 */
492
+ interface ScanTarget {
493
+ dir: string;
494
+ source: ResourceSource;
495
+ /** 该源是否参与本次扫描(如 tmp 仅 workflow 启用) */
496
+ enabled: boolean;
497
+ }
498
+
499
+ /**
500
+ * 读取 XYZ_EXTENSION_PATHS 环境变量(dev-link 写入的扩展源码路径)。
501
+ *
502
+ * delimiter 分隔(POSIX ':' / Windows ';'),trim + 过滤空 + ~ 展开。
503
+ * 每个路径是一个 extension 包目录(dev-link 指向源码),走 processPackage 发现其
504
+ * agents/workflows。解析逻辑与 extension-service.getUserExtensionPaths() 一致。
505
+ */
506
+ function readExtensionPaths(): string[] {
507
+ const raw = process.env.XYZ_EXTENSION_PATHS;
508
+ if (!raw) return [];
509
+ const paths = raw
510
+ .split(delimiter)
511
+ .map((p) => p.trim())
512
+ .filter((p) => p.length > 0)
513
+ .map((p) => (p.startsWith("~") ? join(homedir(), p.slice(1)) : p));
514
+ return [...new Set(paths)];
515
+ }
516
+
517
+ /**
518
+ * 构建所有扫描源(按优先级低→高排列)。
519
+ *
520
+ * agent 和 workflow 共享相同的前缀体系。宿主注入根(config.hostRoots)按 source
521
+ * 标签查表填充三个槽位条目——标签字面是 core 编排的槽位键(同时是 ResourceSource
522
+ * 枚举成员,报告输出与历史值逐字一致);宿主未提供某标签根时该条目跳过(该源在
523
+ * 遮蔽序中自然缺席),其余条目(core 自建:homedir/env/workspaceRoot 推导)留原位
524
+ * 原序,遮蔽优先级语义与注入化前逐字一致。
525
+ */
526
+ function buildScanTargets(config: ScanConfig): ScanTarget[] {
527
+ const { kind, workspaceRoot, hostRoots, includeTmp } = config;
528
+ const home = homedir();
529
+
530
+ // 同标签多条目时取列表靠后者(hostRoots 按优先级低→高,靠后 = 宿主侧更高优先级)
531
+ const hostDirBySource = new Map<string, string>(
532
+ hostRoots.map((root): [string, string] => [root.source, root.dir]),
533
+ );
534
+
535
+ const targets: ScanTarget[] = [];
536
+ // 1. user .pi/agent/{kind}/(宿主注入,pi 壳 source "user-pi")
537
+ const userPiDir = hostDirBySource.get("user-pi");
538
+ if (userPiDir !== undefined) {
539
+ targets.push({ dir: userPiDir, source: "user-pi", enabled: true });
540
+ }
541
+ // 2. user .agents/{kind}/(core 自建:homedir 推导,非宿主依赖)
542
+ targets.push({ dir: join(home, ".agents", kind), source: "user-agents", enabled: true });
543
+ // 3. npm global: <agentDir>/npm/node_modules/*/<pkg>/(宿主注入,pi 壳 source "npm")
544
+ const npmDir = hostDirBySource.get("npm");
545
+ if (npmDir !== undefined) {
546
+ targets.push({ dir: npmDir, source: "npm", enabled: true });
547
+ }
548
+ // 4. npm dev symlink: <agentDir>/extensions/*/<pkg>/(宿主注入,pi 壳 source "npm-dev")
549
+ const npmDevDir = hostDirBySource.get("npm-dev");
550
+ if (npmDevDir !== undefined) {
551
+ targets.push({ dir: npmDevDir, source: "npm-dev", enabled: true });
552
+ }
553
+ // user extension paths (XYZ_EXTENSION_PATHS, dev-link): each path is a package dir,
554
+ // 走 processPackage 读 pi.{kind} manifest 或扫 {kind}/ 目录。优先级高于 npm/npm-dev
555
+ // (dev-link 是开发版 override),低于 project(项目正式资源优先)。
556
+ targets.push(
557
+ ...readExtensionPaths().map((dir) => ({ dir, source: "user-extension-paths" as const, enabled: true })),
558
+ );
559
+ // 5. project .pi/{kind}/
560
+ targets.push({ dir: join(workspaceRoot, ".pi", kind), source: "project-pi", enabled: true });
561
+
562
+ // 6. project .pi/{kind}/.tmp/(仅 workflow)
563
+ if (includeTmp) {
564
+ targets.push({
565
+ dir: join(workspaceRoot, ".pi", kind, ".tmp"),
566
+ source: "project-pi-tmp",
567
+ enabled: true,
568
+ });
569
+ }
570
+
571
+ // 7. project .agents/{kind}/
572
+ targets.push({
573
+ dir: join(workspaceRoot, ".agents", kind),
574
+ source: "project-agents",
575
+ enabled: true,
576
+ });
577
+
578
+ return targets.filter((t) => t.enabled);
579
+ }
580
+
581
+ // ── 公共 API ─────────────────────────────────────────────────
582
+
583
+ /**
584
+ * 发现所有资源文件(agent .md 或 workflow .js/.mjs)。
585
+ *
586
+ * 按优先级低→高扫描所有源,同名资源靠后覆盖靠前(last-writer-wins)。
587
+ * npm/dev 包内:有 manifest 只走 manifest(路径不存在则失败),无 manifest 扫约定目录。
588
+ *
589
+ * Throws on unrecoverable scan errors——未捕获异常向上抛(Promise.all 首个 reject
590
+ * 即整体拒绝,与串行版 discoverResourcesSync 的传播语义一致,见实现内 [perf] 注释)。
591
+ * 预期失败不抛:目录不存在/不可读返回空列表,manifest 声明路径缺失以 available=false 返回。
592
+ *
593
+ * @returns 去重后的资源列表(按优先级合并,高优先级覆盖低优先级同名)
594
+ */
595
+ export async function discoverResources(config: ScanConfig): Promise<DiscoveredResource[]> {
596
+ const targets = buildScanTargets(config);
597
+
598
+ // [perf] 源级并行(swf-perf-impl cleanup TC2/IF2):targets.map + Promise.all,
599
+ // Promise.all 对 map 数组保序——allBySource 顺序 = targets 优先级序(低→高),
600
+ // 与串行逐源 push 完全一致 → 下方合并去重逻辑零改动、输出逐字节等价。
601
+ // 不加新增 catch:每源预期失败路径由内部既有 catch 面承担(scanDirectory access
602
+ // catch / scanNpmDir readdir catch / processPackage 全链 catch),未捕获异常
603
+ // 传播语义与串行版等价(ES2:串行版 target k 抛错中断后续源,并行版全部源已并发
604
+ // 启动、以首个 reject 拒绝——两版对调用方同为 discoverResources 抛出,各源只读无副作用)。
605
+ const allBySource: Array<{ source: ResourceSource; resources: DiscoveredResource[] }> = await Promise.all(
606
+ targets.map(async (target) => {
607
+ if (target.source === "npm" || target.source === "npm-dev") {
608
+ // npm/dev 目录:迭代包,走 manifest 或约定目录
609
+ const resources = await scanNpmDir(target.dir, config.kind);
610
+ // 覆盖 source 标签(scanNpmDir 内部统一标 "npm",这里修正为实际源)
611
+ const tagged = resources.map((r) => ({ ...r, source: target.source }));
612
+ return { source: target.source, resources: tagged };
613
+ }
614
+ if (target.source === "user-extension-paths") {
615
+ // XYZ_EXTENSION_PATHS(dev-link):每个 dir 是单个包目录,走 processPackage
616
+ const resources = await processPackage(target.dir, config.kind);
617
+ const tagged = resources.map((r) => ({ ...r, source: target.source }));
618
+ return { source: target.source, resources: tagged };
619
+ }
620
+ // 普通目录:直接扫
621
+ const files = await scanDirectory(target.dir, config.kind);
622
+ const resources = files.map((f) => ({ path: f, source: target.source, available: true }));
623
+ return { source: target.source, resources };
624
+ }),
625
+ );
626
+
627
+ // 按优先级合并:targets 数组顺序即优先级(低→高),高优先级后写覆盖
628
+ // 用文件名 stem 作为去重 key(与旧逻辑一致:同名资源高优先级覆盖)
629
+ const merged = new Map<string, DiscoveredResource>();
630
+
631
+ for (const { resources } of allBySource) {
632
+ for (const r of resources) {
633
+ const key = stem(r.path);
634
+ const existing = merged.get(key);
635
+ // available=false 的占位不覆盖已有的 available=true
636
+ if (!r.available && existing) {
637
+ continue;
638
+ }
639
+ // [D8d] 同名遮蔽可观测:高优先级源覆盖低优先级同名资源时分级报告——
640
+ // 机器源重复是安装拓扑常态(npm 包与用户目录结构性同名),降 debug 默认静默
641
+ // (XYZ_AGENT_DEBUG=1 文件日志可查);双用户源重复是配置错误,保留 warn 首报。
642
+ // warn 路径进程内去重(Set cap 1024,对齐 ui-request-observability 范式):
643
+ // 每 session 独立进程(process-manager.ts L142-143),进程级去重 ≈ session 级首报。
644
+ if (existing && existing.path !== r.path) {
645
+ const msg =
646
+ `[resource-discovery] duplicate ${config.kind} "${key}" from ${r.source} shadows ${existing.source}`;
647
+ const data = { shadowed: existing.path, kept: r.path };
648
+ if (isMachineSource(existing.source) || isMachineSource(r.source)) {
649
+ // 任一侧为机器源 → 降级 debug(安装拓扑常态,排查走 XYZ_AGENT_DEBUG=1)
650
+ logger.debug(msg, data);
651
+ } else {
652
+ // 双侧均为用户源 → 保持 warn,进程内去重(同 key 只报首次)
653
+ const dedupKey = `${config.kind}|${key}|${existing.path}|${r.path}`;
654
+ if (!shadowWarnDedup.has(dedupKey)) {
655
+ if (shadowWarnDedup.size >= MAX_SHADOW_WARN_DEDUP) {
656
+ shadowWarnDedup.clear();
657
+ }
658
+ shadowWarnDedup.add(dedupKey);
659
+ logger.warn(msg, data);
660
+ }
661
+ }
662
+ }
663
+ merged.set(key, r);
664
+ }
665
+ }
666
+
667
+ return Array.from(merged.values());
668
+ }
669
+
670
+ /**
671
+ * 同步版:扫描单个目录下的资源文件路径(供 agent-registry 的 mtime 缓存模式使用)。
672
+ *
673
+ * agent .md 发现需要 mtime 缓存(hot-reload),不能走 async 全量扫描。
674
+ * 此函数提供目录级同步扫描,npm/dev 包内发现仍需 async(agent-registry 用 builtin 兜底)。
675
+ */
676
+ export function scanDirectorySync(dirPath: string, kind: ResourceKind): string[] {
677
+ try {
678
+ fsSync.accessSync(dirPath);
679
+ } catch {
680
+ return [];
681
+ }
682
+
683
+ let entries: string[];
684
+ try {
685
+ entries = fsSync.readdirSync(dirPath);
686
+ } catch {
687
+ return [];
688
+ }
689
+
690
+ const files: string[] = [];
691
+ for (const entry of entries) {
692
+ if (!isTargetFile(entry, kind)) continue;
693
+ files.push(resolve(dirPath, entry));
694
+ }
695
+ return files;
696
+ }
697
+
698
+ /**
699
+ * 同步版:读取 package.json 的 pi.{kind} manifest。
700
+ * 供 agent-registry 同步路径使用。与 async 版共享 manifestCache(双读者同一 Map)。
701
+ */
702
+ export function readPackageManifestSync(pkgDir: string, kind: ResourceKind): string[] | undefined {
703
+ const pkgJsonPath = resolve(pkgDir, "package.json");
704
+ let mtimeMs: number;
705
+ try {
706
+ mtimeMs = fsSync.statSync(pkgJsonPath).mtimeMs;
707
+ } catch {
708
+ // stat 失败:文件不存在/不可 stat → 驱逐条目(对齐 getCachedFile 语义)
709
+ manifestCache.delete(pkgJsonPath);
710
+ return undefined;
711
+ }
712
+ const entry = manifestCache.get(pkgJsonPath);
713
+ if (entry && entry.mtimeMs === mtimeMs) return piToManifest(entry.pi, kind);
714
+ let pi: Record<string, unknown> | undefined;
715
+ try {
716
+ const content = fsSync.readFileSync(pkgJsonPath, "utf-8");
717
+ pi = parsePiField(content);
718
+ } catch {
719
+ // read 失败或坏 JSON → 不缓存不驱逐已有好条目,下次调用重试
720
+ return undefined;
721
+ }
722
+ manifestCache.set(pkgJsonPath, { mtimeMs, pi });
723
+ return piToManifest(pi, kind);
724
+ }
725
+
726
+ /**
727
+ * 同步版:处理单个 npm/dev 包。
728
+ * 供 agent-registry 同步路径使用。
729
+ */
730
+ export function processPackageSync(pkgDir: string, kind: ResourceKind): DiscoveredResource[] {
731
+ const manifestPaths = readPackageManifestSync(pkgDir, kind);
732
+
733
+ if (manifestPaths && manifestPaths.length > 0) {
734
+ const results: DiscoveredResource[] = [];
735
+
736
+ for (const relPath of manifestPaths) {
737
+ const absPath = resolve(pkgDir, relPath);
738
+ let fileStat: fsSync.Stats | null;
739
+ try {
740
+ fileStat = fsSync.statSync(absPath);
741
+ } catch {
742
+ fileStat = null;
743
+ }
744
+ if (!fileStat) {
745
+ results.push({ path: absPath, source: "npm", available: false });
746
+ continue;
747
+ }
748
+
749
+ if (fileStat.isDirectory()) {
750
+ const files = scanDirectorySync(absPath, kind);
751
+ for (const f of files) {
752
+ results.push({ path: f, source: "npm", available: true });
753
+ }
754
+ } else if (fileStat.isFile()) {
755
+ results.push({ path: absPath, source: "npm", available: true });
756
+ }
757
+ }
758
+
759
+ return results;
760
+ }
761
+
762
+ // 无 manifest:扫约定目录
763
+ const conventionDir = resolve(pkgDir, kind);
764
+ const files = scanDirectorySync(conventionDir, kind);
765
+ return files.map((f) => ({ path: f, source: "npm", available: true }));
766
+ }
767
+
768
+ /**
769
+ * 同步版:扫描 npm node_modules 目录。
770
+ * 供 agent-registry 同步路径使用。
771
+ */
772
+ export function scanNpmDirSync(nodeModulesDir: string, kind: ResourceKind): DiscoveredResource[] {
773
+ let entries: string[];
774
+ try {
775
+ entries = fsSync.readdirSync(nodeModulesDir);
776
+ } catch {
777
+ return [];
778
+ }
779
+
780
+ const results: DiscoveredResource[] = [];
781
+ for (const entry of entries) {
782
+ const entryPath = resolve(nodeModulesDir, entry);
783
+
784
+ if (entry.startsWith("@")) {
785
+ let scopedEntries: string[];
786
+ try {
787
+ scopedEntries = fsSync.readdirSync(entryPath);
788
+ } catch {
789
+ continue;
790
+ }
791
+ for (const scopedPkg of scopedEntries) {
792
+ const scopedPkgDir = resolve(entryPath, scopedPkg);
793
+ results.push(...processPackageSync(scopedPkgDir, kind));
794
+ }
795
+ } else {
796
+ results.push(...processPackageSync(entryPath, kind));
797
+ }
798
+ }
799
+ return results;
800
+ }
801
+
802
+ /**
803
+ * 同步版:发现所有资源(agent-registry 专用,支持 mtime 缓存的 hot-reload)。
804
+ *
805
+ * 与 discoverResources 对应的同步实现,扫描相同的源。
806
+ * 返回所有源的资源(未去重,调用方按需处理优先级)。
807
+ */
808
+ export function discoverResourcesSync(config: ScanConfig): DiscoveredResource[] {
809
+ const targets = buildScanTargets(config);
810
+ const all: DiscoveredResource[] = [];
811
+
812
+ for (const target of targets) {
813
+ if (target.source === "npm" || target.source === "npm-dev") {
814
+ const resources = scanNpmDirSync(target.dir, config.kind);
815
+ all.push(...resources.map((r) => ({ ...r, source: target.source })));
816
+ } else if (target.source === "user-extension-paths") {
817
+ // XYZ_EXTENSION_PATHS(dev-link):每个 dir 是单个包目录
818
+ const resources = processPackageSync(target.dir, config.kind);
819
+ all.push(...resources.map((r) => ({ ...r, source: target.source })));
820
+ } else {
821
+ const files = scanDirectorySync(target.dir, config.kind);
822
+ all.push(...files.map((f) => ({ path: f, source: target.source, available: true })));
823
+ }
824
+ }
825
+
826
+ // 按优先级合并(targets 顺序 = 优先级低→高)
827
+ const merged = new Map<string, DiscoveredResource>();
828
+ for (const r of all) {
829
+ const key = stem(r.path);
830
+ if (!r.available && merged.has(key)) continue;
831
+ merged.set(key, r);
832
+ }
833
+
834
+ return Array.from(merged.values());
835
+ }