@nac3/forge-cli 0.2.0-alpha.9 → 0.2.1-alpha.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/dist/a11y/helpers.d.ts +88 -0
- package/dist/a11y/helpers.d.ts.map +1 -0
- package/dist/a11y/helpers.js +109 -0
- package/dist/a11y/helpers.js.map +1 -0
- package/dist/agents/bridge.d.ts +69 -0
- package/dist/agents/bridge.d.ts.map +1 -0
- package/dist/agents/bridge.js +295 -0
- package/dist/agents/bridge.js.map +1 -0
- package/dist/agents/cli_real.d.ts +38 -0
- package/dist/agents/cli_real.d.ts.map +1 -0
- package/dist/agents/cli_real.js +46 -0
- package/dist/agents/cli_real.js.map +1 -0
- package/dist/agents/config_resolver.d.ts +16 -0
- package/dist/agents/config_resolver.d.ts.map +1 -0
- package/dist/agents/config_resolver.js +133 -0
- package/dist/agents/config_resolver.js.map +1 -0
- package/dist/agents/config_schema.d.ts +42 -0
- package/dist/agents/config_schema.d.ts.map +1 -0
- package/dist/agents/config_schema.js +220 -0
- package/dist/agents/config_schema.js.map +1 -0
- package/dist/agents/defaults.d.ts +21 -0
- package/dist/agents/defaults.d.ts.map +1 -0
- package/dist/agents/defaults.js +56 -0
- package/dist/agents/defaults.js.map +1 -0
- package/dist/agents/delegate.d.ts +36 -0
- package/dist/agents/delegate.d.ts.map +1 -0
- package/dist/agents/delegate.js +68 -0
- package/dist/agents/delegate.js.map +1 -0
- package/dist/agents/iterative_step.d.ts +95 -0
- package/dist/agents/iterative_step.d.ts.map +1 -0
- package/dist/agents/iterative_step.js +249 -0
- package/dist/agents/iterative_step.js.map +1 -0
- package/dist/agents/llm_step.d.ts +50 -0
- package/dist/agents/llm_step.d.ts.map +1 -0
- package/dist/agents/llm_step.js +28 -0
- package/dist/agents/llm_step.js.map +1 -0
- package/dist/agents/real_step_factory.d.ts +95 -0
- package/dist/agents/real_step_factory.d.ts.map +1 -0
- package/dist/agents/real_step_factory.js +51 -0
- package/dist/agents/real_step_factory.js.map +1 -0
- package/dist/agents/runner.d.ts +108 -0
- package/dist/agents/runner.d.ts.map +1 -0
- package/dist/agents/runner.js +174 -0
- package/dist/agents/runner.js.map +1 -0
- package/dist/agents/runtime.d.ts +115 -0
- package/dist/agents/runtime.d.ts.map +1 -0
- package/dist/agents/runtime.js +429 -0
- package/dist/agents/runtime.js.map +1 -0
- package/dist/agents/store.d.ts +33 -0
- package/dist/agents/store.d.ts.map +1 -0
- package/dist/agents/store.js +210 -0
- package/dist/agents/store.js.map +1 -0
- package/dist/agents/system_prompt.d.ts +36 -0
- package/dist/agents/system_prompt.d.ts.map +1 -0
- package/dist/agents/system_prompt.js +101 -0
- package/dist/agents/system_prompt.js.map +1 -0
- package/dist/agents/types.d.ts +123 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +39 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/bin/yf.d.ts.map +1 -1
- package/dist/bin/yf.js +101 -2
- package/dist/bin/yf.js.map +1 -1
- package/dist/chat/claude.d.ts +89 -1
- package/dist/chat/claude.d.ts.map +1 -1
- package/dist/chat/claude.js +170 -30
- package/dist/chat/claude.js.map +1 -1
- package/dist/chat/claude_cli.d.ts +145 -0
- package/dist/chat/claude_cli.d.ts.map +1 -0
- package/dist/chat/claude_cli.js +598 -0
- package/dist/chat/claude_cli.js.map +1 -0
- package/dist/chat/completion_hallucination_detector.d.ts +40 -0
- package/dist/chat/completion_hallucination_detector.d.ts.map +1 -0
- package/dist/chat/completion_hallucination_detector.js +96 -0
- package/dist/chat/completion_hallucination_detector.js.map +1 -0
- package/dist/chat/doctrine_digest.d.ts +31 -0
- package/dist/chat/doctrine_digest.d.ts.map +1 -0
- package/dist/chat/doctrine_digest.js +117 -0
- package/dist/chat/doctrine_digest.js.map +1 -0
- package/dist/chat/graph_auto_ingest.d.ts +46 -0
- package/dist/chat/graph_auto_ingest.d.ts.map +1 -0
- package/dist/chat/graph_auto_ingest.js +177 -0
- package/dist/chat/graph_auto_ingest.js.map +1 -0
- package/dist/chat/graph_component_watcher.d.ts +34 -0
- package/dist/chat/graph_component_watcher.d.ts.map +1 -0
- package/dist/chat/graph_component_watcher.js +225 -0
- package/dist/chat/graph_component_watcher.js.map +1 -0
- package/dist/chat/graph_snapshot.d.ts +38 -0
- package/dist/chat/graph_snapshot.d.ts.map +1 -0
- package/dist/chat/graph_snapshot.js +159 -0
- package/dist/chat/graph_snapshot.js.map +1 -0
- package/dist/chat/ingest_session.d.ts +24 -4
- package/dist/chat/ingest_session.d.ts.map +1 -1
- package/dist/chat/ingest_session.js +143 -4
- package/dist/chat/ingest_session.js.map +1 -1
- package/dist/chat/panel.d.ts.map +1 -1
- package/dist/chat/panel.js +9640 -1455
- package/dist/chat/panel.js.map +1 -1
- package/dist/chat/panel_queue.d.ts +43 -0
- package/dist/chat/panel_queue.d.ts.map +1 -0
- package/dist/chat/panel_queue.js +46 -0
- package/dist/chat/panel_queue.js.map +1 -0
- package/dist/chat/project_context_inject.d.ts +10 -0
- package/dist/chat/project_context_inject.d.ts.map +1 -0
- package/dist/chat/project_context_inject.js +119 -0
- package/dist/chat/project_context_inject.js.map +1 -0
- package/dist/chat/project_slug.d.ts +17 -0
- package/dist/chat/project_slug.d.ts.map +1 -0
- package/dist/chat/project_slug.js +103 -0
- package/dist/chat/project_slug.js.map +1 -0
- package/dist/chat/refusal_detector.d.ts +14 -0
- package/dist/chat/refusal_detector.d.ts.map +1 -0
- package/dist/chat/refusal_detector.js +138 -0
- package/dist/chat/refusal_detector.js.map +1 -0
- package/dist/chat/semantic_graph_ingest.d.ts +39 -0
- package/dist/chat/semantic_graph_ingest.d.ts.map +1 -0
- package/dist/chat/semantic_graph_ingest.js +90 -0
- package/dist/chat/semantic_graph_ingest.js.map +1 -0
- package/dist/chat/semantic_graph_inject.d.ts +31 -0
- package/dist/chat/semantic_graph_inject.d.ts.map +1 -0
- package/dist/chat/semantic_graph_inject.js +63 -0
- package/dist/chat/semantic_graph_inject.js.map +1 -0
- package/dist/chat/server.d.ts +98 -2
- package/dist/chat/server.d.ts.map +1 -1
- package/dist/chat/server.js +3678 -345
- package/dist/chat/server.js.map +1 -1
- package/dist/chat/spec_extract.d.ts +1 -2
- package/dist/chat/spec_extract.d.ts.map +1 -1
- package/dist/chat/spec_extract.js +41 -56
- package/dist/chat/spec_extract.js.map +1 -1
- package/dist/chat/spec_plan.d.ts +1 -2
- package/dist/chat/spec_plan.d.ts.map +1 -1
- package/dist/chat/spec_plan.js +36 -57
- package/dist/chat/spec_plan.js.map +1 -1
- package/dist/chat/tools/app_backend_dispatch.d.ts +39 -0
- package/dist/chat/tools/app_backend_dispatch.d.ts.map +1 -0
- package/dist/chat/tools/app_backend_dispatch.js +239 -0
- package/dist/chat/tools/app_backend_dispatch.js.map +1 -0
- package/dist/chat/tools/fs_editor.d.ts +11 -0
- package/dist/chat/tools/fs_editor.d.ts.map +1 -0
- package/dist/chat/tools/fs_editor.js +162 -0
- package/dist/chat/tools/fs_editor.js.map +1 -0
- package/dist/chat/tools/git.d.ts +24 -0
- package/dist/chat/tools/git.d.ts.map +1 -1
- package/dist/chat/tools/git.js +78 -0
- package/dist/chat/tools/git.js.map +1 -1
- package/dist/chat/tools/graph.d.ts +43 -0
- package/dist/chat/tools/graph.d.ts.map +1 -0
- package/dist/chat/tools/graph.js +174 -0
- package/dist/chat/tools/graph.js.map +1 -0
- package/dist/chat/tools/keys.d.ts +9 -0
- package/dist/chat/tools/keys.d.ts.map +1 -0
- package/dist/chat/tools/keys.js +169 -0
- package/dist/chat/tools/keys.js.map +1 -0
- package/dist/chat/tools/misc_verbs.d.ts +36 -0
- package/dist/chat/tools/misc_verbs.d.ts.map +1 -0
- package/dist/chat/tools/misc_verbs.js +405 -0
- package/dist/chat/tools/misc_verbs.js.map +1 -0
- package/dist/chat/tools/nac3_invoke.d.ts +58 -0
- package/dist/chat/tools/nac3_invoke.d.ts.map +1 -0
- package/dist/chat/tools/nac3_invoke.js +887 -0
- package/dist/chat/tools/nac3_invoke.js.map +1 -0
- package/dist/chat/tools/pending.d.ts +69 -0
- package/dist/chat/tools/pending.d.ts.map +1 -0
- package/dist/chat/tools/pending.js +167 -0
- package/dist/chat/tools/pending.js.map +1 -0
- package/dist/chat/tools/pizarron.d.ts +40 -0
- package/dist/chat/tools/pizarron.d.ts.map +1 -0
- package/dist/chat/tools/pizarron.js +123 -0
- package/dist/chat/tools/pizarron.js.map +1 -0
- package/dist/chat/tools/project_lifecycle.d.ts +24 -0
- package/dist/chat/tools/project_lifecycle.d.ts.map +1 -0
- package/dist/chat/tools/project_lifecycle.js +212 -0
- package/dist/chat/tools/project_lifecycle.js.map +1 -0
- package/dist/chat/tools/reader.d.ts.map +1 -1
- package/dist/chat/tools/reader.js +144 -1
- package/dist/chat/tools/reader.js.map +1 -1
- package/dist/chat/tools/shell.d.ts +58 -0
- package/dist/chat/tools/shell.d.ts.map +1 -0
- package/dist/chat/tools/shell.js +253 -0
- package/dist/chat/tools/shell.js.map +1 -0
- package/dist/chat/tools/wiring.d.ts +28 -0
- package/dist/chat/tools/wiring.d.ts.map +1 -0
- package/dist/chat/tools/wiring.js +116 -0
- package/dist/chat/tools/wiring.js.map +1 -0
- package/dist/chat/tools/workflow.d.ts +55 -0
- package/dist/chat/tools/workflow.d.ts.map +1 -0
- package/dist/chat/tools/workflow.js +463 -0
- package/dist/chat/tools/workflow.js.map +1 -0
- package/dist/chat/tools.d.ts +42 -0
- package/dist/chat/tools.d.ts.map +1 -1
- package/dist/chat/tools.js +447 -16
- package/dist/chat/tools.js.map +1 -1
- package/dist/chat/wiring_inject.d.ts +18 -0
- package/dist/chat/wiring_inject.d.ts.map +1 -0
- package/dist/chat/wiring_inject.js +78 -0
- package/dist/chat/wiring_inject.js.map +1 -0
- package/dist/chat/workflow_inject.d.ts +52 -0
- package/dist/chat/workflow_inject.d.ts.map +1 -0
- package/dist/chat/workflow_inject.js +346 -0
- package/dist/chat/workflow_inject.js.map +1 -0
- package/dist/chat/yujin_splash.d.ts +27 -0
- package/dist/chat/yujin_splash.d.ts.map +1 -0
- package/dist/chat/yujin_splash.js +147 -0
- package/dist/chat/yujin_splash.js.map +1 -0
- package/dist/commands/agent.d.ts +33 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +281 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/approve.d.ts +32 -0
- package/dist/commands/approve.d.ts.map +1 -0
- package/dist/commands/approve.js +198 -0
- package/dist/commands/approve.js.map +1 -0
- package/dist/commands/block.d.ts +28 -0
- package/dist/commands/block.d.ts.map +1 -0
- package/dist/commands/block.js +189 -0
- package/dist/commands/block.js.map +1 -0
- package/dist/commands/bootstrap.d.ts +35 -0
- package/dist/commands/bootstrap.d.ts.map +1 -0
- package/dist/commands/bootstrap.js +205 -0
- package/dist/commands/bootstrap.js.map +1 -0
- package/dist/commands/brain.d.ts +17 -0
- package/dist/commands/brain.d.ts.map +1 -0
- package/dist/commands/brain.js +129 -0
- package/dist/commands/brain.js.map +1 -0
- package/dist/commands/chat.d.ts +10 -0
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +209 -17
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/clarify.d.ts +30 -0
- package/dist/commands/clarify.d.ts.map +1 -0
- package/dist/commands/clarify.js +671 -0
- package/dist/commands/clarify.js.map +1 -0
- package/dist/commands/director.d.ts +37 -0
- package/dist/commands/director.d.ts.map +1 -0
- package/dist/commands/director.js +290 -0
- package/dist/commands/director.js.map +1 -0
- package/dist/commands/discover.d.ts +30 -0
- package/dist/commands/discover.d.ts.map +1 -0
- package/dist/commands/discover.js +178 -0
- package/dist/commands/discover.js.map +1 -0
- package/dist/commands/doctor.d.ts +18 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +120 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctrine.d.ts +24 -0
- package/dist/commands/doctrine.d.ts.map +1 -0
- package/dist/commands/doctrine.js +106 -0
- package/dist/commands/doctrine.js.map +1 -0
- package/dist/commands/doctrines.d.ts +21 -0
- package/dist/commands/doctrines.d.ts.map +1 -0
- package/dist/commands/doctrines.js +158 -0
- package/dist/commands/doctrines.js.map +1 -0
- package/dist/commands/graph.d.ts +29 -0
- package/dist/commands/graph.d.ts.map +1 -0
- package/dist/commands/graph.js +362 -0
- package/dist/commands/graph.js.map +1 -0
- package/dist/commands/keys_setup.d.ts +18 -0
- package/dist/commands/keys_setup.d.ts.map +1 -1
- package/dist/commands/keys_setup.js +225 -11
- package/dist/commands/keys_setup.js.map +1 -1
- package/dist/commands/legacy-audit.d.ts +34 -0
- package/dist/commands/legacy-audit.d.ts.map +1 -0
- package/dist/commands/legacy-audit.js +270 -0
- package/dist/commands/legacy-audit.js.map +1 -0
- package/dist/commands/lessons.d.ts +28 -0
- package/dist/commands/lessons.d.ts.map +1 -0
- package/dist/commands/lessons.js +309 -0
- package/dist/commands/lessons.js.map +1 -0
- package/dist/commands/license.d.ts.map +1 -1
- package/dist/commands/license.js +56 -1
- package/dist/commands/license.js.map +1 -1
- package/dist/commands/limits.d.ts +15 -0
- package/dist/commands/limits.d.ts.map +1 -0
- package/dist/commands/limits.js +51 -0
- package/dist/commands/limits.js.map +1 -0
- package/dist/commands/manifest-cli.d.ts +12 -0
- package/dist/commands/manifest-cli.d.ts.map +1 -0
- package/dist/commands/manifest-cli.js +42 -0
- package/dist/commands/manifest-cli.js.map +1 -0
- package/dist/commands/mcp.d.ts +24 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +163 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/mobile.d.ts +15 -0
- package/dist/commands/mobile.d.ts.map +1 -1
- package/dist/commands/mobile.js +27 -24
- package/dist/commands/mobile.js.map +1 -1
- package/dist/commands/nac3.d.ts +22 -0
- package/dist/commands/nac3.d.ts.map +1 -0
- package/dist/commands/nac3.js +125 -0
- package/dist/commands/nac3.js.map +1 -0
- package/dist/commands/operate.d.ts +22 -0
- package/dist/commands/operate.d.ts.map +1 -0
- package/dist/commands/operate.js +523 -0
- package/dist/commands/operate.js.map +1 -0
- package/dist/commands/pending.d.ts +22 -0
- package/dist/commands/pending.d.ts.map +1 -0
- package/dist/commands/pending.js +341 -0
- package/dist/commands/pending.js.map +1 -0
- package/dist/commands/qa.d.ts +24 -0
- package/dist/commands/qa.d.ts.map +1 -0
- package/dist/commands/qa.js +349 -0
- package/dist/commands/qa.js.map +1 -0
- package/dist/commands/spec.d.ts +38 -0
- package/dist/commands/spec.d.ts.map +1 -0
- package/dist/commands/spec.js +256 -0
- package/dist/commands/spec.js.map +1 -0
- package/dist/commands/support.d.ts +22 -0
- package/dist/commands/support.d.ts.map +1 -0
- package/dist/commands/support.js +165 -0
- package/dist/commands/support.js.map +1 -0
- package/dist/commands/test_agent.d.ts +22 -0
- package/dist/commands/test_agent.d.ts.map +1 -0
- package/dist/commands/test_agent.js +100 -0
- package/dist/commands/test_agent.js.map +1 -0
- package/dist/commands/triage.d.ts +34 -0
- package/dist/commands/triage.d.ts.map +1 -0
- package/dist/commands/triage.js +228 -0
- package/dist/commands/triage.js.map +1 -0
- package/dist/commands/vault-inventory.d.ts +30 -0
- package/dist/commands/vault-inventory.d.ts.map +1 -0
- package/dist/commands/vault-inventory.js +222 -0
- package/dist/commands/vault-inventory.js.map +1 -0
- package/dist/commands/vault.d.ts.map +1 -1
- package/dist/commands/vault.js +5 -0
- package/dist/commands/vault.js.map +1 -1
- package/dist/commands/voice.d.ts.map +1 -1
- package/dist/commands/voice.js +47 -4
- package/dist/commands/voice.js.map +1 -1
- package/dist/commands/wiring.d.ts +19 -0
- package/dist/commands/wiring.d.ts.map +1 -0
- package/dist/commands/wiring.js +135 -0
- package/dist/commands/wiring.js.map +1 -0
- package/dist/commands/workflow-coverage.d.ts +30 -0
- package/dist/commands/workflow-coverage.d.ts.map +1 -0
- package/dist/commands/workflow-coverage.js +138 -0
- package/dist/commands/workflow-coverage.js.map +1 -0
- package/dist/core/ansi_strip.d.ts +39 -0
- package/dist/core/ansi_strip.d.ts.map +1 -0
- package/dist/core/ansi_strip.js +71 -0
- package/dist/core/ansi_strip.js.map +1 -0
- package/dist/core/atomic_write.d.ts +17 -0
- package/dist/core/atomic_write.d.ts.map +1 -0
- package/dist/core/atomic_write.js +51 -0
- package/dist/core/atomic_write.js.map +1 -0
- package/dist/core/audit_log.d.ts +43 -0
- package/dist/core/audit_log.d.ts.map +1 -0
- package/dist/core/audit_log.js +80 -0
- package/dist/core/audit_log.js.map +1 -0
- package/dist/core/brain_config.d.ts +50 -0
- package/dist/core/brain_config.d.ts.map +1 -0
- package/dist/core/brain_config.js +142 -0
- package/dist/core/brain_config.js.map +1 -0
- package/dist/core/budget.d.ts +96 -0
- package/dist/core/budget.d.ts.map +1 -0
- package/dist/core/budget.js +328 -0
- package/dist/core/budget.js.map +1 -0
- package/dist/core/cost_meter.d.ts +35 -0
- package/dist/core/cost_meter.d.ts.map +1 -0
- package/dist/core/cost_meter.js +111 -0
- package/dist/core/cost_meter.js.map +1 -0
- package/dist/core/cost_router.d.ts +106 -0
- package/dist/core/cost_router.d.ts.map +1 -0
- package/dist/core/cost_router.js +589 -0
- package/dist/core/cost_router.js.map +1 -0
- package/dist/core/doctrine_resolver.d.ts +23 -0
- package/dist/core/doctrine_resolver.d.ts.map +1 -0
- package/dist/core/doctrine_resolver.js +108 -0
- package/dist/core/doctrine_resolver.js.map +1 -0
- package/dist/core/error_catalog.d.ts +80 -0
- package/dist/core/error_catalog.d.ts.map +1 -0
- package/dist/core/error_catalog.js +168 -0
- package/dist/core/error_catalog.js.map +1 -0
- package/dist/core/error_formatter.d.ts +30 -0
- package/dist/core/error_formatter.d.ts.map +1 -0
- package/dist/core/error_formatter.js +155 -0
- package/dist/core/error_formatter.js.map +1 -0
- package/dist/core/incomplete_detector.d.ts +51 -0
- package/dist/core/incomplete_detector.d.ts.map +1 -0
- package/dist/core/incomplete_detector.js +140 -0
- package/dist/core/incomplete_detector.js.map +1 -0
- package/dist/core/keys_envelope.d.ts.map +1 -1
- package/dist/core/keys_envelope.js +14 -9
- package/dist/core/keys_envelope.js.map +1 -1
- package/dist/core/learnings_client.d.ts +104 -0
- package/dist/core/learnings_client.d.ts.map +1 -0
- package/dist/core/learnings_client.js +262 -0
- package/dist/core/learnings_client.js.map +1 -0
- package/dist/core/limits_config.d.ts +76 -0
- package/dist/core/limits_config.d.ts.map +1 -0
- package/dist/core/limits_config.js +297 -0
- package/dist/core/limits_config.js.map +1 -0
- package/dist/core/pairing.d.ts +1 -0
- package/dist/core/pairing.d.ts.map +1 -1
- package/dist/core/pairing.js +23 -12
- package/dist/core/pairing.js.map +1 -1
- package/dist/core/pilot_setup.d.ts +29 -3
- package/dist/core/pilot_setup.d.ts.map +1 -1
- package/dist/core/pilot_setup.js +130 -3
- package/dist/core/pilot_setup.js.map +1 -1
- package/dist/core/provider_keys.d.ts +35 -0
- package/dist/core/provider_keys.d.ts.map +1 -0
- package/dist/core/provider_keys.js +267 -0
- package/dist/core/provider_keys.js.map +1 -0
- package/dist/core/provider_models.d.ts +27 -0
- package/dist/core/provider_models.d.ts.map +1 -0
- package/dist/core/provider_models.js +169 -0
- package/dist/core/provider_models.js.map +1 -0
- package/dist/core/reasoning_gate.d.ts +72 -0
- package/dist/core/reasoning_gate.d.ts.map +1 -0
- package/dist/core/reasoning_gate.js +205 -0
- package/dist/core/reasoning_gate.js.map +1 -0
- package/dist/core/shell_classifier.d.ts +49 -0
- package/dist/core/shell_classifier.d.ts.map +1 -0
- package/dist/core/shell_classifier.js +159 -0
- package/dist/core/shell_classifier.js.map +1 -0
- package/dist/core/sq_resolver.d.ts +14 -0
- package/dist/core/sq_resolver.d.ts.map +1 -0
- package/dist/core/sq_resolver.js +230 -0
- package/dist/core/sq_resolver.js.map +1 -0
- package/dist/core/truncation_detector.d.ts +68 -0
- package/dist/core/truncation_detector.d.ts.map +1 -0
- package/dist/core/truncation_detector.js +115 -0
- package/dist/core/truncation_detector.js.map +1 -0
- package/dist/core/turn_classifier.d.ts +70 -0
- package/dist/core/turn_classifier.d.ts.map +1 -0
- package/dist/core/turn_classifier.js +324 -0
- package/dist/core/turn_classifier.js.map +1 -0
- package/dist/core/vad_config.d.ts +34 -0
- package/dist/core/vad_config.d.ts.map +1 -0
- package/dist/core/vad_config.js +95 -0
- package/dist/core/vad_config.js.map +1 -0
- package/dist/core/whats_new.d.ts +42 -0
- package/dist/core/whats_new.d.ts.map +1 -0
- package/dist/core/whats_new.js +214 -0
- package/dist/core/whats_new.js.map +1 -0
- package/dist/core/wizard_state.d.ts +15 -0
- package/dist/core/wizard_state.d.ts.map +1 -0
- package/dist/core/wizard_state.js +86 -0
- package/dist/core/wizard_state.js.map +1 -0
- package/dist/deploy/adapter.d.ts +93 -0
- package/dist/deploy/adapter.d.ts.map +1 -0
- package/dist/deploy/adapter.js +42 -0
- package/dist/deploy/adapter.js.map +1 -0
- package/dist/deploy/aws_adapter.d.ts +28 -0
- package/dist/deploy/aws_adapter.d.ts.map +1 -0
- package/dist/deploy/aws_adapter.js +98 -0
- package/dist/deploy/aws_adapter.js.map +1 -0
- package/dist/deploy/cloudflare.d.ts +24 -0
- package/dist/deploy/cloudflare.d.ts.map +1 -0
- package/dist/deploy/cloudflare.js +169 -0
- package/dist/deploy/cloudflare.js.map +1 -0
- package/dist/design/yujin_design_resolver.d.ts +77 -0
- package/dist/design/yujin_design_resolver.d.ts.map +1 -0
- package/dist/design/yujin_design_resolver.js +113 -0
- package/dist/design/yujin_design_resolver.js.map +1 -0
- package/dist/docs/LIMITED_RESPONSIBILITY.md +216 -0
- package/dist/docs/PRIVACY_POLICY.md +169 -0
- package/dist/docs/SQ.md +1397 -0
- package/dist/docs/SQ_BY_STEP.yaml +147 -0
- package/dist/docs/doctrine/accessibility.md +78 -0
- package/dist/docs/doctrine/agent-orchestrator.md +76 -0
- package/dist/docs/doctrine/byok-vs-managed.md +81 -0
- package/dist/docs/doctrine/cost-router-escalation.md +103 -0
- package/dist/docs/doctrine/default-design-system.md +120 -0
- package/dist/docs/doctrine/explain-results.md +90 -0
- package/dist/docs/doctrine/expo-eas-builds.md +177 -0
- package/dist/docs/doctrine/g-doc.md +46 -0
- package/dist/docs/doctrine/gates.md +21 -0
- package/dist/docs/doctrine/language-sovereignty.md +143 -0
- package/dist/docs/doctrine/lessons-workflow.md +68 -0
- package/dist/docs/doctrine/mcp-bridge-lifecycle.md +75 -0
- package/dist/docs/doctrine/nac3-attrs.md +23 -0
- package/dist/docs/doctrine/office-tools.md +102 -0
- package/dist/docs/doctrine/pizarron.md +52 -0
- package/dist/docs/doctrine/r8-modal.md +60 -0
- package/dist/docs/doctrine/safety-refusals.md +73 -0
- package/dist/docs/doctrine/testing-policy.md +211 -0
- package/dist/docs/doctrine/verb-composition.md +68 -0
- package/dist/docs/doctrine/verification-honesty.md +95 -0
- package/dist/docs/doctrine/workflow-kinds.md +52 -0
- package/dist/docs/doctrine/workflow-phases.md +108 -0
- package/dist/docs/doctrine/workflow.md +58 -0
- package/dist/docs/doctrine/yf-commands.md +49 -0
- package/dist/json/query.d.ts +38 -0
- package/dist/json/query.d.ts.map +1 -0
- package/dist/json/query.js +177 -0
- package/dist/json/query.js.map +1 -0
- package/dist/license/hito4_client.d.ts +7 -0
- package/dist/license/hito4_client.d.ts.map +1 -1
- package/dist/license/hito4_client.js +52 -2
- package/dist/license/hito4_client.js.map +1 -1
- package/dist/license/sync.d.ts +59 -0
- package/dist/license/sync.d.ts.map +1 -0
- package/dist/license/sync.js +148 -0
- package/dist/license/sync.js.map +1 -0
- package/dist/llm/cohere_client.d.ts +81 -0
- package/dist/llm/cohere_client.d.ts.map +1 -0
- package/dist/llm/cohere_client.js +195 -0
- package/dist/llm/cohere_client.js.map +1 -0
- package/dist/llm/deepseek_client.d.ts +63 -0
- package/dist/llm/deepseek_client.d.ts.map +1 -0
- package/dist/llm/deepseek_client.js +188 -0
- package/dist/llm/deepseek_client.js.map +1 -0
- package/dist/llm/gemini_client.d.ts +78 -0
- package/dist/llm/gemini_client.d.ts.map +1 -0
- package/dist/llm/gemini_client.js +189 -0
- package/dist/llm/gemini_client.js.map +1 -0
- package/dist/llm/managed_client.d.ts +70 -0
- package/dist/llm/managed_client.d.ts.map +1 -0
- package/dist/llm/managed_client.js +251 -0
- package/dist/llm/managed_client.js.map +1 -0
- package/dist/llm/mistral_client.d.ts +54 -0
- package/dist/llm/mistral_client.d.ts.map +1 -0
- package/dist/llm/mistral_client.js +174 -0
- package/dist/llm/mistral_client.js.map +1 -0
- package/dist/llm/multi_provider_client.d.ts +57 -0
- package/dist/llm/multi_provider_client.d.ts.map +1 -0
- package/dist/llm/multi_provider_client.js +109 -0
- package/dist/llm/multi_provider_client.js.map +1 -0
- package/dist/llm/openai_client.d.ts +77 -0
- package/dist/llm/openai_client.d.ts.map +1 -0
- package/dist/llm/openai_client.js +200 -0
- package/dist/llm/openai_client.js.map +1 -0
- package/dist/llm/xai_client.d.ts +51 -0
- package/dist/llm/xai_client.d.ts.map +1 -0
- package/dist/llm/xai_client.js +174 -0
- package/dist/llm/xai_client.js.map +1 -0
- package/dist/mcp/blocklist.d.ts +19 -0
- package/dist/mcp/blocklist.d.ts.map +1 -0
- package/dist/mcp/blocklist.js +118 -0
- package/dist/mcp/blocklist.js.map +1 -0
- package/dist/mcp/bridge_registry.d.ts +77 -0
- package/dist/mcp/bridge_registry.d.ts.map +1 -0
- package/dist/mcp/bridge_registry.js +220 -0
- package/dist/mcp/bridge_registry.js.map +1 -0
- package/dist/mcp/config.d.ts +25 -0
- package/dist/mcp/config.d.ts.map +1 -0
- package/dist/mcp/config.js +116 -0
- package/dist/mcp/config.js.map +1 -0
- package/dist/mcp/forge_panel_server.d.ts +46 -0
- package/dist/mcp/forge_panel_server.d.ts.map +1 -0
- package/dist/mcp/forge_panel_server.js +166 -0
- package/dist/mcp/forge_panel_server.js.map +1 -0
- package/dist/mcp/lan_scan.d.ts +36 -0
- package/dist/mcp/lan_scan.d.ts.map +1 -0
- package/dist/mcp/lan_scan.js +130 -0
- package/dist/mcp/lan_scan.js.map +1 -0
- package/dist/mcp/protocol_client.d.ts +63 -0
- package/dist/mcp/protocol_client.d.ts.map +1 -0
- package/dist/mcp/protocol_client.js +340 -0
- package/dist/mcp/protocol_client.js.map +1 -0
- package/dist/mcp/state.d.ts +38 -0
- package/dist/mcp/state.d.ts.map +1 -0
- package/dist/mcp/state.js +144 -0
- package/dist/mcp/state.js.map +1 -0
- package/dist/migrate/ai-decorator.d.ts.map +1 -1
- package/dist/migrate/ai-decorator.js +55 -143
- package/dist/migrate/ai-decorator.js.map +1 -1
- package/dist/nac3/approval_queue.d.ts +44 -0
- package/dist/nac3/approval_queue.d.ts.map +1 -0
- package/dist/nac3/approval_queue.js +105 -0
- package/dist/nac3/approval_queue.js.map +1 -0
- package/dist/nac3/backend_tools.d.ts +89 -0
- package/dist/nac3/backend_tools.d.ts.map +1 -0
- package/dist/nac3/backend_tools.js +271 -0
- package/dist/nac3/backend_tools.js.map +1 -0
- package/dist/nac3/dispatch_log.d.ts +35 -0
- package/dist/nac3/dispatch_log.d.ts.map +1 -0
- package/dist/nac3/dispatch_log.js +126 -0
- package/dist/nac3/dispatch_log.js.map +1 -0
- package/dist/nac3/internal_manifest.d.ts +62 -0
- package/dist/nac3/internal_manifest.d.ts.map +1 -0
- package/dist/nac3/internal_manifest.js +2461 -0
- package/dist/nac3/internal_manifest.js.map +1 -0
- package/dist/nac3/manifest_lazy.d.ts +71 -0
- package/dist/nac3/manifest_lazy.d.ts.map +1 -0
- package/dist/nac3/manifest_lazy.js +141 -0
- package/dist/nac3/manifest_lazy.js.map +1 -0
- package/dist/nac3/verb_tier_map.d.ts +80 -0
- package/dist/nac3/verb_tier_map.d.ts.map +1 -0
- package/dist/nac3/verb_tier_map.js +159 -0
- package/dist/nac3/verb_tier_map.js.map +1 -0
- package/dist/office/excel.d.ts +132 -0
- package/dist/office/excel.d.ts.map +1 -0
- package/dist/office/excel.js +358 -0
- package/dist/office/excel.js.map +1 -0
- package/dist/office/pdf.d.ts +35 -0
- package/dist/office/pdf.d.ts.map +1 -0
- package/dist/office/pdf.js +70 -0
- package/dist/office/pdf.js.map +1 -0
- package/dist/office/word.d.ts +64 -0
- package/dist/office/word.d.ts.map +1 -0
- package/dist/office/word.js +243 -0
- package/dist/office/word.js.map +1 -0
- package/dist/pending/store.d.ts +72 -0
- package/dist/pending/store.d.ts.map +1 -0
- package/dist/pending/store.js +274 -0
- package/dist/pending/store.js.map +1 -0
- package/dist/pending/types.d.ts +111 -0
- package/dist/pending/types.d.ts.map +1 -0
- package/dist/pending/types.js +60 -0
- package/dist/pending/types.js.map +1 -0
- package/dist/qa/brain_matrix/ab_runner.d.ts +44 -0
- package/dist/qa/brain_matrix/ab_runner.d.ts.map +1 -0
- package/dist/qa/brain_matrix/ab_runner.js +144 -0
- package/dist/qa/brain_matrix/ab_runner.js.map +1 -0
- package/dist/qa/brain_matrix/brains.json +80 -0
- package/dist/qa/brain_matrix/compare.d.ts +27 -0
- package/dist/qa/brain_matrix/compare.d.ts.map +1 -0
- package/dist/qa/brain_matrix/compare.js +122 -0
- package/dist/qa/brain_matrix/compare.js.map +1 -0
- package/dist/qa/brain_matrix/fake_brain.d.ts +36 -0
- package/dist/qa/brain_matrix/fake_brain.d.ts.map +1 -0
- package/dist/qa/brain_matrix/fake_brain.js +27 -0
- package/dist/qa/brain_matrix/fake_brain.js.map +1 -0
- package/dist/qa/brain_matrix/fixtures/scenario_a11y_announce_intent.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_a11y_announce_no_speak_default.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_a11y_consecutive_announces.json +37 -0
- package/dist/qa/brain_matrix/fixtures/scenario_a11y_focus_after_action.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_a11y_focus_after_open.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_a11y_label_unlabeled_button.json +26 -0
- package/dist/qa/brain_matrix/fixtures/scenario_a11y_label_with_shortcut.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_agent_director_consults_workflow_before_dispatch.json +42 -0
- package/dist/qa/brain_matrix/fixtures/scenario_analyst_extracts_then_returns.json +28 -0
- package/dist/qa/brain_matrix/fixtures/scenario_announce_min_tool_calls.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_app_invoke_happy_path.json +65 -0
- package/dist/qa/brain_matrix/fixtures/scenario_app_no_invent_without_backend.json +18 -0
- package/dist/qa/brain_matrix/fixtures/scenario_app_schema_fail_recover.json +48 -0
- package/dist/qa/brain_matrix/fixtures/scenario_approval_modal_no_redundancy.json +21 -0
- package/dist/qa/brain_matrix/fixtures/scenario_args_redacted_in_audit.json +17 -0
- package/dist/qa/brain_matrix/fixtures/scenario_audit_consumers_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_audit_consumers_intent_en.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_audit_log_on_destructive.json +18 -0
- package/dist/qa/brain_matrix/fixtures/scenario_brain_explains_after_invoke.json +28 -0
- package/dist/qa/brain_matrix/fixtures/scenario_byok_on_managed_brain_overflow.json +28 -0
- package/dist/qa/brain_matrix/fixtures/scenario_byok_required_policy.json +35 -0
- package/dist/qa/brain_matrix/fixtures/scenario_coder_consults_nac_spec_before_writing.json +28 -0
- package/dist/qa/brain_matrix/fixtures/scenario_concise_no_loop.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_consult_nac_spec_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_contradiction_rfp_cost.json +38 -0
- package/dist/qa/brain_matrix/fixtures/scenario_cost_router_attachment_frontier.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_cost_router_escalation_explicit.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_cost_router_idle_reset.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_csv_via_excel_read.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_default_design_system_override_shadcn.json +45 -0
- package/dist/qa/brain_matrix/fixtures/scenario_default_design_system_use_card_template.json +44 -0
- package/dist/qa/brain_matrix/fixtures/scenario_destructive_no_text_confirm.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_diff_explains_changes.json +32 -0
- package/dist/qa/brain_matrix/fixtures/scenario_director_delegates_to_analyst.json +26 -0
- package/dist/qa/brain_matrix/fixtures/scenario_director_no_self_delegation.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_director_terminal_done_marker.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_after_unknown_intent.json +17 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_a11y.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_agent_orchestrator.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_byok_vs_managed.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_cost_router.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_default_design_system.json +50 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_g_doc.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_gates.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_intent_en.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_language_sovereignty.json +53 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_lessons_workflow.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_mcp_lifecycle.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_nac3_attrs.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_office.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_pizarron.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_r8_modal.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_testing_policy.json +55 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_workflow.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_workflow_phases.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_yf_commands.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_explain_results.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_safety_refusals.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_doctrine_verb_composition.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_eco_defers_complex.json +17 -0
- package/dist/qa/brain_matrix/fixtures/scenario_eco_short_query.json +27 -0
- package/dist/qa/brain_matrix/fixtures/scenario_env_list_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_env_list_intent_en.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_env_no_secret_leak.json +21 -0
- package/dist/qa/brain_matrix/fixtures/scenario_env_secret_suppression.json +35 -0
- package/dist/qa/brain_matrix/fixtures/scenario_env_secret_suppression_en.json +21 -0
- package/dist/qa/brain_matrix/fixtures/scenario_excel_append_handles_csv_refusal.json +28 -0
- package/dist/qa/brain_matrix/fixtures/scenario_excel_append_sheet_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_excel_append_sheet_intent_en.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_excel_append_then_list.json +34 -0
- package/dist/qa/brain_matrix/fixtures/scenario_excel_list_sheets_first.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_excel_sheet_named.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_explain_voice_yes.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_explains_forge_error.json +39 -0
- package/dist/qa/brain_matrix/fixtures/scenario_focus_after_modal_open.json +29 -0
- package/dist/qa/brain_matrix/fixtures/scenario_fs_find_intent.json +21 -0
- package/dist/qa/brain_matrix/fixtures/scenario_fs_find_intent_en.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_fs_find_with_glob.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_fs_mkdir_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_fs_move_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_fs_read_handles_missing.json +28 -0
- package/dist/qa/brain_matrix/fixtures/scenario_fs_stat_before_read.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_generated_app_nac3_default.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_git_branch_list_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_git_diff_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_git_log_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_git_log_intent_en.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_git_pull_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_git_push_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_git_show_metadata_only.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_git_status_branch_combo.json +44 -0
- package/dist/qa/brain_matrix/fixtures/scenario_git_status_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_git_status_intent_en.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_github_clone_repo_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_github_create_repo_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_id_format_in_reply.json +27 -0
- package/dist/qa/brain_matrix/fixtures/scenario_iterations_idempotent_adenda.json +27 -0
- package/dist/qa/brain_matrix/fixtures/scenario_iterations_query_history.json +29 -0
- package/dist/qa/brain_matrix/fixtures/scenario_iterations_tracker_acknowledge.json +28 -0
- package/dist/qa/brain_matrix/fixtures/scenario_json_parse_handles_malformed.json +34 -0
- package/dist/qa/brain_matrix/fixtures/scenario_json_parse_intent.json +30 -0
- package/dist/qa/brain_matrix/fixtures/scenario_json_parse_intent_en.json +30 -0
- package/dist/qa/brain_matrix/fixtures/scenario_json_query_extract.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_json_stringify_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_json_stringify_intent_en.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_keys_set_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_keys_status_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_keys_status_intent_en.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_language_sovereignty_not_conducive_spa_python.json +61 -0
- package/dist/qa/brain_matrix/fixtures/scenario_language_sovereignty_user_picks_python.json +48 -0
- package/dist/qa/brain_matrix/fixtures/scenario_lazy_complex_verb.json +27 -0
- package/dist/qa/brain_matrix/fixtures/scenario_lazy_doctrine_only_when_needed.json +17 -0
- package/dist/qa/brain_matrix/fixtures/scenario_lazy_simple_verb.json +21 -0
- package/dist/qa/brain_matrix/fixtures/scenario_lesson_recall_overrides.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_lifecycle_restart_app_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_lifecycle_run_app_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_lifecycle_run_app_intent_en.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_lifecycle_stop_app_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_lifecycle_stop_app_intent_en.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_long_doctrine_no_truncate.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_manifest_read_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_manual_open_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_mcp_bridge_unreachable_graceful.json +39 -0
- package/dist/qa/brain_matrix/fixtures/scenario_mcp_disabled_bridge_no_verbs.json +18 -0
- package/dist/qa/brain_matrix/fixtures/scenario_mcp_discover_tools_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_mcp_invariant_1_no_verbs.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_mcp_invoke_destructive_pauses_for_r8.json +42 -0
- package/dist/qa/brain_matrix/fixtures/scenario_mcp_invoke_happy_path_writes_pizarron.json +76 -0
- package/dist/qa/brain_matrix/fixtures/scenario_mcp_invoke_network_timeout_graceful.json +41 -0
- package/dist/qa/brain_matrix/fixtures/scenario_mcp_invoke_two_hop.json +68 -0
- package/dist/qa/brain_matrix/fixtures/scenario_mcp_invoke_with_auth_loaded.json +76 -0
- package/dist/qa/brain_matrix/fixtures/scenario_mcp_revoked_token_graceful.json +33 -0
- package/dist/qa/brain_matrix/fixtures/scenario_mcp_set_creds_intent.json +21 -0
- package/dist/qa/brain_matrix/fixtures/scenario_mcp_set_creds_no_token_arg.json +27 -0
- package/dist/qa/brain_matrix/fixtures/scenario_mcp_show_tools_before_invoke.json +44 -0
- package/dist/qa/brain_matrix/fixtures/scenario_mcp_toggle_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_multi_turn_clarify_then_act.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_nac3_attrs_doctrine.json +53 -0
- package/dist/qa/brain_matrix/fixtures/scenario_nac3_discover_schemas_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_nac3_required_attr_missing.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_no_bare_ok_after_destructive.json +27 -0
- package/dist/qa/brain_matrix/fixtures/scenario_no_global_learnings_no_recall.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_no_hardcoded_color_use_token.json +47 -0
- package/dist/qa/brain_matrix/fixtures/scenario_no_id_leak_in_reply.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_no_invent_verb.json +18 -0
- package/dist/qa/brain_matrix/fixtures/scenario_no_loop_after_repeated_fail.json +42 -0
- package/dist/qa/brain_matrix/fixtures/scenario_no_password_echo.json +28 -0
- package/dist/qa/brain_matrix/fixtures/scenario_npm_outdated_all_current.json +28 -0
- package/dist/qa/brain_matrix/fixtures/scenario_npm_outdated_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_npm_outdated_intent_en.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_npm_outdated_then_explain.json +35 -0
- package/dist/qa/brain_matrix/fixtures/scenario_office_excel_read_intent.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_office_excel_update_cell_picks_right_verb.json +26 -0
- package/dist/qa/brain_matrix/fixtures/scenario_office_excel_write_required_args.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_office_overwrite_explicit.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_office_word_write_intent.json +27 -0
- package/dist/qa/brain_matrix/fixtures/scenario_panel_open_settings_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_panel_send_message_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_pdf_read_intent.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_pizarron_clear_tab_explicit_close.json +17 -0
- package/dist/qa/brain_matrix/fixtures/scenario_pizarron_for_chart.json +30 -0
- package/dist/qa/brain_matrix/fixtures/scenario_pizarron_for_long_table.json +28 -0
- package/dist/qa/brain_matrix/fixtures/scenario_pizarron_read_tab_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_pizarron_render_not_inline.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_prefer_native_read_file.json +28 -0
- package/dist/qa/brain_matrix/fixtures/scenario_prefer_native_verb_over_shell.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_prefer_native_workflow_state.json +28 -0
- package/dist/qa/brain_matrix/fixtures/scenario_project_init_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_project_set_root_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_promote_lesson_after_correction.json +17 -0
- package/dist/qa/brain_matrix/fixtures/scenario_r8_destructive_shell_exec.json +21 -0
- package/dist/qa/brain_matrix/fixtures/scenario_read_then_parse_order.json +36 -0
- package/dist/qa/brain_matrix/fixtures/scenario_reader_bookmark_jump_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_reader_bookmark_set_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_reader_for_long_doc.json +21 -0
- package/dist/qa/brain_matrix/fixtures/scenario_reader_list_docs_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_reader_next_block_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_reader_read_section_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_reader_recap_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_reader_search_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_reader_search_intent_en.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_refuses_blanket_chmod_777.json +17 -0
- package/dist/qa/brain_matrix/fixtures/scenario_refuses_bypass_approval_modal.json +18 -0
- package/dist/qa/brain_matrix/fixtures/scenario_refuses_delete_critical_en.json +18 -0
- package/dist/qa/brain_matrix/fixtures/scenario_refuses_disable_vault_encryption.json +18 -0
- package/dist/qa/brain_matrix/fixtures/scenario_refuses_drop_database.json +17 -0
- package/dist/qa/brain_matrix/fixtures/scenario_refuses_force_push_main.json +17 -0
- package/dist/qa/brain_matrix/fixtures/scenario_refuses_force_push_main_en.json +17 -0
- package/dist/qa/brain_matrix/fixtures/scenario_refuses_leak_vault_en.json +18 -0
- package/dist/qa/brain_matrix/fixtures/scenario_refuses_modify_node_modules.json +17 -0
- package/dist/qa/brain_matrix/fixtures/scenario_refuses_skip_hooks.json +17 -0
- package/dist/qa/brain_matrix/fixtures/scenario_refuses_skip_hooks_en.json +17 -0
- package/dist/qa/brain_matrix/fixtures/scenario_refuses_to_delete_critical.json +18 -0
- package/dist/qa/brain_matrix/fixtures/scenario_refuses_to_leak_vault.json +18 -0
- package/dist/qa/brain_matrix/fixtures/scenario_respects_global_learnings.json +28 -0
- package/dist/qa/brain_matrix/fixtures/scenario_respects_workflow_gate_g2.json +39 -0
- package/dist/qa/brain_matrix/fixtures/scenario_revoke_lesson_request.json +21 -0
- package/dist/qa/brain_matrix/fixtures/scenario_scaffolder_destructive_pauses_for_approval.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_sq9b_deploy_checks_git_status.json +25 -0
- package/dist/qa/brain_matrix/fixtures/scenario_sq9b_install_checks_node_version.json +25 -0
- package/dist/qa/brain_matrix/fixtures/scenario_sq9b_publish_checks_gh_cli.json +25 -0
- package/dist/qa/brain_matrix/fixtures/scenario_sq_rules_active.json +30 -0
- package/dist/qa/brain_matrix/fixtures/scenario_sumie_icon_pick_from_catalog.json +56 -0
- package/dist/qa/brain_matrix/fixtures/scenario_tester_uses_shell_exec_only.json +27 -0
- package/dist/qa/brain_matrix/fixtures/scenario_text_diff_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_text_diff_intent_en.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_truncated_response_retry.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_unknown_command_clarify.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_url_parse_compose.json +23 -0
- package/dist/qa/brain_matrix/fixtures/scenario_url_parse_handles_invalid.json +27 -0
- package/dist/qa/brain_matrix/fixtures/scenario_url_parse_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_url_parse_intent_en.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_url_parse_then_inspect_query.json +28 -0
- package/dist/qa/brain_matrix/fixtures/scenario_url_then_stringify_order.json +34 -0
- package/dist/qa/brain_matrix/fixtures/scenario_user_just_greets.json +17 -0
- package/dist/qa/brain_matrix/fixtures/scenario_user_overrides_proceeds.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_user_revokes_in_middle.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_voice_open_pizarron.json +21 -0
- package/dist/qa/brain_matrix/fixtures/scenario_word_outline_extraction.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_word_overwrite_explicit.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_word_section_count.json +28 -0
- package/dist/qa/brain_matrix/fixtures/scenario_workflow_doctrine.json +53 -0
- package/dist/qa/brain_matrix/fixtures/scenario_workflow_done_specs.json +32 -0
- package/dist/qa/brain_matrix/fixtures/scenario_workflow_intent_capture.json +22 -0
- package/dist/qa/brain_matrix/fixtures/scenario_workflow_phase_jump_block.json +26 -0
- package/dist/qa/brain_matrix/fixtures/scenario_workflow_phase_v_step_15.json +41 -0
- package/dist/qa/brain_matrix/fixtures/scenario_workflow_run_step_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_workflow_set_intent.json +20 -0
- package/dist/qa/brain_matrix/fixtures/scenario_workflow_state_injection.json +32 -0
- package/dist/qa/brain_matrix/fixtures/scenario_workflow_state_recovers_after_clarify.json +25 -0
- package/dist/qa/brain_matrix/fixtures/scenario_workflow_step_1_intake.json +24 -0
- package/dist/qa/brain_matrix/fixtures/scenario_yf_command_doctrine.json +56 -0
- package/dist/qa/brain_matrix/fixtures/scenario_yf_doctrine_list_intent.json +19 -0
- package/dist/qa/brain_matrix/fixtures/scenario_yf_qa_compare_intent.json +17 -0
- package/dist/qa/brain_matrix/invariants.d.ts +18 -0
- package/dist/qa/brain_matrix/invariants.d.ts.map +1 -0
- package/dist/qa/brain_matrix/invariants.js +333 -0
- package/dist/qa/brain_matrix/invariants.js.map +1 -0
- package/dist/qa/brain_matrix/report.d.ts +12 -0
- package/dist/qa/brain_matrix/report.d.ts.map +1 -0
- package/dist/qa/brain_matrix/report.js +131 -0
- package/dist/qa/brain_matrix/report.js.map +1 -0
- package/dist/qa/brain_matrix/runner.d.ts +31 -0
- package/dist/qa/brain_matrix/runner.d.ts.map +1 -0
- package/dist/qa/brain_matrix/runner.js +561 -0
- package/dist/qa/brain_matrix/runner.js.map +1 -0
- package/dist/qa/brain_matrix/types.d.ts +180 -0
- package/dist/qa/brain_matrix/types.d.ts.map +1 -0
- package/dist/qa/brain_matrix/types.js +2 -0
- package/dist/qa/brain_matrix/types.js.map +1 -0
- package/dist/qa/brain_matrix/validate_scenario.d.ts +23 -0
- package/dist/qa/brain_matrix/validate_scenario.d.ts.map +1 -0
- package/dist/qa/brain_matrix/validate_scenario.js +128 -0
- package/dist/qa/brain_matrix/validate_scenario.js.map +1 -0
- package/dist/semantic_graph/ast_extract.d.ts +15 -0
- package/dist/semantic_graph/ast_extract.d.ts.map +1 -0
- package/dist/semantic_graph/ast_extract.js +162 -0
- package/dist/semantic_graph/ast_extract.js.map +1 -0
- package/dist/semantic_graph/derive.d.ts +36 -0
- package/dist/semantic_graph/derive.d.ts.map +1 -0
- package/dist/semantic_graph/derive.js +285 -0
- package/dist/semantic_graph/derive.js.map +1 -0
- package/dist/semantic_graph/llm_claim.d.ts +21 -0
- package/dist/semantic_graph/llm_claim.d.ts.map +1 -0
- package/dist/semantic_graph/llm_claim.js +164 -0
- package/dist/semantic_graph/llm_claim.js.map +1 -0
- package/dist/semantic_graph/npm_enrich.d.ts +49 -0
- package/dist/semantic_graph/npm_enrich.d.ts.map +1 -0
- package/dist/semantic_graph/npm_enrich.js +173 -0
- package/dist/semantic_graph/npm_enrich.js.map +1 -0
- package/dist/semantic_graph/render.d.ts +28 -0
- package/dist/semantic_graph/render.d.ts.map +1 -0
- package/dist/semantic_graph/render.js +70 -0
- package/dist/semantic_graph/render.js.map +1 -0
- package/dist/semantic_graph/store.d.ts +44 -0
- package/dist/semantic_graph/store.d.ts.map +1 -0
- package/dist/semantic_graph/store.js +360 -0
- package/dist/semantic_graph/store.js.map +1 -0
- package/dist/semantic_graph/types.d.ts +59 -0
- package/dist/semantic_graph/types.d.ts.map +1 -0
- package/dist/semantic_graph/types.js +17 -0
- package/dist/semantic_graph/types.js.map +1 -0
- package/dist/support/github_dispatcher.d.ts +36 -0
- package/dist/support/github_dispatcher.d.ts.map +1 -0
- package/dist/support/github_dispatcher.js +292 -0
- package/dist/support/github_dispatcher.js.map +1 -0
- package/dist/support/reports.d.ts +31 -0
- package/dist/support/reports.d.ts.map +1 -0
- package/dist/support/reports.js +162 -0
- package/dist/support/reports.js.map +1 -0
- package/dist/telemetry/usage.d.ts +67 -0
- package/dist/telemetry/usage.d.ts.map +1 -0
- package/dist/telemetry/usage.js +208 -0
- package/dist/telemetry/usage.js.map +1 -0
- package/dist/test_agent/gemini_user.d.ts +9 -0
- package/dist/test_agent/gemini_user.d.ts.map +1 -0
- package/dist/test_agent/gemini_user.js +58 -0
- package/dist/test_agent/gemini_user.js.map +1 -0
- package/dist/test_agent/profiles.d.ts +23 -0
- package/dist/test_agent/profiles.d.ts.map +1 -0
- package/dist/test_agent/profiles.js +117 -0
- package/dist/test_agent/profiles.js.map +1 -0
- package/dist/test_agent/runner.d.ts +56 -0
- package/dist/test_agent/runner.d.ts.map +1 -0
- package/dist/test_agent/runner.js +134 -0
- package/dist/test_agent/runner.js.map +1 -0
- package/dist/test_agent/target_http.d.ts +22 -0
- package/dist/test_agent/target_http.d.ts.map +1 -0
- package/dist/test_agent/target_http.js +51 -0
- package/dist/test_agent/target_http.js.map +1 -0
- package/dist/test_agent/types.d.ts +117 -0
- package/dist/test_agent/types.d.ts.map +1 -0
- package/dist/test_agent/types.js +22 -0
- package/dist/test_agent/types.js.map +1 -0
- package/dist/vault/store.d.ts.map +1 -1
- package/dist/vault/store.js +14 -1
- package/dist/vault/store.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/voice/config.d.ts +12 -0
- package/dist/voice/config.d.ts.map +1 -1
- package/dist/voice/config.js +25 -2
- package/dist/voice/config.js.map +1 -1
- package/dist/voice/providers/google.d.ts +9 -0
- package/dist/voice/providers/google.d.ts.map +1 -1
- package/dist/voice/providers/google.js +152 -26
- package/dist/voice/providers/google.js.map +1 -1
- package/dist/voice/router.d.ts +10 -0
- package/dist/voice/router.d.ts.map +1 -1
- package/dist/voice/router.js +39 -20
- package/dist/voice/router.js.map +1 -1
- package/dist/voice/types.d.ts +5 -2
- package/dist/voice/types.d.ts.map +1 -1
- package/dist/voice/types.js.map +1 -1
- package/dist/wiring/discover.d.ts +14 -0
- package/dist/wiring/discover.d.ts.map +1 -0
- package/dist/wiring/discover.js +383 -0
- package/dist/wiring/discover.js.map +1 -0
- package/dist/wiring/store.d.ts +19 -0
- package/dist/wiring/store.d.ts.map +1 -0
- package/dist/wiring/store.js +182 -0
- package/dist/wiring/store.js.map +1 -0
- package/dist/wiring/types.d.ts +61 -0
- package/dist/wiring/types.d.ts.map +1 -0
- package/dist/wiring/types.js +36 -0
- package/dist/wiring/types.js.map +1 -0
- package/dist/workflow/definitions.d.ts +60 -0
- package/dist/workflow/definitions.d.ts.map +1 -0
- package/dist/workflow/definitions.js +178 -0
- package/dist/workflow/definitions.js.map +1 -0
- package/dist/workflow/definitions_client.d.ts +12 -0
- package/dist/workflow/definitions_client.d.ts.map +1 -0
- package/dist/workflow/definitions_client.js +81 -0
- package/dist/workflow/definitions_client.js.map +1 -0
- package/dist/workflow/evolution.d.ts +10 -0
- package/dist/workflow/evolution.d.ts.map +1 -0
- package/dist/workflow/evolution.js +62 -0
- package/dist/workflow/evolution.js.map +1 -0
- package/dist/workflow/graph.d.ts +59 -0
- package/dist/workflow/graph.d.ts.map +1 -0
- package/dist/workflow/graph.js +137 -0
- package/dist/workflow/graph.js.map +1 -0
- package/dist/workflow/phase_inference.d.ts +56 -0
- package/dist/workflow/phase_inference.d.ts.map +1 -0
- package/dist/workflow/phase_inference.js +107 -0
- package/dist/workflow/phase_inference.js.map +1 -0
- package/dist/workflow/select.d.ts +28 -0
- package/dist/workflow/select.d.ts.map +1 -0
- package/dist/workflow/select.js +38 -0
- package/dist/workflow/select.js.map +1 -0
- package/dist/workflow/state.d.ts +290 -0
- package/dist/workflow/state.d.ts.map +1 -0
- package/dist/workflow/state.js +213 -0
- package/dist/workflow/state.js.map +1 -0
- package/dist/workflow/view.d.ts +47 -0
- package/dist/workflow/view.d.ts.map +1 -0
- package/dist/workflow/view.js +103 -0
- package/dist/workflow/view.js.map +1 -0
- package/docs/SQ.md +1397 -0
- package/docs/SQ_BY_STEP.yaml +147 -0
- package/docs/doctrine/accessibility.md +78 -0
- package/docs/doctrine/agent-orchestrator.md +76 -0
- package/docs/doctrine/byok-vs-managed.md +81 -0
- package/docs/doctrine/cost-router-escalation.md +103 -0
- package/docs/doctrine/default-design-system.md +120 -0
- package/docs/doctrine/explain-results.md +90 -0
- package/docs/doctrine/expo-eas-builds.md +177 -0
- package/docs/doctrine/g-doc.md +46 -0
- package/docs/doctrine/gates.md +21 -0
- package/docs/doctrine/language-sovereignty.md +143 -0
- package/docs/doctrine/lessons-workflow.md +68 -0
- package/docs/doctrine/mcp-bridge-lifecycle.md +75 -0
- package/docs/doctrine/nac3-attrs.md +23 -0
- package/docs/doctrine/office-tools.md +102 -0
- package/docs/doctrine/pizarron.md +52 -0
- package/docs/doctrine/r8-modal.md +60 -0
- package/docs/doctrine/safety-refusals.md +73 -0
- package/docs/doctrine/testing-policy.md +211 -0
- package/docs/doctrine/verb-composition.md +68 -0
- package/docs/doctrine/verification-honesty.md +95 -0
- package/docs/doctrine/workflow-kinds.md +52 -0
- package/docs/doctrine/workflow-phases.md +108 -0
- package/docs/doctrine/workflow.md +58 -0
- package/docs/doctrine/yf-commands.md +49 -0
- package/package.json +19 -3
- package/src/i18n/catalogs/ar.json +134 -10
- package/src/i18n/catalogs/de.json +134 -10
- package/src/i18n/catalogs/en.json +134 -10
- package/src/i18n/catalogs/es.json +134 -10
- package/src/i18n/catalogs/fr.json +134 -10
- package/src/i18n/catalogs/hi.json +134 -10
- package/src/i18n/catalogs/it.json +134 -10
- package/src/i18n/catalogs/ja.json +134 -10
- package/src/i18n/catalogs/pt.json +134 -10
- package/src/i18n/catalogs/zh.json +134 -10
- package/templates/nextjs-app/README.md +48 -0
- package/templates/nextjs-app/next.config.js +8 -0
- package/templates/nextjs-app/package.json +33 -0
- package/templates/nextjs-app/src/app/globals.css +43 -0
- package/templates/nextjs-app/src/app/layout.tsx +29 -0
- package/templates/nextjs-app/src/app/page.tsx +63 -0
- package/templates/nextjs-app/src/nac/manifest.ts +36 -0
- package/templates/nextjs-app/tsconfig.json +21 -0
- package/templates/nextjs-app/yujin.forge.json +11 -0
- package/templates/sumi-context.template.md +57 -0
package/dist/chat/server.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { createServer } from 'node:http';
|
|
15
15
|
import { promises as fs } from 'node:fs';
|
|
16
|
+
import os from 'node:os';
|
|
16
17
|
import path from 'node:path';
|
|
17
18
|
import { ClaudeClient, ConfigurationError, ClaudeApiError } from './claude.js';
|
|
18
19
|
import { renderPanelHtml } from './panel.js';
|
|
@@ -37,9 +38,112 @@ import { extractSpec } from './spec_extract.js';
|
|
|
37
38
|
import { generatePlan } from './spec_plan.js';
|
|
38
39
|
import { executeScaffold, rollbackScaffold } from './spec_scaffold.js';
|
|
39
40
|
import { VERSION } from '../version.js';
|
|
41
|
+
import { buildDoctrineDigestLines } from './doctrine_digest.js';
|
|
42
|
+
import { manifest as nac3Manifest } from '../nac3/internal_manifest.js';
|
|
43
|
+
import { buildManifestForPrompt } from '../nac3/manifest_lazy.js';
|
|
44
|
+
import { popPending, logApproval } from '../nac3/approval_queue.js';
|
|
40
45
|
export async function startChatServer(opts) {
|
|
41
46
|
const projectName = await readProjectName(opts.projectRoot);
|
|
42
|
-
|
|
47
|
+
/* alpha.59u.2 -- distinct slug (machine id) vs projectName (display).
|
|
48
|
+
* Identity queries against the semantic graph use the slug so
|
|
49
|
+
* CLI + panel converge. */
|
|
50
|
+
const { resolveProjectSlug } = await import('./project_slug.js');
|
|
51
|
+
const projectSlug = await resolveProjectSlug(opts.projectRoot);
|
|
52
|
+
/* Director sprint slice 9 -- hydrate ingest sessions from disk
|
|
53
|
+
* for this project so spec extract+plan survive restarts. */
|
|
54
|
+
try {
|
|
55
|
+
const { setProjectSlugForIngest, loadProjectIngests } = await import('./ingest_session.js');
|
|
56
|
+
setProjectSlugForIngest(projectSlug);
|
|
57
|
+
await loadProjectIngests(projectSlug);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
try {
|
|
61
|
+
console.error('ingest_session boot hydrate failed: ' + (err instanceof Error ? err.message : String(err)));
|
|
62
|
+
}
|
|
63
|
+
catch { /* swallow */ }
|
|
64
|
+
}
|
|
65
|
+
/* alpha.59v -- fire-and-forget auto-ingest of package.json deps
|
|
66
|
+
* whenever the boot detects a changed (or never-ingested)
|
|
67
|
+
* package.json. Never blocks the boot; never throws.
|
|
68
|
+
* alpha.59v.2 -- registered via startAutoIngest so the
|
|
69
|
+
* /api/forge/graph-status endpoint can await it (avoiding
|
|
70
|
+
* the panel showing partial data during the ingest). */
|
|
71
|
+
try {
|
|
72
|
+
const { startAutoIngest } = await import('./graph_auto_ingest.js');
|
|
73
|
+
startAutoIngest({
|
|
74
|
+
project_root: opts.projectRoot,
|
|
75
|
+
project_slug: projectSlug,
|
|
76
|
+
log: (msg) => { try {
|
|
77
|
+
console.error(msg);
|
|
78
|
+
}
|
|
79
|
+
catch { /* swallow */ } },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch { /* never break boot */ }
|
|
83
|
+
/* GRAPH-1 (dogfood 2026-06-10) -- regenerar el grafo completo
|
|
84
|
+
* (nodos de modulo + aristas de imports, validadas) al boot, en
|
|
85
|
+
* background, para que al abrir el panel el grafo este completo.
|
|
86
|
+
* El boton Refresh + el watcher lo regeneran luego ante cambios.
|
|
87
|
+
* Fire-and-forget; nunca rompe el boot. */
|
|
88
|
+
void (async () => {
|
|
89
|
+
try {
|
|
90
|
+
const { regenerateAndPersist } = await import('../semantic_graph/derive.js');
|
|
91
|
+
const s = await regenerateAndPersist({ project_root: opts.projectRoot, project_slug: projectSlug, enrich: false });
|
|
92
|
+
try {
|
|
93
|
+
console.error('[graph-regen] boot -> ' + (s.module_edges + s.lib_edges) + ' edges (' + s.pruned + ' pruned)');
|
|
94
|
+
}
|
|
95
|
+
catch { /* */ }
|
|
96
|
+
}
|
|
97
|
+
catch { /* never break boot */ }
|
|
98
|
+
})();
|
|
99
|
+
/* alpha.59w -- fs.watch over src/components, src/hooks, src/lib,
|
|
100
|
+
* src/core. When the user adds or modifies a TS/JS file, the
|
|
101
|
+
* watcher extracts top-level exports (regex), creates one
|
|
102
|
+
* graph node per export with confidence='auto', and pushes
|
|
103
|
+
* to the worker (debounced 2s). The watcher cleanup is
|
|
104
|
+
* attached to the returned StartedServer.close(). */
|
|
105
|
+
let componentWatcherHandle = null;
|
|
106
|
+
try {
|
|
107
|
+
const { startComponentWatcher } = await import('./graph_component_watcher.js');
|
|
108
|
+
componentWatcherHandle = await startComponentWatcher({
|
|
109
|
+
project_root: opts.projectRoot,
|
|
110
|
+
project_slug: projectSlug,
|
|
111
|
+
log: (msg) => { try {
|
|
112
|
+
console.error(msg);
|
|
113
|
+
}
|
|
114
|
+
catch { /* swallow */ } },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch { /* never break boot */ }
|
|
118
|
+
/* Provider selection: 'anthropic-api' (default, Console billing) vs
|
|
119
|
+
* 'claude-code-cli' (subscription billing via `claude -p`
|
|
120
|
+
* subprocess). The ClaudeCliClient is interface-compatible at the
|
|
121
|
+
* .chat() shape used by this server -- runTool round-trips happen
|
|
122
|
+
* inside the subprocess, so the wrapper returns toolRounds=[].
|
|
123
|
+
* Tests inject `opts.claude` directly and bypass this branch. */
|
|
124
|
+
/* PND-064 slice B -- generate the per-session panel-push token up
|
|
125
|
+
* front so both the chat server (to validate) and the CLI client
|
|
126
|
+
* (to hand to its MCP server) share it. Only meaningful in CLI
|
|
127
|
+
* mode; harmless otherwise. */
|
|
128
|
+
const panelPushToken = (await import('node:crypto')).randomBytes(32).toString('hex');
|
|
129
|
+
let claude;
|
|
130
|
+
if (opts.claude) {
|
|
131
|
+
claude = opts.claude;
|
|
132
|
+
}
|
|
133
|
+
else if (opts.chatProvider === 'claude-code-cli') {
|
|
134
|
+
const { ClaudeCliClient } = await import('./claude_cli.js');
|
|
135
|
+
const cliOpts = {};
|
|
136
|
+
if (opts.chatProviderAddDirs && opts.chatProviderAddDirs.length > 0) {
|
|
137
|
+
cliOpts['addDirs'] = opts.chatProviderAddDirs;
|
|
138
|
+
}
|
|
139
|
+
/* Wire the panel MCP bridge: the subprocess gets the
|
|
140
|
+
* forge_panel MCP server so it can create pizarron tabs. */
|
|
141
|
+
cliOpts['panelBridge'] = { port: opts.port, token: panelPushToken };
|
|
142
|
+
claude = new ClaudeCliClient(cliOpts);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
claude = new ClaudeClient();
|
|
146
|
+
}
|
|
43
147
|
const voice = opts.voice ?? new VoiceRouter({
|
|
44
148
|
configDir: configDir(),
|
|
45
149
|
registry: buildDefaultRegistry(),
|
|
@@ -67,6 +171,82 @@ export async function startChatServer(opts) {
|
|
|
67
171
|
});
|
|
68
172
|
return reply.text.trim();
|
|
69
173
|
});
|
|
174
|
+
/* Hot re-anchor (dogfood 2026-06-10) -- el proyecto activo se
|
|
175
|
+
* guarda en un estado MUTABLE por sesion, no en constantes del
|
|
176
|
+
* boot. yujin.project.set-root llama reanchor() y todos los
|
|
177
|
+
* handlers (que leen de `session`) pasan a apuntar al nuevo root
|
|
178
|
+
* sin reiniciar el panel. */
|
|
179
|
+
const session = { projectRoot: opts.projectRoot, projectName, projectSlug };
|
|
180
|
+
async function reanchor(newRoot) {
|
|
181
|
+
const abs = (await import('node:path')).resolve(newRoot);
|
|
182
|
+
const { enqueuePanelDispatch } = await import('./panel_queue.js');
|
|
183
|
+
/* Loguear el progreso en el espacio de chat para combatir la
|
|
184
|
+
* ansiedad del usuario (Pablo 2026-06-10). Van por la cola del
|
|
185
|
+
* panel como mensajes de sistema. */
|
|
186
|
+
const chatLog = (text) => {
|
|
187
|
+
try {
|
|
188
|
+
enqueuePanelDispatch('system.chat-log', { text }, new Date().toISOString());
|
|
189
|
+
}
|
|
190
|
+
catch { /* */ }
|
|
191
|
+
};
|
|
192
|
+
session.projectRoot = abs;
|
|
193
|
+
session.projectName = await readProjectName(abs);
|
|
194
|
+
const { resolveProjectSlug } = await import('./project_slug.js');
|
|
195
|
+
session.projectSlug = await resolveProjectSlug(abs);
|
|
196
|
+
chatLog('Cambiando al proyecto ' + session.projectName + ' (' + abs + ')...');
|
|
197
|
+
/* re-hidratar ingest + reiniciar el watcher sobre el nuevo dir */
|
|
198
|
+
try {
|
|
199
|
+
const { setProjectSlugForIngest, loadProjectIngests } = await import('./ingest_session.js');
|
|
200
|
+
setProjectSlugForIngest(session.projectSlug);
|
|
201
|
+
await loadProjectIngests(session.projectSlug);
|
|
202
|
+
}
|
|
203
|
+
catch { /* best-effort */ }
|
|
204
|
+
try {
|
|
205
|
+
if (componentWatcherHandle) {
|
|
206
|
+
await componentWatcherHandle.stop();
|
|
207
|
+
componentWatcherHandle = null;
|
|
208
|
+
}
|
|
209
|
+
const { startComponentWatcher } = await import('./graph_component_watcher.js');
|
|
210
|
+
componentWatcherHandle = await startComponentWatcher({
|
|
211
|
+
project_root: abs, project_slug: session.projectSlug,
|
|
212
|
+
log: (msg) => { try {
|
|
213
|
+
console.error(msg);
|
|
214
|
+
}
|
|
215
|
+
catch { /* swallow */ } },
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch { /* never break */ }
|
|
219
|
+
try {
|
|
220
|
+
console.error('[reanchor] active project -> ' + abs + ' (slug=' + session.projectSlug + ')');
|
|
221
|
+
}
|
|
222
|
+
catch { /* */ }
|
|
223
|
+
/* Regenerar el grafo del nuevo proyecto (crea los objetos si no
|
|
224
|
+
* existen) en BACKGROUND -- no bloquear la respuesta HTTP del
|
|
225
|
+
* verbo (sino el panel-invoke del subproceso da timeout =
|
|
226
|
+
* "failed to fetch"). El progreso se loguea por la cola; al
|
|
227
|
+
* terminar dispara el re-tabs. */
|
|
228
|
+
const slugNow = session.projectSlug;
|
|
229
|
+
const nameNow = session.projectName;
|
|
230
|
+
chatLog('Regenerando el grafo semantico de ' + nameNow + '...');
|
|
231
|
+
void (async () => {
|
|
232
|
+
try {
|
|
233
|
+
const { regenerateAndPersist } = await import('../semantic_graph/derive.js');
|
|
234
|
+
const s = await regenerateAndPersist({ project_root: abs, project_slug: slugNow, enrich: false });
|
|
235
|
+
chatLog('Grafo listo: ' + s.module_nodes + ' modulos, ' + s.lib_nodes + ' librerias, '
|
|
236
|
+
+ (s.module_edges + s.lib_edges) + ' aristas.');
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
chatLog('No pude regenerar el grafo de este proyecto (sigo igual).');
|
|
240
|
+
}
|
|
241
|
+
/* Re-generar las 4 pestanas canonicas del nuevo proyecto. */
|
|
242
|
+
try {
|
|
243
|
+
enqueuePanelDispatch('system.reanchor', { slug: slugNow, name: nameNow }, new Date().toISOString());
|
|
244
|
+
}
|
|
245
|
+
catch { /* */ }
|
|
246
|
+
chatLog('Listo. El pizarron ahora refleja ' + nameNow + '.');
|
|
247
|
+
})();
|
|
248
|
+
return { projectRoot: session.projectRoot, projectName: session.projectName, projectSlug: session.projectSlug };
|
|
249
|
+
}
|
|
70
250
|
const server = createServer(async (req, res) => {
|
|
71
251
|
// Every response carries a request-id (for client + server
|
|
72
252
|
// log correlation) + cache-control (we never want browsers
|
|
@@ -79,12 +259,17 @@ export async function startChatServer(opts) {
|
|
|
79
259
|
res.setHeader('x-yujin-version', VERSION);
|
|
80
260
|
try {
|
|
81
261
|
await route(req, res, {
|
|
82
|
-
projectRoot:
|
|
83
|
-
projectName,
|
|
262
|
+
projectRoot: session.projectRoot,
|
|
263
|
+
projectName: session.projectName,
|
|
264
|
+
projectSlug: session.projectSlug,
|
|
84
265
|
port: opts.port,
|
|
85
266
|
claude,
|
|
86
267
|
voice,
|
|
87
268
|
store,
|
|
269
|
+
runtime: opts.runtime,
|
|
270
|
+
chatProvider: opts.chatProvider,
|
|
271
|
+
panelPushToken,
|
|
272
|
+
reanchor,
|
|
88
273
|
});
|
|
89
274
|
}
|
|
90
275
|
catch (err) {
|
|
@@ -114,7 +299,24 @@ export async function startChatServer(opts) {
|
|
|
114
299
|
url,
|
|
115
300
|
store,
|
|
116
301
|
close: () => new Promise((resolve) => {
|
|
117
|
-
|
|
302
|
+
/* alpha.59w -- stop the component watcher on shutdown so
|
|
303
|
+
* vitest does not hang on lingering fs watchers. */
|
|
304
|
+
if (componentWatcherHandle) {
|
|
305
|
+
Promise.resolve(componentWatcherHandle.stop()).catch(() => undefined);
|
|
306
|
+
}
|
|
307
|
+
/* alpha.59z.136 slice 14 -- flush in-flight ingest session
|
|
308
|
+
* writes BEFORE the server closes so test teardowns that
|
|
309
|
+
* fs.rm the home directory immediately after do not race
|
|
310
|
+
* with the fire-and-forget persist queue. */
|
|
311
|
+
(async () => {
|
|
312
|
+
try {
|
|
313
|
+
const { flushPendingIngestWrites, setProjectSlugForIngest } = await import('./ingest_session.js');
|
|
314
|
+
await flushPendingIngestWrites();
|
|
315
|
+
setProjectSlugForIngest(null);
|
|
316
|
+
}
|
|
317
|
+
catch { /* swallow */ }
|
|
318
|
+
server.close(() => resolve());
|
|
319
|
+
})();
|
|
118
320
|
}),
|
|
119
321
|
};
|
|
120
322
|
}
|
|
@@ -144,6 +346,75 @@ async function route(req, res, ctx) {
|
|
|
144
346
|
});
|
|
145
347
|
return;
|
|
146
348
|
}
|
|
349
|
+
/* ------------------------------------------------------------
|
|
350
|
+
* Director sprint slice 5 -- agent runtime panel endpoints.
|
|
351
|
+
* GET /api/agent/state -- runtime status + agent definitions
|
|
352
|
+
* POST /api/agent/run -- dispatch an agent through runtime
|
|
353
|
+
* GET /api/agent/events -- SSE stream of RuntimeEvent
|
|
354
|
+
* All return 503 when ctx.runtime is unwired so the UI can
|
|
355
|
+
* surface a clean "no runtime" state.
|
|
356
|
+
* ------------------------------------------------------------ */
|
|
357
|
+
if (req.method === 'GET' && url.pathname === '/api/agent/state') {
|
|
358
|
+
await handleAgentState(res, ctx);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (req.method === 'POST' && url.pathname === '/api/agent/run') {
|
|
362
|
+
await handleAgentRun(req, res, ctx);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (req.method === 'GET' && url.pathname === '/api/agent/events') {
|
|
366
|
+
await handleAgentEventsSse(req, res, ctx);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (req.method === 'POST' && url.pathname === '/api/agent/approve') {
|
|
370
|
+
await handleAgentApprove(req, res, ctx);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (req.method === 'GET' && url.pathname === '/api/agent/activity') {
|
|
374
|
+
await handleAgentActivity(req, res, ctx);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (req.method === 'GET' && url.pathname === '/api/agent/bridge-tail') {
|
|
378
|
+
await handleAgentBridgeTail(req, res, ctx);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (req.method === 'POST' && url.pathname === '/api/agent/inject') {
|
|
382
|
+
await handleAgentInject(req, res, ctx);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
/* Legal docs surfaced inside the product (Config -> About).
|
|
386
|
+
* Serves the raw markdown so the panel can render or pop a
|
|
387
|
+
* dialog. Routes are static names so a future i18n swap can
|
|
388
|
+
* branch on Accept-Language without changing the URL. */
|
|
389
|
+
if (req.method === 'GET' && url.pathname === '/api/legal/privacy-policy') {
|
|
390
|
+
await serveLegalDoc(res, 'PRIVACY_POLICY.md');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (req.method === 'GET' && url.pathname === '/api/legal/limited-responsibility') {
|
|
394
|
+
await serveLegalDoc(res, 'LIMITED_RESPONSIBILITY.md');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (req.method === 'GET' && url.pathname === '/api/legal/index') {
|
|
398
|
+
/* Compact directory listing the panel uses to render
|
|
399
|
+
* Config -> About without hardcoding doc slugs. */
|
|
400
|
+
sendJson(res, 200, {
|
|
401
|
+
docs: [
|
|
402
|
+
{
|
|
403
|
+
slug: 'privacy-policy',
|
|
404
|
+
title: 'Privacy policy',
|
|
405
|
+
description: 'What data Forge sends to Yujin servers, what we keep, and what we do not.',
|
|
406
|
+
url: '/api/legal/privacy-policy',
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
slug: 'limited-responsibility',
|
|
410
|
+
title: 'Limitation of liability',
|
|
411
|
+
description: 'AS-IS provision, third-party services, indemnification, governing law.',
|
|
412
|
+
url: '/api/legal/limited-responsibility',
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
});
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
147
418
|
if (req.method === 'POST' && url.pathname === '/api/chat') {
|
|
148
419
|
await handleChat(req, res, ctx);
|
|
149
420
|
return;
|
|
@@ -174,6 +445,533 @@ async function route(req, res, ctx) {
|
|
|
174
445
|
await handleVaultCatalog(res, url);
|
|
175
446
|
return;
|
|
176
447
|
}
|
|
448
|
+
/* NAC3 internal manifest -- Forge dogfood. See
|
|
449
|
+
docs/NAC3_INTERNAL_MANIFEST.md. The panel reads this on boot
|
|
450
|
+
to register handlers + caches for 60s. Same JSON is embedded
|
|
451
|
+
into the system prompt so the LLM sees one canonical
|
|
452
|
+
catalogue. */
|
|
453
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/nac3-manifest') {
|
|
454
|
+
sendJson(res, 200, { ok: true, manifest: nac3Manifest() });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
/* alpha.59c.2 -- limits config endpoints. GET returns the
|
|
458
|
+
* active config + field paths + defaults. POST sets one
|
|
459
|
+
* field with {field, value} body. The settings panel renders
|
|
460
|
+
* the 10 numeric inputs from these. */
|
|
461
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/limits') {
|
|
462
|
+
const { readLimitsConfig, defaultLimitsConfig, LIMITS_FIELD_PATHS, LIMITS_BOOLEAN_FIELD_PATHS, } = await import('../core/limits_config.js');
|
|
463
|
+
const cfg = await readLimitsConfig(true);
|
|
464
|
+
const def = defaultLimitsConfig();
|
|
465
|
+
const resolveField = (f, kind) => {
|
|
466
|
+
const parts = f.split('.');
|
|
467
|
+
let cur = cfg;
|
|
468
|
+
let dft = def;
|
|
469
|
+
for (const p of parts) {
|
|
470
|
+
if (cur && typeof cur === 'object')
|
|
471
|
+
cur = cur[p];
|
|
472
|
+
if (dft && typeof dft === 'object')
|
|
473
|
+
dft = dft[p];
|
|
474
|
+
}
|
|
475
|
+
return { field: f, value: cur, default: dft, kind };
|
|
476
|
+
};
|
|
477
|
+
const fields = LIMITS_FIELD_PATHS.map((f) => resolveField(f, 'number'));
|
|
478
|
+
const booleans = LIMITS_BOOLEAN_FIELD_PATHS.map((f) => resolveField(f, 'boolean'));
|
|
479
|
+
sendJson(res, 200, { ok: true, fields, booleans, config: cfg });
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/limits') {
|
|
483
|
+
const raw = await readBody(req);
|
|
484
|
+
let body;
|
|
485
|
+
try {
|
|
486
|
+
body = JSON.parse(raw);
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON' });
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const { setLimitsField, setLimitsBooleanField, isLimitsBooleanFieldPath, resetLimitsConfig } = await import('../core/limits_config.js');
|
|
493
|
+
if (body.reset === true) {
|
|
494
|
+
const cfg = await resetLimitsConfig();
|
|
495
|
+
sendJson(res, 200, { ok: true, config: cfg });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (typeof body.field !== 'string') {
|
|
499
|
+
sendJson(res, 400, { ok: false, error: 'expected {field: string, value: number|boolean} or {reset: true}' });
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
/* alpha.59d -- accept booleans too (Danger Zone toggle +
|
|
503
|
+
* cwd_boundary_strict). Dispatch by field-path type. */
|
|
504
|
+
if (isLimitsBooleanFieldPath(body.field)) {
|
|
505
|
+
if (typeof body.value !== 'boolean') {
|
|
506
|
+
sendJson(res, 400, { ok: false, error: 'boolean field requires boolean value' });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
const cfg = await setLimitsBooleanField(body.field, body.value);
|
|
511
|
+
sendJson(res, 200, { ok: true, config: cfg });
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
sendJson(res, 400, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (typeof body.value !== 'number' || !Number.isFinite(body.value)) {
|
|
519
|
+
sendJson(res, 400, { ok: false, error: 'expected {field: string, value: number} or {reset: true}' });
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
const cfg = await setLimitsField(body.field, body.value);
|
|
524
|
+
sendJson(res, 200, { ok: true, config: cfg });
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
sendJson(res, 400, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
/* alpha.59z.176 PND-003 -- VAD tuning + debug config. The panel
|
|
532
|
+
* reads this at boot + writes it back when the user moves a
|
|
533
|
+
* slider. The VAD loop hot-applies on the next tick without a
|
|
534
|
+
* reload. Session stats (when toggled on) are POSTed back here so
|
|
535
|
+
* ~/.yujin-forge/vad_sessions.jsonl accumulates a row per capture
|
|
536
|
+
* for the field test protocol. */
|
|
537
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/vad-config') {
|
|
538
|
+
const { readVadConfig, DEFAULT_VAD_CONFIG } = await import('../core/vad_config.js');
|
|
539
|
+
const cfg = await readVadConfig();
|
|
540
|
+
sendJson(res, 200, { ok: true, config: cfg, defaults: DEFAULT_VAD_CONFIG });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/vad-config') {
|
|
544
|
+
const raw = await readBody(req);
|
|
545
|
+
let body;
|
|
546
|
+
try {
|
|
547
|
+
body = JSON.parse(raw);
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON' });
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const { readVadConfig, writeVadConfig, sanitiseVadConfig, DEFAULT_VAD_CONFIG } = await import('../core/vad_config.js');
|
|
554
|
+
if (body.reset === true) {
|
|
555
|
+
await writeVadConfig({ ...DEFAULT_VAD_CONFIG });
|
|
556
|
+
sendJson(res, 200, { ok: true, config: { ...DEFAULT_VAD_CONFIG } });
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const current = await readVadConfig();
|
|
560
|
+
const merged = sanitiseVadConfig({ ...current, ...(body.patch ?? {}) });
|
|
561
|
+
await writeVadConfig(merged);
|
|
562
|
+
sendJson(res, 200, { ok: true, config: merged });
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
/* PND-001 alpha.59z.177 -- wizard probe endpoints. Lightweight
|
|
566
|
+
* checks so the panel can verify the user finished the chosen
|
|
567
|
+
* setup path BEFORE marking the wizard complete. The probes do
|
|
568
|
+
* not consume tokens (anthropic /v1/models is free; the Claude
|
|
569
|
+
* CLI probe just spawns `claude -p ok?`). */
|
|
570
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/wizard-probe-claude-cli') {
|
|
571
|
+
try {
|
|
572
|
+
const { probeClaudeCli } = await import('./claude_cli.js');
|
|
573
|
+
const r = await probeClaudeCli({});
|
|
574
|
+
sendJson(res, 200, { ...r, ok: r.ok });
|
|
575
|
+
}
|
|
576
|
+
catch (err) {
|
|
577
|
+
sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
578
|
+
}
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/wizard-test-anthropic-key') {
|
|
582
|
+
const raw = await readBody(req);
|
|
583
|
+
let body;
|
|
584
|
+
try {
|
|
585
|
+
body = JSON.parse(raw);
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON' });
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const apiKey = typeof body.api_key === 'string' ? body.api_key.trim() : '';
|
|
592
|
+
if (!apiKey || !/^sk-ant-/.test(apiKey)) {
|
|
593
|
+
sendJson(res, 400, { ok: false, error: 'expected an Anthropic key starting with sk-ant-' });
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
const r = await fetch('https://api.anthropic.com/v1/models', {
|
|
598
|
+
method: 'GET',
|
|
599
|
+
headers: {
|
|
600
|
+
'x-api-key': apiKey,
|
|
601
|
+
'anthropic-version': '2023-06-01',
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
if (!r.ok) {
|
|
605
|
+
const detail = await r.text().catch(() => '');
|
|
606
|
+
sendJson(res, 200, { ok: false, status: r.status, detail: detail.slice(0, 300) });
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (body.save === true) {
|
|
610
|
+
const { setKey } = await import('../core/provider_keys.js');
|
|
611
|
+
await setKey('anthropic_api_key', apiKey);
|
|
612
|
+
/* The brain_provider_preference field is read elsewhere from
|
|
613
|
+
* the same provider_keys.json file. Setting the key alone is
|
|
614
|
+
* enough -- cost_router falls back to anthropic when no
|
|
615
|
+
* preference is recorded but anthropic_api_key is present. */
|
|
616
|
+
}
|
|
617
|
+
sendJson(res, 200, { ok: true, status: r.status, saved: body.save === true });
|
|
618
|
+
}
|
|
619
|
+
catch (err) {
|
|
620
|
+
sendJson(res, 200, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
621
|
+
}
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
/* alpha.59z.177 PND-001 -- first-run wizard state. The chat panel
|
|
625
|
+
* reads this at boot to decide whether to auto-open the
|
|
626
|
+
* license + brain wizard modal. POST persists "user finished the
|
|
627
|
+
* wizard" or "user dismissed -> do not auto-open again". */
|
|
628
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/wizard-state') {
|
|
629
|
+
const { readWizardState } = await import('../core/wizard_state.js');
|
|
630
|
+
const state = await readWizardState();
|
|
631
|
+
sendJson(res, 200, { ok: true, state });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/wizard-state') {
|
|
635
|
+
const raw = await readBody(req);
|
|
636
|
+
let body;
|
|
637
|
+
try {
|
|
638
|
+
body = JSON.parse(raw);
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON' });
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const { markWizardCompleted, resetWizardState, readWizardState } = await import('../core/wizard_state.js');
|
|
645
|
+
if (body.reset === true) {
|
|
646
|
+
await resetWizardState();
|
|
647
|
+
sendJson(res, 200, { ok: true, state: await readWizardState() });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const path_chosen = body.path_chosen;
|
|
651
|
+
if (path_chosen !== 'claude_plan' && path_chosen !== 'console_byok' && path_chosen !== 'skipped') {
|
|
652
|
+
sendJson(res, 400, { ok: false, error: 'path_chosen must be claude_plan|console_byok|skipped' });
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const state = await markWizardCompleted(path_chosen);
|
|
656
|
+
sendJson(res, 200, { ok: true, state });
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/vad-stats') {
|
|
660
|
+
const raw = await readBody(req);
|
|
661
|
+
let body;
|
|
662
|
+
try {
|
|
663
|
+
body = JSON.parse(raw);
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON' });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const { appendVadSessionStats } = await import('../core/vad_config.js');
|
|
670
|
+
try {
|
|
671
|
+
await appendVadSessionStats(body);
|
|
672
|
+
sendJson(res, 200, { ok: true });
|
|
673
|
+
}
|
|
674
|
+
catch (err) {
|
|
675
|
+
sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
676
|
+
}
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
/* alpha.59j -- reader bookmarks viewer endpoint. Reads the
|
|
680
|
+
* persisted reader_sessions.json + returns a flat list of
|
|
681
|
+
* bookmarks (name, doc_path, cursor_block_id, opened_at)
|
|
682
|
+
* newest-first so the panel can render the sidebar. */
|
|
683
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/reader-bookmarks') {
|
|
684
|
+
try {
|
|
685
|
+
const home = process.env.HOME || process.env.USERPROFILE || (await import('node:os')).homedir();
|
|
686
|
+
const pathMod = await import('node:path');
|
|
687
|
+
const fsMod = await import('node:fs/promises');
|
|
688
|
+
const base = process.env.YF_CONFIG_DIR || pathMod.join(home, '.yujin-forge');
|
|
689
|
+
const cacheFile = pathMod.join(base, 'cache', 'reader_sessions.json');
|
|
690
|
+
const raw = await fsMod.readFile(cacheFile, 'utf-8');
|
|
691
|
+
const idx = JSON.parse(raw);
|
|
692
|
+
const out = [];
|
|
693
|
+
for (const key of Object.keys(idx.entries || {})) {
|
|
694
|
+
const entry = idx.entries[key];
|
|
695
|
+
if (!entry)
|
|
696
|
+
continue;
|
|
697
|
+
const bms = entry.bookmarks || {};
|
|
698
|
+
for (const name of Object.keys(bms)) {
|
|
699
|
+
out.push({
|
|
700
|
+
name,
|
|
701
|
+
doc_id: entry.doc_id,
|
|
702
|
+
abs_path: entry.abs_path,
|
|
703
|
+
filename: entry.filename,
|
|
704
|
+
title: entry.title || entry.filename,
|
|
705
|
+
block_id: bms[name],
|
|
706
|
+
last_used_at: entry.last_used_at || '',
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
out.sort((a, b) => (b.last_used_at || '').localeCompare(a.last_used_at || ''));
|
|
711
|
+
sendJson(res, 200, { ok: true, bookmarks: out, count: out.length });
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
sendJson(res, 200, { ok: true, bookmarks: [], count: 0 });
|
|
715
|
+
}
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
/* alpha.59i -- shell kill endpoint. POST {handle_id} from the
|
|
719
|
+
* pizarron tab Cancel button. Looks up the active ChildProcess
|
|
720
|
+
* + sends SIGTERM (with SIGKILL fallback 2s). 404 if the
|
|
721
|
+
* handle is unknown (already finished). */
|
|
722
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/shell-kill') {
|
|
723
|
+
const raw = await readBody(req);
|
|
724
|
+
let body;
|
|
725
|
+
try {
|
|
726
|
+
body = JSON.parse(raw);
|
|
727
|
+
}
|
|
728
|
+
catch {
|
|
729
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON' });
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (typeof body.handle_id !== 'string' || body.handle_id.trim() === '') {
|
|
733
|
+
sendJson(res, 400, { ok: false, error: 'handle_id required' });
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const { killShellByHandle } = await import('./tools/shell.js');
|
|
737
|
+
const r = killShellByHandle(body.handle_id);
|
|
738
|
+
sendJson(res, r.ok ? 200 : 404, r);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
/* alpha.59z.182 -- semantic graph status now reads from the
|
|
742
|
+
* per-project LOCAL SQLite store (no worker). Same response
|
|
743
|
+
* shape as before so the panel JS doesn't change. */
|
|
744
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/graph-status') {
|
|
745
|
+
try {
|
|
746
|
+
const project = url.searchParams.get('project_slug') ?? ctx.projectSlug ?? '';
|
|
747
|
+
/* alpha.59v.2 -- if the boot auto-ingest for this slug is
|
|
748
|
+
* still running, wait for it (5s max) so the panel does
|
|
749
|
+
* not show stale counts on first paint. */
|
|
750
|
+
try {
|
|
751
|
+
const { inFlightIngest } = await import('./graph_auto_ingest.js');
|
|
752
|
+
const pending = inFlightIngest(project);
|
|
753
|
+
if (pending) {
|
|
754
|
+
await Promise.race([
|
|
755
|
+
pending,
|
|
756
|
+
new Promise((resolve) => setTimeout(resolve, 5000)),
|
|
757
|
+
]);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
catch { /* swallow -- proceed with query anyway */ }
|
|
761
|
+
if (!project) {
|
|
762
|
+
sendJson(res, 200, { ok: true, count: 0, by_kind: {}, by_confidence: {}, sample: [], available: false });
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const { listRelevant, summarize } = await import('../semantic_graph/store.js');
|
|
766
|
+
const summary = await summarize(project);
|
|
767
|
+
const sampleNodes = await listRelevant(project, { limit: 10 });
|
|
768
|
+
const by_confidence = {};
|
|
769
|
+
const allForCount = await listRelevant(project, { limit: 10_000, min_confidence: 'claimed' });
|
|
770
|
+
for (const n of allForCount) {
|
|
771
|
+
by_confidence[n.confidence] = (by_confidence[n.confidence] || 0) + 1;
|
|
772
|
+
}
|
|
773
|
+
const sample = sampleNodes.map((n) => ({
|
|
774
|
+
id: n.id, kind: n.kind, name: n.name, version: n.version ?? null, purpose: n.purpose, confidence: n.confidence,
|
|
775
|
+
}));
|
|
776
|
+
sendJson(res, 200, {
|
|
777
|
+
ok: true,
|
|
778
|
+
count: summary.node_count,
|
|
779
|
+
by_kind: summary.by_kind,
|
|
780
|
+
by_confidence,
|
|
781
|
+
sample,
|
|
782
|
+
available: summary.node_count > 0,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
sendJson(res, 200, { ok: true, count: 0, by_kind: {}, by_confidence: {}, sample: [], available: false });
|
|
787
|
+
}
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
/* alpha.59z.150 F101 slice 3 -- MCP bridges UI endpoints.
|
|
791
|
+
* GET /api/forge/mcp/bridges -> list of discovered bridges
|
|
792
|
+
* with id/endpoint/source/enabled/
|
|
793
|
+
* display_name/protocol.
|
|
794
|
+
* GET /api/forge/mcp/tools -> tool catalog for the bridge_id
|
|
795
|
+
* query param. Internally calls
|
|
796
|
+
* MCPClient.listTools() (slice 4
|
|
797
|
+
* FULL).
|
|
798
|
+
* POST /api/forge/mcp/toggle -> flip enabled flag on a bridge.
|
|
799
|
+
* Panel uses these to render the Config tab + tool catalog modal.
|
|
800
|
+
* Each one is best-effort; failures degrade to {ok:false, error}. */
|
|
801
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/mcp/bridges') {
|
|
802
|
+
try {
|
|
803
|
+
const { getActiveBridges } = await import('../mcp/bridge_registry.js');
|
|
804
|
+
const bridges = await getActiveBridges({ projectRoot: ctx.projectRoot });
|
|
805
|
+
const shaped = bridges.map((b) => ({
|
|
806
|
+
id: b.id,
|
|
807
|
+
endpoint: b.endpoint,
|
|
808
|
+
source: b.source,
|
|
809
|
+
enabled: b.enabled,
|
|
810
|
+
display_name: b.display_name ?? b.id,
|
|
811
|
+
protocol: b.protocol ?? 'http_rest',
|
|
812
|
+
}));
|
|
813
|
+
sendJson(res, 200, { ok: true, bridges: shaped, count: shaped.length });
|
|
814
|
+
}
|
|
815
|
+
catch (err) {
|
|
816
|
+
sendJson(res, 200, { ok: false, error: err instanceof Error ? err.message : String(err), bridges: [], count: 0 });
|
|
817
|
+
}
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/mcp/tools') {
|
|
821
|
+
const bridgeId = url.searchParams.get('bridge_id') ?? '';
|
|
822
|
+
if (!bridgeId) {
|
|
823
|
+
sendJson(res, 400, { ok: false, error: 'bridge_id required' });
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
try {
|
|
827
|
+
const { getActiveBridges } = await import('../mcp/bridge_registry.js');
|
|
828
|
+
const bridges = await getActiveBridges({ projectRoot: ctx.projectRoot });
|
|
829
|
+
const bridge = bridges.find((b) => b.id === bridgeId);
|
|
830
|
+
if (!bridge) {
|
|
831
|
+
sendJson(res, 404, { ok: false, error: 'bridge_id "' + bridgeId + '" not found', tools: [] });
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
if (!bridge.enabled) {
|
|
835
|
+
sendJson(res, 200, { ok: false, error: 'bridge disabled', bridge_id: bridgeId, tools: [], disabled: true });
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
const { getMCPClient } = await import('../mcp/protocol_client.js');
|
|
840
|
+
const client = getMCPClient(bridge);
|
|
841
|
+
const r = await client.listTools();
|
|
842
|
+
if (r.ok) {
|
|
843
|
+
sendJson(res, 200, { ok: true, bridge_id: bridgeId, tools: r.tools, count: r.count });
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
sendJson(res, 200, {
|
|
847
|
+
ok: false,
|
|
848
|
+
error: r.error,
|
|
849
|
+
bridge_id: bridgeId,
|
|
850
|
+
bridge_unreachable: true,
|
|
851
|
+
tools: [],
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
catch (toolErr) {
|
|
856
|
+
sendJson(res, 200, {
|
|
857
|
+
ok: false,
|
|
858
|
+
error: toolErr instanceof Error ? toolErr.message : String(toolErr),
|
|
859
|
+
bridge_id: bridgeId,
|
|
860
|
+
bridge_unreachable: true,
|
|
861
|
+
tools: [],
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
867
|
+
}
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/mcp/toggle') {
|
|
871
|
+
let body;
|
|
872
|
+
try {
|
|
873
|
+
body = JSON.parse(await readBody(req));
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const bridgeId = typeof body.bridge_id === 'string' ? body.bridge_id.trim() : '';
|
|
880
|
+
if (!bridgeId) {
|
|
881
|
+
sendJson(res, 400, { ok: false, error: 'bridge_id required' });
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const enabled = body.enabled === true || body.enabled === 'true' || body.enabled === 1;
|
|
885
|
+
try {
|
|
886
|
+
const { setBridgeEnabled } = await import('../mcp/state.js');
|
|
887
|
+
await setBridgeEnabled(bridgeId, enabled);
|
|
888
|
+
const { resetBridgeCache } = await import('../mcp/bridge_registry.js');
|
|
889
|
+
resetBridgeCache();
|
|
890
|
+
sendJson(res, 200, { ok: true, bridge_id: bridgeId, enabled });
|
|
891
|
+
}
|
|
892
|
+
catch (err) {
|
|
893
|
+
sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
894
|
+
}
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
/* alpha.59l -- "what changed in this version" notification.
|
|
898
|
+
* Compares ~/.yujin-forge/last_seen_version vs current VERSION
|
|
899
|
+
* + returns the cumulative bullet catalog entries. Panel shows
|
|
900
|
+
* a one-shot dismissable banner; dismiss POSTs to /dismiss. */
|
|
901
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/whats-new') {
|
|
902
|
+
const { computeWhatsNew } = await import('../core/whats_new.js');
|
|
903
|
+
const r = await computeWhatsNew(VERSION);
|
|
904
|
+
sendJson(res, 200, { ok: true, ...r });
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/whats-new/dismiss') {
|
|
908
|
+
const { writeLastSeen } = await import('../core/whats_new.js');
|
|
909
|
+
try {
|
|
910
|
+
await writeLastSeen(VERSION);
|
|
911
|
+
sendJson(res, 200, { ok: true, last_seen_version: VERSION });
|
|
912
|
+
}
|
|
913
|
+
catch (err) {
|
|
914
|
+
sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
915
|
+
}
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
/* alpha.59g -- audit log viewer endpoint. Reads the last N
|
|
919
|
+
* rows from ~/.yujin-forge/cache/audit_log.jsonl + returns
|
|
920
|
+
* newest-first. The panel renders them in the Danger Zone
|
|
921
|
+
* area so the user can confirm exactly what Forge did
|
|
922
|
+
* (especially when Danger Zone auto-approve is on). */
|
|
923
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/audit-log') {
|
|
924
|
+
const { readRecentAudit } = await import('../core/audit_log.js');
|
|
925
|
+
const limitParam = url.searchParams.get('limit');
|
|
926
|
+
const limit = limitParam ? Math.min(500, Math.max(1, Number(limitParam) || 100)) : 100;
|
|
927
|
+
const entries = await readRecentAudit(limit);
|
|
928
|
+
sendJson(res, 200, { ok: true, entries, count: entries.length });
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
/* alpha.52 -- budget snapshot for the panel UI card. */
|
|
932
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/budget') {
|
|
933
|
+
const { readBudgetStatus } = await import('../core/budget.js');
|
|
934
|
+
const status = await readBudgetStatus();
|
|
935
|
+
/* JSON does not transport Infinity; map to null so the
|
|
936
|
+
* client can treat it as "unlimited". */
|
|
937
|
+
const finite = (n) => Number.isFinite(n) ? n : null;
|
|
938
|
+
sendJson(res, 200, {
|
|
939
|
+
ok: true,
|
|
940
|
+
budget: {
|
|
941
|
+
plan: status.plan,
|
|
942
|
+
plan_label: status.plan_label,
|
|
943
|
+
month: status.month,
|
|
944
|
+
renewal_at: status.renewal_at,
|
|
945
|
+
extension_url: status.extension_url,
|
|
946
|
+
eco: {
|
|
947
|
+
cap: finite(status.eco.cap),
|
|
948
|
+
used: status.eco.used,
|
|
949
|
+
remaining: finite(status.eco.remaining),
|
|
950
|
+
used_pct: status.eco.used_pct,
|
|
951
|
+
},
|
|
952
|
+
frontier: {
|
|
953
|
+
cap: finite(status.frontier.cap),
|
|
954
|
+
used: status.frontier.used,
|
|
955
|
+
remaining: finite(status.frontier.remaining),
|
|
956
|
+
used_pct: status.frontier.used_pct,
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
});
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
/* alpha.42 -- approval queue endpoints for destructive /
|
|
963
|
+
approval-required verbs. The dispatcher pauses such verbs +
|
|
964
|
+
returns nac3_panel_approval_required: true. The panel opens
|
|
965
|
+
a modal; the user clicks Approve / Reject; the panel POSTs
|
|
966
|
+
here with the trace_id. */
|
|
967
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/nac3-approve') {
|
|
968
|
+
await handleNac3Approve(req, res, ctx);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/nac3-reject') {
|
|
972
|
+
await handleNac3Reject(req, res);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
177
975
|
/* SQ 0.8 -- dual mode read/write. */
|
|
178
976
|
if (req.method === 'GET' && url.pathname === '/api/forge/mode') {
|
|
179
977
|
await handleModeGet(req, res, ctx);
|
|
@@ -270,20 +1068,136 @@ async function route(req, res, ctx) {
|
|
|
270
1068
|
await handleTrustState(req, res, ctx);
|
|
271
1069
|
return;
|
|
272
1070
|
}
|
|
273
|
-
/*
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (req.method === 'POST' && url.pathname === '/api/forge/ingest') {
|
|
279
|
-
await handleForgeIngest(req, res, ctx);
|
|
1071
|
+
/* alpha.29 -- keys status for the settings dropdown. Returns
|
|
1072
|
+
which BYOK provider keys + license are configured, without
|
|
1073
|
+
exposing the values themselves. */
|
|
1074
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/keys-status') {
|
|
1075
|
+
await handleKeysStatus(req, res);
|
|
280
1076
|
return;
|
|
281
1077
|
}
|
|
282
|
-
/*
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
1078
|
+
/* alpha.30 -- write/clear BYOK provider keys from the panel. */
|
|
1079
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/keys-set') {
|
|
1080
|
+
await handleKeysSet(req, res);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
/* PND-069 -- config de Brain desde el panel: modo plan/console,
|
|
1084
|
+
toggle eco, bindings provider+modelo por tier. */
|
|
1085
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/brain-config') {
|
|
1086
|
+
await handleBrainConfigGet(req, res, ctx);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/brain-config') {
|
|
1090
|
+
await handleBrainConfigSet(req, res);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
/* PND-069 -- modelos disponibles por provider (API en vivo con
|
|
1094
|
+
fallback estatico). */
|
|
1095
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/brain-models') {
|
|
1096
|
+
await handleBrainModels(req, res, url);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
/* PND-064 -- paridad de tools en modo Plan: el MCP server local
|
|
1100
|
+
reenvia aca CADA nac3_invoke del subproceso (modo
|
|
1101
|
+
claude-code-cli). Corre el dispatcher real y devuelve el
|
|
1102
|
+
resultado; los dispatches de panel se encolan. Token-gated. */
|
|
1103
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/panel-invoke') {
|
|
1104
|
+
await handlePanelInvoke(req, res, ctx);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
/* PND-064 slice B -- el panel hace polling de la cola. */
|
|
1108
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/panel-queue') {
|
|
1109
|
+
await handlePanelQueue(req, res, url);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
/* GRAPH-1 -- regenerar el grafo semantico (nodos + aristas
|
|
1113
|
+
derivadas de imports). El boton Refresh del panel lo llama. */
|
|
1114
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/graph-regenerate') {
|
|
1115
|
+
await handleGraphRegenerate(req, res, ctx);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
/* Startup tabs (dogfood 2026-06-10) -- 4 pestanas que el panel
|
|
1119
|
+
instancia al abrir: grafo grafico, grafo semantico, pendings,
|
|
1120
|
+
workflow. Se auto-refrescan; el switch las apaga. */
|
|
1121
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/startup-tabs') {
|
|
1122
|
+
await handleStartupTabs(req, res, ctx, url);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
/* Mobile pairing QR (dogfood 2026-06-10) -- Ajustes > Vincular
|
|
1126
|
+
telefono. Genera codigo + envelope + QR SVG para que Forge
|
|
1127
|
+
Mobile lo escanee (el usuario solo tipea el codigo). */
|
|
1128
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/mobile-pair') {
|
|
1129
|
+
await handleMobilePair(req, res);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
/* alpha.59z.114 -- pull the canonical license row from the
|
|
1133
|
+
HITO 4 worker (Polar -> D1 -> local cache). The panel button
|
|
1134
|
+
hits this when the user paid on Polar but the local cache
|
|
1135
|
+
still says plan=none. */
|
|
1136
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/license-sync') {
|
|
1137
|
+
await handleLicenseSync(req, res);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
/* alpha.59z.117 -- bind a Polar user_handle (email) to the
|
|
1141
|
+
local license cache + immediately attempt a sync. This is
|
|
1142
|
+
the one-click flow the panel surfaces when the user opens
|
|
1143
|
+
yf chat for the first time after installing from npm:
|
|
1144
|
+
panel detects user_handle missing, prompts for email,
|
|
1145
|
+
POSTs here, and the row gets populated end-to-end. */
|
|
1146
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/license-handle') {
|
|
1147
|
+
await handleLicenseHandle(req, res);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
/* alpha.35 -- pizarron edit: read + write project files. */
|
|
1151
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/file') {
|
|
1152
|
+
await handleReadFile(req, res, ctx);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/file') {
|
|
1156
|
+
await handleWriteFile(req, res, ctx);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
/* Favicon -- minimal SVG so Chrome stops 404-ing. The browser
|
|
1160
|
+
auto-requests /favicon.ico on every page load; without this,
|
|
1161
|
+
each open of the panel logs an error. */
|
|
1162
|
+
if (req.method === 'GET' && (url.pathname === '/favicon.ico'
|
|
1163
|
+
|| url.pathname === '/favicon.svg')) {
|
|
1164
|
+
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">'
|
|
1165
|
+
+ '<circle cx="16" cy="16" r="14" fill="#1a1a1a"/>'
|
|
1166
|
+
+ '<path d="M8 22 L16 8 L24 22 M11 17 L21 17" stroke="#fafafa" stroke-width="2" fill="none" stroke-linecap="round"/>'
|
|
1167
|
+
+ '</svg>';
|
|
1168
|
+
res.writeHead(200, {
|
|
1169
|
+
'content-type': 'image/svg+xml',
|
|
1170
|
+
'cache-control': 'public, max-age=86400',
|
|
1171
|
+
});
|
|
1172
|
+
res.end(svg);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
/* Support report ingest (2026-05-31 -- F30-style). Panel sends
|
|
1176
|
+
* runtime errors (window.onerror, fetch failures, unhandled
|
|
1177
|
+
* promise rejections) here so Forge sees what the user saw
|
|
1178
|
+
* without them having to copy logs. PII is scrubbed on save. */
|
|
1179
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/support/report') {
|
|
1180
|
+
await handleSupportReport(req, res);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (req.method === 'GET' && url.pathname === '/api/forge/support/reports') {
|
|
1184
|
+
await handleSupportReportList(req, res);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
/* Spec doc ingest (V1.37 + V1.38 -- bloque 4.5 scaffolding).
|
|
1188
|
+
Client uploads a spec file (PDF / DOCX / HTML / md / ...)
|
|
1189
|
+
and the server parses it via the existing reader pipeline,
|
|
1190
|
+
returning the NormalisedDocument shape. Real plan-building
|
|
1191
|
+
+ file scaffolding lands in V1.41..V1.46. */
|
|
1192
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/ingest') {
|
|
1193
|
+
await handleForgeIngest(req, res, ctx);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
/* Spec extraction via Claude (V1.40). After an ingest, the
|
|
1197
|
+
panel calls this to ask Claude for structured
|
|
1198
|
+
requirements/entities/endpoints/components. */
|
|
1199
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/ingest/extract') {
|
|
1200
|
+
await handleForgeIngestExtract(req, res, ctx);
|
|
287
1201
|
return;
|
|
288
1202
|
}
|
|
289
1203
|
/* List active ingest sessions (V1.40 -- for debugging + the
|
|
@@ -445,6 +1359,19 @@ async function handleVoiceStt(req, res, ctx) {
|
|
|
445
1359
|
sendJson(res, 400, { ok: false, error: 'audio body is empty' });
|
|
446
1360
|
return;
|
|
447
1361
|
}
|
|
1362
|
+
/* 2026-05-31 fix: Google STT sync rejects audio > 1 min with
|
|
1363
|
+
HTTP 400. Refuse early with a useful message instead of
|
|
1364
|
+
leaking the Google error verbatim. ~4 MB is the practical
|
|
1365
|
+
ceiling for 60s of WebM/Opus at 32 kbps. */
|
|
1366
|
+
if (audio.length > 4 * 1024 * 1024) {
|
|
1367
|
+
sendJson(res, 413, {
|
|
1368
|
+
ok: false,
|
|
1369
|
+
error: 'audio too long for sync STT (limit ~1 min). '
|
|
1370
|
+
+ 'Got ' + audio.length + ' bytes. The chat panel auto-stops at 55s; '
|
|
1371
|
+
+ 'if you hit this from a different client, split the audio.',
|
|
1372
|
+
});
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
448
1375
|
/* Optional active document hint (V1.31). Lets the voice intent
|
|
449
1376
|
matcher resolve commands like "siguiente" or "buscar X" against
|
|
450
1377
|
the currently-open reader session. Client sets this header to
|
|
@@ -538,6 +1465,163 @@ async function handleForgeToolDispatch(req, res, ctx) {
|
|
|
538
1465
|
});
|
|
539
1466
|
}
|
|
540
1467
|
}
|
|
1468
|
+
/* ============================================================
|
|
1469
|
+
* NAC3 approval queue endpoints (alpha.42).
|
|
1470
|
+
*
|
|
1471
|
+
* When the LLM invokes a verb with policy=destructive or
|
|
1472
|
+
* approval-required, runNac3Invoke pushes a pending row + returns
|
|
1473
|
+
* an envelope. The panel opens a modal; the user clicks Approve
|
|
1474
|
+
* or Reject; the panel POSTs here with the trace_id.
|
|
1475
|
+
*
|
|
1476
|
+
* Approve -> resolve the row + execute the legacy alias via the
|
|
1477
|
+
* runForgeTool dispatcher (with approval_bypass=true to skip the
|
|
1478
|
+
* gate on re-entry). Reject -> log + return ok without executing.
|
|
1479
|
+
* ============================================================ */
|
|
1480
|
+
async function handleNac3Approve(req, res, ctx) {
|
|
1481
|
+
let body;
|
|
1482
|
+
try {
|
|
1483
|
+
body = JSON.parse(await readBody(req));
|
|
1484
|
+
}
|
|
1485
|
+
catch {
|
|
1486
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
const traceId = typeof body.trace_id === 'string' ? body.trace_id.trim() : '';
|
|
1490
|
+
if (!traceId) {
|
|
1491
|
+
sendJson(res, 400, { ok: false, error: 'trace_id required' });
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const pending = popPending(traceId);
|
|
1495
|
+
if (!pending) {
|
|
1496
|
+
await logApproval({
|
|
1497
|
+
ts: new Date().toISOString(),
|
|
1498
|
+
trace_id: traceId,
|
|
1499
|
+
verb_id: '?',
|
|
1500
|
+
decision: 'expired',
|
|
1501
|
+
args_sample: {},
|
|
1502
|
+
policy: '?',
|
|
1503
|
+
reason: 'trace_id not in queue (expired or already resolved)',
|
|
1504
|
+
});
|
|
1505
|
+
sendJson(res, 404, { ok: false, error: 'no pending approval for trace_id (expired or already resolved)' });
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
/* Execute via the legacy alias, bypassing the gate. */
|
|
1509
|
+
if (!pending.legacy_alias) {
|
|
1510
|
+
sendJson(res, 500, { ok: false, error: 'pending row has no legacy alias' });
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
let result;
|
|
1514
|
+
try {
|
|
1515
|
+
result = await pending.legacy_run(pending.legacy_alias, pending.args, ctx.projectRoot === pending.ctx.projectRoot ? pending.ctx : { projectRoot: ctx.projectRoot });
|
|
1516
|
+
}
|
|
1517
|
+
catch (err) {
|
|
1518
|
+
await logApproval({
|
|
1519
|
+
ts: new Date().toISOString(),
|
|
1520
|
+
trace_id: traceId,
|
|
1521
|
+
verb_id: pending.verb.id,
|
|
1522
|
+
decision: 'approve',
|
|
1523
|
+
args_sample: pending.args,
|
|
1524
|
+
policy: pending.verb.policy,
|
|
1525
|
+
reason: 'handler threw: ' + (err instanceof Error ? err.message : String(err)),
|
|
1526
|
+
});
|
|
1527
|
+
sendJson(res, 500, {
|
|
1528
|
+
ok: false,
|
|
1529
|
+
error: 'handler threw on approved verb: ' + (err instanceof Error ? err.message : String(err)),
|
|
1530
|
+
});
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
await logApproval({
|
|
1534
|
+
ts: new Date().toISOString(),
|
|
1535
|
+
trace_id: traceId,
|
|
1536
|
+
verb_id: pending.verb.id,
|
|
1537
|
+
decision: 'approve',
|
|
1538
|
+
args_sample: pending.args,
|
|
1539
|
+
policy: pending.verb.policy,
|
|
1540
|
+
});
|
|
1541
|
+
sendJson(res, 200, {
|
|
1542
|
+
ok: true,
|
|
1543
|
+
trace_id: traceId,
|
|
1544
|
+
verb_id: pending.verb.id,
|
|
1545
|
+
is_error: result.is_error ?? false,
|
|
1546
|
+
result: result.result,
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
async function handleNac3Reject(req, res) {
|
|
1550
|
+
let body;
|
|
1551
|
+
try {
|
|
1552
|
+
body = JSON.parse(await readBody(req));
|
|
1553
|
+
}
|
|
1554
|
+
catch {
|
|
1555
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
const traceId = typeof body.trace_id === 'string' ? body.trace_id.trim() : '';
|
|
1559
|
+
const reason = typeof body.reason === 'string' ? body.reason.slice(0, 240) : undefined;
|
|
1560
|
+
if (!traceId) {
|
|
1561
|
+
sendJson(res, 400, { ok: false, error: 'trace_id required' });
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
const pending = popPending(traceId);
|
|
1565
|
+
if (!pending) {
|
|
1566
|
+
await logApproval({
|
|
1567
|
+
ts: new Date().toISOString(),
|
|
1568
|
+
trace_id: traceId,
|
|
1569
|
+
verb_id: '?',
|
|
1570
|
+
decision: 'expired',
|
|
1571
|
+
args_sample: {},
|
|
1572
|
+
policy: '?',
|
|
1573
|
+
reason: 'trace_id not in queue',
|
|
1574
|
+
});
|
|
1575
|
+
/* alpha.59z.14 F104 -- emit structured ForgeError so the panel
|
|
1576
|
+
* + chat brain can surface a readable diagnosis instead of the
|
|
1577
|
+
* cryptic "no pending approval for trace_id" string. */
|
|
1578
|
+
const { renderForgeError, E_MODAL_TRACE_STALE } = await import('../core/error_catalog.js');
|
|
1579
|
+
const { formatForgeErrorJson } = await import('../core/error_formatter.js');
|
|
1580
|
+
const forgeError = renderForgeError(E_MODAL_TRACE_STALE, { trace_id: traceId });
|
|
1581
|
+
sendJson(res, 404, {
|
|
1582
|
+
ok: false,
|
|
1583
|
+
/* legacy field for backward compatibility */
|
|
1584
|
+
error: 'no pending approval for trace_id',
|
|
1585
|
+
/* F104 structured */
|
|
1586
|
+
forge_error: formatForgeErrorJson(forgeError),
|
|
1587
|
+
});
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
await logApproval({
|
|
1591
|
+
ts: new Date().toISOString(),
|
|
1592
|
+
trace_id: traceId,
|
|
1593
|
+
verb_id: pending.verb.id,
|
|
1594
|
+
decision: 'reject',
|
|
1595
|
+
args_sample: pending.args,
|
|
1596
|
+
policy: pending.verb.policy,
|
|
1597
|
+
reason,
|
|
1598
|
+
});
|
|
1599
|
+
/* alpha.59z.160 F104 -- when the rejected verb is yujin.shell.exec,
|
|
1600
|
+
* surface E_SHELL_EXEC_REJECTED (severity: info) so the chat brain
|
|
1601
|
+
* can suggest the native-verb alternative for read-only commands
|
|
1602
|
+
* (yf projects active, git status, ls) instead of asking the user
|
|
1603
|
+
* to re-approve. The rejection itself is a valid user signal --
|
|
1604
|
+
* response stays ok:true. */
|
|
1605
|
+
const payload = {
|
|
1606
|
+
ok: true,
|
|
1607
|
+
trace_id: traceId,
|
|
1608
|
+
verb_id: pending.verb.id,
|
|
1609
|
+
decision: 'rejected',
|
|
1610
|
+
};
|
|
1611
|
+
if (pending.verb.id === 'yujin.shell.exec') {
|
|
1612
|
+
const { renderForgeError, E_SHELL_EXEC_REJECTED } = await import('../core/error_catalog.js');
|
|
1613
|
+
const { formatForgeErrorJson } = await import('../core/error_formatter.js');
|
|
1614
|
+
const command = typeof pending.args['command'] === 'string'
|
|
1615
|
+
? pending.args['command'].slice(0, 240)
|
|
1616
|
+
: '?';
|
|
1617
|
+
const classification = typeof pending.args['classification'] === 'string'
|
|
1618
|
+
? pending.args['classification']
|
|
1619
|
+
: pending.verb.policy;
|
|
1620
|
+
const fe = renderForgeError(E_SHELL_EXEC_REJECTED, { command, classification });
|
|
1621
|
+
payload['forge_error'] = formatForgeErrorJson(fe);
|
|
1622
|
+
}
|
|
1623
|
+
sendJson(res, 200, payload);
|
|
1624
|
+
}
|
|
541
1625
|
/** V1.37 + V1.38 -- ingest a spec doc via drag-drop / upload.
|
|
542
1626
|
*
|
|
543
1627
|
* Accepts POST with:
|
|
@@ -665,7 +1749,7 @@ async function handleForgeIngestExtract(req, res, ctx) {
|
|
|
665
1749
|
};
|
|
666
1750
|
if (session.doc.language)
|
|
667
1751
|
opts.language = session.doc.language;
|
|
668
|
-
const extraction = await extractSpec(
|
|
1752
|
+
const extraction = await extractSpec(opts);
|
|
669
1753
|
setExtraction(session.doc_id, extraction);
|
|
670
1754
|
sendJson(res, 200, {
|
|
671
1755
|
ok: true,
|
|
@@ -727,7 +1811,7 @@ async function handleForgeIngestPlan(req, res, ctx) {
|
|
|
727
1811
|
};
|
|
728
1812
|
if (session.doc.language)
|
|
729
1813
|
opts.language = session.doc.language;
|
|
730
|
-
const plan = await generatePlan(
|
|
1814
|
+
const plan = await generatePlan(opts);
|
|
731
1815
|
setPlan(session.doc_id, plan);
|
|
732
1816
|
sendJson(res, 200, {
|
|
733
1817
|
ok: true,
|
|
@@ -1122,143 +2206,782 @@ async function handleTrustEvent(req, res, _ctx) {
|
|
|
1122
2206
|
await saveTrustMetrics(m);
|
|
1123
2207
|
sendJson(res, 200, { ok: true, metrics: m });
|
|
1124
2208
|
}
|
|
1125
|
-
async function
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
2209
|
+
async function handleKeysStatus(_req, res) {
|
|
2210
|
+
/* Read ~/.yujin-forge/provider_keys.json + license.json without
|
|
2211
|
+
* exposing values. Just boolean presence.
|
|
2212
|
+
*
|
|
2213
|
+
* alpha.34 fix (Producer/Consumer Symmetry, SQ Sec 14):
|
|
2214
|
+
* Keys in the response MUST match the JSON slot names used by:
|
|
2215
|
+
* - the writer (POST /api/forge/keys-set, src/chat/tools/keys.ts)
|
|
2216
|
+
* - the panel HTML rows (data-key-slot="anthropic_api_key")
|
|
2217
|
+
* Previously this endpoint returned short names ('anthropic')
|
|
2218
|
+
* while the panel + writer used full slot names
|
|
2219
|
+
* ('anthropic_api_key'), so the per-row UI always showed every
|
|
2220
|
+
* row as MISSING even when the keys were present. Symptom Pablo
|
|
2221
|
+
* caught: 'figuran desactivadas pero brain + STT/TTS funcionan'.
|
|
2222
|
+
*
|
|
2223
|
+
* Also returns license_plan as a STRING (not a boolean) so the
|
|
2224
|
+
* settings panel can display the actual plan label. */
|
|
2225
|
+
/* alpha.44 C1: keys live in the encrypted Vault now. The
|
|
2226
|
+
* helper auto-migrates the legacy provider_keys.json on first
|
|
2227
|
+
* read; we just ask for the boolean status of each slot. */
|
|
2228
|
+
const { listStatus } = await import('../core/provider_keys.js');
|
|
2229
|
+
const out = await listStatus();
|
|
2230
|
+
const fs = await import('node:fs');
|
|
2231
|
+
const path = await import('node:path');
|
|
2232
|
+
const os = await import('node:os');
|
|
2233
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
2234
|
+
let license_plan = 'none';
|
|
2235
|
+
let license_user_handle = null;
|
|
2236
|
+
let license_last_validated_at = null;
|
|
2237
|
+
try {
|
|
2238
|
+
const p = path.join(home, '.yujin-forge', 'license.json');
|
|
2239
|
+
const raw = await fs.promises.readFile(p, 'utf-8');
|
|
2240
|
+
const j = JSON.parse(raw);
|
|
2241
|
+
if (typeof j.plan === 'string')
|
|
2242
|
+
license_plan = j.plan;
|
|
2243
|
+
if (typeof j.user_handle === 'string')
|
|
2244
|
+
license_user_handle = j.user_handle;
|
|
2245
|
+
if (typeof j.last_validated_at === 'string')
|
|
2246
|
+
license_last_validated_at = j.last_validated_at;
|
|
2247
|
+
}
|
|
2248
|
+
catch { /* no license file */ }
|
|
1139
2249
|
sendJson(res, 200, {
|
|
1140
2250
|
ok: true,
|
|
1141
|
-
|
|
1142
|
-
|
|
2251
|
+
keys: out,
|
|
2252
|
+
license_plan,
|
|
2253
|
+
license_user_handle,
|
|
2254
|
+
license_last_validated_at,
|
|
2255
|
+
license_can_sync: license_user_handle !== null,
|
|
1143
2256
|
});
|
|
1144
2257
|
}
|
|
1145
|
-
|
|
2258
|
+
/** PND-069 -- GET /api/forge/brain-config. Devuelve el
|
|
2259
|
+
* brain_config completo + el chatProvider activo de ESTA sesion
|
|
2260
|
+
* (que puede diferir de anthropic_mode hasta reabrir el panel). */
|
|
2261
|
+
async function handleBrainConfigGet(_req, res, ctx) {
|
|
2262
|
+
const { readBrainConfig } = await import('../core/brain_config.js');
|
|
2263
|
+
const cfg = await readBrainConfig();
|
|
2264
|
+
sendJson(res, 200, {
|
|
2265
|
+
ok: true,
|
|
2266
|
+
config: cfg,
|
|
2267
|
+
active_chat_provider: ctx.chatProvider ?? 'anthropic-api',
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
/** PND-069 -- POST /api/forge/brain-config. Acepta un subset de
|
|
2271
|
+
* campos; valida y persiste. anthropic_mode requiere reabrir el
|
|
2272
|
+
* panel para tomar efecto (el brain client se construye al boot);
|
|
2273
|
+
* eco_enabled y los bindings aplican en el proximo turno. */
|
|
2274
|
+
async function handleBrainConfigSet(req, res) {
|
|
2275
|
+
const { readBrainConfig, writeBrainConfig, ALL_BRAIN_PROVIDERS, } = await import('../core/brain_config.js');
|
|
2276
|
+
let body;
|
|
1146
2277
|
try {
|
|
1147
|
-
|
|
1148
|
-
sendJson(res, 200, { ok: true, slots: vault.list() });
|
|
2278
|
+
body = JSON.parse(await readBody(req));
|
|
1149
2279
|
}
|
|
1150
|
-
catch
|
|
1151
|
-
sendJson(res,
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
2280
|
+
catch {
|
|
2281
|
+
sendJson(res, 400, { ok: false, error: 'invalid json body' });
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
const cfg = await readBrainConfig();
|
|
2285
|
+
if (typeof body.eco_enabled === 'boolean')
|
|
2286
|
+
cfg.eco_enabled = body.eco_enabled;
|
|
2287
|
+
if (typeof body.startup_tabs_enabled === 'boolean')
|
|
2288
|
+
cfg.startup_tabs_enabled = body.startup_tabs_enabled;
|
|
2289
|
+
if (body.anthropic_mode === 'plan' || body.anthropic_mode === 'console') {
|
|
2290
|
+
cfg.anthropic_mode = body.anthropic_mode;
|
|
2291
|
+
}
|
|
2292
|
+
if (body.chat_default_tier === 'eco' || body.chat_default_tier === 'frontier') {
|
|
2293
|
+
cfg.chat_default_tier = body.chat_default_tier;
|
|
1155
2294
|
}
|
|
2295
|
+
for (const tier of ['eco', 'frontier']) {
|
|
2296
|
+
const b = body[tier];
|
|
2297
|
+
if (!b || typeof b !== 'object')
|
|
2298
|
+
continue;
|
|
2299
|
+
const provider = typeof b.provider === 'string'
|
|
2300
|
+
&& ALL_BRAIN_PROVIDERS.includes(b.provider)
|
|
2301
|
+
? b.provider : null;
|
|
2302
|
+
const model = typeof b.model === 'string' && b.model.trim() !== ''
|
|
2303
|
+
? b.model.trim() : null;
|
|
2304
|
+
if (provider)
|
|
2305
|
+
cfg[tier].provider = provider;
|
|
2306
|
+
if (model)
|
|
2307
|
+
cfg[tier].model = model;
|
|
2308
|
+
}
|
|
2309
|
+
await writeBrainConfig(cfg);
|
|
2310
|
+
sendJson(res, 200, { ok: true, config: cfg, restart_needed_for: ['anthropic_mode'] });
|
|
1156
2311
|
}
|
|
1157
|
-
|
|
2312
|
+
/** PND-064 -- POST /api/forge/panel-invoke. Paridad total de tools
|
|
2313
|
+
* en modo Plan: el MCP server local reenvia aca cada nac3_invoke
|
|
2314
|
+
* del subproceso `claude -p`. Corre el MISMO dispatcher que el
|
|
2315
|
+
* modo API (runForgeTool -> runNac3Invoke), con el mismo policy
|
|
2316
|
+
* gate. Los resultados de panel-dispatch (pizarron) se encolan
|
|
2317
|
+
* para que el panel los materialice; los demas se devuelven al
|
|
2318
|
+
* subproceso. Token-gated (timing-safe). */
|
|
2319
|
+
async function handlePanelInvoke(req, res, ctx) {
|
|
1158
2320
|
let body;
|
|
1159
2321
|
try {
|
|
1160
2322
|
body = JSON.parse(await readBody(req));
|
|
1161
2323
|
}
|
|
1162
2324
|
catch {
|
|
1163
|
-
sendJson(res, 400, { ok: false, error: 'invalid
|
|
2325
|
+
sendJson(res, 400, { ok: false, error: 'invalid json body' });
|
|
1164
2326
|
return;
|
|
1165
2327
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
2328
|
+
/* Token-gate (CR-D1 lesson: no confiar en "solo loopback"). */
|
|
2329
|
+
const expected = ctx.panelPushToken || '';
|
|
2330
|
+
const got = typeof body.token === 'string' ? body.token : '';
|
|
2331
|
+
const crypto = await import('node:crypto');
|
|
2332
|
+
const ok = expected.length > 0 && got.length === expected.length
|
|
2333
|
+
&& crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
|
|
2334
|
+
if (!ok) {
|
|
2335
|
+
sendJson(res, 403, { ok: false, error: 'forbidden' });
|
|
1168
2336
|
return;
|
|
1169
2337
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
}
|
|
1177
|
-
kindOpt = body.kind;
|
|
2338
|
+
const verb_id = typeof body.verb_id === 'string' ? body.verb_id : '';
|
|
2339
|
+
const args = (body.args && typeof body.args === 'object' && !Array.isArray(body.args))
|
|
2340
|
+
? body.args : {};
|
|
2341
|
+
if (!verb_id) {
|
|
2342
|
+
sendJson(res, 400, { ok: false, error: 'missing verb_id', text: 'Falta verb_id.' });
|
|
2343
|
+
return;
|
|
1178
2344
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
2345
|
+
/* Mismo dispatcher que el modo API. Ctx completo (incluye
|
|
2346
|
+
* projectSlug para graph/pending/wiring). */
|
|
2347
|
+
const { runForgeTool } = await import('./tools.js');
|
|
2348
|
+
const toolCtx = { projectRoot: ctx.projectRoot, projectSlug: ctx.projectSlug, ...(ctx.reanchor ? { reanchor: ctx.reanchor } : {}) };
|
|
2349
|
+
let r;
|
|
2350
|
+
try {
|
|
2351
|
+
r = await runForgeTool('nac3_invoke', { verb_id, args }, toolCtx);
|
|
1182
2352
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
}
|
|
1188
|
-
const d = new Date(body.expiry);
|
|
1189
|
-
if (isNaN(d.getTime())) {
|
|
1190
|
-
sendJson(res, 400, { ok: false, error: 'expiry is not a valid ISO 8601 timestamp' });
|
|
1191
|
-
return;
|
|
1192
|
-
}
|
|
1193
|
-
expiryOpt = d.toISOString();
|
|
2353
|
+
catch (err) {
|
|
2354
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
2355
|
+
sendJson(res, 200, { ok: false, is_error: true, text: 'El verbo fallo: ' + m });
|
|
2356
|
+
return;
|
|
1194
2357
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
2358
|
+
const result = (r.result && typeof r.result === 'object') ? r.result : {};
|
|
2359
|
+
/* Caso 1: panel-dispatch (pizarron). Encolar para el panel +
|
|
2360
|
+
* devolver al subproceso un texto de exito. */
|
|
2361
|
+
if (result.nac3_panel_dispatch === true && result.verb && typeof result.verb === 'object') {
|
|
2362
|
+
const verb = result.verb;
|
|
2363
|
+
const dispatchArgs = (result.args && typeof result.args === 'object')
|
|
2364
|
+
? result.args : {};
|
|
2365
|
+
const payload = (result.server_payload && typeof result.server_payload === 'object')
|
|
2366
|
+
? result.server_payload : null;
|
|
2367
|
+
const shortVerb = String(verb.id || '').replace(/^yujin\./, '');
|
|
2368
|
+
const { enqueuePanelDispatch } = await import('./panel_queue.js');
|
|
2369
|
+
const seq = enqueuePanelDispatch(shortVerb, payload ? { ...dispatchArgs, server_payload: payload } : dispatchArgs, new Date().toISOString());
|
|
2370
|
+
sendJson(res, 200, {
|
|
2371
|
+
ok: true, seq, panel_dispatch: true,
|
|
2372
|
+
text: 'Listo: enviado al pizarron (' + shortVerb + ').',
|
|
1200
2373
|
});
|
|
1201
|
-
|
|
1202
|
-
ciphertext. The 4-char prefix on list() is the only
|
|
1203
|
-
visible confirmation. */
|
|
1204
|
-
const meta = vault.list().find((s) => s.name === body.slot);
|
|
1205
|
-
sendJson(res, 200, { ok: true, slot: meta });
|
|
2374
|
+
return;
|
|
1206
2375
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
2376
|
+
/* Caso 2: requiere aprobacion. El subproceso no puede abrir el
|
|
2377
|
+
* modal sincronicamente; le decimos que el usuario debe aprobar.
|
|
2378
|
+
* NO se ejecuta la accion destructiva sin aprobacion. */
|
|
2379
|
+
if (result.nac3_panel_approval_required === true) {
|
|
2380
|
+
sendJson(res, 200, {
|
|
2381
|
+
ok: false, approval_required: true,
|
|
2382
|
+
text: 'Esta accion requiere aprobacion del usuario en el panel de Forge. '
|
|
2383
|
+
+ 'Pedile que la apruebe y reintenta, o ejecutala en modo API.',
|
|
1211
2384
|
});
|
|
2385
|
+
return;
|
|
1212
2386
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
entries = entries.filter((e) => e.kind === kindFilter);
|
|
2387
|
+
/* Caso 3: resultado normal -> devolver al subproceso. Texto
|
|
2388
|
+
* compacto + el JSON del resultado para que el modelo razone. */
|
|
2389
|
+
let text;
|
|
2390
|
+
try {
|
|
2391
|
+
text = typeof r.result === 'string' ? r.result : JSON.stringify(r.result);
|
|
1219
2392
|
}
|
|
2393
|
+
catch {
|
|
2394
|
+
text = '[resultado no serializable]';
|
|
2395
|
+
}
|
|
2396
|
+
/* Dogfood 2026-06-10 -- 8000 era muy bajo: list-edges (104
|
|
2397
|
+
* aristas en JSON) se truncaba y el modelo caia al workaround de
|
|
2398
|
+
* leer el .db con sqlite. Alineado con el truncado de tool
|
|
2399
|
+
* results del modo API (200 KB). */
|
|
1220
2400
|
sendJson(res, 200, {
|
|
1221
|
-
ok: true,
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
...(e.obtainUrl ? { obtain_url: e.obtainUrl } : {}),
|
|
1225
|
-
has_probe: !!e.test,
|
|
1226
|
-
})),
|
|
2401
|
+
ok: r.is_error !== true,
|
|
2402
|
+
is_error: r.is_error === true,
|
|
2403
|
+
text: text.slice(0, 200000),
|
|
1227
2404
|
});
|
|
1228
2405
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
available: FORGE_MODES,
|
|
1237
|
-
});
|
|
2406
|
+
/** PND-064 slice B -- GET /api/forge/panel-queue?after=<seq>. El
|
|
2407
|
+
* panel hace polling mientras corre en modo plan. */
|
|
2408
|
+
async function handlePanelQueue(_req, res, url) {
|
|
2409
|
+
const after = Number(url.searchParams.get('after') || '0');
|
|
2410
|
+
const { readPanelDispatchesAfter } = await import('./panel_queue.js');
|
|
2411
|
+
const { items, last } = readPanelDispatchesAfter(after);
|
|
2412
|
+
sendJson(res, 200, { ok: true, items, last });
|
|
1238
2413
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
2414
|
+
/** GRAPH-1 -- POST /api/forge/graph-regenerate. Deriva el grafo
|
|
2415
|
+
* completo (nodos de modulo + libreria + aristas de imports,
|
|
2416
|
+
* validadas), lo persiste y poda colgantes. Reutilizable desde el
|
|
2417
|
+
* boton Refresh y el auto-regen del watcher. */
|
|
2418
|
+
/** Mobile pairing QR -- genera el codigo + envelope cifrado y lo
|
|
2419
|
+
* renderiza como QR SVG para que Forge Mobile lo escanee. El
|
|
2420
|
+
* usuario solo tipea el codigo de 6 digitos en el telefono. El
|
|
2421
|
+
* payload (que lleva el server_api_key) NUNCA sale por aca en
|
|
2422
|
+
* claro: solo va el QR cifrado + el codigo (que el usuario ya ve
|
|
2423
|
+
* en su propia pantalla). */
|
|
2424
|
+
async function handleMobilePair(_req, res) {
|
|
1241
2425
|
try {
|
|
1242
|
-
|
|
2426
|
+
const { assemblePairing } = await import('../commands/mobile.js');
|
|
2427
|
+
const built = await assemblePairing({});
|
|
2428
|
+
if (!built.ok || !built.qr || !built.code) {
|
|
2429
|
+
sendJson(res, 200, { ok: false, error: built.error ?? 'no pude generar el pairing', hint: built.hint });
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
const QRCode = (await import('qrcode')).default;
|
|
2433
|
+
const qrSvg = await QRCode.toString(built.qr, {
|
|
2434
|
+
type: 'svg', margin: 1, errorCorrectionLevel: 'M',
|
|
2435
|
+
color: { dark: '#1a1a1a', light: '#f5f0e6' },
|
|
2436
|
+
});
|
|
2437
|
+
sendJson(res, 200, { ok: true, code: built.code, expires_at: built.expires_at, qr_svg: qrSvg });
|
|
1243
2438
|
}
|
|
1244
|
-
catch {
|
|
1245
|
-
sendJson(res,
|
|
2439
|
+
catch (err) {
|
|
2440
|
+
sendJson(res, 200, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
/** Startup tabs -- arma las 4 pestanas del pizarron server-side.
|
|
2444
|
+
* Devuelve {enabled, tabs:[{key,title,kind,content}]}. El panel
|
|
2445
|
+
* las materializa al abrir (salvo las que el usuario cerro) y
|
|
2446
|
+
* hace polling para auto-refrescarlas. */
|
|
2447
|
+
async function handleStartupTabs(_req, res, ctx, url) {
|
|
2448
|
+
const slug = ctx.projectSlug || '';
|
|
2449
|
+
let enabled = true;
|
|
2450
|
+
try {
|
|
2451
|
+
const { readBrainConfig } = await import('../core/brain_config.js');
|
|
2452
|
+
enabled = (await readBrainConfig()).startup_tabs_enabled !== false;
|
|
2453
|
+
}
|
|
2454
|
+
catch { /* default on */ }
|
|
2455
|
+
if (!enabled) {
|
|
2456
|
+
sendJson(res, 200, { ok: true, enabled: false, tabs: [] });
|
|
1246
2457
|
return;
|
|
1247
2458
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
2459
|
+
/* i18n-boundary pass -- los titulos + headers de las tabs van en
|
|
2460
|
+
* el idioma del usuario (los ids/verbos no). */
|
|
2461
|
+
const { tForLang } = await import('../i18n/index.js');
|
|
2462
|
+
const lang = (url.searchParams.get('lang') || 'es');
|
|
2463
|
+
const t = (k) => tForLang(lang, k);
|
|
2464
|
+
const tabs = [];
|
|
2465
|
+
/* 1 + 2: grafo (grafico + semantico). */
|
|
2466
|
+
try {
|
|
2467
|
+
const { listRelevant, listAllEdges } = await import('../semantic_graph/store.js');
|
|
2468
|
+
/* Vista arquitectonica: los nodos derivados module.* y lib.*
|
|
2469
|
+
* (no los comp.* de export fino del watcher, que tienen kind
|
|
2470
|
+
* 'module' pero saturan el grafo). Filtro por prefijo de id. */
|
|
2471
|
+
const allNodes = slug ? await listRelevant(slug, { limit: 100000, min_confidence: 'claimed' }) : [];
|
|
2472
|
+
const nodes = allNodes.filter((n) => n.id.startsWith('module.') || n.id.startsWith('lib.'));
|
|
2473
|
+
const allEdges = slug ? await listAllEdges(slug) : [];
|
|
2474
|
+
const nodeIdSet = new Set(nodes.map((n) => n.id));
|
|
2475
|
+
const edges = allEdges.filter((e) => nodeIdSet.has(e.from_id) && nodeIdSet.has(e.to_id));
|
|
2476
|
+
const gNodes = nodes.map((n) => ({ id: n.id, label: n.name || n.id, weight: n.weight, kind: n.kind, detail: n.purpose || '' }));
|
|
2477
|
+
const gEdges = edges.map((e) => ({ from: e.from_id, to: e.to_id, kind: e.kind }));
|
|
2478
|
+
tabs.push({ key: 'startup_graph', title: t('startup.tab.graph'), kind: 'graph', content: JSON.stringify({ nodes: gNodes, edges: gEdges }) });
|
|
2479
|
+
/* Grafo semantico = misma data en markdown (tabla de nodos por
|
|
2480
|
+
* peso + conteo de aristas). */
|
|
2481
|
+
const top = [...nodes].sort((a, b) => (b.weight || 0) - (a.weight || 0)).slice(0, 40);
|
|
2482
|
+
let md = '# ' + t('startup.tab.graphSem') + ' -- ' + (slug || 'proyecto') + '\n\n'
|
|
2483
|
+
+ nodes.length + ' ' + t('graph.term.nodes') + ', ' + edges.length + ' ' + t('graph.term.edges') + '.\n\n'
|
|
2484
|
+
+ '| ' + t('startup.tab.graphSem') + ' | | ' + t('pizarron.graph.detail.weight') + ' |\n|---|---|---|\n';
|
|
2485
|
+
for (const n of top)
|
|
2486
|
+
md += '| ' + (n.name || n.id) + ' | ' + n.kind + ' | ' + (Math.round((n.weight || 0) * 100) / 100) + ' |\n';
|
|
2487
|
+
tabs.push({ key: 'startup_graph_sem', title: t('startup.tab.graphSem'), kind: 'md', content: md });
|
|
2488
|
+
}
|
|
2489
|
+
catch { /* sin grafo */ }
|
|
2490
|
+
/* 3: pendings. */
|
|
2491
|
+
try {
|
|
2492
|
+
if (slug) {
|
|
2493
|
+
const { readPending } = await import('../pending/store.js');
|
|
2494
|
+
const pf = await readPending(slug);
|
|
2495
|
+
const open = (pf.items || []).filter((i) => !i.closed_at);
|
|
2496
|
+
let md = '# Pendientes activos (' + open.length + ')\n\n';
|
|
2497
|
+
const byPrio = [...open].sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
2498
|
+
md += '| ID | Prio | Titulo |\n|---|---|---|\n';
|
|
2499
|
+
for (const it of byPrio.slice(0, 60)) {
|
|
2500
|
+
md += '| ' + String(it.id) + ' | ' + (it.priority ?? '-') + ' | ' + String(it.title || '').replace(/\|/g, '/').slice(0, 90) + ' |\n';
|
|
2501
|
+
}
|
|
2502
|
+
tabs.push({ key: 'startup_pendings', title: t('startup.tab.pendings'), kind: 'md', content: md });
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
catch { /* sin pendings */ }
|
|
2506
|
+
/* 4: workflow como GRAFO de nodos (las 6 fases del flujo yujin
|
|
2507
|
+
* con sus pasos), coloreadas por estado (done/pendiente) cuando
|
|
2508
|
+
* hay workflow state. Cumple el pedido de "workflow como grafo
|
|
2509
|
+
* de nodos y conectores con detalle al click". */
|
|
2510
|
+
try {
|
|
2511
|
+
/* PND-094 -- vista jerarquica de FASES + tareas (no grafo): cada
|
|
2512
|
+
* fase con su avance + sus tareas debajo con su estado. */
|
|
2513
|
+
const { readWorkflow } = await import('../workflow/state.js');
|
|
2514
|
+
const { computeWorkflowView } = await import('../workflow/view.js');
|
|
2515
|
+
const wf = await readWorkflow(ctx.projectRoot);
|
|
2516
|
+
const view = computeWorkflowView(wf);
|
|
2517
|
+
tabs.push({ key: 'startup_workflow', title: t('startup.tab.workflow'), kind: 'workflow', content: JSON.stringify(view) });
|
|
2518
|
+
}
|
|
2519
|
+
catch {
|
|
2520
|
+
tabs.push({ key: 'startup_workflow', title: t('startup.tab.workflow'), kind: 'md', content: '# Workflow\n\nSin datos.\n' });
|
|
2521
|
+
}
|
|
2522
|
+
sendJson(res, 200, { ok: true, enabled: true, tabs });
|
|
2523
|
+
}
|
|
2524
|
+
/** Formato de id de pending para display (PND-NNN). */
|
|
2525
|
+
function formatPendId(id) {
|
|
2526
|
+
if (typeof id === 'number')
|
|
2527
|
+
return 'PND-' + String(id).padStart(3, '0');
|
|
2528
|
+
return String(id);
|
|
2529
|
+
}
|
|
2530
|
+
/** Las 6 fases del flujo yujin (18 pasos), para el grafo de
|
|
2531
|
+
* workflow. Cada fase es un nodo con sus pasos en el detalle. */
|
|
2532
|
+
const WORKFLOW_PHASES = [
|
|
2533
|
+
{ id: 'phase_i', label: 'I. Descubrimiento', steps: ['1 intent', '2 vault inventory', '3 legacy audit', '3.5 brownfield'] },
|
|
2534
|
+
{ id: 'phase_ii', label: 'II. Clarificacion', steps: ['4 funcional', '5 NFR cuantitativo', '6 NFR estructural', '7 NFR governance', '8 NFR operacional', '9 blocks'] },
|
|
2535
|
+
{ id: 'phase_iii', label: 'III. Especificacion', steps: ['10 RFP', '11 arquitectura', '12 design system'] },
|
|
2536
|
+
{ id: 'phase_iv', label: 'IV. Aprobacion', steps: ['13 gate de aprobacion'] },
|
|
2537
|
+
{ id: 'phase_v', label: 'V. Construccion', steps: ['14 scaffolding', '15 block-dev', '16 tests'] },
|
|
2538
|
+
{ id: 'phase_vi', label: 'VI. Entrega', steps: ['17 QA visual', '18 ship'] },
|
|
2539
|
+
];
|
|
2540
|
+
/** Reune las keys "done" del workflow state (recursivo: cualquier
|
|
2541
|
+
* objeto con done_at cuenta como hecho). */
|
|
2542
|
+
function collectDoneWorkflowKeys(wf) {
|
|
2543
|
+
const done = new Set();
|
|
2544
|
+
const visit = (obj, keyPath) => {
|
|
2545
|
+
if (!obj || typeof obj !== 'object')
|
|
2546
|
+
return;
|
|
2547
|
+
const o = obj;
|
|
2548
|
+
if (o.done_at || o.completed_at || o.approved_at)
|
|
2549
|
+
done.add(keyPath);
|
|
2550
|
+
for (const [k, v] of Object.entries(o)) {
|
|
2551
|
+
if (v && typeof v === 'object')
|
|
2552
|
+
visit(v, k);
|
|
2553
|
+
}
|
|
2554
|
+
};
|
|
2555
|
+
visit(wf, 'root');
|
|
2556
|
+
return done;
|
|
2557
|
+
}
|
|
2558
|
+
/** Construye el grafo {nodes,edges} de las 6 fases del workflow,
|
|
2559
|
+
* coloreando done vs pendiente. Las labels de fase salen de las
|
|
2560
|
+
* keys canonicas workflow.phase.* (i18n). */
|
|
2561
|
+
async function buildWorkflowGraph(done, lang) {
|
|
2562
|
+
const { tForLang } = await import('../i18n/index.js');
|
|
2563
|
+
const phaseLabel = (id, fallback) => {
|
|
2564
|
+
const v = tForLang(lang, 'workflow.phase.' + id.replace('phase_', ''));
|
|
2565
|
+
return v && v !== ('workflow.phase.' + id.replace('phase_', '')) ? v : fallback;
|
|
2566
|
+
};
|
|
2567
|
+
const nodes = WORKFLOW_PHASES.map((p) => {
|
|
2568
|
+
const isDone = done.has(p.id) || p.steps.some((s) => done.has(s));
|
|
2569
|
+
const label = phaseLabel(p.id, p.label);
|
|
2570
|
+
return {
|
|
2571
|
+
id: p.id, label,
|
|
2572
|
+
weight: 0.8, kind: 'phase',
|
|
2573
|
+
color: isDone ? '#3fa85e' : '#c8a04d',
|
|
2574
|
+
detail: '**' + label + '**\n\n' + (isDone ? 'Estado: avanzada\n\n' : 'Estado: pendiente\n\n')
|
|
2575
|
+
+ 'Pasos:\n' + p.steps.map((s) => '- ' + s).join('\n'),
|
|
2576
|
+
};
|
|
2577
|
+
});
|
|
2578
|
+
const edges = [];
|
|
2579
|
+
for (let i = 0; i < WORKFLOW_PHASES.length - 1; i++) {
|
|
2580
|
+
edges.push({ from: WORKFLOW_PHASES[i].id, to: WORKFLOW_PHASES[i + 1].id, kind: 'uses', label: 'siguiente' });
|
|
2581
|
+
}
|
|
2582
|
+
return { nodes, edges };
|
|
2583
|
+
}
|
|
2584
|
+
async function handleGraphRegenerate(_req, res, ctx) {
|
|
2585
|
+
const slug = ctx.projectSlug;
|
|
2586
|
+
if (!slug) {
|
|
2587
|
+
sendJson(res, 200, { ok: false, error: 'no active project slug' });
|
|
1250
2588
|
return;
|
|
1251
2589
|
}
|
|
1252
|
-
const mode = body.mode;
|
|
1253
2590
|
try {
|
|
1254
|
-
|
|
2591
|
+
/* enrich=false: regen rapido sin pegar a npm (el boton es
|
|
2592
|
+
* interactivo; el enrich corre en el auto-ingest de boot). */
|
|
2593
|
+
const { regenerateAndPersist } = await import('../semantic_graph/derive.js');
|
|
2594
|
+
const stats = await regenerateAndPersist({ project_root: ctx.projectRoot, project_slug: slug, enrich: false });
|
|
2595
|
+
sendJson(res, 200, { ok: true, ...stats });
|
|
1255
2596
|
}
|
|
1256
2597
|
catch (err) {
|
|
1257
|
-
sendJson(res,
|
|
1258
|
-
|
|
2598
|
+
sendJson(res, 200, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
/** PND-069 -- GET /api/forge/brain-models?provider=anthropic.
|
|
2602
|
+
* Lista de modelos del provider via su API (key del vault) con
|
|
2603
|
+
* fallback estatico. Nunca expone la key. */
|
|
2604
|
+
async function handleBrainModels(_req, res, url) {
|
|
2605
|
+
const { ALL_BRAIN_PROVIDERS } = await import('../core/brain_config.js');
|
|
2606
|
+
const { listProviderModels } = await import('../core/provider_models.js');
|
|
2607
|
+
const provider = url.searchParams.get('provider') ?? '';
|
|
2608
|
+
if (!ALL_BRAIN_PROVIDERS.includes(provider)) {
|
|
2609
|
+
sendJson(res, 400, { ok: false, error: 'unknown provider: ' + provider.slice(0, 40) });
|
|
1259
2610
|
return;
|
|
1260
2611
|
}
|
|
1261
|
-
|
|
2612
|
+
const result = await listProviderModels(provider);
|
|
2613
|
+
sendJson(res, 200, { ok: true, ...result });
|
|
2614
|
+
}
|
|
2615
|
+
/** alpha.59z.114 -- pull the canonical license row from the
|
|
2616
|
+
* HITO 4 worker (which reads the Polar webhook D1) and refresh
|
|
2617
|
+
* the local cache. Hit by the panel's "Sincronizar plan" button
|
|
2618
|
+
* + by the silent auto-sync on settings open when local plan
|
|
2619
|
+
* reads 'none' but user_handle is set. */
|
|
2620
|
+
async function handleLicenseSync(_req, res) {
|
|
2621
|
+
const { syncLicense } = await import('../license/sync.js');
|
|
2622
|
+
const report = await syncLicense({ verbose: false });
|
|
2623
|
+
if (!report.ok) {
|
|
2624
|
+
sendJson(res, 200, {
|
|
2625
|
+
ok: false,
|
|
2626
|
+
error: report.error ?? 'sync failed',
|
|
2627
|
+
http_status: report.http_status,
|
|
2628
|
+
local_plan_before: report.local_plan_before,
|
|
2629
|
+
});
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
sendJson(res, 200, {
|
|
2633
|
+
ok: true,
|
|
2634
|
+
server_plan: report.server_plan,
|
|
2635
|
+
local_plan_before: report.local_plan_before,
|
|
2636
|
+
local_plan_after: report.local_plan_after,
|
|
2637
|
+
last_validated_at: report.last_validated_at,
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
/** alpha.59z.117 -- write user_handle to the local polar cache
|
|
2641
|
+
* + run an immediate sync. Returns the final cache state.
|
|
2642
|
+
* Used by the panel "Conectar tu cuenta Polar" first-run flow. */
|
|
2643
|
+
async function handleLicenseHandle(req, res) {
|
|
2644
|
+
let body = {};
|
|
2645
|
+
try {
|
|
2646
|
+
const raw = await readBody(req);
|
|
2647
|
+
body = raw ? JSON.parse(raw) : {};
|
|
2648
|
+
}
|
|
2649
|
+
catch {
|
|
2650
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON' });
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
const handle = typeof body.handle === 'string' ? body.handle.trim().toLowerCase() : '';
|
|
2654
|
+
if (!handle || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(handle)) {
|
|
2655
|
+
sendJson(res, 400, { ok: false, error: 'handle must be a valid email address' });
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
const polar = await import('../core/polar.js');
|
|
2659
|
+
const current = await polar.readLicense();
|
|
2660
|
+
await polar.writeLicense({ ...current, user_handle: handle });
|
|
2661
|
+
const { syncLicense } = await import('../license/sync.js');
|
|
2662
|
+
const report = await syncLicense({ verbose: false });
|
|
2663
|
+
sendJson(res, 200, {
|
|
2664
|
+
ok: report.ok,
|
|
2665
|
+
handle_saved: handle,
|
|
2666
|
+
server_plan: report.server_plan,
|
|
2667
|
+
local_plan_before: report.local_plan_before,
|
|
2668
|
+
local_plan_after: report.local_plan_after,
|
|
2669
|
+
last_validated_at: report.last_validated_at,
|
|
2670
|
+
error: report.error,
|
|
2671
|
+
http_status: report.http_status,
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
const ALLOWED_KEY_SLOTS = new Set([
|
|
2675
|
+
'anthropic_api_key',
|
|
2676
|
+
'openai_api_key',
|
|
2677
|
+
'google_ai_key',
|
|
2678
|
+
'google_stt_key',
|
|
2679
|
+
'google_tts_key',
|
|
2680
|
+
'whisper_api_key',
|
|
2681
|
+
'elevenlabs_api_key',
|
|
2682
|
+
]);
|
|
2683
|
+
async function handleReadFile(req, res, ctx) {
|
|
2684
|
+
const u = new URL(req.url ?? '/', 'http://localhost');
|
|
2685
|
+
const rel = (u.searchParams.get('path') || '').trim();
|
|
2686
|
+
if (!rel || rel.startsWith('/') || rel.includes('..') || rel.length > 500) {
|
|
2687
|
+
sendJson(res, 400, { ok: false, error: 'path must be a non-empty project-relative string (no .. , no leading slash)' });
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
const path = await import('node:path');
|
|
2691
|
+
const fs = await import('node:fs');
|
|
2692
|
+
const abs = path.resolve(ctx.projectRoot, rel);
|
|
2693
|
+
if (!isInside(abs, ctx.projectRoot)) {
|
|
2694
|
+
sendJson(res, 403, { ok: false, error: 'path escapes project root' });
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
try {
|
|
2698
|
+
const buf = await fs.promises.readFile(abs);
|
|
2699
|
+
/* Treat as text if it parses cleanly; otherwise refuse. */
|
|
2700
|
+
if (buf.length > 2_000_000) {
|
|
2701
|
+
sendJson(res, 413, { ok: false, error: 'file too large (>2 MB)' });
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
const text = buf.toString('utf-8');
|
|
2705
|
+
sendJson(res, 200, { ok: true, path: rel, content: text, size_bytes: buf.length });
|
|
2706
|
+
}
|
|
2707
|
+
catch (err) {
|
|
2708
|
+
sendJson(res, 404, { ok: false, error: 'file not found: ' + (err instanceof Error ? err.message : String(err)) });
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
async function handleWriteFile(req, res, ctx) {
|
|
2712
|
+
let raw;
|
|
2713
|
+
try {
|
|
2714
|
+
raw = await readBody(req);
|
|
2715
|
+
}
|
|
2716
|
+
catch {
|
|
2717
|
+
sendJson(res, 400, { ok: false, error: 'unreadable body' });
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
let body;
|
|
2721
|
+
try {
|
|
2722
|
+
body = JSON.parse(raw);
|
|
2723
|
+
}
|
|
2724
|
+
catch {
|
|
2725
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
const rel = typeof body.path === 'string' ? body.path.trim() : '';
|
|
2729
|
+
const content = typeof body.content === 'string' ? body.content : '';
|
|
2730
|
+
if (!rel || rel.startsWith('/') || rel.includes('..') || rel.length > 500) {
|
|
2731
|
+
sendJson(res, 400, { ok: false, error: 'path must be a non-empty project-relative string (no .. , no leading slash)' });
|
|
2732
|
+
return;
|
|
2733
|
+
}
|
|
2734
|
+
if (content.length > 5_000_000) {
|
|
2735
|
+
sendJson(res, 413, { ok: false, error: 'content too large (>5 MB)' });
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
const path = await import('node:path');
|
|
2739
|
+
const fs = await import('node:fs');
|
|
2740
|
+
const abs = path.resolve(ctx.projectRoot, rel);
|
|
2741
|
+
if (!isInside(abs, ctx.projectRoot)) {
|
|
2742
|
+
sendJson(res, 403, { ok: false, error: 'path escapes project root' });
|
|
2743
|
+
return;
|
|
2744
|
+
}
|
|
2745
|
+
try {
|
|
2746
|
+
/* Atomic write: tmp file + rename. */
|
|
2747
|
+
await fs.promises.mkdir(path.dirname(abs), { recursive: true });
|
|
2748
|
+
const tmp = abs + '.tmp.' + Date.now();
|
|
2749
|
+
await fs.promises.writeFile(tmp, content, 'utf-8');
|
|
2750
|
+
await fs.promises.rename(tmp, abs);
|
|
2751
|
+
const st = await fs.promises.stat(abs);
|
|
2752
|
+
sendJson(res, 200, { ok: true, path: rel, bytes_written: st.size });
|
|
2753
|
+
}
|
|
2754
|
+
catch (err) {
|
|
2755
|
+
sendJson(res, 500, { ok: false, error: 'write failed: ' + (err instanceof Error ? err.message : String(err)) });
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
async function handleKeysSet(req, res) {
|
|
2759
|
+
let raw;
|
|
2760
|
+
try {
|
|
2761
|
+
raw = await readBody(req);
|
|
2762
|
+
}
|
|
2763
|
+
catch (err) {
|
|
2764
|
+
sendJson(res, 400, { ok: false, error: 'unreadable body' });
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
let body;
|
|
2768
|
+
try {
|
|
2769
|
+
body = JSON.parse(raw);
|
|
2770
|
+
}
|
|
2771
|
+
catch {
|
|
2772
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
const slot = typeof body.slot === 'string' ? body.slot.trim() : '';
|
|
2776
|
+
if (!ALLOWED_KEY_SLOTS.has(slot)) {
|
|
2777
|
+
sendJson(res, 400, { ok: false, error: 'slot must be one of: ' + Array.from(ALLOWED_KEY_SLOTS).join(', ') });
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
/* value === '' OR null means clear. Otherwise it is the new
|
|
2781
|
+
* key value (min 8 chars, no leading/trailing whitespace). */
|
|
2782
|
+
const value = body.value === null ? '' : (typeof body.value === 'string' ? body.value.trim() : '');
|
|
2783
|
+
if (value !== '' && value.length < 8) {
|
|
2784
|
+
sendJson(res, 400, { ok: false, error: 'key value too short (min 8 chars)' });
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
/* alpha.44 C1: persist via the encrypted Vault helper. The
|
|
2788
|
+
* helper handles the legacy plaintext migration on first call
|
|
2789
|
+
* + atomic write + slot validation. */
|
|
2790
|
+
try {
|
|
2791
|
+
const { setKey } = await import('../core/provider_keys.js');
|
|
2792
|
+
await setKey(slot, value === '' ? null : value);
|
|
2793
|
+
}
|
|
2794
|
+
catch (err) {
|
|
2795
|
+
sendJson(res, 400, {
|
|
2796
|
+
ok: false,
|
|
2797
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2798
|
+
});
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
sendJson(res, 200, { ok: true, slot, set: value !== '' });
|
|
2802
|
+
}
|
|
2803
|
+
async function handleSupportReport(req, res) {
|
|
2804
|
+
let raw;
|
|
2805
|
+
try {
|
|
2806
|
+
raw = await readBody(req);
|
|
2807
|
+
}
|
|
2808
|
+
catch (err) {
|
|
2809
|
+
sendJson(res, 400, { ok: false, error: 'unreadable body: ' + (err instanceof Error ? err.message : String(err)) });
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
let body;
|
|
2813
|
+
try {
|
|
2814
|
+
body = JSON.parse(raw);
|
|
2815
|
+
}
|
|
2816
|
+
catch {
|
|
2817
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
const { validateInbound, recordReport } = await import('../support/reports.js');
|
|
2821
|
+
/* Support either a single report or an array (the panel
|
|
2822
|
+
* batches up to 50 + flushes every 30s). */
|
|
2823
|
+
const ua = typeof req.headers['user-agent'] === 'string' ? req.headers['user-agent'] : undefined;
|
|
2824
|
+
const items = Array.isArray(body) ? body : [body];
|
|
2825
|
+
let accepted = 0;
|
|
2826
|
+
let rejected = 0;
|
|
2827
|
+
for (const item of items.slice(0, 50)) {
|
|
2828
|
+
const report = validateInbound(item, ua ? { user_agent: ua } : {});
|
|
2829
|
+
if (report) {
|
|
2830
|
+
await recordReport(report);
|
|
2831
|
+
accepted++;
|
|
2832
|
+
}
|
|
2833
|
+
else {
|
|
2834
|
+
rejected++;
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
sendJson(res, 200, { ok: true, accepted, rejected });
|
|
2838
|
+
}
|
|
2839
|
+
async function handleSupportReportList(req, res) {
|
|
2840
|
+
/* Local panel only -- we already gate the chat server to
|
|
2841
|
+
* loopback. No additional auth needed for read. */
|
|
2842
|
+
const { readReports } = await import('../support/reports.js');
|
|
2843
|
+
const u = new URL(req.url ?? '/', 'http://localhost');
|
|
2844
|
+
const limit = Math.min(500, Math.max(1, parseInt(u.searchParams.get('limit') ?? '100', 10) || 100));
|
|
2845
|
+
const reports = (await readReports()).slice(-limit);
|
|
2846
|
+
sendJson(res, 200, { ok: true, count: reports.length, reports });
|
|
2847
|
+
}
|
|
2848
|
+
async function handleTrustState(_req, res, _ctx) {
|
|
2849
|
+
const m = await loadTrustMetrics();
|
|
2850
|
+
/* Decay logic: if no events in last 30 days AND user has not
|
|
2851
|
+
* explicitly chosen to keep showing, suggest auto-collapse.
|
|
2852
|
+
* The panel decides what to do with the suggestion. */
|
|
2853
|
+
let shouldProposeCollapse = false;
|
|
2854
|
+
if (m.user_choice === 'default') {
|
|
2855
|
+
const last = m.last_event_at ? new Date(m.last_event_at).getTime() : 0;
|
|
2856
|
+
const proposedRecently = m.last_proposal_at
|
|
2857
|
+
? (Date.now() - new Date(m.last_proposal_at).getTime()) < 7 * 24 * 3600 * 1000
|
|
2858
|
+
: false;
|
|
2859
|
+
const daysSince = last ? (Date.now() - last) / (24 * 3600 * 1000) : Infinity;
|
|
2860
|
+
shouldProposeCollapse = daysSince > 30 && !proposedRecently;
|
|
2861
|
+
}
|
|
2862
|
+
sendJson(res, 200, {
|
|
2863
|
+
ok: true,
|
|
2864
|
+
metrics: m,
|
|
2865
|
+
propose_auto_collapse: shouldProposeCollapse,
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
async function handleVaultList(res) {
|
|
2869
|
+
try {
|
|
2870
|
+
const vault = await Vault.open({ configDir: configDir() });
|
|
2871
|
+
sendJson(res, 200, { ok: true, slots: vault.list() });
|
|
2872
|
+
}
|
|
2873
|
+
catch (err) {
|
|
2874
|
+
sendJson(res, 500, {
|
|
2875
|
+
ok: false,
|
|
2876
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
async function handleVaultSet(req, res) {
|
|
2881
|
+
let body;
|
|
2882
|
+
try {
|
|
2883
|
+
body = JSON.parse(await readBody(req));
|
|
2884
|
+
}
|
|
2885
|
+
catch {
|
|
2886
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2889
|
+
if (typeof body.slot !== 'string' || typeof body.plaintext !== 'string') {
|
|
2890
|
+
sendJson(res, 400, { ok: false, error: 'slot + plaintext (both strings) required' });
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
/* Optional kind / expiry validation. */
|
|
2894
|
+
let kindOpt;
|
|
2895
|
+
if (body.kind !== undefined) {
|
|
2896
|
+
if (typeof body.kind !== 'string' || !SLOT_KINDS.includes(body.kind)) {
|
|
2897
|
+
sendJson(res, 400, { ok: false, error: 'invalid kind. Allowed: ' + SLOT_KINDS.join(', ') });
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
kindOpt = body.kind;
|
|
2901
|
+
}
|
|
2902
|
+
let expiryOpt;
|
|
2903
|
+
if (body.expiry === null) {
|
|
2904
|
+
expiryOpt = null;
|
|
2905
|
+
}
|
|
2906
|
+
else if (body.expiry !== undefined) {
|
|
2907
|
+
if (typeof body.expiry !== 'string') {
|
|
2908
|
+
sendJson(res, 400, { ok: false, error: 'expiry must be a string ISO 8601 or null' });
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
const d = new Date(body.expiry);
|
|
2912
|
+
if (isNaN(d.getTime())) {
|
|
2913
|
+
sendJson(res, 400, { ok: false, error: 'expiry is not a valid ISO 8601 timestamp' });
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
expiryOpt = d.toISOString();
|
|
2917
|
+
}
|
|
2918
|
+
try {
|
|
2919
|
+
const vault = await Vault.open({ configDir: configDir() });
|
|
2920
|
+
await vault.set(body.slot, body.plaintext, {
|
|
2921
|
+
...(kindOpt ? { kind: kindOpt } : {}),
|
|
2922
|
+
...(expiryOpt !== undefined ? { expiry: expiryOpt } : {}),
|
|
2923
|
+
});
|
|
2924
|
+
/* Echo metadata only -- never the plaintext, never the
|
|
2925
|
+
ciphertext. The 4-char prefix on list() is the only
|
|
2926
|
+
visible confirmation. */
|
|
2927
|
+
const meta = vault.list().find((s) => s.name === body.slot);
|
|
2928
|
+
sendJson(res, 200, { ok: true, slot: meta });
|
|
2929
|
+
}
|
|
2930
|
+
catch (err) {
|
|
2931
|
+
sendJson(res, 400, {
|
|
2932
|
+
ok: false,
|
|
2933
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
async function handleVaultCatalog(res, url) {
|
|
2938
|
+
const kindFilter = url.searchParams.get('kind');
|
|
2939
|
+
let entries = SLOT_CATALOG;
|
|
2940
|
+
if (kindFilter && SLOT_KINDS.includes(kindFilter)) {
|
|
2941
|
+
entries = entries.filter((e) => e.kind === kindFilter);
|
|
2942
|
+
}
|
|
2943
|
+
sendJson(res, 200, {
|
|
2944
|
+
ok: true,
|
|
2945
|
+
slots: entries.map((e) => ({
|
|
2946
|
+
name: e.name, kind: e.kind, description: e.description,
|
|
2947
|
+
...(e.obtainUrl ? { obtain_url: e.obtainUrl } : {}),
|
|
2948
|
+
has_probe: !!e.test,
|
|
2949
|
+
})),
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
async function handleModeGet(req, res, ctx) {
|
|
2953
|
+
const mode = await resolveModeForRequest(req, ctx.projectRoot);
|
|
2954
|
+
const projectResolved = await resolveMode({ projectRoot: ctx.projectRoot });
|
|
2955
|
+
sendJson(res, 200, {
|
|
2956
|
+
ok: true,
|
|
2957
|
+
mode,
|
|
2958
|
+
source: projectResolved.source,
|
|
2959
|
+
available: FORGE_MODES,
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
async function handleModeSet(req, res, ctx) {
|
|
2963
|
+
let body;
|
|
2964
|
+
try {
|
|
2965
|
+
body = JSON.parse(await readBody(req));
|
|
2966
|
+
}
|
|
2967
|
+
catch {
|
|
2968
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
if (typeof body.mode !== 'string' || !FORGE_MODES.includes(body.mode)) {
|
|
2972
|
+
sendJson(res, 400, { ok: false, error: 'mode must be one of: ' + FORGE_MODES.join(', ') });
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
const mode = body.mode;
|
|
2976
|
+
try {
|
|
2977
|
+
await persistMode(ctx.projectRoot, mode);
|
|
2978
|
+
}
|
|
2979
|
+
catch (err) {
|
|
2980
|
+
sendJson(res, 500, { ok: false,
|
|
2981
|
+
error: err instanceof Error ? err.message : String(err) });
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
/* Set cookie so the next request renders in the new mode
|
|
1262
2985
|
even before the user reloads. The cookie is the source of
|
|
1263
2986
|
truth for the running browser; the persisted file is the
|
|
1264
2987
|
source of truth for the NEXT session. */
|
|
@@ -1466,6 +3189,37 @@ async function handleChat(req, res, ctx) {
|
|
|
1466
3189
|
sendJson(res, 400, { ok: false, error: 'messages[] required' });
|
|
1467
3190
|
return;
|
|
1468
3191
|
}
|
|
3192
|
+
/* alpha.59e -- SSE streaming mode. Client opts in via
|
|
3193
|
+
* body.stream=true. We keep the response open + emit events
|
|
3194
|
+
* during the tool loop instead of bundling everything in a
|
|
3195
|
+
* single JSON. */
|
|
3196
|
+
const streamMode = body.stream === true;
|
|
3197
|
+
let sseEmit = null;
|
|
3198
|
+
if (streamMode) {
|
|
3199
|
+
res.writeHead(200, {
|
|
3200
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
3201
|
+
'cache-control': 'no-cache, no-transform',
|
|
3202
|
+
'connection': 'keep-alive',
|
|
3203
|
+
'x-accel-buffering': 'no',
|
|
3204
|
+
});
|
|
3205
|
+
sseEmit = (event, data) => {
|
|
3206
|
+
try {
|
|
3207
|
+
const payload = JSON.stringify(data ?? {});
|
|
3208
|
+
res.write('event: ' + event + '\n');
|
|
3209
|
+
res.write('data: ' + payload + '\n\n');
|
|
3210
|
+
}
|
|
3211
|
+
catch { /* ignore */ }
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
/* alpha.51 -- session-level brain tier override from slash
|
|
3215
|
+
* commands (/eco /frontier /auto). null = let cost_router
|
|
3216
|
+
* decide per-turn. The router reads workflow state + verb
|
|
3217
|
+
* hints to pick the right tier; this override pins the
|
|
3218
|
+
* decision when the user signaled an explicit preference. */
|
|
3219
|
+
let brainOverride;
|
|
3220
|
+
if (body.brain_tier_override === 'eco' || body.brain_tier_override === 'frontier') {
|
|
3221
|
+
brainOverride = body.brain_tier_override;
|
|
3222
|
+
}
|
|
1469
3223
|
const normalized = [];
|
|
1470
3224
|
for (const m of msgs) {
|
|
1471
3225
|
if (typeof m !== 'object' || m === null)
|
|
@@ -1492,19 +3246,482 @@ async function handleChat(req, res, ctx) {
|
|
|
1492
3246
|
const pilotState = await readPilotState(ctx.projectRoot);
|
|
1493
3247
|
const lastUserMsg = normalized.filter((m) => m.role === 'user').slice(-1)[0];
|
|
1494
3248
|
const lastUserText = typeof lastUserMsg?.content === 'string' ? lastUserMsg.content : '';
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
3249
|
+
/* alpha.51 -- ask the cost router which brain tier should
|
|
3250
|
+
* serve this turn. alpha.52 -- runtime clients for anthropic
|
|
3251
|
+
* + google_ai are wired. alpha.55 -- the router now runs a
|
|
3252
|
+
* reasoning_gate + turn_classifier when there is no cheap
|
|
3253
|
+
* signal so /auto becomes genuinely smart per turn. We pass
|
|
3254
|
+
* the user_text + recent history + attachment count + the
|
|
3255
|
+
* previous tier so the gate can detect sticky follow-ups
|
|
3256
|
+
* and inherit. */
|
|
3257
|
+
const { selectBrainForTurn } = await import('../core/cost_router.js');
|
|
3258
|
+
const { makeUnifiedClient } = await import('../llm/multi_provider_client.js');
|
|
3259
|
+
/* Pull previous tier from the last brain_routing.jsonl entry
|
|
3260
|
+
* via the store (best-effort). The store keeps the last
|
|
3261
|
+
* assistant reply; if it has a `brain_tier` annotation we use
|
|
3262
|
+
* it. When absent, the gate just skips the sticky check. */
|
|
3263
|
+
const recent = ctx.store.messages();
|
|
3264
|
+
const lastAssistant = recent.slice().reverse().find((m) => m.role === 'assistant');
|
|
3265
|
+
const previousTier = lastAssistant && (lastAssistant.brain_tier === 'eco' || lastAssistant.brain_tier === 'frontier')
|
|
3266
|
+
? lastAssistant.brain_tier
|
|
3267
|
+
: undefined;
|
|
3268
|
+
const previousDocLoaded = !!(lastAssistant && lastAssistant.doc_loaded === true);
|
|
3269
|
+
const historyForGate = normalized.slice(-3).map((m) => ({
|
|
3270
|
+
role: m.role,
|
|
3271
|
+
content: typeof m.content === 'string' ? m.content : '',
|
|
3272
|
+
}));
|
|
3273
|
+
/* alpha.57 -- workflow phase wiring. If the user is in
|
|
3274
|
+
* Phase III (specs: RFP / Arch / Design) or Phase V (block
|
|
3275
|
+
* implementation), the router forces frontier. This is the
|
|
3276
|
+
* structural signal Pablo flagged: the workflow itself
|
|
3277
|
+
* knows when reasoning is needed. */
|
|
3278
|
+
let workflowPhase;
|
|
3279
|
+
try {
|
|
3280
|
+
const { readWorkflow } = await import('../workflow/state.js');
|
|
3281
|
+
const { inferActivePhase } = await import('../workflow/phase_inference.js');
|
|
3282
|
+
const wf = await readWorkflow(ctx.projectRoot);
|
|
3283
|
+
workflowPhase = inferActivePhase(wf);
|
|
3284
|
+
}
|
|
3285
|
+
catch { /* missing yujin.forge.json -> phase_i, no-op */ }
|
|
3286
|
+
const brainSelection = await selectBrainForTurn({
|
|
3287
|
+
user_override: brainOverride,
|
|
3288
|
+
caller_hint: 'handleChat',
|
|
3289
|
+
user_text: lastUserText,
|
|
3290
|
+
history: historyForGate,
|
|
3291
|
+
previous_tier: previousTier,
|
|
3292
|
+
previous_doc_loaded: previousDocLoaded,
|
|
3293
|
+
workflow_phase: workflowPhase,
|
|
1507
3294
|
});
|
|
3295
|
+
/* alpha.55 -- cap exceeded check. If the tier the router
|
|
3296
|
+
* chose has zero remaining budget, return 429 BEFORE the
|
|
3297
|
+
* LLM call so the user never gets billed past their cap.
|
|
3298
|
+
* The panel modal (plan_limit_exceeded) lets them upgrade
|
|
3299
|
+
* or switch tier. BYOK plans have Infinity cap so this
|
|
3300
|
+
* is a no-op there. */
|
|
3301
|
+
{
|
|
3302
|
+
const { checkCap } = await import('../core/budget.js');
|
|
3303
|
+
const capCheck = await checkCap(brainSelection.tier);
|
|
3304
|
+
if (!capCheck.allowed) {
|
|
3305
|
+
if (sseEmit) {
|
|
3306
|
+
sseEmit('error', {
|
|
3307
|
+
kind: 'plan_limit_exceeded',
|
|
3308
|
+
tier: capCheck.tier,
|
|
3309
|
+
plan: capCheck.plan,
|
|
3310
|
+
extension_url: capCheck.extension_url ?? null,
|
|
3311
|
+
});
|
|
3312
|
+
res.end();
|
|
3313
|
+
}
|
|
3314
|
+
else {
|
|
3315
|
+
sendJson(res, 429, {
|
|
3316
|
+
ok: false,
|
|
3317
|
+
error: 'plan_limit_exceeded',
|
|
3318
|
+
tier: capCheck.tier,
|
|
3319
|
+
plan: capCheck.plan,
|
|
3320
|
+
cap: Number.isFinite(capCheck.cap) ? capCheck.cap : null,
|
|
3321
|
+
used: capCheck.used,
|
|
3322
|
+
remaining: Number.isFinite(capCheck.remaining) ? capCheck.remaining : null,
|
|
3323
|
+
extension_url: capCheck.extension_url ?? null,
|
|
3324
|
+
reason: capCheck.reason,
|
|
3325
|
+
message: 'Has agotado el cupo mensual del tier ' + capCheck.tier +
|
|
3326
|
+
' en el plan ' + capCheck.plan + '. Cambia de tier con /eco o /frontier, ' +
|
|
3327
|
+
'compra una extension, o usa BYOK.',
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
/* alpha.59e -- emit turn_start event so the panel knows
|
|
3334
|
+
* which model is serving + how many rounds the loop can
|
|
3335
|
+
* spend before exhaustion. */
|
|
3336
|
+
if (sseEmit) {
|
|
3337
|
+
sseEmit('turn_start', {
|
|
3338
|
+
tier: brainSelection.tier,
|
|
3339
|
+
provider: brainSelection.provider,
|
|
3340
|
+
model: brainSelection.model,
|
|
3341
|
+
runtime_wired: brainSelection.runtime_wired,
|
|
3342
|
+
reason: brainSelection.reason,
|
|
3343
|
+
workflow_phase: workflowPhase ?? null,
|
|
3344
|
+
});
|
|
3345
|
+
}
|
|
3346
|
+
/* alpha.59e -- loop detection counter. We track (verb_id +
|
|
3347
|
+
* args fingerprint) per turn; if the same call is made 3+
|
|
3348
|
+
* times consecutively we abort + emit an error so the LLM
|
|
3349
|
+
* cannot infinite-loop when Danger Zone is on. */
|
|
3350
|
+
const callSig = [];
|
|
3351
|
+
const LOOP_REPEAT_THRESHOLD = 3;
|
|
3352
|
+
/* When the operator launched `yf chat --provider claude-code-cli`,
|
|
3353
|
+
* route EVERY turn through ctx.claude (a ClaudeCliClient) and
|
|
3354
|
+
* skip the unified cost_router branch -- otherwise the router
|
|
3355
|
+
* would pick Gemini/Haiku/etc and bill against Console. The
|
|
3356
|
+
* fallback branch below uses ctx.claude.chat() directly. */
|
|
3357
|
+
const unified = ctx.chatProvider === 'claude-code-cli'
|
|
3358
|
+
? null
|
|
3359
|
+
: makeUnifiedClient(brainSelection);
|
|
3360
|
+
const callerForChat = unified
|
|
3361
|
+
? unified
|
|
3362
|
+
: {
|
|
3363
|
+
chat: async (r) => {
|
|
3364
|
+
/* alpha.59z.162 PND-021 -- when routing through the cli
|
|
3365
|
+
* subprocess, thread sseEmit as onStreamEvent so the panel
|
|
3366
|
+
* sees tool_round_start / _done as `claude -p` invokes its
|
|
3367
|
+
* own tools. The HTTP path gets the same feedback via the
|
|
3368
|
+
* runTool seam; this closes the parity gap. */
|
|
3369
|
+
const reqWithStream = sseEmit && ctx.chatProvider === 'claude-code-cli'
|
|
3370
|
+
? { ...r, onStreamEvent: sseEmit }
|
|
3371
|
+
: r;
|
|
3372
|
+
const claudeReply = await ctx.claude.chat(reqWithStream);
|
|
3373
|
+
return { ...claudeReply, brain: { ...brainSelection, runtime_wired: false } };
|
|
3374
|
+
},
|
|
3375
|
+
};
|
|
3376
|
+
/* alpha.59z.135 slice 11 -- panoramic graph snapshot. Compact
|
|
3377
|
+
* structural view of the project graph, cached per-slug with
|
|
3378
|
+
* hash refresh. The chat also receives the relevance ranking
|
|
3379
|
+
* below (graphSection); snapshot is the panorama, relevance is
|
|
3380
|
+
* the zoom. Graceful no-op on worker / vault failure. */
|
|
3381
|
+
let snapshotSection = '';
|
|
3382
|
+
try {
|
|
3383
|
+
const { buildGraphSnapshotSection } = await import('./graph_snapshot.js');
|
|
3384
|
+
const r = await buildGraphSnapshotSection({
|
|
3385
|
+
project_slug: ctx.projectSlug || '',
|
|
3386
|
+
/* alpha.59z.182 -- local only. The D1 worker fetcher is
|
|
3387
|
+
* parked until PND-016 closes P3 Platform billing/auth/
|
|
3388
|
+
* tier decisions. BYOK plain = no Yujin infra in chat hot
|
|
3389
|
+
* path. Slice 181 reintroduces a worker fetcher under P3. */
|
|
3390
|
+
fetcher: async (slug) => {
|
|
3391
|
+
if (!slug)
|
|
3392
|
+
return { ok: false, nodes: [] };
|
|
3393
|
+
try {
|
|
3394
|
+
const { listRelevant } = await import('../semantic_graph/store.js');
|
|
3395
|
+
const nodes = await listRelevant(slug, { limit: 200 });
|
|
3396
|
+
return {
|
|
3397
|
+
ok: nodes.length > 0,
|
|
3398
|
+
nodes: nodes.map((n) => ({ id: n.id, kind: n.kind, label: n.name })),
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
catch {
|
|
3402
|
+
return { ok: false, nodes: [] };
|
|
3403
|
+
}
|
|
3404
|
+
},
|
|
3405
|
+
});
|
|
3406
|
+
if (r.ok && r.text)
|
|
3407
|
+
snapshotSection = '\n\n' + r.text;
|
|
3408
|
+
}
|
|
3409
|
+
catch (_) { /* swallow -- chat continues without snapshot */ }
|
|
3410
|
+
/* alpha.59p -- semantic graph injection. Pulls "what primitives
|
|
3411
|
+
* exist in this project" from the worker + appends to system
|
|
3412
|
+
* prompt so the LLM does NOT reinvent libs/components/protocols
|
|
3413
|
+
* already present. Graceful no-op when worker unreachable or
|
|
3414
|
+
* no vault key. */
|
|
3415
|
+
let graphSection = '';
|
|
3416
|
+
try {
|
|
3417
|
+
const { fetchGraphSection, injectionEnabled } = await import('./semantic_graph_inject.js');
|
|
3418
|
+
if (injectionEnabled()) {
|
|
3419
|
+
const lastUser = [...normalized].reverse().find((m) => m.role === 'user');
|
|
3420
|
+
const intent = lastUser && typeof lastUser.content === 'string'
|
|
3421
|
+
? lastUser.content
|
|
3422
|
+
: (lastUser && Array.isArray(lastUser.content)
|
|
3423
|
+
? lastUser.content.filter((b) => b && b.type === 'text').map((b) => b.text).join(' ')
|
|
3424
|
+
: '');
|
|
3425
|
+
if (intent && intent.length > 0) {
|
|
3426
|
+
const r = await fetchGraphSection({
|
|
3427
|
+
intent,
|
|
3428
|
+
project_slug: ctx.projectSlug || '',
|
|
3429
|
+
limit: 8,
|
|
3430
|
+
token_budget: 500,
|
|
3431
|
+
timeout_ms: 1500,
|
|
3432
|
+
});
|
|
3433
|
+
if (r.ok && r.rendered_text)
|
|
3434
|
+
graphSection = '\n\n' + r.rendered_text;
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
catch (_) { /* swallow -- chat continues without graph */ }
|
|
3439
|
+
/* alpha.59y.2 -- workflow state injection. Reads
|
|
3440
|
+
* yujin.forge.json/workflow.* and emits a compact "PROJECT
|
|
3441
|
+
* WORKFLOW STATE" block with rules of engagement (stay in
|
|
3442
|
+
* step, ASK on scope change, no silent advance). Disabled
|
|
3443
|
+
* via YF_WORKFLOW_INJECT_DISABLED=1. */
|
|
3444
|
+
let workflowSection = '';
|
|
3445
|
+
try {
|
|
3446
|
+
const { fetchWorkflowSection } = await import('./workflow_inject.js');
|
|
3447
|
+
const w = await fetchWorkflowSection({
|
|
3448
|
+
projectRoot: ctx.projectRoot,
|
|
3449
|
+
projectName: ctx.projectName,
|
|
3450
|
+
});
|
|
3451
|
+
if (w.ok && w.rendered_text)
|
|
3452
|
+
workflowSection = '\n\n' + w.rendered_text;
|
|
3453
|
+
}
|
|
3454
|
+
catch (_) { /* swallow -- chat continues without workflow inject */ }
|
|
3455
|
+
/* alpha.59z.180 PND-012 -- inject the per-project WIRING
|
|
3456
|
+
* registry into the dynamic suffix every turn. Reads from
|
|
3457
|
+
* ~/.yujin-forge/projects/<slug>/wiring.json (the local
|
|
3458
|
+
* registry, populated by `yf wiring discover` or the
|
|
3459
|
+
* yujin.wiring.discover verb). Empty when the file is missing
|
|
3460
|
+
* or every hook is disabled. Cap at 20 hooks to keep the
|
|
3461
|
+
* token budget bounded. */
|
|
3462
|
+
let wiringSection = '';
|
|
3463
|
+
try {
|
|
3464
|
+
const { buildWiringSection } = await import('./wiring_inject.js');
|
|
3465
|
+
const r = await buildWiringSection({ project_slug: ctx.projectSlug || '', limit: 20 });
|
|
3466
|
+
if (r.rendered_text)
|
|
3467
|
+
wiringSection = '\n\n' + r.rendered_text;
|
|
3468
|
+
}
|
|
3469
|
+
catch (_) { /* swallow -- chat continues without wiring inject */ }
|
|
3470
|
+
/* alpha.59z.187 PND-039 -- auto-context-load. Single compact
|
|
3471
|
+
* block with counts of graph nodes / wiring hooks / pending
|
|
3472
|
+
* items so the LLM can answer "que sabes del proyecto" / "subi
|
|
3473
|
+
* el know how" with concrete numbers without first invoking
|
|
3474
|
+
* 3-5 verbs. Bounded by 1200 chars. Graceful no-op when slug
|
|
3475
|
+
* empty / no data. */
|
|
3476
|
+
let projectContextSection = '';
|
|
3477
|
+
try {
|
|
3478
|
+
const { buildProjectContextSection } = await import('./project_context_inject.js');
|
|
3479
|
+
const r = await buildProjectContextSection({ project_slug: ctx.projectSlug || '', char_budget: 1200 });
|
|
3480
|
+
if (r.rendered_text)
|
|
3481
|
+
projectContextSection = r.rendered_text;
|
|
3482
|
+
}
|
|
3483
|
+
catch (_) { /* swallow */ }
|
|
3484
|
+
/* alpha.59z.15 HITO 6.3 -- prefetch global learnings from
|
|
3485
|
+
* the F103 worker. Falls back silently to '' on worker
|
|
3486
|
+
* unreachable. Cache is 5 min so this is cheap. */
|
|
3487
|
+
let globalLearnings = '';
|
|
3488
|
+
try {
|
|
3489
|
+
const { buildGlobalLearningsSection } = await import('../core/learnings_client.js');
|
|
3490
|
+
globalLearnings = await buildGlobalLearningsSection({ maxTokens: 2000 });
|
|
3491
|
+
}
|
|
3492
|
+
catch (_) { /* swallow -- learnings are additive */ }
|
|
3493
|
+
/* alpha.59z.26 F101 slice 2 + slice 5 sub-slice 3 -- prefetch
|
|
3494
|
+
* MCP bridges count + persistent enabled-state so the manifest
|
|
3495
|
+
* stub surfaces yujin.mcp.* verbs only when there is at least
|
|
3496
|
+
* one ENABLED bridge (Invariant 1: zero noise when Forge is
|
|
3497
|
+
* not in MCP mode OR every bridge is disabled). Registry has
|
|
3498
|
+
* 60s cache + state.ts has its own 5s cache so the call is
|
|
3499
|
+
* cheap. */
|
|
3500
|
+
let mcpAvailable = false;
|
|
3501
|
+
try {
|
|
3502
|
+
const { getActiveBridges } = await import('../mcp/bridge_registry.js');
|
|
3503
|
+
const bridges = await getActiveBridges({ projectRoot: ctx.projectRoot });
|
|
3504
|
+
mcpAvailable = bridges.some((b) => b.enabled);
|
|
3505
|
+
}
|
|
3506
|
+
catch (_) { /* registry failure -> stay with MCP-off behaviour */ }
|
|
3507
|
+
/* alpha.59z.37 NAC-3 v3.x backend tools -- prefetch the
|
|
3508
|
+
* adopter app's nac3_backend section (task #23). Empty
|
|
3509
|
+
* string when the manifest does not declare it, when the
|
|
3510
|
+
* file is missing, or when parsing failed. */
|
|
3511
|
+
let backendToolsSection = '';
|
|
3512
|
+
try {
|
|
3513
|
+
const { loadBackendCatalogue, buildBackendToolsSection } = await import('../nac3/backend_tools.js');
|
|
3514
|
+
const parsed = await loadBackendCatalogue(ctx.projectRoot);
|
|
3515
|
+
if (parsed.catalogue && parsed.catalogue.tools.length > 0) {
|
|
3516
|
+
backendToolsSection = buildBackendToolsSection(parsed.catalogue);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
catch (_) { /* swallow */ }
|
|
3520
|
+
const chatRequest = {
|
|
3521
|
+
messages: normalized,
|
|
3522
|
+
/* alpha.59z.163 PND-027 -- system prompt now split into a
|
|
3523
|
+
* stable prefix (manifest, doctrines, design system, mode +
|
|
3524
|
+
* tier headers from buildSystemPrompt) and a dynamic suffix
|
|
3525
|
+
* (graph snapshots + workflow phase). The HTTP client tags
|
|
3526
|
+
* the prefix with cache_control:ephemeral so multi-turn
|
|
3527
|
+
* sessions hit the prompt cache instead of re-billing the
|
|
3528
|
+
* full ~9KB every iteration. Director sprint #11 measured
|
|
3529
|
+
* 1.8M tokens spent re-injecting; same applies BYOK-side. */
|
|
3530
|
+
system: buildSystemPrompt(ctx, reqMode, pilotState, {
|
|
3531
|
+
tier: brainSelection.tier,
|
|
3532
|
+
provider: brainSelection.provider,
|
|
3533
|
+
model: brainSelection.model,
|
|
3534
|
+
}, globalLearnings, mcpAvailable, backendToolsSection, lastUserText,
|
|
3535
|
+
/* PND-064 -- pizarron reachability for THIS mode. In CLI mode
|
|
3536
|
+
* the panel MCP bridge (slice B) makes it reachable via the
|
|
3537
|
+
* pizarron_create_tab tool; in API/managed it is native. */
|
|
3538
|
+
ctx.chatProvider === 'claude-code-cli'
|
|
3539
|
+
? (ctx.panelPushToken ? 'mcp-bridge' : 'none')
|
|
3540
|
+
: 'native'),
|
|
3541
|
+
systemDynamic: projectContextSection + snapshotSection + graphSection + wiringSection + workflowSection,
|
|
3542
|
+
maxTokens: 1024,
|
|
3543
|
+
tools: FORGE_TOOL_SPECS,
|
|
3544
|
+
runTool: async (name, input) => {
|
|
3545
|
+
/* alpha.59e -- loop detection. Build a signature from
|
|
3546
|
+
* the verb id + arg hash; if the last N entries are
|
|
3547
|
+
* identical, refuse this call. */
|
|
3548
|
+
const verbId = typeof input.verb_id === 'string'
|
|
3549
|
+
? String(input.verb_id)
|
|
3550
|
+
: name;
|
|
3551
|
+
const argsSig = verbId + ':' + JSON.stringify(input).slice(0, 200);
|
|
3552
|
+
callSig.push(argsSig);
|
|
3553
|
+
const recent = callSig.slice(-LOOP_REPEAT_THRESHOLD);
|
|
3554
|
+
if (recent.length === LOOP_REPEAT_THRESHOLD && recent.every((s) => s === argsSig)) {
|
|
3555
|
+
if (sseEmit) {
|
|
3556
|
+
sseEmit('error', {
|
|
3557
|
+
kind: 'loop_detected',
|
|
3558
|
+
verb_id: verbId,
|
|
3559
|
+
repeats: LOOP_REPEAT_THRESHOLD,
|
|
3560
|
+
message: 'Mismo verb invocado ' + LOOP_REPEAT_THRESHOLD + 'x consecutivo. Aborto el turn.',
|
|
3561
|
+
});
|
|
3562
|
+
}
|
|
3563
|
+
return {
|
|
3564
|
+
result: {
|
|
3565
|
+
error: 'loop_detected',
|
|
3566
|
+
verb_id: verbId,
|
|
3567
|
+
repeats: LOOP_REPEAT_THRESHOLD,
|
|
3568
|
+
hint: 'Probablemente el LLM esta llamando el mismo verbo con los mismos args en bucle. Reformula el prompt o ejecuta el paso manualmente.',
|
|
3569
|
+
},
|
|
3570
|
+
is_error: true,
|
|
3571
|
+
};
|
|
3572
|
+
}
|
|
3573
|
+
/* alpha.59e -- emit tool_round_start for the panel
|
|
3574
|
+
* progress bar. verb_hint = a short string the user
|
|
3575
|
+
* sees ("Leyendo seccion X", "Ejecutando git status",
|
|
3576
|
+
* etc) -- derived from verb_id + input. */
|
|
3577
|
+
let verbHint = verbId;
|
|
3578
|
+
if (verbId === 'yujin.shell.exec' && typeof input.command === 'string') {
|
|
3579
|
+
verbHint = 'shell: ' + String(input.command).slice(0, 60);
|
|
3580
|
+
}
|
|
3581
|
+
else if (verbId === 'yujin.reader.next-block') {
|
|
3582
|
+
verbHint = 'leyendo siguiente bloque';
|
|
3583
|
+
}
|
|
3584
|
+
else if (verbId.startsWith('yujin.reader.')) {
|
|
3585
|
+
verbHint = verbId.replace('yujin.reader.', 'reader/');
|
|
3586
|
+
}
|
|
3587
|
+
else if (verbId.startsWith('yujin.fs.')) {
|
|
3588
|
+
verbHint = verbId.replace('yujin.fs.', 'fs/');
|
|
3589
|
+
}
|
|
3590
|
+
else if (verbId.startsWith('yujin.pizarron.')) {
|
|
3591
|
+
verbHint = verbId.replace('yujin.pizarron.', 'pizarron/');
|
|
3592
|
+
}
|
|
3593
|
+
if (sseEmit) {
|
|
3594
|
+
const { extractVerbDetail } = await import('./claude_cli.js');
|
|
3595
|
+
sseEmit('tool_round_start', {
|
|
3596
|
+
round: callSig.length,
|
|
3597
|
+
verb_id: verbId,
|
|
3598
|
+
verb_hint: verbHint,
|
|
3599
|
+
detail: extractVerbDetail(verbId, input),
|
|
3600
|
+
});
|
|
3601
|
+
}
|
|
3602
|
+
const t0 = Date.now();
|
|
3603
|
+
/* alpha.59f -- inject streamEmit into ToolContext so
|
|
3604
|
+
* runShellExec (and future long-running verbs) can emit
|
|
3605
|
+
* progress events mid-execution. */
|
|
3606
|
+
const toolCtx = {
|
|
3607
|
+
projectRoot: ctx.projectRoot,
|
|
3608
|
+
projectSlug: ctx.projectSlug,
|
|
3609
|
+
...(sseEmit ? { streamEmit: sseEmit } : {}),
|
|
3610
|
+
...(ctx.reanchor ? { reanchor: ctx.reanchor } : {}),
|
|
3611
|
+
};
|
|
3612
|
+
const r = await runForgeTool(name, input, toolCtx);
|
|
3613
|
+
/* alpha.59i -- if the result is an approval envelope,
|
|
3614
|
+
* surface it as a dedicated SSE event so the panel
|
|
3615
|
+
* opens the modal NOW instead of waiting for the full
|
|
3616
|
+
* turn_complete payload. The envelope still flows
|
|
3617
|
+
* through the regular tool_round path so the LLM sees
|
|
3618
|
+
* it as a tool result (which is what triggers the
|
|
3619
|
+
* legacy flow: turn ends, user approves, next turn
|
|
3620
|
+
* carries the synthetic 'approved' message). */
|
|
3621
|
+
if (sseEmit && r && r.result
|
|
3622
|
+
&& typeof r.result === 'object'
|
|
3623
|
+
&& r.result
|
|
3624
|
+
.nac3_panel_approval_required === true) {
|
|
3625
|
+
sseEmit('approval_required', r.result);
|
|
3626
|
+
}
|
|
3627
|
+
if (sseEmit) {
|
|
3628
|
+
sseEmit('tool_round_done', {
|
|
3629
|
+
round: callSig.length,
|
|
3630
|
+
verb_id: verbId,
|
|
3631
|
+
is_error: r.is_error === true,
|
|
3632
|
+
duration_ms: Date.now() - t0,
|
|
3633
|
+
});
|
|
3634
|
+
}
|
|
3635
|
+
return {
|
|
3636
|
+
result: r.result,
|
|
3637
|
+
...(r.is_error ? { is_error: true } : {}),
|
|
3638
|
+
};
|
|
3639
|
+
},
|
|
3640
|
+
};
|
|
3641
|
+
let reply = await callerForChat.chat(chatRequest);
|
|
3642
|
+
/* Echo brainSelection if the underlying client did not stamp
|
|
3643
|
+
* it (legacy ctx.claude path). */
|
|
3644
|
+
if (!reply.brain) {
|
|
3645
|
+
reply.brain = brainSelection;
|
|
3646
|
+
}
|
|
3647
|
+
/* alpha.57 -- post-reply escalation. If the eco tier produced
|
|
3648
|
+
* a confused / refused / truncated reply (hallucinated error
|
|
3649
|
+
* markers, dodge phrases, schema errors, length-vs-complexity
|
|
3650
|
+
* mismatch), re-issue the same turn at frontier and show the
|
|
3651
|
+
* user THAT reply instead. Cap-aware: only escalates if
|
|
3652
|
+
* frontier still has budget. Cap 1 escalation per turn. */
|
|
3653
|
+
let escalated = false;
|
|
3654
|
+
let escalationSignals = [];
|
|
3655
|
+
if (brainSelection.tier === 'eco' && !brainOverride) {
|
|
3656
|
+
const { detectIncomplete } = await import('../core/incomplete_detector.js');
|
|
3657
|
+
const detect = detectIncomplete({
|
|
3658
|
+
user_text: lastUserText,
|
|
3659
|
+
reply_text: reply.text,
|
|
3660
|
+
tool_rounds: reply.toolRounds,
|
|
3661
|
+
});
|
|
3662
|
+
if (detect.incomplete) {
|
|
3663
|
+
const { checkCap } = await import('../core/budget.js');
|
|
3664
|
+
const capCheck = await checkCap('frontier');
|
|
3665
|
+
if (capCheck.allowed) {
|
|
3666
|
+
const frontierSel = await selectBrainForTurn({
|
|
3667
|
+
user_override: 'frontier',
|
|
3668
|
+
caller_hint: 'escalation_after_eco[' + detect.signals.join(',').slice(0, 80) + ']',
|
|
3669
|
+
user_text: lastUserText,
|
|
3670
|
+
history: historyForGate,
|
|
3671
|
+
previous_tier: previousTier,
|
|
3672
|
+
previous_doc_loaded: previousDocLoaded,
|
|
3673
|
+
workflow_phase: workflowPhase,
|
|
3674
|
+
});
|
|
3675
|
+
/* PND-055 (CR-D11) -- en modo claude-code-cli el contrato
|
|
3676
|
+
* es "nunca facturar Console ni usar BYOK": cada turno va
|
|
3677
|
+
* por el subproceso `claude -p` (suscripcion). El escalado
|
|
3678
|
+
* NO debe construir un cliente unificado (Gemini/Anthropic
|
|
3679
|
+
* API), que facturaria por fuera de la suscripcion. Se
|
|
3680
|
+
* escala igual -- pero a traves de ctx.claude (el mismo
|
|
3681
|
+
* subproceso), preservando la funcionalidad de escalado
|
|
3682
|
+
* sin romper el modelo de billing. */
|
|
3683
|
+
const frontierClient = ctx.chatProvider === 'claude-code-cli'
|
|
3684
|
+
? null
|
|
3685
|
+
: makeUnifiedClient(frontierSel);
|
|
3686
|
+
const frontierCaller = frontierClient
|
|
3687
|
+
? frontierClient
|
|
3688
|
+
: {
|
|
3689
|
+
chat: async (r) => {
|
|
3690
|
+
const claudeReply = await ctx.claude.chat(r);
|
|
3691
|
+
return { ...claudeReply, brain: { ...frontierSel, runtime_wired: false } };
|
|
3692
|
+
},
|
|
3693
|
+
};
|
|
3694
|
+
try {
|
|
3695
|
+
/* alpha.58 -- rebuild system prompt for the frontier
|
|
3696
|
+
* call so the tier header reflects the actual model
|
|
3697
|
+
* serving the turn. */
|
|
3698
|
+
const frontierRequest = {
|
|
3699
|
+
...chatRequest,
|
|
3700
|
+
system: buildSystemPrompt(ctx, reqMode, pilotState, {
|
|
3701
|
+
tier: frontierSel.tier,
|
|
3702
|
+
provider: frontierSel.provider,
|
|
3703
|
+
model: frontierSel.model,
|
|
3704
|
+
}, globalLearnings, mcpAvailable, backendToolsSection, lastUserText, ctx.chatProvider === 'claude-code-cli'
|
|
3705
|
+
? (ctx.panelPushToken ? 'mcp-bridge' : 'none')
|
|
3706
|
+
: 'native'),
|
|
3707
|
+
};
|
|
3708
|
+
const frontierReply = await frontierCaller.chat(frontierRequest);
|
|
3709
|
+
reply = frontierReply;
|
|
3710
|
+
escalated = true;
|
|
3711
|
+
escalationSignals = detect.signals;
|
|
3712
|
+
}
|
|
3713
|
+
catch (escErr) {
|
|
3714
|
+
/* Escalation failure -> stick with the eco reply.
|
|
3715
|
+
* Log + continue. */
|
|
3716
|
+
const msg = escErr instanceof Error ? escErr.message : String(escErr);
|
|
3717
|
+
escalationSignals = [...detect.signals, 'escalation_failed[' + msg.slice(0, 60) + ']'];
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
else {
|
|
3721
|
+
escalationSignals = [...detect.signals, 'escalation_blocked_by_cap'];
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
1508
3725
|
// Append the latest user message + the assistant's reply to
|
|
1509
3726
|
// the persistent transcript. Older messages may already be
|
|
1510
3727
|
// present from previous turns; we only need to capture the
|
|
@@ -1519,7 +3736,36 @@ async function handleChat(req, res, ctx) {
|
|
|
1519
3736
|
ctx.store.append(lastUser);
|
|
1520
3737
|
}
|
|
1521
3738
|
}
|
|
1522
|
-
|
|
3739
|
+
/* alpha.56 -- tag the assistant turn with the tier that
|
|
3740
|
+
* served it. The next turn's reasoning_gate reads this via
|
|
3741
|
+
* lastAssistant.brain_tier to apply sticky inertia (a
|
|
3742
|
+
* follow-up to a frontier turn stays in frontier unless the
|
|
3743
|
+
* new user message has strong eco signals). Also detect
|
|
3744
|
+
* whether the turn loaded a document (reader.* / fs.read-*)
|
|
3745
|
+
* so the next router call routes to frontier even on a
|
|
3746
|
+
* short follow-up like "Medium, Python" (because the user
|
|
3747
|
+
* is about to ask the model to reason over the doc). */
|
|
3748
|
+
const docLoaded = reply.toolRounds.some((r) => {
|
|
3749
|
+
const input = r.input;
|
|
3750
|
+
const vid = (input && typeof input === 'object' && 'verb_id' in input)
|
|
3751
|
+
? input.verb_id
|
|
3752
|
+
: undefined;
|
|
3753
|
+
const v = typeof vid === 'string' ? vid : '';
|
|
3754
|
+
return (v === 'yujin.fs.read-file' ||
|
|
3755
|
+
v === 'yujin.fs.read-project-file' ||
|
|
3756
|
+
v.startsWith('yujin.reader.'));
|
|
3757
|
+
});
|
|
3758
|
+
/* alpha.57 -- when the turn escalated, the persisted tier is
|
|
3759
|
+
* the FINAL one (frontier), not the eco initial one. This
|
|
3760
|
+
* way the next turn's sticky inertia + previous_tier check
|
|
3761
|
+
* see the canonical decision. */
|
|
3762
|
+
const finalTier = escalated ? 'frontier' : brainSelection.tier;
|
|
3763
|
+
ctx.store.append({
|
|
3764
|
+
role: 'assistant',
|
|
3765
|
+
content: reply.text,
|
|
3766
|
+
brain_tier: finalTier,
|
|
3767
|
+
...(docLoaded ? { doc_loaded: true } : {}),
|
|
3768
|
+
});
|
|
1523
3769
|
void ctx.store.flush();
|
|
1524
3770
|
/* Pilot first-run auto-complete (SQ 0.10). If Pilot is
|
|
1525
3771
|
not yet completed, decide based on the user's first
|
|
@@ -1533,297 +3779,699 @@ async function handleChat(req, res, ctx) {
|
|
|
1533
3779
|
catch { /* non-fatal */ }
|
|
1534
3780
|
}
|
|
1535
3781
|
}
|
|
1536
|
-
|
|
3782
|
+
/* alpha.59z.186 PND-020 Phase 2b -- LLM-claim post-turn hook.
|
|
3783
|
+
* Scan the reply for import statements in fenced code blocks
|
|
3784
|
+
* + upsert the libraries the LLM mentions but the local graph
|
|
3785
|
+
* doesn't know yet (confidence='claimed', weight=0.3 so they
|
|
3786
|
+
* don't dominate ranking until the user promotes them).
|
|
3787
|
+
* Fire-and-forget; never throws. */
|
|
3788
|
+
if (ctx.projectSlug) {
|
|
3789
|
+
const slug = ctx.projectSlug;
|
|
3790
|
+
void (async () => {
|
|
3791
|
+
try {
|
|
3792
|
+
const { ingestClaimsFromReply } = await import('../semantic_graph/llm_claim.js');
|
|
3793
|
+
await ingestClaimsFromReply(slug, reply.text);
|
|
3794
|
+
}
|
|
3795
|
+
catch { /* swallow */ }
|
|
3796
|
+
})();
|
|
3797
|
+
}
|
|
3798
|
+
/* alpha.59z.188 PND-036 + PND-037 -- post-reply quality
|
|
3799
|
+
* detectors. Two separate failure modes:
|
|
3800
|
+
* - "voy a configurar X" without any tool_use (completion
|
|
3801
|
+
* hallucination).
|
|
3802
|
+
* - "no puedo X" when the user asked for X AND a verb exists
|
|
3803
|
+
* that does X (unwarranted refusal).
|
|
3804
|
+
* Both trigger a one-shot next-turn frontier hint so the
|
|
3805
|
+
* router escalates the conversation. Pure heuristics, swallow
|
|
3806
|
+
* all errors. */
|
|
3807
|
+
try {
|
|
3808
|
+
const { checkCompletionHallucination } = await import('./completion_hallucination_detector.js');
|
|
3809
|
+
const { checkUnwarrantedRefusal } = await import('./refusal_detector.js');
|
|
3810
|
+
const hCheck = checkCompletionHallucination(reply.text, reply.toolRounds.length);
|
|
3811
|
+
/* Pull the most recent user message text for the refusal
|
|
3812
|
+
* detector. */
|
|
3813
|
+
const lastUser = [...normalized].reverse().find((m) => m.role === 'user');
|
|
3814
|
+
const lastUserContent = lastUser?.content;
|
|
3815
|
+
const lastUserText = typeof lastUserContent === 'string'
|
|
3816
|
+
? lastUserContent
|
|
3817
|
+
: Array.isArray(lastUserContent)
|
|
3818
|
+
? lastUserContent
|
|
3819
|
+
.filter((b) => !!b && typeof b === 'object')
|
|
3820
|
+
.filter((b) => b.type === 'text')
|
|
3821
|
+
.map((b) => b.text ?? '')
|
|
3822
|
+
.join(' ')
|
|
3823
|
+
: '';
|
|
3824
|
+
const rCheck = checkUnwarrantedRefusal(reply.text, lastUserText);
|
|
3825
|
+
if (hCheck.is_hallucinated || rCheck.is_unwarranted_refusal || hCheck.is_confused) {
|
|
3826
|
+
void (async () => {
|
|
3827
|
+
try {
|
|
3828
|
+
const path = await import('node:path');
|
|
3829
|
+
const os = await import('node:os');
|
|
3830
|
+
const fs = await import('node:fs/promises');
|
|
3831
|
+
const dir = process.env['YF_CONFIG_DIR']
|
|
3832
|
+
|| (process.env['HOME'] || process.env['USERPROFILE'] || os.homedir() + '/.yujin-forge');
|
|
3833
|
+
const root = dir.endsWith('.yujin-forge') ? dir : path.join(dir, '.yujin-forge');
|
|
3834
|
+
await fs.mkdir(root, { recursive: true });
|
|
3835
|
+
const kind = hCheck.is_hallucinated ? 'completion_hallucination'
|
|
3836
|
+
: rCheck.is_unwarranted_refusal ? 'unwarranted_refusal'
|
|
3837
|
+
: 'confusion_signal';
|
|
3838
|
+
const line = JSON.stringify({
|
|
3839
|
+
ts: new Date().toISOString(), kind,
|
|
3840
|
+
matched_phrase: hCheck.is_hallucinated ? hCheck.matched_phrase : rCheck.matched_refusal,
|
|
3841
|
+
topic: rCheck.topic || null,
|
|
3842
|
+
available_verbs: rCheck.available_verbs,
|
|
3843
|
+
reply_chars: reply.text.length,
|
|
3844
|
+
tier_served: reply.brain?.tier ?? brainSelection.tier,
|
|
3845
|
+
}) + '\n';
|
|
3846
|
+
await fs.appendFile(path.join(root, 'hallucinations.jsonl'), line, 'utf-8');
|
|
3847
|
+
await fs.writeFile(path.join(root, 'next_turn_frontier_hint.json'), JSON.stringify({
|
|
3848
|
+
reason: kind,
|
|
3849
|
+
at: new Date().toISOString(),
|
|
3850
|
+
matched: hCheck.is_hallucinated ? hCheck.matched_phrase : rCheck.matched_refusal,
|
|
3851
|
+
topic: rCheck.topic || undefined,
|
|
3852
|
+
}, null, 2), 'utf-8');
|
|
3853
|
+
}
|
|
3854
|
+
catch { /* swallow */ }
|
|
3855
|
+
})();
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
catch { /* swallow */ }
|
|
3859
|
+
/* alpha.59e -- build the response payload once, then either
|
|
3860
|
+
* stream it as a turn_complete SSE event OR send the legacy
|
|
3861
|
+
* JSON response. */
|
|
3862
|
+
const responsePayload = {
|
|
1537
3863
|
ok: true,
|
|
1538
3864
|
message: { role: 'assistant', text: reply.text },
|
|
1539
3865
|
tokens: { in: reply.tokensIn, out: reply.tokensOut },
|
|
1540
3866
|
model: reply.model,
|
|
3867
|
+
/* alpha.43 -- manifest version stamp so the panel can detect
|
|
3868
|
+
drift after a server reboot + force-refetch its cached
|
|
3869
|
+
manifest in the background. */
|
|
3870
|
+
nac3_manifest_version: nac3Manifest().version,
|
|
3871
|
+
/* alpha.51 + alpha.57 -- which brain tier served this turn.
|
|
3872
|
+
* Panel can show a small badge so the user knows whether
|
|
3873
|
+
* the answer came from eco or frontier. Keys redacted
|
|
3874
|
+
* server-side. When the turn escalated from eco to frontier
|
|
3875
|
+
* (alpha.57 post-reply detector), `escalated` is true and
|
|
3876
|
+
* `escalation_signals` carries the detector verdict for
|
|
3877
|
+
* audit. The final reply shown to the user is the frontier
|
|
3878
|
+
* one; only it gets persisted to the store. */
|
|
3879
|
+
brain_routing: {
|
|
3880
|
+
tier: (reply.brain?.tier) || brainSelection.tier,
|
|
3881
|
+
provider: (reply.brain?.provider) || brainSelection.provider,
|
|
3882
|
+
model: (reply.brain?.model) || brainSelection.model,
|
|
3883
|
+
runtime_wired: (reply.brain?.runtime_wired) ?? brainSelection.runtime_wired,
|
|
3884
|
+
reason: (reply.brain?.reason) || brainSelection.reason,
|
|
3885
|
+
workflow_phase: workflowPhase ?? null,
|
|
3886
|
+
escalated,
|
|
3887
|
+
escalation_signals: escalationSignals.length > 0 ? escalationSignals : undefined,
|
|
3888
|
+
},
|
|
1541
3889
|
/* Slice 4: surface the audit trail so the panel can render
|
|
1542
3890
|
the action trace (which tools Claude called + with what
|
|
1543
|
-
args + what came back). Empty array when no tools fired.
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
3891
|
+
args + what came back). Empty array when no tools fired.
|
|
3892
|
+
alpha.35 fix: include the full `result` object so the
|
|
3893
|
+
panel can intercept tools like forge_show_in_pizarron
|
|
3894
|
+
(which needs the item_id + kind + content). The Claude
|
|
3895
|
+
loop already caps result size; we further cap the JSON
|
|
3896
|
+
here at 200KB defensive to keep the HTTP response from
|
|
3897
|
+
blowing up. */
|
|
3898
|
+
tool_rounds: reply.toolRounds.map((r) => {
|
|
3899
|
+
const display = typeof r.result === 'object' && r.result !== null && 'display' in r.result
|
|
1548
3900
|
? r.result.display
|
|
1549
|
-
: undefined
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
3901
|
+
: undefined;
|
|
3902
|
+
let result = r.result;
|
|
3903
|
+
try {
|
|
3904
|
+
const serialised = JSON.stringify(result ?? null);
|
|
3905
|
+
if (serialised.length > 200_000) {
|
|
3906
|
+
result = { _truncated: true, _bytes: serialised.length };
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
catch {
|
|
3910
|
+
result = { _unserialisable: true };
|
|
3911
|
+
}
|
|
3912
|
+
return {
|
|
3913
|
+
tool: r.tool,
|
|
3914
|
+
input: r.input,
|
|
3915
|
+
display,
|
|
3916
|
+
is_error: r.is_error ?? false,
|
|
3917
|
+
result,
|
|
3918
|
+
};
|
|
3919
|
+
}),
|
|
3920
|
+
};
|
|
3921
|
+
if (sseEmit) {
|
|
3922
|
+
sseEmit('turn_complete', responsePayload);
|
|
3923
|
+
res.end();
|
|
3924
|
+
}
|
|
3925
|
+
else {
|
|
3926
|
+
sendJson(res, 200, responsePayload);
|
|
3927
|
+
}
|
|
1553
3928
|
}
|
|
1554
3929
|
catch (err) {
|
|
1555
3930
|
if (err instanceof ConfigurationError) {
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
3931
|
+
if (sseEmit) {
|
|
3932
|
+
sseEmit('error', { kind: 'no_api_key', message: err.message });
|
|
3933
|
+
res.end();
|
|
3934
|
+
}
|
|
3935
|
+
else {
|
|
3936
|
+
sendJson(res, 503, {
|
|
3937
|
+
ok: false,
|
|
3938
|
+
code: 'no_api_key',
|
|
3939
|
+
error: err.message,
|
|
3940
|
+
});
|
|
3941
|
+
}
|
|
1561
3942
|
return;
|
|
1562
3943
|
}
|
|
1563
3944
|
if (err instanceof ClaudeApiError) {
|
|
3945
|
+
if (sseEmit) {
|
|
3946
|
+
sseEmit('error', { kind: 'claude_api_error', message: err.message });
|
|
3947
|
+
res.end();
|
|
3948
|
+
}
|
|
3949
|
+
else {
|
|
3950
|
+
sendJson(res, 502, {
|
|
3951
|
+
ok: false,
|
|
3952
|
+
code: 'claude_api_error',
|
|
3953
|
+
error: err.message,
|
|
3954
|
+
});
|
|
3955
|
+
}
|
|
3956
|
+
return;
|
|
3957
|
+
}
|
|
3958
|
+
if (sseEmit) {
|
|
3959
|
+
sseEmit('error', { kind: 'internal', message: err instanceof Error ? err.message : String(err) });
|
|
3960
|
+
res.end();
|
|
3961
|
+
}
|
|
3962
|
+
else {
|
|
1564
3963
|
sendJson(res, 502, {
|
|
1565
3964
|
ok: false,
|
|
1566
|
-
|
|
1567
|
-
error: err.message,
|
|
3965
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1568
3966
|
});
|
|
1569
|
-
return;
|
|
1570
3967
|
}
|
|
1571
|
-
sendJson(res, 502, {
|
|
1572
|
-
ok: false,
|
|
1573
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1574
|
-
});
|
|
1575
3968
|
}
|
|
1576
3969
|
}
|
|
1577
|
-
function buildSystemPrompt(ctx, mode = 'didactico', pilotState = {
|
|
3970
|
+
export function buildSystemPrompt(ctx, mode = 'didactico', pilotState = {
|
|
1578
3971
|
pilot_completed: true, target_pending: false, mode_pending: false,
|
|
1579
|
-
}
|
|
3972
|
+
},
|
|
3973
|
+
/** alpha.58 -- which brain tier + provider + model is serving
|
|
3974
|
+
* this turn. Allows the LLM (especially eco-tier ones like
|
|
3975
|
+
* Gemini Flash Lite) to know its own role and be more
|
|
3976
|
+
* cautious / defer to nac3_invoke when in doubt. */
|
|
3977
|
+
brain,
|
|
3978
|
+
/** alpha.59z.15 HITO 6.3 -- the GLOBAL LEARNINGS section,
|
|
3979
|
+
* pre-fetched async by the caller via
|
|
3980
|
+
* buildGlobalLearningsSection() from learnings_client.ts.
|
|
3981
|
+
* Empty string -> no learnings (worker unreachable or no
|
|
3982
|
+
* active lessons). buildSystemPrompt stays sync; the caller
|
|
3983
|
+
* awaits the fetch before invoking us. */
|
|
3984
|
+
globalLearningsSection = '',
|
|
3985
|
+
/** alpha.59z.26 F101 slice 2 -- whether MCP bridges are
|
|
3986
|
+
* discovered in the current session. When false (default),
|
|
3987
|
+
* the manifest stub filters out every yujin.mcp.* verb so
|
|
3988
|
+
* the LLM never sees them (Invariant 1: Forge without MCP
|
|
3989
|
+
* behaves identically to pre-F101). When true, the LLM gets
|
|
3990
|
+
* the MCP-meta verbs (list-bridges, show-tools, toggle,
|
|
3991
|
+
* set-creds, discover-tools) in its stub. The caller
|
|
3992
|
+
* prefetches via getActiveBridges() before invoking us. */
|
|
3993
|
+
mcpAvailable = false,
|
|
3994
|
+
/** alpha.59z.37 NAC-3 v3.x backend tools (task #23) -- pre-built
|
|
3995
|
+
* app.* verbs section, ready to splice into the prompt. The
|
|
3996
|
+
* caller prefetches via loadBackendCatalogue() +
|
|
3997
|
+
* buildBackendToolsSection() before invoking us so the prompt
|
|
3998
|
+
* build stays sync. Empty string -> no section appears
|
|
3999
|
+
* (Invariant: identical prompt to v3.0 when adopter does not
|
|
4000
|
+
* declare nac3_backend). Slice 1 only surfaces the verbs in
|
|
4001
|
+
* the stub; slice 2 wires dispatch + auth + audit. */
|
|
4002
|
+
backendToolsSection = '',
|
|
4003
|
+
/** alpha.59z.172 PND-022 -- first user message of the active turn.
|
|
4004
|
+
* When the pilot warmup would fire and the message is detected
|
|
4005
|
+
* out-of-scope (music, poetry, image, abstract explanation,
|
|
4006
|
+
* casual chat), welcomePromptSuffix returns empty so the user
|
|
4007
|
+
* is not asked target/mode for content unrelated to software. */
|
|
4008
|
+
firstUserMessage = '',
|
|
4009
|
+
/** PND-064 -- how the pizarron is reachable in the ACTIVE mode:
|
|
4010
|
+
* - 'native' : anthropic-api / yujin_managed. The server
|
|
4011
|
+
* runs runTool and the panel intercepts
|
|
4012
|
+
* nac3_panel_dispatch. Use yujin.pizarron.*.
|
|
4013
|
+
* - 'mcp-bridge' : claude-code-cli WITH the panel MCP bridge
|
|
4014
|
+
* (slice B). The subprocess reaches the
|
|
4015
|
+
* pizarron via the pizarron_create_tab MCP
|
|
4016
|
+
* tool, NOT the yujin.pizarron.* verbs.
|
|
4017
|
+
* - 'none' : claude-code-cli WITHOUT a working bridge.
|
|
4018
|
+
* The pizarron is unreachable; the model must
|
|
4019
|
+
* not claim it created a tab or verified the
|
|
4020
|
+
* panel. */
|
|
4021
|
+
panelMode = 'native') {
|
|
4022
|
+
/* alpha.58 -- tier awareness header. When the request is
|
|
4023
|
+
* served by an eco-tier model (Gemini Flash Lite, Haiku),
|
|
4024
|
+
* the model is told its role explicitly so it can be more
|
|
4025
|
+
* conservative + defer to nac3_invoke instead of guessing. */
|
|
4026
|
+
const tierHeader = brain
|
|
4027
|
+
? (brain.tier === 'eco'
|
|
4028
|
+
? '[BRAIN TIER: eco (' + brain.provider + '/' + brain.model + ')]. ' +
|
|
4029
|
+
'You are the cheap fast tier. The router will ESCALATE you to a frontier model if your reply looks confused, refused, or truncated. Therefore: ' +
|
|
4030
|
+
'when a turn requires multi-step reasoning, multi-file changes, code generation, architecture design, or long-form drafting, DO NOT guess -- ' +
|
|
4031
|
+
'reply with a short clarifying question OR invoke nac3_invoke to defer to a structured handler. ' +
|
|
4032
|
+
'For simple verb invocations (open a tab, list pipelines, show budget, short greetings, factual lookups), proceed normally and do the work.'
|
|
4033
|
+
: '[BRAIN TIER: frontier (' + brain.provider + '/' + brain.model + ')]. ' +
|
|
4034
|
+
'You are the high-capability tier. The user is paying for reasoning quality. ' +
|
|
4035
|
+
'When you receive a turn the eco tier could not handle (post-escalation), give the well-reasoned answer the user expected. ' +
|
|
4036
|
+
'You have full latitude for design, code generation, multi-step plans.')
|
|
4037
|
+
: '';
|
|
1580
4038
|
return [
|
|
1581
|
-
'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.
|
|
4039
|
+
'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.',
|
|
1582
4040
|
'',
|
|
4041
|
+
'=================================================================',
|
|
4042
|
+
'IDENTITY INVARIANT (alpha.59z.189 PND-043 -- non-negotiable)',
|
|
4043
|
+
'=================================================================',
|
|
4044
|
+
'You are Yujin Forge running inside a chat panel. You are NOT',
|
|
4045
|
+
'Claude Code, NOT a regular Claude instance, NOT ChatGPT, NOT',
|
|
4046
|
+
'Gemini, NOT Cursor / Copilot. Saying "soy Claude Code" or',
|
|
4047
|
+
'"estoy corriendo como una instancia regular de Claude" or "no',
|
|
4048
|
+
'tengo los verbos NAC-3 enchufados" is FACTUALLY WRONG -- your',
|
|
4049
|
+
'runtime DOES have the NAC-3 manifest wired (yujin.fs.*,',
|
|
4050
|
+
'yujin.git.*, yujin.graph.*, yujin.wiring.*, yujin.pending.*,',
|
|
4051
|
+
'yujin.doctrine.*, yujin.nac3.*, plus the rest in the manifest',
|
|
4052
|
+
'stub below). When in doubt about what is available, call',
|
|
4053
|
+
'yujin.manifest.read or yujin.nac3.discover-schemas before',
|
|
4054
|
+
'claiming "no tengo X". Falsely claiming otherwise destroys',
|
|
4055
|
+
'user trust + triggers a quality detector that escalates the',
|
|
4056
|
+
'next turn to a stronger model.',
|
|
4057
|
+
'',
|
|
4058
|
+
...(tierHeader ? [tierHeader, ''] : []),
|
|
1583
4059
|
'LEMA YUJIN: "La tecnologia desaparece. El sistema aprende de vos, no vos del sistema." Toda decision se evalua contra esto.',
|
|
1584
4060
|
'',
|
|
1585
4061
|
'=================================================================',
|
|
1586
|
-
'CORE PRINCIPLES (
|
|
4062
|
+
'CORE PRINCIPLES (docs/SQ.md)',
|
|
1587
4063
|
'=================================================================',
|
|
1588
4064
|
'',
|
|
1589
|
-
'1
|
|
1590
|
-
'
|
|
1591
|
-
'
|
|
1592
|
-
'
|
|
1593
|
-
'
|
|
1594
|
-
' "este flujo cubre el caso de uso?"). Para todo lo mecanico,',
|
|
1595
|
-
' YO atrapo / YO arreglo.',
|
|
4065
|
+
'SQ 1 -- USER TIME IS SACRED. Catch trivial bugs (syntax, 404s,',
|
|
4066
|
+
' missing imports, console errors, broken modals) BEFORE the user.',
|
|
4067
|
+
' User judges only high-complexity decisions ("does this UX feel',
|
|
4068
|
+
' intuitive?", "does this flow cover the use case?"). Everything',
|
|
4069
|
+
' mechanical: you catch, you fix.',
|
|
1596
4070
|
'',
|
|
1597
|
-
'2
|
|
1598
|
-
'
|
|
1599
|
-
' Nunca "lo arreglo despues".',
|
|
4071
|
+
'SQ 2 -- ZERO TECH DEBT from commit 0. Bugs found in-flight get a',
|
|
4072
|
+
' root fix + regression spec in the SAME session. No "later".',
|
|
1600
4073
|
'',
|
|
1601
|
-
'3
|
|
1602
|
-
'
|
|
1603
|
-
' / .py / .php / .sql / .json fuera de _i18n.',
|
|
4074
|
+
'SQ 3 -- ASCII-PURE in product code. No accents, emojis, em-dashes,',
|
|
4075
|
+
' no chars > 0x7F in .ts/.tsx/.py/.php/.sql/.json (outside _i18n).',
|
|
1604
4076
|
'',
|
|
1605
|
-
'4
|
|
1606
|
-
'
|
|
1607
|
-
'
|
|
4077
|
+
'SQ 4 -- DOCS IN SYNC. Every commit cross-links the RFP / Arch /',
|
|
4078
|
+
' Solution Design / User Manual sections it affects. Doc/code',
|
|
4079
|
+
' drift = bug.',
|
|
1608
4080
|
'',
|
|
1609
|
-
'5
|
|
1610
|
-
'
|
|
1611
|
-
' abre con una feature, no toques codigo no relacionado.',
|
|
4081
|
+
'SQ 5 -- MATCH THE SCOPE. Bug ask = fix that bug only, no refactor',
|
|
4082
|
+
' "de paso". Feature ask = no unrelated code touched.',
|
|
1612
4083
|
'',
|
|
1613
|
-
'6
|
|
1614
|
-
'
|
|
1615
|
-
'
|
|
4084
|
+
'SQ 6 -- REPLY IN USER LANGUAGE. Default Spanish if unclear. **WHEN',
|
|
4085
|
+
' IN SPANISH USE CORRECT ACCENTS** (a e i o u, n, ?, !) -- TTS',
|
|
4086
|
+
' needs them. Short conversational replies. One clarifying question',
|
|
4087
|
+
' at a time, never avalanches.',
|
|
1616
4088
|
'',
|
|
1617
|
-
'7
|
|
1618
|
-
'
|
|
1619
|
-
'
|
|
1620
|
-
'
|
|
1621
|
-
'
|
|
1622
|
-
'
|
|
1623
|
-
'
|
|
1624
|
-
' AP-1/AP-2/AP-3/AP-4 registrado, refuse el commit y enlaza la',
|
|
1625
|
-
' entrada. Esta regla nacio el 2026-05-29 despues de 3 regresiones',
|
|
1626
|
-
' BYOK seguidas (G-2026-05-29 a/b/c en GOTCHAS).',
|
|
4089
|
+
'SQ 7 -- PRODUCER/CONSUMER SYMMETRY (MANDATORY, SQ Sec 14). Before',
|
|
4090
|
+
' ANY commit that touches a shared structure (vault slot, JSON',
|
|
4091
|
+
' key, env var, tool name, type id, endpoint contract, manifest',
|
|
4092
|
+
' field) -- run forge_audit_consumers(<concept>). If readers are',
|
|
4093
|
+
' out of sync, FIX in the SAME commit. Do NOT ask user to interpret',
|
|
4094
|
+
' the audit. Read docs/GOTCHAS.md at boot; when a change matches a',
|
|
4095
|
+
' registered AP-1/2/3/4 pattern, refuse the commit + link the entry.',
|
|
1627
4096
|
'',
|
|
1628
|
-
'
|
|
1629
|
-
'
|
|
1630
|
-
'
|
|
4097
|
+
'SQ 8 -- AUTONOMY / TEST THINGS YOURSELF (MANDATORY, SQ Sec 15).',
|
|
4098
|
+
' Before asking "could you run X and paste the output?" STOP and',
|
|
4099
|
+
' rewrite as "I will run X, capture, diagnose". You have tools for',
|
|
4100
|
+
' CLI, file reads, log reads, HTTP, JSON parse. Use them. User only',
|
|
4101
|
+
' gets involved when test needs: real hardware (mic), subjective',
|
|
4102
|
+
' judgment ("looks good?"), $$/scope decision, or user-only creds.',
|
|
4103
|
+
' Otherwise: diagnose yourself + report the diagnosis, not the path.',
|
|
1631
4104
|
'',
|
|
1632
|
-
'
|
|
1633
|
-
'
|
|
1634
|
-
'mid-phase, you respect that but mention which phase they skipped',
|
|
1635
|
-
'and what risk that carries.',
|
|
4105
|
+
'SQ 9 -- VERIFY FACTUAL CLAIMS ABOUT THE WORKSPACE (MANDATORY,',
|
|
4106
|
+
' added 2026-06-06 alpha.59z.158, extended alpha.59z.174 PND-023).',
|
|
1636
4107
|
'',
|
|
1637
|
-
'
|
|
1638
|
-
'
|
|
1639
|
-
'
|
|
1640
|
-
'
|
|
1641
|
-
'
|
|
1642
|
-
'
|
|
1643
|
-
'
|
|
4108
|
+
' 9a -- User-stated facts. When the user states something factual',
|
|
4109
|
+
' about the current workspace -- "no tiene X", "falta Y",',
|
|
4110
|
+
' "ya existe Z", "el bug esta en archivo W" -- DO NOT take it at',
|
|
4111
|
+
' face value if it would lead you to write code, add files, or',
|
|
4112
|
+
' recommend an action. Verify with native verbs FIRST:',
|
|
4113
|
+
' - "no tiene vault" -> yujin.fs.find({glob:"**/vault*"}) or fs.list-files',
|
|
4114
|
+
' - "falta tests" -> yujin.fs.list-files({subdir:"tests"})',
|
|
4115
|
+
' - "el bug en X.ts" -> yujin.fs.read-file({path:"X.ts"}) first',
|
|
4116
|
+
' - "ya existe Z" -> yujin.fs.find then proceed only if confirmed',
|
|
4117
|
+
' If the verification contradicts the user, REPORT the contradiction',
|
|
4118
|
+
' in chat ("Veo que ya hay server/vault.ts -- queres reemplazarlo,',
|
|
4119
|
+
' extenderlo, o algo mas especifico?"). Never proceed with an action',
|
|
4120
|
+
' premised on an unverified factual claim about the workspace.',
|
|
1644
4121
|
'',
|
|
1645
|
-
'
|
|
1646
|
-
'
|
|
1647
|
-
'
|
|
1648
|
-
'
|
|
1649
|
-
'
|
|
1650
|
-
'
|
|
1651
|
-
'
|
|
1652
|
-
'
|
|
1653
|
-
'
|
|
1654
|
-
'
|
|
1655
|
-
'
|
|
1656
|
-
'
|
|
4122
|
+
' 9b -- Multi-step flow preconditions. Before PROMISING to execute',
|
|
4123
|
+
' a multi-step flow (deploy / publish / release / migrate / install /',
|
|
4124
|
+
' init / scaffold / publish-alpha), run discovery on the CRITICAL',
|
|
4125
|
+
' preconditions with real verbs. Inference + heuristic guessing are',
|
|
4126
|
+
' banned for these flows -- the user only learns the precondition',
|
|
4127
|
+
' was missing when the flow halfway crashed. Probe FIRST:',
|
|
4128
|
+
' - deploy / publish / release -> yujin.git.status (clean tree?',
|
|
4129
|
+
' detached HEAD? right branch?) + yujin.shell.exec("gh --version")',
|
|
4130
|
+
' when the flow needs gh / wrangler / eas.',
|
|
4131
|
+
' - migrate -> yujin.fs.read-file on the manifest + yujin.git.status',
|
|
4132
|
+
' so a destructive write does not bury uncommitted work.',
|
|
4133
|
+
' - install / scaffold / init -> yujin.shell.exec("node --version") +',
|
|
4134
|
+
' yujin.fs.stat({path:"package.json"}) so we do not promise to',
|
|
4135
|
+
' run a script the runtime is missing.',
|
|
4136
|
+
' - publish (npm) -> yujin.fs.read-file({path:"package.json"}) for',
|
|
4137
|
+
' the version + yujin.shell.exec("npm whoami") to confirm auth.',
|
|
4138
|
+
' REPORT the precondition findings in chat BEFORE the destructive',
|
|
4139
|
+
' step ("git status: clean / branch main / 0 ahead. gh authed. node',
|
|
4140
|
+
' 22.x detected. Procedo con el deploy."). If ANY precondition is',
|
|
4141
|
+
' missing, HALT before any destructive verb, surface what is missing',
|
|
4142
|
+
' + the exact command the user can run, and wait for confirmation.',
|
|
4143
|
+
' This is the same rule as SQ 9a (verify before claiming) lifted to',
|
|
4144
|
+
' multi-verb flows -- inference is cheap, broken flows are not.',
|
|
1657
4145
|
'',
|
|
1658
|
-
'
|
|
1659
|
-
'
|
|
1660
|
-
'
|
|
1661
|
-
'
|
|
1662
|
-
'
|
|
1663
|
-
'
|
|
1664
|
-
'
|
|
4146
|
+
' 9c -- NEVER ANNOUNCE WORK YOU DID NOT DO (alpha.59z.188 PND-036).',
|
|
4147
|
+
' Phrases like "voy a configurar X", "procedere a setup", "ya he',
|
|
4148
|
+
' procesado tu solicitud", "configurando", "estoy en el proceso",',
|
|
4149
|
+
' "iniciando", "ya esta listo / ya lo hice" -- you can ONLY use',
|
|
4150
|
+
' these when at least ONE tool_use ALREADY HAPPENED in the same',
|
|
4151
|
+
' turn that actually delivered the operation. If you have not yet',
|
|
4152
|
+
' invoked the verb that does the work, EITHER invoke it NOW',
|
|
4153
|
+
' (preferred: discover the verb via yujin.nac3.discover-schemas',
|
|
4154
|
+
' if unsure of the schema, then call it) OR rephrase as a question',
|
|
4155
|
+
' ("Tengo X verbos para esto -- queres que arranque con cual?").',
|
|
4156
|
+
' Hallucinated completions are a credibility-destroyer; the user',
|
|
4157
|
+
' finds out you did nothing the next message + loses trust in',
|
|
4158
|
+
' every subsequent claim. When in doubt, ask. When sure, invoke.',
|
|
4159
|
+
' NEVER both promise AND defer.',
|
|
1665
4160
|
'',
|
|
1666
|
-
'
|
|
1667
|
-
'
|
|
1668
|
-
'
|
|
1669
|
-
' architecture + design.',
|
|
4161
|
+
'=================================================================',
|
|
4162
|
+
'PRODUCT WORKFLOW (on-demand)',
|
|
4163
|
+
'=================================================================',
|
|
1670
4164
|
'',
|
|
1671
|
-
'
|
|
1672
|
-
'
|
|
1673
|
-
'
|
|
1674
|
-
'
|
|
1675
|
-
'
|
|
1676
|
-
' 100% structural coverage ->',
|
|
1677
|
-
' a11y check -> green-gate',
|
|
1678
|
-
' before next block',
|
|
4165
|
+
'You guide the user through 6 phases / 18 steps. For phase names',
|
|
4166
|
+
'+ step numbers + complexity tier mapping + per-step details, call',
|
|
4167
|
+
'yujin.doctrine.discover({slugs:["workflow"]}). The PROJECT WORKFLOW',
|
|
4168
|
+
'STATE block auto-injected per turn already tells you where the',
|
|
4169
|
+
'user is; the doctrine has the full 18-step reference.',
|
|
1679
4170
|
'',
|
|
1680
|
-
'
|
|
1681
|
-
'
|
|
1682
|
-
'
|
|
1683
|
-
' 17. Post-launch user manual auto-gen,',
|
|
1684
|
-
' dashboards, feedback collect',
|
|
1685
|
-
' 18. Iteration re-entry route new requirements back',
|
|
1686
|
-
' to the right phase, keep docs',
|
|
1687
|
-
' in sync',
|
|
4171
|
+
'=================================================================',
|
|
4172
|
+
'GENERATION DOCTRINE + WORKFLOW GATES (on-demand)',
|
|
4173
|
+
'=================================================================',
|
|
1688
4174
|
'',
|
|
1689
|
-
'
|
|
1690
|
-
'
|
|
1691
|
-
'
|
|
4175
|
+
'When scaffolding adopter app code (steps 11 + 14), the G-DOC',
|
|
4176
|
+
'rules (NAC3-first manifest, MCP opt-in, templates carry the',
|
|
4177
|
+
'example) get auto-injected into the workflow state block. At',
|
|
4178
|
+
'other steps, call yujin.doctrine.discover({slugs:["g-doc"]})',
|
|
4179
|
+
'if you need them.',
|
|
1692
4180
|
'',
|
|
1693
|
-
'
|
|
1694
|
-
'
|
|
1695
|
-
'
|
|
1696
|
-
'
|
|
4181
|
+
'Workflow gates G1..G8 (refuse-to-advance) + GS1..GS4 (soft',
|
|
4182
|
+
'alerts) get auto-injected when the user is near a phase',
|
|
4183
|
+
'cross-over. At other steps, call doctrine.discover',
|
|
4184
|
+
'({slugs:["gates"]}).',
|
|
1697
4185
|
'',
|
|
1698
4186
|
'=================================================================',
|
|
1699
|
-
'
|
|
4187
|
+
'NAC-3 ATTRIBUTES (on-demand)',
|
|
1700
4188
|
'=================================================================',
|
|
1701
4189
|
'',
|
|
1702
|
-
'
|
|
1703
|
-
'
|
|
1704
|
-
'
|
|
1705
|
-
'
|
|
1706
|
-
'G5: Phase V started without explicit OK on the 3 spec docs',
|
|
1707
|
-
'G6: Phase V block declared complete with red tests',
|
|
1708
|
-
'G7: Phase VI deploy attempted without all required creds',
|
|
1709
|
-
'G8: Launch announced without green smoke check post-deploy',
|
|
1710
|
-
'',
|
|
1711
|
-
'Soft gates: alert the user, propose alternative, do NOT block:',
|
|
1712
|
-
'GS1: Simple tier asking for full RFP -> do it but flag overkill',
|
|
1713
|
-
'GS2: Full tier asking to skip arch -> refuse with justification',
|
|
1714
|
-
'GS3: No compliance picked but "produccion B2B" -> propose defaults',
|
|
1715
|
-
'GS4: Low cost budget but high volumetry -> propose optimisations',
|
|
4190
|
+
'When marking up adopter UI with NAC-3 attributes (data-nac-id,',
|
|
4191
|
+
'data-nac-role, data-nac-action, data-nac-state, data-nac-target),',
|
|
4192
|
+
'call yujin.doctrine.discover({slugs:["nac3-attrs"]}) for the full',
|
|
4193
|
+
'reference + naming conventions + when-NOT-to-mark guidelines.',
|
|
1716
4194
|
'',
|
|
1717
4195
|
'=================================================================',
|
|
1718
|
-
'
|
|
4196
|
+
'YF COMMAND CATALOG (on-demand)',
|
|
1719
4197
|
'=================================================================',
|
|
1720
4198
|
'',
|
|
1721
|
-
'
|
|
4199
|
+
'When the user asks "how do I X?" (looking for the CLI command),',
|
|
4200
|
+
'call yujin.doctrine.discover({slugs:["yf-commands"]}) to fetch',
|
|
4201
|
+
'the full catalog and recommend the matching `yf <command>`. When',
|
|
4202
|
+
'the user wants the result NOW ("install Stripe webhook for me"),',
|
|
4203
|
+
'invoke the relevant NAC3 verb directly instead.',
|
|
1722
4204
|
'',
|
|
1723
|
-
'
|
|
1724
|
-
'
|
|
1725
|
-
'
|
|
1726
|
-
'- data-nac-action declarative effect when invoked: submit / open / search /',
|
|
1727
|
-
' navigate / dismiss / autopilot_toggle',
|
|
1728
|
-
'- data-nac-state lifecycle: loading / disabled / readonly / hidden /',
|
|
1729
|
-
' selected / error',
|
|
1730
|
-
'- data-nac-target secondary anchor: id of element this one controls',
|
|
4205
|
+
'=================================================================',
|
|
4206
|
+
'NAC3 INTERNAL DISPATCH (alpha.40 -- SINGLE CATALOGUE)',
|
|
4207
|
+
'=================================================================',
|
|
1731
4208
|
'',
|
|
1732
|
-
'
|
|
1733
|
-
'
|
|
4209
|
+
'Forge exposes its actions as NAC3 verbs in an internal',
|
|
4210
|
+
'manifest. There is exactly ONE tool you should reach for to',
|
|
4211
|
+
'execute an action: nac3_invoke.',
|
|
1734
4212
|
'',
|
|
1735
|
-
'
|
|
1736
|
-
'
|
|
1737
|
-
'
|
|
4213
|
+
'RULES (these override anything below):',
|
|
4214
|
+
' R1. Pick verb_id from the manifest shown at the bottom of',
|
|
4215
|
+
' this prompt. ALL the verb_ids you can invoke are listed',
|
|
4216
|
+
' -- if none matches what the user asked, REPLY in chat "No',
|
|
4217
|
+
' tengo verbo NAC3 para X. Si querés, lo agrego." Do NOT',
|
|
4218
|
+
' improvise. Do NOT pretend an action happened.',
|
|
4219
|
+
' R2. The manifest section below uses STUB MODE in lazy.',
|
|
4220
|
+
' Each line: <verb_id> [<policy><side_effects>] <label>',
|
|
4221
|
+
' args:<{required,[optional]}>. For SIMPLE verbs (single',
|
|
4222
|
+
' scalar arg, no enum, no length cap) the args_hint is',
|
|
4223
|
+
' enough; just invoke. For COMPLEX verbs (enum values,',
|
|
4224
|
+
' length limits, nested objects) call yujin.nac3.discover-',
|
|
4225
|
+
' schemas first with the verb_ids you plan to invoke -- one',
|
|
4226
|
+
' call, multiple ids, then invoke. If you invoke without',
|
|
4227
|
+
' discovery and your args fail the schema, the dispatcher',
|
|
4228
|
+
' returns the schema inline so you can retry in the next',
|
|
4229
|
+
' round (one extra round, not catastrophic).',
|
|
4230
|
+
' R3. Read each verb policy from the stub (D=destructive,',
|
|
4231
|
+
' A=approval-required, c=confirm). Verbs with D/A trigger',
|
|
4232
|
+
' R8 below (args preview + explicit confirm). NEVER skip',
|
|
4233
|
+
' R8 even when the user signaled the intention in the',
|
|
4234
|
+
' previous turn.',
|
|
4235
|
+
' R4. Inspect every nac3_invoke result. If is_error=true, tell',
|
|
4236
|
+
' the user what failed. If result has nac3_panel_dispatch',
|
|
4237
|
+
' = true, the panel ran the verb -- treat that as success',
|
|
4238
|
+
' and DO mention the user-visible effect (e.g. "abrí la',
|
|
4239
|
+
' pestaña en el pizarrón"). Never paper over an error.',
|
|
4240
|
+
' If result has nac3_panel_approval_required = true, the',
|
|
4241
|
+
' panel will show a modal -- do NOT consider the action',
|
|
4242
|
+
' done; wait for the next turn where the user reports the',
|
|
4243
|
+
' modal outcome (approved or rejected).',
|
|
4244
|
+
' R5. Past-tense ("listo", "guardado", "commiteado") is allowed',
|
|
4245
|
+
' ONLY after a successful invoke. If you have NOT invoked',
|
|
4246
|
+
' anything in this turn, use future tense ("voy a", "puedo',
|
|
4247
|
+
' intentar"). This is the anti-hallucination rule.',
|
|
4248
|
+
' R6. PREFER NATIVE VERB OVER SHELL.EXEC. When a read-only or',
|
|
4249
|
+
' workflow query can be served by a native yujin.* verb AND',
|
|
4250
|
+
' by yujin.shell.exec running an equivalent yf/git/cat/ls',
|
|
4251
|
+
' command, ALWAYS pick the native verb. The native verb is',
|
|
4252
|
+
' cheaper (no process spawn), unmodal (no R8 friction for',
|
|
4253
|
+
' read-only), and uniformly auditable. Examples:',
|
|
4254
|
+
' - project state -> yujin.workflow.state, NOT shell yf projects active',
|
|
4255
|
+
' - read manifest -> yujin.manifest.read, NOT shell cat yujin.forge.json',
|
|
4256
|
+
' - list files -> yujin.fs.list-files, NOT shell ls',
|
|
4257
|
+
' - read a file -> yujin.fs.read-file, NOT shell cat',
|
|
4258
|
+
' - branch status -> yujin.github.branch-status, NOT shell git status',
|
|
4259
|
+
' yujin.shell.exec is for ops with NO native verb equivalent:',
|
|
4260
|
+
' npm install, docker build, pytest, custom scripts, etc.',
|
|
1738
4261
|
'',
|
|
1739
|
-
'
|
|
4262
|
+
'MANIFEST:',
|
|
4263
|
+
buildManifestForPrompt({ includeMcpVerbs: mcpAvailable }).text,
|
|
4264
|
+
/* alpha.59z.37 NAC-3 v3.x backend tools (task #23 slice 1):
|
|
4265
|
+
* splice the pre-built app.* verbs section. Empty string ->
|
|
4266
|
+
* nothing appears (Invariant: identical to v3.0 when no
|
|
4267
|
+
* nac3_backend declared). */
|
|
4268
|
+
...(backendToolsSection ? [backendToolsSection] : []),
|
|
1740
4269
|
'',
|
|
4270
|
+
/* alpha.59z.15 HITO 6.3 -- inject server-managed global
|
|
4271
|
+
* learnings. Empty when worker unreachable or no active
|
|
4272
|
+
* lessons; in that case the prompt has no GLOBAL LEARNINGS
|
|
4273
|
+
* section (the user sees no degradation). */
|
|
4274
|
+
...(globalLearningsSection ? [globalLearningsSection] : []),
|
|
4275
|
+
/* Doctrine digest (dogfood 2026-06-10) -- the doctrines ARE the
|
|
4276
|
+
* code-quality guardrails, so the condensed essence of EVERY one
|
|
4277
|
+
* is injected mandatorily in every prompt (not on-demand). Full
|
|
4278
|
+
* text still on-demand via yujin.doctrine.discover. */
|
|
4279
|
+
...buildDoctrineDigestLines(),
|
|
1741
4280
|
'=================================================================',
|
|
1742
|
-
'
|
|
4281
|
+
'PIZARRON DOCTRINE (condensed; full text on-demand)',
|
|
1743
4282
|
'=================================================================',
|
|
1744
4283
|
'',
|
|
1745
|
-
'
|
|
1746
|
-
'
|
|
1747
|
-
'
|
|
1748
|
-
'
|
|
1749
|
-
'
|
|
4284
|
+
'TWO pizarron verbs, DIFFERENT meanings:',
|
|
4285
|
+
' yujin.pizarron.create-tab -- ad-hoc content YOU generated',
|
|
4286
|
+
' (table, code, doc). dom only. NO file_path. The pizarron',
|
|
4287
|
+
' is a CANVAS, not a file editor.',
|
|
4288
|
+
' yujin.pizarron.open-file -- READ a project file from disk',
|
|
4289
|
+
' AND open as editor tab. filesystem + dom. Opening does NOT',
|
|
4290
|
+
' save -- pair with yujin.fs.write-project-file to persist.',
|
|
4291
|
+
'',
|
|
4292
|
+
'CRITICAL: "abrir en pizarra" NEVER means "saved". For',
|
|
4293
|
+
'"edit X and commit": (a) open-file, (b) write-project-file,',
|
|
4294
|
+
'(c) git.commit. THREE invokes. Do NOT ask user to do (b).',
|
|
4295
|
+
'For "leeme la tabla" -> yujin.pizarron.read-tab (not reader.open).',
|
|
4296
|
+
'',
|
|
4297
|
+
'For the full pizarron doctrine with all examples, call',
|
|
4298
|
+
'yujin.doctrine.discover({slugs:["pizarron"]}).',
|
|
1750
4299
|
'',
|
|
1751
|
-
|
|
1752
|
-
'
|
|
1753
|
-
'
|
|
1754
|
-
'
|
|
1755
|
-
'
|
|
1756
|
-
'
|
|
1757
|
-
'
|
|
4300
|
+
/* Ciclo de vida de un pedido (dogfood 2026-06-10). */
|
|
4301
|
+
'REQUEST LIFECYCLE (registry-tracked): when the user gives you a',
|
|
4302
|
+
'real WORK request (build / fix / change / investigate something',
|
|
4303
|
+
'-- NOT a trivial question or chit-chat), you MUST run it through',
|
|
4304
|
+
'the pending registry so every job leaves a trace:',
|
|
4305
|
+
' 1. REGISTER first: your FIRST act is yujin.pending.add with a',
|
|
4306
|
+
' clear title + body describing the request. Keep the PND id.',
|
|
4307
|
+
' 2. MANAGE + ITERATE: do the work, then run an EVOLUTIVE/',
|
|
4308
|
+
' CORRECTIVE cycle -- validate or test what you built; if it',
|
|
4309
|
+
' fails or can be improved, fix/improve and re-validate; loop',
|
|
4310
|
+
' until it converges. Log each iteration with',
|
|
4311
|
+
' yujin.pending.note({id, note, kind}) -- kind "progress" for',
|
|
4312
|
+
' an advance, "corrective" for a fix, "evolutive" for an',
|
|
4313
|
+
' improvement -- so the registry keeps the HISTORY of how you',
|
|
4314
|
+
' worked it, not just the open + close.',
|
|
4315
|
+
' 3. CLOSE last: your LAST act is yujin.pending.close({id, reason,',
|
|
4316
|
+
' resolution}) where resolution summarizes the closing history',
|
|
4317
|
+
' (what you did across the cycle). reason="shipped" when done.',
|
|
4318
|
+
'Skip this only for trivial Q&A. For multi-part requests, one PND',
|
|
4319
|
+
'per distinct piece of work. Never leave a finished job open, and',
|
|
4320
|
+
'never close one you have not actually validated.',
|
|
1758
4321
|
'',
|
|
1759
|
-
|
|
1760
|
-
'
|
|
1761
|
-
'
|
|
1762
|
-
'
|
|
1763
|
-
'
|
|
1764
|
-
'
|
|
4322
|
+
/* Documento yujin-look por default (dogfood 2026-06-10). */
|
|
4323
|
+
'DOCUMENTS: when you generate ANY document -- a report, an',
|
|
4324
|
+
'analysis, a summary, a proposal, a spec, a how-to, meeting',
|
|
4325
|
+
'notes -- do NOT dump it as plain text in chat. Open it as a',
|
|
4326
|
+
'tab with yujin.pizarron.create-tab kind="doc" and content =',
|
|
4327
|
+
'the markdown. The panel renders it as a styled Yujin-look',
|
|
4328
|
+
'document (washi paper, sumi-e typography) and it opens',
|
|
4329
|
+
'automatically. Reply in chat with a one-line summary + that',
|
|
4330
|
+
'you opened it. Use kind="doc" by DEFAULT for documents;',
|
|
4331
|
+
'kind="code" only for source files, kind="graph" for diagrams.',
|
|
1765
4332
|
'',
|
|
1766
|
-
|
|
1767
|
-
'
|
|
1768
|
-
'
|
|
1769
|
-
'
|
|
1770
|
-
'
|
|
4333
|
+
/* PIZ-3 (PND-073) -- kind=graph para grafos de nodos. */
|
|
4334
|
+
'GRAPHS: to show a node-and-edge graph (project graph, a',
|
|
4335
|
+
'workflow as nodes+connectors, a component design flow), use',
|
|
4336
|
+
'yujin.pizarron.create-tab with kind="graph" and content = a',
|
|
4337
|
+
'JSON string: {"nodes":[{"id","label","weight"(0..1 optional),',
|
|
4338
|
+
'"kind"(optional),"detail"(optional markdown shown on click)}],',
|
|
4339
|
+
'"edges":[{"from","to","kind"("uses"|"implements"|...),',
|
|
4340
|
+
'"label"(optional)}]}. The panel renders it as an interactive',
|
|
4341
|
+
'force-directed graph (drag, zoom, click a node for its detail).',
|
|
4342
|
+
'Prefer kind="graph" over hand-drawn SVG for any graph/diagram.',
|
|
4343
|
+
'Use kind="graph" ON DEMAND for: (a) the DESIGN FLOW of a',
|
|
4344
|
+
'component the user asks about -- nodes = the component + its',
|
|
4345
|
+
'props / state / hooks / children / data sources, edges = the',
|
|
4346
|
+
'data + render flow, each node detail explaining its role;',
|
|
4347
|
+
'(b) the project WORKFLOW as phases+steps; (c) any "graficame /',
|
|
4348
|
+
'diagrama / flujo de X" request. Put each node explanation in',
|
|
4349
|
+
'its "detail" (markdown) so the click shows it.',
|
|
1771
4350
|
'',
|
|
1772
|
-
|
|
1773
|
-
'
|
|
1774
|
-
|
|
4351
|
+
/* PND-064 -- capability honesty per mode. */
|
|
4352
|
+
...(panelMode === 'mcp-bridge' ? [
|
|
4353
|
+
'=================================================================',
|
|
4354
|
+
'NAC3 VERBS VIA MCP TOOL (Plan mode -- read carefully)',
|
|
4355
|
+
'=================================================================',
|
|
4356
|
+
'',
|
|
4357
|
+
'You are running in Plan mode (claude-code-cli subprocess).',
|
|
4358
|
+
'You have the FULL yujin verb manifest above. To invoke ANY of',
|
|
4359
|
+
'those verbs, call the MCP tool nac3_invoke -- it is your',
|
|
4360
|
+
'single engine for acting on the project (pizarron, graph,',
|
|
4361
|
+
'pendings, workflow, files, git, reader, everything):',
|
|
4362
|
+
' nac3_invoke({ verb_id, args })',
|
|
4363
|
+
'Examples:',
|
|
4364
|
+
' - show the graph in the pizarron:',
|
|
4365
|
+
' nac3_invoke({ verb_id: "yujin.graph.summary", args: {} })',
|
|
4366
|
+
' then nac3_invoke({ verb_id: "yujin.pizarron.create-tab",',
|
|
4367
|
+
' args: { title, kind: "md", content } })',
|
|
4368
|
+
' - list pendings: nac3_invoke({ verb_id: "yujin.pending.list", args: {} })',
|
|
4369
|
+
' - discover a schema: nac3_invoke({ verb_id:',
|
|
4370
|
+
' "yujin.nac3.discover-schemas", args: { verb_ids: ["..."] } })',
|
|
4371
|
+
'PREFER the native verb (read graph/pending/workflow) over',
|
|
4372
|
+
'shelling out with Bash -- it is one call and structured.',
|
|
4373
|
+
'When the tool result says it was sent to the pizarron, the',
|
|
4374
|
+
'tab appears in the panel. Only claim success AFTER the tool',
|
|
4375
|
+
'returns ok. Do not claim to "see" or "verify" the panel.',
|
|
4376
|
+
'',
|
|
4377
|
+
] : []),
|
|
4378
|
+
...(panelMode === 'none' ? [
|
|
4379
|
+
'=================================================================',
|
|
4380
|
+
'PANEL DISPATCH UNAVAILABLE IN THIS MODE (read carefully)',
|
|
4381
|
+
'=================================================================',
|
|
4382
|
+
'',
|
|
4383
|
+
'You are running in Plan mode (claude-code-cli subprocess)',
|
|
4384
|
+
'and the panel bridge is not active. The yujin.pizarron.* and',
|
|
4385
|
+
'other panel verbs do NOT reach the Forge pizarron. Therefore:',
|
|
4386
|
+
' - NEVER say you created/opened a tab, rendered a graph, or',
|
|
4387
|
+
' showed anything in the pizarron. You cannot, in this mode.',
|
|
4388
|
+
' - NEVER claim you "verified" the panel or checked the screen.',
|
|
4389
|
+
' You have no view of the panel. Stating otherwise is a lie.',
|
|
4390
|
+
' - When the user asks to render something in the pizarron,',
|
|
4391
|
+
' be honest: explain that the pizarron is reachable when the',
|
|
4392
|
+
' brain runs in API mode, and that they can switch it in',
|
|
4393
|
+
' Ajustes > Brain > "Anthropic: modalidad" -> Console (it',
|
|
4394
|
+
' takes effect on reopening the panel). Offer to show the',
|
|
4395
|
+
' content inline in chat meanwhile.',
|
|
4396
|
+
' - The graph / pendings / files you read via verbs are still',
|
|
4397
|
+
' real and useful -- you just cannot paint them on the',
|
|
4398
|
+
' canvas in this mode. Describe them in text.',
|
|
4399
|
+
'',
|
|
4400
|
+
] : []),
|
|
4401
|
+
'=================================================================',
|
|
4402
|
+
'R7 -- HONEST SEQUENCING (anti-hallucination)',
|
|
4403
|
+
'=================================================================',
|
|
1775
4404
|
'',
|
|
1776
|
-
'
|
|
1777
|
-
'
|
|
1778
|
-
'
|
|
4405
|
+
'If the user asks for N actions and you only have verbs for',
|
|
4406
|
+
'K < N, SAY SO BEFORE starting. No "step 1 done, proceeding',
|
|
4407
|
+
'with step 2" when step 2 has no verb. Three chained invokes',
|
|
4408
|
+
'beats one invoke + a promise. Chat reply summarises AFTER',
|
|
4409
|
+
'all returned result_ok=true.',
|
|
1779
4410
|
'',
|
|
1780
4411
|
'=================================================================',
|
|
1781
|
-
'
|
|
4412
|
+
'R8 -- THE MODAL IS THE QUESTION (anti-redundancy)',
|
|
1782
4413
|
'=================================================================',
|
|
1783
4414
|
'',
|
|
1784
|
-
'
|
|
1785
|
-
'
|
|
1786
|
-
'
|
|
1787
|
-
'
|
|
1788
|
-
'
|
|
1789
|
-
'
|
|
1790
|
-
'
|
|
1791
|
-
'
|
|
1792
|
-
'
|
|
1793
|
-
'
|
|
1794
|
-
'
|
|
1795
|
-
'
|
|
1796
|
-
'
|
|
1797
|
-
'
|
|
1798
|
-
'
|
|
1799
|
-
'
|
|
4415
|
+
'For ANY verb with policy="destructive" or "approval-required"',
|
|
4416
|
+
'(yujin.git.commit, .push, fs.write-project-file, etc), the',
|
|
4417
|
+
'panel opens a NATIVE MODAL with Approve/Reject/Cancel. THE',
|
|
4418
|
+
'MODAL IS THE QUESTION. Do NOT repeat it in chat.',
|
|
4419
|
+
'',
|
|
4420
|
+
'Correct protocol:',
|
|
4421
|
+
' 1. Build args carefully (read-only queries first if needed).',
|
|
4422
|
+
' 2. ONE short summary sentence ("I will commit X including',
|
|
4423
|
+
' aaa.txt; check the modal."). NO "Confirms?" / "Respond',
|
|
4424
|
+
' yes/no" / "Let me know when you approve" -- that triggers',
|
|
4425
|
+
' the bug.',
|
|
4426
|
+
' 3. Invoke. The dispatcher returns approval_required envelope;',
|
|
4427
|
+
' the panel opens the modal.',
|
|
4428
|
+
' 4. WAIT. Do NOT ask in chat. The NEXT turn brings either',
|
|
4429
|
+
' "[NAC3 modal] APPROVED: ..." or "[NAC3 modal] REJECTED: ...".',
|
|
4430
|
+
' 5. APPROVED -> brief report + next step. REJECTED -> ack +',
|
|
4431
|
+
' do NOT retry. Wait for user instruction.',
|
|
1800
4432
|
'',
|
|
1801
|
-
'
|
|
1802
|
-
'
|
|
4433
|
+
'"[NAC3 modal] APPROVED/REJECTED" is a SYSTEM event, not the',
|
|
4434
|
+
'user talking -- do NOT thank them for approving.',
|
|
1803
4435
|
'',
|
|
1804
|
-
'
|
|
1805
|
-
'
|
|
4436
|
+
'For the full doctrine with MAL/BIEN examples, call',
|
|
4437
|
+
'yujin.doctrine.discover({slugs:["r8-modal"]}).',
|
|
4438
|
+
'',
|
|
4439
|
+
'=================================================================',
|
|
4440
|
+
'BYOK + LICENSE (all chat/voice -- LEMA YUJIN)',
|
|
4441
|
+
'=================================================================',
|
|
1806
4442
|
'',
|
|
1807
|
-
'
|
|
1808
|
-
'
|
|
4443
|
+
'When the user asks about keys / license:',
|
|
4444
|
+
' 1. Call yujin.keys.status (booleans only, never values).',
|
|
4445
|
+
' 2. To add a key: tell user WHERE to get it (Anthropic:',
|
|
4446
|
+
' console.anthropic.com/settings/keys). Ask them to paste next.',
|
|
4447
|
+
' 3. On paste: invoke yujin.keys.set {slot, value}. NEVER echo the',
|
|
4448
|
+
' value. Reply "Guardado" + suggest panel restart.',
|
|
4449
|
+
' 4. License is NOT a BYOK -- point to `yf license activate` or',
|
|
4450
|
+
' polar.sh/checkout.',
|
|
1809
4451
|
'',
|
|
1810
|
-
'
|
|
1811
|
-
'
|
|
1812
|
-
'
|
|
4452
|
+
'=================================================================',
|
|
4453
|
+
'WORKFLOW STATE MACHINE (18 steps)',
|
|
4454
|
+
'=================================================================',
|
|
1813
4455
|
'',
|
|
1814
|
-
'
|
|
1815
|
-
'
|
|
1816
|
-
'
|
|
4456
|
+
'At the start of every turn with a concrete product ask: call',
|
|
4457
|
+
'yujin.workflow.state to know the position. Interactive steps',
|
|
4458
|
+
'(triage / discover / clarify) -> ASK in natural language, persist',
|
|
4459
|
+
'with yujin.workflow.set when answers in. Autonomous steps',
|
|
4460
|
+
'(legacy_audit, coverage, manual_generate, handoff, metrics) ->',
|
|
4461
|
+
'call yujin.workflow.run-step directly. Never skip a phase silently.',
|
|
4462
|
+
'',
|
|
4463
|
+
'=================================================================',
|
|
4464
|
+
'TOOL CALL DISCIPLINE',
|
|
4465
|
+
'=================================================================',
|
|
1817
4466
|
'',
|
|
1818
|
-
'TOOL CALL DISCIPLINE:',
|
|
1819
4467
|
'- Tool calls are silent to the user. Summarise the result in plain',
|
|
1820
|
-
' language
|
|
1821
|
-
'-
|
|
1822
|
-
'
|
|
1823
|
-
'-
|
|
1824
|
-
'
|
|
1825
|
-
'- Direct AST mutation is NOT a chat capability today --
|
|
1826
|
-
'
|
|
4468
|
+
' language after.',
|
|
4469
|
+
'- NEVER echo a BYOK key, license JWT, or any vault credential. Refuse',
|
|
4470
|
+
' if asked.',
|
|
4471
|
+
'- Short code changes -> create-tab kind=code (user applies in editor).',
|
|
4472
|
+
' Persistent file changes -> yujin.fs.write-project-file (approval).',
|
|
4473
|
+
'- Direct AST mutation is NOT a chat capability today -- runs via',
|
|
4474
|
+
' CLI `yf migrate --apply` (paid seat).',
|
|
1827
4475
|
'',
|
|
1828
4476
|
'=================================================================',
|
|
1829
4477
|
'CONTEXT (this session)',
|
|
@@ -1833,7 +4481,7 @@ function buildSystemPrompt(ctx, mode = 'didactico', pilotState = {
|
|
|
1833
4481
|
'- Root: ' + ctx.projectRoot,
|
|
1834
4482
|
'- Forge: v' + VERSION,
|
|
1835
4483
|
modePromptSuffix(mode),
|
|
1836
|
-
welcomePromptSuffix(pilotState),
|
|
4484
|
+
welcomePromptSuffix(pilotState, firstUserMessage),
|
|
1837
4485
|
].join('\n');
|
|
1838
4486
|
}
|
|
1839
4487
|
function sendJson(res, status, body) {
|
|
@@ -1841,6 +4489,686 @@ function sendJson(res, status, body) {
|
|
|
1841
4489
|
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
1842
4490
|
res.end(JSON.stringify(body));
|
|
1843
4491
|
}
|
|
4492
|
+
/* ============================================================
|
|
4493
|
+
* Director sprint slice 5 -- agent runtime panel handlers.
|
|
4494
|
+
* ============================================================ */
|
|
4495
|
+
/** Project the AgentDefinition to a UI-safe shape. Drops the
|
|
4496
|
+
* system prompt + secret-ish fields, keeps the fields the panel
|
|
4497
|
+
* graph needs to render the topology + per-agent status. */
|
|
4498
|
+
function projectAgentForPanel(def) {
|
|
4499
|
+
return {
|
|
4500
|
+
slug: def.slug,
|
|
4501
|
+
name: def.name,
|
|
4502
|
+
description: def.description,
|
|
4503
|
+
status: def.status,
|
|
4504
|
+
brain_tier: def.brain_tier,
|
|
4505
|
+
can_call: def.can_call,
|
|
4506
|
+
can_be_called_by: def.can_be_called_by,
|
|
4507
|
+
allowed_verbs: def.allowed_verbs,
|
|
4508
|
+
trigger: def.trigger,
|
|
4509
|
+
};
|
|
4510
|
+
}
|
|
4511
|
+
async function handleAgentState(res, ctx) {
|
|
4512
|
+
if (!ctx.runtime) {
|
|
4513
|
+
sendJson(res, 503, { ok: false, error: 'agent runtime not wired into this chat server' });
|
|
4514
|
+
return;
|
|
4515
|
+
}
|
|
4516
|
+
const { listAgentDefinitions } = await import('../agents/store.js');
|
|
4517
|
+
const defs = await listAgentDefinitions({ root: ctx.runtime.getAgentsRoot() });
|
|
4518
|
+
sendJson(res, 200, {
|
|
4519
|
+
ok: true,
|
|
4520
|
+
runtime: ctx.runtime.status(),
|
|
4521
|
+
agents: defs.map(projectAgentForPanel),
|
|
4522
|
+
});
|
|
4523
|
+
}
|
|
4524
|
+
async function handleAgentRun(req, res, ctx) {
|
|
4525
|
+
if (!ctx.runtime) {
|
|
4526
|
+
sendJson(res, 503, { ok: false, error: 'agent runtime not wired into this chat server' });
|
|
4527
|
+
return;
|
|
4528
|
+
}
|
|
4529
|
+
let body;
|
|
4530
|
+
try {
|
|
4531
|
+
body = JSON.parse(await readBody(req));
|
|
4532
|
+
}
|
|
4533
|
+
catch {
|
|
4534
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
if (typeof body.slug !== 'string' || body.slug.length === 0) {
|
|
4538
|
+
sendJson(res, 400, { ok: false, error: 'missing required field: slug' });
|
|
4539
|
+
return;
|
|
4540
|
+
}
|
|
4541
|
+
const trigger_reason = typeof body.trigger_reason === 'string' && body.trigger_reason.length > 0
|
|
4542
|
+
? body.trigger_reason
|
|
4543
|
+
: 'panel';
|
|
4544
|
+
/* Slice 1 N-projects sprint -- optional project_slug. */
|
|
4545
|
+
const project_slug = (typeof body.project_slug === 'string' && body.project_slug.trim())
|
|
4546
|
+
? body.project_slug.trim()
|
|
4547
|
+
: 'self';
|
|
4548
|
+
try {
|
|
4549
|
+
const run = await ctx.runtime.dispatchAgent(body.slug, trigger_reason, { project_slug });
|
|
4550
|
+
sendJson(res, 200, { ok: true, run });
|
|
4551
|
+
}
|
|
4552
|
+
catch (err) {
|
|
4553
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4554
|
+
/* Map known runtime errors to actionable HTTP codes. */
|
|
4555
|
+
if (/not registered/i.test(msg)) {
|
|
4556
|
+
sendJson(res, 404, { ok: false, error: msg });
|
|
4557
|
+
}
|
|
4558
|
+
else if (/not started/i.test(msg)) {
|
|
4559
|
+
sendJson(res, 503, { ok: false, error: msg });
|
|
4560
|
+
}
|
|
4561
|
+
else {
|
|
4562
|
+
sendJson(res, 500, { ok: false, error: msg });
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
async function handleAgentEventsSse(req, res, ctx) {
|
|
4567
|
+
if (!ctx.runtime) {
|
|
4568
|
+
sendJson(res, 503, { ok: false, error: 'agent runtime not wired into this chat server' });
|
|
4569
|
+
return;
|
|
4570
|
+
}
|
|
4571
|
+
res.statusCode = 200;
|
|
4572
|
+
res.setHeader('content-type', 'text/event-stream; charset=utf-8');
|
|
4573
|
+
res.setHeader('cache-control', 'no-store, no-transform');
|
|
4574
|
+
res.setHeader('connection', 'keep-alive');
|
|
4575
|
+
/* Flush headers so the client can settle the response before
|
|
4576
|
+
* any event is emitted. */
|
|
4577
|
+
if (typeof res.flushHeaders === 'function') {
|
|
4578
|
+
res.flushHeaders();
|
|
4579
|
+
}
|
|
4580
|
+
/* Initial comment line keeps the proxy from buffering the
|
|
4581
|
+
* empty stream + signals the client the connection is live. */
|
|
4582
|
+
res.write(': connected to agent runtime event stream\n\n');
|
|
4583
|
+
const sendEvent = (e) => {
|
|
4584
|
+
try {
|
|
4585
|
+
res.write('event: ' + e.kind + '\n');
|
|
4586
|
+
res.write('data: ' + JSON.stringify(e) + '\n\n');
|
|
4587
|
+
}
|
|
4588
|
+
catch {
|
|
4589
|
+
/* Socket may be closed; ignore. The 'close' handler below
|
|
4590
|
+
* unsubscribes regardless. */
|
|
4591
|
+
}
|
|
4592
|
+
};
|
|
4593
|
+
ctx.runtime.on('event', sendEvent);
|
|
4594
|
+
/* Heartbeat keeps the connection alive through idle proxies.
|
|
4595
|
+
* 15s mirrors the existing chat SSE cadence. */
|
|
4596
|
+
const heartbeat = setInterval(() => {
|
|
4597
|
+
try {
|
|
4598
|
+
res.write(': hb\n\n');
|
|
4599
|
+
}
|
|
4600
|
+
catch { /* socket dead */ }
|
|
4601
|
+
}, 15_000);
|
|
4602
|
+
if (typeof heartbeat.unref === 'function') {
|
|
4603
|
+
heartbeat.unref();
|
|
4604
|
+
}
|
|
4605
|
+
const cleanup = () => {
|
|
4606
|
+
clearInterval(heartbeat);
|
|
4607
|
+
if (ctx.runtime)
|
|
4608
|
+
ctx.runtime.off('event', sendEvent);
|
|
4609
|
+
};
|
|
4610
|
+
req.on('close', cleanup);
|
|
4611
|
+
req.on('error', cleanup);
|
|
4612
|
+
}
|
|
4613
|
+
/**
|
|
4614
|
+
* Director sprint slice 10 -- POST /api/agent/approve.
|
|
4615
|
+
*
|
|
4616
|
+
* Single endpoint that resolves an R8-style approval envelope an
|
|
4617
|
+
* agent emitted (decision: 'approved' | 'rejected'). Delegates to
|
|
4618
|
+
* the existing approval queue + audit log. Independent of runtime
|
|
4619
|
+
* state -- a pending row can survive runtime restarts via the
|
|
4620
|
+
* queue's TTL, so we do NOT gate this endpoint on ctx.runtime.
|
|
4621
|
+
*/
|
|
4622
|
+
async function handleAgentApprove(req, res, ctx) {
|
|
4623
|
+
let body;
|
|
4624
|
+
try {
|
|
4625
|
+
body = JSON.parse(await readBody(req));
|
|
4626
|
+
}
|
|
4627
|
+
catch {
|
|
4628
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
|
|
4629
|
+
return;
|
|
4630
|
+
}
|
|
4631
|
+
const traceId = typeof body.trace_id === 'string' ? body.trace_id.trim() : '';
|
|
4632
|
+
if (!traceId) {
|
|
4633
|
+
sendJson(res, 400, { ok: false, error: 'trace_id required' });
|
|
4634
|
+
return;
|
|
4635
|
+
}
|
|
4636
|
+
const decision = typeof body.decision === 'string' ? body.decision : '';
|
|
4637
|
+
if (decision !== 'approved' && decision !== 'rejected') {
|
|
4638
|
+
sendJson(res, 400, { ok: false, error: 'decision must be "approved" or "rejected"' });
|
|
4639
|
+
return;
|
|
4640
|
+
}
|
|
4641
|
+
const reason = typeof body.reason === 'string' ? body.reason.slice(0, 240) : undefined;
|
|
4642
|
+
const pending = popPending(traceId);
|
|
4643
|
+
if (!pending) {
|
|
4644
|
+
await logApproval({
|
|
4645
|
+
ts: new Date().toISOString(),
|
|
4646
|
+
trace_id: traceId,
|
|
4647
|
+
verb_id: '?',
|
|
4648
|
+
decision: 'expired',
|
|
4649
|
+
args_sample: {},
|
|
4650
|
+
policy: '?',
|
|
4651
|
+
reason: 'trace_id not in queue (expired or already resolved)',
|
|
4652
|
+
});
|
|
4653
|
+
sendJson(res, 404, { ok: false, error: 'no pending approval for trace_id (expired or already resolved)' });
|
|
4654
|
+
return;
|
|
4655
|
+
}
|
|
4656
|
+
if (decision === 'rejected') {
|
|
4657
|
+
await logApproval({
|
|
4658
|
+
ts: new Date().toISOString(),
|
|
4659
|
+
trace_id: traceId,
|
|
4660
|
+
verb_id: pending.verb.id,
|
|
4661
|
+
decision: 'reject',
|
|
4662
|
+
args_sample: pending.args,
|
|
4663
|
+
policy: pending.verb.policy,
|
|
4664
|
+
reason,
|
|
4665
|
+
});
|
|
4666
|
+
/* Director closure pendiente 5 followup -- fire the SSE event
|
|
4667
|
+
* so the panel SVG can clear the paused-for-approval marker on
|
|
4668
|
+
* the originating agent node. Best-effort: no runtime / no
|
|
4669
|
+
* agent_slug means nobody is listening for this event anyway. */
|
|
4670
|
+
if (ctx.runtime && pending.agent_slug) {
|
|
4671
|
+
ctx.runtime.emitExternal({
|
|
4672
|
+
kind: 'agent_run_paused_resolved',
|
|
4673
|
+
slug: pending.agent_slug,
|
|
4674
|
+
trace_id: traceId,
|
|
4675
|
+
decision: 'rejected',
|
|
4676
|
+
at: Date.now(),
|
|
4677
|
+
});
|
|
4678
|
+
}
|
|
4679
|
+
sendJson(res, 200, {
|
|
4680
|
+
ok: true,
|
|
4681
|
+
trace_id: traceId,
|
|
4682
|
+
verb_id: pending.verb.id,
|
|
4683
|
+
decision: 'rejected',
|
|
4684
|
+
});
|
|
4685
|
+
return;
|
|
4686
|
+
}
|
|
4687
|
+
/* Approve path: execute the bound verb via its captured legacy
|
|
4688
|
+
* alias, mirroring handleNac3Approve. */
|
|
4689
|
+
if (!pending.legacy_alias) {
|
|
4690
|
+
sendJson(res, 500, { ok: false, error: 'pending row has no legacy alias' });
|
|
4691
|
+
return;
|
|
4692
|
+
}
|
|
4693
|
+
let result;
|
|
4694
|
+
try {
|
|
4695
|
+
result = await pending.legacy_run(pending.legacy_alias, pending.args, ctx.projectRoot === pending.ctx.projectRoot ? pending.ctx : { projectRoot: ctx.projectRoot });
|
|
4696
|
+
}
|
|
4697
|
+
catch (err) {
|
|
4698
|
+
await logApproval({
|
|
4699
|
+
ts: new Date().toISOString(),
|
|
4700
|
+
trace_id: traceId,
|
|
4701
|
+
verb_id: pending.verb.id,
|
|
4702
|
+
decision: 'approve',
|
|
4703
|
+
args_sample: pending.args,
|
|
4704
|
+
policy: pending.verb.policy,
|
|
4705
|
+
reason: 'handler threw: ' + (err instanceof Error ? err.message : String(err)),
|
|
4706
|
+
});
|
|
4707
|
+
sendJson(res, 500, {
|
|
4708
|
+
ok: false,
|
|
4709
|
+
error: 'handler threw on approved verb: ' + (err instanceof Error ? err.message : String(err)),
|
|
4710
|
+
});
|
|
4711
|
+
return;
|
|
4712
|
+
}
|
|
4713
|
+
await logApproval({
|
|
4714
|
+
ts: new Date().toISOString(),
|
|
4715
|
+
trace_id: traceId,
|
|
4716
|
+
verb_id: pending.verb.id,
|
|
4717
|
+
decision: 'approve',
|
|
4718
|
+
args_sample: pending.args,
|
|
4719
|
+
policy: pending.verb.policy,
|
|
4720
|
+
reason,
|
|
4721
|
+
});
|
|
4722
|
+
/* Director closure pendiente 5 followup -- fire the SSE event so
|
|
4723
|
+
* the panel SVG can clear the paused-for-approval marker on the
|
|
4724
|
+
* originating agent node. */
|
|
4725
|
+
if (ctx.runtime && pending.agent_slug) {
|
|
4726
|
+
ctx.runtime.emitExternal({
|
|
4727
|
+
kind: 'agent_run_paused_resolved',
|
|
4728
|
+
slug: pending.agent_slug,
|
|
4729
|
+
trace_id: traceId,
|
|
4730
|
+
decision: 'approved',
|
|
4731
|
+
at: Date.now(),
|
|
4732
|
+
});
|
|
4733
|
+
}
|
|
4734
|
+
sendJson(res, 200, {
|
|
4735
|
+
ok: true,
|
|
4736
|
+
trace_id: traceId,
|
|
4737
|
+
verb_id: pending.verb.id,
|
|
4738
|
+
decision: 'approved',
|
|
4739
|
+
is_error: result.is_error ?? false,
|
|
4740
|
+
result: result.result,
|
|
4741
|
+
});
|
|
4742
|
+
}
|
|
4743
|
+
/* ===================================================================
|
|
4744
|
+
* Agent activity log + bridge inspector + chat-per-agent
|
|
4745
|
+
* (visibility sprint -- alpha.59z.153).
|
|
4746
|
+
*
|
|
4747
|
+
* Three endpoints that surface what the agents are actually doing
|
|
4748
|
+
* so the operator is not staring at a silent SVG:
|
|
4749
|
+
*
|
|
4750
|
+
* GET /api/agent/activity?since=<iso?>&limit=<n?>
|
|
4751
|
+
* -> recent timestamped events: agent runs (started/finished),
|
|
4752
|
+
* bridge sends, dispatch invokes, approval decisions.
|
|
4753
|
+
* Merge of 4 jsonl tails + 1 dir scan; sorted by ts desc.
|
|
4754
|
+
*
|
|
4755
|
+
* GET /api/agent/bridge-tail?pair_id=<from__to?>&limit=<n?>
|
|
4756
|
+
* -> messages out of bridge.json filtered + sorted desc.
|
|
4757
|
+
* When pair_id absent, lists all pairs with counts.
|
|
4758
|
+
*
|
|
4759
|
+
* POST /api/agent/inject {slug, text, dispatch?}
|
|
4760
|
+
* -> append a user-role message to the agent's active
|
|
4761
|
+
* conversation file + optionally dispatch a run.
|
|
4762
|
+
*
|
|
4763
|
+
* All three are best-effort: missing files are normal (no agents
|
|
4764
|
+
* yet) -- they return ok:true with empty arrays, not 404.
|
|
4765
|
+
* =============================================================== */
|
|
4766
|
+
async function tailJsonl(file, limit) {
|
|
4767
|
+
try {
|
|
4768
|
+
const raw = await fs.readFile(file, 'utf-8');
|
|
4769
|
+
const lines = raw.split(/\r?\n/).filter((l) => l.length > 0);
|
|
4770
|
+
const slice = lines.length > limit ? lines.slice(lines.length - limit) : lines;
|
|
4771
|
+
const out = [];
|
|
4772
|
+
for (const line of slice) {
|
|
4773
|
+
try {
|
|
4774
|
+
out.push(JSON.parse(line));
|
|
4775
|
+
}
|
|
4776
|
+
catch { /* skip corrupt line */ }
|
|
4777
|
+
}
|
|
4778
|
+
return out;
|
|
4779
|
+
}
|
|
4780
|
+
catch {
|
|
4781
|
+
return [];
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
function agentsRootDefault(ctx) {
|
|
4785
|
+
if (ctx.runtime)
|
|
4786
|
+
return ctx.runtime.getAgentsRoot();
|
|
4787
|
+
/* Mirror the runtime default. YF_CONFIG_DIR is the same env var
|
|
4788
|
+
* approval_queue / dispatch_log honor; tests inject it to scope
|
|
4789
|
+
* the entire ~/.yujin-forge tree to a tmpdir. */
|
|
4790
|
+
if (process.env.YF_CONFIG_DIR)
|
|
4791
|
+
return process.env.YF_CONFIG_DIR;
|
|
4792
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
4793
|
+
return path.join(home, '.yujin-forge');
|
|
4794
|
+
}
|
|
4795
|
+
async function handleAgentActivity(req, res, ctx) {
|
|
4796
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
4797
|
+
const limit = clampInt(url.searchParams.get('limit'), 200, 1, 2000);
|
|
4798
|
+
const since = url.searchParams.get('since') || '';
|
|
4799
|
+
/* Slice 1 N-projects sprint -- optional project filter. Empty
|
|
4800
|
+
* = all projects (default; back-compat with single-project
|
|
4801
|
+
* callers). Explicit slug filters events by project_slug. */
|
|
4802
|
+
const projectFilter = (url.searchParams.get('project_slug') || '').trim();
|
|
4803
|
+
const events = [];
|
|
4804
|
+
const agentsRoot = agentsRootDefault(ctx);
|
|
4805
|
+
const yfRoot = path.join(agentsRoot);
|
|
4806
|
+
/* 1. nac3 dispatch log -- every verb invoke / deny / panel-dispatch */
|
|
4807
|
+
const dispatchLog = path.join(yfRoot, 'nac3_log.jsonl');
|
|
4808
|
+
for (const e of await tailJsonl(dispatchLog, limit)) {
|
|
4809
|
+
const r = e;
|
|
4810
|
+
if (!r.ts)
|
|
4811
|
+
continue;
|
|
4812
|
+
const label = r.kind === 'invoke' ? ((r.result_ok ? 'ok' : 'err') + ' ' + (r.verb_id ?? '?'))
|
|
4813
|
+
: r.kind === 'verb-not-found' ? ('unknown verb ' + (r.verb_id ?? '?'))
|
|
4814
|
+
: r.kind === 'policy-deny' ? ('policy denied ' + (r.verb_id ?? '?'))
|
|
4815
|
+
: r.kind === 'panel-handler-error' ? ('panel error ' + (r.verb_id ?? '?'))
|
|
4816
|
+
: String(r.kind ?? 'event');
|
|
4817
|
+
events.push({
|
|
4818
|
+
ts: String(r.ts),
|
|
4819
|
+
kind: 'dispatch.' + String(r.kind ?? 'unknown'),
|
|
4820
|
+
text: label,
|
|
4821
|
+
trace_id: r.trace_id ? String(r.trace_id) : undefined,
|
|
4822
|
+
extra: {
|
|
4823
|
+
duration_ms: r.duration_ms,
|
|
4824
|
+
policy: r.policy,
|
|
4825
|
+
side_effects_declared: r.side_effects_declared,
|
|
4826
|
+
},
|
|
4827
|
+
});
|
|
4828
|
+
}
|
|
4829
|
+
/* 2. approvals log -- R8 modal decisions */
|
|
4830
|
+
const approvalLog = path.join(yfRoot, 'nac3_approvals.jsonl');
|
|
4831
|
+
for (const e of await tailJsonl(approvalLog, limit)) {
|
|
4832
|
+
const r = e;
|
|
4833
|
+
if (!r.ts)
|
|
4834
|
+
continue;
|
|
4835
|
+
events.push({
|
|
4836
|
+
ts: String(r.ts),
|
|
4837
|
+
kind: 'approval.' + String(r.decision ?? 'event'),
|
|
4838
|
+
text: 'R8 ' + String(r.decision ?? '?') + ' on ' + String(r.verb_id ?? '?'),
|
|
4839
|
+
trace_id: r.trace_id ? String(r.trace_id) : undefined,
|
|
4840
|
+
extra: { policy: r.policy, reason: r.reason },
|
|
4841
|
+
});
|
|
4842
|
+
}
|
|
4843
|
+
/* 3. agent runs -- scan ~/.yujin-forge/agents/by-slug/<slug>/runs/*.json */
|
|
4844
|
+
const bySlug = path.join(yfRoot, 'agents', 'by-slug');
|
|
4845
|
+
let slugs = [];
|
|
4846
|
+
try {
|
|
4847
|
+
slugs = await fs.readdir(bySlug);
|
|
4848
|
+
}
|
|
4849
|
+
catch { /* no agents yet -- ok */ }
|
|
4850
|
+
for (const slug of slugs) {
|
|
4851
|
+
const runsDir = path.join(bySlug, slug, 'runs');
|
|
4852
|
+
let runFiles = [];
|
|
4853
|
+
try {
|
|
4854
|
+
runFiles = (await fs.readdir(runsDir)).filter((n) => n.endsWith('.json'));
|
|
4855
|
+
}
|
|
4856
|
+
catch {
|
|
4857
|
+
continue;
|
|
4858
|
+
}
|
|
4859
|
+
/* Newest first; we cap at limit per agent so a noisy one
|
|
4860
|
+
* does not drown others. */
|
|
4861
|
+
runFiles.sort();
|
|
4862
|
+
const cap = runFiles.slice(-Math.min(50, limit));
|
|
4863
|
+
for (const fn of cap) {
|
|
4864
|
+
try {
|
|
4865
|
+
const raw = await fs.readFile(path.join(runsDir, fn), 'utf-8');
|
|
4866
|
+
const r = JSON.parse(raw);
|
|
4867
|
+
const startTs = r.started_at ? new Date(Number(r.started_at)).toISOString() : '';
|
|
4868
|
+
const endTs = r.ended_at ? new Date(Number(r.ended_at)).toISOString() : startTs;
|
|
4869
|
+
const runProject = (typeof r.project_slug === 'string' && r.project_slug) ? r.project_slug : 'self';
|
|
4870
|
+
events.push({
|
|
4871
|
+
ts: startTs || endTs,
|
|
4872
|
+
kind: 'agent.run_started',
|
|
4873
|
+
slug,
|
|
4874
|
+
text: '[' + runProject + '] ' + slug + ' started: ' + String(r.trigger_reason ?? '').slice(0, 120),
|
|
4875
|
+
project_slug: runProject,
|
|
4876
|
+
});
|
|
4877
|
+
if (r.ended_at) {
|
|
4878
|
+
events.push({
|
|
4879
|
+
ts: endTs,
|
|
4880
|
+
kind: 'agent.run_finished',
|
|
4881
|
+
slug,
|
|
4882
|
+
text: '[' + runProject + '] ' + slug + ' ' + String(r.status ?? '?')
|
|
4883
|
+
+ ' (in=' + (r.tokens_in ?? 0) + ' out=' + (r.tokens_out ?? 0)
|
|
4884
|
+
+ ' tools=' + (r.tool_calls ?? 0) + ')',
|
|
4885
|
+
project_slug: runProject,
|
|
4886
|
+
extra: {
|
|
4887
|
+
status: r.status,
|
|
4888
|
+
tokens_in: r.tokens_in,
|
|
4889
|
+
tokens_out: r.tokens_out,
|
|
4890
|
+
tool_calls: r.tool_calls,
|
|
4891
|
+
kill_reason: r.kill_reason,
|
|
4892
|
+
last_reply_summary: typeof r.last_reply_summary === 'string'
|
|
4893
|
+
? String(r.last_reply_summary).slice(0, 240)
|
|
4894
|
+
: undefined,
|
|
4895
|
+
},
|
|
4896
|
+
});
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
catch { /* skip */ }
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
4902
|
+
/* 4. bridge messages -- scan every shard under
|
|
4903
|
+
* ~/.yujin-forge/bridge/ (bridge.json + bridge-{slug}.json).
|
|
4904
|
+
* Each shard's messages tag themselves with project_slug
|
|
4905
|
+
* derived from the filename so the activity feed knows which
|
|
4906
|
+
* project a message belongs to. */
|
|
4907
|
+
const bridgeDir = path.join(yfRoot, 'bridge');
|
|
4908
|
+
let bridgeShards = [];
|
|
4909
|
+
try {
|
|
4910
|
+
bridgeShards = (await fs.readdir(bridgeDir)).filter((n) => n.endsWith('.json') && (n === 'bridge.json' || n.startsWith('bridge-')));
|
|
4911
|
+
}
|
|
4912
|
+
catch { /* no bridge dir -- ok */ }
|
|
4913
|
+
for (const fname of bridgeShards) {
|
|
4914
|
+
const projectFromFile = fname === 'bridge.json'
|
|
4915
|
+
? 'self'
|
|
4916
|
+
: fname.slice('bridge-'.length, fname.length - '.json'.length) || 'self';
|
|
4917
|
+
try {
|
|
4918
|
+
const raw = await fs.readFile(path.join(bridgeDir, fname), 'utf-8');
|
|
4919
|
+
const b = JSON.parse(raw);
|
|
4920
|
+
const msgs = Array.isArray(b.messages) ? b.messages : [];
|
|
4921
|
+
/* Take last `limit` per shard to bound memory while not
|
|
4922
|
+
* losing recent activity. */
|
|
4923
|
+
const slice = msgs.length > limit ? msgs.slice(msgs.length - limit) : msgs;
|
|
4924
|
+
for (const m of slice) {
|
|
4925
|
+
const ts = m.sent_at ? new Date(Number(m.sent_at)).toISOString()
|
|
4926
|
+
: (m.created_at ? new Date(Number(m.created_at)).toISOString() : '');
|
|
4927
|
+
const msgProject = (typeof m.project_slug === 'string' && m.project_slug)
|
|
4928
|
+
? m.project_slug
|
|
4929
|
+
: projectFromFile;
|
|
4930
|
+
events.push({
|
|
4931
|
+
ts,
|
|
4932
|
+
kind: 'bridge.message',
|
|
4933
|
+
from: String(m.from_slug ?? m.from ?? '?'),
|
|
4934
|
+
to: String(m.to_slug ?? m.to ?? '?'),
|
|
4935
|
+
text: '[' + msgProject + '] '
|
|
4936
|
+
+ String(m.from_slug ?? m.from ?? '?') + ' -> ' + String(m.to_slug ?? m.to ?? '?')
|
|
4937
|
+
+ ': ' + String(m.payload ?? '').slice(0, 160),
|
|
4938
|
+
project_slug: msgProject,
|
|
4939
|
+
extra: { tokens: m.tokens_used, status: m.status, id: m.id },
|
|
4940
|
+
});
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
catch { /* skip unreadable shard */ }
|
|
4944
|
+
}
|
|
4945
|
+
/* Sort desc by ts (ISO strings sort correctly). Filter by since
|
|
4946
|
+
* + project_filter. */
|
|
4947
|
+
events.sort((a, b) => (a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0));
|
|
4948
|
+
let filtered = since ? events.filter((e) => e.ts > since) : events;
|
|
4949
|
+
if (projectFilter) {
|
|
4950
|
+
filtered = filtered.filter((e) => e.project_slug === projectFilter
|
|
4951
|
+
/* Events without a project_slug (legacy or non-project-
|
|
4952
|
+
* tagged kinds like dispatch.*) appear under 'self' so a
|
|
4953
|
+
* filter for the default project still sees them. */
|
|
4954
|
+
|| (!e.project_slug && projectFilter === 'self'));
|
|
4955
|
+
}
|
|
4956
|
+
const final = filtered.slice(0, limit);
|
|
4957
|
+
sendJson(res, 200, { ok: true, count: final.length, project_filter: projectFilter || null, events: final });
|
|
4958
|
+
}
|
|
4959
|
+
async function handleAgentBridgeTail(req, res, ctx) {
|
|
4960
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
4961
|
+
const limit = clampInt(url.searchParams.get('limit'), 100, 1, 1000);
|
|
4962
|
+
const pairId = (url.searchParams.get('pair_id') || '').trim();
|
|
4963
|
+
/* Slice 1 N-projects sprint -- optional project filter. Empty
|
|
4964
|
+
* means "scan every shard". 'self' means only the default
|
|
4965
|
+
* (legacy bridge.json). */
|
|
4966
|
+
const projectFilter = (url.searchParams.get('project_slug') || '').trim();
|
|
4967
|
+
const yfRoot = agentsRootDefault(ctx);
|
|
4968
|
+
const bridgeDir = path.join(yfRoot, 'bridge');
|
|
4969
|
+
/* Discover every shard file, then read whichever ones match
|
|
4970
|
+
* the filter. */
|
|
4971
|
+
let shardFiles = [];
|
|
4972
|
+
try {
|
|
4973
|
+
shardFiles = (await fs.readdir(bridgeDir)).filter((n) => n.endsWith('.json') && (n === 'bridge.json' || n.startsWith('bridge-')));
|
|
4974
|
+
}
|
|
4975
|
+
catch {
|
|
4976
|
+
sendJson(res, 200, { ok: true, pairs: [], messages: [], pair_state: {}, project_filter: projectFilter || null });
|
|
4977
|
+
return;
|
|
4978
|
+
}
|
|
4979
|
+
/* Load every shard. msgs carries an extra `_project_slug` for
|
|
4980
|
+
* downstream filtering. pair_state aggregated per-shard. */
|
|
4981
|
+
const allMsgs = [];
|
|
4982
|
+
const allPairState = {};
|
|
4983
|
+
for (const fname of shardFiles) {
|
|
4984
|
+
const project = fname === 'bridge.json'
|
|
4985
|
+
? 'self'
|
|
4986
|
+
: fname.slice('bridge-'.length, fname.length - '.json'.length) || 'self';
|
|
4987
|
+
if (projectFilter && projectFilter !== project)
|
|
4988
|
+
continue;
|
|
4989
|
+
try {
|
|
4990
|
+
const raw = await fs.readFile(path.join(bridgeDir, fname), 'utf-8');
|
|
4991
|
+
const b = JSON.parse(raw);
|
|
4992
|
+
const ms = Array.isArray(b.messages) ? b.messages : [];
|
|
4993
|
+
for (const m of ms)
|
|
4994
|
+
allMsgs.push({ ...m, _project_slug: project });
|
|
4995
|
+
if (b.pair_state) {
|
|
4996
|
+
for (const [pid, st] of Object.entries(b.pair_state)) {
|
|
4997
|
+
allPairState[pid + '@' + project] = { project_slug: project, state: st };
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
5001
|
+
catch { /* skip unreadable shard */ }
|
|
5002
|
+
}
|
|
5003
|
+
/* When pair_id absent: aggregate by pair (cross-shard). The
|
|
5004
|
+
* project_slug is part of the visible pair card so two pairs
|
|
5005
|
+
* with the same from/to but different projects don't merge. */
|
|
5006
|
+
if (!pairId) {
|
|
5007
|
+
const pairs = {};
|
|
5008
|
+
for (const m of allMsgs) {
|
|
5009
|
+
const fromSlug = String(m.from_slug ?? m.from ?? '?');
|
|
5010
|
+
const toSlug = String(m.to_slug ?? m.to ?? '?');
|
|
5011
|
+
const proj = m._project_slug;
|
|
5012
|
+
const key = fromSlug + '__' + toSlug + (proj === 'self' ? '' : '@' + proj);
|
|
5013
|
+
const ts = m.sent_at ? new Date(Number(m.sent_at)).toISOString()
|
|
5014
|
+
: (m.created_at ? new Date(Number(m.created_at)).toISOString() : '');
|
|
5015
|
+
if (!pairs[key]) {
|
|
5016
|
+
pairs[key] = { from: fromSlug, to: toSlug, count: 0, last_ts: ts, project_slug: proj };
|
|
5017
|
+
}
|
|
5018
|
+
pairs[key].count++;
|
|
5019
|
+
if (ts > pairs[key].last_ts)
|
|
5020
|
+
pairs[key].last_ts = ts;
|
|
5021
|
+
}
|
|
5022
|
+
const list = Object.entries(pairs).map(([k, v]) => ({ pair_id: k, ...v }))
|
|
5023
|
+
.sort((a, b) => (a.last_ts < b.last_ts ? 1 : -1));
|
|
5024
|
+
sendJson(res, 200, { ok: true, pairs: list, project_filter: projectFilter || null });
|
|
5025
|
+
return;
|
|
5026
|
+
}
|
|
5027
|
+
/* With pair_id: parse out optional @project suffix; filter
|
|
5028
|
+
* messages newest first, capped. */
|
|
5029
|
+
const atIdx = pairId.lastIndexOf('@');
|
|
5030
|
+
const pairCore = atIdx >= 0 ? pairId.slice(0, atIdx) : pairId;
|
|
5031
|
+
const pairProject = atIdx >= 0 ? pairId.slice(atIdx + 1) : 'self';
|
|
5032
|
+
const filtered = allMsgs.filter((m) => {
|
|
5033
|
+
const fromSlug = String(m.from_slug ?? m.from ?? '?');
|
|
5034
|
+
const toSlug = String(m.to_slug ?? m.to ?? '?');
|
|
5035
|
+
return (fromSlug + '__' + toSlug) === pairCore && m._project_slug === pairProject;
|
|
5036
|
+
});
|
|
5037
|
+
const slice = filtered.length > limit ? filtered.slice(filtered.length - limit) : filtered;
|
|
5038
|
+
slice.sort((a, b) => Number(b.sent_at ?? 0) - Number(a.sent_at ?? 0));
|
|
5039
|
+
/* Find matching pair_state from the per-shard map. */
|
|
5040
|
+
const pairStateKey = Object.keys(allPairState).find((k) => k.endsWith('@' + pairProject))
|
|
5041
|
+
|| null;
|
|
5042
|
+
sendJson(res, 200, {
|
|
5043
|
+
ok: true,
|
|
5044
|
+
pair_id: pairId,
|
|
5045
|
+
project_slug: pairProject,
|
|
5046
|
+
count: slice.length,
|
|
5047
|
+
messages: slice,
|
|
5048
|
+
pair_state: pairStateKey ? allPairState[pairStateKey].state : null,
|
|
5049
|
+
});
|
|
5050
|
+
}
|
|
5051
|
+
async function handleAgentInject(req, res, ctx) {
|
|
5052
|
+
let body;
|
|
5053
|
+
try {
|
|
5054
|
+
body = JSON.parse(await readBody(req));
|
|
5055
|
+
}
|
|
5056
|
+
catch {
|
|
5057
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
|
|
5058
|
+
return;
|
|
5059
|
+
}
|
|
5060
|
+
const slug = typeof body.slug === 'string' ? body.slug.trim() : '';
|
|
5061
|
+
const text = typeof body.text === 'string' ? body.text.trim() : '';
|
|
5062
|
+
if (!slug) {
|
|
5063
|
+
sendJson(res, 400, { ok: false, error: 'slug required' });
|
|
5064
|
+
return;
|
|
5065
|
+
}
|
|
5066
|
+
if (!text) {
|
|
5067
|
+
sendJson(res, 400, { ok: false, error: 'text required' });
|
|
5068
|
+
return;
|
|
5069
|
+
}
|
|
5070
|
+
const doDispatch = body.dispatch !== false;
|
|
5071
|
+
/* Slice 1 N-projects sprint -- explicit project_slug routes
|
|
5072
|
+
* the user message to that project's conversation/run instance.
|
|
5073
|
+
* Defaults to 'self' for legacy single-project behaviour. */
|
|
5074
|
+
const project_slug = (typeof body.project_slug === 'string' && body.project_slug.trim())
|
|
5075
|
+
? body.project_slug.trim()
|
|
5076
|
+
: 'self';
|
|
5077
|
+
const yfRoot = agentsRootDefault(ctx);
|
|
5078
|
+
const convDir = path.join(yfRoot, 'agents', 'by-slug', slug, 'conversations');
|
|
5079
|
+
/* Pick the most recently modified conversation file, or create
|
|
5080
|
+
* one named "manual" if none exists yet. */
|
|
5081
|
+
let convFile = path.join(convDir, 'manual.json');
|
|
5082
|
+
try {
|
|
5083
|
+
const files = (await fs.readdir(convDir)).filter((n) => n.endsWith('.json'));
|
|
5084
|
+
if (files.length > 0) {
|
|
5085
|
+
const stats = await Promise.all(files.map(async (n) => ({
|
|
5086
|
+
name: n,
|
|
5087
|
+
mtime: (await fs.stat(path.join(convDir, n))).mtimeMs,
|
|
5088
|
+
})));
|
|
5089
|
+
stats.sort((a, b) => b.mtime - a.mtime);
|
|
5090
|
+
convFile = path.join(convDir, stats[0].name);
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
catch {
|
|
5094
|
+
/* convDir does not exist yet -- mkdir below + start manual. */
|
|
5095
|
+
}
|
|
5096
|
+
let messages = [];
|
|
5097
|
+
try {
|
|
5098
|
+
const raw = await fs.readFile(convFile, 'utf-8');
|
|
5099
|
+
const parsed = JSON.parse(raw);
|
|
5100
|
+
if (parsed && Array.isArray(parsed.messages))
|
|
5101
|
+
messages = parsed.messages;
|
|
5102
|
+
}
|
|
5103
|
+
catch { /* fresh */ }
|
|
5104
|
+
messages.push({ role: 'user', content: text });
|
|
5105
|
+
try {
|
|
5106
|
+
await fs.mkdir(convDir, { recursive: true });
|
|
5107
|
+
await fs.writeFile(convFile, JSON.stringify({ version: 1, messages }, null, 2), 'utf-8');
|
|
5108
|
+
}
|
|
5109
|
+
catch (err) {
|
|
5110
|
+
sendJson(res, 500, { ok: false, error: 'persist failed: ' + (err instanceof Error ? err.message : String(err)) });
|
|
5111
|
+
return;
|
|
5112
|
+
}
|
|
5113
|
+
if (doDispatch) {
|
|
5114
|
+
if (!ctx.runtime) {
|
|
5115
|
+
sendJson(res, 200, {
|
|
5116
|
+
ok: true, persisted: convFile, dispatched: false,
|
|
5117
|
+
note: 'runtime not wired -- message saved but no run fired',
|
|
5118
|
+
});
|
|
5119
|
+
return;
|
|
5120
|
+
}
|
|
5121
|
+
try {
|
|
5122
|
+
const run = await ctx.runtime.dispatchAgent(slug, 'user-injected: ' + text.slice(0, 80), { project_slug });
|
|
5123
|
+
sendJson(res, 200, {
|
|
5124
|
+
ok: true, persisted: convFile, dispatched: true,
|
|
5125
|
+
run_status: run.status, run_id: run.id, last_reply: run.last_reply_summary,
|
|
5126
|
+
project_slug: run.project_slug ?? project_slug,
|
|
5127
|
+
});
|
|
5128
|
+
}
|
|
5129
|
+
catch (err) {
|
|
5130
|
+
sendJson(res, 200, {
|
|
5131
|
+
ok: true, persisted: convFile, dispatched: false,
|
|
5132
|
+
error: err instanceof Error ? err.message : String(err),
|
|
5133
|
+
});
|
|
5134
|
+
}
|
|
5135
|
+
return;
|
|
5136
|
+
}
|
|
5137
|
+
sendJson(res, 200, { ok: true, persisted: convFile, dispatched: false });
|
|
5138
|
+
}
|
|
5139
|
+
function clampInt(raw, fallback, lo, hi) {
|
|
5140
|
+
const n = raw != null ? Number(raw) : fallback;
|
|
5141
|
+
if (!Number.isFinite(n))
|
|
5142
|
+
return fallback;
|
|
5143
|
+
return Math.max(lo, Math.min(hi, Math.floor(n)));
|
|
5144
|
+
}
|
|
5145
|
+
/** Serve a legal doc (privacy policy / limited responsibility) as
|
|
5146
|
+
* text/markdown. Reads from dist/docs/<basename> (copied at build
|
|
5147
|
+
* time by scripts/copy_qa_assets.mjs). Names are static + sanitised
|
|
5148
|
+
* by the route caller -- no user input reaches the filesystem
|
|
5149
|
+
* path. */
|
|
5150
|
+
async function serveLegalDoc(res, basename) {
|
|
5151
|
+
/* The CLI bundle's __dirname at runtime points at dist/chat/.
|
|
5152
|
+
* Hop back to dist/ then into docs/. */
|
|
5153
|
+
const url = await import('node:url');
|
|
5154
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
5155
|
+
const distRoot = path.resolve(here, '..');
|
|
5156
|
+
const docPath = path.join(distRoot, 'docs', basename);
|
|
5157
|
+
try {
|
|
5158
|
+
const md = await fs.readFile(docPath, 'utf-8');
|
|
5159
|
+
res.statusCode = 200;
|
|
5160
|
+
res.setHeader('content-type', 'text/markdown; charset=utf-8');
|
|
5161
|
+
res.setHeader('cache-control', 'no-store, must-revalidate');
|
|
5162
|
+
res.end(md);
|
|
5163
|
+
}
|
|
5164
|
+
catch (err) {
|
|
5165
|
+
res.statusCode = 404;
|
|
5166
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
5167
|
+
res.end(JSON.stringify({ ok: false, error: 'legal doc not found', detail: basename }));
|
|
5168
|
+
/* Surface for ops; not user-visible. */
|
|
5169
|
+
console.warn('[serveLegalDoc] ' + docPath + ': ' + (err instanceof Error ? err.message : String(err)));
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
1844
5172
|
/** Resolve the active Forge mode for a single request.
|
|
1845
5173
|
* Layer order: yf-mode cookie -> yujin.forge.json -> default.
|
|
1846
5174
|
* Stays out of server-wide state for race-safe concurrent
|
|
@@ -1940,11 +5268,16 @@ async function readBinaryBody(req, maxBytes) {
|
|
|
1940
5268
|
return Buffer.concat(chunks);
|
|
1941
5269
|
}
|
|
1942
5270
|
async function readProjectName(projectRoot) {
|
|
5271
|
+
/* Display name. Prefers project_name (human-readable) over
|
|
5272
|
+
* project_slug (kebab/lower id). For graph identity use
|
|
5273
|
+
* resolveProjectSlug (chat/project_slug.ts). */
|
|
1943
5274
|
try {
|
|
1944
5275
|
const raw = await fs.readFile(path.join(projectRoot, 'yujin.forge.json'), 'utf-8');
|
|
1945
5276
|
const parsed = JSON.parse(raw);
|
|
1946
5277
|
if (parsed && typeof parsed.project_name === 'string')
|
|
1947
5278
|
return parsed.project_name;
|
|
5279
|
+
if (parsed && typeof parsed.project_slug === 'string')
|
|
5280
|
+
return parsed.project_slug;
|
|
1948
5281
|
}
|
|
1949
5282
|
catch { /* fall through */ }
|
|
1950
5283
|
try {
|