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,719 @@
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
+
9
+ import {
10
+ ConversationLoop,
11
+ type LoopIO,
12
+ type PhaseHandler,
13
+ } from '../../../src/loop/conversationLoop.js';
14
+ import { createFakeCopilotClient } from '../../../src/shared/copilotClient.js';
15
+ import type { SessionOptions } from '../../../src/shared/copilotClient.js';
16
+ import type { WorkshopSession } from '../../../src/shared/schemas/session.js';
17
+ import type { SofiaEvent } from '../../../src/shared/events.js';
18
+
19
+ // ── Helpers ─────────────────────────────────────────────────────────────────
20
+
21
+ function makeSession(overrides?: Partial<WorkshopSession>): WorkshopSession {
22
+ return {
23
+ sessionId: 'test-session-1',
24
+ schemaVersion: '1.0.0',
25
+ createdAt: '2025-01-01T00:00:00Z',
26
+ updatedAt: '2025-01-01T00:00:00Z',
27
+ phase: 'Discover',
28
+ status: 'Active',
29
+ participants: [{ id: 'p1', displayName: 'Alice', role: 'Facilitator' }],
30
+ artifacts: { generatedFiles: [] },
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ /** Create a LoopIO that feeds predetermined inputs then returns null. */
36
+ function makeIO(inputs: (string | null)[], opts?: { json?: boolean; tty?: boolean }): LoopIO {
37
+ let inputIndex = 0;
38
+ const written: string[] = [];
39
+ const activities: string[] = [];
40
+
41
+ return {
42
+ write(text: string) {
43
+ written.push(text);
44
+ },
45
+ writeActivity(text: string) {
46
+ activities.push(text);
47
+ },
48
+ writeToolSummary(_toolName: string, _summary: string) {
49
+ // no-op for tests
50
+ },
51
+ async readInput(_prompt?: string): Promise<string | null> {
52
+ if (inputIndex >= inputs.length) return null;
53
+ return inputs[inputIndex++] ?? null;
54
+ },
55
+ async showDecisionGate(_phase) {
56
+ return { choice: 'continue' as const };
57
+ },
58
+ isJsonMode: opts?.json ?? false,
59
+ isTTY: opts?.tty ?? true,
60
+ // Expose captured output for assertions
61
+ get _written() {
62
+ return written;
63
+ },
64
+ get _activities() {
65
+ return activities;
66
+ },
67
+ } as LoopIO & { _written: string[]; _activities: string[] };
68
+ }
69
+
70
+ function makePhaseHandler(overrides?: Partial<PhaseHandler>): PhaseHandler {
71
+ return {
72
+ phase: 'Discover',
73
+ buildSystemPrompt: () => 'You are a workshop facilitator.',
74
+ extractResult: (_session) => ({}),
75
+ ...overrides,
76
+ };
77
+ }
78
+
79
+ // ── Tests ────────────────────────────────────────────────────────────────────
80
+
81
+ describe('ConversationLoop', () => {
82
+ beforeEach(() => {
83
+ // Remove any leftover SIGINT listeners from previous tests
84
+ process.removeAllListeners('SIGINT');
85
+ });
86
+
87
+ describe('basic conversation flow', () => {
88
+ it('sends user input to LLM and accumulates turns', async () => {
89
+ const client = createFakeCopilotClient([
90
+ { role: 'assistant', content: 'Tell me about your business.' },
91
+ { role: 'assistant', content: 'Great, let us proceed.' },
92
+ ]);
93
+
94
+ const io = makeIO(['We sell widgets', 'We have 50 employees']);
95
+ const handler = makePhaseHandler();
96
+ const session = makeSession();
97
+
98
+ const loop = new ConversationLoop({
99
+ client,
100
+ io,
101
+ session,
102
+ phaseHandler: handler,
103
+ });
104
+
105
+ const result = await loop.run();
106
+
107
+ // Should have 4 turns: 2 user + 2 assistant
108
+ expect(result.turns).toBeDefined();
109
+ expect(result.turns!.length).toBe(4);
110
+ expect(result.turns![0].role).toBe('user');
111
+ expect(result.turns![0].content).toBe('We sell widgets');
112
+ expect(result.turns![1].role).toBe('assistant');
113
+ expect(result.turns![1].content).toBe('Tell me about your business.');
114
+ expect(result.turns![2].role).toBe('user');
115
+ expect(result.turns![2].content).toBe('We have 50 employees');
116
+ expect(result.turns![3].role).toBe('assistant');
117
+ expect(result.turns![3].content).toBe('Great, let us proceed.');
118
+ });
119
+
120
+ it('terminates on null input (EOF/Ctrl+D)', async () => {
121
+ const client = createFakeCopilotClient([]);
122
+ const io = makeIO([null]);
123
+ const handler = makePhaseHandler();
124
+
125
+ const loop = new ConversationLoop({
126
+ client,
127
+ io,
128
+ session: makeSession(),
129
+ phaseHandler: handler,
130
+ });
131
+
132
+ const result = await loop.run();
133
+ expect(result.turns ?? []).toHaveLength(0);
134
+ });
135
+
136
+ it('updates session after each turn via onSessionUpdate callback', async () => {
137
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'Response one' }]);
138
+
139
+ const io = makeIO(['hello']);
140
+ const updates: WorkshopSession[] = [];
141
+ const onSessionUpdate = vi.fn(async (s: WorkshopSession) => {
142
+ updates.push({ ...s });
143
+ });
144
+
145
+ const loop = new ConversationLoop({
146
+ client,
147
+ io,
148
+ session: makeSession(),
149
+ phaseHandler: makePhaseHandler(),
150
+ onSessionUpdate,
151
+ });
152
+
153
+ await loop.run();
154
+
155
+ expect(onSessionUpdate).toHaveBeenCalledTimes(1);
156
+ expect(updates[0].turns).toHaveLength(2);
157
+ });
158
+ });
159
+
160
+ describe('event dispatching', () => {
161
+ it('emits events for TextDelta and Activity', async () => {
162
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'Hello!' }]);
163
+
164
+ const io = makeIO(['hi']);
165
+ const events: SofiaEvent[] = [];
166
+
167
+ const loop = new ConversationLoop({
168
+ client,
169
+ io,
170
+ session: makeSession(),
171
+ phaseHandler: makePhaseHandler(),
172
+ onEvent: (e) => events.push(e),
173
+ });
174
+
175
+ await loop.run();
176
+
177
+ // Should have at least: Activity (starting phase) + TextDelta
178
+ const activityEvents = events.filter((e) => e.type === 'Activity');
179
+ const textEvents = events.filter((e) => e.type === 'TextDelta');
180
+ expect(activityEvents.length).toBeGreaterThanOrEqual(1);
181
+ expect(textEvents.length).toBe(1);
182
+ expect(textEvents[0].type === 'TextDelta' && textEvents[0].text).toBe('Hello!');
183
+ });
184
+ });
185
+
186
+ describe('streaming output', () => {
187
+ it('writes streamed text to io.write in TTY mode', async () => {
188
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'Streaming content' }]);
189
+
190
+ const io = makeIO(['go'], { tty: true, json: false });
191
+ const loop = new ConversationLoop({
192
+ client,
193
+ io,
194
+ session: makeSession(),
195
+ phaseHandler: makePhaseHandler(),
196
+ });
197
+
198
+ await loop.run();
199
+
200
+ const ioAny = io as unknown as { _written: string[] };
201
+ // In TTY mode, text goes through renderMarkdown which may add formatting
202
+ const allOutput = ioAny._written.join('');
203
+ expect(allOutput).toContain('Streaming content');
204
+ });
205
+
206
+ it('outputs JSON envelope in JSON mode', async () => {
207
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'Result text' }]);
208
+
209
+ const io = makeIO(['go'], { json: true });
210
+ const loop = new ConversationLoop({
211
+ client,
212
+ io,
213
+ session: makeSession(),
214
+ phaseHandler: makePhaseHandler(),
215
+ });
216
+
217
+ await loop.run();
218
+
219
+ const ioAny = io as unknown as { _written: string[] };
220
+ const jsonOutputs = ioAny._written.filter((w: string) => w.startsWith('{'));
221
+ expect(jsonOutputs.length).toBeGreaterThanOrEqual(1);
222
+ const parsed = JSON.parse(jsonOutputs[0]);
223
+ expect(parsed.phase).toBe('Discover');
224
+ expect(parsed.content).toBe('Result text');
225
+ });
226
+ });
227
+
228
+ describe('phase handler integration', () => {
229
+ it('applies extractResult updates to session', async () => {
230
+ const client = createFakeCopilotClient([
231
+ { role: 'assistant', content: 'We identified your challenges' },
232
+ ]);
233
+
234
+ const io = makeIO(['Our business sells widgets']);
235
+ const handler = makePhaseHandler({
236
+ extractResult: (_session, _response) => ({
237
+ businessContext: {
238
+ businessDescription: 'Widget seller',
239
+ challenges: ['Growth'],
240
+ },
241
+ }),
242
+ });
243
+
244
+ const loop = new ConversationLoop({
245
+ client,
246
+ io,
247
+ session: makeSession(),
248
+ phaseHandler: handler,
249
+ });
250
+
251
+ const result = await loop.run();
252
+ expect(result.businessContext).toBeDefined();
253
+ expect(result.businessContext!.businessDescription).toBe('Widget seller');
254
+ expect(result.businessContext!.challenges).toEqual(['Growth']);
255
+ });
256
+
257
+ it('uses system prompt from handler when creating session', async () => {
258
+ const createSessionSpy = vi.fn();
259
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'ok' }]);
260
+
261
+ // Spy on createSession
262
+ const originalCreateSession = client.createSession.bind(client);
263
+ client.createSession = async (opts) => {
264
+ createSessionSpy(opts);
265
+ return originalCreateSession(opts);
266
+ };
267
+
268
+ const io = makeIO(['test']);
269
+ const handler = makePhaseHandler({
270
+ buildSystemPrompt: () => 'Custom system prompt for discover',
271
+ });
272
+
273
+ const loop = new ConversationLoop({
274
+ client,
275
+ io,
276
+ session: makeSession(),
277
+ phaseHandler: handler,
278
+ });
279
+
280
+ await loop.run();
281
+
282
+ expect(createSessionSpy).toHaveBeenCalledWith(
283
+ expect.objectContaining({
284
+ systemPrompt: expect.stringContaining('Custom system prompt for discover'),
285
+ }),
286
+ );
287
+ });
288
+
289
+ it('includes references from handler in session options', async () => {
290
+ const createSessionSpy = vi.fn();
291
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'ok' }]);
292
+
293
+ const originalCreateSession = client.createSession.bind(client);
294
+ client.createSession = async (opts) => {
295
+ createSessionSpy(opts);
296
+ return originalCreateSession(opts);
297
+ };
298
+
299
+ const io = makeIO(['test']);
300
+ const handler = makePhaseHandler({
301
+ getReferences: () => ['doc1.md', 'doc2.md'],
302
+ });
303
+
304
+ const loop = new ConversationLoop({
305
+ client,
306
+ io,
307
+ session: makeSession(),
308
+ phaseHandler: handler,
309
+ });
310
+
311
+ await loop.run();
312
+
313
+ expect(createSessionSpy).toHaveBeenCalledWith(
314
+ expect.objectContaining({
315
+ references: ['doc1.md', 'doc2.md'],
316
+ }),
317
+ );
318
+ });
319
+ });
320
+
321
+ describe('phase completion', () => {
322
+ it('does not break loop on empty input when isComplete returns false', async () => {
323
+ const client = createFakeCopilotClient([
324
+ { role: 'assistant', content: 'Need more info' },
325
+ { role: 'assistant', content: 'Thanks' },
326
+ ]);
327
+
328
+ let callCount = 0;
329
+ const io = makeIO(['', 'more data']);
330
+ const handler = makePhaseHandler({
331
+ isComplete: () => {
332
+ callCount++;
333
+ // First call: not complete; won't be called again because second input is non-empty
334
+ return callCount > 1;
335
+ },
336
+ });
337
+
338
+ const loop = new ConversationLoop({
339
+ client,
340
+ io,
341
+ session: makeSession(),
342
+ phaseHandler: handler,
343
+ });
344
+
345
+ const result = await loop.run();
346
+ // Both inputs should produce turns (empty string still gets sent, "more data" also)
347
+ expect(result.turns!.length).toBeGreaterThanOrEqual(2);
348
+ });
349
+ });
350
+
351
+ describe('session state', () => {
352
+ it('getSession returns a copy of current session', async () => {
353
+ const client = createFakeCopilotClient([]);
354
+ const io = makeIO([null]);
355
+ const session = makeSession();
356
+
357
+ const loop = new ConversationLoop({
358
+ client,
359
+ io,
360
+ session,
361
+ phaseHandler: makePhaseHandler(),
362
+ });
363
+
364
+ const before = loop.getSession();
365
+ expect(before.sessionId).toBe('test-session-1');
366
+ // Mutate the returned copy
367
+ before.sessionId = 'mutated';
368
+ // Original should be unchanged
369
+ expect(loop.getSession().sessionId).toBe('test-session-1');
370
+ });
371
+
372
+ it('updates updatedAt timestamp after each turn', async () => {
373
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'ok' }]);
374
+
375
+ const io = makeIO(['hello']);
376
+ const loop = new ConversationLoop({
377
+ client,
378
+ io,
379
+ session: makeSession({ updatedAt: '2025-01-01T00:00:00Z' }),
380
+ phaseHandler: makePhaseHandler(),
381
+ });
382
+
383
+ const result = await loop.run();
384
+ expect(result.updatedAt).not.toBe('2025-01-01T00:00:00Z');
385
+ });
386
+ });
387
+
388
+ describe('edge cases', () => {
389
+ it('handles handler with no getReferences gracefully', async () => {
390
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'ok' }]);
391
+
392
+ const io = makeIO(['test']);
393
+ const handler = makePhaseHandler();
394
+ delete (handler as Partial<PhaseHandler>).getReferences;
395
+
396
+ const loop = new ConversationLoop({
397
+ client,
398
+ io,
399
+ session: makeSession(),
400
+ phaseHandler: handler,
401
+ });
402
+
403
+ // Should not throw
404
+ const result = await loop.run();
405
+ expect(result.turns).toHaveLength(2);
406
+ });
407
+
408
+ it('handles exhausted fake responses gracefully', async () => {
409
+ // Only 1 response configured but 2 messages sent
410
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'First' }]);
411
+
412
+ const io = makeIO(['msg1', 'msg2']);
413
+ const loop = new ConversationLoop({
414
+ client,
415
+ io,
416
+ session: makeSession(),
417
+ phaseHandler: makePhaseHandler(),
418
+ });
419
+
420
+ const result = await loop.run();
421
+ expect(result.turns).toHaveLength(4); // 2 user + 2 assistant
422
+ expect(result.turns![3].content).toContain('No more responses');
423
+ });
424
+ });
425
+
426
+ // ── T073: Auto-start behavior ──────────────────────────────────────────
427
+
428
+ describe('auto-start with initialMessage (T073)', () => {
429
+ it('sends initialMessage to LLM before readInput()', async () => {
430
+ const client = createFakeCopilotClient([
431
+ { role: 'assistant', content: 'Welcome to the Discover phase!' },
432
+ { role: 'assistant', content: 'Great, thanks for that info.' },
433
+ ]);
434
+
435
+ const readInputCalls: string[] = [];
436
+ const io = makeIO(['user says hello']);
437
+ const origReadInput = io.readInput.bind(io);
438
+ io.readInput = async (prompt?: string) => {
439
+ readInputCalls.push(prompt ?? '');
440
+ return origReadInput(prompt);
441
+ };
442
+
443
+ const loop = new ConversationLoop({
444
+ client,
445
+ io,
446
+ session: makeSession(),
447
+ phaseHandler: makePhaseHandler(),
448
+ initialMessage: 'Introduce the Discover phase and ask the first question.',
449
+ });
450
+
451
+ const result = await loop.run();
452
+
453
+ // Initial message turn + user turn = 4 turns total
454
+ expect(result.turns).toHaveLength(4);
455
+ // First turn pair: system initial message → LLM greeting
456
+ expect(result.turns![0].role).toBe('user');
457
+ expect(result.turns![0].content).toBe(
458
+ 'Introduce the Discover phase and ask the first question.',
459
+ );
460
+ expect(result.turns![1].role).toBe('assistant');
461
+ expect(result.turns![1].content).toBe('Welcome to the Discover phase!');
462
+ });
463
+
464
+ it('streams the greeting response to output', async () => {
465
+ const client = createFakeCopilotClient([
466
+ { role: 'assistant', content: 'Hello! Welcome to sofIA.' },
467
+ ]);
468
+
469
+ const io = makeIO([], { tty: true, json: false });
470
+
471
+ const loop = new ConversationLoop({
472
+ client,
473
+ io,
474
+ session: makeSession(),
475
+ phaseHandler: makePhaseHandler(),
476
+ initialMessage: 'Start the phase.',
477
+ });
478
+
479
+ await loop.run();
480
+
481
+ const ioTyped = io as LoopIO & { _written: string[] };
482
+ const allOutput = ioTyped._written.join('');
483
+ expect(allOutput).toContain('Hello! Welcome to sofIA.');
484
+ });
485
+
486
+ it('records initial exchange in turn history', async () => {
487
+ const client = createFakeCopilotClient([
488
+ { role: 'assistant', content: 'Phase intro response' },
489
+ ]);
490
+
491
+ const io = makeIO([]);
492
+
493
+ const loop = new ConversationLoop({
494
+ client,
495
+ io,
496
+ session: makeSession(),
497
+ phaseHandler: makePhaseHandler(),
498
+ initialMessage: 'Auto-start message',
499
+ });
500
+
501
+ const result = await loop.run();
502
+
503
+ expect(result.turns).toHaveLength(2);
504
+ expect(result.turns![0].role).toBe('user');
505
+ expect(result.turns![0].content).toBe('Auto-start message');
506
+ expect(result.turns![1].role).toBe('assistant');
507
+ expect(result.turns![1].content).toBe('Phase intro response');
508
+ });
509
+
510
+ it('does NOT auto-start when initialMessage is not provided', async () => {
511
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'Response' }]);
512
+
513
+ const io = makeIO(['user input']);
514
+
515
+ const loop = new ConversationLoop({
516
+ client,
517
+ io,
518
+ session: makeSession(),
519
+ phaseHandler: makePhaseHandler(),
520
+ // No initialMessage
521
+ });
522
+
523
+ const result = await loop.run();
524
+
525
+ // Only user + assistant turns, no initial message turn
526
+ expect(result.turns).toHaveLength(2);
527
+ expect(result.turns![0].role).toBe('user');
528
+ expect(result.turns![0].content).toBe('user input');
529
+ });
530
+ });
531
+
532
+ // ── Session resume: conversation history in system prompt ────────────────
533
+
534
+ describe('session resume with prior turns', () => {
535
+ it('injects prior conversation history into the system prompt on resume', async () => {
536
+ const createSessionSpy = vi.fn();
537
+ const client = createFakeCopilotClient([
538
+ { role: 'assistant', content: 'Welcome back! You told me about widgets.' },
539
+ ]);
540
+
541
+ const originalCreateSession = client.createSession.bind(client);
542
+ client.createSession = async (opts: SessionOptions) => {
543
+ createSessionSpy(opts);
544
+ return originalCreateSession(opts);
545
+ };
546
+
547
+ // Session with existing turns from a prior Discover conversation
548
+ const session = makeSession({
549
+ turns: [
550
+ {
551
+ phase: 'Discover',
552
+ sequence: 1,
553
+ role: 'user',
554
+ content: 'We sell widgets worldwide',
555
+ timestamp: '2025-01-01T00:00:00Z',
556
+ },
557
+ {
558
+ phase: 'Discover',
559
+ sequence: 2,
560
+ role: 'assistant',
561
+ content: 'Great, what are your main challenges?',
562
+ timestamp: '2025-01-01T00:01:00Z',
563
+ },
564
+ ],
565
+ });
566
+
567
+ const io = makeIO([]);
568
+ const handler = makePhaseHandler({
569
+ buildSystemPrompt: () => 'You are a workshop facilitator.',
570
+ });
571
+
572
+ const loop = new ConversationLoop({
573
+ client,
574
+ io,
575
+ session,
576
+ phaseHandler: handler,
577
+ initialMessage: 'We are resuming. Summarize progress.',
578
+ });
579
+
580
+ await loop.run();
581
+
582
+ // The system prompt should contain the prior conversation history
583
+ const passedOpts = createSessionSpy.mock.calls[0][0] as SessionOptions;
584
+ expect(passedOpts.systemPrompt).toContain('We sell widgets worldwide');
585
+ expect(passedOpts.systemPrompt).toContain('Great, what are your main challenges?');
586
+ expect(passedOpts.systemPrompt).toContain('Previous conversation');
587
+ });
588
+
589
+ it('does NOT inject history when no prior turns exist', async () => {
590
+ const createSessionSpy = vi.fn();
591
+ const client = createFakeCopilotClient([
592
+ { role: 'assistant', content: 'Welcome to the workshop!' },
593
+ ]);
594
+
595
+ const originalCreateSession = client.createSession.bind(client);
596
+ client.createSession = async (opts: SessionOptions) => {
597
+ createSessionSpy(opts);
598
+ return originalCreateSession(opts);
599
+ };
600
+
601
+ const io = makeIO([]);
602
+ const handler = makePhaseHandler({
603
+ buildSystemPrompt: () => 'You are a workshop facilitator.',
604
+ });
605
+
606
+ const loop = new ConversationLoop({
607
+ client,
608
+ io,
609
+ session: makeSession(), // No turns
610
+ phaseHandler: handler,
611
+ initialMessage: 'Start the Discover phase.',
612
+ });
613
+
614
+ await loop.run();
615
+
616
+ const passedOpts = createSessionSpy.mock.calls[0][0] as SessionOptions;
617
+ // System prompt should contain what the handler returned plus phase boundary
618
+ expect(passedOpts.systemPrompt).toContain('You are a workshop facilitator.');
619
+ });
620
+
621
+ it('only includes turns for the current phase in the history', async () => {
622
+ const createSessionSpy = vi.fn();
623
+ const client = createFakeCopilotClient([
624
+ { role: 'assistant', content: 'Resuming ideation.' },
625
+ ]);
626
+
627
+ const originalCreateSession = client.createSession.bind(client);
628
+ client.createSession = async (opts: SessionOptions) => {
629
+ createSessionSpy(opts);
630
+ return originalCreateSession(opts);
631
+ };
632
+
633
+ const session = makeSession({
634
+ phase: 'Ideate',
635
+ turns: [
636
+ {
637
+ phase: 'Discover',
638
+ sequence: 1,
639
+ role: 'user',
640
+ content: 'Discovery message (should NOT appear)',
641
+ timestamp: '2025-01-01T00:00:00Z',
642
+ },
643
+ {
644
+ phase: 'Ideate',
645
+ sequence: 2,
646
+ role: 'user',
647
+ content: 'Ideation message (should appear)',
648
+ timestamp: '2025-01-01T01:00:00Z',
649
+ },
650
+ {
651
+ phase: 'Ideate',
652
+ sequence: 3,
653
+ role: 'assistant',
654
+ content: 'Ideation response (should appear)',
655
+ timestamp: '2025-01-01T01:01:00Z',
656
+ },
657
+ ],
658
+ });
659
+
660
+ const io = makeIO([]);
661
+ const handler = makePhaseHandler({
662
+ phase: 'Ideate',
663
+ buildSystemPrompt: () => 'Ideation facilitator.',
664
+ });
665
+
666
+ const loop = new ConversationLoop({
667
+ client,
668
+ io,
669
+ session,
670
+ phaseHandler: handler,
671
+ initialMessage: 'Resume ideation.',
672
+ });
673
+
674
+ await loop.run();
675
+
676
+ const passedOpts = createSessionSpy.mock.calls[0][0] as SessionOptions;
677
+ expect(passedOpts.systemPrompt).toContain('Ideation message (should appear)');
678
+ expect(passedOpts.systemPrompt).toContain('Ideation response (should appear)');
679
+ expect(passedOpts.systemPrompt).not.toContain('Discovery message (should NOT appear)');
680
+ });
681
+ });
682
+
683
+ // ── T055: SessionOptions.onUsage callback ─────────────────────────────────
684
+
685
+ describe('SessionOptions.onUsage (T055)', () => {
686
+ it('accepts an onUsage callback on SessionOptions', () => {
687
+ const opts: SessionOptions = {
688
+ systemPrompt: 'Test',
689
+ onUsage: vi.fn(),
690
+ };
691
+ expect(opts.onUsage).toBeDefined();
692
+ expect(typeof opts.onUsage).toBe('function');
693
+ });
694
+
695
+ it('onUsage callback is forwarded when passed through createSession', async () => {
696
+ const usageCb = vi.fn();
697
+ const createSessionSpy = vi.fn();
698
+ const client = createFakeCopilotClient([{ role: 'assistant', content: 'OK' }]);
699
+ const originalCreateSession = client.createSession.bind(client);
700
+ client.createSession = async (opts: SessionOptions) => {
701
+ createSessionSpy(opts);
702
+ return originalCreateSession(opts);
703
+ };
704
+
705
+ await client.createSession({
706
+ systemPrompt: 'Test',
707
+ onUsage: usageCb,
708
+ });
709
+
710
+ const passedOpts = createSessionSpy.mock.calls[0][0] as SessionOptions;
711
+ expect(passedOpts.onUsage).toBe(usageCb);
712
+ });
713
+
714
+ it('omitting onUsage does not set it on SessionOptions', () => {
715
+ const opts: SessionOptions = { systemPrompt: 'Test' };
716
+ expect(opts.onUsage).toBeUndefined();
717
+ });
718
+ });
719
+ });