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,2476 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { spawn } = require('child_process');
5
+
6
+ const CollaborationManager = require('../collab/collab-manager');
7
+ const { runOrchestration } = require('../commands/orchestrate');
8
+ const { decomposeGoalToSpecPortfolio } = require('./goal-decomposer');
9
+ const { getAgentHints } = require('../scene-runtime/scene-ontology');
10
+
11
+ const CLOSE_LOOP_STRATEGY_MEMORY_VERSION = 1;
12
+ const CLOSE_LOOP_STRATEGY_MEMORY_FILE = path.join('.kiro', 'auto', 'close-loop-strategy-memory.json');
13
+ const RISK_LEVEL_ORDER = {
14
+ low: 1,
15
+ medium: 2,
16
+ high: 3
17
+ };
18
+
19
+ async function runAutoCloseLoop(goal, options = {}, dependencies = {}) {
20
+ const projectPath = dependencies.projectPath || process.cwd();
21
+ const executeOrchestration = dependencies.runOrchestration || runOrchestration;
22
+ const decomposeGoal = dependencies.decomposeGoal || decomposeGoalToSpecPortfolio;
23
+ const runCommand = dependencies.runCommand || executeShellCommand;
24
+ const strategyMemory = await loadCloseLoopStrategyMemory(projectPath);
25
+ const strategyMemoryContext = deriveCloseLoopStrategyMemoryContext(goal, options, strategyMemory);
26
+ const runtimeOptions = {
27
+ ...options,
28
+ ...strategyMemoryContext.option_overrides
29
+ };
30
+ const dodConfig = normalizeDefinitionOfDoneConfig(runtimeOptions);
31
+ const sessionConfig = normalizeSessionConfig(runtimeOptions);
32
+ const replanConfig = normalizeReplanConfig(runtimeOptions);
33
+
34
+ let resumedSession = null;
35
+ let decomposition;
36
+ if (sessionConfig.resumeRef) {
37
+ resumedSession = await resolveCloseLoopSession(projectPath, sessionConfig.resumeRef);
38
+ decomposition = buildDecompositionFromSession(resumedSession.data);
39
+ } else {
40
+ const requestedSubSpecCount = runtimeOptions.subs !== undefined ? Number(runtimeOptions.subs) : undefined;
41
+ decomposition = await decomposeGoal(goal, {
42
+ projectPath,
43
+ prefix: runtimeOptions.prefix,
44
+ subSpecCount: requestedSubSpecCount,
45
+ feedbackBias: strategyMemoryContext.track_bias
46
+ });
47
+ }
48
+
49
+ const masterSpec = decomposition.masterSpec;
50
+ const masterSpecName = masterSpec.name;
51
+ const runtimeSubSpecs = [...decomposition.subSpecs];
52
+ const getAllSpecNames = () => [...runtimeSubSpecs.map(spec => spec.name), masterSpecName];
53
+
54
+ if (runtimeOptions.dryRun) {
55
+ const dryRunResult = {
56
+ mode: 'auto-close-loop',
57
+ goal: decomposition.goal,
58
+ dry_run: true,
59
+ status: 'planned',
60
+ portfolio: {
61
+ prefix: decomposition.prefix,
62
+ master_spec: masterSpecName,
63
+ sub_specs: runtimeSubSpecs.map(spec => spec.name),
64
+ dependency_plan: runtimeSubSpecs.map(spec => ({
65
+ spec: spec.name,
66
+ depends_on: spec.dependencies.map(dep => dep.spec)
67
+ }))
68
+ },
69
+ resumed: Boolean(resumedSession),
70
+ strategy_memory: strategyMemoryContext.telemetry,
71
+ resumed_from_session: resumedSession
72
+ ? {
73
+ id: resumedSession.id,
74
+ file: resumedSession.file
75
+ }
76
+ : null,
77
+ strategy: decomposition.strategy,
78
+ replan: {
79
+ enabled: replanConfig.enabled,
80
+ max_attempts: replanConfig.maxAttempts,
81
+ strategy: replanConfig.strategy,
82
+ effective_max_attempts: replanConfig.maxAttempts,
83
+ no_progress_window: replanConfig.noProgressWindow,
84
+ performed: 0,
85
+ attempts: [],
86
+ exhausted: false,
87
+ stalled_signature: null,
88
+ stalled_no_progress_cycles: 0
89
+ },
90
+ next_actions: [
91
+ 'Run without --dry-run to materialize specs and execute orchestration.'
92
+ ]
93
+ };
94
+
95
+ await maybeWriteOutput(dryRunResult, options, projectPath);
96
+ printResult(dryRunResult, options);
97
+ return dryRunResult;
98
+ }
99
+
100
+ const collabManager = dependencies.collaborationManager || new CollaborationManager(projectPath);
101
+
102
+ const executionPlanning = await buildEnhancedExecutionPlanning({
103
+ projectPath,
104
+ subSpecs: runtimeSubSpecs,
105
+ strategy: decomposition && decomposition.strategy ? decomposition.strategy : null,
106
+ options: runtimeOptions
107
+ });
108
+ applyExecutionPlanning(runtimeSubSpecs, executionPlanning);
109
+
110
+ let assignments;
111
+ if (resumedSession) {
112
+ await ensureExistingSpecs(projectPath, getAllSpecNames());
113
+ assignments = resolveAssignmentsFromSession(resumedSession.data, masterSpec, runtimeSubSpecs);
114
+ } else {
115
+ await ensureSpecDirectoriesAreAvailable(projectPath, [masterSpec, ...runtimeSubSpecs]);
116
+ await writeSpecDocuments(projectPath, decomposition);
117
+
118
+ await collabManager.initMasterSpec(
119
+ masterSpecName,
120
+ runtimeSubSpecs.map(spec => ({ name: spec.name, dependencies: spec.dependencies }))
121
+ );
122
+
123
+ assignments = buildAssignments(masterSpec, runtimeSubSpecs);
124
+ for (const assignment of assignments) {
125
+ await collabManager.assignSpec(assignment.spec, assignment.agent);
126
+ }
127
+ await writeAgentSyncPlan(projectPath, masterSpecName, runtimeSubSpecs, assignments, executionPlanning);
128
+ await syncMasterCollaborationMetadata(collabManager, masterSpecName, runtimeSubSpecs);
129
+ }
130
+
131
+ const runtimeAssignments = [...assignments];
132
+ const leaseBySpec = executionPlanning && executionPlanning.lease_plan && executionPlanning.lease_plan.lease_by_spec
133
+ ? executionPlanning.lease_plan.lease_by_spec
134
+ : {};
135
+ for (const assignment of runtimeAssignments) {
136
+ const leaseKey = leaseBySpec[assignment.spec];
137
+ if (leaseKey) {
138
+ assignment.lease_key = leaseKey;
139
+ }
140
+ }
141
+ const replan = {
142
+ enabled: replanConfig.enabled,
143
+ max_attempts: replanConfig.maxAttempts,
144
+ strategy: replanConfig.strategy,
145
+ effective_max_attempts: replanConfig.maxAttempts,
146
+ no_progress_window: replanConfig.noProgressWindow,
147
+ performed: 0,
148
+ attempts: [],
149
+ exhausted: false,
150
+ stalled_signature: null,
151
+ stalled_no_progress_cycles: 0
152
+ };
153
+ const sessionRuntime = buildCloseLoopSessionRuntime(
154
+ projectPath,
155
+ sessionConfig,
156
+ resumedSession,
157
+ decomposition.prefix
158
+ );
159
+
160
+ if (sessionRuntime && runtimeOptions.run !== false) {
161
+ await persistCloseLoopSessionSnapshot({
162
+ goal: decomposition.goal,
163
+ status: 'running',
164
+ resumed: Boolean(resumedSession),
165
+ portfolio: {
166
+ prefix: decomposition.prefix,
167
+ master_spec: masterSpecName,
168
+ sub_specs: runtimeSubSpecs.map(spec => spec.name),
169
+ dependency_plan: runtimeSubSpecs.map(spec => ({
170
+ spec: spec.name,
171
+ depends_on: spec.dependencies.map(dep => dep.spec)
172
+ })),
173
+ assignments: runtimeAssignments,
174
+ execution_plan: buildExecutionPlanSummary(executionPlanning)
175
+ },
176
+ strategy: decomposition.strategy,
177
+ replan,
178
+ dod: {
179
+ enabled: dodConfig.enabled,
180
+ passed: null,
181
+ checks: [],
182
+ failed_checks: []
183
+ },
184
+ orchestration: null,
185
+ next_actions: [
186
+ 'Session is running. If interrupted, resume with `kse auto close-loop --resume interrupted`.'
187
+ ]
188
+ }, sessionRuntime);
189
+ }
190
+
191
+ let orchestrationResult = null;
192
+ if (runtimeOptions.run !== false) {
193
+ const statusReporter = createStatusReporter(runtimeOptions);
194
+ let cycle = 0;
195
+ const failureSignatures = new Set();
196
+ let noProgressCycles = 0;
197
+ let previousProgressSnapshot = null;
198
+
199
+ while (true) {
200
+ const orchestrationSpecNames = getAllSpecNames();
201
+ orchestrationResult = await executeOrchestration({
202
+ specNames: orchestrationSpecNames,
203
+ maxParallel: runtimeOptions.maxParallel,
204
+ json: false,
205
+ silent: true,
206
+ onStatus: statusReporter,
207
+ statusIntervalMs: 1000
208
+ }, {
209
+ workspaceRoot: projectPath
210
+ });
211
+
212
+ await synchronizeCollaborationStatus(collabManager, orchestrationSpecNames, orchestrationResult);
213
+
214
+ const failedSpecs = collectFailedSpecsForReplan(orchestrationResult, masterSpecName);
215
+ const cycleBudget = resolveEffectiveReplanBudget(replanConfig, failedSpecs.length);
216
+ const effectiveBudget = Math.max(replan.effective_max_attempts, cycleBudget);
217
+ replan.effective_max_attempts = effectiveBudget;
218
+
219
+ const progressEvaluation = evaluateReplanProgressStall({
220
+ orchestrationResult,
221
+ failedSpecs,
222
+ previousSnapshot: previousProgressSnapshot,
223
+ noProgressCycles,
224
+ noProgressWindow: replanConfig.noProgressWindow
225
+ });
226
+ previousProgressSnapshot = progressEvaluation.currentSnapshot;
227
+ noProgressCycles = progressEvaluation.noProgressCycles;
228
+ if (progressEvaluation.shouldStall) {
229
+ replan.exhausted = true;
230
+ replan.stalled_no_progress_cycles = noProgressCycles;
231
+ break;
232
+ }
233
+
234
+ if (!shouldRunReplanCycle(orchestrationResult, failedSpecs, replanConfig, cycle, effectiveBudget)) {
235
+ if (replanConfig.enabled && cycle >= effectiveBudget && failedSpecs.length > 0) {
236
+ replan.exhausted = true;
237
+ }
238
+ break;
239
+ }
240
+
241
+ const failedSignature = createFailedSpecSignature(failedSpecs);
242
+ if (failedSignature && failureSignatures.has(failedSignature)) {
243
+ replan.exhausted = true;
244
+ replan.stalled_signature = failedSignature;
245
+ break;
246
+ }
247
+ if (failedSignature) {
248
+ failureSignatures.add(failedSignature);
249
+ }
250
+
251
+ cycle += 1;
252
+ const remediationPlan = await materializeReplanCycle({
253
+ projectPath,
254
+ goal: decomposition.goal,
255
+ masterSpecName,
256
+ runtimeSubSpecs,
257
+ runtimeAssignments,
258
+ failedSpecs,
259
+ cycle,
260
+ collabManager,
261
+ executionPlanning
262
+ });
263
+
264
+ replan.performed = cycle;
265
+ replan.attempts.push({
266
+ cycle,
267
+ trigger_failed_specs: failedSpecs,
268
+ budget_for_cycle: effectiveBudget,
269
+ added_specs: remediationPlan.addedSpecs.map(spec => spec.name),
270
+ added_assignments: remediationPlan.addedAssignments,
271
+ orchestration_status_before: orchestrationResult.status
272
+ });
273
+ }
274
+ }
275
+
276
+ const finalSpecNames = getAllSpecNames();
277
+ const dod = await evaluateDefinitionOfDone({
278
+ projectPath,
279
+ specNames: finalSpecNames,
280
+ orchestrationResult,
281
+ runInvoked: runtimeOptions.run !== false,
282
+ dodConfig,
283
+ runCommand
284
+ });
285
+
286
+ let status = orchestrationResult ? orchestrationResult.status : 'prepared';
287
+ if (dodConfig.enabled && !dod.passed) {
288
+ status = 'failed';
289
+ }
290
+
291
+ const result = {
292
+ mode: 'auto-close-loop',
293
+ goal: decomposition.goal,
294
+ dry_run: false,
295
+ status,
296
+ resumed: Boolean(resumedSession),
297
+ strategy_memory: strategyMemoryContext.telemetry,
298
+ resumed_from_session: resumedSession
299
+ ? {
300
+ id: resumedSession.id,
301
+ file: resumedSession.file
302
+ }
303
+ : null,
304
+ portfolio: {
305
+ prefix: decomposition.prefix,
306
+ master_spec: masterSpecName,
307
+ sub_specs: runtimeSubSpecs.map(spec => spec.name),
308
+ dependency_plan: runtimeSubSpecs.map(spec => ({
309
+ spec: spec.name,
310
+ depends_on: spec.dependencies.map(dep => dep.spec)
311
+ })),
312
+ assignments: runtimeAssignments,
313
+ execution_plan: buildExecutionPlanSummary(executionPlanning)
314
+ },
315
+ strategy: decomposition.strategy,
316
+ replan,
317
+ dod,
318
+ orchestration: orchestrationResult,
319
+ next_actions: buildNextActions(status, dod, replan)
320
+ };
321
+
322
+ await maybeWriteDodReport(result, runtimeOptions, projectPath);
323
+ await maybePersistCloseLoopSession(result, sessionConfig, resumedSession, projectPath, sessionRuntime);
324
+ await maybePruneCloseLoopSessions(result, sessionConfig, projectPath);
325
+ await updateCloseLoopStrategyMemory(projectPath, result, strategyMemoryContext, strategyMemory);
326
+ await maybeWriteOutput(result, runtimeOptions, projectPath);
327
+ printResult(result, runtimeOptions);
328
+ return result;
329
+ }
330
+
331
+ function normalizeSessionConfig(options) {
332
+ if (options.sessionKeep !== undefined && options.sessionKeep !== null) {
333
+ const parsed = Number(options.sessionKeep);
334
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 1000) {
335
+ throw new Error('--session-keep must be an integer between 0 and 1000');
336
+ }
337
+ }
338
+
339
+ if (options.sessionOlderThanDays !== undefined && options.sessionOlderThanDays !== null) {
340
+ const parsed = Number(options.sessionOlderThanDays);
341
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 36500) {
342
+ throw new Error('--session-older-than-days must be an integer between 0 and 36500');
343
+ }
344
+ }
345
+
346
+ return {
347
+ enabled: options.session !== false,
348
+ resumeRef: typeof options.resume === 'string' && options.resume.trim()
349
+ ? options.resume.trim()
350
+ : null,
351
+ sessionId: typeof options.sessionId === 'string' && options.sessionId.trim()
352
+ ? sanitizeSessionId(options.sessionId.trim())
353
+ : null,
354
+ keep: options.sessionKeep !== undefined && options.sessionKeep !== null
355
+ ? Number(options.sessionKeep)
356
+ : null,
357
+ olderThanDays: options.sessionOlderThanDays !== undefined && options.sessionOlderThanDays !== null
358
+ ? Number(options.sessionOlderThanDays)
359
+ : null
360
+ };
361
+ }
362
+
363
+ function sanitizeSessionId(value) {
364
+ return value
365
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
366
+ .replace(/^-+|-+$/g, '')
367
+ .slice(0, 80);
368
+ }
369
+
370
+ function getCloseLoopSessionDir(projectPath) {
371
+ return path.join(projectPath, '.kiro', 'auto', 'close-loop-sessions');
372
+ }
373
+
374
+ async function resolveCloseLoopSession(projectPath, resumeRef) {
375
+ let filePath = null;
376
+ const sessionDir = getCloseLoopSessionDir(projectPath);
377
+ const normalizedResumeRef = `${resumeRef || ''}`.trim();
378
+ const resumeToken = normalizedResumeRef.toLowerCase();
379
+ const looksLikePath = /[\\/]/.test(normalizedResumeRef) || normalizedResumeRef.toLowerCase().endsWith('.json');
380
+
381
+ if (resumeToken === 'latest') {
382
+ filePath = await resolveLatestSessionFile(sessionDir);
383
+ } else if (resumeToken === 'interrupted' || resumeToken === 'latest-interrupted') {
384
+ filePath = await resolveLatestInterruptedSessionFile(sessionDir);
385
+ } else if (looksLikePath) {
386
+ const candidate = path.isAbsolute(normalizedResumeRef)
387
+ ? normalizedResumeRef
388
+ : path.join(projectPath, normalizedResumeRef);
389
+ if (await fs.pathExists(candidate)) {
390
+ filePath = candidate;
391
+ }
392
+ } else {
393
+ const byId = path.join(sessionDir, `${sanitizeSessionId(normalizedResumeRef)}.json`);
394
+ if (await fs.pathExists(byId)) {
395
+ filePath = byId;
396
+ }
397
+ }
398
+
399
+ if (!filePath) {
400
+ if (resumeToken === 'interrupted' || resumeToken === 'latest-interrupted') {
401
+ throw new Error('Close-loop interrupted session not found.');
402
+ }
403
+ throw new Error(`Close-loop session not found for "${resumeRef}".`);
404
+ }
405
+
406
+ const data = await fs.readJson(filePath);
407
+ validateSessionData(data, filePath);
408
+ const resolvedId = data.session_id || path.basename(filePath, '.json');
409
+ return {
410
+ id: resolvedId,
411
+ file: filePath,
412
+ data
413
+ };
414
+ }
415
+
416
+ async function resolveLatestSessionFile(sessionDir) {
417
+ if (!(await fs.pathExists(sessionDir))) {
418
+ return null;
419
+ }
420
+
421
+ const entries = await fs.readdir(sessionDir);
422
+ const candidates = [];
423
+ for (const entry of entries) {
424
+ if (!entry.toLowerCase().endsWith('.json')) {
425
+ continue;
426
+ }
427
+ const filePath = path.join(sessionDir, entry);
428
+ const stats = await fs.stat(filePath);
429
+ candidates.push({
430
+ filePath,
431
+ mtimeMs: stats.mtimeMs
432
+ });
433
+ }
434
+
435
+ if (candidates.length === 0) {
436
+ return null;
437
+ }
438
+
439
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
440
+ return candidates[0].filePath;
441
+ }
442
+
443
+ function normalizeSessionStatusToken(statusCandidate) {
444
+ return `${statusCandidate || ''}`.trim().toLowerCase();
445
+ }
446
+
447
+ async function resolveLatestInterruptedSessionFile(sessionDir) {
448
+ if (!(await fs.pathExists(sessionDir))) {
449
+ return null;
450
+ }
451
+
452
+ const entries = await fs.readdir(sessionDir);
453
+ const candidates = [];
454
+ for (const entry of entries) {
455
+ if (!entry.toLowerCase().endsWith('.json')) {
456
+ continue;
457
+ }
458
+ const filePath = path.join(sessionDir, entry);
459
+ const stats = await fs.stat(filePath);
460
+ let status = 'unknown';
461
+ try {
462
+ const payload = await fs.readJson(filePath);
463
+ status = payload && typeof payload.status === 'string'
464
+ ? payload.status
465
+ : 'unknown';
466
+ } catch (_error) {
467
+ // Ignore parse errors during interrupted-session discovery.
468
+ }
469
+ candidates.push({
470
+ filePath,
471
+ mtimeMs: stats.mtimeMs,
472
+ status
473
+ });
474
+ }
475
+
476
+ if (candidates.length === 0) {
477
+ return null;
478
+ }
479
+
480
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
481
+ const interrupted = candidates.find(item => normalizeSessionStatusToken(item.status) !== 'completed');
482
+ return interrupted ? interrupted.filePath : null;
483
+ }
484
+
485
+ function validateSessionData(data, filePath) {
486
+ if (!data || typeof data !== 'object') {
487
+ throw new Error(`Invalid close-loop session payload: ${filePath}`);
488
+ }
489
+ if (!data.portfolio || typeof data.portfolio !== 'object') {
490
+ throw new Error(`Session missing portfolio metadata: ${filePath}`);
491
+ }
492
+ if (!data.portfolio.master_spec || !Array.isArray(data.portfolio.sub_specs)) {
493
+ throw new Error(`Session missing master/sub spec names: ${filePath}`);
494
+ }
495
+ }
496
+
497
+ function buildDecompositionFromSession(sessionData) {
498
+ const portfolio = sessionData.portfolio;
499
+ const dependencyMap = new Map();
500
+ for (const item of portfolio.dependency_plan || []) {
501
+ dependencyMap.set(item.spec, Array.isArray(item.depends_on) ? item.depends_on : []);
502
+ }
503
+
504
+ const subSpecs = portfolio.sub_specs.map(specName => ({
505
+ name: specName,
506
+ title: specName,
507
+ slug: specName,
508
+ objective: 'Recovered from close-loop session snapshot.',
509
+ dependencies: (dependencyMap.get(specName) || []).map(dep => ({
510
+ spec: dep,
511
+ type: 'requires-completion'
512
+ }))
513
+ }));
514
+
515
+ const inferredPrefix = Number(portfolio.prefix);
516
+ return {
517
+ goal: sessionData.goal || 'Recovered close-loop goal',
518
+ prefix: Number.isInteger(inferredPrefix) ? inferredPrefix : inferPrefixFromSpec(portfolio.master_spec),
519
+ masterSpec: {
520
+ name: portfolio.master_spec,
521
+ title: portfolio.master_spec,
522
+ objective: 'Recovered from close-loop session snapshot.',
523
+ slug: portfolio.master_spec
524
+ },
525
+ subSpecs,
526
+ strategy: sessionData.strategy || {
527
+ source: 'resume-session',
528
+ subSpecCount: subSpecs.length,
529
+ matchedTracks: []
530
+ }
531
+ };
532
+ }
533
+
534
+ function inferPrefixFromSpec(specName) {
535
+ const match = `${specName || ''}`.match(/^(\d+)-\d{2}-/);
536
+ if (!match) {
537
+ return 0;
538
+ }
539
+ const parsed = Number(match[1]);
540
+ return Number.isInteger(parsed) ? parsed : 0;
541
+ }
542
+
543
+ function resolveAssignmentsFromSession(sessionData, masterSpec, subSpecs) {
544
+ const knownSpecs = new Set([masterSpec.name, ...subSpecs.map(spec => spec.name)]);
545
+ const persistedAssignments = Array.isArray(sessionData.portfolio && sessionData.portfolio.assignments)
546
+ ? sessionData.portfolio.assignments
547
+ : [];
548
+
549
+ const normalized = persistedAssignments
550
+ .filter(item => item && typeof item === 'object')
551
+ .map(item => ({
552
+ spec: `${item.spec || ''}`.trim(),
553
+ agent: `${item.agent || ''}`.trim()
554
+ }))
555
+ .filter(item => item.spec && item.agent && knownSpecs.has(item.spec));
556
+
557
+ if (normalized.length === knownSpecs.size) {
558
+ return normalized;
559
+ }
560
+
561
+ return buildAssignments(masterSpec, subSpecs);
562
+ }
563
+
564
+ async function ensureExistingSpecs(projectPath, specNames) {
565
+ const missing = [];
566
+ for (const specName of specNames) {
567
+ const specPath = path.join(projectPath, '.kiro', 'specs', specName);
568
+ if (!(await fs.pathExists(specPath))) {
569
+ missing.push(specName);
570
+ }
571
+ }
572
+ if (missing.length > 0) {
573
+ throw new Error(`Resume failed because specs are missing: ${missing.join(', ')}`);
574
+ }
575
+ }
576
+
577
+ function buildSessionSnapshot(result) {
578
+ return {
579
+ schema_version: '1.0',
580
+ session_version: 1,
581
+ updated_at: new Date().toISOString(),
582
+ goal: result.goal,
583
+ status: result.status,
584
+ resumed: Boolean(result.resumed),
585
+ portfolio: result.portfolio,
586
+ strategy: result.strategy,
587
+ replan: result.replan,
588
+ dod: result.dod,
589
+ orchestration: result.orchestration
590
+ };
591
+ }
592
+
593
+ function getCloseLoopStrategyMemoryFile(projectPath) {
594
+ return path.join(projectPath, CLOSE_LOOP_STRATEGY_MEMORY_FILE);
595
+ }
596
+
597
+ function buildGoalMemorySignature(goal) {
598
+ return `${goal || ''}`
599
+ .trim()
600
+ .toLowerCase()
601
+ .replace(/\s+/g, ' ')
602
+ .replace(/[^a-z0-9\u4e00-\u9fff ]+/g, '');
603
+ }
604
+
605
+ async function loadCloseLoopStrategyMemory(projectPath) {
606
+ const file = getCloseLoopStrategyMemoryFile(projectPath);
607
+ if (!(await fs.pathExists(file))) {
608
+ return {
609
+ version: CLOSE_LOOP_STRATEGY_MEMORY_VERSION,
610
+ updated_at: null,
611
+ goals: {},
612
+ track_feedback: {}
613
+ };
614
+ }
615
+ try {
616
+ const payload = await fs.readJson(file);
617
+ if (!payload || typeof payload !== 'object') {
618
+ throw new Error('invalid payload');
619
+ }
620
+ return {
621
+ version: CLOSE_LOOP_STRATEGY_MEMORY_VERSION,
622
+ updated_at: payload.updated_at || null,
623
+ goals: payload.goals && typeof payload.goals === 'object' ? payload.goals : {},
624
+ track_feedback: payload.track_feedback && typeof payload.track_feedback === 'object'
625
+ ? payload.track_feedback
626
+ : {}
627
+ };
628
+ } catch (error) {
629
+ return {
630
+ version: CLOSE_LOOP_STRATEGY_MEMORY_VERSION,
631
+ updated_at: null,
632
+ goals: {},
633
+ track_feedback: {}
634
+ };
635
+ }
636
+ }
637
+
638
+ function deriveTrackFeedbackBias(trackFeedback) {
639
+ const bias = {};
640
+ const entries = trackFeedback && typeof trackFeedback === 'object'
641
+ ? Object.entries(trackFeedback)
642
+ : [];
643
+ for (const [slug, record] of entries) {
644
+ const attempts = Number(record && record.attempts) || 0;
645
+ const successes = Number(record && record.successes) || 0;
646
+ if (attempts <= 0) {
647
+ continue;
648
+ }
649
+ const successRate = successes / attempts;
650
+ const rawBias = (successRate - 0.5) * 4;
651
+ bias[slug] = Number(Math.max(-2, Math.min(2, rawBias)).toFixed(2));
652
+ }
653
+ return bias;
654
+ }
655
+
656
+ function deriveCloseLoopStrategyMemoryContext(goal, options, strategyMemory) {
657
+ const signature = buildGoalMemorySignature(goal);
658
+ const goalMemory = signature && strategyMemory && strategyMemory.goals
659
+ ? strategyMemory.goals[signature]
660
+ : null;
661
+ const optionOverrides = {};
662
+
663
+ if (goalMemory && typeof goalMemory === 'object') {
664
+ if (options.replanStrategy === undefined && typeof goalMemory.replan_strategy === 'string') {
665
+ optionOverrides.replanStrategy = goalMemory.replan_strategy;
666
+ }
667
+ if (options.replanAttempts === undefined && Number.isInteger(Number(goalMemory.replan_attempts))) {
668
+ optionOverrides.replanAttempts = Number(goalMemory.replan_attempts);
669
+ }
670
+ if (options.dodTests === undefined && typeof goalMemory.dod_tests === 'string' && goalMemory.dod_tests.trim()) {
671
+ optionOverrides.dodTests = goalMemory.dod_tests.trim();
672
+ }
673
+ }
674
+
675
+ const trackBias = deriveTrackFeedbackBias(strategyMemory && strategyMemory.track_feedback);
676
+ return {
677
+ goal_signature: signature || null,
678
+ option_overrides: optionOverrides,
679
+ track_bias: trackBias,
680
+ telemetry: {
681
+ enabled: true,
682
+ goal_signature: signature || null,
683
+ strategy_memory_hit: Boolean(goalMemory),
684
+ applied_option_overrides: Object.keys(optionOverrides),
685
+ track_bias_count: Object.keys(trackBias).length
686
+ }
687
+ };
688
+ }
689
+
690
+ function extractTrackSlugFromSpecName(specName) {
691
+ const normalized = `${specName || ''}`.trim();
692
+ if (!normalized) {
693
+ return null;
694
+ }
695
+ const match = normalized.match(/^\d+-\d+-(.+)$/);
696
+ if (!match || !match[1]) {
697
+ return null;
698
+ }
699
+ return match[1];
700
+ }
701
+
702
+ async function updateCloseLoopStrategyMemory(projectPath, result, context, strategyMemory) {
703
+ if (!context || !context.goal_signature || !strategyMemory) {
704
+ return;
705
+ }
706
+
707
+ const file = getCloseLoopStrategyMemoryFile(projectPath);
708
+ const nextMemory = {
709
+ version: CLOSE_LOOP_STRATEGY_MEMORY_VERSION,
710
+ updated_at: new Date().toISOString(),
711
+ goals: {
712
+ ...(strategyMemory.goals || {})
713
+ },
714
+ track_feedback: {
715
+ ...(strategyMemory.track_feedback || {})
716
+ }
717
+ };
718
+ const goalRecord = nextMemory.goals[context.goal_signature] || {
719
+ attempts: 0,
720
+ successes: 0,
721
+ replan_strategy: null,
722
+ replan_attempts: null,
723
+ dod_tests: null,
724
+ last_status: null
725
+ };
726
+ goalRecord.attempts = Number(goalRecord.attempts || 0) + 1;
727
+ if (`${result && result.status ? result.status : ''}`.trim().toLowerCase() === 'completed') {
728
+ goalRecord.successes = Number(goalRecord.successes || 0) + 1;
729
+ } else {
730
+ goalRecord.successes = Number(goalRecord.successes || 0);
731
+ }
732
+ if (result && result.replan) {
733
+ if (result.replan.strategy) {
734
+ goalRecord.replan_strategy = result.replan.strategy;
735
+ }
736
+ if (Number.isInteger(Number(result.replan.effective_max_attempts))) {
737
+ goalRecord.replan_attempts = Number(result.replan.effective_max_attempts);
738
+ }
739
+ }
740
+ const testCommandCheck = result && result.dod && Array.isArray(result.dod.checks)
741
+ ? result.dod.checks.find(check => check.id === 'tests-command')
742
+ : null;
743
+ if (testCommandCheck && typeof testCommandCheck.message === 'string') {
744
+ const match = testCommandCheck.message.match(/(?:passed|failed):\s(.+?)(?:\s\(code=|$)/i);
745
+ if (match && match[1]) {
746
+ goalRecord.dod_tests = match[1].trim();
747
+ }
748
+ }
749
+ goalRecord.last_status = `${result && result.status ? result.status : ''}`.trim() || 'unknown';
750
+ nextMemory.goals[context.goal_signature] = goalRecord;
751
+
752
+ const trackNames = result && result.strategy && Array.isArray(result.strategy.matchedTracks)
753
+ ? result.strategy.matchedTracks
754
+ : [];
755
+ for (const trackName of trackNames) {
756
+ const normalizedTrackName = `${trackName || ''}`.trim();
757
+ if (!normalizedTrackName) {
758
+ continue;
759
+ }
760
+ const trackRecord = nextMemory.track_feedback[normalizedTrackName] || {
761
+ attempts: 0,
762
+ successes: 0
763
+ };
764
+ trackRecord.attempts = Number(trackRecord.attempts || 0) + 1;
765
+ if (`${result && result.status ? result.status : ''}`.trim().toLowerCase() === 'completed') {
766
+ trackRecord.successes = Number(trackRecord.successes || 0) + 1;
767
+ } else {
768
+ trackRecord.successes = Number(trackRecord.successes || 0);
769
+ }
770
+ nextMemory.track_feedback[normalizedTrackName] = trackRecord;
771
+ }
772
+
773
+ await fs.ensureDir(path.dirname(file));
774
+ await fs.writeJson(file, nextMemory, { spaces: 2 });
775
+ }
776
+
777
+ async function buildEnhancedExecutionPlanning(context) {
778
+ const projectPath = context.projectPath;
779
+ const subSpecs = Array.isArray(context.subSpecs) ? context.subSpecs : [];
780
+ const options = context.options && typeof context.options === 'object' ? context.options : {};
781
+ const ontologyGuidanceEnabled = options.ontologyGuidance !== false;
782
+ const conflictGovernanceEnabled = options.conflictGovernance !== false;
783
+ const ontologyGuidance = ontologyGuidanceEnabled
784
+ ? await loadSceneOntologyExecutionGuidance(projectPath, subSpecs)
785
+ : {
786
+ enabled: false,
787
+ reason: 'disabled-by-option'
788
+ };
789
+ const leasePlan = conflictGovernanceEnabled
790
+ ? buildSubSpecLeasePlan(subSpecs)
791
+ : {
792
+ lease_by_spec: {},
793
+ groups: {},
794
+ conflict_count: 0,
795
+ conflicts: []
796
+ };
797
+ const plannedOrder = computePlannedSubSpecOrder(subSpecs, leasePlan, ontologyGuidance);
798
+ return {
799
+ conflict_governance_enabled: conflictGovernanceEnabled,
800
+ ontology_guidance_enabled: ontologyGuidanceEnabled,
801
+ ontology_guidance: ontologyGuidance,
802
+ lease_plan: leasePlan,
803
+ planned_order: plannedOrder
804
+ };
805
+ }
806
+
807
+ async function loadSceneOntologyExecutionGuidance(projectPath, subSpecs) {
808
+ const manifestPath = path.join(projectPath, 'scene-package.json');
809
+ if (!(await fs.pathExists(manifestPath))) {
810
+ return {
811
+ enabled: false,
812
+ reason: 'scene-package-not-found'
813
+ };
814
+ }
815
+ try {
816
+ const manifest = await fs.readJson(manifestPath);
817
+ const hints = getAgentHints(manifest);
818
+ const suggestedSequence = hints && Array.isArray(hints.suggested_sequence)
819
+ ? hints.suggested_sequence.map(item => `${item || ''}`.trim()).filter(Boolean)
820
+ : [];
821
+ const mapping = {};
822
+ for (const spec of subSpecs) {
823
+ const specName = `${spec && spec.name ? spec.name : ''}`.trim();
824
+ if (!specName) continue;
825
+ const slug = extractTrackSlugFromSpecName(specName) || specName;
826
+ const normalizedSlug = slug.toLowerCase();
827
+ let matchedToken = null;
828
+ let matchedIndex = -1;
829
+ for (let index = 0; index < suggestedSequence.length; index += 1) {
830
+ const token = suggestedSequence[index].toLowerCase();
831
+ if (normalizedSlug.includes(token) || token.includes(normalizedSlug.split('-')[0])) {
832
+ matchedToken = suggestedSequence[index];
833
+ matchedIndex = index;
834
+ break;
835
+ }
836
+ }
837
+ if (matchedIndex >= 0) {
838
+ mapping[specName] = {
839
+ sequence_index: matchedIndex,
840
+ token: matchedToken
841
+ };
842
+ }
843
+ }
844
+ return {
845
+ enabled: true,
846
+ source: manifestPath,
847
+ suggested_sequence: suggestedSequence,
848
+ mapped_specs: mapping
849
+ };
850
+ } catch (error) {
851
+ return {
852
+ enabled: false,
853
+ reason: `scene-package-parse-error:${error.message}`
854
+ };
855
+ }
856
+ }
857
+
858
+ function buildSubSpecLeasePlan(subSpecs) {
859
+ const leaseBySpec = {};
860
+ const groups = {};
861
+ for (const spec of subSpecs) {
862
+ const specName = `${spec && spec.name ? spec.name : ''}`.trim();
863
+ if (!specName) continue;
864
+ const slug = extractTrackSlugFromSpecName(specName) || specName;
865
+ const leaseKey = slug
866
+ .split('-')
867
+ .slice(0, 2)
868
+ .join('-') || slug;
869
+ leaseBySpec[specName] = leaseKey;
870
+ if (!groups[leaseKey]) {
871
+ groups[leaseKey] = [];
872
+ }
873
+ groups[leaseKey].push(specName);
874
+ }
875
+ const conflicts = Object.entries(groups)
876
+ .filter(([, specs]) => specs.length > 1)
877
+ .map(([leaseKey, specs]) => ({
878
+ lease_key: leaseKey,
879
+ specs
880
+ }));
881
+ return {
882
+ lease_by_spec: leaseBySpec,
883
+ groups,
884
+ conflict_count: conflicts.length,
885
+ conflicts
886
+ };
887
+ }
888
+
889
+ function computePlannedSubSpecOrder(subSpecs, leasePlan, ontologyGuidance) {
890
+ const specMap = new Map(subSpecs.map(spec => [spec.name, spec]));
891
+ const orderedByOntology = [...subSpecs].sort((left, right) => {
892
+ const leftIndex = resolveOntologySequenceIndex(left.name, ontologyGuidance);
893
+ const rightIndex = resolveOntologySequenceIndex(right.name, ontologyGuidance);
894
+ if (leftIndex !== rightIndex) {
895
+ return leftIndex - rightIndex;
896
+ }
897
+ return left.name.localeCompare(right.name);
898
+ });
899
+
900
+ const queuesByLease = new Map();
901
+ for (const spec of orderedByOntology) {
902
+ const leaseKey = leasePlan && leasePlan.lease_by_spec && leasePlan.lease_by_spec[spec.name]
903
+ ? leasePlan.lease_by_spec[spec.name]
904
+ : 'default';
905
+ if (!queuesByLease.has(leaseKey)) {
906
+ queuesByLease.set(leaseKey, []);
907
+ }
908
+ queuesByLease.get(leaseKey).push(spec.name);
909
+ }
910
+
911
+ const leaseKeys = [...queuesByLease.keys()].sort((left, right) => left.localeCompare(right));
912
+ const picked = [];
913
+ const pickedSet = new Set();
914
+ let progressed = true;
915
+ while (progressed) {
916
+ progressed = false;
917
+ for (const leaseKey of leaseKeys) {
918
+ const queue = queuesByLease.get(leaseKey);
919
+ if (!queue || queue.length === 0) {
920
+ continue;
921
+ }
922
+ const candidateName = queue[0];
923
+ const candidate = specMap.get(candidateName);
924
+ const deps = Array.isArray(candidate && candidate.dependencies) ? candidate.dependencies : [];
925
+ const depsSatisfied = deps.every(dep => pickedSet.has(dep.spec) || !specMap.has(dep.spec));
926
+ if (!depsSatisfied) {
927
+ continue;
928
+ }
929
+ queue.shift();
930
+ picked.push(candidateName);
931
+ pickedSet.add(candidateName);
932
+ progressed = true;
933
+ }
934
+ }
935
+
936
+ for (const leaseKey of leaseKeys) {
937
+ const queue = queuesByLease.get(leaseKey) || [];
938
+ while (queue.length > 0) {
939
+ const item = queue.shift();
940
+ if (!pickedSet.has(item)) {
941
+ picked.push(item);
942
+ pickedSet.add(item);
943
+ }
944
+ }
945
+ }
946
+
947
+ const original = subSpecs.map(spec => spec.name);
948
+ return {
949
+ original,
950
+ reordered: picked,
951
+ auto_reordered: JSON.stringify(original) !== JSON.stringify(picked)
952
+ };
953
+ }
954
+
955
+ function resolveOntologySequenceIndex(specName, ontologyGuidance) {
956
+ if (!ontologyGuidance || !ontologyGuidance.enabled) {
957
+ return Number.MAX_SAFE_INTEGER;
958
+ }
959
+ const record = ontologyGuidance.mapped_specs && ontologyGuidance.mapped_specs[specName];
960
+ if (!record || !Number.isInteger(record.sequence_index)) {
961
+ return Number.MAX_SAFE_INTEGER;
962
+ }
963
+ return record.sequence_index;
964
+ }
965
+
966
+ function applyExecutionPlanning(subSpecs, executionPlanning) {
967
+ if (!executionPlanning || !executionPlanning.planned_order || !executionPlanning.planned_order.auto_reordered) {
968
+ return;
969
+ }
970
+ const order = executionPlanning.planned_order.reordered;
971
+ const map = new Map(subSpecs.map(spec => [spec.name, spec]));
972
+ const reordered = [];
973
+ for (const name of order) {
974
+ if (map.has(name)) {
975
+ reordered.push(map.get(name));
976
+ map.delete(name);
977
+ }
978
+ }
979
+ for (const rest of map.values()) {
980
+ reordered.push(rest);
981
+ }
982
+ subSpecs.splice(0, subSpecs.length, ...reordered);
983
+ }
984
+
985
+ function buildExecutionPlanSummary(executionPlanning) {
986
+ if (!executionPlanning) {
987
+ return null;
988
+ }
989
+ return {
990
+ conflict_governance_enabled: executionPlanning.conflict_governance_enabled !== false,
991
+ ontology_guidance_enabled: executionPlanning.ontology_guidance_enabled !== false,
992
+ lease_plan: executionPlanning.lease_plan,
993
+ ontology_guidance: executionPlanning.ontology_guidance,
994
+ scheduling: executionPlanning.planned_order
995
+ };
996
+ }
997
+
998
+ function createSessionId(result, sessionConfig) {
999
+ const portfolio = result && result.portfolio && typeof result.portfolio === 'object'
1000
+ ? result.portfolio
1001
+ : {};
1002
+ if (sessionConfig.sessionId) {
1003
+ return sessionConfig.sessionId;
1004
+ }
1005
+ const prefixToken = Number.isInteger(portfolio.prefix)
1006
+ ? String(portfolio.prefix).padStart(2, '0')
1007
+ : 'xx';
1008
+ const timestamp = new Date().toISOString().replace(/[-:.TZ]/g, '');
1009
+ return `${prefixToken}-${timestamp}`;
1010
+ }
1011
+
1012
+ function buildCloseLoopSessionRuntime(projectPath, sessionConfig, resumedSession, prefix) {
1013
+ if (!sessionConfig.enabled) {
1014
+ return null;
1015
+ }
1016
+
1017
+ const sessionDir = getCloseLoopSessionDir(projectPath);
1018
+ const sessionId = resumedSession
1019
+ ? resumedSession.id
1020
+ : createSessionId({ portfolio: { prefix } }, sessionConfig);
1021
+ const sessionFile = resumedSession
1022
+ ? resumedSession.file
1023
+ : path.join(sessionDir, `${sessionId}.json`);
1024
+ const createdAt = resumedSession && resumedSession.data && typeof resumedSession.data.created_at === 'string' &&
1025
+ resumedSession.data.created_at.trim()
1026
+ ? resumedSession.data.created_at.trim()
1027
+ : new Date().toISOString();
1028
+
1029
+ return {
1030
+ id: sessionId,
1031
+ file: sessionFile,
1032
+ resumed: Boolean(resumedSession),
1033
+ created_at: createdAt
1034
+ };
1035
+ }
1036
+
1037
+ async function persistCloseLoopSessionSnapshot(result, sessionRuntime) {
1038
+ if (!sessionRuntime || !sessionRuntime.file) {
1039
+ return;
1040
+ }
1041
+
1042
+ const snapshot = buildSessionSnapshot(result);
1043
+ snapshot.session_id = sessionRuntime.id;
1044
+ snapshot.created_at = sessionRuntime.created_at || snapshot.updated_at;
1045
+
1046
+ await fs.ensureDir(path.dirname(sessionRuntime.file));
1047
+ await fs.writeJson(sessionRuntime.file, snapshot, { spaces: 2 });
1048
+ }
1049
+
1050
+ async function maybePersistCloseLoopSession(result, sessionConfig, resumedSession, projectPath, sessionRuntime = null) {
1051
+ if (!sessionConfig.enabled) {
1052
+ return;
1053
+ }
1054
+
1055
+ const resolvedSession = sessionRuntime || buildCloseLoopSessionRuntime(
1056
+ projectPath,
1057
+ sessionConfig,
1058
+ resumedSession,
1059
+ result && result.portfolio ? result.portfolio.prefix : null
1060
+ );
1061
+ if (!resolvedSession) {
1062
+ return;
1063
+ }
1064
+
1065
+ await persistCloseLoopSessionSnapshot(result, resolvedSession);
1066
+
1067
+ result.session = {
1068
+ id: resolvedSession.id,
1069
+ file: resolvedSession.file,
1070
+ resumed: Boolean(resolvedSession.resumed)
1071
+ };
1072
+ }
1073
+
1074
+ async function maybePruneCloseLoopSessions(result, sessionConfig, projectPath) {
1075
+ if (!sessionConfig.enabled) {
1076
+ return;
1077
+ }
1078
+
1079
+ const hasRetentionPolicy = sessionConfig.keep !== null || sessionConfig.olderThanDays !== null;
1080
+ if (!hasRetentionPolicy) {
1081
+ return;
1082
+ }
1083
+
1084
+ const sessionDir = getCloseLoopSessionDir(projectPath);
1085
+ if (!(await fs.pathExists(sessionDir))) {
1086
+ return;
1087
+ }
1088
+
1089
+ const files = (await fs.readdir(sessionDir))
1090
+ .filter(item => item.toLowerCase().endsWith('.json'));
1091
+ const entries = [];
1092
+
1093
+ for (const file of files) {
1094
+ const filePath = path.join(sessionDir, file);
1095
+ const stats = await fs.stat(filePath);
1096
+ entries.push({
1097
+ file: filePath,
1098
+ mtimeMs: stats.mtimeMs
1099
+ });
1100
+ }
1101
+
1102
+ entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
1103
+
1104
+ const keep = sessionConfig.keep === null ? Number.POSITIVE_INFINITY : sessionConfig.keep;
1105
+ const cutoffMs = sessionConfig.olderThanDays === null
1106
+ ? null
1107
+ : Date.now() - (sessionConfig.olderThanDays * 24 * 60 * 60 * 1000);
1108
+
1109
+ const deleted = [];
1110
+ for (let index = 0; index < entries.length; index += 1) {
1111
+ const entry = entries[index];
1112
+ if (entry.file === (result.session && result.session.file)) {
1113
+ continue;
1114
+ }
1115
+
1116
+ const beyondKeep = Number.isFinite(keep) ? index >= keep : true;
1117
+ const beyondAge = cutoffMs === null || entry.mtimeMs < cutoffMs;
1118
+ if (beyondKeep && beyondAge) {
1119
+ await fs.remove(entry.file);
1120
+ deleted.push(entry.file);
1121
+ }
1122
+ }
1123
+
1124
+ result.session_prune = {
1125
+ enabled: true,
1126
+ keep: Number.isFinite(keep) ? keep : null,
1127
+ older_than_days: sessionConfig.olderThanDays,
1128
+ deleted_count: deleted.length
1129
+ };
1130
+ }
1131
+
1132
+ function normalizeReplanConfig(options) {
1133
+ const strategyCandidate = typeof options.replanStrategy === 'string'
1134
+ ? options.replanStrategy.trim().toLowerCase()
1135
+ : 'adaptive';
1136
+ if (!['fixed', 'adaptive'].includes(strategyCandidate)) {
1137
+ throw new Error('--replan-strategy must be either "fixed" or "adaptive"');
1138
+ }
1139
+
1140
+ if (options.replanAttempts !== undefined && options.replanAttempts !== null) {
1141
+ const requested = Number(options.replanAttempts);
1142
+ if (!Number.isInteger(requested) || requested < 0 || requested > 5) {
1143
+ throw new Error('--replan-attempts must be an integer between 0 and 5');
1144
+ }
1145
+ }
1146
+
1147
+ const resolvedAttempts = options.replanAttempts !== undefined && options.replanAttempts !== null
1148
+ ? Number(options.replanAttempts)
1149
+ : 1;
1150
+ if (options.replanNoProgressWindow !== undefined && options.replanNoProgressWindow !== null) {
1151
+ const requestedWindow = Number(options.replanNoProgressWindow);
1152
+ if (!Number.isInteger(requestedWindow) || requestedWindow < 1 || requestedWindow > 10) {
1153
+ throw new Error('--replan-no-progress-window must be an integer between 1 and 10');
1154
+ }
1155
+ }
1156
+ const noProgressWindow = options.replanNoProgressWindow !== undefined && options.replanNoProgressWindow !== null
1157
+ ? Number(options.replanNoProgressWindow)
1158
+ : 3;
1159
+
1160
+ return {
1161
+ enabled: options.replan !== false && resolvedAttempts > 0,
1162
+ maxAttempts: resolvedAttempts,
1163
+ strategy: strategyCandidate,
1164
+ noProgressWindow
1165
+ };
1166
+ }
1167
+
1168
+ function resolveEffectiveReplanBudget(replanConfig, failedSpecCount) {
1169
+ const base = replanConfig.maxAttempts;
1170
+ if (replanConfig.strategy === 'fixed') {
1171
+ return base;
1172
+ }
1173
+
1174
+ const adaptiveFloor = Math.max(1, Math.ceil(Number(failedSpecCount || 0) / 2));
1175
+ return Math.min(5, Math.max(base, adaptiveFloor));
1176
+ }
1177
+
1178
+ async function syncMasterCollaborationMetadata(collabManager, masterSpecName, subSpecs) {
1179
+ await collabManager.metadataManager.atomicUpdate(masterSpecName, metadata => {
1180
+ metadata.version = metadata.version || '1.0.0';
1181
+ metadata.type = 'master';
1182
+ const uniqueSubSpecs = [...new Set(subSpecs.map(spec => spec.name))];
1183
+ metadata.subSpecs = uniqueSubSpecs;
1184
+ metadata.dependencies = uniqueSubSpecs.map(specName => ({
1185
+ spec: specName,
1186
+ type: 'requires-completion'
1187
+ }));
1188
+
1189
+ const currentStatus = metadata.status && metadata.status.current
1190
+ ? metadata.status.current
1191
+ : 'not-started';
1192
+ metadata.status = {
1193
+ current: currentStatus,
1194
+ updatedAt: new Date().toISOString()
1195
+ };
1196
+ return metadata;
1197
+ });
1198
+ }
1199
+
1200
+ function collectFailedSpecsForReplan(orchestrationResult, masterSpecName) {
1201
+ const failed = new Set([
1202
+ ...(orchestrationResult && Array.isArray(orchestrationResult.failed) ? orchestrationResult.failed : []),
1203
+ ...(orchestrationResult && Array.isArray(orchestrationResult.skipped) ? orchestrationResult.skipped : [])
1204
+ ]);
1205
+
1206
+ failed.delete(masterSpecName);
1207
+ return [...failed];
1208
+ }
1209
+
1210
+ function shouldRunReplanCycle(
1211
+ orchestrationResult,
1212
+ failedSpecs,
1213
+ replanConfig,
1214
+ completedCycles,
1215
+ effectiveBudget
1216
+ ) {
1217
+ if (!replanConfig.enabled) {
1218
+ return false;
1219
+ }
1220
+
1221
+ if (!orchestrationResult || orchestrationResult.status === 'completed') {
1222
+ return false;
1223
+ }
1224
+
1225
+ if (failedSpecs.length === 0) {
1226
+ return false;
1227
+ }
1228
+
1229
+ return completedCycles < effectiveBudget;
1230
+ }
1231
+
1232
+ function evaluateReplanProgressStall(context) {
1233
+ const {
1234
+ orchestrationResult,
1235
+ failedSpecs,
1236
+ previousSnapshot,
1237
+ noProgressCycles,
1238
+ noProgressWindow
1239
+ } = context;
1240
+
1241
+ const currentSnapshot = buildReplanProgressSnapshot(orchestrationResult, failedSpecs);
1242
+ if (!currentSnapshot.shouldTrack || !previousSnapshot || !previousSnapshot.shouldTrack) {
1243
+ return {
1244
+ shouldStall: false,
1245
+ noProgressCycles: 0,
1246
+ currentSnapshot
1247
+ };
1248
+ }
1249
+
1250
+ const hasProgress =
1251
+ currentSnapshot.completedCount > previousSnapshot.completedCount ||
1252
+ currentSnapshot.failedCount < previousSnapshot.failedCount;
1253
+ const nextNoProgressCycles = hasProgress ? 0 : (noProgressCycles + 1);
1254
+ return {
1255
+ shouldStall: nextNoProgressCycles >= noProgressWindow,
1256
+ noProgressCycles: nextNoProgressCycles,
1257
+ currentSnapshot
1258
+ };
1259
+ }
1260
+
1261
+ function buildReplanProgressSnapshot(orchestrationResult, failedSpecs) {
1262
+ const completedCount = orchestrationResult && Array.isArray(orchestrationResult.completed)
1263
+ ? orchestrationResult.completed.length
1264
+ : 0;
1265
+ const failedCount = Array.isArray(failedSpecs) ? failedSpecs.length : 0;
1266
+ const shouldTrack = Boolean(orchestrationResult) &&
1267
+ orchestrationResult.status !== 'completed' &&
1268
+ failedCount > 0;
1269
+
1270
+ return {
1271
+ shouldTrack,
1272
+ completedCount,
1273
+ failedCount
1274
+ };
1275
+ }
1276
+
1277
+ function createFailedSpecSignature(failedSpecs) {
1278
+ if (!Array.isArray(failedSpecs) || failedSpecs.length === 0) {
1279
+ return null;
1280
+ }
1281
+
1282
+ const normalized = [...new Set(
1283
+ failedSpecs
1284
+ .map(item => `${item || ''}`.trim())
1285
+ .filter(Boolean)
1286
+ )].sort();
1287
+
1288
+ if (normalized.length === 0) {
1289
+ return null;
1290
+ }
1291
+
1292
+ return normalized.join('|');
1293
+ }
1294
+
1295
+ async function materializeReplanCycle(context) {
1296
+ const {
1297
+ projectPath,
1298
+ goal,
1299
+ masterSpecName,
1300
+ runtimeSubSpecs,
1301
+ runtimeAssignments,
1302
+ failedSpecs,
1303
+ cycle,
1304
+ collabManager,
1305
+ executionPlanning
1306
+ } = context;
1307
+
1308
+ const remediationSpec = buildRemediationSubSpec(
1309
+ masterSpecName,
1310
+ runtimeSubSpecs,
1311
+ failedSpecs,
1312
+ cycle
1313
+ );
1314
+
1315
+ await ensureSpecDirectoriesAreAvailable(projectPath, [remediationSpec]);
1316
+ await writeSingleSubSpecDocuments(projectPath, goal, remediationSpec);
1317
+ await collabManager.metadataManager.writeMetadata(remediationSpec.name, {
1318
+ version: '1.0.0',
1319
+ type: 'sub',
1320
+ masterSpec: masterSpecName,
1321
+ dependencies: remediationSpec.dependencies,
1322
+ status: {
1323
+ current: 'not-started',
1324
+ updatedAt: new Date().toISOString()
1325
+ },
1326
+ interfaces: {
1327
+ provides: [],
1328
+ consumes: []
1329
+ }
1330
+ });
1331
+
1332
+ const addedAssignments = buildRemediationAssignments(runtimeAssignments, [remediationSpec]);
1333
+ for (const assignment of addedAssignments) {
1334
+ await collabManager.assignSpec(assignment.spec, assignment.agent);
1335
+ }
1336
+
1337
+ runtimeSubSpecs.push(remediationSpec);
1338
+ runtimeAssignments.push(...addedAssignments);
1339
+ await syncMasterCollaborationMetadata(collabManager, masterSpecName, runtimeSubSpecs);
1340
+ await writeAgentSyncPlan(projectPath, masterSpecName, runtimeSubSpecs, runtimeAssignments, executionPlanning);
1341
+
1342
+ return {
1343
+ addedSpecs: [remediationSpec],
1344
+ addedAssignments
1345
+ };
1346
+ }
1347
+
1348
+ function buildRemediationSubSpec(masterSpecName, runtimeSubSpecs, failedSpecs, cycle) {
1349
+ const prefix = inferPrefixFromSpec(masterSpecName);
1350
+ const prefixToken = formatPrefix(prefix > 0 ? prefix : 1);
1351
+ const nextSequence = resolveNextSubSpecSequence(prefixToken, runtimeSubSpecs.map(spec => spec.name));
1352
+ const slug = trimSlug(`replan-remediation-cycle-${cycle}`, 42);
1353
+
1354
+ return {
1355
+ name: `${prefixToken}-${String(nextSequence).padStart(2, '0')}-${slug}`,
1356
+ title: `Replan Remediation Cycle ${cycle}`,
1357
+ slug,
1358
+ objective: `Recover failed orchestration path for: ${failedSpecs.join(', ')}`,
1359
+ remediation_targets: [...failedSpecs],
1360
+ dependencies: []
1361
+ };
1362
+ }
1363
+
1364
+ function resolveNextSubSpecSequence(prefixToken, specNames) {
1365
+ let max = 0;
1366
+ const pattern = new RegExp(`^${escapeRegex(prefixToken)}-(\\d+)-`);
1367
+
1368
+ for (const specName of specNames) {
1369
+ const match = `${specName}`.match(pattern);
1370
+ if (!match) {
1371
+ continue;
1372
+ }
1373
+ const seq = Number(match[1]);
1374
+ if (Number.isInteger(seq) && seq > max) {
1375
+ max = seq;
1376
+ }
1377
+ }
1378
+
1379
+ return max + 1;
1380
+ }
1381
+
1382
+ function buildRemediationAssignments(existingAssignments, remediationSpecs) {
1383
+ const maxSubAgentIndex = existingAssignments.reduce((max, item) => {
1384
+ const match = `${item.agent || ''}`.match(/^agent-sub-(\d+)$/);
1385
+ if (!match) {
1386
+ return max;
1387
+ }
1388
+ const parsed = Number(match[1]);
1389
+ return Number.isInteger(parsed) && parsed > max ? parsed : max;
1390
+ }, 0);
1391
+
1392
+ return remediationSpecs.map((spec, index) => ({
1393
+ spec: spec.name,
1394
+ agent: `agent-sub-${String(maxSubAgentIndex + index + 1).padStart(2, '0')}`
1395
+ }));
1396
+ }
1397
+
1398
+ async function writeSingleSubSpecDocuments(projectPath, goal, subSpec) {
1399
+ const subPath = path.join(projectPath, '.kiro', 'specs', subSpec.name);
1400
+ await fs.ensureDir(subPath);
1401
+ await fs.writeFile(path.join(subPath, 'requirements.md'), buildSubRequirements(goal, subSpec), 'utf8');
1402
+ await fs.writeFile(path.join(subPath, 'design.md'), buildSubDesign(subSpec), 'utf8');
1403
+ await fs.writeFile(path.join(subPath, 'tasks.md'), buildSubTasks(subSpec), 'utf8');
1404
+ }
1405
+
1406
+ function formatPrefix(prefix) {
1407
+ return prefix < 10 ? `0${prefix}` : `${prefix}`;
1408
+ }
1409
+
1410
+ function trimSlug(value, maxLength) {
1411
+ if (!value || value.length <= maxLength) {
1412
+ return value;
1413
+ }
1414
+
1415
+ return value.slice(0, maxLength).replace(/-+$/g, '');
1416
+ }
1417
+
1418
+ function escapeRegex(value) {
1419
+ return `${value}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1420
+ }
1421
+
1422
+ async function ensureSpecDirectoriesAreAvailable(projectPath, specs) {
1423
+ for (const spec of specs) {
1424
+ const specPath = path.join(projectPath, '.kiro', 'specs', spec.name);
1425
+ if (await fs.pathExists(specPath)) {
1426
+ throw new Error(`Spec already exists: ${spec.name}. Use --prefix to select a new portfolio number.`);
1427
+ }
1428
+ }
1429
+ }
1430
+
1431
+ async function writeSpecDocuments(projectPath, decomposition) {
1432
+ const masterPath = path.join(projectPath, '.kiro', 'specs', decomposition.masterSpec.name);
1433
+ await fs.ensureDir(masterPath);
1434
+ await fs.writeFile(path.join(masterPath, 'requirements.md'), buildMasterRequirements(decomposition), 'utf8');
1435
+ await fs.writeFile(path.join(masterPath, 'design.md'), buildMasterDesign(decomposition), 'utf8');
1436
+ await fs.writeFile(path.join(masterPath, 'tasks.md'), buildMasterTasks(decomposition), 'utf8');
1437
+
1438
+ for (const subSpec of decomposition.subSpecs) {
1439
+ const subPath = path.join(projectPath, '.kiro', 'specs', subSpec.name);
1440
+ await fs.ensureDir(subPath);
1441
+ await fs.writeFile(path.join(subPath, 'requirements.md'), buildSubRequirements(decomposition.goal, subSpec), 'utf8');
1442
+ await fs.writeFile(path.join(subPath, 'design.md'), buildSubDesign(subSpec), 'utf8');
1443
+ await fs.writeFile(path.join(subPath, 'tasks.md'), buildSubTasks(subSpec), 'utf8');
1444
+ }
1445
+ }
1446
+
1447
+ function buildAssignments(masterSpec, subSpecs) {
1448
+ const assignments = [
1449
+ { spec: masterSpec.name, agent: 'agent-master' }
1450
+ ];
1451
+
1452
+ subSpecs.forEach((spec, index) => {
1453
+ assignments.push({
1454
+ spec: spec.name,
1455
+ agent: `agent-sub-${String(index + 1).padStart(2, '0')}`
1456
+ });
1457
+ });
1458
+
1459
+ return assignments;
1460
+ }
1461
+
1462
+ async function synchronizeCollaborationStatus(collabManager, allSpecNames, orchestrationResult) {
1463
+ const completed = new Set(orchestrationResult.completed || []);
1464
+ const failed = new Set(orchestrationResult.failed || []);
1465
+ const skipped = new Set(orchestrationResult.skipped || []);
1466
+
1467
+ for (const specName of allSpecNames) {
1468
+ if (completed.has(specName)) {
1469
+ await collabManager.updateSpecStatus(specName, 'completed');
1470
+ continue;
1471
+ }
1472
+
1473
+ if (failed.has(specName)) {
1474
+ await collabManager.updateSpecStatus(specName, 'blocked', 'orchestration-failed');
1475
+ continue;
1476
+ }
1477
+
1478
+ if (skipped.has(specName)) {
1479
+ await collabManager.updateSpecStatus(specName, 'blocked', 'dependency-skipped');
1480
+ continue;
1481
+ }
1482
+
1483
+ await collabManager.updateSpecStatus(specName, 'not-started');
1484
+ }
1485
+ }
1486
+
1487
+ function normalizeDodRiskLevel(levelCandidate) {
1488
+ if (levelCandidate === undefined || levelCandidate === null || `${levelCandidate}`.trim() === '') {
1489
+ return null;
1490
+ }
1491
+ const normalized = `${levelCandidate}`.trim().toLowerCase();
1492
+ if (!Object.prototype.hasOwnProperty.call(RISK_LEVEL_ORDER, normalized)) {
1493
+ throw new Error('--dod-max-risk-level must be one of: low, medium, high');
1494
+ }
1495
+ return normalized;
1496
+ }
1497
+
1498
+ function normalizeDodMinCompletionRate(rateCandidate) {
1499
+ if (rateCandidate === undefined || rateCandidate === null || `${rateCandidate}`.trim() === '') {
1500
+ return null;
1501
+ }
1502
+ const parsed = Number(rateCandidate);
1503
+ if (Number.isNaN(parsed) || parsed < 0 || parsed > 100) {
1504
+ throw new Error('--dod-kpi-min-completion-rate must be a number between 0 and 100');
1505
+ }
1506
+ return Number(parsed.toFixed(2));
1507
+ }
1508
+
1509
+ function normalizeDodMaxSuccessRateDrop(dropCandidate) {
1510
+ if (dropCandidate === undefined || dropCandidate === null || `${dropCandidate}`.trim() === '') {
1511
+ return null;
1512
+ }
1513
+ const parsed = Number(dropCandidate);
1514
+ if (Number.isNaN(parsed) || parsed < 0 || parsed > 100) {
1515
+ throw new Error('--dod-max-success-rate-drop must be a number between 0 and 100');
1516
+ }
1517
+ return Number(parsed.toFixed(2));
1518
+ }
1519
+
1520
+ function normalizeDodBaselineWindow(windowCandidate) {
1521
+ if (windowCandidate === undefined || windowCandidate === null || `${windowCandidate}`.trim() === '') {
1522
+ return 5;
1523
+ }
1524
+ const parsed = Number(windowCandidate);
1525
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 50) {
1526
+ throw new Error('--dod-baseline-window must be an integer between 1 and 50');
1527
+ }
1528
+ return parsed;
1529
+ }
1530
+
1531
+ function normalizeDefinitionOfDoneConfig(options) {
1532
+ const timeoutCandidate = Number(options.dodTestsTimeout);
1533
+ const minCompletionRateCandidate = options.dodKpiMinCompletionRate;
1534
+ const baselineDropCandidate = options.dodMaxSuccessRateDrop;
1535
+ const baselineWindowCandidate = options.dodBaselineWindow;
1536
+
1537
+ return {
1538
+ enabled: options.dod !== false,
1539
+ requireDocs: options.dodDocs !== false,
1540
+ requireCollabCompleted: options.dodCollab !== false,
1541
+ requireOrchestrationCompleted: options.run !== false,
1542
+ requireTasksClosed: Boolean(options.dodTasksClosed),
1543
+ maxRiskLevel: normalizeDodRiskLevel(options.dodMaxRiskLevel),
1544
+ minCompletionRatePercent: normalizeDodMinCompletionRate(minCompletionRateCandidate),
1545
+ maxSuccessRateDropPercent: normalizeDodMaxSuccessRateDrop(baselineDropCandidate),
1546
+ baselineWindow: normalizeDodBaselineWindow(baselineWindowCandidate),
1547
+ testCommand: typeof options.dodTests === 'string' && options.dodTests.trim()
1548
+ ? options.dodTests.trim()
1549
+ : null,
1550
+ testTimeoutMs: Number.isFinite(timeoutCandidate) && timeoutCandidate > 0
1551
+ ? timeoutCandidate
1552
+ : 10 * 60 * 1000
1553
+ };
1554
+ }
1555
+
1556
+ async function evaluateDefinitionOfDone(context) {
1557
+ const {
1558
+ projectPath,
1559
+ specNames,
1560
+ orchestrationResult,
1561
+ runInvoked,
1562
+ dodConfig,
1563
+ runCommand
1564
+ } = context;
1565
+
1566
+ if (!dodConfig.enabled) {
1567
+ return {
1568
+ enabled: false,
1569
+ passed: true,
1570
+ failed_checks: [],
1571
+ checks: []
1572
+ };
1573
+ }
1574
+
1575
+ const checks = [];
1576
+ const completionRatePercent = calculateCloseLoopCompletionRatePercent(orchestrationResult, specNames);
1577
+ const derivedRiskLevel = deriveCloseLoopRiskLevel(orchestrationResult, specNames);
1578
+
1579
+ if (dodConfig.requireDocs) {
1580
+ const missingDocs = await findMissingSpecDocuments(projectPath, specNames);
1581
+ checks.push({
1582
+ id: 'docs-complete',
1583
+ status: missingDocs.length === 0 ? 'passed' : 'failed',
1584
+ message: missingDocs.length === 0
1585
+ ? 'All spec docs are present (requirements/design/tasks).'
1586
+ : `Missing spec docs: ${missingDocs.join(', ')}`
1587
+ });
1588
+ } else {
1589
+ checks.push({
1590
+ id: 'docs-complete',
1591
+ status: 'skipped',
1592
+ message: 'Doc completeness gate disabled.'
1593
+ });
1594
+ }
1595
+
1596
+ if (dodConfig.requireOrchestrationCompleted) {
1597
+ const completed = orchestrationResult && orchestrationResult.status === 'completed';
1598
+ checks.push({
1599
+ id: 'orchestration-completed',
1600
+ status: completed ? 'passed' : 'failed',
1601
+ message: completed
1602
+ ? 'Orchestration reached completed terminal state.'
1603
+ : `Orchestration terminal status is ${orchestrationResult ? orchestrationResult.status : 'unknown'}.`
1604
+ });
1605
+ } else {
1606
+ checks.push({
1607
+ id: 'orchestration-completed',
1608
+ status: 'skipped',
1609
+ message: 'Orchestration gate skipped (--no-run).'
1610
+ });
1611
+ }
1612
+
1613
+ if (dodConfig.maxRiskLevel) {
1614
+ const passesRisk = compareRiskLevels(derivedRiskLevel, dodConfig.maxRiskLevel) <= 0;
1615
+ checks.push({
1616
+ id: 'risk-level-threshold',
1617
+ status: passesRisk ? 'passed' : 'failed',
1618
+ message: passesRisk
1619
+ ? `Derived run risk "${derivedRiskLevel}" is within threshold "${dodConfig.maxRiskLevel}".`
1620
+ : `Derived run risk "${derivedRiskLevel}" exceeds threshold "${dodConfig.maxRiskLevel}".`,
1621
+ details: {
1622
+ derived_risk_level: derivedRiskLevel,
1623
+ max_risk_level: dodConfig.maxRiskLevel
1624
+ }
1625
+ });
1626
+ } else {
1627
+ checks.push({
1628
+ id: 'risk-level-threshold',
1629
+ status: 'skipped',
1630
+ message: 'Risk threshold gate disabled.'
1631
+ });
1632
+ }
1633
+
1634
+ if (dodConfig.minCompletionRatePercent !== null) {
1635
+ const completionPassed = completionRatePercent >= dodConfig.minCompletionRatePercent;
1636
+ checks.push({
1637
+ id: 'kpi-completion-rate-threshold',
1638
+ status: completionPassed ? 'passed' : 'failed',
1639
+ message: completionPassed
1640
+ ? `Completion KPI ${completionRatePercent}% meets threshold ${dodConfig.minCompletionRatePercent}%.`
1641
+ : `Completion KPI ${completionRatePercent}% is below threshold ${dodConfig.minCompletionRatePercent}%.`,
1642
+ details: {
1643
+ completion_rate_percent: completionRatePercent,
1644
+ min_completion_rate_percent: dodConfig.minCompletionRatePercent
1645
+ }
1646
+ });
1647
+ } else {
1648
+ checks.push({
1649
+ id: 'kpi-completion-rate-threshold',
1650
+ status: 'skipped',
1651
+ message: 'Completion KPI threshold gate disabled.'
1652
+ });
1653
+ }
1654
+
1655
+ if (dodConfig.maxSuccessRateDropPercent !== null) {
1656
+ const baselineSnapshot = await buildCloseLoopSuccessRateBaseline(projectPath, dodConfig.baselineWindow);
1657
+ if (baselineSnapshot.sample_count <= 0) {
1658
+ checks.push({
1659
+ id: 'kpi-baseline-drop-threshold',
1660
+ status: 'skipped',
1661
+ message: 'Baseline KPI gate skipped because no historical close-loop sessions were found.',
1662
+ details: {
1663
+ baseline_window: dodConfig.baselineWindow
1664
+ }
1665
+ });
1666
+ } else {
1667
+ const baselineDrop = Number((baselineSnapshot.average_success_rate_percent - completionRatePercent).toFixed(2));
1668
+ const dropPassed = baselineDrop <= dodConfig.maxSuccessRateDropPercent;
1669
+ checks.push({
1670
+ id: 'kpi-baseline-drop-threshold',
1671
+ status: dropPassed ? 'passed' : 'failed',
1672
+ message: dropPassed
1673
+ ? `Completion KPI drop ${baselineDrop}% is within allowed baseline drop ${dodConfig.maxSuccessRateDropPercent}%.`
1674
+ : `Completion KPI drop ${baselineDrop}% exceeds allowed baseline drop ${dodConfig.maxSuccessRateDropPercent}%.`,
1675
+ details: {
1676
+ baseline_window: baselineSnapshot.sample_count,
1677
+ baseline_success_rate_percent: baselineSnapshot.average_success_rate_percent,
1678
+ latest_completion_rate_percent: completionRatePercent,
1679
+ baseline_drop_percent: baselineDrop,
1680
+ max_allowed_drop_percent: dodConfig.maxSuccessRateDropPercent
1681
+ }
1682
+ });
1683
+ }
1684
+ } else {
1685
+ checks.push({
1686
+ id: 'kpi-baseline-drop-threshold',
1687
+ status: 'skipped',
1688
+ message: 'Baseline KPI drop threshold gate disabled.'
1689
+ });
1690
+ }
1691
+
1692
+ if (dodConfig.requireCollabCompleted) {
1693
+ if (!runInvoked) {
1694
+ checks.push({
1695
+ id: 'collaboration-completed',
1696
+ status: 'skipped',
1697
+ message: 'Collaboration completion gate skipped because orchestration did not run.'
1698
+ });
1699
+ } else {
1700
+ const nonCompleted = await findNonCompletedCollaborationSpecs(projectPath, specNames);
1701
+ checks.push({
1702
+ id: 'collaboration-completed',
1703
+ status: nonCompleted.length === 0 ? 'passed' : 'failed',
1704
+ message: nonCompleted.length === 0
1705
+ ? 'Collaboration statuses are completed for all specs.'
1706
+ : `Collaboration not completed: ${nonCompleted.join(', ')}`
1707
+ });
1708
+ }
1709
+ } else {
1710
+ checks.push({
1711
+ id: 'collaboration-completed',
1712
+ status: 'skipped',
1713
+ message: 'Collaboration gate disabled.'
1714
+ });
1715
+ }
1716
+
1717
+ if (dodConfig.requireTasksClosed) {
1718
+ const openTasks = await findSpecsWithOpenTasks(projectPath, specNames);
1719
+ checks.push({
1720
+ id: 'tasks-checklist-closed',
1721
+ status: openTasks.length === 0 ? 'passed' : 'failed',
1722
+ message: openTasks.length === 0
1723
+ ? 'All tasks checklists are fully closed.'
1724
+ : `Open checklist items remain in: ${openTasks.join(', ')}`
1725
+ });
1726
+ } else {
1727
+ checks.push({
1728
+ id: 'tasks-checklist-closed',
1729
+ status: 'skipped',
1730
+ message: 'Tasks checklist closure gate disabled.'
1731
+ });
1732
+ }
1733
+
1734
+ if (dodConfig.testCommand) {
1735
+ const testResult = await runCommand(dodConfig.testCommand, {
1736
+ cwd: projectPath,
1737
+ timeoutMs: dodConfig.testTimeoutMs
1738
+ });
1739
+ checks.push({
1740
+ id: 'tests-command',
1741
+ status: testResult.success ? 'passed' : 'failed',
1742
+ message: testResult.success
1743
+ ? `Test command passed: ${dodConfig.testCommand}`
1744
+ : `Test command failed: ${dodConfig.testCommand} (code=${testResult.code === null ? 'n/a' : testResult.code})`,
1745
+ details: summarizeCommandFailure(testResult)
1746
+ });
1747
+ } else {
1748
+ checks.push({
1749
+ id: 'tests-command',
1750
+ status: 'skipped',
1751
+ message: 'No DoD test command configured.'
1752
+ });
1753
+ }
1754
+
1755
+ const failedChecks = checks.filter(check => check.status === 'failed');
1756
+ return {
1757
+ enabled: true,
1758
+ passed: failedChecks.length === 0,
1759
+ failed_checks: failedChecks.map(check => check.id),
1760
+ checks
1761
+ };
1762
+ }
1763
+
1764
+ function compareRiskLevels(left, right) {
1765
+ const leftValue = RISK_LEVEL_ORDER[left] || RISK_LEVEL_ORDER.high;
1766
+ const rightValue = RISK_LEVEL_ORDER[right] || RISK_LEVEL_ORDER.high;
1767
+ return leftValue - rightValue;
1768
+ }
1769
+
1770
+ function calculateCloseLoopCompletionRatePercent(orchestrationResult, specNames) {
1771
+ const totalSpecs = Array.isArray(specNames) ? specNames.length : 0;
1772
+ if (totalSpecs <= 0) {
1773
+ return 0;
1774
+ }
1775
+ if (!orchestrationResult || typeof orchestrationResult !== 'object') {
1776
+ return 0;
1777
+ }
1778
+ const completedList = Array.isArray(orchestrationResult.completed) ? orchestrationResult.completed : [];
1779
+ const uniqueCompleted = new Set(completedList.map(item => `${item || ''}`.trim()).filter(Boolean)).size;
1780
+ return Number(((uniqueCompleted / totalSpecs) * 100).toFixed(2));
1781
+ }
1782
+
1783
+ function deriveCloseLoopRiskLevel(orchestrationResult, specNames) {
1784
+ const totalSpecs = Array.isArray(specNames) ? specNames.length : 0;
1785
+ if (totalSpecs <= 0 || !orchestrationResult || typeof orchestrationResult !== 'object') {
1786
+ return 'high';
1787
+ }
1788
+ const failedList = [
1789
+ ...(Array.isArray(orchestrationResult.failed) ? orchestrationResult.failed : []),
1790
+ ...(Array.isArray(orchestrationResult.skipped) ? orchestrationResult.skipped : [])
1791
+ ];
1792
+ const failedSpecs = new Set(failedList.map(item => `${item || ''}`.trim()).filter(Boolean)).size;
1793
+ if (failedSpecs <= 0 && `${orchestrationResult.status || ''}`.trim().toLowerCase() === 'completed') {
1794
+ return 'low';
1795
+ }
1796
+ const failedRatio = totalSpecs > 0 ? failedSpecs / totalSpecs : 1;
1797
+ if (failedRatio >= 0.4) {
1798
+ return 'high';
1799
+ }
1800
+ return 'medium';
1801
+ }
1802
+
1803
+ async function buildCloseLoopSuccessRateBaseline(projectPath, baselineWindow) {
1804
+ const sessionDir = getCloseLoopSessionDir(projectPath);
1805
+ const windowSize = Number.isInteger(baselineWindow) ? baselineWindow : 5;
1806
+ if (!(await fs.pathExists(sessionDir))) {
1807
+ return {
1808
+ sample_count: 0,
1809
+ average_success_rate_percent: 0
1810
+ };
1811
+ }
1812
+
1813
+ const entries = (await fs.readdir(sessionDir))
1814
+ .filter(item => item.toLowerCase().endsWith('.json'))
1815
+ .map(item => path.join(sessionDir, item));
1816
+ const sessions = [];
1817
+ for (const file of entries) {
1818
+ let payload = null;
1819
+ try {
1820
+ payload = await fs.readJson(file);
1821
+ } catch (error) {
1822
+ continue;
1823
+ }
1824
+ const stats = await fs.stat(file);
1825
+ sessions.push({
1826
+ payload,
1827
+ mtimeMs: stats.mtimeMs
1828
+ });
1829
+ }
1830
+
1831
+ sessions.sort((left, right) => right.mtimeMs - left.mtimeMs);
1832
+ const scoped = sessions.slice(0, windowSize);
1833
+ const successRates = scoped
1834
+ .map(item => {
1835
+ const payload = item.payload || {};
1836
+ const portfolio = payload.portfolio && typeof payload.portfolio === 'object' ? payload.portfolio : {};
1837
+ const totalSpecs = (Array.isArray(portfolio.sub_specs) ? portfolio.sub_specs.length : 0) + 1;
1838
+ const orchestration = payload.orchestration && typeof payload.orchestration === 'object' ? payload.orchestration : null;
1839
+ if (orchestration && Array.isArray(orchestration.completed) && totalSpecs > 0) {
1840
+ const completed = new Set(orchestration.completed.map(spec => `${spec || ''}`.trim()).filter(Boolean)).size;
1841
+ return Number(((completed / totalSpecs) * 100).toFixed(2));
1842
+ }
1843
+ const status = `${payload.status || ''}`.trim().toLowerCase();
1844
+ if (status === 'completed') {
1845
+ return 100;
1846
+ }
1847
+ if (status === 'failed' || status === 'partial-failed') {
1848
+ return 0;
1849
+ }
1850
+ return null;
1851
+ })
1852
+ .filter(value => Number.isFinite(value));
1853
+
1854
+ if (successRates.length === 0) {
1855
+ return {
1856
+ sample_count: 0,
1857
+ average_success_rate_percent: 0
1858
+ };
1859
+ }
1860
+ const average = successRates.reduce((sum, value) => sum + value, 0) / successRates.length;
1861
+ return {
1862
+ sample_count: successRates.length,
1863
+ average_success_rate_percent: Number(average.toFixed(2))
1864
+ };
1865
+ }
1866
+
1867
+ async function findMissingSpecDocuments(projectPath, specNames) {
1868
+ const requiredDocs = ['requirements.md', 'design.md', 'tasks.md'];
1869
+ const missing = [];
1870
+
1871
+ for (const specName of specNames) {
1872
+ const basePath = path.join(projectPath, '.kiro', 'specs', specName);
1873
+ for (const docName of requiredDocs) {
1874
+ const filePath = path.join(basePath, docName);
1875
+ if (!(await fs.pathExists(filePath))) {
1876
+ missing.push(`${specName}/${docName}`);
1877
+ }
1878
+ }
1879
+ }
1880
+
1881
+ return missing;
1882
+ }
1883
+
1884
+ async function findNonCompletedCollaborationSpecs(projectPath, specNames) {
1885
+ const notCompleted = [];
1886
+
1887
+ for (const specName of specNames) {
1888
+ const collabPath = path.join(projectPath, '.kiro', 'specs', specName, 'collaboration.json');
1889
+ if (!(await fs.pathExists(collabPath))) {
1890
+ notCompleted.push(`${specName}(missing-collaboration-json)`);
1891
+ continue;
1892
+ }
1893
+
1894
+ const metadata = await fs.readJson(collabPath);
1895
+ const currentStatus = metadata.status && metadata.status.current;
1896
+ if (currentStatus !== 'completed') {
1897
+ notCompleted.push(`${specName}(${currentStatus || 'unknown'})`);
1898
+ }
1899
+ }
1900
+
1901
+ return notCompleted;
1902
+ }
1903
+
1904
+ async function findSpecsWithOpenTasks(projectPath, specNames) {
1905
+ const specsWithOpenTasks = [];
1906
+
1907
+ for (const specName of specNames) {
1908
+ const tasksPath = path.join(projectPath, '.kiro', 'specs', specName, 'tasks.md');
1909
+ if (!(await fs.pathExists(tasksPath))) {
1910
+ specsWithOpenTasks.push(specName);
1911
+ continue;
1912
+ }
1913
+
1914
+ const content = await fs.readFile(tasksPath, 'utf8');
1915
+ if (/\n\s*-\s*\[\s\]\s+/.test(`\n${content}`)) {
1916
+ specsWithOpenTasks.push(specName);
1917
+ }
1918
+ }
1919
+
1920
+ return specsWithOpenTasks;
1921
+ }
1922
+
1923
+ function summarizeCommandFailure(result) {
1924
+ if (result.success) {
1925
+ return null;
1926
+ }
1927
+
1928
+ if (result.timedOut) {
1929
+ return `Timed out after ${result.timeoutMs}ms`;
1930
+ }
1931
+
1932
+ if (result.error) {
1933
+ return result.error;
1934
+ }
1935
+
1936
+ const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
1937
+ const stdout = typeof result.stdout === 'string' ? result.stdout.trim() : '';
1938
+ return stderr || stdout || 'No error output captured.';
1939
+ }
1940
+
1941
+ function executeShellCommand(command, options = {}) {
1942
+ const cwd = options.cwd || process.cwd();
1943
+ const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
1944
+ ? options.timeoutMs
1945
+ : 10 * 60 * 1000;
1946
+
1947
+ return new Promise(resolve => {
1948
+ let settled = false;
1949
+ let stdout = '';
1950
+ let stderr = '';
1951
+ let timeout = null;
1952
+ const maxOutputChars = 50_000;
1953
+
1954
+ const finish = result => {
1955
+ if (settled) return;
1956
+ settled = true;
1957
+ if (timeout) {
1958
+ clearTimeout(timeout);
1959
+ }
1960
+ resolve(result);
1961
+ };
1962
+
1963
+ const child = spawn(command, {
1964
+ cwd,
1965
+ shell: true,
1966
+ env: process.env
1967
+ });
1968
+
1969
+ const append = (current, chunk) => {
1970
+ const next = current + chunk.toString();
1971
+ if (next.length <= maxOutputChars) {
1972
+ return next;
1973
+ }
1974
+ return next.slice(next.length - maxOutputChars);
1975
+ };
1976
+
1977
+ child.stdout.on('data', data => {
1978
+ stdout = append(stdout, data);
1979
+ });
1980
+
1981
+ child.stderr.on('data', data => {
1982
+ stderr = append(stderr, data);
1983
+ });
1984
+
1985
+ child.on('close', (code, signal) => {
1986
+ finish({
1987
+ success: code === 0,
1988
+ code,
1989
+ signal,
1990
+ stdout,
1991
+ stderr
1992
+ });
1993
+ });
1994
+
1995
+ child.on('error', error => {
1996
+ finish({
1997
+ success: false,
1998
+ code: null,
1999
+ signal: null,
2000
+ stdout,
2001
+ stderr,
2002
+ error: error.message
2003
+ });
2004
+ });
2005
+
2006
+ timeout = setTimeout(() => {
2007
+ try {
2008
+ child.kill('SIGTERM');
2009
+ } catch (_err) {
2010
+ // Child process may already be gone.
2011
+ }
2012
+ finish({
2013
+ success: false,
2014
+ code: null,
2015
+ signal: 'SIGTERM',
2016
+ stdout,
2017
+ stderr,
2018
+ timedOut: true,
2019
+ timeoutMs
2020
+ });
2021
+ }, timeoutMs);
2022
+
2023
+ if (typeof timeout.unref === 'function') {
2024
+ timeout.unref();
2025
+ }
2026
+ });
2027
+ }
2028
+
2029
+ function buildMasterRequirements(decomposition) {
2030
+ const lines = [
2031
+ '# Requirements',
2032
+ '',
2033
+ `## Goal`,
2034
+ decomposition.goal,
2035
+ '',
2036
+ '## Functional Requirements',
2037
+ '1. THE SYSTEM SHALL decompose the goal into a coordinated master/sub-spec portfolio automatically.',
2038
+ '2. THE SYSTEM SHALL execute the portfolio in a closed loop until orchestration reaches a terminal state.',
2039
+ '3. THE SYSTEM SHALL synchronize collaboration metadata, ownership, and dependency status across all specs.',
2040
+ '4. THE SYSTEM SHALL emit machine-readable execution evidence for downstream auditing.',
2041
+ '',
2042
+ '## Success Criteria',
2043
+ `- Master Spec: \`${decomposition.masterSpec.name}\``,
2044
+ `- Sub Specs: ${decomposition.subSpecs.map(spec => `\`${spec.name}\``).join(', ')}`,
2045
+ '- Portfolio can be rerun deterministically with the same topology.',
2046
+ ''
2047
+ ];
2048
+
2049
+ return lines.join('\n');
2050
+ }
2051
+
2052
+ function buildMasterDesign(decomposition) {
2053
+ const lines = [
2054
+ '# Design',
2055
+ '',
2056
+ '## Requirement Mapping',
2057
+ '- FR1 -> Portfolio decomposition engine + naming strategy',
2058
+ '- FR2 -> Orchestrate runtime invocation with dependency-aware order',
2059
+ '- FR3 -> Collaboration metadata synchronization (status + assignment)',
2060
+ '- FR4 -> JSON result artifact and terminal summary',
2061
+ '',
2062
+ '## Coordination Topology',
2063
+ `- Master: \`${decomposition.masterSpec.name}\``,
2064
+ ...decomposition.subSpecs.map(subSpec => {
2065
+ const deps = subSpec.dependencies.map(dep => dep.spec).join(', ');
2066
+ return `- Sub: \`${subSpec.name}\`${deps ? ` (depends on: ${deps})` : ''}`;
2067
+ }),
2068
+ '',
2069
+ '## Integration Contract',
2070
+ '- All Sub Specs must be marked completed before the Master Spec can be completed.',
2071
+ '- Blocked/failed Sub Specs propagate a blocked state to dependent Specs.',
2072
+ '- Final result is published as a single orchestration report payload.',
2073
+ ''
2074
+ ];
2075
+
2076
+ return lines.join('\n');
2077
+ }
2078
+
2079
+ function buildMasterTasks(decomposition) {
2080
+ const lines = [
2081
+ '# Tasks',
2082
+ '',
2083
+ '- [ ] 1. Confirm portfolio topology and dependency contracts',
2084
+ ' - **Requirement**: FR1',
2085
+ ' - **Design**: Coordination Topology',
2086
+ ' - **Validation**: Dependencies are explicit and acyclic',
2087
+ '',
2088
+ '- [ ] 2. Launch orchestrate runtime for all Sub Specs and Master',
2089
+ ' - **Requirement**: FR2',
2090
+ ' - **Design**: Integration Contract',
2091
+ ' - **Validation**: `kse orchestrate run` reaches terminal state',
2092
+ '',
2093
+ '- [ ] 3. Reconcile collaboration status and produce closure evidence',
2094
+ ' - **Requirement**: FR3, FR4',
2095
+ ' - **Design**: Integration Contract',
2096
+ ' - **Validation**: collaboration metadata + JSON artifact are consistent',
2097
+ '',
2098
+ `## Linked Sub Specs`,
2099
+ ...decomposition.subSpecs.map(subSpec => `- [ ] ${subSpec.name}`),
2100
+ ''
2101
+ ];
2102
+
2103
+ return lines.join('\n');
2104
+ }
2105
+
2106
+ function buildSubRequirements(goal, subSpec) {
2107
+ const lines = [
2108
+ '# Requirements',
2109
+ '',
2110
+ '## Goal Alignment',
2111
+ goal,
2112
+ '',
2113
+ `## Sub Capability: ${subSpec.title}`,
2114
+ subSpec.objective,
2115
+ '',
2116
+ '## Functional Requirements',
2117
+ '1. THE SYSTEM SHALL implement the capability scope defined in this Sub Spec.',
2118
+ '2. THE SYSTEM SHALL provide integration-ready outputs for downstream dependent Specs.',
2119
+ '3. THE SYSTEM SHALL maintain testable acceptance evidence for completion gates.',
2120
+ ''
2121
+ ];
2122
+
2123
+ return lines.join('\n');
2124
+ }
2125
+
2126
+ function buildSubDesign(subSpec) {
2127
+ const dependencyNames = subSpec.dependencies.map(dep => dep.spec);
2128
+ const dependencyLine = dependencyNames.length > 0
2129
+ ? dependencyNames.join(', ')
2130
+ : 'None';
2131
+
2132
+ const lines = [
2133
+ '# Design',
2134
+ '',
2135
+ '## Requirement Mapping',
2136
+ '- FR1 -> Capability implementation unit',
2137
+ '- FR2 -> Integration output contract',
2138
+ '- FR3 -> Validation and gate evidence',
2139
+ '',
2140
+ '## Execution Contract',
2141
+ `- Capability: ${subSpec.title}`,
2142
+ `- Dependencies: ${dependencyLine}`,
2143
+ '- Output: updated requirements/design/tasks and implementation artifacts',
2144
+ '- Validation: tests + gate evidence for completion',
2145
+ ''
2146
+ ];
2147
+
2148
+ return lines.join('\n');
2149
+ }
2150
+
2151
+ function buildSubTasks(subSpec) {
2152
+ const lines = [
2153
+ '# Tasks',
2154
+ '',
2155
+ '- [ ] 1. Implement capability scope for this Sub Spec',
2156
+ ' - **Requirement**: FR1',
2157
+ ' - **Design**: Execution Contract',
2158
+ ' - **Validation**: Deliver scoped implementation with clear boundaries',
2159
+ '',
2160
+ '- [ ] 2. Produce integration-ready outputs and contracts',
2161
+ ' - **Requirement**: FR2',
2162
+ ' - **Design**: Execution Contract',
2163
+ ' - **Validation**: Downstream Specs can consume outputs without ambiguity',
2164
+ '',
2165
+ '- [ ] 3. Complete validation evidence and handoff summary',
2166
+ ' - **Requirement**: FR3',
2167
+ ' - **Design**: Execution Contract',
2168
+ ' - **Validation**: Tests and gate evidence are attached',
2169
+ ''
2170
+ ];
2171
+
2172
+ if (subSpec.dependencies.length > 0) {
2173
+ lines.push('## Dependencies');
2174
+ subSpec.dependencies.forEach(dep => {
2175
+ lines.push(`- [ ] ${dep.spec} (${dep.type})`);
2176
+ });
2177
+ lines.push('');
2178
+ }
2179
+
2180
+ return lines.join('\n');
2181
+ }
2182
+
2183
+ function buildNextActions(status, dod, replan) {
2184
+ if ((status === 'failed' || status === 'stopped') && replan && replan.enabled && replan.exhausted) {
2185
+ const budget = replan.effective_max_attempts || replan.max_attempts;
2186
+ const stalledMessage = replan.stalled_signature
2187
+ ? `Replan stopped because failed-spec signature repeated: ${replan.stalled_signature}.`
2188
+ : replan.stalled_no_progress_cycles > 0
2189
+ ? `Replan stopped because no progress was detected for ${replan.stalled_no_progress_cycles} consecutive failed cycles.`
2190
+ : `Automatic replan attempts exhausted (${budget}).`;
2191
+ return [
2192
+ stalledMessage,
2193
+ 'Run `kse auto close-loop --resume latest --replan-attempts <n>` to continue with higher replan budget.',
2194
+ 'Use `kse orchestrate status --json` for failure diagnostics.'
2195
+ ];
2196
+ }
2197
+
2198
+ if (dod && dod.enabled && dod.passed === false) {
2199
+ const failures = dod.failed_checks && dod.failed_checks.length > 0
2200
+ ? dod.failed_checks.join(', ')
2201
+ : 'unknown';
2202
+ return [
2203
+ `Resolve failed Definition-of-Done gates: ${failures}.`,
2204
+ 'Run `kse auto close-loop "<goal>" --dod-tests "<command>"` again after fixes.',
2205
+ 'Use `kse orchestrate status --json` for detailed orchestration diagnostics.'
2206
+ ];
2207
+ }
2208
+
2209
+ if (status === 'completed') {
2210
+ const replanHint = replan && replan.performed > 0
2211
+ ? `Replan cycles executed: ${replan.performed}.`
2212
+ : 'No replan cycle was required.';
2213
+ return [
2214
+ replanHint,
2215
+ 'Inspect orchestration summary and merged outputs from all sub-specs.',
2216
+ 'Run `kse collab status --graph` to verify final dependency graph health.'
2217
+ ];
2218
+ }
2219
+
2220
+ if (status === 'failed' || status === 'stopped') {
2221
+ return [
2222
+ 'Run `kse orchestrate status --json` for failure details.',
2223
+ 'Resolve blocked specs and rerun the close-loop command with a new prefix.'
2224
+ ];
2225
+ }
2226
+
2227
+ return [
2228
+ 'Run `kse orchestrate status` to observe runtime progress.'
2229
+ ];
2230
+ }
2231
+
2232
+ function normalizeDodReportPath(result, options, projectPath) {
2233
+ if (typeof options.dodReport === 'string' && options.dodReport.trim()) {
2234
+ const customPath = options.dodReport.trim();
2235
+ return path.isAbsolute(customPath)
2236
+ ? customPath
2237
+ : path.join(projectPath, customPath);
2238
+ }
2239
+
2240
+ return path.join(
2241
+ projectPath,
2242
+ '.kiro',
2243
+ 'specs',
2244
+ result.portfolio.master_spec,
2245
+ 'custom',
2246
+ 'dod-report.json'
2247
+ );
2248
+ }
2249
+
2250
+ function buildDodReportPayload(result) {
2251
+ const orchestrationSummary = result.orchestration
2252
+ ? {
2253
+ status: result.orchestration.status,
2254
+ completed_count: (result.orchestration.completed || []).length,
2255
+ failed_count: (result.orchestration.failed || []).length,
2256
+ skipped_count: (result.orchestration.skipped || []).length
2257
+ }
2258
+ : null;
2259
+
2260
+ return {
2261
+ mode: 'auto-close-loop-dod-report',
2262
+ generated_at: new Date().toISOString(),
2263
+ goal: result.goal,
2264
+ status: result.status,
2265
+ portfolio: {
2266
+ prefix: result.portfolio.prefix,
2267
+ master_spec: result.portfolio.master_spec,
2268
+ sub_specs: result.portfolio.sub_specs
2269
+ },
2270
+ replan: result.replan,
2271
+ dod: result.dod,
2272
+ orchestration: orchestrationSummary,
2273
+ next_actions: result.next_actions
2274
+ };
2275
+ }
2276
+
2277
+ async function maybeWriteDodReport(result, options, projectPath) {
2278
+ if (options.dodReport === false) {
2279
+ return;
2280
+ }
2281
+
2282
+ const reportPath = normalizeDodReportPath(result, options, projectPath);
2283
+ const payload = buildDodReportPayload(result);
2284
+ await fs.ensureDir(path.dirname(reportPath));
2285
+ await fs.writeJson(reportPath, payload, { spaces: 2 });
2286
+ result.dod_report_file = reportPath;
2287
+ }
2288
+
2289
+ async function maybeWriteOutput(result, options, projectPath) {
2290
+ if (!options.out) {
2291
+ return;
2292
+ }
2293
+
2294
+ const outputPath = path.isAbsolute(options.out)
2295
+ ? options.out
2296
+ : path.join(projectPath, options.out);
2297
+
2298
+ await fs.ensureDir(path.dirname(outputPath));
2299
+ await fs.writeJson(outputPath, result, { spaces: 2 });
2300
+ result.output_file = outputPath;
2301
+ }
2302
+
2303
+ function printResult(result, options) {
2304
+ if (options.quiet) {
2305
+ return;
2306
+ }
2307
+
2308
+ if (options.json) {
2309
+ console.log(JSON.stringify(result, null, 2));
2310
+ return;
2311
+ }
2312
+
2313
+ console.log(chalk.blue('🚀') + ' Autonomous close-loop portfolio generated');
2314
+ console.log(chalk.gray(` Goal: ${result.goal}`));
2315
+ console.log(chalk.gray(` Status: ${result.status}`));
2316
+ console.log(chalk.gray(` Master: ${result.portfolio.master_spec}`));
2317
+ console.log(chalk.gray(` Sub Specs: ${result.portfolio.sub_specs.join(', ')}`));
2318
+
2319
+ if (result.orchestration) {
2320
+ console.log(chalk.gray(` Orchestration: ${result.orchestration.status}`));
2321
+ }
2322
+
2323
+ if (result.replan && result.replan.enabled) {
2324
+ const summary = `${result.replan.performed}/${result.replan.effective_max_attempts || result.replan.max_attempts} cycles`;
2325
+ const strategy = result.replan.strategy ? ` strategy=${result.replan.strategy}` : '';
2326
+ const exhaustedTag = result.replan.exhausted ? ' (exhausted)' : '';
2327
+ console.log(chalk.gray(` Replan: ${summary}${strategy}${exhaustedTag}`));
2328
+ }
2329
+
2330
+ if (result.dod && result.dod.enabled) {
2331
+ const failedCount = result.dod.failed_checks.length;
2332
+ const totalChecks = result.dod.checks.length;
2333
+ console.log(chalk.gray(
2334
+ ` DoD: ${result.dod.passed ? 'passed' : 'failed'} (${totalChecks - failedCount}/${totalChecks} checks passed)`
2335
+ ));
2336
+ if (!result.dod.passed) {
2337
+ console.log(chalk.gray(` DoD failures: ${result.dod.failed_checks.join(', ')}`));
2338
+ }
2339
+ }
2340
+
2341
+ if (result.output_file) {
2342
+ console.log(chalk.gray(` Output: ${result.output_file}`));
2343
+ }
2344
+
2345
+ if (result.dod_report_file) {
2346
+ console.log(chalk.gray(` DoD report: ${result.dod_report_file}`));
2347
+ }
2348
+
2349
+ if (result.session) {
2350
+ const resumeTag = result.session.resumed ? ' (resumed)' : '';
2351
+ console.log(chalk.gray(` Session: ${result.session.id}${resumeTag}`));
2352
+ console.log(chalk.gray(` Session file: ${result.session.file}`));
2353
+ }
2354
+
2355
+ if (result.session_prune && result.session_prune.enabled) {
2356
+ console.log(chalk.gray(
2357
+ ` Session prune: deleted=${result.session_prune.deleted_count} keep=` +
2358
+ `${result.session_prune.keep === null ? 'all' : result.session_prune.keep}`
2359
+ ));
2360
+ }
2361
+ }
2362
+
2363
+ function createStatusReporter(options) {
2364
+ if (options.json || options.stream === false) {
2365
+ return null;
2366
+ }
2367
+
2368
+ let lastSignature = '';
2369
+ let previousSpecStates = new Map();
2370
+
2371
+ return status => {
2372
+ const signature = [
2373
+ status.status,
2374
+ status.currentBatch || 0,
2375
+ status.totalBatches || 0,
2376
+ status.completedSpecs || 0,
2377
+ status.failedSpecs || 0,
2378
+ status.runningSpecs || 0
2379
+ ].join('|');
2380
+
2381
+ if (signature !== lastSignature) {
2382
+ console.log(chalk.gray(
2383
+ `[orchestrate] status=${status.status} batch=${status.currentBatch || 0}/${status.totalBatches || 0} ` +
2384
+ `completed=${status.completedSpecs || 0} failed=${status.failedSpecs || 0} running=${status.runningSpecs || 0}`
2385
+ ));
2386
+ lastSignature = signature;
2387
+ }
2388
+
2389
+ const specEntries = Object.entries(status.specs || {});
2390
+ for (const [specName, info] of specEntries) {
2391
+ const prev = previousSpecStates.get(specName);
2392
+ if (prev !== info.status) {
2393
+ console.log(chalk.gray(` [spec] ${specName} -> ${info.status}`));
2394
+ previousSpecStates.set(specName, info.status);
2395
+ }
2396
+ }
2397
+ };
2398
+ }
2399
+
2400
+ module.exports = {
2401
+ runAutoCloseLoop,
2402
+ buildAssignments,
2403
+ writeSpecDocuments
2404
+ };
2405
+
2406
+ async function writeAgentSyncPlan(projectPath, masterSpecName, subSpecs, assignments, executionPlanning = null) {
2407
+ const customDir = path.join(projectPath, '.kiro', 'specs', masterSpecName, 'custom');
2408
+ await fs.ensureDir(customDir);
2409
+ const leaseBySpec = executionPlanning && executionPlanning.lease_plan && executionPlanning.lease_plan.lease_by_spec
2410
+ ? executionPlanning.lease_plan.lease_by_spec
2411
+ : {};
2412
+ const conflictGroups = executionPlanning && executionPlanning.lease_plan && Array.isArray(executionPlanning.lease_plan.conflicts)
2413
+ ? executionPlanning.lease_plan.conflicts
2414
+ : [];
2415
+ const scheduling = executionPlanning && executionPlanning.planned_order
2416
+ ? executionPlanning.planned_order
2417
+ : null;
2418
+ const ontologyGuidance = executionPlanning && executionPlanning.ontology_guidance
2419
+ ? executionPlanning.ontology_guidance
2420
+ : null;
2421
+
2422
+ const lines = [
2423
+ '# Agent Sync Plan',
2424
+ '',
2425
+ '## Agent Topology',
2426
+ ...assignments.map(item => {
2427
+ const leaseKey = leaseBySpec[item.spec] ? ` lease=\`${leaseBySpec[item.spec]}\`` : '';
2428
+ return `- \`${item.agent}\`: owns \`${item.spec}\`${leaseKey}`;
2429
+ }),
2430
+ '',
2431
+ '## Dependency Cadence',
2432
+ ...subSpecs.map(spec => {
2433
+ const deps = spec.dependencies.map(dep => dep.spec).join(', ');
2434
+ return deps
2435
+ ? `- \`${spec.name}\` starts after: ${deps}`
2436
+ : `- \`${spec.name}\` can start immediately`;
2437
+ }),
2438
+ '',
2439
+ '## Close-Loop Rules',
2440
+ '1. Sub specs update collaboration status immediately after each milestone.',
2441
+ '2. Master spec only transitions to completed when all subs are completed.',
2442
+ '3. Any failed/blocked sub spec propagates blocked state to dependent specs.',
2443
+ ''
2444
+ ];
2445
+
2446
+ if (conflictGroups.length > 0) {
2447
+ lines.push(
2448
+ '## Lease Conflict Guard',
2449
+ ...conflictGroups.map(group => `- lease \`${group.lease_key}\`: ${group.specs.join(', ')}`),
2450
+ ''
2451
+ );
2452
+ }
2453
+
2454
+ if (scheduling && Array.isArray(scheduling.reordered) && scheduling.reordered.length > 0) {
2455
+ lines.push(
2456
+ '## Scheduling Plan',
2457
+ `- Auto reordered: ${scheduling.auto_reordered ? 'yes' : 'no'}`,
2458
+ `- Sequence: ${scheduling.reordered.join(' -> ')}`,
2459
+ ''
2460
+ );
2461
+ }
2462
+
2463
+ if (ontologyGuidance && ontologyGuidance.enabled) {
2464
+ const suggested = Array.isArray(ontologyGuidance.suggested_sequence)
2465
+ ? ontologyGuidance.suggested_sequence.join(' -> ')
2466
+ : '(none)';
2467
+ lines.push(
2468
+ '## Ontology Guidance',
2469
+ `- Source: ${ontologyGuidance.source}`,
2470
+ `- Suggested sequence: ${suggested}`,
2471
+ ''
2472
+ );
2473
+ }
2474
+
2475
+ await fs.writeFile(path.join(customDir, 'agent-sync-plan.md'), lines.join('\n'), 'utf8');
2476
+ }