@zhushanwen/subagent-core 0.5.1 → 0.7.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 (318) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-KXVBIVOU.js +16 -0
  3. package/dist/chunk-T2SYBW3I.js +24 -0
  4. package/dist/chunk-VURUAGMM.js +2305 -0
  5. package/dist/{types-Dv4QhSJ_.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +835 -668
  6. package/dist/{types-Dv4QhSJ_.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +835 -668
  7. package/dist/execution/engine/engine-discovery-scan.cjs +2210 -0
  8. package/dist/execution/engine/engine-discovery-scan.d.cts +4 -0
  9. package/dist/execution/engine/engine-discovery-scan.d.ts +4 -0
  10. package/dist/execution/engine/engine-discovery-scan.js +20 -0
  11. package/dist/execution/engine/paths.cjs +6 -23
  12. package/dist/execution/engine/paths.d.cts +1 -8
  13. package/dist/execution/engine/paths.d.ts +1 -8
  14. package/dist/execution/engine/paths.js +1 -1
  15. package/dist/execution/relay-env.cjs +12 -28
  16. package/dist/execution/relay-env.d.cts +1 -33
  17. package/dist/execution/relay-env.d.ts +1 -33
  18. package/dist/execution/relay-env.js +5 -3
  19. package/dist/index.cjs +14123 -15467
  20. package/dist/index.d.cts +1179 -1540
  21. package/dist/index.d.ts +1179 -1540
  22. package/dist/index.js +7694 -10986
  23. package/dist.bundle/index.cjs +33485 -0
  24. package/package.json +20 -26
  25. package/src/__tests__/record-store-last-line.test.ts +16 -7
  26. package/src/core/error-message.ts +7 -6
  27. package/src/core/host-services.ts +8 -2
  28. package/src/core/notify-ports.ts +17 -2
  29. package/src/execution/__tests__/agent-registry.test.ts +1 -1
  30. package/src/execution/__tests__/alive-store.test.ts +1 -1
  31. package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
  32. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
  33. package/src/execution/__tests__/collect-budget.test.ts +291 -0
  34. package/src/execution/__tests__/collect-coordinator-service.test.ts +316 -0
  35. package/src/execution/__tests__/collect-coordinator.test.ts +371 -0
  36. package/src/execution/__tests__/collect-mixed-dispatch.test.ts +229 -0
  37. package/src/execution/__tests__/config-collect-sync.test.ts +120 -0
  38. package/src/execution/__tests__/config.test.ts +1 -1
  39. package/src/execution/__tests__/delivery-methods.test.ts +179 -237
  40. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
  41. package/src/execution/__tests__/execute-nesting.test.ts +20 -13
  42. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +21 -34
  43. package/src/execution/__tests__/finalized-marker.test.ts +1 -1
  44. package/src/execution/__tests__/gc-timer.test.ts +8 -23
  45. package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
  46. package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
  47. package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
  48. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
  49. package/src/execution/__tests__/idle-gc.test.ts +200 -0
  50. package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
  51. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +1 -1
  52. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
  53. package/src/execution/__tests__/notify-batch.test.ts +500 -0
  54. package/src/execution/__tests__/notify-ledger.test.ts +27 -0
  55. package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
  56. package/src/execution/__tests__/record-entry-collect.test.ts +102 -0
  57. package/src/execution/__tests__/record-store.test.ts +60 -6
  58. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
  59. package/src/execution/__tests__/relay-env.test.ts +12 -1
  60. package/src/execution/__tests__/round-supervisor.test.ts +293 -0
  61. package/src/execution/__tests__/session-file-gc.test.ts +1 -1
  62. package/src/execution/__tests__/session-pending.test.ts +119 -17
  63. package/src/execution/__tests__/start-collect-guard.test.ts +255 -0
  64. package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
  65. package/src/execution/__tests__/subagent-actions-core.test.ts +6 -0
  66. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +14 -11
  67. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
  68. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
  69. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
  70. package/src/execution/__tests__/subagent-service.test.ts +2 -2
  71. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
  72. package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
  73. package/src/execution/__tests__/sync-collect-recovery.test.ts +1102 -0
  74. package/src/execution/__tests__/tombstone-store.test.ts +1 -1
  75. package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
  76. package/src/execution/__tests__/worktree-git-ops.test.ts +1 -1
  77. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +2 -2
  78. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +1 -1
  79. package/src/execution/__tests__/worktree-registry.test.ts +1 -1
  80. package/src/execution/agent-registry.ts +6 -2
  81. package/src/execution/collect-coordinator.ts +200 -0
  82. package/src/execution/config.ts +77 -5
  83. package/src/execution/dialog-queue.ts +6 -73
  84. package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
  85. package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
  86. package/src/execution/engine/__tests__/common/event-journal.test.ts +1 -1
  87. package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
  88. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
  89. package/src/execution/engine/__tests__/common/pool-manager.test.ts +2 -2
  90. package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
  91. package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
  92. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
  93. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
  94. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
  95. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
  96. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
  97. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
  98. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
  99. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +82 -55
  100. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
  101. package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
  102. package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
  103. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +8 -3
  104. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
  105. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
  106. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
  107. package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
  108. package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
  109. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
  110. package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
  111. package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
  112. package/src/execution/engine/__tests__/registry.test.ts +206 -1
  113. package/src/execution/engine/__tests__/routing.test.ts +97 -0
  114. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
  115. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
  116. package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
  117. package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
  118. package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
  119. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
  120. package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
  121. package/src/execution/engine/client/client-options.ts +103 -0
  122. package/src/execution/engine/client/engine-client.ts +687 -0
  123. package/src/execution/engine/client/mirror.ts +167 -0
  124. package/src/execution/engine/client/pid-file.ts +392 -0
  125. package/src/execution/engine/client/reaper.ts +95 -0
  126. package/src/execution/engine/client/remote-engine.ts +441 -0
  127. package/src/execution/engine/client/reverse-router.ts +255 -0
  128. package/src/execution/engine/common/capability-gate.ts +109 -22
  129. package/src/execution/engine/common/errors.ts +9 -25
  130. package/src/execution/engine/common/event-journal.ts +4 -50
  131. package/src/execution/engine/common/journal-replay.ts +3 -1
  132. package/src/execution/engine/common/kill-chain.ts +18 -251
  133. package/src/execution/engine/common/nesting-guard.ts +18 -114
  134. package/src/execution/engine/common/pool-manager.ts +4 -0
  135. package/src/execution/engine/common/session-view-service.ts +56 -84
  136. package/src/execution/engine/config.ts +151 -0
  137. package/src/execution/engine/d8-compat.ts +346 -0
  138. package/src/execution/engine/engine-discovery-roots.ts +107 -0
  139. package/src/execution/engine/engine-discovery-scan.ts +269 -0
  140. package/src/execution/engine/engine-discovery.ts +30 -7
  141. package/src/execution/engine/engine-inspect-package.ts +282 -0
  142. package/src/execution/engine/engine-manifest.ts +244 -0
  143. package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
  144. package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
  145. package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
  146. package/src/execution/engine/host/host-bridge.ts +150 -0
  147. package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
  148. package/src/execution/engine/host/pi-host-binding.ts +91 -0
  149. package/src/execution/engine/host/spawned-children.ts +139 -0
  150. package/src/execution/engine/host-task-spec.ts +29 -2
  151. package/src/execution/engine/model-validation.ts +55 -2
  152. package/src/execution/engine/paths.ts +16 -41
  153. package/src/execution/engine/port.ts +48 -2
  154. package/src/execution/engine/registry.ts +153 -34
  155. package/src/execution/engine/routing.ts +225 -52
  156. package/src/execution/engine/types.ts +25 -200
  157. package/src/execution/execution-record.ts +7 -0
  158. package/src/execution/finalize-record.ts +40 -11
  159. package/src/execution/idle-gc.ts +89 -14
  160. package/src/execution/lifecycle-manager.ts +10 -7
  161. package/src/execution/lifecycle-predicates.ts +10 -8
  162. package/src/execution/notifier.ts +234 -25
  163. package/src/execution/notify-host.ts +21 -1
  164. package/src/execution/record-entry.ts +16 -0
  165. package/src/execution/record-store.ts +189 -31
  166. package/src/execution/relay-env.ts +19 -36
  167. package/src/execution/round-supervisor/domain.ts +89 -0
  168. package/src/execution/round-supervisor/index.ts +33 -0
  169. package/src/execution/round-supervisor/notify-accounting.ts +87 -0
  170. package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
  171. package/src/execution/round-supervisor/service-binding.test.ts +661 -0
  172. package/src/execution/round-supervisor/service-binding.ts +273 -0
  173. package/src/execution/round-supervisor/supervisor.ts +426 -0
  174. package/src/execution/session-pending.ts +68 -74
  175. package/src/execution/settled-watchdog.ts +39 -0
  176. package/src/execution/subagent-actions-core.ts +62 -6
  177. package/src/execution/subagent-service.ts +1161 -406
  178. package/src/execution/subprocess-agent-runner.ts +16 -8
  179. package/src/execution/sync-rebuild.ts +87 -0
  180. package/src/execution/types.ts +104 -117
  181. package/src/execution/ui-channels.ts +13 -214
  182. package/src/execution/ui-request-handler-factory.ts +7 -4
  183. package/src/execution/ui-request-observability.ts +2 -2
  184. package/src/execution/workflow-state-root.ts +67 -0
  185. package/src/execution/worktree-git-ops.ts +5 -0
  186. package/src/execution/worktree-manager.ts +11 -1
  187. package/src/index.ts +55 -26
  188. package/src/orchestration/__tests__/config-loader.test.ts +1 -1
  189. package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
  190. package/src/orchestration/__tests__/file-run-store-prune.test.ts +1 -1
  191. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +1 -1
  192. package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +1 -1
  193. package/src/orchestration/__tests__/script-generate.test.ts +5 -5
  194. package/src/orchestration/__tests__/skill-discovery.test.ts +2 -2
  195. package/src/orchestration/__tests__/workflow-files.test.ts +10 -10
  196. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +1 -1
  197. package/src/orchestration/execute-agent-call.ts +1 -1
  198. package/src/orchestration/file-run-store.ts +144 -59
  199. package/src/orchestration/models/types.ts +43 -42
  200. package/src/shared/__tests__/atomic-write.test.ts +1 -1
  201. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +5 -5
  202. package/src/shared/zcode-model-ref.ts +31 -0
  203. package/dist/chunk-43ONBFZX.js +0 -240
  204. package/dist/chunk-A4IVZWVX.js +0 -31
  205. package/dist/chunk-APZY4IME.js +0 -27
  206. package/dist/chunk-V3VHZ2VX.js +0 -59
  207. package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
  208. package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
  209. package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
  210. package/dist/execution/engine/engines/zcode/constants.js +0 -54
  211. package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
  212. package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
  213. package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
  214. package/dist/execution/engine/engines/zcode/reader.js +0 -9
  215. package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
  216. package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
  217. package/src/__tests__/session-runner.test.ts +0 -69
  218. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  219. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
  220. package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
  221. package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
  222. package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
  223. package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
  224. package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
  225. package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
  226. package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
  227. package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
  228. package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
  229. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
  230. package/src/execution/__tests__/output-collector.test.ts +0 -469
  231. package/src/execution/__tests__/pi-invocation.test.ts +0 -134
  232. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
  233. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  234. package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
  235. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
  236. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
  237. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
  238. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  239. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  240. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  241. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
  242. package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
  243. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
  244. package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
  245. package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
  246. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
  247. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
  248. package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
  249. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
  250. package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
  251. package/src/execution/__tests__/spawn-args.test.ts +0 -446
  252. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
  253. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  254. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
  255. package/src/execution/__tests__/spawned-children.test.ts +0 -236
  256. package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
  257. package/src/execution/__tests__/stdin-writer.test.ts +0 -464
  258. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
  259. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  260. package/src/execution/__tests__/timeout-integration.test.ts +0 -616
  261. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  262. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  263. package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
  264. package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
  265. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
  266. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  267. package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
  268. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  269. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
  270. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  271. package/src/execution/engine/common/schema-emulation.ts +0 -189
  272. package/src/execution/engine/common/session-view-projection.ts +0 -51
  273. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
  274. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  275. package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
  276. package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
  277. package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
  278. package/src/execution/engine/engines/pi/output-collector.ts +0 -317
  279. package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
  280. package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
  281. package/src/execution/engine/engines/pi/reader.ts +0 -59
  282. package/src/execution/engine/engines/pi/registration.ts +0 -37
  283. package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
  284. package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
  285. package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
  286. package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
  287. package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
  288. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
  289. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
  290. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
  291. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
  292. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
  293. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
  294. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  295. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
  296. package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
  297. package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
  298. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
  299. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
  300. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
  301. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
  302. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
  303. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
  304. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
  305. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
  306. package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
  307. package/src/execution/engine/engines/zcode/connection.ts +0 -598
  308. package/src/execution/engine/engines/zcode/constants.ts +0 -176
  309. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
  310. package/src/execution/engine/engines/zcode/parser.ts +0 -102
  311. package/src/execution/engine/engines/zcode/preparer.ts +0 -235
  312. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  313. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  314. package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
  315. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
  316. package/src/execution/round-settlement.ts +0 -93
  317. package/src/execution/ui-request-queue.ts +0 -215
  318. package/src/shared/schema-env.ts +0 -44
@@ -0,0 +1,558 @@
1
+ // engine-discovery-scan.test.ts —— [W4] 引擎发现器(impl-plan §2.4 / A4/A12):
2
+ // manifest schema 字段级正反例、三级搜索路径、L1 分隔符与非法条目、engines.json 投影
3
+ // 合流与幂等零写、同 id 覆盖留痕、bin 不可执行标记、ensureEngineDiscovered 补扫。
4
+ //
5
+ // 纪律:fixture 一律 mkdtempSync(tmpdir()) 自建自删(禁真实数据目录);L2 根在用例内
6
+ // 显式传 nodeModuleRoots(屏蔽宿主 node_modules 的真实引擎包污染);env 用注入对象
7
+ // (不动 process.env——vitest.setup 净化面之外零残留)。
8
+
9
+ import * as fs from "node:fs";
10
+ import * as os from "node:os";
11
+ import * as path from "node:path";
12
+
13
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
14
+
15
+ import type { SubagentEnginesFile } from "@xyz-agent/extension-protocol";
16
+
17
+ import { configureCore, resetCoreForTests } from "../../../core/host-services.ts";
18
+ import type { DiscoveryRoot } from "../../../core/host-services.ts";
19
+ import { clearEngines, getEngine, hasEngine, listEngines, registerEngine } from "../registry.ts";
20
+ import type { EnginePort } from "../port.ts";
21
+ import { getEnginesFilePath, syncEnginesFile } from "../engine-discovery.ts";
22
+ import {
23
+ ENGINE_ROOTS_ENV,
24
+ deriveNodeModuleRoots,
25
+ discoverAndRegisterEngines,
26
+ ensureEngineDiscovered,
27
+ parseEngineRootsEnv,
28
+ scanEngines,
29
+ } from "../engine-discovery-scan.ts";
30
+
31
+ // ── fixture helpers ─────────────────────────────────────────────
32
+
33
+ /** 可执行 bin 脚本内容(POSIX shebang;发现器只做 accessSync(X_OK),不执行)。 */
34
+ const BIN_CONTENT = "#!/bin/sh\nexit 0\n";
35
+
36
+ interface PkgOptions {
37
+ /** bin 文件权限;缺省 0o755(可执行)。0o644 用于「不可执行」反例。 */
38
+ binMode?: number;
39
+ /** bin 文件不落盘(bin 缺失反例)。 */
40
+ omitBin?: boolean;
41
+ }
42
+
43
+ /** 在 rootDir 下造一个带 manifest 的引擎包,返回包目录。 */
44
+ function makeEnginePkg(
45
+ rootDir: string,
46
+ pkgDirName: string,
47
+ manifest: Record<string, unknown>,
48
+ opts: PkgOptions = {},
49
+ ): string {
50
+ const pkgDir = path.join(rootDir, pkgDirName);
51
+ fs.mkdirSync(pkgDir, { recursive: true });
52
+ const binRel = "./bin/cli.mjs";
53
+ fs.writeFileSync(
54
+ path.join(pkgDir, "package.json"),
55
+ JSON.stringify({
56
+ name: `test-${pkgDirName}`,
57
+ bin: { "test-engine-cli": binRel },
58
+ "xyz-agent": { subagentEngine: { id: pkgDirName, bin: "test-engine-cli", protocol: 1, ...manifest } },
59
+ }),
60
+ );
61
+ if (!opts.omitBin) {
62
+ const binPath = path.join(pkgDir, binRel);
63
+ fs.mkdirSync(path.dirname(binPath), { recursive: true });
64
+ fs.writeFileSync(binPath, BIN_CONTENT);
65
+ fs.chmodSync(binPath, opts.binMode ?? 0o755);
66
+ }
67
+ return pkgDir;
68
+ }
69
+
70
+ /** 收集宿主日志的 HostServices 注入(warn/debug 留痕断言用)。 */
71
+ function makeLogCollector(): { logs: Array<{ level: string; message: string }>; host: Parameters<typeof configureCore>[0] } {
72
+ const logs: Array<{ level: string; message: string }> = [];
73
+ const host = {
74
+ dataRoot: () => "/test-data-root",
75
+ log: (level: string, _component: string, message: string) => {
76
+ logs.push({ level, message });
77
+ },
78
+ };
79
+ return { logs, host: host as unknown as Parameters<typeof configureCore>[0] };
80
+ }
81
+
82
+ /** 扫描选项基线:屏蔽 L2(宿主 node_modules 污染)、env 注入(roots 走 L1 env 根)、
83
+ * 数据根走 env。 */
84
+ function scanOpts(overrides: {
85
+ env?: Record<string, string | undefined>;
86
+ /** L1 发现根(经 ENGINE_ROOTS_ENV 注入;与 overrides.env 拼接)。 */
87
+ roots?: string[];
88
+ nodeModuleRoots?: string[];
89
+ extraRoots?: DiscoveryRoot[];
90
+ agentDir?: string;
91
+ }) {
92
+ const env: Record<string, string | undefined> = {
93
+ XYZ_AGENT_DATA_DIR: "/test-data-dir",
94
+ ...overrides.env,
95
+ };
96
+ if (overrides.roots !== undefined) {
97
+ env[ENGINE_ROOTS_ENV] = overrides.roots.join(path.delimiter);
98
+ }
99
+ return {
100
+ hostKind: "test",
101
+ env,
102
+ nodeModuleRoots: overrides.nodeModuleRoots ?? [],
103
+ ...(overrides.extraRoots !== undefined ? { extraRoots: overrides.extraRoots } : {}),
104
+ ...(overrides.agentDir !== undefined ? { agentDir: overrides.agentDir } : {}),
105
+ };
106
+ }
107
+
108
+ function stubEngine(id: string): EnginePort {
109
+ return {
110
+ id,
111
+ capabilities: () => ({
112
+ schemaEnforcement: "emulated",
113
+ steer: "unsupported",
114
+ conversation: "unsupported",
115
+ personaInjection: "prompt",
116
+ eventGranularity: "coarse",
117
+ sandbox: "none",
118
+ sessionRead: "outcome-only",
119
+ resume: "unsupported",
120
+ interrupt: "kill-only",
121
+ permissionMode: "ignored",
122
+ maxTurns: false,
123
+ }),
124
+ probe: async () => ({ ok: true, engineVersion: "test", checks: [] }),
125
+ run: async () => {
126
+ throw new Error("unused");
127
+ },
128
+ interact: async () => {
129
+ throw new Error("unused");
130
+ },
131
+ read: async () => ({ engineId: id, turns: [], source: "outcome-only" }),
132
+ };
133
+ }
134
+
135
+ const FULL_CAPABILITIES = {
136
+ schemaEnforcement: "native",
137
+ steer: "native",
138
+ conversation: "native",
139
+ personaInjection: "file",
140
+ eventGranularity: "stream",
141
+ sandbox: "native",
142
+ sessionRead: "full",
143
+ resume: "native",
144
+ interrupt: "native",
145
+ permissionMode: "native",
146
+ maxTurns: true,
147
+ };
148
+
149
+ let tmpRoot: string;
150
+ let collectedLogs: Array<{ level: string; message: string }>;
151
+
152
+ beforeEach(() => {
153
+ tmpRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "engine-scan-"));
154
+ const collector = makeLogCollector();
155
+ collectedLogs = collector.logs;
156
+ configureCore(collector.host);
157
+ });
158
+
159
+ afterEach(() => {
160
+ resetCoreForTests();
161
+ clearEngines();
162
+ fs.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
163
+ });
164
+
165
+ // ── manifest schema 字段级(§2.4)────────────────────────────────
166
+
167
+ describe("manifest schema 字段级解析", () => {
168
+ it("正例:完整 manifest → discovered,capabilities/modelCatalog/displayName 原样入快照", () => {
169
+ makeEnginePkg(tmpRoot, "foo", {
170
+ displayName: "Foo Engine",
171
+ description: "test engine",
172
+ envPrefixes: ["FOO_"],
173
+ modelCatalog: { dynamic: false, models: [{ id: "m1", aliases: ["one"], canonicalRef: "prov/m1" }] },
174
+ capabilities: FULL_CAPABILITIES,
175
+ });
176
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
177
+ expect(result.discovered).toHaveLength(1);
178
+ const entry = result.discovered[0];
179
+ expect(entry.id).toBe("foo");
180
+ expect(entry.descriptor.kind).toBe("cli");
181
+ expect(entry.descriptor.capabilities).toEqual(FULL_CAPABILITIES);
182
+ expect(entry.descriptor.manifest).toEqual({
183
+ displayName: "Foo Engine",
184
+ modelCatalog: { dynamic: false, models: [{ id: "m1", aliases: ["one"], canonicalRef: "prov/m1" }] },
185
+ });
186
+ });
187
+
188
+ it("bin 解析:manifest bin = package.json bin map 的 key → 入口绝对路径", () => {
189
+ makeEnginePkg(tmpRoot, "binmap", { capabilities: FULL_CAPABILITIES });
190
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
191
+ expect(result.discovered[0].descriptor.command).toBe(
192
+ path.join(tmpRoot, "binmap", "bin/cli.mjs"),
193
+ );
194
+ });
195
+
196
+ it("必需字段缺失 → skip:id / bin / protocol 各自缺失均不装载", () => {
197
+ for (const key of ["id", "bin", "protocol"]) {
198
+ const manifest: Record<string, unknown> = { capabilities: FULL_CAPABILITIES };
199
+ if (key === "protocol") manifest["protocol"] = undefined;
200
+ else if (key === "bin") manifest["bin"] = undefined;
201
+ else manifest["id"] = undefined;
202
+ makeEnginePkg(tmpRoot, `missing-${key}`, manifest);
203
+ }
204
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
205
+ expect(result.discovered).toHaveLength(0);
206
+ expect(result.skipped).toHaveLength(3);
207
+ for (const key of ["id", "bin", "protocol"]) {
208
+ expect(result.skipped.some((s) => s.reason.includes(key))).toBe(true);
209
+ }
210
+ expect(collectedLogs.filter((l) => l.level === "warn").length).toBeGreaterThanOrEqual(3);
211
+ });
212
+
213
+ it("protocol 不兼容(2 越界)→ unusable 不装载;protocol 1 兼容", () => {
214
+ makeEnginePkg(tmpRoot, "future", { protocol: 2, capabilities: FULL_CAPABILITIES });
215
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
216
+ expect(result.discovered).toHaveLength(0);
217
+ expect(result.unusable).toHaveLength(1);
218
+ expect(result.unusable[0].id).toBe("future");
219
+ expect(result.unusable[0].reason).toContain("protocol");
220
+ });
221
+
222
+ it("capabilities 缺键取最保守值 + warn;未知键忽略 + warn", () => {
223
+ makeEnginePkg(tmpRoot, "partial-caps", {
224
+ capabilities: { schemaEnforcement: "native", unknownKey: "x" },
225
+ });
226
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
227
+ expect(result.discovered).toHaveLength(1);
228
+ expect(result.discovered[0].descriptor.capabilities).toEqual({
229
+ schemaEnforcement: "native",
230
+ steer: "unsupported",
231
+ conversation: "unsupported",
232
+ personaInjection: "prompt",
233
+ eventGranularity: "coarse",
234
+ sandbox: "none",
235
+ sessionRead: "outcome-only",
236
+ resume: "unsupported",
237
+ interrupt: "kill-only",
238
+ permissionMode: "ignored",
239
+ maxTurns: false,
240
+ });
241
+ const warns = collectedLogs.filter((l) => l.level === "warn").map((l) => l.message);
242
+ expect(warns.some((m) => m.includes("capabilities.steer missing"))).toBe(true);
243
+ expect(warns.some((m) => m.includes("unknown capabilities key(s) ignored: unknownKey"))).toBe(true);
244
+ // 未知键不进 capabilities(词表封闭)
245
+ expect(Object.keys(result.discovered[0].descriptor.capabilities)).toHaveLength(11);
246
+ });
247
+
248
+ it("capabilities 整段缺失 → 全保守 + warn(包仍装载)", () => {
249
+ makeEnginePkg(tmpRoot, "no-caps", {});
250
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
251
+ expect(result.discovered).toHaveLength(1);
252
+ expect(result.discovered[0].descriptor.capabilities.maxTurns).toBe(false);
253
+ expect(result.discovered[0].descriptor.capabilities.steer).toBe("unsupported");
254
+ expect(collectedLogs.some((l) => l.message.includes("most conservative values"))).toBe(true);
255
+ });
256
+
257
+ it("envPrefixes 缺省 [];空串/含 */非法形态/保留前缀 → 丢弃该前缀 + warn,包仍可用", () => {
258
+ makeEnginePkg(tmpRoot, "bad-prefixes", {
259
+ envPrefixes: ["", "HAS*STAR", "BAD-DASH", "XYZ_HOST", "xyz_agent_", "GOOD_", "good_"],
260
+ capabilities: FULL_CAPABILITIES,
261
+ });
262
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
263
+ // 包仍可用(A12②):装载成功
264
+ expect(result.discovered).toHaveLength(1);
265
+ expect(result.skipped).toHaveLength(0);
266
+ const warns = collectedLogs.filter((l) => l.level === "warn").map((l) => l.message);
267
+ // 非法形态逐条拒绝(空串 / 含 * / 非法字符)
268
+ expect(warns.filter((m) => m.includes("dropping the prefix")).length).toBeGreaterThanOrEqual(5);
269
+ // 保留前缀大小写不敏感(XYZ_HOST / xyz_agent_ 均拒)
270
+ expect(warns.some((m) => m.includes("reserved host prefix"))).toBe(true);
271
+ });
272
+
273
+ it("modelCatalog 三态:缺省不注入(undefined);null 等价省略;显式数组原样;dynamic 缺省 true", () => {
274
+ makeEnginePkg(tmpRoot, "mc-omitted", { capabilities: FULL_CAPABILITIES });
275
+ makeEnginePkg(tmpRoot, "mc-null", { modelCatalog: null, capabilities: FULL_CAPABILITIES });
276
+ makeEnginePkg(tmpRoot, "mc-explicit", {
277
+ modelCatalog: { models: [{ id: "m1" }] },
278
+ capabilities: FULL_CAPABILITIES,
279
+ });
280
+ makeEnginePkg(tmpRoot, "mc-no-models", { modelCatalog: { dynamic: false }, capabilities: FULL_CAPABILITIES });
281
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
282
+ const manifestOf = (id: string) => result.discovered.find((d) => d.id === id)!.descriptor.manifest!;
283
+ // 缺省 = 不注入(解析器不得把省略填成 models: []——「无枚举面」语义必须可达)
284
+ expect("modelCatalog" in manifestOf("mc-omitted")).toBe(false);
285
+ expect(manifestOf("mc-null").modelCatalog).toBeNull();
286
+ expect(manifestOf("mc-explicit").modelCatalog).toEqual({ dynamic: true, models: [{ id: "m1" }] });
287
+ // models 缺失 = 无有效枚举面 → null(不是 []——[] 是作者显式「有面但空」声明)
288
+ expect(manifestOf("mc-no-models").modelCatalog).toBeNull();
289
+ });
290
+
291
+ it("displayName 可选(缺省不入快照,消费面回落 id);description 仅形态校验", () => {
292
+ makeEnginePkg(tmpRoot, "plain", { capabilities: FULL_CAPABILITIES });
293
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
294
+ expect("displayName" in result.discovered[0].descriptor.manifest!).toBe(false);
295
+ });
296
+ });
297
+
298
+ // ── 三级搜索路径 ─────────────────────────────────────────────────
299
+
300
+ describe("三级搜索路径", () => {
301
+ it("L1 env 根:多根 + org 分组二层布局都命中", () => {
302
+ makeEnginePkg(path.join(tmpRoot, "r1"), "from-env-root", { capabilities: FULL_CAPABILITIES });
303
+ // org 分组:<root>/@scope/<pkg>/package.json
304
+ makeEnginePkg(path.join(tmpRoot, "r2", "@scope"), "scoped-engine", { capabilities: FULL_CAPABILITIES });
305
+ const result = scanEngines(
306
+ scanOpts({ env: { [ENGINE_ROOTS_ENV]: `${path.join(tmpRoot, "r1")}${path.delimiter}${path.join(tmpRoot, "r2")}` } }),
307
+ );
308
+ expect(result.discovered.map((d) => d.id).sort()).toEqual(["from-env-root", "scoped-engine"]);
309
+ });
310
+
311
+ it("L1 宿主根第二通道:HostServices.discoveryRoots().engines 合并扫描", () => {
312
+ makeEnginePkg(path.join(tmpRoot, "hostroot"), "from-host-root", { capabilities: FULL_CAPABILITIES });
313
+ resetCoreForTests();
314
+ const collector = makeLogCollector();
315
+ collectedLogs = collector.logs;
316
+ configureCore({
317
+ ...collector.host,
318
+ discoveryRoots: () => ({ engines: [{ dir: path.join(tmpRoot, "hostroot"), source: "host-npm" }] }),
319
+ } as Parameters<typeof configureCore>[0]);
320
+ const result = scanEngines(scanOpts({}));
321
+ expect(result.discovered.map((d) => d.id)).toEqual(["from-host-root"]);
322
+ expect(result.discovered[0].source).toBe("host-npm");
323
+ });
324
+
325
+ it("L2 node 解析根:nodeModuleRoots 注入命中;deriveNodeModuleRoots 沿上溯链收 node_modules", () => {
326
+ makeEnginePkg(path.join(tmpRoot, "nm"), "from-node-modules", { capabilities: FULL_CAPABILITIES });
327
+ const result = scanEngines(scanOpts({ nodeModuleRoots: [path.join(tmpRoot, "nm")] }));
328
+ expect(result.discovered.map((d) => d.id)).toEqual(["from-node-modules"]);
329
+
330
+ // deriveNodeModuleRoots:entry 位于 <tmp>/a/b/entry.js,<tmp>/node_modules 存在 → 命中
331
+ fs.mkdirSync(path.join(tmpRoot, "a", "b"), { recursive: true });
332
+ fs.mkdirSync(path.join(tmpRoot, "node_modules"));
333
+ fs.writeFileSync(path.join(tmpRoot, "a", "b", "entry.js"), "// entry");
334
+ const derived = deriveNodeModuleRoots(path.join(tmpRoot, "a", "b", "entry.js"));
335
+ expect(derived).toContain(path.join(tmpRoot, "node_modules"));
336
+ // 由内向外:更深的 node_modules 在前
337
+ expect(derived.indexOf(path.join(tmpRoot, "node_modules"))).toBeGreaterThan(
338
+ derived.indexOf(path.join(tmpRoot, "a", "node_modules")),
339
+ );
340
+ });
341
+
342
+ it("L3 显式配置:enabled:false 不装载(A12⑤);command 缺失跳过;config 透传 + cwd 透传", () => {
343
+ makeEnginePkg(tmpRoot, "l3-disabled", { capabilities: FULL_CAPABILITIES });
344
+ const agentDir = path.join(tmpRoot, "agent");
345
+ fs.mkdirSync(path.join(agentDir, "subagents"), { recursive: true });
346
+ fs.writeFileSync(
347
+ path.join(agentDir, "subagents", "config.json"),
348
+ JSON.stringify({
349
+ engines: {
350
+ "l3-disabled": { enabled: false, command: "whatever" },
351
+ "l3-broken": { args: [] },
352
+ "l3-ok": {
353
+ command: path.join(tmpRoot, "l3-disabled", "bin/cli.mjs"),
354
+ args: ["--flag"],
355
+ config: { apiKeyHint: "no-secrets-here" },
356
+ cwd: tmpRoot,
357
+ },
358
+ },
359
+ }),
360
+ );
361
+ const result = scanEngines(scanOpts({ agentDir }));
362
+ expect(result.discovered.map((d) => d.id)).toEqual(["l3-ok"]);
363
+ expect(result.discovered[0].engineConfig).toEqual({ apiKeyHint: "no-secrets-here" });
364
+ // enabled:false 不进清单也不进 unusable(显式禁用非错误)
365
+ expect(result.discovered.some((d) => d.id === "l3-disabled")).toBe(false);
366
+ expect(result.unusable.some((u) => u.id === "l3-disabled")).toBe(false);
367
+ // command 缺失条目:warn 跳过(readExplicitEngines 丢弃,不影响其他条目)
368
+ expect(
369
+ collectedLogs.some((l) => l.message.includes("engines.l3-broken.command is required")),
370
+ ).toBe(true);
371
+ });
372
+
373
+ it("L3 裸命令名经 PATH 解析;解析失败 → unusable 不进清单(投影 = 已发现且可执行)", () => {
374
+ const binDir = path.join(tmpRoot, "pathbin");
375
+ fs.mkdirSync(binDir);
376
+ fs.writeFileSync(path.join(binDir, "l3-on-path"), BIN_CONTENT);
377
+ fs.chmodSync(path.join(binDir, "l3-on-path"), 0o755);
378
+ const agentDir = path.join(tmpRoot, "agent");
379
+ fs.mkdirSync(path.join(agentDir, "subagents"), { recursive: true });
380
+ fs.writeFileSync(
381
+ path.join(agentDir, "subagents", "config.json"),
382
+ JSON.stringify({
383
+ engines: {
384
+ "l3-path": { command: "l3-on-path" },
385
+ "l3-ghost": { command: "definitely-not-on-path-xyz" },
386
+ },
387
+ }),
388
+ );
389
+ const result = scanEngines(
390
+ scanOpts({ agentDir, env: { PATH: binDir } }),
391
+ );
392
+ expect(result.discovered.map((d) => d.id)).toEqual(["l3-path"]);
393
+ expect(result.discovered[0].descriptor.command).toBe(path.join(binDir, "l3-on-path"));
394
+ expect(result.unusable).toHaveLength(1);
395
+ expect(result.unusable[0].id).toBe("l3-ghost");
396
+ });
397
+ });
398
+
399
+ // ── L1 env 解析(分隔符 / 非法条目 / 去重)───────────────────────
400
+
401
+ describe("XYZ_AGENT_ENGINE_ROOTS env 解析", () => {
402
+ it("path.delimiter 分隔;空段跳过;非绝对路径丢弃 + warn;去重(大小写敏感)", () => {
403
+ const abs1 = path.join(tmpRoot, "a");
404
+ const abs2 = path.join(tmpRoot, "b");
405
+ const env = {
406
+ [ENGINE_ROOTS_ENV]: [abs1, "relative/path", "", abs1, abs2].join(path.delimiter),
407
+ };
408
+ const roots = parseEngineRootsEnv(env);
409
+ expect(roots).toEqual([abs1, abs2]);
410
+ expect(collectedLogs.some((l) => l.message.includes("not an absolute path"))).toBe(true);
411
+ });
412
+
413
+ it("env 未设 / 空白 → 空清单(无 warn)", () => {
414
+ expect(parseEngineRootsEnv({})).toEqual([]);
415
+ expect(parseEngineRootsEnv({ [ENGINE_ROOTS_ENV]: " " })).toEqual([]);
416
+ expect(collectedLogs).toHaveLength(0);
417
+ });
418
+ });
419
+
420
+ // ── 冲突与失败(A12)─────────────────────────────────────────────
421
+
422
+ describe("冲突与失败", () => {
423
+ it("bin 不存在 → unusable;存在但不可执行(0o644)→ unusable,均不进清单", () => {
424
+ if (process.platform === "win32") {
425
+ // Windows 无 X_OK 语义(accessSync 仅查存在性),不可执行反例只在 POSIX 跑
426
+ makeEnginePkg(tmpRoot, "missing-bin", { capabilities: FULL_CAPABILITIES }, { omitBin: true });
427
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
428
+ expect(result.discovered).toHaveLength(0);
429
+ expect(result.unusable).toHaveLength(1);
430
+ return;
431
+ }
432
+ makeEnginePkg(tmpRoot, "missing-bin", { capabilities: FULL_CAPABILITIES }, { omitBin: true });
433
+ makeEnginePkg(tmpRoot, "not-executable", { capabilities: FULL_CAPABILITIES }, { binMode: 0o644 });
434
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
435
+ expect(result.discovered).toHaveLength(0);
436
+ expect(result.unusable).toHaveLength(2);
437
+ expect(result.unusable.map((u) => u.id).sort()).toEqual(["missing-bin", "not-executable"]);
438
+ });
439
+
440
+ it("同 id 覆盖:后发现者胜 + debug 留痕(same-id override)", () => {
441
+ makeEnginePkg(path.join(tmpRoot, "first"), "dup", {
442
+ displayName: "First",
443
+ capabilities: FULL_CAPABILITIES,
444
+ });
445
+ makeEnginePkg(path.join(tmpRoot, "second"), "dup", {
446
+ displayName: "Second",
447
+ capabilities: FULL_CAPABILITIES,
448
+ });
449
+ const result = scanEngines(
450
+ scanOpts({
451
+ env: {
452
+ [ENGINE_ROOTS_ENV]: `${path.join(tmpRoot, "first")}${path.delimiter}${path.join(tmpRoot, "second")}`,
453
+ },
454
+ }),
455
+ );
456
+ expect(result.discovered).toHaveLength(1);
457
+ expect(result.discovered[0].descriptor.manifest?.displayName).toBe("Second");
458
+ expect(collectedLogs.some((l) => l.message.includes("same-id override"))).toBe(true);
459
+ });
460
+
461
+ it("坏包不阻断其他引擎(A12②):坏 manifest 与好包共存扫描,好包正常装载", () => {
462
+ // 缺 id 的坏 manifest(makeEnginePkg 的 spread 默认补齐 id,故直写 JSON)
463
+ const brokenDir = path.join(tmpRoot, "broken-id");
464
+ fs.mkdirSync(brokenDir, { recursive: true });
465
+ fs.writeFileSync(
466
+ path.join(brokenDir, "package.json"),
467
+ JSON.stringify({ name: "broken", "xyz-agent": { subagentEngine: { bin: "x", protocol: 1 } } }),
468
+ );
469
+ fs.mkdirSync(path.join(tmpRoot, "torn-json"), { recursive: true });
470
+ fs.writeFileSync(path.join(tmpRoot, "torn-json", "package.json"), "{ torn");
471
+ makeEnginePkg(tmpRoot, "healthy", { capabilities: FULL_CAPABILITIES });
472
+ const result = scanEngines(scanOpts({ roots: [tmpRoot] }));
473
+ expect(result.discovered.map((d) => d.id)).toEqual(["healthy"]);
474
+ expect(result.skipped.length).toBeGreaterThanOrEqual(2);
475
+ });
476
+ });
477
+
478
+ // ── 装载与投影 ───────────────────────────────────────────────────
479
+
480
+ describe("装载与注册表", () => {
481
+ it("discoverAndRegisterEngines 装载 cli descriptor:hasEngine true、getEngine 返回 id 命中的 port(构造同步不 spawn)", () => {
482
+ makeEnginePkg(tmpRoot, "loaded", { displayName: "Loaded", capabilities: FULL_CAPABILITIES });
483
+ makeEnginePkg(tmpRoot, "loaded-catalog", {
484
+ capabilities: FULL_CAPABILITIES,
485
+ modelCatalog: { models: [{ id: "m1" }] },
486
+ });
487
+ discoverAndRegisterEngines(scanOpts({ roots: [tmpRoot] }));
488
+ expect(hasEngine("loaded")).toBe(true);
489
+ const port = getEngine("loaded");
490
+ expect(port.id).toBe("loaded");
491
+ // 同步成员直读 manifest 快照(W2 RemoteEngine 形态):无 modelCatalog →
492
+ // listModels 返回 null(无枚举面)、validateModel 成员不实现(构造器摘除)
493
+ expect(port.listModels?.()).toBeNull();
494
+ expect(typeof port.validateModel).toBe("undefined");
495
+ // 显式 modelCatalog → listModels 返回数组(有枚举面)
496
+ const portWithCatalog = getEngine("loaded-catalog");
497
+ expect(portWithCatalog.listModels?.()).toEqual([{ id: "m1" }]);
498
+ });
499
+
500
+ it("ensureEngineDiscovered:快照命中零扫描;未命中补扫一次后装载", () => {
501
+ makeEnginePkg(tmpRoot, "rescan", { capabilities: FULL_CAPABILITIES });
502
+ const opts = scanOpts({ roots: [tmpRoot] });
503
+ // 未装载:miss → 补扫 → true
504
+ expect(ensureEngineDiscovered("rescan", opts)).toBe(true);
505
+ expect(hasEngine("rescan")).toBe(true);
506
+ // 快照命中:直接 true(再次调用安全)
507
+ expect(ensureEngineDiscovered("rescan", opts)).toBe(true);
508
+ // 确实不存在的 id:补扫后仍 false
509
+ expect(ensureEngineDiscovered("ghost", opts)).toBe(false);
510
+ });
511
+
512
+ it("engines.json 投影合流:发现 cli ∪ inproc 注册;幂等零写;契约 {v:1, engines} 不变", () => {
513
+ makeEnginePkg(tmpRoot, "projected", { capabilities: FULL_CAPABILITIES });
514
+ registerEngine("pi", () => stubEngine("pi"));
515
+ // syncEnginesFile 读 process.env(生产语义)——L1 根经真实 env 注入,afterEach 恢复
516
+ const prevRoots = process.env[ENGINE_ROOTS_ENV];
517
+ process.env[ENGINE_ROOTS_ENV] = tmpRoot;
518
+ try {
519
+ const agentDir = path.join(tmpRoot, "agent");
520
+ syncEnginesFile(agentDir);
521
+ const filePath = getEnginesFilePath(agentDir);
522
+ const file = JSON.parse(fs.readFileSync(filePath, "utf8")) as SubagentEnginesFile;
523
+ expect(file.v).toBe(1);
524
+ expect(file.engines.sort()).toEqual(["pi", "projected"]);
525
+ expect(typeof file.updatedAt).toBe("number");
526
+
527
+ // 幂等零写(内容不变 mtime 不动)
528
+ const statAfterFirst = fs.statSync(filePath);
529
+ syncEnginesFile(agentDir);
530
+ expect(fs.statSync(filePath).mtimeMs).toBe(statAfterFirst.mtimeMs);
531
+
532
+ // 发现面变化(新增引擎包)触发重写
533
+ makeEnginePkg(tmpRoot, "second-projected", { capabilities: FULL_CAPABILITIES });
534
+ syncEnginesFile(agentDir);
535
+ const updated = JSON.parse(fs.readFileSync(filePath, "utf8")) as SubagentEnginesFile;
536
+ expect(updated.engines.sort()).toEqual(["pi", "projected", "second-projected"]);
537
+ } finally {
538
+ if (prevRoots === undefined) delete process.env[ENGINE_ROOTS_ENV];
539
+ else process.env[ENGINE_ROOTS_ENV] = prevRoots;
540
+ }
541
+ });
542
+
543
+ it("syncEnginesFile fail-safe:发现异常不阻塞投影(发现根全部不存在时仍写 inproc 清单)", () => {
544
+ registerEngine("pi", () => stubEngine("pi"));
545
+ const agentDir = path.join(tmpRoot, "agent");
546
+ syncEnginesFile(agentDir);
547
+ const file = JSON.parse(fs.readFileSync(getEnginesFilePath(agentDir), "utf8")) as SubagentEnginesFile;
548
+ expect(file.engines).toEqual(["pi"]);
549
+ });
550
+
551
+ it("零发现零注册时投影空清单(W11 删 inproc 后的终态语义预演)", () => {
552
+ const agentDir = path.join(tmpRoot, "agent");
553
+ syncEnginesFile(agentDir);
554
+ const file = JSON.parse(fs.readFileSync(getEnginesFilePath(agentDir), "utf8")) as SubagentEnginesFile;
555
+ expect(file.engines).toEqual([]);
556
+ expect(listEngines()).toEqual([]);
557
+ });
558
+ });
@@ -0,0 +1,45 @@
1
+ // host-task-spec.test.ts —— ExecuteOptions → AgentCallOpts 直译映射单测(fork-from 载体)。
2
+ //
3
+ // 背景(W3 断链修复):fork-from 的 ExecuteOptions.forkFromSessionFile 在 chat 域协议化
4
+ // 改写后曾断链(本 mapper 不映射 → 协议 run 帧无承载位 → fork-from 轮次以无 --fork 的
5
+ // 全新 session 跑,继承历史丢失)。本文件钉死宿主侧映射面:forkFromSessionFile →
6
+ // forkSource 改名透传(三层同名的唯一改名点)+ 缺省不落键(协议负向兼容)。
7
+ // 端到端消费链由两侧专项覆盖:
8
+ // - chat 域执行链观测(subagent-workflow ended-message-and-fork-from.test 的 fake.runs);
9
+ // - pi 引擎接收面(pi-subagent-cli chat-protocol.test 一次性 run + spawn-args.test
10
+ // 的 forkSource → --fork 直测)。
11
+
12
+ import { describe, expect, it } from "vitest";
13
+
14
+ import { executeOptionsToEngineTaskSpec } from "../host-task-spec.ts";
15
+ import type { ExecuteOptions } from "../../types.ts";
16
+
17
+ function baseOpts(): ExecuteOptions {
18
+ return { task: "do work", slug: "map-test" };
19
+ }
20
+
21
+ describe("executeOptionsToEngineTaskSpec(ExecuteOptions → AgentCallOpts 直译)", () => {
22
+ it("forkFromSessionFile 存在 → forkSource 透传(fork-from 协议载体的宿主侧源头)", () => {
23
+ const spec = executeOptionsToEngineTaskSpec({
24
+ ...baseOpts(),
25
+ forkFromSessionFile: "/sessions/sa-src.jsonl",
26
+ });
27
+ expect(spec.forkSource).toBe("/sessions/sa-src.jsonl");
28
+ expect(spec.prompt).toBe("do work");
29
+ });
30
+
31
+ it("缺省 → 键不落(负向兼容:无 fork-from 的调用帧形状不变)", () => {
32
+ const spec = executeOptionsToEngineTaskSpec(baseOpts());
33
+ expect("forkSource" in spec).toBe(false);
34
+ });
35
+
36
+ it("fork 与 forkSource 互不挤占(独立可选透传;取值优先级语义归引擎侧)", () => {
37
+ const spec = executeOptionsToEngineTaskSpec({
38
+ ...baseOpts(),
39
+ fork: true,
40
+ forkFromSessionFile: "/sessions/sa-src.jsonl",
41
+ });
42
+ expect(spec.fork).toBe(true);
43
+ expect(spec.forkSource).toBe("/sessions/sa-src.jsonl");
44
+ });
45
+ });
@@ -0,0 +1,74 @@
1
+ // model-ref-split.test.ts —— [W3 契约变更④] 无斜杠 canonicalRef 的 core 侧拆分单测。
2
+ //
3
+ // 设计权威源:docs/design/subagent-engine-protocolization.md §3.3 validateModel 行
4
+ // (「core 必须改 resolveIdentityForEngine 与续聊回读对无斜杠 ref 的处理:provider="",
5
+ // id=ref,整串进 name,否则落成 "<ref>/" 畸形」)。拆分单一权威 = splitEngineModelRef,
6
+ // 写入词形 = joinEngineModelRef(provider 空串不拼斜杠);两消费方(chat 域留痕 /
7
+ // 续聊回读)必须同源,否则写入/回读对同一 ref 给出不同 provider/id。
8
+
9
+ import { describe, expect, it } from "vitest";
10
+
11
+ import { joinEngineModelRef, splitEngineModelRef } from "../model-validation.ts";
12
+
13
+ describe("splitEngineModelRef(契约④:无斜杠 ref 不落畸形)", () => {
14
+ it("有斜杠 ref:provider/id 常规拆分,name = 整串", () => {
15
+ expect(splitEngineModelRef("zai/glm-4.6")).toEqual({
16
+ provider: "zai",
17
+ id: "glm-4.6",
18
+ name: "zai/glm-4.6",
19
+ });
20
+ });
21
+
22
+ it("无斜杠 ref(引擎原样返回):provider=''、id=整串、name=整串——不落 \"<ref>/\" 畸形", () => {
23
+ expect(splitEngineModelRef("glm-4.6")).toEqual({
24
+ provider: "",
25
+ id: "glm-4.6",
26
+ name: "glm-4.6",
27
+ });
28
+ });
29
+
30
+ it("空串(引擎未实现校验面且无显式 model 的防御形态):provider/id/name 全空串", () => {
31
+ expect(splitEngineModelRef("")).toEqual({ provider: "", id: "", name: "" });
32
+ });
33
+
34
+ it("开头斜杠(异常形态防御):按无斜杠分支处理,不 crash", () => {
35
+ expect(splitEngineModelRef("/glm")).toEqual({ provider: "", id: "/glm", name: "/glm" });
36
+ });
37
+
38
+ it("多斜杠 ref:首个斜杠拆分(provider 不含斜杠、id 保留其余)", () => {
39
+ expect(splitEngineModelRef("prov/org/model")).toEqual({
40
+ provider: "prov",
41
+ id: "org/model",
42
+ name: "prov/org/model",
43
+ });
44
+ });
45
+ });
46
+
47
+ describe("joinEngineModelRef(record.model 写入词形,与拆分往返自洽)", () => {
48
+ it("provider 非空:provider/id 词形", () => {
49
+ expect(joinEngineModelRef({ provider: "zai", id: "glm-4.6", name: "zai/glm-4.6" })).toBe("zai/glm-4.6");
50
+ });
51
+
52
+ it("provider 空串(契约④):只写 id,无头斜杠——续聊回读可还原", () => {
53
+ expect(joinEngineModelRef({ provider: "", id: "glm-4.6", name: "glm-4.6" })).toBe("glm-4.6");
54
+ });
55
+
56
+ it("写入 → 回读往返(join ∘ split 恒等;split ∘ join 语义还原)", () => {
57
+ for (const modelStr of ["zai/glm-4.6", "glm-4.6", ""]) {
58
+ const split = splitEngineModelRef(modelStr);
59
+ const joined = joinEngineModelRef(split);
60
+ expect(joined).toBe(modelStr); // 写出词形 == 原 ref(无任何斜杠畸形增删)
61
+ const reSplit = splitEngineModelRef(joined);
62
+ expect(reSplit).toEqual(split); // 回读还原同一 provider/id/name
63
+ }
64
+ });
65
+
66
+ it("回归锚(旧行为对照):无斜杠 ref 旧实现落 record.model=\"<ref>/\" 且续聊还原 provider=\"<ref>\"——现两处均消除", () => {
67
+ const ref = "glm-4.6";
68
+ const split = splitEngineModelRef(ref);
69
+ // 写入侧:无尾斜杠
70
+ expect(joinEngineModelRef(split)).not.toContain("/");
71
+ // 回读侧:provider 为空串(旧行为是 "unknown"/整串,给续轮注入虚构 provider)
72
+ expect(splitEngineModelRef(joinEngineModelRef(split)).provider).toBe("");
73
+ });
74
+ });