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.
- package/.github/agents/copilot-instructions.md +39 -0
- package/.github/agents/speckit.analyze.agent.md +184 -0
- package/.github/agents/speckit.checklist.agent.md +294 -0
- package/.github/agents/speckit.clarify.agent.md +181 -0
- package/.github/agents/speckit.constitution.agent.md +84 -0
- package/.github/agents/speckit.implement.agent.md +135 -0
- package/.github/agents/speckit.plan.agent.md +90 -0
- package/.github/agents/speckit.specify.agent.md +258 -0
- package/.github/agents/speckit.tasks.agent.md +137 -0
- package/.github/agents/speckit.taskstoissues.agent.md +30 -0
- package/.github/copilot-instructions.md +257 -0
- package/.github/prompts/speckit.analyze.prompt.md +3 -0
- package/.github/prompts/speckit.checklist.prompt.md +3 -0
- package/.github/prompts/speckit.clarify.prompt.md +3 -0
- package/.github/prompts/speckit.constitution.prompt.md +3 -0
- package/.github/prompts/speckit.implement.prompt.md +3 -0
- package/.github/prompts/speckit.plan.prompt.md +3 -0
- package/.github/prompts/speckit.specify.prompt.md +3 -0
- package/.github/prompts/speckit.tasks.prompt.md +3 -0
- package/.github/prompts/speckit.taskstoissues.prompt.md +3 -0
- package/.github/workflows/ci.yml +38 -0
- package/.prettierrc +6 -0
- package/.specify/memory/constitution.md +181 -0
- package/.specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.specify/scripts/bash/common.sh +156 -0
- package/.specify/scripts/bash/create-new-feature.sh +297 -0
- package/.specify/scripts/bash/setup-plan.sh +61 -0
- package/.specify/scripts/bash/update-agent-context.sh +810 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/constitution-template.md +50 -0
- package/.specify/templates/plan-template.md +113 -0
- package/.specify/templates/spec-template.md +115 -0
- package/.specify/templates/tasks-template.md +251 -0
- package/.vscode/mcp.json +42 -0
- package/.vscode/settings.json +19 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/LICENSE +21 -0
- package/README.md +213 -0
- package/dist/src/cli/developCommand.js +240 -0
- package/dist/src/cli/directCommands.js +143 -0
- package/dist/src/cli/envLoader.js +16 -0
- package/dist/src/cli/exportCommand.js +53 -0
- package/dist/src/cli/index.js +203 -0
- package/dist/src/cli/ioContext.js +109 -0
- package/dist/src/cli/preflight.js +57 -0
- package/dist/src/cli/statusCommand.js +110 -0
- package/dist/src/cli/workshopCommand.js +400 -0
- package/dist/src/develop/checkpointState.js +86 -0
- package/dist/src/develop/codeGenerator.js +319 -0
- package/dist/src/develop/dynamicScaffolder.js +226 -0
- package/dist/src/develop/githubMcpAdapter.js +122 -0
- package/dist/src/develop/index.js +15 -0
- package/dist/src/develop/mcpContextEnricher.js +195 -0
- package/dist/src/develop/pocScaffolder.js +542 -0
- package/dist/src/develop/ralphLoop.js +659 -0
- package/dist/src/develop/templateRegistry.js +364 -0
- package/dist/src/develop/testRunner.js +202 -0
- package/dist/src/logging/logger.js +58 -0
- package/dist/src/loop/conversationLoop.js +227 -0
- package/dist/src/loop/phaseSummarizer.js +87 -0
- package/dist/src/mcp/mcpManager.js +267 -0
- package/dist/src/mcp/mcpTransport.js +391 -0
- package/dist/src/mcp/retryPolicy.js +47 -0
- package/dist/src/mcp/webSearch.js +254 -0
- package/dist/src/phases/contextSummarizer.js +101 -0
- package/dist/src/phases/discoveryEnricher.js +156 -0
- package/dist/src/phases/phaseExtractors.js +222 -0
- package/dist/src/phases/phaseHandlers.js +328 -0
- package/dist/src/prompts/design.md +51 -0
- package/dist/src/prompts/develop-boundary.md +51 -0
- package/dist/src/prompts/develop.md +111 -0
- package/dist/src/prompts/discover.md +58 -0
- package/dist/src/prompts/ideate.md +56 -0
- package/dist/src/prompts/plan.md +51 -0
- package/dist/src/prompts/promptLoader.js +167 -0
- package/dist/src/prompts/promptLoader.ts +198 -0
- package/dist/src/prompts/select.md +47 -0
- package/dist/src/prompts/summarize/README.md +8 -0
- package/dist/src/prompts/summarize/design-summary.md +37 -0
- package/dist/src/prompts/summarize/develop-summary.md +25 -0
- package/dist/src/prompts/summarize/ideate-summary.md +27 -0
- package/dist/src/prompts/summarize/plan-summary.md +27 -0
- package/dist/src/prompts/summarize/select-summary.md +21 -0
- package/dist/src/prompts/system.md +28 -0
- package/dist/src/sessions/exportPaths.js +22 -0
- package/dist/src/sessions/exportWriter.js +406 -0
- package/dist/src/sessions/sessionManager.js +81 -0
- package/dist/src/sessions/sessionStore.js +65 -0
- package/dist/src/shared/activitySpinner.js +91 -0
- package/dist/src/shared/copilotClient.js +129 -0
- package/dist/src/shared/data/cards.json +1249 -0
- package/dist/src/shared/data/cardsLoader.js +51 -0
- package/dist/src/shared/errorClassifier.js +120 -0
- package/dist/src/shared/events.js +28 -0
- package/dist/src/shared/markdownRenderer.js +34 -0
- package/dist/src/shared/schemas/session.js +265 -0
- package/dist/src/shared/tableRenderer.js +20 -0
- package/dist/src/vendor/chalk.js +2 -0
- package/dist/src/vendor/cli-table3.js +3 -0
- package/dist/src/vendor/commander.js +2 -0
- package/dist/src/vendor/marked-terminal.js +3 -0
- package/dist/src/vendor/marked.js +2 -0
- package/dist/src/vendor/ora.js +2 -0
- package/dist/src/vendor/pino.js +2 -0
- package/dist/src/vendor/zod.js +2 -0
- package/dist/tests/e2e/developE2e.spec.js +126 -0
- package/dist/tests/e2e/developFailureE2e.spec.js +247 -0
- package/dist/tests/e2e/developPty.spec.js +75 -0
- package/dist/tests/e2e/discoveryWebSearchRelevance.spec.js +84 -0
- package/dist/tests/e2e/harness.spec.js +83 -0
- package/dist/tests/e2e/mcpLive.spec.js +120 -0
- package/dist/tests/e2e/newSession.e2e.spec.js +177 -0
- package/dist/tests/e2e/ralphLoopEnrichmentComparison.spec.js +62 -0
- package/dist/tests/e2e/workiqEnrichment.spec.js +56 -0
- package/dist/tests/e2e/zavaSimulation.spec.js +452 -0
- package/dist/tests/fixtures/test-fixture-project/src/add.js +3 -0
- package/dist/tests/fixtures/test-fixture-project/tests/failing.test.js +6 -0
- package/dist/tests/fixtures/test-fixture-project/tests/hanging.test.js +8 -0
- package/dist/tests/fixtures/test-fixture-project/tests/passing.test.js +10 -0
- package/dist/tests/fixtures/test-fixture-project/vitest.config.js +6 -0
- package/dist/tests/integration/autoStartConversation.spec.js +138 -0
- package/dist/tests/integration/defaultCommand.spec.js +147 -0
- package/dist/tests/integration/directCommandNonTty.spec.js +224 -0
- package/dist/tests/integration/directCommandTty.spec.js +151 -0
- package/dist/tests/integration/discoveryEnrichmentFlow.spec.js +175 -0
- package/dist/tests/integration/exportArtifacts.spec.js +202 -0
- package/dist/tests/integration/exportFallbackFlow.spec.js +99 -0
- package/dist/tests/integration/mcpDegradationFlow.spec.js +190 -0
- package/dist/tests/integration/mcpTransportFlow.spec.js +139 -0
- package/dist/tests/integration/newSessionFlow.spec.js +343 -0
- package/dist/tests/integration/pocGithubMcp.spec.js +186 -0
- package/dist/tests/integration/pocLocalFallback.spec.js +171 -0
- package/dist/tests/integration/pocScaffold.spec.js +163 -0
- package/dist/tests/integration/ralphLoopFlow.spec.js +359 -0
- package/dist/tests/integration/ralphLoopPartial.spec.js +368 -0
- package/dist/tests/integration/resumeAndBacktrack.spec.js +247 -0
- package/dist/tests/integration/spinnerLifecycle.spec.js +220 -0
- package/dist/tests/integration/summarizationFlow.spec.js +115 -0
- package/dist/tests/integration/testRunnerReal.spec.js +52 -0
- package/dist/tests/integration/webSearchAgent.spec.js +128 -0
- package/dist/tests/live/copilotSdkLive.spec.js +107 -0
- package/dist/tests/live/zavaFullWorkshop.spec.js +392 -0
- package/dist/tests/setup/loadEnv.js +3 -0
- package/dist/tests/unit/cli/developCommand.spec.js +567 -0
- package/dist/tests/unit/cli/directCommands.spec.js +279 -0
- package/dist/tests/unit/cli/envLoader.spec.js +58 -0
- package/dist/tests/unit/cli/ioContext.spec.js +119 -0
- package/dist/tests/unit/cli/preflight.spec.js +108 -0
- package/dist/tests/unit/cli/statusCommand.spec.js +111 -0
- package/dist/tests/unit/cli/workshopClientFallback.spec.js +80 -0
- package/dist/tests/unit/cli/workshopCommand.spec.js +329 -0
- package/dist/tests/unit/config/vitestEnvSetup.spec.js +13 -0
- package/dist/tests/unit/develop/checkpointState.spec.js +315 -0
- package/dist/tests/unit/develop/codeGenerator.spec.js +355 -0
- package/dist/tests/unit/develop/githubMcpAdapter.spec.js +231 -0
- package/dist/tests/unit/develop/mcpContextEnricher.spec.js +433 -0
- package/dist/tests/unit/develop/outputValidator.spec.js +119 -0
- package/dist/tests/unit/develop/pocScaffolder.spec.js +353 -0
- package/dist/tests/unit/develop/ralphLoop.spec.js +1248 -0
- package/dist/tests/unit/develop/templateRegistry.spec.js +85 -0
- package/dist/tests/unit/develop/testRunner.spec.js +249 -0
- package/dist/tests/unit/infraBicep.spec.js +92 -0
- package/dist/tests/unit/infraDeploy.spec.js +82 -0
- package/dist/tests/unit/infraTeardown.spec.js +63 -0
- package/dist/tests/unit/logging/logger.spec.js +43 -0
- package/dist/tests/unit/loop/conversationLoop.spec.js +592 -0
- package/dist/tests/unit/loop/phaseSummarizer.spec.js +141 -0
- package/dist/tests/unit/loop/streamingMarkdown.spec.js +147 -0
- package/dist/tests/unit/mcp/mcpManager.spec.js +279 -0
- package/dist/tests/unit/mcp/mcpTransport.spec.js +529 -0
- package/dist/tests/unit/mcp/retryPolicy.spec.js +218 -0
- package/dist/tests/unit/mcp/timeoutValidation.spec.js +46 -0
- package/dist/tests/unit/mcp/webSearch.spec.js +567 -0
- package/dist/tests/unit/phases/contextSummarizer.spec.js +140 -0
- package/dist/tests/unit/phases/discoveryEnricher.repeatCalls.spec.js +93 -0
- package/dist/tests/unit/phases/discoveryEnricher.spec.js +411 -0
- package/dist/tests/unit/phases/phaseExtractors.spec.js +352 -0
- package/dist/tests/unit/phases/phaseHandlers.spec.js +425 -0
- package/dist/tests/unit/prompts/promptLoader.spec.js +118 -0
- package/dist/tests/unit/schemas/pocSchemas.spec.js +412 -0
- package/dist/tests/unit/schemas/session.spec.js +257 -0
- package/dist/tests/unit/sessions/exportPaths.spec.js +31 -0
- package/dist/tests/unit/sessions/exportWriter.spec.js +655 -0
- package/dist/tests/unit/sessions/sessionManager.spec.js +151 -0
- package/dist/tests/unit/sessions/sessionStore.spec.js +116 -0
- package/dist/tests/unit/shared/activitySpinner.spec.js +175 -0
- package/dist/tests/unit/shared/cardsLoader.spec.js +76 -0
- package/dist/tests/unit/shared/copilotClient.spec.js +155 -0
- package/dist/tests/unit/shared/errorClassifier.spec.js +131 -0
- package/dist/tests/unit/shared/events.spec.js +55 -0
- package/dist/tests/unit/shared/markdownRenderer.spec.js +35 -0
- package/dist/tests/unit/shared/markdownRendererChunks.spec.js +70 -0
- package/dist/tests/unit/shared/tableRenderer.spec.js +34 -0
- package/dist/vitest.config.js +14 -0
- package/dist/vitest.live.config.js +18 -0
- package/docs/README.md +35 -0
- package/docs/architecture.md +169 -0
- package/docs/cli-usage.md +207 -0
- package/docs/environment.md +66 -0
- package/docs/export-format.md +146 -0
- package/docs/session-model.md +113 -0
- package/eslint.config.js +35 -0
- package/infra/deploy.sh +193 -0
- package/infra/gather-env.sh +211 -0
- package/infra/main.bicep +90 -0
- package/infra/main.bicepparam +18 -0
- package/infra/resources.bicep +134 -0
- package/infra/teardown.sh +114 -0
- package/package.json +63 -0
- package/specs/001-cli-workshop-rebuild/checklists/requirements.md +35 -0
- package/specs/001-cli-workshop-rebuild/contracts/cli.md +59 -0
- package/specs/001-cli-workshop-rebuild/contracts/export-summary-json.md +23 -0
- package/specs/001-cli-workshop-rebuild/contracts/session-json.md +30 -0
- package/specs/001-cli-workshop-rebuild/data-model.md +210 -0
- package/specs/001-cli-workshop-rebuild/plan.md +361 -0
- package/specs/001-cli-workshop-rebuild/quickstart.md +83 -0
- package/specs/001-cli-workshop-rebuild/research.md +116 -0
- package/specs/001-cli-workshop-rebuild/spec.md +240 -0
- package/specs/001-cli-workshop-rebuild/tasks.md +476 -0
- package/specs/002-poc-generation/contracts/poc-output.md +172 -0
- package/specs/002-poc-generation/contracts/ralph-loop.md +113 -0
- package/specs/002-poc-generation/data-model.md +172 -0
- package/specs/002-poc-generation/plan.md +109 -0
- package/specs/002-poc-generation/quickstart.md +97 -0
- package/specs/002-poc-generation/research.md +786 -0
- package/specs/002-poc-generation/spec.md +81 -0
- package/specs/002-poc-generation/tasks-fix.md +198 -0
- package/specs/002-poc-generation/tasks.md +252 -0
- package/specs/003-mcp-transport-integration/checklists/requirements.md +37 -0
- package/specs/003-mcp-transport-integration/contracts/context-enricher.md +220 -0
- package/specs/003-mcp-transport-integration/contracts/discovery-enricher.md +267 -0
- package/specs/003-mcp-transport-integration/contracts/github-adapter.md +149 -0
- package/specs/003-mcp-transport-integration/contracts/mcp-transport.md +288 -0
- package/specs/003-mcp-transport-integration/data-model.md +326 -0
- package/specs/003-mcp-transport-integration/plan.md +114 -0
- package/specs/003-mcp-transport-integration/quickstart.md +311 -0
- package/specs/003-mcp-transport-integration/research.md +395 -0
- package/specs/003-mcp-transport-integration/spec.md +234 -0
- package/specs/003-mcp-transport-integration/tasks.md +324 -0
- package/specs/003-next-spec-gaps.md +150 -0
- package/specs/004-dev-resume-hardening/checklists/requirements.md +37 -0
- package/specs/004-dev-resume-hardening/contracts/cli.md +160 -0
- package/specs/004-dev-resume-hardening/data-model.md +321 -0
- package/specs/004-dev-resume-hardening/plan.md +107 -0
- package/specs/004-dev-resume-hardening/quickstart.md +115 -0
- package/specs/004-dev-resume-hardening/research.md +142 -0
- package/specs/004-dev-resume-hardening/spec.md +221 -0
- package/specs/004-dev-resume-hardening/tasks.md +333 -0
- package/specs/005-ai-search-deploy/checklists/requirements.md +39 -0
- package/specs/005-ai-search-deploy/contracts/web-search-tool.md +241 -0
- package/specs/005-ai-search-deploy/data-model.md +130 -0
- package/specs/005-ai-search-deploy/plan.md +93 -0
- package/specs/005-ai-search-deploy/quickstart.md +96 -0
- package/specs/005-ai-search-deploy/research.md +187 -0
- package/specs/005-ai-search-deploy/spec.md +143 -0
- package/specs/005-ai-search-deploy/tasks.md +284 -0
- package/specs/006-workshop-extraction-fixes/checklists/requirements.md +61 -0
- package/specs/006-workshop-extraction-fixes/contracts/summarization-and-export.md +131 -0
- package/specs/006-workshop-extraction-fixes/data-model.md +149 -0
- package/specs/006-workshop-extraction-fixes/plan.md +123 -0
- package/specs/006-workshop-extraction-fixes/quickstart.md +101 -0
- package/specs/006-workshop-extraction-fixes/research.md +143 -0
- package/specs/006-workshop-extraction-fixes/spec.md +210 -0
- package/specs/006-workshop-extraction-fixes/tasks.md +316 -0
- package/src/cli/developCommand.ts +308 -0
- package/src/cli/directCommands.ts +195 -0
- package/src/cli/envLoader.ts +17 -0
- package/src/cli/exportCommand.ts +65 -0
- package/src/cli/index.ts +249 -0
- package/src/cli/ioContext.ts +139 -0
- package/src/cli/preflight.ts +86 -0
- package/src/cli/statusCommand.ts +118 -0
- package/src/cli/workshopCommand.ts +496 -0
- package/src/develop/checkpointState.ts +121 -0
- package/src/develop/codeGenerator.ts +402 -0
- package/src/develop/dynamicScaffolder.ts +284 -0
- package/src/develop/githubMcpAdapter.ts +199 -0
- package/src/develop/index.ts +34 -0
- package/src/develop/mcpContextEnricher.ts +279 -0
- package/src/develop/pocScaffolder.ts +646 -0
- package/src/develop/ralphLoop.ts +1044 -0
- package/src/develop/templateRegistry.ts +427 -0
- package/src/develop/testRunner.ts +276 -0
- package/src/logging/logger.ts +73 -0
- package/src/loop/conversationLoop.ts +355 -0
- package/src/loop/phaseSummarizer.ts +114 -0
- package/src/mcp/mcpManager.ts +365 -0
- package/src/mcp/mcpTransport.ts +562 -0
- package/src/mcp/retryPolicy.ts +87 -0
- package/src/mcp/webSearch.ts +388 -0
- package/src/originalPrompts/design_thinking.md +178 -0
- package/src/originalPrompts/design_thinking_persona.md +76 -0
- package/src/originalPrompts/document_generator_example.md +77 -0
- package/src/originalPrompts/document_generator_persona.md +47 -0
- package/src/originalPrompts/facilitator_persona.md +125 -0
- package/src/originalPrompts/guardrails.md +47 -0
- package/src/phases/contextSummarizer.ts +154 -0
- package/src/phases/discoveryEnricher.ts +223 -0
- package/src/phases/phaseExtractors.ts +247 -0
- package/src/phases/phaseHandlers.ts +450 -0
- package/src/prompts/design.md +51 -0
- package/src/prompts/develop-boundary.md +51 -0
- package/src/prompts/develop.md +111 -0
- package/src/prompts/discover.md +58 -0
- package/src/prompts/ideate.md +56 -0
- package/src/prompts/plan.md +51 -0
- package/src/prompts/promptLoader.ts +198 -0
- package/src/prompts/select.md +47 -0
- package/src/prompts/summarize/README.md +8 -0
- package/src/prompts/summarize/design-summary.md +37 -0
- package/src/prompts/summarize/develop-summary.md +25 -0
- package/src/prompts/summarize/ideate-summary.md +27 -0
- package/src/prompts/summarize/plan-summary.md +27 -0
- package/src/prompts/summarize/select-summary.md +21 -0
- package/src/prompts/system.md +28 -0
- package/src/sessions/exportPaths.ts +28 -0
- package/src/sessions/exportWriter.ts +490 -0
- package/src/sessions/sessionManager.ts +119 -0
- package/src/sessions/sessionStore.ts +69 -0
- package/src/shared/activitySpinner.ts +108 -0
- package/src/shared/copilotClient.ts +291 -0
- package/src/shared/data/cards.json +1249 -0
- package/src/shared/data/cardsLoader.ts +70 -0
- package/src/shared/errorClassifier.ts +160 -0
- package/src/shared/events.ts +103 -0
- package/src/shared/markdownRenderer.ts +44 -0
- package/src/shared/schemas/session.ts +346 -0
- package/src/shared/tableRenderer.ts +28 -0
- package/src/types/marked-terminal.d.ts +5 -0
- package/src/vendor/chalk.ts +2 -0
- package/src/vendor/cli-table3.ts +3 -0
- package/src/vendor/commander.ts +2 -0
- package/src/vendor/marked-terminal.ts +3 -0
- package/src/vendor/marked.ts +2 -0
- package/src/vendor/ora.ts +2 -0
- package/src/vendor/pino.ts +3 -0
- package/src/vendor/zod.ts +3 -0
- package/tests/e2e/developE2e.spec.ts +152 -0
- package/tests/e2e/developFailureE2e.spec.ts +289 -0
- package/tests/e2e/developPty.spec.ts +86 -0
- package/tests/e2e/discoveryWebSearchRelevance.spec.ts +103 -0
- package/tests/e2e/harness.spec.ts +104 -0
- package/tests/e2e/mcpLive.spec.ts +149 -0
- package/tests/e2e/newSession.e2e.spec.ts +245 -0
- package/tests/e2e/ralphLoopEnrichmentComparison.spec.ts +70 -0
- package/tests/e2e/workiqEnrichment.spec.ts +72 -0
- package/tests/e2e/zava-assessment/agent-interaction-script.md +258 -0
- package/tests/e2e/zava-assessment/company-profile.md +98 -0
- package/tests/e2e/zava-assessment/expected-results-checklist.md +454 -0
- package/tests/e2e/zavaSimulation.spec.ts +511 -0
- package/tests/fixtures/completedSession.json +141 -0
- package/tests/fixtures/test-fixture-project/package-lock.json +1585 -0
- package/tests/fixtures/test-fixture-project/package.json +12 -0
- package/tests/fixtures/test-fixture-project/src/add.ts +3 -0
- package/tests/fixtures/test-fixture-project/tests/failing.test.ts +7 -0
- package/tests/fixtures/test-fixture-project/tests/hanging.test.ts +9 -0
- package/tests/fixtures/test-fixture-project/tests/passing.test.ts +13 -0
- package/tests/fixtures/test-fixture-project/vitest.config.ts +7 -0
- package/tests/integration/autoStartConversation.spec.ts +168 -0
- package/tests/integration/defaultCommand.spec.ts +179 -0
- package/tests/integration/directCommandNonTty.spec.ts +260 -0
- package/tests/integration/directCommandTty.spec.ts +185 -0
- package/tests/integration/discoveryEnrichmentFlow.spec.ts +209 -0
- package/tests/integration/exportArtifacts.spec.ts +232 -0
- package/tests/integration/exportFallbackFlow.spec.ts +115 -0
- package/tests/integration/mcpDegradationFlow.spec.ts +231 -0
- package/tests/integration/mcpTransportFlow.spec.ts +178 -0
- package/tests/integration/newSessionFlow.spec.ts +406 -0
- package/tests/integration/pocGithubMcp.spec.ts +224 -0
- package/tests/integration/pocLocalFallback.spec.ts +205 -0
- package/tests/integration/pocScaffold.spec.ts +220 -0
- package/tests/integration/ralphLoopFlow.spec.ts +430 -0
- package/tests/integration/ralphLoopPartial.spec.ts +416 -0
- package/tests/integration/resumeAndBacktrack.spec.ts +278 -0
- package/tests/integration/spinnerLifecycle.spec.ts +270 -0
- package/tests/integration/summarizationFlow.spec.ts +135 -0
- package/tests/integration/testRunnerReal.spec.ts +63 -0
- package/tests/integration/webSearchAgent.spec.ts +155 -0
- package/tests/live/copilotSdkLive.spec.ts +149 -0
- package/tests/live/zavaFullWorkshop.spec.ts +515 -0
- package/tests/setup/loadEnv.ts +5 -0
- package/tests/unit/cli/developCommand.spec.ts +679 -0
- package/tests/unit/cli/directCommands.spec.ts +325 -0
- package/tests/unit/cli/envLoader.spec.ts +73 -0
- package/tests/unit/cli/ioContext.spec.ts +148 -0
- package/tests/unit/cli/preflight.spec.ts +125 -0
- package/tests/unit/cli/statusCommand.spec.ts +134 -0
- package/tests/unit/cli/workshopClientFallback.spec.ts +100 -0
- package/tests/unit/cli/workshopCommand.spec.ts +378 -0
- package/tests/unit/config/vitestEnvSetup.spec.ts +24 -0
- package/tests/unit/develop/checkpointState.spec.ts +378 -0
- package/tests/unit/develop/codeGenerator.spec.ts +447 -0
- package/tests/unit/develop/githubMcpAdapter.spec.ts +283 -0
- package/tests/unit/develop/mcpContextEnricher.spec.ts +564 -0
- package/tests/unit/develop/outputValidator.spec.ts +134 -0
- package/tests/unit/develop/pocScaffolder.spec.ts +451 -0
- package/tests/unit/develop/ralphLoop.spec.ts +1439 -0
- package/tests/unit/develop/templateRegistry.spec.ts +106 -0
- package/tests/unit/develop/testRunner.spec.ts +294 -0
- package/tests/unit/infraBicep.spec.ts +116 -0
- package/tests/unit/infraDeploy.spec.ts +102 -0
- package/tests/unit/infraTeardown.spec.ts +77 -0
- package/tests/unit/logging/logger.spec.ts +50 -0
- package/tests/unit/loop/conversationLoop.spec.ts +719 -0
- package/tests/unit/loop/phaseSummarizer.spec.ts +169 -0
- package/tests/unit/loop/streamingMarkdown.spec.ts +180 -0
- package/tests/unit/mcp/mcpManager.spec.ts +336 -0
- package/tests/unit/mcp/mcpTransport.spec.ts +689 -0
- package/tests/unit/mcp/retryPolicy.spec.ts +278 -0
- package/tests/unit/mcp/timeoutValidation.spec.ts +55 -0
- package/tests/unit/mcp/webSearch.spec.ts +718 -0
- package/tests/unit/phases/contextSummarizer.spec.ts +158 -0
- package/tests/unit/phases/discoveryEnricher.repeatCalls.spec.ts +125 -0
- package/tests/unit/phases/discoveryEnricher.spec.ts +512 -0
- package/tests/unit/phases/phaseExtractors.spec.ts +406 -0
- package/tests/unit/phases/phaseHandlers.spec.ts +483 -0
- package/tests/unit/prompts/promptLoader.spec.ts +144 -0
- package/tests/unit/schemas/pocSchemas.spec.ts +457 -0
- package/tests/unit/schemas/session.spec.ts +328 -0
- package/tests/unit/sessions/exportPaths.spec.ts +38 -0
- package/tests/unit/sessions/exportWriter.spec.ts +737 -0
- package/tests/unit/sessions/sessionManager.spec.ts +174 -0
- package/tests/unit/sessions/sessionStore.spec.ts +136 -0
- package/tests/unit/shared/activitySpinner.spec.ts +211 -0
- package/tests/unit/shared/cardsLoader.spec.ts +89 -0
- package/tests/unit/shared/copilotClient.spec.ts +185 -0
- package/tests/unit/shared/errorClassifier.spec.ts +152 -0
- package/tests/unit/shared/events.spec.ts +71 -0
- package/tests/unit/shared/markdownRenderer.spec.ts +42 -0
- package/tests/unit/shared/markdownRendererChunks.spec.ts +83 -0
- package/tests/unit/shared/tableRenderer.spec.ts +38 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +15 -0
- 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
|
+
}
|