scene-capability-engine 3.0.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/CHANGELOG.md +2513 -0
- package/LICENSE +21 -0
- package/README.md +765 -0
- package/README.zh.md +630 -0
- package/bin/kiro-spec-engine.js +796 -0
- package/bin/kse.js +3 -0
- package/bin/sce.js +3 -0
- package/bin/sco.js +3 -0
- package/docs/331-poc-adaptation-roadmap.md +156 -0
- package/docs/331-poc-dual-track-integration-guide.md +120 -0
- package/docs/331-poc-weekly-delivery-checklist.md +52 -0
- package/docs/OFFLINE_INSTALL.md +96 -0
- package/docs/README.md +279 -0
- package/docs/adopt-migration-guide.md +599 -0
- package/docs/adoption-guide.md +616 -0
- package/docs/agent-hooks-analysis.md +815 -0
- package/docs/architecture.md +733 -0
- package/docs/articles/ai-driven-development-philosophy-and-practice-review.md +208 -0
- package/docs/articles/ai-driven-development-philosophy-and-practice.en.md +459 -0
- package/docs/articles/ai-driven-development-philosophy-and-practice.md +492 -0
- package/docs/autonomous-control-guide.md +851 -0
- package/docs/command-reference.md +1368 -0
- package/docs/community.md +115 -0
- package/docs/cross-tool-guide.md +555 -0
- package/docs/developer-guide.md +619 -0
- package/docs/document-governance.md +865 -0
- package/docs/environment-management-guide.md +526 -0
- package/docs/examples/add-export-command/design.md +194 -0
- package/docs/examples/add-export-command/requirements.md +110 -0
- package/docs/examples/add-export-command/tasks.md +88 -0
- package/docs/examples/add-rest-api/design.md +855 -0
- package/docs/examples/add-rest-api/requirements.md +323 -0
- package/docs/examples/add-rest-api/tasks.md +355 -0
- package/docs/examples/add-user-dashboard/design.md +192 -0
- package/docs/examples/add-user-dashboard/requirements.md +143 -0
- package/docs/examples/add-user-dashboard/tasks.md +91 -0
- package/docs/faq.md +697 -0
- package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.json +156 -0
- package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.md +24 -0
- package/docs/images/wechat-qr.png +0 -0
- package/docs/integration-modes.md +529 -0
- package/docs/integration-philosophy.md +313 -0
- package/docs/knowledge-management-guide.md +263 -0
- package/docs/manual-workflows-guide.md +418 -0
- package/docs/moqui-capability-matrix.md +73 -0
- package/docs/moqui-template-core-library-playbook.md +109 -0
- package/docs/multi-agent-coordination-guide.md +553 -0
- package/docs/multi-repo-management-guide.md +1344 -0
- package/docs/quick-start-with-ai-tools.md +375 -0
- package/docs/quick-start.md +146 -0
- package/docs/release-checklist.md +121 -0
- package/docs/releases/README.md +13 -0
- package/docs/releases/v1.46.2-validation.md +45 -0
- package/docs/releases/v1.46.2.md +50 -0
- package/docs/scene-runtime-guide.md +347 -0
- package/docs/spec-collaboration-guide.md +369 -0
- package/docs/spec-locking-guide.md +225 -0
- package/docs/spec-numbering-guide.md +348 -0
- package/docs/spec-workflow.md +519 -0
- package/docs/steering-strategy-guide.md +196 -0
- package/docs/team-collaboration-guide.md +465 -0
- package/docs/testing-strategy.md +272 -0
- package/docs/tools/claude-guide.md +654 -0
- package/docs/tools/cursor-guide.md +706 -0
- package/docs/tools/generic-guide.md +446 -0
- package/docs/tools/kiro-guide.md +308 -0
- package/docs/tools/vscode-guide.md +445 -0
- package/docs/tools/windsurf-guide.md +391 -0
- package/docs/troubleshooting.md +1135 -0
- package/docs/upgrade-guide.md +639 -0
- package/docs/value-observability-guide.md +127 -0
- package/docs/zh/README.md +341 -0
- package/docs/zh/quick-start.md +764 -0
- package/docs/zh/release-checklist.md +121 -0
- package/docs/zh/releases/README.md +13 -0
- package/docs/zh/releases/v1.46.2-validation.md +45 -0
- package/docs/zh/releases/v1.46.2.md +50 -0
- package/docs/zh/spec-numbering-guide.md +348 -0
- package/docs/zh/tools/claude-guide.md +349 -0
- package/docs/zh/tools/cursor-guide.md +281 -0
- package/docs/zh/tools/generic-guide.md +499 -0
- package/docs/zh/tools/kiro-guide.md +342 -0
- package/docs/zh/tools/vscode-guide.md +449 -0
- package/docs/zh/tools/windsurf-guide.md +378 -0
- package/docs/zh/value-observability-guide.md +127 -0
- package/docs//344/272/244/344/273/230/346/270/205/345/215/225.md +75 -0
- package/lib/adoption/adoption-logger.js +487 -0
- package/lib/adoption/adoption-strategy.js +538 -0
- package/lib/adoption/backup-manager.js +420 -0
- package/lib/adoption/conflict-resolver.js +410 -0
- package/lib/adoption/detection-engine.js +275 -0
- package/lib/adoption/diff-viewer.js +226 -0
- package/lib/adoption/error-formatter.js +509 -0
- package/lib/adoption/file-classifier.js +385 -0
- package/lib/adoption/progress-reporter.js +534 -0
- package/lib/adoption/smart-orchestrator.js +470 -0
- package/lib/adoption/strategy-selector.js +218 -0
- package/lib/adoption/summary-generator.js +493 -0
- package/lib/adoption/template-sync.js +605 -0
- package/lib/auto/autonomous-engine.js +485 -0
- package/lib/auto/checkpoint-manager.js +300 -0
- package/lib/auto/close-loop-runner.js +2476 -0
- package/lib/auto/config-schema.js +176 -0
- package/lib/auto/decision-engine.js +344 -0
- package/lib/auto/error-recovery-manager.js +580 -0
- package/lib/auto/goal-decomposer.js +278 -0
- package/lib/auto/progress-tracker.js +502 -0
- package/lib/auto/safety-manager.js +186 -0
- package/lib/auto/semantic-decomposer.js +137 -0
- package/lib/auto/state-manager.js +126 -0
- package/lib/auto/task-queue-manager.js +340 -0
- package/lib/backup/backup-system.js +372 -0
- package/lib/backup/selective-backup.js +207 -0
- package/lib/collab/agent-registry.js +240 -0
- package/lib/collab/collab-manager.js +285 -0
- package/lib/collab/contract-manager.js +320 -0
- package/lib/collab/coordinator.js +370 -0
- package/lib/collab/dependency-manager.js +280 -0
- package/lib/collab/index.js +20 -0
- package/lib/collab/integration-manager.js +202 -0
- package/lib/collab/merge-coordinator.js +252 -0
- package/lib/collab/metadata-manager.js +233 -0
- package/lib/collab/multi-agent-config.js +120 -0
- package/lib/collab/spec-lifecycle-manager.js +304 -0
- package/lib/collab/sync-barrier.js +88 -0
- package/lib/collab/visualizer.js +208 -0
- package/lib/commands/adopt.js +749 -0
- package/lib/commands/auto.js +19559 -0
- package/lib/commands/collab.js +275 -0
- package/lib/commands/context.js +99 -0
- package/lib/commands/docs.js +808 -0
- package/lib/commands/doctor.js +273 -0
- package/lib/commands/env.js +420 -0
- package/lib/commands/knowledge.js +309 -0
- package/lib/commands/lock.js +235 -0
- package/lib/commands/ops.js +409 -0
- package/lib/commands/orchestrate.js +446 -0
- package/lib/commands/prompt.js +105 -0
- package/lib/commands/repo.js +118 -0
- package/lib/commands/rollback.js +219 -0
- package/lib/commands/scene.js +15549 -0
- package/lib/commands/spec-bootstrap.js +147 -0
- package/lib/commands/spec-gate.js +157 -0
- package/lib/commands/spec-pipeline.js +205 -0
- package/lib/commands/status.js +321 -0
- package/lib/commands/task.js +199 -0
- package/lib/commands/templates.js +654 -0
- package/lib/commands/upgrade.js +231 -0
- package/lib/commands/value.js +569 -0
- package/lib/commands/watch.js +684 -0
- package/lib/commands/workflows.js +240 -0
- package/lib/commands/workspace-multi.js +325 -0
- package/lib/commands/workspace.js +189 -0
- package/lib/context/context-exporter.js +378 -0
- package/lib/context/prompt-generator.js +482 -0
- package/lib/data/moqui-capability-lexicon.json +45 -0
- package/lib/environment/backup-system.js +189 -0
- package/lib/environment/environment-manager.js +379 -0
- package/lib/environment/environment-registry.js +168 -0
- package/lib/gitignore/gitignore-backup.js +229 -0
- package/lib/gitignore/gitignore-detector.js +239 -0
- package/lib/gitignore/gitignore-integration.js +267 -0
- package/lib/gitignore/gitignore-transformer.js +193 -0
- package/lib/gitignore/layered-rules-template.js +42 -0
- package/lib/governance/archive-tool.js +284 -0
- package/lib/governance/cleanup-tool.js +237 -0
- package/lib/governance/config-manager.js +186 -0
- package/lib/governance/diagnostic-engine.js +271 -0
- package/lib/governance/doc-reference-checker.js +200 -0
- package/lib/governance/execution-logger.js +243 -0
- package/lib/governance/file-scanner.js +285 -0
- package/lib/governance/hooks-manager.js +333 -0
- package/lib/governance/reporter.js +337 -0
- package/lib/governance/validation-engine.js +181 -0
- package/lib/i18n.js +79 -0
- package/lib/knowledge/entry-manager.js +208 -0
- package/lib/knowledge/index-manager.js +261 -0
- package/lib/knowledge/knowledge-manager.js +273 -0
- package/lib/knowledge/template-manager.js +191 -0
- package/lib/lock/index.js +21 -0
- package/lib/lock/lock-file.js +192 -0
- package/lib/lock/lock-manager.js +321 -0
- package/lib/lock/machine-identifier.js +135 -0
- package/lib/lock/steering-file-lock.js +207 -0
- package/lib/lock/task-lock-manager.js +345 -0
- package/lib/operations/audit-logger.js +293 -0
- package/lib/operations/feedback-manager.js +1147 -0
- package/lib/operations/index.js +23 -0
- package/lib/operations/models/index.js +170 -0
- package/lib/operations/operations-manager.js +151 -0
- package/lib/operations/operations-validator.js +280 -0
- package/lib/operations/permission-manager.js +354 -0
- package/lib/operations/template-loader.js +143 -0
- package/lib/orchestrator/agent-spawner.js +629 -0
- package/lib/orchestrator/bootstrap-prompt-builder.js +236 -0
- package/lib/orchestrator/index.js +19 -0
- package/lib/orchestrator/orchestration-engine.js +1270 -0
- package/lib/orchestrator/orchestrator-config.js +173 -0
- package/lib/orchestrator/status-monitor.js +591 -0
- package/lib/python-checker.js +209 -0
- package/lib/repo/config-manager.js +580 -0
- package/lib/repo/errors/config-error.js +13 -0
- package/lib/repo/errors/git-error.js +15 -0
- package/lib/repo/errors/repo-error.js +14 -0
- package/lib/repo/git-operations.js +181 -0
- package/lib/repo/handlers/.gitkeep +1 -0
- package/lib/repo/handlers/exec-handler.js +155 -0
- package/lib/repo/handlers/health-handler.js +169 -0
- package/lib/repo/handlers/init-handler.js +197 -0
- package/lib/repo/handlers/status-handler.js +176 -0
- package/lib/repo/output-formatter.js +184 -0
- package/lib/repo/path-resolver.js +178 -0
- package/lib/repo/repo-manager.js +514 -0
- package/lib/scene-runtime/audit-emitter.js +59 -0
- package/lib/scene-runtime/binding-plugin-loader.js +351 -0
- package/lib/scene-runtime/binding-registry.js +349 -0
- package/lib/scene-runtime/eval-bridge.js +44 -0
- package/lib/scene-runtime/index.js +19 -0
- package/lib/scene-runtime/moqui-adapter.js +620 -0
- package/lib/scene-runtime/moqui-client.js +606 -0
- package/lib/scene-runtime/moqui-extractor.js +2029 -0
- package/lib/scene-runtime/plan-compiler.js +208 -0
- package/lib/scene-runtime/policy-gate.js +58 -0
- package/lib/scene-runtime/runtime-executor.js +358 -0
- package/lib/scene-runtime/scene-loader.js +96 -0
- package/lib/scene-runtime/scene-ontology.js +959 -0
- package/lib/scene-runtime/scene-template-linter.js +852 -0
- package/lib/scene-runtime/templates/scene-template-erp-query-v0.1.yaml +28 -0
- package/lib/scene-runtime/templates/scene-template-hybrid-shadow-v0.1.yaml +34 -0
- package/lib/spec/bootstrap/context-collector.js +48 -0
- package/lib/spec/bootstrap/draft-generator.js +158 -0
- package/lib/spec/bootstrap/questionnaire-engine.js +70 -0
- package/lib/spec/bootstrap/trace-emitter.js +59 -0
- package/lib/spec/multi-spec-orchestrate.js +93 -0
- package/lib/spec/pipeline/constants.js +6 -0
- package/lib/spec/pipeline/stage-adapters.js +118 -0
- package/lib/spec/pipeline/stage-runner.js +146 -0
- package/lib/spec/pipeline/state-store.js +119 -0
- package/lib/spec-gate/engine/gate-engine.js +165 -0
- package/lib/spec-gate/policy/default-policy.js +22 -0
- package/lib/spec-gate/policy/policy-loader.js +103 -0
- package/lib/spec-gate/result-emitter.js +81 -0
- package/lib/spec-gate/rules/default-rules.js +156 -0
- package/lib/spec-gate/rules/rule-registry.js +51 -0
- package/lib/steering/adoption-config.js +164 -0
- package/lib/steering/compliance-auto-fixer.js +204 -0
- package/lib/steering/compliance-cache.js +99 -0
- package/lib/steering/compliance-error-reporter.js +70 -0
- package/lib/steering/context-sync-manager.js +273 -0
- package/lib/steering/index.js +92 -0
- package/lib/steering/spec-steering.js +230 -0
- package/lib/steering/steering-compliance-checker.js +73 -0
- package/lib/steering/steering-loader.js +144 -0
- package/lib/steering/steering-manager.js +289 -0
- package/lib/task/index.js +12 -0
- package/lib/task/task-claimer.js +489 -0
- package/lib/task/task-status-store.js +418 -0
- package/lib/templates/cache-manager.js +440 -0
- package/lib/templates/content-generalizer.js +247 -0
- package/lib/templates/frontmatter-generator.js +128 -0
- package/lib/templates/git-handler.js +471 -0
- package/lib/templates/metadata-collector.js +328 -0
- package/lib/templates/path-utils.js +144 -0
- package/lib/templates/registry-parser.js +505 -0
- package/lib/templates/spec-reader.js +216 -0
- package/lib/templates/template-applicator.js +249 -0
- package/lib/templates/template-creator.js +256 -0
- package/lib/templates/template-error.js +143 -0
- package/lib/templates/template-exporter.js +502 -0
- package/lib/templates/template-manager.js +782 -0
- package/lib/templates/template-validator.js +361 -0
- package/lib/upgrade/migration-engine.js +382 -0
- package/lib/upgrade/migrations/.gitkeep +52 -0
- package/lib/upgrade/migrations/1.0.0-to-1.1.0.js +78 -0
- package/lib/utils/file-diff.js +177 -0
- package/lib/utils/fs-utils.js +274 -0
- package/lib/utils/tool-detector.js +383 -0
- package/lib/utils/validation.js +324 -0
- package/lib/value/gate-summary-emitter.js +99 -0
- package/lib/value/metric-contract-loader.js +210 -0
- package/lib/value/risk-evaluator.js +117 -0
- package/lib/value/weekly-snapshot-builder.js +61 -0
- package/lib/version/version-checker.js +156 -0
- package/lib/version/version-manager.js +327 -0
- package/lib/watch/action-executor.js +458 -0
- package/lib/watch/event-debouncer.js +323 -0
- package/lib/watch/execution-logger.js +550 -0
- package/lib/watch/file-watcher.js +499 -0
- package/lib/watch/presets.js +266 -0
- package/lib/watch/watch-manager.js +533 -0
- package/lib/workspace/multi/global-config.js +150 -0
- package/lib/workspace/multi/index.js +22 -0
- package/lib/workspace/multi/path-utils.js +173 -0
- package/lib/workspace/multi/workspace-context-resolver.js +244 -0
- package/lib/workspace/multi/workspace-registry.js +196 -0
- package/lib/workspace/multi/workspace-state-manager.js +537 -0
- package/lib/workspace/multi/workspace.js +90 -0
- package/lib/workspace/workspace-manager.js +370 -0
- package/lib/workspace/workspace-sync.js +356 -0
- package/locales/en.json +114 -0
- package/locales/zh.json +114 -0
- package/package.json +102 -0
- package/template/.kiro/README.md +247 -0
- package/template/.kiro/hooks/check-spec-on-create.kiro.hook +17 -0
- package/template/.kiro/hooks/run-tests-on-save.kiro.hook +13 -0
- package/template/.kiro/hooks/sync-tasks-on-edit.kiro.hook +16 -0
- package/template/.kiro/specs/SPEC_WORKFLOW_GUIDE.md +134 -0
- package/template/.kiro/steering/CORE_PRINCIPLES.md +133 -0
- package/template/.kiro/steering/CURRENT_CONTEXT.md +30 -0
- package/template/.kiro/steering/ENVIRONMENT.md +35 -0
- package/template/.kiro/steering/RULES_GUIDE.md +46 -0
- package/template/.kiro/templates/operations/default/change-impact.md +112 -0
- package/template/.kiro/templates/operations/default/deployment.md +91 -0
- package/template/.kiro/templates/operations/default/feedback-response.md +269 -0
- package/template/.kiro/templates/operations/default/migration-plan.md +172 -0
- package/template/.kiro/templates/operations/default/monitoring.md +135 -0
- package/template/.kiro/templates/operations/default/operations.md +135 -0
- package/template/.kiro/templates/operations/default/rollback.md +143 -0
- package/template/.kiro/templates/operations/default/tools.yaml +364 -0
- package/template/.kiro/templates/operations/default/troubleshooting.md +123 -0
- package/template/.kiro/tools/backup_manager.py +295 -0
- package/template/.kiro/tools/configuration_manager.py +218 -0
- package/template/.kiro/tools/document_evaluator.py +550 -0
- package/template/.kiro/tools/enhancement_logger.py +168 -0
- package/template/.kiro/tools/error_handler.py +335 -0
- package/template/.kiro/tools/improvement_identifier.py +444 -0
- package/template/.kiro/tools/modification_applicator.py +737 -0
- package/template/.kiro/tools/quality_gate_enforcer.py +207 -0
- package/template/.kiro/tools/quality_scorer.py +305 -0
- package/template/.kiro/tools/report_generator.py +154 -0
- package/template/.kiro/tools/ultrawork_enhancer.py +676 -0
- package/template/.kiro/tools/ultrawork_enhancer_refactored.py +0 -0
- package/template/.kiro/tools/ultrawork_enhancer_v2.py +463 -0
- package/template/.kiro/tools/ultrawork_enhancer_v3.py +606 -0
- package/template/.kiro/tools/workflow_quality_gate.py +100 -0
- package/template/README.md +111 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Spawner — Process Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages Codex CLI sub-processes via Node.js child_process.spawn.
|
|
5
|
+
* Each spawned agent executes a single Spec in full-auto mode and
|
|
6
|
+
* streams JSON Lines events back to the orchestrator.
|
|
7
|
+
*
|
|
8
|
+
* Requirements: 1.1 (spawn via child_process), 1.2 (CODEX_API_KEY env),
|
|
9
|
+
* 1.3 (--json flag), 1.4 (exit 0 → completed),
|
|
10
|
+
* 1.5 (exit non-0 → failed), 1.6 (timeout → terminate),
|
|
11
|
+
* 1.7 (register in AgentRegistry)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { EventEmitter } = require('events');
|
|
15
|
+
const { spawn, spawnSync } = require('child_process');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
|
|
20
|
+
class AgentSpawner extends EventEmitter {
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} workspaceRoot - Absolute path to the project root
|
|
23
|
+
* @param {import('./orchestrator-config').OrchestratorConfig} orchestratorConfig
|
|
24
|
+
* @param {import('../collab/agent-registry').AgentRegistry} agentRegistry
|
|
25
|
+
* @param {import('./bootstrap-prompt-builder').BootstrapPromptBuilder} bootstrapPromptBuilder
|
|
26
|
+
*/
|
|
27
|
+
constructor(workspaceRoot, orchestratorConfig, agentRegistry, bootstrapPromptBuilder) {
|
|
28
|
+
super();
|
|
29
|
+
this._workspaceRoot = workspaceRoot;
|
|
30
|
+
this._orchestratorConfig = orchestratorConfig;
|
|
31
|
+
this._agentRegistry = agentRegistry;
|
|
32
|
+
this._bootstrapPromptBuilder = bootstrapPromptBuilder;
|
|
33
|
+
this._commandAvailabilityCache = new Map();
|
|
34
|
+
/** @type {Map<string, import('./agent-spawner').SpawnedAgent>} */
|
|
35
|
+
this._agents = new Map();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Spawn a Codex CLI sub-process to execute the given Spec.
|
|
40
|
+
*
|
|
41
|
+
* 1. Builds the bootstrap prompt via BootstrapPromptBuilder
|
|
42
|
+
* 2. Registers the agent in AgentRegistry
|
|
43
|
+
* 3. Spawns `codex exec --full-auto --json --sandbox danger-full-access "<prompt>"`
|
|
44
|
+
* 4. Sets up stdout/stderr/close handlers and timeout timer
|
|
45
|
+
*
|
|
46
|
+
* @param {string} specName - Spec to execute (e.g. "96-00-agent-orchestrator")
|
|
47
|
+
* @returns {Promise<object>} The SpawnedAgent record
|
|
48
|
+
*/
|
|
49
|
+
async spawn(specName) {
|
|
50
|
+
const config = await this._orchestratorConfig.getConfig();
|
|
51
|
+
|
|
52
|
+
// Resolve API key: env var → ~/.codex/auth.json fallback
|
|
53
|
+
const apiKeyEnvVar = config.apiKeyEnvVar || 'CODEX_API_KEY';
|
|
54
|
+
let apiKey = process.env[apiKeyEnvVar];
|
|
55
|
+
if (!apiKey) {
|
|
56
|
+
apiKey = this._readCodexAuthFile();
|
|
57
|
+
}
|
|
58
|
+
if (!apiKey) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Cannot find API key. Set environment variable ${apiKeyEnvVar}, ` +
|
|
61
|
+
'or configure Codex CLI auth via `codex auth` (~/.codex/auth.json).'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Build the bootstrap prompt (Req 2.1-2.3)
|
|
66
|
+
const prompt = await this._bootstrapPromptBuilder.buildPrompt(specName);
|
|
67
|
+
this._assertValidBootstrapPrompt(prompt, specName, 'BootstrapPromptBuilder.buildPrompt()');
|
|
68
|
+
|
|
69
|
+
// Register in AgentRegistry (Req 1.7)
|
|
70
|
+
const { agentId } = await this._agentRegistry.register({
|
|
71
|
+
currentTask: { specName },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Assemble command arguments (Req 1.1, 1.3)
|
|
75
|
+
const args = [
|
|
76
|
+
'exec',
|
|
77
|
+
'--full-auto',
|
|
78
|
+
'--json',
|
|
79
|
+
'--sandbox', 'danger-full-access',
|
|
80
|
+
...(config.codexArgs || []),
|
|
81
|
+
prompt,
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Resolve codex command: config → auto-detect
|
|
85
|
+
const { command, prependArgs } = this._resolveCodexCommand(config);
|
|
86
|
+
|
|
87
|
+
// Spawn the child process (Req 1.1, 1.2)
|
|
88
|
+
//
|
|
89
|
+
// On Windows we must use a shell to execute .cmd/.ps1 wrappers, but
|
|
90
|
+
// cmd.exe has an 8191-character command-line limit which the bootstrap
|
|
91
|
+
// prompt easily exceeds. To avoid this we write the prompt to a temp
|
|
92
|
+
// file and pass the file path to codex via a shell read expression.
|
|
93
|
+
//
|
|
94
|
+
// Strategy per platform:
|
|
95
|
+
// Windows → write prompt to temp file, spawn via cmd.exe with
|
|
96
|
+
// `type <file>` piped through a FOR /F or via PowerShell.
|
|
97
|
+
// Simplest: use stdin pipe (stdio[0] = 'pipe') so the
|
|
98
|
+
// prompt never appears on the command line at all.
|
|
99
|
+
// Others → pass prompt directly as argument (no length issue).
|
|
100
|
+
const isWindows = process.platform === 'win32';
|
|
101
|
+
const needsShell = isWindows || command === 'npx';
|
|
102
|
+
|
|
103
|
+
// On Windows, remove the prompt from args and pipe it via stdin instead,
|
|
104
|
+
// completely bypassing the cmd.exe 8191-char command-line limit.
|
|
105
|
+
let useStdinPrompt = false;
|
|
106
|
+
let stdinPrompt = null;
|
|
107
|
+
const finalArgs = [...prependArgs, ...args];
|
|
108
|
+
|
|
109
|
+
// When shell: true, Node.js concatenates args into a single string without
|
|
110
|
+
// escaping. Arguments containing spaces must be quoted so the shell does
|
|
111
|
+
// not split them into separate tokens.
|
|
112
|
+
if (needsShell) {
|
|
113
|
+
for (let i = 0; i < finalArgs.length; i++) {
|
|
114
|
+
if (/\s/.test(finalArgs[i])) {
|
|
115
|
+
finalArgs[i] = `"${finalArgs[i].replace(/"/g, '\\"')}"`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (isWindows) {
|
|
121
|
+
// Remove the prompt (last element of args portion) from command line
|
|
122
|
+
// and deliver it via stdin to avoid cmd.exe length limit.
|
|
123
|
+
stdinPrompt = finalArgs.pop(); // remove prompt
|
|
124
|
+
this._assertValidBootstrapPrompt(stdinPrompt, specName, 'Windows prompt extraction');
|
|
125
|
+
|
|
126
|
+
// If the prompt was quoted by the escaping above, unwrap it
|
|
127
|
+
if (stdinPrompt.startsWith('"') && stdinPrompt.endsWith('"')) {
|
|
128
|
+
stdinPrompt = stdinPrompt.slice(1, -1).replace(/\\"/g, '"');
|
|
129
|
+
}
|
|
130
|
+
useStdinPrompt = true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const env = { ...process.env, [apiKeyEnvVar]: apiKey };
|
|
134
|
+
|
|
135
|
+
// When using stdin for the prompt, write it to a temp file and pass
|
|
136
|
+
// the file path as the last argument using a short placeholder.
|
|
137
|
+
// Codex exec reads the prompt from argv, so we use a temp-file approach:
|
|
138
|
+
// write prompt → pass file path via shell read.
|
|
139
|
+
let promptTmpFile = null;
|
|
140
|
+
if (useStdinPrompt) {
|
|
141
|
+
// Sanitize agentId for use in filename to avoid Windows invalid path characters.
|
|
142
|
+
const safeAgentId = this._sanitizeWindowsFilenamePart(agentId);
|
|
143
|
+
promptTmpFile = path.join(os.tmpdir(), `kse-prompt-${safeAgentId}-${Date.now()}.txt`);
|
|
144
|
+
fs.writeFileSync(promptTmpFile, stdinPrompt, 'utf-8');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Build the final spawn arguments
|
|
148
|
+
let spawnCommand = command;
|
|
149
|
+
let spawnArgs = finalArgs;
|
|
150
|
+
let spawnShell = needsShell;
|
|
151
|
+
|
|
152
|
+
if (promptTmpFile) {
|
|
153
|
+
// On Windows, use PowerShell to read the temp file as the last argument.
|
|
154
|
+
// PowerShell does not have the 8191-char limit of cmd.exe.
|
|
155
|
+
// We construct a PowerShell script that:
|
|
156
|
+
// 1. Reads the prompt file into a variable (with UTF-8 encoding)
|
|
157
|
+
// 2. Passes the variable as a single argument to codex
|
|
158
|
+
const cmdParts = [command, ...finalArgs].map(a => {
|
|
159
|
+
if (a.startsWith('"') && a.endsWith('"')) return a;
|
|
160
|
+
return /\s/.test(a) ? `"${a}"` : a;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Use -Encoding UTF8 to correctly read UTF-8 files with non-ASCII characters (e.g., Chinese steering files).
|
|
164
|
+
// Pipe prompt via stdin (`-` prompt argument) to avoid Windows native argument splitting for long/multi-line prompts.
|
|
165
|
+
const psScript = `$prompt = Get-Content -Raw -Encoding UTF8 '${promptTmpFile.replace(/'/g, "''")}'; $prompt | & ${cmdParts.join(' ')} -`;
|
|
166
|
+
|
|
167
|
+
spawnCommand = 'powershell.exe';
|
|
168
|
+
spawnArgs = ['-NoProfile', '-Command', psScript];
|
|
169
|
+
spawnShell = false; // spawning powershell.exe directly
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const child = spawn(spawnCommand, spawnArgs, {
|
|
173
|
+
cwd: this._workspaceRoot,
|
|
174
|
+
env,
|
|
175
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
176
|
+
windowsHide: true,
|
|
177
|
+
shell: spawnShell,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const now = new Date().toISOString();
|
|
181
|
+
|
|
182
|
+
/** @type {object} */
|
|
183
|
+
const agent = {
|
|
184
|
+
agentId,
|
|
185
|
+
specName,
|
|
186
|
+
process: child,
|
|
187
|
+
status: 'running',
|
|
188
|
+
startedAt: now,
|
|
189
|
+
completedAt: null,
|
|
190
|
+
exitCode: null,
|
|
191
|
+
retryCount: 0,
|
|
192
|
+
stderr: '',
|
|
193
|
+
events: [],
|
|
194
|
+
_promptTmpFile: promptTmpFile,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
this._agents.set(agentId, agent);
|
|
198
|
+
|
|
199
|
+
// --- stdout: parse JSON Lines events ---
|
|
200
|
+
this._setupStdoutHandler(agent);
|
|
201
|
+
|
|
202
|
+
// --- stderr: buffer for error reporting ---
|
|
203
|
+
this._setupStderrHandler(agent);
|
|
204
|
+
|
|
205
|
+
// --- process close: determine final status ---
|
|
206
|
+
this._setupCloseHandler(agent);
|
|
207
|
+
|
|
208
|
+
// --- timeout detection (Req 1.6) ---
|
|
209
|
+
this._setupTimeout(agent, config.timeoutSeconds);
|
|
210
|
+
|
|
211
|
+
return agent;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Terminate a specific sub-process.
|
|
216
|
+
* Sends SIGTERM first, then SIGKILL after a 5-second grace period.
|
|
217
|
+
*
|
|
218
|
+
* @param {string} agentId
|
|
219
|
+
* @returns {Promise<void>}
|
|
220
|
+
*/
|
|
221
|
+
async kill(agentId) {
|
|
222
|
+
const agent = this._agents.get(agentId);
|
|
223
|
+
if (!agent || agent.status !== 'running') {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
await this._terminateProcess(agent);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Terminate all running sub-processes.
|
|
231
|
+
* @returns {Promise<void>}
|
|
232
|
+
*/
|
|
233
|
+
async killAll() {
|
|
234
|
+
const killPromises = [];
|
|
235
|
+
for (const agent of this._agents.values()) {
|
|
236
|
+
if (agent.status === 'running') {
|
|
237
|
+
killPromises.push(this._terminateProcess(agent));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
await Promise.all(killPromises);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get all agents (active and completed).
|
|
245
|
+
* @returns {Map<string, object>}
|
|
246
|
+
*/
|
|
247
|
+
getActiveAgents() {
|
|
248
|
+
return new Map(this._agents);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Private helpers
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Ensure bootstrap prompt is a non-empty string before using it in spawn args.
|
|
257
|
+
* @param {unknown} prompt
|
|
258
|
+
* @param {string} specName
|
|
259
|
+
* @param {string} source
|
|
260
|
+
* @private
|
|
261
|
+
*/
|
|
262
|
+
_assertValidBootstrapPrompt(prompt, specName, source) {
|
|
263
|
+
const isString = typeof prompt === 'string';
|
|
264
|
+
const length = isString ? prompt.length : 0;
|
|
265
|
+
const hasContent = isString && prompt.trim().length > 0;
|
|
266
|
+
if (!hasContent) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Invalid bootstrap prompt for spec "${specName}" from ${source}: ` +
|
|
269
|
+
`expected non-empty string, got ${typeof prompt} with length ${length}.`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Convert an arbitrary identifier into a Windows-safe filename segment.
|
|
276
|
+
* @param {string} value
|
|
277
|
+
* @returns {string}
|
|
278
|
+
* @private
|
|
279
|
+
*/
|
|
280
|
+
_sanitizeWindowsFilenamePart(value) {
|
|
281
|
+
const sanitized = String(value)
|
|
282
|
+
.replace(/[<>:"/\\|?*\x00-\x1F]/g, '-')
|
|
283
|
+
.replace(/[. ]+$/g, '')
|
|
284
|
+
.slice(0, 120);
|
|
285
|
+
return sanitized || 'agent';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Parse stdout line-by-line for JSON Lines events.
|
|
290
|
+
* @param {object} agent
|
|
291
|
+
* @private
|
|
292
|
+
*/
|
|
293
|
+
_setupStdoutHandler(agent) {
|
|
294
|
+
let buffer = '';
|
|
295
|
+
|
|
296
|
+
agent.process.stdout.on('data', (chunk) => {
|
|
297
|
+
buffer += chunk.toString();
|
|
298
|
+
const lines = buffer.split('\n');
|
|
299
|
+
// Keep the last (possibly incomplete) line in the buffer
|
|
300
|
+
buffer = lines.pop() || '';
|
|
301
|
+
|
|
302
|
+
for (const line of lines) {
|
|
303
|
+
const trimmed = line.trim();
|
|
304
|
+
if (!trimmed) continue;
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const event = JSON.parse(trimmed);
|
|
308
|
+
agent.events.push(event);
|
|
309
|
+
this.emit('agent:output', {
|
|
310
|
+
agentId: agent.agentId,
|
|
311
|
+
specName: agent.specName,
|
|
312
|
+
event,
|
|
313
|
+
});
|
|
314
|
+
} catch (_err) {
|
|
315
|
+
// Non-JSON line — ignore silently
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Buffer stderr output for error reporting.
|
|
323
|
+
* @param {object} agent
|
|
324
|
+
* @private
|
|
325
|
+
*/
|
|
326
|
+
_setupStderrHandler(agent) {
|
|
327
|
+
agent.process.stderr.on('data', (chunk) => {
|
|
328
|
+
agent.stderr += chunk.toString();
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Handle process close event to determine final status.
|
|
334
|
+
* @param {object} agent
|
|
335
|
+
* @private
|
|
336
|
+
*/
|
|
337
|
+
_setupCloseHandler(agent) {
|
|
338
|
+
agent.process.on('close', async (code) => {
|
|
339
|
+
// Clean up prompt temp file if used
|
|
340
|
+
this._cleanupPromptTmpFile(agent);
|
|
341
|
+
|
|
342
|
+
// Clear timeout timer if still pending
|
|
343
|
+
if (agent._timeoutTimer) {
|
|
344
|
+
clearTimeout(agent._timeoutTimer);
|
|
345
|
+
agent._timeoutTimer = null;
|
|
346
|
+
}
|
|
347
|
+
if (agent._killTimer) {
|
|
348
|
+
clearTimeout(agent._killTimer);
|
|
349
|
+
agent._killTimer = null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Already finalized (e.g. by timeout handler) — skip
|
|
353
|
+
if (agent.status !== 'running') {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
agent.exitCode = code;
|
|
358
|
+
agent.completedAt = new Date().toISOString();
|
|
359
|
+
|
|
360
|
+
if (code === 0) {
|
|
361
|
+
// Req 1.4: exit 0 → completed
|
|
362
|
+
agent.status = 'completed';
|
|
363
|
+
this.emit('agent:completed', {
|
|
364
|
+
agentId: agent.agentId,
|
|
365
|
+
specName: agent.specName,
|
|
366
|
+
exitCode: code,
|
|
367
|
+
});
|
|
368
|
+
} else {
|
|
369
|
+
// Req 1.5: exit non-0 → failed
|
|
370
|
+
agent.status = 'failed';
|
|
371
|
+
this.emit('agent:failed', {
|
|
372
|
+
agentId: agent.agentId,
|
|
373
|
+
specName: agent.specName,
|
|
374
|
+
exitCode: code,
|
|
375
|
+
stderr: agent.stderr,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Deregister from AgentRegistry
|
|
380
|
+
await this._deregisterAgent(agent.agentId);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Handle spawn errors (e.g. command not found)
|
|
384
|
+
agent.process.on('error', async (err) => {
|
|
385
|
+
// Clean up prompt temp file if used
|
|
386
|
+
this._cleanupPromptTmpFile(agent);
|
|
387
|
+
|
|
388
|
+
if (agent._timeoutTimer) {
|
|
389
|
+
clearTimeout(agent._timeoutTimer);
|
|
390
|
+
agent._timeoutTimer = null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (agent.status !== 'running') return;
|
|
394
|
+
|
|
395
|
+
agent.status = 'failed';
|
|
396
|
+
agent.completedAt = new Date().toISOString();
|
|
397
|
+
agent.stderr += `\nSpawn error: ${err.message}`;
|
|
398
|
+
|
|
399
|
+
this.emit('agent:failed', {
|
|
400
|
+
agentId: agent.agentId,
|
|
401
|
+
specName: agent.specName,
|
|
402
|
+
exitCode: null,
|
|
403
|
+
stderr: agent.stderr,
|
|
404
|
+
error: err.message,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
await this._deregisterAgent(agent.agentId);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Set up a timeout timer that terminates the process if it runs too long.
|
|
413
|
+
* Sends SIGTERM first, then SIGKILL after 5 seconds (Req 1.6).
|
|
414
|
+
*
|
|
415
|
+
* @param {object} agent
|
|
416
|
+
* @param {number} timeoutSeconds
|
|
417
|
+
* @private
|
|
418
|
+
*/
|
|
419
|
+
_setupTimeout(agent, timeoutSeconds) {
|
|
420
|
+
if (!timeoutSeconds || timeoutSeconds <= 0) return;
|
|
421
|
+
|
|
422
|
+
agent._timeoutTimer = setTimeout(async () => {
|
|
423
|
+
if (agent.status !== 'running') return;
|
|
424
|
+
|
|
425
|
+
// Clean up prompt temp file if used
|
|
426
|
+
this._cleanupPromptTmpFile(agent);
|
|
427
|
+
|
|
428
|
+
agent.status = 'timeout';
|
|
429
|
+
agent.completedAt = new Date().toISOString();
|
|
430
|
+
|
|
431
|
+
this.emit('agent:timeout', {
|
|
432
|
+
agentId: agent.agentId,
|
|
433
|
+
specName: agent.specName,
|
|
434
|
+
timeoutSeconds,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// SIGTERM → 5s grace → SIGKILL
|
|
438
|
+
try {
|
|
439
|
+
agent.process.kill('SIGTERM');
|
|
440
|
+
} catch (_err) {
|
|
441
|
+
// Process may have already exited
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
agent._killTimer = setTimeout(() => {
|
|
445
|
+
try {
|
|
446
|
+
agent.process.kill('SIGKILL');
|
|
447
|
+
} catch (_err) {
|
|
448
|
+
// Process may have already exited
|
|
449
|
+
}
|
|
450
|
+
}, 5000);
|
|
451
|
+
|
|
452
|
+
await this._deregisterAgent(agent.agentId);
|
|
453
|
+
}, timeoutSeconds * 1000);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Terminate a process: SIGTERM first, SIGKILL after 5s grace period.
|
|
458
|
+
* @param {object} agent
|
|
459
|
+
* @returns {Promise<void>}
|
|
460
|
+
* @private
|
|
461
|
+
*/
|
|
462
|
+
_terminateProcess(agent) {
|
|
463
|
+
return new Promise((resolve) => {
|
|
464
|
+
let settled = false;
|
|
465
|
+
let killTimer = null;
|
|
466
|
+
let safetyTimer = null;
|
|
467
|
+
|
|
468
|
+
const settle = () => {
|
|
469
|
+
if (settled) return;
|
|
470
|
+
settled = true;
|
|
471
|
+
if (killTimer) clearTimeout(killTimer);
|
|
472
|
+
if (safetyTimer) clearTimeout(safetyTimer);
|
|
473
|
+
resolve();
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// Clear any existing timeout timer
|
|
477
|
+
if (agent._timeoutTimer) {
|
|
478
|
+
clearTimeout(agent._timeoutTimer);
|
|
479
|
+
agent._timeoutTimer = null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
agent.process.kill('SIGTERM');
|
|
484
|
+
} catch (_err) {
|
|
485
|
+
settle();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
killTimer = setTimeout(() => {
|
|
490
|
+
try {
|
|
491
|
+
agent.process.kill('SIGKILL');
|
|
492
|
+
} catch (_err) {
|
|
493
|
+
// Already exited
|
|
494
|
+
}
|
|
495
|
+
}, 5000);
|
|
496
|
+
|
|
497
|
+
agent.process.once('close', () => {
|
|
498
|
+
settle();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Safety net: resolve after 10s regardless
|
|
502
|
+
safetyTimer = setTimeout(() => {
|
|
503
|
+
settle();
|
|
504
|
+
}, 10000);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Deregister an agent from the AgentRegistry.
|
|
510
|
+
* Failures are logged but do not propagate (non-fatal).
|
|
511
|
+
*
|
|
512
|
+
* @param {string} agentId
|
|
513
|
+
* @returns {Promise<void>}
|
|
514
|
+
* @private
|
|
515
|
+
*/
|
|
516
|
+
async _deregisterAgent(agentId) {
|
|
517
|
+
try {
|
|
518
|
+
await this._agentRegistry.deregister(agentId);
|
|
519
|
+
} catch (err) {
|
|
520
|
+
console.warn(
|
|
521
|
+
`[AgentSpawner] Failed to deregister agent ${agentId}: ${err.message}`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Remove the temporary prompt file created for Windows spawns.
|
|
528
|
+
* Silently ignores errors (file may already be gone).
|
|
529
|
+
* @param {object} agent
|
|
530
|
+
* @private
|
|
531
|
+
*/
|
|
532
|
+
_cleanupPromptTmpFile(agent) {
|
|
533
|
+
if (agent._promptTmpFile) {
|
|
534
|
+
try {
|
|
535
|
+
fs.unlinkSync(agent._promptTmpFile);
|
|
536
|
+
} catch (_err) {
|
|
537
|
+
// Ignore — file may already be deleted
|
|
538
|
+
}
|
|
539
|
+
agent._promptTmpFile = null;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Read API key from Codex CLI's native auth file (~/.codex/auth.json).
|
|
545
|
+
* Returns the key string or null if not found.
|
|
546
|
+
*
|
|
547
|
+
* @returns {string|null}
|
|
548
|
+
* @private
|
|
549
|
+
*/
|
|
550
|
+
_readCodexAuthFile() {
|
|
551
|
+
try {
|
|
552
|
+
const authPath = path.join(os.homedir(), '.codex', 'auth.json');
|
|
553
|
+
if (!fs.existsSync(authPath)) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
const auth = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
557
|
+
return auth.OPENAI_API_KEY || auth.CODEX_API_KEY || null;
|
|
558
|
+
} catch (_err) {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Resolve the codex command and any prepended arguments.
|
|
565
|
+
*
|
|
566
|
+
* Priority:
|
|
567
|
+
* 1. config.codexCommand (user-specified, e.g. "npx @openai/codex" or "codex")
|
|
568
|
+
* 2. "codex" (default — assumes global install)
|
|
569
|
+
*
|
|
570
|
+
* When codexCommand contains spaces (e.g. "npx @openai/codex"),
|
|
571
|
+
* the first token becomes the command and the rest become prependArgs.
|
|
572
|
+
*
|
|
573
|
+
* @param {object} config
|
|
574
|
+
* @returns {{ command: string, prependArgs: string[] }}
|
|
575
|
+
* @private
|
|
576
|
+
*/
|
|
577
|
+
_resolveCodexCommand(config) {
|
|
578
|
+
let raw = config.codexCommand;
|
|
579
|
+
if (!raw) {
|
|
580
|
+
if (this._isCommandAvailable('codex')) {
|
|
581
|
+
raw = 'codex';
|
|
582
|
+
} else if (this._isCommandAvailable('npx')) {
|
|
583
|
+
raw = 'npx @openai/codex';
|
|
584
|
+
} else {
|
|
585
|
+
// Keep historical default to preserve error semantics on misconfigured hosts.
|
|
586
|
+
raw = 'codex';
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const parts = raw.trim().split(/\s+/);
|
|
591
|
+
return {
|
|
592
|
+
command: parts[0],
|
|
593
|
+
prependArgs: parts.slice(1),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Best-effort command availability probe.
|
|
599
|
+
* Uses `where` on Windows and `which` on POSIX systems.
|
|
600
|
+
*
|
|
601
|
+
* @param {string} command
|
|
602
|
+
* @returns {boolean}
|
|
603
|
+
* @private
|
|
604
|
+
*/
|
|
605
|
+
_isCommandAvailable(command) {
|
|
606
|
+
if (!command || typeof command !== 'string') {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
if (this._commandAvailabilityCache.has(command)) {
|
|
610
|
+
return this._commandAvailabilityCache.get(command);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
try {
|
|
614
|
+
const lookupCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
615
|
+
const result = spawnSync(lookupCmd, [command], {
|
|
616
|
+
windowsHide: true,
|
|
617
|
+
stdio: 'ignore',
|
|
618
|
+
});
|
|
619
|
+
const available = result.status === 0;
|
|
620
|
+
this._commandAvailabilityCache.set(command, available);
|
|
621
|
+
return available;
|
|
622
|
+
} catch (_err) {
|
|
623
|
+
this._commandAvailabilityCache.set(command, false);
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
module.exports = { AgentSpawner };
|