xcoder-dna 0.0.0-stage → 1.3.0
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/LICENSE +66 -0
- package/README.md +87 -2
- package/dist/agents/ai-chat-debate.d.ts +36 -0
- package/dist/agents/ai-chat-debate.js +300 -0
- package/dist/agents/ai-chat-report-writer.d.ts +7 -0
- package/dist/agents/ai-chat-report-writer.js +100 -0
- package/dist/agents/duplicate-detector.d.ts +41 -0
- package/dist/agents/duplicate-detector.js +65 -0
- package/dist/agents/genesis-agent.d.ts +61 -0
- package/dist/agents/genesis-agent.js +340 -0
- package/dist/agents/samurai-agent.d.ts +53 -0
- package/dist/agents/samurai-agent.js +279 -0
- package/dist/agents/sub-agent.d.ts +56 -0
- package/dist/agents/sub-agent.js +353 -0
- package/dist/backlog/backlog-requirement-validator.d.ts +10 -0
- package/dist/backlog/backlog-requirement-validator.js +60 -0
- package/dist/backlog/backlog-store.d.ts +29 -0
- package/dist/backlog/backlog-store.js +148 -0
- package/dist/backlog/po-agent.d.ts +16 -0
- package/dist/backlog/po-agent.js +121 -0
- package/dist/backlog/types.d.ts +25 -0
- package/dist/backlog/types.js +4 -0
- package/dist/bench/run.d.ts +11 -0
- package/dist/bench/run.js +185 -0
- package/dist/bench/types.d.ts +50 -0
- package/dist/bench/types.js +1 -0
- package/dist/cli/swarm.d.ts +10 -0
- package/dist/cli/swarm.js +100 -0
- package/dist/cli/task.d.ts +14 -0
- package/dist/cli/task.js +121 -0
- package/dist/cli/wbc-arg.d.ts +6 -0
- package/dist/cli/wbc-arg.js +40 -0
- package/dist/cli/wbc.d.ts +45 -0
- package/dist/cli/wbc.js +98 -0
- package/dist/commands/license.d.ts +9 -0
- package/dist/commands/license.js +140 -0
- package/dist/commands/release.d.ts +44 -0
- package/dist/commands/release.js +180 -0
- package/dist/commands/test.d.ts +50 -0
- package/dist/commands/test.js +385 -0
- package/dist/config/loadConfig.d.ts +82 -0
- package/dist/config/loadConfig.js +240 -0
- package/dist/config/secure-mode.d.ts +29 -0
- package/dist/config/secure-mode.js +63 -0
- package/dist/config/user-config.d.ts +39 -0
- package/dist/config/user-config.js +202 -0
- package/dist/engine/reasoning-engine.d.ts +21 -0
- package/dist/engine/reasoning-engine.js +59 -0
- package/dist/events/bus.d.ts +22 -0
- package/dist/events/bus.js +18 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +77 -0
- package/dist/indexing/ignoreRules.d.ts +1 -0
- package/dist/indexing/ignoreRules.js +53 -0
- package/dist/instance-swarm/instance-swarm-orchestrator.d.ts +57 -0
- package/dist/instance-swarm/instance-swarm-orchestrator.js +204 -0
- package/dist/instance-swarm/protocol-name-resolver.d.ts +23 -0
- package/dist/instance-swarm/protocol-name-resolver.js +64 -0
- package/dist/license/gate.d.ts +31 -0
- package/dist/license/gate.js +188 -0
- package/dist/license/http-client.d.ts +43 -0
- package/dist/license/http-client.js +195 -0
- package/dist/license/manager.d.ts +39 -0
- package/dist/license/manager.js +175 -0
- package/dist/license/receipt.d.ts +27 -0
- package/dist/license/receipt.js +108 -0
- package/dist/license/state-codec.d.ts +72 -0
- package/dist/license/state-codec.js +218 -0
- package/dist/license/state-paths.d.ts +17 -0
- package/dist/license/state-paths.js +64 -0
- package/dist/license/store.d.ts +44 -0
- package/dist/license/store.js +223 -0
- package/dist/license/trust-anchor.d.ts +24 -0
- package/dist/license/trust-anchor.js +74 -0
- package/dist/license/types.d.ts +34 -0
- package/dist/license/types.js +19 -0
- package/dist/llm/budget.d.ts +23 -0
- package/dist/llm/budget.js +58 -0
- package/dist/llm/client.d.ts +53 -0
- package/dist/llm/client.js +122 -0
- package/dist/llm/factory.d.ts +14 -0
- package/dist/llm/factory.js +32 -0
- package/dist/orchestrator/defect-logger.d.ts +31 -0
- package/dist/orchestrator/defect-logger.js +101 -0
- package/dist/orchestrator/defect-manager.d.ts +118 -0
- package/dist/orchestrator/defect-manager.js +338 -0
- package/dist/orchestrator/defect-migration.d.ts +14 -0
- package/dist/orchestrator/defect-migration.js +174 -0
- package/dist/orchestrator/defect-store.d.ts +48 -0
- package/dist/orchestrator/defect-store.js +76 -0
- package/dist/orchestrator/healing.d.ts +14 -0
- package/dist/orchestrator/healing.js +11 -0
- package/dist/orchestrator/interactive-steering.d.ts +64 -0
- package/dist/orchestrator/interactive-steering.js +107 -0
- package/dist/orchestrator/interrupt-handler.d.ts +30 -0
- package/dist/orchestrator/interrupt-handler.js +70 -0
- package/dist/orchestrator/lesson-manager.d.ts +25 -0
- package/dist/orchestrator/lesson-manager.js +122 -0
- package/dist/orchestrator/progress-summary.d.ts +15 -0
- package/dist/orchestrator/progress-summary.js +106 -0
- package/dist/orchestrator/reasoning-score.d.ts +49 -0
- package/dist/orchestrator/reasoning-score.js +117 -0
- package/dist/orchestrator/swarm.d.ts +36 -0
- package/dist/orchestrator/swarm.js +77 -0
- package/dist/orchestrator/task-factory.d.ts +15 -0
- package/dist/orchestrator/task-factory.js +176 -0
- package/dist/orchestrator/validation-gate.d.ts +23 -0
- package/dist/orchestrator/validation-gate.js +155 -0
- package/dist/protocol/react-contract.d.ts +136 -0
- package/dist/protocol/react-contract.js +698 -0
- package/dist/protocol/wks-protocol-manager.d.ts +22 -0
- package/dist/protocol/wks-protocol-manager.js +72 -0
- package/dist/sdlc-runtime/conversation-runner.d.ts +8 -0
- package/dist/sdlc-runtime/conversation-runner.js +64 -0
- package/dist/sdlc-runtime/dag-utils.d.ts +15 -0
- package/dist/sdlc-runtime/dag-utils.js +67 -0
- package/dist/sdlc-runtime/defect-integration.d.ts +7 -0
- package/dist/sdlc-runtime/defect-integration.js +99 -0
- package/dist/sdlc-runtime/execution-loop.d.ts +38 -0
- package/dist/sdlc-runtime/execution-loop.js +274 -0
- package/dist/sdlc-runtime/lesson-recorder.d.ts +9 -0
- package/dist/sdlc-runtime/lesson-recorder.js +27 -0
- package/dist/sdlc-runtime/plan-initializer.d.ts +47 -0
- package/dist/sdlc-runtime/plan-initializer.js +135 -0
- package/dist/sdlc-runtime/progress-persister.d.ts +10 -0
- package/dist/sdlc-runtime/progress-persister.js +34 -0
- package/dist/sdlc-runtime/protocol-loader.d.ts +10 -0
- package/dist/sdlc-runtime/protocol-loader.js +21 -0
- package/dist/sdlc-runtime/reporting.d.ts +24 -0
- package/dist/sdlc-runtime/reporting.js +68 -0
- package/dist/sdlc-runtime/sdlc-orchestrator.d.ts +59 -0
- package/dist/sdlc-runtime/sdlc-orchestrator.js +170 -0
- package/dist/sdlc-runtime/sdlc-skill.d.ts +44 -0
- package/dist/sdlc-runtime/sdlc-skill.js +148 -0
- package/dist/sdlc-runtime/steering-handler.d.ts +14 -0
- package/dist/sdlc-runtime/steering-handler.js +51 -0
- package/dist/sdlc-runtime/trace-recorder.d.ts +10 -0
- package/dist/sdlc-runtime/trace-recorder.js +7 -0
- package/dist/security/index.d.ts +16 -0
- package/dist/security/index.js +12 -0
- package/dist/security/input-screen.d.ts +30 -0
- package/dist/security/input-screen.js +75 -0
- package/dist/security/tool-call-guard.d.ts +16 -0
- package/dist/security/tool-call-guard.js +163 -0
- package/dist/security/untrusted-content.d.ts +34 -0
- package/dist/security/untrusted-content.js +63 -0
- package/dist/skills/advanced-dfir/SKILL.md +85 -0
- package/dist/skills/advanced-pentest-lab/SKILL.md +88 -0
- package/dist/skills/analyst/SKILL.md +55 -0
- package/dist/skills/architect/SKILL.md +54 -0
- package/dist/skills/aws/SKILL.md +92 -0
- package/dist/skills/azure/SKILL.md +90 -0
- package/dist/skills/bylaw-context.d.ts +45 -0
- package/dist/skills/bylaw-context.js +61 -0
- package/dist/skills/bylaw-review.d.ts +20 -0
- package/dist/skills/bylaw-review.js +101 -0
- package/dist/skills/conversation/SKILL.md +29 -0
- package/dist/skills/design-patterns/SKILL.md +190 -0
- package/dist/skills/devops/SKILL.md +55 -0
- package/dist/skills/directives.d.ts +20 -0
- package/dist/skills/directives.js +313 -0
- package/dist/skills/docker/SKILL.md +76 -0
- package/dist/skills/docker-expert/SKILL.md +56 -0
- package/dist/skills/enterprise-systems-analyst/SKILL.md +83 -0
- package/dist/skills/enterprise-systems-analyst/references/crm.md +41 -0
- package/dist/skills/enterprise-systems-analyst/references/erp.md +40 -0
- package/dist/skills/enterprise-systems-analyst/references/financial-systems.md +35 -0
- package/dist/skills/enterprise-systems-analyst/references/hcm.md +38 -0
- package/dist/skills/enterprise-systems-analyst/references/mrp.md +37 -0
- package/dist/skills/enterprise-systems-analyst/references/scm.md +32 -0
- package/dist/skills/ethical-hacking-methodology/SKILL.md +119 -0
- package/dist/skills/externalSkillLoader.d.ts +25 -0
- package/dist/skills/externalSkillLoader.js +65 -0
- package/dist/skills/externalSkillRegistry.d.ts +33 -0
- package/dist/skills/externalSkillRegistry.js +94 -0
- package/dist/skills/filesystem-management/SKILL.md +61 -0
- package/dist/skills/first-run-seed.d.ts +26 -0
- package/dist/skills/first-run-seed.js +75 -0
- package/dist/skills/git-vcs/SKILL.md +84 -0
- package/dist/skills/kafka/SKILL.md +89 -0
- package/dist/skills/kubernetes/SKILL.md +93 -0
- package/dist/skills/kubernetes-expert/SKILL.md +54 -0
- package/dist/skills/openshift/SKILL.md +93 -0
- package/dist/skills/pentester/SKILL.md +59 -0
- package/dist/skills/performance-tester/SKILL.md +57 -0
- package/dist/skills/playwright-ui-tester/SKILL.md +67 -0
- package/dist/skills/programmer/SKILL.md +58 -0
- package/dist/skills/project-scaffolding/SKILL.md +133 -0
- package/dist/skills/qa-engineer/SKILL.md +91 -0
- package/dist/skills/rca/SKILL.md +58 -0
- package/dist/skills/redhat/SKILL.md +92 -0
- package/dist/skills/rosa/SKILL.md +80 -0
- package/dist/skills/scrum-framework/SKILL.md +74 -0
- package/dist/skills/scrum-framework/references/artifacts_and_quality.md +79 -0
- package/dist/skills/scrum-framework/references/roles_and_events.md +105 -0
- package/dist/skills/scrum-master-agent/SKILL.md +74 -0
- package/dist/skills/scrum-master-agent/references/behaviors.md +56 -0
- package/dist/skills/sdlc-skill.d.ts +1 -0
- package/dist/skills/sdlc-skill.js +4 -0
- package/dist/skills/secops/SKILL.md +52 -0
- package/dist/skills/secure-architect.bylaws.d.ts +68 -0
- package/dist/skills/secure-architect.bylaws.js +525 -0
- package/dist/skills/seed-skills.d.ts +48 -0
- package/dist/skills/seed-skills.js +213 -0
- package/dist/skills/skill-authoring/SKILL.md +123 -0
- package/dist/skills/skill-header.d.ts +51 -0
- package/dist/skills/skill-header.js +163 -0
- package/dist/skills/skill-path-guard.d.ts +41 -0
- package/dist/skills/skill-path-guard.js +95 -0
- package/dist/skills/skillRouter.d.ts +51 -0
- package/dist/skills/skillRouter.js +90 -0
- package/dist/skills/skills-root.d.ts +64 -0
- package/dist/skills/skills-root.js +168 -0
- package/dist/skills/software-architect/SKILL.md +77 -0
- package/dist/skills/software-engineer/SKILL.md +74 -0
- package/dist/skills/solid-principles/SKILL.md +94 -0
- package/dist/skills/task-planning/SKILL.md +115 -0
- package/dist/skills/tester/SKILL.md +54 -0
- package/dist/skills/ubuntu/SKILL.md +87 -0
- package/dist/skills/ui-ux-design/SKILL.md +114 -0
- package/dist/skills/workspace-context/SKILL.md +103 -0
- package/dist/sprint/controller.d.ts +37 -0
- package/dist/sprint/controller.js +220 -0
- package/dist/sprint/dag-builder.d.ts +14 -0
- package/dist/sprint/dag-builder.js +105 -0
- package/dist/sprint/reporter.d.ts +11 -0
- package/dist/sprint/reporter.js +97 -0
- package/dist/sprint/state-store.d.ts +10 -0
- package/dist/sprint/state-store.js +71 -0
- package/dist/sprint/types.d.ts +24 -0
- package/dist/sprint/types.js +4 -0
- package/dist/store/file-version-store.d.ts +24 -0
- package/dist/store/file-version-store.js +73 -0
- package/dist/store/task-store.d.ts +90 -0
- package/dist/store/task-store.js +166 -0
- package/dist/task-ingest/architect-detailer.d.ts +43 -0
- package/dist/task-ingest/architect-detailer.js +141 -0
- package/dist/task-ingest/detail-schema.d.ts +9 -0
- package/dist/task-ingest/detail-schema.js +195 -0
- package/dist/task-ingest/detailed-run.d.ts +36 -0
- package/dist/task-ingest/detailed-run.js +108 -0
- package/dist/task-ingest/extract-id.d.ts +9 -0
- package/dist/task-ingest/extract-id.js +24 -0
- package/dist/task-ingest/index.d.ts +23 -0
- package/dist/task-ingest/index.js +14 -0
- package/dist/task-ingest/ingest.d.ts +45 -0
- package/dist/task-ingest/ingest.js +163 -0
- package/dist/task-ingest/list-ingested.d.ts +13 -0
- package/dist/task-ingest/list-ingested.js +30 -0
- package/dist/task-ingest/pending-filter.d.ts +30 -0
- package/dist/task-ingest/pending-filter.js +59 -0
- package/dist/task-ingest/scanner.d.ts +56 -0
- package/dist/task-ingest/scanner.js +216 -0
- package/dist/task-ingest/source-reader.d.ts +25 -0
- package/dist/task-ingest/source-reader.js +93 -0
- package/dist/task-ingest/spec-builder.d.ts +34 -0
- package/dist/task-ingest/spec-builder.js +89 -0
- package/dist/task-ingest/wbs-extract.d.ts +50 -0
- package/dist/task-ingest/wbs-extract.js +226 -0
- package/dist/task-ingest/wbs-parser.d.ts +36 -0
- package/dist/task-ingest/wbs-parser.js +170 -0
- package/dist/tools/apiTestTool.d.ts +37 -0
- package/dist/tools/apiTestTool.js +128 -0
- package/dist/tools/ast-tools.d.ts +183 -0
- package/dist/tools/ast-tools.js +853 -0
- package/dist/tools/browse-tool.d.ts +105 -0
- package/dist/tools/browse-tool.js +394 -0
- package/dist/tools/credential.d.ts +17 -0
- package/dist/tools/credential.js +32 -0
- package/dist/tools/dependency-graph.d.ts +23 -0
- package/dist/tools/dependency-graph.js +164 -0
- package/dist/tools/edit-tool.d.ts +34 -0
- package/dist/tools/edit-tool.js +95 -0
- package/dist/tools/file-header-manager.d.ts +27 -0
- package/dist/tools/file-header-manager.js +153 -0
- package/dist/tools/fs-tools.d.ts +154 -0
- package/dist/tools/fs-tools.js +337 -0
- package/dist/tools/git-tools.d.ts +79 -0
- package/dist/tools/git-tools.js +348 -0
- package/dist/tools/github-tool.d.ts +48 -0
- package/dist/tools/github-tool.js +147 -0
- package/dist/tools/globTool.d.ts +20 -0
- package/dist/tools/globTool.js +34 -0
- package/dist/tools/grepTool.d.ts +19 -0
- package/dist/tools/grepTool.js +44 -0
- package/dist/tools/index-freshness.d.ts +58 -0
- package/dist/tools/index-freshness.js +190 -0
- package/dist/tools/index-glob.d.ts +48 -0
- package/dist/tools/index-glob.js +175 -0
- package/dist/tools/load-skill-tool.d.ts +15 -0
- package/dist/tools/load-skill-tool.js +114 -0
- package/dist/tools/patch-tools.d.ts +58 -0
- package/dist/tools/patch-tools.js +153 -0
- package/dist/tools/playwright-tool.d.ts +66 -0
- package/dist/tools/playwright-tool.js +121 -0
- package/dist/tools/project-detect.d.ts +20 -0
- package/dist/tools/project-detect.js +87 -0
- package/dist/tools/prompt-guidance.d.ts +22 -0
- package/dist/tools/prompt-guidance.js +23 -0
- package/dist/tools/scheduleTool.d.ts +19 -0
- package/dist/tools/scheduleTool.js +181 -0
- package/dist/tools/search-tools.d.ts +27 -0
- package/dist/tools/search-tools.js +78 -0
- package/dist/tools/sed-tool.d.ts +66 -0
- package/dist/tools/sed-tool.js +402 -0
- package/dist/tools/shell-result.d.ts +11 -0
- package/dist/tools/shell-result.js +1 -0
- package/dist/tools/shell-tool.d.ts +45 -0
- package/dist/tools/shell-tool.js +141 -0
- package/dist/tools/siteCrawlerTool.d.ts +32 -0
- package/dist/tools/siteCrawlerTool.js +205 -0
- package/dist/tools/tool-registry.d.ts +67 -0
- package/dist/tools/tool-registry.js +596 -0
- package/dist/tools/validate-tool.d.ts +23 -0
- package/dist/tools/validate-tool.js +201 -0
- package/dist/tools/workspace-index.d.ts +46 -0
- package/dist/tools/workspace-index.js +173 -0
- package/dist/types.d.ts +222 -0
- package/dist/types.js +7 -0
- package/dist/updater/index.d.ts +14 -0
- package/dist/updater/index.js +120 -0
- package/dist/util/banner.d.ts +25 -0
- package/dist/util/banner.js +140 -0
- package/dist/util/command-suggester.d.ts +6 -0
- package/dist/util/command-suggester.js +25 -0
- package/dist/util/confirm-interrupt.d.ts +10 -0
- package/dist/util/confirm-interrupt.js +31 -0
- package/dist/util/continue-trigger.d.ts +11 -0
- package/dist/util/continue-trigger.js +20 -0
- package/dist/util/env-info.d.ts +18 -0
- package/dist/util/env-info.js +37 -0
- package/dist/util/file-access-log.d.ts +35 -0
- package/dist/util/file-access-log.js +140 -0
- package/dist/util/footer-manager.d.ts +124 -0
- package/dist/util/footer-manager.js +195 -0
- package/dist/util/footer-renderer.d.ts +67 -0
- package/dist/util/footer-renderer.js +319 -0
- package/dist/util/footer-state.d.ts +34 -0
- package/dist/util/footer-state.js +18 -0
- package/dist/util/footer-text.d.ts +51 -0
- package/dist/util/footer-text.js +74 -0
- package/dist/util/ids.d.ts +1 -0
- package/dist/util/ids.js +4 -0
- package/dist/util/levenshtein.d.ts +6 -0
- package/dist/util/levenshtein.js +34 -0
- package/dist/util/logger.d.ts +10 -0
- package/dist/util/logger.js +41 -0
- package/dist/util/logs.d.ts +26 -0
- package/dist/util/logs.js +88 -0
- package/dist/util/output-handler.d.ts +89 -0
- package/dist/util/output-handler.js +193 -0
- package/dist/util/output-interceptor.d.ts +18 -0
- package/dist/util/output-interceptor.js +49 -0
- package/dist/util/output-paths.d.ts +43 -0
- package/dist/util/output-paths.js +121 -0
- package/dist/util/package-json.d.ts +6 -0
- package/dist/util/package-json.js +36 -0
- package/dist/util/process-kill.d.ts +27 -0
- package/dist/util/process-kill.js +148 -0
- package/dist/util/protocol-error-logger.d.ts +33 -0
- package/dist/util/protocol-error-logger.js +73 -0
- package/dist/util/redact.d.ts +25 -0
- package/dist/util/redact.js +103 -0
- package/dist/util/scroll-region-guard.d.ts +33 -0
- package/dist/util/scroll-region-guard.js +63 -0
- package/dist/util/spinner.d.ts +15 -0
- package/dist/util/spinner.js +49 -0
- package/dist/util/terminal-output.d.ts +34 -0
- package/dist/util/terminal-output.js +1 -0
- package/dist/util/terminal-silent.d.ts +6 -0
- package/dist/util/terminal-silent.js +20 -0
- package/dist/util/terminal.d.ts +56 -0
- package/dist/util/terminal.js +124 -0
- package/dist/util/text-sanitize.d.ts +8 -0
- package/dist/util/text-sanitize.js +45 -0
- package/dist/util/thinking-spinner.d.ts +27 -0
- package/dist/util/thinking-spinner.js +189 -0
- package/dist/util/tool-error-logger.d.ts +33 -0
- package/dist/util/tool-error-logger.js +77 -0
- package/dist/util/tty.d.ts +13 -0
- package/dist/util/tty.js +16 -0
- package/dist/util/xcoder-paths.d.ts +56 -0
- package/dist/util/xcoder-paths.js +63 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/dist/worker-agent/to-subtask-node.d.ts +35 -0
- package/dist/worker-agent/to-subtask-node.js +41 -0
- package/dist/worker-agent/worker-orchestrator.d.ts +104 -0
- package/dist/worker-agent/worker-orchestrator.js +201 -0
- package/package.json +39 -4
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// xcoder:version 3 | xcoder:task task-f9cf18 | xcoder:updated 2026-11-02T16:14:36.637Z | xcoder:subtask code-st-077ea2
|
|
2
|
+
// LLM config — per-role provider configuration (design: llm-provider-config)
|
|
3
|
+
import dotenv from "dotenv";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import * as yaml from "js-yaml";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import { log } from "../util/logger.js";
|
|
9
|
+
import { createXcoderPaths } from "../util/xcoder-paths.js";
|
|
10
|
+
import { getAppDir, resolveSkillsRoot } from "../skills/skills-root.js";
|
|
11
|
+
import { applyUserConfig, getActiveUserConfig } from "./user-config.js";
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Environment Initialization (Must run before reading process.env)
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// getAppDir now lives in skills-root.ts (the single source of truth for skill
|
|
16
|
+
// discovery) and is re-exported here so existing importers keep working.
|
|
17
|
+
export { getAppDir };
|
|
18
|
+
function loadXcoderEnv() {
|
|
19
|
+
const envCandidates = [];
|
|
20
|
+
// 1. App directory (where xcoder.exe / app bundle lives)
|
|
21
|
+
const appDir = getAppDir();
|
|
22
|
+
envCandidates.push(path.join(appDir, ".env"));
|
|
23
|
+
// 2. XCODER_HOME directory (if defined)
|
|
24
|
+
if (process.env.XCODER_HOME) {
|
|
25
|
+
envCandidates.push(path.join(process.env.XCODER_HOME, ".env"));
|
|
26
|
+
}
|
|
27
|
+
// 3. User's global home directory (~/.xcoder/.env or C:\Users\<user>\.xcoder\.env)
|
|
28
|
+
const globalXcoderDir = path.join(os.homedir(), ".xcoder");
|
|
29
|
+
envCandidates.push(path.join(globalXcoderDir, ".env"));
|
|
30
|
+
// Ensure global ~/.xcoder directory exists for users
|
|
31
|
+
if (!fs.existsSync(globalXcoderDir)) {
|
|
32
|
+
fs.mkdirSync(globalXcoderDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
// Load global/system candidate .env files first
|
|
35
|
+
for (const envPath of envCandidates) {
|
|
36
|
+
if (fs.existsSync(envPath)) {
|
|
37
|
+
dotenv.config({ path: envPath });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// 4. Current working directory .env (highest priority local overrides)
|
|
41
|
+
const cwdEnvPath = path.join(process.cwd(), ".env");
|
|
42
|
+
if (fs.existsSync(cwdEnvPath)) {
|
|
43
|
+
dotenv.config({ path: cwdEnvPath, override: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Execute environment loader immediately on module load
|
|
47
|
+
loadXcoderEnv();
|
|
48
|
+
// ~/.xcoder/config.json (provider / key / model / base_url / env) — beats .env files, loses to the real shell env.
|
|
49
|
+
applyUserConfig();
|
|
50
|
+
// Back-compat: accept legacy RONIN_* variables from before the rebrand.
|
|
51
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
52
|
+
if (key.startsWith("RONIN_")) {
|
|
53
|
+
const renamed = `XCODER_${key.slice("RONIN_".length)}`;
|
|
54
|
+
if (process.env[renamed] === undefined)
|
|
55
|
+
process.env[renamed] = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export const AGENT_ROLES = [
|
|
59
|
+
"orchestrator",
|
|
60
|
+
"sub_agent",
|
|
61
|
+
"swarm_agent",
|
|
62
|
+
"instance_agent",
|
|
63
|
+
];
|
|
64
|
+
const RELATIVE_CONFIG_PATH = ".xcoder/config/llm.yaml";
|
|
65
|
+
const RELATIVE_HOME_PATH = path.join("./");
|
|
66
|
+
const RELATIVE_AGENT_PATH = ".xcoder";
|
|
67
|
+
/**
|
|
68
|
+
* Shared candidate-path resolution used by resolveSkillsPath, resolveConfigPath,
|
|
69
|
+
* and resolveAgentPath. Checks, in order:
|
|
70
|
+
* 1. App directory (where this source file/code is running from)
|
|
71
|
+
* 2. XCODER_HOME env variable (if set)
|
|
72
|
+
* 3. Current working directory
|
|
73
|
+
* 4. User's home directory
|
|
74
|
+
* Returns the first candidate that exists on disk, or the app-directory
|
|
75
|
+
* candidate as a fallback if none exist (so callers always get a usable
|
|
76
|
+
* default path even when nothing has been created yet).
|
|
77
|
+
*
|
|
78
|
+
* Set XCODER_DEBUG_CONFIG_PATH=1 to log which candidate was selected.
|
|
79
|
+
*/
|
|
80
|
+
function resolveFirstExisting(relativePath) {
|
|
81
|
+
const appDir = getAppDir();
|
|
82
|
+
const candidatePaths = [path.join(appDir, relativePath)];
|
|
83
|
+
if (process.env.XCODER_HOME) {
|
|
84
|
+
candidatePaths.push(path.join(process.env.XCODER_HOME, relativePath));
|
|
85
|
+
}
|
|
86
|
+
candidatePaths.push(path.join(process.cwd(), relativePath));
|
|
87
|
+
candidatePaths.push(path.join(os.homedir(), relativePath));
|
|
88
|
+
for (const candidate of candidatePaths) {
|
|
89
|
+
if (fs.existsSync(candidate)) {
|
|
90
|
+
if (process.env.XCODER_DEBUG_CONFIG_PATH) {
|
|
91
|
+
log.dim(`configPath: ${candidate}`);
|
|
92
|
+
}
|
|
93
|
+
return candidate;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Nothing found anywhere — fall back to the app-directory candidate so
|
|
97
|
+
// downstream code (e.g. loadLlmConfig) still gets a deterministic path
|
|
98
|
+
// and applies its own defaults.
|
|
99
|
+
return candidatePaths[0] ?? path.join(process.cwd(), RELATIVE_CONFIG_PATH);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Returns a directory D such that `D/skills` is the resolved skills directory.
|
|
103
|
+
*
|
|
104
|
+
* @deprecated Use `resolveSkillsRoot()` from `src/skills/skills-root.ts` — it
|
|
105
|
+
* is the single source of truth and also tells you which candidate won and
|
|
106
|
+
* how many packs are actually usable. This wrapper is kept only so existing
|
|
107
|
+
* importers do not break.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveSkillsPath() {
|
|
110
|
+
return resolveSkillsRoot().root;
|
|
111
|
+
}
|
|
112
|
+
export function resolveConfigPath() {
|
|
113
|
+
return resolveFirstExisting(RELATIVE_HOME_PATH);
|
|
114
|
+
}
|
|
115
|
+
export function resolveAgentPath() {
|
|
116
|
+
return resolveFirstExisting(RELATIVE_AGENT_PATH);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Built-in DeepSeek default — used when no config file exists and as the
|
|
120
|
+
* fallback for every role. Reads XCODER_MODEL lazily so env changes between
|
|
121
|
+
* calls are honored.
|
|
122
|
+
*
|
|
123
|
+
* Provider `deepseek` is served through DeepSeek's Anthropic-compatible
|
|
124
|
+
* endpoint (`@anthropic-ai/sdk`, which appends `/v1/messages` to the base
|
|
125
|
+
* URL), so the canonical root is `https://api.deepseek.com/anthropic` —
|
|
126
|
+
* NOT the OpenAI-compatible `https://api.deepseek.com/v1`, which would
|
|
127
|
+
* produce the invalid `.../v1/v1/messages` request path.
|
|
128
|
+
*/
|
|
129
|
+
export function deepseekDefaultConfig() {
|
|
130
|
+
return {
|
|
131
|
+
provider: "deepseek",
|
|
132
|
+
base_url: "https://api.deepseek.com/anthropic",
|
|
133
|
+
model: process.env.XCODER_MODEL ?? "deepseek-chat",
|
|
134
|
+
api_key_env: "DEEPSEEK_API_KEY",
|
|
135
|
+
max_tokens: 16384,
|
|
136
|
+
temperature: 0.0,
|
|
137
|
+
thinking: false,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Read `.xcoder/config/llm.yaml` and return the raw per-role schema.
|
|
142
|
+
* Merge happens in `resolveLlmConfig`, not here. A legacy file shaped as a
|
|
143
|
+
* single provider block is interpreted as the `default` block.
|
|
144
|
+
*/
|
|
145
|
+
export function loadLlmConfigFile(configPath) {
|
|
146
|
+
const resolved = configPath ?? createXcoderPaths(process.cwd()).llmConfigPath();
|
|
147
|
+
if (!fs.existsSync(resolved)) {
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
const raw = yaml.load(fs.readFileSync(resolved, "utf-8"));
|
|
151
|
+
if (!raw || typeof raw !== "object") {
|
|
152
|
+
return {};
|
|
153
|
+
}
|
|
154
|
+
const obj = raw;
|
|
155
|
+
// New per-role schema wins even when the file also carries legacy
|
|
156
|
+
// top-level keys (e.g. a partially migrated file with both a legacy
|
|
157
|
+
// `provider:` block and `default`/role blocks). Treating such a file as
|
|
158
|
+
// a single legacy `default` block silently discards every role block.
|
|
159
|
+
if ("default" in obj ||
|
|
160
|
+
AGENT_ROLES.some((role) => obj[role] && typeof obj[role] === "object")) {
|
|
161
|
+
return obj;
|
|
162
|
+
}
|
|
163
|
+
if ("provider" in obj || "model" in obj) {
|
|
164
|
+
return { default: obj };
|
|
165
|
+
}
|
|
166
|
+
return obj;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Resolve the effective LLM config for a role.
|
|
170
|
+
* Precedence: role block → `default` block → built-in DeepSeek default.
|
|
171
|
+
* `XCODER_MODEL` is applied as an env override for `model` only.
|
|
172
|
+
*/
|
|
173
|
+
const PROVIDER_DEFAULTS = {
|
|
174
|
+
deepseek: { base_url: "https://api.deepseek.com/anthropic", model: "deepseek-chat" },
|
|
175
|
+
anthropic: {},
|
|
176
|
+
"openai-compatible": {},
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Overlay `~/.xcoder/config.json` (provider / key / model / base_url) on a
|
|
180
|
+
* resolved block. Switching provider also resets base_url to that provider's
|
|
181
|
+
* default, so a key is never sent to the previous provider's host.
|
|
182
|
+
*/
|
|
183
|
+
export function applyUserLlmOverrides(cfg) {
|
|
184
|
+
const uc = getActiveUserConfig();
|
|
185
|
+
if (!uc.provider && !uc.key && !uc.model && !uc.base_url)
|
|
186
|
+
return cfg;
|
|
187
|
+
const out = { ...cfg };
|
|
188
|
+
if (uc.provider && uc.provider !== cfg.provider) {
|
|
189
|
+
out.provider = uc.provider;
|
|
190
|
+
out.base_url = PROVIDER_DEFAULTS[uc.provider].base_url;
|
|
191
|
+
out.endpoint = undefined;
|
|
192
|
+
if (!uc.model && PROVIDER_DEFAULTS[uc.provider].model)
|
|
193
|
+
out.model = PROVIDER_DEFAULTS[uc.provider].model;
|
|
194
|
+
}
|
|
195
|
+
if (uc.base_url)
|
|
196
|
+
out.base_url = uc.base_url;
|
|
197
|
+
if (uc.model)
|
|
198
|
+
out.model = uc.model;
|
|
199
|
+
if (uc.key)
|
|
200
|
+
out.api_key_env = "XCODER_API_KEY";
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
export function resolveLlmConfig(role, file) {
|
|
204
|
+
const selected = applyUserLlmOverrides((file && (file[role] ?? file.default)) ?? deepseekDefaultConfig());
|
|
205
|
+
const envModel = process.env.XCODER_MODEL;
|
|
206
|
+
return {
|
|
207
|
+
...selected,
|
|
208
|
+
provider: selected.provider ?? "deepseek",
|
|
209
|
+
model: envModel ?? selected.model ?? "deepseek-chat",
|
|
210
|
+
max_tokens: selected.max_tokens ?? 16384,
|
|
211
|
+
temperature: selected.temperature ?? 0.0,
|
|
212
|
+
api_key_env: selected.api_key_env ?? "DEEPSEEK_API_KEY",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* @deprecated Legacy loader — returns the merged `default` block. New code
|
|
217
|
+
* should use `loadLlmConfigFile()` + `resolveLlmConfig(role, file)`.
|
|
218
|
+
*/
|
|
219
|
+
export function loadLlmConfig(configPath) {
|
|
220
|
+
const file = loadLlmConfigFile(configPath);
|
|
221
|
+
const block = applyUserLlmOverrides(file.default ?? deepseekDefaultConfig());
|
|
222
|
+
const envModel = process.env.XCODER_MODEL;
|
|
223
|
+
return {
|
|
224
|
+
...block,
|
|
225
|
+
provider: block.provider ?? "deepseek",
|
|
226
|
+
model: envModel ?? block.model ?? "deepseek-chat",
|
|
227
|
+
max_tokens: block.max_tokens ?? 16384,
|
|
228
|
+
temperature: block.temperature ?? 0.0,
|
|
229
|
+
api_key_env: block.api_key_env ?? "DEEPSEEK_API_KEY",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
export function resolveModelForSkill(config, skillName) {
|
|
233
|
+
const override = skillName ? config.overrides?.[skillName] : undefined;
|
|
234
|
+
return {
|
|
235
|
+
model: override?.model ?? config.model,
|
|
236
|
+
temperature: override?.temperature ?? config.temperature,
|
|
237
|
+
thinking: override?.thinking ?? config.thinking ?? false,
|
|
238
|
+
reasoningEffort: override?.reasoning_effort ?? config.reasoning_effort,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for secure-mode resolution.
|
|
3
|
+
*
|
|
4
|
+
* No CLI, orchestrator or agent may read `process.argv` or `XCODER_SECURE`
|
|
5
|
+
* directly (CORE-04: one default, one source of truth). Every entry point
|
|
6
|
+
* resolves the mode here and threads the result inward.
|
|
7
|
+
*
|
|
8
|
+
* Resolution order: explicit flag > XCODER_SECURE env > default ("disabled").
|
|
9
|
+
* Never throws: an unparseable env value falls back to the default and is
|
|
10
|
+
* reported as `source: "default"` — a bad env var must not brick the CLI.
|
|
11
|
+
*/
|
|
12
|
+
export type SecureMode = "enforced" | "disabled";
|
|
13
|
+
export type SecureModeSource = "flag" | "env" | "default";
|
|
14
|
+
export interface SecureModeResolution {
|
|
15
|
+
readonly mode: SecureMode;
|
|
16
|
+
readonly source: SecureModeSource;
|
|
17
|
+
/** Convenience boolean mirroring `mode === "enforced"`. */
|
|
18
|
+
readonly enabled: boolean;
|
|
19
|
+
/** Human-readable, e.g. "secure enforcement: on (flag)". */
|
|
20
|
+
readonly description: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function resolveSecureMode(input: {
|
|
23
|
+
readonly secureFlag?: boolean;
|
|
24
|
+
readonly env?: string | undefined;
|
|
25
|
+
}): SecureModeResolution;
|
|
26
|
+
/** Default mode constant, exported so consumers never re-declare the default. */
|
|
27
|
+
export declare const DEFAULT_SECURE_MODE: SecureMode;
|
|
28
|
+
/** Env var name consumed by every CLI entry point. */
|
|
29
|
+
export declare const SECURE_MODE_ENV_VAR = "XCODER_SECURE";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// xcoder:version 1 | xcoder:task task-ebf4f7 | xcoder:updated 2026-10-17T12:16:05.781Z | xcoder:subtask test-st-f454da
|
|
2
|
+
/**
|
|
3
|
+
* Single source of truth for secure-mode resolution.
|
|
4
|
+
*
|
|
5
|
+
* No CLI, orchestrator or agent may read `process.argv` or `XCODER_SECURE`
|
|
6
|
+
* directly (CORE-04: one default, one source of truth). Every entry point
|
|
7
|
+
* resolves the mode here and threads the result inward.
|
|
8
|
+
*
|
|
9
|
+
* Resolution order: explicit flag > XCODER_SECURE env > default ("disabled").
|
|
10
|
+
* Never throws: an unparseable env value falls back to the default and is
|
|
11
|
+
* reported as `source: "default"` — a bad env var must not brick the CLI.
|
|
12
|
+
*/
|
|
13
|
+
const TRUTHY = new Set(["on", "true", "1", "yes", "enabled", "enforce", "enforced"]);
|
|
14
|
+
const FALSY = new Set(["off", "false", "0", "no", "disabled", "disable"]);
|
|
15
|
+
/**
|
|
16
|
+
* Parses a raw env value into a mode, or `undefined` when it is absent or
|
|
17
|
+
* unrecognised. Unrecognised values deliberately do NOT throw.
|
|
18
|
+
*/
|
|
19
|
+
function parseEnv(env) {
|
|
20
|
+
if (typeof env !== "string")
|
|
21
|
+
return undefined;
|
|
22
|
+
const normalized = env.trim().toLowerCase();
|
|
23
|
+
if (normalized.length === 0)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (TRUTHY.has(normalized))
|
|
26
|
+
return "enforced";
|
|
27
|
+
if (FALSY.has(normalized))
|
|
28
|
+
return "disabled";
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
function describe(mode, source) {
|
|
32
|
+
const state = mode === "enforced" ? "on" : "off";
|
|
33
|
+
return `secure enforcement: ${state} (${source})`;
|
|
34
|
+
}
|
|
35
|
+
export function resolveSecureMode(input) {
|
|
36
|
+
const safe = input ?? {};
|
|
37
|
+
// 1. Explicit flag always wins, in both directions.
|
|
38
|
+
if (typeof safe.secureFlag === "boolean") {
|
|
39
|
+
const mode = safe.secureFlag ? "enforced" : "disabled";
|
|
40
|
+
return { mode, source: "flag", enabled: mode === "enforced", description: describe(mode, "flag") };
|
|
41
|
+
}
|
|
42
|
+
// 2. Environment variable, when set to a recognised value.
|
|
43
|
+
const fromEnv = parseEnv(safe.env);
|
|
44
|
+
if (fromEnv !== undefined) {
|
|
45
|
+
return {
|
|
46
|
+
mode: fromEnv,
|
|
47
|
+
source: "env",
|
|
48
|
+
enabled: fromEnv === "enforced",
|
|
49
|
+
description: describe(fromEnv, "env"),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// 3. Default — also the landing spot for unparseable env values.
|
|
53
|
+
return {
|
|
54
|
+
mode: "disabled",
|
|
55
|
+
source: "default",
|
|
56
|
+
enabled: false,
|
|
57
|
+
description: describe("disabled", "default"),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Default mode constant, exported so consumers never re-declare the default. */
|
|
61
|
+
export const DEFAULT_SECURE_MODE = "disabled";
|
|
62
|
+
/** Env var name consumed by every CLI entry point. */
|
|
63
|
+
export const SECURE_MODE_ENV_VAR = "XCODER_SECURE";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type UserProvider = "deepseek" | "anthropic" | "openai-compatible";
|
|
2
|
+
export interface UserConfig {
|
|
3
|
+
provider?: UserProvider;
|
|
4
|
+
key?: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
base_url?: string;
|
|
7
|
+
env?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
export declare const PROVIDER_KEY_ENV: Record<UserProvider, string>;
|
|
10
|
+
export declare function userConfigPath(): string;
|
|
11
|
+
export declare function maskSecret(value: string): string;
|
|
12
|
+
/** Clears the memoised config — for tests and long-lived processes that re-read config. */
|
|
13
|
+
export declare function resetUserConfigCache(): void;
|
|
14
|
+
/** Read config.json. Missing file → {}. Corrupt file → throws (never silently overwritten). */
|
|
15
|
+
export declare function readUserConfig(): UserConfig;
|
|
16
|
+
export declare function getActiveUserConfig(): UserConfig;
|
|
17
|
+
/** Push config.json into process.env (without overriding variables exported in the shell). */
|
|
18
|
+
export declare function applyUserConfig(): UserConfig;
|
|
19
|
+
export interface ParsedConfigArg {
|
|
20
|
+
/** Top-level fields to set ("" = remove). */
|
|
21
|
+
fields: Partial<Record<"provider" | "key" | "model" | "base_url", string>>;
|
|
22
|
+
/** Env vars to set ("" = remove). */
|
|
23
|
+
env: Record<string, string>;
|
|
24
|
+
errors: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare function parseConfigArg(arg: string): ParsedConfigArg;
|
|
27
|
+
/** Merge parsed updates into config.json (atomic write, owner-only permissions). */
|
|
28
|
+
export declare function saveUserConfig(parsed: ParsedConfigArg): UserConfig;
|
|
29
|
+
/** Human-readable view with secrets masked. */
|
|
30
|
+
export declare function formatUserConfig(cfg: UserConfig): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Pull `--config <value>` / `--config=<value>` out of argv so it works anywhere
|
|
33
|
+
* on the command line. Returns the remaining argv.
|
|
34
|
+
*/
|
|
35
|
+
export declare function extractConfigArg(argv: string[]): {
|
|
36
|
+
value?: string;
|
|
37
|
+
argv: string[];
|
|
38
|
+
error?: string;
|
|
39
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// xcoder:version 1 | xcoder:task task-c4a64f | xcoder:updated 2026-10-17T03:03:22.008Z | xcoder:subtask test-st-3a3927
|
|
2
|
+
// User-level runtime config: <home>/.xcoder/config.json
|
|
3
|
+
//
|
|
4
|
+
// xcoder --config "provider=deepseek,key=sk-…,model=deepseek-chat"
|
|
5
|
+
//
|
|
6
|
+
// Holds the LLM provider/key/model/base_url plus any extra environment
|
|
7
|
+
// variables (the old `.env` settings). Precedence at startup:
|
|
8
|
+
// real shell environment > ~/.xcoder/config.json > .env files
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
/** Variables that were really exported in the shell — config.json never overrides these. */
|
|
13
|
+
const shellKeys = new Set(Object.keys(process.env));
|
|
14
|
+
export const PROVIDER_KEY_ENV = {
|
|
15
|
+
deepseek: "DEEPSEEK_API_KEY",
|
|
16
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
17
|
+
"openai-compatible": "OPENAI_API_KEY",
|
|
18
|
+
};
|
|
19
|
+
const PROVIDER_ALIASES = {
|
|
20
|
+
deepseek: "deepseek",
|
|
21
|
+
anthropic: "anthropic",
|
|
22
|
+
claude: "anthropic",
|
|
23
|
+
openai: "openai-compatible",
|
|
24
|
+
"openai-compatible": "openai-compatible",
|
|
25
|
+
};
|
|
26
|
+
/** Env names that could redirect execution or traffic — never settable from config. */
|
|
27
|
+
const BLOCKED_ENV = /^(PATH|PATHEXT|NODE_OPTIONS|NODE_PATH|NODE_EXTRA_CA_CERTS|LD_.*|DYLD_.*|HOME|USERPROFILE|SHELL|COMSPEC|PYTHONPATH|BUN_.*|NPM_CONFIG_.*|(HTTPS?|ALL|NO)_PROXY)$/i;
|
|
28
|
+
const SECRET_NAME = /(KEY|TOKEN|SECRET|PASSWORD)/i;
|
|
29
|
+
export function userConfigPath() {
|
|
30
|
+
return process.env.XCODER_CONFIG_FILE ?? path.join(os.homedir(), ".xcoder", "config.json");
|
|
31
|
+
}
|
|
32
|
+
export function maskSecret(value) {
|
|
33
|
+
return value.length > 8 ? `****${value.slice(-4)}` : "****";
|
|
34
|
+
}
|
|
35
|
+
let active = null;
|
|
36
|
+
/** Clears the memoised config — for tests and long-lived processes that re-read config. */
|
|
37
|
+
export function resetUserConfigCache() {
|
|
38
|
+
active = null;
|
|
39
|
+
}
|
|
40
|
+
/** Read config.json. Missing file → {}. Corrupt file → throws (never silently overwritten). */
|
|
41
|
+
export function readUserConfig() {
|
|
42
|
+
const file = userConfigPath();
|
|
43
|
+
if (!fs.existsSync(file))
|
|
44
|
+
return {};
|
|
45
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
46
|
+
if (!raw.trim())
|
|
47
|
+
return {};
|
|
48
|
+
const parsed = JSON.parse(raw);
|
|
49
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
50
|
+
throw new Error(`${file} must contain a JSON object`);
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
export function getActiveUserConfig() {
|
|
54
|
+
if (active)
|
|
55
|
+
return active;
|
|
56
|
+
try {
|
|
57
|
+
active = readUserConfig();
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
process.stderr.write(`[xcoder] ignoring unreadable ${userConfigPath()}: ${err.message}\n`);
|
|
61
|
+
active = {};
|
|
62
|
+
}
|
|
63
|
+
return active;
|
|
64
|
+
}
|
|
65
|
+
/** Push config.json into process.env (without overriding variables exported in the shell). */
|
|
66
|
+
export function applyUserConfig() {
|
|
67
|
+
active = null;
|
|
68
|
+
const cfg = getActiveUserConfig();
|
|
69
|
+
const set = (name, value) => {
|
|
70
|
+
if (value === undefined || shellKeys.has(name))
|
|
71
|
+
return;
|
|
72
|
+
process.env[name] = value;
|
|
73
|
+
};
|
|
74
|
+
for (const [name, value] of Object.entries(cfg.env ?? {})) {
|
|
75
|
+
if (!BLOCKED_ENV.test(name))
|
|
76
|
+
set(name, String(value));
|
|
77
|
+
}
|
|
78
|
+
if (cfg.key) {
|
|
79
|
+
set("XCODER_API_KEY", cfg.key);
|
|
80
|
+
set(PROVIDER_KEY_ENV[cfg.provider ?? "deepseek"], cfg.key);
|
|
81
|
+
}
|
|
82
|
+
if (cfg.model)
|
|
83
|
+
set("XCODER_MODEL", cfg.model);
|
|
84
|
+
return cfg;
|
|
85
|
+
}
|
|
86
|
+
export function parseConfigArg(arg) {
|
|
87
|
+
const out = { fields: {}, env: {}, errors: [] };
|
|
88
|
+
const tokens = arg.split(/,(?=\s*[A-Za-z_][A-Za-z0-9_.-]*\s*=)/);
|
|
89
|
+
for (const token of tokens) {
|
|
90
|
+
const eq = token.indexOf("=");
|
|
91
|
+
if (eq < 1) {
|
|
92
|
+
if (token.trim())
|
|
93
|
+
out.errors.push(`expected name=value, got "${token.trim()}"`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const name = token.slice(0, eq).trim();
|
|
97
|
+
const value = token.slice(eq + 1).trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
98
|
+
const lower = name.toLowerCase();
|
|
99
|
+
if (lower === "provider") {
|
|
100
|
+
if (value === "")
|
|
101
|
+
out.fields.provider = "";
|
|
102
|
+
else if (PROVIDER_ALIASES[value.toLowerCase()])
|
|
103
|
+
out.fields.provider = PROVIDER_ALIASES[value.toLowerCase()];
|
|
104
|
+
else
|
|
105
|
+
out.errors.push(`unknown provider "${value}" (use deepseek, anthropic or openai-compatible)`);
|
|
106
|
+
}
|
|
107
|
+
else if (lower === "key" || lower === "api_key" || lower === "apikey") {
|
|
108
|
+
out.fields.key = value;
|
|
109
|
+
}
|
|
110
|
+
else if (lower === "model") {
|
|
111
|
+
out.fields.model = value;
|
|
112
|
+
}
|
|
113
|
+
else if (lower === "base_url" || lower === "baseurl" || lower === "url") {
|
|
114
|
+
if (value !== "" && !/^https?:\/\/[^\s]+$/i.test(value))
|
|
115
|
+
out.errors.push(`base_url must start with http:// or https:// (got "${value}")`);
|
|
116
|
+
else
|
|
117
|
+
out.fields.base_url = value;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const envName = lower.startsWith("env.") ? name.slice(4) : name;
|
|
121
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(envName))
|
|
122
|
+
out.errors.push(`unknown setting "${name}" (use provider, key, model, base_url, or an UPPER_CASE env var)`);
|
|
123
|
+
else if (BLOCKED_ENV.test(envName))
|
|
124
|
+
out.errors.push(`${envName} cannot be set from config (security)`);
|
|
125
|
+
else
|
|
126
|
+
out.env[envName] = value;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
/** Merge parsed updates into config.json (atomic write, owner-only permissions). */
|
|
132
|
+
export function saveUserConfig(parsed) {
|
|
133
|
+
const current = readUserConfig(); // throws on corrupt JSON instead of clobbering it
|
|
134
|
+
const next = { ...current, env: { ...(current.env ?? {}) } };
|
|
135
|
+
for (const [k, v] of Object.entries(parsed.fields)) {
|
|
136
|
+
if (v === "")
|
|
137
|
+
delete next[k];
|
|
138
|
+
else
|
|
139
|
+
next[k] = v;
|
|
140
|
+
}
|
|
141
|
+
for (const [k, v] of Object.entries(parsed.env)) {
|
|
142
|
+
if (v === "")
|
|
143
|
+
delete next.env[k];
|
|
144
|
+
else
|
|
145
|
+
next.env[k] = v;
|
|
146
|
+
}
|
|
147
|
+
if (Object.keys(next.env ?? {}).length === 0)
|
|
148
|
+
delete next.env;
|
|
149
|
+
const file = userConfigPath();
|
|
150
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
151
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
152
|
+
fs.writeFileSync(tmp, JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
|
|
153
|
+
fs.renameSync(tmp, file);
|
|
154
|
+
try {
|
|
155
|
+
fs.chmodSync(file, 0o600);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// best-effort (e.g. Windows)
|
|
159
|
+
}
|
|
160
|
+
active = null;
|
|
161
|
+
return next;
|
|
162
|
+
}
|
|
163
|
+
/** Human-readable view with secrets masked. */
|
|
164
|
+
export function formatUserConfig(cfg) {
|
|
165
|
+
const lines = [];
|
|
166
|
+
if (cfg.provider)
|
|
167
|
+
lines.push(`provider = ${cfg.provider}`);
|
|
168
|
+
if (cfg.key)
|
|
169
|
+
lines.push(`key = ${maskSecret(cfg.key)}`);
|
|
170
|
+
if (cfg.model)
|
|
171
|
+
lines.push(`model = ${cfg.model}`);
|
|
172
|
+
if (cfg.base_url)
|
|
173
|
+
lines.push(`base_url = ${cfg.base_url}`);
|
|
174
|
+
for (const [k, v] of Object.entries(cfg.env ?? {}))
|
|
175
|
+
lines.push(`${k} = ${SECRET_NAME.test(k) ? maskSecret(String(v)) : v}`);
|
|
176
|
+
return lines.length ? lines : ["(empty)"];
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Pull `--config <value>` / `--config=<value>` out of argv so it works anywhere
|
|
180
|
+
* on the command line. Returns the remaining argv.
|
|
181
|
+
*/
|
|
182
|
+
export function extractConfigArg(argv) {
|
|
183
|
+
const rest = argv.slice(0, 2);
|
|
184
|
+
let value;
|
|
185
|
+
for (let i = 2; i < argv.length; i++) {
|
|
186
|
+
const a = argv[i];
|
|
187
|
+
if (a === "--config") {
|
|
188
|
+
const next = argv[i + 1];
|
|
189
|
+
if (next === undefined || next.startsWith("--"))
|
|
190
|
+
return { argv, error: '--config needs a value, e.g. --config "provider=deepseek,key=sk-…" (or --config show)' };
|
|
191
|
+
value = next;
|
|
192
|
+
i++;
|
|
193
|
+
}
|
|
194
|
+
else if (a.startsWith("--config=")) {
|
|
195
|
+
value = a.slice("--config=".length);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
rest.push(a);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { value, argv: rest };
|
|
202
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DecisionContext, EngineName, OrchestratorAction, ReasoningEngine } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The Master Orchestrator's own decision-making engine — distinct from the
|
|
4
|
+
* per-subtask engine a Sub Agent runs (see `agents/sub-agent.ts`, which
|
|
5
|
+
* today only implements `react-v1`). This is "Orchestrator ReAct": at every
|
|
6
|
+
* DAG event, decide the next orchestration action.
|
|
7
|
+
*
|
|
8
|
+
* `react-v1` here is fully deterministic on purpose — the decision space
|
|
9
|
+
* (dispatch the next-ready frontier, retry with a hint, escalate past the
|
|
10
|
+
* healing cap) doesn't need an LLM call to get right, and keeping it
|
|
11
|
+
* deterministic means the orchestrator's own behavior is reproducible and
|
|
12
|
+
* cheap to run at swarm scale. The other three names are registered as
|
|
13
|
+
* real extension points (e.g. an LLM-driven `plan-execute-v1` that
|
|
14
|
+
* proactively reshapes the DAG) but are not implemented yet — calling them
|
|
15
|
+
* throws a clear "not implemented" rather than silently falling back.
|
|
16
|
+
*/
|
|
17
|
+
export declare class ReactOrchestratorEngine implements ReasoningEngine {
|
|
18
|
+
name: EngineName;
|
|
19
|
+
decide(context: DecisionContext): Promise<OrchestratorAction>;
|
|
20
|
+
}
|
|
21
|
+
export declare function getOrchestratorEngine(name: EngineName): ReasoningEngine;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Master Orchestrator's own decision-making engine — distinct from the
|
|
3
|
+
* per-subtask engine a Sub Agent runs (see `agents/sub-agent.ts`, which
|
|
4
|
+
* today only implements `react-v1`). This is "Orchestrator ReAct": at every
|
|
5
|
+
* DAG event, decide the next orchestration action.
|
|
6
|
+
*
|
|
7
|
+
* `react-v1` here is fully deterministic on purpose — the decision space
|
|
8
|
+
* (dispatch the next-ready frontier, retry with a hint, escalate past the
|
|
9
|
+
* healing cap) doesn't need an LLM call to get right, and keeping it
|
|
10
|
+
* deterministic means the orchestrator's own behavior is reproducible and
|
|
11
|
+
* cheap to run at swarm scale. The other three names are registered as
|
|
12
|
+
* real extension points (e.g. an LLM-driven `plan-execute-v1` that
|
|
13
|
+
* proactively reshapes the DAG) but are not implemented yet — calling them
|
|
14
|
+
* throws a clear "not implemented" rather than silently falling back.
|
|
15
|
+
*/
|
|
16
|
+
export class ReactOrchestratorEngine {
|
|
17
|
+
name = "react-v1";
|
|
18
|
+
async decide(context) {
|
|
19
|
+
const { triggeringEvent, subtask } = context;
|
|
20
|
+
if (triggeringEvent === "subtask_done") {
|
|
21
|
+
const ready = context.dag.filter((n) => n.status === "ready");
|
|
22
|
+
return ready.length
|
|
23
|
+
? { type: "dispatch", subtaskIds: ready.map((n) => n.id) }
|
|
24
|
+
: { type: "dispatch", subtaskIds: [] };
|
|
25
|
+
}
|
|
26
|
+
if (triggeringEvent === "subtask_failed" && subtask) {
|
|
27
|
+
if (subtask.healing.attempts >= subtask.healing.maxAttempts) {
|
|
28
|
+
return {
|
|
29
|
+
type: "escalate",
|
|
30
|
+
subtaskId: subtask.id,
|
|
31
|
+
reason: `healing exhausted after ${subtask.healing.attempts}/${subtask.healing.maxAttempts} attempts`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const lastReason = subtask.healing.log.at(-1) ?? "validation failed";
|
|
35
|
+
return {
|
|
36
|
+
type: "retry",
|
|
37
|
+
subtaskId: subtask.id,
|
|
38
|
+
hint: `Previous attempt failed validation: ${lastReason}. Address this specifically before re-checking your own work.`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (triggeringEvent === "budget_warning") {
|
|
42
|
+
return { type: "halt", reason: "budget ceiling reached" };
|
|
43
|
+
}
|
|
44
|
+
if (triggeringEvent === "stuck_timeout" && subtask) {
|
|
45
|
+
return {
|
|
46
|
+
type: "escalate",
|
|
47
|
+
subtaskId: subtask.id,
|
|
48
|
+
reason: "subtask exceeded its expected runtime with no progress",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return { type: "halt", reason: `unhandled triggering event: ${triggeringEvent}` };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function getOrchestratorEngine(name) {
|
|
55
|
+
if (name === "react-v1")
|
|
56
|
+
return new ReactOrchestratorEngine();
|
|
57
|
+
throw new Error(`orchestrator engine "${name}" is registered as an extension point but not implemented yet. ` +
|
|
58
|
+
`Use "react-v1".`);
|
|
59
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
export type XcoderEventType = "task:intro" | "task:outro" | "task:dag" | "orchestrator:note" | "footer:update" | "agent:thinking-start" | "agent:thought" | "agent:action" | "agent:observation" | "agent:tokens" | "agent:final" | "agent:crash" | "agent:protocol-error" | "agent:score" | "agent:duplicate" | "agent:exhausted" | "debate:intro" | "debate:turn-start" | "debate:agent-thought" | "debate:agent-action" | "debate:agent-observation" | "debate:agent-final" | "debate:agent-tokens" | "debate:outro";
|
|
3
|
+
export interface XcoderEvent {
|
|
4
|
+
type: XcoderEventType;
|
|
5
|
+
taskId: string;
|
|
6
|
+
subtaskId?: string;
|
|
7
|
+
ts: string;
|
|
8
|
+
data?: unknown;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A single EventEmitter every RAO-producing part of the system emits to —
|
|
12
|
+
* `agents/sub-agent.ts`, `orchestrator/swarm.ts`, `orchestrator/
|
|
13
|
+
* sdlc-orchestrator.ts`. Two things consume it today: `util/terminal.ts`
|
|
14
|
+
* (indirectly — those modules still call `term.*` directly for the CLI's
|
|
15
|
+
* colored output) and `xcoder observe --json`, which prints every event as
|
|
16
|
+
* a JSON line. Adding another
|
|
17
|
+
* consumer (a plain JSON event log, a metrics exporter) means subscribing
|
|
18
|
+
* here, not threading a new parameter through every function that produces
|
|
19
|
+
* an event.
|
|
20
|
+
*/
|
|
21
|
+
export declare const xcoderEvents: EventEmitter<[never]>;
|
|
22
|
+
export declare function emitXcoderEvent(ev: Omit<XcoderEvent, "ts">): void;
|