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,1248 @@
1
+ /**
2
+ * T022: Unit tests for RalphLoop orchestrator.
3
+ *
4
+ * Verifies:
5
+ * - Lifecycle (validate → scaffold → install → iterate)
6
+ * - Termination on tests-passing
7
+ * - Termination on max-iterations
8
+ * - Iteration count tracking
9
+ * - Session persistence callback called after each iteration
10
+ * - Ctrl+C handling sets user-stopped
11
+ */
12
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
13
+ import { mkdtemp, rm } from 'node:fs/promises';
14
+ import { join } from 'node:path';
15
+ import { tmpdir } from 'node:os';
16
+ import { RalphLoop } from '../../../src/develop/ralphLoop.js';
17
+ import { PocScaffolder, validatePocOutput } from '../../../src/develop/pocScaffolder.js';
18
+ // ── Mocks ─────────────────────────────────────────────────────────────────────
19
+ // Mock npm install to always succeed
20
+ vi.mock('node:child_process', async (importOriginal) => {
21
+ const actual = await importOriginal();
22
+ return {
23
+ ...actual,
24
+ spawn: vi.fn((cmd, args) => {
25
+ if (cmd === 'npm' && args.includes('install')) {
26
+ // Simulate successful npm install
27
+ const emitter = {
28
+ stdout: { on: vi.fn() },
29
+ stderr: { on: vi.fn() },
30
+ on: vi.fn((event, cb) => {
31
+ if (event === 'close')
32
+ cb(0);
33
+ }),
34
+ kill: vi.fn(),
35
+ killed: false,
36
+ };
37
+ return emitter;
38
+ }
39
+ return actual.spawn(cmd, args);
40
+ }),
41
+ };
42
+ });
43
+ // Mock validatePocOutput — default: valid
44
+ vi.mock('../../../src/develop/pocScaffolder.js', async (importOriginal) => {
45
+ const actual = await importOriginal();
46
+ return {
47
+ ...actual,
48
+ validatePocOutput: vi.fn().mockResolvedValue({ valid: true, missingFiles: [], errors: [] }),
49
+ };
50
+ });
51
+ // ── Helpers ───────────────────────────────────────────────────────────────────
52
+ function makeSession(overrides) {
53
+ const now = new Date().toISOString();
54
+ return {
55
+ sessionId: 'ralph-test-session',
56
+ schemaVersion: '1.0.0',
57
+ createdAt: now,
58
+ updatedAt: now,
59
+ phase: 'Develop',
60
+ status: 'Active',
61
+ participants: [],
62
+ artifacts: { generatedFiles: [] },
63
+ ideas: [
64
+ {
65
+ id: 'idea-1',
66
+ title: 'Test AI App',
67
+ description: 'A test AI application.',
68
+ workflowStepIds: [],
69
+ },
70
+ ],
71
+ selection: {
72
+ ideaId: 'idea-1',
73
+ selectionRationale: 'Best idea',
74
+ confirmedByUser: true,
75
+ },
76
+ plan: {
77
+ milestones: [{ id: 'm1', title: 'Setup', items: [] }],
78
+ architectureNotes: 'Node.js + TypeScript',
79
+ },
80
+ ...overrides,
81
+ };
82
+ }
83
+ function makeIo() {
84
+ return {
85
+ write: vi.fn(),
86
+ writeActivity: vi.fn(),
87
+ writeToolSummary: vi.fn(),
88
+ readInput: vi.fn().mockResolvedValue(null),
89
+ showDecisionGate: vi.fn(),
90
+ isJsonMode: false,
91
+ isTTY: false,
92
+ };
93
+ }
94
+ function makePassingClient() {
95
+ return {
96
+ createSession: vi.fn().mockResolvedValue({
97
+ send: vi.fn().mockReturnValue({
98
+ async *[Symbol.asyncIterator]() {
99
+ yield {
100
+ type: 'TextDelta',
101
+ text: '```typescript file=src/index.ts\nexport function main() { return "ok"; }\n```',
102
+ timestamp: '',
103
+ };
104
+ },
105
+ }),
106
+ getHistory: () => [],
107
+ }),
108
+ };
109
+ }
110
+ function makePassingTestRunner() {
111
+ return {
112
+ run: vi.fn().mockResolvedValue({
113
+ passed: 2,
114
+ failed: 0,
115
+ skipped: 0,
116
+ total: 2,
117
+ durationMs: 300,
118
+ failures: [],
119
+ rawOutput: 'All tests pass',
120
+ }),
121
+ };
122
+ }
123
+ function _makeFailingTestRunner(failCount = 1) {
124
+ let callCount = 0;
125
+ return {
126
+ run: vi.fn().mockImplementation(async () => {
127
+ callCount++;
128
+ if (callCount > failCount) {
129
+ return {
130
+ passed: 2,
131
+ failed: 0,
132
+ skipped: 0,
133
+ total: 2,
134
+ durationMs: 300,
135
+ failures: [],
136
+ rawOutput: 'All pass',
137
+ };
138
+ }
139
+ return {
140
+ passed: 0,
141
+ failed: 1,
142
+ skipped: 0,
143
+ total: 1,
144
+ durationMs: 400,
145
+ failures: [{ testName: 'suite > test A', message: 'Expected 1 but got 0' }],
146
+ rawOutput: 'FAIL tests/index.test.ts',
147
+ };
148
+ }),
149
+ };
150
+ }
151
+ function makeAlwaysFailingTestRunner() {
152
+ return {
153
+ run: vi.fn().mockResolvedValue({
154
+ passed: 0,
155
+ failed: 1,
156
+ skipped: 0,
157
+ total: 1,
158
+ durationMs: 400,
159
+ failures: [{ testName: 'suite > always fail', message: 'always fails' }],
160
+ rawOutput: 'FAIL',
161
+ }),
162
+ };
163
+ }
164
+ function makeFakeScaffolder(outputDir) {
165
+ return {
166
+ scaffold: vi.fn().mockImplementation(async () => {
167
+ // Create minimal required files
168
+ const { writeFile, mkdir } = await import('node:fs/promises');
169
+ await mkdir(join(outputDir, 'src'), { recursive: true });
170
+ await mkdir(join(outputDir, 'tests'), { recursive: true });
171
+ await writeFile(join(outputDir, 'package.json'), JSON.stringify({
172
+ name: 'test-poc',
173
+ scripts: { test: 'vitest run' },
174
+ dependencies: {},
175
+ devDependencies: {},
176
+ }), 'utf-8');
177
+ await writeFile(join(outputDir, 'src', 'index.ts'), 'export function main() {}', 'utf-8');
178
+ return {
179
+ createdFiles: ['package.json', 'src/index.ts'],
180
+ skippedFiles: [],
181
+ context: {
182
+ projectName: 'test-poc',
183
+ ideaTitle: 'Test',
184
+ ideaDescription: 'Test',
185
+ techStack: { language: 'TypeScript', runtime: 'Node.js 20', testRunner: 'npm test' },
186
+ planSummary: 'Test',
187
+ sessionId: 'ralph-test-session',
188
+ outputDir,
189
+ },
190
+ };
191
+ }),
192
+ getTemplateFiles: () => ['package.json', 'src/index.ts'],
193
+ };
194
+ }
195
+ // ── Tests ─────────────────────────────────────────────────────────────────────
196
+ describe('RalphLoop', () => {
197
+ let tmpDir;
198
+ beforeEach(async () => {
199
+ tmpDir = await mkdtemp(join(tmpdir(), 'sofia-ralph-test-'));
200
+ });
201
+ afterEach(async () => {
202
+ await rm(tmpDir, { recursive: true, force: true });
203
+ vi.clearAllMocks();
204
+ });
205
+ describe('validation', () => {
206
+ it('throws when session has no selection', async () => {
207
+ const session = makeSession({ selection: undefined });
208
+ const io = makeIo();
209
+ const client = makePassingClient();
210
+ const ralph = new RalphLoop({
211
+ client,
212
+ io,
213
+ session,
214
+ outputDir: tmpDir,
215
+ maxIterations: 1,
216
+ });
217
+ await expect(ralph.run()).rejects.toThrow(/selection/i);
218
+ });
219
+ it('throws when session has no plan', async () => {
220
+ const session = makeSession({ plan: undefined });
221
+ const io = makeIo();
222
+ const client = makePassingClient();
223
+ const ralph = new RalphLoop({
224
+ client,
225
+ io,
226
+ session,
227
+ outputDir: tmpDir,
228
+ maxIterations: 1,
229
+ });
230
+ await expect(ralph.run()).rejects.toThrow(/plan/i);
231
+ });
232
+ });
233
+ describe('lifecycle with passing tests', () => {
234
+ it('terminates with tests-passing when all tests pass immediately', async () => {
235
+ const session = makeSession();
236
+ const io = makeIo();
237
+ const client = makePassingClient();
238
+ const testRunner = makePassingTestRunner();
239
+ const scaffolder = makeFakeScaffolder(tmpDir);
240
+ const ralph = new RalphLoop({
241
+ client,
242
+ io,
243
+ session,
244
+ outputDir: tmpDir,
245
+ maxIterations: 5,
246
+ testRunner,
247
+ scaffolder,
248
+ });
249
+ const result = await ralph.run();
250
+ expect(result.finalStatus).toBe('success');
251
+ expect(result.terminationReason).toBe('tests-passing');
252
+ });
253
+ it('tracks iteration count', async () => {
254
+ const session = makeSession();
255
+ const io = makeIo();
256
+ const client = makePassingClient();
257
+ const testRunner = makePassingTestRunner();
258
+ const scaffolder = makeFakeScaffolder(tmpDir);
259
+ const ralph = new RalphLoop({
260
+ client,
261
+ io,
262
+ session,
263
+ outputDir: tmpDir,
264
+ maxIterations: 5,
265
+ testRunner,
266
+ scaffolder,
267
+ });
268
+ const result = await ralph.run();
269
+ // Iteration 1 (scaffold) + Iteration 2 (test run, passing)
270
+ expect(result.iterationsCompleted).toBeGreaterThanOrEqual(2);
271
+ });
272
+ it('calls onSessionUpdate after each iteration', async () => {
273
+ const session = makeSession();
274
+ const io = makeIo();
275
+ const client = makePassingClient();
276
+ const testRunner = makePassingTestRunner();
277
+ const scaffolder = makeFakeScaffolder(tmpDir);
278
+ const onSessionUpdate = vi.fn().mockResolvedValue(undefined);
279
+ const ralph = new RalphLoop({
280
+ client,
281
+ io,
282
+ session,
283
+ outputDir: tmpDir,
284
+ maxIterations: 3,
285
+ testRunner,
286
+ scaffolder,
287
+ onSessionUpdate,
288
+ });
289
+ await ralph.run();
290
+ // Should be called at least once (after scaffold, after passing tests)
291
+ expect(onSessionUpdate).toHaveBeenCalled();
292
+ });
293
+ it('returns updated session with poc state', async () => {
294
+ const session = makeSession();
295
+ const io = makeIo();
296
+ const client = makePassingClient();
297
+ const testRunner = makePassingTestRunner();
298
+ const scaffolder = makeFakeScaffolder(tmpDir);
299
+ const ralph = new RalphLoop({
300
+ client,
301
+ io,
302
+ session,
303
+ outputDir: tmpDir,
304
+ maxIterations: 5,
305
+ testRunner,
306
+ scaffolder,
307
+ });
308
+ const result = await ralph.run();
309
+ expect(result.session.poc).toBeDefined();
310
+ expect(result.session.poc.iterations.length).toBeGreaterThan(0);
311
+ expect(result.session.poc.iterations[0].outcome).toBe('scaffold');
312
+ expect(result.session.poc.finalStatus).toBe('success');
313
+ });
314
+ });
315
+ describe('termination on max-iterations', () => {
316
+ it('terminates with max-iterations when all tests keep failing', async () => {
317
+ const session = makeSession();
318
+ const io = makeIo();
319
+ const client = makePassingClient();
320
+ const testRunner = makeAlwaysFailingTestRunner();
321
+ const scaffolder = makeFakeScaffolder(tmpDir);
322
+ const ralph = new RalphLoop({
323
+ client,
324
+ io,
325
+ session,
326
+ outputDir: tmpDir,
327
+ maxIterations: 3,
328
+ testRunner,
329
+ scaffolder,
330
+ });
331
+ const result = await ralph.run();
332
+ expect(result.terminationReason).toBe('max-iterations');
333
+ expect(result.iterationsCompleted).toBe(3); // 1 scaffold + 2 test iterations
334
+ });
335
+ it('sets finalStatus=partial when some tests pass at max-iterations', async () => {
336
+ const session = makeSession();
337
+ const io = makeIo();
338
+ const client = makePassingClient();
339
+ // Partially passing test runner
340
+ const testRunner = {
341
+ run: vi.fn().mockResolvedValue({
342
+ passed: 1,
343
+ failed: 1,
344
+ skipped: 0,
345
+ total: 2,
346
+ durationMs: 400,
347
+ failures: [{ testName: 'test B', message: 'fails' }],
348
+ rawOutput: '',
349
+ }),
350
+ };
351
+ const scaffolder = makeFakeScaffolder(tmpDir);
352
+ const ralph = new RalphLoop({
353
+ client,
354
+ io,
355
+ session,
356
+ outputDir: tmpDir,
357
+ maxIterations: 2,
358
+ testRunner,
359
+ scaffolder,
360
+ });
361
+ const result = await ralph.run();
362
+ expect(result.terminationReason).toBe('max-iterations');
363
+ expect(result.finalStatus).toBe('partial');
364
+ });
365
+ it('sets finalStatus=failed when no tests pass at max-iterations', async () => {
366
+ const session = makeSession();
367
+ const io = makeIo();
368
+ const client = makePassingClient();
369
+ const testRunner = makeAlwaysFailingTestRunner();
370
+ const scaffolder = makeFakeScaffolder(tmpDir);
371
+ const ralph = new RalphLoop({
372
+ client,
373
+ io,
374
+ session,
375
+ outputDir: tmpDir,
376
+ maxIterations: 2,
377
+ testRunner,
378
+ scaffolder,
379
+ });
380
+ const result = await ralph.run();
381
+ expect(result.terminationReason).toBe('max-iterations');
382
+ expect(result.finalStatus).toBe('failed');
383
+ });
384
+ it('leaves session.poc.finalStatus unset when user stops (Ctrl+C)', async () => {
385
+ const session = makeSession();
386
+ const io = makeIo();
387
+ const testRunner = makeAlwaysFailingTestRunner();
388
+ const scaffolder = makeFakeScaffolder(tmpDir);
389
+ // Client that emits SIGINT mid-generation to simulate Ctrl+C
390
+ const client = {
391
+ createSession: vi.fn().mockResolvedValue({
392
+ send: vi.fn().mockReturnValue({
393
+ async *[Symbol.asyncIterator]() {
394
+ process.emit('SIGINT');
395
+ yield {
396
+ type: 'TextDelta',
397
+ text: '```typescript file=src/index.ts\nexport function main() { return "ok"; }\n```',
398
+ timestamp: '',
399
+ };
400
+ },
401
+ }),
402
+ getHistory: () => [],
403
+ }),
404
+ };
405
+ const sessionUpdates = [];
406
+ const ralph = new RalphLoop({
407
+ client,
408
+ io,
409
+ session,
410
+ outputDir: tmpDir,
411
+ maxIterations: 5,
412
+ testRunner,
413
+ scaffolder,
414
+ onSessionUpdate: async (s) => {
415
+ sessionUpdates.push(s);
416
+ },
417
+ });
418
+ const result = await ralph.run();
419
+ expect(result.terminationReason).toBe('user-stopped');
420
+ // Contract: finalStatus must NOT be persisted to the session on user abort
421
+ expect(result.session.poc?.finalStatus).toBeUndefined();
422
+ // The persisted session updates should also not have finalStatus set
423
+ const lastUpdate = sessionUpdates[sessionUpdates.length - 1];
424
+ expect(lastUpdate?.poc?.finalStatus).toBeUndefined();
425
+ });
426
+ });
427
+ describe('event emission', () => {
428
+ it('emits events during loop lifecycle', async () => {
429
+ const session = makeSession();
430
+ const io = makeIo();
431
+ const client = makePassingClient();
432
+ const testRunner = makePassingTestRunner();
433
+ const scaffolder = makeFakeScaffolder(tmpDir);
434
+ const events = [];
435
+ const ralph = new RalphLoop({
436
+ client,
437
+ io,
438
+ session,
439
+ outputDir: tmpDir,
440
+ maxIterations: 3,
441
+ testRunner,
442
+ scaffolder,
443
+ onEvent: (e) => events.push(e.type),
444
+ });
445
+ await ralph.run();
446
+ expect(events).toContain('Activity');
447
+ });
448
+ });
449
+ describe('output directory', () => {
450
+ it('returns outputDir in result', async () => {
451
+ const session = makeSession();
452
+ const io = makeIo();
453
+ const client = makePassingClient();
454
+ const testRunner = makePassingTestRunner();
455
+ const scaffolder = makeFakeScaffolder(tmpDir);
456
+ const ralph = new RalphLoop({
457
+ client,
458
+ io,
459
+ session,
460
+ outputDir: tmpDir,
461
+ maxIterations: 3,
462
+ testRunner,
463
+ scaffolder,
464
+ });
465
+ const result = await ralph.run();
466
+ expect(result.outputDir).toBe(tmpDir);
467
+ });
468
+ });
469
+ describe('final test run after max iterations (F006)', () => {
470
+ it('runs a final test after the last LLM iteration and returns success when tests pass', async () => {
471
+ const session = makeSession();
472
+ const io = makeIo();
473
+ const client = makePassingClient();
474
+ const scaffolder = makeFakeScaffolder(tmpDir);
475
+ // First test run fails, second (final run after loop) passes
476
+ let runCount = 0;
477
+ const testRunner = {
478
+ run: vi.fn().mockImplementation(async () => {
479
+ runCount++;
480
+ if (runCount <= 1) {
481
+ return {
482
+ passed: 0,
483
+ failed: 1,
484
+ skipped: 0,
485
+ total: 1,
486
+ durationMs: 100,
487
+ failures: [{ testName: 'test A', message: 'fails' }],
488
+ rawOutput: 'FAIL',
489
+ };
490
+ }
491
+ return {
492
+ passed: 1,
493
+ failed: 0,
494
+ skipped: 0,
495
+ total: 1,
496
+ durationMs: 100,
497
+ failures: [],
498
+ rawOutput: 'PASS',
499
+ };
500
+ }),
501
+ };
502
+ const ralph = new RalphLoop({
503
+ client,
504
+ io,
505
+ session,
506
+ outputDir: tmpDir,
507
+ maxIterations: 2, // scaffold + 1 iterate = 2 iterations, then final test
508
+ testRunner,
509
+ scaffolder,
510
+ });
511
+ const result = await ralph.run();
512
+ // The final test run detected the fix, so status should be success
513
+ expect(result.finalStatus).toBe('success');
514
+ expect(result.terminationReason).toBe('tests-passing');
515
+ });
516
+ });
517
+ describe('SIGINT handler stale session (F009)', () => {
518
+ it('persists latest session with iteration data when SIGINT fires after iterations', async () => {
519
+ const session = makeSession();
520
+ const io = makeIo();
521
+ const client = makePassingClient();
522
+ const scaffolder = makeFakeScaffolder(tmpDir);
523
+ let persistedSession = null;
524
+ // Slow test runner: yields after first call so SIGINT can fire
525
+ let runCount = 0;
526
+ const testRunner = {
527
+ run: vi.fn().mockImplementation(async () => {
528
+ runCount++;
529
+ // After first iteration completes, delay so SIGINT can fire
530
+ if (runCount >= 2) {
531
+ await new Promise((resolve) => setTimeout(resolve, 500));
532
+ }
533
+ return {
534
+ passed: 0,
535
+ failed: 1,
536
+ skipped: 0,
537
+ total: 1,
538
+ durationMs: 100,
539
+ failures: [{ testName: 'test A', message: 'fails' }],
540
+ rawOutput: 'FAIL',
541
+ };
542
+ }),
543
+ };
544
+ const onSessionUpdate = vi.fn().mockImplementation(async (s) => {
545
+ persistedSession = s;
546
+ });
547
+ const ralph = new RalphLoop({
548
+ client,
549
+ io,
550
+ session,
551
+ outputDir: tmpDir,
552
+ maxIterations: 10,
553
+ testRunner,
554
+ scaffolder,
555
+ onSessionUpdate,
556
+ });
557
+ // Start the loop, then fire SIGINT after enough time for first iteration
558
+ const runPromise = ralph.run();
559
+ // Wait for at least scaffold + first test run iteration
560
+ await new Promise((resolve) => setTimeout(resolve, 300));
561
+ process.emit('SIGINT', 'SIGINT');
562
+ const result = await runPromise;
563
+ expect(result.terminationReason).toBe('user-stopped');
564
+ // The persisted session should have iteration data from completed iterations
565
+ expect(persistedSession).not.toBeNull();
566
+ expect(persistedSession.poc).toBeDefined();
567
+ expect(persistedSession.poc.iterations.length).toBeGreaterThanOrEqual(1);
568
+ });
569
+ });
570
+ describe('user-stopped status (F011)', () => {
571
+ it('returns finalStatus=partial when user stops and some tests were passing', async () => {
572
+ const session = makeSession();
573
+ const io = makeIo();
574
+ const client = makePassingClient();
575
+ const scaffolder = makeFakeScaffolder(tmpDir);
576
+ // Partially passing test runner that delays so SIGINT can fire
577
+ let runCount = 0;
578
+ const testRunner = {
579
+ run: vi.fn().mockImplementation(async () => {
580
+ runCount++;
581
+ if (runCount >= 2) {
582
+ await new Promise((resolve) => setTimeout(resolve, 500));
583
+ }
584
+ return {
585
+ passed: 1,
586
+ failed: 1,
587
+ skipped: 0,
588
+ total: 2,
589
+ durationMs: 100,
590
+ failures: [{ testName: 'test B', message: 'fails' }],
591
+ rawOutput: 'PARTIAL',
592
+ };
593
+ }),
594
+ };
595
+ const ralph = new RalphLoop({
596
+ client,
597
+ io,
598
+ session,
599
+ outputDir: tmpDir,
600
+ maxIterations: 10,
601
+ testRunner,
602
+ scaffolder,
603
+ });
604
+ const runPromise = ralph.run();
605
+ await new Promise((resolve) => setTimeout(resolve, 300));
606
+ process.emit('SIGINT', 'SIGINT');
607
+ const result = await runPromise;
608
+ expect(result.terminationReason).toBe('user-stopped');
609
+ expect(result.finalStatus).toBe('partial');
610
+ });
611
+ });
612
+ // SKIPPED: Auto-push to GitHub removed per user safety requirements
613
+ // sofIA now initializes git locally only — users push manually
614
+ describe.skip('GitHub MCP adapter integration', () => {
615
+ it('reads written files from disk and passes their content to pushFiles', async () => {
616
+ const session = makeSession();
617
+ const io = makeIo();
618
+ // LLM returns a file with known content
619
+ const knownContent = 'export function main() { return "hello"; }\n';
620
+ const client = {
621
+ createSession: vi.fn().mockResolvedValue({
622
+ send: vi.fn().mockReturnValue({
623
+ async *[Symbol.asyncIterator]() {
624
+ yield {
625
+ type: 'TextDelta',
626
+ text: `\`\`\`typescript file=src/index.ts\n${knownContent}\`\`\``,
627
+ timestamp: '',
628
+ };
629
+ },
630
+ }),
631
+ getHistory: () => [],
632
+ }),
633
+ };
634
+ // Fail on first run so the LLM turn (and pushFiles) is reached; pass on second
635
+ let runCount = 0;
636
+ const testRunner = {
637
+ run: vi.fn().mockImplementation(async () => {
638
+ runCount++;
639
+ if (runCount === 1) {
640
+ return {
641
+ passed: 0,
642
+ failed: 1,
643
+ skipped: 0,
644
+ total: 1,
645
+ durationMs: 100,
646
+ failures: [{ testName: 'main > works', message: 'not implemented' }],
647
+ rawOutput: 'FAIL',
648
+ };
649
+ }
650
+ return {
651
+ passed: 1,
652
+ failed: 0,
653
+ skipped: 0,
654
+ total: 1,
655
+ durationMs: 100,
656
+ failures: [],
657
+ rawOutput: 'OK',
658
+ };
659
+ }),
660
+ };
661
+ const scaffolder = makeFakeScaffolder(tmpDir);
662
+ // Create a mock GitHub adapter that captures pushFiles calls
663
+ const pushFilesMock = vi.fn().mockResolvedValue({ available: true, commitSha: 'abc123' });
664
+ const _githubAdapter = {
665
+ isAvailable: () => true,
666
+ getRepoUrl: () => 'https://github.com/acme/poc-test',
667
+ pushFiles: pushFilesMock,
668
+ createRepository: vi.fn().mockResolvedValue({
669
+ available: true,
670
+ repoUrl: 'https://github.com/acme/poc-test',
671
+ repoName: 'poc-test',
672
+ }),
673
+ };
674
+ const ralph = new RalphLoop({
675
+ client,
676
+ io,
677
+ session,
678
+ outputDir: tmpDir,
679
+ maxIterations: 5,
680
+ testRunner,
681
+ scaffolder,
682
+ });
683
+ await ralph.run();
684
+ // pushFiles should have been called at least twice:
685
+ // call[0] = scaffold push, call[1+] = iteration pushes
686
+ expect(pushFilesMock.mock.calls.length).toBeGreaterThanOrEqual(2);
687
+ // The iteration push (call[1]) should contain the LLM-written file content
688
+ const callArgs = pushFilesMock.mock.calls[1][0];
689
+ const pushedFile = callArgs.files.find((f) => f.path === 'src/index.ts');
690
+ expect(pushedFile).toBeDefined();
691
+ expect(pushedFile.content).toBe(knownContent);
692
+ expect(pushedFile.content).not.toBe('');
693
+ });
694
+ });
695
+ describe('validatePocOutput integration (F027)', () => {
696
+ it('downgrades success to partial when validatePocOutput reports missing files', async () => {
697
+ // Mock validatePocOutput to fail
698
+ vi.mocked(validatePocOutput).mockResolvedValueOnce({
699
+ valid: false,
700
+ missingFiles: ['README.md'],
701
+ errors: [],
702
+ });
703
+ const session = makeSession();
704
+ const io = makeIo();
705
+ const testRunner = {
706
+ run: vi.fn().mockResolvedValue({
707
+ passed: 3,
708
+ failed: 0,
709
+ skipped: 0,
710
+ total: 3,
711
+ durationMs: 100,
712
+ failures: [],
713
+ rawOutput: 'ALL PASS',
714
+ }),
715
+ };
716
+ const ralph = new RalphLoop({
717
+ client: makePassingClient(),
718
+ io,
719
+ session,
720
+ outputDir: tmpDir,
721
+ maxIterations: 5,
722
+ testRunner,
723
+ scaffolder: makeFakeScaffolder(tmpDir),
724
+ });
725
+ const result = await ralph.run();
726
+ expect(result.finalStatus).toBe('partial');
727
+ expect(result.terminationReason).toBe('tests-passing');
728
+ // validatePocOutput should have been called
729
+ expect(validatePocOutput).toHaveBeenCalledWith(tmpDir);
730
+ });
731
+ it('keeps success when validatePocOutput reports valid', async () => {
732
+ vi.mocked(validatePocOutput).mockResolvedValueOnce({
733
+ valid: true,
734
+ missingFiles: [],
735
+ errors: [],
736
+ });
737
+ const session = makeSession();
738
+ const io = makeIo();
739
+ const testRunner = {
740
+ run: vi.fn().mockResolvedValue({
741
+ passed: 3,
742
+ failed: 0,
743
+ skipped: 0,
744
+ total: 3,
745
+ durationMs: 100,
746
+ failures: [],
747
+ rawOutput: 'ALL PASS',
748
+ }),
749
+ };
750
+ const ralph = new RalphLoop({
751
+ client: makePassingClient(),
752
+ io,
753
+ session,
754
+ outputDir: tmpDir,
755
+ maxIterations: 5,
756
+ testRunner,
757
+ scaffolder: makeFakeScaffolder(tmpDir),
758
+ });
759
+ const result = await ralph.run();
760
+ expect(result.finalStatus).toBe('success');
761
+ expect(validatePocOutput).toHaveBeenCalledWith(tmpDir);
762
+ });
763
+ });
764
+ // ── Resume iteration seeding (T013) ─────────────────────────────────────
765
+ describe('resume iteration seeding', () => {
766
+ it('seeds iterations from session.poc.iterations and starts from correct iterNum', async () => {
767
+ const io = makeIo();
768
+ const testRunner = makePassingTestRunner();
769
+ const session = makeSession({
770
+ poc: {
771
+ repoSource: 'local',
772
+ iterations: [
773
+ {
774
+ iteration: 1,
775
+ startedAt: new Date().toISOString(),
776
+ endedAt: new Date().toISOString(),
777
+ outcome: 'scaffold',
778
+ filesChanged: ['package.json'],
779
+ },
780
+ {
781
+ iteration: 2,
782
+ startedAt: new Date().toISOString(),
783
+ endedAt: new Date().toISOString(),
784
+ outcome: 'tests-failing',
785
+ filesChanged: ['src/index.ts'],
786
+ testResults: {
787
+ passed: 1,
788
+ failed: 1,
789
+ skipped: 0,
790
+ total: 2,
791
+ durationMs: 100,
792
+ failures: [{ testName: 'test1', message: 'fail' }],
793
+ },
794
+ },
795
+ ],
796
+ },
797
+ });
798
+ const ralph = new RalphLoop({
799
+ client: makePassingClient(),
800
+ io,
801
+ session,
802
+ outputDir: tmpDir,
803
+ maxIterations: 10,
804
+ testRunner,
805
+ scaffolder: makeFakeScaffolder(tmpDir),
806
+ checkpoint: {
807
+ hasPriorRun: true,
808
+ completedIterations: 2,
809
+ lastIterationIncomplete: false,
810
+ resumeFromIteration: 3,
811
+ canSkipScaffold: false,
812
+ priorFinalStatus: undefined,
813
+ priorIterations: session.poc.iterations,
814
+ },
815
+ });
816
+ const result = await ralph.run();
817
+ // Iterations should include the 2 prior ones + scaffold (no skip) + tests-passing
818
+ expect(result.iterationsCompleted).toBeGreaterThanOrEqual(3);
819
+ expect(result.finalStatus).toBe('success');
820
+ });
821
+ it('skips scaffold when checkpoint says canSkipScaffold=true (T014)', async () => {
822
+ const io = makeIo();
823
+ const testRunner = makePassingTestRunner();
824
+ const scaffolder = makeFakeScaffolder(tmpDir);
825
+ const session = makeSession({
826
+ poc: {
827
+ repoSource: 'local',
828
+ iterations: [
829
+ {
830
+ iteration: 1,
831
+ startedAt: new Date().toISOString(),
832
+ endedAt: new Date().toISOString(),
833
+ outcome: 'scaffold',
834
+ filesChanged: [],
835
+ },
836
+ ],
837
+ },
838
+ });
839
+ const ralph = new RalphLoop({
840
+ client: makePassingClient(),
841
+ io,
842
+ session,
843
+ outputDir: tmpDir,
844
+ maxIterations: 10,
845
+ testRunner,
846
+ scaffolder,
847
+ checkpoint: {
848
+ hasPriorRun: true,
849
+ completedIterations: 1,
850
+ lastIterationIncomplete: false,
851
+ resumeFromIteration: 2,
852
+ canSkipScaffold: true,
853
+ priorFinalStatus: undefined,
854
+ priorIterations: session.poc.iterations,
855
+ },
856
+ });
857
+ await ralph.run();
858
+ // Scaffold should NOT have been called — it was skipped
859
+ expect(scaffolder.scaffold).not.toHaveBeenCalled();
860
+ // Should log that scaffold was skipped
861
+ expect(io.writeActivity).toHaveBeenCalledWith(expect.stringContaining('Skipping scaffold'));
862
+ });
863
+ it('pops incomplete last iteration and re-runs it (T015, FR-001a)', async () => {
864
+ const io = makeIo();
865
+ const testRunner = makePassingTestRunner();
866
+ const incompleteIter = {
867
+ iteration: 2,
868
+ startedAt: new Date().toISOString(),
869
+ outcome: 'tests-failing',
870
+ filesChanged: [],
871
+ // No testResults — incomplete
872
+ };
873
+ const session = makeSession({
874
+ poc: {
875
+ repoSource: 'local',
876
+ iterations: [
877
+ {
878
+ iteration: 1,
879
+ startedAt: new Date().toISOString(),
880
+ endedAt: new Date().toISOString(),
881
+ outcome: 'scaffold',
882
+ filesChanged: [],
883
+ },
884
+ incompleteIter,
885
+ ],
886
+ },
887
+ });
888
+ const ralph = new RalphLoop({
889
+ client: makePassingClient(),
890
+ io,
891
+ session,
892
+ outputDir: tmpDir,
893
+ maxIterations: 10,
894
+ testRunner,
895
+ scaffolder: makeFakeScaffolder(tmpDir),
896
+ checkpoint: {
897
+ hasPriorRun: true,
898
+ completedIterations: 1,
899
+ lastIterationIncomplete: true,
900
+ resumeFromIteration: 2,
901
+ canSkipScaffold: false,
902
+ priorFinalStatus: undefined,
903
+ priorIterations: [session.poc.iterations[0]], // Only completed iters
904
+ },
905
+ });
906
+ const result = await ralph.run();
907
+ // Should log about re-running incomplete iteration
908
+ expect(io.writeActivity).toHaveBeenCalledWith(expect.stringContaining('Re-running incomplete iteration'));
909
+ expect(result.finalStatus).toBe('success');
910
+ });
911
+ it('re-scaffolds when output directory is missing but iterations exist (T018, FR-007)', async () => {
912
+ const io = makeIo();
913
+ const testRunner = makePassingTestRunner();
914
+ const scaffolder = makeFakeScaffolder(tmpDir);
915
+ const session = makeSession({
916
+ poc: {
917
+ repoSource: 'local',
918
+ iterations: [
919
+ {
920
+ iteration: 1,
921
+ startedAt: new Date().toISOString(),
922
+ endedAt: new Date().toISOString(),
923
+ outcome: 'scaffold',
924
+ filesChanged: [],
925
+ },
926
+ ],
927
+ },
928
+ });
929
+ const ralph = new RalphLoop({
930
+ client: makePassingClient(),
931
+ io,
932
+ session,
933
+ outputDir: tmpDir,
934
+ maxIterations: 10,
935
+ testRunner,
936
+ scaffolder,
937
+ checkpoint: {
938
+ hasPriorRun: true,
939
+ completedIterations: 1,
940
+ lastIterationIncomplete: false,
941
+ resumeFromIteration: 2,
942
+ canSkipScaffold: false, // Output dir missing
943
+ priorFinalStatus: undefined,
944
+ priorIterations: session.poc.iterations,
945
+ },
946
+ });
947
+ await ralph.run();
948
+ // Scaffold SHOULD have been called since canSkipScaffold is false
949
+ expect(scaffolder.scaffold).toHaveBeenCalled();
950
+ expect(io.writeActivity).toHaveBeenCalledWith(expect.stringContaining('re-scaffolding'));
951
+ });
952
+ });
953
+ // SKIPPED: Auto-push to GitHub removed per user safety requirements
954
+ // sofIA now initializes git locally only — users push manually
955
+ describe.skip('post-scaffold push (T024 — US2)', () => {
956
+ it('pushes scaffold files to GitHub after npm install, before first test iteration', async () => {
957
+ const session = makeSession();
958
+ const io = makeIo();
959
+ const client = makePassingClient();
960
+ const testRunner = makePassingTestRunner();
961
+ const scaffolder = makeFakeScaffolder(tmpDir);
962
+ const pushOrder = [];
963
+ const pushFilesMock = vi.fn().mockImplementation(async () => {
964
+ pushOrder.push('pushFiles');
965
+ return { available: true, commitSha: 'scaffold-sha' };
966
+ });
967
+ const _githubAdapter = {
968
+ isAvailable: () => true,
969
+ getRepoUrl: () => 'https://github.com/acme/poc-test',
970
+ pushFiles: pushFilesMock,
971
+ createRepository: vi.fn().mockResolvedValue({
972
+ available: true,
973
+ repoUrl: 'https://github.com/acme/poc-test',
974
+ repoName: 'poc-test',
975
+ }),
976
+ };
977
+ // Wrap testRunner.run to record ordering
978
+ const originalRun = testRunner.run;
979
+ testRunner.run = vi
980
+ .fn()
981
+ .mockImplementation(async (...args) => {
982
+ pushOrder.push('testRun');
983
+ return originalRun(...args);
984
+ });
985
+ const ralph = new RalphLoop({
986
+ client,
987
+ io,
988
+ session,
989
+ outputDir: tmpDir,
990
+ maxIterations: 3,
991
+ testRunner,
992
+ scaffolder,
993
+ });
994
+ await ralph.run();
995
+ // pushFiles should have been called at least once for the scaffold
996
+ expect(pushFilesMock).toHaveBeenCalled();
997
+ const firstPushArgs = pushFilesMock.mock.calls[0][0];
998
+ // Should push the scaffold files ('package.json', 'src/index.ts')
999
+ expect(firstPushArgs.files.length).toBeGreaterThanOrEqual(1);
1000
+ const packageJson = firstPushArgs.files.find((f) => f.path === 'package.json');
1001
+ expect(packageJson).toBeDefined();
1002
+ expect(packageJson.content).not.toBe('');
1003
+ const indexTs = firstPushArgs.files.find((f) => f.path === 'src/index.ts');
1004
+ expect(indexTs).toBeDefined();
1005
+ expect(indexTs.content).not.toBe('');
1006
+ // Commit message should indicate scaffold
1007
+ expect(firstPushArgs.commitMessage).toContain('scaffold');
1008
+ // The scaffold push should come BEFORE the first test run
1009
+ const firstPush = pushOrder.indexOf('pushFiles');
1010
+ const firstTest = pushOrder.indexOf('testRun');
1011
+ expect(firstPush).toBeLessThan(firstTest);
1012
+ });
1013
+ it('always re-runs dependency install even when scaffolding is skipped (T065, FR-003)', async () => {
1014
+ const io = makeIo();
1015
+ const testRunner = makePassingTestRunner();
1016
+ const scaffolder = makeFakeScaffolder(tmpDir);
1017
+ const session = makeSession({
1018
+ poc: {
1019
+ repoSource: 'local',
1020
+ iterations: [
1021
+ {
1022
+ iteration: 1,
1023
+ startedAt: new Date().toISOString(),
1024
+ endedAt: new Date().toISOString(),
1025
+ outcome: 'scaffold',
1026
+ filesChanged: [],
1027
+ },
1028
+ ],
1029
+ },
1030
+ });
1031
+ const ralph = new RalphLoop({
1032
+ client: makePassingClient(),
1033
+ io,
1034
+ session,
1035
+ outputDir: tmpDir,
1036
+ maxIterations: 10,
1037
+ testRunner,
1038
+ scaffolder,
1039
+ checkpoint: {
1040
+ hasPriorRun: true,
1041
+ completedIterations: 1,
1042
+ lastIterationIncomplete: false,
1043
+ resumeFromIteration: 2,
1044
+ canSkipScaffold: true,
1045
+ priorFinalStatus: undefined,
1046
+ priorIterations: session.poc.iterations,
1047
+ },
1048
+ });
1049
+ await ralph.run();
1050
+ // Scaffold should be skipped
1051
+ expect(scaffolder.scaffold).not.toHaveBeenCalled();
1052
+ // But install should still run
1053
+ expect(io.writeActivity).toHaveBeenCalledWith(expect.stringContaining('Re-running dependency installation'));
1054
+ });
1055
+ it('includes prior iteration history in LLM prompt context (T066, FR-004)', async () => {
1056
+ const io = makeIo();
1057
+ const testRunner = makeAlwaysFailingTestRunner();
1058
+ const priorIters = [
1059
+ {
1060
+ iteration: 1,
1061
+ startedAt: new Date().toISOString(),
1062
+ endedAt: new Date().toISOString(),
1063
+ outcome: 'scaffold',
1064
+ filesChanged: ['package.json'],
1065
+ },
1066
+ {
1067
+ iteration: 2,
1068
+ startedAt: new Date().toISOString(),
1069
+ endedAt: new Date().toISOString(),
1070
+ outcome: 'tests-failing',
1071
+ filesChanged: ['src/index.ts'],
1072
+ testResults: {
1073
+ passed: 1,
1074
+ failed: 1,
1075
+ skipped: 0,
1076
+ total: 2,
1077
+ durationMs: 100,
1078
+ failures: [{ testName: 'test1', message: 'fail' }],
1079
+ },
1080
+ },
1081
+ ];
1082
+ const session = makeSession({
1083
+ poc: {
1084
+ repoSource: 'local',
1085
+ iterations: priorIters,
1086
+ },
1087
+ });
1088
+ // Track LLM prompts
1089
+ const capturedPrompts = [];
1090
+ const client = {
1091
+ createSession: vi.fn().mockResolvedValue({
1092
+ send: vi.fn().mockImplementation((msg) => {
1093
+ capturedPrompts.push(msg.content);
1094
+ return {
1095
+ async *[Symbol.asyncIterator]() {
1096
+ yield {
1097
+ type: 'TextDelta',
1098
+ text: '```typescript file=src/index.ts\nexport const x = 1;\n```',
1099
+ timestamp: '',
1100
+ };
1101
+ },
1102
+ };
1103
+ }),
1104
+ getHistory: () => [],
1105
+ }),
1106
+ };
1107
+ const ralph = new RalphLoop({
1108
+ client,
1109
+ io,
1110
+ session,
1111
+ outputDir: tmpDir,
1112
+ maxIterations: 4,
1113
+ testRunner,
1114
+ scaffolder: makeFakeScaffolder(tmpDir),
1115
+ checkpoint: {
1116
+ hasPriorRun: true,
1117
+ completedIterations: 2,
1118
+ lastIterationIncomplete: false,
1119
+ resumeFromIteration: 3,
1120
+ canSkipScaffold: false,
1121
+ priorFinalStatus: undefined,
1122
+ priorIterations: priorIters,
1123
+ },
1124
+ });
1125
+ await ralph.run();
1126
+ // The LLM should have received prior iteration history
1127
+ // The context enrichment path merges priorHistoryContext into mcpContext
1128
+ // This is hard to test directly without peeking at internals, but we can verify
1129
+ // that the prior iterations were seeded properly
1130
+ expect(io.writeActivity).toHaveBeenCalledWith(expect.stringContaining('Re-running dependency installation'));
1131
+ });
1132
+ it('resume decision logging emits info-level messages (T067, FR-007a)', async () => {
1133
+ const io = makeIo();
1134
+ const testRunner = makePassingTestRunner();
1135
+ const scaffolder = makeFakeScaffolder(tmpDir);
1136
+ const session = makeSession({
1137
+ poc: {
1138
+ repoSource: 'local',
1139
+ iterations: [
1140
+ {
1141
+ iteration: 1,
1142
+ startedAt: new Date().toISOString(),
1143
+ endedAt: new Date().toISOString(),
1144
+ outcome: 'scaffold',
1145
+ filesChanged: [],
1146
+ },
1147
+ ],
1148
+ },
1149
+ });
1150
+ const ralph = new RalphLoop({
1151
+ client: makePassingClient(),
1152
+ io,
1153
+ session,
1154
+ outputDir: tmpDir,
1155
+ maxIterations: 10,
1156
+ testRunner,
1157
+ scaffolder,
1158
+ checkpoint: {
1159
+ hasPriorRun: true,
1160
+ completedIterations: 1,
1161
+ lastIterationIncomplete: false,
1162
+ resumeFromIteration: 2,
1163
+ canSkipScaffold: true,
1164
+ priorFinalStatus: undefined,
1165
+ priorIterations: session.poc.iterations,
1166
+ },
1167
+ });
1168
+ await ralph.run();
1169
+ // Should have logged skip scaffold, re-run install messages
1170
+ const calls = io.writeActivity.mock.calls.flat();
1171
+ expect(calls.some((c) => c.includes('Skipping scaffold'))).toBe(true);
1172
+ expect(calls.some((c) => c.includes('Re-running dependency installation'))).toBe(true);
1173
+ });
1174
+ });
1175
+ // ── T073: TODO marker rescan after iteration updates .sofia-metadata.json ──
1176
+ describe('TODO marker rescan after iteration (T073)', () => {
1177
+ it('calls scanAndRecordTodos after each failing iteration', async () => {
1178
+ const scanSpy = vi
1179
+ .spyOn(PocScaffolder, 'scanAndRecordTodos')
1180
+ .mockResolvedValue({ totalInitial: 3, remaining: 2, markers: [] });
1181
+ const session = makeSession();
1182
+ const io = {
1183
+ write: vi.fn(),
1184
+ writeActivity: vi.fn(),
1185
+ writeToolSummary: vi.fn(),
1186
+ readInput: vi.fn().mockResolvedValue(null),
1187
+ showDecisionGate: vi.fn(),
1188
+ isJsonMode: false,
1189
+ isTTY: false,
1190
+ };
1191
+ const testRunner = makeAlwaysFailingTestRunner();
1192
+ const client = makePassingClient();
1193
+ const scaffolder = makeFakeScaffolder(tmpDir);
1194
+ const ralph = new RalphLoop({
1195
+ client,
1196
+ io,
1197
+ session,
1198
+ outputDir: tmpDir,
1199
+ maxIterations: 2,
1200
+ testRunner,
1201
+ scaffolder,
1202
+ });
1203
+ await ralph.run();
1204
+ // scanAndRecordTodos should have been called for each failing iteration
1205
+ expect(scanSpy).toHaveBeenCalled();
1206
+ expect(scanSpy).toHaveBeenCalledWith(tmpDir);
1207
+ scanSpy.mockRestore();
1208
+ });
1209
+ });
1210
+ // ── T054: infiniteSessions config forwarding ──────────────────────────────
1211
+ describe('infiniteSessions config (T054)', () => {
1212
+ it('passes infiniteSessions config to createSession for context management', async () => {
1213
+ const createSessionSpy = vi.fn().mockResolvedValue({
1214
+ send: vi.fn().mockReturnValue({
1215
+ async *[Symbol.asyncIterator]() {
1216
+ yield {
1217
+ type: 'TextDelta',
1218
+ text: '```typescript file=src/index.ts\nexport function main() { return "ok"; }\n```',
1219
+ timestamp: '',
1220
+ };
1221
+ },
1222
+ }),
1223
+ getHistory: () => [],
1224
+ });
1225
+ const client = { createSession: createSessionSpy };
1226
+ const io = makeIo();
1227
+ const session = makeSession();
1228
+ const testRunner = makeAlwaysFailingTestRunner();
1229
+ const scaffolder = makeFakeScaffolder(tmpDir);
1230
+ const ralph = new RalphLoop({
1231
+ client,
1232
+ io,
1233
+ session,
1234
+ outputDir: tmpDir,
1235
+ maxIterations: 2,
1236
+ testRunner,
1237
+ scaffolder,
1238
+ });
1239
+ await ralph.run();
1240
+ expect(createSessionSpy).toHaveBeenCalledWith(expect.objectContaining({
1241
+ infiniteSessions: {
1242
+ backgroundCompactionThreshold: 0.7,
1243
+ bufferExhaustionThreshold: 0.9,
1244
+ },
1245
+ }));
1246
+ });
1247
+ });
1248
+ });