@soleri/core 9.3.1 → 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.
Files changed (172) hide show
  1. package/dist/brain/intelligence.d.ts +5 -0
  2. package/dist/brain/intelligence.d.ts.map +1 -1
  3. package/dist/brain/intelligence.js +115 -26
  4. package/dist/brain/intelligence.js.map +1 -1
  5. package/dist/brain/learning-radar.d.ts +3 -3
  6. package/dist/brain/learning-radar.d.ts.map +1 -1
  7. package/dist/brain/learning-radar.js +8 -4
  8. package/dist/brain/learning-radar.js.map +1 -1
  9. package/dist/control/intent-router.d.ts +2 -2
  10. package/dist/control/intent-router.d.ts.map +1 -1
  11. package/dist/control/intent-router.js +35 -1
  12. package/dist/control/intent-router.js.map +1 -1
  13. package/dist/control/types.d.ts +10 -2
  14. package/dist/control/types.d.ts.map +1 -1
  15. package/dist/curator/curator.d.ts +4 -0
  16. package/dist/curator/curator.d.ts.map +1 -1
  17. package/dist/curator/curator.js +23 -1
  18. package/dist/curator/curator.js.map +1 -1
  19. package/dist/curator/schema.d.ts +1 -1
  20. package/dist/curator/schema.d.ts.map +1 -1
  21. package/dist/curator/schema.js +8 -0
  22. package/dist/curator/schema.js.map +1 -1
  23. package/dist/domain-packs/types.d.ts +6 -0
  24. package/dist/domain-packs/types.d.ts.map +1 -1
  25. package/dist/domain-packs/types.js +1 -0
  26. package/dist/domain-packs/types.js.map +1 -1
  27. package/dist/engine/module-manifest.js +3 -3
  28. package/dist/engine/module-manifest.js.map +1 -1
  29. package/dist/engine/register-engine.d.ts +9 -0
  30. package/dist/engine/register-engine.d.ts.map +1 -1
  31. package/dist/engine/register-engine.js +59 -1
  32. package/dist/engine/register-engine.js.map +1 -1
  33. package/dist/facades/types.d.ts +5 -1
  34. package/dist/facades/types.d.ts.map +1 -1
  35. package/dist/facades/types.js.map +1 -1
  36. package/dist/index.d.ts +4 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +3 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/operator/operator-context-store.d.ts +54 -0
  41. package/dist/operator/operator-context-store.d.ts.map +1 -0
  42. package/dist/operator/operator-context-store.js +434 -0
  43. package/dist/operator/operator-context-store.js.map +1 -0
  44. package/dist/operator/operator-context-types.d.ts +101 -0
  45. package/dist/operator/operator-context-types.d.ts.map +1 -0
  46. package/dist/operator/operator-context-types.js +27 -0
  47. package/dist/operator/operator-context-types.js.map +1 -0
  48. package/dist/packs/index.d.ts +2 -2
  49. package/dist/packs/index.d.ts.map +1 -1
  50. package/dist/packs/index.js +1 -1
  51. package/dist/packs/index.js.map +1 -1
  52. package/dist/packs/lockfile.d.ts +3 -0
  53. package/dist/packs/lockfile.d.ts.map +1 -1
  54. package/dist/packs/lockfile.js.map +1 -1
  55. package/dist/packs/types.d.ts +8 -2
  56. package/dist/packs/types.d.ts.map +1 -1
  57. package/dist/packs/types.js +6 -0
  58. package/dist/packs/types.js.map +1 -1
  59. package/dist/planning/plan-lifecycle.d.ts +12 -1
  60. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  61. package/dist/planning/plan-lifecycle.js +52 -19
  62. package/dist/planning/plan-lifecycle.js.map +1 -1
  63. package/dist/planning/planner-types.d.ts +6 -0
  64. package/dist/planning/planner-types.d.ts.map +1 -1
  65. package/dist/planning/planner.d.ts +21 -1
  66. package/dist/planning/planner.d.ts.map +1 -1
  67. package/dist/planning/planner.js +62 -3
  68. package/dist/planning/planner.js.map +1 -1
  69. package/dist/planning/task-complexity-assessor.d.ts.map +1 -1
  70. package/dist/planning/task-complexity-assessor.js.map +1 -1
  71. package/dist/plugins/types.d.ts +18 -18
  72. package/dist/runtime/admin-ops.d.ts +1 -1
  73. package/dist/runtime/admin-ops.d.ts.map +1 -1
  74. package/dist/runtime/admin-ops.js +100 -3
  75. package/dist/runtime/admin-ops.js.map +1 -1
  76. package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
  77. package/dist/runtime/admin-setup-ops.js +19 -9
  78. package/dist/runtime/admin-setup-ops.js.map +1 -1
  79. package/dist/runtime/capture-ops.d.ts.map +1 -1
  80. package/dist/runtime/capture-ops.js +35 -7
  81. package/dist/runtime/capture-ops.js.map +1 -1
  82. package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
  83. package/dist/runtime/facades/brain-facade.js +4 -2
  84. package/dist/runtime/facades/brain-facade.js.map +1 -1
  85. package/dist/runtime/facades/control-facade.d.ts.map +1 -1
  86. package/dist/runtime/facades/control-facade.js +8 -2
  87. package/dist/runtime/facades/control-facade.js.map +1 -1
  88. package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
  89. package/dist/runtime/facades/curator-facade.js +13 -0
  90. package/dist/runtime/facades/curator-facade.js.map +1 -1
  91. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  92. package/dist/runtime/facades/memory-facade.js +10 -12
  93. package/dist/runtime/facades/memory-facade.js.map +1 -1
  94. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  95. package/dist/runtime/facades/orchestrate-facade.js +36 -1
  96. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  97. package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
  98. package/dist/runtime/facades/plan-facade.js +20 -4
  99. package/dist/runtime/facades/plan-facade.js.map +1 -1
  100. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  101. package/dist/runtime/orchestrate-ops.js +71 -4
  102. package/dist/runtime/orchestrate-ops.js.map +1 -1
  103. package/dist/runtime/plan-feedback-helper.d.ts +21 -0
  104. package/dist/runtime/plan-feedback-helper.d.ts.map +1 -0
  105. package/dist/runtime/plan-feedback-helper.js +52 -0
  106. package/dist/runtime/plan-feedback-helper.js.map +1 -0
  107. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  108. package/dist/runtime/planning-extra-ops.js +73 -34
  109. package/dist/runtime/planning-extra-ops.js.map +1 -1
  110. package/dist/runtime/session-briefing.d.ts.map +1 -1
  111. package/dist/runtime/session-briefing.js +9 -1
  112. package/dist/runtime/session-briefing.js.map +1 -1
  113. package/dist/runtime/types.d.ts +3 -0
  114. package/dist/runtime/types.d.ts.map +1 -1
  115. package/dist/skills/sync-skills.d.ts.map +1 -1
  116. package/dist/skills/sync-skills.js +13 -7
  117. package/dist/skills/sync-skills.js.map +1 -1
  118. package/package.json +1 -1
  119. package/src/brain/brain-intelligence.test.ts +30 -0
  120. package/src/brain/extraction-quality.test.ts +323 -0
  121. package/src/brain/intelligence.ts +133 -30
  122. package/src/brain/learning-radar.ts +8 -5
  123. package/src/brain/second-brain-features.test.ts +1 -1
  124. package/src/control/intent-router.test.ts +73 -3
  125. package/src/control/intent-router.ts +38 -1
  126. package/src/control/types.ts +13 -2
  127. package/src/curator/curator.test.ts +92 -0
  128. package/src/curator/curator.ts +29 -1
  129. package/src/curator/schema.ts +8 -0
  130. package/src/domain-packs/types.ts +8 -0
  131. package/src/engine/module-manifest.test.ts +8 -2
  132. package/src/engine/module-manifest.ts +3 -3
  133. package/src/engine/register-engine.test.ts +73 -1
  134. package/src/engine/register-engine.ts +61 -1
  135. package/src/facades/types.ts +5 -0
  136. package/src/index.ts +22 -0
  137. package/src/operator/operator-context-store.test.ts +698 -0
  138. package/src/operator/operator-context-store.ts +569 -0
  139. package/src/operator/operator-context-types.ts +139 -0
  140. package/src/packs/index.ts +3 -1
  141. package/src/packs/lockfile.ts +3 -0
  142. package/src/packs/types.ts +9 -0
  143. package/src/planning/plan-lifecycle.ts +80 -22
  144. package/src/planning/planner-types.ts +6 -0
  145. package/src/planning/planner.ts +74 -4
  146. package/src/planning/task-complexity-assessor.test.ts +6 -2
  147. package/src/planning/task-complexity-assessor.ts +1 -4
  148. package/src/runtime/admin-ops.test.ts +139 -6
  149. package/src/runtime/admin-ops.ts +104 -3
  150. package/src/runtime/admin-setup-ops.ts +30 -10
  151. package/src/runtime/capture-ops.test.ts +84 -0
  152. package/src/runtime/capture-ops.ts +35 -7
  153. package/src/runtime/facades/admin-facade.test.ts +1 -1
  154. package/src/runtime/facades/brain-facade.ts +6 -3
  155. package/src/runtime/facades/control-facade.ts +10 -2
  156. package/src/runtime/facades/curator-facade.ts +18 -0
  157. package/src/runtime/facades/memory-facade.test.ts +14 -12
  158. package/src/runtime/facades/memory-facade.ts +10 -12
  159. package/src/runtime/facades/orchestrate-facade.ts +33 -1
  160. package/src/runtime/facades/plan-facade.test.ts +213 -0
  161. package/src/runtime/facades/plan-facade.ts +23 -4
  162. package/src/runtime/orchestrate-ops.test.ts +202 -2
  163. package/src/runtime/orchestrate-ops.ts +85 -4
  164. package/src/runtime/plan-feedback-helper.test.ts +173 -0
  165. package/src/runtime/plan-feedback-helper.ts +63 -0
  166. package/src/runtime/planning-extra-ops.test.ts +43 -1
  167. package/src/runtime/planning-extra-ops.ts +96 -33
  168. package/src/runtime/session-briefing.test.ts +1 -0
  169. package/src/runtime/session-briefing.ts +10 -1
  170. package/src/runtime/types.ts +3 -0
  171. package/src/skills/sync-skills.ts +14 -7
  172. package/vitest.config.ts +1 -0
@@ -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';
@@ -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 */
@@ -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<{ title: string; description: string }>;
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): void {
257
+ export function applyIteration(plan: Plan, changes: IterateChanges): number {
250
258
  const now = Date.now();
251
- if (changes.objective !== undefined) plan.objective = changes.objective;
252
- if (changes.scope !== undefined) plan.scope = changes.scope;
253
- if (changes.decisions !== undefined) plan.decisions = changes.decisions;
254
- if (changes.approach !== undefined) plan.approach = changes.approach;
255
- if (changes.context !== undefined) plan.context = changes.context;
256
- if (changes.success_criteria !== undefined) plan.success_criteria = changes.success_criteria;
257
- if (changes.tool_chain !== undefined) plan.tool_chain = changes.tool_chain;
258
- if (changes.flow !== undefined) plan.flow = changes.flow;
259
- if (changes.target_mode !== undefined) plan.target_mode = changes.target_mode;
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: changes.addTasks[i].title,
273
- description: changes.addTasks[i].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
- plan.updatedAt = now;
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<{ title: string; description: string }>;
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). */
@@ -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
- this.save();
246
- return plan;
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>;
@@ -72,7 +72,10 @@ describe('assessTaskComplexity — complex tasks', () => {
72
72
  });
73
73
 
74
74
  it('classifies many-file task with design decision as complex', () => {
75
- const result = assess({ prompt: 'how should we update styles across the app', filesEstimated: 5 });
75
+ const result = assess({
76
+ prompt: 'how should we update styles across the app',
77
+ filesEstimated: 5,
78
+ });
76
79
  expect(result.classification).toBe('complex');
77
80
  expect(result.score).toBeGreaterThanOrEqual(40);
78
81
  expect(signalByName(result, 'file-count')!.triggered).toBe(true);
@@ -117,7 +120,8 @@ describe('assessTaskComplexity — edge cases', () => {
117
120
 
118
121
  it('clamps score to 100 maximum', () => {
119
122
  const result = assess({
120
- prompt: 'add authentication, migrate the DB, install new package, how should we design this, refactor across all modules',
123
+ prompt:
124
+ 'add authentication, migrate the DB, install new package, how should we design this, refactor across all modules',
121
125
  filesEstimated: 10,
122
126
  domains: ['vault', 'brain', 'planning'],
123
127
  });
@@ -164,10 +164,7 @@ export function assessTaskComplexity(input: AssessmentInput): AssessmentResult {
164
164
  detectMultiDomain(input),
165
165
  ];
166
166
 
167
- const rawScore = signals.reduce(
168
- (sum, s) => sum + (s.triggered ? s.weight : 0),
169
- 0,
170
- );
167
+ const rawScore = signals.reduce((sum, s) => sum + (s.triggered ? s.weight : 0), 0);
171
168
 
172
169
  // Clamp to 0-100
173
170
  const score = Math.max(0, Math.min(100, rawScore));
@@ -35,6 +35,9 @@ function mockRuntime(): AgentRuntime {
35
35
  curator: {
36
36
  getStatus: vi.fn().mockReturnValue({ initialized: true }),
37
37
  },
38
+ packInstaller: {
39
+ list: vi.fn().mockReturnValue([]),
40
+ },
38
41
  contextHealth: {
39
42
  check: vi.fn().mockReturnValue({
40
43
  level: 'green',
@@ -67,8 +70,8 @@ describe('createAdminOps', () => {
67
70
  ops = createAdminOps(rt);
68
71
  });
69
72
 
70
- it('returns 9 ops', () => {
71
- expect(ops.length).toBe(9);
73
+ it('returns 11 ops', () => {
74
+ expect(ops.length).toBe(11);
72
75
  });
73
76
 
74
77
  // ─── admin_health ─────────────────────────────────────────────
@@ -98,6 +101,50 @@ describe('createAdminOps', () => {
98
101
  expect(llm.openai).toBe(true);
99
102
  expect(llm.anthropic).toBe(false);
100
103
  });
104
+
105
+ it('reports skills status', async () => {
106
+ const op = findOp(ops, 'admin_health');
107
+ const result = (await op.handler({})) as Record<string, unknown>;
108
+ const skills = result.skills as Record<string, unknown>;
109
+ expect(skills).toBeDefined();
110
+ expect(skills.count).toBe(0);
111
+ expect(skills.agent).toEqual([]);
112
+ expect(skills.packs).toEqual([]);
113
+ });
114
+
115
+ it('reports hooks status', async () => {
116
+ const op = findOp(ops, 'admin_health');
117
+ const result = (await op.handler({})) as Record<string, unknown>;
118
+ const hooks = result.hooks as Record<string, unknown>;
119
+ expect(hooks).toBeDefined();
120
+ expect(hooks.count).toBe(0);
121
+ expect(hooks.packs).toEqual([]);
122
+ });
123
+
124
+ it('includes pack skills and hooks when packs are installed', async () => {
125
+ vi.mocked(rt.packInstaller.list).mockReturnValue([
126
+ {
127
+ id: 'test-pack',
128
+ manifest: {} as never,
129
+ directory: '/tmp/pack',
130
+ status: 'installed',
131
+ vaultEntries: 5,
132
+ skills: ['my-skill', 'another-skill'],
133
+ hooks: ['my-hook'],
134
+ facadesRegistered: false,
135
+ installedAt: Date.now(),
136
+ },
137
+ ]);
138
+ const updatedOps = createAdminOps(rt);
139
+ const op = findOp(updatedOps, 'admin_health');
140
+ const result = (await op.handler({})) as Record<string, unknown>;
141
+ const skills = result.skills as Record<string, unknown>;
142
+ expect(skills.count).toBe(2);
143
+ expect(skills.packs).toEqual(['my-skill', 'another-skill']);
144
+ const hooks = result.hooks as Record<string, unknown>;
145
+ expect(hooks.count).toBe(1);
146
+ expect(hooks.packs).toEqual(['my-hook']);
147
+ });
101
148
  });
102
149
 
103
150
  // ─── context_health ───────────────────────────────────────────
@@ -135,9 +182,7 @@ describe('createAdminOps', () => {
135
182
 
136
183
  it('returns routing hints in grouped mode', async () => {
137
184
  const op = findOp(ops, 'admin_tool_list');
138
- const allOps = [
139
- { name: 'admin_health', description: 'Health check', auth: 'read' },
140
- ];
185
+ const allOps = [{ name: 'admin_health', description: 'Health check', auth: 'read' }];
141
186
  const result = (await op.handler({ _allOps: allOps })) as Record<string, unknown>;
142
187
  const routing = result.routing as Record<string, string>;
143
188
  expect(routing).toBeDefined();
@@ -229,6 +274,94 @@ describe('createAdminOps', () => {
229
274
  });
230
275
  });
231
276
 
277
+ // ─── operator_context_inspect ────────────────────────────────
278
+
279
+ describe('operator_context_inspect', () => {
280
+ it('returns full profile when store is available', async () => {
281
+ const mockContext = {
282
+ expertise: [
283
+ {
284
+ topic: 'TypeScript',
285
+ level: 'expert',
286
+ confidence: 0.9,
287
+ sessionCount: 5,
288
+ lastObserved: Date.now(),
289
+ },
290
+ ],
291
+ corrections: [],
292
+ interests: [
293
+ { tag: 'testing', confidence: 0.7, mentionCount: 3, lastMentioned: Date.now() },
294
+ ],
295
+ patterns: [],
296
+ sessionCount: 5,
297
+ lastUpdated: Date.now(),
298
+ };
299
+ (rt as Record<string, unknown>).operatorContextStore = {
300
+ inspect: vi.fn().mockReturnValue(mockContext),
301
+ deleteItem: vi.fn(),
302
+ };
303
+ const updatedOps = createAdminOps(rt);
304
+ const op = findOp(updatedOps, 'operator_context_inspect');
305
+ const result = (await op.handler({})) as Record<string, unknown>;
306
+ expect(result.available).toBe(true);
307
+ expect(result.expertise).toEqual(mockContext.expertise);
308
+ expect(result.interests).toEqual(mockContext.interests);
309
+ });
310
+
311
+ it('returns not-available when store is missing', async () => {
312
+ // Default mock runtime has no operatorContextStore
313
+ const op = findOp(ops, 'operator_context_inspect');
314
+ const result = (await op.handler({})) as Record<string, unknown>;
315
+ expect(result.available).toBe(false);
316
+ expect(result.message).toBe('Operator context not configured');
317
+ });
318
+ });
319
+
320
+ // ─── operator_context_delete ───────────────────────────────────
321
+
322
+ describe('operator_context_delete', () => {
323
+ it('removes an item successfully', async () => {
324
+ (rt as Record<string, unknown>).operatorContextStore = {
325
+ inspect: vi.fn(),
326
+ deleteItem: vi.fn().mockReturnValue(true),
327
+ };
328
+ const updatedOps = createAdminOps(rt);
329
+ const op = findOp(updatedOps, 'operator_context_delete');
330
+ const result = (await op.handler({ type: 'expertise', id: 'abc-123' })) as Record<
331
+ string,
332
+ unknown
333
+ >;
334
+ expect(result.deleted).toBe(true);
335
+ expect(result.type).toBe('expertise');
336
+ expect(result.id).toBe('abc-123');
337
+ });
338
+
339
+ it('returns false for missing item', async () => {
340
+ (rt as Record<string, unknown>).operatorContextStore = {
341
+ inspect: vi.fn(),
342
+ deleteItem: vi.fn().mockReturnValue(false),
343
+ };
344
+ const updatedOps = createAdminOps(rt);
345
+ const op = findOp(updatedOps, 'operator_context_delete');
346
+ const result = (await op.handler({ type: 'pattern', id: 'nonexistent' })) as Record<
347
+ string,
348
+ unknown
349
+ >;
350
+ expect(result.deleted).toBe(false);
351
+ expect(result.message).toBe('Item not found');
352
+ });
353
+
354
+ it('returns not-available when store is missing', async () => {
355
+ const op = findOp(ops, 'operator_context_delete');
356
+ const result = (await op.handler({ type: 'expertise', id: 'abc' })) as Record<
357
+ string,
358
+ unknown
359
+ >;
360
+ expect(result.deleted).toBe(false);
361
+ expect(result.message).toBe('Operator context not configured');
362
+ });
363
+ });
364
+
232
365
  // ─── admin_diagnostic ─────────────────────────────────────────
233
366
 
234
367
  describe('admin_diagnostic', () => {
@@ -241,7 +374,7 @@ describe('createAdminOps', () => {
241
374
  expect(result).toHaveProperty('checks');
242
375
  expect(result).toHaveProperty('summary');
243
376
  const checks = result.checks as Array<Record<string, string>>;
244
- expect(checks.length).toBeGreaterThanOrEqual(5);
377
+ expect(checks.length).toBeGreaterThanOrEqual(8);
245
378
  });
246
379
 
247
380
  it('reports degraded when LLM unavailable', async () => {