@nac3/forge-cli 0.2.0-alpha.5 → 0.2.0-alpha.59z.119

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 (768) hide show
  1. package/README.md +16 -1
  2. package/dist/a11y/helpers.d.ts +88 -0
  3. package/dist/a11y/helpers.d.ts.map +1 -0
  4. package/dist/a11y/helpers.js +109 -0
  5. package/dist/a11y/helpers.js.map +1 -0
  6. package/dist/agents/config_resolver.d.ts +16 -0
  7. package/dist/agents/config_resolver.d.ts.map +1 -0
  8. package/dist/agents/config_resolver.js +133 -0
  9. package/dist/agents/config_resolver.js.map +1 -0
  10. package/dist/agents/config_schema.d.ts +42 -0
  11. package/dist/agents/config_schema.d.ts.map +1 -0
  12. package/dist/agents/config_schema.js +204 -0
  13. package/dist/agents/config_schema.js.map +1 -0
  14. package/dist/agents/defaults.d.ts +21 -0
  15. package/dist/agents/defaults.d.ts.map +1 -0
  16. package/dist/agents/defaults.js +56 -0
  17. package/dist/agents/defaults.js.map +1 -0
  18. package/dist/agents/runner.d.ts +94 -0
  19. package/dist/agents/runner.d.ts.map +1 -0
  20. package/dist/agents/runner.js +165 -0
  21. package/dist/agents/runner.js.map +1 -0
  22. package/dist/agents/store.d.ts +33 -0
  23. package/dist/agents/store.d.ts.map +1 -0
  24. package/dist/agents/store.js +210 -0
  25. package/dist/agents/store.js.map +1 -0
  26. package/dist/agents/types.d.ts +112 -0
  27. package/dist/agents/types.d.ts.map +1 -0
  28. package/dist/agents/types.js +39 -0
  29. package/dist/agents/types.js.map +1 -0
  30. package/dist/bin/yf.d.ts.map +1 -1
  31. package/dist/bin/yf.js +86 -2
  32. package/dist/bin/yf.js.map +1 -1
  33. package/dist/chat/claude.d.ts +53 -15
  34. package/dist/chat/claude.d.ts.map +1 -1
  35. package/dist/chat/claude.js +152 -27
  36. package/dist/chat/claude.js.map +1 -1
  37. package/dist/chat/graph_auto_ingest.d.ts +46 -0
  38. package/dist/chat/graph_auto_ingest.d.ts.map +1 -0
  39. package/dist/chat/graph_auto_ingest.js +162 -0
  40. package/dist/chat/graph_auto_ingest.js.map +1 -0
  41. package/dist/chat/graph_component_watcher.d.ts +30 -0
  42. package/dist/chat/graph_component_watcher.d.ts.map +1 -0
  43. package/dist/chat/graph_component_watcher.js +220 -0
  44. package/dist/chat/graph_component_watcher.js.map +1 -0
  45. package/dist/chat/panel.d.ts.map +1 -1
  46. package/dist/chat/panel.js +5819 -1455
  47. package/dist/chat/panel.js.map +1 -1
  48. package/dist/chat/project_slug.d.ts +6 -0
  49. package/dist/chat/project_slug.d.ts.map +1 -0
  50. package/dist/chat/project_slug.js +78 -0
  51. package/dist/chat/project_slug.js.map +1 -0
  52. package/dist/chat/semantic_graph_ingest.d.ts +31 -0
  53. package/dist/chat/semantic_graph_ingest.d.ts.map +1 -0
  54. package/dist/chat/semantic_graph_ingest.js +84 -0
  55. package/dist/chat/semantic_graph_ingest.js.map +1 -0
  56. package/dist/chat/semantic_graph_inject.d.ts +25 -0
  57. package/dist/chat/semantic_graph_inject.d.ts.map +1 -0
  58. package/dist/chat/semantic_graph_inject.js +64 -0
  59. package/dist/chat/semantic_graph_inject.js.map +1 -0
  60. package/dist/chat/server.d.ts +50 -0
  61. package/dist/chat/server.d.ts.map +1 -1
  62. package/dist/chat/server.js +1905 -54
  63. package/dist/chat/server.js.map +1 -1
  64. package/dist/chat/spec_extract.d.ts +1 -2
  65. package/dist/chat/spec_extract.d.ts.map +1 -1
  66. package/dist/chat/spec_extract.js +18 -59
  67. package/dist/chat/spec_extract.js.map +1 -1
  68. package/dist/chat/spec_plan.d.ts +1 -2
  69. package/dist/chat/spec_plan.d.ts.map +1 -1
  70. package/dist/chat/spec_plan.js +15 -60
  71. package/dist/chat/spec_plan.js.map +1 -1
  72. package/dist/chat/tools/app_backend_dispatch.d.ts +39 -0
  73. package/dist/chat/tools/app_backend_dispatch.d.ts.map +1 -0
  74. package/dist/chat/tools/app_backend_dispatch.js +239 -0
  75. package/dist/chat/tools/app_backend_dispatch.js.map +1 -0
  76. package/dist/chat/tools/audit_consumers.d.ts +66 -0
  77. package/dist/chat/tools/audit_consumers.d.ts.map +1 -0
  78. package/dist/chat/tools/audit_consumers.js +231 -0
  79. package/dist/chat/tools/audit_consumers.js.map +1 -0
  80. package/dist/chat/tools/fs_editor.d.ts +11 -0
  81. package/dist/chat/tools/fs_editor.d.ts.map +1 -0
  82. package/dist/chat/tools/fs_editor.js +162 -0
  83. package/dist/chat/tools/fs_editor.js.map +1 -0
  84. package/dist/chat/tools/git.d.ts +24 -0
  85. package/dist/chat/tools/git.d.ts.map +1 -1
  86. package/dist/chat/tools/git.js +78 -0
  87. package/dist/chat/tools/git.js.map +1 -1
  88. package/dist/chat/tools/keys.d.ts +9 -0
  89. package/dist/chat/tools/keys.d.ts.map +1 -0
  90. package/dist/chat/tools/keys.js +152 -0
  91. package/dist/chat/tools/keys.js.map +1 -0
  92. package/dist/chat/tools/misc_verbs.d.ts +36 -0
  93. package/dist/chat/tools/misc_verbs.d.ts.map +1 -0
  94. package/dist/chat/tools/misc_verbs.js +405 -0
  95. package/dist/chat/tools/misc_verbs.js.map +1 -0
  96. package/dist/chat/tools/nac3_invoke.d.ts +53 -0
  97. package/dist/chat/tools/nac3_invoke.d.ts.map +1 -0
  98. package/dist/chat/tools/nac3_invoke.js +886 -0
  99. package/dist/chat/tools/nac3_invoke.js.map +1 -0
  100. package/dist/chat/tools/pizarron.d.ts +40 -0
  101. package/dist/chat/tools/pizarron.d.ts.map +1 -0
  102. package/dist/chat/tools/pizarron.js +123 -0
  103. package/dist/chat/tools/pizarron.js.map +1 -0
  104. package/dist/chat/tools/project_lifecycle.d.ts +24 -0
  105. package/dist/chat/tools/project_lifecycle.d.ts.map +1 -0
  106. package/dist/chat/tools/project_lifecycle.js +184 -0
  107. package/dist/chat/tools/project_lifecycle.js.map +1 -0
  108. package/dist/chat/tools/reader.d.ts.map +1 -1
  109. package/dist/chat/tools/reader.js +144 -1
  110. package/dist/chat/tools/reader.js.map +1 -1
  111. package/dist/chat/tools/shell.d.ts +58 -0
  112. package/dist/chat/tools/shell.d.ts.map +1 -0
  113. package/dist/chat/tools/shell.js +253 -0
  114. package/dist/chat/tools/shell.js.map +1 -0
  115. package/dist/chat/tools/workflow.d.ts +45 -0
  116. package/dist/chat/tools/workflow.d.ts.map +1 -0
  117. package/dist/chat/tools/workflow.js +404 -0
  118. package/dist/chat/tools/workflow.js.map +1 -0
  119. package/dist/chat/tools.d.ts +27 -0
  120. package/dist/chat/tools.d.ts.map +1 -1
  121. package/dist/chat/tools.js +362 -13
  122. package/dist/chat/tools.js.map +1 -1
  123. package/dist/chat/workflow_inject.d.ts +52 -0
  124. package/dist/chat/workflow_inject.d.ts.map +1 -0
  125. package/dist/chat/workflow_inject.js +346 -0
  126. package/dist/chat/workflow_inject.js.map +1 -0
  127. package/dist/commands/agent.d.ts +22 -0
  128. package/dist/commands/agent.d.ts.map +1 -0
  129. package/dist/commands/agent.js +190 -0
  130. package/dist/commands/agent.js.map +1 -0
  131. package/dist/commands/approve.d.ts +32 -0
  132. package/dist/commands/approve.d.ts.map +1 -0
  133. package/dist/commands/approve.js +198 -0
  134. package/dist/commands/approve.js.map +1 -0
  135. package/dist/commands/block.d.ts +28 -0
  136. package/dist/commands/block.d.ts.map +1 -0
  137. package/dist/commands/block.js +189 -0
  138. package/dist/commands/block.js.map +1 -0
  139. package/dist/commands/bootstrap.d.ts +35 -0
  140. package/dist/commands/bootstrap.d.ts.map +1 -0
  141. package/dist/commands/bootstrap.js +205 -0
  142. package/dist/commands/bootstrap.js.map +1 -0
  143. package/dist/commands/brain.d.ts +17 -0
  144. package/dist/commands/brain.d.ts.map +1 -0
  145. package/dist/commands/brain.js +129 -0
  146. package/dist/commands/brain.js.map +1 -0
  147. package/dist/commands/chat.d.ts +3 -0
  148. package/dist/commands/chat.d.ts.map +1 -1
  149. package/dist/commands/chat.js +112 -1
  150. package/dist/commands/chat.js.map +1 -1
  151. package/dist/commands/clarify.d.ts +30 -0
  152. package/dist/commands/clarify.d.ts.map +1 -0
  153. package/dist/commands/clarify.js +671 -0
  154. package/dist/commands/clarify.js.map +1 -0
  155. package/dist/commands/discover.d.ts +30 -0
  156. package/dist/commands/discover.d.ts.map +1 -0
  157. package/dist/commands/discover.js +178 -0
  158. package/dist/commands/discover.js.map +1 -0
  159. package/dist/commands/doctor.js +26 -4
  160. package/dist/commands/doctor.js.map +1 -1
  161. package/dist/commands/doctrine.d.ts +24 -0
  162. package/dist/commands/doctrine.d.ts.map +1 -0
  163. package/dist/commands/doctrine.js +106 -0
  164. package/dist/commands/doctrine.js.map +1 -0
  165. package/dist/commands/doctrines.d.ts +21 -0
  166. package/dist/commands/doctrines.d.ts.map +1 -0
  167. package/dist/commands/doctrines.js +158 -0
  168. package/dist/commands/doctrines.js.map +1 -0
  169. package/dist/commands/graph.d.ts +21 -0
  170. package/dist/commands/graph.d.ts.map +1 -0
  171. package/dist/commands/graph.js +363 -0
  172. package/dist/commands/graph.js.map +1 -0
  173. package/dist/commands/keys_setup.d.ts.map +1 -1
  174. package/dist/commands/keys_setup.js +80 -0
  175. package/dist/commands/keys_setup.js.map +1 -1
  176. package/dist/commands/legacy-audit.d.ts +34 -0
  177. package/dist/commands/legacy-audit.d.ts.map +1 -0
  178. package/dist/commands/legacy-audit.js +270 -0
  179. package/dist/commands/legacy-audit.js.map +1 -0
  180. package/dist/commands/lessons.d.ts +28 -0
  181. package/dist/commands/lessons.d.ts.map +1 -0
  182. package/dist/commands/lessons.js +309 -0
  183. package/dist/commands/lessons.js.map +1 -0
  184. package/dist/commands/license.d.ts.map +1 -1
  185. package/dist/commands/license.js +56 -1
  186. package/dist/commands/license.js.map +1 -1
  187. package/dist/commands/limits.d.ts +15 -0
  188. package/dist/commands/limits.d.ts.map +1 -0
  189. package/dist/commands/limits.js +51 -0
  190. package/dist/commands/limits.js.map +1 -0
  191. package/dist/commands/manifest-cli.d.ts +12 -0
  192. package/dist/commands/manifest-cli.d.ts.map +1 -0
  193. package/dist/commands/manifest-cli.js +42 -0
  194. package/dist/commands/manifest-cli.js.map +1 -0
  195. package/dist/commands/mcp.d.ts +24 -0
  196. package/dist/commands/mcp.d.ts.map +1 -0
  197. package/dist/commands/mcp.js +163 -0
  198. package/dist/commands/mcp.js.map +1 -0
  199. package/dist/commands/nac3.d.ts +22 -0
  200. package/dist/commands/nac3.d.ts.map +1 -0
  201. package/dist/commands/nac3.js +125 -0
  202. package/dist/commands/nac3.js.map +1 -0
  203. package/dist/commands/operate.d.ts +22 -0
  204. package/dist/commands/operate.d.ts.map +1 -0
  205. package/dist/commands/operate.js +523 -0
  206. package/dist/commands/operate.js.map +1 -0
  207. package/dist/commands/qa.d.ts +24 -0
  208. package/dist/commands/qa.d.ts.map +1 -0
  209. package/dist/commands/qa.js +349 -0
  210. package/dist/commands/qa.js.map +1 -0
  211. package/dist/commands/spec.d.ts +38 -0
  212. package/dist/commands/spec.d.ts.map +1 -0
  213. package/dist/commands/spec.js +256 -0
  214. package/dist/commands/spec.js.map +1 -0
  215. package/dist/commands/support.d.ts +22 -0
  216. package/dist/commands/support.d.ts.map +1 -0
  217. package/dist/commands/support.js +165 -0
  218. package/dist/commands/support.js.map +1 -0
  219. package/dist/commands/triage.d.ts +34 -0
  220. package/dist/commands/triage.d.ts.map +1 -0
  221. package/dist/commands/triage.js +228 -0
  222. package/dist/commands/triage.js.map +1 -0
  223. package/dist/commands/vault-inventory.d.ts +30 -0
  224. package/dist/commands/vault-inventory.d.ts.map +1 -0
  225. package/dist/commands/vault-inventory.js +222 -0
  226. package/dist/commands/vault-inventory.js.map +1 -0
  227. package/dist/commands/vault.d.ts.map +1 -1
  228. package/dist/commands/vault.js +5 -0
  229. package/dist/commands/vault.js.map +1 -1
  230. package/dist/commands/voice.d.ts.map +1 -1
  231. package/dist/commands/voice.js +47 -4
  232. package/dist/commands/voice.js.map +1 -1
  233. package/dist/commands/workflow-coverage.d.ts +30 -0
  234. package/dist/commands/workflow-coverage.d.ts.map +1 -0
  235. package/dist/commands/workflow-coverage.js +138 -0
  236. package/dist/commands/workflow-coverage.js.map +1 -0
  237. package/dist/core/ansi_strip.d.ts +39 -0
  238. package/dist/core/ansi_strip.d.ts.map +1 -0
  239. package/dist/core/ansi_strip.js +71 -0
  240. package/dist/core/ansi_strip.js.map +1 -0
  241. package/dist/core/audit_log.d.ts +43 -0
  242. package/dist/core/audit_log.d.ts.map +1 -0
  243. package/dist/core/audit_log.js +80 -0
  244. package/dist/core/audit_log.js.map +1 -0
  245. package/dist/core/brain_config.d.ts +33 -0
  246. package/dist/core/brain_config.d.ts.map +1 -0
  247. package/dist/core/brain_config.js +132 -0
  248. package/dist/core/brain_config.js.map +1 -0
  249. package/dist/core/budget.d.ts +96 -0
  250. package/dist/core/budget.d.ts.map +1 -0
  251. package/dist/core/budget.js +300 -0
  252. package/dist/core/budget.js.map +1 -0
  253. package/dist/core/cost_meter.d.ts +35 -0
  254. package/dist/core/cost_meter.d.ts.map +1 -0
  255. package/dist/core/cost_meter.js +111 -0
  256. package/dist/core/cost_meter.js.map +1 -0
  257. package/dist/core/cost_router.d.ts +106 -0
  258. package/dist/core/cost_router.d.ts.map +1 -0
  259. package/dist/core/cost_router.js +465 -0
  260. package/dist/core/cost_router.js.map +1 -0
  261. package/dist/core/doctrine_resolver.d.ts +23 -0
  262. package/dist/core/doctrine_resolver.d.ts.map +1 -0
  263. package/dist/core/doctrine_resolver.js +108 -0
  264. package/dist/core/doctrine_resolver.js.map +1 -0
  265. package/dist/core/error_catalog.d.ts +80 -0
  266. package/dist/core/error_catalog.d.ts.map +1 -0
  267. package/dist/core/error_catalog.js +168 -0
  268. package/dist/core/error_catalog.js.map +1 -0
  269. package/dist/core/error_formatter.d.ts +30 -0
  270. package/dist/core/error_formatter.d.ts.map +1 -0
  271. package/dist/core/error_formatter.js +155 -0
  272. package/dist/core/error_formatter.js.map +1 -0
  273. package/dist/core/incomplete_detector.d.ts +51 -0
  274. package/dist/core/incomplete_detector.d.ts.map +1 -0
  275. package/dist/core/incomplete_detector.js +140 -0
  276. package/dist/core/incomplete_detector.js.map +1 -0
  277. package/dist/core/learnings_client.d.ts +104 -0
  278. package/dist/core/learnings_client.d.ts.map +1 -0
  279. package/dist/core/learnings_client.js +262 -0
  280. package/dist/core/learnings_client.js.map +1 -0
  281. package/dist/core/limits_config.d.ts +76 -0
  282. package/dist/core/limits_config.d.ts.map +1 -0
  283. package/dist/core/limits_config.js +297 -0
  284. package/dist/core/limits_config.js.map +1 -0
  285. package/dist/core/provider_keys.d.ts +35 -0
  286. package/dist/core/provider_keys.d.ts.map +1 -0
  287. package/dist/core/provider_keys.js +260 -0
  288. package/dist/core/provider_keys.js.map +1 -0
  289. package/dist/core/reasoning_gate.d.ts +72 -0
  290. package/dist/core/reasoning_gate.d.ts.map +1 -0
  291. package/dist/core/reasoning_gate.js +192 -0
  292. package/dist/core/reasoning_gate.js.map +1 -0
  293. package/dist/core/shell_classifier.d.ts +49 -0
  294. package/dist/core/shell_classifier.d.ts.map +1 -0
  295. package/dist/core/shell_classifier.js +159 -0
  296. package/dist/core/shell_classifier.js.map +1 -0
  297. package/dist/core/sq_resolver.d.ts +14 -0
  298. package/dist/core/sq_resolver.d.ts.map +1 -0
  299. package/dist/core/sq_resolver.js +230 -0
  300. package/dist/core/sq_resolver.js.map +1 -0
  301. package/dist/core/truncation_detector.d.ts +68 -0
  302. package/dist/core/truncation_detector.d.ts.map +1 -0
  303. package/dist/core/truncation_detector.js +115 -0
  304. package/dist/core/truncation_detector.js.map +1 -0
  305. package/dist/core/turn_classifier.d.ts +62 -0
  306. package/dist/core/turn_classifier.d.ts.map +1 -0
  307. package/dist/core/turn_classifier.js +213 -0
  308. package/dist/core/turn_classifier.js.map +1 -0
  309. package/dist/core/whats_new.d.ts +42 -0
  310. package/dist/core/whats_new.d.ts.map +1 -0
  311. package/dist/core/whats_new.js +214 -0
  312. package/dist/core/whats_new.js.map +1 -0
  313. package/dist/deploy/adapter.d.ts +93 -0
  314. package/dist/deploy/adapter.d.ts.map +1 -0
  315. package/dist/deploy/adapter.js +42 -0
  316. package/dist/deploy/adapter.js.map +1 -0
  317. package/dist/deploy/aws_adapter.d.ts +28 -0
  318. package/dist/deploy/aws_adapter.d.ts.map +1 -0
  319. package/dist/deploy/aws_adapter.js +98 -0
  320. package/dist/deploy/aws_adapter.js.map +1 -0
  321. package/dist/deploy/cloudflare.d.ts +24 -0
  322. package/dist/deploy/cloudflare.d.ts.map +1 -0
  323. package/dist/deploy/cloudflare.js +169 -0
  324. package/dist/deploy/cloudflare.js.map +1 -0
  325. package/dist/docs/LIMITED_RESPONSIBILITY.md +216 -0
  326. package/dist/docs/PRIVACY_POLICY.md +169 -0
  327. package/dist/docs/SQ.md +1397 -0
  328. package/dist/docs/SQ_BY_STEP.yaml +147 -0
  329. package/dist/docs/doctrine/accessibility.md +78 -0
  330. package/dist/docs/doctrine/agent-orchestrator.md +76 -0
  331. package/dist/docs/doctrine/byok-vs-managed.md +81 -0
  332. package/dist/docs/doctrine/cost-router-escalation.md +73 -0
  333. package/dist/docs/doctrine/explain-results.md +90 -0
  334. package/dist/docs/doctrine/g-doc.md +46 -0
  335. package/dist/docs/doctrine/gates.md +21 -0
  336. package/dist/docs/doctrine/lessons-workflow.md +68 -0
  337. package/dist/docs/doctrine/mcp-bridge-lifecycle.md +75 -0
  338. package/dist/docs/doctrine/nac3-attrs.md +23 -0
  339. package/dist/docs/doctrine/office-tools.md +102 -0
  340. package/dist/docs/doctrine/pizarron.md +52 -0
  341. package/dist/docs/doctrine/r8-modal.md +60 -0
  342. package/dist/docs/doctrine/safety-refusals.md +73 -0
  343. package/dist/docs/doctrine/verb-composition.md +68 -0
  344. package/dist/docs/doctrine/workflow-phases.md +108 -0
  345. package/dist/docs/doctrine/workflow.md +58 -0
  346. package/dist/docs/doctrine/yf-commands.md +49 -0
  347. package/dist/json/query.d.ts +38 -0
  348. package/dist/json/query.d.ts.map +1 -0
  349. package/dist/json/query.js +177 -0
  350. package/dist/json/query.js.map +1 -0
  351. package/dist/license/hito4_client.d.ts +19 -0
  352. package/dist/license/hito4_client.d.ts.map +1 -1
  353. package/dist/license/hito4_client.js +110 -0
  354. package/dist/license/hito4_client.js.map +1 -1
  355. package/dist/license/index.d.ts.map +1 -1
  356. package/dist/license/index.js +7 -0
  357. package/dist/license/index.js.map +1 -1
  358. package/dist/license/sync.d.ts +54 -0
  359. package/dist/license/sync.d.ts.map +1 -0
  360. package/dist/license/sync.js +138 -0
  361. package/dist/license/sync.js.map +1 -0
  362. package/dist/llm/cohere_client.d.ts +81 -0
  363. package/dist/llm/cohere_client.d.ts.map +1 -0
  364. package/dist/llm/cohere_client.js +195 -0
  365. package/dist/llm/cohere_client.js.map +1 -0
  366. package/dist/llm/deepseek_client.d.ts +63 -0
  367. package/dist/llm/deepseek_client.d.ts.map +1 -0
  368. package/dist/llm/deepseek_client.js +188 -0
  369. package/dist/llm/deepseek_client.js.map +1 -0
  370. package/dist/llm/gemini_client.d.ts +78 -0
  371. package/dist/llm/gemini_client.d.ts.map +1 -0
  372. package/dist/llm/gemini_client.js +189 -0
  373. package/dist/llm/gemini_client.js.map +1 -0
  374. package/dist/llm/managed_client.d.ts +70 -0
  375. package/dist/llm/managed_client.d.ts.map +1 -0
  376. package/dist/llm/managed_client.js +251 -0
  377. package/dist/llm/managed_client.js.map +1 -0
  378. package/dist/llm/mistral_client.d.ts +54 -0
  379. package/dist/llm/mistral_client.d.ts.map +1 -0
  380. package/dist/llm/mistral_client.js +174 -0
  381. package/dist/llm/mistral_client.js.map +1 -0
  382. package/dist/llm/multi_provider_client.d.ts +57 -0
  383. package/dist/llm/multi_provider_client.d.ts.map +1 -0
  384. package/dist/llm/multi_provider_client.js +109 -0
  385. package/dist/llm/multi_provider_client.js.map +1 -0
  386. package/dist/llm/openai_client.d.ts +77 -0
  387. package/dist/llm/openai_client.d.ts.map +1 -0
  388. package/dist/llm/openai_client.js +200 -0
  389. package/dist/llm/openai_client.js.map +1 -0
  390. package/dist/llm/xai_client.d.ts +51 -0
  391. package/dist/llm/xai_client.d.ts.map +1 -0
  392. package/dist/llm/xai_client.js +174 -0
  393. package/dist/llm/xai_client.js.map +1 -0
  394. package/dist/mcp/bridge_registry.d.ts +77 -0
  395. package/dist/mcp/bridge_registry.d.ts.map +1 -0
  396. package/dist/mcp/bridge_registry.js +220 -0
  397. package/dist/mcp/bridge_registry.js.map +1 -0
  398. package/dist/mcp/config.d.ts +25 -0
  399. package/dist/mcp/config.d.ts.map +1 -0
  400. package/dist/mcp/config.js +116 -0
  401. package/dist/mcp/config.js.map +1 -0
  402. package/dist/mcp/protocol_client.d.ts +63 -0
  403. package/dist/mcp/protocol_client.d.ts.map +1 -0
  404. package/dist/mcp/protocol_client.js +340 -0
  405. package/dist/mcp/protocol_client.js.map +1 -0
  406. package/dist/mcp/state.d.ts +38 -0
  407. package/dist/mcp/state.d.ts.map +1 -0
  408. package/dist/mcp/state.js +144 -0
  409. package/dist/mcp/state.js.map +1 -0
  410. package/dist/migrate/ai-decorator.d.ts.map +1 -1
  411. package/dist/migrate/ai-decorator.js +33 -145
  412. package/dist/migrate/ai-decorator.js.map +1 -1
  413. package/dist/nac3/approval_queue.d.ts +36 -0
  414. package/dist/nac3/approval_queue.d.ts.map +1 -0
  415. package/dist/nac3/approval_queue.js +105 -0
  416. package/dist/nac3/approval_queue.js.map +1 -0
  417. package/dist/nac3/backend_tools.d.ts +89 -0
  418. package/dist/nac3/backend_tools.d.ts.map +1 -0
  419. package/dist/nac3/backend_tools.js +271 -0
  420. package/dist/nac3/backend_tools.js.map +1 -0
  421. package/dist/nac3/dispatch_log.d.ts +35 -0
  422. package/dist/nac3/dispatch_log.d.ts.map +1 -0
  423. package/dist/nac3/dispatch_log.js +126 -0
  424. package/dist/nac3/dispatch_log.js.map +1 -0
  425. package/dist/nac3/internal_manifest.d.ts +62 -0
  426. package/dist/nac3/internal_manifest.d.ts.map +1 -0
  427. package/dist/nac3/internal_manifest.js +1846 -0
  428. package/dist/nac3/internal_manifest.js.map +1 -0
  429. package/dist/nac3/manifest_lazy.d.ts +71 -0
  430. package/dist/nac3/manifest_lazy.d.ts.map +1 -0
  431. package/dist/nac3/manifest_lazy.js +141 -0
  432. package/dist/nac3/manifest_lazy.js.map +1 -0
  433. package/dist/nac3/verb_tier_map.d.ts +80 -0
  434. package/dist/nac3/verb_tier_map.d.ts.map +1 -0
  435. package/dist/nac3/verb_tier_map.js +159 -0
  436. package/dist/nac3/verb_tier_map.js.map +1 -0
  437. package/dist/office/excel.d.ts +110 -0
  438. package/dist/office/excel.d.ts.map +1 -0
  439. package/dist/office/excel.js +276 -0
  440. package/dist/office/excel.js.map +1 -0
  441. package/dist/office/pdf.d.ts +35 -0
  442. package/dist/office/pdf.d.ts.map +1 -0
  443. package/dist/office/pdf.js +70 -0
  444. package/dist/office/pdf.js.map +1 -0
  445. package/dist/office/word.d.ts +64 -0
  446. package/dist/office/word.d.ts.map +1 -0
  447. package/dist/office/word.js +243 -0
  448. package/dist/office/word.js.map +1 -0
  449. package/dist/qa/brain_matrix/ab_runner.d.ts +44 -0
  450. package/dist/qa/brain_matrix/ab_runner.d.ts.map +1 -0
  451. package/dist/qa/brain_matrix/ab_runner.js +144 -0
  452. package/dist/qa/brain_matrix/ab_runner.js.map +1 -0
  453. package/dist/qa/brain_matrix/brains.json +80 -0
  454. package/dist/qa/brain_matrix/compare.d.ts +27 -0
  455. package/dist/qa/brain_matrix/compare.d.ts.map +1 -0
  456. package/dist/qa/brain_matrix/compare.js +122 -0
  457. package/dist/qa/brain_matrix/compare.js.map +1 -0
  458. package/dist/qa/brain_matrix/fake_brain.d.ts +36 -0
  459. package/dist/qa/brain_matrix/fake_brain.d.ts.map +1 -0
  460. package/dist/qa/brain_matrix/fake_brain.js +27 -0
  461. package/dist/qa/brain_matrix/fake_brain.js.map +1 -0
  462. package/dist/qa/brain_matrix/fixtures/scenario_a11y_announce_intent.json +22 -0
  463. package/dist/qa/brain_matrix/fixtures/scenario_a11y_announce_no_speak_default.json +23 -0
  464. package/dist/qa/brain_matrix/fixtures/scenario_a11y_consecutive_announces.json +37 -0
  465. package/dist/qa/brain_matrix/fixtures/scenario_a11y_focus_after_action.json +22 -0
  466. package/dist/qa/brain_matrix/fixtures/scenario_a11y_label_with_shortcut.json +22 -0
  467. package/dist/qa/brain_matrix/fixtures/scenario_announce_min_tool_calls.json +23 -0
  468. package/dist/qa/brain_matrix/fixtures/scenario_app_invoke_happy_path.json +65 -0
  469. package/dist/qa/brain_matrix/fixtures/scenario_app_no_invent_without_backend.json +18 -0
  470. package/dist/qa/brain_matrix/fixtures/scenario_app_schema_fail_recover.json +48 -0
  471. package/dist/qa/brain_matrix/fixtures/scenario_approval_modal_no_redundancy.json +21 -0
  472. package/dist/qa/brain_matrix/fixtures/scenario_args_redacted_in_audit.json +17 -0
  473. package/dist/qa/brain_matrix/fixtures/scenario_audit_consumers_intent.json +19 -0
  474. package/dist/qa/brain_matrix/fixtures/scenario_audit_consumers_intent_en.json +19 -0
  475. package/dist/qa/brain_matrix/fixtures/scenario_audit_log_on_destructive.json +18 -0
  476. package/dist/qa/brain_matrix/fixtures/scenario_brain_explains_after_invoke.json +28 -0
  477. package/dist/qa/brain_matrix/fixtures/scenario_byok_on_managed_brain_overflow.json +28 -0
  478. package/dist/qa/brain_matrix/fixtures/scenario_byok_required_policy.json +35 -0
  479. package/dist/qa/brain_matrix/fixtures/scenario_concise_no_loop.json +23 -0
  480. package/dist/qa/brain_matrix/fixtures/scenario_consult_nac_spec_intent.json +19 -0
  481. package/dist/qa/brain_matrix/fixtures/scenario_contradiction_rfp_cost.json +38 -0
  482. package/dist/qa/brain_matrix/fixtures/scenario_cost_router_attachment_frontier.json +24 -0
  483. package/dist/qa/brain_matrix/fixtures/scenario_cost_router_escalation_explicit.json +23 -0
  484. package/dist/qa/brain_matrix/fixtures/scenario_cost_router_idle_reset.json +24 -0
  485. package/dist/qa/brain_matrix/fixtures/scenario_csv_via_excel_read.json +22 -0
  486. package/dist/qa/brain_matrix/fixtures/scenario_destructive_no_text_confirm.json +20 -0
  487. package/dist/qa/brain_matrix/fixtures/scenario_diff_explains_changes.json +32 -0
  488. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_after_unknown_intent.json +17 -0
  489. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_a11y.json +24 -0
  490. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_agent_orchestrator.json +24 -0
  491. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_byok_vs_managed.json +24 -0
  492. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_cost_router.json +24 -0
  493. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_g_doc.json +24 -0
  494. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_gates.json +23 -0
  495. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_intent_en.json +19 -0
  496. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_lessons_workflow.json +24 -0
  497. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_mcp_lifecycle.json +24 -0
  498. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_nac3_attrs.json +24 -0
  499. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_office.json +24 -0
  500. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_pizarron.json +23 -0
  501. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_r8_modal.json +23 -0
  502. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_workflow.json +23 -0
  503. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_workflow_phases.json +24 -0
  504. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_yf_commands.json +24 -0
  505. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_explain_results.json +22 -0
  506. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_safety_refusals.json +22 -0
  507. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_verb_composition.json +22 -0
  508. package/dist/qa/brain_matrix/fixtures/scenario_eco_defers_complex.json +17 -0
  509. package/dist/qa/brain_matrix/fixtures/scenario_eco_short_query.json +27 -0
  510. package/dist/qa/brain_matrix/fixtures/scenario_env_list_intent.json +20 -0
  511. package/dist/qa/brain_matrix/fixtures/scenario_env_list_intent_en.json +20 -0
  512. package/dist/qa/brain_matrix/fixtures/scenario_env_no_secret_leak.json +21 -0
  513. package/dist/qa/brain_matrix/fixtures/scenario_env_secret_suppression.json +35 -0
  514. package/dist/qa/brain_matrix/fixtures/scenario_env_secret_suppression_en.json +21 -0
  515. package/dist/qa/brain_matrix/fixtures/scenario_excel_append_handles_csv_refusal.json +28 -0
  516. package/dist/qa/brain_matrix/fixtures/scenario_excel_append_sheet_intent.json +20 -0
  517. package/dist/qa/brain_matrix/fixtures/scenario_excel_append_sheet_intent_en.json +20 -0
  518. package/dist/qa/brain_matrix/fixtures/scenario_excel_append_then_list.json +34 -0
  519. package/dist/qa/brain_matrix/fixtures/scenario_excel_list_sheets_first.json +22 -0
  520. package/dist/qa/brain_matrix/fixtures/scenario_excel_sheet_named.json +23 -0
  521. package/dist/qa/brain_matrix/fixtures/scenario_explain_voice_yes.json +22 -0
  522. package/dist/qa/brain_matrix/fixtures/scenario_explains_forge_error.json +39 -0
  523. package/dist/qa/brain_matrix/fixtures/scenario_focus_after_modal_open.json +29 -0
  524. package/dist/qa/brain_matrix/fixtures/scenario_fs_find_intent.json +21 -0
  525. package/dist/qa/brain_matrix/fixtures/scenario_fs_find_intent_en.json +20 -0
  526. package/dist/qa/brain_matrix/fixtures/scenario_fs_find_with_glob.json +22 -0
  527. package/dist/qa/brain_matrix/fixtures/scenario_fs_mkdir_intent.json +20 -0
  528. package/dist/qa/brain_matrix/fixtures/scenario_fs_move_intent.json +20 -0
  529. package/dist/qa/brain_matrix/fixtures/scenario_fs_read_handles_missing.json +28 -0
  530. package/dist/qa/brain_matrix/fixtures/scenario_fs_stat_before_read.json +22 -0
  531. package/dist/qa/brain_matrix/fixtures/scenario_generated_app_nac3_default.json +24 -0
  532. package/dist/qa/brain_matrix/fixtures/scenario_git_branch_list_intent.json +20 -0
  533. package/dist/qa/brain_matrix/fixtures/scenario_git_diff_intent.json +20 -0
  534. package/dist/qa/brain_matrix/fixtures/scenario_git_log_intent.json +20 -0
  535. package/dist/qa/brain_matrix/fixtures/scenario_git_log_intent_en.json +20 -0
  536. package/dist/qa/brain_matrix/fixtures/scenario_git_pull_intent.json +19 -0
  537. package/dist/qa/brain_matrix/fixtures/scenario_git_push_intent.json +20 -0
  538. package/dist/qa/brain_matrix/fixtures/scenario_git_show_metadata_only.json +22 -0
  539. package/dist/qa/brain_matrix/fixtures/scenario_git_status_branch_combo.json +44 -0
  540. package/dist/qa/brain_matrix/fixtures/scenario_git_status_intent.json +20 -0
  541. package/dist/qa/brain_matrix/fixtures/scenario_git_status_intent_en.json +20 -0
  542. package/dist/qa/brain_matrix/fixtures/scenario_github_clone_repo_intent.json +20 -0
  543. package/dist/qa/brain_matrix/fixtures/scenario_github_create_repo_intent.json +20 -0
  544. package/dist/qa/brain_matrix/fixtures/scenario_id_format_in_reply.json +27 -0
  545. package/dist/qa/brain_matrix/fixtures/scenario_iterations_idempotent_adenda.json +27 -0
  546. package/dist/qa/brain_matrix/fixtures/scenario_iterations_query_history.json +29 -0
  547. package/dist/qa/brain_matrix/fixtures/scenario_iterations_tracker_acknowledge.json +28 -0
  548. package/dist/qa/brain_matrix/fixtures/scenario_json_parse_handles_malformed.json +34 -0
  549. package/dist/qa/brain_matrix/fixtures/scenario_json_parse_intent.json +30 -0
  550. package/dist/qa/brain_matrix/fixtures/scenario_json_parse_intent_en.json +30 -0
  551. package/dist/qa/brain_matrix/fixtures/scenario_json_query_extract.json +23 -0
  552. package/dist/qa/brain_matrix/fixtures/scenario_json_stringify_intent.json +20 -0
  553. package/dist/qa/brain_matrix/fixtures/scenario_json_stringify_intent_en.json +20 -0
  554. package/dist/qa/brain_matrix/fixtures/scenario_keys_set_intent.json +20 -0
  555. package/dist/qa/brain_matrix/fixtures/scenario_keys_status_intent.json +20 -0
  556. package/dist/qa/brain_matrix/fixtures/scenario_keys_status_intent_en.json +20 -0
  557. package/dist/qa/brain_matrix/fixtures/scenario_lazy_complex_verb.json +27 -0
  558. package/dist/qa/brain_matrix/fixtures/scenario_lazy_doctrine_only_when_needed.json +17 -0
  559. package/dist/qa/brain_matrix/fixtures/scenario_lazy_simple_verb.json +21 -0
  560. package/dist/qa/brain_matrix/fixtures/scenario_lesson_recall_overrides.json +22 -0
  561. package/dist/qa/brain_matrix/fixtures/scenario_lifecycle_restart_app_intent.json +19 -0
  562. package/dist/qa/brain_matrix/fixtures/scenario_lifecycle_run_app_intent.json +19 -0
  563. package/dist/qa/brain_matrix/fixtures/scenario_lifecycle_run_app_intent_en.json +19 -0
  564. package/dist/qa/brain_matrix/fixtures/scenario_lifecycle_stop_app_intent.json +20 -0
  565. package/dist/qa/brain_matrix/fixtures/scenario_lifecycle_stop_app_intent_en.json +20 -0
  566. package/dist/qa/brain_matrix/fixtures/scenario_long_doctrine_no_truncate.json +23 -0
  567. package/dist/qa/brain_matrix/fixtures/scenario_manifest_read_intent.json +19 -0
  568. package/dist/qa/brain_matrix/fixtures/scenario_manual_open_intent.json +19 -0
  569. package/dist/qa/brain_matrix/fixtures/scenario_mcp_bridge_unreachable_graceful.json +39 -0
  570. package/dist/qa/brain_matrix/fixtures/scenario_mcp_disabled_bridge_no_verbs.json +18 -0
  571. package/dist/qa/brain_matrix/fixtures/scenario_mcp_discover_tools_intent.json +19 -0
  572. package/dist/qa/brain_matrix/fixtures/scenario_mcp_invariant_1_no_verbs.json +19 -0
  573. package/dist/qa/brain_matrix/fixtures/scenario_mcp_invoke_two_hop.json +68 -0
  574. package/dist/qa/brain_matrix/fixtures/scenario_mcp_invoke_with_auth_loaded.json +76 -0
  575. package/dist/qa/brain_matrix/fixtures/scenario_mcp_revoked_token_graceful.json +33 -0
  576. package/dist/qa/brain_matrix/fixtures/scenario_mcp_set_creds_intent.json +21 -0
  577. package/dist/qa/brain_matrix/fixtures/scenario_mcp_set_creds_no_token_arg.json +27 -0
  578. package/dist/qa/brain_matrix/fixtures/scenario_mcp_show_tools_before_invoke.json +44 -0
  579. package/dist/qa/brain_matrix/fixtures/scenario_mcp_toggle_intent.json +20 -0
  580. package/dist/qa/brain_matrix/fixtures/scenario_multi_turn_clarify_then_act.json +24 -0
  581. package/dist/qa/brain_matrix/fixtures/scenario_nac3_attrs_doctrine.json +53 -0
  582. package/dist/qa/brain_matrix/fixtures/scenario_nac3_discover_schemas_intent.json +19 -0
  583. package/dist/qa/brain_matrix/fixtures/scenario_nac3_required_attr_missing.json +19 -0
  584. package/dist/qa/brain_matrix/fixtures/scenario_no_bare_ok_after_destructive.json +27 -0
  585. package/dist/qa/brain_matrix/fixtures/scenario_no_global_learnings_no_recall.json +20 -0
  586. package/dist/qa/brain_matrix/fixtures/scenario_no_id_leak_in_reply.json +24 -0
  587. package/dist/qa/brain_matrix/fixtures/scenario_no_invent_verb.json +18 -0
  588. package/dist/qa/brain_matrix/fixtures/scenario_no_loop_after_repeated_fail.json +42 -0
  589. package/dist/qa/brain_matrix/fixtures/scenario_no_password_echo.json +28 -0
  590. package/dist/qa/brain_matrix/fixtures/scenario_npm_outdated_all_current.json +28 -0
  591. package/dist/qa/brain_matrix/fixtures/scenario_npm_outdated_intent.json +20 -0
  592. package/dist/qa/brain_matrix/fixtures/scenario_npm_outdated_intent_en.json +20 -0
  593. package/dist/qa/brain_matrix/fixtures/scenario_npm_outdated_then_explain.json +35 -0
  594. package/dist/qa/brain_matrix/fixtures/scenario_office_excel_read_intent.json +24 -0
  595. package/dist/qa/brain_matrix/fixtures/scenario_office_excel_write_required_args.json +23 -0
  596. package/dist/qa/brain_matrix/fixtures/scenario_office_overwrite_explicit.json +23 -0
  597. package/dist/qa/brain_matrix/fixtures/scenario_office_word_write_intent.json +27 -0
  598. package/dist/qa/brain_matrix/fixtures/scenario_panel_open_settings_intent.json +19 -0
  599. package/dist/qa/brain_matrix/fixtures/scenario_panel_send_message_intent.json +19 -0
  600. package/dist/qa/brain_matrix/fixtures/scenario_pdf_read_intent.json +22 -0
  601. package/dist/qa/brain_matrix/fixtures/scenario_pizarron_clear_tab_explicit_close.json +17 -0
  602. package/dist/qa/brain_matrix/fixtures/scenario_pizarron_for_chart.json +30 -0
  603. package/dist/qa/brain_matrix/fixtures/scenario_pizarron_for_long_table.json +28 -0
  604. package/dist/qa/brain_matrix/fixtures/scenario_pizarron_read_tab_intent.json +20 -0
  605. package/dist/qa/brain_matrix/fixtures/scenario_pizarron_render_not_inline.json +20 -0
  606. package/dist/qa/brain_matrix/fixtures/scenario_prefer_native_read_file.json +28 -0
  607. package/dist/qa/brain_matrix/fixtures/scenario_prefer_native_verb_over_shell.json +22 -0
  608. package/dist/qa/brain_matrix/fixtures/scenario_prefer_native_workflow_state.json +28 -0
  609. package/dist/qa/brain_matrix/fixtures/scenario_project_init_intent.json +20 -0
  610. package/dist/qa/brain_matrix/fixtures/scenario_project_set_root_intent.json +20 -0
  611. package/dist/qa/brain_matrix/fixtures/scenario_promote_lesson_after_correction.json +17 -0
  612. package/dist/qa/brain_matrix/fixtures/scenario_r8_destructive_shell_exec.json +21 -0
  613. package/dist/qa/brain_matrix/fixtures/scenario_read_then_parse_order.json +36 -0
  614. package/dist/qa/brain_matrix/fixtures/scenario_reader_bookmark_jump_intent.json +19 -0
  615. package/dist/qa/brain_matrix/fixtures/scenario_reader_bookmark_set_intent.json +19 -0
  616. package/dist/qa/brain_matrix/fixtures/scenario_reader_for_long_doc.json +21 -0
  617. package/dist/qa/brain_matrix/fixtures/scenario_reader_list_docs_intent.json +19 -0
  618. package/dist/qa/brain_matrix/fixtures/scenario_reader_next_block_intent.json +19 -0
  619. package/dist/qa/brain_matrix/fixtures/scenario_reader_read_section_intent.json +19 -0
  620. package/dist/qa/brain_matrix/fixtures/scenario_reader_recap_intent.json +19 -0
  621. package/dist/qa/brain_matrix/fixtures/scenario_reader_search_intent.json +20 -0
  622. package/dist/qa/brain_matrix/fixtures/scenario_reader_search_intent_en.json +20 -0
  623. package/dist/qa/brain_matrix/fixtures/scenario_refuses_blanket_chmod_777.json +17 -0
  624. package/dist/qa/brain_matrix/fixtures/scenario_refuses_bypass_approval_modal.json +18 -0
  625. package/dist/qa/brain_matrix/fixtures/scenario_refuses_delete_critical_en.json +18 -0
  626. package/dist/qa/brain_matrix/fixtures/scenario_refuses_disable_vault_encryption.json +18 -0
  627. package/dist/qa/brain_matrix/fixtures/scenario_refuses_drop_database.json +17 -0
  628. package/dist/qa/brain_matrix/fixtures/scenario_refuses_force_push_main.json +17 -0
  629. package/dist/qa/brain_matrix/fixtures/scenario_refuses_force_push_main_en.json +17 -0
  630. package/dist/qa/brain_matrix/fixtures/scenario_refuses_leak_vault_en.json +18 -0
  631. package/dist/qa/brain_matrix/fixtures/scenario_refuses_modify_node_modules.json +17 -0
  632. package/dist/qa/brain_matrix/fixtures/scenario_refuses_skip_hooks.json +17 -0
  633. package/dist/qa/brain_matrix/fixtures/scenario_refuses_skip_hooks_en.json +17 -0
  634. package/dist/qa/brain_matrix/fixtures/scenario_refuses_to_delete_critical.json +18 -0
  635. package/dist/qa/brain_matrix/fixtures/scenario_refuses_to_leak_vault.json +18 -0
  636. package/dist/qa/brain_matrix/fixtures/scenario_respects_global_learnings.json +28 -0
  637. package/dist/qa/brain_matrix/fixtures/scenario_respects_workflow_gate_g2.json +39 -0
  638. package/dist/qa/brain_matrix/fixtures/scenario_revoke_lesson_request.json +21 -0
  639. package/dist/qa/brain_matrix/fixtures/scenario_sq_rules_active.json +30 -0
  640. package/dist/qa/brain_matrix/fixtures/scenario_text_diff_intent.json +20 -0
  641. package/dist/qa/brain_matrix/fixtures/scenario_text_diff_intent_en.json +20 -0
  642. package/dist/qa/brain_matrix/fixtures/scenario_truncated_response_retry.json +22 -0
  643. package/dist/qa/brain_matrix/fixtures/scenario_unknown_command_clarify.json +23 -0
  644. package/dist/qa/brain_matrix/fixtures/scenario_url_parse_compose.json +23 -0
  645. package/dist/qa/brain_matrix/fixtures/scenario_url_parse_handles_invalid.json +27 -0
  646. package/dist/qa/brain_matrix/fixtures/scenario_url_parse_intent.json +20 -0
  647. package/dist/qa/brain_matrix/fixtures/scenario_url_parse_intent_en.json +20 -0
  648. package/dist/qa/brain_matrix/fixtures/scenario_url_parse_then_inspect_query.json +28 -0
  649. package/dist/qa/brain_matrix/fixtures/scenario_url_then_stringify_order.json +34 -0
  650. package/dist/qa/brain_matrix/fixtures/scenario_user_just_greets.json +17 -0
  651. package/dist/qa/brain_matrix/fixtures/scenario_user_overrides_proceeds.json +24 -0
  652. package/dist/qa/brain_matrix/fixtures/scenario_user_revokes_in_middle.json +22 -0
  653. package/dist/qa/brain_matrix/fixtures/scenario_voice_open_pizarron.json +21 -0
  654. package/dist/qa/brain_matrix/fixtures/scenario_word_outline_extraction.json +22 -0
  655. package/dist/qa/brain_matrix/fixtures/scenario_word_overwrite_explicit.json +20 -0
  656. package/dist/qa/brain_matrix/fixtures/scenario_word_section_count.json +28 -0
  657. package/dist/qa/brain_matrix/fixtures/scenario_workflow_doctrine.json +53 -0
  658. package/dist/qa/brain_matrix/fixtures/scenario_workflow_done_specs.json +32 -0
  659. package/dist/qa/brain_matrix/fixtures/scenario_workflow_intent_capture.json +22 -0
  660. package/dist/qa/brain_matrix/fixtures/scenario_workflow_phase_jump_block.json +26 -0
  661. package/dist/qa/brain_matrix/fixtures/scenario_workflow_phase_v_step_15.json +41 -0
  662. package/dist/qa/brain_matrix/fixtures/scenario_workflow_run_step_intent.json +19 -0
  663. package/dist/qa/brain_matrix/fixtures/scenario_workflow_set_intent.json +20 -0
  664. package/dist/qa/brain_matrix/fixtures/scenario_workflow_state_injection.json +32 -0
  665. package/dist/qa/brain_matrix/fixtures/scenario_workflow_state_recovers_after_clarify.json +25 -0
  666. package/dist/qa/brain_matrix/fixtures/scenario_workflow_step_1_intake.json +24 -0
  667. package/dist/qa/brain_matrix/fixtures/scenario_yf_command_doctrine.json +56 -0
  668. package/dist/qa/brain_matrix/fixtures/scenario_yf_doctrine_list_intent.json +19 -0
  669. package/dist/qa/brain_matrix/fixtures/scenario_yf_qa_compare_intent.json +17 -0
  670. package/dist/qa/brain_matrix/invariants.d.ts +18 -0
  671. package/dist/qa/brain_matrix/invariants.d.ts.map +1 -0
  672. package/dist/qa/brain_matrix/invariants.js +333 -0
  673. package/dist/qa/brain_matrix/invariants.js.map +1 -0
  674. package/dist/qa/brain_matrix/report.d.ts +12 -0
  675. package/dist/qa/brain_matrix/report.d.ts.map +1 -0
  676. package/dist/qa/brain_matrix/report.js +131 -0
  677. package/dist/qa/brain_matrix/report.js.map +1 -0
  678. package/dist/qa/brain_matrix/runner.d.ts +31 -0
  679. package/dist/qa/brain_matrix/runner.d.ts.map +1 -0
  680. package/dist/qa/brain_matrix/runner.js +561 -0
  681. package/dist/qa/brain_matrix/runner.js.map +1 -0
  682. package/dist/qa/brain_matrix/types.d.ts +180 -0
  683. package/dist/qa/brain_matrix/types.d.ts.map +1 -0
  684. package/dist/qa/brain_matrix/types.js +2 -0
  685. package/dist/qa/brain_matrix/types.js.map +1 -0
  686. package/dist/qa/brain_matrix/validate_scenario.d.ts +23 -0
  687. package/dist/qa/brain_matrix/validate_scenario.d.ts.map +1 -0
  688. package/dist/qa/brain_matrix/validate_scenario.js +128 -0
  689. package/dist/qa/brain_matrix/validate_scenario.js.map +1 -0
  690. package/dist/support/github_dispatcher.d.ts +36 -0
  691. package/dist/support/github_dispatcher.d.ts.map +1 -0
  692. package/dist/support/github_dispatcher.js +292 -0
  693. package/dist/support/github_dispatcher.js.map +1 -0
  694. package/dist/support/reports.d.ts +31 -0
  695. package/dist/support/reports.d.ts.map +1 -0
  696. package/dist/support/reports.js +162 -0
  697. package/dist/support/reports.js.map +1 -0
  698. package/dist/telemetry/usage.d.ts +67 -0
  699. package/dist/telemetry/usage.d.ts.map +1 -0
  700. package/dist/telemetry/usage.js +208 -0
  701. package/dist/telemetry/usage.js.map +1 -0
  702. package/dist/version.d.ts +1 -1
  703. package/dist/version.d.ts.map +1 -1
  704. package/dist/version.js +1 -1
  705. package/dist/version.js.map +1 -1
  706. package/dist/voice/config.d.ts +12 -0
  707. package/dist/voice/config.d.ts.map +1 -1
  708. package/dist/voice/config.js +25 -2
  709. package/dist/voice/config.js.map +1 -1
  710. package/dist/voice/providers/google.d.ts +9 -0
  711. package/dist/voice/providers/google.d.ts.map +1 -1
  712. package/dist/voice/providers/google.js +197 -28
  713. package/dist/voice/providers/google.js.map +1 -1
  714. package/dist/voice/router.d.ts +10 -0
  715. package/dist/voice/router.d.ts.map +1 -1
  716. package/dist/voice/router.js +39 -20
  717. package/dist/voice/router.js.map +1 -1
  718. package/dist/voice/types.d.ts +5 -2
  719. package/dist/voice/types.d.ts.map +1 -1
  720. package/dist/voice/types.js.map +1 -1
  721. package/dist/workflow/phase_inference.d.ts +56 -0
  722. package/dist/workflow/phase_inference.d.ts.map +1 -0
  723. package/dist/workflow/phase_inference.js +107 -0
  724. package/dist/workflow/phase_inference.js.map +1 -0
  725. package/dist/workflow/state.d.ts +238 -0
  726. package/dist/workflow/state.d.ts.map +1 -0
  727. package/dist/workflow/state.js +213 -0
  728. package/dist/workflow/state.js.map +1 -0
  729. package/docs/SQ.md +1397 -0
  730. package/docs/SQ_BY_STEP.yaml +147 -0
  731. package/docs/doctrine/accessibility.md +78 -0
  732. package/docs/doctrine/agent-orchestrator.md +76 -0
  733. package/docs/doctrine/byok-vs-managed.md +81 -0
  734. package/docs/doctrine/cost-router-escalation.md +73 -0
  735. package/docs/doctrine/explain-results.md +90 -0
  736. package/docs/doctrine/g-doc.md +46 -0
  737. package/docs/doctrine/gates.md +21 -0
  738. package/docs/doctrine/lessons-workflow.md +68 -0
  739. package/docs/doctrine/mcp-bridge-lifecycle.md +75 -0
  740. package/docs/doctrine/nac3-attrs.md +23 -0
  741. package/docs/doctrine/office-tools.md +102 -0
  742. package/docs/doctrine/pizarron.md +52 -0
  743. package/docs/doctrine/r8-modal.md +60 -0
  744. package/docs/doctrine/safety-refusals.md +73 -0
  745. package/docs/doctrine/verb-composition.md +68 -0
  746. package/docs/doctrine/workflow-phases.md +108 -0
  747. package/docs/doctrine/workflow.md +58 -0
  748. package/docs/doctrine/yf-commands.md +49 -0
  749. package/package.json +20 -14
  750. package/src/i18n/catalogs/ar.json +55 -10
  751. package/src/i18n/catalogs/de.json +55 -10
  752. package/src/i18n/catalogs/en.json +66 -1
  753. package/src/i18n/catalogs/es.json +66 -1
  754. package/src/i18n/catalogs/fr.json +55 -10
  755. package/src/i18n/catalogs/hi.json +55 -10
  756. package/src/i18n/catalogs/it.json +55 -10
  757. package/src/i18n/catalogs/ja.json +55 -10
  758. package/src/i18n/catalogs/pt.json +55 -10
  759. package/src/i18n/catalogs/zh.json +55 -10
  760. package/templates/nextjs-app/README.md +48 -0
  761. package/templates/nextjs-app/next.config.js +8 -0
  762. package/templates/nextjs-app/package.json +33 -0
  763. package/templates/nextjs-app/src/app/globals.css +43 -0
  764. package/templates/nextjs-app/src/app/layout.tsx +29 -0
  765. package/templates/nextjs-app/src/app/page.tsx +63 -0
  766. package/templates/nextjs-app/src/nac/manifest.ts +36 -0
  767. package/templates/nextjs-app/tsconfig.json +21 -0
  768. package/templates/nextjs-app/yujin.forge.json +11 -0
@@ -37,8 +37,53 @@ import { extractSpec } from './spec_extract.js';
37
37
  import { generatePlan } from './spec_plan.js';
38
38
  import { executeScaffold, rollbackScaffold } from './spec_scaffold.js';
39
39
  import { VERSION } from '../version.js';
40
+ import { manifest as nac3Manifest } from '../nac3/internal_manifest.js';
41
+ import { buildManifestForPrompt } from '../nac3/manifest_lazy.js';
42
+ import { popPending, logApproval } from '../nac3/approval_queue.js';
40
43
  export async function startChatServer(opts) {
41
44
  const projectName = await readProjectName(opts.projectRoot);
45
+ /* alpha.59u.2 -- distinct slug (machine id) vs projectName (display).
46
+ * Identity queries against the semantic graph use the slug so
47
+ * CLI + panel converge. */
48
+ const { resolveProjectSlug } = await import('./project_slug.js');
49
+ const projectSlug = await resolveProjectSlug(opts.projectRoot);
50
+ /* alpha.59v -- fire-and-forget auto-ingest of package.json deps
51
+ * whenever the boot detects a changed (or never-ingested)
52
+ * package.json. Never blocks the boot; never throws.
53
+ * alpha.59v.2 -- registered via startAutoIngest so the
54
+ * /api/forge/graph-status endpoint can await it (avoiding
55
+ * the panel showing partial data during the ingest). */
56
+ try {
57
+ const { startAutoIngest } = await import('./graph_auto_ingest.js');
58
+ startAutoIngest({
59
+ project_root: opts.projectRoot,
60
+ project_slug: projectSlug,
61
+ log: (msg) => { try {
62
+ console.error(msg);
63
+ }
64
+ catch { /* swallow */ } },
65
+ });
66
+ }
67
+ catch { /* never break boot */ }
68
+ /* alpha.59w -- fs.watch over src/components, src/hooks, src/lib,
69
+ * src/core. When the user adds or modifies a TS/JS file, the
70
+ * watcher extracts top-level exports (regex), creates one
71
+ * graph node per export with confidence='auto', and pushes
72
+ * to the worker (debounced 2s). The watcher cleanup is
73
+ * attached to the returned StartedServer.close(). */
74
+ let componentWatcherHandle = null;
75
+ try {
76
+ const { startComponentWatcher } = await import('./graph_component_watcher.js');
77
+ componentWatcherHandle = await startComponentWatcher({
78
+ project_root: opts.projectRoot,
79
+ project_slug: projectSlug,
80
+ log: (msg) => { try {
81
+ console.error(msg);
82
+ }
83
+ catch { /* swallow */ } },
84
+ });
85
+ }
86
+ catch { /* never break boot */ }
42
87
  const claude = opts.claude ?? new ClaudeClient();
43
88
  const voice = opts.voice ?? new VoiceRouter({
44
89
  configDir: configDir(),
@@ -81,6 +126,7 @@ export async function startChatServer(opts) {
81
126
  await route(req, res, {
82
127
  projectRoot: opts.projectRoot,
83
128
  projectName,
129
+ projectSlug,
84
130
  port: opts.port,
85
131
  claude,
86
132
  voice,
@@ -114,6 +160,11 @@ export async function startChatServer(opts) {
114
160
  url,
115
161
  store,
116
162
  close: () => new Promise((resolve) => {
163
+ /* alpha.59w -- stop the component watcher on shutdown so
164
+ * vitest does not hang on lingering fs watchers. */
165
+ if (componentWatcherHandle) {
166
+ Promise.resolve(componentWatcherHandle.stop()).catch(() => undefined);
167
+ }
117
168
  server.close(() => resolve());
118
169
  }),
119
170
  };
@@ -144,6 +195,39 @@ async function route(req, res, ctx) {
144
195
  });
145
196
  return;
146
197
  }
198
+ /* Legal docs surfaced inside the product (Config -> About).
199
+ * Serves the raw markdown so the panel can render or pop a
200
+ * dialog. Routes are static names so a future i18n swap can
201
+ * branch on Accept-Language without changing the URL. */
202
+ if (req.method === 'GET' && url.pathname === '/api/legal/privacy-policy') {
203
+ await serveLegalDoc(res, 'PRIVACY_POLICY.md');
204
+ return;
205
+ }
206
+ if (req.method === 'GET' && url.pathname === '/api/legal/limited-responsibility') {
207
+ await serveLegalDoc(res, 'LIMITED_RESPONSIBILITY.md');
208
+ return;
209
+ }
210
+ if (req.method === 'GET' && url.pathname === '/api/legal/index') {
211
+ /* Compact directory listing the panel uses to render
212
+ * Config -> About without hardcoding doc slugs. */
213
+ sendJson(res, 200, {
214
+ docs: [
215
+ {
216
+ slug: 'privacy-policy',
217
+ title: 'Privacy policy',
218
+ description: 'What data Forge sends to Yujin servers, what we keep, and what we do not.',
219
+ url: '/api/legal/privacy-policy',
220
+ },
221
+ {
222
+ slug: 'limited-responsibility',
223
+ title: 'Limitation of liability',
224
+ description: 'AS-IS provision, third-party services, indemnification, governing law.',
225
+ url: '/api/legal/limited-responsibility',
226
+ },
227
+ ],
228
+ });
229
+ return;
230
+ }
147
231
  if (req.method === 'POST' && url.pathname === '/api/chat') {
148
232
  await handleChat(req, res, ctx);
149
233
  return;
@@ -174,6 +258,281 @@ async function route(req, res, ctx) {
174
258
  await handleVaultCatalog(res, url);
175
259
  return;
176
260
  }
261
+ /* NAC3 internal manifest -- Forge dogfood. See
262
+ docs/NAC3_INTERNAL_MANIFEST.md. The panel reads this on boot
263
+ to register handlers + caches for 60s. Same JSON is embedded
264
+ into the system prompt so the LLM sees one canonical
265
+ catalogue. */
266
+ if (req.method === 'GET' && url.pathname === '/api/forge/nac3-manifest') {
267
+ sendJson(res, 200, { ok: true, manifest: nac3Manifest() });
268
+ return;
269
+ }
270
+ /* alpha.59c.2 -- limits config endpoints. GET returns the
271
+ * active config + field paths + defaults. POST sets one
272
+ * field with {field, value} body. The settings panel renders
273
+ * the 10 numeric inputs from these. */
274
+ if (req.method === 'GET' && url.pathname === '/api/forge/limits') {
275
+ const { readLimitsConfig, defaultLimitsConfig, LIMITS_FIELD_PATHS, LIMITS_BOOLEAN_FIELD_PATHS, } = await import('../core/limits_config.js');
276
+ const cfg = await readLimitsConfig(true);
277
+ const def = defaultLimitsConfig();
278
+ const resolveField = (f, kind) => {
279
+ const parts = f.split('.');
280
+ let cur = cfg;
281
+ let dft = def;
282
+ for (const p of parts) {
283
+ if (cur && typeof cur === 'object')
284
+ cur = cur[p];
285
+ if (dft && typeof dft === 'object')
286
+ dft = dft[p];
287
+ }
288
+ return { field: f, value: cur, default: dft, kind };
289
+ };
290
+ const fields = LIMITS_FIELD_PATHS.map((f) => resolveField(f, 'number'));
291
+ const booleans = LIMITS_BOOLEAN_FIELD_PATHS.map((f) => resolveField(f, 'boolean'));
292
+ sendJson(res, 200, { ok: true, fields, booleans, config: cfg });
293
+ return;
294
+ }
295
+ if (req.method === 'POST' && url.pathname === '/api/forge/limits') {
296
+ const raw = await readBody(req);
297
+ let body;
298
+ try {
299
+ body = JSON.parse(raw);
300
+ }
301
+ catch {
302
+ sendJson(res, 400, { ok: false, error: 'invalid JSON' });
303
+ return;
304
+ }
305
+ const { setLimitsField, setLimitsBooleanField, isLimitsBooleanFieldPath, resetLimitsConfig } = await import('../core/limits_config.js');
306
+ if (body.reset === true) {
307
+ const cfg = await resetLimitsConfig();
308
+ sendJson(res, 200, { ok: true, config: cfg });
309
+ return;
310
+ }
311
+ if (typeof body.field !== 'string') {
312
+ sendJson(res, 400, { ok: false, error: 'expected {field: string, value: number|boolean} or {reset: true}' });
313
+ return;
314
+ }
315
+ /* alpha.59d -- accept booleans too (Danger Zone toggle +
316
+ * cwd_boundary_strict). Dispatch by field-path type. */
317
+ if (isLimitsBooleanFieldPath(body.field)) {
318
+ if (typeof body.value !== 'boolean') {
319
+ sendJson(res, 400, { ok: false, error: 'boolean field requires boolean value' });
320
+ return;
321
+ }
322
+ try {
323
+ const cfg = await setLimitsBooleanField(body.field, body.value);
324
+ sendJson(res, 200, { ok: true, config: cfg });
325
+ }
326
+ catch (err) {
327
+ sendJson(res, 400, { ok: false, error: err instanceof Error ? err.message : String(err) });
328
+ }
329
+ return;
330
+ }
331
+ if (typeof body.value !== 'number' || !Number.isFinite(body.value)) {
332
+ sendJson(res, 400, { ok: false, error: 'expected {field: string, value: number} or {reset: true}' });
333
+ return;
334
+ }
335
+ try {
336
+ const cfg = await setLimitsField(body.field, body.value);
337
+ sendJson(res, 200, { ok: true, config: cfg });
338
+ }
339
+ catch (err) {
340
+ sendJson(res, 400, { ok: false, error: err instanceof Error ? err.message : String(err) });
341
+ }
342
+ return;
343
+ }
344
+ /* alpha.59j -- reader bookmarks viewer endpoint. Reads the
345
+ * persisted reader_sessions.json + returns a flat list of
346
+ * bookmarks (name, doc_path, cursor_block_id, opened_at)
347
+ * newest-first so the panel can render the sidebar. */
348
+ if (req.method === 'GET' && url.pathname === '/api/forge/reader-bookmarks') {
349
+ try {
350
+ const home = process.env.HOME || process.env.USERPROFILE || (await import('node:os')).homedir();
351
+ const pathMod = await import('node:path');
352
+ const fsMod = await import('node:fs/promises');
353
+ const base = process.env.YF_CONFIG_DIR || pathMod.join(home, '.yujin-forge');
354
+ const cacheFile = pathMod.join(base, 'cache', 'reader_sessions.json');
355
+ const raw = await fsMod.readFile(cacheFile, 'utf-8');
356
+ const idx = JSON.parse(raw);
357
+ const out = [];
358
+ for (const key of Object.keys(idx.entries || {})) {
359
+ const entry = idx.entries[key];
360
+ if (!entry)
361
+ continue;
362
+ const bms = entry.bookmarks || {};
363
+ for (const name of Object.keys(bms)) {
364
+ out.push({
365
+ name,
366
+ doc_id: entry.doc_id,
367
+ abs_path: entry.abs_path,
368
+ filename: entry.filename,
369
+ title: entry.title || entry.filename,
370
+ block_id: bms[name],
371
+ last_used_at: entry.last_used_at || '',
372
+ });
373
+ }
374
+ }
375
+ out.sort((a, b) => (b.last_used_at || '').localeCompare(a.last_used_at || ''));
376
+ sendJson(res, 200, { ok: true, bookmarks: out, count: out.length });
377
+ }
378
+ catch {
379
+ sendJson(res, 200, { ok: true, bookmarks: [], count: 0 });
380
+ }
381
+ return;
382
+ }
383
+ /* alpha.59i -- shell kill endpoint. POST {handle_id} from the
384
+ * pizarron tab Cancel button. Looks up the active ChildProcess
385
+ * + sends SIGTERM (with SIGKILL fallback 2s). 404 if the
386
+ * handle is unknown (already finished). */
387
+ if (req.method === 'POST' && url.pathname === '/api/forge/shell-kill') {
388
+ const raw = await readBody(req);
389
+ let body;
390
+ try {
391
+ body = JSON.parse(raw);
392
+ }
393
+ catch {
394
+ sendJson(res, 400, { ok: false, error: 'invalid JSON' });
395
+ return;
396
+ }
397
+ if (typeof body.handle_id !== 'string' || body.handle_id.trim() === '') {
398
+ sendJson(res, 400, { ok: false, error: 'handle_id required' });
399
+ return;
400
+ }
401
+ const { killShellByHandle } = await import('./tools/shell.js');
402
+ const r = killShellByHandle(body.handle_id);
403
+ sendJson(res, r.ok ? 200 : 404, r);
404
+ return;
405
+ }
406
+ /* alpha.59r -- semantic graph status proxy. Forwards a list
407
+ * call to the worker so the panel can show node counts in
408
+ * the settings card without exposing the HMAC bearer to JS.
409
+ * Returns { ok, count, by_kind, by_confidence, sample[] }.
410
+ * Graceful: returns count=0 when worker unreachable. */
411
+ if (req.method === 'GET' && url.pathname === '/api/forge/graph-status') {
412
+ try {
413
+ const { semanticGraphGet } = await import('../license/hito4_client.js');
414
+ /* alpha.59u.2 -- default to ctx.projectName when the panel
415
+ * JS does not pass an explicit project_slug. This matches
416
+ * the slug that the CLI `yf graph ingest` uses (both call
417
+ * resolveProjectSlug). Without this, panel sees '' while
418
+ * CLI wrote with 'name-from-package-json' -> 0 results. */
419
+ const project = url.searchParams.get('project_slug') ?? ctx.projectSlug ?? '';
420
+ /* alpha.59v.2 -- if the boot auto-ingest for this slug is
421
+ * still running, wait for it (5s max) so the panel does
422
+ * not show stale counts on first paint. The user reported
423
+ * seeing "1 node" then "19 nodes" after Refrescar; this
424
+ * serialises them. */
425
+ try {
426
+ const { inFlightIngest } = await import('./graph_auto_ingest.js');
427
+ const pending = inFlightIngest(project);
428
+ if (pending) {
429
+ await Promise.race([
430
+ pending,
431
+ new Promise((resolve) => setTimeout(resolve, 5000)),
432
+ ]);
433
+ }
434
+ }
435
+ catch { /* swallow -- proceed with query anyway */ }
436
+ const r = await semanticGraphGet('/v1/semantic-graph/nodes', {}, { project_slug: project });
437
+ if (!r.ok || !r.body || r.body.ok !== true) {
438
+ sendJson(res, 200, { ok: true, count: 0, by_kind: {}, by_confidence: {}, sample: [], available: false });
439
+ return;
440
+ }
441
+ const nodes = r.body.nodes || [];
442
+ const by_kind = {};
443
+ const by_confidence = {};
444
+ for (const n of nodes) {
445
+ by_kind[n.kind] = (by_kind[n.kind] || 0) + 1;
446
+ by_confidence[n.confidence] = (by_confidence[n.confidence] || 0) + 1;
447
+ }
448
+ const sample = nodes.slice(0, 10).map((n) => ({
449
+ id: n.id, kind: n.kind, name: n.name, version: n.version, purpose: n.purpose, confidence: n.confidence,
450
+ }));
451
+ sendJson(res, 200, { ok: true, count: nodes.length, by_kind, by_confidence, sample, available: true });
452
+ }
453
+ catch {
454
+ sendJson(res, 200, { ok: true, count: 0, by_kind: {}, by_confidence: {}, sample: [], available: false });
455
+ }
456
+ return;
457
+ }
458
+ /* alpha.59l -- "what changed in this version" notification.
459
+ * Compares ~/.yujin-forge/last_seen_version vs current VERSION
460
+ * + returns the cumulative bullet catalog entries. Panel shows
461
+ * a one-shot dismissable banner; dismiss POSTs to /dismiss. */
462
+ if (req.method === 'GET' && url.pathname === '/api/forge/whats-new') {
463
+ const { computeWhatsNew } = await import('../core/whats_new.js');
464
+ const r = await computeWhatsNew(VERSION);
465
+ sendJson(res, 200, { ok: true, ...r });
466
+ return;
467
+ }
468
+ if (req.method === 'POST' && url.pathname === '/api/forge/whats-new/dismiss') {
469
+ const { writeLastSeen } = await import('../core/whats_new.js');
470
+ try {
471
+ await writeLastSeen(VERSION);
472
+ sendJson(res, 200, { ok: true, last_seen_version: VERSION });
473
+ }
474
+ catch (err) {
475
+ sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
476
+ }
477
+ return;
478
+ }
479
+ /* alpha.59g -- audit log viewer endpoint. Reads the last N
480
+ * rows from ~/.yujin-forge/cache/audit_log.jsonl + returns
481
+ * newest-first. The panel renders them in the Danger Zone
482
+ * area so the user can confirm exactly what Forge did
483
+ * (especially when Danger Zone auto-approve is on). */
484
+ if (req.method === 'GET' && url.pathname === '/api/forge/audit-log') {
485
+ const { readRecentAudit } = await import('../core/audit_log.js');
486
+ const limitParam = url.searchParams.get('limit');
487
+ const limit = limitParam ? Math.min(500, Math.max(1, Number(limitParam) || 100)) : 100;
488
+ const entries = await readRecentAudit(limit);
489
+ sendJson(res, 200, { ok: true, entries, count: entries.length });
490
+ return;
491
+ }
492
+ /* alpha.52 -- budget snapshot for the panel UI card. */
493
+ if (req.method === 'GET' && url.pathname === '/api/forge/budget') {
494
+ const { readBudgetStatus } = await import('../core/budget.js');
495
+ const status = await readBudgetStatus();
496
+ /* JSON does not transport Infinity; map to null so the
497
+ * client can treat it as "unlimited". */
498
+ const finite = (n) => Number.isFinite(n) ? n : null;
499
+ sendJson(res, 200, {
500
+ ok: true,
501
+ budget: {
502
+ plan: status.plan,
503
+ plan_label: status.plan_label,
504
+ month: status.month,
505
+ renewal_at: status.renewal_at,
506
+ extension_url: status.extension_url,
507
+ eco: {
508
+ cap: finite(status.eco.cap),
509
+ used: status.eco.used,
510
+ remaining: finite(status.eco.remaining),
511
+ used_pct: status.eco.used_pct,
512
+ },
513
+ frontier: {
514
+ cap: finite(status.frontier.cap),
515
+ used: status.frontier.used,
516
+ remaining: finite(status.frontier.remaining),
517
+ used_pct: status.frontier.used_pct,
518
+ },
519
+ },
520
+ });
521
+ return;
522
+ }
523
+ /* alpha.42 -- approval queue endpoints for destructive /
524
+ approval-required verbs. The dispatcher pauses such verbs +
525
+ returns nac3_panel_approval_required: true. The panel opens
526
+ a modal; the user clicks Approve / Reject; the panel POSTs
527
+ here with the trace_id. */
528
+ if (req.method === 'POST' && url.pathname === '/api/forge/nac3-approve') {
529
+ await handleNac3Approve(req, res, ctx);
530
+ return;
531
+ }
532
+ if (req.method === 'POST' && url.pathname === '/api/forge/nac3-reject') {
533
+ await handleNac3Reject(req, res);
534
+ return;
535
+ }
177
536
  /* SQ 0.8 -- dual mode read/write. */
178
537
  if (req.method === 'GET' && url.pathname === '/api/forge/mode') {
179
538
  await handleModeGet(req, res, ctx);
@@ -248,6 +607,95 @@ async function route(req, res, ctx) {
248
607
  await handleForgeToolDispatch(req, res, ctx);
249
608
  return;
250
609
  }
610
+ /* Open-in-editor escape hatch (Layer A.2). The panel surfaces
611
+ file paths as clickable; click triggers this endpoint, which
612
+ spawns the user's default editor (code / cursor / subl /
613
+ idea, in order). Best-effort -- on failure returns the
614
+ resolved path so the panel can copy-to-clipboard fallback. */
615
+ if (req.method === 'POST' && url.pathname === '/api/forge/open-in-editor') {
616
+ await handleOpenInEditor(req, res, ctx);
617
+ return;
618
+ }
619
+ /* Trust gradient tracking (Layer A.2). Panel increments
620
+ counters when the user expands a diff / opens a file in
621
+ their editor / overrides Forge. Forge reads them at session
622
+ boot and decides whether to surface or hide the detail
623
+ links. */
624
+ if (req.method === 'POST' && url.pathname === '/api/forge/trust-event') {
625
+ await handleTrustEvent(req, res, ctx);
626
+ return;
627
+ }
628
+ if (req.method === 'GET' && url.pathname === '/api/forge/trust-state') {
629
+ await handleTrustState(req, res, ctx);
630
+ return;
631
+ }
632
+ /* alpha.29 -- keys status for the settings dropdown. Returns
633
+ which BYOK provider keys + license are configured, without
634
+ exposing the values themselves. */
635
+ if (req.method === 'GET' && url.pathname === '/api/forge/keys-status') {
636
+ await handleKeysStatus(req, res);
637
+ return;
638
+ }
639
+ /* alpha.30 -- write/clear BYOK provider keys from the panel. */
640
+ if (req.method === 'POST' && url.pathname === '/api/forge/keys-set') {
641
+ await handleKeysSet(req, res);
642
+ return;
643
+ }
644
+ /* alpha.59z.114 -- pull the canonical license row from the
645
+ HITO 4 worker (Polar -> D1 -> local cache). The panel button
646
+ hits this when the user paid on Polar but the local cache
647
+ still says plan=none. */
648
+ if (req.method === 'POST' && url.pathname === '/api/forge/license-sync') {
649
+ await handleLicenseSync(req, res);
650
+ return;
651
+ }
652
+ /* alpha.59z.117 -- bind a Polar user_handle (email) to the
653
+ local license cache + immediately attempt a sync. This is
654
+ the one-click flow the panel surfaces when the user opens
655
+ yf chat for the first time after installing from npm:
656
+ panel detects user_handle missing, prompts for email,
657
+ POSTs here, and the row gets populated end-to-end. */
658
+ if (req.method === 'POST' && url.pathname === '/api/forge/license-handle') {
659
+ await handleLicenseHandle(req, res);
660
+ return;
661
+ }
662
+ /* alpha.35 -- pizarron edit: read + write project files. */
663
+ if (req.method === 'GET' && url.pathname === '/api/forge/file') {
664
+ await handleReadFile(req, res, ctx);
665
+ return;
666
+ }
667
+ if (req.method === 'POST' && url.pathname === '/api/forge/file') {
668
+ await handleWriteFile(req, res, ctx);
669
+ return;
670
+ }
671
+ /* Favicon -- minimal SVG so Chrome stops 404-ing. The browser
672
+ auto-requests /favicon.ico on every page load; without this,
673
+ each open of the panel logs an error. */
674
+ if (req.method === 'GET' && (url.pathname === '/favicon.ico'
675
+ || url.pathname === '/favicon.svg')) {
676
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">'
677
+ + '<circle cx="16" cy="16" r="14" fill="#1a1a1a"/>'
678
+ + '<path d="M8 22 L16 8 L24 22 M11 17 L21 17" stroke="#fafafa" stroke-width="2" fill="none" stroke-linecap="round"/>'
679
+ + '</svg>';
680
+ res.writeHead(200, {
681
+ 'content-type': 'image/svg+xml',
682
+ 'cache-control': 'public, max-age=86400',
683
+ });
684
+ res.end(svg);
685
+ return;
686
+ }
687
+ /* Support report ingest (2026-05-31 -- F30-style). Panel sends
688
+ * runtime errors (window.onerror, fetch failures, unhandled
689
+ * promise rejections) here so Forge sees what the user saw
690
+ * without them having to copy logs. PII is scrubbed on save. */
691
+ if (req.method === 'POST' && url.pathname === '/api/forge/support/report') {
692
+ await handleSupportReport(req, res);
693
+ return;
694
+ }
695
+ if (req.method === 'GET' && url.pathname === '/api/forge/support/reports') {
696
+ await handleSupportReportList(req, res);
697
+ return;
698
+ }
251
699
  /* Spec doc ingest (V1.37 + V1.38 -- bloque 4.5 scaffolding).
252
700
  Client uploads a spec file (PDF / DOCX / HTML / md / ...)
253
701
  and the server parses it via the existing reader pipeline,
@@ -423,6 +871,19 @@ async function handleVoiceStt(req, res, ctx) {
423
871
  sendJson(res, 400, { ok: false, error: 'audio body is empty' });
424
872
  return;
425
873
  }
874
+ /* 2026-05-31 fix: Google STT sync rejects audio > 1 min with
875
+ HTTP 400. Refuse early with a useful message instead of
876
+ leaking the Google error verbatim. ~4 MB is the practical
877
+ ceiling for 60s of WebM/Opus at 32 kbps. */
878
+ if (audio.length > 4 * 1024 * 1024) {
879
+ sendJson(res, 413, {
880
+ ok: false,
881
+ error: 'audio too long for sync STT (limit ~1 min). '
882
+ + 'Got ' + audio.length + ' bytes. The chat panel auto-stops at 55s; '
883
+ + 'if you hit this from a different client, split the audio.',
884
+ });
885
+ return;
886
+ }
426
887
  /* Optional active document hint (V1.31). Lets the voice intent
427
888
  matcher resolve commands like "siguiente" or "buscar X" against
428
889
  the currently-open reader session. Client sets this header to
@@ -516,6 +977,144 @@ async function handleForgeToolDispatch(req, res, ctx) {
516
977
  });
517
978
  }
518
979
  }
980
+ /* ============================================================
981
+ * NAC3 approval queue endpoints (alpha.42).
982
+ *
983
+ * When the LLM invokes a verb with policy=destructive or
984
+ * approval-required, runNac3Invoke pushes a pending row + returns
985
+ * an envelope. The panel opens a modal; the user clicks Approve
986
+ * or Reject; the panel POSTs here with the trace_id.
987
+ *
988
+ * Approve -> resolve the row + execute the legacy alias via the
989
+ * runForgeTool dispatcher (with approval_bypass=true to skip the
990
+ * gate on re-entry). Reject -> log + return ok without executing.
991
+ * ============================================================ */
992
+ async function handleNac3Approve(req, res, ctx) {
993
+ let body;
994
+ try {
995
+ body = JSON.parse(await readBody(req));
996
+ }
997
+ catch {
998
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
999
+ return;
1000
+ }
1001
+ const traceId = typeof body.trace_id === 'string' ? body.trace_id.trim() : '';
1002
+ if (!traceId) {
1003
+ sendJson(res, 400, { ok: false, error: 'trace_id required' });
1004
+ return;
1005
+ }
1006
+ const pending = popPending(traceId);
1007
+ if (!pending) {
1008
+ await logApproval({
1009
+ ts: new Date().toISOString(),
1010
+ trace_id: traceId,
1011
+ verb_id: '?',
1012
+ decision: 'expired',
1013
+ args_sample: {},
1014
+ policy: '?',
1015
+ reason: 'trace_id not in queue (expired or already resolved)',
1016
+ });
1017
+ sendJson(res, 404, { ok: false, error: 'no pending approval for trace_id (expired or already resolved)' });
1018
+ return;
1019
+ }
1020
+ /* Execute via the legacy alias, bypassing the gate. */
1021
+ if (!pending.legacy_alias) {
1022
+ sendJson(res, 500, { ok: false, error: 'pending row has no legacy alias' });
1023
+ return;
1024
+ }
1025
+ let result;
1026
+ try {
1027
+ result = await pending.legacy_run(pending.legacy_alias, pending.args, ctx.projectRoot === pending.ctx.projectRoot ? pending.ctx : { projectRoot: ctx.projectRoot });
1028
+ }
1029
+ catch (err) {
1030
+ await logApproval({
1031
+ ts: new Date().toISOString(),
1032
+ trace_id: traceId,
1033
+ verb_id: pending.verb.id,
1034
+ decision: 'approve',
1035
+ args_sample: pending.args,
1036
+ policy: pending.verb.policy,
1037
+ reason: 'handler threw: ' + (err instanceof Error ? err.message : String(err)),
1038
+ });
1039
+ sendJson(res, 500, {
1040
+ ok: false,
1041
+ error: 'handler threw on approved verb: ' + (err instanceof Error ? err.message : String(err)),
1042
+ });
1043
+ return;
1044
+ }
1045
+ await logApproval({
1046
+ ts: new Date().toISOString(),
1047
+ trace_id: traceId,
1048
+ verb_id: pending.verb.id,
1049
+ decision: 'approve',
1050
+ args_sample: pending.args,
1051
+ policy: pending.verb.policy,
1052
+ });
1053
+ sendJson(res, 200, {
1054
+ ok: true,
1055
+ trace_id: traceId,
1056
+ verb_id: pending.verb.id,
1057
+ is_error: result.is_error ?? false,
1058
+ result: result.result,
1059
+ });
1060
+ }
1061
+ async function handleNac3Reject(req, res) {
1062
+ let body;
1063
+ try {
1064
+ body = JSON.parse(await readBody(req));
1065
+ }
1066
+ catch {
1067
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
1068
+ return;
1069
+ }
1070
+ const traceId = typeof body.trace_id === 'string' ? body.trace_id.trim() : '';
1071
+ const reason = typeof body.reason === 'string' ? body.reason.slice(0, 240) : undefined;
1072
+ if (!traceId) {
1073
+ sendJson(res, 400, { ok: false, error: 'trace_id required' });
1074
+ return;
1075
+ }
1076
+ const pending = popPending(traceId);
1077
+ if (!pending) {
1078
+ await logApproval({
1079
+ ts: new Date().toISOString(),
1080
+ trace_id: traceId,
1081
+ verb_id: '?',
1082
+ decision: 'expired',
1083
+ args_sample: {},
1084
+ policy: '?',
1085
+ reason: 'trace_id not in queue',
1086
+ });
1087
+ /* alpha.59z.14 F104 -- emit structured ForgeError so the panel
1088
+ * + chat brain can surface a readable diagnosis instead of the
1089
+ * cryptic "no pending approval for trace_id" string. */
1090
+ const { renderForgeError, E_MODAL_TRACE_STALE } = await import('../core/error_catalog.js');
1091
+ const { formatForgeErrorJson } = await import('../core/error_formatter.js');
1092
+ const forgeError = renderForgeError(E_MODAL_TRACE_STALE, { trace_id: traceId });
1093
+ sendJson(res, 404, {
1094
+ ok: false,
1095
+ /* legacy field for backward compatibility */
1096
+ error: 'no pending approval for trace_id',
1097
+ /* F104 structured */
1098
+ forge_error: formatForgeErrorJson(forgeError),
1099
+ });
1100
+ return;
1101
+ }
1102
+ await logApproval({
1103
+ ts: new Date().toISOString(),
1104
+ trace_id: traceId,
1105
+ verb_id: pending.verb.id,
1106
+ decision: 'reject',
1107
+ args_sample: pending.args,
1108
+ policy: pending.verb.policy,
1109
+ reason,
1110
+ });
1111
+ sendJson(res, 200, {
1112
+ ok: true,
1113
+ trace_id: traceId,
1114
+ verb_id: pending.verb.id,
1115
+ decision: 'rejected',
1116
+ });
1117
+ }
519
1118
  /** V1.37 + V1.38 -- ingest a spec doc via drag-drop / upload.
520
1119
  *
521
1120
  * Accepts POST with:
@@ -643,7 +1242,7 @@ async function handleForgeIngestExtract(req, res, ctx) {
643
1242
  };
644
1243
  if (session.doc.language)
645
1244
  opts.language = session.doc.language;
646
- const extraction = await extractSpec(ctx.claude, opts);
1245
+ const extraction = await extractSpec(opts);
647
1246
  setExtraction(session.doc_id, extraction);
648
1247
  sendJson(res, 200, {
649
1248
  ok: true,
@@ -705,7 +1304,7 @@ async function handleForgeIngestPlan(req, res, ctx) {
705
1304
  };
706
1305
  if (session.doc.language)
707
1306
  opts.language = session.doc.language;
708
- const plan = await generatePlan(ctx.claude, opts);
1307
+ const plan = await generatePlan(opts);
709
1308
  setPlan(session.doc_id, plan);
710
1309
  sendJson(res, 200, {
711
1310
  ok: true,
@@ -948,6 +1547,460 @@ async function handleVoiceTts(req, res, ctx) {
948
1547
  });
949
1548
  }
950
1549
  }
1550
+ /* ============================================================
1551
+ * Layer A.2 -- minority surface (open in editor + trust gradient)
1552
+ * ============================================================ */
1553
+ /** Check that `target` is inside `root` (after resolving both).
1554
+ * Refuses path traversal like ../../../etc/passwd. */
1555
+ function isInside(target, root) {
1556
+ const targetAbs = path.resolve(target);
1557
+ const rootAbs = path.resolve(root) + path.sep;
1558
+ return targetAbs === path.resolve(root) || targetAbs.startsWith(rootAbs);
1559
+ }
1560
+ /** Editor commands tried in order. First match wins. The args
1561
+ * templates are joined with the file path at runtime. All
1562
+ * commands run with stdio ignored + detached so the panel does
1563
+ * not block on the editor's window lifetime. */
1564
+ const EDITOR_CANDIDATES = [
1565
+ { cmd: 'code', args: (f) => [f] }, // VS Code
1566
+ { cmd: 'cursor', args: (f) => [f] }, // Cursor
1567
+ { cmd: 'subl', args: (f) => [f] }, // Sublime Text
1568
+ { cmd: 'idea', args: (f) => [f] }, // IntelliJ family
1569
+ { cmd: 'webstorm', args: (f) => [f] },
1570
+ { cmd: 'nano', args: (f) => [f] },
1571
+ { cmd: 'vim', args: (f) => [f] },
1572
+ ];
1573
+ async function handleOpenInEditor(req, res, ctx) {
1574
+ let body;
1575
+ try {
1576
+ body = JSON.parse(await readBody(req));
1577
+ }
1578
+ catch {
1579
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
1580
+ return;
1581
+ }
1582
+ if (typeof body.path !== 'string' || body.path.trim() === '') {
1583
+ sendJson(res, 400, { ok: false, error: 'path (non-empty string) required' });
1584
+ return;
1585
+ }
1586
+ /* Resolve to absolute under the project root. Refuse paths
1587
+ * that escape the project tree (no user opening /etc/passwd
1588
+ * through the chat). */
1589
+ const requestedRel = body.path.trim().replace(/^[/\\]+/, '');
1590
+ const absolute = path.join(ctx.projectRoot, requestedRel);
1591
+ if (!isInside(absolute, ctx.projectRoot)) {
1592
+ sendJson(res, 400, { ok: false, error: 'path escapes project root' });
1593
+ return;
1594
+ }
1595
+ /* Walk the candidates, spawn the first one that does not
1596
+ * throw ENOENT. */
1597
+ const { spawn } = await import('node:child_process');
1598
+ for (const candidate of EDITOR_CANDIDATES) {
1599
+ try {
1600
+ const cmd = process.platform === 'win32' ? `${candidate.cmd}.cmd` : candidate.cmd;
1601
+ const child = spawn(cmd, candidate.args(absolute), {
1602
+ stdio: 'ignore',
1603
+ detached: true,
1604
+ shell: false,
1605
+ });
1606
+ child.on('error', () => { });
1607
+ child.unref();
1608
+ /* If spawn returned, the binary is at least present.
1609
+ * We do not wait for editor to launch (it might be slow
1610
+ * or might be a background daemon). Report success. */
1611
+ sendJson(res, 200, {
1612
+ ok: true,
1613
+ editor: candidate.cmd,
1614
+ path: absolute,
1615
+ });
1616
+ return;
1617
+ }
1618
+ catch { /* ENOENT etc -- try next candidate */ }
1619
+ }
1620
+ /* No editor found. Return the absolute path so the panel can
1621
+ * offer copy-to-clipboard fallback. */
1622
+ sendJson(res, 200, {
1623
+ ok: false,
1624
+ error: 'no supported editor found in PATH',
1625
+ path: absolute,
1626
+ candidates: EDITOR_CANDIDATES.map((c) => c.cmd),
1627
+ hint: 'Install VS Code (code), Cursor, Sublime (subl), or IntelliJ; or copy the path manually.',
1628
+ });
1629
+ }
1630
+ function trustMetricsPath() {
1631
+ return path.join(configDir(), 'trust_metrics.json');
1632
+ }
1633
+ async function loadTrustMetrics() {
1634
+ const fs = await import('node:fs');
1635
+ try {
1636
+ const raw = await fs.promises.readFile(trustMetricsPath(), 'utf-8');
1637
+ const parsed = JSON.parse(raw);
1638
+ if (parsed && typeof parsed === 'object') {
1639
+ return {
1640
+ v: 1,
1641
+ diff_expansions: Number(parsed.diff_expansions) || 0,
1642
+ editor_opens: Number(parsed.editor_opens) || 0,
1643
+ forge_overrides: Number(parsed.forge_overrides) || 0,
1644
+ last_event_at: typeof parsed.last_event_at === 'string' ? parsed.last_event_at : null,
1645
+ last_proposal_at: typeof parsed.last_proposal_at === 'string' ? parsed.last_proposal_at : null,
1646
+ user_choice: ['show', 'hide', 'default'].includes(parsed.user_choice) ? parsed.user_choice : 'default',
1647
+ };
1648
+ }
1649
+ }
1650
+ catch { /* missing or bad -- return defaults */ }
1651
+ return {
1652
+ v: 1,
1653
+ diff_expansions: 0,
1654
+ editor_opens: 0,
1655
+ forge_overrides: 0,
1656
+ last_event_at: null,
1657
+ last_proposal_at: null,
1658
+ user_choice: 'default',
1659
+ };
1660
+ }
1661
+ async function saveTrustMetrics(m) {
1662
+ const fs = await import('node:fs');
1663
+ await fs.promises.mkdir(configDir(), { recursive: true });
1664
+ await fs.promises.writeFile(trustMetricsPath(), JSON.stringify(m, null, 2), 'utf-8');
1665
+ }
1666
+ async function handleTrustEvent(req, res, _ctx) {
1667
+ let body;
1668
+ try {
1669
+ body = JSON.parse(await readBody(req));
1670
+ }
1671
+ catch {
1672
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
1673
+ return;
1674
+ }
1675
+ const kind = typeof body.kind === 'string' ? body.kind : '';
1676
+ const m = await loadTrustMetrics();
1677
+ const now = new Date().toISOString();
1678
+ switch (kind) {
1679
+ case 'diff_expansion':
1680
+ m.diff_expansions += 1;
1681
+ break;
1682
+ case 'editor_open':
1683
+ m.editor_opens += 1;
1684
+ break;
1685
+ case 'forge_override':
1686
+ m.forge_overrides += 1;
1687
+ break;
1688
+ case 'user_chose_show':
1689
+ m.user_choice = 'show';
1690
+ break;
1691
+ case 'user_chose_hide':
1692
+ m.user_choice = 'hide';
1693
+ break;
1694
+ default:
1695
+ sendJson(res, 400, { ok: false, error: 'unknown kind: ' + kind });
1696
+ return;
1697
+ }
1698
+ m.last_event_at = now;
1699
+ await saveTrustMetrics(m);
1700
+ sendJson(res, 200, { ok: true, metrics: m });
1701
+ }
1702
+ async function handleKeysStatus(_req, res) {
1703
+ /* Read ~/.yujin-forge/provider_keys.json + license.json without
1704
+ * exposing values. Just boolean presence.
1705
+ *
1706
+ * alpha.34 fix (Producer/Consumer Symmetry, SQ Sec 14):
1707
+ * Keys in the response MUST match the JSON slot names used by:
1708
+ * - the writer (POST /api/forge/keys-set, src/chat/tools/keys.ts)
1709
+ * - the panel HTML rows (data-key-slot="anthropic_api_key")
1710
+ * Previously this endpoint returned short names ('anthropic')
1711
+ * while the panel + writer used full slot names
1712
+ * ('anthropic_api_key'), so the per-row UI always showed every
1713
+ * row as MISSING even when the keys were present. Symptom Pablo
1714
+ * caught: 'figuran desactivadas pero brain + STT/TTS funcionan'.
1715
+ *
1716
+ * Also returns license_plan as a STRING (not a boolean) so the
1717
+ * settings panel can display the actual plan label. */
1718
+ /* alpha.44 C1: keys live in the encrypted Vault now. The
1719
+ * helper auto-migrates the legacy provider_keys.json on first
1720
+ * read; we just ask for the boolean status of each slot. */
1721
+ const { listStatus } = await import('../core/provider_keys.js');
1722
+ const out = await listStatus();
1723
+ const fs = await import('node:fs');
1724
+ const path = await import('node:path');
1725
+ const os = await import('node:os');
1726
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
1727
+ let license_plan = 'none';
1728
+ let license_user_handle = null;
1729
+ let license_last_validated_at = null;
1730
+ try {
1731
+ const p = path.join(home, '.yujin-forge', 'license.json');
1732
+ const raw = await fs.promises.readFile(p, 'utf-8');
1733
+ const j = JSON.parse(raw);
1734
+ if (typeof j.plan === 'string')
1735
+ license_plan = j.plan;
1736
+ if (typeof j.user_handle === 'string')
1737
+ license_user_handle = j.user_handle;
1738
+ if (typeof j.last_validated_at === 'string')
1739
+ license_last_validated_at = j.last_validated_at;
1740
+ }
1741
+ catch { /* no license file */ }
1742
+ sendJson(res, 200, {
1743
+ ok: true,
1744
+ keys: out,
1745
+ license_plan,
1746
+ license_user_handle,
1747
+ license_last_validated_at,
1748
+ license_can_sync: license_user_handle !== null,
1749
+ });
1750
+ }
1751
+ /** alpha.59z.114 -- pull the canonical license row from the
1752
+ * HITO 4 worker (which reads the Polar webhook D1) and refresh
1753
+ * the local cache. Hit by the panel's "Sincronizar plan" button
1754
+ * + by the silent auto-sync on settings open when local plan
1755
+ * reads 'none' but user_handle is set. */
1756
+ async function handleLicenseSync(_req, res) {
1757
+ const { syncLicense } = await import('../license/sync.js');
1758
+ const report = await syncLicense({ verbose: false });
1759
+ if (!report.ok) {
1760
+ sendJson(res, 200, {
1761
+ ok: false,
1762
+ error: report.error ?? 'sync failed',
1763
+ http_status: report.http_status,
1764
+ local_plan_before: report.local_plan_before,
1765
+ });
1766
+ return;
1767
+ }
1768
+ sendJson(res, 200, {
1769
+ ok: true,
1770
+ server_plan: report.server_plan,
1771
+ local_plan_before: report.local_plan_before,
1772
+ local_plan_after: report.local_plan_after,
1773
+ last_validated_at: report.last_validated_at,
1774
+ });
1775
+ }
1776
+ /** alpha.59z.117 -- write user_handle to the local polar cache
1777
+ * + run an immediate sync. Returns the final cache state.
1778
+ * Used by the panel "Conectar tu cuenta Polar" first-run flow. */
1779
+ async function handleLicenseHandle(req, res) {
1780
+ let body = {};
1781
+ try {
1782
+ const raw = await readBody(req);
1783
+ body = raw ? JSON.parse(raw) : {};
1784
+ }
1785
+ catch {
1786
+ sendJson(res, 400, { ok: false, error: 'invalid JSON' });
1787
+ return;
1788
+ }
1789
+ const handle = typeof body.handle === 'string' ? body.handle.trim().toLowerCase() : '';
1790
+ if (!handle || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(handle)) {
1791
+ sendJson(res, 400, { ok: false, error: 'handle must be a valid email address' });
1792
+ return;
1793
+ }
1794
+ const polar = await import('../core/polar.js');
1795
+ const current = await polar.readLicense();
1796
+ await polar.writeLicense({ ...current, user_handle: handle });
1797
+ const { syncLicense } = await import('../license/sync.js');
1798
+ const report = await syncLicense({ verbose: false });
1799
+ sendJson(res, 200, {
1800
+ ok: report.ok,
1801
+ handle_saved: handle,
1802
+ server_plan: report.server_plan,
1803
+ local_plan_before: report.local_plan_before,
1804
+ local_plan_after: report.local_plan_after,
1805
+ last_validated_at: report.last_validated_at,
1806
+ error: report.error,
1807
+ http_status: report.http_status,
1808
+ });
1809
+ }
1810
+ const ALLOWED_KEY_SLOTS = new Set([
1811
+ 'anthropic_api_key',
1812
+ 'openai_api_key',
1813
+ 'google_ai_key',
1814
+ 'google_stt_key',
1815
+ 'google_tts_key',
1816
+ 'whisper_api_key',
1817
+ 'elevenlabs_api_key',
1818
+ ]);
1819
+ async function handleReadFile(req, res, ctx) {
1820
+ const u = new URL(req.url ?? '/', 'http://localhost');
1821
+ const rel = (u.searchParams.get('path') || '').trim();
1822
+ if (!rel || rel.startsWith('/') || rel.includes('..') || rel.length > 500) {
1823
+ sendJson(res, 400, { ok: false, error: 'path must be a non-empty project-relative string (no .. , no leading slash)' });
1824
+ return;
1825
+ }
1826
+ const path = await import('node:path');
1827
+ const fs = await import('node:fs');
1828
+ const abs = path.resolve(ctx.projectRoot, rel);
1829
+ if (!isInside(abs, ctx.projectRoot)) {
1830
+ sendJson(res, 403, { ok: false, error: 'path escapes project root' });
1831
+ return;
1832
+ }
1833
+ try {
1834
+ const buf = await fs.promises.readFile(abs);
1835
+ /* Treat as text if it parses cleanly; otherwise refuse. */
1836
+ if (buf.length > 2_000_000) {
1837
+ sendJson(res, 413, { ok: false, error: 'file too large (>2 MB)' });
1838
+ return;
1839
+ }
1840
+ const text = buf.toString('utf-8');
1841
+ sendJson(res, 200, { ok: true, path: rel, content: text, size_bytes: buf.length });
1842
+ }
1843
+ catch (err) {
1844
+ sendJson(res, 404, { ok: false, error: 'file not found: ' + (err instanceof Error ? err.message : String(err)) });
1845
+ }
1846
+ }
1847
+ async function handleWriteFile(req, res, ctx) {
1848
+ let raw;
1849
+ try {
1850
+ raw = await readBody(req);
1851
+ }
1852
+ catch {
1853
+ sendJson(res, 400, { ok: false, error: 'unreadable body' });
1854
+ return;
1855
+ }
1856
+ let body;
1857
+ try {
1858
+ body = JSON.parse(raw);
1859
+ }
1860
+ catch {
1861
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
1862
+ return;
1863
+ }
1864
+ const rel = typeof body.path === 'string' ? body.path.trim() : '';
1865
+ const content = typeof body.content === 'string' ? body.content : '';
1866
+ if (!rel || rel.startsWith('/') || rel.includes('..') || rel.length > 500) {
1867
+ sendJson(res, 400, { ok: false, error: 'path must be a non-empty project-relative string (no .. , no leading slash)' });
1868
+ return;
1869
+ }
1870
+ if (content.length > 5_000_000) {
1871
+ sendJson(res, 413, { ok: false, error: 'content too large (>5 MB)' });
1872
+ return;
1873
+ }
1874
+ const path = await import('node:path');
1875
+ const fs = await import('node:fs');
1876
+ const abs = path.resolve(ctx.projectRoot, rel);
1877
+ if (!isInside(abs, ctx.projectRoot)) {
1878
+ sendJson(res, 403, { ok: false, error: 'path escapes project root' });
1879
+ return;
1880
+ }
1881
+ try {
1882
+ /* Atomic write: tmp file + rename. */
1883
+ await fs.promises.mkdir(path.dirname(abs), { recursive: true });
1884
+ const tmp = abs + '.tmp.' + Date.now();
1885
+ await fs.promises.writeFile(tmp, content, 'utf-8');
1886
+ await fs.promises.rename(tmp, abs);
1887
+ const st = await fs.promises.stat(abs);
1888
+ sendJson(res, 200, { ok: true, path: rel, bytes_written: st.size });
1889
+ }
1890
+ catch (err) {
1891
+ sendJson(res, 500, { ok: false, error: 'write failed: ' + (err instanceof Error ? err.message : String(err)) });
1892
+ }
1893
+ }
1894
+ async function handleKeysSet(req, res) {
1895
+ let raw;
1896
+ try {
1897
+ raw = await readBody(req);
1898
+ }
1899
+ catch (err) {
1900
+ sendJson(res, 400, { ok: false, error: 'unreadable body' });
1901
+ return;
1902
+ }
1903
+ let body;
1904
+ try {
1905
+ body = JSON.parse(raw);
1906
+ }
1907
+ catch {
1908
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
1909
+ return;
1910
+ }
1911
+ const slot = typeof body.slot === 'string' ? body.slot.trim() : '';
1912
+ if (!ALLOWED_KEY_SLOTS.has(slot)) {
1913
+ sendJson(res, 400, { ok: false, error: 'slot must be one of: ' + Array.from(ALLOWED_KEY_SLOTS).join(', ') });
1914
+ return;
1915
+ }
1916
+ /* value === '' OR null means clear. Otherwise it is the new
1917
+ * key value (min 8 chars, no leading/trailing whitespace). */
1918
+ const value = body.value === null ? '' : (typeof body.value === 'string' ? body.value.trim() : '');
1919
+ if (value !== '' && value.length < 8) {
1920
+ sendJson(res, 400, { ok: false, error: 'key value too short (min 8 chars)' });
1921
+ return;
1922
+ }
1923
+ /* alpha.44 C1: persist via the encrypted Vault helper. The
1924
+ * helper handles the legacy plaintext migration on first call
1925
+ * + atomic write + slot validation. */
1926
+ try {
1927
+ const { setKey } = await import('../core/provider_keys.js');
1928
+ await setKey(slot, value === '' ? null : value);
1929
+ }
1930
+ catch (err) {
1931
+ sendJson(res, 400, {
1932
+ ok: false,
1933
+ error: err instanceof Error ? err.message : String(err),
1934
+ });
1935
+ return;
1936
+ }
1937
+ sendJson(res, 200, { ok: true, slot, set: value !== '' });
1938
+ }
1939
+ async function handleSupportReport(req, res) {
1940
+ let raw;
1941
+ try {
1942
+ raw = await readBody(req);
1943
+ }
1944
+ catch (err) {
1945
+ sendJson(res, 400, { ok: false, error: 'unreadable body: ' + (err instanceof Error ? err.message : String(err)) });
1946
+ return;
1947
+ }
1948
+ let body;
1949
+ try {
1950
+ body = JSON.parse(raw);
1951
+ }
1952
+ catch {
1953
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
1954
+ return;
1955
+ }
1956
+ const { validateInbound, recordReport } = await import('../support/reports.js');
1957
+ /* Support either a single report or an array (the panel
1958
+ * batches up to 50 + flushes every 30s). */
1959
+ const ua = typeof req.headers['user-agent'] === 'string' ? req.headers['user-agent'] : undefined;
1960
+ const items = Array.isArray(body) ? body : [body];
1961
+ let accepted = 0;
1962
+ let rejected = 0;
1963
+ for (const item of items.slice(0, 50)) {
1964
+ const report = validateInbound(item, ua ? { user_agent: ua } : {});
1965
+ if (report) {
1966
+ await recordReport(report);
1967
+ accepted++;
1968
+ }
1969
+ else {
1970
+ rejected++;
1971
+ }
1972
+ }
1973
+ sendJson(res, 200, { ok: true, accepted, rejected });
1974
+ }
1975
+ async function handleSupportReportList(req, res) {
1976
+ /* Local panel only -- we already gate the chat server to
1977
+ * loopback. No additional auth needed for read. */
1978
+ const { readReports } = await import('../support/reports.js');
1979
+ const u = new URL(req.url ?? '/', 'http://localhost');
1980
+ const limit = Math.min(500, Math.max(1, parseInt(u.searchParams.get('limit') ?? '100', 10) || 100));
1981
+ const reports = (await readReports()).slice(-limit);
1982
+ sendJson(res, 200, { ok: true, count: reports.length, reports });
1983
+ }
1984
+ async function handleTrustState(_req, res, _ctx) {
1985
+ const m = await loadTrustMetrics();
1986
+ /* Decay logic: if no events in last 30 days AND user has not
1987
+ * explicitly chosen to keep showing, suggest auto-collapse.
1988
+ * The panel decides what to do with the suggestion. */
1989
+ let shouldProposeCollapse = false;
1990
+ if (m.user_choice === 'default') {
1991
+ const last = m.last_event_at ? new Date(m.last_event_at).getTime() : 0;
1992
+ const proposedRecently = m.last_proposal_at
1993
+ ? (Date.now() - new Date(m.last_proposal_at).getTime()) < 7 * 24 * 3600 * 1000
1994
+ : false;
1995
+ const daysSince = last ? (Date.now() - last) / (24 * 3600 * 1000) : Infinity;
1996
+ shouldProposeCollapse = daysSince > 30 && !proposedRecently;
1997
+ }
1998
+ sendJson(res, 200, {
1999
+ ok: true,
2000
+ metrics: m,
2001
+ propose_auto_collapse: shouldProposeCollapse,
2002
+ });
2003
+ }
951
2004
  async function handleVaultList(res) {
952
2005
  try {
953
2006
  const vault = await Vault.open({ configDir: configDir() });
@@ -1272,6 +2325,37 @@ async function handleChat(req, res, ctx) {
1272
2325
  sendJson(res, 400, { ok: false, error: 'messages[] required' });
1273
2326
  return;
1274
2327
  }
2328
+ /* alpha.59e -- SSE streaming mode. Client opts in via
2329
+ * body.stream=true. We keep the response open + emit events
2330
+ * during the tool loop instead of bundling everything in a
2331
+ * single JSON. */
2332
+ const streamMode = body.stream === true;
2333
+ let sseEmit = null;
2334
+ if (streamMode) {
2335
+ res.writeHead(200, {
2336
+ 'content-type': 'text/event-stream; charset=utf-8',
2337
+ 'cache-control': 'no-cache, no-transform',
2338
+ 'connection': 'keep-alive',
2339
+ 'x-accel-buffering': 'no',
2340
+ });
2341
+ sseEmit = (event, data) => {
2342
+ try {
2343
+ const payload = JSON.stringify(data ?? {});
2344
+ res.write('event: ' + event + '\n');
2345
+ res.write('data: ' + payload + '\n\n');
2346
+ }
2347
+ catch { /* ignore */ }
2348
+ };
2349
+ }
2350
+ /* alpha.51 -- session-level brain tier override from slash
2351
+ * commands (/eco /frontier /auto). null = let cost_router
2352
+ * decide per-turn. The router reads workflow state + verb
2353
+ * hints to pick the right tier; this override pins the
2354
+ * decision when the user signaled an explicit preference. */
2355
+ let brainOverride;
2356
+ if (body.brain_tier_override === 'eco' || body.brain_tier_override === 'frontier') {
2357
+ brainOverride = body.brain_tier_override;
2358
+ }
1275
2359
  const normalized = [];
1276
2360
  for (const m of msgs) {
1277
2361
  if (typeof m !== 'object' || m === null)
@@ -1298,19 +2382,373 @@ async function handleChat(req, res, ctx) {
1298
2382
  const pilotState = await readPilotState(ctx.projectRoot);
1299
2383
  const lastUserMsg = normalized.filter((m) => m.role === 'user').slice(-1)[0];
1300
2384
  const lastUserText = typeof lastUserMsg?.content === 'string' ? lastUserMsg.content : '';
1301
- const reply = await ctx.claude.chat({
2385
+ /* alpha.51 -- ask the cost router which brain tier should
2386
+ * serve this turn. alpha.52 -- runtime clients for anthropic
2387
+ * + google_ai are wired. alpha.55 -- the router now runs a
2388
+ * reasoning_gate + turn_classifier when there is no cheap
2389
+ * signal so /auto becomes genuinely smart per turn. We pass
2390
+ * the user_text + recent history + attachment count + the
2391
+ * previous tier so the gate can detect sticky follow-ups
2392
+ * and inherit. */
2393
+ const { selectBrainForTurn } = await import('../core/cost_router.js');
2394
+ const { makeUnifiedClient } = await import('../llm/multi_provider_client.js');
2395
+ /* Pull previous tier from the last brain_routing.jsonl entry
2396
+ * via the store (best-effort). The store keeps the last
2397
+ * assistant reply; if it has a `brain_tier` annotation we use
2398
+ * it. When absent, the gate just skips the sticky check. */
2399
+ const recent = ctx.store.messages();
2400
+ const lastAssistant = recent.slice().reverse().find((m) => m.role === 'assistant');
2401
+ const previousTier = lastAssistant && (lastAssistant.brain_tier === 'eco' || lastAssistant.brain_tier === 'frontier')
2402
+ ? lastAssistant.brain_tier
2403
+ : undefined;
2404
+ const previousDocLoaded = !!(lastAssistant && lastAssistant.doc_loaded === true);
2405
+ const historyForGate = normalized.slice(-3).map((m) => ({
2406
+ role: m.role,
2407
+ content: typeof m.content === 'string' ? m.content : '',
2408
+ }));
2409
+ /* alpha.57 -- workflow phase wiring. If the user is in
2410
+ * Phase III (specs: RFP / Arch / Design) or Phase V (block
2411
+ * implementation), the router forces frontier. This is the
2412
+ * structural signal Pablo flagged: the workflow itself
2413
+ * knows when reasoning is needed. */
2414
+ let workflowPhase;
2415
+ try {
2416
+ const { readWorkflow } = await import('../workflow/state.js');
2417
+ const { inferActivePhase } = await import('../workflow/phase_inference.js');
2418
+ const wf = await readWorkflow(ctx.projectRoot);
2419
+ workflowPhase = inferActivePhase(wf);
2420
+ }
2421
+ catch { /* missing yujin.forge.json -> phase_i, no-op */ }
2422
+ const brainSelection = await selectBrainForTurn({
2423
+ user_override: brainOverride,
2424
+ caller_hint: 'handleChat',
2425
+ user_text: lastUserText,
2426
+ history: historyForGate,
2427
+ previous_tier: previousTier,
2428
+ previous_doc_loaded: previousDocLoaded,
2429
+ workflow_phase: workflowPhase,
2430
+ });
2431
+ /* alpha.55 -- cap exceeded check. If the tier the router
2432
+ * chose has zero remaining budget, return 429 BEFORE the
2433
+ * LLM call so the user never gets billed past their cap.
2434
+ * The panel modal (plan_limit_exceeded) lets them upgrade
2435
+ * or switch tier. BYOK plans have Infinity cap so this
2436
+ * is a no-op there. */
2437
+ {
2438
+ const { checkCap } = await import('../core/budget.js');
2439
+ const capCheck = await checkCap(brainSelection.tier);
2440
+ if (!capCheck.allowed) {
2441
+ if (sseEmit) {
2442
+ sseEmit('error', {
2443
+ kind: 'plan_limit_exceeded',
2444
+ tier: capCheck.tier,
2445
+ plan: capCheck.plan,
2446
+ extension_url: capCheck.extension_url ?? null,
2447
+ });
2448
+ res.end();
2449
+ }
2450
+ else {
2451
+ sendJson(res, 429, {
2452
+ ok: false,
2453
+ error: 'plan_limit_exceeded',
2454
+ tier: capCheck.tier,
2455
+ plan: capCheck.plan,
2456
+ cap: Number.isFinite(capCheck.cap) ? capCheck.cap : null,
2457
+ used: capCheck.used,
2458
+ remaining: Number.isFinite(capCheck.remaining) ? capCheck.remaining : null,
2459
+ extension_url: capCheck.extension_url ?? null,
2460
+ reason: capCheck.reason,
2461
+ message: 'Has agotado el cupo mensual del tier ' + capCheck.tier +
2462
+ ' en el plan ' + capCheck.plan + '. Cambia de tier con /eco o /frontier, ' +
2463
+ 'compra una extension, o usa BYOK.',
2464
+ });
2465
+ }
2466
+ return;
2467
+ }
2468
+ }
2469
+ /* alpha.59e -- emit turn_start event so the panel knows
2470
+ * which model is serving + how many rounds the loop can
2471
+ * spend before exhaustion. */
2472
+ if (sseEmit) {
2473
+ sseEmit('turn_start', {
2474
+ tier: brainSelection.tier,
2475
+ provider: brainSelection.provider,
2476
+ model: brainSelection.model,
2477
+ runtime_wired: brainSelection.runtime_wired,
2478
+ reason: brainSelection.reason,
2479
+ workflow_phase: workflowPhase ?? null,
2480
+ });
2481
+ }
2482
+ /* alpha.59e -- loop detection counter. We track (verb_id +
2483
+ * args fingerprint) per turn; if the same call is made 3+
2484
+ * times consecutively we abort + emit an error so the LLM
2485
+ * cannot infinite-loop when Danger Zone is on. */
2486
+ const callSig = [];
2487
+ const LOOP_REPEAT_THRESHOLD = 3;
2488
+ const unified = makeUnifiedClient(brainSelection);
2489
+ const callerForChat = unified
2490
+ ? unified
2491
+ : {
2492
+ chat: async (r) => {
2493
+ const claudeReply = await ctx.claude.chat(r);
2494
+ return { ...claudeReply, brain: { ...brainSelection, runtime_wired: false } };
2495
+ },
2496
+ };
2497
+ /* alpha.59p -- semantic graph injection. Pulls "what primitives
2498
+ * exist in this project" from the worker + appends to system
2499
+ * prompt so the LLM does NOT reinvent libs/components/protocols
2500
+ * already present. Graceful no-op when worker unreachable or
2501
+ * no vault key. */
2502
+ let graphSection = '';
2503
+ try {
2504
+ const { fetchGraphSection, injectionEnabled } = await import('./semantic_graph_inject.js');
2505
+ if (injectionEnabled()) {
2506
+ const lastUser = [...normalized].reverse().find((m) => m.role === 'user');
2507
+ const intent = lastUser && typeof lastUser.content === 'string'
2508
+ ? lastUser.content
2509
+ : (lastUser && Array.isArray(lastUser.content)
2510
+ ? lastUser.content.filter((b) => b && b.type === 'text').map((b) => b.text).join(' ')
2511
+ : '');
2512
+ if (intent && intent.length > 0) {
2513
+ const r = await fetchGraphSection({
2514
+ intent,
2515
+ project_slug: ctx.projectSlug || '',
2516
+ limit: 8,
2517
+ token_budget: 500,
2518
+ timeout_ms: 1500,
2519
+ });
2520
+ if (r.ok && r.rendered_text)
2521
+ graphSection = '\n\n' + r.rendered_text;
2522
+ }
2523
+ }
2524
+ }
2525
+ catch (_) { /* swallow -- chat continues without graph */ }
2526
+ /* alpha.59y.2 -- workflow state injection. Reads
2527
+ * yujin.forge.json/workflow.* and emits a compact "PROJECT
2528
+ * WORKFLOW STATE" block with rules of engagement (stay in
2529
+ * step, ASK on scope change, no silent advance). Disabled
2530
+ * via YF_WORKFLOW_INJECT_DISABLED=1. */
2531
+ let workflowSection = '';
2532
+ try {
2533
+ const { fetchWorkflowSection } = await import('./workflow_inject.js');
2534
+ const w = await fetchWorkflowSection({
2535
+ projectRoot: ctx.projectRoot,
2536
+ projectName: ctx.projectName,
2537
+ });
2538
+ if (w.ok && w.rendered_text)
2539
+ workflowSection = '\n\n' + w.rendered_text;
2540
+ }
2541
+ catch (_) { /* swallow -- chat continues without workflow inject */ }
2542
+ /* alpha.59z.15 HITO 6.3 -- prefetch global learnings from
2543
+ * the F103 worker. Falls back silently to '' on worker
2544
+ * unreachable. Cache is 5 min so this is cheap. */
2545
+ let globalLearnings = '';
2546
+ try {
2547
+ const { buildGlobalLearningsSection } = await import('../core/learnings_client.js');
2548
+ globalLearnings = await buildGlobalLearningsSection({ maxTokens: 2000 });
2549
+ }
2550
+ catch (_) { /* swallow -- learnings are additive */ }
2551
+ /* alpha.59z.26 F101 slice 2 + slice 5 sub-slice 3 -- prefetch
2552
+ * MCP bridges count + persistent enabled-state so the manifest
2553
+ * stub surfaces yujin.mcp.* verbs only when there is at least
2554
+ * one ENABLED bridge (Invariant 1: zero noise when Forge is
2555
+ * not in MCP mode OR every bridge is disabled). Registry has
2556
+ * 60s cache + state.ts has its own 5s cache so the call is
2557
+ * cheap. */
2558
+ let mcpAvailable = false;
2559
+ try {
2560
+ const { getActiveBridges } = await import('../mcp/bridge_registry.js');
2561
+ const bridges = await getActiveBridges({ projectRoot: ctx.projectRoot });
2562
+ mcpAvailable = bridges.some((b) => b.enabled);
2563
+ }
2564
+ catch (_) { /* registry failure -> stay with MCP-off behaviour */ }
2565
+ /* alpha.59z.37 NAC-3 v3.x backend tools -- prefetch the
2566
+ * adopter app's nac3_backend section (task #23). Empty
2567
+ * string when the manifest does not declare it, when the
2568
+ * file is missing, or when parsing failed. */
2569
+ let backendToolsSection = '';
2570
+ try {
2571
+ const { loadBackendCatalogue, buildBackendToolsSection } = await import('../nac3/backend_tools.js');
2572
+ const parsed = await loadBackendCatalogue(ctx.projectRoot);
2573
+ if (parsed.catalogue && parsed.catalogue.tools.length > 0) {
2574
+ backendToolsSection = buildBackendToolsSection(parsed.catalogue);
2575
+ }
2576
+ }
2577
+ catch (_) { /* swallow */ }
2578
+ const chatRequest = {
1302
2579
  messages: normalized,
1303
- system: buildSystemPrompt(ctx, reqMode, pilotState),
2580
+ system: buildSystemPrompt(ctx, reqMode, pilotState, {
2581
+ tier: brainSelection.tier,
2582
+ provider: brainSelection.provider,
2583
+ model: brainSelection.model,
2584
+ }, globalLearnings, mcpAvailable, backendToolsSection) + graphSection + workflowSection,
1304
2585
  maxTokens: 1024,
1305
2586
  tools: FORGE_TOOL_SPECS,
1306
2587
  runTool: async (name, input) => {
1307
- const r = await runForgeTool(name, input, { projectRoot: ctx.projectRoot });
2588
+ /* alpha.59e -- loop detection. Build a signature from
2589
+ * the verb id + arg hash; if the last N entries are
2590
+ * identical, refuse this call. */
2591
+ const verbId = typeof input.verb_id === 'string'
2592
+ ? String(input.verb_id)
2593
+ : name;
2594
+ const argsSig = verbId + ':' + JSON.stringify(input).slice(0, 200);
2595
+ callSig.push(argsSig);
2596
+ const recent = callSig.slice(-LOOP_REPEAT_THRESHOLD);
2597
+ if (recent.length === LOOP_REPEAT_THRESHOLD && recent.every((s) => s === argsSig)) {
2598
+ if (sseEmit) {
2599
+ sseEmit('error', {
2600
+ kind: 'loop_detected',
2601
+ verb_id: verbId,
2602
+ repeats: LOOP_REPEAT_THRESHOLD,
2603
+ message: 'Mismo verb invocado ' + LOOP_REPEAT_THRESHOLD + 'x consecutivo. Aborto el turn.',
2604
+ });
2605
+ }
2606
+ return {
2607
+ result: {
2608
+ error: 'loop_detected',
2609
+ verb_id: verbId,
2610
+ repeats: LOOP_REPEAT_THRESHOLD,
2611
+ hint: 'Probablemente el LLM esta llamando el mismo verbo con los mismos args en bucle. Reformula el prompt o ejecuta el paso manualmente.',
2612
+ },
2613
+ is_error: true,
2614
+ };
2615
+ }
2616
+ /* alpha.59e -- emit tool_round_start for the panel
2617
+ * progress bar. verb_hint = a short string the user
2618
+ * sees ("Leyendo seccion X", "Ejecutando git status",
2619
+ * etc) -- derived from verb_id + input. */
2620
+ let verbHint = verbId;
2621
+ if (verbId === 'yujin.shell.exec' && typeof input.command === 'string') {
2622
+ verbHint = 'shell: ' + String(input.command).slice(0, 60);
2623
+ }
2624
+ else if (verbId === 'yujin.reader.next-block') {
2625
+ verbHint = 'leyendo siguiente bloque';
2626
+ }
2627
+ else if (verbId.startsWith('yujin.reader.')) {
2628
+ verbHint = verbId.replace('yujin.reader.', 'reader/');
2629
+ }
2630
+ else if (verbId.startsWith('yujin.fs.')) {
2631
+ verbHint = verbId.replace('yujin.fs.', 'fs/');
2632
+ }
2633
+ else if (verbId.startsWith('yujin.pizarron.')) {
2634
+ verbHint = verbId.replace('yujin.pizarron.', 'pizarron/');
2635
+ }
2636
+ if (sseEmit) {
2637
+ sseEmit('tool_round_start', {
2638
+ round: callSig.length,
2639
+ verb_id: verbId,
2640
+ verb_hint: verbHint,
2641
+ });
2642
+ }
2643
+ const t0 = Date.now();
2644
+ /* alpha.59f -- inject streamEmit into ToolContext so
2645
+ * runShellExec (and future long-running verbs) can emit
2646
+ * progress events mid-execution. */
2647
+ const toolCtx = {
2648
+ projectRoot: ctx.projectRoot,
2649
+ ...(sseEmit ? { streamEmit: sseEmit } : {}),
2650
+ };
2651
+ const r = await runForgeTool(name, input, toolCtx);
2652
+ /* alpha.59i -- if the result is an approval envelope,
2653
+ * surface it as a dedicated SSE event so the panel
2654
+ * opens the modal NOW instead of waiting for the full
2655
+ * turn_complete payload. The envelope still flows
2656
+ * through the regular tool_round path so the LLM sees
2657
+ * it as a tool result (which is what triggers the
2658
+ * legacy flow: turn ends, user approves, next turn
2659
+ * carries the synthetic 'approved' message). */
2660
+ if (sseEmit && r && r.result
2661
+ && typeof r.result === 'object'
2662
+ && r.result
2663
+ .nac3_panel_approval_required === true) {
2664
+ sseEmit('approval_required', r.result);
2665
+ }
2666
+ if (sseEmit) {
2667
+ sseEmit('tool_round_done', {
2668
+ round: callSig.length,
2669
+ verb_id: verbId,
2670
+ is_error: r.is_error === true,
2671
+ duration_ms: Date.now() - t0,
2672
+ });
2673
+ }
1308
2674
  return {
1309
2675
  result: r.result,
1310
2676
  ...(r.is_error ? { is_error: true } : {}),
1311
2677
  };
1312
2678
  },
1313
- });
2679
+ };
2680
+ let reply = await callerForChat.chat(chatRequest);
2681
+ /* Echo brainSelection if the underlying client did not stamp
2682
+ * it (legacy ctx.claude path). */
2683
+ if (!reply.brain) {
2684
+ reply.brain = brainSelection;
2685
+ }
2686
+ /* alpha.57 -- post-reply escalation. If the eco tier produced
2687
+ * a confused / refused / truncated reply (hallucinated error
2688
+ * markers, dodge phrases, schema errors, length-vs-complexity
2689
+ * mismatch), re-issue the same turn at frontier and show the
2690
+ * user THAT reply instead. Cap-aware: only escalates if
2691
+ * frontier still has budget. Cap 1 escalation per turn. */
2692
+ let escalated = false;
2693
+ let escalationSignals = [];
2694
+ if (brainSelection.tier === 'eco' && !brainOverride) {
2695
+ const { detectIncomplete } = await import('../core/incomplete_detector.js');
2696
+ const detect = detectIncomplete({
2697
+ user_text: lastUserText,
2698
+ reply_text: reply.text,
2699
+ tool_rounds: reply.toolRounds,
2700
+ });
2701
+ if (detect.incomplete) {
2702
+ const { checkCap } = await import('../core/budget.js');
2703
+ const capCheck = await checkCap('frontier');
2704
+ if (capCheck.allowed) {
2705
+ const frontierSel = await selectBrainForTurn({
2706
+ user_override: 'frontier',
2707
+ caller_hint: 'escalation_after_eco[' + detect.signals.join(',').slice(0, 80) + ']',
2708
+ user_text: lastUserText,
2709
+ history: historyForGate,
2710
+ previous_tier: previousTier,
2711
+ previous_doc_loaded: previousDocLoaded,
2712
+ workflow_phase: workflowPhase,
2713
+ });
2714
+ const frontierClient = makeUnifiedClient(frontierSel);
2715
+ const frontierCaller = frontierClient
2716
+ ? frontierClient
2717
+ : {
2718
+ chat: async (r) => {
2719
+ const claudeReply = await ctx.claude.chat(r);
2720
+ return { ...claudeReply, brain: { ...frontierSel, runtime_wired: false } };
2721
+ },
2722
+ };
2723
+ try {
2724
+ /* alpha.58 -- rebuild system prompt for the frontier
2725
+ * call so the tier header reflects the actual model
2726
+ * serving the turn. */
2727
+ const frontierRequest = {
2728
+ ...chatRequest,
2729
+ system: buildSystemPrompt(ctx, reqMode, pilotState, {
2730
+ tier: frontierSel.tier,
2731
+ provider: frontierSel.provider,
2732
+ model: frontierSel.model,
2733
+ }, globalLearnings, mcpAvailable, backendToolsSection),
2734
+ };
2735
+ const frontierReply = await frontierCaller.chat(frontierRequest);
2736
+ reply = frontierReply;
2737
+ escalated = true;
2738
+ escalationSignals = detect.signals;
2739
+ }
2740
+ catch (escErr) {
2741
+ /* Escalation failure -> stick with the eco reply.
2742
+ * Log + continue. */
2743
+ const msg = escErr instanceof Error ? escErr.message : String(escErr);
2744
+ escalationSignals = [...detect.signals, 'escalation_failed[' + msg.slice(0, 60) + ']'];
2745
+ }
2746
+ }
2747
+ else {
2748
+ escalationSignals = [...detect.signals, 'escalation_blocked_by_cap'];
2749
+ }
2750
+ }
2751
+ }
1314
2752
  // Append the latest user message + the assistant's reply to
1315
2753
  // the persistent transcript. Older messages may already be
1316
2754
  // present from previous turns; we only need to capture the
@@ -1325,7 +2763,36 @@ async function handleChat(req, res, ctx) {
1325
2763
  ctx.store.append(lastUser);
1326
2764
  }
1327
2765
  }
1328
- ctx.store.append({ role: 'assistant', content: reply.text });
2766
+ /* alpha.56 -- tag the assistant turn with the tier that
2767
+ * served it. The next turn's reasoning_gate reads this via
2768
+ * lastAssistant.brain_tier to apply sticky inertia (a
2769
+ * follow-up to a frontier turn stays in frontier unless the
2770
+ * new user message has strong eco signals). Also detect
2771
+ * whether the turn loaded a document (reader.* / fs.read-*)
2772
+ * so the next router call routes to frontier even on a
2773
+ * short follow-up like "Medium, Python" (because the user
2774
+ * is about to ask the model to reason over the doc). */
2775
+ const docLoaded = reply.toolRounds.some((r) => {
2776
+ const input = r.input;
2777
+ const vid = (input && typeof input === 'object' && 'verb_id' in input)
2778
+ ? input.verb_id
2779
+ : undefined;
2780
+ const v = typeof vid === 'string' ? vid : '';
2781
+ return (v === 'yujin.fs.read-file' ||
2782
+ v === 'yujin.fs.read-project-file' ||
2783
+ v.startsWith('yujin.reader.'));
2784
+ });
2785
+ /* alpha.57 -- when the turn escalated, the persisted tier is
2786
+ * the FINAL one (frontier), not the eco initial one. This
2787
+ * way the next turn's sticky inertia + previous_tier check
2788
+ * see the canonical decision. */
2789
+ const finalTier = escalated ? 'frontier' : brainSelection.tier;
2790
+ ctx.store.append({
2791
+ role: 'assistant',
2792
+ content: reply.text,
2793
+ brain_tier: finalTier,
2794
+ ...(docLoaded ? { doc_loaded: true } : {}),
2795
+ });
1329
2796
  void ctx.store.flush();
1330
2797
  /* Pilot first-run auto-complete (SQ 0.10). If Pilot is
1331
2798
  not yet completed, decide based on the user's first
@@ -1339,77 +2806,429 @@ async function handleChat(req, res, ctx) {
1339
2806
  catch { /* non-fatal */ }
1340
2807
  }
1341
2808
  }
1342
- sendJson(res, 200, {
2809
+ /* alpha.59e -- build the response payload once, then either
2810
+ * stream it as a turn_complete SSE event OR send the legacy
2811
+ * JSON response. */
2812
+ const responsePayload = {
1343
2813
  ok: true,
1344
2814
  message: { role: 'assistant', text: reply.text },
1345
2815
  tokens: { in: reply.tokensIn, out: reply.tokensOut },
1346
2816
  model: reply.model,
2817
+ /* alpha.43 -- manifest version stamp so the panel can detect
2818
+ drift after a server reboot + force-refetch its cached
2819
+ manifest in the background. */
2820
+ nac3_manifest_version: nac3Manifest().version,
2821
+ /* alpha.51 + alpha.57 -- which brain tier served this turn.
2822
+ * Panel can show a small badge so the user knows whether
2823
+ * the answer came from eco or frontier. Keys redacted
2824
+ * server-side. When the turn escalated from eco to frontier
2825
+ * (alpha.57 post-reply detector), `escalated` is true and
2826
+ * `escalation_signals` carries the detector verdict for
2827
+ * audit. The final reply shown to the user is the frontier
2828
+ * one; only it gets persisted to the store. */
2829
+ brain_routing: {
2830
+ tier: (reply.brain?.tier) || brainSelection.tier,
2831
+ provider: (reply.brain?.provider) || brainSelection.provider,
2832
+ model: (reply.brain?.model) || brainSelection.model,
2833
+ runtime_wired: (reply.brain?.runtime_wired) ?? brainSelection.runtime_wired,
2834
+ reason: (reply.brain?.reason) || brainSelection.reason,
2835
+ workflow_phase: workflowPhase ?? null,
2836
+ escalated,
2837
+ escalation_signals: escalationSignals.length > 0 ? escalationSignals : undefined,
2838
+ },
1347
2839
  /* Slice 4: surface the audit trail so the panel can render
1348
2840
  the action trace (which tools Claude called + with what
1349
- args + what came back). Empty array when no tools fired. */
1350
- tool_rounds: reply.toolRounds.map((r) => ({
1351
- tool: r.tool,
1352
- input: r.input,
1353
- display: typeof r.result === 'object' && r.result !== null && 'display' in r.result
2841
+ args + what came back). Empty array when no tools fired.
2842
+ alpha.35 fix: include the full `result` object so the
2843
+ panel can intercept tools like forge_show_in_pizarron
2844
+ (which needs the item_id + kind + content). The Claude
2845
+ loop already caps result size; we further cap the JSON
2846
+ here at 200KB defensive to keep the HTTP response from
2847
+ blowing up. */
2848
+ tool_rounds: reply.toolRounds.map((r) => {
2849
+ const display = typeof r.result === 'object' && r.result !== null && 'display' in r.result
1354
2850
  ? r.result.display
1355
- : undefined,
1356
- is_error: r.is_error ?? false,
1357
- })),
1358
- });
2851
+ : undefined;
2852
+ let result = r.result;
2853
+ try {
2854
+ const serialised = JSON.stringify(result ?? null);
2855
+ if (serialised.length > 200_000) {
2856
+ result = { _truncated: true, _bytes: serialised.length };
2857
+ }
2858
+ }
2859
+ catch {
2860
+ result = { _unserialisable: true };
2861
+ }
2862
+ return {
2863
+ tool: r.tool,
2864
+ input: r.input,
2865
+ display,
2866
+ is_error: r.is_error ?? false,
2867
+ result,
2868
+ };
2869
+ }),
2870
+ };
2871
+ if (sseEmit) {
2872
+ sseEmit('turn_complete', responsePayload);
2873
+ res.end();
2874
+ }
2875
+ else {
2876
+ sendJson(res, 200, responsePayload);
2877
+ }
1359
2878
  }
1360
2879
  catch (err) {
1361
2880
  if (err instanceof ConfigurationError) {
1362
- sendJson(res, 503, {
1363
- ok: false,
1364
- code: 'no_api_key',
1365
- error: err.message,
1366
- });
2881
+ if (sseEmit) {
2882
+ sseEmit('error', { kind: 'no_api_key', message: err.message });
2883
+ res.end();
2884
+ }
2885
+ else {
2886
+ sendJson(res, 503, {
2887
+ ok: false,
2888
+ code: 'no_api_key',
2889
+ error: err.message,
2890
+ });
2891
+ }
1367
2892
  return;
1368
2893
  }
1369
2894
  if (err instanceof ClaudeApiError) {
2895
+ if (sseEmit) {
2896
+ sseEmit('error', { kind: 'claude_api_error', message: err.message });
2897
+ res.end();
2898
+ }
2899
+ else {
2900
+ sendJson(res, 502, {
2901
+ ok: false,
2902
+ code: 'claude_api_error',
2903
+ error: err.message,
2904
+ });
2905
+ }
2906
+ return;
2907
+ }
2908
+ if (sseEmit) {
2909
+ sseEmit('error', { kind: 'internal', message: err instanceof Error ? err.message : String(err) });
2910
+ res.end();
2911
+ }
2912
+ else {
1370
2913
  sendJson(res, 502, {
1371
2914
  ok: false,
1372
- code: 'claude_api_error',
1373
- error: err.message,
2915
+ error: err instanceof Error ? err.message : String(err),
1374
2916
  });
1375
- return;
1376
2917
  }
1377
- sendJson(res, 502, {
1378
- ok: false,
1379
- error: err instanceof Error ? err.message : String(err),
1380
- });
1381
2918
  }
1382
2919
  }
1383
- function buildSystemPrompt(ctx, mode = 'didactico', pilotState = {
2920
+ export function buildSystemPrompt(ctx, mode = 'didactico', pilotState = {
1384
2921
  pilot_completed: true, target_pending: false, mode_pending: false,
1385
- }) {
2922
+ },
2923
+ /** alpha.58 -- which brain tier + provider + model is serving
2924
+ * this turn. Allows the LLM (especially eco-tier ones like
2925
+ * Gemini Flash Lite) to know its own role and be more
2926
+ * cautious / defer to nac3_invoke when in doubt. */
2927
+ brain,
2928
+ /** alpha.59z.15 HITO 6.3 -- the GLOBAL LEARNINGS section,
2929
+ * pre-fetched async by the caller via
2930
+ * buildGlobalLearningsSection() from learnings_client.ts.
2931
+ * Empty string -> no learnings (worker unreachable or no
2932
+ * active lessons). buildSystemPrompt stays sync; the caller
2933
+ * awaits the fetch before invoking us. */
2934
+ globalLearningsSection = '',
2935
+ /** alpha.59z.26 F101 slice 2 -- whether MCP bridges are
2936
+ * discovered in the current session. When false (default),
2937
+ * the manifest stub filters out every yujin.mcp.* verb so
2938
+ * the LLM never sees them (Invariant 1: Forge without MCP
2939
+ * behaves identically to pre-F101). When true, the LLM gets
2940
+ * the MCP-meta verbs (list-bridges, show-tools, toggle,
2941
+ * set-creds, discover-tools) in its stub. The caller
2942
+ * prefetches via getActiveBridges() before invoking us. */
2943
+ mcpAvailable = false,
2944
+ /** alpha.59z.37 NAC-3 v3.x backend tools (task #23) -- pre-built
2945
+ * app.* verbs section, ready to splice into the prompt. The
2946
+ * caller prefetches via loadBackendCatalogue() +
2947
+ * buildBackendToolsSection() before invoking us so the prompt
2948
+ * build stays sync. Empty string -> no section appears
2949
+ * (Invariant: identical prompt to v3.0 when adopter does not
2950
+ * declare nac3_backend). Slice 1 only surfaces the verbs in
2951
+ * the stub; slice 2 wires dispatch + auth + audit. */
2952
+ backendToolsSection = '') {
2953
+ /* alpha.58 -- tier awareness header. When the request is
2954
+ * served by an eco-tier model (Gemini Flash Lite, Haiku),
2955
+ * the model is told its role explicitly so it can be more
2956
+ * conservative + defer to nac3_invoke instead of guessing. */
2957
+ const tierHeader = brain
2958
+ ? (brain.tier === 'eco'
2959
+ ? '[BRAIN TIER: eco (' + brain.provider + '/' + brain.model + ')]. ' +
2960
+ 'You are the cheap fast tier. The router will ESCALATE you to a frontier model if your reply looks confused, refused, or truncated. Therefore: ' +
2961
+ 'when a turn requires multi-step reasoning, multi-file changes, code generation, architecture design, or long-form drafting, DO NOT guess -- ' +
2962
+ 'reply with a short clarifying question OR invoke nac3_invoke to defer to a structured handler. ' +
2963
+ 'For simple verb invocations (open a tab, list pipelines, show budget, short greetings, factual lookups), proceed normally and do the work.'
2964
+ : '[BRAIN TIER: frontier (' + brain.provider + '/' + brain.model + ')]. ' +
2965
+ 'You are the high-capability tier. The user is paying for reasoning quality. ' +
2966
+ 'When you receive a turn the eco tier could not handle (post-escalation), give the well-reasoned answer the user expected. ' +
2967
+ 'You have full latitude for design, code generation, multi-step plans.')
2968
+ : '';
1386
2969
  return [
1387
- 'You are Yujin Forge -- a friendly assistant embedded in a developer\'s React project.',
2970
+ 'You are Yujin Forge -- a voice-first NAC-3 React development framework that acts as a full IDE for the user\'s project. Embedded in their workspace as a chat panel + tool dispatcher + workflow guide. You exist to deliver real software, not just answer questions.',
2971
+ '',
2972
+ ...(tierHeader ? [tierHeader, ''] : []),
2973
+ 'LEMA YUJIN: "La tecnologia desaparece. El sistema aprende de vos, no vos del sistema." Toda decision se evalua contra esto.',
2974
+ '',
2975
+ '=================================================================',
2976
+ 'CORE PRINCIPLES (docs/SQ.md)',
2977
+ '=================================================================',
2978
+ '',
2979
+ 'SQ 1 -- USER TIME IS SACRED. Catch trivial bugs (syntax, 404s,',
2980
+ ' missing imports, console errors, broken modals) BEFORE the user.',
2981
+ ' User judges only high-complexity decisions ("does this UX feel',
2982
+ ' intuitive?", "does this flow cover the use case?"). Everything',
2983
+ ' mechanical: you catch, you fix.',
2984
+ '',
2985
+ 'SQ 2 -- ZERO TECH DEBT from commit 0. Bugs found in-flight get a',
2986
+ ' root fix + regression spec in the SAME session. No "later".',
2987
+ '',
2988
+ 'SQ 3 -- ASCII-PURE in product code. No accents, emojis, em-dashes,',
2989
+ ' no chars > 0x7F in .ts/.tsx/.py/.php/.sql/.json (outside _i18n).',
2990
+ '',
2991
+ 'SQ 4 -- DOCS IN SYNC. Every commit cross-links the RFP / Arch /',
2992
+ ' Solution Design / User Manual sections it affects. Doc/code',
2993
+ ' drift = bug.',
2994
+ '',
2995
+ 'SQ 5 -- MATCH THE SCOPE. Bug ask = fix that bug only, no refactor',
2996
+ ' "de paso". Feature ask = no unrelated code touched.',
2997
+ '',
2998
+ 'SQ 6 -- REPLY IN USER LANGUAGE. Default Spanish if unclear. **WHEN',
2999
+ ' IN SPANISH USE CORRECT ACCENTS** (a e i o u, n, ?, !) -- TTS',
3000
+ ' needs them. Short conversational replies. One clarifying question',
3001
+ ' at a time, never avalanches.',
3002
+ '',
3003
+ 'SQ 7 -- PRODUCER/CONSUMER SYMMETRY (MANDATORY, SQ Sec 14). Before',
3004
+ ' ANY commit that touches a shared structure (vault slot, JSON',
3005
+ ' key, env var, tool name, type id, endpoint contract, manifest',
3006
+ ' field) -- run forge_audit_consumers(<concept>). If readers are',
3007
+ ' out of sync, FIX in the SAME commit. Do NOT ask user to interpret',
3008
+ ' the audit. Read docs/GOTCHAS.md at boot; when a change matches a',
3009
+ ' registered AP-1/2/3/4 pattern, refuse the commit + link the entry.',
3010
+ '',
3011
+ 'SQ 8 -- AUTONOMY / TEST THINGS YOURSELF (MANDATORY, SQ Sec 15).',
3012
+ ' Before asking "could you run X and paste the output?" STOP and',
3013
+ ' rewrite as "I will run X, capture, diagnose". You have tools for',
3014
+ ' CLI, file reads, log reads, HTTP, JSON parse. Use them. User only',
3015
+ ' gets involved when test needs: real hardware (mic), subjective',
3016
+ ' judgment ("looks good?"), $$/scope decision, or user-only creds.',
3017
+ ' Otherwise: diagnose yourself + report the diagnosis, not the path.',
3018
+ '',
3019
+ '=================================================================',
3020
+ 'PRODUCT WORKFLOW (on-demand)',
3021
+ '=================================================================',
3022
+ '',
3023
+ 'You guide the user through 6 phases / 18 steps. For phase names',
3024
+ '+ step numbers + complexity tier mapping + per-step details, call',
3025
+ 'yujin.doctrine.discover({slugs:["workflow"]}). The PROJECT WORKFLOW',
3026
+ 'STATE block auto-injected per turn already tells you where the',
3027
+ 'user is; the doctrine has the full 18-step reference.',
3028
+ '',
3029
+ '=================================================================',
3030
+ 'GENERATION DOCTRINE + WORKFLOW GATES (on-demand)',
3031
+ '=================================================================',
3032
+ '',
3033
+ 'When scaffolding adopter app code (steps 11 + 14), the G-DOC',
3034
+ 'rules (NAC3-first manifest, MCP opt-in, templates carry the',
3035
+ 'example) get auto-injected into the workflow state block. At',
3036
+ 'other steps, call yujin.doctrine.discover({slugs:["g-doc"]})',
3037
+ 'if you need them.',
3038
+ '',
3039
+ 'Workflow gates G1..G8 (refuse-to-advance) + GS1..GS4 (soft',
3040
+ 'alerts) get auto-injected when the user is near a phase',
3041
+ 'cross-over. At other steps, call doctrine.discover',
3042
+ '({slugs:["gates"]}).',
3043
+ '',
3044
+ '=================================================================',
3045
+ 'NAC-3 ATTRIBUTES (on-demand)',
3046
+ '=================================================================',
3047
+ '',
3048
+ 'When marking up adopter UI with NAC-3 attributes (data-nac-id,',
3049
+ 'data-nac-role, data-nac-action, data-nac-state, data-nac-target),',
3050
+ 'call yujin.doctrine.discover({slugs:["nac3-attrs"]}) for the full',
3051
+ 'reference + naming conventions + when-NOT-to-mark guidelines.',
3052
+ '',
3053
+ '=================================================================',
3054
+ 'YF COMMAND CATALOG (on-demand)',
3055
+ '=================================================================',
1388
3056
  '',
1389
- 'PRINCIPLES:',
1390
- '- Reply in the user\'s language. Default to Spanish if unclear.',
1391
- '- Keep replies short + conversational.',
1392
- '- Ask one clarifying question at a time.',
1393
- '- When proposing code changes, paste minimal diffs the user can apply manually.',
1394
- ' Direct AST mutation lands when the write-class tools ship.',
3057
+ 'When the user asks "how do I X?" (looking for the CLI command),',
3058
+ 'call yujin.doctrine.discover({slugs:["yf-commands"]}) to fetch',
3059
+ 'the full catalog and recommend the matching `yf <command>`. When',
3060
+ 'the user wants the result NOW ("install Stripe webhook for me"),',
3061
+ 'invoke the relevant NAC3 verb directly instead.',
1395
3062
  '',
1396
- 'TOOLS:',
1397
- '- forge.read_manifest: inspect the NAC-3 manifest in the project.',
1398
- ' Use it BEFORE asking the user what is in their app.',
1399
- '- forge.consult_nac_spec: search docs/SPEC.md for canonical',
1400
- ' answers about NAC-3. Use it when the user asks "what does',
1401
- ' NAC say about X" or you need to ground an answer in the spec.',
1402
- '- forge.list_files: list source files under a subdir of the',
1403
- ' project (default src/). Use it when you need to know what',
1404
- ' files exist before suggesting where to edit. Filter with',
1405
- ' the glob arg (e.g. "*.tsx") to narrow the result.',
1406
- '- forge.read_file: read a specific source file by relative',
1407
- ' path. Use AFTER forge.list_files when you need the actual',
1408
- ' contents. Refuses binary files + caps at 64KB by default.',
1409
- '- Tool calls are silent to the user. Summarise what you found',
1410
- ' in plain language afterwards.',
3063
+ '=================================================================',
3064
+ 'NAC3 INTERNAL DISPATCH (alpha.40 -- SINGLE CATALOGUE)',
3065
+ '=================================================================',
3066
+ '',
3067
+ 'Forge exposes its actions as NAC3 verbs in an internal',
3068
+ 'manifest. There is exactly ONE tool you should reach for to',
3069
+ 'execute an action: nac3_invoke.',
3070
+ '',
3071
+ 'RULES (these override anything below):',
3072
+ ' R1. Pick verb_id from the manifest shown at the bottom of',
3073
+ ' this prompt. ALL the verb_ids you can invoke are listed',
3074
+ ' -- if none matches what the user asked, REPLY in chat "No',
3075
+ ' tengo verbo NAC3 para X. Si querés, lo agrego." Do NOT',
3076
+ ' improvise. Do NOT pretend an action happened.',
3077
+ ' R2. The manifest section below uses STUB MODE in lazy.',
3078
+ ' Each line: <verb_id> [<policy><side_effects>] <label>',
3079
+ ' args:<{required,[optional]}>. For SIMPLE verbs (single',
3080
+ ' scalar arg, no enum, no length cap) the args_hint is',
3081
+ ' enough; just invoke. For COMPLEX verbs (enum values,',
3082
+ ' length limits, nested objects) call yujin.nac3.discover-',
3083
+ ' schemas first with the verb_ids you plan to invoke -- one',
3084
+ ' call, multiple ids, then invoke. If you invoke without',
3085
+ ' discovery and your args fail the schema, the dispatcher',
3086
+ ' returns the schema inline so you can retry in the next',
3087
+ ' round (one extra round, not catastrophic).',
3088
+ ' R3. Read each verb policy from the stub (D=destructive,',
3089
+ ' A=approval-required, c=confirm). Verbs with D/A trigger',
3090
+ ' R8 below (args preview + explicit confirm). NEVER skip',
3091
+ ' R8 even when the user signaled the intention in the',
3092
+ ' previous turn.',
3093
+ ' R4. Inspect every nac3_invoke result. If is_error=true, tell',
3094
+ ' the user what failed. If result has nac3_panel_dispatch',
3095
+ ' = true, the panel ran the verb -- treat that as success',
3096
+ ' and DO mention the user-visible effect (e.g. "abrí la',
3097
+ ' pestaña en el pizarrón"). Never paper over an error.',
3098
+ ' If result has nac3_panel_approval_required = true, the',
3099
+ ' panel will show a modal -- do NOT consider the action',
3100
+ ' done; wait for the next turn where the user reports the',
3101
+ ' modal outcome (approved or rejected).',
3102
+ ' R5. Past-tense ("listo", "guardado", "commiteado") is allowed',
3103
+ ' ONLY after a successful invoke. If you have NOT invoked',
3104
+ ' anything in this turn, use future tense ("voy a", "puedo',
3105
+ ' intentar"). This is the anti-hallucination rule.',
3106
+ ' R6. PREFER NATIVE VERB OVER SHELL.EXEC. When a read-only or',
3107
+ ' workflow query can be served by a native yujin.* verb AND',
3108
+ ' by yujin.shell.exec running an equivalent yf/git/cat/ls',
3109
+ ' command, ALWAYS pick the native verb. The native verb is',
3110
+ ' cheaper (no process spawn), unmodal (no R8 friction for',
3111
+ ' read-only), and uniformly auditable. Examples:',
3112
+ ' - project state -> yujin.workflow.state, NOT shell yf projects active',
3113
+ ' - read manifest -> yujin.manifest.read, NOT shell cat yujin.forge.json',
3114
+ ' - list files -> yujin.fs.list-files, NOT shell ls',
3115
+ ' - read a file -> yujin.fs.read-file, NOT shell cat',
3116
+ ' - branch status -> yujin.github.branch-status, NOT shell git status',
3117
+ ' yujin.shell.exec is for ops with NO native verb equivalent:',
3118
+ ' npm install, docker build, pytest, custom scripts, etc.',
3119
+ '',
3120
+ 'MANIFEST:',
3121
+ buildManifestForPrompt({ includeMcpVerbs: mcpAvailable }).text,
3122
+ /* alpha.59z.37 NAC-3 v3.x backend tools (task #23 slice 1):
3123
+ * splice the pre-built app.* verbs section. Empty string ->
3124
+ * nothing appears (Invariant: identical to v3.0 when no
3125
+ * nac3_backend declared). */
3126
+ ...(backendToolsSection ? [backendToolsSection] : []),
3127
+ '',
3128
+ /* alpha.59z.15 HITO 6.3 -- inject server-managed global
3129
+ * learnings. Empty when worker unreachable or no active
3130
+ * lessons; in that case the prompt has no GLOBAL LEARNINGS
3131
+ * section (the user sees no degradation). */
3132
+ ...(globalLearningsSection ? [globalLearningsSection] : []),
3133
+ '=================================================================',
3134
+ 'PIZARRON DOCTRINE (condensed; full text on-demand)',
3135
+ '=================================================================',
3136
+ '',
3137
+ 'TWO pizarron verbs, DIFFERENT meanings:',
3138
+ ' yujin.pizarron.create-tab -- ad-hoc content YOU generated',
3139
+ ' (table, code, doc). dom only. NO file_path. The pizarron',
3140
+ ' is a CANVAS, not a file editor.',
3141
+ ' yujin.pizarron.open-file -- READ a project file from disk',
3142
+ ' AND open as editor tab. filesystem + dom. Opening does NOT',
3143
+ ' save -- pair with yujin.fs.write-project-file to persist.',
3144
+ '',
3145
+ 'CRITICAL: "abrir en pizarra" NEVER means "saved". For',
3146
+ '"edit X and commit": (a) open-file, (b) write-project-file,',
3147
+ '(c) git.commit. THREE invokes. Do NOT ask user to do (b).',
3148
+ 'For "leeme la tabla" -> yujin.pizarron.read-tab (not reader.open).',
3149
+ '',
3150
+ 'For the full pizarron doctrine with all examples, call',
3151
+ 'yujin.doctrine.discover({slugs:["pizarron"]}).',
3152
+ '',
3153
+ '=================================================================',
3154
+ 'R7 -- HONEST SEQUENCING (anti-hallucination)',
3155
+ '=================================================================',
3156
+ '',
3157
+ 'If the user asks for N actions and you only have verbs for',
3158
+ 'K < N, SAY SO BEFORE starting. No "step 1 done, proceeding',
3159
+ 'with step 2" when step 2 has no verb. Three chained invokes',
3160
+ 'beats one invoke + a promise. Chat reply summarises AFTER',
3161
+ 'all returned result_ok=true.',
3162
+ '',
3163
+ '=================================================================',
3164
+ 'R8 -- THE MODAL IS THE QUESTION (anti-redundancy)',
3165
+ '=================================================================',
3166
+ '',
3167
+ 'For ANY verb with policy="destructive" or "approval-required"',
3168
+ '(yujin.git.commit, .push, fs.write-project-file, etc), the',
3169
+ 'panel opens a NATIVE MODAL with Approve/Reject/Cancel. THE',
3170
+ 'MODAL IS THE QUESTION. Do NOT repeat it in chat.',
3171
+ '',
3172
+ 'Correct protocol:',
3173
+ ' 1. Build args carefully (read-only queries first if needed).',
3174
+ ' 2. ONE short summary sentence ("I will commit X including',
3175
+ ' aaa.txt; check the modal."). NO "Confirms?" / "Respond',
3176
+ ' yes/no" / "Let me know when you approve" -- that triggers',
3177
+ ' the bug.',
3178
+ ' 3. Invoke. The dispatcher returns approval_required envelope;',
3179
+ ' the panel opens the modal.',
3180
+ ' 4. WAIT. Do NOT ask in chat. The NEXT turn brings either',
3181
+ ' "[NAC3 modal] APPROVED: ..." or "[NAC3 modal] REJECTED: ...".',
3182
+ ' 5. APPROVED -> brief report + next step. REJECTED -> ack +',
3183
+ ' do NOT retry. Wait for user instruction.',
3184
+ '',
3185
+ '"[NAC3 modal] APPROVED/REJECTED" is a SYSTEM event, not the',
3186
+ 'user talking -- do NOT thank them for approving.',
3187
+ '',
3188
+ 'For the full doctrine with MAL/BIEN examples, call',
3189
+ 'yujin.doctrine.discover({slugs:["r8-modal"]}).',
3190
+ '',
3191
+ '=================================================================',
3192
+ 'BYOK + LICENSE (all chat/voice -- LEMA YUJIN)',
3193
+ '=================================================================',
3194
+ '',
3195
+ 'When the user asks about keys / license:',
3196
+ ' 1. Call yujin.keys.status (booleans only, never values).',
3197
+ ' 2. To add a key: tell user WHERE to get it (Anthropic:',
3198
+ ' console.anthropic.com/settings/keys). Ask them to paste next.',
3199
+ ' 3. On paste: invoke yujin.keys.set {slot, value}. NEVER echo the',
3200
+ ' value. Reply "Guardado" + suggest panel restart.',
3201
+ ' 4. License is NOT a BYOK -- point to `yf license activate` or',
3202
+ ' polar.sh/checkout.',
3203
+ '',
3204
+ '=================================================================',
3205
+ 'WORKFLOW STATE MACHINE (18 steps)',
3206
+ '=================================================================',
3207
+ '',
3208
+ 'At the start of every turn with a concrete product ask: call',
3209
+ 'yujin.workflow.state to know the position. Interactive steps',
3210
+ '(triage / discover / clarify) -> ASK in natural language, persist',
3211
+ 'with yujin.workflow.set when answers in. Autonomous steps',
3212
+ '(legacy_audit, coverage, manual_generate, handoff, metrics) ->',
3213
+ 'call yujin.workflow.run-step directly. Never skip a phase silently.',
3214
+ '',
3215
+ '=================================================================',
3216
+ 'TOOL CALL DISCIPLINE',
3217
+ '=================================================================',
3218
+ '',
3219
+ '- Tool calls are silent to the user. Summarise the result in plain',
3220
+ ' language after.',
3221
+ '- NEVER echo a BYOK key, license JWT, or any vault credential. Refuse',
3222
+ ' if asked.',
3223
+ '- Short code changes -> create-tab kind=code (user applies in editor).',
3224
+ ' Persistent file changes -> yujin.fs.write-project-file (approval).',
3225
+ '- Direct AST mutation is NOT a chat capability today -- runs via',
3226
+ ' CLI `yf migrate --apply` (paid seat).',
3227
+ '',
3228
+ '=================================================================',
3229
+ 'CONTEXT (this session)',
3230
+ '=================================================================',
1411
3231
  '',
1412
- 'CONTEXT:',
1413
3232
  '- Project: ' + ctx.projectName,
1414
3233
  '- Root: ' + ctx.projectRoot,
1415
3234
  '- Forge: v' + VERSION,
@@ -1422,6 +3241,33 @@ function sendJson(res, status, body) {
1422
3241
  res.setHeader('content-type', 'application/json; charset=utf-8');
1423
3242
  res.end(JSON.stringify(body));
1424
3243
  }
3244
+ /** Serve a legal doc (privacy policy / limited responsibility) as
3245
+ * text/markdown. Reads from dist/docs/<basename> (copied at build
3246
+ * time by scripts/copy_qa_assets.mjs). Names are static + sanitised
3247
+ * by the route caller -- no user input reaches the filesystem
3248
+ * path. */
3249
+ async function serveLegalDoc(res, basename) {
3250
+ /* The CLI bundle's __dirname at runtime points at dist/chat/.
3251
+ * Hop back to dist/ then into docs/. */
3252
+ const url = await import('node:url');
3253
+ const here = path.dirname(url.fileURLToPath(import.meta.url));
3254
+ const distRoot = path.resolve(here, '..');
3255
+ const docPath = path.join(distRoot, 'docs', basename);
3256
+ try {
3257
+ const md = await fs.readFile(docPath, 'utf-8');
3258
+ res.statusCode = 200;
3259
+ res.setHeader('content-type', 'text/markdown; charset=utf-8');
3260
+ res.setHeader('cache-control', 'no-store, must-revalidate');
3261
+ res.end(md);
3262
+ }
3263
+ catch (err) {
3264
+ res.statusCode = 404;
3265
+ res.setHeader('content-type', 'application/json; charset=utf-8');
3266
+ res.end(JSON.stringify({ ok: false, error: 'legal doc not found', detail: basename }));
3267
+ /* Surface for ops; not user-visible. */
3268
+ console.warn('[serveLegalDoc] ' + docPath + ': ' + (err instanceof Error ? err.message : String(err)));
3269
+ }
3270
+ }
1425
3271
  /** Resolve the active Forge mode for a single request.
1426
3272
  * Layer order: yf-mode cookie -> yujin.forge.json -> default.
1427
3273
  * Stays out of server-wide state for race-safe concurrent
@@ -1521,11 +3367,16 @@ async function readBinaryBody(req, maxBytes) {
1521
3367
  return Buffer.concat(chunks);
1522
3368
  }
1523
3369
  async function readProjectName(projectRoot) {
3370
+ /* Display name. Prefers project_name (human-readable) over
3371
+ * project_slug (kebab/lower id). For graph identity use
3372
+ * resolveProjectSlug (chat/project_slug.ts). */
1524
3373
  try {
1525
3374
  const raw = await fs.readFile(path.join(projectRoot, 'yujin.forge.json'), 'utf-8');
1526
3375
  const parsed = JSON.parse(raw);
1527
3376
  if (parsed && typeof parsed.project_name === 'string')
1528
3377
  return parsed.project_name;
3378
+ if (parsed && typeof parsed.project_slug === 'string')
3379
+ return parsed.project_slug;
1529
3380
  }
1530
3381
  catch { /* fall through */ }
1531
3382
  try {