@soleri/core 2.1.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brain/brain.d.ts +10 -1
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +116 -13
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts +36 -1
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +119 -14
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/types.d.ts +34 -2
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/cognee/client.d.ts +3 -0
- package/dist/cognee/client.d.ts.map +1 -1
- package/dist/cognee/client.js +17 -0
- package/dist/cognee/client.js.map +1 -1
- package/dist/cognee/sync-manager.d.ts +94 -0
- package/dist/cognee/sync-manager.d.ts.map +1 -0
- package/dist/cognee/sync-manager.js +293 -0
- package/dist/cognee/sync-manager.js.map +1 -0
- package/dist/control/identity-manager.d.ts +22 -0
- package/dist/control/identity-manager.d.ts.map +1 -0
- package/dist/control/identity-manager.js +233 -0
- package/dist/control/identity-manager.js.map +1 -0
- package/dist/control/intent-router.d.ts +32 -0
- package/dist/control/intent-router.d.ts.map +1 -0
- package/dist/control/intent-router.js +242 -0
- package/dist/control/intent-router.js.map +1 -0
- package/dist/control/types.d.ts +68 -0
- package/dist/control/types.d.ts.map +1 -0
- package/dist/control/types.js +9 -0
- package/dist/control/types.js.map +1 -0
- package/dist/curator/curator.d.ts +37 -1
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +199 -1
- package/dist/curator/curator.js.map +1 -1
- package/dist/errors/classify.d.ts +13 -0
- package/dist/errors/classify.d.ts.map +1 -0
- package/dist/errors/classify.js +97 -0
- package/dist/errors/classify.js.map +1 -0
- package/dist/errors/index.d.ts +6 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +4 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/retry.d.ts +40 -0
- package/dist/errors/retry.d.ts.map +1 -0
- package/dist/errors/retry.js +97 -0
- package/dist/errors/retry.js.map +1 -0
- package/dist/errors/types.d.ts +48 -0
- package/dist/errors/types.d.ts.map +1 -0
- package/dist/errors/types.js +59 -0
- package/dist/errors/types.js.map +1 -0
- package/dist/facades/types.d.ts +1 -1
- package/dist/governance/governance.d.ts +42 -0
- package/dist/governance/governance.d.ts.map +1 -0
- package/dist/governance/governance.js +488 -0
- package/dist/governance/governance.js.map +1 -0
- package/dist/governance/index.d.ts +3 -0
- package/dist/governance/index.d.ts.map +1 -0
- package/dist/governance/index.js +2 -0
- package/dist/governance/index.js.map +1 -0
- package/dist/governance/types.d.ts +102 -0
- package/dist/governance/types.d.ts.map +1 -0
- package/dist/governance/types.js +3 -0
- package/dist/governance/types.js.map +1 -0
- package/dist/index.d.ts +52 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -1
- package/dist/index.js.map +1 -1
- package/dist/intake/content-classifier.d.ts +14 -0
- package/dist/intake/content-classifier.d.ts.map +1 -0
- package/dist/intake/content-classifier.js +125 -0
- package/dist/intake/content-classifier.js.map +1 -0
- package/dist/intake/dedup-gate.d.ts +17 -0
- package/dist/intake/dedup-gate.d.ts.map +1 -0
- package/dist/intake/dedup-gate.js +66 -0
- package/dist/intake/dedup-gate.js.map +1 -0
- package/dist/intake/intake-pipeline.d.ts +63 -0
- package/dist/intake/intake-pipeline.d.ts.map +1 -0
- package/dist/intake/intake-pipeline.js +373 -0
- package/dist/intake/intake-pipeline.js.map +1 -0
- package/dist/intake/types.d.ts +65 -0
- package/dist/intake/types.d.ts.map +1 -0
- package/dist/intake/types.js +3 -0
- package/dist/intake/types.js.map +1 -0
- package/dist/intelligence/loader.js +1 -1
- package/dist/intelligence/loader.js.map +1 -1
- package/dist/intelligence/types.d.ts +3 -1
- package/dist/intelligence/types.d.ts.map +1 -1
- package/dist/logging/logger.d.ts +37 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +145 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/logging/types.d.ts +19 -0
- package/dist/logging/types.d.ts.map +1 -0
- package/dist/logging/types.js +2 -0
- package/dist/logging/types.js.map +1 -0
- package/dist/loop/loop-manager.d.ts +100 -0
- package/dist/loop/loop-manager.d.ts.map +1 -0
- package/dist/loop/loop-manager.js +379 -0
- package/dist/loop/loop-manager.js.map +1 -0
- package/dist/loop/types.d.ts +103 -0
- package/dist/loop/types.d.ts.map +1 -0
- package/dist/loop/types.js +11 -0
- package/dist/loop/types.js.map +1 -0
- package/dist/persistence/index.d.ts +3 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/index.js +2 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/sqlite-provider.d.ts +25 -0
- package/dist/persistence/sqlite-provider.d.ts.map +1 -0
- package/dist/persistence/sqlite-provider.js +59 -0
- package/dist/persistence/sqlite-provider.js.map +1 -0
- package/dist/persistence/types.d.ts +36 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/persistence/types.js +8 -0
- package/dist/persistence/types.js.map +1 -0
- package/dist/planning/gap-analysis.d.ts +72 -0
- package/dist/planning/gap-analysis.d.ts.map +1 -0
- package/dist/planning/gap-analysis.js +442 -0
- package/dist/planning/gap-analysis.js.map +1 -0
- package/dist/planning/gap-types.d.ts +29 -0
- package/dist/planning/gap-types.d.ts.map +1 -0
- package/dist/planning/gap-types.js +28 -0
- package/dist/planning/gap-types.js.map +1 -0
- package/dist/planning/planner.d.ts +421 -4
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +949 -21
- package/dist/planning/planner.js.map +1 -1
- package/dist/playbooks/generic/brainstorming.d.ts +9 -0
- package/dist/playbooks/generic/brainstorming.d.ts.map +1 -0
- package/dist/playbooks/generic/brainstorming.js +105 -0
- package/dist/playbooks/generic/brainstorming.js.map +1 -0
- package/dist/playbooks/generic/code-review.d.ts +11 -0
- package/dist/playbooks/generic/code-review.d.ts.map +1 -0
- package/dist/playbooks/generic/code-review.js +176 -0
- package/dist/playbooks/generic/code-review.js.map +1 -0
- package/dist/playbooks/generic/subagent-execution.d.ts +9 -0
- package/dist/playbooks/generic/subagent-execution.d.ts.map +1 -0
- package/dist/playbooks/generic/subagent-execution.js +68 -0
- package/dist/playbooks/generic/subagent-execution.js.map +1 -0
- package/dist/playbooks/generic/systematic-debugging.d.ts +9 -0
- package/dist/playbooks/generic/systematic-debugging.d.ts.map +1 -0
- package/dist/playbooks/generic/systematic-debugging.js +87 -0
- package/dist/playbooks/generic/systematic-debugging.js.map +1 -0
- package/dist/playbooks/generic/tdd.d.ts +9 -0
- package/dist/playbooks/generic/tdd.d.ts.map +1 -0
- package/dist/playbooks/generic/tdd.js +70 -0
- package/dist/playbooks/generic/tdd.js.map +1 -0
- package/dist/playbooks/generic/verification.d.ts +9 -0
- package/dist/playbooks/generic/verification.d.ts.map +1 -0
- package/dist/playbooks/generic/verification.js +74 -0
- package/dist/playbooks/generic/verification.js.map +1 -0
- package/dist/playbooks/index.d.ts +4 -0
- package/dist/playbooks/index.d.ts.map +1 -0
- package/dist/playbooks/index.js +5 -0
- package/dist/playbooks/index.js.map +1 -0
- package/dist/playbooks/playbook-registry.d.ts +42 -0
- package/dist/playbooks/playbook-registry.d.ts.map +1 -0
- package/dist/playbooks/playbook-registry.js +227 -0
- package/dist/playbooks/playbook-registry.js.map +1 -0
- package/dist/playbooks/playbook-seeder.d.ts +47 -0
- package/dist/playbooks/playbook-seeder.d.ts.map +1 -0
- package/dist/playbooks/playbook-seeder.js +104 -0
- package/dist/playbooks/playbook-seeder.js.map +1 -0
- package/dist/playbooks/playbook-types.d.ts +132 -0
- package/dist/playbooks/playbook-types.d.ts.map +1 -0
- package/dist/playbooks/playbook-types.js +12 -0
- package/dist/playbooks/playbook-types.js.map +1 -0
- package/dist/project/project-registry.d.ts +79 -0
- package/dist/project/project-registry.d.ts.map +1 -0
- package/dist/project/project-registry.js +274 -0
- package/dist/project/project-registry.js.map +1 -0
- package/dist/project/types.d.ts +28 -0
- package/dist/project/types.d.ts.map +1 -0
- package/dist/project/types.js +5 -0
- package/dist/project/types.js.map +1 -0
- package/dist/prompts/index.d.ts +4 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +3 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/parser.d.ts +17 -0
- package/dist/prompts/parser.d.ts.map +1 -0
- package/dist/prompts/parser.js +47 -0
- package/dist/prompts/parser.js.map +1 -0
- package/dist/prompts/template-manager.d.ts +25 -0
- package/dist/prompts/template-manager.d.ts.map +1 -0
- package/dist/prompts/template-manager.js +71 -0
- package/dist/prompts/template-manager.js.map +1 -0
- package/dist/prompts/types.d.ts +26 -0
- package/dist/prompts/types.d.ts.map +1 -0
- package/dist/prompts/types.js +5 -0
- package/dist/prompts/types.js.map +1 -0
- package/dist/runtime/admin-extra-ops.d.ts +15 -0
- package/dist/runtime/admin-extra-ops.d.ts.map +1 -0
- package/dist/runtime/admin-extra-ops.js +595 -0
- package/dist/runtime/admin-extra-ops.js.map +1 -0
- package/dist/runtime/admin-ops.d.ts +15 -0
- package/dist/runtime/admin-ops.d.ts.map +1 -0
- package/dist/runtime/admin-ops.js +329 -0
- package/dist/runtime/admin-ops.js.map +1 -0
- package/dist/runtime/capture-ops.d.ts +15 -0
- package/dist/runtime/capture-ops.d.ts.map +1 -0
- package/dist/runtime/capture-ops.js +363 -0
- package/dist/runtime/capture-ops.js.map +1 -0
- package/dist/runtime/cognee-sync-ops.d.ts +12 -0
- package/dist/runtime/cognee-sync-ops.d.ts.map +1 -0
- package/dist/runtime/cognee-sync-ops.js +55 -0
- package/dist/runtime/cognee-sync-ops.js.map +1 -0
- package/dist/runtime/core-ops.d.ts +9 -3
- package/dist/runtime/core-ops.d.ts.map +1 -1
- package/dist/runtime/core-ops.js +693 -10
- package/dist/runtime/core-ops.js.map +1 -1
- package/dist/runtime/curator-extra-ops.d.ts +9 -0
- package/dist/runtime/curator-extra-ops.d.ts.map +1 -0
- package/dist/runtime/curator-extra-ops.js +71 -0
- package/dist/runtime/curator-extra-ops.js.map +1 -0
- package/dist/runtime/domain-ops.d.ts.map +1 -1
- package/dist/runtime/domain-ops.js +61 -15
- package/dist/runtime/domain-ops.js.map +1 -1
- package/dist/runtime/grading-ops.d.ts +14 -0
- package/dist/runtime/grading-ops.d.ts.map +1 -0
- package/dist/runtime/grading-ops.js +105 -0
- package/dist/runtime/grading-ops.js.map +1 -0
- package/dist/runtime/intake-ops.d.ts +14 -0
- package/dist/runtime/intake-ops.d.ts.map +1 -0
- package/dist/runtime/intake-ops.js +110 -0
- package/dist/runtime/intake-ops.js.map +1 -0
- package/dist/runtime/loop-ops.d.ts +14 -0
- package/dist/runtime/loop-ops.d.ts.map +1 -0
- package/dist/runtime/loop-ops.js +251 -0
- package/dist/runtime/loop-ops.js.map +1 -0
- package/dist/runtime/memory-cross-project-ops.d.ts +12 -0
- package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -0
- package/dist/runtime/memory-cross-project-ops.js +165 -0
- package/dist/runtime/memory-cross-project-ops.js.map +1 -0
- package/dist/runtime/memory-extra-ops.d.ts +13 -0
- package/dist/runtime/memory-extra-ops.d.ts.map +1 -0
- package/dist/runtime/memory-extra-ops.js +173 -0
- package/dist/runtime/memory-extra-ops.js.map +1 -0
- package/dist/runtime/orchestrate-ops.d.ts +17 -0
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -0
- package/dist/runtime/orchestrate-ops.js +246 -0
- package/dist/runtime/orchestrate-ops.js.map +1 -0
- package/dist/runtime/planning-extra-ops.d.ts +25 -0
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -0
- package/dist/runtime/planning-extra-ops.js +663 -0
- package/dist/runtime/planning-extra-ops.js.map +1 -0
- package/dist/runtime/playbook-ops.d.ts +14 -0
- package/dist/runtime/playbook-ops.d.ts.map +1 -0
- package/dist/runtime/playbook-ops.js +141 -0
- package/dist/runtime/playbook-ops.js.map +1 -0
- package/dist/runtime/project-ops.d.ts +15 -0
- package/dist/runtime/project-ops.d.ts.map +1 -0
- package/dist/runtime/project-ops.js +186 -0
- package/dist/runtime/project-ops.js.map +1 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +65 -3
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +29 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/vault-extra-ops.d.ts +10 -0
- package/dist/runtime/vault-extra-ops.d.ts.map +1 -0
- package/dist/runtime/vault-extra-ops.js +536 -0
- package/dist/runtime/vault-extra-ops.js.map +1 -0
- package/dist/telemetry/telemetry.d.ts +48 -0
- package/dist/telemetry/telemetry.d.ts.map +1 -0
- package/dist/telemetry/telemetry.js +87 -0
- package/dist/telemetry/telemetry.js.map +1 -0
- package/dist/vault/playbook.d.ts +34 -0
- package/dist/vault/playbook.d.ts.map +1 -0
- package/dist/vault/playbook.js +60 -0
- package/dist/vault/playbook.js.map +1 -0
- package/dist/vault/vault.d.ts +97 -4
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +424 -65
- package/dist/vault/vault.js.map +1 -1
- package/package.json +7 -3
- package/src/__tests__/admin-extra-ops.test.ts +467 -0
- package/src/__tests__/admin-ops.test.ts +271 -0
- package/src/__tests__/brain-intelligence.test.ts +205 -0
- package/src/__tests__/brain.test.ts +134 -3
- package/src/__tests__/capture-ops.test.ts +509 -0
- package/src/__tests__/cognee-integration.test.ts +80 -0
- package/src/__tests__/cognee-sync-manager.test.ts +103 -0
- package/src/__tests__/core-ops.test.ts +292 -2
- package/src/__tests__/curator-extra-ops.test.ts +381 -0
- package/src/__tests__/domain-ops.test.ts +66 -0
- package/src/__tests__/errors.test.ts +388 -0
- package/src/__tests__/governance.test.ts +522 -0
- package/src/__tests__/grading-ops.test.ts +361 -0
- package/src/__tests__/identity-manager.test.ts +243 -0
- package/src/__tests__/intake-pipeline.test.ts +162 -0
- package/src/__tests__/intent-router.test.ts +222 -0
- package/src/__tests__/logger.test.ts +200 -0
- package/src/__tests__/loop-ops.test.ts +469 -0
- package/src/__tests__/memory-cross-project-ops.test.ts +248 -0
- package/src/__tests__/memory-extra-ops.test.ts +352 -0
- package/src/__tests__/orchestrate-ops.test.ts +289 -0
- package/src/__tests__/persistence.test.ts +225 -0
- package/src/__tests__/planner.test.ts +416 -7
- package/src/__tests__/planning-extra-ops.test.ts +706 -0
- package/src/__tests__/playbook-registry.test.ts +326 -0
- package/src/__tests__/playbook-seeder.test.ts +163 -0
- package/src/__tests__/playbook.test.ts +389 -0
- package/src/__tests__/project-ops.test.ts +381 -0
- package/src/__tests__/template-manager.test.ts +222 -0
- package/src/__tests__/vault-extra-ops.test.ts +482 -0
- package/src/brain/brain.ts +185 -16
- package/src/brain/intelligence.ts +179 -10
- package/src/brain/types.ts +40 -2
- package/src/cognee/client.ts +18 -0
- package/src/cognee/sync-manager.ts +389 -0
- package/src/control/identity-manager.ts +354 -0
- package/src/control/intent-router.ts +326 -0
- package/src/control/types.ts +102 -0
- package/src/curator/curator.ts +295 -1
- package/src/errors/classify.ts +102 -0
- package/src/errors/index.ts +5 -0
- package/src/errors/retry.ts +132 -0
- package/src/errors/types.ts +81 -0
- package/src/governance/governance.ts +698 -0
- package/src/governance/index.ts +18 -0
- package/src/governance/types.ts +111 -0
- package/src/index.ts +213 -2
- package/src/intake/content-classifier.ts +146 -0
- package/src/intake/dedup-gate.ts +92 -0
- package/src/intake/intake-pipeline.ts +503 -0
- package/src/intake/types.ts +69 -0
- package/src/intelligence/loader.ts +1 -1
- package/src/intelligence/types.ts +3 -1
- package/src/logging/logger.ts +154 -0
- package/src/logging/types.ts +21 -0
- package/src/loop/loop-manager.ts +448 -0
- package/src/loop/types.ts +115 -0
- package/src/persistence/index.ts +7 -0
- package/src/persistence/sqlite-provider.ts +62 -0
- package/src/persistence/types.ts +44 -0
- package/src/planning/gap-analysis.ts +775 -0
- package/src/planning/gap-types.ts +61 -0
- package/src/planning/planner.ts +1273 -24
- package/src/playbooks/generic/brainstorming.ts +110 -0
- package/src/playbooks/generic/code-review.ts +181 -0
- package/src/playbooks/generic/subagent-execution.ts +74 -0
- package/src/playbooks/generic/systematic-debugging.ts +92 -0
- package/src/playbooks/generic/tdd.ts +75 -0
- package/src/playbooks/generic/verification.ts +79 -0
- package/src/playbooks/index.ts +27 -0
- package/src/playbooks/playbook-registry.ts +284 -0
- package/src/playbooks/playbook-seeder.ts +119 -0
- package/src/playbooks/playbook-types.ts +162 -0
- package/src/project/project-registry.ts +370 -0
- package/src/project/types.ts +31 -0
- package/src/prompts/index.ts +3 -0
- package/src/prompts/parser.ts +59 -0
- package/src/prompts/template-manager.ts +77 -0
- package/src/prompts/types.ts +28 -0
- package/src/runtime/admin-extra-ops.ts +652 -0
- package/src/runtime/admin-ops.ts +340 -0
- package/src/runtime/capture-ops.ts +404 -0
- package/src/runtime/cognee-sync-ops.ts +63 -0
- package/src/runtime/core-ops.ts +787 -9
- package/src/runtime/curator-extra-ops.ts +85 -0
- package/src/runtime/domain-ops.ts +67 -15
- package/src/runtime/grading-ops.ts +130 -0
- package/src/runtime/intake-ops.ts +126 -0
- package/src/runtime/loop-ops.ts +277 -0
- package/src/runtime/memory-cross-project-ops.ts +191 -0
- package/src/runtime/memory-extra-ops.ts +186 -0
- package/src/runtime/orchestrate-ops.ts +278 -0
- package/src/runtime/planning-extra-ops.ts +718 -0
- package/src/runtime/playbook-ops.ts +169 -0
- package/src/runtime/project-ops.ts +202 -0
- package/src/runtime/runtime.ts +77 -3
- package/src/runtime/types.ts +29 -0
- package/src/runtime/vault-extra-ops.ts +606 -0
- package/src/telemetry/telemetry.ts +118 -0
- package/src/vault/playbook.ts +87 -0
- package/src/vault/vault.ts +575 -98
package/src/planning/planner.ts
CHANGED
|
@@ -1,24 +1,285 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { dirname } from 'node:path';
|
|
4
|
+
import type { PlanGap } from './gap-types.js';
|
|
5
|
+
import { SEVERITY_WEIGHTS, CATEGORY_PENALTY_CAPS } from './gap-types.js';
|
|
6
|
+
import { runGapAnalysis } from './gap-analysis.js';
|
|
7
|
+
import type { GapAnalysisOptions } from './gap-analysis.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Plan lifecycle status.
|
|
11
|
+
* Ported from Salvador's PlanLifecycleStatus with full 8-state lifecycle.
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle: brainstorming → draft → approved → executing → [validating] → reconciling → completed → archived
|
|
14
|
+
*/
|
|
15
|
+
export type PlanStatus =
|
|
16
|
+
| 'brainstorming'
|
|
17
|
+
| 'draft'
|
|
18
|
+
| 'approved'
|
|
19
|
+
| 'executing'
|
|
20
|
+
| 'validating'
|
|
21
|
+
| 'reconciling'
|
|
22
|
+
| 'completed'
|
|
23
|
+
| 'archived';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Valid status transitions.
|
|
27
|
+
* Each key maps to the set of statuses it can transition to.
|
|
28
|
+
* Ported from Salvador's LIFECYCLE_TRANSITIONS.
|
|
29
|
+
*/
|
|
30
|
+
export const LIFECYCLE_TRANSITIONS: Record<PlanStatus, PlanStatus[]> = {
|
|
31
|
+
brainstorming: ['draft'],
|
|
32
|
+
draft: ['approved'],
|
|
33
|
+
approved: ['executing'],
|
|
34
|
+
executing: ['validating', 'reconciling'],
|
|
35
|
+
validating: ['reconciling', 'executing'],
|
|
36
|
+
reconciling: ['completed'],
|
|
37
|
+
completed: ['archived'],
|
|
38
|
+
archived: [],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Statuses where the 30-minute TTL should NOT apply.
|
|
43
|
+
* Plans in these states may span multiple sessions.
|
|
44
|
+
*/
|
|
45
|
+
export const NON_EXPIRING_STATUSES: PlanStatus[] = [
|
|
46
|
+
'brainstorming',
|
|
47
|
+
'executing',
|
|
48
|
+
'validating',
|
|
49
|
+
'reconciling',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validate a lifecycle status transition.
|
|
54
|
+
* Returns true if the transition is valid, false otherwise.
|
|
55
|
+
*/
|
|
56
|
+
export function isValidTransition(from: PlanStatus, to: PlanStatus): boolean {
|
|
57
|
+
return LIFECYCLE_TRANSITIONS[from].includes(to);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get the valid next statuses for a given status.
|
|
62
|
+
*/
|
|
63
|
+
export function getValidNextStatuses(status: PlanStatus): PlanStatus[] {
|
|
64
|
+
return LIFECYCLE_TRANSITIONS[status];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if a status should have TTL expiration.
|
|
69
|
+
* Plans in executing/reconciling states persist indefinitely.
|
|
70
|
+
*/
|
|
71
|
+
export function shouldExpire(status: PlanStatus): boolean {
|
|
72
|
+
return !NON_EXPIRING_STATUSES.includes(status);
|
|
73
|
+
}
|
|
3
74
|
|
|
4
|
-
export type PlanStatus = 'draft' | 'approved' | 'executing' | 'completed';
|
|
5
75
|
export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'skipped' | 'failed';
|
|
6
76
|
|
|
77
|
+
export interface TaskEvidence {
|
|
78
|
+
/** What the evidence proves (maps to an acceptance criterion). */
|
|
79
|
+
criterion: string;
|
|
80
|
+
/** Evidence content — command output, URL, file path, description. */
|
|
81
|
+
content: string;
|
|
82
|
+
/** Evidence type. */
|
|
83
|
+
type: 'command_output' | 'url' | 'file' | 'description';
|
|
84
|
+
submittedAt: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface TaskMetrics {
|
|
88
|
+
durationMs?: number;
|
|
89
|
+
iterations?: number;
|
|
90
|
+
toolCalls?: number;
|
|
91
|
+
modelTier?: string;
|
|
92
|
+
estimatedCostUsd?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface TaskDeliverable {
|
|
96
|
+
type: 'file' | 'vault_entry' | 'url';
|
|
97
|
+
path: string;
|
|
98
|
+
hash?: string;
|
|
99
|
+
verifiedAt?: number;
|
|
100
|
+
stale?: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ExecutionSummary {
|
|
104
|
+
totalDurationMs: number;
|
|
105
|
+
tasksCompleted: number;
|
|
106
|
+
tasksSkipped: number;
|
|
107
|
+
tasksFailed: number;
|
|
108
|
+
avgTaskDurationMs: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
7
111
|
export interface PlanTask {
|
|
8
112
|
id: string;
|
|
9
113
|
title: string;
|
|
10
114
|
description: string;
|
|
11
115
|
status: TaskStatus;
|
|
116
|
+
/** Optional dependency IDs — tasks that must complete before this one. */
|
|
117
|
+
dependsOn?: string[];
|
|
118
|
+
/** Evidence submitted for task acceptance criteria. */
|
|
119
|
+
evidence?: TaskEvidence[];
|
|
120
|
+
/** Whether this task has been verified (all evidence checked + reviews passed). */
|
|
121
|
+
verified?: boolean;
|
|
122
|
+
/** Task-level acceptance criteria (for verification checking). */
|
|
123
|
+
acceptanceCriteria?: string[];
|
|
124
|
+
/** Timestamp when task was first moved to in_progress. */
|
|
125
|
+
startedAt?: number;
|
|
126
|
+
/** Timestamp when task reached a terminal state (completed/skipped/failed). */
|
|
127
|
+
completedAt?: number;
|
|
128
|
+
/** Per-task execution metrics. */
|
|
129
|
+
metrics?: TaskMetrics;
|
|
130
|
+
/** Deliverables produced by this task. */
|
|
131
|
+
deliverables?: TaskDeliverable[];
|
|
12
132
|
updatedAt: number;
|
|
13
133
|
}
|
|
14
134
|
|
|
135
|
+
export interface DriftItem {
|
|
136
|
+
/** Type of drift */
|
|
137
|
+
type: 'skipped' | 'added' | 'modified' | 'reordered';
|
|
138
|
+
/** What drifted */
|
|
139
|
+
description: string;
|
|
140
|
+
/** How much this affected the plan */
|
|
141
|
+
impact: 'low' | 'medium' | 'high';
|
|
142
|
+
/** Why the drift occurred */
|
|
143
|
+
rationale: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Severity weights for drift accuracy score calculation.
|
|
148
|
+
* Score = 100 - sum(drift_items * weight_per_impact)
|
|
149
|
+
* Ported from Salvador's plan-lifecycle-types.ts.
|
|
150
|
+
*/
|
|
151
|
+
export const DRIFT_WEIGHTS: Record<DriftItem['impact'], number> = {
|
|
152
|
+
high: 20,
|
|
153
|
+
medium: 10,
|
|
154
|
+
low: 5,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Calculate drift accuracy score from drift items.
|
|
159
|
+
* Score = max(0, 100 - sum(weight_per_impact))
|
|
160
|
+
* Ported from Salvador's calculateDriftScore.
|
|
161
|
+
*/
|
|
162
|
+
export function calculateDriftScore(items: DriftItem[]): number {
|
|
163
|
+
let deductions = 0;
|
|
164
|
+
for (const item of items) {
|
|
165
|
+
deductions += DRIFT_WEIGHTS[item.impact];
|
|
166
|
+
}
|
|
167
|
+
return Math.max(0, 100 - deductions);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface ReconciliationReport {
|
|
171
|
+
planId: string;
|
|
172
|
+
/** Accuracy score: 100 = perfect execution, 0 = total drift. Impact-weighted. */
|
|
173
|
+
accuracy: number;
|
|
174
|
+
driftItems: DriftItem[];
|
|
175
|
+
/** Human-readable summary of the drift */
|
|
176
|
+
summary: string;
|
|
177
|
+
reconciledAt: number;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface ReviewEvidence {
|
|
181
|
+
planId: string;
|
|
182
|
+
taskId?: string;
|
|
183
|
+
reviewer: string;
|
|
184
|
+
outcome: 'approved' | 'rejected' | 'needs_changes';
|
|
185
|
+
comments: string;
|
|
186
|
+
reviewedAt: number;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export type PlanGrade = 'A+' | 'A' | 'B' | 'C' | 'D' | 'F';
|
|
190
|
+
|
|
191
|
+
export interface PlanCheck {
|
|
192
|
+
checkId: string;
|
|
193
|
+
planId: string;
|
|
194
|
+
grade: PlanGrade;
|
|
195
|
+
score: number; // 0-100
|
|
196
|
+
gaps: PlanGap[];
|
|
197
|
+
iteration: number;
|
|
198
|
+
checkedAt: number;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Calculate score from gaps with severity-weighted deductions and iteration leniency.
|
|
203
|
+
* Ported from Salvador MCP's plan-grading.ts.
|
|
204
|
+
*
|
|
205
|
+
* - Minor gaps: weight=0 on iteration 1 (free sketching), weight=1 on iteration 2, full weight on 3+
|
|
206
|
+
* - Per-category deductions are capped before summing (prevents one category from tanking the score)
|
|
207
|
+
* - Score = max(0, 100 - totalDeductions)
|
|
208
|
+
*/
|
|
209
|
+
export function calculateScore(gaps: PlanGap[], iteration: number = 1): number {
|
|
210
|
+
const categoryTotals = new Map<string, number>();
|
|
211
|
+
|
|
212
|
+
for (const gap of gaps) {
|
|
213
|
+
let weight: number = SEVERITY_WEIGHTS[gap.severity];
|
|
214
|
+
|
|
215
|
+
// Iteration leniency for minor gaps
|
|
216
|
+
if (gap.severity === 'minor') {
|
|
217
|
+
if (iteration === 1) weight = 0;
|
|
218
|
+
else if (iteration === 2) weight = 1;
|
|
219
|
+
// iteration 3+: full weight (2)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const category = gap.category;
|
|
223
|
+
categoryTotals.set(category, (categoryTotals.get(category) ?? 0) + weight);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let deductions = 0;
|
|
227
|
+
for (const [category, total] of categoryTotals) {
|
|
228
|
+
const cap = CATEGORY_PENALTY_CAPS[category];
|
|
229
|
+
deductions += cap !== undefined ? Math.min(total, cap) : total;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return Math.max(0, 100 - deductions);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* A structured decision with rationale.
|
|
237
|
+
* Ported from Salvador's PlanContent.decisions.
|
|
238
|
+
*/
|
|
239
|
+
export interface PlanDecision {
|
|
240
|
+
decision: string;
|
|
241
|
+
rationale: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
15
244
|
export interface Plan {
|
|
16
245
|
id: string;
|
|
17
246
|
objective: string;
|
|
18
247
|
scope: string;
|
|
19
248
|
status: PlanStatus;
|
|
20
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Decisions can be flat strings (backward compat) or structured {decision, rationale}.
|
|
251
|
+
* New plans should prefer PlanDecision[].
|
|
252
|
+
*/
|
|
253
|
+
decisions: (string | PlanDecision)[];
|
|
21
254
|
tasks: PlanTask[];
|
|
255
|
+
/** High-level approach description. Ported from Salvador's PlanContent. */
|
|
256
|
+
approach?: string;
|
|
257
|
+
/** Additional context for the plan. */
|
|
258
|
+
context?: string;
|
|
259
|
+
/** Measurable success criteria. */
|
|
260
|
+
success_criteria?: string[];
|
|
261
|
+
/** Tools to use in execution order. */
|
|
262
|
+
tool_chain?: string[];
|
|
263
|
+
/** Flow definition to follow (e.g., 'developer', 'reviewer', 'designer'). */
|
|
264
|
+
flow?: string;
|
|
265
|
+
/** Target operational mode (e.g., 'build', 'review', 'fix'). */
|
|
266
|
+
target_mode?: string;
|
|
267
|
+
/** Reconciliation report — populated by reconcile(). */
|
|
268
|
+
reconciliation?: ReconciliationReport;
|
|
269
|
+
/** Review evidence — populated by addReview(). */
|
|
270
|
+
reviews?: ReviewEvidence[];
|
|
271
|
+
/** Latest grading check. */
|
|
272
|
+
latestCheck?: PlanCheck;
|
|
273
|
+
/** All check history. */
|
|
274
|
+
checks: PlanCheck[];
|
|
275
|
+
/** Matched playbook info (set by orchestration layer via playbook_match). */
|
|
276
|
+
playbookMatch?: {
|
|
277
|
+
label: string;
|
|
278
|
+
genericId?: string;
|
|
279
|
+
domainId?: string;
|
|
280
|
+
};
|
|
281
|
+
/** Aggregate execution metrics — populated by reconcile() and complete(). */
|
|
282
|
+
executionSummary?: ExecutionSummary;
|
|
22
283
|
createdAt: number;
|
|
23
284
|
updatedAt: number;
|
|
24
285
|
}
|
|
@@ -31,9 +292,11 @@ export interface PlanStore {
|
|
|
31
292
|
export class Planner {
|
|
32
293
|
private filePath: string;
|
|
33
294
|
private store: PlanStore;
|
|
295
|
+
private gapOptions?: GapAnalysisOptions;
|
|
34
296
|
|
|
35
|
-
constructor(filePath: string) {
|
|
297
|
+
constructor(filePath: string, gapOptions?: GapAnalysisOptions) {
|
|
36
298
|
this.filePath = filePath;
|
|
299
|
+
this.gapOptions = gapOptions;
|
|
37
300
|
this.store = this.load();
|
|
38
301
|
}
|
|
39
302
|
|
|
@@ -43,7 +306,12 @@ export class Planner {
|
|
|
43
306
|
}
|
|
44
307
|
try {
|
|
45
308
|
const data = readFileSync(this.filePath, 'utf-8');
|
|
46
|
-
|
|
309
|
+
const store = JSON.parse(data) as PlanStore;
|
|
310
|
+
// Backward compatibility: ensure every plan has a checks array
|
|
311
|
+
for (const plan of store.plans) {
|
|
312
|
+
plan.checks = plan.checks ?? [];
|
|
313
|
+
}
|
|
314
|
+
return store;
|
|
47
315
|
} catch {
|
|
48
316
|
return { version: '1.0', plans: [] };
|
|
49
317
|
}
|
|
@@ -57,15 +325,23 @@ export class Planner {
|
|
|
57
325
|
create(params: {
|
|
58
326
|
objective: string;
|
|
59
327
|
scope: string;
|
|
60
|
-
decisions?: string[];
|
|
328
|
+
decisions?: (string | PlanDecision)[];
|
|
61
329
|
tasks?: Array<{ title: string; description: string }>;
|
|
330
|
+
approach?: string;
|
|
331
|
+
context?: string;
|
|
332
|
+
success_criteria?: string[];
|
|
333
|
+
tool_chain?: string[];
|
|
334
|
+
flow?: string;
|
|
335
|
+
target_mode?: string;
|
|
336
|
+
/** Start in 'brainstorming' instead of 'draft'. Default: 'draft'. */
|
|
337
|
+
initialStatus?: 'brainstorming' | 'draft';
|
|
62
338
|
}): Plan {
|
|
63
339
|
const now = Date.now();
|
|
64
340
|
const plan: Plan = {
|
|
65
341
|
id: `plan-${now}-${Math.random().toString(36).slice(2, 8)}`,
|
|
66
342
|
objective: params.objective,
|
|
67
343
|
scope: params.scope,
|
|
68
|
-
status: 'draft',
|
|
344
|
+
status: params.initialStatus ?? 'draft',
|
|
69
345
|
decisions: params.decisions ?? [],
|
|
70
346
|
tasks: (params.tasks ?? []).map((t, i) => ({
|
|
71
347
|
id: `task-${i + 1}`,
|
|
@@ -74,6 +350,13 @@ export class Planner {
|
|
|
74
350
|
status: 'pending' as TaskStatus,
|
|
75
351
|
updatedAt: now,
|
|
76
352
|
})),
|
|
353
|
+
...(params.approach !== undefined && { approach: params.approach }),
|
|
354
|
+
...(params.context !== undefined && { context: params.context }),
|
|
355
|
+
...(params.success_criteria !== undefined && { success_criteria: params.success_criteria }),
|
|
356
|
+
...(params.tool_chain !== undefined && { tool_chain: params.tool_chain }),
|
|
357
|
+
...(params.flow !== undefined && { flow: params.flow }),
|
|
358
|
+
...(params.target_mode !== undefined && { target_mode: params.target_mode }),
|
|
359
|
+
checks: [],
|
|
77
360
|
createdAt: now,
|
|
78
361
|
updatedAt: now,
|
|
79
362
|
};
|
|
@@ -90,13 +373,38 @@ export class Planner {
|
|
|
90
373
|
return [...this.store.plans];
|
|
91
374
|
}
|
|
92
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Transition a plan to a new status using the typed FSM.
|
|
378
|
+
* Validates that the transition is allowed before applying it.
|
|
379
|
+
*/
|
|
380
|
+
private transition(plan: Plan, to: PlanStatus): void {
|
|
381
|
+
if (!isValidTransition(plan.status, to)) {
|
|
382
|
+
const valid = getValidNextStatuses(plan.status);
|
|
383
|
+
throw new Error(
|
|
384
|
+
`Invalid transition: '${plan.status}' → '${to}'. ` +
|
|
385
|
+
`Valid transitions from '${plan.status}': ${valid.length > 0 ? valid.join(', ') : 'none'}`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
plan.status = to;
|
|
389
|
+
plan.updatedAt = Date.now();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Promote a brainstorming plan to draft status.
|
|
394
|
+
* Only allowed from 'brainstorming'.
|
|
395
|
+
*/
|
|
396
|
+
promoteToDraft(planId: string): Plan {
|
|
397
|
+
const plan = this.get(planId);
|
|
398
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
399
|
+
this.transition(plan, 'draft');
|
|
400
|
+
this.save();
|
|
401
|
+
return plan;
|
|
402
|
+
}
|
|
403
|
+
|
|
93
404
|
approve(planId: string): Plan {
|
|
94
405
|
const plan = this.get(planId);
|
|
95
406
|
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
96
|
-
|
|
97
|
-
throw new Error(`Cannot approve plan in '${plan.status}' status — must be 'draft'`);
|
|
98
|
-
plan.status = 'approved';
|
|
99
|
-
plan.updatedAt = Date.now();
|
|
407
|
+
this.transition(plan, 'approved');
|
|
100
408
|
this.save();
|
|
101
409
|
return plan;
|
|
102
410
|
}
|
|
@@ -104,10 +412,7 @@ export class Planner {
|
|
|
104
412
|
startExecution(planId: string): Plan {
|
|
105
413
|
const plan = this.get(planId);
|
|
106
414
|
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
107
|
-
|
|
108
|
-
throw new Error(`Cannot execute plan in '${plan.status}' status — must be 'approved'`);
|
|
109
|
-
plan.status = 'executing';
|
|
110
|
-
plan.updatedAt = Date.now();
|
|
415
|
+
this.transition(plan, 'executing');
|
|
111
416
|
this.save();
|
|
112
417
|
return plan;
|
|
113
418
|
}
|
|
@@ -115,37 +420,981 @@ export class Planner {
|
|
|
115
420
|
updateTask(planId: string, taskId: string, status: TaskStatus): Plan {
|
|
116
421
|
const plan = this.get(planId);
|
|
117
422
|
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
118
|
-
if (plan.status !== 'executing')
|
|
423
|
+
if (plan.status !== 'executing' && plan.status !== 'validating')
|
|
119
424
|
throw new Error(
|
|
120
|
-
`Cannot update tasks on plan in '${plan.status}' status — must be 'executing'`,
|
|
425
|
+
`Cannot update tasks on plan in '${plan.status}' status — must be 'executing' or 'validating'`,
|
|
121
426
|
);
|
|
122
427
|
const task = plan.tasks.find((t) => t.id === taskId);
|
|
123
428
|
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
429
|
+
|
|
430
|
+
const now = Date.now();
|
|
431
|
+
|
|
432
|
+
// Auto-set startedAt on first in_progress transition
|
|
433
|
+
if (status === 'in_progress' && !task.startedAt) {
|
|
434
|
+
task.startedAt = now;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Auto-set completedAt and compute durationMs on terminal transitions
|
|
438
|
+
if (status === 'completed' || status === 'skipped' || status === 'failed') {
|
|
439
|
+
task.completedAt = now;
|
|
440
|
+
if (task.startedAt) {
|
|
441
|
+
if (!task.metrics) task.metrics = {};
|
|
442
|
+
task.metrics.durationMs = now - task.startedAt;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
124
446
|
task.status = status;
|
|
125
|
-
task.updatedAt =
|
|
126
|
-
plan.updatedAt =
|
|
447
|
+
task.updatedAt = now;
|
|
448
|
+
plan.updatedAt = now;
|
|
449
|
+
this.save();
|
|
450
|
+
return plan;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Transition plan to 'validating' state (post-execution verification).
|
|
455
|
+
* Only allowed from 'executing'.
|
|
456
|
+
*/
|
|
457
|
+
startValidation(planId: string): Plan {
|
|
458
|
+
const plan = this.get(planId);
|
|
459
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
460
|
+
this.transition(plan, 'validating');
|
|
461
|
+
this.save();
|
|
462
|
+
return plan;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Transition plan to 'reconciling' state.
|
|
467
|
+
* Allowed from 'executing' or 'validating'.
|
|
468
|
+
*/
|
|
469
|
+
startReconciliation(planId: string): Plan {
|
|
470
|
+
const plan = this.get(planId);
|
|
471
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
472
|
+
this.transition(plan, 'reconciling');
|
|
127
473
|
this.save();
|
|
128
474
|
return plan;
|
|
129
475
|
}
|
|
130
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Complete a plan. Only allowed from 'reconciling'.
|
|
479
|
+
* Use startReconciliation() + reconcile() + complete() for the full lifecycle,
|
|
480
|
+
* or reconcile() which auto-transitions through reconciling → completed.
|
|
481
|
+
*/
|
|
131
482
|
complete(planId: string): Plan {
|
|
132
483
|
const plan = this.get(planId);
|
|
133
484
|
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
plan.status = 'completed';
|
|
137
|
-
plan.updatedAt = Date.now();
|
|
485
|
+
plan.executionSummary = this.computeExecutionSummary(plan);
|
|
486
|
+
this.transition(plan, 'completed');
|
|
138
487
|
this.save();
|
|
139
488
|
return plan;
|
|
140
489
|
}
|
|
141
490
|
|
|
142
491
|
getExecuting(): Plan[] {
|
|
143
|
-
return this.store.plans.filter((p) => p.status === 'executing');
|
|
492
|
+
return this.store.plans.filter((p) => p.status === 'executing' || p.status === 'validating');
|
|
144
493
|
}
|
|
145
494
|
|
|
146
495
|
getActive(): Plan[] {
|
|
147
496
|
return this.store.plans.filter(
|
|
148
|
-
(p) =>
|
|
497
|
+
(p) =>
|
|
498
|
+
p.status === 'brainstorming' ||
|
|
499
|
+
p.status === 'draft' ||
|
|
500
|
+
p.status === 'approved' ||
|
|
501
|
+
p.status === 'executing' ||
|
|
502
|
+
p.status === 'validating' ||
|
|
503
|
+
p.status === 'reconciling',
|
|
149
504
|
);
|
|
150
505
|
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Iterate on a draft plan — modify objective, scope, decisions, or tasks.
|
|
509
|
+
* Only allowed on plans in 'draft' status.
|
|
510
|
+
*/
|
|
511
|
+
iterate(
|
|
512
|
+
planId: string,
|
|
513
|
+
changes: {
|
|
514
|
+
objective?: string;
|
|
515
|
+
scope?: string;
|
|
516
|
+
decisions?: (string | PlanDecision)[];
|
|
517
|
+
addTasks?: Array<{ title: string; description: string }>;
|
|
518
|
+
removeTasks?: string[];
|
|
519
|
+
approach?: string;
|
|
520
|
+
context?: string;
|
|
521
|
+
success_criteria?: string[];
|
|
522
|
+
tool_chain?: string[];
|
|
523
|
+
flow?: string;
|
|
524
|
+
target_mode?: string;
|
|
525
|
+
},
|
|
526
|
+
): Plan {
|
|
527
|
+
const plan = this.get(planId);
|
|
528
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
529
|
+
if (plan.status !== 'draft' && plan.status !== 'brainstorming')
|
|
530
|
+
throw new Error(
|
|
531
|
+
`Cannot iterate plan in '${plan.status}' status — must be 'draft' or 'brainstorming'`,
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
const now = Date.now();
|
|
535
|
+
if (changes.objective !== undefined) plan.objective = changes.objective;
|
|
536
|
+
if (changes.scope !== undefined) plan.scope = changes.scope;
|
|
537
|
+
if (changes.decisions !== undefined) plan.decisions = changes.decisions;
|
|
538
|
+
if (changes.approach !== undefined) plan.approach = changes.approach;
|
|
539
|
+
if (changes.context !== undefined) plan.context = changes.context;
|
|
540
|
+
if (changes.success_criteria !== undefined) plan.success_criteria = changes.success_criteria;
|
|
541
|
+
if (changes.tool_chain !== undefined) plan.tool_chain = changes.tool_chain;
|
|
542
|
+
if (changes.flow !== undefined) plan.flow = changes.flow;
|
|
543
|
+
if (changes.target_mode !== undefined) plan.target_mode = changes.target_mode;
|
|
544
|
+
|
|
545
|
+
// Remove tasks by ID
|
|
546
|
+
if (changes.removeTasks && changes.removeTasks.length > 0) {
|
|
547
|
+
const removeSet = new Set(changes.removeTasks);
|
|
548
|
+
plan.tasks = plan.tasks.filter((t) => !removeSet.has(t.id));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Add new tasks
|
|
552
|
+
if (changes.addTasks && changes.addTasks.length > 0) {
|
|
553
|
+
const maxIndex = plan.tasks.reduce((max, t) => {
|
|
554
|
+
const num = parseInt(t.id.replace('task-', ''), 10);
|
|
555
|
+
return isNaN(num) ? max : Math.max(max, num);
|
|
556
|
+
}, 0);
|
|
557
|
+
for (let i = 0; i < changes.addTasks.length; i++) {
|
|
558
|
+
plan.tasks.push({
|
|
559
|
+
id: `task-${maxIndex + i + 1}`,
|
|
560
|
+
title: changes.addTasks[i].title,
|
|
561
|
+
description: changes.addTasks[i].description,
|
|
562
|
+
status: 'pending',
|
|
563
|
+
updatedAt: now,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
plan.updatedAt = now;
|
|
569
|
+
this.save();
|
|
570
|
+
return plan;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Split a plan's tasks into sub-tasks with dependency tracking.
|
|
575
|
+
* Replaces existing tasks with a new set that includes dependency references.
|
|
576
|
+
* Only allowed on 'draft' or 'approved' plans.
|
|
577
|
+
*/
|
|
578
|
+
splitTasks(
|
|
579
|
+
planId: string,
|
|
580
|
+
tasks: Array<{
|
|
581
|
+
title: string;
|
|
582
|
+
description: string;
|
|
583
|
+
dependsOn?: string[];
|
|
584
|
+
acceptanceCriteria?: string[];
|
|
585
|
+
}>,
|
|
586
|
+
): Plan {
|
|
587
|
+
const plan = this.get(planId);
|
|
588
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
589
|
+
if (plan.status !== 'brainstorming' && plan.status !== 'draft' && plan.status !== 'approved')
|
|
590
|
+
throw new Error(
|
|
591
|
+
`Cannot split tasks on plan in '${plan.status}' status — must be 'brainstorming', 'draft', or 'approved'`,
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
const now = Date.now();
|
|
595
|
+
plan.tasks = tasks.map((t, i) => ({
|
|
596
|
+
id: `task-${i + 1}`,
|
|
597
|
+
title: t.title,
|
|
598
|
+
description: t.description,
|
|
599
|
+
status: 'pending' as TaskStatus,
|
|
600
|
+
dependsOn: t.dependsOn,
|
|
601
|
+
...(t.acceptanceCriteria && { acceptanceCriteria: t.acceptanceCriteria }),
|
|
602
|
+
updatedAt: now,
|
|
603
|
+
}));
|
|
604
|
+
|
|
605
|
+
// Validate dependency references
|
|
606
|
+
const taskIds = new Set(plan.tasks.map((t) => t.id));
|
|
607
|
+
for (const task of plan.tasks) {
|
|
608
|
+
if (task.dependsOn) {
|
|
609
|
+
for (const dep of task.dependsOn) {
|
|
610
|
+
if (!taskIds.has(dep)) {
|
|
611
|
+
throw new Error(`Task '${task.id}' depends on unknown task '${dep}'`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
plan.updatedAt = now;
|
|
618
|
+
this.save();
|
|
619
|
+
return plan;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Reconcile a plan — compare what was planned vs what actually happened.
|
|
624
|
+
* Uses impact-weighted drift scoring (ported from Salvador's calculateDriftScore).
|
|
625
|
+
*
|
|
626
|
+
* Transitions: executing → reconciling → completed (automatic).
|
|
627
|
+
* Also allowed from 'validating' and 'reconciling' states.
|
|
628
|
+
*/
|
|
629
|
+
reconcile(
|
|
630
|
+
planId: string,
|
|
631
|
+
report: {
|
|
632
|
+
actualOutcome: string;
|
|
633
|
+
driftItems?: DriftItem[];
|
|
634
|
+
/** Who initiated the reconciliation. */
|
|
635
|
+
reconciledBy?: 'human' | 'auto';
|
|
636
|
+
},
|
|
637
|
+
): Plan {
|
|
638
|
+
const plan = this.get(planId);
|
|
639
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
640
|
+
if (
|
|
641
|
+
plan.status !== 'executing' &&
|
|
642
|
+
plan.status !== 'validating' &&
|
|
643
|
+
plan.status !== 'reconciling'
|
|
644
|
+
)
|
|
645
|
+
throw new Error(
|
|
646
|
+
`Cannot reconcile plan in '${plan.status}' status — must be 'executing', 'validating', or 'reconciling'`,
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
const driftItems = report.driftItems ?? [];
|
|
650
|
+
|
|
651
|
+
// Impact-weighted drift scoring (ported from Salvador)
|
|
652
|
+
const accuracy = calculateDriftScore(driftItems);
|
|
653
|
+
|
|
654
|
+
plan.reconciliation = {
|
|
655
|
+
planId,
|
|
656
|
+
accuracy,
|
|
657
|
+
driftItems,
|
|
658
|
+
summary: report.actualOutcome,
|
|
659
|
+
reconciledAt: Date.now(),
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
// Compute execution summary from per-task metrics
|
|
663
|
+
plan.executionSummary = this.computeExecutionSummary(plan);
|
|
664
|
+
|
|
665
|
+
// Transition through reconciling → completed via FSM
|
|
666
|
+
if (plan.status === 'executing' || plan.status === 'validating') {
|
|
667
|
+
plan.status = 'reconciling';
|
|
668
|
+
}
|
|
669
|
+
// Auto-complete after reconciliation
|
|
670
|
+
plan.status = 'completed';
|
|
671
|
+
plan.updatedAt = Date.now();
|
|
672
|
+
this.save();
|
|
673
|
+
return plan;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Add review evidence to a plan or specific task.
|
|
678
|
+
*/
|
|
679
|
+
addReview(
|
|
680
|
+
planId: string,
|
|
681
|
+
review: {
|
|
682
|
+
taskId?: string;
|
|
683
|
+
reviewer: string;
|
|
684
|
+
outcome: 'approved' | 'rejected' | 'needs_changes';
|
|
685
|
+
comments: string;
|
|
686
|
+
},
|
|
687
|
+
): Plan {
|
|
688
|
+
const plan = this.get(planId);
|
|
689
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
690
|
+
|
|
691
|
+
if (review.taskId) {
|
|
692
|
+
const task = plan.tasks.find((t) => t.id === review.taskId);
|
|
693
|
+
if (!task) throw new Error(`Task not found: ${review.taskId}`);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (!plan.reviews) plan.reviews = [];
|
|
697
|
+
plan.reviews.push({
|
|
698
|
+
planId,
|
|
699
|
+
taskId: review.taskId,
|
|
700
|
+
reviewer: review.reviewer,
|
|
701
|
+
outcome: review.outcome,
|
|
702
|
+
comments: review.comments,
|
|
703
|
+
reviewedAt: Date.now(),
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
plan.updatedAt = Date.now();
|
|
707
|
+
this.save();
|
|
708
|
+
return plan;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Get dispatch instructions for a specific task — returns the task and its
|
|
713
|
+
* unmet dependencies so a subagent knows what to work on and what to wait for.
|
|
714
|
+
*/
|
|
715
|
+
getDispatch(
|
|
716
|
+
planId: string,
|
|
717
|
+
taskId: string,
|
|
718
|
+
): {
|
|
719
|
+
task: PlanTask;
|
|
720
|
+
unmetDependencies: PlanTask[];
|
|
721
|
+
ready: boolean;
|
|
722
|
+
deliverableStatus?: { count: number; staleCount: number };
|
|
723
|
+
} {
|
|
724
|
+
const plan = this.get(planId);
|
|
725
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
726
|
+
const task = plan.tasks.find((t) => t.id === taskId);
|
|
727
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
728
|
+
|
|
729
|
+
const unmetDependencies: PlanTask[] = [];
|
|
730
|
+
if (task.dependsOn) {
|
|
731
|
+
for (const depId of task.dependsOn) {
|
|
732
|
+
const dep = plan.tasks.find((t) => t.id === depId);
|
|
733
|
+
if (dep && dep.status !== 'completed') {
|
|
734
|
+
unmetDependencies.push(dep);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const result: {
|
|
740
|
+
task: PlanTask;
|
|
741
|
+
unmetDependencies: PlanTask[];
|
|
742
|
+
ready: boolean;
|
|
743
|
+
deliverableStatus?: { count: number; staleCount: number };
|
|
744
|
+
} = {
|
|
745
|
+
task,
|
|
746
|
+
unmetDependencies,
|
|
747
|
+
ready: unmetDependencies.length === 0,
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
// Include deliverable status if deliverables exist
|
|
751
|
+
if (task.deliverables && task.deliverables.length > 0) {
|
|
752
|
+
result.deliverableStatus = {
|
|
753
|
+
count: task.deliverables.length,
|
|
754
|
+
staleCount: task.deliverables.filter((d) => d.stale).length,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// ─── Execution Metrics & Deliverables ──────────────────────────
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Compute aggregate execution summary from per-task metrics.
|
|
765
|
+
* Called from reconcile() and complete() to populate plan.executionSummary.
|
|
766
|
+
*/
|
|
767
|
+
private computeExecutionSummary(plan: Plan): ExecutionSummary {
|
|
768
|
+
let totalDurationMs = 0;
|
|
769
|
+
let tasksCompleted = 0;
|
|
770
|
+
let tasksSkipped = 0;
|
|
771
|
+
let tasksFailed = 0;
|
|
772
|
+
let tasksWithDuration = 0;
|
|
773
|
+
|
|
774
|
+
for (const task of plan.tasks) {
|
|
775
|
+
if (task.status === 'completed') tasksCompleted++;
|
|
776
|
+
else if (task.status === 'skipped') tasksSkipped++;
|
|
777
|
+
else if (task.status === 'failed') tasksFailed++;
|
|
778
|
+
|
|
779
|
+
if (task.metrics?.durationMs) {
|
|
780
|
+
totalDurationMs += task.metrics.durationMs;
|
|
781
|
+
tasksWithDuration++;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return {
|
|
786
|
+
totalDurationMs,
|
|
787
|
+
tasksCompleted,
|
|
788
|
+
tasksSkipped,
|
|
789
|
+
tasksFailed,
|
|
790
|
+
avgTaskDurationMs:
|
|
791
|
+
tasksWithDuration > 0 ? Math.round(totalDurationMs / tasksWithDuration) : 0,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Submit a deliverable for a task. Auto-computes SHA-256 hash for file deliverables.
|
|
797
|
+
*/
|
|
798
|
+
submitDeliverable(
|
|
799
|
+
planId: string,
|
|
800
|
+
taskId: string,
|
|
801
|
+
deliverable: { type: TaskDeliverable['type']; path: string; hash?: string },
|
|
802
|
+
): PlanTask {
|
|
803
|
+
const plan = this.get(planId);
|
|
804
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
805
|
+
const task = plan.tasks.find((t) => t.id === taskId);
|
|
806
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
807
|
+
|
|
808
|
+
const entry: TaskDeliverable = {
|
|
809
|
+
type: deliverable.type,
|
|
810
|
+
path: deliverable.path,
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
// Auto-compute hash for file deliverables
|
|
814
|
+
if (deliverable.type === 'file' && !deliverable.hash) {
|
|
815
|
+
try {
|
|
816
|
+
if (existsSync(deliverable.path)) {
|
|
817
|
+
const content = readFileSync(deliverable.path);
|
|
818
|
+
entry.hash = createHash('sha256').update(content).digest('hex');
|
|
819
|
+
}
|
|
820
|
+
} catch {
|
|
821
|
+
// Graceful degradation — skip hash if file can't be read
|
|
822
|
+
}
|
|
823
|
+
} else if (deliverable.hash) {
|
|
824
|
+
entry.hash = deliverable.hash;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (!task.deliverables) task.deliverables = [];
|
|
828
|
+
task.deliverables.push(entry);
|
|
829
|
+
task.updatedAt = Date.now();
|
|
830
|
+
plan.updatedAt = Date.now();
|
|
831
|
+
this.save();
|
|
832
|
+
return task;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Verify all deliverables for a task.
|
|
837
|
+
* - file: checks existsSync + SHA-256 hash match
|
|
838
|
+
* - vault_entry: checks vault.get(path) non-null (requires vault instance)
|
|
839
|
+
* - url: skips (just records, no fetch)
|
|
840
|
+
*/
|
|
841
|
+
verifyDeliverables(
|
|
842
|
+
planId: string,
|
|
843
|
+
taskId: string,
|
|
844
|
+
vault?: { get(id: string): unknown | null },
|
|
845
|
+
): { verified: boolean; deliverables: TaskDeliverable[]; staleCount: number } {
|
|
846
|
+
const plan = this.get(planId);
|
|
847
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
848
|
+
const task = plan.tasks.find((t) => t.id === taskId);
|
|
849
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
850
|
+
|
|
851
|
+
const deliverables = task.deliverables ?? [];
|
|
852
|
+
let staleCount = 0;
|
|
853
|
+
const now = Date.now();
|
|
854
|
+
|
|
855
|
+
for (const d of deliverables) {
|
|
856
|
+
d.stale = false;
|
|
857
|
+
|
|
858
|
+
if (d.type === 'file') {
|
|
859
|
+
if (!existsSync(d.path)) {
|
|
860
|
+
d.stale = true;
|
|
861
|
+
staleCount++;
|
|
862
|
+
} else if (d.hash) {
|
|
863
|
+
try {
|
|
864
|
+
const content = readFileSync(d.path);
|
|
865
|
+
const currentHash = createHash('sha256').update(content).digest('hex');
|
|
866
|
+
if (currentHash !== d.hash) {
|
|
867
|
+
d.stale = true;
|
|
868
|
+
staleCount++;
|
|
869
|
+
}
|
|
870
|
+
} catch {
|
|
871
|
+
d.stale = true;
|
|
872
|
+
staleCount++;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
d.verifiedAt = now;
|
|
876
|
+
} else if (d.type === 'vault_entry') {
|
|
877
|
+
if (vault) {
|
|
878
|
+
const entry = vault.get(d.path);
|
|
879
|
+
if (!entry) {
|
|
880
|
+
d.stale = true;
|
|
881
|
+
staleCount++;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
d.verifiedAt = now;
|
|
885
|
+
}
|
|
886
|
+
// url: skip — just record
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
plan.updatedAt = Date.now();
|
|
890
|
+
this.save();
|
|
891
|
+
|
|
892
|
+
return { verified: staleCount === 0, deliverables, staleCount };
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// ─── Evidence & Verification ────────────────────────────────────
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Submit evidence for a task acceptance criterion.
|
|
899
|
+
* Evidence is stored on the task and used by verifyTask() to check completeness.
|
|
900
|
+
*/
|
|
901
|
+
submitEvidence(
|
|
902
|
+
planId: string,
|
|
903
|
+
taskId: string,
|
|
904
|
+
evidence: { criterion: string; content: string; type: TaskEvidence['type'] },
|
|
905
|
+
): PlanTask {
|
|
906
|
+
const plan = this.get(planId);
|
|
907
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
908
|
+
const task = plan.tasks.find((t) => t.id === taskId);
|
|
909
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
910
|
+
if (!task.evidence) task.evidence = [];
|
|
911
|
+
task.evidence.push({
|
|
912
|
+
criterion: evidence.criterion,
|
|
913
|
+
content: evidence.content,
|
|
914
|
+
type: evidence.type,
|
|
915
|
+
submittedAt: Date.now(),
|
|
916
|
+
});
|
|
917
|
+
task.updatedAt = Date.now();
|
|
918
|
+
plan.updatedAt = Date.now();
|
|
919
|
+
this.save();
|
|
920
|
+
return task;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Verify a task — check that evidence exists for all acceptance criteria
|
|
925
|
+
* and any reviews have passed.
|
|
926
|
+
* Returns verification status with details.
|
|
927
|
+
*/
|
|
928
|
+
verifyTask(
|
|
929
|
+
planId: string,
|
|
930
|
+
taskId: string,
|
|
931
|
+
): {
|
|
932
|
+
verified: boolean;
|
|
933
|
+
task: PlanTask;
|
|
934
|
+
missingCriteria: string[];
|
|
935
|
+
reviewStatus: 'approved' | 'rejected' | 'needs_changes' | 'no_reviews';
|
|
936
|
+
} {
|
|
937
|
+
const plan = this.get(planId);
|
|
938
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
939
|
+
const task = plan.tasks.find((t) => t.id === taskId);
|
|
940
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
941
|
+
|
|
942
|
+
// Check evidence coverage
|
|
943
|
+
const criteria = task.acceptanceCriteria ?? [];
|
|
944
|
+
const evidencedCriteria = new Set((task.evidence ?? []).map((e) => e.criterion));
|
|
945
|
+
const missingCriteria = criteria.filter((c) => !evidencedCriteria.has(c));
|
|
946
|
+
|
|
947
|
+
// Check task-level reviews
|
|
948
|
+
const taskReviews = (plan.reviews ?? []).filter((r) => r.taskId === taskId);
|
|
949
|
+
let reviewStatus: 'approved' | 'rejected' | 'needs_changes' | 'no_reviews' = 'no_reviews';
|
|
950
|
+
if (taskReviews.length > 0) {
|
|
951
|
+
const latest = taskReviews[taskReviews.length - 1];
|
|
952
|
+
reviewStatus = latest.outcome;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const verified =
|
|
956
|
+
task.status === 'completed' &&
|
|
957
|
+
missingCriteria.length === 0 &&
|
|
958
|
+
(reviewStatus === 'approved' || reviewStatus === 'no_reviews');
|
|
959
|
+
|
|
960
|
+
if (verified !== task.verified) {
|
|
961
|
+
task.verified = verified;
|
|
962
|
+
task.updatedAt = Date.now();
|
|
963
|
+
plan.updatedAt = Date.now();
|
|
964
|
+
this.save();
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
return { verified, task, missingCriteria, reviewStatus };
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Verify an entire plan — check all tasks are in a final state,
|
|
972
|
+
* all verification-required tasks have evidence, no tasks stuck in_progress.
|
|
973
|
+
* Returns a validation report.
|
|
974
|
+
*/
|
|
975
|
+
verifyPlan(planId: string): {
|
|
976
|
+
valid: boolean;
|
|
977
|
+
planId: string;
|
|
978
|
+
issues: Array<{ taskId: string; issue: string }>;
|
|
979
|
+
summary: {
|
|
980
|
+
total: number;
|
|
981
|
+
completed: number;
|
|
982
|
+
skipped: number;
|
|
983
|
+
failed: number;
|
|
984
|
+
pending: number;
|
|
985
|
+
inProgress: number;
|
|
986
|
+
verified: number;
|
|
987
|
+
};
|
|
988
|
+
} {
|
|
989
|
+
const plan = this.get(planId);
|
|
990
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
991
|
+
|
|
992
|
+
const issues: Array<{ taskId: string; issue: string }> = [];
|
|
993
|
+
let verified = 0;
|
|
994
|
+
let completed = 0;
|
|
995
|
+
let skipped = 0;
|
|
996
|
+
let failed = 0;
|
|
997
|
+
let pending = 0;
|
|
998
|
+
let inProgress = 0;
|
|
999
|
+
|
|
1000
|
+
for (const task of plan.tasks) {
|
|
1001
|
+
switch (task.status) {
|
|
1002
|
+
case 'completed':
|
|
1003
|
+
completed++;
|
|
1004
|
+
break;
|
|
1005
|
+
case 'skipped':
|
|
1006
|
+
skipped++;
|
|
1007
|
+
break;
|
|
1008
|
+
case 'failed':
|
|
1009
|
+
failed++;
|
|
1010
|
+
break;
|
|
1011
|
+
case 'pending':
|
|
1012
|
+
pending++;
|
|
1013
|
+
break;
|
|
1014
|
+
case 'in_progress':
|
|
1015
|
+
inProgress++;
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (task.verified) verified++;
|
|
1020
|
+
|
|
1021
|
+
// Check for stuck tasks
|
|
1022
|
+
if (task.status === 'in_progress') {
|
|
1023
|
+
issues.push({ taskId: task.id, issue: 'Task stuck in in_progress state' });
|
|
1024
|
+
}
|
|
1025
|
+
if (task.status === 'pending') {
|
|
1026
|
+
issues.push({ taskId: task.id, issue: 'Task still pending — not started' });
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Check evidence for completed tasks with acceptance criteria
|
|
1030
|
+
if (
|
|
1031
|
+
task.status === 'completed' &&
|
|
1032
|
+
task.acceptanceCriteria &&
|
|
1033
|
+
task.acceptanceCriteria.length > 0
|
|
1034
|
+
) {
|
|
1035
|
+
const evidencedCriteria = new Set((task.evidence ?? []).map((e) => e.criterion));
|
|
1036
|
+
const missing = task.acceptanceCriteria.filter((c) => !evidencedCriteria.has(c));
|
|
1037
|
+
if (missing.length > 0) {
|
|
1038
|
+
issues.push({
|
|
1039
|
+
taskId: task.id,
|
|
1040
|
+
issue: `Missing evidence for ${missing.length} criteria: ${missing.join(', ')}`,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const valid = issues.length === 0 && pending === 0 && inProgress === 0;
|
|
1047
|
+
|
|
1048
|
+
return {
|
|
1049
|
+
valid,
|
|
1050
|
+
planId,
|
|
1051
|
+
issues,
|
|
1052
|
+
summary: {
|
|
1053
|
+
total: plan.tasks.length,
|
|
1054
|
+
completed,
|
|
1055
|
+
skipped,
|
|
1056
|
+
failed,
|
|
1057
|
+
pending,
|
|
1058
|
+
inProgress,
|
|
1059
|
+
verified,
|
|
1060
|
+
},
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Auto-reconcile a plan — fast path for plans with minimal drift.
|
|
1066
|
+
* Checks all tasks are in final state, generates reconciliation report automatically.
|
|
1067
|
+
* Returns null if drift is too significant for auto-reconciliation (>2 non-completed tasks).
|
|
1068
|
+
*/
|
|
1069
|
+
autoReconcile(planId: string): Plan | null {
|
|
1070
|
+
const plan = this.get(planId);
|
|
1071
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
1072
|
+
if (plan.status !== 'executing' && plan.status !== 'validating')
|
|
1073
|
+
throw new Error(
|
|
1074
|
+
`Cannot auto-reconcile plan in '${plan.status}' status — must be 'executing' or 'validating'`,
|
|
1075
|
+
);
|
|
1076
|
+
|
|
1077
|
+
const completed = plan.tasks.filter((t) => t.status === 'completed').length;
|
|
1078
|
+
const skipped = plan.tasks.filter((t) => t.status === 'skipped').length;
|
|
1079
|
+
const failed = plan.tasks.filter((t) => t.status === 'failed').length;
|
|
1080
|
+
const pending = plan.tasks.filter((t) => t.status === 'pending').length;
|
|
1081
|
+
const inProgress = plan.tasks.filter((t) => t.status === 'in_progress').length;
|
|
1082
|
+
|
|
1083
|
+
// Can't auto-reconcile if tasks are still in progress
|
|
1084
|
+
if (inProgress > 0) return null;
|
|
1085
|
+
// Can't auto-reconcile if too many non-completed tasks
|
|
1086
|
+
if (pending + failed > 2) return null;
|
|
1087
|
+
|
|
1088
|
+
const driftItems: DriftItem[] = [];
|
|
1089
|
+
|
|
1090
|
+
for (const task of plan.tasks) {
|
|
1091
|
+
if (task.status === 'skipped') {
|
|
1092
|
+
driftItems.push({
|
|
1093
|
+
type: 'skipped',
|
|
1094
|
+
description: `Task '${task.title}' was skipped`,
|
|
1095
|
+
impact: 'medium',
|
|
1096
|
+
rationale: 'Task not executed during plan implementation',
|
|
1097
|
+
});
|
|
1098
|
+
} else if (task.status === 'failed') {
|
|
1099
|
+
driftItems.push({
|
|
1100
|
+
type: 'modified',
|
|
1101
|
+
description: `Task '${task.title}' failed`,
|
|
1102
|
+
impact: 'high',
|
|
1103
|
+
rationale: 'Task execution failed',
|
|
1104
|
+
});
|
|
1105
|
+
} else if (task.status === 'pending') {
|
|
1106
|
+
driftItems.push({
|
|
1107
|
+
type: 'skipped',
|
|
1108
|
+
description: `Task '${task.title}' was never started`,
|
|
1109
|
+
impact: 'low',
|
|
1110
|
+
rationale: 'Task left in pending state',
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
return this.reconcile(planId, {
|
|
1116
|
+
actualOutcome: `Auto-reconciled: ${completed}/${plan.tasks.length} tasks completed, ${skipped} skipped, ${failed} failed`,
|
|
1117
|
+
driftItems,
|
|
1118
|
+
reconciledBy: 'auto',
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Generate a review prompt for spec compliance checking.
|
|
1124
|
+
* Used by subagent dispatch — the controller generates the prompt, a subagent executes it.
|
|
1125
|
+
*/
|
|
1126
|
+
generateReviewSpec(
|
|
1127
|
+
planId: string,
|
|
1128
|
+
taskId: string,
|
|
1129
|
+
): { prompt: string; task: PlanTask; plan: Plan } {
|
|
1130
|
+
const plan = this.get(planId);
|
|
1131
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
1132
|
+
const task = plan.tasks.find((t) => t.id === taskId);
|
|
1133
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
1134
|
+
|
|
1135
|
+
const criteria = task.acceptanceCriteria?.length
|
|
1136
|
+
? `\n\nAcceptance Criteria:\n${task.acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join('\n')}`
|
|
1137
|
+
: '';
|
|
1138
|
+
|
|
1139
|
+
const prompt = [
|
|
1140
|
+
`# Spec Compliance Review`,
|
|
1141
|
+
``,
|
|
1142
|
+
`## Task: ${task.title}`,
|
|
1143
|
+
`**Description:** ${task.description}`,
|
|
1144
|
+
`**Plan Objective:** ${plan.objective}${criteria}`,
|
|
1145
|
+
``,
|
|
1146
|
+
`## Review Checklist`,
|
|
1147
|
+
`1. Does the implementation match the task description?`,
|
|
1148
|
+
`2. Are all acceptance criteria satisfied?`,
|
|
1149
|
+
`3. Does it align with the plan's overall objective?`,
|
|
1150
|
+
`4. Are there any spec deviations?`,
|
|
1151
|
+
``,
|
|
1152
|
+
`Provide: outcome (approved/rejected/needs_changes) and detailed comments.`,
|
|
1153
|
+
].join('\n');
|
|
1154
|
+
|
|
1155
|
+
return { prompt, task, plan };
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Generate a review prompt for code quality checking.
|
|
1160
|
+
*/
|
|
1161
|
+
generateReviewQuality(
|
|
1162
|
+
planId: string,
|
|
1163
|
+
taskId: string,
|
|
1164
|
+
): { prompt: string; task: PlanTask; plan: Plan } {
|
|
1165
|
+
const plan = this.get(planId);
|
|
1166
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
1167
|
+
const task = plan.tasks.find((t) => t.id === taskId);
|
|
1168
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
1169
|
+
|
|
1170
|
+
const prompt = [
|
|
1171
|
+
`# Code Quality Review`,
|
|
1172
|
+
``,
|
|
1173
|
+
`## Task: ${task.title}`,
|
|
1174
|
+
`**Description:** ${task.description}`,
|
|
1175
|
+
``,
|
|
1176
|
+
`## Quality Checklist`,
|
|
1177
|
+
`1. **Correctness** — Does it work as intended?`,
|
|
1178
|
+
`2. **Security** — No injection, XSS, or OWASP top 10 vulnerabilities?`,
|
|
1179
|
+
`3. **Performance** — No unnecessary allocations, N+1 queries, or blocking calls?`,
|
|
1180
|
+
`4. **Maintainability** — Clear naming, appropriate abstractions, documented intent?`,
|
|
1181
|
+
`5. **Testing** — Adequate test coverage for the changes?`,
|
|
1182
|
+
`6. **Error Handling** — Graceful degradation, no swallowed errors?`,
|
|
1183
|
+
`7. **Conventions** — Follows project coding standards?`,
|
|
1184
|
+
``,
|
|
1185
|
+
`Provide: outcome (approved/rejected/needs_changes) and detailed comments.`,
|
|
1186
|
+
].join('\n');
|
|
1187
|
+
|
|
1188
|
+
return { prompt, task, plan };
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Archive completed plans — transitions them to 'archived' status.
|
|
1193
|
+
* If olderThanDays is provided, only archives plans older than that.
|
|
1194
|
+
* Returns the archived plans.
|
|
1195
|
+
*/
|
|
1196
|
+
archive(olderThanDays?: number): Plan[] {
|
|
1197
|
+
const cutoff =
|
|
1198
|
+
olderThanDays !== undefined
|
|
1199
|
+
? Date.now() - olderThanDays * 24 * 60 * 60 * 1000
|
|
1200
|
+
: Date.now() + 1; // +1ms so archive() with no args archives all completed plans
|
|
1201
|
+
const toArchive = this.store.plans.filter(
|
|
1202
|
+
(p) => p.status === 'completed' && p.updatedAt < cutoff,
|
|
1203
|
+
);
|
|
1204
|
+
for (const plan of toArchive) {
|
|
1205
|
+
plan.status = 'archived';
|
|
1206
|
+
plan.updatedAt = Date.now();
|
|
1207
|
+
}
|
|
1208
|
+
if (toArchive.length > 0) {
|
|
1209
|
+
this.save();
|
|
1210
|
+
}
|
|
1211
|
+
return toArchive;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Get statistics about all plans.
|
|
1216
|
+
*/
|
|
1217
|
+
stats(): {
|
|
1218
|
+
total: number;
|
|
1219
|
+
byStatus: Record<PlanStatus, number>;
|
|
1220
|
+
avgTasksPerPlan: number;
|
|
1221
|
+
totalTasks: number;
|
|
1222
|
+
tasksByStatus: Record<TaskStatus, number>;
|
|
1223
|
+
} {
|
|
1224
|
+
const plans = this.store.plans;
|
|
1225
|
+
const byStatus: Record<PlanStatus, number> = {
|
|
1226
|
+
brainstorming: 0,
|
|
1227
|
+
draft: 0,
|
|
1228
|
+
approved: 0,
|
|
1229
|
+
executing: 0,
|
|
1230
|
+
validating: 0,
|
|
1231
|
+
reconciling: 0,
|
|
1232
|
+
completed: 0,
|
|
1233
|
+
archived: 0,
|
|
1234
|
+
};
|
|
1235
|
+
const tasksByStatus: Record<TaskStatus, number> = {
|
|
1236
|
+
pending: 0,
|
|
1237
|
+
in_progress: 0,
|
|
1238
|
+
completed: 0,
|
|
1239
|
+
skipped: 0,
|
|
1240
|
+
failed: 0,
|
|
1241
|
+
};
|
|
1242
|
+
let totalTasks = 0;
|
|
1243
|
+
|
|
1244
|
+
for (const p of plans) {
|
|
1245
|
+
byStatus[p.status]++;
|
|
1246
|
+
totalTasks += p.tasks.length;
|
|
1247
|
+
for (const t of p.tasks) {
|
|
1248
|
+
tasksByStatus[t.status]++;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
return {
|
|
1253
|
+
total: plans.length,
|
|
1254
|
+
byStatus,
|
|
1255
|
+
avgTasksPerPlan: plans.length > 0 ? Math.round((totalTasks / plans.length) * 100) / 100 : 0,
|
|
1256
|
+
totalTasks,
|
|
1257
|
+
tasksByStatus,
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// ─── Grading ──────────────────────────────────────────────────────
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Grade a plan using gap analysis with severity-weighted scoring.
|
|
1265
|
+
* Ported from Salvador MCP's multi-pass grading engine.
|
|
1266
|
+
*
|
|
1267
|
+
* 6 built-in passes + optional custom passes (domain-specific checks).
|
|
1268
|
+
*
|
|
1269
|
+
* Scoring:
|
|
1270
|
+
* - Each gap has a severity (critical=30, major=15, minor=2, info=0)
|
|
1271
|
+
* - Deductions are per-category with optional caps
|
|
1272
|
+
* - Iteration leniency: minor gaps free on iter 1, half on iter 2, full on 3+
|
|
1273
|
+
* - Score = max(0, 100 - deductions)
|
|
1274
|
+
*
|
|
1275
|
+
* Grade thresholds: A+=95, A=90, B=80, C=70, D=60, F=<60
|
|
1276
|
+
*/
|
|
1277
|
+
grade(planId: string): PlanCheck {
|
|
1278
|
+
const plan = this.get(planId);
|
|
1279
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
1280
|
+
|
|
1281
|
+
// Run 6-pass gap analysis
|
|
1282
|
+
const gaps = runGapAnalysis(plan, this.gapOptions);
|
|
1283
|
+
|
|
1284
|
+
// Add circular dependency check (structural, not covered by gap-analysis passes)
|
|
1285
|
+
if (this.hasCircularDependencies(plan)) {
|
|
1286
|
+
gaps.push({
|
|
1287
|
+
id: `gap_${Date.now()}_circ`,
|
|
1288
|
+
severity: 'critical',
|
|
1289
|
+
category: 'structure',
|
|
1290
|
+
description: 'Circular dependencies detected among tasks.',
|
|
1291
|
+
recommendation: 'Remove circular dependency chains so tasks can be executed in order.',
|
|
1292
|
+
location: 'tasks',
|
|
1293
|
+
_trigger: 'circular_dependencies',
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Iteration = number of previous checks + 1
|
|
1298
|
+
const iteration = plan.checks.length + 1;
|
|
1299
|
+
const score = calculateScore(gaps, iteration);
|
|
1300
|
+
const grade = this.scoreToGrade(score);
|
|
1301
|
+
|
|
1302
|
+
const check: PlanCheck = {
|
|
1303
|
+
checkId: `chk-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1304
|
+
planId,
|
|
1305
|
+
grade,
|
|
1306
|
+
score,
|
|
1307
|
+
gaps,
|
|
1308
|
+
iteration,
|
|
1309
|
+
checkedAt: Date.now(),
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
plan.checks.push(check);
|
|
1313
|
+
plan.latestCheck = check;
|
|
1314
|
+
plan.updatedAt = Date.now();
|
|
1315
|
+
this.save();
|
|
1316
|
+
|
|
1317
|
+
return check;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Get the latest check for a plan.
|
|
1322
|
+
*/
|
|
1323
|
+
getLatestCheck(planId: string): PlanCheck | null {
|
|
1324
|
+
const plan = this.get(planId);
|
|
1325
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
1326
|
+
return plan.latestCheck ?? null;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Get all checks for a plan (history).
|
|
1331
|
+
*/
|
|
1332
|
+
getCheckHistory(planId: string): PlanCheck[] {
|
|
1333
|
+
const plan = this.get(planId);
|
|
1334
|
+
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
1335
|
+
return [...plan.checks];
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Auto-grade: grade the plan and return whether it meets a target grade.
|
|
1340
|
+
*/
|
|
1341
|
+
meetsGrade(planId: string, targetGrade: PlanGrade): { meets: boolean; check: PlanCheck } {
|
|
1342
|
+
const check = this.grade(planId);
|
|
1343
|
+
const targetScore = this.gradeToMinScore(targetGrade);
|
|
1344
|
+
return { meets: check.score >= targetScore, check };
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// ─── Grading Helpers ──────────────────────────────────────────────
|
|
1348
|
+
|
|
1349
|
+
private scoreToGrade(score: number): PlanGrade {
|
|
1350
|
+
if (score >= 95) return 'A+';
|
|
1351
|
+
if (score >= 90) return 'A';
|
|
1352
|
+
if (score >= 80) return 'B';
|
|
1353
|
+
if (score >= 70) return 'C';
|
|
1354
|
+
if (score >= 60) return 'D';
|
|
1355
|
+
return 'F';
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
private gradeToMinScore(grade: PlanGrade): number {
|
|
1359
|
+
switch (grade) {
|
|
1360
|
+
case 'A+':
|
|
1361
|
+
return 95;
|
|
1362
|
+
case 'A':
|
|
1363
|
+
return 90;
|
|
1364
|
+
case 'B':
|
|
1365
|
+
return 80;
|
|
1366
|
+
case 'C':
|
|
1367
|
+
return 70;
|
|
1368
|
+
case 'D':
|
|
1369
|
+
return 60;
|
|
1370
|
+
case 'F':
|
|
1371
|
+
return 0;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
private hasCircularDependencies(plan: Plan): boolean {
|
|
1376
|
+
const visited = new Set<string>();
|
|
1377
|
+
const inStack = new Set<string>();
|
|
1378
|
+
const taskMap = new Map(plan.tasks.map((t) => [t.id, t]));
|
|
1379
|
+
|
|
1380
|
+
const dfs = (taskId: string): boolean => {
|
|
1381
|
+
if (inStack.has(taskId)) return true;
|
|
1382
|
+
if (visited.has(taskId)) return false;
|
|
1383
|
+
visited.add(taskId);
|
|
1384
|
+
inStack.add(taskId);
|
|
1385
|
+
const task = taskMap.get(taskId);
|
|
1386
|
+
if (task?.dependsOn) {
|
|
1387
|
+
for (const dep of task.dependsOn) {
|
|
1388
|
+
if (dfs(dep)) return true;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
inStack.delete(taskId);
|
|
1392
|
+
return false;
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
for (const task of plan.tasks) {
|
|
1396
|
+
if (dfs(task.id)) return true;
|
|
1397
|
+
}
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
151
1400
|
}
|