sofia-cli 0.1.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 (435) hide show
  1. package/.github/agents/copilot-instructions.md +39 -0
  2. package/.github/agents/speckit.analyze.agent.md +184 -0
  3. package/.github/agents/speckit.checklist.agent.md +294 -0
  4. package/.github/agents/speckit.clarify.agent.md +181 -0
  5. package/.github/agents/speckit.constitution.agent.md +84 -0
  6. package/.github/agents/speckit.implement.agent.md +135 -0
  7. package/.github/agents/speckit.plan.agent.md +90 -0
  8. package/.github/agents/speckit.specify.agent.md +258 -0
  9. package/.github/agents/speckit.tasks.agent.md +137 -0
  10. package/.github/agents/speckit.taskstoissues.agent.md +30 -0
  11. package/.github/copilot-instructions.md +257 -0
  12. package/.github/prompts/speckit.analyze.prompt.md +3 -0
  13. package/.github/prompts/speckit.checklist.prompt.md +3 -0
  14. package/.github/prompts/speckit.clarify.prompt.md +3 -0
  15. package/.github/prompts/speckit.constitution.prompt.md +3 -0
  16. package/.github/prompts/speckit.implement.prompt.md +3 -0
  17. package/.github/prompts/speckit.plan.prompt.md +3 -0
  18. package/.github/prompts/speckit.specify.prompt.md +3 -0
  19. package/.github/prompts/speckit.tasks.prompt.md +3 -0
  20. package/.github/prompts/speckit.taskstoissues.prompt.md +3 -0
  21. package/.github/workflows/ci.yml +38 -0
  22. package/.prettierrc +6 -0
  23. package/.specify/memory/constitution.md +181 -0
  24. package/.specify/scripts/bash/check-prerequisites.sh +166 -0
  25. package/.specify/scripts/bash/common.sh +156 -0
  26. package/.specify/scripts/bash/create-new-feature.sh +297 -0
  27. package/.specify/scripts/bash/setup-plan.sh +61 -0
  28. package/.specify/scripts/bash/update-agent-context.sh +810 -0
  29. package/.specify/templates/agent-file-template.md +28 -0
  30. package/.specify/templates/checklist-template.md +40 -0
  31. package/.specify/templates/constitution-template.md +50 -0
  32. package/.specify/templates/plan-template.md +113 -0
  33. package/.specify/templates/spec-template.md +115 -0
  34. package/.specify/templates/tasks-template.md +251 -0
  35. package/.vscode/mcp.json +42 -0
  36. package/.vscode/settings.json +19 -0
  37. package/CODE_OF_CONDUCT.md +128 -0
  38. package/LICENSE +21 -0
  39. package/README.md +213 -0
  40. package/dist/src/cli/developCommand.js +240 -0
  41. package/dist/src/cli/directCommands.js +143 -0
  42. package/dist/src/cli/envLoader.js +16 -0
  43. package/dist/src/cli/exportCommand.js +53 -0
  44. package/dist/src/cli/index.js +203 -0
  45. package/dist/src/cli/ioContext.js +109 -0
  46. package/dist/src/cli/preflight.js +57 -0
  47. package/dist/src/cli/statusCommand.js +110 -0
  48. package/dist/src/cli/workshopCommand.js +400 -0
  49. package/dist/src/develop/checkpointState.js +86 -0
  50. package/dist/src/develop/codeGenerator.js +319 -0
  51. package/dist/src/develop/dynamicScaffolder.js +226 -0
  52. package/dist/src/develop/githubMcpAdapter.js +122 -0
  53. package/dist/src/develop/index.js +15 -0
  54. package/dist/src/develop/mcpContextEnricher.js +195 -0
  55. package/dist/src/develop/pocScaffolder.js +542 -0
  56. package/dist/src/develop/ralphLoop.js +659 -0
  57. package/dist/src/develop/templateRegistry.js +364 -0
  58. package/dist/src/develop/testRunner.js +202 -0
  59. package/dist/src/logging/logger.js +58 -0
  60. package/dist/src/loop/conversationLoop.js +227 -0
  61. package/dist/src/loop/phaseSummarizer.js +87 -0
  62. package/dist/src/mcp/mcpManager.js +267 -0
  63. package/dist/src/mcp/mcpTransport.js +391 -0
  64. package/dist/src/mcp/retryPolicy.js +47 -0
  65. package/dist/src/mcp/webSearch.js +254 -0
  66. package/dist/src/phases/contextSummarizer.js +101 -0
  67. package/dist/src/phases/discoveryEnricher.js +156 -0
  68. package/dist/src/phases/phaseExtractors.js +222 -0
  69. package/dist/src/phases/phaseHandlers.js +328 -0
  70. package/dist/src/prompts/design.md +51 -0
  71. package/dist/src/prompts/develop-boundary.md +51 -0
  72. package/dist/src/prompts/develop.md +111 -0
  73. package/dist/src/prompts/discover.md +58 -0
  74. package/dist/src/prompts/ideate.md +56 -0
  75. package/dist/src/prompts/plan.md +51 -0
  76. package/dist/src/prompts/promptLoader.js +167 -0
  77. package/dist/src/prompts/promptLoader.ts +198 -0
  78. package/dist/src/prompts/select.md +47 -0
  79. package/dist/src/prompts/summarize/README.md +8 -0
  80. package/dist/src/prompts/summarize/design-summary.md +37 -0
  81. package/dist/src/prompts/summarize/develop-summary.md +25 -0
  82. package/dist/src/prompts/summarize/ideate-summary.md +27 -0
  83. package/dist/src/prompts/summarize/plan-summary.md +27 -0
  84. package/dist/src/prompts/summarize/select-summary.md +21 -0
  85. package/dist/src/prompts/system.md +28 -0
  86. package/dist/src/sessions/exportPaths.js +22 -0
  87. package/dist/src/sessions/exportWriter.js +406 -0
  88. package/dist/src/sessions/sessionManager.js +81 -0
  89. package/dist/src/sessions/sessionStore.js +65 -0
  90. package/dist/src/shared/activitySpinner.js +91 -0
  91. package/dist/src/shared/copilotClient.js +129 -0
  92. package/dist/src/shared/data/cards.json +1249 -0
  93. package/dist/src/shared/data/cardsLoader.js +51 -0
  94. package/dist/src/shared/errorClassifier.js +120 -0
  95. package/dist/src/shared/events.js +28 -0
  96. package/dist/src/shared/markdownRenderer.js +34 -0
  97. package/dist/src/shared/schemas/session.js +265 -0
  98. package/dist/src/shared/tableRenderer.js +20 -0
  99. package/dist/src/vendor/chalk.js +2 -0
  100. package/dist/src/vendor/cli-table3.js +3 -0
  101. package/dist/src/vendor/commander.js +2 -0
  102. package/dist/src/vendor/marked-terminal.js +3 -0
  103. package/dist/src/vendor/marked.js +2 -0
  104. package/dist/src/vendor/ora.js +2 -0
  105. package/dist/src/vendor/pino.js +2 -0
  106. package/dist/src/vendor/zod.js +2 -0
  107. package/dist/tests/e2e/developE2e.spec.js +126 -0
  108. package/dist/tests/e2e/developFailureE2e.spec.js +247 -0
  109. package/dist/tests/e2e/developPty.spec.js +75 -0
  110. package/dist/tests/e2e/discoveryWebSearchRelevance.spec.js +84 -0
  111. package/dist/tests/e2e/harness.spec.js +83 -0
  112. package/dist/tests/e2e/mcpLive.spec.js +120 -0
  113. package/dist/tests/e2e/newSession.e2e.spec.js +177 -0
  114. package/dist/tests/e2e/ralphLoopEnrichmentComparison.spec.js +62 -0
  115. package/dist/tests/e2e/workiqEnrichment.spec.js +56 -0
  116. package/dist/tests/e2e/zavaSimulation.spec.js +452 -0
  117. package/dist/tests/fixtures/test-fixture-project/src/add.js +3 -0
  118. package/dist/tests/fixtures/test-fixture-project/tests/failing.test.js +6 -0
  119. package/dist/tests/fixtures/test-fixture-project/tests/hanging.test.js +8 -0
  120. package/dist/tests/fixtures/test-fixture-project/tests/passing.test.js +10 -0
  121. package/dist/tests/fixtures/test-fixture-project/vitest.config.js +6 -0
  122. package/dist/tests/integration/autoStartConversation.spec.js +138 -0
  123. package/dist/tests/integration/defaultCommand.spec.js +147 -0
  124. package/dist/tests/integration/directCommandNonTty.spec.js +224 -0
  125. package/dist/tests/integration/directCommandTty.spec.js +151 -0
  126. package/dist/tests/integration/discoveryEnrichmentFlow.spec.js +175 -0
  127. package/dist/tests/integration/exportArtifacts.spec.js +202 -0
  128. package/dist/tests/integration/exportFallbackFlow.spec.js +99 -0
  129. package/dist/tests/integration/mcpDegradationFlow.spec.js +190 -0
  130. package/dist/tests/integration/mcpTransportFlow.spec.js +139 -0
  131. package/dist/tests/integration/newSessionFlow.spec.js +343 -0
  132. package/dist/tests/integration/pocGithubMcp.spec.js +186 -0
  133. package/dist/tests/integration/pocLocalFallback.spec.js +171 -0
  134. package/dist/tests/integration/pocScaffold.spec.js +163 -0
  135. package/dist/tests/integration/ralphLoopFlow.spec.js +359 -0
  136. package/dist/tests/integration/ralphLoopPartial.spec.js +368 -0
  137. package/dist/tests/integration/resumeAndBacktrack.spec.js +247 -0
  138. package/dist/tests/integration/spinnerLifecycle.spec.js +220 -0
  139. package/dist/tests/integration/summarizationFlow.spec.js +115 -0
  140. package/dist/tests/integration/testRunnerReal.spec.js +52 -0
  141. package/dist/tests/integration/webSearchAgent.spec.js +128 -0
  142. package/dist/tests/live/copilotSdkLive.spec.js +107 -0
  143. package/dist/tests/live/zavaFullWorkshop.spec.js +392 -0
  144. package/dist/tests/setup/loadEnv.js +3 -0
  145. package/dist/tests/unit/cli/developCommand.spec.js +567 -0
  146. package/dist/tests/unit/cli/directCommands.spec.js +279 -0
  147. package/dist/tests/unit/cli/envLoader.spec.js +58 -0
  148. package/dist/tests/unit/cli/ioContext.spec.js +119 -0
  149. package/dist/tests/unit/cli/preflight.spec.js +108 -0
  150. package/dist/tests/unit/cli/statusCommand.spec.js +111 -0
  151. package/dist/tests/unit/cli/workshopClientFallback.spec.js +80 -0
  152. package/dist/tests/unit/cli/workshopCommand.spec.js +329 -0
  153. package/dist/tests/unit/config/vitestEnvSetup.spec.js +13 -0
  154. package/dist/tests/unit/develop/checkpointState.spec.js +315 -0
  155. package/dist/tests/unit/develop/codeGenerator.spec.js +355 -0
  156. package/dist/tests/unit/develop/githubMcpAdapter.spec.js +231 -0
  157. package/dist/tests/unit/develop/mcpContextEnricher.spec.js +433 -0
  158. package/dist/tests/unit/develop/outputValidator.spec.js +119 -0
  159. package/dist/tests/unit/develop/pocScaffolder.spec.js +353 -0
  160. package/dist/tests/unit/develop/ralphLoop.spec.js +1248 -0
  161. package/dist/tests/unit/develop/templateRegistry.spec.js +85 -0
  162. package/dist/tests/unit/develop/testRunner.spec.js +249 -0
  163. package/dist/tests/unit/infraBicep.spec.js +92 -0
  164. package/dist/tests/unit/infraDeploy.spec.js +82 -0
  165. package/dist/tests/unit/infraTeardown.spec.js +63 -0
  166. package/dist/tests/unit/logging/logger.spec.js +43 -0
  167. package/dist/tests/unit/loop/conversationLoop.spec.js +592 -0
  168. package/dist/tests/unit/loop/phaseSummarizer.spec.js +141 -0
  169. package/dist/tests/unit/loop/streamingMarkdown.spec.js +147 -0
  170. package/dist/tests/unit/mcp/mcpManager.spec.js +279 -0
  171. package/dist/tests/unit/mcp/mcpTransport.spec.js +529 -0
  172. package/dist/tests/unit/mcp/retryPolicy.spec.js +218 -0
  173. package/dist/tests/unit/mcp/timeoutValidation.spec.js +46 -0
  174. package/dist/tests/unit/mcp/webSearch.spec.js +567 -0
  175. package/dist/tests/unit/phases/contextSummarizer.spec.js +140 -0
  176. package/dist/tests/unit/phases/discoveryEnricher.repeatCalls.spec.js +93 -0
  177. package/dist/tests/unit/phases/discoveryEnricher.spec.js +411 -0
  178. package/dist/tests/unit/phases/phaseExtractors.spec.js +352 -0
  179. package/dist/tests/unit/phases/phaseHandlers.spec.js +425 -0
  180. package/dist/tests/unit/prompts/promptLoader.spec.js +118 -0
  181. package/dist/tests/unit/schemas/pocSchemas.spec.js +412 -0
  182. package/dist/tests/unit/schemas/session.spec.js +257 -0
  183. package/dist/tests/unit/sessions/exportPaths.spec.js +31 -0
  184. package/dist/tests/unit/sessions/exportWriter.spec.js +655 -0
  185. package/dist/tests/unit/sessions/sessionManager.spec.js +151 -0
  186. package/dist/tests/unit/sessions/sessionStore.spec.js +116 -0
  187. package/dist/tests/unit/shared/activitySpinner.spec.js +175 -0
  188. package/dist/tests/unit/shared/cardsLoader.spec.js +76 -0
  189. package/dist/tests/unit/shared/copilotClient.spec.js +155 -0
  190. package/dist/tests/unit/shared/errorClassifier.spec.js +131 -0
  191. package/dist/tests/unit/shared/events.spec.js +55 -0
  192. package/dist/tests/unit/shared/markdownRenderer.spec.js +35 -0
  193. package/dist/tests/unit/shared/markdownRendererChunks.spec.js +70 -0
  194. package/dist/tests/unit/shared/tableRenderer.spec.js +34 -0
  195. package/dist/vitest.config.js +14 -0
  196. package/dist/vitest.live.config.js +18 -0
  197. package/docs/README.md +35 -0
  198. package/docs/architecture.md +169 -0
  199. package/docs/cli-usage.md +207 -0
  200. package/docs/environment.md +66 -0
  201. package/docs/export-format.md +146 -0
  202. package/docs/session-model.md +113 -0
  203. package/eslint.config.js +35 -0
  204. package/infra/deploy.sh +193 -0
  205. package/infra/gather-env.sh +211 -0
  206. package/infra/main.bicep +90 -0
  207. package/infra/main.bicepparam +18 -0
  208. package/infra/resources.bicep +134 -0
  209. package/infra/teardown.sh +114 -0
  210. package/package.json +63 -0
  211. package/specs/001-cli-workshop-rebuild/checklists/requirements.md +35 -0
  212. package/specs/001-cli-workshop-rebuild/contracts/cli.md +59 -0
  213. package/specs/001-cli-workshop-rebuild/contracts/export-summary-json.md +23 -0
  214. package/specs/001-cli-workshop-rebuild/contracts/session-json.md +30 -0
  215. package/specs/001-cli-workshop-rebuild/data-model.md +210 -0
  216. package/specs/001-cli-workshop-rebuild/plan.md +361 -0
  217. package/specs/001-cli-workshop-rebuild/quickstart.md +83 -0
  218. package/specs/001-cli-workshop-rebuild/research.md +116 -0
  219. package/specs/001-cli-workshop-rebuild/spec.md +240 -0
  220. package/specs/001-cli-workshop-rebuild/tasks.md +476 -0
  221. package/specs/002-poc-generation/contracts/poc-output.md +172 -0
  222. package/specs/002-poc-generation/contracts/ralph-loop.md +113 -0
  223. package/specs/002-poc-generation/data-model.md +172 -0
  224. package/specs/002-poc-generation/plan.md +109 -0
  225. package/specs/002-poc-generation/quickstart.md +97 -0
  226. package/specs/002-poc-generation/research.md +786 -0
  227. package/specs/002-poc-generation/spec.md +81 -0
  228. package/specs/002-poc-generation/tasks-fix.md +198 -0
  229. package/specs/002-poc-generation/tasks.md +252 -0
  230. package/specs/003-mcp-transport-integration/checklists/requirements.md +37 -0
  231. package/specs/003-mcp-transport-integration/contracts/context-enricher.md +220 -0
  232. package/specs/003-mcp-transport-integration/contracts/discovery-enricher.md +267 -0
  233. package/specs/003-mcp-transport-integration/contracts/github-adapter.md +149 -0
  234. package/specs/003-mcp-transport-integration/contracts/mcp-transport.md +288 -0
  235. package/specs/003-mcp-transport-integration/data-model.md +326 -0
  236. package/specs/003-mcp-transport-integration/plan.md +114 -0
  237. package/specs/003-mcp-transport-integration/quickstart.md +311 -0
  238. package/specs/003-mcp-transport-integration/research.md +395 -0
  239. package/specs/003-mcp-transport-integration/spec.md +234 -0
  240. package/specs/003-mcp-transport-integration/tasks.md +324 -0
  241. package/specs/003-next-spec-gaps.md +150 -0
  242. package/specs/004-dev-resume-hardening/checklists/requirements.md +37 -0
  243. package/specs/004-dev-resume-hardening/contracts/cli.md +160 -0
  244. package/specs/004-dev-resume-hardening/data-model.md +321 -0
  245. package/specs/004-dev-resume-hardening/plan.md +107 -0
  246. package/specs/004-dev-resume-hardening/quickstart.md +115 -0
  247. package/specs/004-dev-resume-hardening/research.md +142 -0
  248. package/specs/004-dev-resume-hardening/spec.md +221 -0
  249. package/specs/004-dev-resume-hardening/tasks.md +333 -0
  250. package/specs/005-ai-search-deploy/checklists/requirements.md +39 -0
  251. package/specs/005-ai-search-deploy/contracts/web-search-tool.md +241 -0
  252. package/specs/005-ai-search-deploy/data-model.md +130 -0
  253. package/specs/005-ai-search-deploy/plan.md +93 -0
  254. package/specs/005-ai-search-deploy/quickstart.md +96 -0
  255. package/specs/005-ai-search-deploy/research.md +187 -0
  256. package/specs/005-ai-search-deploy/spec.md +143 -0
  257. package/specs/005-ai-search-deploy/tasks.md +284 -0
  258. package/specs/006-workshop-extraction-fixes/checklists/requirements.md +61 -0
  259. package/specs/006-workshop-extraction-fixes/contracts/summarization-and-export.md +131 -0
  260. package/specs/006-workshop-extraction-fixes/data-model.md +149 -0
  261. package/specs/006-workshop-extraction-fixes/plan.md +123 -0
  262. package/specs/006-workshop-extraction-fixes/quickstart.md +101 -0
  263. package/specs/006-workshop-extraction-fixes/research.md +143 -0
  264. package/specs/006-workshop-extraction-fixes/spec.md +210 -0
  265. package/specs/006-workshop-extraction-fixes/tasks.md +316 -0
  266. package/src/cli/developCommand.ts +308 -0
  267. package/src/cli/directCommands.ts +195 -0
  268. package/src/cli/envLoader.ts +17 -0
  269. package/src/cli/exportCommand.ts +65 -0
  270. package/src/cli/index.ts +249 -0
  271. package/src/cli/ioContext.ts +139 -0
  272. package/src/cli/preflight.ts +86 -0
  273. package/src/cli/statusCommand.ts +118 -0
  274. package/src/cli/workshopCommand.ts +496 -0
  275. package/src/develop/checkpointState.ts +121 -0
  276. package/src/develop/codeGenerator.ts +402 -0
  277. package/src/develop/dynamicScaffolder.ts +284 -0
  278. package/src/develop/githubMcpAdapter.ts +199 -0
  279. package/src/develop/index.ts +34 -0
  280. package/src/develop/mcpContextEnricher.ts +279 -0
  281. package/src/develop/pocScaffolder.ts +646 -0
  282. package/src/develop/ralphLoop.ts +1044 -0
  283. package/src/develop/templateRegistry.ts +427 -0
  284. package/src/develop/testRunner.ts +276 -0
  285. package/src/logging/logger.ts +73 -0
  286. package/src/loop/conversationLoop.ts +355 -0
  287. package/src/loop/phaseSummarizer.ts +114 -0
  288. package/src/mcp/mcpManager.ts +365 -0
  289. package/src/mcp/mcpTransport.ts +562 -0
  290. package/src/mcp/retryPolicy.ts +87 -0
  291. package/src/mcp/webSearch.ts +388 -0
  292. package/src/originalPrompts/design_thinking.md +178 -0
  293. package/src/originalPrompts/design_thinking_persona.md +76 -0
  294. package/src/originalPrompts/document_generator_example.md +77 -0
  295. package/src/originalPrompts/document_generator_persona.md +47 -0
  296. package/src/originalPrompts/facilitator_persona.md +125 -0
  297. package/src/originalPrompts/guardrails.md +47 -0
  298. package/src/phases/contextSummarizer.ts +154 -0
  299. package/src/phases/discoveryEnricher.ts +223 -0
  300. package/src/phases/phaseExtractors.ts +247 -0
  301. package/src/phases/phaseHandlers.ts +450 -0
  302. package/src/prompts/design.md +51 -0
  303. package/src/prompts/develop-boundary.md +51 -0
  304. package/src/prompts/develop.md +111 -0
  305. package/src/prompts/discover.md +58 -0
  306. package/src/prompts/ideate.md +56 -0
  307. package/src/prompts/plan.md +51 -0
  308. package/src/prompts/promptLoader.ts +198 -0
  309. package/src/prompts/select.md +47 -0
  310. package/src/prompts/summarize/README.md +8 -0
  311. package/src/prompts/summarize/design-summary.md +37 -0
  312. package/src/prompts/summarize/develop-summary.md +25 -0
  313. package/src/prompts/summarize/ideate-summary.md +27 -0
  314. package/src/prompts/summarize/plan-summary.md +27 -0
  315. package/src/prompts/summarize/select-summary.md +21 -0
  316. package/src/prompts/system.md +28 -0
  317. package/src/sessions/exportPaths.ts +28 -0
  318. package/src/sessions/exportWriter.ts +490 -0
  319. package/src/sessions/sessionManager.ts +119 -0
  320. package/src/sessions/sessionStore.ts +69 -0
  321. package/src/shared/activitySpinner.ts +108 -0
  322. package/src/shared/copilotClient.ts +291 -0
  323. package/src/shared/data/cards.json +1249 -0
  324. package/src/shared/data/cardsLoader.ts +70 -0
  325. package/src/shared/errorClassifier.ts +160 -0
  326. package/src/shared/events.ts +103 -0
  327. package/src/shared/markdownRenderer.ts +44 -0
  328. package/src/shared/schemas/session.ts +346 -0
  329. package/src/shared/tableRenderer.ts +28 -0
  330. package/src/types/marked-terminal.d.ts +5 -0
  331. package/src/vendor/chalk.ts +2 -0
  332. package/src/vendor/cli-table3.ts +3 -0
  333. package/src/vendor/commander.ts +2 -0
  334. package/src/vendor/marked-terminal.ts +3 -0
  335. package/src/vendor/marked.ts +2 -0
  336. package/src/vendor/ora.ts +2 -0
  337. package/src/vendor/pino.ts +3 -0
  338. package/src/vendor/zod.ts +3 -0
  339. package/tests/e2e/developE2e.spec.ts +152 -0
  340. package/tests/e2e/developFailureE2e.spec.ts +289 -0
  341. package/tests/e2e/developPty.spec.ts +86 -0
  342. package/tests/e2e/discoveryWebSearchRelevance.spec.ts +103 -0
  343. package/tests/e2e/harness.spec.ts +104 -0
  344. package/tests/e2e/mcpLive.spec.ts +149 -0
  345. package/tests/e2e/newSession.e2e.spec.ts +245 -0
  346. package/tests/e2e/ralphLoopEnrichmentComparison.spec.ts +70 -0
  347. package/tests/e2e/workiqEnrichment.spec.ts +72 -0
  348. package/tests/e2e/zava-assessment/agent-interaction-script.md +258 -0
  349. package/tests/e2e/zava-assessment/company-profile.md +98 -0
  350. package/tests/e2e/zava-assessment/expected-results-checklist.md +454 -0
  351. package/tests/e2e/zavaSimulation.spec.ts +511 -0
  352. package/tests/fixtures/completedSession.json +141 -0
  353. package/tests/fixtures/test-fixture-project/package-lock.json +1585 -0
  354. package/tests/fixtures/test-fixture-project/package.json +12 -0
  355. package/tests/fixtures/test-fixture-project/src/add.ts +3 -0
  356. package/tests/fixtures/test-fixture-project/tests/failing.test.ts +7 -0
  357. package/tests/fixtures/test-fixture-project/tests/hanging.test.ts +9 -0
  358. package/tests/fixtures/test-fixture-project/tests/passing.test.ts +13 -0
  359. package/tests/fixtures/test-fixture-project/vitest.config.ts +7 -0
  360. package/tests/integration/autoStartConversation.spec.ts +168 -0
  361. package/tests/integration/defaultCommand.spec.ts +179 -0
  362. package/tests/integration/directCommandNonTty.spec.ts +260 -0
  363. package/tests/integration/directCommandTty.spec.ts +185 -0
  364. package/tests/integration/discoveryEnrichmentFlow.spec.ts +209 -0
  365. package/tests/integration/exportArtifacts.spec.ts +232 -0
  366. package/tests/integration/exportFallbackFlow.spec.ts +115 -0
  367. package/tests/integration/mcpDegradationFlow.spec.ts +231 -0
  368. package/tests/integration/mcpTransportFlow.spec.ts +178 -0
  369. package/tests/integration/newSessionFlow.spec.ts +406 -0
  370. package/tests/integration/pocGithubMcp.spec.ts +224 -0
  371. package/tests/integration/pocLocalFallback.spec.ts +205 -0
  372. package/tests/integration/pocScaffold.spec.ts +220 -0
  373. package/tests/integration/ralphLoopFlow.spec.ts +430 -0
  374. package/tests/integration/ralphLoopPartial.spec.ts +416 -0
  375. package/tests/integration/resumeAndBacktrack.spec.ts +278 -0
  376. package/tests/integration/spinnerLifecycle.spec.ts +270 -0
  377. package/tests/integration/summarizationFlow.spec.ts +135 -0
  378. package/tests/integration/testRunnerReal.spec.ts +63 -0
  379. package/tests/integration/webSearchAgent.spec.ts +155 -0
  380. package/tests/live/copilotSdkLive.spec.ts +149 -0
  381. package/tests/live/zavaFullWorkshop.spec.ts +515 -0
  382. package/tests/setup/loadEnv.ts +5 -0
  383. package/tests/unit/cli/developCommand.spec.ts +679 -0
  384. package/tests/unit/cli/directCommands.spec.ts +325 -0
  385. package/tests/unit/cli/envLoader.spec.ts +73 -0
  386. package/tests/unit/cli/ioContext.spec.ts +148 -0
  387. package/tests/unit/cli/preflight.spec.ts +125 -0
  388. package/tests/unit/cli/statusCommand.spec.ts +134 -0
  389. package/tests/unit/cli/workshopClientFallback.spec.ts +100 -0
  390. package/tests/unit/cli/workshopCommand.spec.ts +378 -0
  391. package/tests/unit/config/vitestEnvSetup.spec.ts +24 -0
  392. package/tests/unit/develop/checkpointState.spec.ts +378 -0
  393. package/tests/unit/develop/codeGenerator.spec.ts +447 -0
  394. package/tests/unit/develop/githubMcpAdapter.spec.ts +283 -0
  395. package/tests/unit/develop/mcpContextEnricher.spec.ts +564 -0
  396. package/tests/unit/develop/outputValidator.spec.ts +134 -0
  397. package/tests/unit/develop/pocScaffolder.spec.ts +451 -0
  398. package/tests/unit/develop/ralphLoop.spec.ts +1439 -0
  399. package/tests/unit/develop/templateRegistry.spec.ts +106 -0
  400. package/tests/unit/develop/testRunner.spec.ts +294 -0
  401. package/tests/unit/infraBicep.spec.ts +116 -0
  402. package/tests/unit/infraDeploy.spec.ts +102 -0
  403. package/tests/unit/infraTeardown.spec.ts +77 -0
  404. package/tests/unit/logging/logger.spec.ts +50 -0
  405. package/tests/unit/loop/conversationLoop.spec.ts +719 -0
  406. package/tests/unit/loop/phaseSummarizer.spec.ts +169 -0
  407. package/tests/unit/loop/streamingMarkdown.spec.ts +180 -0
  408. package/tests/unit/mcp/mcpManager.spec.ts +336 -0
  409. package/tests/unit/mcp/mcpTransport.spec.ts +689 -0
  410. package/tests/unit/mcp/retryPolicy.spec.ts +278 -0
  411. package/tests/unit/mcp/timeoutValidation.spec.ts +55 -0
  412. package/tests/unit/mcp/webSearch.spec.ts +718 -0
  413. package/tests/unit/phases/contextSummarizer.spec.ts +158 -0
  414. package/tests/unit/phases/discoveryEnricher.repeatCalls.spec.ts +125 -0
  415. package/tests/unit/phases/discoveryEnricher.spec.ts +512 -0
  416. package/tests/unit/phases/phaseExtractors.spec.ts +406 -0
  417. package/tests/unit/phases/phaseHandlers.spec.ts +483 -0
  418. package/tests/unit/prompts/promptLoader.spec.ts +144 -0
  419. package/tests/unit/schemas/pocSchemas.spec.ts +457 -0
  420. package/tests/unit/schemas/session.spec.ts +328 -0
  421. package/tests/unit/sessions/exportPaths.spec.ts +38 -0
  422. package/tests/unit/sessions/exportWriter.spec.ts +737 -0
  423. package/tests/unit/sessions/sessionManager.spec.ts +174 -0
  424. package/tests/unit/sessions/sessionStore.spec.ts +136 -0
  425. package/tests/unit/shared/activitySpinner.spec.ts +211 -0
  426. package/tests/unit/shared/cardsLoader.spec.ts +89 -0
  427. package/tests/unit/shared/copilotClient.spec.ts +185 -0
  428. package/tests/unit/shared/errorClassifier.spec.ts +152 -0
  429. package/tests/unit/shared/events.spec.ts +71 -0
  430. package/tests/unit/shared/markdownRenderer.spec.ts +42 -0
  431. package/tests/unit/shared/markdownRendererChunks.spec.ts +83 -0
  432. package/tests/unit/shared/tableRenderer.spec.ts +38 -0
  433. package/tsconfig.json +20 -0
  434. package/vitest.config.ts +15 -0
  435. package/vitest.live.config.ts +19 -0
@@ -0,0 +1,592 @@
1
+ /**
2
+ * ConversationLoop tests.
3
+ *
4
+ * Validates the multi-turn conversation orchestration, streaming render,
5
+ * event dispatching, phase handling, and shutdown behavior.
6
+ */
7
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
8
+ import { ConversationLoop, } from '../../../src/loop/conversationLoop.js';
9
+ import { createFakeCopilotClient } from '../../../src/shared/copilotClient.js';
10
+ // ── Helpers ─────────────────────────────────────────────────────────────────
11
+ function makeSession(overrides) {
12
+ return {
13
+ sessionId: 'test-session-1',
14
+ schemaVersion: '1.0.0',
15
+ createdAt: '2025-01-01T00:00:00Z',
16
+ updatedAt: '2025-01-01T00:00:00Z',
17
+ phase: 'Discover',
18
+ status: 'Active',
19
+ participants: [{ id: 'p1', displayName: 'Alice', role: 'Facilitator' }],
20
+ artifacts: { generatedFiles: [] },
21
+ ...overrides,
22
+ };
23
+ }
24
+ /** Create a LoopIO that feeds predetermined inputs then returns null. */
25
+ function makeIO(inputs, opts) {
26
+ let inputIndex = 0;
27
+ const written = [];
28
+ const activities = [];
29
+ return {
30
+ write(text) {
31
+ written.push(text);
32
+ },
33
+ writeActivity(text) {
34
+ activities.push(text);
35
+ },
36
+ writeToolSummary(_toolName, _summary) {
37
+ // no-op for tests
38
+ },
39
+ async readInput(_prompt) {
40
+ if (inputIndex >= inputs.length)
41
+ return null;
42
+ return inputs[inputIndex++] ?? null;
43
+ },
44
+ async showDecisionGate(_phase) {
45
+ return { choice: 'continue' };
46
+ },
47
+ isJsonMode: opts?.json ?? false,
48
+ isTTY: opts?.tty ?? true,
49
+ // Expose captured output for assertions
50
+ get _written() {
51
+ return written;
52
+ },
53
+ get _activities() {
54
+ return activities;
55
+ },
56
+ };
57
+ }
58
+ function makePhaseHandler(overrides) {
59
+ return {
60
+ phase: 'Discover',
61
+ buildSystemPrompt: () => 'You are a workshop facilitator.',
62
+ extractResult: (_session) => ({}),
63
+ ...overrides,
64
+ };
65
+ }
66
+ // ── Tests ────────────────────────────────────────────────────────────────────
67
+ describe('ConversationLoop', () => {
68
+ beforeEach(() => {
69
+ // Remove any leftover SIGINT listeners from previous tests
70
+ process.removeAllListeners('SIGINT');
71
+ });
72
+ describe('basic conversation flow', () => {
73
+ it('sends user input to LLM and accumulates turns', async () => {
74
+ const client = createFakeCopilotClient([
75
+ { role: 'assistant', content: 'Tell me about your business.' },
76
+ { role: 'assistant', content: 'Great, let us proceed.' },
77
+ ]);
78
+ const io = makeIO(['We sell widgets', 'We have 50 employees']);
79
+ const handler = makePhaseHandler();
80
+ const session = makeSession();
81
+ const loop = new ConversationLoop({
82
+ client,
83
+ io,
84
+ session,
85
+ phaseHandler: handler,
86
+ });
87
+ const result = await loop.run();
88
+ // Should have 4 turns: 2 user + 2 assistant
89
+ expect(result.turns).toBeDefined();
90
+ expect(result.turns.length).toBe(4);
91
+ expect(result.turns[0].role).toBe('user');
92
+ expect(result.turns[0].content).toBe('We sell widgets');
93
+ expect(result.turns[1].role).toBe('assistant');
94
+ expect(result.turns[1].content).toBe('Tell me about your business.');
95
+ expect(result.turns[2].role).toBe('user');
96
+ expect(result.turns[2].content).toBe('We have 50 employees');
97
+ expect(result.turns[3].role).toBe('assistant');
98
+ expect(result.turns[3].content).toBe('Great, let us proceed.');
99
+ });
100
+ it('terminates on null input (EOF/Ctrl+D)', async () => {
101
+ const client = createFakeCopilotClient([]);
102
+ const io = makeIO([null]);
103
+ const handler = makePhaseHandler();
104
+ const loop = new ConversationLoop({
105
+ client,
106
+ io,
107
+ session: makeSession(),
108
+ phaseHandler: handler,
109
+ });
110
+ const result = await loop.run();
111
+ expect(result.turns ?? []).toHaveLength(0);
112
+ });
113
+ it('updates session after each turn via onSessionUpdate callback', async () => {
114
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'Response one' }]);
115
+ const io = makeIO(['hello']);
116
+ const updates = [];
117
+ const onSessionUpdate = vi.fn(async (s) => {
118
+ updates.push({ ...s });
119
+ });
120
+ const loop = new ConversationLoop({
121
+ client,
122
+ io,
123
+ session: makeSession(),
124
+ phaseHandler: makePhaseHandler(),
125
+ onSessionUpdate,
126
+ });
127
+ await loop.run();
128
+ expect(onSessionUpdate).toHaveBeenCalledTimes(1);
129
+ expect(updates[0].turns).toHaveLength(2);
130
+ });
131
+ });
132
+ describe('event dispatching', () => {
133
+ it('emits events for TextDelta and Activity', async () => {
134
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'Hello!' }]);
135
+ const io = makeIO(['hi']);
136
+ const events = [];
137
+ const loop = new ConversationLoop({
138
+ client,
139
+ io,
140
+ session: makeSession(),
141
+ phaseHandler: makePhaseHandler(),
142
+ onEvent: (e) => events.push(e),
143
+ });
144
+ await loop.run();
145
+ // Should have at least: Activity (starting phase) + TextDelta
146
+ const activityEvents = events.filter((e) => e.type === 'Activity');
147
+ const textEvents = events.filter((e) => e.type === 'TextDelta');
148
+ expect(activityEvents.length).toBeGreaterThanOrEqual(1);
149
+ expect(textEvents.length).toBe(1);
150
+ expect(textEvents[0].type === 'TextDelta' && textEvents[0].text).toBe('Hello!');
151
+ });
152
+ });
153
+ describe('streaming output', () => {
154
+ it('writes streamed text to io.write in TTY mode', async () => {
155
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'Streaming content' }]);
156
+ const io = makeIO(['go'], { tty: true, json: false });
157
+ const loop = new ConversationLoop({
158
+ client,
159
+ io,
160
+ session: makeSession(),
161
+ phaseHandler: makePhaseHandler(),
162
+ });
163
+ await loop.run();
164
+ const ioAny = io;
165
+ // In TTY mode, text goes through renderMarkdown which may add formatting
166
+ const allOutput = ioAny._written.join('');
167
+ expect(allOutput).toContain('Streaming content');
168
+ });
169
+ it('outputs JSON envelope in JSON mode', async () => {
170
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'Result text' }]);
171
+ const io = makeIO(['go'], { json: true });
172
+ const loop = new ConversationLoop({
173
+ client,
174
+ io,
175
+ session: makeSession(),
176
+ phaseHandler: makePhaseHandler(),
177
+ });
178
+ await loop.run();
179
+ const ioAny = io;
180
+ const jsonOutputs = ioAny._written.filter((w) => w.startsWith('{'));
181
+ expect(jsonOutputs.length).toBeGreaterThanOrEqual(1);
182
+ const parsed = JSON.parse(jsonOutputs[0]);
183
+ expect(parsed.phase).toBe('Discover');
184
+ expect(parsed.content).toBe('Result text');
185
+ });
186
+ });
187
+ describe('phase handler integration', () => {
188
+ it('applies extractResult updates to session', async () => {
189
+ const client = createFakeCopilotClient([
190
+ { role: 'assistant', content: 'We identified your challenges' },
191
+ ]);
192
+ const io = makeIO(['Our business sells widgets']);
193
+ const handler = makePhaseHandler({
194
+ extractResult: (_session, _response) => ({
195
+ businessContext: {
196
+ businessDescription: 'Widget seller',
197
+ challenges: ['Growth'],
198
+ },
199
+ }),
200
+ });
201
+ const loop = new ConversationLoop({
202
+ client,
203
+ io,
204
+ session: makeSession(),
205
+ phaseHandler: handler,
206
+ });
207
+ const result = await loop.run();
208
+ expect(result.businessContext).toBeDefined();
209
+ expect(result.businessContext.businessDescription).toBe('Widget seller');
210
+ expect(result.businessContext.challenges).toEqual(['Growth']);
211
+ });
212
+ it('uses system prompt from handler when creating session', async () => {
213
+ const createSessionSpy = vi.fn();
214
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'ok' }]);
215
+ // Spy on createSession
216
+ const originalCreateSession = client.createSession.bind(client);
217
+ client.createSession = async (opts) => {
218
+ createSessionSpy(opts);
219
+ return originalCreateSession(opts);
220
+ };
221
+ const io = makeIO(['test']);
222
+ const handler = makePhaseHandler({
223
+ buildSystemPrompt: () => 'Custom system prompt for discover',
224
+ });
225
+ const loop = new ConversationLoop({
226
+ client,
227
+ io,
228
+ session: makeSession(),
229
+ phaseHandler: handler,
230
+ });
231
+ await loop.run();
232
+ expect(createSessionSpy).toHaveBeenCalledWith(expect.objectContaining({
233
+ systemPrompt: expect.stringContaining('Custom system prompt for discover'),
234
+ }));
235
+ });
236
+ it('includes references from handler in session options', async () => {
237
+ const createSessionSpy = vi.fn();
238
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'ok' }]);
239
+ const originalCreateSession = client.createSession.bind(client);
240
+ client.createSession = async (opts) => {
241
+ createSessionSpy(opts);
242
+ return originalCreateSession(opts);
243
+ };
244
+ const io = makeIO(['test']);
245
+ const handler = makePhaseHandler({
246
+ getReferences: () => ['doc1.md', 'doc2.md'],
247
+ });
248
+ const loop = new ConversationLoop({
249
+ client,
250
+ io,
251
+ session: makeSession(),
252
+ phaseHandler: handler,
253
+ });
254
+ await loop.run();
255
+ expect(createSessionSpy).toHaveBeenCalledWith(expect.objectContaining({
256
+ references: ['doc1.md', 'doc2.md'],
257
+ }));
258
+ });
259
+ });
260
+ describe('phase completion', () => {
261
+ it('does not break loop on empty input when isComplete returns false', async () => {
262
+ const client = createFakeCopilotClient([
263
+ { role: 'assistant', content: 'Need more info' },
264
+ { role: 'assistant', content: 'Thanks' },
265
+ ]);
266
+ let callCount = 0;
267
+ const io = makeIO(['', 'more data']);
268
+ const handler = makePhaseHandler({
269
+ isComplete: () => {
270
+ callCount++;
271
+ // First call: not complete; won't be called again because second input is non-empty
272
+ return callCount > 1;
273
+ },
274
+ });
275
+ const loop = new ConversationLoop({
276
+ client,
277
+ io,
278
+ session: makeSession(),
279
+ phaseHandler: handler,
280
+ });
281
+ const result = await loop.run();
282
+ // Both inputs should produce turns (empty string still gets sent, "more data" also)
283
+ expect(result.turns.length).toBeGreaterThanOrEqual(2);
284
+ });
285
+ });
286
+ describe('session state', () => {
287
+ it('getSession returns a copy of current session', async () => {
288
+ const client = createFakeCopilotClient([]);
289
+ const io = makeIO([null]);
290
+ const session = makeSession();
291
+ const loop = new ConversationLoop({
292
+ client,
293
+ io,
294
+ session,
295
+ phaseHandler: makePhaseHandler(),
296
+ });
297
+ const before = loop.getSession();
298
+ expect(before.sessionId).toBe('test-session-1');
299
+ // Mutate the returned copy
300
+ before.sessionId = 'mutated';
301
+ // Original should be unchanged
302
+ expect(loop.getSession().sessionId).toBe('test-session-1');
303
+ });
304
+ it('updates updatedAt timestamp after each turn', async () => {
305
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'ok' }]);
306
+ const io = makeIO(['hello']);
307
+ const loop = new ConversationLoop({
308
+ client,
309
+ io,
310
+ session: makeSession({ updatedAt: '2025-01-01T00:00:00Z' }),
311
+ phaseHandler: makePhaseHandler(),
312
+ });
313
+ const result = await loop.run();
314
+ expect(result.updatedAt).not.toBe('2025-01-01T00:00:00Z');
315
+ });
316
+ });
317
+ describe('edge cases', () => {
318
+ it('handles handler with no getReferences gracefully', async () => {
319
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'ok' }]);
320
+ const io = makeIO(['test']);
321
+ const handler = makePhaseHandler();
322
+ delete handler.getReferences;
323
+ const loop = new ConversationLoop({
324
+ client,
325
+ io,
326
+ session: makeSession(),
327
+ phaseHandler: handler,
328
+ });
329
+ // Should not throw
330
+ const result = await loop.run();
331
+ expect(result.turns).toHaveLength(2);
332
+ });
333
+ it('handles exhausted fake responses gracefully', async () => {
334
+ // Only 1 response configured but 2 messages sent
335
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'First' }]);
336
+ const io = makeIO(['msg1', 'msg2']);
337
+ const loop = new ConversationLoop({
338
+ client,
339
+ io,
340
+ session: makeSession(),
341
+ phaseHandler: makePhaseHandler(),
342
+ });
343
+ const result = await loop.run();
344
+ expect(result.turns).toHaveLength(4); // 2 user + 2 assistant
345
+ expect(result.turns[3].content).toContain('No more responses');
346
+ });
347
+ });
348
+ // ── T073: Auto-start behavior ──────────────────────────────────────────
349
+ describe('auto-start with initialMessage (T073)', () => {
350
+ it('sends initialMessage to LLM before readInput()', async () => {
351
+ const client = createFakeCopilotClient([
352
+ { role: 'assistant', content: 'Welcome to the Discover phase!' },
353
+ { role: 'assistant', content: 'Great, thanks for that info.' },
354
+ ]);
355
+ const readInputCalls = [];
356
+ const io = makeIO(['user says hello']);
357
+ const origReadInput = io.readInput.bind(io);
358
+ io.readInput = async (prompt) => {
359
+ readInputCalls.push(prompt ?? '');
360
+ return origReadInput(prompt);
361
+ };
362
+ const loop = new ConversationLoop({
363
+ client,
364
+ io,
365
+ session: makeSession(),
366
+ phaseHandler: makePhaseHandler(),
367
+ initialMessage: 'Introduce the Discover phase and ask the first question.',
368
+ });
369
+ const result = await loop.run();
370
+ // Initial message turn + user turn = 4 turns total
371
+ expect(result.turns).toHaveLength(4);
372
+ // First turn pair: system initial message → LLM greeting
373
+ expect(result.turns[0].role).toBe('user');
374
+ expect(result.turns[0].content).toBe('Introduce the Discover phase and ask the first question.');
375
+ expect(result.turns[1].role).toBe('assistant');
376
+ expect(result.turns[1].content).toBe('Welcome to the Discover phase!');
377
+ });
378
+ it('streams the greeting response to output', async () => {
379
+ const client = createFakeCopilotClient([
380
+ { role: 'assistant', content: 'Hello! Welcome to sofIA.' },
381
+ ]);
382
+ const io = makeIO([], { tty: true, json: false });
383
+ const loop = new ConversationLoop({
384
+ client,
385
+ io,
386
+ session: makeSession(),
387
+ phaseHandler: makePhaseHandler(),
388
+ initialMessage: 'Start the phase.',
389
+ });
390
+ await loop.run();
391
+ const ioTyped = io;
392
+ const allOutput = ioTyped._written.join('');
393
+ expect(allOutput).toContain('Hello! Welcome to sofIA.');
394
+ });
395
+ it('records initial exchange in turn history', async () => {
396
+ const client = createFakeCopilotClient([
397
+ { role: 'assistant', content: 'Phase intro response' },
398
+ ]);
399
+ const io = makeIO([]);
400
+ const loop = new ConversationLoop({
401
+ client,
402
+ io,
403
+ session: makeSession(),
404
+ phaseHandler: makePhaseHandler(),
405
+ initialMessage: 'Auto-start message',
406
+ });
407
+ const result = await loop.run();
408
+ expect(result.turns).toHaveLength(2);
409
+ expect(result.turns[0].role).toBe('user');
410
+ expect(result.turns[0].content).toBe('Auto-start message');
411
+ expect(result.turns[1].role).toBe('assistant');
412
+ expect(result.turns[1].content).toBe('Phase intro response');
413
+ });
414
+ it('does NOT auto-start when initialMessage is not provided', async () => {
415
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'Response' }]);
416
+ const io = makeIO(['user input']);
417
+ const loop = new ConversationLoop({
418
+ client,
419
+ io,
420
+ session: makeSession(),
421
+ phaseHandler: makePhaseHandler(),
422
+ // No initialMessage
423
+ });
424
+ const result = await loop.run();
425
+ // Only user + assistant turns, no initial message turn
426
+ expect(result.turns).toHaveLength(2);
427
+ expect(result.turns[0].role).toBe('user');
428
+ expect(result.turns[0].content).toBe('user input');
429
+ });
430
+ });
431
+ // ── Session resume: conversation history in system prompt ────────────────
432
+ describe('session resume with prior turns', () => {
433
+ it('injects prior conversation history into the system prompt on resume', async () => {
434
+ const createSessionSpy = vi.fn();
435
+ const client = createFakeCopilotClient([
436
+ { role: 'assistant', content: 'Welcome back! You told me about widgets.' },
437
+ ]);
438
+ const originalCreateSession = client.createSession.bind(client);
439
+ client.createSession = async (opts) => {
440
+ createSessionSpy(opts);
441
+ return originalCreateSession(opts);
442
+ };
443
+ // Session with existing turns from a prior Discover conversation
444
+ const session = makeSession({
445
+ turns: [
446
+ {
447
+ phase: 'Discover',
448
+ sequence: 1,
449
+ role: 'user',
450
+ content: 'We sell widgets worldwide',
451
+ timestamp: '2025-01-01T00:00:00Z',
452
+ },
453
+ {
454
+ phase: 'Discover',
455
+ sequence: 2,
456
+ role: 'assistant',
457
+ content: 'Great, what are your main challenges?',
458
+ timestamp: '2025-01-01T00:01:00Z',
459
+ },
460
+ ],
461
+ });
462
+ const io = makeIO([]);
463
+ const handler = makePhaseHandler({
464
+ buildSystemPrompt: () => 'You are a workshop facilitator.',
465
+ });
466
+ const loop = new ConversationLoop({
467
+ client,
468
+ io,
469
+ session,
470
+ phaseHandler: handler,
471
+ initialMessage: 'We are resuming. Summarize progress.',
472
+ });
473
+ await loop.run();
474
+ // The system prompt should contain the prior conversation history
475
+ const passedOpts = createSessionSpy.mock.calls[0][0];
476
+ expect(passedOpts.systemPrompt).toContain('We sell widgets worldwide');
477
+ expect(passedOpts.systemPrompt).toContain('Great, what are your main challenges?');
478
+ expect(passedOpts.systemPrompt).toContain('Previous conversation');
479
+ });
480
+ it('does NOT inject history when no prior turns exist', async () => {
481
+ const createSessionSpy = vi.fn();
482
+ const client = createFakeCopilotClient([
483
+ { role: 'assistant', content: 'Welcome to the workshop!' },
484
+ ]);
485
+ const originalCreateSession = client.createSession.bind(client);
486
+ client.createSession = async (opts) => {
487
+ createSessionSpy(opts);
488
+ return originalCreateSession(opts);
489
+ };
490
+ const io = makeIO([]);
491
+ const handler = makePhaseHandler({
492
+ buildSystemPrompt: () => 'You are a workshop facilitator.',
493
+ });
494
+ const loop = new ConversationLoop({
495
+ client,
496
+ io,
497
+ session: makeSession(), // No turns
498
+ phaseHandler: handler,
499
+ initialMessage: 'Start the Discover phase.',
500
+ });
501
+ await loop.run();
502
+ const passedOpts = createSessionSpy.mock.calls[0][0];
503
+ // System prompt should contain what the handler returned plus phase boundary
504
+ expect(passedOpts.systemPrompt).toContain('You are a workshop facilitator.');
505
+ });
506
+ it('only includes turns for the current phase in the history', async () => {
507
+ const createSessionSpy = vi.fn();
508
+ const client = createFakeCopilotClient([
509
+ { role: 'assistant', content: 'Resuming ideation.' },
510
+ ]);
511
+ const originalCreateSession = client.createSession.bind(client);
512
+ client.createSession = async (opts) => {
513
+ createSessionSpy(opts);
514
+ return originalCreateSession(opts);
515
+ };
516
+ const session = makeSession({
517
+ phase: 'Ideate',
518
+ turns: [
519
+ {
520
+ phase: 'Discover',
521
+ sequence: 1,
522
+ role: 'user',
523
+ content: 'Discovery message (should NOT appear)',
524
+ timestamp: '2025-01-01T00:00:00Z',
525
+ },
526
+ {
527
+ phase: 'Ideate',
528
+ sequence: 2,
529
+ role: 'user',
530
+ content: 'Ideation message (should appear)',
531
+ timestamp: '2025-01-01T01:00:00Z',
532
+ },
533
+ {
534
+ phase: 'Ideate',
535
+ sequence: 3,
536
+ role: 'assistant',
537
+ content: 'Ideation response (should appear)',
538
+ timestamp: '2025-01-01T01:01:00Z',
539
+ },
540
+ ],
541
+ });
542
+ const io = makeIO([]);
543
+ const handler = makePhaseHandler({
544
+ phase: 'Ideate',
545
+ buildSystemPrompt: () => 'Ideation facilitator.',
546
+ });
547
+ const loop = new ConversationLoop({
548
+ client,
549
+ io,
550
+ session,
551
+ phaseHandler: handler,
552
+ initialMessage: 'Resume ideation.',
553
+ });
554
+ await loop.run();
555
+ const passedOpts = createSessionSpy.mock.calls[0][0];
556
+ expect(passedOpts.systemPrompt).toContain('Ideation message (should appear)');
557
+ expect(passedOpts.systemPrompt).toContain('Ideation response (should appear)');
558
+ expect(passedOpts.systemPrompt).not.toContain('Discovery message (should NOT appear)');
559
+ });
560
+ });
561
+ // ── T055: SessionOptions.onUsage callback ─────────────────────────────────
562
+ describe('SessionOptions.onUsage (T055)', () => {
563
+ it('accepts an onUsage callback on SessionOptions', () => {
564
+ const opts = {
565
+ systemPrompt: 'Test',
566
+ onUsage: vi.fn(),
567
+ };
568
+ expect(opts.onUsage).toBeDefined();
569
+ expect(typeof opts.onUsage).toBe('function');
570
+ });
571
+ it('onUsage callback is forwarded when passed through createSession', async () => {
572
+ const usageCb = vi.fn();
573
+ const createSessionSpy = vi.fn();
574
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'OK' }]);
575
+ const originalCreateSession = client.createSession.bind(client);
576
+ client.createSession = async (opts) => {
577
+ createSessionSpy(opts);
578
+ return originalCreateSession(opts);
579
+ };
580
+ await client.createSession({
581
+ systemPrompt: 'Test',
582
+ onUsage: usageCb,
583
+ });
584
+ const passedOpts = createSessionSpy.mock.calls[0][0];
585
+ expect(passedOpts.onUsage).toBe(usageCb);
586
+ });
587
+ it('omitting onUsage does not set it on SessionOptions', () => {
588
+ const opts = { systemPrompt: 'Test' };
589
+ expect(opts.onUsage).toBeUndefined();
590
+ });
591
+ });
592
+ });