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,391 @@
1
+ /**
2
+ * MCP Transport Layer.
3
+ *
4
+ * Provides the `McpTransport` interface and two implementations:
5
+ * - `StdioMcpTransport`: subprocess-based JSON-RPC 2.0 over stdin/stdout
6
+ * - `HttpMcpTransport`: stateless HTTPS JSON-RPC 2.0 via native fetch()
7
+ *
8
+ * These transports handle the *programmatic adapter path* — deterministic tool
9
+ * calls made by application code (GitHub adapter, Context7 enricher, Azure
10
+ * enricher) that bypass the LLM.
11
+ *
12
+ * LLM-initiated tool calls go through the Copilot SDK's native `mcpServers`
13
+ * support. See research.md Topic 1 for the dual-path architecture.
14
+ */
15
+ import { spawn, execSync } from 'node:child_process';
16
+ import { createInterface } from 'node:readline';
17
+ /**
18
+ * Error type for MCP transport failures.
19
+ * Callers use `classifyMcpError()` to determine retry eligibility.
20
+ */
21
+ export class McpTransportError extends Error {
22
+ serverName;
23
+ toolName;
24
+ cause;
25
+ constructor(message, serverName, toolName, cause) {
26
+ super(message);
27
+ this.serverName = serverName;
28
+ this.toolName = toolName;
29
+ this.cause = cause;
30
+ this.name = 'McpTransportError';
31
+ }
32
+ }
33
+ function normalizeRpcErrorMessage(message) {
34
+ const trimmed = message.trim();
35
+ const parseValidationArray = (raw) => {
36
+ try {
37
+ const parsed = JSON.parse(raw);
38
+ if (!Array.isArray(parsed) || parsed.length === 0) {
39
+ return null;
40
+ }
41
+ const details = parsed
42
+ .map((item) => {
43
+ const path = Array.isArray(item.path) && item.path.length > 0 ? item.path.join('.') : 'input';
44
+ const detail = item.message ?? 'Invalid input';
45
+ return `${path}: ${detail}`;
46
+ })
47
+ .join('; ');
48
+ return details;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ };
54
+ const arraySuffixMatch = trimmed.match(/^(.*?):\s*(\[[\s\S]*\])$/);
55
+ if (arraySuffixMatch) {
56
+ const details = parseValidationArray(arraySuffixMatch[2]);
57
+ if (details) {
58
+ return `${arraySuffixMatch[1]}: ${details}`;
59
+ }
60
+ }
61
+ const wholeArrayDetails = parseValidationArray(trimmed);
62
+ if (wholeArrayDetails) {
63
+ return `Input validation error: ${wholeArrayDetails}`;
64
+ }
65
+ return trimmed;
66
+ }
67
+ // ── StdioMcpTransport ────────────────────────────────────────────────────────
68
+ /**
69
+ * MCP transport over stdio subprocess (JSON-RPC 2.0 newline-delimited).
70
+ *
71
+ * Used for: Context7, Azure MCP, WorkIQ, Playwright.
72
+ */
73
+ export class StdioMcpTransport {
74
+ config;
75
+ logger;
76
+ process = null;
77
+ pendingRequests = new Map();
78
+ nextId = 1;
79
+ connected = false;
80
+ constructor(config, logger) {
81
+ this.config = config;
82
+ this.logger = logger;
83
+ }
84
+ /**
85
+ * Spawn the subprocess and perform the JSON-RPC `initialize` handshake.
86
+ * Must be called before `callTool()`.
87
+ */
88
+ async connect() {
89
+ const { command, args, env, cwd, name } = this.config;
90
+ const child = spawn(command, args, {
91
+ stdio: ['pipe', 'pipe', 'pipe'],
92
+ env: { ...process.env, ...env },
93
+ ...(cwd ? { cwd } : {}),
94
+ });
95
+ this.process = child;
96
+ // Parse stdout lines as JSON-RPC responses
97
+ const rl = createInterface({ input: child.stdout });
98
+ rl.on('line', (line) => {
99
+ try {
100
+ const msg = JSON.parse(line);
101
+ if (msg.id != null) {
102
+ const pending = this.pendingRequests.get(msg.id);
103
+ if (pending) {
104
+ clearTimeout(pending.timer);
105
+ this.pendingRequests.delete(msg.id);
106
+ if (msg.error) {
107
+ pending.reject(new McpTransportError(normalizeRpcErrorMessage(msg.error.message), name, 'rpc-error'));
108
+ }
109
+ else {
110
+ pending.resolve({
111
+ content: this.extractContent(msg.result),
112
+ raw: msg,
113
+ });
114
+ }
115
+ }
116
+ }
117
+ }
118
+ catch {
119
+ this.logger.debug({ line: line.slice(0, 200) }, 'Skipping non-JSON stdout line');
120
+ }
121
+ });
122
+ // Handle subprocess exit
123
+ child.on('exit', (code) => {
124
+ this.connected = false;
125
+ // Reject all pending requests
126
+ for (const [id, pending] of this.pendingRequests) {
127
+ clearTimeout(pending.timer);
128
+ this.pendingRequests.delete(id);
129
+ const err = new McpTransportError(`MCP subprocess exited with code ${code}`, name, 'subprocess-exit');
130
+ err.code = 'ECONNREFUSED';
131
+ pending.reject(err);
132
+ }
133
+ });
134
+ // Send initialize handshake
135
+ const initId = this.nextId++;
136
+ const initResult = await new Promise((resolve, reject) => {
137
+ const timer = setTimeout(() => {
138
+ this.pendingRequests.delete(initId);
139
+ child.kill('SIGTERM');
140
+ const err = new McpTransportError(`MCP stdio server '${name}' initialization timed out after 5 seconds`, name, 'initialize');
141
+ err.code = 'ETIMEDOUT';
142
+ reject(err);
143
+ }, 5000);
144
+ this.pendingRequests.set(initId, {
145
+ resolve: () => {
146
+ resolve();
147
+ },
148
+ reject,
149
+ timer,
150
+ });
151
+ const initRequest = JSON.stringify({
152
+ jsonrpc: '2.0',
153
+ id: initId,
154
+ method: 'initialize',
155
+ params: {
156
+ protocolVersion: '1.0',
157
+ clientInfo: { name: 'sofIA', version: '0.1.0' },
158
+ capabilities: {},
159
+ },
160
+ });
161
+ child.stdin.write(initRequest + '\n');
162
+ });
163
+ void initResult;
164
+ this.connected = true;
165
+ this.logger.info({ server: name }, 'MCP stdio server connected');
166
+ }
167
+ async callTool(toolName, args, timeoutMs) {
168
+ if (!this.connected || !this.process) {
169
+ throw new McpTransportError(`Transport not connected for server '${this.config.name}'`, this.config.name, toolName);
170
+ }
171
+ const id = this.nextId++;
172
+ return new Promise((resolve, reject) => {
173
+ const timer = setTimeout(() => {
174
+ this.pendingRequests.delete(id);
175
+ const err = new McpTransportError(`MCP tool call timed out after ${timeoutMs}ms: ${this.config.name}.${toolName}`, this.config.name, toolName);
176
+ err.code = 'ETIMEDOUT';
177
+ reject(err);
178
+ }, timeoutMs);
179
+ this.pendingRequests.set(id, { resolve, reject, timer });
180
+ const request = JSON.stringify({
181
+ jsonrpc: '2.0',
182
+ id,
183
+ method: 'tools/call',
184
+ params: { name: toolName, arguments: args },
185
+ });
186
+ this.process.stdin.write(request + '\n');
187
+ });
188
+ }
189
+ isConnected() {
190
+ return this.connected;
191
+ }
192
+ async disconnect() {
193
+ // Reject all pending requests
194
+ for (const [id, pending] of this.pendingRequests) {
195
+ clearTimeout(pending.timer);
196
+ this.pendingRequests.delete(id);
197
+ pending.reject(new McpTransportError('Transport disconnected', this.config.name, 'disconnect'));
198
+ }
199
+ if (this.process) {
200
+ this.process.kill('SIGTERM');
201
+ this.process = null;
202
+ }
203
+ this.connected = false;
204
+ }
205
+ /**
206
+ * Extract content from a JSON-RPC result.
207
+ * MCP responses may have `result.content[0].text` or just `result` directly.
208
+ */
209
+ extractContent(result) {
210
+ if (result && typeof result === 'object') {
211
+ const r = result;
212
+ if (Array.isArray(r.content) && r.content.length > 0) {
213
+ const first = r.content[0];
214
+ if (typeof first.text === 'string') {
215
+ return first.text;
216
+ }
217
+ return first;
218
+ }
219
+ return r;
220
+ }
221
+ return String(result);
222
+ }
223
+ }
224
+ // ── HttpMcpTransport ─────────────────────────────────────────────────────────
225
+ /**
226
+ * Retrieve GitHub token from GitHub CLI if available.
227
+ * @returns Token string or null if GitHub CLI is not installed/authenticated.
228
+ */
229
+ function getGitHubCliToken() {
230
+ try {
231
+ const token = execSync('gh auth token', {
232
+ encoding: 'utf8',
233
+ stdio: ['pipe', 'pipe', 'ignore'], // suppress stderr
234
+ timeout: 2000,
235
+ }).trim();
236
+ return token || null;
237
+ }
238
+ catch {
239
+ return null;
240
+ }
241
+ }
242
+ /**
243
+ * MCP transport over HTTPS (stateless JSON-RPC 2.0 via native fetch).
244
+ *
245
+ * Used for: GitHub MCP, Microsoft Docs MCP.
246
+ */
247
+ export class HttpMcpTransport {
248
+ config;
249
+ logger;
250
+ nextId = 1;
251
+ constructor(config, logger) {
252
+ this.config = config;
253
+ this.logger = logger;
254
+ }
255
+ async callTool(toolName, args, timeoutMs) {
256
+ const id = this.nextId++;
257
+ const body = {
258
+ jsonrpc: '2.0',
259
+ id,
260
+ method: 'tools/call',
261
+ params: { name: toolName, arguments: args },
262
+ };
263
+ // Build headers
264
+ const headers = {
265
+ 'Content-Type': 'application/json',
266
+ ...this.config.headers,
267
+ };
268
+ // Add auth token if available (GITHUB_TOKEN env var takes precedence, fallback to GitHub CLI)
269
+ const token = process.env.GITHUB_TOKEN || (this.config.name === 'github' ? getGitHubCliToken() : null);
270
+ if (token) {
271
+ headers['Authorization'] = `Bearer ${token}`;
272
+ }
273
+ // AbortController for timeout
274
+ const controller = new AbortController();
275
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
276
+ try {
277
+ const response = await fetch(this.config.url, {
278
+ method: 'POST',
279
+ headers,
280
+ body: JSON.stringify(body),
281
+ signal: controller.signal,
282
+ });
283
+ clearTimeout(timer);
284
+ // Handle HTTP error statuses
285
+ if (response.status === 401 || response.status === 403) {
286
+ const err = new McpTransportError(`HTTP ${response.status} from ${this.config.name}: authentication failed`, this.config.name, toolName);
287
+ err.code = 'ERR_TLS_CERT_ALTNAME_INVALID';
288
+ throw err;
289
+ }
290
+ if (response.status >= 500) {
291
+ throw new McpTransportError(`HTTP ${response.status} from ${this.config.name}: server error`, this.config.name, toolName);
292
+ }
293
+ // Parse response body (read as text first for better error reporting)
294
+ const bodyText = await response.text();
295
+ const contentType = response.headers.get('content-type') || '';
296
+ let parsed;
297
+ // Handle Server-Sent Events (SSE) format from GitHub Copilot MCP
298
+ if (contentType.includes('text/event-stream')) {
299
+ try {
300
+ // Parse SSE format: "event: message\ndata: {...}\n\n"
301
+ const dataMatch = bodyText.match(/^data:\s*(.+)$/m);
302
+ if (!dataMatch) {
303
+ throw new Error('No data field in SSE response');
304
+ }
305
+ parsed = JSON.parse(dataMatch[1]);
306
+ }
307
+ catch (_sseError) {
308
+ this.logger.error({ status: response.status, contentType, bodyPreview: bodyText.slice(0, 500) }, `Invalid SSE format from ${this.config.name}`);
309
+ throw new McpTransportError(`Invalid SSE format from ${this.config.name}`, this.config.name, toolName);
310
+ }
311
+ }
312
+ else {
313
+ // Regular JSON response
314
+ try {
315
+ parsed = JSON.parse(bodyText);
316
+ }
317
+ catch (_parseError) {
318
+ // Log response details for debugging
319
+ const preview = bodyText.slice(0, 500);
320
+ this.logger.error({ status: response.status, contentType, bodyPreview: preview }, `Non-JSON response from ${this.config.name}`);
321
+ // Also output to stderr for visibility in tests
322
+ console.error(`[McpTransport] Non-JSON response from ${this.config.name}:`);
323
+ console.error(` Status: ${response.status}`);
324
+ console.error(` Content-Type: ${contentType}`);
325
+ console.error(` Body preview: ${preview}`);
326
+ throw new McpTransportError(`Non-JSON response from ${this.config.name} (HTTP ${response.status})`, this.config.name, toolName);
327
+ }
328
+ }
329
+ // Handle JSON-RPC error
330
+ if (parsed.error) {
331
+ const rpcError = parsed.error;
332
+ throw new McpTransportError(normalizeRpcErrorMessage(rpcError.message ?? 'JSON-RPC error'), this.config.name, toolName);
333
+ }
334
+ return {
335
+ content: this.extractContent(parsed.result),
336
+ raw: parsed,
337
+ };
338
+ }
339
+ catch (err) {
340
+ clearTimeout(timer);
341
+ // Re-throw McpTransportError as-is
342
+ if (err instanceof McpTransportError) {
343
+ throw err;
344
+ }
345
+ // Handle AbortError (timeout)
346
+ if (err instanceof Error && err.name === 'AbortError') {
347
+ const timeoutErr = new McpTransportError(`MCP HTTP call timed out after ${timeoutMs}ms: ${this.config.name}.${toolName}`, this.config.name, toolName);
348
+ timeoutErr.code = 'ETIMEDOUT';
349
+ throw timeoutErr;
350
+ }
351
+ // Re-throw other errors as McpTransportError
352
+ throw new McpTransportError(err instanceof Error ? err.message : String(err), this.config.name, toolName, err);
353
+ }
354
+ }
355
+ isConnected() {
356
+ return true; // HTTP is stateless
357
+ }
358
+ async disconnect() {
359
+ // No-op for HTTP
360
+ }
361
+ /**
362
+ * Extract content from a JSON-RPC result.
363
+ */
364
+ extractContent(result) {
365
+ if (result && typeof result === 'object') {
366
+ const r = result;
367
+ if (Array.isArray(r.content) && r.content.length > 0) {
368
+ const first = r.content[0];
369
+ if (typeof first.text === 'string') {
370
+ return first.text;
371
+ }
372
+ return first;
373
+ }
374
+ return r;
375
+ }
376
+ return String(result);
377
+ }
378
+ }
379
+ // ── Factory ──────────────────────────────────────────────────────────────────
380
+ /**
381
+ * Create the correct transport implementation for a server config.
382
+ */
383
+ export function createTransport(config, logger) {
384
+ if (config.type === 'stdio') {
385
+ return new StdioMcpTransport(config, logger);
386
+ }
387
+ if (config.type === 'http') {
388
+ return new HttpMcpTransport(config, logger);
389
+ }
390
+ throw new Error(`Unsupported MCP transport type: ${config.type}`);
391
+ }
@@ -0,0 +1,47 @@
1
+ import { classifyMcpError } from './mcpManager.js';
2
+ // ── Retryable error classes ──────────────────────────────────────────────────
3
+ const RETRYABLE_CLASSES = new Set([
4
+ 'connection-refused',
5
+ 'timeout',
6
+ 'dns-failure',
7
+ ]);
8
+ /**
9
+ * Determine if an error is transient and should be retried.
10
+ */
11
+ function isRetryable(err) {
12
+ return RETRYABLE_CLASSES.has(classifyMcpError(err));
13
+ }
14
+ // ── withRetry ────────────────────────────────────────────────────────────────
15
+ /**
16
+ * Wrap an async function with a single-retry policy for transient MCP errors.
17
+ *
18
+ * On transient error: waits `initialDelayMs ± jitter`, then calls fn() once more.
19
+ * If the retry also fails, the second error is thrown (not the first).
20
+ *
21
+ * Non-retryable errors (auth-failure, unknown, validation) are thrown immediately.
22
+ */
23
+ export async function withRetry(fn, options) {
24
+ const { serverName, toolName, initialDelayMs = 1000, jitter = 0.2, logger } = options;
25
+ try {
26
+ return await fn();
27
+ }
28
+ catch (firstError) {
29
+ if (!isRetryable(firstError)) {
30
+ throw firstError;
31
+ }
32
+ // Calculate delay with jitter: initialDelayMs * (1 ± jitter)
33
+ const jitterFactor = 1 + (Math.random() * 2 - 1) * jitter;
34
+ const delayMs = Math.round(initialDelayMs * jitterFactor);
35
+ const errorClass = classifyMcpError(firstError);
36
+ logger?.warn({ server: serverName, tool: toolName, attempt: 1, delayMs, errorClass }, `MCP transient error — retrying after ${delayMs}ms`);
37
+ await sleep(delayMs);
38
+ // Second attempt — if this fails, its error is thrown
39
+ return await fn();
40
+ }
41
+ }
42
+ /** Internal sleep helper. */
43
+ function sleep(ms) {
44
+ return new Promise((resolve) => setTimeout(resolve, ms));
45
+ }
46
+ // Re-export for convenience
47
+ export { classifyMcpError, isRetryable };
@@ -0,0 +1,254 @@
1
+ // ── Tool definition ──────────────────────────────────────────────────────────
2
+ export const WEB_SEARCH_TOOL_DEFINITION = {
3
+ name: 'web.search',
4
+ description: 'Search the web for information about companies, industries, technologies, and trends. ' +
5
+ 'Returns structured results with title, URL, and snippet.',
6
+ parameters: {
7
+ type: 'object',
8
+ properties: {
9
+ query: {
10
+ type: 'string',
11
+ description: 'The search query string.',
12
+ },
13
+ },
14
+ required: ['query'],
15
+ },
16
+ };
17
+ // ── Configuration check ──────────────────────────────────────────────────────
18
+ /**
19
+ * Check if web search is configured via environment variables.
20
+ *
21
+ * Uses the new env vars (`FOUNDRY_PROJECT_ENDPOINT`, `FOUNDRY_MODEL_DEPLOYMENT_NAME`)
22
+ * instead of legacy vars (`SOFIA_FOUNDRY_AGENT_ENDPOINT`, `SOFIA_FOUNDRY_AGENT_KEY`).
23
+ */
24
+ export function isWebSearchConfigured() {
25
+ return Boolean(process.env.FOUNDRY_PROJECT_ENDPOINT && process.env.FOUNDRY_MODEL_DEPLOYMENT_NAME);
26
+ }
27
+ // ── Citation extraction ──────────────────────────────────────────────────────
28
+ /**
29
+ * Extract citations from a Foundry agent response.
30
+ *
31
+ * Parses `url_citation` annotations from the response output items and maps
32
+ * them into `WebSearchResultItem[]`. Deduplicates sources by URL.
33
+ */
34
+ export function extractCitations(output) {
35
+ const results = [];
36
+ const seenUrls = new Set();
37
+ for (const item of output) {
38
+ const messageItem = item;
39
+ if (messageItem.type !== 'message')
40
+ continue;
41
+ const content = messageItem.content;
42
+ if (!Array.isArray(content))
43
+ continue;
44
+ for (const contentBlock of content) {
45
+ const block = contentBlock;
46
+ if (block.type !== 'output_text')
47
+ continue;
48
+ const text = String(block.text ?? '');
49
+ const annotations = block.annotations;
50
+ if (!Array.isArray(annotations))
51
+ continue;
52
+ for (const annotation of annotations) {
53
+ const ann = annotation;
54
+ if (ann.type !== 'url_citation')
55
+ continue;
56
+ const url = String(ann.url ?? '');
57
+ const title = String(ann.title ?? url);
58
+ const startIndex = Number(ann.start_index ?? 0);
59
+ const endIndex = Number(ann.end_index ?? text.length);
60
+ const snippet = text.slice(startIndex, Math.min(endIndex, startIndex + 200)) || title;
61
+ if (url && !seenUrls.has(url)) {
62
+ seenUrls.add(url);
63
+ results.push({ title, url, snippet });
64
+ }
65
+ }
66
+ }
67
+ }
68
+ return { results, sources: [...seenUrls] };
69
+ }
70
+ /**
71
+ * Fallback extraction when a response has no URL citations.
72
+ *
73
+ * Some Foundry responses can contain only plain output text without
74
+ * `url_citation` annotations. This extracts text blocks into lightweight
75
+ * snippets so downstream enrichment still has useful context.
76
+ */
77
+ export function extractTextSnippets(output) {
78
+ const snippets = [];
79
+ for (const item of output) {
80
+ const messageItem = item;
81
+ if (messageItem.type !== 'message')
82
+ continue;
83
+ const content = messageItem.content;
84
+ if (!Array.isArray(content))
85
+ continue;
86
+ for (let i = 0; i < content.length; i++) {
87
+ const block = content[i];
88
+ if (block.type !== 'output_text')
89
+ continue;
90
+ const text = String(block.text ?? '').trim();
91
+ if (!text)
92
+ continue;
93
+ snippets.push({
94
+ title: 'Foundry response',
95
+ url: `foundry://response/${i + 1}`,
96
+ snippet: text.length > 300 ? `${text.slice(0, 300)}…` : text,
97
+ });
98
+ }
99
+ }
100
+ return snippets;
101
+ }
102
+ const MAX_QUERIES_PER_AGENT = 3;
103
+ const MAX_RETRIES = 2;
104
+ const RETRY_DELAY_MS = 2000; // 2 second initial delay
105
+ let sessionState = null;
106
+ let sessionDeps = null;
107
+ /**
108
+ * Create default dependencies using the real Azure SDK.
109
+ */
110
+ async function createDefaultDeps() {
111
+ const { AIProjectClient } = await import('@azure/ai-projects');
112
+ const { DefaultAzureCredential } = await import('@azure/identity');
113
+ return {
114
+ createClient: (endpoint) => new AIProjectClient(endpoint, new DefaultAzureCredential()),
115
+ getOpenAIClient: async (client) => client.getOpenAIClient(),
116
+ createAgentVersion: async (client, name, options) => {
117
+ const aiClient = client;
118
+ const result = await aiClient.agents.createVersion(name, options);
119
+ return { name: result.name, version: result.version };
120
+ },
121
+ deleteAgentVersion: async (client, name, version) => {
122
+ await client.agents.deleteVersion(name, version);
123
+ },
124
+ createConversation: async (openAIClient) => {
125
+ const oai = openAIClient;
126
+ return oai.conversations.create();
127
+ },
128
+ deleteConversation: async (openAIClient, id) => {
129
+ const oai = openAIClient;
130
+ await oai.conversations.delete(id);
131
+ },
132
+ createResponse: async (openAIClient, conversationId, input, agentName) => {
133
+ const oai = openAIClient;
134
+ return oai.responses.create({ conversation: conversationId, input }, { body: { agent: { name: agentName, type: 'agent_reference' } } });
135
+ },
136
+ };
137
+ }
138
+ // ── Tool factory ─────────────────────────────────────────────────────────────
139
+ async function sleep(ms) {
140
+ return new Promise((resolve) => setTimeout(resolve, ms));
141
+ }
142
+ async function cleanupCurrentAgent(keepDeps) {
143
+ if (!sessionState?.initialized || !sessionDeps) {
144
+ sessionState = null;
145
+ return;
146
+ }
147
+ const { client, agentName, agentVersion } = sessionState;
148
+ sessionState = null;
149
+ try {
150
+ await sessionDeps.deleteAgentVersion(client, agentName, agentVersion);
151
+ }
152
+ catch {
153
+ // Best-effort cleanup.
154
+ }
155
+ if (!keepDeps) {
156
+ sessionDeps = null;
157
+ }
158
+ }
159
+ /**
160
+ * Create a web search function that calls the Azure AI Foundry Agent Service.
161
+ *
162
+ * The returned function:
163
+ * - Lazily creates an ephemeral agent with web_search_preview on first call
164
+ * - Reuses the agent for subsequent calls
165
+ * - Rotates the agent after a few queries to avoid stale response behavior
166
+ * - Uses a fresh conversation per query to keep citation output stable
167
+ * - Returns structured results with URL citations
168
+ * - Degrades gracefully on errors (returns empty results with degraded flag)
169
+ *
170
+ * Pass `deps` for testing to inject mocked SDK clients.
171
+ */
172
+ export function createWebSearchTool(config, deps) {
173
+ sessionDeps = deps ?? null;
174
+ return async (query) => {
175
+ try {
176
+ const resolvedDeps = sessionDeps ?? (await createDefaultDeps());
177
+ sessionDeps = resolvedDeps;
178
+ if (sessionState?.initialized && sessionState.queryCount >= MAX_QUERIES_PER_AGENT) {
179
+ await cleanupCurrentAgent(true);
180
+ }
181
+ // Lazy initialization
182
+ if (!sessionState?.initialized) {
183
+ const client = resolvedDeps.createClient(config.projectEndpoint);
184
+ const openAIClient = await resolvedDeps.getOpenAIClient(client);
185
+ const agent = await resolvedDeps.createAgentVersion(client, 'sofia-web-search', {
186
+ kind: 'prompt',
187
+ model: config.modelDeploymentName,
188
+ instructions: 'You are a web search assistant. Search the web and return relevant results with citations.',
189
+ tools: [{ type: 'web_search_preview' }],
190
+ });
191
+ sessionState = {
192
+ client,
193
+ openAIClient,
194
+ agentName: agent.name,
195
+ agentVersion: agent.version,
196
+ queryCount: 0,
197
+ initialized: true,
198
+ };
199
+ }
200
+ // Execute query in an isolated conversation with retry logic for rate limiting.
201
+ const conversation = await sessionDeps.createConversation(sessionState.openAIClient);
202
+ let response;
203
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
204
+ try {
205
+ response = await sessionDeps.createResponse(sessionState.openAIClient, conversation.id, query, sessionState.agentName);
206
+ break;
207
+ }
208
+ catch (err) {
209
+ const message = err instanceof Error ? err.message : String(err);
210
+ // Check for 429 rate limiting
211
+ if (message.includes('429') && attempt < MAX_RETRIES) {
212
+ const delayMs = RETRY_DELAY_MS * Math.pow(2, attempt);
213
+ await sleep(delayMs);
214
+ continue;
215
+ }
216
+ throw err;
217
+ }
218
+ }
219
+ try {
220
+ await sessionDeps.deleteConversation(sessionState.openAIClient, conversation.id);
221
+ }
222
+ catch {
223
+ // Conversation cleanup failures should not fail web search results.
224
+ }
225
+ sessionState.queryCount += 1;
226
+ // Extract citations. response is guaranteed to be assigned by loop logic:
227
+ // either break assigns it, or catch throws (exiting function).
228
+ const { results: citationResults, sources } = extractCitations(response.output ?? []);
229
+ const results = citationResults.length > 0 ? citationResults : extractTextSnippets(response.output ?? []);
230
+ return { results, sources };
231
+ }
232
+ catch (err) {
233
+ const message = err instanceof Error ? err.message : String(err);
234
+ return {
235
+ results: [],
236
+ degraded: true,
237
+ error: message,
238
+ };
239
+ }
240
+ };
241
+ }
242
+ // ── Session cleanup ──────────────────────────────────────────────────────────
243
+ /**
244
+ * Destroy the ephemeral web search agent and conversation.
245
+ *
246
+ * Safe to call multiple times. Logs warnings on cleanup failure but does not throw.
247
+ */
248
+ export async function destroyWebSearchSession() {
249
+ await cleanupCurrentAgent(false);
250
+ }
251
+ // Register cleanup on process exit
252
+ process.on('beforeExit', () => {
253
+ void destroyWebSearchSession();
254
+ });