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