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,406 @@
1
+ /**
2
+ * Export writer.
3
+ *
4
+ * Generates Markdown artifacts per phase and summary.json
5
+ * under the export directory for a session.
6
+ *
7
+ * Contract: specs/001-cli-workshop-rebuild/contracts/export-summary-json.md
8
+ */
9
+ import { writeFile, mkdir } from 'node:fs/promises';
10
+ import { join } from 'node:path';
11
+ // ── Phase Markdown Generators ────────────────────────────────────────────────
12
+ function generateDiscoverMarkdown(session) {
13
+ const lines = ['# Discover Phase\n'];
14
+ if (session.businessContext) {
15
+ lines.push('## Business Context\n');
16
+ lines.push(`**Business**: ${session.businessContext.businessDescription}\n`);
17
+ lines.push('### Challenges\n');
18
+ for (const c of session.businessContext.challenges) {
19
+ lines.push(`- ${c}`);
20
+ }
21
+ lines.push('');
22
+ }
23
+ if (session.workflow) {
24
+ lines.push('## Workflow\n');
25
+ lines.push('### Activities\n');
26
+ for (const act of session.workflow.activities) {
27
+ lines.push(`- **${act.name}** (${act.id})`);
28
+ }
29
+ lines.push('');
30
+ if (session.workflow.edges.length > 0) {
31
+ lines.push('### Connections\n');
32
+ for (const edge of session.workflow.edges) {
33
+ lines.push(`- ${edge.fromStepId} → ${edge.toStepId}`);
34
+ }
35
+ lines.push('');
36
+ }
37
+ }
38
+ // Include conversation turns for this phase
39
+ const discoverTurns = session.turns?.filter((t) => t.phase === 'Discover') ?? [];
40
+ if (discoverTurns.length > 0) {
41
+ lines.push('## Conversation\n');
42
+ for (const turn of discoverTurns) {
43
+ lines.push(`**${turn.role}**: ${turn.content}\n`);
44
+ }
45
+ }
46
+ return lines.length > 1 ? lines.join('\n') : null;
47
+ }
48
+ function generateIdeateMarkdown(session) {
49
+ const lines = ['# Ideate Phase\n'];
50
+ // FR-020: Render structured data if available
51
+ if (session.ideas?.length) {
52
+ lines.push('## Ideas\n');
53
+ for (const idea of session.ideas) {
54
+ lines.push(`### ${idea.title}\n`);
55
+ lines.push(`${idea.description}\n`);
56
+ if (idea.workflowStepIds.length > 0) {
57
+ lines.push(`**Workflow steps**: ${idea.workflowStepIds.join(', ')}\n`);
58
+ }
59
+ }
60
+ }
61
+ // FR-022: Always include conversation turns if they exist
62
+ const ideateTurns = session.turns?.filter((t) => t.phase === 'Ideate') ?? [];
63
+ if (ideateTurns.length > 0) {
64
+ lines.push('## Conversation\n');
65
+ for (const turn of ideateTurns) {
66
+ lines.push(`**${turn.role}**: ${turn.content}\n`);
67
+ }
68
+ }
69
+ return lines.length > 1 ? lines.join('\n') : null;
70
+ }
71
+ function generateDesignMarkdown(session) {
72
+ const lines = ['# Design Phase\n'];
73
+ // FR-020: Render structured data if available
74
+ if (session.evaluation) {
75
+ lines.push(`## Evaluation\n`);
76
+ lines.push(`**Method**: ${session.evaluation.method}\n`);
77
+ lines.push('### Evaluated Ideas\n');
78
+ for (const eval_ of session.evaluation.ideas) {
79
+ lines.push(`- **${eval_.ideaId}**: Feasibility=${eval_.feasibility ?? 'N/A'}, Value=${eval_.value ?? 'N/A'}`);
80
+ }
81
+ lines.push('');
82
+ }
83
+ // FR-022: Always include conversation turns if they exist
84
+ const designTurns = session.turns?.filter((t) => t.phase === 'Design') ?? [];
85
+ if (designTurns.length > 0) {
86
+ lines.push('## Conversation\n');
87
+ for (const turn of designTurns) {
88
+ lines.push(`**${turn.role}**: ${turn.content}\n`);
89
+ }
90
+ }
91
+ return lines.length > 1 ? lines.join('\n') : null;
92
+ }
93
+ function generateSelectMarkdown(session) {
94
+ const lines = ['# Select Phase\n'];
95
+ // FR-020: Render structured data if available
96
+ if (session.selection) {
97
+ lines.push(`## Selection\n`);
98
+ lines.push(`**Selected Idea**: ${session.selection.ideaId}\n`);
99
+ lines.push(`**Rationale**: ${session.selection.selectionRationale}\n`);
100
+ lines.push(`**Confirmed**: ${session.selection.confirmedByUser ? 'Yes' : 'No'}\n`);
101
+ if (session.selection.confirmedAt) {
102
+ lines.push(`**Confirmed At**: ${session.selection.confirmedAt}\n`);
103
+ }
104
+ }
105
+ // FR-022: Always include conversation turns if they exist
106
+ const selectTurns = session.turns?.filter((t) => t.phase === 'Select') ?? [];
107
+ if (selectTurns.length > 0) {
108
+ lines.push('## Conversation\n');
109
+ for (const turn of selectTurns) {
110
+ lines.push(`**${turn.role}**: ${turn.content}\n`);
111
+ }
112
+ }
113
+ return lines.length > 1 ? lines.join('\n') : null;
114
+ }
115
+ function generatePlanMarkdown(session) {
116
+ const lines = ['# Plan Phase\n'];
117
+ // FR-020: Render structured data if available
118
+ if (session.plan?.milestones?.length) {
119
+ lines.push('## Milestones\n');
120
+ for (const m of session.plan.milestones) {
121
+ lines.push(`### ${m.title}\n`);
122
+ for (const item of m.items) {
123
+ lines.push(`- ${item}`);
124
+ }
125
+ lines.push('');
126
+ }
127
+ }
128
+ // FR-022: Always include conversation turns if they exist
129
+ const planTurns = session.turns?.filter((t) => t.phase === 'Plan') ?? [];
130
+ if (planTurns.length > 0) {
131
+ lines.push('## Conversation\n');
132
+ for (const turn of planTurns) {
133
+ lines.push(`**${turn.role}**: ${turn.content}\n`);
134
+ }
135
+ }
136
+ return lines.length > 1 ? lines.join('\n') : null;
137
+ }
138
+ function generateDevelopMarkdown(session) {
139
+ const lines = ['# Develop Phase\n'];
140
+ // FR-020: Render structured data if available
141
+ if (session.poc) {
142
+ const poc = session.poc;
143
+ // Repository location
144
+ lines.push('## PoC Repository\n');
145
+ if (poc.repoUrl) {
146
+ lines.push(`**Repository URL**: ${poc.repoUrl}\n`);
147
+ }
148
+ else if (poc.repoPath) {
149
+ lines.push(`**Repository Path**: ${poc.repoPath}\n`);
150
+ }
151
+ if (poc.repoSource) {
152
+ lines.push(`**Source**: ${poc.repoSource}\n`);
153
+ }
154
+ // Technology stack
155
+ if (poc.techStack) {
156
+ lines.push('## Technology Stack\n');
157
+ lines.push(`- **Language**: ${poc.techStack.language}`);
158
+ lines.push(`- **Runtime**: ${poc.techStack.runtime}`);
159
+ lines.push(`- **Test Runner**: ${poc.techStack.testRunner}`);
160
+ if (poc.techStack.framework) {
161
+ lines.push(`- **Framework**: ${poc.techStack.framework}`);
162
+ }
163
+ if (poc.techStack.buildCommand) {
164
+ lines.push(`- **Build Command**: ${poc.techStack.buildCommand}`);
165
+ }
166
+ lines.push('');
167
+ }
168
+ // Final status and termination
169
+ if (poc.finalStatus) {
170
+ lines.push('## Result\n');
171
+ lines.push(`**Status**: ${poc.finalStatus}\n`);
172
+ if (poc.terminationReason) {
173
+ lines.push(`**Termination Reason**: ${poc.terminationReason}\n`);
174
+ }
175
+ if (poc.totalDurationMs !== undefined) {
176
+ lines.push(`**Total Duration**: ${(poc.totalDurationMs / 1000).toFixed(1)}s\n`);
177
+ }
178
+ }
179
+ // Final test results
180
+ if (poc.finalTestResults) {
181
+ const tr = poc.finalTestResults;
182
+ lines.push('## Final Test Results\n');
183
+ lines.push(`- **Passed**: ${tr.passed}`);
184
+ lines.push(`- **Failed**: ${tr.failed}`);
185
+ lines.push(`- **Skipped**: ${tr.skipped}`);
186
+ lines.push(`- **Total**: ${tr.total}`);
187
+ lines.push(`- **Duration**: ${tr.durationMs}ms`);
188
+ if (tr.failures.length > 0) {
189
+ lines.push('\n### Failures\n');
190
+ for (const f of tr.failures) {
191
+ lines.push(`#### ${f.testName}`);
192
+ lines.push(`\`\`\`\n${f.message}\n\`\`\`\n`);
193
+ }
194
+ }
195
+ lines.push('');
196
+ }
197
+ // Iteration timeline
198
+ if (poc.iterations.length > 0) {
199
+ lines.push('## Iteration Timeline\n');
200
+ for (const iter of poc.iterations) {
201
+ const duration = iter.endedAt
202
+ ? `${((new Date(iter.endedAt).getTime() - new Date(iter.startedAt).getTime()) / 1000).toFixed(1)}s`
203
+ : 'in progress';
204
+ lines.push(`### Iteration ${iter.iteration} — ${iter.outcome} (${duration})\n`);
205
+ if (iter.changesSummary) {
206
+ lines.push(`${iter.changesSummary}\n`);
207
+ }
208
+ if (iter.filesChanged.length > 0) {
209
+ lines.push(`**Files changed**: ${iter.filesChanged.join(', ')}\n`);
210
+ }
211
+ if (iter.testResults) {
212
+ const tr = iter.testResults;
213
+ lines.push(`**Tests**: ${tr.passed} passed, ${tr.failed} failed, ${tr.skipped} skipped (${tr.durationMs}ms)\n`);
214
+ }
215
+ if (iter.errorMessage) {
216
+ lines.push(`**Error**: ${iter.errorMessage}\n`);
217
+ }
218
+ }
219
+ }
220
+ }
221
+ // FR-022: Always include conversation turns if they exist
222
+ const developTurns = session.turns?.filter((t) => t.phase === 'Develop') ?? [];
223
+ if (developTurns.length > 0) {
224
+ lines.push('## Conversation\n');
225
+ for (const turn of developTurns) {
226
+ lines.push(`**${turn.role}**: ${turn.content}\n`);
227
+ }
228
+ }
229
+ return lines.length > 1 ? lines.join('\n') : null;
230
+ }
231
+ // ── Phase generator mapping ──────────────────────────────────────────────────
232
+ const PHASE_GENERATORS = {
233
+ Discover: generateDiscoverMarkdown,
234
+ Ideate: generateIdeateMarkdown,
235
+ Design: generateDesignMarkdown,
236
+ Select: generateSelectMarkdown,
237
+ Plan: generatePlanMarkdown,
238
+ Develop: generateDevelopMarkdown,
239
+ };
240
+ /**
241
+ * Generators for the pre-Develop phases only (the workshop decision history).
242
+ * Develop is excluded because the repo itself IS the PoC output.
243
+ */
244
+ const WORKSHOP_DOC_GENERATORS = {
245
+ Discover: generateDiscoverMarkdown,
246
+ Ideate: generateIdeateMarkdown,
247
+ Design: generateDesignMarkdown,
248
+ Select: generateSelectMarkdown,
249
+ Plan: generatePlanMarkdown,
250
+ };
251
+ /**
252
+ * Export workshop documentation into a PoC repository.
253
+ *
254
+ * Creates `docs/workshop/<phase>.md` files for Discover → Plan and a
255
+ * `WORKSHOP.md` index at the repo root. This preserves the full decision
256
+ * history (why this PoC was selected, how it was designed) alongside the code.
257
+ *
258
+ * The Develop phase is intentionally excluded — the repo itself is the PoC.
259
+ *
260
+ * @param session The workshop session to export docs from
261
+ * @param repoDir Root directory of the PoC repository
262
+ * @returns List of created files relative to repoDir
263
+ */
264
+ export async function exportWorkshopDocs(session, repoDir) {
265
+ const workshopDir = join(repoDir, 'docs', 'workshop');
266
+ await mkdir(workshopDir, { recursive: true });
267
+ const createdFiles = [];
268
+ const phaseFiles = [];
269
+ // Generate per-phase markdown files
270
+ for (const [phaseName, generator] of Object.entries(WORKSHOP_DOC_GENERATORS)) {
271
+ const content = generator(session);
272
+ if (content) {
273
+ const fileName = `${phaseName.toLowerCase()}.md`;
274
+ await writeFile(join(workshopDir, fileName), content, 'utf-8');
275
+ createdFiles.push(`docs/workshop/${fileName}`);
276
+ phaseFiles.push({ phase: phaseName, fileName });
277
+ }
278
+ }
279
+ // Generate WORKSHOP.md index at the repo root
280
+ const indexLines = [
281
+ '# Workshop Summary\n',
282
+ `This project was generated by [sofIA](https://github.com/jmservera/sofIA-cli) — an AI Discovery Workshop CLI.\n`,
283
+ `**Session**: \`${session.sessionId}\` `,
284
+ `**Generated**: ${new Date().toISOString()}\n`,
285
+ ];
286
+ if (session.businessContext) {
287
+ indexLines.push(`## Business Context\n`);
288
+ indexLines.push(`${session.businessContext.businessDescription}\n`);
289
+ }
290
+ if (session.selection) {
291
+ const idea = session.ideas?.find((i) => i.id === session.selection?.ideaId);
292
+ indexLines.push(`## Selected Idea\n`);
293
+ if (idea) {
294
+ indexLines.push(`**${idea.title}**: ${idea.description}\n`);
295
+ }
296
+ indexLines.push(`**Rationale**: ${session.selection.selectionRationale}\n`);
297
+ }
298
+ if (phaseFiles.length > 0) {
299
+ indexLines.push('## Workshop Documentation\n');
300
+ indexLines.push('Detailed documentation for each workshop phase:\n');
301
+ for (const { phase, fileName } of phaseFiles) {
302
+ indexLines.push(`- [${phase} Phase](docs/workshop/${fileName})`);
303
+ }
304
+ indexLines.push('');
305
+ }
306
+ const indexContent = indexLines.join('\n');
307
+ await writeFile(join(repoDir, 'WORKSHOP.md'), indexContent, 'utf-8');
308
+ createdFiles.push('WORKSHOP.md');
309
+ return { createdFiles };
310
+ }
311
+ /**
312
+ * Export a workshop session to the specified directory.
313
+ * Generates Markdown files for each phase with data and a summary.json.
314
+ */
315
+ export async function exportSession(session, exportDir) {
316
+ await mkdir(exportDir, { recursive: true });
317
+ const files = [];
318
+ // Generate phase Markdown files
319
+ for (const [phaseName, generator] of Object.entries(PHASE_GENERATORS)) {
320
+ const content = generator(session);
321
+ if (content) {
322
+ const fileName = `${phaseName.toLowerCase()}.md`;
323
+ await writeFile(join(exportDir, fileName), content, 'utf-8');
324
+ files.push({ path: fileName, type: 'markdown' });
325
+ }
326
+ }
327
+ // Generate highlights — include one entry per phase with data or turns (FR-024)
328
+ const highlights = [];
329
+ if (session.businessContext) {
330
+ highlights.push(`Business: ${session.businessContext.businessDescription}`);
331
+ }
332
+ else {
333
+ const discoverTurns = session.turns?.filter((t) => t.phase === 'Discover' && t.role === 'assistant') ?? [];
334
+ if (discoverTurns.length > 0) {
335
+ highlights.push(`Discover: ${discoverTurns[0].content.slice(0, 100)}`);
336
+ }
337
+ }
338
+ if (session.ideas?.length) {
339
+ highlights.push(`Ideas: ${session.ideas.length} ideas generated`);
340
+ }
341
+ else {
342
+ const ideateTurns = session.turns?.filter((t) => t.phase === 'Ideate' && t.role === 'assistant') ?? [];
343
+ if (ideateTurns.length > 0) {
344
+ highlights.push(`Ideate: ${ideateTurns[0].content.slice(0, 100)}`);
345
+ }
346
+ }
347
+ if (session.evaluation) {
348
+ highlights.push(`Evaluation: ${session.evaluation.ideas.length} ideas evaluated`);
349
+ }
350
+ else {
351
+ const designTurns = session.turns?.filter((t) => t.phase === 'Design' && t.role === 'assistant') ?? [];
352
+ if (designTurns.length > 0) {
353
+ highlights.push(`Design: ${designTurns[0].content.slice(0, 100)}`);
354
+ }
355
+ }
356
+ if (session.selection) {
357
+ highlights.push(`Selected idea: ${session.selection.ideaId}`);
358
+ }
359
+ else {
360
+ const selectTurns = session.turns?.filter((t) => t.phase === 'Select' && t.role === 'assistant') ?? [];
361
+ if (selectTurns.length > 0) {
362
+ highlights.push(`Select: ${selectTurns[0].content.slice(0, 100)}`);
363
+ }
364
+ }
365
+ if (session.plan?.milestones?.length) {
366
+ highlights.push(`${session.plan.milestones.length} milestones planned`);
367
+ }
368
+ else {
369
+ const planTurns = session.turns?.filter((t) => t.phase === 'Plan' && t.role === 'assistant') ?? [];
370
+ if (planTurns.length > 0) {
371
+ highlights.push(`Plan: ${planTurns[0].content.slice(0, 100)}`);
372
+ }
373
+ }
374
+ // PoC highlights
375
+ if (session.poc) {
376
+ const poc = session.poc;
377
+ if (poc.finalStatus) {
378
+ highlights.push(`PoC status: ${poc.finalStatus}`);
379
+ }
380
+ if (poc.iterations?.length) {
381
+ highlights.push(`PoC iterations: ${poc.iterations.length}`);
382
+ }
383
+ if (poc.terminationReason) {
384
+ highlights.push(`PoC termination: ${poc.terminationReason}`);
385
+ }
386
+ }
387
+ else {
388
+ const developTurns = session.turns?.filter((t) => t.phase === 'Develop' && t.role === 'assistant') ?? [];
389
+ if (developTurns.length > 0) {
390
+ highlights.push(`Develop: ${developTurns[0].content.slice(0, 100)}`);
391
+ }
392
+ }
393
+ // Generate summary.json
394
+ const summary = {
395
+ sessionId: session.sessionId,
396
+ exportedAt: new Date().toISOString(),
397
+ phase: session.phase,
398
+ status: session.status,
399
+ files,
400
+ highlights: highlights.length > 0 ? highlights : undefined,
401
+ };
402
+ const summaryFileName = 'summary.json';
403
+ await writeFile(join(exportDir, summaryFileName), JSON.stringify(summary, null, 2), 'utf-8');
404
+ files.push({ path: summaryFileName, type: 'json' });
405
+ return { exportDir, files };
406
+ }
@@ -0,0 +1,81 @@
1
+ import { getPhaseOrder } from '../phases/phaseHandlers.js';
2
+ // ── Phase-to-fields mapping ──────────────────────────────────────────────────
3
+ /**
4
+ * Maps each phase to the session fields it produces.
5
+ * When invalidating a phase, these fields are cleared.
6
+ */
7
+ const PHASE_FIELDS = {
8
+ Discover: ['businessContext', 'workflow'],
9
+ Ideate: ['ideas'],
10
+ Design: ['evaluation'],
11
+ Select: ['selection'],
12
+ Plan: ['plan'],
13
+ Develop: ['poc'],
14
+ };
15
+ // ── Backtrack ────────────────────────────────────────────────────────────────
16
+ /**
17
+ * Backtrack a session to an earlier phase.
18
+ *
19
+ * This invalidates all downstream phase data (fields and turns) to ensure
20
+ * deterministic re-computation when the phase is re-run.
21
+ *
22
+ * - Backtracking to the same phase is a no-op (preserves all data).
23
+ * - Backtracking forward (to a later phase) is rejected.
24
+ * - The target phase's fields are also cleared (since it will be re-run).
25
+ */
26
+ export function backtrackSession(session, targetPhase) {
27
+ const phaseOrder = getPhaseOrder();
28
+ const currentIdx = phaseOrder.indexOf(session.phase);
29
+ const targetIdx = phaseOrder.indexOf(targetPhase);
30
+ // Handle phases not in the order (e.g., Complete)
31
+ const effectiveCurrentIdx = currentIdx === -1 ? phaseOrder.length : currentIdx;
32
+ if (targetIdx === -1) {
33
+ return {
34
+ success: false,
35
+ session,
36
+ invalidatedPhases: [],
37
+ error: `Unknown phase: ${targetPhase}`,
38
+ };
39
+ }
40
+ if (targetIdx > effectiveCurrentIdx) {
41
+ return {
42
+ success: false,
43
+ session,
44
+ invalidatedPhases: [],
45
+ error: `Cannot backtrack forward from "${session.phase}" to "${targetPhase}".`,
46
+ };
47
+ }
48
+ // Same phase → no-op
49
+ if (targetIdx === effectiveCurrentIdx) {
50
+ return {
51
+ success: true,
52
+ session: { ...session },
53
+ invalidatedPhases: [],
54
+ };
55
+ }
56
+ // Collect phases to invalidate: from targetPhase to current (inclusive)
57
+ const invalidatedPhases = [];
58
+ const updatedSession = { ...session };
59
+ for (let i = targetIdx; i <= effectiveCurrentIdx && i < phaseOrder.length; i++) {
60
+ const phase = phaseOrder[i];
61
+ invalidatedPhases.push(phase);
62
+ // Clear fields produced by this phase
63
+ const fields = PHASE_FIELDS[phase] ?? [];
64
+ for (const field of fields) {
65
+ updatedSession[field] = undefined;
66
+ }
67
+ }
68
+ // Remove turns from invalidated phases
69
+ if (updatedSession.turns) {
70
+ const validPhases = new Set(phaseOrder.slice(0, targetIdx));
71
+ updatedSession.turns = updatedSession.turns.filter(t => validPhases.has(t.phase));
72
+ }
73
+ updatedSession.phase = targetPhase;
74
+ updatedSession.status = 'Active';
75
+ updatedSession.updatedAt = new Date().toISOString();
76
+ return {
77
+ success: true,
78
+ session: updatedSession,
79
+ invalidatedPhases,
80
+ };
81
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Session persistence adapter.
3
+ *
4
+ * Reads/writes WorkshopSession JSON files to .sofia/sessions/<sessionId>.json.
5
+ * Uses write-then-rename for atomic writes.
6
+ */
7
+ import { readFile, writeFile, mkdir, readdir, unlink, rename, access } from 'node:fs/promises';
8
+ import { join } from 'node:path';
9
+ import { randomBytes } from 'node:crypto';
10
+ import { workshopSessionSchema } from '../shared/schemas/session.js';
11
+ export class SessionStore {
12
+ baseDir;
13
+ constructor(baseDir) {
14
+ this.baseDir = baseDir;
15
+ }
16
+ /** Persist a session atomically (write to temp, rename). */
17
+ async save(session) {
18
+ await mkdir(this.baseDir, { recursive: true });
19
+ const filePath = this.filePath(session.sessionId);
20
+ const tmpPath = filePath + '.tmp.' + randomBytes(4).toString('hex');
21
+ const json = JSON.stringify(session, null, 2);
22
+ await writeFile(tmpPath, json, 'utf-8');
23
+ await rename(tmpPath, filePath);
24
+ }
25
+ /** Load and validate a session by ID. Throws if not found or invalid. */
26
+ async load(sessionId) {
27
+ const filePath = this.filePath(sessionId);
28
+ const raw = await readFile(filePath, 'utf-8');
29
+ const parsed = JSON.parse(raw);
30
+ return workshopSessionSchema.parse(parsed);
31
+ }
32
+ /** List all session IDs in the store. */
33
+ async list() {
34
+ try {
35
+ const files = await readdir(this.baseDir);
36
+ return files
37
+ .filter((f) => f.endsWith('.json'))
38
+ .map((f) => f.replace(/\.json$/, ''));
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ }
44
+ /** Check if a session exists. */
45
+ async exists(sessionId) {
46
+ try {
47
+ await access(this.filePath(sessionId));
48
+ return true;
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
54
+ /** Delete a session file. */
55
+ async delete(sessionId) {
56
+ await unlink(this.filePath(sessionId));
57
+ }
58
+ filePath(sessionId) {
59
+ return join(this.baseDir, `${sessionId}.json`);
60
+ }
61
+ }
62
+ /** Default store location relative to CWD: .sofia/sessions/ */
63
+ export function createDefaultStore() {
64
+ return new SessionStore(join(process.cwd(), '.sofia', 'sessions'));
65
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Activity spinner module.
3
+ *
4
+ * Wraps `ora` to provide unified visual feedback during LLM processing,
5
+ * tool calls, and internal operations (FR-043a, FR-043c).
6
+ *
7
+ * Spinner lifecycle:
8
+ * User input → startThinking() → "Thinking..."
9
+ * → ToolCall event → startToolCall(name) → "⠋ <name>..."
10
+ * → ToolResult event → completeToolCall(name, summary) → "✓ <name>: <summary>"
11
+ * → startThinking() (if more processing expected)
12
+ * → TextDelta event → stop() → stream text
13
+ *
14
+ * All operations are no-ops when non-TTY or JSON mode.
15
+ */
16
+ import ora from 'ora';
17
+ export class ActivitySpinner {
18
+ spinner = null;
19
+ _active = false;
20
+ enabled;
21
+ debug;
22
+ stream;
23
+ constructor(options) {
24
+ this.enabled = options.isTTY && !options.isJsonMode;
25
+ this.debug = options.debugMode ?? false;
26
+ this.stream = options.stream ?? process.stderr;
27
+ }
28
+ /** Display "Thinking..." spinner during silent gaps. */
29
+ startThinking() {
30
+ if (!this.enabled)
31
+ return;
32
+ if (this._active && this.spinner) {
33
+ this.spinner.text = 'Thinking...';
34
+ return;
35
+ }
36
+ this.spinner = ora({
37
+ text: 'Thinking...',
38
+ stream: this.stream,
39
+ discardStdin: false,
40
+ }).start();
41
+ this._active = true;
42
+ }
43
+ /** Transition spinner to show tool-specific status. */
44
+ startToolCall(toolName) {
45
+ if (!this.enabled)
46
+ return;
47
+ if (this._active && this.spinner) {
48
+ this.spinner.text = `${toolName}...`;
49
+ }
50
+ else {
51
+ this.spinner = ora({
52
+ text: `${toolName}...`,
53
+ stream: this.stream,
54
+ discardStdin: false,
55
+ }).start();
56
+ this._active = true;
57
+ }
58
+ }
59
+ /**
60
+ * Stop spinner and print a one-line tool completion summary.
61
+ * The summary line remains visible in the output stream.
62
+ */
63
+ completeToolCall(_toolName, _summary) {
64
+ if (!this.enabled)
65
+ return;
66
+ if (this._active && this.spinner) {
67
+ this.spinner.stop();
68
+ }
69
+ this._active = false;
70
+ // Summary output is handled by io.writeToolSummary() to respect
71
+ // JSON/debug mode — no duplicate write here.
72
+ }
73
+ /** Stop any active spinner. */
74
+ stop() {
75
+ if (this.spinner) {
76
+ this.spinner.stop();
77
+ }
78
+ this.spinner = null;
79
+ this._active = false;
80
+ }
81
+ /** Check if a spinner is currently active. */
82
+ isActive() {
83
+ return this._active;
84
+ }
85
+ }
86
+ /**
87
+ * Create a no-op spinner for tests or non-TTY environments.
88
+ */
89
+ export function createNoOpSpinner() {
90
+ return new ActivitySpinner({ isTTY: false, isJsonMode: true });
91
+ }