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.
Files changed (336) hide show
  1. package/CHANGELOG.md +2513 -0
  2. package/LICENSE +21 -0
  3. package/README.md +765 -0
  4. package/README.zh.md +630 -0
  5. package/bin/kiro-spec-engine.js +796 -0
  6. package/bin/kse.js +3 -0
  7. package/bin/sce.js +3 -0
  8. package/bin/sco.js +3 -0
  9. package/docs/331-poc-adaptation-roadmap.md +156 -0
  10. package/docs/331-poc-dual-track-integration-guide.md +120 -0
  11. package/docs/331-poc-weekly-delivery-checklist.md +52 -0
  12. package/docs/OFFLINE_INSTALL.md +96 -0
  13. package/docs/README.md +279 -0
  14. package/docs/adopt-migration-guide.md +599 -0
  15. package/docs/adoption-guide.md +616 -0
  16. package/docs/agent-hooks-analysis.md +815 -0
  17. package/docs/architecture.md +733 -0
  18. package/docs/articles/ai-driven-development-philosophy-and-practice-review.md +208 -0
  19. package/docs/articles/ai-driven-development-philosophy-and-practice.en.md +459 -0
  20. package/docs/articles/ai-driven-development-philosophy-and-practice.md +492 -0
  21. package/docs/autonomous-control-guide.md +851 -0
  22. package/docs/command-reference.md +1368 -0
  23. package/docs/community.md +115 -0
  24. package/docs/cross-tool-guide.md +555 -0
  25. package/docs/developer-guide.md +619 -0
  26. package/docs/document-governance.md +865 -0
  27. package/docs/environment-management-guide.md +526 -0
  28. package/docs/examples/add-export-command/design.md +194 -0
  29. package/docs/examples/add-export-command/requirements.md +110 -0
  30. package/docs/examples/add-export-command/tasks.md +88 -0
  31. package/docs/examples/add-rest-api/design.md +855 -0
  32. package/docs/examples/add-rest-api/requirements.md +323 -0
  33. package/docs/examples/add-rest-api/tasks.md +355 -0
  34. package/docs/examples/add-user-dashboard/design.md +192 -0
  35. package/docs/examples/add-user-dashboard/requirements.md +143 -0
  36. package/docs/examples/add-user-dashboard/tasks.md +91 -0
  37. package/docs/faq.md +697 -0
  38. package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.json +156 -0
  39. package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.md +24 -0
  40. package/docs/images/wechat-qr.png +0 -0
  41. package/docs/integration-modes.md +529 -0
  42. package/docs/integration-philosophy.md +313 -0
  43. package/docs/knowledge-management-guide.md +263 -0
  44. package/docs/manual-workflows-guide.md +418 -0
  45. package/docs/moqui-capability-matrix.md +73 -0
  46. package/docs/moqui-template-core-library-playbook.md +109 -0
  47. package/docs/multi-agent-coordination-guide.md +553 -0
  48. package/docs/multi-repo-management-guide.md +1344 -0
  49. package/docs/quick-start-with-ai-tools.md +375 -0
  50. package/docs/quick-start.md +146 -0
  51. package/docs/release-checklist.md +121 -0
  52. package/docs/releases/README.md +13 -0
  53. package/docs/releases/v1.46.2-validation.md +45 -0
  54. package/docs/releases/v1.46.2.md +50 -0
  55. package/docs/scene-runtime-guide.md +347 -0
  56. package/docs/spec-collaboration-guide.md +369 -0
  57. package/docs/spec-locking-guide.md +225 -0
  58. package/docs/spec-numbering-guide.md +348 -0
  59. package/docs/spec-workflow.md +519 -0
  60. package/docs/steering-strategy-guide.md +196 -0
  61. package/docs/team-collaboration-guide.md +465 -0
  62. package/docs/testing-strategy.md +272 -0
  63. package/docs/tools/claude-guide.md +654 -0
  64. package/docs/tools/cursor-guide.md +706 -0
  65. package/docs/tools/generic-guide.md +446 -0
  66. package/docs/tools/kiro-guide.md +308 -0
  67. package/docs/tools/vscode-guide.md +445 -0
  68. package/docs/tools/windsurf-guide.md +391 -0
  69. package/docs/troubleshooting.md +1135 -0
  70. package/docs/upgrade-guide.md +639 -0
  71. package/docs/value-observability-guide.md +127 -0
  72. package/docs/zh/README.md +341 -0
  73. package/docs/zh/quick-start.md +764 -0
  74. package/docs/zh/release-checklist.md +121 -0
  75. package/docs/zh/releases/README.md +13 -0
  76. package/docs/zh/releases/v1.46.2-validation.md +45 -0
  77. package/docs/zh/releases/v1.46.2.md +50 -0
  78. package/docs/zh/spec-numbering-guide.md +348 -0
  79. package/docs/zh/tools/claude-guide.md +349 -0
  80. package/docs/zh/tools/cursor-guide.md +281 -0
  81. package/docs/zh/tools/generic-guide.md +499 -0
  82. package/docs/zh/tools/kiro-guide.md +342 -0
  83. package/docs/zh/tools/vscode-guide.md +449 -0
  84. package/docs/zh/tools/windsurf-guide.md +378 -0
  85. package/docs/zh/value-observability-guide.md +127 -0
  86. package/docs//344/272/244/344/273/230/346/270/205/345/215/225.md +75 -0
  87. package/lib/adoption/adoption-logger.js +487 -0
  88. package/lib/adoption/adoption-strategy.js +538 -0
  89. package/lib/adoption/backup-manager.js +420 -0
  90. package/lib/adoption/conflict-resolver.js +410 -0
  91. package/lib/adoption/detection-engine.js +275 -0
  92. package/lib/adoption/diff-viewer.js +226 -0
  93. package/lib/adoption/error-formatter.js +509 -0
  94. package/lib/adoption/file-classifier.js +385 -0
  95. package/lib/adoption/progress-reporter.js +534 -0
  96. package/lib/adoption/smart-orchestrator.js +470 -0
  97. package/lib/adoption/strategy-selector.js +218 -0
  98. package/lib/adoption/summary-generator.js +493 -0
  99. package/lib/adoption/template-sync.js +605 -0
  100. package/lib/auto/autonomous-engine.js +485 -0
  101. package/lib/auto/checkpoint-manager.js +300 -0
  102. package/lib/auto/close-loop-runner.js +2476 -0
  103. package/lib/auto/config-schema.js +176 -0
  104. package/lib/auto/decision-engine.js +344 -0
  105. package/lib/auto/error-recovery-manager.js +580 -0
  106. package/lib/auto/goal-decomposer.js +278 -0
  107. package/lib/auto/progress-tracker.js +502 -0
  108. package/lib/auto/safety-manager.js +186 -0
  109. package/lib/auto/semantic-decomposer.js +137 -0
  110. package/lib/auto/state-manager.js +126 -0
  111. package/lib/auto/task-queue-manager.js +340 -0
  112. package/lib/backup/backup-system.js +372 -0
  113. package/lib/backup/selective-backup.js +207 -0
  114. package/lib/collab/agent-registry.js +240 -0
  115. package/lib/collab/collab-manager.js +285 -0
  116. package/lib/collab/contract-manager.js +320 -0
  117. package/lib/collab/coordinator.js +370 -0
  118. package/lib/collab/dependency-manager.js +280 -0
  119. package/lib/collab/index.js +20 -0
  120. package/lib/collab/integration-manager.js +202 -0
  121. package/lib/collab/merge-coordinator.js +252 -0
  122. package/lib/collab/metadata-manager.js +233 -0
  123. package/lib/collab/multi-agent-config.js +120 -0
  124. package/lib/collab/spec-lifecycle-manager.js +304 -0
  125. package/lib/collab/sync-barrier.js +88 -0
  126. package/lib/collab/visualizer.js +208 -0
  127. package/lib/commands/adopt.js +749 -0
  128. package/lib/commands/auto.js +19559 -0
  129. package/lib/commands/collab.js +275 -0
  130. package/lib/commands/context.js +99 -0
  131. package/lib/commands/docs.js +808 -0
  132. package/lib/commands/doctor.js +273 -0
  133. package/lib/commands/env.js +420 -0
  134. package/lib/commands/knowledge.js +309 -0
  135. package/lib/commands/lock.js +235 -0
  136. package/lib/commands/ops.js +409 -0
  137. package/lib/commands/orchestrate.js +446 -0
  138. package/lib/commands/prompt.js +105 -0
  139. package/lib/commands/repo.js +118 -0
  140. package/lib/commands/rollback.js +219 -0
  141. package/lib/commands/scene.js +15549 -0
  142. package/lib/commands/spec-bootstrap.js +147 -0
  143. package/lib/commands/spec-gate.js +157 -0
  144. package/lib/commands/spec-pipeline.js +205 -0
  145. package/lib/commands/status.js +321 -0
  146. package/lib/commands/task.js +199 -0
  147. package/lib/commands/templates.js +654 -0
  148. package/lib/commands/upgrade.js +231 -0
  149. package/lib/commands/value.js +569 -0
  150. package/lib/commands/watch.js +684 -0
  151. package/lib/commands/workflows.js +240 -0
  152. package/lib/commands/workspace-multi.js +325 -0
  153. package/lib/commands/workspace.js +189 -0
  154. package/lib/context/context-exporter.js +378 -0
  155. package/lib/context/prompt-generator.js +482 -0
  156. package/lib/data/moqui-capability-lexicon.json +45 -0
  157. package/lib/environment/backup-system.js +189 -0
  158. package/lib/environment/environment-manager.js +379 -0
  159. package/lib/environment/environment-registry.js +168 -0
  160. package/lib/gitignore/gitignore-backup.js +229 -0
  161. package/lib/gitignore/gitignore-detector.js +239 -0
  162. package/lib/gitignore/gitignore-integration.js +267 -0
  163. package/lib/gitignore/gitignore-transformer.js +193 -0
  164. package/lib/gitignore/layered-rules-template.js +42 -0
  165. package/lib/governance/archive-tool.js +284 -0
  166. package/lib/governance/cleanup-tool.js +237 -0
  167. package/lib/governance/config-manager.js +186 -0
  168. package/lib/governance/diagnostic-engine.js +271 -0
  169. package/lib/governance/doc-reference-checker.js +200 -0
  170. package/lib/governance/execution-logger.js +243 -0
  171. package/lib/governance/file-scanner.js +285 -0
  172. package/lib/governance/hooks-manager.js +333 -0
  173. package/lib/governance/reporter.js +337 -0
  174. package/lib/governance/validation-engine.js +181 -0
  175. package/lib/i18n.js +79 -0
  176. package/lib/knowledge/entry-manager.js +208 -0
  177. package/lib/knowledge/index-manager.js +261 -0
  178. package/lib/knowledge/knowledge-manager.js +273 -0
  179. package/lib/knowledge/template-manager.js +191 -0
  180. package/lib/lock/index.js +21 -0
  181. package/lib/lock/lock-file.js +192 -0
  182. package/lib/lock/lock-manager.js +321 -0
  183. package/lib/lock/machine-identifier.js +135 -0
  184. package/lib/lock/steering-file-lock.js +207 -0
  185. package/lib/lock/task-lock-manager.js +345 -0
  186. package/lib/operations/audit-logger.js +293 -0
  187. package/lib/operations/feedback-manager.js +1147 -0
  188. package/lib/operations/index.js +23 -0
  189. package/lib/operations/models/index.js +170 -0
  190. package/lib/operations/operations-manager.js +151 -0
  191. package/lib/operations/operations-validator.js +280 -0
  192. package/lib/operations/permission-manager.js +354 -0
  193. package/lib/operations/template-loader.js +143 -0
  194. package/lib/orchestrator/agent-spawner.js +629 -0
  195. package/lib/orchestrator/bootstrap-prompt-builder.js +236 -0
  196. package/lib/orchestrator/index.js +19 -0
  197. package/lib/orchestrator/orchestration-engine.js +1270 -0
  198. package/lib/orchestrator/orchestrator-config.js +173 -0
  199. package/lib/orchestrator/status-monitor.js +591 -0
  200. package/lib/python-checker.js +209 -0
  201. package/lib/repo/config-manager.js +580 -0
  202. package/lib/repo/errors/config-error.js +13 -0
  203. package/lib/repo/errors/git-error.js +15 -0
  204. package/lib/repo/errors/repo-error.js +14 -0
  205. package/lib/repo/git-operations.js +181 -0
  206. package/lib/repo/handlers/.gitkeep +1 -0
  207. package/lib/repo/handlers/exec-handler.js +155 -0
  208. package/lib/repo/handlers/health-handler.js +169 -0
  209. package/lib/repo/handlers/init-handler.js +197 -0
  210. package/lib/repo/handlers/status-handler.js +176 -0
  211. package/lib/repo/output-formatter.js +184 -0
  212. package/lib/repo/path-resolver.js +178 -0
  213. package/lib/repo/repo-manager.js +514 -0
  214. package/lib/scene-runtime/audit-emitter.js +59 -0
  215. package/lib/scene-runtime/binding-plugin-loader.js +351 -0
  216. package/lib/scene-runtime/binding-registry.js +349 -0
  217. package/lib/scene-runtime/eval-bridge.js +44 -0
  218. package/lib/scene-runtime/index.js +19 -0
  219. package/lib/scene-runtime/moqui-adapter.js +620 -0
  220. package/lib/scene-runtime/moqui-client.js +606 -0
  221. package/lib/scene-runtime/moqui-extractor.js +2029 -0
  222. package/lib/scene-runtime/plan-compiler.js +208 -0
  223. package/lib/scene-runtime/policy-gate.js +58 -0
  224. package/lib/scene-runtime/runtime-executor.js +358 -0
  225. package/lib/scene-runtime/scene-loader.js +96 -0
  226. package/lib/scene-runtime/scene-ontology.js +959 -0
  227. package/lib/scene-runtime/scene-template-linter.js +852 -0
  228. package/lib/scene-runtime/templates/scene-template-erp-query-v0.1.yaml +28 -0
  229. package/lib/scene-runtime/templates/scene-template-hybrid-shadow-v0.1.yaml +34 -0
  230. package/lib/spec/bootstrap/context-collector.js +48 -0
  231. package/lib/spec/bootstrap/draft-generator.js +158 -0
  232. package/lib/spec/bootstrap/questionnaire-engine.js +70 -0
  233. package/lib/spec/bootstrap/trace-emitter.js +59 -0
  234. package/lib/spec/multi-spec-orchestrate.js +93 -0
  235. package/lib/spec/pipeline/constants.js +6 -0
  236. package/lib/spec/pipeline/stage-adapters.js +118 -0
  237. package/lib/spec/pipeline/stage-runner.js +146 -0
  238. package/lib/spec/pipeline/state-store.js +119 -0
  239. package/lib/spec-gate/engine/gate-engine.js +165 -0
  240. package/lib/spec-gate/policy/default-policy.js +22 -0
  241. package/lib/spec-gate/policy/policy-loader.js +103 -0
  242. package/lib/spec-gate/result-emitter.js +81 -0
  243. package/lib/spec-gate/rules/default-rules.js +156 -0
  244. package/lib/spec-gate/rules/rule-registry.js +51 -0
  245. package/lib/steering/adoption-config.js +164 -0
  246. package/lib/steering/compliance-auto-fixer.js +204 -0
  247. package/lib/steering/compliance-cache.js +99 -0
  248. package/lib/steering/compliance-error-reporter.js +70 -0
  249. package/lib/steering/context-sync-manager.js +273 -0
  250. package/lib/steering/index.js +92 -0
  251. package/lib/steering/spec-steering.js +230 -0
  252. package/lib/steering/steering-compliance-checker.js +73 -0
  253. package/lib/steering/steering-loader.js +144 -0
  254. package/lib/steering/steering-manager.js +289 -0
  255. package/lib/task/index.js +12 -0
  256. package/lib/task/task-claimer.js +489 -0
  257. package/lib/task/task-status-store.js +418 -0
  258. package/lib/templates/cache-manager.js +440 -0
  259. package/lib/templates/content-generalizer.js +247 -0
  260. package/lib/templates/frontmatter-generator.js +128 -0
  261. package/lib/templates/git-handler.js +471 -0
  262. package/lib/templates/metadata-collector.js +328 -0
  263. package/lib/templates/path-utils.js +144 -0
  264. package/lib/templates/registry-parser.js +505 -0
  265. package/lib/templates/spec-reader.js +216 -0
  266. package/lib/templates/template-applicator.js +249 -0
  267. package/lib/templates/template-creator.js +256 -0
  268. package/lib/templates/template-error.js +143 -0
  269. package/lib/templates/template-exporter.js +502 -0
  270. package/lib/templates/template-manager.js +782 -0
  271. package/lib/templates/template-validator.js +361 -0
  272. package/lib/upgrade/migration-engine.js +382 -0
  273. package/lib/upgrade/migrations/.gitkeep +52 -0
  274. package/lib/upgrade/migrations/1.0.0-to-1.1.0.js +78 -0
  275. package/lib/utils/file-diff.js +177 -0
  276. package/lib/utils/fs-utils.js +274 -0
  277. package/lib/utils/tool-detector.js +383 -0
  278. package/lib/utils/validation.js +324 -0
  279. package/lib/value/gate-summary-emitter.js +99 -0
  280. package/lib/value/metric-contract-loader.js +210 -0
  281. package/lib/value/risk-evaluator.js +117 -0
  282. package/lib/value/weekly-snapshot-builder.js +61 -0
  283. package/lib/version/version-checker.js +156 -0
  284. package/lib/version/version-manager.js +327 -0
  285. package/lib/watch/action-executor.js +458 -0
  286. package/lib/watch/event-debouncer.js +323 -0
  287. package/lib/watch/execution-logger.js +550 -0
  288. package/lib/watch/file-watcher.js +499 -0
  289. package/lib/watch/presets.js +266 -0
  290. package/lib/watch/watch-manager.js +533 -0
  291. package/lib/workspace/multi/global-config.js +150 -0
  292. package/lib/workspace/multi/index.js +22 -0
  293. package/lib/workspace/multi/path-utils.js +173 -0
  294. package/lib/workspace/multi/workspace-context-resolver.js +244 -0
  295. package/lib/workspace/multi/workspace-registry.js +196 -0
  296. package/lib/workspace/multi/workspace-state-manager.js +537 -0
  297. package/lib/workspace/multi/workspace.js +90 -0
  298. package/lib/workspace/workspace-manager.js +370 -0
  299. package/lib/workspace/workspace-sync.js +356 -0
  300. package/locales/en.json +114 -0
  301. package/locales/zh.json +114 -0
  302. package/package.json +102 -0
  303. package/template/.kiro/README.md +247 -0
  304. package/template/.kiro/hooks/check-spec-on-create.kiro.hook +17 -0
  305. package/template/.kiro/hooks/run-tests-on-save.kiro.hook +13 -0
  306. package/template/.kiro/hooks/sync-tasks-on-edit.kiro.hook +16 -0
  307. package/template/.kiro/specs/SPEC_WORKFLOW_GUIDE.md +134 -0
  308. package/template/.kiro/steering/CORE_PRINCIPLES.md +133 -0
  309. package/template/.kiro/steering/CURRENT_CONTEXT.md +30 -0
  310. package/template/.kiro/steering/ENVIRONMENT.md +35 -0
  311. package/template/.kiro/steering/RULES_GUIDE.md +46 -0
  312. package/template/.kiro/templates/operations/default/change-impact.md +112 -0
  313. package/template/.kiro/templates/operations/default/deployment.md +91 -0
  314. package/template/.kiro/templates/operations/default/feedback-response.md +269 -0
  315. package/template/.kiro/templates/operations/default/migration-plan.md +172 -0
  316. package/template/.kiro/templates/operations/default/monitoring.md +135 -0
  317. package/template/.kiro/templates/operations/default/operations.md +135 -0
  318. package/template/.kiro/templates/operations/default/rollback.md +143 -0
  319. package/template/.kiro/templates/operations/default/tools.yaml +364 -0
  320. package/template/.kiro/templates/operations/default/troubleshooting.md +123 -0
  321. package/template/.kiro/tools/backup_manager.py +295 -0
  322. package/template/.kiro/tools/configuration_manager.py +218 -0
  323. package/template/.kiro/tools/document_evaluator.py +550 -0
  324. package/template/.kiro/tools/enhancement_logger.py +168 -0
  325. package/template/.kiro/tools/error_handler.py +335 -0
  326. package/template/.kiro/tools/improvement_identifier.py +444 -0
  327. package/template/.kiro/tools/modification_applicator.py +737 -0
  328. package/template/.kiro/tools/quality_gate_enforcer.py +207 -0
  329. package/template/.kiro/tools/quality_scorer.py +305 -0
  330. package/template/.kiro/tools/report_generator.py +154 -0
  331. package/template/.kiro/tools/ultrawork_enhancer.py +676 -0
  332. package/template/.kiro/tools/ultrawork_enhancer_refactored.py +0 -0
  333. package/template/.kiro/tools/ultrawork_enhancer_v2.py +463 -0
  334. package/template/.kiro/tools/ultrawork_enhancer_v3.py +606 -0
  335. package/template/.kiro/tools/workflow_quality_gate.py +100 -0
  336. package/template/README.md +111 -0
@@ -0,0 +1,1270 @@
1
+ /**
2
+ * Orchestration Engine — Batch Scheduling Engine (Core)
3
+ *
4
+ * Coordinates all orchestrator components: builds dependency graphs via
5
+ * DependencyManager, computes topological batches, spawns agents via
6
+ * AgentSpawner, tracks status via StatusMonitor, and integrates with
7
+ * SpecLifecycleManager and AgentRegistry.
8
+ *
9
+ * Requirements: 3.1-3.7 (dependency graph, batches, parallel, failure propagation)
10
+ * 5.1-5.6 (crash detection, retry, timeout, graceful stop, deregister)
11
+ * 8.1-8.5 (SLM transitions, AgentRegistry, TaskLockManager, CSM sync)
12
+ */
13
+
14
+ const { EventEmitter } = require('events');
15
+ const path = require('path');
16
+ const fsUtils = require('../utils/fs-utils');
17
+
18
+ const SPECS_DIR = '.kiro/specs';
19
+ const DEFAULT_RATE_LIMIT_MAX_RETRIES = 6;
20
+ const DEFAULT_RATE_LIMIT_BACKOFF_BASE_MS = 1000;
21
+ const DEFAULT_RATE_LIMIT_BACKOFF_MAX_MS = 30000;
22
+ const DEFAULT_RATE_LIMIT_ADAPTIVE_PARALLEL = true;
23
+ const DEFAULT_RATE_LIMIT_PARALLEL_FLOOR = 1;
24
+ const DEFAULT_RATE_LIMIT_COOLDOWN_MS = 30000;
25
+ const DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_PER_MINUTE = 12;
26
+ const DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_WINDOW_MS = 60000;
27
+ const RATE_LIMIT_BACKOFF_JITTER_RATIO = 0.5;
28
+ const RATE_LIMIT_RETRY_AFTER_MAX_MS = 10 * 60 * 1000;
29
+ const RATE_LIMIT_ERROR_PATTERNS = [
30
+ /(^|[^0-9])429([^0-9]|$)/i,
31
+ /too many requests/i,
32
+ /rate[\s-]?limit/i,
33
+ /resource exhausted/i,
34
+ /quota exceeded/i,
35
+ /exceeded.*quota/i,
36
+ /requests per minute/i,
37
+ /tokens per minute/i,
38
+ ];
39
+
40
+ class OrchestrationEngine extends EventEmitter {
41
+ /**
42
+ * @param {string} workspaceRoot - Absolute path to the project root
43
+ * @param {object} options
44
+ * @param {import('./agent-spawner').AgentSpawner} options.agentSpawner
45
+ * @param {import('../collab/dependency-manager')} options.dependencyManager
46
+ * @param {import('../collab/spec-lifecycle-manager').SpecLifecycleManager} options.specLifecycleManager
47
+ * @param {import('./status-monitor').StatusMonitor} options.statusMonitor
48
+ * @param {import('./orchestrator-config').OrchestratorConfig} options.orchestratorConfig
49
+ * @param {import('../collab/agent-registry').AgentRegistry} options.agentRegistry
50
+ */
51
+ constructor(workspaceRoot, options) {
52
+ super();
53
+ this._workspaceRoot = workspaceRoot;
54
+ this._agentSpawner = options.agentSpawner;
55
+ this._dependencyManager = options.dependencyManager;
56
+ this._specLifecycleManager = options.specLifecycleManager;
57
+ this._statusMonitor = options.statusMonitor;
58
+ this._orchestratorConfig = options.orchestratorConfig;
59
+ this._agentRegistry = options.agentRegistry;
60
+
61
+ /** @type {'idle'|'running'|'completed'|'failed'|'stopped'} */
62
+ this._state = 'idle';
63
+ /** @type {Map<string, string>} specName → agentId */
64
+ this._runningAgents = new Map();
65
+ /** @type {Map<string, number>} specName → retry count */
66
+ this._retryCounts = new Map();
67
+ /** @type {Set<string>} specs marked as final failure */
68
+ this._failedSpecs = new Set();
69
+ /** @type {Set<string>} specs skipped due to dependency failure */
70
+ this._skippedSpecs = new Set();
71
+ /** @type {Set<string>} specs completed successfully */
72
+ this._completedSpecs = new Set();
73
+ /** @type {boolean} whether stop() has been called */
74
+ this._stopped = false;
75
+ /** @type {object|null} execution plan */
76
+ this._executionPlan = null;
77
+ /** @type {number} max retries for rate-limit failures */
78
+ this._rateLimitMaxRetries = DEFAULT_RATE_LIMIT_MAX_RETRIES;
79
+ /** @type {number} base delay for rate-limit retries */
80
+ this._rateLimitBackoffBaseMs = DEFAULT_RATE_LIMIT_BACKOFF_BASE_MS;
81
+ /** @type {number} max delay for rate-limit retries */
82
+ this._rateLimitBackoffMaxMs = DEFAULT_RATE_LIMIT_BACKOFF_MAX_MS;
83
+ /** @type {boolean} enable adaptive parallel throttling on rate-limit signals */
84
+ this._rateLimitAdaptiveParallel = DEFAULT_RATE_LIMIT_ADAPTIVE_PARALLEL;
85
+ /** @type {number} minimum effective parallelism during rate-limit cooldown */
86
+ this._rateLimitParallelFloor = DEFAULT_RATE_LIMIT_PARALLEL_FLOOR;
87
+ /** @type {number} cooldown before each adaptive parallel recovery step */
88
+ this._rateLimitCooldownMs = DEFAULT_RATE_LIMIT_COOLDOWN_MS;
89
+ /** @type {number|null} configured max parallel for current run */
90
+ this._baseMaxParallel = null;
91
+ /** @type {number|null} dynamic effective parallel limit for current run */
92
+ this._effectiveMaxParallel = null;
93
+ /** @type {number} timestamp after which recovery can step up */
94
+ this._rateLimitCooldownUntil = 0;
95
+ /** @type {number} timestamp before which new launches are paused after rate-limit */
96
+ this._rateLimitLaunchHoldUntil = 0;
97
+ /** @type {number} max spec launches allowed within rolling launch-budget window */
98
+ this._rateLimitLaunchBudgetPerMinute = DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_PER_MINUTE;
99
+ /** @type {number} rolling window size for launch-budget throttling */
100
+ this._rateLimitLaunchBudgetWindowMs = DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_WINDOW_MS;
101
+ /** @type {number[]} timestamps (ms) of recent spec launches for rolling budget accounting */
102
+ this._rateLimitLaunchTimestamps = [];
103
+ /** @type {number} last launch-budget hold telemetry emission timestamp (ms) */
104
+ this._launchBudgetLastHoldSignalAt = 0;
105
+ /** @type {number} last launch-budget hold duration emitted to telemetry (ms) */
106
+ this._launchBudgetLastHoldMs = 0;
107
+ /** @type {() => number} */
108
+ this._random = typeof options.random === 'function' ? options.random : Math.random;
109
+ /** @type {() => number} */
110
+ this._now = typeof options.now === 'function' ? options.now : Date.now;
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Public API
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /**
118
+ * Start orchestration execution.
119
+ *
120
+ * 1. Validate spec existence
121
+ * 2. Build dependency graph via DependencyManager (Req 3.1, 3.7)
122
+ * 3. Detect circular dependencies (Req 3.2)
123
+ * 4. Compute batches via topological sort (Req 3.3)
124
+ * 5. Execute batches sequentially, specs within batch in parallel (Req 3.4, 3.5)
125
+ *
126
+ * @param {string[]} specNames - Specs to orchestrate
127
+ * @param {object} [options]
128
+ * @param {number} [options.maxParallel] - Override max parallel from config
129
+ * @returns {Promise<object>} OrchestrationResult
130
+ */
131
+ async start(specNames, options = {}) {
132
+ if (this._state === 'running') {
133
+ throw new Error('Orchestration is already running');
134
+ }
135
+
136
+ this._reset();
137
+ this._state = 'running';
138
+ this._stopped = false;
139
+ this._statusMonitor.setOrchestrationState('running');
140
+
141
+ try {
142
+ // Step 1: Validate spec existence (Req 6.4)
143
+ const missingSpecs = await this._validateSpecExistence(specNames);
144
+ if (missingSpecs.length > 0) {
145
+ const error = `Specs not found: ${missingSpecs.join(', ')}`;
146
+ this._state = 'failed';
147
+ this._statusMonitor.setOrchestrationState('failed');
148
+ return this._buildResult('failed', error);
149
+ }
150
+
151
+ // Step 2: Build dependency graph (Req 3.1, 3.7)
152
+ const graph = await this._dependencyManager.buildDependencyGraph(specNames);
153
+
154
+ // Step 3: Detect circular dependencies (Req 3.2)
155
+ const cyclePath = this._dependencyManager.detectCircularDependencies(graph);
156
+ if (cyclePath) {
157
+ const error = `Circular dependency detected: ${cyclePath.join(' → ')}`;
158
+ this._state = 'failed';
159
+ this._statusMonitor.setOrchestrationState('failed');
160
+ this._executionPlan = {
161
+ specs: specNames,
162
+ batches: [],
163
+ dependencies: this._extractDependencies(graph, specNames),
164
+ hasCycle: true,
165
+ cyclePath,
166
+ };
167
+ return this._buildResult('failed', error);
168
+ }
169
+
170
+ // Step 4: Compute batches (Req 3.3)
171
+ const dependencies = this._extractDependencies(graph, specNames);
172
+ const batches = this._computeBatches(specNames, dependencies);
173
+
174
+ this._executionPlan = {
175
+ specs: specNames,
176
+ batches,
177
+ dependencies,
178
+ hasCycle: false,
179
+ cyclePath: null,
180
+ };
181
+
182
+ // Initialize specs in StatusMonitor
183
+ for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
184
+ for (const specName of batches[batchIdx]) {
185
+ this._statusMonitor.initSpec(specName, batchIdx);
186
+ }
187
+ }
188
+ this._statusMonitor.setBatchInfo(0, batches.length);
189
+
190
+ // Get config for maxParallel and maxRetries
191
+ const config = await this._orchestratorConfig.getConfig();
192
+ this._applyRetryPolicyConfig(config);
193
+ const maxParallel = options.maxParallel || config.maxParallel || 3;
194
+ const maxRetries = config.maxRetries || 2;
195
+ this._initializeAdaptiveParallel(maxParallel);
196
+
197
+ // Step 5: Execute batches (Req 3.4)
198
+ await this._executeBatches(batches, maxParallel, maxRetries);
199
+
200
+ // Determine final state
201
+ if (this._stopped) {
202
+ this._state = 'stopped';
203
+ this._statusMonitor.setOrchestrationState('stopped');
204
+ } else if (this._failedSpecs.size > 0) {
205
+ this._state = 'failed';
206
+ this._statusMonitor.setOrchestrationState('failed');
207
+ } else {
208
+ this._state = 'completed';
209
+ this._statusMonitor.setOrchestrationState('completed');
210
+ }
211
+
212
+ this.emit('orchestration:complete', this._buildResult(this._state));
213
+ return this._buildResult(this._state);
214
+ } catch (err) {
215
+ this._state = 'failed';
216
+ this._statusMonitor.setOrchestrationState('failed');
217
+ return this._buildResult('failed', err.message);
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Gracefully stop all running agents and halt orchestration (Req 5.5).
223
+ * @returns {Promise<void>}
224
+ */
225
+ async stop() {
226
+ this._stopped = true;
227
+
228
+ if (this._state !== 'running') {
229
+ return;
230
+ }
231
+
232
+ // Kill all running agents
233
+ await this._agentSpawner.killAll();
234
+
235
+ // Mark running specs as stopped
236
+ for (const [specName] of this._runningAgents) {
237
+ this._statusMonitor.updateSpecStatus(specName, 'skipped', null, 'Orchestration stopped');
238
+ }
239
+ this._runningAgents.clear();
240
+
241
+ this._state = 'stopped';
242
+ this._statusMonitor.setOrchestrationState('stopped');
243
+ }
244
+
245
+ /**
246
+ * Get current orchestration status.
247
+ * @returns {object} OrchestrationStatus
248
+ */
249
+ getStatus() {
250
+ return this._statusMonitor.getOrchestrationStatus();
251
+ }
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Batch Execution
255
+ // ---------------------------------------------------------------------------
256
+
257
+ /**
258
+ * Execute all batches sequentially.
259
+ * Within each batch, specs run in parallel up to maxParallel.
260
+ *
261
+ * @param {string[][]} batches
262
+ * @param {number} maxParallel
263
+ * @param {number} maxRetries
264
+ * @returns {Promise<void>}
265
+ * @private
266
+ */
267
+ async _executeBatches(batches, maxParallel, maxRetries) {
268
+ for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
269
+ if (this._stopped) break;
270
+
271
+ const batch = batches[batchIdx];
272
+ this._statusMonitor.setBatchInfo(batchIdx + 1, batches.length);
273
+
274
+ // Filter out skipped specs (dependency failures)
275
+ const executableSpecs = batch.filter(s => !this._skippedSpecs.has(s));
276
+
277
+ if (executableSpecs.length === 0) {
278
+ continue;
279
+ }
280
+
281
+ this.emit('batch:start', { batch: batchIdx, specs: executableSpecs });
282
+
283
+ // Execute specs in parallel with maxParallel limit
284
+ await this._executeSpecsInParallel(executableSpecs, maxParallel, maxRetries);
285
+
286
+ this.emit('batch:complete', {
287
+ batch: batchIdx,
288
+ completed: executableSpecs.filter(s => this._completedSpecs.has(s)),
289
+ failed: executableSpecs.filter(s => this._failedSpecs.has(s)),
290
+ skipped: executableSpecs.filter(s => this._skippedSpecs.has(s)),
291
+ });
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Execute a set of specs in parallel, respecting maxParallel limit (Req 3.5).
297
+ *
298
+ * @param {string[]} specNames
299
+ * @param {number} maxParallel
300
+ * @param {number} maxRetries
301
+ * @returns {Promise<void>}
302
+ * @private
303
+ */
304
+ async _executeSpecsInParallel(specNames, maxParallel, maxRetries) {
305
+ const pending = [...specNames];
306
+ const inFlight = new Map(); // specName → Promise
307
+
308
+ const launchNext = async () => {
309
+ while (pending.length > 0 && !this._stopped) {
310
+ const rateLimitHoldMs = this._getRateLimitLaunchHoldRemainingMs();
311
+ const launchBudgetHoldMs = this._getLaunchBudgetHoldRemainingMs();
312
+ const launchHoldMs = Math.max(rateLimitHoldMs, launchBudgetHoldMs);
313
+ if (launchHoldMs > 0) {
314
+ // Pause new launches when provider asks us to retry later or launch budget is exhausted.
315
+ if (launchBudgetHoldMs > 0) {
316
+ this._onLaunchBudgetHold(launchBudgetHoldMs);
317
+ }
318
+ await this._sleep(Math.min(launchHoldMs, 1000));
319
+ continue;
320
+ }
321
+
322
+ if (inFlight.size >= this._getEffectiveMaxParallel(maxParallel)) {
323
+ break;
324
+ }
325
+
326
+ const specName = pending.shift();
327
+ if (this._skippedSpecs.has(specName)) continue;
328
+
329
+ this._recordLaunchStart();
330
+ const promise = this._executeSpec(specName, maxRetries);
331
+ inFlight.set(specName, promise);
332
+
333
+ // When done, remove from inFlight and try to launch more
334
+ promise.then(() => {
335
+ inFlight.delete(specName);
336
+ });
337
+ }
338
+ };
339
+
340
+ // Initial launch
341
+ await launchNext();
342
+
343
+ // Wait for all in-flight specs to complete, launching new ones as slots open
344
+ while (inFlight.size > 0 && !this._stopped) {
345
+ // Wait for any one to complete
346
+ await Promise.race(inFlight.values());
347
+ // Launch more if slots available
348
+ await launchNext();
349
+ }
350
+ }
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // Single Spec Execution
354
+ // ---------------------------------------------------------------------------
355
+
356
+ /**
357
+ * Execute a single spec with retry support (Req 5.2, 5.3).
358
+ *
359
+ * @param {string} specName
360
+ * @param {number} maxRetries
361
+ * @returns {Promise<void>}
362
+ * @private
363
+ */
364
+ async _executeSpec(specName, maxRetries) {
365
+ if (this._stopped) return;
366
+
367
+ this._retryCounts.set(specName, this._retryCounts.get(specName) || 0);
368
+
369
+ // Transition to assigned then in-progress via SLM (Req 8.1)
370
+ await this._transitionSafe(specName, 'assigned');
371
+ await this._transitionSafe(specName, 'in-progress');
372
+
373
+ this._statusMonitor.updateSpecStatus(specName, 'running');
374
+ this.emit('spec:start', { specName });
375
+
376
+ try {
377
+ // Spawn agent via AgentSpawner
378
+ const agent = await this._agentSpawner.spawn(specName);
379
+ this._runningAgents.set(specName, agent.agentId);
380
+
381
+ // Wait for agent completion
382
+ const result = await this._waitForAgent(specName, agent.agentId);
383
+
384
+ this._runningAgents.delete(specName);
385
+
386
+ if (result.status === 'completed') {
387
+ await this._handleSpecCompleted(specName, agent.agentId);
388
+ } else {
389
+ // failed or timeout (Req 5.1, 5.4)
390
+ await this._handleSpecFailed(specName, agent.agentId, maxRetries, result.error);
391
+ }
392
+ } catch (err) {
393
+ // Spawn failure (Req 5.1)
394
+ this._runningAgents.delete(specName);
395
+ await this._handleSpecFailed(specName, null, maxRetries, err.message);
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Wait for an agent to complete, fail, or timeout.
401
+ * Returns a promise that resolves with the outcome.
402
+ *
403
+ * @param {string} specName
404
+ * @param {string} agentId
405
+ * @returns {Promise<{status: string, error: string|null}>}
406
+ * @private
407
+ */
408
+ _waitForAgent(specName, agentId) {
409
+ return new Promise((resolve) => {
410
+ const onCompleted = (data) => {
411
+ if (data.agentId === agentId) {
412
+ cleanup();
413
+ resolve({ status: 'completed', error: null });
414
+ }
415
+ };
416
+
417
+ const onFailed = (data) => {
418
+ if (data.agentId === agentId) {
419
+ cleanup();
420
+ const error = data.stderr || data.error || `Exit code: ${data.exitCode}`;
421
+ resolve({ status: 'failed', error });
422
+ }
423
+ };
424
+
425
+ const onTimeout = (data) => {
426
+ if (data.agentId === agentId) {
427
+ cleanup();
428
+ resolve({ status: 'timeout', error: `Timeout after ${data.timeoutSeconds}s` });
429
+ }
430
+ };
431
+
432
+ const cleanup = () => {
433
+ this._agentSpawner.removeListener('agent:completed', onCompleted);
434
+ this._agentSpawner.removeListener('agent:failed', onFailed);
435
+ this._agentSpawner.removeListener('agent:timeout', onTimeout);
436
+ };
437
+
438
+ this._agentSpawner.on('agent:completed', onCompleted);
439
+ this._agentSpawner.on('agent:failed', onFailed);
440
+ this._agentSpawner.on('agent:timeout', onTimeout);
441
+ });
442
+ }
443
+
444
+ /**
445
+ * Handle successful spec completion (Req 8.2, 5.6).
446
+ *
447
+ * @param {string} specName
448
+ * @param {string} agentId
449
+ * @returns {Promise<void>}
450
+ * @private
451
+ */
452
+ async _handleSpecCompleted(specName, agentId) {
453
+ this._completedSpecs.add(specName);
454
+ this._statusMonitor.updateSpecStatus(specName, 'completed', agentId);
455
+
456
+ // Transition to completed via SLM (Req 8.2)
457
+ await this._transitionSafe(specName, 'completed');
458
+
459
+ // Sync external status (Req 8.5)
460
+ await this._syncExternalSafe(specName, 'completed');
461
+
462
+ this.emit('spec:complete', { specName, agentId });
463
+ }
464
+
465
+ /**
466
+ * Handle spec failure — retry or propagate (Req 5.2, 5.3, 3.6).
467
+ *
468
+ * @param {string} specName
469
+ * @param {string|null} agentId
470
+ * @param {number} maxRetries
471
+ * @param {string} error
472
+ * @returns {Promise<void>}
473
+ * @private
474
+ */
475
+ async _handleSpecFailed(specName, agentId, maxRetries, error) {
476
+ const resolvedError = `${error || 'Unknown error'}`;
477
+ const retryCount = this._retryCounts.get(specName) || 0;
478
+ const isRateLimitError = this._isRateLimitError(resolvedError);
479
+ const retryLimit = isRateLimitError
480
+ ? Math.max(maxRetries, this._rateLimitMaxRetries || DEFAULT_RATE_LIMIT_MAX_RETRIES)
481
+ : maxRetries;
482
+
483
+ if (retryCount < retryLimit && !this._stopped) {
484
+ // Retry (Req 5.2)
485
+ this._retryCounts.set(specName, retryCount + 1);
486
+ this._statusMonitor.incrementRetry(specName);
487
+ this._statusMonitor.updateSpecStatus(specName, 'pending', null, resolvedError);
488
+
489
+ const retryDelayMs = isRateLimitError
490
+ ? Math.max(
491
+ this._calculateRateLimitBackoffMs(retryCount),
492
+ this._extractRateLimitRetryAfterMs(resolvedError)
493
+ )
494
+ : 0;
495
+ if (retryDelayMs > 0) {
496
+ this._onRateLimitSignal(retryDelayMs);
497
+ const launchHoldMs = this._getRateLimitLaunchHoldRemainingMs();
498
+ this._updateStatusMonitorRateLimit({
499
+ specName,
500
+ retryCount,
501
+ retryDelayMs,
502
+ launchHoldMs,
503
+ error: resolvedError,
504
+ });
505
+ this.emit('spec:rate-limited', {
506
+ specName,
507
+ retryCount,
508
+ retryDelayMs,
509
+ launchHoldMs,
510
+ error: resolvedError,
511
+ });
512
+ await this._sleep(retryDelayMs);
513
+ if (this._stopped) {
514
+ return;
515
+ }
516
+ }
517
+
518
+ // Re-execute
519
+ await this._executeSpec(specName, maxRetries);
520
+ } else {
521
+ // Final failure (Req 5.3)
522
+ this._failedSpecs.add(specName);
523
+ this._statusMonitor.updateSpecStatus(specName, 'failed', agentId, resolvedError);
524
+
525
+ // Sync external status
526
+ await this._syncExternalSafe(specName, 'failed');
527
+
528
+ this.emit('spec:failed', { specName, agentId, error: resolvedError, retryCount });
529
+
530
+ // Propagate failure to dependents (Req 3.6)
531
+ this._propagateFailure(specName);
532
+ }
533
+ }
534
+
535
+ // ---------------------------------------------------------------------------
536
+ // Dependency Graph & Batch Computation
537
+ // ---------------------------------------------------------------------------
538
+
539
+ /**
540
+ * Extract dependency map from the graph for the given specs.
541
+ * edges go FROM dependent TO dependency (from: specA, to: specB means specA depends on specB).
542
+ *
543
+ * @param {object} graph - {nodes, edges}
544
+ * @param {string[]} specNames
545
+ * @returns {object} {[specName]: string[]} - each spec maps to its dependencies
546
+ * @private
547
+ */
548
+ _extractDependencies(graph, specNames) {
549
+ const specSet = new Set(specNames);
550
+ const deps = {};
551
+
552
+ for (const specName of specNames) {
553
+ deps[specName] = [];
554
+ }
555
+
556
+ for (const edge of graph.edges) {
557
+ if (specSet.has(edge.from) && specSet.has(edge.to)) {
558
+ deps[edge.from].push(edge.to);
559
+ }
560
+ }
561
+
562
+ return deps;
563
+ }
564
+
565
+ /**
566
+ * Compute execution batches via topological sort (Req 3.3).
567
+ * Specs with no dependencies → batch 0.
568
+ * Specs whose dependencies are all in earlier batches → next batch.
569
+ *
570
+ * @param {string[]} specNames
571
+ * @param {object} dependencies - {[specName]: string[]}
572
+ * @returns {string[][]} Array of batches
573
+ * @private
574
+ */
575
+ _computeBatches(specNames, dependencies) {
576
+ const batches = [];
577
+ const assigned = new Set(); // specs already assigned to a batch
578
+
579
+ while (assigned.size < specNames.length) {
580
+ const batch = [];
581
+
582
+ for (const specName of specNames) {
583
+ if (assigned.has(specName)) continue;
584
+
585
+ // Check if all dependencies are in earlier batches
586
+ const deps = dependencies[specName] || [];
587
+ const allDepsAssigned = deps.every(d => assigned.has(d));
588
+
589
+ if (allDepsAssigned) {
590
+ batch.push(specName);
591
+ }
592
+ }
593
+
594
+ if (batch.length === 0) {
595
+ // Should not happen if cycle detection passed, but safety guard
596
+ break;
597
+ }
598
+
599
+ batches.push(batch);
600
+ for (const specName of batch) {
601
+ assigned.add(specName);
602
+ }
603
+ }
604
+
605
+ return batches;
606
+ }
607
+
608
+ /**
609
+ * Propagate failure: mark all direct and indirect dependents as skipped (Req 3.6).
610
+ *
611
+ * @param {string} failedSpec
612
+ * @private
613
+ */
614
+ _propagateFailure(failedSpec) {
615
+ if (!this._executionPlan) return;
616
+
617
+ const deps = this._executionPlan.dependencies;
618
+ const toSkip = new Set();
619
+
620
+ // Find all specs that directly or indirectly depend on failedSpec
621
+ const findDependents = (specName) => {
622
+ for (const candidate of this._executionPlan.specs) {
623
+ if (toSkip.has(candidate) || this._completedSpecs.has(candidate)) continue;
624
+ const candidateDeps = deps[candidate] || [];
625
+ if (candidateDeps.includes(specName)) {
626
+ toSkip.add(candidate);
627
+ findDependents(candidate); // recursive: indirect dependents
628
+ }
629
+ }
630
+ };
631
+
632
+ findDependents(failedSpec);
633
+
634
+ for (const specName of toSkip) {
635
+ this._skippedSpecs.add(specName);
636
+ this._statusMonitor.updateSpecStatus(
637
+ specName, 'skipped', null,
638
+ `Skipped: dependency '${failedSpec}' failed`
639
+ );
640
+ }
641
+ }
642
+
643
+ // ---------------------------------------------------------------------------
644
+ // Validation & Helpers
645
+ // ---------------------------------------------------------------------------
646
+
647
+ /**
648
+ * Resolve retry-related runtime config with safe defaults.
649
+ *
650
+ * @param {object} config
651
+ * @private
652
+ */
653
+ _applyRetryPolicyConfig(config) {
654
+ this._rateLimitMaxRetries = this._toNonNegativeInteger(
655
+ config && config.rateLimitMaxRetries,
656
+ DEFAULT_RATE_LIMIT_MAX_RETRIES
657
+ );
658
+
659
+ const baseMs = this._toPositiveInteger(
660
+ config && config.rateLimitBackoffBaseMs,
661
+ DEFAULT_RATE_LIMIT_BACKOFF_BASE_MS
662
+ );
663
+ const maxMs = this._toPositiveInteger(
664
+ config && config.rateLimitBackoffMaxMs,
665
+ DEFAULT_RATE_LIMIT_BACKOFF_MAX_MS
666
+ );
667
+
668
+ this._rateLimitBackoffBaseMs = Math.min(baseMs, maxMs);
669
+ this._rateLimitBackoffMaxMs = Math.max(baseMs, maxMs);
670
+ this._rateLimitAdaptiveParallel = this._toBoolean(
671
+ config && config.rateLimitAdaptiveParallel,
672
+ DEFAULT_RATE_LIMIT_ADAPTIVE_PARALLEL
673
+ );
674
+ this._rateLimitParallelFloor = this._toPositiveInteger(
675
+ config && config.rateLimitParallelFloor,
676
+ DEFAULT_RATE_LIMIT_PARALLEL_FLOOR
677
+ );
678
+ this._rateLimitCooldownMs = this._toPositiveInteger(
679
+ config && config.rateLimitCooldownMs,
680
+ DEFAULT_RATE_LIMIT_COOLDOWN_MS
681
+ );
682
+ this._rateLimitLaunchBudgetPerMinute = this._toNonNegativeInteger(
683
+ config && config.rateLimitLaunchBudgetPerMinute,
684
+ DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_PER_MINUTE
685
+ );
686
+ this._rateLimitLaunchBudgetWindowMs = this._toPositiveInteger(
687
+ config && config.rateLimitLaunchBudgetWindowMs,
688
+ DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_WINDOW_MS
689
+ );
690
+ }
691
+
692
+ /**
693
+ * @param {number} maxParallel
694
+ * @private
695
+ */
696
+ _initializeAdaptiveParallel(maxParallel) {
697
+ const boundedMax = this._toPositiveInteger(maxParallel, 1);
698
+ this._baseMaxParallel = boundedMax;
699
+ this._effectiveMaxParallel = boundedMax;
700
+ this._rateLimitCooldownUntil = 0;
701
+ this._rateLimitLaunchHoldUntil = 0;
702
+ this._rateLimitLaunchTimestamps = [];
703
+ this._launchBudgetLastHoldSignalAt = 0;
704
+ this._launchBudgetLastHoldMs = 0;
705
+ this._updateStatusMonitorParallelTelemetry({
706
+ adaptive: this._isAdaptiveParallelEnabled(),
707
+ maxParallel: boundedMax,
708
+ effectiveMaxParallel: boundedMax,
709
+ floor: Math.min(
710
+ boundedMax,
711
+ this._toPositiveInteger(this._rateLimitParallelFloor, DEFAULT_RATE_LIMIT_PARALLEL_FLOOR)
712
+ ),
713
+ });
714
+ const launchBudgetConfig = this._getLaunchBudgetConfig();
715
+ if (launchBudgetConfig.budgetPerMinute > 0) {
716
+ this._updateStatusMonitorLaunchBudget({
717
+ budgetPerMinute: launchBudgetConfig.budgetPerMinute,
718
+ windowMs: launchBudgetConfig.windowMs,
719
+ used: 0,
720
+ holdMs: 0,
721
+ });
722
+ }
723
+ }
724
+
725
+ /**
726
+ * @param {number} maxParallel
727
+ * @returns {number}
728
+ * @private
729
+ */
730
+ _getEffectiveMaxParallel(maxParallel) {
731
+ const boundedMax = this._toPositiveInteger(maxParallel, 1);
732
+ const floor = Math.min(
733
+ boundedMax,
734
+ this._toPositiveInteger(this._rateLimitParallelFloor, DEFAULT_RATE_LIMIT_PARALLEL_FLOOR)
735
+ );
736
+
737
+ if (!this._isAdaptiveParallelEnabled()) {
738
+ this._baseMaxParallel = boundedMax;
739
+ this._effectiveMaxParallel = boundedMax;
740
+ this._updateStatusMonitorParallelTelemetry({
741
+ adaptive: false,
742
+ maxParallel: boundedMax,
743
+ effectiveMaxParallel: boundedMax,
744
+ floor,
745
+ });
746
+ return boundedMax;
747
+ }
748
+
749
+ this._baseMaxParallel = boundedMax;
750
+ this._maybeRecoverParallelLimit(boundedMax);
751
+
752
+ const effective = this._toPositiveInteger(this._effectiveMaxParallel, boundedMax);
753
+ const resolved = Math.max(floor, Math.min(boundedMax, effective));
754
+ this._updateStatusMonitorParallelTelemetry({
755
+ adaptive: true,
756
+ maxParallel: boundedMax,
757
+ effectiveMaxParallel: resolved,
758
+ floor,
759
+ });
760
+ return resolved;
761
+ }
762
+
763
+ /**
764
+ * @private
765
+ */
766
+ _onRateLimitSignal(retryDelayMs = 0) {
767
+ const now = this._getNow();
768
+ const launchHoldMs = this._toNonNegativeInteger(retryDelayMs, 0);
769
+ if (launchHoldMs > 0) {
770
+ const currentHoldUntil = this._toNonNegativeInteger(this._rateLimitLaunchHoldUntil, 0);
771
+ this._rateLimitLaunchHoldUntil = Math.max(currentHoldUntil, now + launchHoldMs);
772
+ }
773
+
774
+ if (!this._isAdaptiveParallelEnabled()) {
775
+ return;
776
+ }
777
+
778
+ const base = this._toPositiveInteger(this._baseMaxParallel, 1);
779
+ const current = this._toPositiveInteger(this._effectiveMaxParallel, base);
780
+ const floor = Math.min(
781
+ base,
782
+ this._toPositiveInteger(this._rateLimitParallelFloor, DEFAULT_RATE_LIMIT_PARALLEL_FLOOR)
783
+ );
784
+ const next = Math.max(floor, Math.floor(current / 2));
785
+
786
+ if (next < current) {
787
+ this._effectiveMaxParallel = next;
788
+ this._updateStatusMonitorParallelTelemetry({
789
+ event: 'throttled',
790
+ reason: 'rate-limit',
791
+ adaptive: true,
792
+ maxParallel: base,
793
+ effectiveMaxParallel: next,
794
+ floor,
795
+ });
796
+ this.emit('parallel:throttled', {
797
+ reason: 'rate-limit',
798
+ previousMaxParallel: current,
799
+ effectiveMaxParallel: next,
800
+ floor,
801
+ });
802
+ } else {
803
+ this._effectiveMaxParallel = current;
804
+ }
805
+
806
+ this._rateLimitCooldownUntil = now + this._rateLimitCooldownMs;
807
+ }
808
+
809
+ /**
810
+ * @param {number} maxParallel
811
+ * @private
812
+ */
813
+ _maybeRecoverParallelLimit(maxParallel) {
814
+ if (!this._isAdaptiveParallelEnabled()) {
815
+ return;
816
+ }
817
+
818
+ const boundedMax = this._toPositiveInteger(maxParallel, 1);
819
+ const current = this._toPositiveInteger(this._effectiveMaxParallel, boundedMax);
820
+ if (current >= boundedMax) {
821
+ this._effectiveMaxParallel = boundedMax;
822
+ return;
823
+ }
824
+
825
+ if (this._getNow() < this._rateLimitCooldownUntil) {
826
+ return;
827
+ }
828
+
829
+ const next = Math.min(boundedMax, current + 1);
830
+ if (next > current) {
831
+ this._effectiveMaxParallel = next;
832
+ this._rateLimitCooldownUntil = this._getNow() + this._rateLimitCooldownMs;
833
+ this._updateStatusMonitorParallelTelemetry({
834
+ event: 'recovered',
835
+ adaptive: true,
836
+ maxParallel: boundedMax,
837
+ effectiveMaxParallel: next,
838
+ });
839
+ this.emit('parallel:recovered', {
840
+ previousMaxParallel: current,
841
+ effectiveMaxParallel: next,
842
+ maxParallel: boundedMax,
843
+ });
844
+ }
845
+ }
846
+
847
+ /**
848
+ * @returns {boolean}
849
+ * @private
850
+ */
851
+ _isAdaptiveParallelEnabled() {
852
+ if (typeof this._rateLimitAdaptiveParallel === 'boolean') {
853
+ return this._rateLimitAdaptiveParallel;
854
+ }
855
+ return DEFAULT_RATE_LIMIT_ADAPTIVE_PARALLEL;
856
+ }
857
+
858
+ /**
859
+ * @returns {number}
860
+ * @private
861
+ */
862
+ _getNow() {
863
+ return typeof this._now === 'function' ? this._now() : Date.now();
864
+ }
865
+
866
+ /**
867
+ * @returns {number}
868
+ * @private
869
+ */
870
+ _getRateLimitLaunchHoldRemainingMs() {
871
+ const holdUntil = this._toNonNegativeInteger(this._rateLimitLaunchHoldUntil, 0);
872
+ if (holdUntil <= 0) {
873
+ return 0;
874
+ }
875
+ return Math.max(0, holdUntil - this._getNow());
876
+ }
877
+
878
+ /**
879
+ * @returns {{ budgetPerMinute: number, windowMs: number }}
880
+ * @private
881
+ */
882
+ _getLaunchBudgetConfig() {
883
+ return {
884
+ budgetPerMinute: this._toNonNegativeInteger(
885
+ this._rateLimitLaunchBudgetPerMinute,
886
+ DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_PER_MINUTE
887
+ ),
888
+ windowMs: this._toPositiveInteger(
889
+ this._rateLimitLaunchBudgetWindowMs,
890
+ DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_WINDOW_MS
891
+ ),
892
+ };
893
+ }
894
+
895
+ /**
896
+ * @param {number} windowMs
897
+ * @private
898
+ */
899
+ _pruneLaunchBudgetHistory(windowMs) {
900
+ const now = this._getNow();
901
+ const history = Array.isArray(this._rateLimitLaunchTimestamps)
902
+ ? this._rateLimitLaunchTimestamps
903
+ : [];
904
+ this._rateLimitLaunchTimestamps = history
905
+ .filter((timestamp) => Number.isFinite(timestamp) && timestamp > (now - windowMs));
906
+ }
907
+
908
+ /**
909
+ * @returns {number}
910
+ * @private
911
+ */
912
+ _getLaunchBudgetHoldRemainingMs() {
913
+ const { budgetPerMinute, windowMs } = this._getLaunchBudgetConfig();
914
+ if (budgetPerMinute <= 0) {
915
+ return 0;
916
+ }
917
+ this._pruneLaunchBudgetHistory(windowMs);
918
+ if (this._rateLimitLaunchTimestamps.length < budgetPerMinute) {
919
+ return 0;
920
+ }
921
+ const oldest = this._rateLimitLaunchTimestamps[0];
922
+ if (!Number.isFinite(oldest)) {
923
+ return 0;
924
+ }
925
+ return Math.max(0, windowMs - (this._getNow() - oldest));
926
+ }
927
+
928
+ /**
929
+ * @private
930
+ */
931
+ _recordLaunchStart() {
932
+ const { budgetPerMinute, windowMs } = this._getLaunchBudgetConfig();
933
+ if (budgetPerMinute <= 0) {
934
+ return;
935
+ }
936
+ this._pruneLaunchBudgetHistory(windowMs);
937
+ this._rateLimitLaunchTimestamps.push(this._getNow());
938
+ const holdMs = this._getLaunchBudgetHoldRemainingMs();
939
+ this._updateStatusMonitorLaunchBudget({
940
+ budgetPerMinute,
941
+ windowMs,
942
+ used: this._rateLimitLaunchTimestamps.length,
943
+ holdMs,
944
+ });
945
+ }
946
+
947
+ /**
948
+ * @param {number} holdMs
949
+ * @private
950
+ */
951
+ _onLaunchBudgetHold(holdMs) {
952
+ const { budgetPerMinute, windowMs } = this._getLaunchBudgetConfig();
953
+ if (budgetPerMinute <= 0 || holdMs <= 0) {
954
+ return;
955
+ }
956
+ if (!Array.isArray(this._rateLimitLaunchTimestamps)) {
957
+ this._rateLimitLaunchTimestamps = [];
958
+ }
959
+
960
+ const now = this._getNow();
961
+ const lastSignalAt = this._toNonNegativeInteger(this._launchBudgetLastHoldSignalAt, 0);
962
+ const lastHoldMs = this._toNonNegativeInteger(this._launchBudgetLastHoldMs, 0);
963
+ const deltaFromLast = now - lastSignalAt;
964
+ const holdDelta = Math.abs(holdMs - lastHoldMs);
965
+ if (deltaFromLast < 1000 && holdDelta < 200) {
966
+ return;
967
+ }
968
+ this._launchBudgetLastHoldSignalAt = now;
969
+ this._launchBudgetLastHoldMs = holdMs;
970
+
971
+ this._updateStatusMonitorLaunchBudget({
972
+ event: 'hold',
973
+ budgetPerMinute,
974
+ windowMs,
975
+ used: this._rateLimitLaunchTimestamps.length,
976
+ holdMs,
977
+ });
978
+ this.emit('launch:budget-hold', {
979
+ reason: 'rate-limit-launch-budget',
980
+ holdMs,
981
+ budgetPerMinute,
982
+ windowMs,
983
+ used: this._rateLimitLaunchTimestamps.length,
984
+ });
985
+ }
986
+
987
+ /**
988
+ * @param {any} value
989
+ * @param {boolean} fallback
990
+ * @returns {boolean}
991
+ * @private
992
+ */
993
+ _toBoolean(value, fallback) {
994
+ if (typeof value === 'boolean') {
995
+ return value;
996
+ }
997
+ return fallback;
998
+ }
999
+
1000
+ /**
1001
+ * @param {any} value
1002
+ * @param {number} fallback
1003
+ * @returns {number}
1004
+ * @private
1005
+ */
1006
+ _toPositiveInteger(value, fallback) {
1007
+ const numeric = Number(value);
1008
+ if (!Number.isFinite(numeric) || numeric <= 0) {
1009
+ return fallback;
1010
+ }
1011
+ return Math.floor(numeric);
1012
+ }
1013
+
1014
+ /**
1015
+ * @param {any} value
1016
+ * @param {number} fallback
1017
+ * @returns {number}
1018
+ * @private
1019
+ */
1020
+ _toNonNegativeInteger(value, fallback) {
1021
+ const numeric = Number(value);
1022
+ if (!Number.isFinite(numeric) || numeric < 0) {
1023
+ return fallback;
1024
+ }
1025
+ return Math.floor(numeric);
1026
+ }
1027
+
1028
+ /**
1029
+ * @param {string} error
1030
+ * @returns {boolean}
1031
+ * @private
1032
+ */
1033
+ _isRateLimitError(error) {
1034
+ return RATE_LIMIT_ERROR_PATTERNS.some(pattern => pattern.test(`${error || ''}`));
1035
+ }
1036
+
1037
+ /**
1038
+ * Parse retry-after hints from rate-limit error messages.
1039
+ * Supports formats like:
1040
+ * - "Retry-After: 7"
1041
+ * - "retry after 2s"
1042
+ * - "try again in 1500ms"
1043
+ *
1044
+ * @param {string} error
1045
+ * @returns {number} delay in ms (0 when no hint)
1046
+ * @private
1047
+ */
1048
+ _extractRateLimitRetryAfterMs(error) {
1049
+ const message = `${error || ''}`;
1050
+ if (!message) {
1051
+ return 0;
1052
+ }
1053
+
1054
+ const patterns = [
1055
+ /retry[-_\s]?after\s*[:=]?\s*(\d+(?:\.\d+)?)\s*(ms|msec|milliseconds?|s|sec|seconds?|m|min|minutes?)?/i,
1056
+ /try\s+again\s+in\s+(\d+(?:\.\d+)?)\s*(ms|msec|milliseconds?|s|sec|seconds?|m|min|minutes?)?/i,
1057
+ ];
1058
+
1059
+ for (const pattern of patterns) {
1060
+ const match = pattern.exec(message);
1061
+ if (!match) {
1062
+ continue;
1063
+ }
1064
+
1065
+ const value = Number(match[1]);
1066
+ if (!Number.isFinite(value) || value <= 0) {
1067
+ continue;
1068
+ }
1069
+
1070
+ const unit = `${match[2] || 's'}`.trim().toLowerCase();
1071
+ let multiplier = 1000;
1072
+ if (unit === 'ms' || unit === 'msec' || unit.startsWith('millisecond')) {
1073
+ multiplier = 1;
1074
+ } else if (unit === 'm' || unit === 'min' || unit.startsWith('minute')) {
1075
+ multiplier = 60 * 1000;
1076
+ } else {
1077
+ multiplier = 1000;
1078
+ }
1079
+
1080
+ const delayMs = Math.round(value * multiplier);
1081
+ return Math.max(0, Math.min(RATE_LIMIT_RETRY_AFTER_MAX_MS, delayMs));
1082
+ }
1083
+
1084
+ return 0;
1085
+ }
1086
+
1087
+ /**
1088
+ * @param {number} retryCount
1089
+ * @returns {number}
1090
+ * @private
1091
+ */
1092
+ _calculateRateLimitBackoffMs(retryCount) {
1093
+ const exponent = Math.max(0, retryCount);
1094
+ const cappedBaseDelay = Math.min(
1095
+ this._rateLimitBackoffMaxMs || DEFAULT_RATE_LIMIT_BACKOFF_MAX_MS,
1096
+ (this._rateLimitBackoffBaseMs || DEFAULT_RATE_LIMIT_BACKOFF_BASE_MS) * (2 ** exponent)
1097
+ );
1098
+
1099
+ const randomValue = typeof this._random === 'function' ? this._random() : Math.random();
1100
+ const normalizedRandom = Number.isFinite(randomValue)
1101
+ ? Math.min(1, Math.max(0, randomValue))
1102
+ : 0.5;
1103
+ const jitterFactor = (1 - RATE_LIMIT_BACKOFF_JITTER_RATIO)
1104
+ + (normalizedRandom * RATE_LIMIT_BACKOFF_JITTER_RATIO);
1105
+
1106
+ return Math.max(1, Math.round(cappedBaseDelay * jitterFactor));
1107
+ }
1108
+
1109
+ /**
1110
+ * @param {number} ms
1111
+ * @returns {Promise<void>}
1112
+ * @private
1113
+ */
1114
+ _sleep(ms) {
1115
+ if (!ms || ms <= 0) {
1116
+ return Promise.resolve();
1117
+ }
1118
+ return new Promise((resolve) => setTimeout(resolve, ms));
1119
+ }
1120
+
1121
+ /**
1122
+ * Safely update StatusMonitor rate-limit telemetry.
1123
+ *
1124
+ * @param {object} payload
1125
+ * @private
1126
+ */
1127
+ _updateStatusMonitorRateLimit(payload) {
1128
+ const handler = this._statusMonitor && this._statusMonitor.recordRateLimitEvent;
1129
+ if (typeof handler === 'function') {
1130
+ try {
1131
+ handler.call(this._statusMonitor, payload);
1132
+ } catch (_err) {
1133
+ // Non-fatal status telemetry update.
1134
+ }
1135
+ }
1136
+ }
1137
+
1138
+ /**
1139
+ * Safely update StatusMonitor adaptive parallel telemetry.
1140
+ *
1141
+ * @param {object} payload
1142
+ * @private
1143
+ */
1144
+ _updateStatusMonitorParallelTelemetry(payload) {
1145
+ const handler = this._statusMonitor && this._statusMonitor.updateParallelTelemetry;
1146
+ if (typeof handler === 'function') {
1147
+ try {
1148
+ handler.call(this._statusMonitor, payload);
1149
+ } catch (_err) {
1150
+ // Non-fatal status telemetry update.
1151
+ }
1152
+ }
1153
+ }
1154
+
1155
+ /**
1156
+ * Safely update StatusMonitor launch-budget telemetry.
1157
+ *
1158
+ * @param {object} payload
1159
+ * @private
1160
+ */
1161
+ _updateStatusMonitorLaunchBudget(payload) {
1162
+ const handler = this._statusMonitor && this._statusMonitor.updateLaunchBudgetTelemetry;
1163
+ if (typeof handler === 'function') {
1164
+ try {
1165
+ handler.call(this._statusMonitor, payload);
1166
+ } catch (_err) {
1167
+ // Non-fatal status telemetry update.
1168
+ }
1169
+ }
1170
+ }
1171
+
1172
+ /**
1173
+ * Validate that all spec directories exist (Req 6.4).
1174
+ *
1175
+ * @param {string[]} specNames
1176
+ * @returns {Promise<string[]>} List of missing spec names
1177
+ * @private
1178
+ */
1179
+ async _validateSpecExistence(specNames) {
1180
+ const missing = [];
1181
+ for (const specName of specNames) {
1182
+ const specDir = path.join(this._workspaceRoot, SPECS_DIR, specName);
1183
+ const exists = await fsUtils.pathExists(specDir);
1184
+ if (!exists) {
1185
+ missing.push(specName);
1186
+ }
1187
+ }
1188
+ return missing;
1189
+ }
1190
+
1191
+ /**
1192
+ * Safely transition a spec via SpecLifecycleManager (Req 8.1, 8.2).
1193
+ * Failures are logged but do not propagate (non-fatal).
1194
+ *
1195
+ * @param {string} specName
1196
+ * @param {string} newStatus
1197
+ * @returns {Promise<void>}
1198
+ * @private
1199
+ */
1200
+ async _transitionSafe(specName, newStatus) {
1201
+ try {
1202
+ await this._specLifecycleManager.transition(specName, newStatus);
1203
+ } catch (err) {
1204
+ console.warn(
1205
+ `[OrchestrationEngine] SLM transition failed for ${specName} → ${newStatus}: ${err.message}`
1206
+ );
1207
+ }
1208
+ }
1209
+
1210
+ /**
1211
+ * Safely sync external status via StatusMonitor (Req 8.5).
1212
+ * Failures are logged but do not propagate (non-fatal).
1213
+ *
1214
+ * @param {string} specName
1215
+ * @param {string} status
1216
+ * @returns {Promise<void>}
1217
+ * @private
1218
+ */
1219
+ async _syncExternalSafe(specName, status) {
1220
+ try {
1221
+ await this._statusMonitor.syncExternalStatus(specName, status);
1222
+ } catch (err) {
1223
+ console.warn(
1224
+ `[OrchestrationEngine] External sync failed for ${specName}: ${err.message}`
1225
+ );
1226
+ }
1227
+ }
1228
+
1229
+ /**
1230
+ * Build the orchestration result object.
1231
+ *
1232
+ * @param {string} status
1233
+ * @param {string|null} [error=null]
1234
+ * @returns {object}
1235
+ * @private
1236
+ */
1237
+ _buildResult(status, error = null) {
1238
+ return {
1239
+ status,
1240
+ plan: this._executionPlan,
1241
+ completed: [...this._completedSpecs],
1242
+ failed: [...this._failedSpecs],
1243
+ skipped: [...this._skippedSpecs],
1244
+ error,
1245
+ };
1246
+ }
1247
+
1248
+ /**
1249
+ * Reset internal state for a new orchestration run.
1250
+ * @private
1251
+ */
1252
+ _reset() {
1253
+ this._runningAgents.clear();
1254
+ this._retryCounts.clear();
1255
+ this._failedSpecs.clear();
1256
+ this._skippedSpecs.clear();
1257
+ this._completedSpecs.clear();
1258
+ this._executionPlan = null;
1259
+ this._stopped = false;
1260
+ this._baseMaxParallel = null;
1261
+ this._effectiveMaxParallel = null;
1262
+ this._rateLimitCooldownUntil = 0;
1263
+ this._rateLimitLaunchHoldUntil = 0;
1264
+ this._rateLimitLaunchTimestamps = [];
1265
+ this._launchBudgetLastHoldSignalAt = 0;
1266
+ this._launchBudgetLastHoldMs = 0;
1267
+ }
1268
+ }
1269
+
1270
+ module.exports = { OrchestrationEngine };