scene-capability-engine 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2513 -0
- package/LICENSE +21 -0
- package/README.md +765 -0
- package/README.zh.md +630 -0
- package/bin/kiro-spec-engine.js +796 -0
- package/bin/kse.js +3 -0
- package/bin/sce.js +3 -0
- package/bin/sco.js +3 -0
- package/docs/331-poc-adaptation-roadmap.md +156 -0
- package/docs/331-poc-dual-track-integration-guide.md +120 -0
- package/docs/331-poc-weekly-delivery-checklist.md +52 -0
- package/docs/OFFLINE_INSTALL.md +96 -0
- package/docs/README.md +279 -0
- package/docs/adopt-migration-guide.md +599 -0
- package/docs/adoption-guide.md +616 -0
- package/docs/agent-hooks-analysis.md +815 -0
- package/docs/architecture.md +733 -0
- package/docs/articles/ai-driven-development-philosophy-and-practice-review.md +208 -0
- package/docs/articles/ai-driven-development-philosophy-and-practice.en.md +459 -0
- package/docs/articles/ai-driven-development-philosophy-and-practice.md +492 -0
- package/docs/autonomous-control-guide.md +851 -0
- package/docs/command-reference.md +1368 -0
- package/docs/community.md +115 -0
- package/docs/cross-tool-guide.md +555 -0
- package/docs/developer-guide.md +619 -0
- package/docs/document-governance.md +865 -0
- package/docs/environment-management-guide.md +526 -0
- package/docs/examples/add-export-command/design.md +194 -0
- package/docs/examples/add-export-command/requirements.md +110 -0
- package/docs/examples/add-export-command/tasks.md +88 -0
- package/docs/examples/add-rest-api/design.md +855 -0
- package/docs/examples/add-rest-api/requirements.md +323 -0
- package/docs/examples/add-rest-api/tasks.md +355 -0
- package/docs/examples/add-user-dashboard/design.md +192 -0
- package/docs/examples/add-user-dashboard/requirements.md +143 -0
- package/docs/examples/add-user-dashboard/tasks.md +91 -0
- package/docs/faq.md +697 -0
- package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.json +156 -0
- package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.md +24 -0
- package/docs/images/wechat-qr.png +0 -0
- package/docs/integration-modes.md +529 -0
- package/docs/integration-philosophy.md +313 -0
- package/docs/knowledge-management-guide.md +263 -0
- package/docs/manual-workflows-guide.md +418 -0
- package/docs/moqui-capability-matrix.md +73 -0
- package/docs/moqui-template-core-library-playbook.md +109 -0
- package/docs/multi-agent-coordination-guide.md +553 -0
- package/docs/multi-repo-management-guide.md +1344 -0
- package/docs/quick-start-with-ai-tools.md +375 -0
- package/docs/quick-start.md +146 -0
- package/docs/release-checklist.md +121 -0
- package/docs/releases/README.md +13 -0
- package/docs/releases/v1.46.2-validation.md +45 -0
- package/docs/releases/v1.46.2.md +50 -0
- package/docs/scene-runtime-guide.md +347 -0
- package/docs/spec-collaboration-guide.md +369 -0
- package/docs/spec-locking-guide.md +225 -0
- package/docs/spec-numbering-guide.md +348 -0
- package/docs/spec-workflow.md +519 -0
- package/docs/steering-strategy-guide.md +196 -0
- package/docs/team-collaboration-guide.md +465 -0
- package/docs/testing-strategy.md +272 -0
- package/docs/tools/claude-guide.md +654 -0
- package/docs/tools/cursor-guide.md +706 -0
- package/docs/tools/generic-guide.md +446 -0
- package/docs/tools/kiro-guide.md +308 -0
- package/docs/tools/vscode-guide.md +445 -0
- package/docs/tools/windsurf-guide.md +391 -0
- package/docs/troubleshooting.md +1135 -0
- package/docs/upgrade-guide.md +639 -0
- package/docs/value-observability-guide.md +127 -0
- package/docs/zh/README.md +341 -0
- package/docs/zh/quick-start.md +764 -0
- package/docs/zh/release-checklist.md +121 -0
- package/docs/zh/releases/README.md +13 -0
- package/docs/zh/releases/v1.46.2-validation.md +45 -0
- package/docs/zh/releases/v1.46.2.md +50 -0
- package/docs/zh/spec-numbering-guide.md +348 -0
- package/docs/zh/tools/claude-guide.md +349 -0
- package/docs/zh/tools/cursor-guide.md +281 -0
- package/docs/zh/tools/generic-guide.md +499 -0
- package/docs/zh/tools/kiro-guide.md +342 -0
- package/docs/zh/tools/vscode-guide.md +449 -0
- package/docs/zh/tools/windsurf-guide.md +378 -0
- package/docs/zh/value-observability-guide.md +127 -0
- package/docs//344/272/244/344/273/230/346/270/205/345/215/225.md +75 -0
- package/lib/adoption/adoption-logger.js +487 -0
- package/lib/adoption/adoption-strategy.js +538 -0
- package/lib/adoption/backup-manager.js +420 -0
- package/lib/adoption/conflict-resolver.js +410 -0
- package/lib/adoption/detection-engine.js +275 -0
- package/lib/adoption/diff-viewer.js +226 -0
- package/lib/adoption/error-formatter.js +509 -0
- package/lib/adoption/file-classifier.js +385 -0
- package/lib/adoption/progress-reporter.js +534 -0
- package/lib/adoption/smart-orchestrator.js +470 -0
- package/lib/adoption/strategy-selector.js +218 -0
- package/lib/adoption/summary-generator.js +493 -0
- package/lib/adoption/template-sync.js +605 -0
- package/lib/auto/autonomous-engine.js +485 -0
- package/lib/auto/checkpoint-manager.js +300 -0
- package/lib/auto/close-loop-runner.js +2476 -0
- package/lib/auto/config-schema.js +176 -0
- package/lib/auto/decision-engine.js +344 -0
- package/lib/auto/error-recovery-manager.js +580 -0
- package/lib/auto/goal-decomposer.js +278 -0
- package/lib/auto/progress-tracker.js +502 -0
- package/lib/auto/safety-manager.js +186 -0
- package/lib/auto/semantic-decomposer.js +137 -0
- package/lib/auto/state-manager.js +126 -0
- package/lib/auto/task-queue-manager.js +340 -0
- package/lib/backup/backup-system.js +372 -0
- package/lib/backup/selective-backup.js +207 -0
- package/lib/collab/agent-registry.js +240 -0
- package/lib/collab/collab-manager.js +285 -0
- package/lib/collab/contract-manager.js +320 -0
- package/lib/collab/coordinator.js +370 -0
- package/lib/collab/dependency-manager.js +280 -0
- package/lib/collab/index.js +20 -0
- package/lib/collab/integration-manager.js +202 -0
- package/lib/collab/merge-coordinator.js +252 -0
- package/lib/collab/metadata-manager.js +233 -0
- package/lib/collab/multi-agent-config.js +120 -0
- package/lib/collab/spec-lifecycle-manager.js +304 -0
- package/lib/collab/sync-barrier.js +88 -0
- package/lib/collab/visualizer.js +208 -0
- package/lib/commands/adopt.js +749 -0
- package/lib/commands/auto.js +19559 -0
- package/lib/commands/collab.js +275 -0
- package/lib/commands/context.js +99 -0
- package/lib/commands/docs.js +808 -0
- package/lib/commands/doctor.js +273 -0
- package/lib/commands/env.js +420 -0
- package/lib/commands/knowledge.js +309 -0
- package/lib/commands/lock.js +235 -0
- package/lib/commands/ops.js +409 -0
- package/lib/commands/orchestrate.js +446 -0
- package/lib/commands/prompt.js +105 -0
- package/lib/commands/repo.js +118 -0
- package/lib/commands/rollback.js +219 -0
- package/lib/commands/scene.js +15549 -0
- package/lib/commands/spec-bootstrap.js +147 -0
- package/lib/commands/spec-gate.js +157 -0
- package/lib/commands/spec-pipeline.js +205 -0
- package/lib/commands/status.js +321 -0
- package/lib/commands/task.js +199 -0
- package/lib/commands/templates.js +654 -0
- package/lib/commands/upgrade.js +231 -0
- package/lib/commands/value.js +569 -0
- package/lib/commands/watch.js +684 -0
- package/lib/commands/workflows.js +240 -0
- package/lib/commands/workspace-multi.js +325 -0
- package/lib/commands/workspace.js +189 -0
- package/lib/context/context-exporter.js +378 -0
- package/lib/context/prompt-generator.js +482 -0
- package/lib/data/moqui-capability-lexicon.json +45 -0
- package/lib/environment/backup-system.js +189 -0
- package/lib/environment/environment-manager.js +379 -0
- package/lib/environment/environment-registry.js +168 -0
- package/lib/gitignore/gitignore-backup.js +229 -0
- package/lib/gitignore/gitignore-detector.js +239 -0
- package/lib/gitignore/gitignore-integration.js +267 -0
- package/lib/gitignore/gitignore-transformer.js +193 -0
- package/lib/gitignore/layered-rules-template.js +42 -0
- package/lib/governance/archive-tool.js +284 -0
- package/lib/governance/cleanup-tool.js +237 -0
- package/lib/governance/config-manager.js +186 -0
- package/lib/governance/diagnostic-engine.js +271 -0
- package/lib/governance/doc-reference-checker.js +200 -0
- package/lib/governance/execution-logger.js +243 -0
- package/lib/governance/file-scanner.js +285 -0
- package/lib/governance/hooks-manager.js +333 -0
- package/lib/governance/reporter.js +337 -0
- package/lib/governance/validation-engine.js +181 -0
- package/lib/i18n.js +79 -0
- package/lib/knowledge/entry-manager.js +208 -0
- package/lib/knowledge/index-manager.js +261 -0
- package/lib/knowledge/knowledge-manager.js +273 -0
- package/lib/knowledge/template-manager.js +191 -0
- package/lib/lock/index.js +21 -0
- package/lib/lock/lock-file.js +192 -0
- package/lib/lock/lock-manager.js +321 -0
- package/lib/lock/machine-identifier.js +135 -0
- package/lib/lock/steering-file-lock.js +207 -0
- package/lib/lock/task-lock-manager.js +345 -0
- package/lib/operations/audit-logger.js +293 -0
- package/lib/operations/feedback-manager.js +1147 -0
- package/lib/operations/index.js +23 -0
- package/lib/operations/models/index.js +170 -0
- package/lib/operations/operations-manager.js +151 -0
- package/lib/operations/operations-validator.js +280 -0
- package/lib/operations/permission-manager.js +354 -0
- package/lib/operations/template-loader.js +143 -0
- package/lib/orchestrator/agent-spawner.js +629 -0
- package/lib/orchestrator/bootstrap-prompt-builder.js +236 -0
- package/lib/orchestrator/index.js +19 -0
- package/lib/orchestrator/orchestration-engine.js +1270 -0
- package/lib/orchestrator/orchestrator-config.js +173 -0
- package/lib/orchestrator/status-monitor.js +591 -0
- package/lib/python-checker.js +209 -0
- package/lib/repo/config-manager.js +580 -0
- package/lib/repo/errors/config-error.js +13 -0
- package/lib/repo/errors/git-error.js +15 -0
- package/lib/repo/errors/repo-error.js +14 -0
- package/lib/repo/git-operations.js +181 -0
- package/lib/repo/handlers/.gitkeep +1 -0
- package/lib/repo/handlers/exec-handler.js +155 -0
- package/lib/repo/handlers/health-handler.js +169 -0
- package/lib/repo/handlers/init-handler.js +197 -0
- package/lib/repo/handlers/status-handler.js +176 -0
- package/lib/repo/output-formatter.js +184 -0
- package/lib/repo/path-resolver.js +178 -0
- package/lib/repo/repo-manager.js +514 -0
- package/lib/scene-runtime/audit-emitter.js +59 -0
- package/lib/scene-runtime/binding-plugin-loader.js +351 -0
- package/lib/scene-runtime/binding-registry.js +349 -0
- package/lib/scene-runtime/eval-bridge.js +44 -0
- package/lib/scene-runtime/index.js +19 -0
- package/lib/scene-runtime/moqui-adapter.js +620 -0
- package/lib/scene-runtime/moqui-client.js +606 -0
- package/lib/scene-runtime/moqui-extractor.js +2029 -0
- package/lib/scene-runtime/plan-compiler.js +208 -0
- package/lib/scene-runtime/policy-gate.js +58 -0
- package/lib/scene-runtime/runtime-executor.js +358 -0
- package/lib/scene-runtime/scene-loader.js +96 -0
- package/lib/scene-runtime/scene-ontology.js +959 -0
- package/lib/scene-runtime/scene-template-linter.js +852 -0
- package/lib/scene-runtime/templates/scene-template-erp-query-v0.1.yaml +28 -0
- package/lib/scene-runtime/templates/scene-template-hybrid-shadow-v0.1.yaml +34 -0
- package/lib/spec/bootstrap/context-collector.js +48 -0
- package/lib/spec/bootstrap/draft-generator.js +158 -0
- package/lib/spec/bootstrap/questionnaire-engine.js +70 -0
- package/lib/spec/bootstrap/trace-emitter.js +59 -0
- package/lib/spec/multi-spec-orchestrate.js +93 -0
- package/lib/spec/pipeline/constants.js +6 -0
- package/lib/spec/pipeline/stage-adapters.js +118 -0
- package/lib/spec/pipeline/stage-runner.js +146 -0
- package/lib/spec/pipeline/state-store.js +119 -0
- package/lib/spec-gate/engine/gate-engine.js +165 -0
- package/lib/spec-gate/policy/default-policy.js +22 -0
- package/lib/spec-gate/policy/policy-loader.js +103 -0
- package/lib/spec-gate/result-emitter.js +81 -0
- package/lib/spec-gate/rules/default-rules.js +156 -0
- package/lib/spec-gate/rules/rule-registry.js +51 -0
- package/lib/steering/adoption-config.js +164 -0
- package/lib/steering/compliance-auto-fixer.js +204 -0
- package/lib/steering/compliance-cache.js +99 -0
- package/lib/steering/compliance-error-reporter.js +70 -0
- package/lib/steering/context-sync-manager.js +273 -0
- package/lib/steering/index.js +92 -0
- package/lib/steering/spec-steering.js +230 -0
- package/lib/steering/steering-compliance-checker.js +73 -0
- package/lib/steering/steering-loader.js +144 -0
- package/lib/steering/steering-manager.js +289 -0
- package/lib/task/index.js +12 -0
- package/lib/task/task-claimer.js +489 -0
- package/lib/task/task-status-store.js +418 -0
- package/lib/templates/cache-manager.js +440 -0
- package/lib/templates/content-generalizer.js +247 -0
- package/lib/templates/frontmatter-generator.js +128 -0
- package/lib/templates/git-handler.js +471 -0
- package/lib/templates/metadata-collector.js +328 -0
- package/lib/templates/path-utils.js +144 -0
- package/lib/templates/registry-parser.js +505 -0
- package/lib/templates/spec-reader.js +216 -0
- package/lib/templates/template-applicator.js +249 -0
- package/lib/templates/template-creator.js +256 -0
- package/lib/templates/template-error.js +143 -0
- package/lib/templates/template-exporter.js +502 -0
- package/lib/templates/template-manager.js +782 -0
- package/lib/templates/template-validator.js +361 -0
- package/lib/upgrade/migration-engine.js +382 -0
- package/lib/upgrade/migrations/.gitkeep +52 -0
- package/lib/upgrade/migrations/1.0.0-to-1.1.0.js +78 -0
- package/lib/utils/file-diff.js +177 -0
- package/lib/utils/fs-utils.js +274 -0
- package/lib/utils/tool-detector.js +383 -0
- package/lib/utils/validation.js +324 -0
- package/lib/value/gate-summary-emitter.js +99 -0
- package/lib/value/metric-contract-loader.js +210 -0
- package/lib/value/risk-evaluator.js +117 -0
- package/lib/value/weekly-snapshot-builder.js +61 -0
- package/lib/version/version-checker.js +156 -0
- package/lib/version/version-manager.js +327 -0
- package/lib/watch/action-executor.js +458 -0
- package/lib/watch/event-debouncer.js +323 -0
- package/lib/watch/execution-logger.js +550 -0
- package/lib/watch/file-watcher.js +499 -0
- package/lib/watch/presets.js +266 -0
- package/lib/watch/watch-manager.js +533 -0
- package/lib/workspace/multi/global-config.js +150 -0
- package/lib/workspace/multi/index.js +22 -0
- package/lib/workspace/multi/path-utils.js +173 -0
- package/lib/workspace/multi/workspace-context-resolver.js +244 -0
- package/lib/workspace/multi/workspace-registry.js +196 -0
- package/lib/workspace/multi/workspace-state-manager.js +537 -0
- package/lib/workspace/multi/workspace.js +90 -0
- package/lib/workspace/workspace-manager.js +370 -0
- package/lib/workspace/workspace-sync.js +356 -0
- package/locales/en.json +114 -0
- package/locales/zh.json +114 -0
- package/package.json +102 -0
- package/template/.kiro/README.md +247 -0
- package/template/.kiro/hooks/check-spec-on-create.kiro.hook +17 -0
- package/template/.kiro/hooks/run-tests-on-save.kiro.hook +13 -0
- package/template/.kiro/hooks/sync-tasks-on-edit.kiro.hook +16 -0
- package/template/.kiro/specs/SPEC_WORKFLOW_GUIDE.md +134 -0
- package/template/.kiro/steering/CORE_PRINCIPLES.md +133 -0
- package/template/.kiro/steering/CURRENT_CONTEXT.md +30 -0
- package/template/.kiro/steering/ENVIRONMENT.md +35 -0
- package/template/.kiro/steering/RULES_GUIDE.md +46 -0
- package/template/.kiro/templates/operations/default/change-impact.md +112 -0
- package/template/.kiro/templates/operations/default/deployment.md +91 -0
- package/template/.kiro/templates/operations/default/feedback-response.md +269 -0
- package/template/.kiro/templates/operations/default/migration-plan.md +172 -0
- package/template/.kiro/templates/operations/default/monitoring.md +135 -0
- package/template/.kiro/templates/operations/default/operations.md +135 -0
- package/template/.kiro/templates/operations/default/rollback.md +143 -0
- package/template/.kiro/templates/operations/default/tools.yaml +364 -0
- package/template/.kiro/templates/operations/default/troubleshooting.md +123 -0
- package/template/.kiro/tools/backup_manager.py +295 -0
- package/template/.kiro/tools/configuration_manager.py +218 -0
- package/template/.kiro/tools/document_evaluator.py +550 -0
- package/template/.kiro/tools/enhancement_logger.py +168 -0
- package/template/.kiro/tools/error_handler.py +335 -0
- package/template/.kiro/tools/improvement_identifier.py +444 -0
- package/template/.kiro/tools/modification_applicator.py +737 -0
- package/template/.kiro/tools/quality_gate_enforcer.py +207 -0
- package/template/.kiro/tools/quality_scorer.py +305 -0
- package/template/.kiro/tools/report_generator.py +154 -0
- package/template/.kiro/tools/ultrawork_enhancer.py +676 -0
- package/template/.kiro/tools/ultrawork_enhancer_refactored.py +0 -0
- package/template/.kiro/tools/ultrawork_enhancer_v2.py +463 -0
- package/template/.kiro/tools/ultrawork_enhancer_v3.py +606 -0
- package/template/.kiro/tools/workflow_quality_gate.py +100 -0
- package/template/README.md +111 -0
|
@@ -0,0 +1,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
|
+
}
|