@soleri/core 9.3.0 → 9.4.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/intelligence.d.ts +5 -0
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +115 -26
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/learning-radar.d.ts +3 -3
- package/dist/brain/learning-radar.d.ts.map +1 -1
- package/dist/brain/learning-radar.js +8 -4
- package/dist/brain/learning-radar.js.map +1 -1
- package/dist/control/intent-router.d.ts +2 -2
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +35 -1
- package/dist/control/intent-router.js.map +1 -1
- package/dist/control/types.d.ts +10 -2
- package/dist/control/types.d.ts.map +1 -1
- package/dist/curator/curator.d.ts +4 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +23 -1
- package/dist/curator/curator.js.map +1 -1
- package/dist/curator/schema.d.ts +1 -1
- package/dist/curator/schema.d.ts.map +1 -1
- package/dist/curator/schema.js +8 -0
- package/dist/curator/schema.js.map +1 -1
- package/dist/domain-packs/types.d.ts +6 -0
- package/dist/domain-packs/types.d.ts.map +1 -1
- package/dist/domain-packs/types.js +1 -0
- package/dist/domain-packs/types.js.map +1 -1
- package/dist/engine/module-manifest.d.ts +2 -0
- package/dist/engine/module-manifest.d.ts.map +1 -1
- package/dist/engine/module-manifest.js +117 -2
- package/dist/engine/module-manifest.js.map +1 -1
- package/dist/engine/register-engine.d.ts +9 -0
- package/dist/engine/register-engine.d.ts.map +1 -1
- package/dist/engine/register-engine.js +59 -1
- package/dist/engine/register-engine.js.map +1 -1
- package/dist/facades/types.d.ts +5 -1
- package/dist/facades/types.d.ts.map +1 -1
- package/dist/facades/types.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/operator/operator-context-store.d.ts +54 -0
- package/dist/operator/operator-context-store.d.ts.map +1 -0
- package/dist/operator/operator-context-store.js +434 -0
- package/dist/operator/operator-context-store.js.map +1 -0
- package/dist/operator/operator-context-types.d.ts +101 -0
- package/dist/operator/operator-context-types.d.ts.map +1 -0
- package/dist/operator/operator-context-types.js +27 -0
- package/dist/operator/operator-context-types.js.map +1 -0
- package/dist/packs/index.d.ts +2 -2
- package/dist/packs/index.d.ts.map +1 -1
- package/dist/packs/index.js +1 -1
- package/dist/packs/index.js.map +1 -1
- package/dist/packs/lockfile.d.ts +3 -0
- package/dist/packs/lockfile.d.ts.map +1 -1
- package/dist/packs/lockfile.js.map +1 -1
- package/dist/packs/types.d.ts +8 -2
- package/dist/packs/types.d.ts.map +1 -1
- package/dist/packs/types.js +6 -0
- package/dist/packs/types.js.map +1 -1
- package/dist/planning/plan-lifecycle.d.ts +12 -1
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +52 -19
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/planning/planner-types.d.ts +6 -0
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/planning/planner.d.ts +21 -1
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +62 -3
- package/dist/planning/planner.js.map +1 -1
- package/dist/planning/task-complexity-assessor.d.ts +42 -0
- package/dist/planning/task-complexity-assessor.d.ts.map +1 -0
- package/dist/planning/task-complexity-assessor.js +132 -0
- package/dist/planning/task-complexity-assessor.js.map +1 -0
- package/dist/plugins/types.d.ts +18 -18
- package/dist/runtime/admin-ops.d.ts +1 -1
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +118 -3
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.js +19 -9
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +35 -7
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
- package/dist/runtime/facades/brain-facade.js +4 -2
- package/dist/runtime/facades/brain-facade.js.map +1 -1
- package/dist/runtime/facades/control-facade.d.ts.map +1 -1
- package/dist/runtime/facades/control-facade.js +8 -2
- package/dist/runtime/facades/control-facade.js.map +1 -1
- package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
- package/dist/runtime/facades/curator-facade.js +13 -0
- package/dist/runtime/facades/curator-facade.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +10 -12
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +36 -1
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
- package/dist/runtime/facades/plan-facade.js +20 -4
- package/dist/runtime/facades/plan-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +109 -31
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/plan-feedback-helper.d.ts +21 -0
- package/dist/runtime/plan-feedback-helper.d.ts.map +1 -0
- package/dist/runtime/plan-feedback-helper.js +52 -0
- package/dist/runtime/plan-feedback-helper.js.map +1 -0
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +73 -34
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/session-briefing.d.ts.map +1 -1
- package/dist/runtime/session-briefing.js +9 -1
- package/dist/runtime/session-briefing.js.map +1 -1
- package/dist/runtime/types.d.ts +3 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +13 -7
- package/dist/skills/sync-skills.js.map +1 -1
- package/package.json +1 -1
- package/src/brain/brain-intelligence.test.ts +30 -0
- package/src/brain/extraction-quality.test.ts +323 -0
- package/src/brain/intelligence.ts +133 -30
- package/src/brain/learning-radar.ts +8 -5
- package/src/brain/second-brain-features.test.ts +1 -1
- package/src/control/intent-router.test.ts +73 -3
- package/src/control/intent-router.ts +38 -1
- package/src/control/types.ts +13 -2
- package/src/curator/curator.test.ts +92 -0
- package/src/curator/curator.ts +29 -1
- package/src/curator/schema.ts +8 -0
- package/src/domain-packs/types.ts +8 -0
- package/src/engine/module-manifest.test.ts +51 -2
- package/src/engine/module-manifest.ts +119 -2
- package/src/engine/register-engine.test.ts +73 -1
- package/src/engine/register-engine.ts +61 -1
- package/src/facades/types.ts +5 -0
- package/src/index.ts +30 -0
- package/src/operator/operator-context-store.test.ts +698 -0
- package/src/operator/operator-context-store.ts +569 -0
- package/src/operator/operator-context-types.ts +139 -0
- package/src/packs/index.ts +3 -1
- package/src/packs/lockfile.ts +3 -0
- package/src/packs/types.ts +9 -0
- package/src/planning/plan-lifecycle.ts +80 -22
- package/src/planning/planner-types.ts +6 -0
- package/src/planning/planner.ts +74 -4
- package/src/planning/task-complexity-assessor.test.ts +302 -0
- package/src/planning/task-complexity-assessor.ts +180 -0
- package/src/runtime/admin-ops.test.ts +159 -3
- package/src/runtime/admin-ops.ts +123 -3
- package/src/runtime/admin-setup-ops.ts +30 -10
- package/src/runtime/capture-ops.test.ts +84 -0
- package/src/runtime/capture-ops.ts +35 -7
- package/src/runtime/facades/admin-facade.test.ts +1 -1
- package/src/runtime/facades/brain-facade.ts +6 -3
- package/src/runtime/facades/control-facade.ts +10 -2
- package/src/runtime/facades/curator-facade.ts +18 -0
- package/src/runtime/facades/memory-facade.test.ts +14 -12
- package/src/runtime/facades/memory-facade.ts +10 -12
- package/src/runtime/facades/orchestrate-facade.ts +33 -1
- package/src/runtime/facades/plan-facade.test.ts +213 -0
- package/src/runtime/facades/plan-facade.ts +23 -4
- package/src/runtime/orchestrate-ops.test.ts +404 -0
- package/src/runtime/orchestrate-ops.ts +129 -37
- package/src/runtime/plan-feedback-helper.test.ts +173 -0
- package/src/runtime/plan-feedback-helper.ts +63 -0
- package/src/runtime/planning-extra-ops.test.ts +43 -1
- package/src/runtime/planning-extra-ops.ts +96 -33
- package/src/runtime/session-briefing.test.ts +1 -0
- package/src/runtime/session-briefing.ts +10 -1
- package/src/runtime/types.ts +3 -0
- package/src/skills/sync-skills.ts +14 -7
- package/src/vault/vault-scaling.test.ts +5 -5
- package/vitest.config.ts +1 -0
package/src/packs/index.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
export {
|
|
6
6
|
packManifestSchema,
|
|
7
|
+
PACK_TIERS,
|
|
7
8
|
type PackManifest,
|
|
9
|
+
type PackTier as ManifestPackTier,
|
|
8
10
|
type PackStatus,
|
|
9
11
|
type InstalledPack,
|
|
10
12
|
type InstallResult,
|
|
@@ -14,7 +16,7 @@ export {
|
|
|
14
16
|
export { PackInstaller } from './pack-installer.js';
|
|
15
17
|
|
|
16
18
|
export { PackLockfile, inferPackType } from './lockfile.js';
|
|
17
|
-
export type { LockEntry, PackType, PackSource, LockfileData } from './lockfile.js';
|
|
19
|
+
export type { LockEntry, PackType, PackSource, PackTier, LockfileData } from './lockfile.js';
|
|
18
20
|
|
|
19
21
|
export { resolvePack, checkNpmVersion, checkVersionCompat } from './resolver.js';
|
|
20
22
|
export type { ResolvedPack, ResolveOptions } from './resolver.js';
|
package/src/packs/lockfile.ts
CHANGED
|
@@ -39,10 +39,13 @@ export interface LockEntry {
|
|
|
39
39
|
facadesRegistered: boolean;
|
|
40
40
|
/** Compatible @soleri/core version range (from manifest "soleri" field) */
|
|
41
41
|
soleriRange?: string;
|
|
42
|
+
/** Pack tier: default (ships with engine), community (free), premium (unlocked today) */
|
|
43
|
+
tier?: PackTier;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export type PackType = 'hooks' | 'skills' | 'knowledge' | 'domain' | 'bundle';
|
|
45
47
|
export type PackSource = 'built-in' | 'local' | 'npm';
|
|
48
|
+
export type PackTier = 'default' | 'community' | 'premium';
|
|
46
49
|
|
|
47
50
|
export interface LockfileData {
|
|
48
51
|
/** Lockfile format version */
|
package/src/packs/types.ts
CHANGED
|
@@ -23,11 +23,20 @@ import { z } from 'zod';
|
|
|
23
23
|
// MANIFEST SCHEMA
|
|
24
24
|
// =============================================================================
|
|
25
25
|
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Pack Tiers — determines visibility, licensing, and install behavior
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export const PACK_TIERS = ['default', 'community', 'premium'] as const;
|
|
31
|
+
export type PackTier = (typeof PACK_TIERS)[number];
|
|
32
|
+
|
|
26
33
|
export const packManifestSchema = z.object({
|
|
27
34
|
id: z.string().regex(/^[a-z0-9-]+$/, 'Pack ID must be lowercase alphanumeric with hyphens'),
|
|
28
35
|
name: z.string().min(1),
|
|
29
36
|
version: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver (x.y.z)'),
|
|
30
37
|
description: z.string().optional().default(''),
|
|
38
|
+
/** Pack tier: 'default' (ships with engine), 'community' (free, npm), 'premium' (unlocked today, gated later) */
|
|
39
|
+
tier: z.enum(PACK_TIERS).optional().default('community'),
|
|
31
40
|
/** Domains this pack covers */
|
|
32
41
|
domains: z.array(z.string()).optional().default([]),
|
|
33
42
|
/** Minimum engine version required (semver range) */
|
|
@@ -232,7 +232,13 @@ export interface IterateChanges {
|
|
|
232
232
|
objective?: string;
|
|
233
233
|
scope?: string;
|
|
234
234
|
decisions?: (string | PlanDecision)[];
|
|
235
|
-
addTasks?: Array<{
|
|
235
|
+
addTasks?: Array<{
|
|
236
|
+
title: string;
|
|
237
|
+
description: string;
|
|
238
|
+
phase?: string;
|
|
239
|
+
milestone?: string;
|
|
240
|
+
parentTaskId?: string;
|
|
241
|
+
}>;
|
|
236
242
|
removeTasks?: string[];
|
|
237
243
|
approach?: string;
|
|
238
244
|
context?: string;
|
|
@@ -240,26 +246,61 @@ export interface IterateChanges {
|
|
|
240
246
|
tool_chain?: string[];
|
|
241
247
|
flow?: string;
|
|
242
248
|
target_mode?: string;
|
|
249
|
+
alternatives?: import('./planner-types.js').PlanAlternative[];
|
|
243
250
|
}
|
|
244
251
|
|
|
245
252
|
/**
|
|
246
253
|
* Apply iteration changes to a plan (mutates in place).
|
|
247
254
|
* Caller is responsible for status validation and persistence.
|
|
255
|
+
* Returns the number of fields actually mutated (0 = no-op).
|
|
248
256
|
*/
|
|
249
|
-
export function applyIteration(plan: Plan, changes: IterateChanges):
|
|
257
|
+
export function applyIteration(plan: Plan, changes: IterateChanges): number {
|
|
250
258
|
const now = Date.now();
|
|
251
|
-
|
|
252
|
-
if (changes.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (changes.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
259
|
+
let mutated = 0;
|
|
260
|
+
if (changes.objective !== undefined) {
|
|
261
|
+
plan.objective = changes.objective;
|
|
262
|
+
mutated++;
|
|
263
|
+
}
|
|
264
|
+
if (changes.scope !== undefined) {
|
|
265
|
+
plan.scope = changes.scope;
|
|
266
|
+
mutated++;
|
|
267
|
+
}
|
|
268
|
+
if (changes.decisions !== undefined) {
|
|
269
|
+
plan.decisions = changes.decisions;
|
|
270
|
+
mutated++;
|
|
271
|
+
}
|
|
272
|
+
if (changes.approach !== undefined) {
|
|
273
|
+
plan.approach = changes.approach;
|
|
274
|
+
mutated++;
|
|
275
|
+
}
|
|
276
|
+
if (changes.context !== undefined) {
|
|
277
|
+
plan.context = changes.context;
|
|
278
|
+
mutated++;
|
|
279
|
+
}
|
|
280
|
+
if (changes.success_criteria !== undefined) {
|
|
281
|
+
plan.success_criteria = changes.success_criteria;
|
|
282
|
+
mutated++;
|
|
283
|
+
}
|
|
284
|
+
if (changes.tool_chain !== undefined) {
|
|
285
|
+
plan.tool_chain = changes.tool_chain;
|
|
286
|
+
mutated++;
|
|
287
|
+
}
|
|
288
|
+
if (changes.flow !== undefined) {
|
|
289
|
+
plan.flow = changes.flow;
|
|
290
|
+
mutated++;
|
|
291
|
+
}
|
|
292
|
+
if (changes.target_mode !== undefined) {
|
|
293
|
+
plan.target_mode = changes.target_mode;
|
|
294
|
+
mutated++;
|
|
295
|
+
}
|
|
296
|
+
if (changes.alternatives !== undefined) {
|
|
297
|
+
plan.alternatives = changes.alternatives;
|
|
298
|
+
mutated++;
|
|
299
|
+
}
|
|
260
300
|
if (changes.removeTasks?.length) {
|
|
261
301
|
const removeSet = new Set(changes.removeTasks);
|
|
262
302
|
plan.tasks = plan.tasks.filter((t) => !removeSet.has(t.id));
|
|
303
|
+
mutated++;
|
|
263
304
|
}
|
|
264
305
|
if (changes.addTasks?.length) {
|
|
265
306
|
const maxIndex = plan.tasks.reduce((max, t) => {
|
|
@@ -267,16 +308,24 @@ export function applyIteration(plan: Plan, changes: IterateChanges): void {
|
|
|
267
308
|
return isNaN(num) ? max : Math.max(max, num);
|
|
268
309
|
}, 0);
|
|
269
310
|
for (let i = 0; i < changes.addTasks.length; i++) {
|
|
311
|
+
const t = changes.addTasks[i];
|
|
270
312
|
plan.tasks.push({
|
|
271
313
|
id: `task-${maxIndex + i + 1}`,
|
|
272
|
-
title:
|
|
273
|
-
description:
|
|
314
|
+
title: t.title,
|
|
315
|
+
description: t.description,
|
|
274
316
|
status: 'pending' as TaskStatus,
|
|
317
|
+
...(t.phase !== undefined && { phase: t.phase }),
|
|
318
|
+
...(t.milestone !== undefined && { milestone: t.milestone }),
|
|
319
|
+
...(t.parentTaskId !== undefined && { parentTaskId: t.parentTaskId }),
|
|
275
320
|
updatedAt: now,
|
|
276
321
|
});
|
|
277
322
|
}
|
|
323
|
+
mutated++;
|
|
278
324
|
}
|
|
279
|
-
|
|
325
|
+
if (mutated > 0) {
|
|
326
|
+
plan.updatedAt = now;
|
|
327
|
+
}
|
|
328
|
+
return mutated;
|
|
280
329
|
}
|
|
281
330
|
|
|
282
331
|
/**
|
|
@@ -308,7 +357,13 @@ export function createPlanObject(params: {
|
|
|
308
357
|
objective: string;
|
|
309
358
|
scope: string;
|
|
310
359
|
decisions?: (string | PlanDecision)[];
|
|
311
|
-
tasks?: Array<{
|
|
360
|
+
tasks?: Array<{
|
|
361
|
+
title: string;
|
|
362
|
+
description: string;
|
|
363
|
+
phase?: string;
|
|
364
|
+
milestone?: string;
|
|
365
|
+
parentTaskId?: string;
|
|
366
|
+
}>;
|
|
312
367
|
approach?: string;
|
|
313
368
|
context?: string;
|
|
314
369
|
success_criteria?: string[];
|
|
@@ -325,13 +380,7 @@ export function createPlanObject(params: {
|
|
|
325
380
|
scope: params.scope,
|
|
326
381
|
status: params.initialStatus ?? 'draft',
|
|
327
382
|
decisions: params.decisions ?? [],
|
|
328
|
-
tasks: (params.tasks ?? []).map((t, i) => ({
|
|
329
|
-
id: `task-${i + 1}`,
|
|
330
|
-
title: t.title,
|
|
331
|
-
description: t.description,
|
|
332
|
-
status: 'pending' as TaskStatus,
|
|
333
|
-
updatedAt: now,
|
|
334
|
-
})),
|
|
383
|
+
tasks: (params.tasks ?? []).map((t, i) => (Object.assign({id:`task-${i+1}`,title:t.title,description:t.description,status:`pending` as TaskStatus}, t.phase!==undefined&&{phase:t.phase}, t.milestone!==undefined&&{milestone:t.milestone}, t.parentTaskId!==undefined&&{parentTaskId:t.parentTaskId}, {updatedAt:now}))),
|
|
335
384
|
...(params.approach !== undefined && { approach: params.approach }),
|
|
336
385
|
...(params.context !== undefined && { context: params.context }),
|
|
337
386
|
...(params.success_criteria !== undefined && { success_criteria: params.success_criteria }),
|
|
@@ -352,6 +401,9 @@ export function applySplitTasks(
|
|
|
352
401
|
description: string;
|
|
353
402
|
dependsOn?: string[];
|
|
354
403
|
acceptanceCriteria?: string[];
|
|
404
|
+
phase?: string;
|
|
405
|
+
milestone?: string;
|
|
406
|
+
parentTaskId?: string;
|
|
355
407
|
}>,
|
|
356
408
|
): void {
|
|
357
409
|
const now = Date.now();
|
|
@@ -362,6 +414,9 @@ export function applySplitTasks(
|
|
|
362
414
|
status: 'pending' as TaskStatus,
|
|
363
415
|
dependsOn: t.dependsOn,
|
|
364
416
|
...(t.acceptanceCriteria && { acceptanceCriteria: t.acceptanceCriteria }),
|
|
417
|
+
...(t.phase !== undefined && { phase: t.phase }),
|
|
418
|
+
...(t.milestone !== undefined && { milestone: t.milestone }),
|
|
419
|
+
...(t.parentTaskId !== undefined && { parentTaskId: t.parentTaskId }),
|
|
365
420
|
updatedAt: now,
|
|
366
421
|
}));
|
|
367
422
|
const taskIds = new Set(plan.tasks.map((t) => t.id));
|
|
@@ -372,6 +427,9 @@ export function applySplitTasks(
|
|
|
372
427
|
throw new Error(`Task '${task.id}' depends on unknown task '${dep}'`);
|
|
373
428
|
}
|
|
374
429
|
}
|
|
430
|
+
if (task.parentTaskId && !taskIds.has(task.parentTaskId)) {
|
|
431
|
+
throw new Error(`Task '${task.id}' references unknown parent task '${task.parentTaskId}'`);
|
|
432
|
+
}
|
|
375
433
|
}
|
|
376
434
|
plan.updatedAt = now;
|
|
377
435
|
}
|
|
@@ -66,6 +66,12 @@ export interface PlanTask {
|
|
|
66
66
|
status: TaskStatus;
|
|
67
67
|
/** Optional dependency IDs — tasks that must complete before this one. */
|
|
68
68
|
dependsOn?: string[];
|
|
69
|
+
/** Phase this task belongs to (e.g., "wave-1", "discovery", "implementation"). */
|
|
70
|
+
phase?: string;
|
|
71
|
+
/** Milestone this task contributes to (e.g., "v1.0", "mvp", "beta"). */
|
|
72
|
+
milestone?: string;
|
|
73
|
+
/** Parent task ID — enables sub-task hierarchy within a plan. */
|
|
74
|
+
parentTaskId?: string;
|
|
69
75
|
/** Evidence submitted for task acceptance criteria. */
|
|
70
76
|
evidence?: TaskEvidence[];
|
|
71
77
|
/** Whether this task has been verified (all evidence checked + reviews passed). */
|
package/src/planning/planner.ts
CHANGED
|
@@ -235,15 +235,17 @@ export class Planner {
|
|
|
235
235
|
);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
iterate(planId: string, changes: IterateChanges): Plan {
|
|
238
|
+
iterate(planId: string, changes: IterateChanges): { plan: Plan; mutated: number } {
|
|
239
239
|
const plan = this.requirePlan(planId);
|
|
240
240
|
if (plan.status !== 'draft' && plan.status !== 'brainstorming')
|
|
241
241
|
throw new Error(
|
|
242
242
|
`Cannot iterate plan in '${plan.status}' status — must be 'draft' or 'brainstorming'`,
|
|
243
243
|
);
|
|
244
|
-
applyIteration(plan, changes);
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
const mutated = applyIteration(plan, changes);
|
|
245
|
+
if (mutated > 0) {
|
|
246
|
+
this.save();
|
|
247
|
+
}
|
|
248
|
+
return { plan, mutated };
|
|
247
249
|
}
|
|
248
250
|
|
|
249
251
|
splitTasks(
|
|
@@ -253,6 +255,9 @@ export class Planner {
|
|
|
253
255
|
description: string;
|
|
254
256
|
dependsOn?: string[];
|
|
255
257
|
acceptanceCriteria?: string[];
|
|
258
|
+
phase?: string;
|
|
259
|
+
milestone?: string;
|
|
260
|
+
parentTaskId?: string;
|
|
256
261
|
}>,
|
|
257
262
|
): Plan {
|
|
258
263
|
const plan = this.requirePlan(planId);
|
|
@@ -445,6 +450,71 @@ export class Planner {
|
|
|
445
450
|
return toArchive;
|
|
446
451
|
}
|
|
447
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Close stale plans — plans in non-terminal states older than the given threshold.
|
|
455
|
+
* For draft/approved: uses 30 min TTL by default.
|
|
456
|
+
* For executing/reconciling: uses olderThanMs parameter (24h default).
|
|
457
|
+
* Returns the list of closed plan IDs.
|
|
458
|
+
*/
|
|
459
|
+
closeStale(olderThanMs?: number): {
|
|
460
|
+
closedIds: string[];
|
|
461
|
+
closedPlans: Array<{ id: string; previousStatus: string; reason: string }>;
|
|
462
|
+
} {
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
const forceAll = olderThanMs === 0;
|
|
465
|
+
const defaultTtl = forceAll ? 0 : 30 * 60 * 1000; // 30 minutes for draft/approved
|
|
466
|
+
const executingTtl = forceAll ? 0 : (olderThanMs ?? 24 * 60 * 60 * 1000); // 24h default for executing/reconciling
|
|
467
|
+
const closed: Array<{ id: string; previousStatus: string; reason: string }> = [];
|
|
468
|
+
|
|
469
|
+
for (const plan of this.store.plans) {
|
|
470
|
+
// Skip terminal states
|
|
471
|
+
if (plan.status === 'completed' || plan.status === 'archived') continue;
|
|
472
|
+
|
|
473
|
+
const age = now - plan.updatedAt;
|
|
474
|
+
let shouldClose = false;
|
|
475
|
+
let reason = '';
|
|
476
|
+
|
|
477
|
+
if (plan.status === 'draft' || plan.status === 'approved') {
|
|
478
|
+
// Short TTL for draft/approved — these should move quickly
|
|
479
|
+
if (age >= defaultTtl) {
|
|
480
|
+
shouldClose = true;
|
|
481
|
+
reason = `ttl-expired (${plan.status}, age: ${Math.round(age / 60000)}min)`;
|
|
482
|
+
}
|
|
483
|
+
} else if (
|
|
484
|
+
plan.status === 'executing' ||
|
|
485
|
+
plan.status === 'validating' ||
|
|
486
|
+
plan.status === 'reconciling' ||
|
|
487
|
+
plan.status === 'brainstorming'
|
|
488
|
+
) {
|
|
489
|
+
// Longer TTL for active states
|
|
490
|
+
if (age >= executingTtl) {
|
|
491
|
+
shouldClose = true;
|
|
492
|
+
reason = `stale-closed (${plan.status}, age: ${Math.round(age / 3600000)}h)`;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (shouldClose) {
|
|
497
|
+
const previousStatus = plan.status;
|
|
498
|
+
// Force transition to completed (bypass FSM since these are stale)
|
|
499
|
+
plan.status = 'completed';
|
|
500
|
+
plan.updatedAt = now;
|
|
501
|
+
if (!plan.reconciliation) {
|
|
502
|
+
plan.reconciliation = {
|
|
503
|
+
planId: plan.id,
|
|
504
|
+
accuracy: 0,
|
|
505
|
+
driftItems: [],
|
|
506
|
+
summary: `Auto-closed: ${reason}`,
|
|
507
|
+
reconciledAt: now,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
closed.push({ id: plan.id, previousStatus, reason });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (closed.length > 0) this.save();
|
|
515
|
+
return { closedIds: closed.map((c) => c.id), closedPlans: closed };
|
|
516
|
+
}
|
|
517
|
+
|
|
448
518
|
stats(): {
|
|
449
519
|
total: number;
|
|
450
520
|
byStatus: Record<PlanStatus, number>;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
assessTaskComplexity,
|
|
4
|
+
type AssessmentInput,
|
|
5
|
+
type AssessmentResult,
|
|
6
|
+
} from './task-complexity-assessor.js';
|
|
7
|
+
|
|
8
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function assess(partial: Partial<AssessmentInput> & { prompt: string }): AssessmentResult {
|
|
11
|
+
return assessTaskComplexity(partial);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function signalByName(result: AssessmentResult, name: string) {
|
|
15
|
+
return result.signals.find((s) => s.name === name);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ─── Simple Tasks ───────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
describe('assessTaskComplexity — simple tasks', () => {
|
|
21
|
+
it('classifies "rename variable X" as simple', () => {
|
|
22
|
+
const result = assess({ prompt: 'rename variable X to Y' });
|
|
23
|
+
expect(result.classification).toBe('simple');
|
|
24
|
+
expect(result.score).toBeLessThan(40);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('classifies "fix typo in README" as simple', () => {
|
|
28
|
+
const result = assess({ prompt: 'fix typo in README' });
|
|
29
|
+
expect(result.classification).toBe('simple');
|
|
30
|
+
expect(result.score).toBeLessThan(40);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('classifies "add CSS class" as simple', () => {
|
|
34
|
+
const result = assess({ prompt: 'add CSS class to the header' });
|
|
35
|
+
expect(result.classification).toBe('simple');
|
|
36
|
+
expect(result.score).toBeLessThan(40);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('classifies single-file estimate as simple', () => {
|
|
40
|
+
const result = assess({ prompt: 'update button color', filesEstimated: 1 });
|
|
41
|
+
expect(result.classification).toBe('simple');
|
|
42
|
+
expect(signalByName(result, 'file-count')!.triggered).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('classifies task with 2 files as simple', () => {
|
|
46
|
+
const result = assess({ prompt: 'update two files', filesEstimated: 2 });
|
|
47
|
+
expect(result.classification).toBe('simple');
|
|
48
|
+
expect(signalByName(result, 'file-count')!.triggered).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ─── Complex Tasks ──────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
describe('assessTaskComplexity — complex tasks', () => {
|
|
55
|
+
it('classifies "add authentication" touching multiple files as complex', () => {
|
|
56
|
+
const result = assess({ prompt: 'add authentication to the API', filesEstimated: 4 });
|
|
57
|
+
expect(result.classification).toBe('complex');
|
|
58
|
+
expect(result.score).toBeGreaterThanOrEqual(40);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('classifies "refactor the vault module" as complex via cross-cutting when combined with files', () => {
|
|
62
|
+
const result = assess({ prompt: 'refactor across the vault module', filesEstimated: 5 });
|
|
63
|
+
expect(result.classification).toBe('complex');
|
|
64
|
+
expect(result.score).toBeGreaterThanOrEqual(40);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('classifies "migrate database schema" touching multiple files as complex', () => {
|
|
68
|
+
const result = assess({ prompt: 'migrate database schema to v2', filesEstimated: 3 });
|
|
69
|
+
expect(result.classification).toBe('complex');
|
|
70
|
+
expect(signalByName(result, 'cross-cutting-keywords')!.triggered).toBe(true);
|
|
71
|
+
expect(signalByName(result, 'file-count')!.triggered).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('classifies many-file task with design decision as complex', () => {
|
|
75
|
+
const result = assess({
|
|
76
|
+
prompt: 'how should we update styles across the app',
|
|
77
|
+
filesEstimated: 5,
|
|
78
|
+
});
|
|
79
|
+
expect(result.classification).toBe('complex');
|
|
80
|
+
expect(result.score).toBeGreaterThanOrEqual(40);
|
|
81
|
+
expect(signalByName(result, 'file-count')!.triggered).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('classifies task with design decision as complex', () => {
|
|
85
|
+
const result = assess({
|
|
86
|
+
prompt: 'how should we structure the new cache layer',
|
|
87
|
+
filesEstimated: 3,
|
|
88
|
+
});
|
|
89
|
+
expect(result.classification).toBe('complex');
|
|
90
|
+
expect(signalByName(result, 'design-decisions-needed')!.triggered).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('classifies task with new dependency as complex', () => {
|
|
94
|
+
const result = assess({
|
|
95
|
+
prompt: 'add a new package for rate limiting and install it',
|
|
96
|
+
filesEstimated: 3,
|
|
97
|
+
});
|
|
98
|
+
expect(result.classification).toBe('complex');
|
|
99
|
+
expect(signalByName(result, 'new-dependencies')!.triggered).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ─── Edge Cases ─────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
describe('assessTaskComplexity — edge cases', () => {
|
|
106
|
+
it('handles empty prompt as simple', () => {
|
|
107
|
+
const result = assess({ prompt: '' });
|
|
108
|
+
expect(result.classification).toBe('simple');
|
|
109
|
+
expect(result.score).toBe(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('clamps score to 0 minimum (negative weights only)', () => {
|
|
113
|
+
const result = assess({
|
|
114
|
+
prompt: 'do the thing',
|
|
115
|
+
hasParentPlan: true,
|
|
116
|
+
});
|
|
117
|
+
expect(result.score).toBe(0);
|
|
118
|
+
expect(result.classification).toBe('simple');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('clamps score to 100 maximum', () => {
|
|
122
|
+
const result = assess({
|
|
123
|
+
prompt:
|
|
124
|
+
'add authentication, migrate the DB, install new package, how should we design this, refactor across all modules',
|
|
125
|
+
filesEstimated: 10,
|
|
126
|
+
domains: ['vault', 'brain', 'planning'],
|
|
127
|
+
});
|
|
128
|
+
expect(result.score).toBeLessThanOrEqual(100);
|
|
129
|
+
expect(result.score).toBeGreaterThanOrEqual(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('parent context reduces complexity', () => {
|
|
133
|
+
const withoutParent = assess({
|
|
134
|
+
prompt: 'add authorization to the API',
|
|
135
|
+
filesEstimated: 4,
|
|
136
|
+
});
|
|
137
|
+
const withParent = assess({
|
|
138
|
+
prompt: 'add authorization to the API',
|
|
139
|
+
filesEstimated: 4,
|
|
140
|
+
hasParentPlan: true,
|
|
141
|
+
});
|
|
142
|
+
expect(withParent.score).toBeLessThan(withoutParent.score);
|
|
143
|
+
expect(signalByName(withParent, 'approach-already-described')!.triggered).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('parentIssueContext also reduces complexity', () => {
|
|
147
|
+
const result = assess({
|
|
148
|
+
prompt: 'add authorization to the API',
|
|
149
|
+
filesEstimated: 4,
|
|
150
|
+
parentIssueContext: 'Use middleware pattern as described in RFC-42',
|
|
151
|
+
});
|
|
152
|
+
expect(signalByName(result, 'approach-already-described')!.triggered).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('borderline score at exactly 40 is complex', () => {
|
|
156
|
+
// file-count (25) + new-dependencies (15) = 40
|
|
157
|
+
const result = assess({
|
|
158
|
+
prompt: 'install the redis package',
|
|
159
|
+
filesEstimated: 3,
|
|
160
|
+
});
|
|
161
|
+
expect(result.score).toBe(40);
|
|
162
|
+
expect(result.classification).toBe('complex');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('borderline score at 39 is simple', () => {
|
|
166
|
+
// file-count (25) + new-dependencies (15) + approach-described (-15) = 25
|
|
167
|
+
const result = assess({
|
|
168
|
+
prompt: 'install the redis package',
|
|
169
|
+
filesEstimated: 3,
|
|
170
|
+
hasParentPlan: true,
|
|
171
|
+
});
|
|
172
|
+
expect(result.score).toBeLessThan(40);
|
|
173
|
+
expect(result.classification).toBe('simple');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ─── Individual Signals ─────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
describe('assessTaskComplexity — individual signals', () => {
|
|
180
|
+
describe('file-count signal', () => {
|
|
181
|
+
it('triggers at 3 files', () => {
|
|
182
|
+
const result = assess({ prompt: 'task', filesEstimated: 3 });
|
|
183
|
+
expect(signalByName(result, 'file-count')!.triggered).toBe(true);
|
|
184
|
+
expect(signalByName(result, 'file-count')!.weight).toBe(25);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('does not trigger at 2 files', () => {
|
|
188
|
+
const result = assess({ prompt: 'task', filesEstimated: 2 });
|
|
189
|
+
expect(signalByName(result, 'file-count')!.triggered).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('does not trigger when no estimate provided', () => {
|
|
193
|
+
const result = assess({ prompt: 'task' });
|
|
194
|
+
expect(signalByName(result, 'file-count')!.triggered).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('cross-cutting-keywords signal', () => {
|
|
199
|
+
it.each([
|
|
200
|
+
'add authentication',
|
|
201
|
+
'implement authorization',
|
|
202
|
+
'migrate the database',
|
|
203
|
+
'refactor across modules',
|
|
204
|
+
'handle cross-cutting concerns',
|
|
205
|
+
])('triggers for: "%s"', (prompt) => {
|
|
206
|
+
const result = assess({ prompt });
|
|
207
|
+
expect(signalByName(result, 'cross-cutting-keywords')!.triggered).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('does not trigger for benign text', () => {
|
|
211
|
+
const result = assess({ prompt: 'fix button alignment' });
|
|
212
|
+
expect(signalByName(result, 'cross-cutting-keywords')!.triggered).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('new-dependencies signal', () => {
|
|
217
|
+
it.each([
|
|
218
|
+
'add dependency for caching',
|
|
219
|
+
'install redis',
|
|
220
|
+
'new package for validation',
|
|
221
|
+
'npm install lodash',
|
|
222
|
+
])('triggers for: "%s"', (prompt) => {
|
|
223
|
+
const result = assess({ prompt });
|
|
224
|
+
expect(signalByName(result, 'new-dependencies')!.triggered).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('does not trigger for normal text', () => {
|
|
228
|
+
const result = assess({ prompt: 'update existing code' });
|
|
229
|
+
expect(signalByName(result, 'new-dependencies')!.triggered).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('design-decisions-needed signal', () => {
|
|
234
|
+
it.each([
|
|
235
|
+
'how should we handle caching',
|
|
236
|
+
'which approach for the API',
|
|
237
|
+
'design decision on storage',
|
|
238
|
+
'architectural decision for events',
|
|
239
|
+
'evaluate the trade-off between speed and accuracy',
|
|
240
|
+
])('triggers for: "%s"', (prompt) => {
|
|
241
|
+
const result = assess({ prompt });
|
|
242
|
+
expect(signalByName(result, 'design-decisions-needed')!.triggered).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('approach-already-described signal', () => {
|
|
247
|
+
it('triggers with hasParentPlan', () => {
|
|
248
|
+
const result = assess({ prompt: 'task', hasParentPlan: true });
|
|
249
|
+
const signal = signalByName(result, 'approach-already-described')!;
|
|
250
|
+
expect(signal.triggered).toBe(true);
|
|
251
|
+
expect(signal.weight).toBe(-15);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('triggers with parentIssueContext', () => {
|
|
255
|
+
const result = assess({ prompt: 'task', parentIssueContext: 'Steps described here' });
|
|
256
|
+
expect(signalByName(result, 'approach-already-described')!.triggered).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('does not trigger with empty parentIssueContext', () => {
|
|
260
|
+
const result = assess({ prompt: 'task', parentIssueContext: ' ' });
|
|
261
|
+
expect(signalByName(result, 'approach-already-described')!.triggered).toBe(false);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('multi-domain signal', () => {
|
|
266
|
+
it('triggers with 2+ domains', () => {
|
|
267
|
+
const result = assess({ prompt: 'task', domains: ['vault', 'brain'] });
|
|
268
|
+
expect(signalByName(result, 'multi-domain')!.triggered).toBe(true);
|
|
269
|
+
expect(signalByName(result, 'multi-domain')!.weight).toBe(5);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('does not trigger with single domain', () => {
|
|
273
|
+
const result = assess({ prompt: 'task', domains: ['vault'] });
|
|
274
|
+
expect(signalByName(result, 'multi-domain')!.triggered).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('does not trigger with no domains', () => {
|
|
278
|
+
const result = assess({ prompt: 'task' });
|
|
279
|
+
expect(signalByName(result, 'multi-domain')!.triggered).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ─── Reasoning Output ───────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
describe('assessTaskComplexity — reasoning', () => {
|
|
287
|
+
it('includes signal names in reasoning when triggered', () => {
|
|
288
|
+
const result = assess({ prompt: 'migrate the database', filesEstimated: 5 });
|
|
289
|
+
expect(result.reasoning).toContain('cross-cutting-keywords');
|
|
290
|
+
expect(result.reasoning).toContain('file-count');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('provides fallback reasoning when nothing triggers', () => {
|
|
294
|
+
const result = assess({ prompt: 'fix typo' });
|
|
295
|
+
expect(result.reasoning).toContain('No complexity signals detected');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('always returns 6 signals', () => {
|
|
299
|
+
const result = assess({ prompt: 'anything' });
|
|
300
|
+
expect(result.signals).toHaveLength(6);
|
|
301
|
+
});
|
|
302
|
+
});
|