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
|
@@ -4,6 +4,7 @@ import { runGit } from '../../core/git.js';
|
|
|
4
4
|
import { ERROR_CODES } from '../../core/error-codes.js';
|
|
5
5
|
import { fail } from '../../core/response.js';
|
|
6
6
|
import { GATE_RESULT, STATUS } from '../../core/constants.js';
|
|
7
|
+
import { resolveDepBlocked } from './dependency-scheduler-service.js';
|
|
7
8
|
|
|
8
9
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
10
|
type AnyRecord = Record<string, any>;
|
|
@@ -51,12 +52,58 @@ export class MergeService {
|
|
|
51
52
|
this.port = port;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
private async cleanupMergedFeatureArtifacts(featureId: string): Promise<void> {
|
|
56
|
+
const repoRoot = this.port.getRepoRoot();
|
|
57
|
+
const worktreePath = this.port.worktreePath(featureId);
|
|
58
|
+
|
|
59
|
+
const removeWorktreeResult = await runGit(repoRoot, ['worktree', 'remove', '--force', worktreePath]);
|
|
60
|
+
if (removeWorktreeResult.code !== 0) {
|
|
61
|
+
// Best-effort cleanup; merged state must remain intact even if git cleanup fails.
|
|
62
|
+
console.warn(
|
|
63
|
+
`[aop] auto-cleanup worktree removal failed for ${featureId}: ${removeWorktreeResult.stderr.trim() || `exit ${removeWorktreeResult.code}`}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const removeBranchResult = await runGit(repoRoot, ['branch', '-D', featureId]);
|
|
68
|
+
if (removeBranchResult.code !== 0) {
|
|
69
|
+
// Best-effort cleanup; branch may already be removed.
|
|
70
|
+
console.warn(
|
|
71
|
+
`[aop] auto-cleanup branch removal failed for ${featureId}: ${removeBranchResult.stderr.trim() || `exit ${removeBranchResult.code}`}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await fs.rm(this.port.featurePath(featureId), { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private scheduleFeatureCleanup(featureId: string, gracePeriodSeconds: number): void {
|
|
79
|
+
const delayMs = Math.max(0, Math.floor(gracePeriodSeconds * 1000));
|
|
80
|
+
const timeout = setTimeout(() => {
|
|
81
|
+
void this.cleanupMergedFeatureArtifacts(featureId).catch((error) => {
|
|
82
|
+
console.warn(
|
|
83
|
+
`[aop] auto-cleanup failed for ${featureId}: ${error instanceof Error ? error.message : String(error)}`
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
}, delayMs);
|
|
87
|
+
if (typeof timeout.unref === 'function') {
|
|
88
|
+
timeout.unref();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
54
92
|
async featureReadyToMerge(
|
|
55
93
|
featureId: string,
|
|
56
94
|
commitMessage: string,
|
|
57
95
|
mergeStrategy: string,
|
|
58
96
|
userApprovalToken: string | null
|
|
59
|
-
): Promise<{
|
|
97
|
+
): Promise<{
|
|
98
|
+
data: {
|
|
99
|
+
feature_id: string;
|
|
100
|
+
merge_sha: string;
|
|
101
|
+
merge_strategy: string;
|
|
102
|
+
status: string;
|
|
103
|
+
retained_for_cleanup?: boolean;
|
|
104
|
+
cleanup_grace_period_seconds?: number;
|
|
105
|
+
};
|
|
106
|
+
}> {
|
|
60
107
|
const policy = this.port.getPolicySnapshot();
|
|
61
108
|
const state = await this.port.readState(featureId);
|
|
62
109
|
|
|
@@ -227,19 +274,36 @@ export class MergeService {
|
|
|
227
274
|
runtimeSessions.feature_sessions = featureSessions;
|
|
228
275
|
index.runtime_sessions = runtimeSessions;
|
|
229
276
|
|
|
277
|
+
// N4: Promote features that were blocked waiting for this feature to merge
|
|
278
|
+
resolveDepBlocked(index, featureId);
|
|
279
|
+
|
|
230
280
|
index.version += 1;
|
|
231
281
|
index.updated_at = nowIso();
|
|
232
282
|
await this.port.writeIndex(index);
|
|
233
283
|
});
|
|
234
284
|
|
|
235
|
-
|
|
285
|
+
const cleanupPolicy = asRecord(policy.cleanup);
|
|
286
|
+
const autoAfterMerge = cleanupPolicy.auto_after_merge === true;
|
|
287
|
+
const gracePeriodSeconds =
|
|
288
|
+
typeof cleanupPolicy.grace_period_seconds === 'number' && Number.isFinite(cleanupPolicy.grace_period_seconds)
|
|
289
|
+
? cleanupPolicy.grace_period_seconds
|
|
290
|
+
: 3600;
|
|
291
|
+
const shouldCleanupNow = autoAfterMerge && gracePeriodSeconds <= 0;
|
|
292
|
+
|
|
293
|
+
if (shouldCleanupNow) {
|
|
294
|
+
await this.cleanupMergedFeatureArtifacts(featureId);
|
|
295
|
+
} else if (autoAfterMerge) {
|
|
296
|
+
this.scheduleFeatureCleanup(featureId, gracePeriodSeconds);
|
|
297
|
+
}
|
|
236
298
|
|
|
237
299
|
return {
|
|
238
300
|
data: {
|
|
239
301
|
feature_id: featureId,
|
|
240
302
|
merge_sha: mergeSha,
|
|
241
303
|
merge_strategy: mergeStrategy,
|
|
242
|
-
status: STATUS.MERGED
|
|
304
|
+
status: STATUS.MERGED,
|
|
305
|
+
retained_for_cleanup: !shouldCleanupNow,
|
|
306
|
+
cleanup_grace_period_seconds: gracePeriodSeconds
|
|
243
307
|
}
|
|
244
308
|
};
|
|
245
309
|
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
export type NotificationPriority = 'critical' | 'warning' | 'info';
|
|
7
|
+
type CanonicalPriority = NotificationPriority;
|
|
8
|
+
export type NotificationEvent =
|
|
9
|
+
| 'gate_failed'
|
|
10
|
+
| 'collision_detected'
|
|
11
|
+
| 'feature_blocked'
|
|
12
|
+
| 'ready_to_merge'
|
|
13
|
+
| 'feature_merged'
|
|
14
|
+
| 'stale_lease'
|
|
15
|
+
| 'agent_stuck'
|
|
16
|
+
| 'changes_requested'
|
|
17
|
+
| 'budget_exceeded'
|
|
18
|
+
| 'budget_alert';
|
|
19
|
+
|
|
20
|
+
export interface NotificationPayload {
|
|
21
|
+
event: NotificationEvent;
|
|
22
|
+
priority: NotificationPriority;
|
|
23
|
+
feature_id?: string;
|
|
24
|
+
message: string;
|
|
25
|
+
details?: Record<string, unknown>;
|
|
26
|
+
timestamp: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface NotifierChannel {
|
|
30
|
+
name: string;
|
|
31
|
+
send(payload: NotificationPayload): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface NotificationChannelConfig {
|
|
35
|
+
desktop?: { enabled?: boolean };
|
|
36
|
+
slack?: { enabled?: boolean; webhook?: string; channel?: string };
|
|
37
|
+
webhook?: { enabled?: boolean; url?: string };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface NotificationRoutingConfig {
|
|
41
|
+
critical?: string[];
|
|
42
|
+
warning?: string[];
|
|
43
|
+
info?: string[];
|
|
44
|
+
urgent?: string[];
|
|
45
|
+
action?: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface NotificationConfig {
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
channels?: NotificationChannelConfig;
|
|
51
|
+
routing?: NotificationRoutingConfig;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const DEFAULT_ROUTING: Record<NotificationPriority, string[]> = {
|
|
55
|
+
critical: ['desktop', 'slack'],
|
|
56
|
+
warning: ['slack', 'desktop'],
|
|
57
|
+
info: ['slack']
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const EVENT_PRIORITY: Record<NotificationEvent, CanonicalPriority> = {
|
|
61
|
+
gate_failed: 'critical',
|
|
62
|
+
collision_detected: 'critical',
|
|
63
|
+
feature_blocked: 'warning',
|
|
64
|
+
ready_to_merge: 'info',
|
|
65
|
+
feature_merged: 'info',
|
|
66
|
+
stale_lease: 'warning',
|
|
67
|
+
agent_stuck: 'warning',
|
|
68
|
+
changes_requested: 'warning',
|
|
69
|
+
budget_exceeded: 'critical',
|
|
70
|
+
budget_alert: 'warning'
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const THROTTLE_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
|
|
74
|
+
|
|
75
|
+
export class DesktopNotifierChannel implements NotifierChannel {
|
|
76
|
+
readonly name = 'desktop';
|
|
77
|
+
|
|
78
|
+
async send(payload: NotificationPayload): Promise<void> {
|
|
79
|
+
const title = `AOP [${payload.priority}]`;
|
|
80
|
+
const body = payload.feature_id
|
|
81
|
+
? `[${payload.feature_id}] ${payload.message}`
|
|
82
|
+
: payload.message;
|
|
83
|
+
|
|
84
|
+
const platform = process.platform;
|
|
85
|
+
try {
|
|
86
|
+
if (platform === 'linux') {
|
|
87
|
+
await execFileAsync('notify-send', [title, body], { timeout: 3000 });
|
|
88
|
+
} else if (platform === 'darwin') {
|
|
89
|
+
const script = `display notification "${body.replaceAll('"', '\\"')}" with title "${title.replaceAll('"', '\\"')}"`;
|
|
90
|
+
await execFileAsync('osascript', ['-e', script], { timeout: 3000 });
|
|
91
|
+
}
|
|
92
|
+
// Other platforms: silently skip
|
|
93
|
+
} catch {
|
|
94
|
+
// Notification tool not available — silently skip
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class SlackNotifierChannel implements NotifierChannel {
|
|
100
|
+
readonly name = 'slack';
|
|
101
|
+
private readonly webhookUrl: string;
|
|
102
|
+
private readonly channel: string;
|
|
103
|
+
|
|
104
|
+
constructor(webhookUrl: string, channel: string) {
|
|
105
|
+
this.webhookUrl = webhookUrl;
|
|
106
|
+
this.channel = channel;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async send(payload: NotificationPayload): Promise<void> {
|
|
110
|
+
const details = payload.details ?? {};
|
|
111
|
+
const dashboardFeatureUrl =
|
|
112
|
+
typeof details.dashboard_feature_url === 'string' && details.dashboard_feature_url.length > 0
|
|
113
|
+
? details.dashboard_feature_url
|
|
114
|
+
: typeof details.dashboard_url === 'string' && details.dashboard_url.length > 0
|
|
115
|
+
? details.dashboard_url
|
|
116
|
+
: null;
|
|
117
|
+
const detailsJson = JSON.stringify(details);
|
|
118
|
+
const detailsSuffix =
|
|
119
|
+
detailsJson !== '{}' ? `\nContext: \`${detailsJson.length > 800 ? `${detailsJson.slice(0, 800)}…` : detailsJson}\`` : '';
|
|
120
|
+
const dashboardSuffix = dashboardFeatureUrl ? `\nDashboard: ${dashboardFeatureUrl}` : '';
|
|
121
|
+
const text = payload.feature_id
|
|
122
|
+
? `*[${payload.priority.toUpperCase()}]* \`${payload.feature_id}\`: ${payload.message}`
|
|
123
|
+
: `*[${payload.priority.toUpperCase()}]* ${payload.message}`;
|
|
124
|
+
|
|
125
|
+
const body = JSON.stringify({ channel: this.channel, text: `${text}${dashboardSuffix}${detailsSuffix}` });
|
|
126
|
+
const response = await fetch(this.webhookUrl, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
|
+
body
|
|
130
|
+
});
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
throw new Error(`Slack webhook returned ${response.status}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class WebhookNotifierChannel implements NotifierChannel {
|
|
138
|
+
readonly name = 'webhook';
|
|
139
|
+
private readonly url: string;
|
|
140
|
+
|
|
141
|
+
constructor(url: string) {
|
|
142
|
+
this.url = url;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async send(payload: NotificationPayload): Promise<void> {
|
|
146
|
+
const response = await fetch(this.url, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: { 'Content-Type': 'application/json' },
|
|
149
|
+
body: JSON.stringify(payload)
|
|
150
|
+
});
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
throw new Error(`Webhook returned ${response.status}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export class NotifierService {
|
|
158
|
+
private readonly config: NotificationConfig;
|
|
159
|
+
private readonly channels: Map<string, NotifierChannel>;
|
|
160
|
+
private readonly routing: Record<CanonicalPriority, string[]>;
|
|
161
|
+
private readonly throttleCache: Map<string, number>;
|
|
162
|
+
private readonly dashboardUrl: string | null;
|
|
163
|
+
|
|
164
|
+
constructor(config: NotificationConfig, dashboardUrl: string | null = null) {
|
|
165
|
+
this.config = config;
|
|
166
|
+
this.throttleCache = new Map();
|
|
167
|
+
this.routing = this.buildRouting(config.routing);
|
|
168
|
+
this.channels = this.buildChannels(config.channels);
|
|
169
|
+
this.dashboardUrl = dashboardUrl;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private buildRouting(routing: NotificationRoutingConfig | undefined): Record<CanonicalPriority, string[]> {
|
|
173
|
+
const critical = routing?.critical ?? routing?.urgent ?? routing?.action ?? DEFAULT_ROUTING.critical;
|
|
174
|
+
return {
|
|
175
|
+
critical,
|
|
176
|
+
warning: routing?.warning ?? DEFAULT_ROUTING.warning,
|
|
177
|
+
info: routing?.info ?? DEFAULT_ROUTING.info
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private buildChannels(channelsConfig: NotificationChannelConfig | undefined): Map<string, NotifierChannel> {
|
|
182
|
+
const map = new Map<string, NotifierChannel>();
|
|
183
|
+
if (!channelsConfig) {
|
|
184
|
+
return map;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (channelsConfig.desktop?.enabled === true) {
|
|
188
|
+
map.set('desktop', new DesktopNotifierChannel());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const slack = channelsConfig.slack;
|
|
192
|
+
if (slack?.enabled === true && slack.webhook) {
|
|
193
|
+
map.set('slack', new SlackNotifierChannel(slack.webhook, slack.channel ?? '#aop-alerts'));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const webhook = channelsConfig.webhook;
|
|
197
|
+
if (webhook?.enabled === true && webhook.url) {
|
|
198
|
+
map.set('webhook', new WebhookNotifierChannel(webhook.url));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return map;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private isThrottled(featureId: string | undefined, event: NotificationEvent): boolean {
|
|
205
|
+
const key = `${featureId ?? '__global__'}:${event}`;
|
|
206
|
+
const last = this.throttleCache.get(key);
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
if (last !== undefined && now - last < THROTTLE_WINDOW_MS) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
this.throttleCache.set(key, now);
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async notify(
|
|
216
|
+
event: NotificationEvent,
|
|
217
|
+
context: { feature_id?: string; message: string; details?: Record<string, unknown> }
|
|
218
|
+
): Promise<void> {
|
|
219
|
+
if (!this.config.enabled) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (this.isThrottled(context.feature_id, event)) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const priority = EVENT_PRIORITY[event];
|
|
227
|
+
const details: Record<string, unknown> = {
|
|
228
|
+
...(context.details ?? {})
|
|
229
|
+
};
|
|
230
|
+
if (this.dashboardUrl) {
|
|
231
|
+
details.dashboard_url = this.dashboardUrl;
|
|
232
|
+
if (context.feature_id) {
|
|
233
|
+
details.dashboard_feature_url = `${this.dashboardUrl}?feature=${encodeURIComponent(context.feature_id)}`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const payload: NotificationPayload = {
|
|
237
|
+
event,
|
|
238
|
+
priority,
|
|
239
|
+
feature_id: context.feature_id,
|
|
240
|
+
message: context.message,
|
|
241
|
+
details,
|
|
242
|
+
timestamp: new Date().toISOString()
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const targetChannelNames = this.routing[priority] ?? [];
|
|
246
|
+
|
|
247
|
+
for (const channelName of targetChannelNames) {
|
|
248
|
+
const channel = this.channels.get(channelName);
|
|
249
|
+
if (!channel) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
await channel.send(payload);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
process.stderr.write(`[NotifierService] Channel "${channelName}" failed: ${String(err)}\n`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function createNotifierService(
|
|
262
|
+
policySnapshot: Record<string, unknown>,
|
|
263
|
+
preferredChannel: string | null = null
|
|
264
|
+
): NotifierService {
|
|
265
|
+
const raw = policySnapshot.notifications;
|
|
266
|
+
if (!raw || typeof raw !== 'object') {
|
|
267
|
+
return new NotifierService({ enabled: false });
|
|
268
|
+
}
|
|
269
|
+
const n = raw as Record<string, unknown>;
|
|
270
|
+
const config: NotificationConfig = {
|
|
271
|
+
enabled: n.enabled === true,
|
|
272
|
+
channels: (n.channels as NotificationChannelConfig | undefined) ?? {},
|
|
273
|
+
routing: (n.routing as NotificationRoutingConfig | undefined) ?? {}
|
|
274
|
+
};
|
|
275
|
+
if (preferredChannel) {
|
|
276
|
+
const channels = config.channels ?? {};
|
|
277
|
+
const forceEnabled = { enabled: true };
|
|
278
|
+
config.channels = {
|
|
279
|
+
desktop: preferredChannel === 'desktop' ? { ...(channels.desktop ?? {}), ...forceEnabled } : { enabled: false },
|
|
280
|
+
slack: preferredChannel === 'slack' ? { ...(channels.slack ?? {}), ...forceEnabled } : { enabled: false },
|
|
281
|
+
webhook: preferredChannel === 'webhook' ? { ...(channels.webhook ?? {}), ...forceEnabled } : { enabled: false }
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const dashboardRaw =
|
|
285
|
+
policySnapshot.dashboard && typeof policySnapshot.dashboard === 'object'
|
|
286
|
+
? (policySnapshot.dashboard as Record<string, unknown>)
|
|
287
|
+
: {};
|
|
288
|
+
const dashboardPort =
|
|
289
|
+
typeof dashboardRaw.port === 'number' && Number.isFinite(dashboardRaw.port) ? Math.floor(dashboardRaw.port) : 3000;
|
|
290
|
+
const dashboardUrl =
|
|
291
|
+
typeof process.env.AOP_DASHBOARD_URL === 'string' && process.env.AOP_DASHBOARD_URL.trim().length > 0
|
|
292
|
+
? process.env.AOP_DASHBOARD_URL.trim()
|
|
293
|
+
: `http://localhost:${dashboardPort}`;
|
|
294
|
+
return new NotifierService(config, dashboardUrl);
|
|
295
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { atomicWriteJson, ensureDir, nowIso, readJson } from '../../core/fs.js';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface FeatureOutcome {
|
|
5
|
+
feature_id: string;
|
|
6
|
+
provider: string;
|
|
7
|
+
model: string;
|
|
8
|
+
status: string;
|
|
9
|
+
gate_pass: boolean;
|
|
10
|
+
retry_count: number;
|
|
11
|
+
duration_ms: number;
|
|
12
|
+
cost_usd: number;
|
|
13
|
+
recorded_at: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProviderModelStats {
|
|
17
|
+
provider: string;
|
|
18
|
+
model: string;
|
|
19
|
+
total_features: number;
|
|
20
|
+
success_count: number;
|
|
21
|
+
success_rate: number;
|
|
22
|
+
avg_retry_count: number;
|
|
23
|
+
avg_duration_ms: number;
|
|
24
|
+
avg_cost_usd: number;
|
|
25
|
+
last_updated: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AnalyticsSnapshot {
|
|
29
|
+
outcomes: FeatureOutcome[];
|
|
30
|
+
aggregates: ProviderModelStats[];
|
|
31
|
+
generated_at: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PerformanceAnalyticsServicePort {
|
|
35
|
+
analyticsPath(): string;
|
|
36
|
+
getPolicySnapshot(): Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function computeAggregates(outcomes: FeatureOutcome[]): ProviderModelStats[] {
|
|
40
|
+
const groups = new Map<string, FeatureOutcome[]>();
|
|
41
|
+
for (const o of outcomes) {
|
|
42
|
+
const key = `${o.provider}::${o.model}`;
|
|
43
|
+
const list = groups.get(key);
|
|
44
|
+
if (list) {
|
|
45
|
+
list.push(o);
|
|
46
|
+
} else {
|
|
47
|
+
groups.set(key, [o]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const aggregates: ProviderModelStats[] = [];
|
|
52
|
+
for (const [, group] of groups) {
|
|
53
|
+
const first = group[0];
|
|
54
|
+
const total = group.length;
|
|
55
|
+
const successCount = group.filter((o) => o.gate_pass).length;
|
|
56
|
+
const sumRetry = group.reduce((s, o) => s + o.retry_count, 0);
|
|
57
|
+
const sumDuration = group.reduce((s, o) => s + o.duration_ms, 0);
|
|
58
|
+
const sumCost = group.reduce((s, o) => s + o.cost_usd, 0);
|
|
59
|
+
|
|
60
|
+
aggregates.push({
|
|
61
|
+
provider: first.provider,
|
|
62
|
+
model: first.model,
|
|
63
|
+
total_features: total,
|
|
64
|
+
success_count: successCount,
|
|
65
|
+
success_rate: total > 0 ? successCount / total : 0,
|
|
66
|
+
avg_retry_count: total > 0 ? sumRetry / total : 0,
|
|
67
|
+
avg_duration_ms: total > 0 ? sumDuration / total : 0,
|
|
68
|
+
avg_cost_usd: total > 0 ? sumCost / total : 0,
|
|
69
|
+
last_updated: nowIso()
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return aggregates;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class PerformanceAnalyticsService {
|
|
77
|
+
private readonly port: PerformanceAnalyticsServicePort;
|
|
78
|
+
|
|
79
|
+
constructor(port: PerformanceAnalyticsServicePort) {
|
|
80
|
+
this.port = port;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async recordOutcome(outcome: FeatureOutcome): Promise<AnalyticsSnapshot> {
|
|
84
|
+
const filePath = this.port.analyticsPath();
|
|
85
|
+
await ensureDir(path.dirname(filePath));
|
|
86
|
+
|
|
87
|
+
const current = await this.readSnapshot();
|
|
88
|
+
current.outcomes.push(outcome);
|
|
89
|
+
current.aggregates = computeAggregates(current.outcomes);
|
|
90
|
+
current.generated_at = nowIso();
|
|
91
|
+
|
|
92
|
+
await atomicWriteJson(filePath, current);
|
|
93
|
+
return current;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async getAnalytics(): Promise<AnalyticsSnapshot> {
|
|
97
|
+
return await this.readSnapshot();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async getProviderStats(provider?: string, model?: string): Promise<ProviderModelStats[]> {
|
|
101
|
+
const snapshot = await this.readSnapshot();
|
|
102
|
+
let stats = snapshot.aggregates;
|
|
103
|
+
|
|
104
|
+
if (provider) {
|
|
105
|
+
stats = stats.filter((s) => s.provider === provider);
|
|
106
|
+
}
|
|
107
|
+
if (model) {
|
|
108
|
+
stats = stats.filter((s) => s.model === model);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return stats;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private async readSnapshot(): Promise<AnalyticsSnapshot> {
|
|
115
|
+
const existing = await readJson<AnalyticsSnapshot>(this.port.analyticsPath(), null);
|
|
116
|
+
return existing ?? {
|
|
117
|
+
outcomes: [],
|
|
118
|
+
aggregates: [],
|
|
119
|
+
generated_at: nowIso()
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -4,6 +4,7 @@ import { sortResourcesDeterministically } from '../../core/path-rules.js';
|
|
|
4
4
|
import { ERROR_CODES } from '../../core/error-codes.js';
|
|
5
5
|
import { fail } from '../../core/response.js';
|
|
6
6
|
import { GATE_RESULT, STATUS } from '../../core/constants.js';
|
|
7
|
+
import { getUnresolvedDeps, detectCircularDependency, addToDepBlocked } from './dependency-scheduler-service.js';
|
|
7
8
|
|
|
8
9
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
10
|
type AnyRecord = Record<string, any>;
|
|
@@ -227,6 +228,53 @@ export class PlanService {
|
|
|
227
228
|
};
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
async checkDependencies(featureId: string | null): Promise<void> {
|
|
232
|
+
if (!featureId) {return;}
|
|
233
|
+
const state = await this.port.readState(featureId);
|
|
234
|
+
const dependsOn: string[] = Array.isArray(state.frontMatter.depends_on)
|
|
235
|
+
? (state.frontMatter.depends_on as string[]).filter((d): d is string => typeof d === 'string')
|
|
236
|
+
: [];
|
|
237
|
+
if (dependsOn.length === 0) {return;}
|
|
238
|
+
|
|
239
|
+
// Circular detection: traverse the dependency chain
|
|
240
|
+
const depsCache = new Map<string, string[]>([[featureId, dependsOn]]);
|
|
241
|
+
const getDirectDeps = (id: string): string[] => {
|
|
242
|
+
if (depsCache.has(id)) {return depsCache.get(id);}
|
|
243
|
+
return [];
|
|
244
|
+
};
|
|
245
|
+
const cycle = detectCircularDependency(featureId, getDirectDeps);
|
|
246
|
+
if (cycle) {
|
|
247
|
+
throw {
|
|
248
|
+
normalizedResponse: fail(ERROR_CODES.DEPENDENCY_CIRCULAR, `Circular dependency detected: ${cycle}`, {
|
|
249
|
+
feature_id: featureId,
|
|
250
|
+
cycle,
|
|
251
|
+
retryable: false,
|
|
252
|
+
requires_human: true
|
|
253
|
+
})
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const index = await this.port.readIndex();
|
|
258
|
+
const mergedFeatures: string[] = Array.isArray(index.merged) ? (index.merged as string[]) : [];
|
|
259
|
+
const unresolved = getUnresolvedDeps(dependsOn, mergedFeatures);
|
|
260
|
+
if (unresolved.length > 0) {
|
|
261
|
+
await this.port.withIndexLock(async () => {
|
|
262
|
+
const idx = await this.port.readIndex();
|
|
263
|
+
addToDepBlocked(idx, featureId, unresolved);
|
|
264
|
+
idx.updated_at = nowIso();
|
|
265
|
+
await this.port.writeIndex(idx);
|
|
266
|
+
});
|
|
267
|
+
throw {
|
|
268
|
+
normalizedResponse: fail(ERROR_CODES.DEPENDENCY_UNRESOLVED, 'Plan deferred: dependencies not yet merged', {
|
|
269
|
+
feature_id: featureId,
|
|
270
|
+
depends_on_unresolved: unresolved,
|
|
271
|
+
retryable: true,
|
|
272
|
+
requires_human: false
|
|
273
|
+
})
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
230
278
|
async planSubmit(
|
|
231
279
|
featureId: string | null,
|
|
232
280
|
plan: AnyRecord | null,
|
|
@@ -255,6 +303,9 @@ export class PlanService {
|
|
|
255
303
|
await this.validatePlanSchema(plan);
|
|
256
304
|
await this.assertPlanLocksHeld(featureId, plan);
|
|
257
305
|
|
|
306
|
+
// N4: Dependency-aware scheduling — check depends_on before accepting plan
|
|
307
|
+
await this.checkDependencies(featureId);
|
|
308
|
+
|
|
258
309
|
const existing = await readJson(this.port.planPath(featureId), null);
|
|
259
310
|
this.validatePlanRevisionRules(existing, plan, expectedVersion);
|
|
260
311
|
|