@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,421 @@
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 { describe, it } from "node:test";
6
+ import {
7
+ acceptanceFailureMessage,
8
+ aggregateAcceptanceReport,
9
+ evaluateAcceptance,
10
+ formatAcceptancePrompt,
11
+ parseAcceptanceReport,
12
+ resolveEffectiveAcceptance,
13
+ stripAcceptanceReport,
14
+ validateAcceptanceInput,
15
+ } from "../../src/runs/shared/acceptance.ts";
16
+
17
+ function reportData(overrides: Record<string, unknown> = {}): Record<string, unknown> {
18
+ return {
19
+ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "verified in test" }],
20
+ changedFiles: ["src/file.ts"],
21
+ testsAddedOrUpdated: ["test/file.test.ts"],
22
+ commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }],
23
+ validationOutput: ["tests passed"],
24
+ residualRisks: [],
25
+ noStagedFiles: true,
26
+ notes: "complete",
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ function report(overrides: Record<string, unknown> = {}, fence = "acceptance-report"): string {
32
+ return [
33
+ "done",
34
+ `\`\`\`${fence}`,
35
+ JSON.stringify(reportData(overrides)),
36
+ "```",
37
+ ].join("\n");
38
+ }
39
+
40
+ function tempRepo(): string {
41
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "subagent-acceptance-"));
42
+ fs.writeFileSync(path.join(dir, "file.txt"), "hello\n", "utf-8");
43
+ return dir;
44
+ }
45
+
46
+ describe("acceptance gates", () => {
47
+ it("infers different policies for reviewer, writer, async writer, and dynamic contexts", () => {
48
+ assert.equal(resolveEffectiveAcceptance({ agentName: "reviewer", task: "Review-only. Do not edit.", mode: "single" }).level, "attested");
49
+ assert.equal(resolveEffectiveAcceptance({ agentName: "worker", task: "Implement the fix", mode: "single" }).level, "checked");
50
+ assert.equal(resolveEffectiveAcceptance({ agentName: "worker", task: "Implement the fix", mode: "single", async: true }).level, "reviewed");
51
+ assert.equal(resolveEffectiveAcceptance({ agentName: "worker", task: "Fix each item", mode: "chain", dynamic: true }).level, "reviewed");
52
+ });
53
+
54
+ it("explicit acceptance can strengthen inferred policy", () => {
55
+ const resolved = resolveEffectiveAcceptance({
56
+ agentName: "reviewer",
57
+ task: "Review-only.",
58
+ explicit: { level: "verified", verify: [{ id: "ok", command: "node --version" }] },
59
+ });
60
+
61
+ assert.equal(resolved.level, "verified");
62
+ assert.equal(resolved.verify[0]?.id, "ok");
63
+ });
64
+
65
+ it("formats a standardized child prompt section", () => {
66
+ const resolved = resolveEffectiveAcceptance({
67
+ agentName: "worker",
68
+ task: "Implement a fix",
69
+ explicit: { level: "checked", criteria: ["Patch the bug"], stopRules: ["Do not stop after analysis"] },
70
+ });
71
+ const prompt = formatAcceptancePrompt(resolved);
72
+
73
+ assert.match(prompt, /## Acceptance Contract/);
74
+ assert.match(prompt, /Acceptance level: checked/);
75
+ assert.match(prompt, /Patch the bug/);
76
+ assert.match(prompt, /```acceptance-report/);
77
+ assert.match(prompt, /array fields contain strings/);
78
+ assert.match(prompt, /"reviewFindings": \[\n "blocker:/);
79
+ });
80
+
81
+ it("parses acceptance-report fences and ignores unrelated json fences", () => {
82
+ const parsed = parseAcceptanceReport(report());
83
+
84
+ assert.ok(parsed.report);
85
+ assert.deepEqual(parsed.report.changedFiles, ["src/file.ts"]);
86
+ assert.equal(parsed.error, undefined);
87
+
88
+ const genericJson = parseAcceptanceReport(`done\n\
89
+ \
90
+ \`\`\`json\n{\"notes\":\"not an acceptance report\"}\n\`\`\``);
91
+ assert.equal(genericJson.report, undefined);
92
+ assert.match(genericJson.error ?? "", /Structured acceptance report not found/);
93
+
94
+ const criteriaOnlyJson = parseAcceptanceReport(`done\n\
95
+ \
96
+ \`\`\`json\n{\"criteriaSatisfied\":[{\"id\":\"criterion-1\",\"status\":\"satisfied\",\"evidence\":\"example\"}]}\n\`\`\``);
97
+ assert.equal(criteriaOnlyJson.report, undefined);
98
+ assert.match(criteriaOnlyJson.error ?? "", /Structured acceptance report not found/);
99
+
100
+ const invalidSignalJson = `done\n\
101
+ \
102
+ \`\`\`json\n{\"criteriaSatisfied\":[{\"id\":\"criterion-1\",\"status\":\"satisfied\",\"evidence\":\"example\"}],\"changedFiles\":false}\n\`\`\``;
103
+ const genericJsonWithInvalidSignal = parseAcceptanceReport(invalidSignalJson);
104
+ assert.equal(genericJsonWithInvalidSignal.report, undefined);
105
+ assert.match(genericJsonWithInvalidSignal.error ?? "", /Structured acceptance report not found/);
106
+ assert.equal(stripAcceptanceReport(invalidSignalJson), invalidSignalJson);
107
+
108
+ const partialWrapperJson = `done\n\
109
+ \
110
+ \`\`\`json\n{\"acceptance\":{\"changedFiles\":[\"src/file.ts\"]}}\n\`\`\``;
111
+ const genericJsonWithPartialWrapper = parseAcceptanceReport(partialWrapperJson);
112
+ assert.equal(genericJsonWithPartialWrapper.report, undefined);
113
+ assert.match(genericJsonWithPartialWrapper.error ?? "", /Structured acceptance report not found/);
114
+ assert.equal(stripAcceptanceReport(partialWrapperJson), partialWrapperJson);
115
+
116
+ const reportShapedJson = `done\n\
117
+ \
118
+ \`\`\`json\n{\"changedFiles\":[\"src/file.ts\"]}\n\`\`\``;
119
+ const genericReportShapedJson = parseAcceptanceReport(reportShapedJson);
120
+ assert.equal(genericReportShapedJson.report, undefined);
121
+ assert.match(genericReportShapedJson.error ?? "", /Structured acceptance report not found/);
122
+ assert.equal(stripAcceptanceReport(reportShapedJson), reportShapedJson);
123
+
124
+ const malformed = parseAcceptanceReport("```acceptance-report\n{bad-json\n```");
125
+ assert.equal(malformed.report, undefined);
126
+ assert.match(malformed.error ?? "", /Failed to parse acceptance-report/);
127
+ });
128
+
129
+ it("parses acceptance reports from json-family fences", () => {
130
+ for (const fence of ["json", "jsonc", "json5"]) {
131
+ const output = report({}, fence);
132
+ const parsed = parseAcceptanceReport(output);
133
+
134
+ assert.ok(parsed.report);
135
+ assert.deepEqual(parsed.report.changedFiles, ["src/file.ts"]);
136
+ assert.equal(parsed.error, undefined);
137
+ assert.equal(stripAcceptanceReport(output), "done");
138
+ }
139
+ });
140
+
141
+ it("strips trailing json-family reports after earlier unrelated json fences", () => {
142
+ const output = [
143
+ "metadata",
144
+ "```json",
145
+ JSON.stringify({ notes: "not an acceptance report" }),
146
+ "```",
147
+ "done",
148
+ "```json",
149
+ JSON.stringify(reportData()),
150
+ "```",
151
+ ].join("\n");
152
+ const parsed = parseAcceptanceReport(output);
153
+
154
+ assert.ok(parsed.report);
155
+ assert.equal(stripAcceptanceReport(output), [
156
+ "metadata",
157
+ "```json",
158
+ JSON.stringify({ notes: "not an acceptance report" }),
159
+ "```",
160
+ "done",
161
+ ].join("\n"));
162
+ });
163
+
164
+ it("unwraps acceptance-report wrapper objects", () => {
165
+ const output = [
166
+ "done",
167
+ "```json",
168
+ JSON.stringify({ "acceptance-report": reportData() }),
169
+ "```",
170
+ ].join("\n");
171
+ const parsed = parseAcceptanceReport(output);
172
+
173
+ assert.ok(parsed.report);
174
+ assert.deepEqual(parsed.report.testsAddedOrUpdated, ["test/file.test.ts"]);
175
+ assert.equal(stripAcceptanceReport(output), "done");
176
+ });
177
+
178
+ it("reports field-level validation errors for malformed acceptance-report fields", () => {
179
+ const invalidReviewerReport = parseAcceptanceReport(report({
180
+ reviewFindings: [{ id: "B-1", severity: "blocker", finding: "Missing evidence" }],
181
+ }));
182
+ assert.equal(invalidReviewerReport.report, undefined);
183
+ assert.match(invalidReviewerReport.error ?? "", /reviewFindings\[0\]: expected string; got object/);
184
+
185
+ const invalidCommandReport = parseAcceptanceReport(report({
186
+ commandsRun: [{ command: "npm test", exitCode: 0 }],
187
+ }));
188
+ assert.equal(invalidCommandReport.report, undefined);
189
+ assert.match(invalidCommandReport.error ?? "", /commandsRun\[0\]\.result: expected one of "passed", "failed", "not-run"; got missing/);
190
+ assert.match(invalidCommandReport.error ?? "", /commandsRun\[0\]\.summary: expected string; got missing/);
191
+
192
+ const invalidCriteriaReport = parseAcceptanceReport(report({
193
+ criteriaSatisfied: [{ id: 7, status: "done", evidence: "" }],
194
+ }));
195
+ assert.equal(invalidCriteriaReport.report, undefined);
196
+ assert.match(invalidCriteriaReport.error ?? "", /criteriaSatisfied\[0\]\.id: expected string; got number 7/);
197
+ assert.match(invalidCriteriaReport.error ?? "", /criteriaSatisfied\[0\]\.status: expected one of "satisfied", "not-satisfied", "not-applicable"; got "done"/);
198
+ assert.match(invalidCriteriaReport.error ?? "", /criteriaSatisfied\[0\]\.evidence: expected non-empty string; got ""/);
199
+ });
200
+
201
+ it("explicit none disables inferred gates when a reason is present", () => {
202
+ const acceptance = resolveEffectiveAcceptance({
203
+ agentName: "worker",
204
+ task: "Implement a fix",
205
+ explicit: { level: "none", reason: "parent is doing manual acceptance" },
206
+ });
207
+
208
+ assert.equal(acceptance.level, "none");
209
+ assert.deepEqual(acceptance.evidence, []);
210
+ });
211
+
212
+ it("checked mode rejects missing required evidence", async () => {
213
+ const cwd = tempRepo();
214
+ try {
215
+ const acceptance = resolveEffectiveAcceptance({
216
+ agentName: "worker",
217
+ task: "Implement a fix",
218
+ explicit: { level: "checked" },
219
+ });
220
+ const ledger = await evaluateAcceptance({
221
+ acceptance,
222
+ output: report({ testsAddedOrUpdated: [] }),
223
+ cwd,
224
+ });
225
+
226
+ assert.equal(ledger.status, "rejected");
227
+ assert.match(acceptanceFailureMessage(ledger) ?? "", /tests-added evidence missing/);
228
+ } finally {
229
+ fs.rmSync(cwd, { recursive: true, force: true });
230
+ }
231
+ });
232
+
233
+ it("surfaces parse validation details in acceptance failure messages", async () => {
234
+ const cwd = tempRepo();
235
+ try {
236
+ const acceptance = resolveEffectiveAcceptance({
237
+ agentName: "reviewer",
238
+ task: "Review-only. Do not edit.",
239
+ explicit: { level: "attested", evidence: ["review-findings"] },
240
+ });
241
+ const ledger = await evaluateAcceptance({
242
+ acceptance,
243
+ output: report({ reviewFindings: [{ id: "B-1", finding: "Missing evidence" }] }),
244
+ cwd,
245
+ });
246
+
247
+ assert.equal(ledger.status, "rejected");
248
+ assert.match(acceptanceFailureMessage(ledger) ?? "", /Failed to parse acceptance-report/);
249
+ assert.match(acceptanceFailureMessage(ledger) ?? "", /reviewFindings\[0\]: expected string; got object/);
250
+ } finally {
251
+ fs.rmSync(cwd, { recursive: true, force: true });
252
+ }
253
+ });
254
+
255
+ it("checked mode rejects not-satisfied required criteria", async () => {
256
+ const cwd = tempRepo();
257
+ try {
258
+ const acceptance = resolveEffectiveAcceptance({
259
+ agentName: "worker",
260
+ task: "Implement a fix",
261
+ explicit: { level: "checked", criteria: [{ id: "regression", must: "Regression is covered" }] },
262
+ });
263
+ const ledger = await evaluateAcceptance({
264
+ acceptance,
265
+ output: report({ criteriaSatisfied: [{ id: "regression", status: "not-satisfied", evidence: "test missing" }] }),
266
+ cwd,
267
+ });
268
+
269
+ assert.equal(ledger.status, "rejected");
270
+ assert.match(acceptanceFailureMessage(ledger) ?? "", /Required criterion 'regression' was reported as not-satisfied/);
271
+ } finally {
272
+ fs.rmSync(cwd, { recursive: true, force: true });
273
+ }
274
+ });
275
+
276
+ it("verified mode records runtime command success and failure separately from child command claims", async () => {
277
+ const cwd = tempRepo();
278
+ try {
279
+ const passing = resolveEffectiveAcceptance({
280
+ agentName: "worker",
281
+ task: "Implement a fix",
282
+ explicit: { level: "verified", verify: [{ id: "pass", command: "node -e \"process.exit(0)\"", timeoutMs: 10_000 }] },
283
+ });
284
+ const passLedger = await evaluateAcceptance({ acceptance: passing, output: report(), cwd });
285
+ assert.equal(passLedger.status, "verified");
286
+ assert.equal(passLedger.verifyRuns[0]?.status, "passed");
287
+
288
+ const failing = resolveEffectiveAcceptance({
289
+ agentName: "worker",
290
+ task: "Implement a fix",
291
+ explicit: { level: "verified", verify: [{ id: "fail", command: "node -e \"process.exit(7)\"", timeoutMs: 10_000 }] },
292
+ });
293
+ const failLedger = await evaluateAcceptance({ acceptance: failing, output: report(), cwd });
294
+ assert.equal(failLedger.status, "rejected");
295
+ assert.equal(failLedger.childReport?.commandsRun?.[0]?.result, "passed");
296
+ assert.equal(failLedger.verifyRuns[0]?.status, "failed");
297
+ } finally {
298
+ fs.rmSync(cwd, { recursive: true, force: true });
299
+ }
300
+ });
301
+
302
+ it("reviewed mode records no-blocker and blocker reviewer outcomes", async () => {
303
+ const cwd = tempRepo();
304
+ try {
305
+ const acceptance = resolveEffectiveAcceptance({
306
+ agentName: "worker",
307
+ task: "Implement a risky fix",
308
+ explicit: { level: "reviewed", review: { agent: "reviewer", required: true } },
309
+ });
310
+ const noBlockers = await evaluateAcceptance({
311
+ acceptance,
312
+ output: report(),
313
+ cwd,
314
+ reviewResult: { status: "no-blockers", findings: [] },
315
+ });
316
+ assert.equal(noBlockers.status, "reviewed");
317
+ assert.equal(noBlockers.reviewResult?.status, "no-blockers");
318
+
319
+ const blockers = await evaluateAcceptance({
320
+ acceptance,
321
+ output: report(),
322
+ cwd,
323
+ reviewResult: {
324
+ status: "blockers",
325
+ findings: [{ severity: "blocker", issue: "Missing test", rationale: "Acceptance requires test evidence." }],
326
+ },
327
+ });
328
+ assert.equal(blockers.status, "rejected");
329
+ assert.equal(blockers.reviewResult?.status, "blockers");
330
+
331
+ const unavailable = await evaluateAcceptance({ acceptance, output: report(), cwd });
332
+ assert.equal(unavailable.status, "rejected");
333
+ assert.equal(unavailable.reviewResult?.status, "needs-parent-decision");
334
+ } finally {
335
+ fs.rmSync(cwd, { recursive: true, force: true });
336
+ }
337
+ });
338
+
339
+ it("does not make explicit checked acceptance an explicit reviewed blocker when inference recommends review", async () => {
340
+ const cwd = tempRepo();
341
+ try {
342
+ const acceptance = resolveEffectiveAcceptance({
343
+ agentName: "worker",
344
+ task: "Implement each dynamic item",
345
+ dynamic: true,
346
+ explicit: { level: "checked" },
347
+ });
348
+
349
+ assert.equal(acceptance.level, "reviewed");
350
+ assert.equal(acceptance.review && acceptance.review !== false ? acceptance.review.required : undefined, false);
351
+ const ledger = await evaluateAcceptance({ acceptance, output: report({ criteriaSatisfied: [
352
+ { id: "criterion-1", status: "satisfied", evidence: "implemented" },
353
+ { id: "criterion-2", status: "satisfied", evidence: "evidence returned" },
354
+ ] }), cwd });
355
+ assert.equal(ledger.status, "checked");
356
+ assert.equal(ledger.reviewResult?.status, "needs-parent-decision");
357
+ } finally {
358
+ fs.rmSync(cwd, { recursive: true, force: true });
359
+ }
360
+ });
361
+
362
+ it("does not mark reviewed without an independent reviewer result", async () => {
363
+ const cwd = tempRepo();
364
+ try {
365
+ const acceptance = resolveEffectiveAcceptance({
366
+ agentName: "worker",
367
+ task: "Implement a fix",
368
+ explicit: {
369
+ level: "reviewed",
370
+ review: false,
371
+ },
372
+ });
373
+ assert.equal(acceptance.level, "reviewed");
374
+
375
+ const ledger = await evaluateAcceptance({ acceptance, output: report(), cwd });
376
+ assert.equal(ledger.status, "rejected");
377
+ assert.equal(ledger.reviewResult?.status, "needs-parent-decision");
378
+ assert.match(acceptanceFailureMessage(ledger) ?? "", /review required/i);
379
+ } finally {
380
+ fs.rmSync(cwd, { recursive: true, force: true });
381
+ }
382
+ });
383
+
384
+ it("zero-child aggregate reports do not fabricate required evidence", async () => {
385
+ const cwd = tempRepo();
386
+ try {
387
+ const acceptance = resolveEffectiveAcceptance({
388
+ agentName: "worker",
389
+ task: "Implement dynamic fanout fixes",
390
+ explicit: { level: "checked" },
391
+ });
392
+ const ledger = await evaluateAcceptance({
393
+ acceptance,
394
+ output: "",
395
+ report: aggregateAcceptanceReport({ results: [] }),
396
+ cwd,
397
+ });
398
+
399
+ assert.equal(ledger.status, "rejected");
400
+ assert.match(acceptanceFailureMessage(ledger) ?? "", /criterion|changed-files|tests-added|commands-run|validation-output|no-staged-files/);
401
+ } finally {
402
+ fs.rmSync(cwd, { recursive: true, force: true });
403
+ }
404
+ });
405
+
406
+ it("validates invalid disable and verify shapes", () => {
407
+ assert.deepEqual(validateAcceptanceInput({ level: "none" }), ["acceptance.reason is required when level is none."]);
408
+ assert.deepEqual(validateAcceptanceInput({ verify: [{ id: "missing-command" }] }), ["acceptance.verify[0].command is required."]);
409
+ assert.deepEqual(validateAcceptanceInput({ verify: [{ id: "fractional", command: "npm test", timeoutMs: 1.5 }] }), ["acceptance.verify[0].timeoutMs must be an integer >= 1."]);
410
+ assert.deepEqual(validateAcceptanceInput(false), []);
411
+ assert.deepEqual(validateAcceptanceInput("checked"), []);
412
+ assert.deepEqual(validateAcceptanceInput({ criteria: ["ship the fix"], review: false, stopRules: ["stay scoped"] }), []);
413
+ assert.match(validateAcceptanceInput({ criteria: [{ id: "missing-must" }] }).join("\n"), /acceptance\.criteria\[0\]\.must is required/);
414
+ assert.match(validateAcceptanceInput({ criteria: [123] }).join("\n"), /acceptance\.criteria\[0\] must be a string or an object/);
415
+ assert.match(validateAcceptanceInput({ evidence: ["bogus"] }).join("\n"), /acceptance\.evidence\[0\] is not a supported evidence kind/);
416
+ assert.match(validateAcceptanceInput({ review: true }).join("\n"), /acceptance\.review must be false or an object/);
417
+ assert.match(validateAcceptanceInput({ review: { required: "yes" } }).join("\n"), /acceptance\.review\.required must be a boolean/);
418
+ assert.match(validateAcceptanceInput({ stopRules: [123] }).join("\n"), /acceptance\.stopRules\[0\] must be a string/);
419
+ assert.match(validateAcceptanceInput({ surprise: true }).join("\n"), /acceptance\.surprise is not supported/);
420
+ });
421
+ });
@@ -0,0 +1,213 @@
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, beforeEach, describe, it } from "node:test";
6
+ import { buildBuiltinOverrideConfig, discoverAgents, discoverAgentsAll } from "../../src/agents/agents.ts";
7
+ import { handleList } from "../../src/agents/agent-management.ts";
8
+
9
+ let tempHome = "";
10
+ let tempProject = "";
11
+ const originalHome = process.env.HOME;
12
+ const originalUserProfile = process.env.USERPROFILE;
13
+
14
+ function writeJson(filePath: string, value: unknown): void {
15
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
16
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
17
+ }
18
+
19
+ function readText(result: { content: Array<{ type: string; text?: string }> }): string {
20
+ const first = result.content[0];
21
+ assert.ok(first);
22
+ assert.equal(first.type, "text");
23
+ assert.equal(typeof first.text, "string");
24
+ return first.text;
25
+ }
26
+
27
+ describe("builtin agent disabling", () => {
28
+ beforeEach(() => {
29
+ tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-disabled-home-"));
30
+ tempProject = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-disabled-project-"));
31
+ process.env.HOME = tempHome;
32
+ process.env.USERPROFILE = tempHome;
33
+ });
34
+
35
+ afterEach(() => {
36
+ if (originalHome === undefined) delete process.env.HOME;
37
+ else process.env.HOME = originalHome;
38
+ if (originalUserProfile === undefined) delete process.env.USERPROFILE;
39
+ else process.env.USERPROFILE = originalUserProfile;
40
+ fs.rmSync(tempHome, { recursive: true, force: true });
41
+ fs.rmSync(tempProject, { recursive: true, force: true });
42
+ });
43
+
44
+ it("filters a per-agent disabled builtin from runtime discovery while keeping it in discoverAgentsAll", () => {
45
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
46
+ subagents: {
47
+ agentOverrides: {
48
+ reviewer: { disabled: true },
49
+ },
50
+ },
51
+ });
52
+
53
+ const runtimeReviewer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "reviewer");
54
+ assert.equal(runtimeReviewer, undefined);
55
+
56
+ const allReviewer = discoverAgentsAll(tempProject).builtin.find((agent) => agent.name === "reviewer");
57
+ assert.ok(allReviewer);
58
+ assert.equal(allReviewer.disabled, true);
59
+ assert.equal(allReviewer.override?.scope, "user");
60
+ });
61
+
62
+ it("surfaces malformed disabled overrides instead of silently ignoring them", () => {
63
+ const settingsPath = path.join(tempHome, ".selesai", "agent", "settings.json");
64
+ writeJson(settingsPath, {
65
+ subagents: {
66
+ agentOverrides: {
67
+ reviewer: { disabled: "true" },
68
+ },
69
+ },
70
+ });
71
+
72
+ assert.throws(
73
+ () => discoverAgents(tempProject, "both"),
74
+ (error: unknown) => error instanceof Error
75
+ && error.message.includes(settingsPath)
76
+ && error.message.includes("reviewer")
77
+ && error.message.includes("disabled"),
78
+ );
79
+ });
80
+
81
+ it("bulk disableBuiltins hides builtins at runtime and marks them disabled in discoverAgentsAll", () => {
82
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
83
+ subagents: { disableBuiltins: true },
84
+ });
85
+
86
+ const runtimeBuiltinCount = discoverAgents(tempProject, "both").agents.filter((agent) => agent.source === "builtin").length;
87
+ assert.equal(runtimeBuiltinCount, 0);
88
+
89
+ const allBuiltins = discoverAgentsAll(tempProject).builtin;
90
+ assert.ok(allBuiltins.length > 0);
91
+ assert.ok(allBuiltins.every((agent) => agent.disabled === true));
92
+ assert.ok(allBuiltins.every((agent) => agent.override?.scope === "user"));
93
+ });
94
+
95
+ it("an explicit user override opts a builtin out of user-scope bulk disable", () => {
96
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
97
+ subagents: {
98
+ disableBuiltins: true,
99
+ agentOverrides: {
100
+ reviewer: { model: "openai/gpt-5.4" },
101
+ },
102
+ },
103
+ });
104
+
105
+ const reviewer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "reviewer");
106
+ assert.ok(reviewer);
107
+ assert.equal(reviewer.disabled, undefined);
108
+ assert.equal(reviewer.model, "openai/gpt-5.4");
109
+ assert.equal(reviewer.override?.scope, "user");
110
+ });
111
+
112
+ it("project disableBuiltins false re-enables builtins hidden by user bulk disable", () => {
113
+ fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
114
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
115
+ subagents: { disableBuiltins: true },
116
+ });
117
+ writeJson(path.join(tempProject, ".selesai", "settings.json"), {
118
+ subagents: { disableBuiltins: false },
119
+ });
120
+
121
+ assert.ok(discoverAgents(tempProject, "both").agents.some((agent) => agent.source === "builtin"));
122
+ });
123
+
124
+ it("project bulk disable beats user per-agent re-enable overrides", () => {
125
+ fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
126
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
127
+ subagents: {
128
+ disableBuiltins: true,
129
+ agentOverrides: {
130
+ reviewer: { disabled: false, model: "openai/gpt-5.4" },
131
+ },
132
+ },
133
+ });
134
+ writeJson(path.join(tempProject, ".selesai", "settings.json"), {
135
+ subagents: { disableBuiltins: true },
136
+ });
137
+
138
+ const reviewer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "reviewer");
139
+ assert.equal(reviewer, undefined);
140
+
141
+ const allReviewer = discoverAgentsAll(tempProject).builtin.find((agent) => agent.name === "reviewer");
142
+ assert.ok(allReviewer);
143
+ assert.equal(allReviewer.disabled, true);
144
+ assert.equal(allReviewer.override?.scope, "project");
145
+ });
146
+
147
+ it("surfaces malformed disableBuiltins values instead of silently ignoring them", () => {
148
+ const settingsPath = path.join(tempHome, ".selesai", "agent", "settings.json");
149
+ writeJson(settingsPath, {
150
+ subagents: { disableBuiltins: "true" },
151
+ });
152
+
153
+ assert.throws(
154
+ () => discoverAgents(tempProject, "both"),
155
+ (error: unknown) => error instanceof Error
156
+ && error.message.includes(settingsPath)
157
+ && error.message.includes("disableBuiltins"),
158
+ );
159
+ });
160
+
161
+ it("hides disabled builtins from agent-facing management list output", () => {
162
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
163
+ subagents: { disableBuiltins: true },
164
+ });
165
+ const agentsDir = path.join(tempProject, ".selesai", "agents");
166
+ fs.mkdirSync(agentsDir, { recursive: true });
167
+ fs.writeFileSync(
168
+ path.join(agentsDir, "helper.md"),
169
+ "---\nname: helper\ndescription: Helper\n---\n\nHelp.\n",
170
+ "utf-8",
171
+ );
172
+ const disabledBuiltinNames = discoverAgentsAll(tempProject).builtin.map((agent) => agent.name);
173
+ assert.ok(disabledBuiltinNames.length > 0);
174
+
175
+ const text = readText(handleList(
176
+ {},
177
+ { cwd: tempProject, modelRegistry: { getAvailable: () => [] } },
178
+ ));
179
+
180
+ assert.match(text, /Executable agents:\n- helper \(project\): Helper/);
181
+ assert.doesNotMatch(text, /Disabled builtins:/);
182
+ for (const name of disabledBuiltinNames) {
183
+ assert.doesNotMatch(text, new RegExp(`^- ${name} \\(builtin`, "m"));
184
+ }
185
+ });
186
+
187
+ it("buildBuiltinOverrideConfig emits disabled false when re-enabling a builtin", () => {
188
+ const override = buildBuiltinOverrideConfig(
189
+ {
190
+ systemPromptMode: "replace",
191
+ inheritProjectContext: false,
192
+ inheritSkills: false,
193
+ disabled: undefined,
194
+ systemPrompt: "Base prompt",
195
+ },
196
+ {
197
+ model: undefined,
198
+ fallbackModels: undefined,
199
+ thinking: undefined,
200
+ systemPromptMode: "replace",
201
+ inheritProjectContext: false,
202
+ inheritSkills: false,
203
+ disabled: false,
204
+ systemPrompt: "Base prompt",
205
+ skills: undefined,
206
+ tools: undefined,
207
+ mcpDirectTools: undefined,
208
+ },
209
+ );
210
+
211
+ assert.deepEqual(override, { disabled: false });
212
+ });
213
+ });