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,496 @@
1
+ /**
2
+ * Workshop command handler.
3
+ *
4
+ * Implements `sofia workshop` — the main interactive workshop command
5
+ * with New Session, Resume Session, Status, and Export menu options.
6
+ * Drives phase-by-phase conversation with decision gates.
7
+ */
8
+ import { join } from 'node:path';
9
+
10
+ import { ConversationLoop } from '../loop/conversationLoop.js';
11
+ import { createCopilotClient } from '../shared/copilotClient.js';
12
+ import type { CopilotClient } from '../shared/copilotClient.js';
13
+ import { ActivitySpinner } from '../shared/activitySpinner.js';
14
+ import { getLogger } from '../logging/logger.js';
15
+ import type { WorkshopSession, PhaseValue } from '../shared/schemas/session.js';
16
+ import { SessionStore, createDefaultStore } from '../sessions/sessionStore.js';
17
+ import { createLoopIO } from './ioContext.js';
18
+ import { createPhaseHandler, getPhaseOrder, getNextPhase } from '../phases/phaseHandlers.js';
19
+ import type { PhaseHandlerConfig } from '../phases/phaseHandlers.js';
20
+ import type { SofiaEvent } from '../shared/events.js';
21
+ import { renderMarkdown } from '../shared/markdownRenderer.js';
22
+ import { destroyWebSearchSession, isWebSearchConfigured, createWebSearchTool } from '../mcp/webSearch.js';
23
+ import type { WebSearchConfig } from '../mcp/webSearch.js';
24
+ import { loadEnvFile } from './envLoader.js';
25
+ import { loadMcpConfig, McpManager } from '../mcp/mcpManager.js';
26
+ import { RalphLoop } from '../develop/ralphLoop.js';
27
+ import { deriveCheckpointState } from '../develop/checkpointState.js';
28
+ import { createDefaultRegistry, selectTemplate } from '../develop/templateRegistry.js';
29
+
30
+ export interface WorkshopCommandOptions {
31
+ session?: string;
32
+ json?: boolean;
33
+ debug?: boolean;
34
+ logFile?: string;
35
+ nonInteractive?: boolean;
36
+ newSession?: boolean;
37
+ phase?: string;
38
+ retry?: number;
39
+ }
40
+
41
+ /**
42
+ * Generate a timestamp-based session ID for human-friendly filenames.
43
+ * Format: YYYY-MM-DD_HHmmss (e.g. "2026-02-27_143052")
44
+ */
45
+ function generateSessionId(): string {
46
+ const d = new Date();
47
+ const pad = (n: number) => String(n).padStart(2, '0');
48
+ return [
49
+ d.getFullYear(),
50
+ '-',
51
+ pad(d.getMonth() + 1),
52
+ '-',
53
+ pad(d.getDate()),
54
+ '_',
55
+ pad(d.getHours()),
56
+ pad(d.getMinutes()),
57
+ pad(d.getSeconds()),
58
+ ].join('');
59
+ }
60
+
61
+ /**
62
+ * Format a session identifier for display.
63
+ * Shows "name (id)" when a name exists, otherwise just the id.
64
+ */
65
+ function sessionDisplayName(session: { sessionId: string; name?: string }): string {
66
+ return session.name ? `${session.name} (${session.sessionId})` : session.sessionId;
67
+ }
68
+
69
+ /**
70
+ * Create a new empty workshop session.
71
+ */
72
+ function createNewSession(): WorkshopSession {
73
+ const now = new Date().toISOString();
74
+ return {
75
+ sessionId: generateSessionId(),
76
+ schemaVersion: '1.0.0',
77
+ createdAt: now,
78
+ updatedAt: now,
79
+ phase: 'Discover',
80
+ status: 'Active',
81
+ participants: [],
82
+ artifacts: { generatedFiles: [] },
83
+ turns: [],
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Show the main workshop menu and return user choice.
89
+ */
90
+ async function showMainMenu(
91
+ io: ReturnType<typeof createLoopIO>,
92
+ existingSessions: string[],
93
+ ): Promise<'new' | 'resume' | 'exit'> {
94
+ const hasExisting = existingSessions.length > 0;
95
+
96
+ const menuText = [
97
+ '\n# sofIA — AI Discovery Workshop\n',
98
+ 'Choose an option:\n',
99
+ ' 1. Start a new workshop session',
100
+ hasExisting ? ' 2. Resume an existing session' : '',
101
+ ' 3. Exit',
102
+ '',
103
+ ]
104
+ .filter(Boolean)
105
+ .join('\n');
106
+
107
+ io.write(renderMarkdown(menuText, { isTTY: io.isTTY }));
108
+
109
+ const answer = await io.readInput('Choose [1-3]: ');
110
+ if (answer === null || answer.trim() === '3') return 'exit';
111
+ if (answer.trim() === '2' && hasExisting) return 'resume';
112
+ return 'new';
113
+ }
114
+
115
+ /**
116
+ * Run the workshop flow for a session through its phases.
117
+ */
118
+ async function runWorkshop(
119
+ session: WorkshopSession,
120
+ client: CopilotClient,
121
+ io: ReturnType<typeof createLoopIO>,
122
+ store: SessionStore,
123
+ options: WorkshopCommandOptions,
124
+ handlerConfig?: PhaseHandlerConfig,
125
+ ): Promise<void> {
126
+ const logger = getLogger();
127
+ const phaseOrder = getPhaseOrder();
128
+ let currentPhaseIdx = phaseOrder.indexOf(session.phase);
129
+ if (currentPhaseIdx === -1) currentPhaseIdx = 0;
130
+
131
+ // If a specific phase was requested, jump to it
132
+ if (options.phase) {
133
+ const requestedIdx = phaseOrder.indexOf(options.phase as PhaseValue);
134
+ if (requestedIdx !== -1) {
135
+ currentPhaseIdx = requestedIdx;
136
+ session.phase = phaseOrder[currentPhaseIdx];
137
+ }
138
+ }
139
+
140
+ while (currentPhaseIdx < phaseOrder.length) {
141
+ const phase = phaseOrder[currentPhaseIdx];
142
+ session.phase = phase;
143
+ session.updatedAt = new Date().toISOString();
144
+ await store.save(session);
145
+
146
+ io.write(renderMarkdown(`\n## Phase: ${phase}\n`, { isTTY: io.isTTY }));
147
+
148
+ // Create and preload the phase handler
149
+ const handler = createPhaseHandler(phase, handlerConfig);
150
+ await handler._preload();
151
+
152
+ // Generate initial message for auto-start
153
+ const initialMessage = handler.getInitialMessage?.(session);
154
+
155
+ const events: SofiaEvent[] = [];
156
+
157
+ // Create activity spinner for visual feedback
158
+ const spinner = new ActivitySpinner({
159
+ isTTY: io.isTTY,
160
+ isJsonMode: io.isJsonMode,
161
+ debugMode: options.debug,
162
+ });
163
+
164
+ const loop = new ConversationLoop({
165
+ client,
166
+ io,
167
+ session,
168
+ phaseHandler: handler,
169
+ initialMessage,
170
+ spinner,
171
+ onEvent: (e) => {
172
+ events.push(e);
173
+ if (options.debug && e.type === 'Activity') {
174
+ io.writeActivity(e.message);
175
+ }
176
+ },
177
+ onSessionUpdate: async (updatedSession) => {
178
+ session = updatedSession;
179
+ await store.save(session);
180
+ },
181
+ });
182
+
183
+ // Run the conversation loop for this phase
184
+ session = await loop.run();
185
+ session.updatedAt = new Date().toISOString();
186
+ await store.save(session);
187
+
188
+ // Decision gate
189
+ const gateResult = await io.showDecisionGate(phase);
190
+
191
+ switch (gateResult.choice) {
192
+ case 'continue': {
193
+ const next = getNextPhase(phase);
194
+ if (next) {
195
+ // FR-020: Show transition guidance when Plan → Develop
196
+ if (next === 'Develop') {
197
+ io.write(
198
+ renderMarkdown(
199
+ `\n### Starting PoC Generation\n\n` +
200
+ `The Plan phase is complete. Now generating proof-of-concept code...\n`,
201
+ { isTTY: io.isTTY },
202
+ ),
203
+ );
204
+
205
+ // FR-021: Always run PoC generation after Plan phase
206
+ try {
207
+ const outputDir = join(process.cwd(), '..', 'poc', session.sessionId);
208
+ const checkpoint = deriveCheckpointState(session, outputDir);
209
+ const templateEntry = selectTemplate(
210
+ createDefaultRegistry(),
211
+ session.plan?.architectureNotes,
212
+ session.plan?.dependencies,
213
+ );
214
+
215
+ const ralphLoop = new RalphLoop({
216
+ client,
217
+ io,
218
+ session,
219
+ spinner,
220
+ maxIterations: 20,
221
+ outputDir,
222
+ onSessionUpdate: async (updated) => {
223
+ session = updated;
224
+ await store.save(session);
225
+ },
226
+ onEvent: (event: SofiaEvent) => {
227
+ // Log PoC generation events if debug is enabled
228
+ if (options.debug) {
229
+ logger.debug('RalphLoop event', { event });
230
+ }
231
+ },
232
+ checkpoint,
233
+ templateEntry,
234
+ });
235
+
236
+ const result = await ralphLoop.run();
237
+ session = result.session;
238
+
239
+ if (result.finalStatus === 'success') {
240
+ io.write(
241
+ renderMarkdown(
242
+ `\n✅ PoC generated successfully in ${result.outputDir}\n\n` +
243
+ `Next steps:\n` +
244
+ `- Review the generated code\n` +
245
+ `- Run tests: \`cd ${result.outputDir} && npm test\`\n` +
246
+ `- Continue developing: \`sofia dev --session ${session.sessionId}\`\n`,
247
+ { isTTY: io.isTTY },
248
+ ),
249
+ );
250
+ } else {
251
+ io.write(
252
+ renderMarkdown(
253
+ `\n⚠️ PoC generation incomplete (${result.terminationReason})\n\n` +
254
+ `The workshop can continue. To retry or resume PoC development:\n` +
255
+ `\`\`\`\n` +
256
+ `sofia dev --session ${session.sessionId}\n` +
257
+ `\`\`\`\n`,
258
+ { isTTY: io.isTTY },
259
+ ),
260
+ );
261
+ }
262
+ } catch (err) {
263
+ const msg = err instanceof Error ? err.message : String(err);
264
+ io.write(
265
+ renderMarkdown(
266
+ `\n⚠️ PoC generation failed: ${msg}\n\n` +
267
+ `The workshop can continue. To retry:\n` +
268
+ `\`\`\`\n` +
269
+ `sofia dev --session ${session.sessionId}\n` +
270
+ `\`\`\`\n`,
271
+ { isTTY: io.isTTY },
272
+ ),
273
+ );
274
+ }
275
+
276
+ // Continue to next phase regardless of PoC outcome
277
+ }
278
+ currentPhaseIdx = phaseOrder.indexOf(next);
279
+ } else {
280
+ // Mark session as completed
281
+ session.phase = 'Complete';
282
+ session.status = 'Completed';
283
+ session.updatedAt = new Date().toISOString();
284
+ await store.save(session);
285
+
286
+ io.write(
287
+ renderMarkdown(
288
+ '\n## Workshop Complete!\n\nUse `sofia export` to generate artifacts.\n',
289
+ {
290
+ isTTY: io.isTTY,
291
+ },
292
+ ),
293
+ );
294
+ return;
295
+ }
296
+ break;
297
+ }
298
+ case 'refine':
299
+ // Stay on the same phase
300
+ break;
301
+ case 'menu':
302
+ return; // Return to main menu
303
+ case 'exit':
304
+ session.status = 'Paused';
305
+ session.updatedAt = new Date().toISOString();
306
+ await store.save(session);
307
+ io.write(
308
+ renderMarkdown(
309
+ `\nSession **${sessionDisplayName(session)}** paused. Resume later with \`sofia workshop --session ${session.sessionId}\`.\n`,
310
+ { isTTY: io.isTTY },
311
+ ),
312
+ );
313
+ return;
314
+ }
315
+ }
316
+ }
317
+
318
+ export async function workshopCommand(opts: WorkshopCommandOptions): Promise<void> {
319
+ try {
320
+ await workshopCommandInner(opts);
321
+ } finally {
322
+ // Clean up ephemeral web search agent on workshop exit (FR-015)
323
+ await destroyWebSearchSession();
324
+ }
325
+ }
326
+
327
+ async function workshopCommandInner(opts: WorkshopCommandOptions): Promise<void> {
328
+ // FR-010: Load .env before any env var checks (e.g., isWebSearchConfigured)
329
+ loadEnvFile(join(process.cwd(), '.env'));
330
+
331
+ const store = createDefaultStore();
332
+ const io = createLoopIO({
333
+ json: opts.json,
334
+ nonInteractive: opts.nonInteractive,
335
+ debug: opts.debug,
336
+ });
337
+
338
+ // Get Copilot client
339
+ let client: CopilotClient;
340
+ try {
341
+ client = await createCopilotClient();
342
+ } catch (err: unknown) {
343
+ const logger = getLogger();
344
+ logger.error({ err }, 'Failed to create Copilot client — cannot start workshop');
345
+ const msg = err instanceof Error ? err.message : 'Unknown error creating Copilot client';
346
+ if (opts.json) {
347
+ process.stdout.write(JSON.stringify({ error: msg }) + '\n');
348
+ } else {
349
+ console.error(`Error: ${msg}`);
350
+ }
351
+ process.exitCode = 1;
352
+ return;
353
+ }
354
+
355
+ // FR-011: Create McpManager from .vscode/mcp.json
356
+ let mcpManager: McpManager | undefined;
357
+ try {
358
+ const mcpConfigPath = join(process.cwd(), '.vscode', 'mcp.json');
359
+ const mcpConfig = await loadMcpConfig(mcpConfigPath);
360
+ mcpManager = new McpManager(mcpConfig);
361
+ } catch {
362
+ // MCP not configured — proceed without it
363
+ }
364
+
365
+ // FR-012: Create WebSearchClient when configured
366
+ let webSearchClient: import('../phases/discoveryEnricher.js').WebSearchClient | undefined;
367
+ if (isWebSearchConfigured()) {
368
+ const config: WebSearchConfig = {
369
+ projectEndpoint: process.env.FOUNDRY_PROJECT_ENDPOINT!,
370
+ modelDeploymentName: process.env.FOUNDRY_MODEL_DEPLOYMENT_NAME!,
371
+ };
372
+ const searchFn = createWebSearchTool(config);
373
+ webSearchClient = { search: searchFn };
374
+ }
375
+
376
+ // Build handler config with MCP + web search
377
+ const handlerConfig: PhaseHandlerConfig = {
378
+ discover: {
379
+ io,
380
+ mcpManager,
381
+ webSearchClient,
382
+ },
383
+ mcpManager,
384
+ webSearchClient,
385
+ };
386
+
387
+ // Direct session resumption
388
+ if (opts.session) {
389
+ if (await store.exists(opts.session)) {
390
+ const session = await store.load(opts.session);
391
+ io.write(
392
+ renderMarkdown(`\nResuming session: **${sessionDisplayName(session)}**\n`, {
393
+ isTTY: io.isTTY,
394
+ }),
395
+ );
396
+ await runWorkshop(session, client, io, store, opts, handlerConfig);
397
+ } else {
398
+ const msg = `Session "${opts.session}" not found.`;
399
+ if (opts.json) {
400
+ process.stdout.write(JSON.stringify({ error: msg }) + '\n');
401
+ } else {
402
+ console.error(`Error: ${msg}`);
403
+ }
404
+ process.exitCode = 1;
405
+ }
406
+ return;
407
+ }
408
+
409
+ // New session flag
410
+ if (opts.newSession) {
411
+ const session = createNewSession();
412
+ await store.save(session);
413
+ if (opts.json) {
414
+ process.stdout.write(
415
+ JSON.stringify({ sessionId: session.sessionId, phase: session.phase }) + '\n',
416
+ );
417
+ } else {
418
+ io.write(
419
+ renderMarkdown(`\nNew session created: **${session.sessionId}**\n`, { isTTY: io.isTTY }),
420
+ );
421
+ }
422
+ await runWorkshop(session, client, io, store, opts, handlerConfig);
423
+ return;
424
+ }
425
+
426
+ // Interactive menu
427
+ if (opts.nonInteractive) {
428
+ if (opts.json) {
429
+ process.stdout.write(
430
+ JSON.stringify({ error: 'Non-interactive mode requires --session or --new-session.' }) +
431
+ '\n',
432
+ );
433
+ } else {
434
+ console.error('Error: Non-interactive mode requires --session or --new-session.');
435
+ }
436
+ process.exitCode = 1;
437
+ return;
438
+ }
439
+
440
+ const existingSessions = await store.list();
441
+ const choice = await showMainMenu(io, existingSessions);
442
+
443
+ switch (choice) {
444
+ case 'new': {
445
+ const session = createNewSession();
446
+ await store.save(session);
447
+ io.write(renderMarkdown(`\nNew session: **${session.sessionId}**\n`, { isTTY: io.isTTY }));
448
+ await runWorkshop(session, client, io, store, opts, handlerConfig);
449
+ break;
450
+ }
451
+ case 'resume': {
452
+ const sessions = await store.list();
453
+ if (sessions.length === 0) {
454
+ io.write('No existing sessions found.\n');
455
+ break;
456
+ }
457
+ const sessionEntries = (
458
+ await Promise.all(
459
+ sessions.map(async (id) => {
460
+ try {
461
+ const session = await store.load(id);
462
+ return { id, display: sessionDisplayName(session), session };
463
+ } catch {
464
+ return { id, display: id, session: undefined };
465
+ }
466
+ }),
467
+ )
468
+ ).filter((entry) => entry.display !== entry.id);
469
+
470
+ // Show session list
471
+ io.write('\nAvailable sessions:\n');
472
+ sessionEntries.forEach((entry, idx) => {
473
+ io.write(` ${idx + 1}. ${entry.display}\n`);
474
+ });
475
+ const answer = await io.readInput('Choose session number: ');
476
+ if (answer === null) break;
477
+ const idx = parseInt(answer.trim(), 10) - 1;
478
+ if (idx >= 0 && idx < sessionEntries.length) {
479
+ const entry = sessionEntries[idx];
480
+ const session = entry.session ?? (await store.load(entry.id));
481
+ io.write(
482
+ renderMarkdown(`\nResuming session: **${sessionDisplayName(session)}**\n`, {
483
+ isTTY: io.isTTY,
484
+ }),
485
+ );
486
+ await runWorkshop(session, client, io, store, opts, handlerConfig);
487
+ } else {
488
+ io.write('Invalid selection.\n');
489
+ }
490
+ break;
491
+ }
492
+ case 'exit':
493
+ io.write('Goodbye!\n');
494
+ break;
495
+ }
496
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Checkpoint State Derivation.
3
+ *
4
+ * Derives resume behavior from existing session state. This is a runtime-only
5
+ * convenience type — it is NOT persisted to the session.
6
+ *
7
+ * Contract: specs/004-dev-resume-hardening/contracts/cli.md
8
+ */
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+
12
+ import type { PocIteration, WorkshopSession } from '../shared/schemas/session.js';
13
+
14
+ // ── Types ────────────────────────────────────────────────────────────────────
15
+
16
+ export interface CheckpointState {
17
+ /** Whether a prior PoC run exists */
18
+ hasPriorRun: boolean;
19
+ /** Number of fully completed iterations (with testResults) */
20
+ completedIterations: number;
21
+ /** Whether the last iteration was interrupted (no testResults) */
22
+ lastIterationIncomplete: boolean;
23
+ /** The iteration number to resume from */
24
+ resumeFromIteration: number;
25
+ /** Whether scaffolding can be skipped (output dir + metadata exist) */
26
+ canSkipScaffold: boolean;
27
+ /** Final status from prior run, if any */
28
+ priorFinalStatus: 'success' | 'failed' | 'partial' | undefined;
29
+ /** Prior iterations for LLM context seeding */
30
+ priorIterations: PocIteration[];
31
+ }
32
+
33
+ // ── Derivation ───────────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * Derive the checkpoint state from an existing session and output directory.
37
+ *
38
+ * Used by `developCommand` and `RalphLoop` to decide resume behavior.
39
+ */
40
+ export function deriveCheckpointState(
41
+ session: WorkshopSession,
42
+ outputDir: string,
43
+ ): CheckpointState {
44
+ const poc = session.poc;
45
+
46
+ // No prior run
47
+ if (!poc || !Array.isArray(poc.iterations) || poc.iterations.length === 0) {
48
+ return {
49
+ hasPriorRun: false,
50
+ completedIterations: 0,
51
+ lastIterationIncomplete: false,
52
+ resumeFromIteration: 1,
53
+ canSkipScaffold: false,
54
+ priorFinalStatus: undefined,
55
+ priorIterations: [],
56
+ };
57
+ }
58
+
59
+ // Validate iteration entries — if any are corrupt, fall back to fresh run
60
+ if (!validateIterations(poc.iterations)) {
61
+ return {
62
+ hasPriorRun: false,
63
+ completedIterations: 0,
64
+ lastIterationIncomplete: false,
65
+ resumeFromIteration: 1,
66
+ canSkipScaffold: false,
67
+ priorFinalStatus: undefined,
68
+ priorIterations: [],
69
+ };
70
+ }
71
+
72
+ const lastIter = poc.iterations[poc.iterations.length - 1];
73
+ const lastIncomplete = !lastIter.testResults && lastIter.outcome !== 'scaffold';
74
+ const completedIters = lastIncomplete ? poc.iterations.slice(0, -1) : poc.iterations;
75
+
76
+ const metadataPath = join(outputDir, '.sofia-metadata.json');
77
+ const metadataExists = existsSync(metadataPath);
78
+
79
+ // Validate metadata integrity: sessionId must match
80
+ let canSkipScaffold = metadataExists;
81
+ if (metadataExists) {
82
+ try {
83
+ const raw = readFileSync(metadataPath, 'utf-8');
84
+ const metadata = JSON.parse(raw) as { sessionId?: string };
85
+ if (metadata.sessionId !== session.sessionId) {
86
+ canSkipScaffold = false;
87
+ }
88
+ } catch {
89
+ canSkipScaffold = false;
90
+ }
91
+ }
92
+
93
+ return {
94
+ hasPriorRun: true,
95
+ completedIterations: completedIters.length,
96
+ lastIterationIncomplete: lastIncomplete,
97
+ resumeFromIteration: completedIters.length + 1,
98
+ canSkipScaffold,
99
+ priorFinalStatus: poc.finalStatus,
100
+ priorIterations: completedIters,
101
+ };
102
+ }
103
+
104
+ // ── Validation ───────────────────────────────────────────────────────────────
105
+
106
+ /**
107
+ * Validate iteration entries have required fields and valid shapes.
108
+ * Returns false if any iteration is corrupt.
109
+ */
110
+ function validateIterations(iterations: PocIteration[]): boolean {
111
+ for (const iter of iterations) {
112
+ if (
113
+ typeof iter.iteration !== 'number' ||
114
+ typeof iter.startedAt !== 'string' ||
115
+ !iter.startedAt
116
+ ) {
117
+ return false;
118
+ }
119
+ }
120
+ return true;
121
+ }