@soleri/core 2.4.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 +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/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/curator/curator.d.ts +8 -1
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +64 -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/index.d.ts +25 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -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 +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 +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.map +1 -1
- package/dist/project/project-registry.js +9 -11
- 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 +322 -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 +27 -8
- 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 +3 -2
- package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
- package/dist/runtime/vault-extra-ops.js +345 -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 +31 -32
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +201 -181
- package/dist/vault/vault.js.map +1 -1
- package/package.json +7 -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 +30 -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 +225 -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__/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/brain/brain.ts +71 -9
- package/src/brain/types.ts +2 -2
- package/src/cognee/client.ts +18 -0
- package/src/cognee/sync-manager.ts +389 -0
- package/src/curator/curator.ts +88 -7
- 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/index.ts +114 -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 +7 -0
- package/src/persistence/sqlite-provider.ts +62 -0
- package/src/persistence/types.ts +44 -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 +29 -17
- 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 +358 -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 +35 -9
- package/src/runtime/types.ts +8 -0
- package/src/runtime/vault-extra-ops.ts +385 -4
- package/src/vault/playbook.ts +87 -0
- package/src/vault/vault.ts +301 -235
package/dist/planning/planner.js
CHANGED
|
@@ -1,7 +1,75 @@
|
|
|
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 { SEVERITY_WEIGHTS, CATEGORY_PENALTY_CAPS } from './gap-types.js';
|
|
4
5
|
import { runGapAnalysis } from './gap-analysis.js';
|
|
6
|
+
/**
|
|
7
|
+
* Valid status transitions.
|
|
8
|
+
* Each key maps to the set of statuses it can transition to.
|
|
9
|
+
* Ported from Salvador's LIFECYCLE_TRANSITIONS.
|
|
10
|
+
*/
|
|
11
|
+
export const LIFECYCLE_TRANSITIONS = {
|
|
12
|
+
brainstorming: ['draft'],
|
|
13
|
+
draft: ['approved'],
|
|
14
|
+
approved: ['executing'],
|
|
15
|
+
executing: ['validating', 'reconciling'],
|
|
16
|
+
validating: ['reconciling', 'executing'],
|
|
17
|
+
reconciling: ['completed'],
|
|
18
|
+
completed: ['archived'],
|
|
19
|
+
archived: [],
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Statuses where the 30-minute TTL should NOT apply.
|
|
23
|
+
* Plans in these states may span multiple sessions.
|
|
24
|
+
*/
|
|
25
|
+
export const NON_EXPIRING_STATUSES = [
|
|
26
|
+
'brainstorming',
|
|
27
|
+
'executing',
|
|
28
|
+
'validating',
|
|
29
|
+
'reconciling',
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Validate a lifecycle status transition.
|
|
33
|
+
* Returns true if the transition is valid, false otherwise.
|
|
34
|
+
*/
|
|
35
|
+
export function isValidTransition(from, to) {
|
|
36
|
+
return LIFECYCLE_TRANSITIONS[from].includes(to);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get the valid next statuses for a given status.
|
|
40
|
+
*/
|
|
41
|
+
export function getValidNextStatuses(status) {
|
|
42
|
+
return LIFECYCLE_TRANSITIONS[status];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check if a status should have TTL expiration.
|
|
46
|
+
* Plans in executing/reconciling states persist indefinitely.
|
|
47
|
+
*/
|
|
48
|
+
export function shouldExpire(status) {
|
|
49
|
+
return !NON_EXPIRING_STATUSES.includes(status);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Severity weights for drift accuracy score calculation.
|
|
53
|
+
* Score = 100 - sum(drift_items * weight_per_impact)
|
|
54
|
+
* Ported from Salvador's plan-lifecycle-types.ts.
|
|
55
|
+
*/
|
|
56
|
+
export const DRIFT_WEIGHTS = {
|
|
57
|
+
high: 20,
|
|
58
|
+
medium: 10,
|
|
59
|
+
low: 5,
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Calculate drift accuracy score from drift items.
|
|
63
|
+
* Score = max(0, 100 - sum(weight_per_impact))
|
|
64
|
+
* Ported from Salvador's calculateDriftScore.
|
|
65
|
+
*/
|
|
66
|
+
export function calculateDriftScore(items) {
|
|
67
|
+
let deductions = 0;
|
|
68
|
+
for (const item of items) {
|
|
69
|
+
deductions += DRIFT_WEIGHTS[item.impact];
|
|
70
|
+
}
|
|
71
|
+
return Math.max(0, 100 - deductions);
|
|
72
|
+
}
|
|
5
73
|
/**
|
|
6
74
|
* Calculate score from gaps with severity-weighted deductions and iteration leniency.
|
|
7
75
|
* Ported from Salvador MCP's plan-grading.ts.
|
|
@@ -68,7 +136,7 @@ export class Planner {
|
|
|
68
136
|
id: `plan-${now}-${Math.random().toString(36).slice(2, 8)}`,
|
|
69
137
|
objective: params.objective,
|
|
70
138
|
scope: params.scope,
|
|
71
|
-
status: 'draft',
|
|
139
|
+
status: params.initialStatus ?? 'draft',
|
|
72
140
|
decisions: params.decisions ?? [],
|
|
73
141
|
tasks: (params.tasks ?? []).map((t, i) => ({
|
|
74
142
|
id: `task-${i + 1}`,
|
|
@@ -77,6 +145,12 @@ export class Planner {
|
|
|
77
145
|
status: 'pending',
|
|
78
146
|
updatedAt: now,
|
|
79
147
|
})),
|
|
148
|
+
...(params.approach !== undefined && { approach: params.approach }),
|
|
149
|
+
...(params.context !== undefined && { context: params.context }),
|
|
150
|
+
...(params.success_criteria !== undefined && { success_criteria: params.success_criteria }),
|
|
151
|
+
...(params.tool_chain !== undefined && { tool_chain: params.tool_chain }),
|
|
152
|
+
...(params.flow !== undefined && { flow: params.flow }),
|
|
153
|
+
...(params.target_mode !== undefined && { target_mode: params.target_mode }),
|
|
80
154
|
checks: [],
|
|
81
155
|
createdAt: now,
|
|
82
156
|
updatedAt: now,
|
|
@@ -91,14 +165,36 @@ export class Planner {
|
|
|
91
165
|
list() {
|
|
92
166
|
return [...this.store.plans];
|
|
93
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Transition a plan to a new status using the typed FSM.
|
|
170
|
+
* Validates that the transition is allowed before applying it.
|
|
171
|
+
*/
|
|
172
|
+
transition(plan, to) {
|
|
173
|
+
if (!isValidTransition(plan.status, to)) {
|
|
174
|
+
const valid = getValidNextStatuses(plan.status);
|
|
175
|
+
throw new Error(`Invalid transition: '${plan.status}' → '${to}'. ` +
|
|
176
|
+
`Valid transitions from '${plan.status}': ${valid.length > 0 ? valid.join(', ') : 'none'}`);
|
|
177
|
+
}
|
|
178
|
+
plan.status = to;
|
|
179
|
+
plan.updatedAt = Date.now();
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Promote a brainstorming plan to draft status.
|
|
183
|
+
* Only allowed from 'brainstorming'.
|
|
184
|
+
*/
|
|
185
|
+
promoteToDraft(planId) {
|
|
186
|
+
const plan = this.get(planId);
|
|
187
|
+
if (!plan)
|
|
188
|
+
throw new Error(`Plan not found: ${planId}`);
|
|
189
|
+
this.transition(plan, 'draft');
|
|
190
|
+
this.save();
|
|
191
|
+
return plan;
|
|
192
|
+
}
|
|
94
193
|
approve(planId) {
|
|
95
194
|
const plan = this.get(planId);
|
|
96
195
|
if (!plan)
|
|
97
196
|
throw new Error(`Plan not found: ${planId}`);
|
|
98
|
-
|
|
99
|
-
throw new Error(`Cannot approve plan in '${plan.status}' status — must be 'draft'`);
|
|
100
|
-
plan.status = 'approved';
|
|
101
|
-
plan.updatedAt = Date.now();
|
|
197
|
+
this.transition(plan, 'approved');
|
|
102
198
|
this.save();
|
|
103
199
|
return plan;
|
|
104
200
|
}
|
|
@@ -106,10 +202,7 @@ export class Planner {
|
|
|
106
202
|
const plan = this.get(planId);
|
|
107
203
|
if (!plan)
|
|
108
204
|
throw new Error(`Plan not found: ${planId}`);
|
|
109
|
-
|
|
110
|
-
throw new Error(`Cannot execute plan in '${plan.status}' status — must be 'approved'`);
|
|
111
|
-
plan.status = 'executing';
|
|
112
|
-
plan.updatedAt = Date.now();
|
|
205
|
+
this.transition(plan, 'executing');
|
|
113
206
|
this.save();
|
|
114
207
|
return plan;
|
|
115
208
|
}
|
|
@@ -117,33 +210,79 @@ export class Planner {
|
|
|
117
210
|
const plan = this.get(planId);
|
|
118
211
|
if (!plan)
|
|
119
212
|
throw new Error(`Plan not found: ${planId}`);
|
|
120
|
-
if (plan.status !== 'executing')
|
|
121
|
-
throw new Error(`Cannot update tasks on plan in '${plan.status}' status — must be 'executing'`);
|
|
213
|
+
if (plan.status !== 'executing' && plan.status !== 'validating')
|
|
214
|
+
throw new Error(`Cannot update tasks on plan in '${plan.status}' status — must be 'executing' or 'validating'`);
|
|
122
215
|
const task = plan.tasks.find((t) => t.id === taskId);
|
|
123
216
|
if (!task)
|
|
124
217
|
throw new Error(`Task not found: ${taskId}`);
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
// Auto-set startedAt on first in_progress transition
|
|
220
|
+
if (status === 'in_progress' && !task.startedAt) {
|
|
221
|
+
task.startedAt = now;
|
|
222
|
+
}
|
|
223
|
+
// Auto-set completedAt and compute durationMs on terminal transitions
|
|
224
|
+
if (status === 'completed' || status === 'skipped' || status === 'failed') {
|
|
225
|
+
task.completedAt = now;
|
|
226
|
+
if (task.startedAt) {
|
|
227
|
+
if (!task.metrics)
|
|
228
|
+
task.metrics = {};
|
|
229
|
+
task.metrics.durationMs = now - task.startedAt;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
125
232
|
task.status = status;
|
|
126
|
-
task.updatedAt =
|
|
127
|
-
plan.updatedAt =
|
|
233
|
+
task.updatedAt = now;
|
|
234
|
+
plan.updatedAt = now;
|
|
128
235
|
this.save();
|
|
129
236
|
return plan;
|
|
130
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Transition plan to 'validating' state (post-execution verification).
|
|
240
|
+
* Only allowed from 'executing'.
|
|
241
|
+
*/
|
|
242
|
+
startValidation(planId) {
|
|
243
|
+
const plan = this.get(planId);
|
|
244
|
+
if (!plan)
|
|
245
|
+
throw new Error(`Plan not found: ${planId}`);
|
|
246
|
+
this.transition(plan, 'validating');
|
|
247
|
+
this.save();
|
|
248
|
+
return plan;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Transition plan to 'reconciling' state.
|
|
252
|
+
* Allowed from 'executing' or 'validating'.
|
|
253
|
+
*/
|
|
254
|
+
startReconciliation(planId) {
|
|
255
|
+
const plan = this.get(planId);
|
|
256
|
+
if (!plan)
|
|
257
|
+
throw new Error(`Plan not found: ${planId}`);
|
|
258
|
+
this.transition(plan, 'reconciling');
|
|
259
|
+
this.save();
|
|
260
|
+
return plan;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Complete a plan. Only allowed from 'reconciling'.
|
|
264
|
+
* Use startReconciliation() + reconcile() + complete() for the full lifecycle,
|
|
265
|
+
* or reconcile() which auto-transitions through reconciling → completed.
|
|
266
|
+
*/
|
|
131
267
|
complete(planId) {
|
|
132
268
|
const plan = this.get(planId);
|
|
133
269
|
if (!plan)
|
|
134
270
|
throw new Error(`Plan not found: ${planId}`);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
plan.status = 'completed';
|
|
138
|
-
plan.updatedAt = Date.now();
|
|
271
|
+
plan.executionSummary = this.computeExecutionSummary(plan);
|
|
272
|
+
this.transition(plan, 'completed');
|
|
139
273
|
this.save();
|
|
140
274
|
return plan;
|
|
141
275
|
}
|
|
142
276
|
getExecuting() {
|
|
143
|
-
return this.store.plans.filter((p) => p.status === 'executing');
|
|
277
|
+
return this.store.plans.filter((p) => p.status === 'executing' || p.status === 'validating');
|
|
144
278
|
}
|
|
145
279
|
getActive() {
|
|
146
|
-
return this.store.plans.filter((p) => p.status === '
|
|
280
|
+
return this.store.plans.filter((p) => p.status === 'brainstorming' ||
|
|
281
|
+
p.status === 'draft' ||
|
|
282
|
+
p.status === 'approved' ||
|
|
283
|
+
p.status === 'executing' ||
|
|
284
|
+
p.status === 'validating' ||
|
|
285
|
+
p.status === 'reconciling');
|
|
147
286
|
}
|
|
148
287
|
/**
|
|
149
288
|
* Iterate on a draft plan — modify objective, scope, decisions, or tasks.
|
|
@@ -153,8 +292,8 @@ export class Planner {
|
|
|
153
292
|
const plan = this.get(planId);
|
|
154
293
|
if (!plan)
|
|
155
294
|
throw new Error(`Plan not found: ${planId}`);
|
|
156
|
-
if (plan.status !== 'draft')
|
|
157
|
-
throw new Error(`Cannot iterate plan in '${plan.status}' status — must be 'draft'`);
|
|
295
|
+
if (plan.status !== 'draft' && plan.status !== 'brainstorming')
|
|
296
|
+
throw new Error(`Cannot iterate plan in '${plan.status}' status — must be 'draft' or 'brainstorming'`);
|
|
158
297
|
const now = Date.now();
|
|
159
298
|
if (changes.objective !== undefined)
|
|
160
299
|
plan.objective = changes.objective;
|
|
@@ -162,6 +301,18 @@ export class Planner {
|
|
|
162
301
|
plan.scope = changes.scope;
|
|
163
302
|
if (changes.decisions !== undefined)
|
|
164
303
|
plan.decisions = changes.decisions;
|
|
304
|
+
if (changes.approach !== undefined)
|
|
305
|
+
plan.approach = changes.approach;
|
|
306
|
+
if (changes.context !== undefined)
|
|
307
|
+
plan.context = changes.context;
|
|
308
|
+
if (changes.success_criteria !== undefined)
|
|
309
|
+
plan.success_criteria = changes.success_criteria;
|
|
310
|
+
if (changes.tool_chain !== undefined)
|
|
311
|
+
plan.tool_chain = changes.tool_chain;
|
|
312
|
+
if (changes.flow !== undefined)
|
|
313
|
+
plan.flow = changes.flow;
|
|
314
|
+
if (changes.target_mode !== undefined)
|
|
315
|
+
plan.target_mode = changes.target_mode;
|
|
165
316
|
// Remove tasks by ID
|
|
166
317
|
if (changes.removeTasks && changes.removeTasks.length > 0) {
|
|
167
318
|
const removeSet = new Set(changes.removeTasks);
|
|
@@ -196,8 +347,8 @@ export class Planner {
|
|
|
196
347
|
const plan = this.get(planId);
|
|
197
348
|
if (!plan)
|
|
198
349
|
throw new Error(`Plan not found: ${planId}`);
|
|
199
|
-
if (plan.status !== 'draft' && plan.status !== 'approved')
|
|
200
|
-
throw new Error(`Cannot split tasks on plan in '${plan.status}' status — must be 'draft' or 'approved'`);
|
|
350
|
+
if (plan.status !== 'brainstorming' && plan.status !== 'draft' && plan.status !== 'approved')
|
|
351
|
+
throw new Error(`Cannot split tasks on plan in '${plan.status}' status — must be 'brainstorming', 'draft', or 'approved'`);
|
|
201
352
|
const now = Date.now();
|
|
202
353
|
plan.tasks = tasks.map((t, i) => ({
|
|
203
354
|
id: `task-${i + 1}`,
|
|
@@ -205,6 +356,7 @@ export class Planner {
|
|
|
205
356
|
description: t.description,
|
|
206
357
|
status: 'pending',
|
|
207
358
|
dependsOn: t.dependsOn,
|
|
359
|
+
...(t.acceptanceCriteria && { acceptanceCriteria: t.acceptanceCriteria }),
|
|
208
360
|
updatedAt: now,
|
|
209
361
|
}));
|
|
210
362
|
// Validate dependency references
|
|
@@ -224,29 +376,37 @@ export class Planner {
|
|
|
224
376
|
}
|
|
225
377
|
/**
|
|
226
378
|
* Reconcile a plan — compare what was planned vs what actually happened.
|
|
227
|
-
*
|
|
379
|
+
* Uses impact-weighted drift scoring (ported from Salvador's calculateDriftScore).
|
|
380
|
+
*
|
|
381
|
+
* Transitions: executing → reconciling → completed (automatic).
|
|
382
|
+
* Also allowed from 'validating' and 'reconciling' states.
|
|
228
383
|
*/
|
|
229
384
|
reconcile(planId, report) {
|
|
230
385
|
const plan = this.get(planId);
|
|
231
386
|
if (!plan)
|
|
232
387
|
throw new Error(`Plan not found: ${planId}`);
|
|
233
|
-
if (plan.status !== 'executing' &&
|
|
234
|
-
|
|
388
|
+
if (plan.status !== 'executing' &&
|
|
389
|
+
plan.status !== 'validating' &&
|
|
390
|
+
plan.status !== 'reconciling')
|
|
391
|
+
throw new Error(`Cannot reconcile plan in '${plan.status}' status — must be 'executing', 'validating', or 'reconciling'`);
|
|
235
392
|
const driftItems = report.driftItems ?? [];
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
const accuracy = totalTasks > 0 ? Math.round(((totalTasks - driftCount) / totalTasks) * 100) : 100;
|
|
393
|
+
// Impact-weighted drift scoring (ported from Salvador)
|
|
394
|
+
const accuracy = calculateDriftScore(driftItems);
|
|
239
395
|
plan.reconciliation = {
|
|
240
396
|
planId,
|
|
241
|
-
accuracy
|
|
397
|
+
accuracy,
|
|
242
398
|
driftItems,
|
|
243
399
|
summary: report.actualOutcome,
|
|
244
400
|
reconciledAt: Date.now(),
|
|
245
401
|
};
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
402
|
+
// Compute execution summary from per-task metrics
|
|
403
|
+
plan.executionSummary = this.computeExecutionSummary(plan);
|
|
404
|
+
// Transition through reconciling → completed via FSM
|
|
405
|
+
if (plan.status === 'executing' || plan.status === 'validating') {
|
|
406
|
+
plan.status = 'reconciling';
|
|
249
407
|
}
|
|
408
|
+
// Auto-complete after reconciliation
|
|
409
|
+
plan.status = 'completed';
|
|
250
410
|
plan.updatedAt = Date.now();
|
|
251
411
|
this.save();
|
|
252
412
|
return plan;
|
|
@@ -297,17 +457,405 @@ export class Planner {
|
|
|
297
457
|
}
|
|
298
458
|
}
|
|
299
459
|
}
|
|
300
|
-
|
|
460
|
+
const result = {
|
|
461
|
+
task,
|
|
462
|
+
unmetDependencies,
|
|
463
|
+
ready: unmetDependencies.length === 0,
|
|
464
|
+
};
|
|
465
|
+
// Include deliverable status if deliverables exist
|
|
466
|
+
if (task.deliverables && task.deliverables.length > 0) {
|
|
467
|
+
result.deliverableStatus = {
|
|
468
|
+
count: task.deliverables.length,
|
|
469
|
+
staleCount: task.deliverables.filter((d) => d.stale).length,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
// ─── Execution Metrics & Deliverables ──────────────────────────
|
|
475
|
+
/**
|
|
476
|
+
* Compute aggregate execution summary from per-task metrics.
|
|
477
|
+
* Called from reconcile() and complete() to populate plan.executionSummary.
|
|
478
|
+
*/
|
|
479
|
+
computeExecutionSummary(plan) {
|
|
480
|
+
let totalDurationMs = 0;
|
|
481
|
+
let tasksCompleted = 0;
|
|
482
|
+
let tasksSkipped = 0;
|
|
483
|
+
let tasksFailed = 0;
|
|
484
|
+
let tasksWithDuration = 0;
|
|
485
|
+
for (const task of plan.tasks) {
|
|
486
|
+
if (task.status === 'completed')
|
|
487
|
+
tasksCompleted++;
|
|
488
|
+
else if (task.status === 'skipped')
|
|
489
|
+
tasksSkipped++;
|
|
490
|
+
else if (task.status === 'failed')
|
|
491
|
+
tasksFailed++;
|
|
492
|
+
if (task.metrics?.durationMs) {
|
|
493
|
+
totalDurationMs += task.metrics.durationMs;
|
|
494
|
+
tasksWithDuration++;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
totalDurationMs,
|
|
499
|
+
tasksCompleted,
|
|
500
|
+
tasksSkipped,
|
|
501
|
+
tasksFailed,
|
|
502
|
+
avgTaskDurationMs: tasksWithDuration > 0 ? Math.round(totalDurationMs / tasksWithDuration) : 0,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Submit a deliverable for a task. Auto-computes SHA-256 hash for file deliverables.
|
|
507
|
+
*/
|
|
508
|
+
submitDeliverable(planId, taskId, deliverable) {
|
|
509
|
+
const plan = this.get(planId);
|
|
510
|
+
if (!plan)
|
|
511
|
+
throw new Error(`Plan not found: ${planId}`);
|
|
512
|
+
const task = plan.tasks.find((t) => t.id === taskId);
|
|
513
|
+
if (!task)
|
|
514
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
515
|
+
const entry = {
|
|
516
|
+
type: deliverable.type,
|
|
517
|
+
path: deliverable.path,
|
|
518
|
+
};
|
|
519
|
+
// Auto-compute hash for file deliverables
|
|
520
|
+
if (deliverable.type === 'file' && !deliverable.hash) {
|
|
521
|
+
try {
|
|
522
|
+
if (existsSync(deliverable.path)) {
|
|
523
|
+
const content = readFileSync(deliverable.path);
|
|
524
|
+
entry.hash = createHash('sha256').update(content).digest('hex');
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
// Graceful degradation — skip hash if file can't be read
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
else if (deliverable.hash) {
|
|
532
|
+
entry.hash = deliverable.hash;
|
|
533
|
+
}
|
|
534
|
+
if (!task.deliverables)
|
|
535
|
+
task.deliverables = [];
|
|
536
|
+
task.deliverables.push(entry);
|
|
537
|
+
task.updatedAt = Date.now();
|
|
538
|
+
plan.updatedAt = Date.now();
|
|
539
|
+
this.save();
|
|
540
|
+
return task;
|
|
301
541
|
}
|
|
302
542
|
/**
|
|
303
|
-
*
|
|
304
|
-
*
|
|
543
|
+
* Verify all deliverables for a task.
|
|
544
|
+
* - file: checks existsSync + SHA-256 hash match
|
|
545
|
+
* - vault_entry: checks vault.get(path) non-null (requires vault instance)
|
|
546
|
+
* - url: skips (just records, no fetch)
|
|
547
|
+
*/
|
|
548
|
+
verifyDeliverables(planId, taskId, vault) {
|
|
549
|
+
const plan = this.get(planId);
|
|
550
|
+
if (!plan)
|
|
551
|
+
throw new Error(`Plan not found: ${planId}`);
|
|
552
|
+
const task = plan.tasks.find((t) => t.id === taskId);
|
|
553
|
+
if (!task)
|
|
554
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
555
|
+
const deliverables = task.deliverables ?? [];
|
|
556
|
+
let staleCount = 0;
|
|
557
|
+
const now = Date.now();
|
|
558
|
+
for (const d of deliverables) {
|
|
559
|
+
d.stale = false;
|
|
560
|
+
if (d.type === 'file') {
|
|
561
|
+
if (!existsSync(d.path)) {
|
|
562
|
+
d.stale = true;
|
|
563
|
+
staleCount++;
|
|
564
|
+
}
|
|
565
|
+
else if (d.hash) {
|
|
566
|
+
try {
|
|
567
|
+
const content = readFileSync(d.path);
|
|
568
|
+
const currentHash = createHash('sha256').update(content).digest('hex');
|
|
569
|
+
if (currentHash !== d.hash) {
|
|
570
|
+
d.stale = true;
|
|
571
|
+
staleCount++;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
d.stale = true;
|
|
576
|
+
staleCount++;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
d.verifiedAt = now;
|
|
580
|
+
}
|
|
581
|
+
else if (d.type === 'vault_entry') {
|
|
582
|
+
if (vault) {
|
|
583
|
+
const entry = vault.get(d.path);
|
|
584
|
+
if (!entry) {
|
|
585
|
+
d.stale = true;
|
|
586
|
+
staleCount++;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
d.verifiedAt = now;
|
|
590
|
+
}
|
|
591
|
+
// url: skip — just record
|
|
592
|
+
}
|
|
593
|
+
plan.updatedAt = Date.now();
|
|
594
|
+
this.save();
|
|
595
|
+
return { verified: staleCount === 0, deliverables, staleCount };
|
|
596
|
+
}
|
|
597
|
+
// ─── Evidence & Verification ────────────────────────────────────
|
|
598
|
+
/**
|
|
599
|
+
* Submit evidence for a task acceptance criterion.
|
|
600
|
+
* Evidence is stored on the task and used by verifyTask() to check completeness.
|
|
601
|
+
*/
|
|
602
|
+
submitEvidence(planId, taskId, evidence) {
|
|
603
|
+
const plan = this.get(planId);
|
|
604
|
+
if (!plan)
|
|
605
|
+
throw new Error(`Plan not found: ${planId}`);
|
|
606
|
+
const task = plan.tasks.find((t) => t.id === taskId);
|
|
607
|
+
if (!task)
|
|
608
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
609
|
+
if (!task.evidence)
|
|
610
|
+
task.evidence = [];
|
|
611
|
+
task.evidence.push({
|
|
612
|
+
criterion: evidence.criterion,
|
|
613
|
+
content: evidence.content,
|
|
614
|
+
type: evidence.type,
|
|
615
|
+
submittedAt: Date.now(),
|
|
616
|
+
});
|
|
617
|
+
task.updatedAt = Date.now();
|
|
618
|
+
plan.updatedAt = Date.now();
|
|
619
|
+
this.save();
|
|
620
|
+
return task;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Verify a task — check that evidence exists for all acceptance criteria
|
|
624
|
+
* and any reviews have passed.
|
|
625
|
+
* Returns verification status with details.
|
|
626
|
+
*/
|
|
627
|
+
verifyTask(planId, taskId) {
|
|
628
|
+
const plan = this.get(planId);
|
|
629
|
+
if (!plan)
|
|
630
|
+
throw new Error(`Plan not found: ${planId}`);
|
|
631
|
+
const task = plan.tasks.find((t) => t.id === taskId);
|
|
632
|
+
if (!task)
|
|
633
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
634
|
+
// Check evidence coverage
|
|
635
|
+
const criteria = task.acceptanceCriteria ?? [];
|
|
636
|
+
const evidencedCriteria = new Set((task.evidence ?? []).map((e) => e.criterion));
|
|
637
|
+
const missingCriteria = criteria.filter((c) => !evidencedCriteria.has(c));
|
|
638
|
+
// Check task-level reviews
|
|
639
|
+
const taskReviews = (plan.reviews ?? []).filter((r) => r.taskId === taskId);
|
|
640
|
+
let reviewStatus = 'no_reviews';
|
|
641
|
+
if (taskReviews.length > 0) {
|
|
642
|
+
const latest = taskReviews[taskReviews.length - 1];
|
|
643
|
+
reviewStatus = latest.outcome;
|
|
644
|
+
}
|
|
645
|
+
const verified = task.status === 'completed' &&
|
|
646
|
+
missingCriteria.length === 0 &&
|
|
647
|
+
(reviewStatus === 'approved' || reviewStatus === 'no_reviews');
|
|
648
|
+
if (verified !== task.verified) {
|
|
649
|
+
task.verified = verified;
|
|
650
|
+
task.updatedAt = Date.now();
|
|
651
|
+
plan.updatedAt = Date.now();
|
|
652
|
+
this.save();
|
|
653
|
+
}
|
|
654
|
+
return { verified, task, missingCriteria, reviewStatus };
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Verify an entire plan — check all tasks are in a final state,
|
|
658
|
+
* all verification-required tasks have evidence, no tasks stuck in_progress.
|
|
659
|
+
* Returns a validation report.
|
|
660
|
+
*/
|
|
661
|
+
verifyPlan(planId) {
|
|
662
|
+
const plan = this.get(planId);
|
|
663
|
+
if (!plan)
|
|
664
|
+
throw new Error(`Plan not found: ${planId}`);
|
|
665
|
+
const issues = [];
|
|
666
|
+
let verified = 0;
|
|
667
|
+
let completed = 0;
|
|
668
|
+
let skipped = 0;
|
|
669
|
+
let failed = 0;
|
|
670
|
+
let pending = 0;
|
|
671
|
+
let inProgress = 0;
|
|
672
|
+
for (const task of plan.tasks) {
|
|
673
|
+
switch (task.status) {
|
|
674
|
+
case 'completed':
|
|
675
|
+
completed++;
|
|
676
|
+
break;
|
|
677
|
+
case 'skipped':
|
|
678
|
+
skipped++;
|
|
679
|
+
break;
|
|
680
|
+
case 'failed':
|
|
681
|
+
failed++;
|
|
682
|
+
break;
|
|
683
|
+
case 'pending':
|
|
684
|
+
pending++;
|
|
685
|
+
break;
|
|
686
|
+
case 'in_progress':
|
|
687
|
+
inProgress++;
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
if (task.verified)
|
|
691
|
+
verified++;
|
|
692
|
+
// Check for stuck tasks
|
|
693
|
+
if (task.status === 'in_progress') {
|
|
694
|
+
issues.push({ taskId: task.id, issue: 'Task stuck in in_progress state' });
|
|
695
|
+
}
|
|
696
|
+
if (task.status === 'pending') {
|
|
697
|
+
issues.push({ taskId: task.id, issue: 'Task still pending — not started' });
|
|
698
|
+
}
|
|
699
|
+
// Check evidence for completed tasks with acceptance criteria
|
|
700
|
+
if (task.status === 'completed' &&
|
|
701
|
+
task.acceptanceCriteria &&
|
|
702
|
+
task.acceptanceCriteria.length > 0) {
|
|
703
|
+
const evidencedCriteria = new Set((task.evidence ?? []).map((e) => e.criterion));
|
|
704
|
+
const missing = task.acceptanceCriteria.filter((c) => !evidencedCriteria.has(c));
|
|
705
|
+
if (missing.length > 0) {
|
|
706
|
+
issues.push({
|
|
707
|
+
taskId: task.id,
|
|
708
|
+
issue: `Missing evidence for ${missing.length} criteria: ${missing.join(', ')}`,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const valid = issues.length === 0 && pending === 0 && inProgress === 0;
|
|
714
|
+
return {
|
|
715
|
+
valid,
|
|
716
|
+
planId,
|
|
717
|
+
issues,
|
|
718
|
+
summary: {
|
|
719
|
+
total: plan.tasks.length,
|
|
720
|
+
completed,
|
|
721
|
+
skipped,
|
|
722
|
+
failed,
|
|
723
|
+
pending,
|
|
724
|
+
inProgress,
|
|
725
|
+
verified,
|
|
726
|
+
},
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Auto-reconcile a plan — fast path for plans with minimal drift.
|
|
731
|
+
* Checks all tasks are in final state, generates reconciliation report automatically.
|
|
732
|
+
* Returns null if drift is too significant for auto-reconciliation (>2 non-completed tasks).
|
|
733
|
+
*/
|
|
734
|
+
autoReconcile(planId) {
|
|
735
|
+
const plan = this.get(planId);
|
|
736
|
+
if (!plan)
|
|
737
|
+
throw new Error(`Plan not found: ${planId}`);
|
|
738
|
+
if (plan.status !== 'executing' && plan.status !== 'validating')
|
|
739
|
+
throw new Error(`Cannot auto-reconcile plan in '${plan.status}' status — must be 'executing' or 'validating'`);
|
|
740
|
+
const completed = plan.tasks.filter((t) => t.status === 'completed').length;
|
|
741
|
+
const skipped = plan.tasks.filter((t) => t.status === 'skipped').length;
|
|
742
|
+
const failed = plan.tasks.filter((t) => t.status === 'failed').length;
|
|
743
|
+
const pending = plan.tasks.filter((t) => t.status === 'pending').length;
|
|
744
|
+
const inProgress = plan.tasks.filter((t) => t.status === 'in_progress').length;
|
|
745
|
+
// Can't auto-reconcile if tasks are still in progress
|
|
746
|
+
if (inProgress > 0)
|
|
747
|
+
return null;
|
|
748
|
+
// Can't auto-reconcile if too many non-completed tasks
|
|
749
|
+
if (pending + failed > 2)
|
|
750
|
+
return null;
|
|
751
|
+
const driftItems = [];
|
|
752
|
+
for (const task of plan.tasks) {
|
|
753
|
+
if (task.status === 'skipped') {
|
|
754
|
+
driftItems.push({
|
|
755
|
+
type: 'skipped',
|
|
756
|
+
description: `Task '${task.title}' was skipped`,
|
|
757
|
+
impact: 'medium',
|
|
758
|
+
rationale: 'Task not executed during plan implementation',
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
else if (task.status === 'failed') {
|
|
762
|
+
driftItems.push({
|
|
763
|
+
type: 'modified',
|
|
764
|
+
description: `Task '${task.title}' failed`,
|
|
765
|
+
impact: 'high',
|
|
766
|
+
rationale: 'Task execution failed',
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
else if (task.status === 'pending') {
|
|
770
|
+
driftItems.push({
|
|
771
|
+
type: 'skipped',
|
|
772
|
+
description: `Task '${task.title}' was never started`,
|
|
773
|
+
impact: 'low',
|
|
774
|
+
rationale: 'Task left in pending state',
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return this.reconcile(planId, {
|
|
779
|
+
actualOutcome: `Auto-reconciled: ${completed}/${plan.tasks.length} tasks completed, ${skipped} skipped, ${failed} failed`,
|
|
780
|
+
driftItems,
|
|
781
|
+
reconciledBy: 'auto',
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Generate a review prompt for spec compliance checking.
|
|
786
|
+
* Used by subagent dispatch — the controller generates the prompt, a subagent executes it.
|
|
787
|
+
*/
|
|
788
|
+
generateReviewSpec(planId, taskId) {
|
|
789
|
+
const plan = this.get(planId);
|
|
790
|
+
if (!plan)
|
|
791
|
+
throw new Error(`Plan not found: ${planId}`);
|
|
792
|
+
const task = plan.tasks.find((t) => t.id === taskId);
|
|
793
|
+
if (!task)
|
|
794
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
795
|
+
const criteria = task.acceptanceCriteria?.length
|
|
796
|
+
? `\n\nAcceptance Criteria:\n${task.acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join('\n')}`
|
|
797
|
+
: '';
|
|
798
|
+
const prompt = [
|
|
799
|
+
`# Spec Compliance Review`,
|
|
800
|
+
``,
|
|
801
|
+
`## Task: ${task.title}`,
|
|
802
|
+
`**Description:** ${task.description}`,
|
|
803
|
+
`**Plan Objective:** ${plan.objective}${criteria}`,
|
|
804
|
+
``,
|
|
805
|
+
`## Review Checklist`,
|
|
806
|
+
`1. Does the implementation match the task description?`,
|
|
807
|
+
`2. Are all acceptance criteria satisfied?`,
|
|
808
|
+
`3. Does it align with the plan's overall objective?`,
|
|
809
|
+
`4. Are there any spec deviations?`,
|
|
810
|
+
``,
|
|
811
|
+
`Provide: outcome (approved/rejected/needs_changes) and detailed comments.`,
|
|
812
|
+
].join('\n');
|
|
813
|
+
return { prompt, task, plan };
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Generate a review prompt for code quality checking.
|
|
817
|
+
*/
|
|
818
|
+
generateReviewQuality(planId, taskId) {
|
|
819
|
+
const plan = this.get(planId);
|
|
820
|
+
if (!plan)
|
|
821
|
+
throw new Error(`Plan not found: ${planId}`);
|
|
822
|
+
const task = plan.tasks.find((t) => t.id === taskId);
|
|
823
|
+
if (!task)
|
|
824
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
825
|
+
const prompt = [
|
|
826
|
+
`# Code Quality Review`,
|
|
827
|
+
``,
|
|
828
|
+
`## Task: ${task.title}`,
|
|
829
|
+
`**Description:** ${task.description}`,
|
|
830
|
+
``,
|
|
831
|
+
`## Quality Checklist`,
|
|
832
|
+
`1. **Correctness** — Does it work as intended?`,
|
|
833
|
+
`2. **Security** — No injection, XSS, or OWASP top 10 vulnerabilities?`,
|
|
834
|
+
`3. **Performance** — No unnecessary allocations, N+1 queries, or blocking calls?`,
|
|
835
|
+
`4. **Maintainability** — Clear naming, appropriate abstractions, documented intent?`,
|
|
836
|
+
`5. **Testing** — Adequate test coverage for the changes?`,
|
|
837
|
+
`6. **Error Handling** — Graceful degradation, no swallowed errors?`,
|
|
838
|
+
`7. **Conventions** — Follows project coding standards?`,
|
|
839
|
+
``,
|
|
840
|
+
`Provide: outcome (approved/rejected/needs_changes) and detailed comments.`,
|
|
841
|
+
].join('\n');
|
|
842
|
+
return { prompt, task, plan };
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Archive completed plans — transitions them to 'archived' status.
|
|
846
|
+
* If olderThanDays is provided, only archives plans older than that.
|
|
847
|
+
* Returns the archived plans.
|
|
305
848
|
*/
|
|
306
849
|
archive(olderThanDays) {
|
|
307
|
-
const cutoff =
|
|
850
|
+
const cutoff = olderThanDays !== undefined
|
|
851
|
+
? Date.now() - olderThanDays * 24 * 60 * 60 * 1000
|
|
852
|
+
: Date.now() + 1; // +1ms so archive() with no args archives all completed plans
|
|
308
853
|
const toArchive = this.store.plans.filter((p) => p.status === 'completed' && p.updatedAt < cutoff);
|
|
854
|
+
for (const plan of toArchive) {
|
|
855
|
+
plan.status = 'archived';
|
|
856
|
+
plan.updatedAt = Date.now();
|
|
857
|
+
}
|
|
309
858
|
if (toArchive.length > 0) {
|
|
310
|
-
this.store.plans = this.store.plans.filter((p) => !(p.status === 'completed' && p.updatedAt < cutoff));
|
|
311
859
|
this.save();
|
|
312
860
|
}
|
|
313
861
|
return toArchive;
|
|
@@ -317,7 +865,16 @@ export class Planner {
|
|
|
317
865
|
*/
|
|
318
866
|
stats() {
|
|
319
867
|
const plans = this.store.plans;
|
|
320
|
-
const byStatus = {
|
|
868
|
+
const byStatus = {
|
|
869
|
+
brainstorming: 0,
|
|
870
|
+
draft: 0,
|
|
871
|
+
approved: 0,
|
|
872
|
+
executing: 0,
|
|
873
|
+
validating: 0,
|
|
874
|
+
reconciling: 0,
|
|
875
|
+
completed: 0,
|
|
876
|
+
archived: 0,
|
|
877
|
+
};
|
|
321
878
|
const tasksByStatus = {
|
|
322
879
|
pending: 0,
|
|
323
880
|
in_progress: 0,
|
|
@@ -343,9 +900,11 @@ export class Planner {
|
|
|
343
900
|
}
|
|
344
901
|
// ─── Grading ──────────────────────────────────────────────────────
|
|
345
902
|
/**
|
|
346
|
-
* Grade a plan using
|
|
903
|
+
* Grade a plan using gap analysis with severity-weighted scoring.
|
|
347
904
|
* Ported from Salvador MCP's multi-pass grading engine.
|
|
348
905
|
*
|
|
906
|
+
* 6 built-in passes + optional custom passes (domain-specific checks).
|
|
907
|
+
*
|
|
349
908
|
* Scoring:
|
|
350
909
|
* - Each gap has a severity (critical=30, major=15, minor=2, info=0)
|
|
351
910
|
* - Deductions are per-category with optional caps
|
|
@@ -433,12 +992,18 @@ export class Planner {
|
|
|
433
992
|
}
|
|
434
993
|
gradeToMinScore(grade) {
|
|
435
994
|
switch (grade) {
|
|
436
|
-
case 'A+':
|
|
437
|
-
|
|
438
|
-
case '
|
|
439
|
-
|
|
440
|
-
case '
|
|
441
|
-
|
|
995
|
+
case 'A+':
|
|
996
|
+
return 95;
|
|
997
|
+
case 'A':
|
|
998
|
+
return 90;
|
|
999
|
+
case 'B':
|
|
1000
|
+
return 80;
|
|
1001
|
+
case 'C':
|
|
1002
|
+
return 70;
|
|
1003
|
+
case 'D':
|
|
1004
|
+
return 60;
|
|
1005
|
+
case 'F':
|
|
1006
|
+
return 0;
|
|
442
1007
|
}
|
|
443
1008
|
}
|
|
444
1009
|
hasCircularDependencies(plan) {
|