@soleri/core 2.4.0 → 2.6.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 +7 -0
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +56 -9
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts +1 -0
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +164 -148
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/types.d.ts +2 -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 +3 -1
- package/dist/control/identity-manager.d.ts.map +1 -1
- package/dist/control/identity-manager.js +49 -51
- package/dist/control/identity-manager.js.map +1 -1
- package/dist/control/intent-router.d.ts +1 -0
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +32 -32
- package/dist/control/intent-router.js.map +1 -1
- package/dist/curator/curator.d.ts +9 -1
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +104 -92
- 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/governance/governance.d.ts +1 -0
- package/dist/governance/governance.d.ts.map +1 -1
- package/dist/governance/governance.js +51 -68
- package/dist/governance/governance.js.map +1 -1
- package/dist/index.d.ts +26 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -3
- 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/loop/loop-manager.d.ts +58 -7
- package/dist/loop/loop-manager.d.ts.map +1 -1
- package/dist/loop/loop-manager.js +280 -6
- package/dist/loop/loop-manager.js.map +1 -1
- package/dist/loop/types.d.ts +69 -1
- package/dist/loop/types.d.ts.map +1 -1
- package/dist/loop/types.js +4 -1
- package/dist/loop/types.js.map +1 -1
- package/dist/persistence/index.d.ts +4 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/index.js +3 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/postgres-provider.d.ts +46 -0
- package/dist/persistence/postgres-provider.d.ts.map +1 -0
- package/dist/persistence/postgres-provider.js +115 -0
- package/dist/persistence/postgres-provider.js.map +1 -0
- package/dist/persistence/sqlite-provider.d.ts +28 -0
- package/dist/persistence/sqlite-provider.d.ts.map +1 -0
- package/dist/persistence/sqlite-provider.js +97 -0
- package/dist/persistence/sqlite-provider.js.map +1 -0
- package/dist/persistence/types.d.ts +58 -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 +47 -4
- package/dist/planning/gap-analysis.d.ts.map +1 -1
- package/dist/planning/gap-analysis.js +190 -13
- package/dist/planning/gap-analysis.js.map +1 -1
- package/dist/planning/gap-types.d.ts +1 -1
- package/dist/planning/gap-types.d.ts.map +1 -1
- package/dist/planning/gap-types.js.map +1 -1
- package/dist/planning/planner.d.ts +277 -9
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +611 -46
- 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 +4 -4
- package/dist/project/project-registry.d.ts.map +1 -1
- package/dist/project/project-registry.js +30 -57
- package/dist/project/project-registry.js.map +1 -1
- 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 +5 -3
- package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
- package/dist/runtime/admin-extra-ops.js +348 -11
- package/dist/runtime/admin-extra-ops.js.map +1 -1
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +10 -3
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +20 -2
- package/dist/runtime/capture-ops.js.map +1 -1
- 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 +8 -6
- package/dist/runtime/core-ops.d.ts.map +1 -1
- package/dist/runtime/core-ops.js +226 -9
- package/dist/runtime/core-ops.js.map +1 -1
- package/dist/runtime/curator-extra-ops.d.ts +2 -2
- package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
- package/dist/runtime/curator-extra-ops.js +15 -3
- package/dist/runtime/curator-extra-ops.js.map +1 -1
- package/dist/runtime/domain-ops.js +2 -2
- package/dist/runtime/domain-ops.js.map +1 -1
- package/dist/runtime/grading-ops.d.ts.map +1 -1
- package/dist/runtime/grading-ops.js.map +1 -1
- 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 +5 -4
- package/dist/runtime/loop-ops.d.ts.map +1 -1
- package/dist/runtime/loop-ops.js +84 -12
- package/dist/runtime/loop-ops.js.map +1 -1
- package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -1
- package/dist/runtime/memory-cross-project-ops.js.map +1 -1
- package/dist/runtime/memory-extra-ops.js +5 -5
- package/dist/runtime/memory-extra-ops.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +8 -2
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/planning-extra-ops.d.ts +13 -5
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +381 -18
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- 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.map +1 -1
- package/dist/runtime/project-ops.js +7 -2
- package/dist/runtime/project-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +28 -9
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +8 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/vault-extra-ops.d.ts +4 -2
- package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
- package/dist/runtime/vault-extra-ops.js +383 -4
- package/dist/runtime/vault-extra-ops.js.map +1 -1
- 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 +52 -32
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +300 -181
- package/dist/vault/vault.js.map +1 -1
- package/package.json +9 -3
- package/src/__tests__/admin-extra-ops.test.ts +62 -15
- package/src/__tests__/admin-ops.test.ts +2 -2
- package/src/__tests__/brain.test.ts +3 -3
- 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 +36 -4
- package/src/__tests__/curator-extra-ops.test.ts +24 -2
- package/src/__tests__/errors.test.ts +388 -0
- package/src/__tests__/grading-ops.test.ts +28 -7
- package/src/__tests__/intake-pipeline.test.ts +162 -0
- package/src/__tests__/loop-ops.test.ts +74 -3
- package/src/__tests__/memory-cross-project-ops.test.ts +3 -1
- package/src/__tests__/orchestrate-ops.test.ts +8 -3
- package/src/__tests__/persistence.test.ts +291 -0
- package/src/__tests__/planner.test.ts +99 -21
- package/src/__tests__/planning-extra-ops.test.ts +168 -10
- 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__/postgres-provider.test.ts +58 -0
- package/src/__tests__/project-ops.test.ts +18 -4
- package/src/__tests__/template-manager.test.ts +222 -0
- package/src/__tests__/vault-extra-ops.test.ts +82 -7
- package/src/__tests__/vault.test.ts +184 -0
- package/src/brain/brain.ts +71 -9
- package/src/brain/intelligence.ts +258 -307
- package/src/brain/types.ts +2 -2
- package/src/cognee/client.ts +18 -0
- package/src/cognee/sync-manager.ts +389 -0
- package/src/control/identity-manager.ts +77 -75
- package/src/control/intent-router.ts +55 -57
- package/src/curator/curator.ts +199 -139
- 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 +90 -107
- package/src/index.ts +116 -3
- 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/loop/loop-manager.ts +325 -7
- package/src/loop/types.ts +72 -1
- package/src/persistence/index.ts +9 -0
- package/src/persistence/postgres-provider.ts +157 -0
- package/src/persistence/sqlite-provider.ts +115 -0
- package/src/persistence/types.ts +74 -0
- package/src/planning/gap-analysis.ts +286 -17
- package/src/planning/gap-types.ts +4 -1
- package/src/planning/planner.ts +828 -55
- 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 +81 -74
- 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 +391 -13
- package/src/runtime/admin-ops.ts +17 -6
- package/src/runtime/capture-ops.ts +25 -6
- package/src/runtime/cognee-sync-ops.ts +63 -0
- package/src/runtime/core-ops.ts +258 -8
- package/src/runtime/curator-extra-ops.ts +17 -3
- package/src/runtime/domain-ops.ts +2 -2
- package/src/runtime/grading-ops.ts +11 -2
- package/src/runtime/intake-ops.ts +126 -0
- package/src/runtime/loop-ops.ts +96 -13
- package/src/runtime/memory-cross-project-ops.ts +1 -2
- package/src/runtime/memory-extra-ops.ts +5 -5
- package/src/runtime/orchestrate-ops.ts +8 -2
- package/src/runtime/planning-extra-ops.ts +414 -23
- package/src/runtime/playbook-ops.ts +169 -0
- package/src/runtime/project-ops.ts +9 -3
- package/src/runtime/runtime.ts +36 -10
- package/src/runtime/types.ts +8 -0
- package/src/runtime/vault-extra-ops.ts +425 -4
- package/src/vault/playbook.ts +87 -0
- package/src/vault/vault.ts +419 -235
package/src/planning/planner.ts
CHANGED
|
@@ -1,13 +1,113 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { dirname } from 'node:path';
|
|
3
4
|
import type { PlanGap } from './gap-types.js';
|
|
4
5
|
import { SEVERITY_WEIGHTS, CATEGORY_PENALTY_CAPS } from './gap-types.js';
|
|
5
6
|
import { runGapAnalysis } from './gap-analysis.js';
|
|
6
7
|
import type { GapAnalysisOptions } from './gap-analysis.js';
|
|
7
8
|
|
|
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
|
+
}
|
|
74
|
+
|
|
9
75
|
export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'skipped' | 'failed';
|
|
10
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
|
+
|
|
11
111
|
export interface PlanTask {
|
|
12
112
|
id: string;
|
|
13
113
|
title: string;
|
|
@@ -15,20 +115,64 @@ export interface PlanTask {
|
|
|
15
115
|
status: TaskStatus;
|
|
16
116
|
/** Optional dependency IDs — tasks that must complete before this one. */
|
|
17
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[];
|
|
18
132
|
updatedAt: number;
|
|
19
133
|
}
|
|
20
134
|
|
|
21
135
|
export interface DriftItem {
|
|
136
|
+
/** Type of drift */
|
|
22
137
|
type: 'skipped' | 'added' | 'modified' | 'reordered';
|
|
138
|
+
/** What drifted */
|
|
23
139
|
description: string;
|
|
140
|
+
/** How much this affected the plan */
|
|
24
141
|
impact: 'low' | 'medium' | 'high';
|
|
142
|
+
/** Why the drift occurred */
|
|
25
143
|
rationale: string;
|
|
26
144
|
}
|
|
27
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
|
+
|
|
28
170
|
export interface ReconciliationReport {
|
|
29
171
|
planId: string;
|
|
172
|
+
/** Accuracy score: 100 = perfect execution, 0 = total drift. Impact-weighted. */
|
|
30
173
|
accuracy: number;
|
|
31
174
|
driftItems: DriftItem[];
|
|
175
|
+
/** Human-readable summary of the drift */
|
|
32
176
|
summary: string;
|
|
33
177
|
reconciledAt: number;
|
|
34
178
|
}
|
|
@@ -88,13 +232,38 @@ export function calculateScore(gaps: PlanGap[], iteration: number = 1): number {
|
|
|
88
232
|
return Math.max(0, 100 - deductions);
|
|
89
233
|
}
|
|
90
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
|
+
|
|
91
244
|
export interface Plan {
|
|
92
245
|
id: string;
|
|
93
246
|
objective: string;
|
|
94
247
|
scope: string;
|
|
95
248
|
status: PlanStatus;
|
|
96
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Decisions can be flat strings (backward compat) or structured {decision, rationale}.
|
|
251
|
+
* New plans should prefer PlanDecision[].
|
|
252
|
+
*/
|
|
253
|
+
decisions: (string | PlanDecision)[];
|
|
97
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;
|
|
98
267
|
/** Reconciliation report — populated by reconcile(). */
|
|
99
268
|
reconciliation?: ReconciliationReport;
|
|
100
269
|
/** Review evidence — populated by addReview(). */
|
|
@@ -103,6 +272,14 @@ export interface Plan {
|
|
|
103
272
|
latestCheck?: PlanCheck;
|
|
104
273
|
/** All check history. */
|
|
105
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;
|
|
106
283
|
createdAt: number;
|
|
107
284
|
updatedAt: number;
|
|
108
285
|
}
|
|
@@ -148,15 +325,23 @@ export class Planner {
|
|
|
148
325
|
create(params: {
|
|
149
326
|
objective: string;
|
|
150
327
|
scope: string;
|
|
151
|
-
decisions?: string[];
|
|
328
|
+
decisions?: (string | PlanDecision)[];
|
|
152
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';
|
|
153
338
|
}): Plan {
|
|
154
339
|
const now = Date.now();
|
|
155
340
|
const plan: Plan = {
|
|
156
341
|
id: `plan-${now}-${Math.random().toString(36).slice(2, 8)}`,
|
|
157
342
|
objective: params.objective,
|
|
158
343
|
scope: params.scope,
|
|
159
|
-
status: 'draft',
|
|
344
|
+
status: params.initialStatus ?? 'draft',
|
|
160
345
|
decisions: params.decisions ?? [],
|
|
161
346
|
tasks: (params.tasks ?? []).map((t, i) => ({
|
|
162
347
|
id: `task-${i + 1}`,
|
|
@@ -165,6 +350,12 @@ export class Planner {
|
|
|
165
350
|
status: 'pending' as TaskStatus,
|
|
166
351
|
updatedAt: now,
|
|
167
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 }),
|
|
168
359
|
checks: [],
|
|
169
360
|
createdAt: now,
|
|
170
361
|
updatedAt: now,
|
|
@@ -182,13 +373,38 @@ export class Planner {
|
|
|
182
373
|
return [...this.store.plans];
|
|
183
374
|
}
|
|
184
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
|
+
|
|
185
404
|
approve(planId: string): Plan {
|
|
186
405
|
const plan = this.get(planId);
|
|
187
406
|
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
188
|
-
|
|
189
|
-
throw new Error(`Cannot approve plan in '${plan.status}' status — must be 'draft'`);
|
|
190
|
-
plan.status = 'approved';
|
|
191
|
-
plan.updatedAt = Date.now();
|
|
407
|
+
this.transition(plan, 'approved');
|
|
192
408
|
this.save();
|
|
193
409
|
return plan;
|
|
194
410
|
}
|
|
@@ -196,10 +412,7 @@ export class Planner {
|
|
|
196
412
|
startExecution(planId: string): Plan {
|
|
197
413
|
const plan = this.get(planId);
|
|
198
414
|
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
199
|
-
|
|
200
|
-
throw new Error(`Cannot execute plan in '${plan.status}' status — must be 'approved'`);
|
|
201
|
-
plan.status = 'executing';
|
|
202
|
-
plan.updatedAt = Date.now();
|
|
415
|
+
this.transition(plan, 'executing');
|
|
203
416
|
this.save();
|
|
204
417
|
return plan;
|
|
205
418
|
}
|
|
@@ -207,37 +420,87 @@ export class Planner {
|
|
|
207
420
|
updateTask(planId: string, taskId: string, status: TaskStatus): Plan {
|
|
208
421
|
const plan = this.get(planId);
|
|
209
422
|
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
210
|
-
if (plan.status !== 'executing')
|
|
423
|
+
if (plan.status !== 'executing' && plan.status !== 'validating')
|
|
211
424
|
throw new Error(
|
|
212
|
-
`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'`,
|
|
213
426
|
);
|
|
214
427
|
const task = plan.tasks.find((t) => t.id === taskId);
|
|
215
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
|
+
|
|
216
446
|
task.status = status;
|
|
217
|
-
task.updatedAt =
|
|
218
|
-
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');
|
|
219
473
|
this.save();
|
|
220
474
|
return plan;
|
|
221
475
|
}
|
|
222
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
|
+
*/
|
|
223
482
|
complete(planId: string): Plan {
|
|
224
483
|
const plan = this.get(planId);
|
|
225
484
|
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
plan.status = 'completed';
|
|
229
|
-
plan.updatedAt = Date.now();
|
|
485
|
+
plan.executionSummary = this.computeExecutionSummary(plan);
|
|
486
|
+
this.transition(plan, 'completed');
|
|
230
487
|
this.save();
|
|
231
488
|
return plan;
|
|
232
489
|
}
|
|
233
490
|
|
|
234
491
|
getExecuting(): Plan[] {
|
|
235
|
-
return this.store.plans.filter((p) => p.status === 'executing');
|
|
492
|
+
return this.store.plans.filter((p) => p.status === 'executing' || p.status === 'validating');
|
|
236
493
|
}
|
|
237
494
|
|
|
238
495
|
getActive(): Plan[] {
|
|
239
496
|
return this.store.plans.filter(
|
|
240
|
-
(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',
|
|
241
504
|
);
|
|
242
505
|
}
|
|
243
506
|
|
|
@@ -250,20 +513,34 @@ export class Planner {
|
|
|
250
513
|
changes: {
|
|
251
514
|
objective?: string;
|
|
252
515
|
scope?: string;
|
|
253
|
-
decisions?: string[];
|
|
516
|
+
decisions?: (string | PlanDecision)[];
|
|
254
517
|
addTasks?: Array<{ title: string; description: string }>;
|
|
255
518
|
removeTasks?: string[];
|
|
519
|
+
approach?: string;
|
|
520
|
+
context?: string;
|
|
521
|
+
success_criteria?: string[];
|
|
522
|
+
tool_chain?: string[];
|
|
523
|
+
flow?: string;
|
|
524
|
+
target_mode?: string;
|
|
256
525
|
},
|
|
257
526
|
): Plan {
|
|
258
527
|
const plan = this.get(planId);
|
|
259
528
|
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
260
|
-
if (plan.status !== 'draft')
|
|
261
|
-
throw new Error(
|
|
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
|
+
);
|
|
262
533
|
|
|
263
534
|
const now = Date.now();
|
|
264
535
|
if (changes.objective !== undefined) plan.objective = changes.objective;
|
|
265
536
|
if (changes.scope !== undefined) plan.scope = changes.scope;
|
|
266
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;
|
|
267
544
|
|
|
268
545
|
// Remove tasks by ID
|
|
269
546
|
if (changes.removeTasks && changes.removeTasks.length > 0) {
|
|
@@ -300,13 +577,18 @@ export class Planner {
|
|
|
300
577
|
*/
|
|
301
578
|
splitTasks(
|
|
302
579
|
planId: string,
|
|
303
|
-
tasks: Array<{
|
|
580
|
+
tasks: Array<{
|
|
581
|
+
title: string;
|
|
582
|
+
description: string;
|
|
583
|
+
dependsOn?: string[];
|
|
584
|
+
acceptanceCriteria?: string[];
|
|
585
|
+
}>,
|
|
304
586
|
): Plan {
|
|
305
587
|
const plan = this.get(planId);
|
|
306
588
|
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
307
|
-
if (plan.status !== 'draft' && plan.status !== 'approved')
|
|
589
|
+
if (plan.status !== 'brainstorming' && plan.status !== 'draft' && plan.status !== 'approved')
|
|
308
590
|
throw new Error(
|
|
309
|
-
`Cannot split tasks on plan in '${plan.status}' status — must be 'draft' or 'approved'`,
|
|
591
|
+
`Cannot split tasks on plan in '${plan.status}' status — must be 'brainstorming', 'draft', or 'approved'`,
|
|
310
592
|
);
|
|
311
593
|
|
|
312
594
|
const now = Date.now();
|
|
@@ -316,6 +598,7 @@ export class Planner {
|
|
|
316
598
|
description: t.description,
|
|
317
599
|
status: 'pending' as TaskStatus,
|
|
318
600
|
dependsOn: t.dependsOn,
|
|
601
|
+
...(t.acceptanceCriteria && { acceptanceCriteria: t.acceptanceCriteria }),
|
|
319
602
|
updatedAt: now,
|
|
320
603
|
}));
|
|
321
604
|
|
|
@@ -338,39 +621,53 @@ export class Planner {
|
|
|
338
621
|
|
|
339
622
|
/**
|
|
340
623
|
* Reconcile a plan — compare what was planned vs what actually happened.
|
|
341
|
-
*
|
|
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.
|
|
342
628
|
*/
|
|
343
629
|
reconcile(
|
|
344
630
|
planId: string,
|
|
345
631
|
report: {
|
|
346
632
|
actualOutcome: string;
|
|
347
633
|
driftItems?: DriftItem[];
|
|
634
|
+
/** Who initiated the reconciliation. */
|
|
635
|
+
reconciledBy?: 'human' | 'auto';
|
|
348
636
|
},
|
|
349
637
|
): Plan {
|
|
350
638
|
const plan = this.get(planId);
|
|
351
639
|
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
352
|
-
if (
|
|
640
|
+
if (
|
|
641
|
+
plan.status !== 'executing' &&
|
|
642
|
+
plan.status !== 'validating' &&
|
|
643
|
+
plan.status !== 'reconciling'
|
|
644
|
+
)
|
|
353
645
|
throw new Error(
|
|
354
|
-
`Cannot reconcile plan in '${plan.status}' status — must be 'executing' or '
|
|
646
|
+
`Cannot reconcile plan in '${plan.status}' status — must be 'executing', 'validating', or 'reconciling'`,
|
|
355
647
|
);
|
|
356
648
|
|
|
357
649
|
const driftItems = report.driftItems ?? [];
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const accuracy =
|
|
650
|
+
|
|
651
|
+
// Impact-weighted drift scoring (ported from Salvador)
|
|
652
|
+
const accuracy = calculateDriftScore(driftItems);
|
|
361
653
|
|
|
362
654
|
plan.reconciliation = {
|
|
363
655
|
planId,
|
|
364
|
-
accuracy
|
|
656
|
+
accuracy,
|
|
365
657
|
driftItems,
|
|
366
658
|
summary: report.actualOutcome,
|
|
367
659
|
reconciledAt: Date.now(),
|
|
368
660
|
};
|
|
369
661
|
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
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';
|
|
373
668
|
}
|
|
669
|
+
// Auto-complete after reconciliation
|
|
670
|
+
plan.status = 'completed';
|
|
374
671
|
plan.updatedAt = Date.now();
|
|
375
672
|
this.save();
|
|
376
673
|
return plan;
|
|
@@ -418,7 +715,12 @@ export class Planner {
|
|
|
418
715
|
getDispatch(
|
|
419
716
|
planId: string,
|
|
420
717
|
taskId: string,
|
|
421
|
-
): {
|
|
718
|
+
): {
|
|
719
|
+
task: PlanTask;
|
|
720
|
+
unmetDependencies: PlanTask[];
|
|
721
|
+
ready: boolean;
|
|
722
|
+
deliverableStatus?: { count: number; staleCount: number };
|
|
723
|
+
} {
|
|
422
724
|
const plan = this.get(planId);
|
|
423
725
|
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
424
726
|
const task = plan.tasks.find((t) => t.id === taskId);
|
|
@@ -434,22 +736,476 @@ export class Planner {
|
|
|
434
736
|
}
|
|
435
737
|
}
|
|
436
738
|
|
|
437
|
-
|
|
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;
|
|
438
759
|
}
|
|
439
760
|
|
|
761
|
+
// ─── Execution Metrics & Deliverables ──────────────────────────
|
|
762
|
+
|
|
440
763
|
/**
|
|
441
|
-
*
|
|
442
|
-
*
|
|
764
|
+
* Compute aggregate execution summary from per-task metrics.
|
|
765
|
+
* Called from reconcile() and complete() to populate plan.executionSummary.
|
|
443
766
|
*/
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
446
1201
|
const toArchive = this.store.plans.filter(
|
|
447
1202
|
(p) => p.status === 'completed' && p.updatedAt < cutoff,
|
|
448
1203
|
);
|
|
1204
|
+
for (const plan of toArchive) {
|
|
1205
|
+
plan.status = 'archived';
|
|
1206
|
+
plan.updatedAt = Date.now();
|
|
1207
|
+
}
|
|
449
1208
|
if (toArchive.length > 0) {
|
|
450
|
-
this.store.plans = this.store.plans.filter(
|
|
451
|
-
(p) => !(p.status === 'completed' && p.updatedAt < cutoff),
|
|
452
|
-
);
|
|
453
1209
|
this.save();
|
|
454
1210
|
}
|
|
455
1211
|
return toArchive;
|
|
@@ -466,7 +1222,16 @@ export class Planner {
|
|
|
466
1222
|
tasksByStatus: Record<TaskStatus, number>;
|
|
467
1223
|
} {
|
|
468
1224
|
const plans = this.store.plans;
|
|
469
|
-
const byStatus: Record<PlanStatus, number> = {
|
|
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
|
+
};
|
|
470
1235
|
const tasksByStatus: Record<TaskStatus, number> = {
|
|
471
1236
|
pending: 0,
|
|
472
1237
|
in_progress: 0,
|
|
@@ -496,9 +1261,11 @@ export class Planner {
|
|
|
496
1261
|
// ─── Grading ──────────────────────────────────────────────────────
|
|
497
1262
|
|
|
498
1263
|
/**
|
|
499
|
-
* Grade a plan using
|
|
1264
|
+
* Grade a plan using gap analysis with severity-weighted scoring.
|
|
500
1265
|
* Ported from Salvador MCP's multi-pass grading engine.
|
|
501
1266
|
*
|
|
1267
|
+
* 6 built-in passes + optional custom passes (domain-specific checks).
|
|
1268
|
+
*
|
|
502
1269
|
* Scoring:
|
|
503
1270
|
* - Each gap has a severity (critical=30, major=15, minor=2, info=0)
|
|
504
1271
|
* - Deductions are per-category with optional caps
|
|
@@ -590,12 +1357,18 @@ export class Planner {
|
|
|
590
1357
|
|
|
591
1358
|
private gradeToMinScore(grade: PlanGrade): number {
|
|
592
1359
|
switch (grade) {
|
|
593
|
-
case 'A+':
|
|
594
|
-
|
|
595
|
-
case '
|
|
596
|
-
|
|
597
|
-
case '
|
|
598
|
-
|
|
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;
|
|
599
1372
|
}
|
|
600
1373
|
}
|
|
601
1374
|
|