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,659 @@
1
+ /**
2
+ * Ralph Loop Orchestrator.
3
+ *
4
+ * Autonomous code-generation-test-refine cycle for the Develop phase.
5
+ * Lifecycle: validate session → scaffold → install → iterate → terminate.
6
+ *
7
+ * Contract: specs/002-poc-generation/contracts/ralph-loop.md
8
+ */
9
+ import { spawn } from 'node:child_process';
10
+ import { readFile } from 'node:fs/promises';
11
+ import { join } from 'node:path';
12
+ import { createActivityEvent } from '../shared/events.js';
13
+ // McpManager import removed - accessed via McpContextEnricher.mcpManager public property
14
+ import { exportWorkshopDocs } from '../sessions/exportWriter.js';
15
+ import { PocScaffolder, validatePocOutput } from './pocScaffolder.js';
16
+ import { generateDynamicScaffold } from './dynamicScaffolder.js';
17
+ import { TestRunner } from './testRunner.js';
18
+ import { CodeGenerator } from './codeGenerator.js';
19
+ // ── npm helper ───────────────────────────────────────────────────────────────
20
+ function runInstallCommand(cwd, installCommand = 'npm install') {
21
+ return new Promise((resolve) => {
22
+ const parts = installCommand.split(/\s+/);
23
+ const cmd = parts[0];
24
+ const args = parts.slice(1);
25
+ const child = spawn(cmd, args, {
26
+ cwd,
27
+ shell: false,
28
+ stdio: 'pipe',
29
+ });
30
+ const stderr = [];
31
+ child.stderr?.on('data', (chunk) => {
32
+ stderr.push(chunk.toString());
33
+ });
34
+ child.on('close', (code) => {
35
+ if (code === 0) {
36
+ resolve({ success: true });
37
+ }
38
+ else {
39
+ resolve({ success: false, error: stderr.join('').slice(-500) || 'npm install failed' });
40
+ }
41
+ });
42
+ child.on('error', (err) => {
43
+ resolve({ success: false, error: err.message });
44
+ });
45
+ });
46
+ }
47
+ // ── RalphLoop ────────────────────────────────────────────────────────────────
48
+ /**
49
+ * Orchestrates the Ralph loop: scaffold → install → iterate → terminate.
50
+ *
51
+ * Implements the full lifecycle from the ralph-loop contract:
52
+ * 1. Validate session (selection + plan present)
53
+ * 2. Scaffold PoC (iteration 1)
54
+ * 3. npm install
55
+ * 4. For each iteration 2..max: run tests → check → build prompt → LLM → apply → persist
56
+ * 5. Return result with final status
57
+ */
58
+ export class RalphLoop {
59
+ options;
60
+ aborted = false;
61
+ sigintHandler = null;
62
+ /** Mutable reference to the latest session state, used by the SIGINT handler (F010). */
63
+ currentSession;
64
+ constructor(options) {
65
+ const outputDir = options.outputDir ?? join(process.cwd(), '..', 'poc', options.session.sessionId);
66
+ this.options = {
67
+ maxIterations: 20,
68
+ onSessionUpdate: async () => { },
69
+ onEvent: () => { },
70
+ ...options,
71
+ outputDir,
72
+ };
73
+ this.currentSession = options.session;
74
+ }
75
+ /**
76
+ * Run the Ralph loop.
77
+ *
78
+ * Returns when: all tests pass, max iterations reached, user stopped (Ctrl+C), or error.
79
+ */
80
+ async run() {
81
+ const { session: initialSession, io, client, maxIterations, outputDir, onEvent, spinner, } = this.options;
82
+ const onSessionUpdate = this.options.onSessionUpdate ?? (async () => { });
83
+ // ── Validate session ───────────────────────────────────────────────────
84
+ if (!initialSession.selection) {
85
+ throw new Error('Session is missing a selection. Run the Select phase first before generating a PoC.');
86
+ }
87
+ if (!initialSession.plan) {
88
+ throw new Error('Session is missing an implementation plan. Run the Plan phase first before generating a PoC.');
89
+ }
90
+ let session = { ...initialSession };
91
+ const startTime = Date.now();
92
+ // ── Setup SIGINT handler ───────────────────────────────────────────────
93
+ const safeOnEvent = onEvent ?? (() => { });
94
+ this.setupSigintHandler(session, onSessionUpdate, safeOnEvent);
95
+ // ── Resume detection ───────────────────────────────────────────────────
96
+ const checkpoint = this.options.checkpoint;
97
+ const templateEntry = this.options.templateEntry;
98
+ const installCommand = templateEntry?.installCommand ?? 'npm install';
99
+ const testCommandStr = templateEntry?.testCommand;
100
+ // Seed iterations from prior session state (FR-001)
101
+ const iterations = checkpoint?.hasPriorRun
102
+ ? [...checkpoint.priorIterations]
103
+ : [];
104
+ // If last iteration was incomplete, pop it and re-run (FR-001a)
105
+ if (checkpoint?.lastIterationIncomplete && iterations.length > 0) {
106
+ io.writeActivity(`Re-running incomplete iteration ${iterations.length + 1} (no test results recorded)`);
107
+ }
108
+ // ── Determine repo source ──────────────────────────────────────────────
109
+ // Always use local git repositories for safety - never auto-push to GitHub
110
+ const repoSource = 'local';
111
+ io.writeActivity(`Output directory: ${outputDir}`);
112
+ // ── Scaffold (skip if resuming with valid output dir) ──────────────────
113
+ const shouldSkipScaffold = checkpoint?.canSkipScaffold === true;
114
+ let scaffoldResult;
115
+ let techStack = {
116
+ language: 'unknown',
117
+ runtime: 'unknown',
118
+ testRunner: 'unknown',
119
+ };
120
+ if (shouldSkipScaffold) {
121
+ io.writeActivity('Skipping scaffold — output directory and .sofia-metadata.json present');
122
+ // When skipping scaffold, try to load tech stack from metadata
123
+ try {
124
+ const metadataPath = join(outputDir, '.sofia-metadata.json');
125
+ const metadataContent = await readFile(metadataPath, 'utf-8');
126
+ const metadata = JSON.parse(metadataContent);
127
+ if (metadata.techStack) {
128
+ techStack = metadata.techStack;
129
+ }
130
+ }
131
+ catch {
132
+ // If metadata is missing or invalid, keep default techStack
133
+ }
134
+ }
135
+ else {
136
+ // FR-007: Re-scaffold when output dir is missing but iterations exist
137
+ if (checkpoint?.hasPriorRun) {
138
+ io.writeActivity('Output directory missing — re-scaffolding for resumed session');
139
+ }
140
+ io.writeActivity('Scaffolding PoC project structure...');
141
+ spinner?.startThinking();
142
+ const scaffoldStart = Date.now();
143
+ try {
144
+ scaffoldResult = await generateDynamicScaffold({
145
+ client,
146
+ session,
147
+ outputDir,
148
+ });
149
+ techStack = scaffoldResult.techStack;
150
+ }
151
+ catch (err) {
152
+ spinner?.stop();
153
+ const msg = err instanceof Error ? err.message : String(err);
154
+ return this.terminate(session, iterations, 'failed', 'error', startTime, outputDir, repoSource, techStack, onEvent, `Scaffold failed: ${msg}`);
155
+ }
156
+ const scaffoldIteration = {
157
+ iteration: iterations.length + 1,
158
+ startedAt: new Date(scaffoldStart).toISOString(),
159
+ endedAt: new Date().toISOString(),
160
+ outcome: 'scaffold',
161
+ filesChanged: scaffoldResult.createdFiles,
162
+ changesSummary: `Scaffold created ${scaffoldResult.createdFiles.length} files`,
163
+ };
164
+ iterations.push(scaffoldIteration);
165
+ spinner?.stop();
166
+ io.writeActivity(`Scaffold complete: ${scaffoldResult.createdFiles.length} files created`);
167
+ // Export workshop documentation into the PoC repo (docs/workshop/ + WORKSHOP.md)
168
+ try {
169
+ const workshopResult = await exportWorkshopDocs(session, outputDir);
170
+ io.writeActivity(`✓ Workshop documentation added: ${workshopResult.createdFiles.length} files (docs/workshop/)`);
171
+ }
172
+ catch {
173
+ // Non-fatal — the PoC is still usable without workshop docs
174
+ io.writeActivity('⚠️ Could not export workshop documentation');
175
+ }
176
+ // Initialize local git repository
177
+ const gitInitialized = await PocScaffolder.initializeGitRepo(outputDir);
178
+ if (gitInitialized) {
179
+ io.writeActivity('✓ Initialized git repository with initial commit');
180
+ io.writeActivity('');
181
+ io.writeActivity('📌 Next steps:');
182
+ io.writeActivity(' 1. Review the generated code');
183
+ io.writeActivity(` 2. cd ${outputDir}`);
184
+ io.writeActivity(' 3. Create a GitHub repo: gh repo create --source=. --push');
185
+ io.writeActivity(' 4. Or push to existing remote: git remote add origin <url> && git push -u origin main');
186
+ io.writeActivity('');
187
+ }
188
+ else {
189
+ io.writeActivity('⚠️ Could not initialize git (git may not be installed)');
190
+ }
191
+ // Persist after scaffold
192
+ session = this.updateSessionPoc(session, iterations, repoSource, outputDir, undefined, techStack);
193
+ await onSessionUpdate(session);
194
+ }
195
+ // ── Dependency install (FR-003: always re-run on resume) ───────────────
196
+ if (!this.aborted) {
197
+ io.writeActivity(`Re-running dependency installation (${installCommand})`);
198
+ spinner?.startThinking();
199
+ const installResult = await runInstallCommand(outputDir, installCommand);
200
+ spinner?.stop();
201
+ if (!installResult.success) {
202
+ io.writeActivity(`${installCommand} failed: ${installResult.error}`);
203
+ return this.terminate(session, iterations, 'failed', 'error', startTime, outputDir, repoSource, techStack, onEvent, `${installCommand} failed: ${installResult.error}`);
204
+ }
205
+ io.writeActivity(`${installCommand} complete`);
206
+ }
207
+ // ── Iteration loop ─────────────────────────────────────────────────────
208
+ const testRunner = this.options.testRunner ??
209
+ new TestRunner(testCommandStr ? { testCommand: testCommandStr } : undefined);
210
+ // ── Iteration 2..max ──────────────────────────────────────────────────
211
+ const codeGenerator = new CodeGenerator(outputDir);
212
+ const enricher = this.options.enricher;
213
+ let stuckIterations = 0;
214
+ let prevFailingTests = [];
215
+ // FR-004: Build prior iteration history for LLM context
216
+ const priorHistoryContext = checkpoint?.hasPriorRun
217
+ ? this.buildPriorHistoryContext(checkpoint.priorIterations)
218
+ : '';
219
+ const startIterNum = iterations.length + 1;
220
+ for (let iterNum = startIterNum; iterNum <= maxIterations; iterNum++) {
221
+ if (this.aborted)
222
+ break;
223
+ const iterStart = Date.now();
224
+ safeOnEvent(createActivityEvent(`Starting iteration ${iterNum} of ${maxIterations}`));
225
+ // ── Run tests ──────────────────────────────────────────────────────
226
+ io.writeActivity(`Iteration ${iterNum}/${maxIterations}: Running tests...`);
227
+ spinner?.startThinking();
228
+ const testResults = await testRunner.run(outputDir);
229
+ spinner?.stop();
230
+ safeOnEvent(createActivityEvent(`Test results: ${testResults.passed} passed, ${testResults.failed} failed`));
231
+ // ── Check if all tests pass ────────────────────────────────────────
232
+ if (testResults.failed === 0 && testResults.total > 0) {
233
+ io.writeActivity(`All ${testResults.passed} tests pass! Loop complete.`);
234
+ const successIteration = {
235
+ iteration: iterNum,
236
+ startedAt: new Date(iterStart).toISOString(),
237
+ endedAt: new Date().toISOString(),
238
+ outcome: 'tests-passing',
239
+ filesChanged: [],
240
+ testResults,
241
+ };
242
+ iterations.push(successIteration);
243
+ session = this.updateSessionPoc(session, iterations, repoSource, outputDir, undefined, // No GitHub repo URL - local only
244
+ techStack, 'success', 'tests-passing', Date.now() - startTime, testResults);
245
+ await onSessionUpdate(session);
246
+ safeOnEvent(createActivityEvent('Ralph loop terminated: tests-passing'));
247
+ this.cleanupSigint();
248
+ // Validate PoC output; downgrade to 'partial' if validation fails
249
+ const validation = await validatePocOutput(outputDir);
250
+ if (!validation.valid) {
251
+ const issues = [
252
+ ...validation.missingFiles.map((f) => `missing: ${f}`),
253
+ ...validation.errors,
254
+ ];
255
+ io.writeActivity(`PoC validation warning: ${issues.join('; ')}`);
256
+ session = this.updateSessionPoc(session, iterations, repoSource, outputDir, undefined, // No GitHub repo URL - local only
257
+ techStack, 'partial', 'tests-passing', Date.now() - startTime, testResults);
258
+ await onSessionUpdate(session);
259
+ return {
260
+ session,
261
+ finalStatus: 'partial',
262
+ terminationReason: 'tests-passing',
263
+ iterationsCompleted: iterNum,
264
+ outputDir,
265
+ };
266
+ }
267
+ return {
268
+ session,
269
+ finalStatus: 'success',
270
+ terminationReason: 'tests-passing',
271
+ iterationsCompleted: iterNum,
272
+ outputDir,
273
+ };
274
+ }
275
+ // ── Check for stuck iterations (same failures) ─────────────────────
276
+ const currentFailingTests = testResults.failures.map((f) => f.testName);
277
+ if (currentFailingTests.length > 0 &&
278
+ JSON.stringify(currentFailingTests.sort()) === JSON.stringify(prevFailingTests.sort())) {
279
+ stuckIterations++;
280
+ }
281
+ else {
282
+ stuckIterations = 0;
283
+ }
284
+ prevFailingTests = currentFailingTests;
285
+ // ── Enrich context (MCP) ───────────────────────────────────────────
286
+ let mcpContext = '';
287
+ if (enricher) {
288
+ try {
289
+ const enriched = await enricher.enrich({
290
+ // enricher.mcpManager is a public readonly property on McpContextEnricher
291
+ mcpManager: enricher.mcpManager,
292
+ dependencies: session.plan?.dependencies ?? [],
293
+ architectureNotes: session.plan?.architectureNotes ?? '',
294
+ stuckIterations,
295
+ failingTests: currentFailingTests,
296
+ });
297
+ mcpContext = enriched.combined;
298
+ }
299
+ catch {
300
+ // Degrade gracefully
301
+ }
302
+ }
303
+ // ── Build LLM prompt ───────────────────────────────────────────────
304
+ const filesInPoc = codeGenerator.getFilesInPoc();
305
+ const prevIteration = iterations[iterations.length - 1];
306
+ const prevOutcome = prevIteration?.outcome ?? 'scaffold';
307
+ // Read actual file contents so the LLM can see the code (F003/F004)
308
+ const fileContents = await this.readFileContents(outputDir, filesInPoc, currentFailingTests, testResults);
309
+ const prompt = codeGenerator.buildIterationPrompt({
310
+ iteration: iterNum,
311
+ maxIterations,
312
+ previousOutcome: prevOutcome,
313
+ testResults,
314
+ filesInPoc,
315
+ mcpContext: [priorHistoryContext, mcpContext].filter(Boolean).join('\n') || undefined,
316
+ fileContents,
317
+ });
318
+ const promptSummary = codeGenerator.buildPromptContextSummary({
319
+ iteration: iterNum,
320
+ maxIterations,
321
+ previousOutcome: prevOutcome,
322
+ testResults,
323
+ filesInPoc,
324
+ mcpContext: mcpContext || undefined,
325
+ });
326
+ safeOnEvent(createActivityEvent(`Sending iteration ${iterNum} prompt to LLM`));
327
+ // ── LLM turn (single-turn, auto-completing) ────────────────────────
328
+ io.writeActivity(`Generating code for iteration ${iterNum}...`);
329
+ spinner?.startThinking();
330
+ let llmResponse = '';
331
+ let llmError;
332
+ try {
333
+ llmResponse = await this.runSingleLlmTurn(client, session, prompt);
334
+ }
335
+ catch (err) {
336
+ llmError = err instanceof Error ? err.message : String(err);
337
+ }
338
+ spinner?.stop();
339
+ if (llmError || !llmResponse) {
340
+ io.writeActivity(`LLM error in iteration ${iterNum}: ${llmError ?? 'empty response'}`);
341
+ const errIteration = {
342
+ iteration: iterNum,
343
+ startedAt: new Date(iterStart).toISOString(),
344
+ endedAt: new Date().toISOString(),
345
+ outcome: 'error',
346
+ filesChanged: [],
347
+ errorMessage: llmError ?? 'LLM returned empty response',
348
+ llmPromptContext: promptSummary,
349
+ testResults,
350
+ };
351
+ iterations.push(errIteration);
352
+ session = this.updateSessionPoc(session, iterations, repoSource, outputDir, undefined, // No GitHub repo URL - local only
353
+ techStack);
354
+ await onSessionUpdate(session);
355
+ continue; // Continue to next iteration — LLM may recover
356
+ }
357
+ safeOnEvent(createActivityEvent(`LLM generated code for iteration ${iterNum}`));
358
+ // ── Apply code changes ─────────────────────────────────────────────
359
+ const applyResult = await codeGenerator.applyChanges(llmResponse);
360
+ applyResult.llmPromptContext = promptSummary;
361
+ io.writeActivity(`Applied ${applyResult.writtenFiles.length} file(s): ${applyResult.writtenFiles.slice(0, 5).join(', ')}`);
362
+ // Re-run install command if dependencies changed
363
+ if (applyResult.dependenciesChanged) {
364
+ io.writeActivity(`New dependencies detected — running ${installCommand}...`);
365
+ spinner?.startThinking();
366
+ const reinstall = await runInstallCommand(outputDir, installCommand);
367
+ spinner?.stop();
368
+ if (!reinstall.success) {
369
+ io.writeActivity(`npm install failed in iteration ${iterNum}: ${reinstall.error}`);
370
+ const errIteration = {
371
+ iteration: iterNum,
372
+ startedAt: new Date(iterStart).toISOString(),
373
+ endedAt: new Date().toISOString(),
374
+ outcome: 'error',
375
+ filesChanged: applyResult.writtenFiles,
376
+ errorMessage: `npm install failed: ${reinstall.error}`,
377
+ llmPromptContext: promptSummary,
378
+ };
379
+ iterations.push(errIteration);
380
+ session = this.updateSessionPoc(session, iterations, repoSource, outputDir, undefined, // No GitHub repo URL - local only
381
+ techStack);
382
+ await onSessionUpdate(session);
383
+ continue; // Continue — LLM may fix the bad dependency
384
+ }
385
+ }
386
+ // FR-022: Rescan TODO markers after applying changes
387
+ try {
388
+ await PocScaffolder.scanAndRecordTodos(outputDir);
389
+ }
390
+ catch {
391
+ // Non-critical — ignore scanning errors
392
+ }
393
+ const failIteration = {
394
+ iteration: iterNum,
395
+ startedAt: new Date(iterStart).toISOString(),
396
+ endedAt: new Date().toISOString(),
397
+ outcome: 'tests-failing',
398
+ filesChanged: applyResult.writtenFiles,
399
+ testResults,
400
+ llmPromptContext: promptSummary,
401
+ };
402
+ iterations.push(failIteration);
403
+ session = this.updateSessionPoc(session, iterations, repoSource, outputDir, undefined, // No GitHub repo URL - local only
404
+ techStack);
405
+ await onSessionUpdate(session);
406
+ if (this.aborted)
407
+ break;
408
+ }
409
+ // ── Max iterations reached (or user-stopped) ───────────────────────
410
+ if (this.aborted) {
411
+ this.cleanupSigint();
412
+ // Compute finalStatus for the result, but do NOT persist it to the session.
413
+ // Contract: on user abort, session.poc.finalStatus must remain unset so the
414
+ // session can be resumed later without a stale terminal status. (F012)
415
+ const lastIterForAbort = iterations[iterations.length - 1];
416
+ const lastTestsForAbort = lastIterForAbort?.testResults;
417
+ const abortFinalStatus = (lastTestsForAbort?.passed ?? 0) > 0 ? 'partial' : 'failed';
418
+ session = this.updateSessionPoc(session, iterations, repoSource, outputDir, undefined, // No GitHub repo URL - local only
419
+ techStack, undefined, // finalStatus deliberately omitted on user-stop
420
+ 'user-stopped', Date.now() - startTime, lastTestsForAbort);
421
+ await onSessionUpdate(session);
422
+ safeOnEvent(createActivityEvent('Ralph loop terminated: user-stopped'));
423
+ return {
424
+ session,
425
+ finalStatus: abortFinalStatus,
426
+ terminationReason: 'user-stopped',
427
+ iterationsCompleted: iterations.length,
428
+ outputDir,
429
+ };
430
+ }
431
+ // Determine final status based on a final test run after the last code changes (F008)
432
+ io.writeActivity('Running final test pass after last iteration...');
433
+ const lastIter = iterations[iterations.length - 1];
434
+ let finalTestResults;
435
+ try {
436
+ finalTestResults = await testRunner.run(outputDir);
437
+ }
438
+ catch {
439
+ // If test runner fails, fall back to last iteration's results
440
+ }
441
+ if (finalTestResults && finalTestResults.failed === 0 && finalTestResults.passed > 0) {
442
+ // Last code fix actually resolved all failures!
443
+ const finalIteration = {
444
+ iteration: iterations.length + 1,
445
+ startedAt: new Date().toISOString(),
446
+ endedAt: new Date().toISOString(),
447
+ outcome: 'tests-passing',
448
+ filesChanged: [],
449
+ testResults: finalTestResults,
450
+ };
451
+ iterations.push(finalIteration);
452
+ session = this.updateSessionPoc(session, iterations, repoSource, outputDir, undefined, // No GitHub repo URL - local only
453
+ techStack, 'success', 'tests-passing', Date.now() - startTime, finalTestResults);
454
+ await onSessionUpdate(session);
455
+ safeOnEvent(createActivityEvent('Ralph loop terminated: tests-passing (final run)'));
456
+ this.cleanupSigint();
457
+ // Validate PoC output; downgrade to 'partial' if validation fails
458
+ const validation = await validatePocOutput(outputDir);
459
+ if (!validation.valid) {
460
+ const issues = [
461
+ ...validation.missingFiles.map((f) => `missing: ${f}`),
462
+ ...validation.errors,
463
+ ];
464
+ io.writeActivity(`PoC validation warning: ${issues.join('; ')}`);
465
+ session = this.updateSessionPoc(session, iterations, repoSource, outputDir, undefined, // No GitHub repo URL - local only
466
+ techStack, 'partial', 'tests-passing', Date.now() - startTime, finalTestResults);
467
+ await onSessionUpdate(session);
468
+ return {
469
+ session,
470
+ finalStatus: 'partial',
471
+ terminationReason: 'tests-passing',
472
+ iterationsCompleted: iterations.length,
473
+ outputDir,
474
+ };
475
+ }
476
+ return {
477
+ session,
478
+ finalStatus: 'success',
479
+ terminationReason: 'tests-passing',
480
+ iterationsCompleted: iterations.length,
481
+ outputDir,
482
+ };
483
+ }
484
+ // Use final test results if available, otherwise fall back to last iteration
485
+ const effectiveTestResults = finalTestResults ?? lastIter?.testResults;
486
+ const someTestsPassed = (effectiveTestResults?.passed ?? 0) > 0;
487
+ const finalStatus = someTestsPassed ? 'partial' : 'failed';
488
+ io.writeActivity(`Max iterations (${maxIterations}) reached. Final status: ${finalStatus}`);
489
+ safeOnEvent(createActivityEvent(`Ralph loop terminated: max-iterations (${finalStatus})`));
490
+ session = this.updateSessionPoc(session, iterations, repoSource, outputDir, undefined, // No GitHub repo URL - local only
491
+ techStack, finalStatus, 'max-iterations', Date.now() - startTime, effectiveTestResults);
492
+ await onSessionUpdate(session);
493
+ this.cleanupSigint();
494
+ return {
495
+ session,
496
+ finalStatus,
497
+ terminationReason: 'max-iterations',
498
+ iterationsCompleted: iterations.length,
499
+ outputDir,
500
+ };
501
+ }
502
+ // ── Private helpers ──────────────────────────────────────────────────────
503
+ /**
504
+ * Build a concise summary of prior iteration history for LLM context (FR-004).
505
+ */
506
+ buildPriorHistoryContext(priorIterations) {
507
+ if (priorIterations.length === 0)
508
+ return '';
509
+ const lines = ['## Prior Iteration History (Resume Context)', ''];
510
+ for (const iter of priorIterations) {
511
+ const status = iter.testResults
512
+ ? `${iter.testResults.passed} passed, ${iter.testResults.failed} failed`
513
+ : iter.outcome;
514
+ const files = iter.filesChanged?.length
515
+ ? ` — files: ${iter.filesChanged.slice(0, 5).join(', ')}`
516
+ : '';
517
+ lines.push(`- Iteration ${iter.iteration}: ${status}${files}`);
518
+ }
519
+ lines.push('');
520
+ return lines.join('\n');
521
+ }
522
+ /** Maximum total size of file contents to include in the prompt (50KB). */
523
+ static MAX_FILE_CONTENT_BYTES = 50 * 1024;
524
+ /**
525
+ * Read file contents from the PoC directory for inclusion in the iteration prompt.
526
+ *
527
+ * If the total content exceeds MAX_FILE_CONTENT_BYTES, includes only files
528
+ * referenced in test failures plus core files (src/index.ts, package.json).
529
+ */
530
+ async readFileContents(outputDir, filesInPoc, failingTests, testResults) {
531
+ // Flatten the tree listing into actual relative file paths
532
+ const flatFiles = filesInPoc
533
+ .map((f) => f.replace(/^\s+/, ''))
534
+ .filter((f) => !f.endsWith('/') && f.length > 0);
535
+ // Read all file contents
536
+ const allContents = [];
537
+ for (const relPath of flatFiles) {
538
+ try {
539
+ const fullPath = join(outputDir, relPath);
540
+ const content = await readFile(fullPath, 'utf-8');
541
+ allContents.push({ path: relPath, content });
542
+ }
543
+ catch {
544
+ // skip unreadable files
545
+ }
546
+ }
547
+ // Check total size
548
+ const totalSize = allContents.reduce((sum, f) => sum + Buffer.byteLength(f.content, 'utf-8'), 0);
549
+ if (totalSize <= RalphLoop.MAX_FILE_CONTENT_BYTES) {
550
+ return allContents;
551
+ }
552
+ // Over budget — filter to only failure-referenced files + core files
553
+ this.options.io.writeActivity(`File content exceeds 50KB (${(totalSize / 1024).toFixed(1)}KB), including only failure-referenced files`);
554
+ const coreFiles = new Set(['src/index.ts', 'package.json']);
555
+ // Gather file references from failures
556
+ const failureFiles = new Set();
557
+ for (const failure of testResults.failures) {
558
+ if (failure.file)
559
+ failureFiles.add(failure.file);
560
+ }
561
+ return allContents.filter((f) => coreFiles.has(f.path) || failureFiles.has(f.path));
562
+ }
563
+ /**
564
+ * Run a single auto-completing LLM turn.
565
+ * Creates a minimal conversation session with the system prompt.
566
+ */
567
+ async runSingleLlmTurn(client, session, prompt) {
568
+ const { buildSystemPrompt } = await import('../prompts/promptLoader.js');
569
+ let systemPrompt;
570
+ try {
571
+ systemPrompt = await buildSystemPrompt('Develop');
572
+ }
573
+ catch {
574
+ systemPrompt =
575
+ 'You are a TypeScript code generator. Output complete files in fenced code blocks with file= paths.';
576
+ }
577
+ const conversationSession = await client.createSession({
578
+ systemPrompt,
579
+ infiniteSessions: {
580
+ backgroundCompactionThreshold: 0.7,
581
+ bufferExhaustionThreshold: 0.9,
582
+ },
583
+ });
584
+ let response = '';
585
+ const stream = conversationSession.send({ role: 'user', content: prompt });
586
+ for await (const event of stream) {
587
+ if (event.type === 'TextDelta') {
588
+ response += event.text;
589
+ }
590
+ }
591
+ return response;
592
+ }
593
+ /**
594
+ * Update the session's poc field with current state.
595
+ */
596
+ updateSessionPoc(session, iterations, repoSource, outputDir, repoUrl, techStack, finalStatus, terminationReason, totalDurationMs, finalTestResults) {
597
+ const poc = {
598
+ repoSource,
599
+ repoPath: outputDir,
600
+ repoUrl: repoUrl, // User can manually set this after pushing
601
+ techStack,
602
+ iterations,
603
+ finalStatus,
604
+ terminationReason,
605
+ totalDurationMs,
606
+ finalTestResults,
607
+ };
608
+ const updated = {
609
+ ...session,
610
+ poc,
611
+ updatedAt: new Date().toISOString(),
612
+ };
613
+ // Keep the mutable reference up to date for the SIGINT handler (F010)
614
+ this.currentSession = updated;
615
+ return updated;
616
+ }
617
+ /**
618
+ * Build a terminate result with session updated.
619
+ */
620
+ async terminate(session, iterations, finalStatus, terminationReason, startTime, outputDir, repoSource, techStack, onEvent, errorMessage) {
621
+ if (errorMessage) {
622
+ this.options.io.writeActivity(`Error: ${errorMessage}`);
623
+ }
624
+ const updatedSession = this.updateSessionPoc(session, iterations, repoSource, outputDir, undefined, techStack, finalStatus, terminationReason, Date.now() - startTime);
625
+ await this.options.onSessionUpdate?.(updatedSession);
626
+ if (onEvent)
627
+ onEvent(createActivityEvent(`Ralph loop terminated: ${terminationReason}`));
628
+ this.cleanupSigint();
629
+ return {
630
+ session: updatedSession,
631
+ finalStatus,
632
+ terminationReason,
633
+ iterationsCompleted: iterations.length,
634
+ outputDir,
635
+ };
636
+ }
637
+ /**
638
+ * Setup SIGINT handler for Ctrl+C.
639
+ * Uses `this.currentSession` (mutable) so the handler always persists the latest state (F010).
640
+ */
641
+ setupSigintHandler(_session, onSessionUpdate, onEvent) {
642
+ this.sigintHandler = () => {
643
+ this.aborted = true;
644
+ this.options.io.writeActivity('\nCtrl+C detected — stopping after current iteration...');
645
+ onEvent(createActivityEvent('User requested stop (SIGINT)'));
646
+ void onSessionUpdate(this.currentSession);
647
+ };
648
+ process.once('SIGINT', this.sigintHandler);
649
+ }
650
+ /**
651
+ * Remove the SIGINT handler.
652
+ */
653
+ cleanupSigint() {
654
+ if (this.sigintHandler) {
655
+ process.removeListener('SIGINT', this.sigintHandler);
656
+ this.sigintHandler = null;
657
+ }
658
+ }
659
+ }