agentic-orchestrator 0.1.2 → 0.1.4
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/.claude/settings.local.json +15 -0
- package/CLAUDE.md +126 -0
- package/README.md +166 -25
- package/agentic/orchestrator/adapters.yaml +3 -0
- package/agentic/orchestrator/gates.yaml +47 -0
- package/agentic/orchestrator/policy.yaml +89 -0
- package/agentic/orchestrator/schemas/adapters.schema.json +12 -0
- package/agentic/orchestrator/schemas/gates.schema.json +6 -1
- package/agentic/orchestrator/schemas/index.schema.json +14 -0
- package/agentic/orchestrator/schemas/multi-project.schema.json +41 -0
- package/agentic/orchestrator/schemas/policy.schema.json +449 -52
- package/agentic/orchestrator/schemas/state.schema.json +16 -0
- package/agentic/orchestrator/tools/catalog.json +68 -0
- package/agentic/orchestrator/tools/schemas/input/cost.get.input.schema.json +10 -0
- package/agentic/orchestrator/tools/schemas/input/cost.record.input.schema.json +13 -0
- package/agentic/orchestrator/tools/schemas/input/feature.send_message.input.schema.json +11 -0
- package/agentic/orchestrator/tools/schemas/input/performance.get_analytics.input.schema.json +10 -0
- package/agentic/orchestrator/tools/schemas/input/performance.record_outcome.input.schema.json +18 -0
- package/agentic/orchestrator/tools/schemas/output/cost.get.output.schema.json +13 -0
- package/agentic/orchestrator/tools/schemas/output/cost.record.output.schema.json +13 -0
- package/agentic/orchestrator/tools/schemas/output/feature.ready_to_merge.output.schema.json +7 -0
- package/agentic/orchestrator/tools/schemas/output/feature.send_message.output.schema.json +23 -0
- package/agentic/orchestrator/tools/schemas/output/performance.get_analytics.output.schema.json +46 -0
- package/agentic/orchestrator/tools/schemas/output/performance.record_outcome.output.schema.json +10 -0
- package/agentic/orchestrator/tools.md +5 -0
- package/apps/control-plane/scripts/validate-architecture-rules.mjs +28 -2
- package/apps/control-plane/scripts/validate-docker-mcp-contract.mjs +12 -0
- package/apps/control-plane/scripts/validate-mcp-contracts.ts +92 -0
- package/apps/control-plane/src/application/adapters/adapter-registry.ts +169 -0
- package/apps/control-plane/src/application/multi-project-loader.ts +119 -0
- package/apps/control-plane/src/application/services/activity-monitor-service.ts +199 -0
- package/apps/control-plane/src/application/services/cost-tracking-service.ts +82 -0
- package/apps/control-plane/src/application/services/dependency-scheduler-service.ts +86 -0
- package/apps/control-plane/src/application/services/feature-deletion-service.ts +8 -7
- package/apps/control-plane/src/application/services/gate-interpolation-service.ts +15 -0
- package/apps/control-plane/src/application/services/gate-service.ts +38 -2
- package/apps/control-plane/src/application/services/instance-isolation-service.ts +18 -0
- package/apps/control-plane/src/application/services/issue-tracker-service.ts +469 -0
- package/apps/control-plane/src/application/services/merge-service.ts +67 -3
- package/apps/control-plane/src/application/services/notifier-service.ts +295 -0
- package/apps/control-plane/src/application/services/performance-analytics-service.ts +122 -0
- package/apps/control-plane/src/application/services/plan-service.ts +51 -0
- package/apps/control-plane/src/application/services/pr-monitor-service.ts +262 -0
- package/apps/control-plane/src/application/services/reactions-service.ts +175 -0
- package/apps/control-plane/src/application/services/reporting-service.ts +17 -2
- package/apps/control-plane/src/application/services/run-lease-service.ts +16 -38
- package/apps/control-plane/src/application/tools/tool-metadata.ts +4 -1
- package/apps/control-plane/src/cli/aop.ts +1 -1
- package/apps/control-plane/src/cli/attach-command-handler.ts +120 -0
- package/apps/control-plane/src/cli/cleanup-command-handler.ts +190 -0
- package/apps/control-plane/src/cli/cli-argument-parser.ts +69 -3
- package/apps/control-plane/src/cli/dashboard-command-handler.ts +57 -0
- package/apps/control-plane/src/cli/help-command-handler.ts +163 -0
- package/apps/control-plane/src/cli/init-command-handler.ts +609 -0
- package/apps/control-plane/src/cli/resume-command-handler.ts +1 -0
- package/apps/control-plane/src/cli/retry-command-handler.ts +138 -0
- package/apps/control-plane/src/cli/run-command-handler.ts +115 -3
- package/apps/control-plane/src/cli/send-command-handler.ts +65 -0
- package/apps/control-plane/src/cli/status-command-handler.ts +102 -2
- package/apps/control-plane/src/cli/types.ts +26 -1
- package/apps/control-plane/src/core/constants.ts +8 -2
- package/apps/control-plane/src/core/error-codes.ts +3 -1
- package/apps/control-plane/src/core/gates.ts +170 -50
- package/apps/control-plane/src/core/kernel.ts +280 -5
- package/apps/control-plane/src/core/path-layout.ts +12 -0
- package/apps/control-plane/src/core/tool-caller.ts +36 -0
- package/apps/control-plane/src/core/workspace-hooks.ts +87 -0
- package/apps/control-plane/src/interfaces/cli/bootstrap.ts +258 -9
- package/apps/control-plane/src/providers/providers.ts +235 -14
- package/apps/control-plane/src/supervisor/build-wave-executor.ts +129 -8
- package/apps/control-plane/src/supervisor/qa-wave-executor.ts +123 -5
- package/apps/control-plane/src/supervisor/run-coordinator.ts +143 -6
- package/apps/control-plane/src/supervisor/runtime.ts +135 -6
- package/apps/control-plane/src/supervisor/types.ts +12 -21
- package/apps/control-plane/src/supervisor/worker-decision-loop.ts +8 -0
- package/apps/control-plane/test/activity-monitor.spec.ts +294 -0
- package/apps/control-plane/test/adapter-registry.spec.ts +132 -0
- package/apps/control-plane/test/batch-operations.spec.ts +112 -0
- package/apps/control-plane/test/bootstrap-attach.spec.ts +102 -0
- package/apps/control-plane/test/bootstrap-edge-cases.spec.ts +252 -0
- package/apps/control-plane/test/bootstrap.spec.ts +560 -0
- package/apps/control-plane/test/cleanup-command.spec.ts +301 -0
- package/apps/control-plane/test/cli-helpers.spec.ts +404 -1
- package/apps/control-plane/test/cli.unit.spec.ts +182 -1
- package/apps/control-plane/test/collision-queue.spec.ts +104 -1
- package/apps/control-plane/test/core-utils.spec.ts +175 -2
- package/apps/control-plane/test/cost-tracking.spec.ts +143 -0
- package/apps/control-plane/test/dashboard-api.integration.spec.ts +247 -0
- package/apps/control-plane/test/dashboard-client.spec.ts +116 -0
- package/apps/control-plane/test/dashboard-command.spec.ts +103 -0
- package/apps/control-plane/test/dependency-scheduler.spec.ts +189 -0
- package/apps/control-plane/test/epoch-tracking.spec.ts +4 -4
- package/apps/control-plane/test/feature-deletion-service.spec.ts +422 -0
- package/apps/control-plane/test/feature-lifecycle.spec.ts +202 -0
- package/apps/control-plane/test/git-spawn-error.spec.ts +24 -0
- package/apps/control-plane/test/incremental-gates.spec.ts +137 -0
- package/apps/control-plane/test/init-wizard.spec.ts +506 -0
- package/apps/control-plane/test/instance-isolation.spec.ts +83 -0
- package/apps/control-plane/test/issue-tracker.spec.ts +890 -0
- package/apps/control-plane/test/kernel.coverage.spec.ts +3 -5
- package/apps/control-plane/test/kernel.coverage2.spec.ts +871 -0
- package/apps/control-plane/test/kernel.spec.ts +13 -11
- package/apps/control-plane/test/lock-service.spec.ts +508 -0
- package/apps/control-plane/test/mcp-helpers.spec.ts +176 -0
- package/apps/control-plane/test/mcp.spec.ts +50 -15
- package/apps/control-plane/test/merge-service.spec.ts +67 -4
- package/apps/control-plane/test/multi-project.spec.ts +372 -0
- package/apps/control-plane/test/notifier-service.spec.ts +388 -0
- package/apps/control-plane/test/parallel-gates.spec.ts +312 -0
- package/apps/control-plane/test/patch-service.spec.ts +253 -0
- package/apps/control-plane/test/performance-analytics.spec.ts +338 -0
- package/apps/control-plane/test/planning-wave-executor.spec.ts +168 -0
- package/apps/control-plane/test/pr-monitor.spec.ts +385 -0
- package/apps/control-plane/test/providers.spec.ts +344 -1
- package/apps/control-plane/test/reactions.spec.ts +392 -0
- package/apps/control-plane/test/resume-command.spec.ts +390 -0
- package/apps/control-plane/test/run-coordinator.spec.ts +481 -2
- package/apps/control-plane/test/schema-date-time.spec.ts +46 -0
- package/apps/control-plane/test/service-retry-paths.spec.ts +30 -0
- package/apps/control-plane/test/services.spec.ts +95 -2
- package/apps/control-plane/test/session-management.spec.ts +450 -0
- package/apps/control-plane/test/spec-ingestion.spec.ts +190 -0
- package/apps/control-plane/test/supervisor-collaborators.spec.ts +699 -2
- package/apps/control-plane/test/supervisor.spec.ts +36 -30
- package/apps/control-plane/test/supervisor.unit.spec.ts +405 -0
- package/apps/control-plane/test/worker-decision-loop.spec.ts +57 -0
- package/apps/control-plane/test/workspace-hooks.spec.ts +177 -0
- package/apps/control-plane/vitest.config.ts +21 -5
- package/dist/apps/control-plane/application/adapters/adapter-registry.d.ts +44 -0
- package/dist/apps/control-plane/application/adapters/adapter-registry.js +76 -0
- package/dist/apps/control-plane/application/adapters/adapter-registry.js.map +1 -0
- package/dist/apps/control-plane/application/multi-project-loader.d.ts +31 -0
- package/dist/apps/control-plane/application/multi-project-loader.js +82 -0
- package/dist/apps/control-plane/application/multi-project-loader.js.map +1 -0
- package/dist/apps/control-plane/application/services/activity-monitor-service.d.ts +43 -0
- package/dist/apps/control-plane/application/services/activity-monitor-service.js +132 -0
- package/dist/apps/control-plane/application/services/activity-monitor-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/cost-tracking-service.d.ts +28 -0
- package/dist/apps/control-plane/application/services/cost-tracking-service.js +48 -0
- package/dist/apps/control-plane/application/services/cost-tracking-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/dependency-scheduler-service.d.ts +26 -0
- package/dist/apps/control-plane/application/services/dependency-scheduler-service.js +75 -0
- package/dist/apps/control-plane/application/services/dependency-scheduler-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/feature-deletion-service.d.ts +2 -0
- package/dist/apps/control-plane/application/services/feature-deletion-service.js +6 -7
- package/dist/apps/control-plane/application/services/feature-deletion-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/gate-interpolation-service.d.ts +7 -0
- package/dist/apps/control-plane/application/services/gate-interpolation-service.js +7 -0
- package/dist/apps/control-plane/application/services/gate-interpolation-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/gate-service.js +32 -2
- package/dist/apps/control-plane/application/services/gate-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/instance-isolation-service.d.ts +11 -0
- package/dist/apps/control-plane/application/services/instance-isolation-service.js +17 -0
- package/dist/apps/control-plane/application/services/instance-isolation-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/issue-tracker-service.d.ts +65 -0
- package/dist/apps/control-plane/application/services/issue-tracker-service.js +358 -0
- package/dist/apps/control-plane/application/services/issue-tracker-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/merge-service.d.ts +4 -0
- package/dist/apps/control-plane/application/services/merge-service.js +44 -2
- package/dist/apps/control-plane/application/services/merge-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/notifier-service.d.ts +74 -0
- package/dist/apps/control-plane/application/services/notifier-service.js +212 -0
- package/dist/apps/control-plane/application/services/notifier-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/performance-analytics-service.d.ts +39 -0
- package/dist/apps/control-plane/application/services/performance-analytics-service.js +75 -0
- package/dist/apps/control-plane/application/services/performance-analytics-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/plan-service.d.ts +1 -0
- package/dist/apps/control-plane/application/services/plan-service.js +53 -0
- package/dist/apps/control-plane/application/services/plan-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/pr-monitor-service.d.ts +44 -0
- package/dist/apps/control-plane/application/services/pr-monitor-service.js +192 -0
- package/dist/apps/control-plane/application/services/pr-monitor-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/reactions-service.d.ts +67 -0
- package/dist/apps/control-plane/application/services/reactions-service.js +114 -0
- package/dist/apps/control-plane/application/services/reactions-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/reporting-service.d.ts +1 -0
- package/dist/apps/control-plane/application/services/reporting-service.js +13 -2
- package/dist/apps/control-plane/application/services/reporting-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/run-lease-service.d.ts +2 -0
- package/dist/apps/control-plane/application/services/run-lease-service.js +14 -38
- package/dist/apps/control-plane/application/services/run-lease-service.js.map +1 -1
- package/dist/apps/control-plane/application/tools/tool-metadata.js +3 -1
- package/dist/apps/control-plane/application/tools/tool-metadata.js.map +1 -1
- package/dist/apps/control-plane/cli/aop.d.ts +1 -1
- package/dist/apps/control-plane/cli/aop.js +1 -1
- package/dist/apps/control-plane/cli/attach-command-handler.d.ts +12 -0
- package/dist/apps/control-plane/cli/attach-command-handler.js +98 -0
- package/dist/apps/control-plane/cli/attach-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/cleanup-command-handler.d.ts +12 -0
- package/dist/apps/control-plane/cli/cleanup-command-handler.js +162 -0
- package/dist/apps/control-plane/cli/cleanup-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/cli-argument-parser.js +73 -3
- package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
- package/dist/apps/control-plane/cli/dashboard-command-handler.d.ts +7 -0
- package/dist/apps/control-plane/cli/dashboard-command-handler.js +45 -0
- package/dist/apps/control-plane/cli/dashboard-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/help-command-handler.d.ts +8 -0
- package/dist/apps/control-plane/cli/help-command-handler.js +146 -0
- package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/init-command-handler.d.ts +26 -0
- package/dist/apps/control-plane/cli/init-command-handler.js +517 -0
- package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/resume-command-handler.js +1 -1
- package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/retry-command-handler.d.ts +8 -0
- package/dist/apps/control-plane/cli/retry-command-handler.js +111 -0
- package/dist/apps/control-plane/cli/retry-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/run-command-handler.d.ts +5 -0
- package/dist/apps/control-plane/cli/run-command-handler.js +82 -3
- package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/send-command-handler.d.ts +8 -0
- package/dist/apps/control-plane/cli/send-command-handler.js +55 -0
- package/dist/apps/control-plane/cli/send-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/status-command-handler.d.ts +12 -1
- package/dist/apps/control-plane/cli/status-command-handler.js +55 -2
- package/dist/apps/control-plane/cli/status-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/types.d.ts +26 -1
- package/dist/apps/control-plane/cli/types.js +15 -1
- package/dist/apps/control-plane/cli/types.js.map +1 -1
- package/dist/apps/control-plane/core/constants.d.ts +6 -0
- package/dist/apps/control-plane/core/constants.js +8 -2
- package/dist/apps/control-plane/core/constants.js.map +1 -1
- package/dist/apps/control-plane/core/error-codes.d.ts +2 -0
- package/dist/apps/control-plane/core/error-codes.js +3 -1
- package/dist/apps/control-plane/core/error-codes.js.map +1 -1
- package/dist/apps/control-plane/core/gates.d.ts +4 -0
- package/dist/apps/control-plane/core/gates.js +140 -43
- package/dist/apps/control-plane/core/gates.js.map +1 -1
- package/dist/apps/control-plane/core/kernel.d.ts +50 -1
- package/dist/apps/control-plane/core/kernel.js +220 -7
- package/dist/apps/control-plane/core/kernel.js.map +1 -1
- package/dist/apps/control-plane/core/path-layout.d.ts +3 -0
- package/dist/apps/control-plane/core/path-layout.js +9 -0
- package/dist/apps/control-plane/core/path-layout.js.map +1 -1
- package/dist/apps/control-plane/core/tool-caller.d.ts +32 -0
- package/dist/apps/control-plane/core/tool-caller.js +2 -0
- package/dist/apps/control-plane/core/tool-caller.js.map +1 -0
- package/dist/apps/control-plane/core/workspace-hooks.d.ts +20 -0
- package/dist/apps/control-plane/core/workspace-hooks.js +69 -0
- package/dist/apps/control-plane/core/workspace-hooks.js.map +1 -0
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js +245 -9
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
- package/dist/apps/control-plane/providers/providers.d.ts +42 -3
- package/dist/apps/control-plane/providers/providers.js +216 -5
- package/dist/apps/control-plane/providers/providers.js.map +1 -1
- package/dist/apps/control-plane/supervisor/build-wave-executor.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/build-wave-executor.js +115 -6
- package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/qa-wave-executor.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js +109 -5
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +15 -0
- package/dist/apps/control-plane/supervisor/run-coordinator.js +132 -6
- package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
- package/dist/apps/control-plane/supervisor/runtime.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/runtime.js +110 -6
- package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
- package/dist/apps/control-plane/supervisor/types.d.ts +9 -16
- package/dist/apps/control-plane/supervisor/types.js.map +1 -1
- package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js +5 -0
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
- package/eslint.config.mjs +2 -1
- package/package.json +12 -2
- package/packages/web-dashboard/next-env.d.ts +5 -0
- package/packages/web-dashboard/next.config.js +7 -0
- package/packages/web-dashboard/package.json +26 -0
- package/packages/web-dashboard/src/app/api/actions/route.ts +64 -0
- package/packages/web-dashboard/src/app/api/events/route.ts +51 -0
- package/packages/web-dashboard/src/app/api/features/[id]/checkout/route.ts +256 -0
- package/packages/web-dashboard/src/app/api/features/[id]/diff/route.ts +10 -0
- package/packages/web-dashboard/src/app/api/features/[id]/evidence/[artifact]/route.ts +25 -0
- package/packages/web-dashboard/src/app/api/features/[id]/review/route.ts +63 -0
- package/packages/web-dashboard/src/app/api/features/[id]/route.ts +16 -0
- package/packages/web-dashboard/src/app/api/projects/route.ts +31 -0
- package/packages/web-dashboard/src/app/api/status/route.ts +15 -0
- package/packages/web-dashboard/src/app/globals.css +2 -0
- package/packages/web-dashboard/src/app/layout.tsx +15 -0
- package/packages/web-dashboard/src/app/page.tsx +393 -0
- package/packages/web-dashboard/src/lib/aop-client.ts +244 -0
- package/packages/web-dashboard/src/lib/multi-project-config.ts +116 -0
- package/packages/web-dashboard/src/lib/orchestrator-tools.ts +284 -0
- package/packages/web-dashboard/src/lib/types.ts +58 -0
- package/packages/web-dashboard/tsconfig.json +40 -0
- package/packages/web-dashboard/vitest.config.ts +6 -0
- package/spec-files/completed/agentic_orchestrator_feature_gaps_closure_spec.md +1764 -0
- package/spec-files/outstanding/agentic_orchestrator_enterprise_governance_dashboard_spec.md +348 -0
- package/spec-files/outstanding/agentic_orchestrator_knowledge_canary_spec.md +344 -0
- package/spec-files/outstanding/agentic_orchestrator_observability_integrity_diagnostics_spec.md +374 -0
- package/spec-files/outstanding/agentic_orchestrator_performance_improvements_spec.md +1059 -0
- package/spec-files/outstanding/agentic_orchestrator_planning_review_quality_spec.md +466 -0
- package/spec-files/outstanding/agentic_orchestrator_quality_adoption_execution_spec.md +198 -0
- package/spec-files/outstanding/agentic_orchestrator_validator_hardening_spec.md +365 -0
- package/spec-files/progress.md +481 -52
- /package/spec-files/{agentic_orchestrator_cli_delete_command_spec.md → completed/agentic_orchestrator_cli_delete_command_spec.md} +0 -0
- /package/spec-files/{agentic_orchestrator_dot_aop_generated_artifacts_spec.md → completed/agentic_orchestrator_dot_aop_generated_artifacts_spec.md} +0 -0
- /package/spec-files/{agentic_orchestrator_mcp_formalization_spec.md → completed/agentic_orchestrator_mcp_formalization_spec.md} +0 -0
- /package/spec-files/{agentic_orchestrator_oop_refactor_spec.md → completed/agentic_orchestrator_oop_refactor_spec.md} +0 -0
- /package/spec-files/{agentic_orchestrator_single_global_orchestrator_spec.md → completed/agentic_orchestrator_single_global_orchestrator_spec.md} +0 -0
- /package/spec-files/{agentic_orchestrator_spec.md → completed/agentic_orchestrator_spec.md} +0 -0
|
@@ -0,0 +1,1059 @@
|
|
|
1
|
+
# Feature Spec: Runtime Performance & Agent Context Optimization
|
|
2
|
+
|
|
3
|
+
**Version:** 1.0
|
|
4
|
+
**Date:** 2026-03-03
|
|
5
|
+
**Status:** Draft
|
|
6
|
+
**Milestone:** M30 – Performance & Efficiency
|
|
7
|
+
|
|
8
|
+
> **Purpose:** Diagnose and implement fixes for sub-optimal runtime, memory, and agent context-size patterns in the control plane. Section 2 is the diagnostic record (what is wrong and why). Section 3 is the implementation plan (what to change and how). Each task in Section 3 maps to one or more findings in Section 2.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 0. Implementation Standards & References
|
|
13
|
+
|
|
14
|
+
### 0.1 Testing Standards
|
|
15
|
+
|
|
16
|
+
All new and modified code MUST follow the testing standards already established in the repo:
|
|
17
|
+
|
|
18
|
+
- Use Vitest (`describe/it/expect`, `vi` mocks/spies)
|
|
19
|
+
- Test files live in `apps/control-plane/test/*.spec.ts`
|
|
20
|
+
- Use **Given / When / Then** naming: `GIVEN_<context>_WHEN_<action>_THEN_<expected>`
|
|
21
|
+
- Maintain coverage thresholds: Lines ≥70%, Branches ≥70%, Functions ≥85%
|
|
22
|
+
- Time-dependent tests must use `vi.useFakeTimers()`
|
|
23
|
+
- No real filesystem I/O in unit tests; use `tmp` directories or `vi.mock('node:fs/promises')`
|
|
24
|
+
|
|
25
|
+
### 0.2 Guiding Constraints
|
|
26
|
+
|
|
27
|
+
- **No tool contract changes.** Input/output schemas for all 33 MCP tools remain unchanged.
|
|
28
|
+
- **No index/state schema changes.** On-disk artifact formats stay compatible.
|
|
29
|
+
- **No behavioral changes.** Parallelizing reads must not alter ordering of writes or state transitions.
|
|
30
|
+
- **All existing tests must remain green** after each task.
|
|
31
|
+
- **Each task is independently mergeable** — do not batch unrelated changes.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 1. Objectives
|
|
36
|
+
|
|
37
|
+
### 1.1 Must-Have Outcomes
|
|
38
|
+
|
|
39
|
+
- Reduce per-orchestration-iteration latency by eliminating unnecessary sequential I/O in wave executors.
|
|
40
|
+
- Eliminate the duplicate context fetch on gate failure in `BuildWaveExecutor`.
|
|
41
|
+
- Bound evidence directory growth with a configurable retention policy.
|
|
42
|
+
- Reduce agent context window token usage by 40–60% through role-specific projections and pre-filtering.
|
|
43
|
+
- Evict dead entries from `RunCoordinator.statusCache` to prevent slow memory growth over long runs.
|
|
44
|
+
- Replace the `JSON.stringify` diff in `readIndex` with a cheap version guard.
|
|
45
|
+
|
|
46
|
+
### 1.2 Non-Goals (Patterns Examined and Rejected)
|
|
47
|
+
|
|
48
|
+
The following patterns were reviewed but are **not considered sub-optimal** given current constraints:
|
|
49
|
+
|
|
50
|
+
- **Schema validation on every state write** (`kernel.ts`): Correctness is the priority; AJV-compiled validators are fast after first compilation. Sampling or skipping would risk silent schema drift.
|
|
51
|
+
- **AJV lazy compilation** (`schemas.ts`): First-call compilation is a one-time cost per schema type; the cache works correctly afterwards, and eager preloading adds startup latency without runtime benefit.
|
|
52
|
+
- **Session activation polling loop** (`kernel.ts`): Fixed 250ms polling bounded by a 5s timeout is an internal synchronization mechanism on a cold path, not a hot loop.
|
|
53
|
+
- **Object spread in `makeDefaultState`** (`kernel.ts`): Feature initialization is a cold path; spread of small constant-size default objects is negligible.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 2. Problem Analysis
|
|
58
|
+
|
|
59
|
+
Findings are organized by category. Numbers assume 10 active features over 3 orchestration iterations unless otherwise noted. Each finding cross-references its implementation task in Section 3.
|
|
60
|
+
|
|
61
|
+
### 2.1 Runtime / I/O Hotspots
|
|
62
|
+
|
|
63
|
+
#### Finding I-1 — `featureGetContext`: 5 sequential disk reads per call (HIGH) → PER-T-001
|
|
64
|
+
|
|
65
|
+
**File:** `apps/control-plane/src/application/services/feature-lifecycle-service.ts:142–146`
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
const state = await this.port.featureStateGet(featureId); // read 1
|
|
69
|
+
const plan = await this.port.planGet(featureId); // read 2
|
|
70
|
+
const qaIndex = await this.port.qaTestIndexGet(featureId); // read 3
|
|
71
|
+
const evidence = await this.port.evidenceLatest(featureId); // read 4
|
|
72
|
+
const specText = await fs.readFile(this.port.specPath(...)); // read 5
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
All five reads are sequential `await`s with zero data dependencies on each other. Under MCP transport every read incurs a full round-trip. This function is called at least once per feature per wave by all three wave executors, plus a second time in `BuildWaveExecutor` on gate failure.
|
|
76
|
+
|
|
77
|
+
**Quantified impact:** 10 features × 3 waves × 5 reads = 150 serial I/O calls per iteration that could be reduced to 30 parallel batches.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
#### Finding I-2 — `BuildWaveExecutor`: duplicate full context fetch on gate failure (HIGH) → PER-T-003
|
|
82
|
+
|
|
83
|
+
**File:** `apps/control-plane/src/supervisor/build-wave-executor.ts:42` and `95`
|
|
84
|
+
|
|
85
|
+
The context bundle fetched before the initial `workerDecisionRunner.execute` call (line 42) remains valid through the gate failure event. A second identical `FEATURE_GET_CONTEXT` call at line 95 re-fetches the same 50KB+ bundle unconditionally before entering the repair retry loop.
|
|
86
|
+
|
|
87
|
+
**Quantified impact:** With 5 retry attempts per failing feature × 50 KB context = 250 KB of redundant I/O per failing feature per wave.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
#### Finding I-3 — Build + QA wave executors: sequential state-filter loops (HIGH) → PER-T-002
|
|
92
|
+
|
|
93
|
+
**Files:** `apps/control-plane/src/supervisor/build-wave-executor.ts:30–38`, `qa-wave-executor.ts:54–62`
|
|
94
|
+
|
|
95
|
+
Both executors loop sequentially over all active features to filter by status before any gate work begins. With 20 active features this is 20 serial `FEATURE_STATE_GET` tool calls just to produce a smaller subset.
|
|
96
|
+
|
|
97
|
+
**Quantified impact:** 20 features × 2 wave executors = 40 serial tool calls per iteration that could be 2 parallel batches.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
#### Finding I-4 — `reportDashboard`: sequential state + cost reads per feature (MEDIUM) → PER-T-004
|
|
102
|
+
|
|
103
|
+
**File:** `apps/control-plane/src/application/services/reporting-service.ts:81–108`
|
|
104
|
+
|
|
105
|
+
The dashboard loop reads `state.md` and the cost JSON file for each feature one after another. Called once per orchestration cycle via `TOOLS.REPORT_DASHBOARD` and also from the CLI `status` command.
|
|
106
|
+
|
|
107
|
+
**Quantified impact:** 50 features × 2 reads = 100 serial file reads per `reportDashboard` call.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
#### Finding I-5 — `evidenceLatest`: O(N) stat scan to find newest file (MEDIUM) → PER-T-005
|
|
112
|
+
|
|
113
|
+
**File:** `apps/control-plane/src/application/services/gate-service.ts:236–256`
|
|
114
|
+
|
|
115
|
+
On every `featureGetContext` call, `evidenceLatest` lists the entire evidence directory, calls `fs.stat()` on every `.json` file to read mtimes, sorts by mtime, then reads the newest file. The evidence directory grows by 1–3 files per gate run (one per profile per retry).
|
|
116
|
+
|
|
117
|
+
Critically, `gates.ts:430` already writes a `latest.json` sentinel on every gate run. `evidenceLatest` ignores it entirely and rediscovers the newest file the hard way every call.
|
|
118
|
+
|
|
119
|
+
**Quantified impact:** 50 retries × 3 profiles = 150 evidence files per feature × 10 features = 1,500 `stat()` calls per context fetch, all avoidable by reading `latest.json` directly.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
#### Finding I-6 — `readIndex`: `JSON.stringify` comparison on every call (MEDIUM) → PER-T-011
|
|
124
|
+
|
|
125
|
+
**File:** `apps/control-plane/src/core/kernel.ts:761`
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const changed = JSON.stringify(existing ?? null) !== JSON.stringify(normalized);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`readIndex` is called on every orchestration cycle and every state transition. Serializing a 20–50 KB index object twice on every call is wasted CPU — schema migration (`changed === true`) only occurs after a code change that alters `normalizeIndexShape`.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
#### Finding I-7 — `collisionsScan`: O(n²) plan comparisons with `JSON.stringify` in inner loop (MEDIUM) — Deferred
|
|
136
|
+
|
|
137
|
+
**File:** `apps/control-plane/src/application/services/reporting-service.ts:40–68`
|
|
138
|
+
|
|
139
|
+
`collisionsScan` reads every feature's `plan.json` (N disk reads via `collectAcceptedPlans`) then compares every pair (N²/2 `detectPlanCollisions` calls). `createCollisionFingerprint` runs `JSON.stringify(collisions)` inside the inner loop. For 100 features: 100 file reads + 4,950 comparisons.
|
|
140
|
+
|
|
141
|
+
**Status:** Deferred from this spec. Preferred fix is event-driven: compute the collision matrix at plan-submission time and cache it, rather than recomputing on every dashboard poll. Requires a design change to the plan-submission flow.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
#### Finding I-8 — `QaWaveExecutor`: `loadRolePrompts` called inside per-feature loop (LOW) → PER-T-014
|
|
146
|
+
|
|
147
|
+
**File:** `apps/control-plane/src/supervisor/qa-wave-executor.ts:196`
|
|
148
|
+
|
|
149
|
+
`loadRolePrompts` is called once per feature inside the QA batch loop. `PromptBundleLoader` caches after the first load so subsequent calls are cheap — but the placement inside the loop is structurally fragile. If the cache is externally invalidated, each iteration independently hits the filesystem, and the regression is invisible.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
### 2.2 Memory / Allocation Hotspots
|
|
154
|
+
|
|
155
|
+
#### Finding M-1 — `normalizeSet` copy-pasted in 4 files (LOW) → PER-T-013
|
|
156
|
+
|
|
157
|
+
**Files:** `reporting-service.ts:7–9`, `merge-service.ts:17–19`, `feature-lifecycle-service.ts:15–17`, `feature-deletion-service.ts:67–69`
|
|
158
|
+
|
|
159
|
+
The same 3-line function (deduplicate + sort string array) is independently defined in four service files. Four copies that must be kept in sync; any change to deduplication or sort semantics must be applied in all four places.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
#### Finding M-2 — `structuredClone(plan)` for a 5-field mutation (LOW) → PER-T-012
|
|
164
|
+
|
|
165
|
+
**File:** `apps/control-plane/src/supervisor/planning-wave-executor.ts:298`
|
|
166
|
+
|
|
167
|
+
`buildUpdatedPlan` uses `structuredClone` to deep-copy the entire plan object, then immediately overwrites 5 top-level fields. Since those fields are scalars or wholly replaced arrays (not mutated in-place), a shallow object spread produces identical semantics at a fraction of the cost.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
#### Finding M-3 — `statusCache` entries never evicted for terminal features (MEDIUM) → PER-T-010
|
|
172
|
+
|
|
173
|
+
**File:** `apps/control-plane/src/supervisor/run-coordinator.ts:61–62, 272`
|
|
174
|
+
|
|
175
|
+
`statusCache` accumulates one entry per feature that ever transitions state. When `closeFeatureCluster` is called for a terminal feature (`MERGED`, `FAILED`, `PAUSED_BUDGET`), the Map entry is not deleted. Over a long run processing hundreds of features, the Map holds dead entries indefinitely.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
#### Finding M-4 — Evidence directory grows unbounded across gate retry cycles (HIGH) → PER-T-006
|
|
180
|
+
|
|
181
|
+
**File:** `apps/control-plane/src/core/gates.ts:428–430`
|
|
182
|
+
|
|
183
|
+
Every gate run appends a new timestamped JSON file to `.aop/features/<id>/evidence/`. With 5 retry cycles × 3 gate profiles per feature the directory accumulates 15+ files per feature with no pruning. This directly worsens Finding I-5: the `stat()` scan cost scales linearly with the number of accumulated files.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### 2.3 Agent Context Size
|
|
188
|
+
|
|
189
|
+
#### Finding C-1 — Full QA index + full evidence + full state delivered to every agent unconditionally (HIGH) → PER-T-008, PER-T-009
|
|
190
|
+
|
|
191
|
+
**File:** `apps/control-plane/src/application/services/feature-lifecycle-service.ts:132–158`
|
|
192
|
+
|
|
193
|
+
The context bundle returned to every agent role includes the complete QA test index (all test records, including passed), the full gate evidence JSON (verbose stdout/stderr), and the complete state frontmatter (all historical lock, gate, and PR metadata). A feature with 200 tests and 50 gate retry cycles produces a bundle exceeding 80–100 KB — roughly 20,000–25,000 tokens per agent invocation.
|
|
194
|
+
|
|
195
|
+
Relevant bloat by field:
|
|
196
|
+
|
|
197
|
+
| Field | Problem |
|
|
198
|
+
|-------|---------|
|
|
199
|
+
| `qa_test_index` | All test records including passed — agents only need `summary` + `failed` + `pending` |
|
|
200
|
+
| `latest_evidence` | Full verbose gate output — agents need overall result + top failing steps |
|
|
201
|
+
| `state.front_matter` | All historical locks/gates/PR metadata — agents need current status + held locks |
|
|
202
|
+
| `plan` | Full plan when builder/QA roles only need current-phase tasks |
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
#### Finding C-2 — `PlanningWaveExecutor` fetches full context for non-planning features (MEDIUM) → PER-T-007
|
|
207
|
+
|
|
208
|
+
**File:** `apps/control-plane/src/supervisor/planning-wave-executor.ts:125–133`
|
|
209
|
+
|
|
210
|
+
`run()` fetches the full 50KB context bundle for every active feature, then immediately discards it if status is not `PLANNING` or `BLOCKED`. For 10 active features where 8 are in `BUILDING` status, 8 full context bundles (400 KB total) are fetched and thrown away on each planning wave.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
#### Finding C-3 — Decision log entries embed raw `JSON.stringify` of complex objects (LOW) — Deferred
|
|
215
|
+
|
|
216
|
+
**File:** `apps/control-plane/src/supervisor/planning-wave-executor.ts:313`
|
|
217
|
+
|
|
218
|
+
`appendDecisionLog` serializes an `AnyRecord` containing `qa_summary`, `edge_case_checklist`, `reasons`, and `latest_gate_overall` as a raw JSON string in the decisions log. These accumulate linearly with iteration count. If decisions logs are surfaced to agents in future context bundles, this becomes a context-size problem.
|
|
219
|
+
|
|
220
|
+
**Status:** Deferred. No agents currently consume decisions logs directly; addressing this requires a typed `DecisionLogEntry` interface and a schema for the log format, which is out of scope for this spec.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
#### Finding C-4 — `loadRolePrompts` call placement risks sending prompts redundantly (LOW) → PER-T-014
|
|
225
|
+
|
|
226
|
+
See Finding I-8. The structural fix (hoist above the loop) also ensures prompts are loaded once and passed at session creation time rather than risking re-loading mid-batch.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### 2.4 Complete Findings Table
|
|
231
|
+
|
|
232
|
+
| ID | File | Lines | Severity | Category | Task |
|
|
233
|
+
|----|------|-------|----------|----------|------|
|
|
234
|
+
| I-1 | `feature-lifecycle-service.ts` | 142–146 | HIGH | I/O | PER-T-001 |
|
|
235
|
+
| I-2 | `build-wave-executor.ts` | 42, 95 | HIGH | I/O | PER-T-003 |
|
|
236
|
+
| I-3 | `build-wave-executor.ts`, `qa-wave-executor.ts` | 30–38, 54–62 | HIGH | I/O | PER-T-002 |
|
|
237
|
+
| I-4 | `reporting-service.ts` | 81–108 | MEDIUM | I/O | PER-T-004 |
|
|
238
|
+
| I-5 | `gate-service.ts` | 236–256 | MEDIUM | I/O | PER-T-005 |
|
|
239
|
+
| I-6 | `kernel.ts` | 761 | MEDIUM | CPU | PER-T-011 |
|
|
240
|
+
| I-7 | `reporting-service.ts` | 40–68 | MEDIUM | CPU/I/O | Deferred |
|
|
241
|
+
| I-8 | `qa-wave-executor.ts` | 196 | LOW | I/O | PER-T-014 |
|
|
242
|
+
| M-1 | 4 service files | various | LOW | Memory | PER-T-013 |
|
|
243
|
+
| M-2 | `planning-wave-executor.ts` | 298 | LOW | Memory | PER-T-012 |
|
|
244
|
+
| M-3 | `run-coordinator.ts` | 61–62, 272 | MEDIUM | Memory | PER-T-010 |
|
|
245
|
+
| M-4 | `core/gates.ts` | 428–430 | HIGH | Disk | PER-T-006 |
|
|
246
|
+
| C-1 | `feature-lifecycle-service.ts` | 132–158 | HIGH | Context | PER-T-008, PER-T-009 |
|
|
247
|
+
| C-2 | `planning-wave-executor.ts` | 125–133 | MEDIUM | Context | PER-T-007 |
|
|
248
|
+
| C-3 | `planning-wave-executor.ts` | 313 | LOW | Context | Deferred |
|
|
249
|
+
| C-4 | `qa-wave-executor.ts`, `prompt-bundle-loader.ts` | 196, 15–52 | LOW | Context | PER-T-014 |
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 3. Milestone Plan
|
|
254
|
+
|
|
255
|
+
### PER-M1: Parallel I/O in Wave Executors (Highest Impact)
|
|
256
|
+
|
|
257
|
+
**Goal:** Eliminate sequential awaits where there are no data dependencies. No behavioral changes — only I/O ordering changes; write ordering is unaffected.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
#### PER-T-001: Parallelize the 5 sequential reads in `featureGetContext`
|
|
262
|
+
|
|
263
|
+
**Fixes:** Finding I-1
|
|
264
|
+
**File:** `apps/control-plane/src/application/services/feature-lifecycle-service.ts`
|
|
265
|
+
**Lines:** 142–146
|
|
266
|
+
|
|
267
|
+
**Before:**
|
|
268
|
+
```typescript
|
|
269
|
+
const state = await this.port.featureStateGet(featureId);
|
|
270
|
+
const plan = await this.port.planGet(featureId);
|
|
271
|
+
const qaIndex = await this.port.qaTestIndexGet(featureId);
|
|
272
|
+
const evidence = await this.port.evidenceLatest(featureId);
|
|
273
|
+
const specText = await fs.readFile(this.port.specPath(featureId), 'utf8').catch(() => '');
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**After:**
|
|
277
|
+
```typescript
|
|
278
|
+
const [state, plan, qaIndex, evidence, specText] = await Promise.all([
|
|
279
|
+
this.port.featureStateGet(featureId),
|
|
280
|
+
this.port.planGet(featureId),
|
|
281
|
+
this.port.qaTestIndexGet(featureId),
|
|
282
|
+
this.port.evidenceLatest(featureId),
|
|
283
|
+
fs.readFile(this.port.specPath(featureId), 'utf8').catch(() => '')
|
|
284
|
+
]);
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Tests to write:** `apps/control-plane/test/feature-lifecycle-service.spec.ts`
|
|
288
|
+
- `GIVEN_featureGetContext_WHEN_called_THEN_all_reads_are_parallel` — assert each port method is called exactly once and all are called before any result is consumed.
|
|
289
|
+
|
|
290
|
+
**Acceptance criteria:**
|
|
291
|
+
1. `featureGetContext` returns identical output before and after.
|
|
292
|
+
2. All five port method spies are called; the individual calls are non-ordered.
|
|
293
|
+
3. A single I/O error in any one read propagates as a rejected promise (existing behavior).
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
#### PER-T-002: Parallelize the status-filter loops in `BuildWaveExecutor` and `QaWaveExecutor`
|
|
298
|
+
|
|
299
|
+
**Fixes:** Finding I-3
|
|
300
|
+
**Files:**
|
|
301
|
+
- `apps/control-plane/src/supervisor/build-wave-executor.ts` (lines 30–38)
|
|
302
|
+
- `apps/control-plane/src/supervisor/qa-wave-executor.ts` (lines 54–62)
|
|
303
|
+
|
|
304
|
+
**Before (BuildWaveExecutor):**
|
|
305
|
+
```typescript
|
|
306
|
+
const batch: string[] = [];
|
|
307
|
+
for (const featureId of featureIds) {
|
|
308
|
+
const state = await this.toolCaller.callTool<FeatureStatePayload>('builder', TOOLS.FEATURE_STATE_GET, {
|
|
309
|
+
feature_id: featureId
|
|
310
|
+
});
|
|
311
|
+
if (state.data.front_matter.status === STATUS.BUILDING) {
|
|
312
|
+
batch.push(featureId);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**After (BuildWaveExecutor):**
|
|
318
|
+
```typescript
|
|
319
|
+
const states = await Promise.all(
|
|
320
|
+
featureIds.map((featureId) =>
|
|
321
|
+
this.toolCaller.callTool<FeatureStatePayload>('builder', TOOLS.FEATURE_STATE_GET, {
|
|
322
|
+
feature_id: featureId
|
|
323
|
+
})
|
|
324
|
+
)
|
|
325
|
+
);
|
|
326
|
+
const batch = featureIds.filter((_, i) => states[i].data.front_matter.status === STATUS.BUILDING);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Apply the same transformation to `QaWaveExecutor` substituting role `'qa'` and status `STATUS.QA`.
|
|
330
|
+
|
|
331
|
+
**Tests to write:** `apps/control-plane/test/batch-operations.spec.ts` (existing file — add cases)
|
|
332
|
+
- `GIVEN_BuildWaveExecutor_run_WHEN_multiple_features_THEN_state_reads_are_parallel`
|
|
333
|
+
- `GIVEN_QaWaveExecutor_run_WHEN_multiple_features_THEN_state_reads_are_parallel`
|
|
334
|
+
|
|
335
|
+
**Acceptance criteria:**
|
|
336
|
+
1. Identical `batch` array produced before and after (same filter logic, same contents).
|
|
337
|
+
2. All `FEATURE_STATE_GET` calls are issued concurrently (spy call ordering is non-sequential).
|
|
338
|
+
3. `selected` slice behavior (`batch.slice(0, maxParallelGateRuns)`) is unchanged.
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
#### PER-T-003: Eliminate duplicate `FEATURE_GET_CONTEXT` fetch in `BuildWaveExecutor` retry path
|
|
343
|
+
|
|
344
|
+
**Fixes:** Finding I-2
|
|
345
|
+
**File:** `apps/control-plane/src/supervisor/build-wave-executor.ts`
|
|
346
|
+
**Lines:** 40–52 (initial fetch), 94–97 (duplicate fetch)
|
|
347
|
+
|
|
348
|
+
The `context` variable captured at line 42 is valid through the gate failure. Re-fetching it at line 95 duplicates the entire I/O bundle for no benefit.
|
|
349
|
+
|
|
350
|
+
**Before:**
|
|
351
|
+
```typescript
|
|
352
|
+
// Line 41–52: first fetch used for initial workerDecisionRunner call
|
|
353
|
+
for (const featureId of selected) {
|
|
354
|
+
const context = await this.toolCaller.callTool('builder', TOOLS.FEATURE_GET_CONTEXT, {
|
|
355
|
+
feature_id: featureId
|
|
356
|
+
});
|
|
357
|
+
await this.workerDecisionRunner.execute({ ..., contextBundle: context.data, ... });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Line 54+: separate Promise.all map — context is re-fetched inside
|
|
361
|
+
const executing = selected.map(async (featureId) => {
|
|
362
|
+
...
|
|
363
|
+
if (this.reactionsService && gateOverall === GATE_RESULT.FAIL) {
|
|
364
|
+
const context = await this.toolCaller.callTool('builder', TOOLS.FEATURE_GET_CONTEXT, { // DUPLICATE
|
|
365
|
+
feature_id: featureId
|
|
366
|
+
});
|
|
367
|
+
...
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**After:** Hoist the context fetch into the `executing` map so a single variable serves both the initial decision loop and the retry loop. The sequential `for` loop over `selected` (lines 41–52) is eliminated entirely.
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
const executing = selected.map(async (featureId) => {
|
|
376
|
+
const context = await this.toolCaller.callTool('builder', TOOLS.FEATURE_GET_CONTEXT, {
|
|
377
|
+
feature_id: featureId
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
await this.workerDecisionRunner.execute({
|
|
381
|
+
role: 'builder',
|
|
382
|
+
featureId,
|
|
383
|
+
contextBundle: context.data,
|
|
384
|
+
instructions: '...'
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const stateForRetry = await this.toolCaller.callTool<FeatureStatePayload>('builder', TOOLS.FEATURE_STATE_GET, {
|
|
388
|
+
feature_id: featureId
|
|
389
|
+
});
|
|
390
|
+
// ... retry loop reuses context.data from above
|
|
391
|
+
});
|
|
392
|
+
await Promise.allSettled(executing);
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Tests to write:** `apps/control-plane/test/batch-operations.spec.ts`
|
|
396
|
+
- `GIVEN_BuildWaveExecutor_gate_fails_WHEN_retry_loop_runs_THEN_FEATURE_GET_CONTEXT_called_once_per_feature`
|
|
397
|
+
|
|
398
|
+
**Acceptance criteria:**
|
|
399
|
+
1. `FEATURE_GET_CONTEXT` is called exactly once per feature per `run()` invocation regardless of gate outcome.
|
|
400
|
+
2. The same `context.data` is used for both the initial decision loop and the repair retry loop.
|
|
401
|
+
3. Gate retry behavior (shouldRetry, recordRetry, escalate) is identical.
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
#### PER-T-004: Parallelize state and cost reads in `reportDashboard`
|
|
406
|
+
|
|
407
|
+
**Fixes:** Finding I-4
|
|
408
|
+
**File:** `apps/control-plane/src/application/services/reporting-service.ts`
|
|
409
|
+
**Lines:** 81–108
|
|
410
|
+
|
|
411
|
+
**Before:**
|
|
412
|
+
```typescript
|
|
413
|
+
const features = [];
|
|
414
|
+
for (const featureId of featureIds) {
|
|
415
|
+
const statePath = this.port.statePath(featureId);
|
|
416
|
+
if (!(await pathExists(statePath))) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const state = await this.port.readState(featureId);
|
|
420
|
+
const costData = await readJson<...>(this.port.featureCostPath(featureId), null);
|
|
421
|
+
features.push({ ... });
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**After:**
|
|
426
|
+
```typescript
|
|
427
|
+
const features = (
|
|
428
|
+
await Promise.all(
|
|
429
|
+
featureIds.map(async (featureId) => {
|
|
430
|
+
const statePath = this.port.statePath(featureId);
|
|
431
|
+
if (!(await pathExists(statePath))) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
const [state, costData] = await Promise.all([
|
|
435
|
+
this.port.readState(featureId),
|
|
436
|
+
readJson<{ estimated_cost_usd: number; tokens_used: number }>(this.port.featureCostPath(featureId), null)
|
|
437
|
+
]);
|
|
438
|
+
return {
|
|
439
|
+
feature_id: featureId,
|
|
440
|
+
status: state.frontMatter.status,
|
|
441
|
+
branch: typeof state.frontMatter.worktree_branch === 'string'
|
|
442
|
+
? state.frontMatter.worktree_branch
|
|
443
|
+
: typeof state.frontMatter.branch === 'string'
|
|
444
|
+
? state.frontMatter.branch
|
|
445
|
+
: null,
|
|
446
|
+
locks: readHeldLocks(state.frontMatter),
|
|
447
|
+
gate_profile: state.frontMatter.gate_profile,
|
|
448
|
+
gates: state.frontMatter.gates,
|
|
449
|
+
pr: state.frontMatter.pr ?? null,
|
|
450
|
+
last_updated: state.frontMatter.last_updated,
|
|
451
|
+
activity_state: state.frontMatter.activity_state,
|
|
452
|
+
activity_last_event_at: state.frontMatter.activity_last_event_at,
|
|
453
|
+
activity_detected_via: state.frontMatter.activity_detected_via,
|
|
454
|
+
cost: costData
|
|
455
|
+
? { estimated_cost_usd: costData.estimated_cost_usd, tokens_used: costData.tokens_used }
|
|
456
|
+
: null
|
|
457
|
+
};
|
|
458
|
+
})
|
|
459
|
+
)
|
|
460
|
+
).filter((entry): entry is NonNullable<typeof entry> => entry !== null);
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
**Tests to write:** `apps/control-plane/test/services.spec.ts` (existing) or new `reporting-service.spec.ts`
|
|
464
|
+
- `GIVEN_reportDashboard_WHEN_multiple_features_exist_THEN_reads_are_parallel`
|
|
465
|
+
- `GIVEN_reportDashboard_WHEN_state_file_missing_THEN_feature_is_omitted`
|
|
466
|
+
|
|
467
|
+
**Acceptance criteria:**
|
|
468
|
+
1. Returned `features` array contains identical entries in the same order as before.
|
|
469
|
+
2. Features without a state file are omitted from output (unchanged behavior).
|
|
470
|
+
3. `readState` and `readJson` (cost) are called in parallel per feature.
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
### PER-M2: Evidence Directory Efficiency
|
|
475
|
+
|
|
476
|
+
**Goal:** Stop scanning the full evidence directory on every context fetch. Read the `latest.json` sentinel that `gates.ts` already writes. Add a retention policy so the directory stays bounded.
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
#### PER-T-005: Read `latest.json` directly in `evidenceLatest` instead of scanning the directory
|
|
481
|
+
|
|
482
|
+
**Fixes:** Finding I-5
|
|
483
|
+
**File:** `apps/control-plane/src/application/services/gate-service.ts`
|
|
484
|
+
**Lines:** 224–266
|
|
485
|
+
|
|
486
|
+
`gates.ts` already writes two files on every gate run:
|
|
487
|
+
- `evidence/<timestamp>-<profile>.json` — the timestamped archive record
|
|
488
|
+
- `latest.json` — always the most recent result
|
|
489
|
+
|
|
490
|
+
`evidenceLatest` ignores `latest.json` and instead lists the entire directory, stats every file, sorts by mtime, and reads the newest. This is O(N) in the number of historical evidence files.
|
|
491
|
+
|
|
492
|
+
**Before:**
|
|
493
|
+
```typescript
|
|
494
|
+
const files = (await fs.readdir(evidenceDir))
|
|
495
|
+
.filter((file) => file.endsWith('.json'))
|
|
496
|
+
.map((file) => path.join(evidenceDir, file));
|
|
497
|
+
|
|
498
|
+
if (files.length === 0) { return { data: { feature_id: featureId, latest: null } }; }
|
|
499
|
+
|
|
500
|
+
const withStats = await Promise.all(
|
|
501
|
+
files.map(async (file) => {
|
|
502
|
+
const stat = await fs.stat(file);
|
|
503
|
+
return { file, mtimeMs: stat.mtimeMs };
|
|
504
|
+
})
|
|
505
|
+
);
|
|
506
|
+
withStats.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
507
|
+
const latestFile = withStats[0].file;
|
|
508
|
+
const latest = JSON.parse(await fs.readFile(latestFile, 'utf8'));
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**After:**
|
|
512
|
+
```typescript
|
|
513
|
+
const latestPath = path.join(evidenceDir, 'latest.json');
|
|
514
|
+
if (!(await pathExists(latestPath))) {
|
|
515
|
+
return { data: { feature_id: featureId, latest: null } };
|
|
516
|
+
}
|
|
517
|
+
const latest = await readJson<AnyRecord>(latestPath, null);
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
The method drops from O(N) stat calls to O(1). Add a port method `evidenceLatestPath(featureId: string): string` to `GateServicePort` if not already present.
|
|
521
|
+
|
|
522
|
+
**Tests to write:** `apps/control-plane/test/gate-service.spec.ts` (create if not existing)
|
|
523
|
+
- `GIVEN_evidenceLatest_WHEN_latest_json_exists_THEN_returns_its_contents`
|
|
524
|
+
- `GIVEN_evidenceLatest_WHEN_no_evidence_dir_exists_THEN_returns_null`
|
|
525
|
+
- `GIVEN_evidenceLatest_WHEN_latest_json_missing_THEN_returns_null`
|
|
526
|
+
|
|
527
|
+
**Acceptance criteria:**
|
|
528
|
+
1. `evidenceLatest` reads only `latest.json`; no `readdir` or `stat` calls.
|
|
529
|
+
2. Return shape `{ data: { feature_id, latest, path? } }` is identical to the existing interface.
|
|
530
|
+
3. Existing tests that rely on `latest` content remain green.
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
#### PER-T-006: Add configurable evidence retention policy
|
|
535
|
+
|
|
536
|
+
**Fixes:** Finding M-4
|
|
537
|
+
**Files:**
|
|
538
|
+
- `agentic/orchestrator/policy.yaml` — add `evidence_retention_count` field
|
|
539
|
+
- `agentic/orchestrator/schemas/policy.schema.json` — add field to schema
|
|
540
|
+
- `apps/control-plane/src/core/gates.ts` — prune after writing new evidence file
|
|
541
|
+
|
|
542
|
+
**Policy change (`policy.yaml`):**
|
|
543
|
+
```yaml
|
|
544
|
+
cleanup:
|
|
545
|
+
grace_period_seconds: 300
|
|
546
|
+
auto_after_merge: true
|
|
547
|
+
evidence_retention_count: 10 # NEW: keep the N most recent evidence files per feature
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**Schema change (`schemas/policy.schema.json`):** Add to the `cleanup` object properties:
|
|
551
|
+
```json
|
|
552
|
+
"evidence_retention_count": {
|
|
553
|
+
"type": "integer",
|
|
554
|
+
"minimum": 1,
|
|
555
|
+
"maximum": 100,
|
|
556
|
+
"default": 10,
|
|
557
|
+
"description": "Maximum number of timestamped evidence files to retain per feature. Oldest are pruned after each gate run."
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
**Implementation in `gates.ts`** — after writing the new evidence file (currently line 428), add pruning:
|
|
562
|
+
```typescript
|
|
563
|
+
await pruneEvidenceFiles(evidenceDir, retentionCount);
|
|
564
|
+
|
|
565
|
+
async function pruneEvidenceFiles(dir: string, keep: number): Promise<void> {
|
|
566
|
+
const entries = await fs.readdir(dir);
|
|
567
|
+
// Only prune timestamped files; never prune latest.json
|
|
568
|
+
const archived = entries
|
|
569
|
+
.filter((f) => f.endsWith('.json') && f !== 'latest.json')
|
|
570
|
+
.map((f) => ({ name: f, path: path.join(dir, f) }));
|
|
571
|
+
|
|
572
|
+
if (archived.length <= keep) return;
|
|
573
|
+
|
|
574
|
+
// Sort by name ascending — timestamp-prefixed filenames sort chronologically
|
|
575
|
+
archived.sort((a, b) => a.name.localeCompare(b.name));
|
|
576
|
+
const toDelete = archived.slice(0, archived.length - keep);
|
|
577
|
+
await Promise.all(toDelete.map(({ path: p }) => fs.unlink(p).catch(() => undefined)));
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
The `retentionCount` value is read from the policy snapshot, defaulting to `10` if absent.
|
|
582
|
+
|
|
583
|
+
**Tests to write:** `apps/control-plane/test/incremental-gates.spec.ts` (existing) — add cases:
|
|
584
|
+
- `GIVEN_pruneEvidenceFiles_WHEN_archived_count_exceeds_retention_THEN_oldest_are_deleted`
|
|
585
|
+
- `GIVEN_pruneEvidenceFiles_WHEN_archived_count_within_retention_THEN_nothing_deleted`
|
|
586
|
+
- `GIVEN_pruneEvidenceFiles_WHEN_latest_json_present_THEN_it_is_never_pruned`
|
|
587
|
+
|
|
588
|
+
**Acceptance criteria:**
|
|
589
|
+
1. After every gate run, the evidence directory contains at most `evidence_retention_count` timestamped files.
|
|
590
|
+
2. `latest.json` is never deleted by pruning.
|
|
591
|
+
3. Pruning occurs only after the new evidence file is successfully written.
|
|
592
|
+
4. Default `evidence_retention_count` of 10 applies when the field is absent from `policy.yaml`.
|
|
593
|
+
5. Policy schema validates correctly; invalid values (negative, >100) are rejected.
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
### PER-M3: Agent Context Slimming
|
|
598
|
+
|
|
599
|
+
**Goal:** Reduce the token footprint of context bundles delivered to agents. The biggest savings come from role-specific projections and pre-filtering non-relevant features before fetching full context.
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
#### PER-T-007: Pre-filter non-planning features in `PlanningWaveExecutor` before fetching full context
|
|
604
|
+
|
|
605
|
+
**Fixes:** Finding C-2
|
|
606
|
+
**File:** `apps/control-plane/src/supervisor/planning-wave-executor.ts`
|
|
607
|
+
**Lines:** 124–153 (`run`) and 156–228 (`runPostQaReconciliation`)
|
|
608
|
+
|
|
609
|
+
`run()` calls `FEATURE_GET_CONTEXT` (full 50KB+ bundle) for every active feature, then immediately discards it if status is not `PLANNING` or `BLOCKED`. For 10 features where 8 are in `BUILDING`, this wastes 400 KB of I/O per planning wave.
|
|
610
|
+
|
|
611
|
+
**Before:**
|
|
612
|
+
```typescript
|
|
613
|
+
async run(featureIds: string[]): Promise<void> {
|
|
614
|
+
for (const featureId of featureIds) {
|
|
615
|
+
const context = await this.toolCaller.callTool<FeatureContextPayload>('planner', TOOLS.FEATURE_GET_CONTEXT, {
|
|
616
|
+
feature_id: featureId
|
|
617
|
+
});
|
|
618
|
+
const state = context.data.state.front_matter;
|
|
619
|
+
if (state.status !== STATUS.PLANNING && state.status !== STATUS.BLOCKED) {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
// ...use context
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**After:**
|
|
628
|
+
```typescript
|
|
629
|
+
async run(featureIds: string[]): Promise<void> {
|
|
630
|
+
// Phase 1: Batch-fetch lightweight state to identify planning features
|
|
631
|
+
const states = await Promise.all(
|
|
632
|
+
featureIds.map((featureId) =>
|
|
633
|
+
this.toolCaller.callTool<FeatureStatePayload>('planner', TOOLS.FEATURE_STATE_GET, {
|
|
634
|
+
feature_id: featureId
|
|
635
|
+
})
|
|
636
|
+
)
|
|
637
|
+
);
|
|
638
|
+
const planningFeatureIds = featureIds.filter((_, i) => {
|
|
639
|
+
const status = states[i].data.front_matter.status;
|
|
640
|
+
return status === STATUS.PLANNING || status === STATUS.BLOCKED;
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// Phase 2: Fetch full context only for features that need planning
|
|
644
|
+
for (const featureId of planningFeatureIds) {
|
|
645
|
+
const context = await this.toolCaller.callTool<FeatureContextPayload>('planner', TOOLS.FEATURE_GET_CONTEXT, {
|
|
646
|
+
feature_id: featureId
|
|
647
|
+
});
|
|
648
|
+
// ...rest unchanged
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Apply the same pre-filter pattern to `runPostQaReconciliation`, substituting `isPostQaStatus` as the predicate.
|
|
654
|
+
|
|
655
|
+
**Tests to write:** `apps/control-plane/test/planning-wave-executor.spec.ts` (existing — add cases)
|
|
656
|
+
- `GIVEN_run_WHEN_features_not_in_planning_status_THEN_FEATURE_GET_CONTEXT_not_called`
|
|
657
|
+
- `GIVEN_run_WHEN_features_in_planning_THEN_FEATURE_GET_CONTEXT_called_only_for_those`
|
|
658
|
+
- `GIVEN_runPostQaReconciliation_WHEN_features_not_in_post_qa_status_THEN_context_not_fetched`
|
|
659
|
+
|
|
660
|
+
**Acceptance criteria:**
|
|
661
|
+
1. `FEATURE_GET_CONTEXT` is never called for features not in `PLANNING`, `BLOCKED`, `QA`, or `READY_TO_MERGE` status (as applicable per method).
|
|
662
|
+
2. `FEATURE_STATE_GET` batch calls are issued in parallel.
|
|
663
|
+
3. Planning logic for features that reach the context fetch is unchanged.
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
#### PER-T-008: Introduce role-specific context projections in `featureGetContext`
|
|
668
|
+
|
|
669
|
+
**Fixes:** Finding C-1
|
|
670
|
+
**Files:**
|
|
671
|
+
- `apps/control-plane/src/application/services/feature-lifecycle-service.ts`
|
|
672
|
+
- `apps/control-plane/src/core/constants.ts` (add new tool constant)
|
|
673
|
+
- `agentic/orchestrator/tools/catalog.json` (register new tool)
|
|
674
|
+
- Tool input/output schemas under `agentic/orchestrator/tools/schemas/`
|
|
675
|
+
|
|
676
|
+
Add an optional `role` parameter to the existing `feature.get_context` tool (backward-compatible: defaults to `'full'`):
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
// Input schema addition
|
|
680
|
+
{
|
|
681
|
+
"feature_id": { "type": "string" },
|
|
682
|
+
"role": {
|
|
683
|
+
"type": "string",
|
|
684
|
+
"enum": ["full", "planner", "builder", "qa"],
|
|
685
|
+
"default": "full",
|
|
686
|
+
"description": "Returns a role-scoped projection of the context bundle to reduce token usage."
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
**Context projections by role:**
|
|
692
|
+
|
|
693
|
+
| Field | `full` | `planner` | `builder` | `qa` |
|
|
694
|
+
|-------|--------|-----------|-----------|------|
|
|
695
|
+
| `feature_id` | ✓ | ✓ | ✓ | ✓ |
|
|
696
|
+
| `spec` | ✓ | ✓ | ✓ (trimmed to 8KB) | ✓ (trimmed to 4KB) |
|
|
697
|
+
| `state.front_matter` | full | full | `status`, `branch`, `locks.held`, `gates`, `gate_profile` | `status`, `branch`, `gates`, `gate_profile` |
|
|
698
|
+
| `plan` | full | full | `tasks` (current phase only), `acceptance_criteria` | `acceptance_criteria`, `risk` |
|
|
699
|
+
| `qa_test_index` | full | `summary` only | `summary` only | `summary` + `failed` + `pending` (no `passed`) |
|
|
700
|
+
| `latest_evidence` | full | `{ overall, profile }` | `{ overall, profile, failed_steps[0..4] }` | `{ overall, profile, failed_steps[0..9], coverage }` |
|
|
701
|
+
|
|
702
|
+
**Implementation in `FeatureLifecycleService`:**
|
|
703
|
+
|
|
704
|
+
```typescript
|
|
705
|
+
async featureGetContext(featureId: string, role: 'full' | 'planner' | 'builder' | 'qa' = 'full') {
|
|
706
|
+
const [state, plan, qaIndex, evidence, specText] = await Promise.all([...]); // PER-T-001
|
|
707
|
+
|
|
708
|
+
const projected = projectContext({ state, plan, qaIndex, evidence, specText }, role);
|
|
709
|
+
return { data: { feature_id: featureId, ...projected } };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function projectContext(bundle: FullContextBundle, role: string): ProjectedBundle {
|
|
713
|
+
if (role === 'full') return bundle; // no-op for backward compatibility
|
|
714
|
+
|
|
715
|
+
const spec = trimSpec(bundle.specText, specBudgetByRole[role]);
|
|
716
|
+
const state = projectState(bundle.state, role);
|
|
717
|
+
const plan = projectPlan(bundle.plan, role);
|
|
718
|
+
const qaIndex = projectQaIndex(bundle.qaIndex, role);
|
|
719
|
+
const evidence = projectEvidence(bundle.evidence, role);
|
|
720
|
+
|
|
721
|
+
return { spec, state, plan, qa_test_index: qaIndex, latest_evidence: evidence };
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
Wave executors pass their role when calling context:
|
|
726
|
+
```typescript
|
|
727
|
+
// BuildWaveExecutor
|
|
728
|
+
const context = await this.toolCaller.callTool('builder', TOOLS.FEATURE_GET_CONTEXT, {
|
|
729
|
+
feature_id: featureId,
|
|
730
|
+
role: 'builder' // NEW
|
|
731
|
+
});
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
**Tests to write:** `apps/control-plane/test/feature-lifecycle-service.spec.ts`
|
|
735
|
+
- `GIVEN_featureGetContext_with_role_builder_WHEN_called_THEN_qa_index_is_summary_only`
|
|
736
|
+
- `GIVEN_featureGetContext_with_role_qa_WHEN_called_THEN_passed_tests_are_omitted`
|
|
737
|
+
- `GIVEN_featureGetContext_with_role_planner_WHEN_called_THEN_state_is_projected`
|
|
738
|
+
- `GIVEN_featureGetContext_with_role_full_WHEN_called_THEN_bundle_is_unchanged`
|
|
739
|
+
|
|
740
|
+
**Acceptance criteria:**
|
|
741
|
+
1. `role: 'full'` (and omitting `role`) returns the existing complete bundle — no regression for callers that do not pass a role.
|
|
742
|
+
2. `role: 'builder'` omits passed test records from `qa_test_index`; only `summary`, `failed`, and `pending` keys are present.
|
|
743
|
+
3. `role: 'qa'` omits passed test records; includes `failed_steps` in evidence (max 10).
|
|
744
|
+
4. `role: 'planner'` projects `state` to `{ status, branch, locks, gates, gate_profile }`.
|
|
745
|
+
5. Context byte size for `builder` and `qa` roles is ≤50% of `full` bundle when QA index has ≥50 test records.
|
|
746
|
+
6. Input schema is updated and validated by the MCP tool runtime.
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
#### PER-T-009: Extract and test the `projectQaIndex` projection helper
|
|
751
|
+
|
|
752
|
+
**Fixes:** Finding C-1 (supporting task for PER-T-008)
|
|
753
|
+
**File:** `apps/control-plane/src/application/services/feature-lifecycle-service.ts`
|
|
754
|
+
|
|
755
|
+
Extracted from PER-T-008 as a standalone, independently testable function:
|
|
756
|
+
|
|
757
|
+
```typescript
|
|
758
|
+
export function projectQaIndex(
|
|
759
|
+
qaIndex: QaIndexRecord,
|
|
760
|
+
role: 'full' | 'planner' | 'builder' | 'qa'
|
|
761
|
+
): Partial<QaIndexRecord> {
|
|
762
|
+
if (role === 'full') return qaIndex;
|
|
763
|
+
if (role === 'planner') return { summary: qaIndex.summary };
|
|
764
|
+
if (role === 'builder') return { summary: qaIndex.summary };
|
|
765
|
+
// 'qa': return summary + failed + pending; omit passed
|
|
766
|
+
const { passed: _omitted, ...rest } = qaIndex;
|
|
767
|
+
return rest;
|
|
768
|
+
}
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
**Tests to write:** `apps/control-plane/test/feature-lifecycle-service.spec.ts`
|
|
772
|
+
- `GIVEN_projectQaIndex_role_qa_WHEN_index_has_passed_tests_THEN_passed_is_omitted`
|
|
773
|
+
- `GIVEN_projectQaIndex_role_full_WHEN_called_THEN_index_is_unchanged`
|
|
774
|
+
- `GIVEN_projectQaIndex_role_planner_WHEN_called_THEN_only_summary_returned`
|
|
775
|
+
|
|
776
|
+
---
|
|
777
|
+
|
|
778
|
+
### PER-M4: Memory & CPU Cleanup
|
|
779
|
+
|
|
780
|
+
**Goal:** Fix the slow memory leak in `RunCoordinator`, eliminate redundant serialization in `readIndex`, replace `structuredClone` with a targeted spread, and unify the four duplicated `normalizeSet` implementations.
|
|
781
|
+
|
|
782
|
+
---
|
|
783
|
+
|
|
784
|
+
#### PER-T-010: Evict `statusCache` entries on feature termination
|
|
785
|
+
|
|
786
|
+
**Fixes:** Finding M-3
|
|
787
|
+
**File:** `apps/control-plane/src/supervisor/run-coordinator.ts`
|
|
788
|
+
**Lines:** 183–191 (`rebalanceActiveFeatures`)
|
|
789
|
+
|
|
790
|
+
When `closeFeatureCluster(featureId)` is called for a terminal-status feature, the corresponding `statusCache` entry is never deleted. Over a long run processing hundreds of features, the Map accumulates dead entries indefinitely.
|
|
791
|
+
|
|
792
|
+
**Before:**
|
|
793
|
+
```typescript
|
|
794
|
+
for (const featureId of sortedCurrent) {
|
|
795
|
+
const status = await this.readFeatureStatus(featureId);
|
|
796
|
+
if (status && RunCoordinator.TERMINAL_STATUSES.has(status)) {
|
|
797
|
+
await this.sessionOrchestrator.closeFeatureCluster(featureId);
|
|
798
|
+
continue; // cache entry remains
|
|
799
|
+
}
|
|
800
|
+
survivingActiveFeatureIds.push(featureId);
|
|
801
|
+
}
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
**After:**
|
|
805
|
+
```typescript
|
|
806
|
+
for (const featureId of sortedCurrent) {
|
|
807
|
+
const status = await this.readFeatureStatus(featureId);
|
|
808
|
+
if (status && RunCoordinator.TERMINAL_STATUSES.has(status)) {
|
|
809
|
+
await this.sessionOrchestrator.closeFeatureCluster(featureId);
|
|
810
|
+
this.statusCache.delete(featureId); // NEW
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
survivingActiveFeatureIds.push(featureId);
|
|
814
|
+
}
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
Apply the same eviction in the queue-drain loop (lines 193–207) where features popped from `this.state.queue` are found to already be terminal.
|
|
818
|
+
|
|
819
|
+
**Tests to write:** `apps/control-plane/test/run-coordinator.spec.ts` (existing — add case)
|
|
820
|
+
- `GIVEN_rebalanceActiveFeatures_WHEN_feature_reaches_terminal_status_THEN_statusCache_entry_is_evicted`
|
|
821
|
+
|
|
822
|
+
**Acceptance criteria:**
|
|
823
|
+
1. After `closeFeatureCluster` is called for a terminal feature, `statusCache.has(featureId)` returns `false`.
|
|
824
|
+
2. `notifyStatusTransitions` is unaffected — it reads `statusCache` only for currently active features.
|
|
825
|
+
3. No change to orchestration loop behavior or status transition notification logic.
|
|
826
|
+
|
|
827
|
+
---
|
|
828
|
+
|
|
829
|
+
#### PER-T-011: Replace `JSON.stringify` diff with version-based guard in `readIndex`
|
|
830
|
+
|
|
831
|
+
**Fixes:** Finding I-6
|
|
832
|
+
**File:** `apps/control-plane/src/core/kernel.ts`
|
|
833
|
+
**Lines:** 758–766
|
|
834
|
+
|
|
835
|
+
`readIndex` serializes the entire index (20–50 KB) twice on every call just to detect whether schema migration is needed — a cold-path concern paid on every hot-path read.
|
|
836
|
+
|
|
837
|
+
**Current behavior:**
|
|
838
|
+
```typescript
|
|
839
|
+
const changed = JSON.stringify(existing ?? null) !== JSON.stringify(normalized);
|
|
840
|
+
if (changed) {
|
|
841
|
+
await atomicWriteJson(this.indexPath, normalized);
|
|
842
|
+
}
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
**After:** Add a `schema_version` field to the index shape. `normalizeIndexShape` sets it to the current constant. Migration check becomes a single integer comparison:
|
|
846
|
+
|
|
847
|
+
```typescript
|
|
848
|
+
const CURRENT_INDEX_SCHEMA_VERSION = 1; // increment when normalizeIndexShape changes shape
|
|
849
|
+
|
|
850
|
+
async readIndex(): Promise<AnyRecord> {
|
|
851
|
+
const existing = await readJson(this.indexPath, null);
|
|
852
|
+
const needsMigration =
|
|
853
|
+
existing === null ||
|
|
854
|
+
(existing as AnyRecord).schema_version !== CURRENT_INDEX_SCHEMA_VERSION;
|
|
855
|
+
if (needsMigration) {
|
|
856
|
+
const normalized = this.normalizeIndexShape(existing);
|
|
857
|
+
await atomicWriteJson(this.indexPath, normalized);
|
|
858
|
+
return normalized;
|
|
859
|
+
}
|
|
860
|
+
return existing as AnyRecord;
|
|
861
|
+
}
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
`normalizeIndexShape` output gains `schema_version: CURRENT_INDEX_SCHEMA_VERSION`. The field is added to `schemas/index.schema.json` as an optional integer.
|
|
865
|
+
|
|
866
|
+
**Tests to write:** `apps/control-plane/test/kernel.spec.ts` (existing — add cases)
|
|
867
|
+
- `GIVEN_readIndex_WHEN_schema_version_matches_THEN_normalizeIndexShape_not_called`
|
|
868
|
+
- `GIVEN_readIndex_WHEN_schema_version_absent_THEN_migration_runs_and_writes_file`
|
|
869
|
+
- `GIVEN_readIndex_WHEN_index_is_null_THEN_migration_runs`
|
|
870
|
+
|
|
871
|
+
**Acceptance criteria:**
|
|
872
|
+
1. When the on-disk index has the current `schema_version`, `normalizeIndexShape` is not called and no write occurs.
|
|
873
|
+
2. When the field is absent (legacy index), normalization and re-write happen exactly once.
|
|
874
|
+
3. The `schema_version` field is included in `index.schema.json` and validates correctly.
|
|
875
|
+
4. No other fields in the index are changed by this migration.
|
|
876
|
+
|
|
877
|
+
---
|
|
878
|
+
|
|
879
|
+
#### PER-T-012: Replace `structuredClone(plan)` with targeted field spread in `buildUpdatedPlan`
|
|
880
|
+
|
|
881
|
+
**Fixes:** Finding M-2
|
|
882
|
+
**File:** `apps/control-plane/src/supervisor/planning-wave-executor.ts`
|
|
883
|
+
**Line:** 298
|
|
884
|
+
|
|
885
|
+
`structuredClone` serializes the entire plan object graph for a mutation that only touches five top-level fields. Since those fields are scalars or wholly replaced arrays, a shallow spread is sufficient and does not risk mutating the original.
|
|
886
|
+
|
|
887
|
+
**Before:**
|
|
888
|
+
```typescript
|
|
889
|
+
private buildUpdatedPlan(plan: AnyRecord, planVersion: number, decision: ReconciliationDecision): AnyRecord {
|
|
890
|
+
const nextPlan = structuredClone(plan);
|
|
891
|
+
const acceptanceCriteria = asStringArray(nextPlan.acceptance_criteria);
|
|
892
|
+
const riskItems = asStringArray(nextPlan.risk);
|
|
893
|
+
|
|
894
|
+
nextPlan.plan_version = planVersion + 1;
|
|
895
|
+
nextPlan.revision_of = planVersion;
|
|
896
|
+
nextPlan.revision_reason = decision.reasons.join('; ');
|
|
897
|
+
nextPlan.acceptance_criteria = normalizeList([...acceptanceCriteria, ...decision.acceptanceCriteriaAdditions]);
|
|
898
|
+
nextPlan.risk = normalizeList([...riskItems, ...decision.edgeCaseChecklist]);
|
|
899
|
+
return nextPlan;
|
|
900
|
+
}
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
**After:**
|
|
904
|
+
```typescript
|
|
905
|
+
private buildUpdatedPlan(plan: AnyRecord, planVersion: number, decision: ReconciliationDecision): AnyRecord {
|
|
906
|
+
return {
|
|
907
|
+
...plan,
|
|
908
|
+
plan_version: planVersion + 1,
|
|
909
|
+
revision_of: planVersion,
|
|
910
|
+
revision_reason: decision.reasons.join('; '),
|
|
911
|
+
acceptance_criteria: normalizeList([
|
|
912
|
+
...asStringArray(plan.acceptance_criteria),
|
|
913
|
+
...decision.acceptanceCriteriaAdditions
|
|
914
|
+
]),
|
|
915
|
+
risk: normalizeList([...asStringArray(plan.risk), ...decision.edgeCaseChecklist])
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
**Tests to write:** `apps/control-plane/test/planning-wave-executor.spec.ts` (existing — add cases)
|
|
921
|
+
- `GIVEN_buildUpdatedPlan_WHEN_called_THEN_original_plan_is_not_mutated`
|
|
922
|
+
- `GIVEN_buildUpdatedPlan_WHEN_called_THEN_only_mutated_fields_change`
|
|
923
|
+
|
|
924
|
+
**Acceptance criteria:**
|
|
925
|
+
1. Returned plan object has all fields from the original plan plus the five updated fields.
|
|
926
|
+
2. The original `plan` argument is not mutated.
|
|
927
|
+
3. `structuredClone` is not called.
|
|
928
|
+
4. Existing `planVersion + 1` and `normalizeList` behavior is preserved.
|
|
929
|
+
|
|
930
|
+
---
|
|
931
|
+
|
|
932
|
+
#### PER-T-013: Extract `normalizeSet` to a shared utility
|
|
933
|
+
|
|
934
|
+
**Fixes:** Finding M-1
|
|
935
|
+
**Files:**
|
|
936
|
+
- Create or update: `apps/control-plane/src/core/utils.ts`
|
|
937
|
+
- Refactor: `apps/control-plane/src/application/services/reporting-service.ts:7–9`
|
|
938
|
+
- Refactor: `apps/control-plane/src/application/services/merge-service.ts:17–19`
|
|
939
|
+
- Refactor: `apps/control-plane/src/application/services/feature-lifecycle-service.ts:15–17`
|
|
940
|
+
- Refactor: `apps/control-plane/src/application/services/feature-deletion-service.ts:67–69`
|
|
941
|
+
|
|
942
|
+
Add to `apps/control-plane/src/core/utils.ts`:
|
|
943
|
+
```typescript
|
|
944
|
+
/** Deduplicate and sort a string array. */
|
|
945
|
+
export function normalizeSet(array: string[]): string[] {
|
|
946
|
+
return [...new Set(array)].sort((a, b) => a.localeCompare(b));
|
|
947
|
+
}
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
In each of the four service files, remove the local definition and add:
|
|
951
|
+
```typescript
|
|
952
|
+
import { normalizeSet } from '../../core/utils.js';
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
**Tests to write:** `apps/control-plane/test/core-utils.spec.ts` (existing — add cases)
|
|
956
|
+
- `GIVEN_normalizeSet_WHEN_array_has_duplicates_THEN_deduplicates`
|
|
957
|
+
- `GIVEN_normalizeSet_WHEN_array_is_unsorted_THEN_sorts_lexicographically`
|
|
958
|
+
- `GIVEN_normalizeSet_WHEN_empty_array_THEN_returns_empty`
|
|
959
|
+
|
|
960
|
+
**Acceptance criteria:**
|
|
961
|
+
1. All four service files import from `../../core/utils.js`; no local `normalizeSet` definitions remain.
|
|
962
|
+
2. Behavior is identical to the four previous implementations.
|
|
963
|
+
3. No existing tests fail.
|
|
964
|
+
|
|
965
|
+
---
|
|
966
|
+
|
|
967
|
+
#### PER-T-014: Move `loadRolePrompts` outside the per-feature loop in `QaWaveExecutor`
|
|
968
|
+
|
|
969
|
+
**Fixes:** Findings I-8, C-4
|
|
970
|
+
**File:** `apps/control-plane/src/supervisor/qa-wave-executor.ts`
|
|
971
|
+
**Line:** 196
|
|
972
|
+
|
|
973
|
+
`loadRolePrompts()` is called once per feature inside the QA batch loop. The `PromptBundleLoader` cache makes subsequent calls cheap, but the placement is structurally fragile — cache invalidation silently causes per-feature file I/O.
|
|
974
|
+
|
|
975
|
+
**Before:**
|
|
976
|
+
```typescript
|
|
977
|
+
for (const featureId of batch.slice(0, maxParallelGateRuns)) {
|
|
978
|
+
// ... gate run logic ...
|
|
979
|
+
const prompts = await this.promptProvider.loadRolePrompts(); // INSIDE LOOP
|
|
980
|
+
const newQa = await this.provider.createSession('qa', featureId, prompts.qa);
|
|
981
|
+
// ...
|
|
982
|
+
}
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
**After:**
|
|
986
|
+
```typescript
|
|
987
|
+
const prompts = await this.promptProvider.loadRolePrompts(); // OUTSIDE LOOP
|
|
988
|
+
|
|
989
|
+
for (const featureId of batch.slice(0, maxParallelGateRuns)) {
|
|
990
|
+
// ... gate run logic ...
|
|
991
|
+
const newQa = await this.provider.createSession('qa', featureId, prompts.qa);
|
|
992
|
+
// ...
|
|
993
|
+
}
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
**Tests to write:** `apps/control-plane/test/session-management.spec.ts` (existing — add case)
|
|
997
|
+
- `GIVEN_QaWaveExecutor_run_WHEN_multiple_features_in_batch_THEN_loadRolePrompts_called_once`
|
|
998
|
+
|
|
999
|
+
**Acceptance criteria:**
|
|
1000
|
+
1. `loadRolePrompts` is called exactly once per `run()` invocation regardless of batch size.
|
|
1001
|
+
2. The same `prompts` object is used for all session creations in the batch.
|
|
1002
|
+
3. QA session rotation behavior is unchanged.
|
|
1003
|
+
|
|
1004
|
+
---
|
|
1005
|
+
|
|
1006
|
+
## 4. Acceptance Criteria (Spec-Level)
|
|
1007
|
+
|
|
1008
|
+
1. All 14 tasks pass their own unit tests and the full existing test suite.
|
|
1009
|
+
2. `npm run typecheck` and `npm run lint` report zero errors and zero warnings after all tasks.
|
|
1010
|
+
3. MCP contract validation (`npm run validate:mcp-contracts`) passes.
|
|
1011
|
+
4. Architecture validation (`npm run validate:architecture`) passes.
|
|
1012
|
+
5. No on-disk artifact format is changed except for the optional `schema_version` field added to the index.
|
|
1013
|
+
6. Tool input/output schemas for all existing tools are backward-compatible; `role` is an optional field with a default of `'full'`.
|
|
1014
|
+
7. The context bundle returned by `feature.get_context` with `role: 'full'` (or no role) is byte-for-byte identical to the current output.
|
|
1015
|
+
|
|
1016
|
+
---
|
|
1017
|
+
|
|
1018
|
+
## 5. Risks and Mitigations
|
|
1019
|
+
|
|
1020
|
+
| Risk | Mitigation |
|
|
1021
|
+
|------|-----------|
|
|
1022
|
+
| `Promise.all` parallelism exposes hidden race conditions in test mocks | Add ordering assertions in tests; use `vi.fn()` with explicit mock return sequences |
|
|
1023
|
+
| Context projection silently drops fields agents depend on | Gate behind `role: 'full'` default; add integration test asserting agent tools still resolve |
|
|
1024
|
+
| `schema_version` migration triggers a spurious re-write on first deploy | Acceptable one-time cost; no data loss; migration is idempotent |
|
|
1025
|
+
| `structuredClone` removal causes mutation if spread is too shallow | The five mutated fields are scalars or wholly replaced arrays; add mutation-guard test to confirm |
|
|
1026
|
+
| Evidence pruning deletes a file being read concurrently | Pruning occurs after the gate write completes; `fs.unlink` uses `.catch(() => undefined)` |
|
|
1027
|
+
| `normalizeSet` extraction breaks an unknown caller | Signature is identical; all four service files updated atomically in this task |
|
|
1028
|
+
|
|
1029
|
+
---
|
|
1030
|
+
|
|
1031
|
+
## 6. Task Backlog
|
|
1032
|
+
|
|
1033
|
+
| ID | Milestone | File(s) | Description |
|
|
1034
|
+
|----|-----------|---------|-------------|
|
|
1035
|
+
| PER-T-001 | PER-M1 | `feature-lifecycle-service.ts` | Parallelize 5 sequential reads in `featureGetContext` |
|
|
1036
|
+
| PER-T-002 | PER-M1 | `build-wave-executor.ts`, `qa-wave-executor.ts` | Parallelize status-filter loops |
|
|
1037
|
+
| PER-T-003 | PER-M1 | `build-wave-executor.ts` | Eliminate duplicate context fetch in retry path |
|
|
1038
|
+
| PER-T-004 | PER-M1 | `reporting-service.ts` | Parallelize `reportDashboard` state+cost reads |
|
|
1039
|
+
| PER-T-005 | PER-M2 | `gate-service.ts` | Read `latest.json` directly; remove directory scan |
|
|
1040
|
+
| PER-T-006 | PER-M2 | `policy.yaml`, `policy.schema.json`, `gates.ts` | Add `evidence_retention_count` and pruning logic |
|
|
1041
|
+
| PER-T-007 | PER-M3 | `planning-wave-executor.ts` | Pre-filter non-planning features before context fetch |
|
|
1042
|
+
| PER-T-008 | PER-M3 | `feature-lifecycle-service.ts`, schemas, catalog | Role-specific context projections |
|
|
1043
|
+
| PER-T-009 | PER-M3 | `feature-lifecycle-service.ts` | Extract and test `projectQaIndex` helper |
|
|
1044
|
+
| PER-T-010 | PER-M4 | `run-coordinator.ts` | Evict `statusCache` on feature termination |
|
|
1045
|
+
| PER-T-011 | PER-M4 | `kernel.ts`, `schemas/index.schema.json` | Replace `JSON.stringify` diff with `schema_version` guard |
|
|
1046
|
+
| PER-T-012 | PER-M4 | `planning-wave-executor.ts` | Replace `structuredClone(plan)` with spread |
|
|
1047
|
+
| PER-T-013 | PER-M4 | `core/utils.ts` + 4 service files | Deduplicate `normalizeSet` into shared utility |
|
|
1048
|
+
| PER-T-014 | PER-M4 | `qa-wave-executor.ts` | Move `loadRolePrompts` outside per-feature loop |
|
|
1049
|
+
|
|
1050
|
+
---
|
|
1051
|
+
|
|
1052
|
+
## 7. Implementation Order
|
|
1053
|
+
|
|
1054
|
+
Tasks within each milestone are independent and can be implemented in any order or in parallel. Milestone ordering recommendation:
|
|
1055
|
+
|
|
1056
|
+
1. **PER-M1 first** — highest-frequency hotpaths; no schema or interface changes; all changes are local refactors.
|
|
1057
|
+
2. **PER-M2 second** — bounds disk growth; PER-T-005 depends on `latest.json` being reliably written (confirmed at `gates.ts:430`).
|
|
1058
|
+
3. **PER-M4 third** — low-risk cleanups; PER-T-013 produces the shared utility that PER-T-008 will use.
|
|
1059
|
+
4. **PER-M3 last** — largest change surface; requires schema and catalog updates; builds on PER-T-001 from PER-M1.
|