@selesai/code 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/dist/config.d.ts +4 -15
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +5 -45
  4. package/dist/config.js.map +1 -1
  5. package/dist/core/agent-session.d.ts +1 -0
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +8 -0
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/diagnostics.d.ts +1 -0
  10. package/dist/core/diagnostics.d.ts.map +1 -1
  11. package/dist/core/diagnostics.js.map +1 -1
  12. package/dist/core/model-registry.d.ts +4 -1
  13. package/dist/core/model-registry.d.ts.map +1 -1
  14. package/dist/core/model-registry.js +101 -51
  15. package/dist/core/model-registry.js.map +1 -1
  16. package/dist/core/package-manager.d.ts +10 -0
  17. package/dist/core/package-manager.d.ts.map +1 -1
  18. package/dist/core/package-manager.js +93 -8
  19. package/dist/core/package-manager.js.map +1 -1
  20. package/dist/core/package-manager.test.d.ts +2 -0
  21. package/dist/core/package-manager.test.d.ts.map +1 -0
  22. package/dist/core/package-manager.test.js +20 -0
  23. package/dist/core/package-manager.test.js.map +1 -0
  24. package/dist/core/resource-loader.d.ts +3 -0
  25. package/dist/core/resource-loader.d.ts.map +1 -1
  26. package/dist/core/resource-loader.js +10 -0
  27. package/dist/core/resource-loader.js.map +1 -1
  28. package/dist/core/settings-manager.d.ts +1 -0
  29. package/dist/core/settings-manager.d.ts.map +1 -1
  30. package/dist/core/settings-manager.js.map +1 -1
  31. package/dist/defaults/models.json +64 -113
  32. package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
  33. package/dist/extensions/caveman/index.js +116 -0
  34. package/dist/extensions/caveman/package.json +8 -0
  35. package/dist/extensions/caveman/test/extension.test.js +185 -0
  36. package/dist/extensions/caveman/test/helpers.test.js +67 -0
  37. package/dist/extensions/copy-turn.ts +2 -2
  38. package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
  39. package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
  40. package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
  41. package/dist/extensions/pi-intercom/config.ts +2 -2
  42. package/dist/extensions/pi-intercom/index.ts +2 -2
  43. package/dist/extensions/pi-intercom/package.json +2 -2
  44. package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
  45. package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
  46. package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
  47. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +2 -2
  48. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
  49. package/dist/extensions/pi-powerline-footer/index.ts +40 -47
  50. package/dist/extensions/pi-powerline-footer/package.json +2 -2
  51. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +10 -10
  52. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +1 -1
  53. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +2 -2
  54. package/dist/extensions/pi-powerline-footer/theme.ts +6 -6
  55. package/dist/extensions/pi-powerline-footer/tps.ts +1 -1
  56. package/dist/extensions/pi-powerline-footer/types.ts +7 -7
  57. package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
  58. package/dist/extensions/pi-powerline-footer/working-vibes.ts +52 -59
  59. package/dist/extensions/pi-rewind-hook/index.ts +1 -1
  60. package/dist/extensions/pi-rewind-hook/package.json +1 -1
  61. package/dist/extensions/pi-subagents/README.md +224 -52
  62. package/dist/extensions/pi-subagents/agents/architect.md +188 -0
  63. package/dist/extensions/pi-subagents/agents/builder.md +120 -0
  64. package/dist/extensions/pi-subagents/agents/commentator.md +131 -0
  65. package/dist/extensions/pi-subagents/agents/explorer.md +51 -0
  66. package/dist/extensions/pi-subagents/agents/recapper.md +21 -0
  67. package/dist/extensions/pi-subagents/install.mjs +93 -0
  68. package/dist/extensions/pi-subagents/package.json +4 -5
  69. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  70. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  71. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
  72. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
  73. package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
  74. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
  75. package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
  76. package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
  77. package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
  78. package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
  79. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
  80. package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
  81. package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
  82. package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
  83. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
  84. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
  85. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
  86. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
  87. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
  88. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
  89. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
  90. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
  91. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
  92. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
  93. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
  94. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
  95. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
  96. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
  97. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
  98. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
  99. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
  100. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
  101. package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
  102. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
  103. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
  104. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
  105. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
  106. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
  107. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
  108. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
  109. package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
  110. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
  112. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
  113. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
  114. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
  115. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
  116. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
  117. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
  118. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
  119. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
  120. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
  121. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
  122. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
  123. package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
  124. package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
  125. package/dist/extensions/pi-subagents/src/shared/utils.ts +108 -17
  126. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
  127. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
  128. package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
  129. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
  130. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
  131. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
  132. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
  133. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
  134. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
  135. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
  136. package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
  137. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
  138. package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
  139. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
  140. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
  141. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
  142. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
  143. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
  144. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
  145. package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
  146. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
  147. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
  148. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
  149. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
  150. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
  151. package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
  152. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
  153. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
  154. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
  155. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
  156. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
  157. package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
  158. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
  159. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
  160. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
  161. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
  162. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
  163. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
  164. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
  165. package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
  166. package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
  167. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
  168. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
  169. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
  170. package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
  171. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
  172. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
  173. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
  174. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
  175. package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
  176. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
  177. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
  178. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
  179. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
  180. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
  181. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
  182. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
  183. package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
  184. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
  185. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
  186. package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
  187. package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
  188. package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
  189. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
  190. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
  191. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
  192. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
  193. package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
  194. package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
  195. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
  196. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
  197. package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
  198. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
  199. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
  200. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
  201. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
  202. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
  203. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
  204. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
  205. package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
  206. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
  207. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
  208. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
  209. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
  210. package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
  211. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
  212. package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
  213. package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
  214. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
  215. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
  216. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
  217. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
  218. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
  219. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
  220. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
  221. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
  222. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
  223. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
  224. package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
  225. package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
  226. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
  227. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
  228. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
  229. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
  230. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
  231. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
  232. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
  233. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
  234. package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
  235. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
  236. package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
  237. package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
  238. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
  239. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
  240. package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
  241. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
  242. package/dist/extensions/pi-web-agent/package.json +2 -2
  243. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +2 -2
  244. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
  245. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +1 -1
  246. package/dist/extensions/pi-web-agent/src/extension.ts +1 -1
  247. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
  248. package/dist/extensions/question/helpers.ts +3 -3
  249. package/dist/extensions/question/index.ts +2 -2
  250. package/dist/extensions/question/navigation.ts +2 -2
  251. package/dist/extensions/question/package.json +2 -2
  252. package/dist/extensions/question/question-list.ts +2 -2
  253. package/dist/extensions/question/tests/question-list.test.ts +2 -2
  254. package/dist/extensions/question/tui-adapter.ts +2 -2
  255. package/dist/extensions/question/types.ts +2 -2
  256. package/dist/extensions/question/ui-protocol.ts +2 -2
  257. package/dist/extensions/rtk.ts +2 -2
  258. package/dist/extensions/tokenin-onboarding.ts +2 -2
  259. package/dist/extensions/undo.ts +1 -1
  260. package/dist/extensions/web-agent-onboarding.ts +3 -3
  261. package/dist/extensions/workflow/adapter.ts +16 -8
  262. package/dist/extensions/workflow/extension.ts +1 -1
  263. package/dist/extensions/workflow/package.json +1 -1
  264. package/dist/main.d.ts.map +1 -1
  265. package/dist/main.js +4 -0
  266. package/dist/main.js.map +1 -1
  267. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  268. package/dist/modes/interactive/components/assistant-message.js +2 -0
  269. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  270. package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
  271. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  272. package/dist/modes/interactive/components/first-time-setup.js +19 -5
  273. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  274. package/dist/skills/caveman/SKILL.md +49 -0
  275. package/dist/skills/handoff-text/SKILL.md +13 -0
  276. package/dist/utils/thinking-tags.d.ts +7 -0
  277. package/dist/utils/thinking-tags.d.ts.map +1 -0
  278. package/dist/utils/thinking-tags.js +63 -0
  279. package/dist/utils/thinking-tags.js.map +1 -0
  280. package/docs/docs.json +4 -0
  281. package/docs/extensions.md +2 -0
  282. package/docs/settings.md +15 -0
  283. package/docs/shared-host-extensions.md +109 -0
  284. package/package.json +2 -2
@@ -0,0 +1,1199 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { afterEach, describe, it } from "node:test";
6
+ import { handleManagementAction } from "../../src/agents/agent-management.ts";
7
+ import { serializeAgent } from "../../src/agents/agent-serializer.ts";
8
+ import { parseChain, serializeChain } from "../../src/agents/chain-serializer.ts";
9
+ import { discoverAgents, discoverAgentsAll, type AgentConfig } from "../../src/agents/agents.ts";
10
+
11
+ const tempDirs: string[] = [];
12
+
13
+ function writeJson(filePath: string, value: unknown): void {
14
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
15
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
16
+ }
17
+
18
+ function writeAgent(filePath: string, body: string): void {
19
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
20
+ fs.writeFileSync(filePath, body, "utf-8");
21
+ }
22
+
23
+ function withTempHome<T>(fn: (home: string) => T): T {
24
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-package-home-"));
25
+ tempDirs.push(home);
26
+ const oldHome = process.env.HOME;
27
+ const oldUserProfile = process.env.USERPROFILE;
28
+ const oldPiCodingAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
29
+ const oldExtraAgentDirs = process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
30
+ process.env.HOME = home;
31
+ process.env.USERPROFILE = home;
32
+ delete process.env.SELESAI_CODING_AGENT_DIR;
33
+ delete process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
34
+ try {
35
+ return fn(home);
36
+ } finally {
37
+ if (oldHome === undefined) delete process.env.HOME;
38
+ else process.env.HOME = oldHome;
39
+ if (oldUserProfile === undefined) delete process.env.USERPROFILE;
40
+ else process.env.USERPROFILE = oldUserProfile;
41
+ if (oldPiCodingAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
42
+ else process.env.SELESAI_CODING_AGENT_DIR = oldPiCodingAgentDir;
43
+ if (oldExtraAgentDirs === undefined) delete process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
44
+ else process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS = oldExtraAgentDirs;
45
+ }
46
+ }
47
+
48
+ afterEach(() => {
49
+ while (tempDirs.length > 0) {
50
+ const dir = tempDirs.pop();
51
+ if (!dir) continue;
52
+ fs.rmSync(dir, { recursive: true, force: true });
53
+ }
54
+ });
55
+
56
+ describe("agent permission frontmatter", () => {
57
+ it("preserves nested permission YAML blocks through discovery and serialization", () => {
58
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-permission-frontmatter-"));
59
+ tempDirs.push(dir);
60
+ const agentsDir = path.join(dir, ".selesai", "agents");
61
+ fs.mkdirSync(agentsDir, { recursive: true });
62
+ fs.writeFileSync(path.join(agentsDir, "worker.md"), `---
63
+ name: worker
64
+ description: Worker
65
+ tools: bash,read,write
66
+ permission:
67
+ "*": ask
68
+ read: allow
69
+ bash:
70
+ "*": ask
71
+ "git *": allow
72
+ ---
73
+
74
+ Do work
75
+ `, "utf-8");
76
+
77
+ const result = discoverAgents(dir, "project");
78
+ const worker = result.agents.find((agent) => agent.name === "worker");
79
+ assert.equal(worker?.extraFields?.permission, `"*": ask
80
+ read: allow
81
+ bash:
82
+ "*": ask
83
+ "git *": allow`);
84
+
85
+ const serialized = serializeAgent(worker!);
86
+ assert.match(serialized, /^permission:\n "\*": ask\n read: allow\n bash:\n "\*": ask\n "git \*": allow$/m);
87
+ });
88
+ });
89
+
90
+ describe("agent frontmatter defaultContext", () => {
91
+ it("serializes defaultContext into agent frontmatter", () => {
92
+ const agent: AgentConfig = {
93
+ name: "worker",
94
+ description: "Worker",
95
+ systemPrompt: "Do work",
96
+ systemPromptMode: "replace",
97
+ inheritProjectContext: true,
98
+ inheritSkills: false,
99
+ source: "project",
100
+ filePath: "/tmp/worker.md",
101
+ defaultContext: "fork",
102
+ };
103
+
104
+ const serialized = serializeAgent(agent);
105
+ assert.match(serialized, /defaultContext: fork/);
106
+ });
107
+
108
+ it("parses defaultContext from discovered agent frontmatter", () => {
109
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-default-context-"));
110
+ tempDirs.push(dir);
111
+ const agentsDir = path.join(dir, ".selesai", "agents");
112
+ fs.mkdirSync(agentsDir, { recursive: true });
113
+ fs.writeFileSync(path.join(agentsDir, "worker.md"), `---
114
+ name: worker
115
+ description: Worker
116
+ defaultContext: fork
117
+ ---
118
+
119
+ Do work
120
+ `, "utf-8");
121
+
122
+ const result = discoverAgents(dir, "project");
123
+ const worker = result.agents.find((agent) => agent.name === "worker");
124
+ assert.equal(worker?.defaultContext, "fork");
125
+ });
126
+
127
+ it("loads packaged planner, worker, and oracle with fork defaultContext", () => {
128
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-default-context-"));
129
+ tempDirs.push(dir);
130
+ const agents = discoverAgentsAll(dir).builtin;
131
+
132
+ for (const name of ["planner", "worker", "oracle"]) {
133
+ const agent = agents.find((candidate) => candidate.name === name);
134
+ assert.equal(agent?.defaultContext, "fork", `${name} should default to fork context`);
135
+ }
136
+ });
137
+ });
138
+
139
+ describe("chain discovery", () => {
140
+ it("prefers same-scope .chain.json over .chain.md for the same runtime name", () => {
141
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-chain-format-precedence-"));
142
+ tempDirs.push(dir);
143
+ const chainsDir = path.join(dir, ".selesai", "chains");
144
+ fs.mkdirSync(chainsDir, { recursive: true });
145
+ fs.writeFileSync(path.join(chainsDir, "dynamic-review.chain.md"), `---
146
+ name: dynamic-review
147
+ description: Markdown fallback
148
+ ---
149
+
150
+ ## scout
151
+
152
+ Run the markdown chain
153
+ `, "utf-8");
154
+ fs.writeFileSync(path.join(chainsDir, "dynamic-review.chain.json"), JSON.stringify({
155
+ name: "dynamic-review",
156
+ description: "JSON dynamic chain",
157
+ chain: [
158
+ {
159
+ agent: "scout",
160
+ task: "Return targets",
161
+ as: "targets",
162
+ outputSchema: { type: "object" },
163
+ },
164
+ {
165
+ expand: { from: { output: "targets", path: "/items" }, maxItems: 4 },
166
+ parallel: { agent: "reviewer", task: "Review {item.path}" },
167
+ collect: { as: "reviews" },
168
+ },
169
+ ],
170
+ }), "utf-8");
171
+
172
+ const result = discoverAgentsAll(dir);
173
+ const chain = result.chains.find((candidate) => candidate.name === "dynamic-review");
174
+ assert.equal(chain?.description, "JSON dynamic chain");
175
+ assert.equal(chain?.filePath.endsWith(".chain.json"), true);
176
+ assert.equal("expand" in (chain?.steps[1] ?? {}), true);
177
+ });
178
+ });
179
+
180
+ describe("package-provided agents and chains", () => {
181
+ it("discovers package agents and chains from installed package manifests", () => withTempHome(() => {
182
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-package-discovery-"));
183
+ tempDirs.push(dir);
184
+ const workflowRoot = path.join(dir, ".selesai", "npm", "node_modules", "my-pi-workflow");
185
+ const chainsRoot = path.join(dir, ".selesai", "npm", "node_modules", "@scope", "chain-workflow");
186
+ writeJson(path.join(workflowRoot, "package.json"), {
187
+ name: "my-pi-workflow",
188
+ "pi-subagents": {
189
+ agents: ["./agents"],
190
+ },
191
+ });
192
+ writeAgent(path.join(workflowRoot, "agents", "reviewer.md"), `---
193
+ name: reviewer
194
+ package: my-workflow
195
+ description: Review changes for this workflow.
196
+ ---
197
+
198
+ Review the workflow.
199
+ `);
200
+ writeJson(path.join(chainsRoot, "package.json"), {
201
+ name: "@scope/chain-workflow",
202
+ pi: {
203
+ subagents: {
204
+ chains: ["./chains"],
205
+ },
206
+ },
207
+ });
208
+ writeAgent(path.join(chainsRoot, "chains", "review.chain.md"), `---
209
+ name: review
210
+ package: my-workflow
211
+ description: Run workflow review.
212
+ ---
213
+
214
+ ## my-workflow.reviewer
215
+
216
+ Review the task.
217
+ `);
218
+
219
+ const all = discoverAgentsAll(dir);
220
+ const packagedAgent = all.package.find((agent) => agent.name === "my-workflow.reviewer");
221
+ assert.ok(packagedAgent);
222
+ assert.equal(packagedAgent.source, "package");
223
+ assert.equal(packagedAgent.filePath, path.join(workflowRoot, "agents", "reviewer.md"));
224
+ assert.equal(discoverAgents(dir, "both").agents.find((agent) => agent.name === "my-workflow.reviewer")?.source, "package");
225
+
226
+ const packagedChain = all.chains.find((chain) => chain.name === "my-workflow.review");
227
+ assert.ok(packagedChain);
228
+ assert.equal(packagedChain.source, "package");
229
+ assert.equal(packagedChain.steps[0]?.agent, "my-workflow.reviewer");
230
+ }));
231
+
232
+ it("loads packages referenced from Pi settings", () => withTempHome(() => {
233
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-settings-package-"));
234
+ tempDirs.push(dir);
235
+ const packageRoot = path.join(dir, ".selesai", "vendor", "workflow");
236
+ writeJson(path.join(dir, ".selesai", "settings.json"), {
237
+ packages: [{ source: "file:./vendor/workflow" }],
238
+ });
239
+ writeJson(path.join(packageRoot, "package.json"), {
240
+ name: "settings-workflow",
241
+ pi: {
242
+ subagents: {
243
+ agents: ["./agents"],
244
+ },
245
+ },
246
+ });
247
+ writeAgent(path.join(packageRoot, "agents", "planner.md"), `---
248
+ name: planner
249
+ package: settings-workflow
250
+ description: Plan from a settings-installed package.
251
+ ---
252
+
253
+ Plan the work.
254
+ `);
255
+
256
+ const agent = discoverAgents(dir, "both").agents.find((candidate) => candidate.name === "settings-workflow.planner");
257
+ assert.ok(agent);
258
+ assert.equal(agent.source, "package");
259
+ }));
260
+
261
+ it("discovers project package agents when cwd is nested below the project root", () => withTempHome(() => {
262
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-nested-package-discovery-"));
263
+ tempDirs.push(dir);
264
+ const nested = path.join(dir, "packages", "app", "src");
265
+ const packageRoot = path.join(dir, ".selesai", "npm", "node_modules", "nested-workflow");
266
+ fs.mkdirSync(nested, { recursive: true });
267
+ writeJson(path.join(packageRoot, "package.json"), {
268
+ name: "nested-workflow",
269
+ "pi-subagents": {
270
+ agents: ["./agents"],
271
+ },
272
+ });
273
+ writeAgent(path.join(packageRoot, "agents", "reviewer.md"), `---
274
+ name: reviewer
275
+ package: nested-workflow
276
+ description: Review from a project package.
277
+ ---
278
+
279
+ Review nested project work.
280
+ `);
281
+
282
+ const agent = discoverAgents(nested, "both").agents.find((candidate) => candidate.name === "nested-workflow.reviewer");
283
+ assert.ok(agent);
284
+ assert.equal(agent.source, "package");
285
+ assert.equal(agent.filePath, path.join(packageRoot, "agents", "reviewer.md"));
286
+ }));
287
+
288
+ it("does not register legacy skill files from broad package agent roots", () => withTempHome(() => {
289
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-broad-package-skills-"));
290
+ tempDirs.push(dir);
291
+ const packageRoot = path.join(dir, ".selesai", "npm", "node_modules", "broad-workflow");
292
+ writeJson(path.join(packageRoot, "package.json"), {
293
+ name: "broad-workflow",
294
+ "pi-subagents": {
295
+ agents: ["."],
296
+ },
297
+ });
298
+ writeAgent(path.join(packageRoot, "agent.md"), `---
299
+ name: package-agent
300
+ description: Package agent
301
+ ---
302
+
303
+ Package prompt
304
+ `);
305
+ writeAgent(path.join(packageRoot, ".agents", "skills", "package-skill", "SKILL.md"), `---
306
+ name: package-skill
307
+ description: Package skill
308
+ ---
309
+
310
+ Skill prompt
311
+ `);
312
+ writeAgent(path.join(packageRoot, "agents", "SKILL.md"), `---
313
+ name: skill-named-package-agent
314
+ description: Skill-named package agent
315
+ ---
316
+
317
+ Agent prompt
318
+ `);
319
+
320
+ const packageAgents = discoverAgentsAll(dir).package;
321
+ assert.ok(packageAgents.find((agent) => agent.name === "package-agent" && agent.filePath === path.join(packageRoot, "agent.md")));
322
+ assert.ok(packageAgents.find((agent) => agent.name === "skill-named-package-agent" && agent.filePath === path.join(packageRoot, "agents", "SKILL.md")));
323
+ assert.equal(packageAgents.some((agent) => agent.filePath.includes(`${path.sep}.agents${path.sep}skills${path.sep}`)), false);
324
+ assert.equal(packageAgents.some((agent) => agent.name === "package-skill"), false);
325
+ }));
326
+
327
+ it("keeps package definitions below user and project overrides", () => withTempHome((home) => {
328
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-package-precedence-"));
329
+ tempDirs.push(dir);
330
+ const packageRoot = path.join(dir, ".selesai", "npm", "node_modules", "override-workflow");
331
+ writeJson(path.join(packageRoot, "package.json"), {
332
+ name: "override-workflow",
333
+ "pi-subagents": {
334
+ agents: ["./agents"],
335
+ chains: ["./chains"],
336
+ },
337
+ });
338
+ writeAgent(path.join(packageRoot, "agents", "scout.md"), `---
339
+ name: scout
340
+ description: Package scout
341
+ ---
342
+
343
+ Package scout.
344
+ `);
345
+ writeAgent(path.join(packageRoot, "chains", "shared.chain.md"), `---
346
+ name: shared
347
+ description: Package chain
348
+ ---
349
+
350
+ ## scout
351
+
352
+ Package chain.
353
+ `);
354
+ writeAgent(path.join(home, ".selesai", "agent", "agents", "scout.md"), `---
355
+ name: scout
356
+ description: User scout
357
+ ---
358
+
359
+ User scout.
360
+ `);
361
+ writeAgent(path.join(dir, ".selesai", "agents", "scout.md"), `---
362
+ name: scout
363
+ description: Project scout
364
+ ---
365
+
366
+ Project scout.
367
+ `);
368
+ writeAgent(path.join(home, ".selesai", "agent", "chains", "shared.chain.md"), `---
369
+ name: shared
370
+ description: User chain
371
+ ---
372
+
373
+ ## scout
374
+
375
+ User chain.
376
+ `);
377
+ writeAgent(path.join(dir, ".selesai", "chains", "shared.chain.md"), `---
378
+ name: shared
379
+ description: Project chain
380
+ ---
381
+
382
+ ## scout
383
+
384
+ Project chain.
385
+ `);
386
+
387
+ assert.equal(discoverAgents(dir, "user").agents.find((agent) => agent.name === "scout")?.source, "user");
388
+ assert.equal(discoverAgents(dir, "project").agents.find((agent) => agent.name === "scout")?.source, "project");
389
+ const chainByName = new Map(discoverAgentsAll(dir).chains.map((chain) => [chain.name, chain]));
390
+ assert.equal(chainByName.get("shared")?.source, "project");
391
+ }));
392
+
393
+ it("does not allow management updates to package agents", () => withTempHome(() => {
394
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-package-readonly-"));
395
+ tempDirs.push(dir);
396
+ const packageRoot = path.join(dir, ".selesai", "npm", "node_modules", "readonly-workflow");
397
+ writeJson(path.join(packageRoot, "package.json"), {
398
+ name: "readonly-workflow",
399
+ "pi-subagents": {
400
+ agents: ["./agents"],
401
+ },
402
+ });
403
+ writeAgent(path.join(packageRoot, "agents", "reviewer.md"), `---
404
+ name: reviewer
405
+ package: readonly-workflow
406
+ description: Read-only package reviewer.
407
+ ---
408
+
409
+ Review only.
410
+ `);
411
+
412
+ const result = handleManagementAction("update", {
413
+ agent: "readonly-workflow.reviewer",
414
+ config: { description: "Changed" },
415
+ }, {
416
+ cwd: dir,
417
+ modelRegistry: { getAvailable: () => [] },
418
+ });
419
+
420
+ assert.equal(result.isError, true);
421
+ assert.match(result.content[0]?.text ?? "", /read-only/);
422
+ }));
423
+ });
424
+
425
+ describe("agent frontmatter completionGuard", () => {
426
+ it("serializes disabled completion guard into agent frontmatter", () => {
427
+ const agent: AgentConfig = {
428
+ name: "test-runner",
429
+ description: "Test runner",
430
+ systemPrompt: "Validate changes",
431
+ systemPromptMode: "replace",
432
+ inheritProjectContext: false,
433
+ inheritSkills: false,
434
+ source: "project",
435
+ filePath: "/tmp/test-runner.md",
436
+ completionGuard: false,
437
+ };
438
+
439
+ const serialized = serializeAgent(agent);
440
+ assert.match(serialized, /completionGuard: false/);
441
+ });
442
+
443
+ it("omits enabled completion guard from serialized frontmatter", () => {
444
+ const agent: AgentConfig = {
445
+ name: "test-runner",
446
+ description: "Test runner",
447
+ systemPrompt: "Validate changes",
448
+ systemPromptMode: "replace",
449
+ inheritProjectContext: false,
450
+ inheritSkills: false,
451
+ source: "project",
452
+ filePath: "/tmp/test-runner.md",
453
+ completionGuard: true,
454
+ };
455
+
456
+ const serialized = serializeAgent(agent);
457
+ assert.doesNotMatch(serialized, /completionGuard:/);
458
+ });
459
+
460
+ it("parses completionGuard from discovered agent frontmatter", () => {
461
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-completion-guard-"));
462
+ tempDirs.push(dir);
463
+ const agentsDir = path.join(dir, ".selesai", "agents");
464
+ fs.mkdirSync(agentsDir, { recursive: true });
465
+ fs.writeFileSync(path.join(agentsDir, "test-runner.md"), `---
466
+ name: test-runner
467
+ description: Test runner
468
+ completionGuard: false
469
+ ---
470
+
471
+ Validate changes
472
+ `, "utf-8");
473
+
474
+ const result = discoverAgents(dir, "project");
475
+ const runner = result.agents.find((agent) => agent.name === "test-runner");
476
+ assert.equal(runner?.completionGuard, false);
477
+ assert.equal(runner?.extraFields?.completionGuard, undefined);
478
+ });
479
+ });
480
+
481
+ describe("agent frontmatter maxSubagentDepth", () => {
482
+ it("serializes maxSubagentDepth into agent frontmatter", () => {
483
+ const agent: AgentConfig = {
484
+ name: "scout",
485
+ description: "Scout",
486
+ systemPrompt: "Inspect code",
487
+ systemPromptMode: "replace",
488
+ inheritProjectContext: false,
489
+ inheritSkills: false,
490
+ source: "project",
491
+ filePath: "/tmp/scout.md",
492
+ maxSubagentDepth: 1,
493
+ };
494
+
495
+ const serialized = serializeAgent(agent);
496
+ assert.match(serialized, /maxSubagentDepth: 1/);
497
+ });
498
+
499
+ it("parses maxSubagentDepth from discovered agent frontmatter", () => {
500
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-frontmatter-"));
501
+ tempDirs.push(dir);
502
+ const agentsDir = path.join(dir, ".selesai", "agents");
503
+ fs.mkdirSync(agentsDir, { recursive: true });
504
+ fs.writeFileSync(path.join(agentsDir, "scout.md"), `---
505
+ name: scout
506
+ description: Scout
507
+ maxSubagentDepth: 1
508
+ ---
509
+
510
+ Inspect code
511
+ `, "utf-8");
512
+
513
+ const result = discoverAgents(dir, "project");
514
+ const scout = result.agents.find((agent) => agent.name === "scout");
515
+ assert.equal(scout?.maxSubagentDepth, 1);
516
+ });
517
+ });
518
+
519
+ describe("agent frontmatter fallbackModels", () => {
520
+ it("serializes fallbackModels into agent frontmatter", () => {
521
+ const agent: AgentConfig = {
522
+ name: "worker",
523
+ description: "Worker",
524
+ systemPrompt: "Do work",
525
+ systemPromptMode: "replace",
526
+ inheritProjectContext: false,
527
+ inheritSkills: false,
528
+ source: "project",
529
+ filePath: "/tmp/worker.md",
530
+ fallbackModels: ["openai/gpt-5-mini", "anthropic/claude-sonnet-4"],
531
+ };
532
+
533
+ const serialized = serializeAgent(agent);
534
+ assert.match(serialized, /fallbackModels: openai\/gpt-5-mini, anthropic\/claude-sonnet-4/);
535
+ });
536
+
537
+ it("parses fallbackModels from discovered agent frontmatter", () => {
538
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-fallback-frontmatter-"));
539
+ tempDirs.push(dir);
540
+ const agentsDir = path.join(dir, ".selesai", "agents");
541
+ fs.mkdirSync(agentsDir, { recursive: true });
542
+ fs.writeFileSync(path.join(agentsDir, "worker.md"), `---
543
+ name: worker
544
+ description: Worker
545
+ fallbackModels: openai/gpt-5-mini, anthropic/claude-sonnet-4
546
+ ---
547
+
548
+ Do work
549
+ `, "utf-8");
550
+
551
+ const result = discoverAgents(dir, "project");
552
+ const worker = result.agents.find((agent) => agent.name === "worker");
553
+ assert.deepEqual(worker?.fallbackModels, ["openai/gpt-5-mini", "anthropic/claude-sonnet-4"]);
554
+ });
555
+ });
556
+
557
+ describe("agent frontmatter systemPromptMode", () => {
558
+ it("serializes systemPromptMode into agent frontmatter", () => {
559
+ const agent: AgentConfig = {
560
+ name: "worker",
561
+ description: "Worker",
562
+ systemPrompt: "Do work",
563
+ systemPromptMode: "replace",
564
+ inheritProjectContext: false,
565
+ inheritSkills: false,
566
+ source: "project",
567
+ filePath: "/tmp/worker.md",
568
+ };
569
+
570
+ const serialized = serializeAgent(agent);
571
+ assert.match(serialized, /systemPromptMode: replace/);
572
+ });
573
+
574
+ it("parses systemPromptMode from discovered agent frontmatter", () => {
575
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-prompt-mode-frontmatter-"));
576
+ tempDirs.push(dir);
577
+ const agentsDir = path.join(dir, ".selesai", "agents");
578
+ fs.mkdirSync(agentsDir, { recursive: true });
579
+ fs.writeFileSync(path.join(agentsDir, "worker.md"), `---
580
+ name: worker
581
+ description: Worker
582
+ systemPromptMode: replace
583
+ ---
584
+
585
+ Do work
586
+ `, "utf-8");
587
+
588
+ const result = discoverAgents(dir, "project");
589
+ const worker = result.agents.find((agent) => agent.name === "worker");
590
+ assert.equal(worker?.systemPromptMode, "replace");
591
+ });
592
+ });
593
+
594
+ describe("agent frontmatter prompt inheritance flags", () => {
595
+ it("serializes inheritProjectContext and inheritSkills into agent frontmatter", () => {
596
+ const agent: AgentConfig = {
597
+ name: "worker",
598
+ description: "Worker",
599
+ systemPrompt: "Do work",
600
+ systemPromptMode: "replace",
601
+ inheritProjectContext: true,
602
+ inheritSkills: true,
603
+ source: "project",
604
+ filePath: "/tmp/worker.md",
605
+ };
606
+
607
+ const serialized = serializeAgent(agent);
608
+ assert.match(serialized, /inheritProjectContext: true/);
609
+ assert.match(serialized, /inheritSkills: true/);
610
+ });
611
+
612
+ it("parses inheritProjectContext and inheritSkills from discovered agent frontmatter", () => {
613
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-prompt-inheritance-frontmatter-"));
614
+ tempDirs.push(dir);
615
+ const agentsDir = path.join(dir, ".selesai", "agents");
616
+ fs.mkdirSync(agentsDir, { recursive: true });
617
+ fs.writeFileSync(path.join(agentsDir, "worker.md"), `---
618
+ name: worker
619
+ description: Worker
620
+ inheritProjectContext: true
621
+ inheritSkills: true
622
+ ---
623
+
624
+ Do work
625
+ `, "utf-8");
626
+
627
+ const result = discoverAgents(dir, "project");
628
+ const worker = result.agents.find((agent) => agent.name === "worker");
629
+ assert.equal(worker?.inheritProjectContext, true);
630
+ assert.equal(worker?.inheritSkills, true);
631
+ });
632
+ });
633
+
634
+ describe("agent frontmatter subagentOnlyExtensions", () => {
635
+ it("serializes subagentOnlyExtensions into agent frontmatter", () => {
636
+ const agent: AgentConfig = {
637
+ name: "worker",
638
+ description: "Worker",
639
+ systemPrompt: "Do work",
640
+ systemPromptMode: "replace",
641
+ inheritProjectContext: false,
642
+ inheritSkills: false,
643
+ source: "project",
644
+ filePath: "/tmp/worker.md",
645
+ subagentOnlyExtensions: ["./tools/child-search.ts", "/opt/pi/child-only.ts"],
646
+ };
647
+
648
+ const serialized = serializeAgent(agent);
649
+ assert.match(serialized, /subagentOnlyExtensions: \.\/tools\/child-search\.ts, \/opt\/pi\/child-only\.ts/);
650
+ });
651
+
652
+ it("parses subagentOnlyExtensions from discovered agent frontmatter", () => {
653
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-child-ext-"));
654
+ tempDirs.push(dir);
655
+ const agentsDir = path.join(dir, ".selesai", "agents");
656
+ fs.mkdirSync(agentsDir, { recursive: true });
657
+ fs.writeFileSync(path.join(agentsDir, "worker.md"), `---
658
+ name: worker
659
+ description: Worker
660
+ subagentOnlyExtensions: ./tools/child-search.ts, /opt/pi/child-only.ts
661
+ ---
662
+
663
+ Do work
664
+ `, "utf-8");
665
+
666
+ const result = discoverAgents(dir, "project");
667
+ const worker = result.agents.find((agent) => agent.name === "worker");
668
+ assert.deepEqual(worker?.subagentOnlyExtensions, ["./tools/child-search.ts", "/opt/pi/child-only.ts"]);
669
+ });
670
+ });
671
+
672
+ describe("agent frontmatter prompt assembly defaults", () => {
673
+ it("defaults ordinary agents to replace mode with no inherited context or skills", () => {
674
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-default-prompt-settings-"));
675
+ tempDirs.push(dir);
676
+ const agentsDir = path.join(dir, ".selesai", "agents");
677
+ fs.mkdirSync(agentsDir, { recursive: true });
678
+ fs.writeFileSync(path.join(agentsDir, "worker.md"), `---
679
+ name: worker
680
+ description: Worker
681
+ ---
682
+
683
+ Do work
684
+ `, "utf-8");
685
+
686
+ const result = discoverAgents(dir, "project");
687
+ const worker = result.agents.find((agent) => agent.name === "worker");
688
+ assert.equal(worker?.systemPromptMode, "replace");
689
+ assert.equal(worker?.inheritProjectContext, false);
690
+ assert.equal(worker?.inheritSkills, false);
691
+ });
692
+
693
+ it("builtin agents inherit project context by default", () => {
694
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-default-prompt-settings-"));
695
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-default-home-"));
696
+ tempDirs.push(dir);
697
+ tempDirs.push(homeDir);
698
+ const previousHome = process.env.HOME;
699
+ const previousUserProfile = process.env.USERPROFILE;
700
+
701
+ try {
702
+ process.env.HOME = homeDir;
703
+ process.env.USERPROFILE = homeDir;
704
+
705
+ const result = discoverAgents(dir, "both");
706
+ const scout = result.agents.find((agent) => agent.name === "scout");
707
+ const reviewer = result.agents.find((agent) => agent.name === "reviewer");
708
+ const delegate = result.agents.find((agent) => agent.name === "delegate");
709
+ assert.equal(scout?.inheritProjectContext, true);
710
+ assert.equal(reviewer?.inheritProjectContext, true);
711
+ assert.equal(delegate?.inheritProjectContext, true);
712
+ } finally {
713
+ if (previousHome === undefined) delete process.env.HOME;
714
+ else process.env.HOME = previousHome;
715
+ if (previousUserProfile === undefined) delete process.env.USERPROFILE;
716
+ else process.env.USERPROFILE = previousUserProfile;
717
+ }
718
+ });
719
+
720
+ it("bundled agents all have explicit tool allowlists", () => {
721
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-tools-"));
722
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-tools-home-"));
723
+ tempDirs.push(dir);
724
+ tempDirs.push(homeDir);
725
+ const previousHome = process.env.HOME;
726
+ const previousUserProfile = process.env.USERPROFILE;
727
+
728
+ try {
729
+ process.env.HOME = homeDir;
730
+ process.env.USERPROFILE = homeDir;
731
+ const builtins = discoverAgentsAll(dir).builtin;
732
+ assert.ok(builtins.length > 0);
733
+ for (const agent of builtins) {
734
+ assert.ok(agent.tools && agent.tools.length > 0, `${agent.name} should have explicit tools frontmatter`);
735
+ }
736
+ } finally {
737
+ if (previousHome === undefined) delete process.env.HOME;
738
+ else process.env.HOME = previousHome;
739
+ if (previousUserProfile === undefined) delete process.env.USERPROFILE;
740
+ else process.env.USERPROFILE = previousUserProfile;
741
+ }
742
+ });
743
+
744
+ it("worker and delegate include the child-facing supervisor tool", () => {
745
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-supervisor-tool-"));
746
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-supervisor-tool-home-"));
747
+ tempDirs.push(dir);
748
+ tempDirs.push(homeDir);
749
+ const previousHome = process.env.HOME;
750
+ const previousUserProfile = process.env.USERPROFILE;
751
+
752
+ try {
753
+ process.env.HOME = homeDir;
754
+ process.env.USERPROFILE = homeDir;
755
+ const agents = discoverAgentsAll(dir).builtin;
756
+ for (const name of ["worker", "delegate"]) {
757
+ const agent = agents.find((candidate) => candidate.name === name);
758
+ assert.ok(agent, `${name} builtin should be discovered`);
759
+ assert.deepEqual(agent?.tools, ["read", "grep", "find", "ls", "bash", "edit", "write", "contact_supervisor"]);
760
+ }
761
+ } finally {
762
+ if (previousHome === undefined) delete process.env.HOME;
763
+ else process.env.HOME = previousHome;
764
+ if (previousUserProfile === undefined) delete process.env.USERPROFILE;
765
+ else process.env.USERPROFILE = previousUserProfile;
766
+ }
767
+ });
768
+
769
+ it("defaults delegate to append mode with inherited project context", () => {
770
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-delegate-default-prompt-settings-"));
771
+ tempDirs.push(dir);
772
+ const agentsDir = path.join(dir, ".selesai", "agents");
773
+ fs.mkdirSync(agentsDir, { recursive: true });
774
+ fs.writeFileSync(path.join(agentsDir, "delegate.md"), `---
775
+ name: delegate
776
+ description: Delegate
777
+ ---
778
+
779
+ Do work
780
+ `, "utf-8");
781
+
782
+ const result = discoverAgents(dir, "project");
783
+ const delegate = result.agents.find((agent) => agent.name === "delegate");
784
+ assert.equal(delegate?.systemPromptMode, "append");
785
+ assert.equal(delegate?.inheritProjectContext, true);
786
+ assert.equal(delegate?.inheritSkills, false);
787
+ });
788
+ });
789
+
790
+ describe("packaged agent and chain discovery", () => {
791
+ it("recursively discovers nested project agents while keeping chain files separate", () => {
792
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-recursive-agent-discovery-"));
793
+ tempDirs.push(dir);
794
+ const nestedDir = path.join(dir, ".selesai", "agents", "code-analysis", "deep");
795
+ const nestedChainDir = path.join(dir, ".selesai", "chains", "code-analysis", "deep");
796
+ fs.mkdirSync(nestedDir, { recursive: true });
797
+ fs.mkdirSync(nestedChainDir, { recursive: true });
798
+ fs.writeFileSync(path.join(nestedDir, "scout.md"), `---
799
+ name: scout
800
+ description: Nested scout
801
+ ---
802
+
803
+ Inspect code
804
+ `, "utf-8");
805
+ fs.writeFileSync(path.join(nestedChainDir, "review.chain.md"), `---
806
+ name: review-flow
807
+ description: Review flow
808
+ ---
809
+
810
+ ## scout
811
+
812
+ Review
813
+ `, "utf-8");
814
+
815
+ const result = discoverAgentsAll(dir);
816
+ assert.ok(result.project.find((agent) => agent.name === "scout" && agent.filePath === path.join(nestedDir, "scout.md")));
817
+ assert.ok(result.chains.find((chain) => chain.name === "review-flow" && chain.filePath === path.join(nestedChainDir, "review.chain.md")));
818
+ assert.equal(result.project.some((agent) => agent.filePath.endsWith("review.chain.md")), false);
819
+ });
820
+
821
+ it("registers packaged agents by runtime name and serializes local name plus package", () => {
822
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-packaged-agent-"));
823
+ tempDirs.push(dir);
824
+ const agentsDir = path.join(dir, ".selesai", "agents");
825
+ fs.mkdirSync(agentsDir, { recursive: true });
826
+ fs.writeFileSync(path.join(agentsDir, "scout.md"), `---
827
+ name: scout
828
+ package: code-analysis
829
+ description: Fast recon
830
+ ---
831
+
832
+ Inspect code
833
+ `, "utf-8");
834
+
835
+ const scout = discoverAgents(dir, "project").agents.find((agent) => agent.name === "code-analysis.scout");
836
+ assert.ok(scout);
837
+ assert.equal(scout.localName, "scout");
838
+ assert.equal(scout.packageName, "code-analysis");
839
+ const serialized = serializeAgent(scout);
840
+ assert.match(serialized, /^name: scout$/m);
841
+ assert.match(serialized, /^package: code-analysis$/m);
842
+ assert.doesNotMatch(serialized, /^name: code-analysis\.scout$/m);
843
+ });
844
+
845
+ it("recursively discovers packaged chains by runtime name and preserves package on serialize", () => {
846
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-packaged-chain-"));
847
+ tempDirs.push(dir);
848
+ const nestedDir = path.join(dir, ".selesai", "chains", "flows");
849
+ fs.mkdirSync(nestedDir, { recursive: true });
850
+ const content = `---
851
+ name: review-flow
852
+ package: code-analysis
853
+ description: Review flow
854
+ ---
855
+
856
+ ## code-analysis.scout
857
+
858
+ Inspect {task}
859
+ `;
860
+ fs.writeFileSync(path.join(nestedDir, "review.chain.md"), content, "utf-8");
861
+
862
+ const chain = discoverAgentsAll(dir).chains.find((candidate) => candidate.name === "code-analysis.review-flow");
863
+ assert.ok(chain);
864
+ assert.equal(chain.localName, "review-flow");
865
+ assert.equal(chain.packageName, "code-analysis");
866
+ assert.equal(chain.steps[0]?.agent, "code-analysis.scout");
867
+ const serialized = serializeChain(chain);
868
+ assert.match(serialized, /^name: review-flow$/m);
869
+ assert.match(serialized, /^package: code-analysis$/m);
870
+ assert.match(serialized, /^## code-analysis\.scout$/m);
871
+ assert.doesNotMatch(serialized, /^name: code-analysis\.review-flow$/m);
872
+ });
873
+
874
+ it("keeps packaged and un-packaged runtime names distinct while preserving un-packaged precedence", () => {
875
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-packaged-collisions-"));
876
+ tempDirs.push(dir);
877
+ fs.mkdirSync(path.join(dir, ".agents"), { recursive: true });
878
+ fs.mkdirSync(path.join(dir, ".selesai", "agents"), { recursive: true });
879
+ fs.writeFileSync(path.join(dir, ".agents", "scout.md"), `---
880
+ name: scout
881
+ description: Legacy scout
882
+ ---
883
+
884
+ Legacy
885
+ `, "utf-8");
886
+ fs.writeFileSync(path.join(dir, ".selesai", "agents", "scout.md"), `---
887
+ name: scout
888
+ description: Project scout
889
+ ---
890
+
891
+ Project
892
+ `, "utf-8");
893
+ fs.writeFileSync(path.join(dir, ".selesai", "agents", "packaged.md"), `---
894
+ name: scout
895
+ package: code-analysis
896
+ description: Packaged scout
897
+ ---
898
+
899
+ Packaged
900
+ `, "utf-8");
901
+
902
+ const agents = discoverAgents(dir, "project").agents;
903
+ const unqualified = agents.find((agent) => agent.name === "scout");
904
+ const packaged = agents.find((agent) => agent.name === "code-analysis.scout");
905
+ assert.equal(unqualified?.description, "Project scout");
906
+ assert.equal(unqualified?.filePath, path.join(dir, ".selesai", "agents", "scout.md"));
907
+ assert.equal(packaged?.description, "Packaged scout");
908
+ });
909
+
910
+ it("parses packaged chains directly from serializer helpers", () => {
911
+ const parsed = parseChain(`---
912
+ name: review-flow
913
+ package: code-analysis
914
+ description: Review flow
915
+ ---
916
+
917
+ ## code-analysis.scout
918
+
919
+ Inspect
920
+ `, "project", "/tmp/review.chain.md");
921
+
922
+ assert.equal(parsed.name, "code-analysis.review-flow");
923
+ assert.equal(parsed.localName, "review-flow");
924
+ assert.equal(parsed.packageName, "code-analysis");
925
+ assert.match(serializeChain(parsed), /^name: review-flow$/m);
926
+ });
927
+
928
+ it("normalizes package frontmatter consistently for agents and chains", () => {
929
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-package-normalize-"));
930
+ tempDirs.push(dir);
931
+ const agentsDir = path.join(dir, ".selesai", "agents");
932
+ const chainsDir = path.join(dir, ".selesai", "chains");
933
+ fs.mkdirSync(agentsDir, { recursive: true });
934
+ fs.mkdirSync(chainsDir, { recursive: true });
935
+ fs.writeFileSync(path.join(agentsDir, "scout.md"), `---
936
+ name: scout
937
+ package: Code Analysis!
938
+ description: Fast recon
939
+ ---
940
+
941
+ Inspect
942
+ `, "utf-8");
943
+ fs.writeFileSync(path.join(chainsDir, "review.chain.md"), `---
944
+ name: review-flow
945
+ package: Code Analysis!
946
+ description: Review flow
947
+ ---
948
+
949
+ ## code-analysis.scout
950
+
951
+ Review
952
+ `, "utf-8");
953
+
954
+ const result = discoverAgentsAll(dir);
955
+ assert.ok(result.project.find((agent) => agent.name === "code-analysis.scout"));
956
+ assert.ok(result.chains.find((chain) => chain.name === "code-analysis.review-flow"));
957
+ });
958
+
959
+ it("skips invalid package frontmatter that cannot be normalized", () => {
960
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-invalid-package-"));
961
+ tempDirs.push(dir);
962
+ const agentsDir = path.join(dir, ".selesai", "agents");
963
+ const chainsDir = path.join(dir, ".selesai", "chains");
964
+ fs.mkdirSync(agentsDir, { recursive: true });
965
+ fs.mkdirSync(chainsDir, { recursive: true });
966
+ fs.writeFileSync(path.join(agentsDir, "scout.md"), `---
967
+ name: scout
968
+ package: !!!
969
+ description: Fast recon
970
+ ---
971
+
972
+ Inspect
973
+ `, "utf-8");
974
+ fs.writeFileSync(path.join(chainsDir, "review.chain.md"), `---
975
+ name: review-flow
976
+ package: !!!
977
+ description: Review flow
978
+ ---
979
+
980
+ ## scout
981
+
982
+ Review
983
+ `, "utf-8");
984
+
985
+ const result = discoverAgentsAll(dir);
986
+ assert.equal(result.project.some((agent) => agent.filePath.endsWith("scout.md")), false);
987
+ assert.equal(result.chains.some((chain) => chain.filePath.endsWith("review.chain.md")), false);
988
+ });
989
+ });
990
+
991
+ describe("project agent directory discovery", () => {
992
+ it("discovers project agents from both .agents and .pi/agents", () => {
993
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-project-agent-dirs-"));
994
+ tempDirs.push(dir);
995
+ fs.mkdirSync(path.join(dir, ".agents", "skills"), { recursive: true });
996
+ fs.mkdirSync(path.join(dir, ".selesai", "agents"), { recursive: true });
997
+ fs.writeFileSync(path.join(dir, ".agents", "legacy.md"), `---
998
+ name: legacy
999
+ description: Legacy
1000
+ ---
1001
+
1002
+ Legacy prompt
1003
+ `, "utf-8");
1004
+ fs.writeFileSync(path.join(dir, ".selesai", "agents", "canonical.md"), `---
1005
+ name: canonical
1006
+ description: Canonical
1007
+ ---
1008
+
1009
+ Canonical prompt
1010
+ `, "utf-8");
1011
+ fs.writeFileSync(path.join(dir, ".selesai", "agents", "SKILL.md"), `---
1012
+ name: skill-named-agent
1013
+ description: Skill-named agent
1014
+ ---
1015
+
1016
+ Skill-named agent prompt
1017
+ `, "utf-8");
1018
+
1019
+ const result = discoverAgents(dir, "project");
1020
+ assert.ok(result.agents.find((agent) => agent.name === "legacy" && agent.filePath === path.join(dir, ".agents", "legacy.md")));
1021
+ assert.ok(result.agents.find((agent) => agent.name === "canonical" && agent.filePath === path.join(dir, ".selesai", "agents", "canonical.md")));
1022
+ assert.ok(result.agents.find((agent) => agent.name === "skill-named-agent" && agent.filePath === path.join(dir, ".selesai", "agents", "SKILL.md")));
1023
+ assert.equal(result.projectAgentsDir, path.join(dir, ".selesai", "agents"));
1024
+ });
1025
+
1026
+ it("does not register legacy project skill files as agents", () => {
1027
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-project-skills-not-agents-"));
1028
+ tempDirs.push(dir);
1029
+ writeAgent(path.join(dir, ".agents", "legacy.md"), `---
1030
+ name: legacy
1031
+ description: Legacy
1032
+ ---
1033
+
1034
+ Legacy prompt
1035
+ `);
1036
+ writeAgent(path.join(dir, ".agents", "skills", "directory-skill", "SKILL.md"), `---
1037
+ name: directory-skill
1038
+ description: Directory skill
1039
+ ---
1040
+
1041
+ Skill prompt
1042
+ `);
1043
+ writeAgent(path.join(dir, ".agents", "skills", "file-skill.md"), `---
1044
+ name: file-skill
1045
+ description: File skill
1046
+ ---
1047
+
1048
+ Skill prompt
1049
+ `);
1050
+
1051
+ const agents = discoverAgents(dir, "project").agents;
1052
+ assert.ok(agents.find((agent) => agent.name === "legacy"));
1053
+ assert.equal(agents.some((agent) => agent.filePath.includes(`${path.sep}.agents${path.sep}skills${path.sep}`)), false);
1054
+ assert.equal(agents.some((agent) => agent.name === "directory-skill"), false);
1055
+ assert.equal(agents.some((agent) => agent.name === "file-skill"), false);
1056
+ });
1057
+
1058
+ it("does not register user SKILL.md files as agents", () => withTempHome((home) => {
1059
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-user-skills-not-agents-"));
1060
+ tempDirs.push(dir);
1061
+ writeAgent(path.join(home, ".agents", "user-agent.md"), `---
1062
+ name: user-agent
1063
+ description: User agent
1064
+ ---
1065
+
1066
+ User prompt
1067
+ `);
1068
+ writeAgent(path.join(home, ".agents", "skills", "user-skill", "SKILL.md"), `---
1069
+ name: user-skill
1070
+ description: User skill
1071
+ ---
1072
+
1073
+ Skill prompt
1074
+ `);
1075
+
1076
+ const agents = discoverAgents(dir, "user").agents;
1077
+ assert.ok(agents.find((agent) => agent.name === "user-agent"));
1078
+ assert.equal(agents.some((agent) => agent.filePath.includes(`${path.sep}.agents${path.sep}skills${path.sep}`)), false);
1079
+ assert.equal(agents.some((agent) => agent.name === "user-skill"), false);
1080
+ }));
1081
+
1082
+ it("prefers .pi/agents over .agents on project agent name collisions", () => {
1083
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-project-agent-collision-"));
1084
+ tempDirs.push(dir);
1085
+ fs.mkdirSync(path.join(dir, ".agents"), { recursive: true });
1086
+ fs.mkdirSync(path.join(dir, ".selesai", "agents"), { recursive: true });
1087
+ fs.writeFileSync(path.join(dir, ".agents", "shared.md"), `---
1088
+ name: shared
1089
+ description: Legacy shared
1090
+ ---
1091
+
1092
+ Legacy prompt
1093
+ `, "utf-8");
1094
+ fs.writeFileSync(path.join(dir, ".selesai", "agents", "shared.md"), `---
1095
+ name: shared
1096
+ description: Canonical shared
1097
+ ---
1098
+
1099
+ Canonical prompt
1100
+ `, "utf-8");
1101
+
1102
+ const shared = discoverAgents(dir, "project").agents.find((agent) => agent.name === "shared");
1103
+ assert.ok(shared);
1104
+ assert.equal(shared.filePath, path.join(dir, ".selesai", "agents", "shared.md"));
1105
+ assert.equal(shared.description, "Canonical shared");
1106
+ assert.equal(shared.systemPrompt.trim(), "Canonical prompt");
1107
+ });
1108
+
1109
+ it("uses the project root for the canonical project agent dir even when only .agents exists", () => {
1110
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-project-agent-root-"));
1111
+ tempDirs.push(dir);
1112
+ const nested = path.join(dir, "packages", "app");
1113
+ fs.mkdirSync(path.join(dir, ".agents", "skills"), { recursive: true });
1114
+ fs.mkdirSync(nested, { recursive: true });
1115
+
1116
+ const result = discoverAgentsAll(nested);
1117
+ assert.equal(result.projectDir, path.join(dir, ".selesai", "agents"));
1118
+ });
1119
+
1120
+ it("discovers project chains from .pi/chains", () => {
1121
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-project-chain-dirs-"));
1122
+ tempDirs.push(dir);
1123
+ fs.mkdirSync(path.join(dir, ".selesai", "agents"), { recursive: true });
1124
+ fs.mkdirSync(path.join(dir, ".selesai", "chains", "flows"), { recursive: true });
1125
+ fs.writeFileSync(path.join(dir, ".selesai", "agents", "ignored.chain.md"), `---
1126
+ name: ignored-chain
1127
+ description: Ignored chain
1128
+ ---
1129
+
1130
+ ## scout
1131
+
1132
+ Ignore
1133
+ `, "utf-8");
1134
+ fs.writeFileSync(path.join(dir, ".selesai", "chains", "flows", "canonical.chain.md"), `---
1135
+ name: canonical-chain
1136
+ description: Canonical chain
1137
+ ---
1138
+
1139
+ ## worker
1140
+
1141
+ Inspect canonical
1142
+ `, "utf-8");
1143
+
1144
+ const result = discoverAgentsAll(dir);
1145
+ assert.equal(result.chains.some((chain) => chain.name === "ignored-chain"), false);
1146
+ assert.ok(result.chains.find((chain) => chain.name === "canonical-chain" && chain.filePath === path.join(dir, ".selesai", "chains", "flows", "canonical.chain.md")));
1147
+ assert.equal(result.projectDir, path.join(dir, ".selesai", "agents"));
1148
+ assert.equal(result.projectChainDir, path.join(dir, ".selesai", "chains"));
1149
+ });
1150
+
1151
+ it("prefers project .pi/chains over user chains on name collisions", () => {
1152
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-project-chain-collision-"));
1153
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-user-chain-home-"));
1154
+ tempDirs.push(dir, home);
1155
+ const oldHome = process.env.HOME;
1156
+ const oldUserProfile = process.env.USERPROFILE;
1157
+ process.env.HOME = home;
1158
+ process.env.USERPROFILE = home;
1159
+ try {
1160
+ const userChainsDir = path.join(home, ".selesai", "agent", "chains");
1161
+ fs.mkdirSync(userChainsDir, { recursive: true });
1162
+ fs.mkdirSync(path.join(dir, ".selesai", "chains"), { recursive: true });
1163
+ fs.writeFileSync(path.join(userChainsDir, "shared.chain.md"), `---
1164
+ name: shared-chain
1165
+ description: User chain
1166
+ ---
1167
+
1168
+ ## scout
1169
+
1170
+ Inspect user
1171
+ `, "utf-8");
1172
+ fs.writeFileSync(path.join(dir, ".selesai", "chains", "shared.chain.md"), `---
1173
+ name: shared-chain
1174
+ description: Project chain
1175
+ ---
1176
+
1177
+ ## worker
1178
+
1179
+ Inspect project
1180
+ `, "utf-8");
1181
+
1182
+ const sharedChains = discoverAgentsAll(dir).chains.filter((chain) => chain.name === "shared-chain");
1183
+ assert.equal(sharedChains.length, 2);
1184
+ assert.deepEqual(sharedChains.map((chain) => chain.source), ["user", "project"]);
1185
+ const savedChainLookup = new Map(sharedChains.map((chain) => [chain.name, chain]));
1186
+ const shared = savedChainLookup.get("shared-chain");
1187
+ assert.ok(shared);
1188
+ assert.equal(shared.filePath, path.join(dir, ".selesai", "chains", "shared.chain.md"));
1189
+ assert.equal(shared.description, "Project chain");
1190
+ assert.equal(shared.steps[0]?.agent, "worker");
1191
+ assert.equal(shared.steps[0]?.task, "Inspect project");
1192
+ } finally {
1193
+ if (oldHome === undefined) delete process.env.HOME;
1194
+ else process.env.HOME = oldHome;
1195
+ if (oldUserProfile === undefined) delete process.env.USERPROFILE;
1196
+ else process.env.USERPROFILE = oldUserProfile;
1197
+ }
1198
+ });
1199
+ });