@soleri/core 9.6.0 → 9.7.1

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 (90) hide show
  1. package/dist/index.d.ts +10 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +8 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/packs/index.d.ts +1 -1
  6. package/dist/packs/index.d.ts.map +1 -1
  7. package/dist/packs/index.js.map +1 -1
  8. package/dist/packs/types.d.ts +69 -42
  9. package/dist/packs/types.d.ts.map +1 -1
  10. package/dist/packs/types.js.map +1 -1
  11. package/dist/planning/github-projection.d.ts +3 -1
  12. package/dist/planning/github-projection.d.ts.map +1 -1
  13. package/dist/planning/github-projection.js +5 -1
  14. package/dist/planning/github-projection.js.map +1 -1
  15. package/dist/planning/goal-ancestry.d.ts +72 -0
  16. package/dist/planning/goal-ancestry.d.ts.map +1 -0
  17. package/dist/planning/goal-ancestry.js +137 -0
  18. package/dist/planning/goal-ancestry.js.map +1 -0
  19. package/dist/planning/plan-lifecycle.d.ts +2 -0
  20. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  21. package/dist/planning/plan-lifecycle.js +1 -0
  22. package/dist/planning/plan-lifecycle.js.map +1 -1
  23. package/dist/planning/planner-types.d.ts +2 -0
  24. package/dist/planning/planner-types.d.ts.map +1 -1
  25. package/dist/runtime/context-health.d.ts +14 -1
  26. package/dist/runtime/context-health.d.ts.map +1 -1
  27. package/dist/runtime/context-health.js +30 -2
  28. package/dist/runtime/context-health.js.map +1 -1
  29. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  30. package/dist/runtime/facades/orchestrate-facade.js +11 -0
  31. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  32. package/dist/session/compaction-evaluator.d.ts +20 -0
  33. package/dist/session/compaction-evaluator.d.ts.map +1 -0
  34. package/dist/session/compaction-evaluator.js +73 -0
  35. package/dist/session/compaction-evaluator.js.map +1 -0
  36. package/dist/session/compaction-policy.d.ts +50 -0
  37. package/dist/session/compaction-policy.d.ts.map +1 -0
  38. package/dist/session/compaction-policy.js +17 -0
  39. package/dist/session/compaction-policy.js.map +1 -0
  40. package/dist/session/handoff-renderer.d.ts +22 -0
  41. package/dist/session/handoff-renderer.d.ts.map +1 -0
  42. package/dist/session/handoff-renderer.js +49 -0
  43. package/dist/session/handoff-renderer.js.map +1 -0
  44. package/dist/session/index.d.ts +6 -0
  45. package/dist/session/index.d.ts.map +1 -0
  46. package/dist/session/index.js +5 -0
  47. package/dist/session/index.js.map +1 -0
  48. package/dist/session/policy-resolver.d.ts +20 -0
  49. package/dist/session/policy-resolver.d.ts.map +1 -0
  50. package/dist/session/policy-resolver.js +28 -0
  51. package/dist/session/policy-resolver.js.map +1 -0
  52. package/dist/skills/sync-skills.d.ts +27 -0
  53. package/dist/skills/sync-skills.d.ts.map +1 -1
  54. package/dist/skills/sync-skills.js +92 -1
  55. package/dist/skills/sync-skills.js.map +1 -1
  56. package/dist/skills/trust-classifier.d.ts +32 -0
  57. package/dist/skills/trust-classifier.d.ts.map +1 -0
  58. package/dist/skills/trust-classifier.js +109 -0
  59. package/dist/skills/trust-classifier.js.map +1 -0
  60. package/dist/subagent/dispatcher.d.ts +4 -0
  61. package/dist/subagent/dispatcher.d.ts.map +1 -1
  62. package/dist/subagent/dispatcher.js +14 -2
  63. package/dist/subagent/dispatcher.js.map +1 -1
  64. package/dist/update-check.d.ts +19 -0
  65. package/dist/update-check.d.ts.map +1 -1
  66. package/dist/update-check.js +51 -4
  67. package/dist/update-check.js.map +1 -1
  68. package/package.json +1 -4
  69. package/src/index.ts +44 -0
  70. package/src/packs/index.ts +4 -0
  71. package/src/packs/types.ts +32 -0
  72. package/src/planning/github-projection.ts +6 -0
  73. package/src/planning/goal-ancestry.test.ts +427 -0
  74. package/src/planning/goal-ancestry.ts +187 -0
  75. package/src/planning/plan-lifecycle.ts +3 -0
  76. package/src/planning/planner-types.ts +2 -0
  77. package/src/runtime/context-health.ts +42 -2
  78. package/src/runtime/facades/orchestrate-facade.ts +14 -0
  79. package/src/session/compaction-evaluator.ts +87 -0
  80. package/src/session/compaction-policy.ts +66 -0
  81. package/src/session/compaction.test.ts +259 -0
  82. package/src/session/handoff-renderer.ts +56 -0
  83. package/src/session/index.ts +12 -0
  84. package/src/session/policy-resolver.ts +34 -0
  85. package/src/skills/sync-skills.ts +114 -1
  86. package/src/skills/trust-classifier.test.ts +252 -0
  87. package/src/skills/trust-classifier.ts +127 -0
  88. package/src/subagent/dispatcher.ts +18 -2
  89. package/src/update-check.test.ts +91 -0
  90. package/src/update-check.ts +76 -6
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Goal Ancestry — hierarchical goal tracking for plans and tasks.
3
+ *
4
+ * Goals form a tree: objective → project → plan → task.
5
+ * Each plan/task can reference its parent goal, enabling context
6
+ * to flow from high-level objectives down to individual work items.
7
+ */
8
+
9
+ // ─── Types ────────────────────────────────────────────────────────
10
+
11
+ export type GoalLevel = 'objective' | 'project' | 'plan' | 'task';
12
+
13
+ export type GoalStatus = 'planned' | 'active' | 'completed' | 'abandoned';
14
+
15
+ export interface Goal {
16
+ id: string;
17
+ title: string;
18
+ level: GoalLevel;
19
+ parentId?: string;
20
+ status: GoalStatus;
21
+ createdAt?: number;
22
+ updatedAt?: number;
23
+ }
24
+
25
+ // ─── Goal Store ───────────────────────────────────────────────────
26
+
27
+ export interface GoalStore {
28
+ version: string;
29
+ goals: Goal[];
30
+ }
31
+
32
+ /**
33
+ * Persistent goal repository backed by a JSON file.
34
+ * Follows the same pattern as PlanStore in planner.ts.
35
+ */
36
+ export interface GoalRepository {
37
+ getById(id: string): Goal | null;
38
+ getByParentId(parentId: string): Goal[];
39
+ create(goal: Omit<Goal, 'createdAt' | 'updatedAt'>): Goal;
40
+ updateStatus(id: string, status: GoalStatus): Goal;
41
+ list(): Goal[];
42
+ }
43
+
44
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
45
+ import { dirname } from 'node:path';
46
+
47
+ export class JsonGoalRepository implements GoalRepository {
48
+ private store: GoalStore;
49
+
50
+ constructor(private filePath: string) {
51
+ this.store = this.load();
52
+ }
53
+
54
+ private load(): GoalStore {
55
+ if (!existsSync(this.filePath)) return { version: '1.0', goals: [] };
56
+ try {
57
+ const data = readFileSync(this.filePath, 'utf-8');
58
+ return JSON.parse(data) as GoalStore;
59
+ } catch {
60
+ return { version: '1.0', goals: [] };
61
+ }
62
+ }
63
+
64
+ private save(): void {
65
+ mkdirSync(dirname(this.filePath), { recursive: true });
66
+ writeFileSync(this.filePath, JSON.stringify(this.store, null, 2), 'utf-8');
67
+ }
68
+
69
+ getById(id: string): Goal | null {
70
+ return this.store.goals.find((g) => g.id === id) ?? null;
71
+ }
72
+
73
+ getByParentId(parentId: string): Goal[] {
74
+ return this.store.goals.filter((g) => g.parentId === parentId);
75
+ }
76
+
77
+ create(goal: Omit<Goal, 'createdAt' | 'updatedAt'>): Goal {
78
+ const now = Date.now();
79
+ const full: Goal = { ...goal, createdAt: now, updatedAt: now };
80
+ this.store.goals.push(full);
81
+ this.save();
82
+ return full;
83
+ }
84
+
85
+ updateStatus(id: string, status: GoalStatus): Goal {
86
+ const goal = this.getById(id);
87
+ if (!goal) throw new Error(`Goal not found: ${id}`);
88
+ goal.status = status;
89
+ goal.updatedAt = Date.now();
90
+ this.save();
91
+ return goal;
92
+ }
93
+
94
+ list(): Goal[] {
95
+ return [...this.store.goals];
96
+ }
97
+ }
98
+
99
+ // ─── Max ancestor depth ──────────────────────────────────────────
100
+
101
+ const MAX_ANCESTOR_DEPTH = 10;
102
+
103
+ // ─── GoalAncestry ────────────────────────────────────────────────
104
+
105
+ export class GoalAncestry {
106
+ constructor(private repo: GoalRepository) {}
107
+
108
+ /**
109
+ * Walk the parent chain from a goal up to the root.
110
+ * Returns ancestors from immediate parent to root (closest first).
111
+ * Max 10 levels; throws on cycle detection.
112
+ */
113
+ getAncestors(goalId: string): Goal[] {
114
+ const ancestors: Goal[] = [];
115
+ const visited = new Set<string>();
116
+ let currentId: string | undefined = goalId;
117
+
118
+ // Start by finding the goal itself to get its parentId
119
+ const start = this.repo.getById(goalId);
120
+ if (!start) return [];
121
+ currentId = start.parentId;
122
+
123
+ while (currentId && ancestors.length < MAX_ANCESTOR_DEPTH) {
124
+ if (visited.has(currentId)) {
125
+ throw new Error(`Cycle detected in goal hierarchy at goal '${currentId}'`);
126
+ }
127
+ visited.add(currentId);
128
+
129
+ const parent = this.repo.getById(currentId);
130
+ if (!parent) break;
131
+
132
+ ancestors.push(parent);
133
+ currentId = parent.parentId;
134
+ }
135
+
136
+ return ancestors;
137
+ }
138
+
139
+ /**
140
+ * Render a markdown summary of the goal hierarchy for a given goal.
141
+ * Shows the full chain from root objective down to the current goal.
142
+ */
143
+ getContext(goalId: string): string {
144
+ const goal = this.repo.getById(goalId);
145
+ if (!goal) return '';
146
+
147
+ const ancestors = this.getAncestors(goalId);
148
+ // Build chain from root to current: reverse ancestors then append current
149
+ const chain = [...ancestors].reverse();
150
+ chain.push(goal);
151
+
152
+ const lines: string[] = ['## Goal Context', ''];
153
+
154
+ for (let i = 0; i < chain.length; i++) {
155
+ const g = chain[i];
156
+ const indent = ' '.repeat(i);
157
+ const marker = i === chain.length - 1 ? '**→**' : '-';
158
+ lines.push(`${indent}${marker} [${g.level}] ${g.title} (${g.status})`);
159
+ }
160
+
161
+ return lines.join('\n');
162
+ }
163
+
164
+ /**
165
+ * Inject goal ancestry context into an execution context metadata object.
166
+ * Returns a new context with goalAncestry added to config.
167
+ */
168
+ inject<T extends { config?: Record<string, unknown> }>(ctx: T, goalId: string): T {
169
+ const rendered = this.getContext(goalId);
170
+ if (!rendered) return ctx;
171
+
172
+ return {
173
+ ...ctx,
174
+ config: {
175
+ ...ctx.config,
176
+ goalAncestry: rendered,
177
+ },
178
+ };
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Generate a goal ID with the given level prefix.
184
+ */
185
+ export function generateGoalId(level: GoalLevel): string {
186
+ return `goal-${level}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
187
+ }
@@ -372,6 +372,8 @@ export function createPlanObject(params: {
372
372
  target_mode?: string;
373
373
  alternatives?: import('./planner-types.js').PlanAlternative[];
374
374
  initialStatus?: 'brainstorming' | 'draft';
375
+ /** Optional goal ID to link this plan to the goal hierarchy. */
376
+ goalId?: string;
375
377
  }): Plan {
376
378
  const now = Date.now();
377
379
  return {
@@ -401,6 +403,7 @@ export function createPlanObject(params: {
401
403
  ...(params.flow !== undefined && { flow: params.flow }),
402
404
  ...(params.target_mode !== undefined && { target_mode: params.target_mode }),
403
405
  ...(params.alternatives !== undefined && { alternatives: params.alternatives }),
406
+ ...(params.goalId !== undefined && { goalId: params.goalId }),
404
407
  checks: [],
405
408
  createdAt: now,
406
409
  updatedAt: now,
@@ -205,6 +205,8 @@ export interface Plan {
205
205
  };
206
206
  /** Aggregate execution metrics — populated by reconcile() and complete(). */
207
207
  executionSummary?: ExecutionSummary;
208
+ /** Goal ID linking this plan to the goal hierarchy. */
209
+ goalId?: string;
208
210
  createdAt: number;
209
211
  updatedAt: number;
210
212
  }
@@ -4,8 +4,18 @@
4
4
  *
5
5
  * Heuristic: tool calls x average payload size x 1.5 overhead factor.
6
6
  * Assumes ~200K token context window for fill estimation.
7
+ *
8
+ * Also integrates with CompactionPolicy to trigger session rotation
9
+ * when policy thresholds are breached.
7
10
  */
8
11
 
12
+ import type {
13
+ CompactionPolicy,
14
+ CompactionResult,
15
+ SessionState,
16
+ } from '../session/compaction-policy.js';
17
+ import { shouldCompact } from '../session/compaction-evaluator.js';
18
+
9
19
  // =============================================================================
10
20
  // TYPES
11
21
  // =============================================================================
@@ -18,6 +28,8 @@ export interface ContextHealthStatus {
18
28
  toolCallCount: number;
19
29
  estimatedTokens: number;
20
30
  recommendation: string;
31
+ /** When a compaction policy is set, this contains the evaluation result. */
32
+ compaction?: CompactionResult;
21
33
  }
22
34
 
23
35
  export interface TrackEvent {
@@ -47,6 +59,17 @@ const RECOMMENDATIONS: Record<HealthLevel, string> = {
47
59
  export class ContextHealthMonitor {
48
60
  private toolCallCount = 0;
49
61
  private totalPayloadSize = 0;
62
+ private compactionPolicy: CompactionPolicy | undefined;
63
+ private sessionStartedAt: string | undefined;
64
+
65
+ /**
66
+ * Set the compaction policy and session start time.
67
+ * When set, `check()` will evaluate compaction thresholds.
68
+ */
69
+ setCompactionPolicy(policy: CompactionPolicy, startedAt?: string): void {
70
+ this.compactionPolicy = policy;
71
+ this.sessionStartedAt = startedAt ?? new Date().toISOString();
72
+ }
50
73
 
51
74
  /** Track a tool call event. */
52
75
  track(event: TrackEvent): void {
@@ -54,19 +77,36 @@ export class ContextHealthMonitor {
54
77
  this.totalPayloadSize += event.payloadSize;
55
78
  }
56
79
 
57
- /** Check current context health status. */
80
+ /** Check current context health status (including compaction policy). */
58
81
  check(): ContextHealthStatus {
59
82
  const estimatedTokens = Math.round(this.totalPayloadSize * OVERHEAD_FACTOR);
60
83
  const estimatedFill = Math.min(estimatedTokens / CONTEXT_WINDOW, 1.0);
61
84
  const level = this.classifyLevel(estimatedFill);
62
85
 
63
- return {
86
+ const status: ContextHealthStatus = {
64
87
  level,
65
88
  estimatedFill: Math.round(estimatedFill * 1000) / 1000,
66
89
  toolCallCount: this.toolCallCount,
67
90
  estimatedTokens,
68
91
  recommendation: RECOMMENDATIONS[level],
69
92
  };
93
+
94
+ // Evaluate compaction policy if configured
95
+ if (this.compactionPolicy && this.sessionStartedAt) {
96
+ const session: SessionState = {
97
+ runCount: this.toolCallCount,
98
+ inputTokens: estimatedTokens,
99
+ startedAt: this.sessionStartedAt,
100
+ };
101
+ status.compaction = shouldCompact(session, this.compactionPolicy);
102
+
103
+ // Escalate recommendation when compaction is triggered
104
+ if (status.compaction.compact) {
105
+ status.recommendation = `Compaction triggered: ${status.compaction.reason}`;
106
+ }
107
+ }
108
+
109
+ return status;
70
110
  }
71
111
 
72
112
  /** Reset all tracking (on session clear). */
@@ -9,6 +9,7 @@ import type { AgentRuntime } from '../types.js';
9
9
  import { createOrchestrateOps } from '../orchestrate-ops.js';
10
10
  import { createProjectOps } from '../project-ops.js';
11
11
  import { createPlaybookOps } from '../playbook-ops.js';
12
+ import { checkForUpdate } from '../../update-check.js';
12
13
 
13
14
  export function createOrchestrateFacadeOps(runtime: AgentRuntime): OpDefinition[] {
14
15
  const { vault, governance, projectRegistry } = runtime;
@@ -71,6 +72,19 @@ export function createOrchestrateFacadeOps(runtime: AgentRuntime): OpDefinition[
71
72
  // Non-critical — don't fail session start over staging check
72
73
  }
73
74
 
75
+ // Fire-and-forget update check — never blocks, never throws
76
+ try {
77
+ const enginePkgUrl = new URL('../../../package.json', import.meta.url);
78
+ const { readFileSync: readFs } = await import('node:fs');
79
+ const enginePkg = JSON.parse(readFs(enginePkgUrl, 'utf-8'));
80
+ void checkForUpdate(
81
+ runtime.config.agentId ?? 'unknown',
82
+ enginePkg.version ?? '0.0.0',
83
+ ).catch(() => {});
84
+ } catch {
85
+ // package.json not readable — skip update check silently
86
+ }
87
+
74
88
  return {
75
89
  project,
76
90
  is_new: isNew,
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Compaction Evaluator — checks session state against a compaction policy
3
+ * and determines whether the session should be rotated.
4
+ */
5
+
6
+ import type { CompactionPolicy, CompactionResult, SessionState } from './compaction-policy.js';
7
+
8
+ // =============================================================================
9
+ // DURATION PARSER
10
+ // =============================================================================
11
+
12
+ const DURATION_RE = /^(\d+)(ms|s|m|h|d)$/;
13
+
14
+ const MULTIPLIERS: Record<string, number> = {
15
+ ms: 1,
16
+ s: 1_000,
17
+ m: 60_000,
18
+ h: 3_600_000,
19
+ d: 86_400_000,
20
+ };
21
+
22
+ /**
23
+ * Parse a simple duration string (e.g. '72h', '30m', '7d') into milliseconds.
24
+ * Returns `undefined` for invalid input.
25
+ */
26
+ export function parseDuration(duration: string): number | undefined {
27
+ const match = DURATION_RE.exec(duration.trim());
28
+ if (!match) return undefined;
29
+ const [, value, unit] = match;
30
+ return Number(value) * MULTIPLIERS[unit];
31
+ }
32
+
33
+ // =============================================================================
34
+ // EVALUATOR
35
+ // =============================================================================
36
+
37
+ /**
38
+ * Evaluate whether a session should be compacted based on policy thresholds.
39
+ *
40
+ * Returns the first triggered threshold as the reason. Checks in order:
41
+ * 1. maxRuns
42
+ * 2. maxInputTokens
43
+ * 3. maxAge
44
+ */
45
+ export function shouldCompact(
46
+ session: SessionState,
47
+ policy: CompactionPolicy,
48
+ now: Date = new Date(),
49
+ ): CompactionResult {
50
+ const noCompact: CompactionResult = { compact: false, reason: '', handoff: '' };
51
+
52
+ // Check maxRuns
53
+ if (policy.maxRuns !== undefined && session.runCount >= policy.maxRuns) {
54
+ return {
55
+ compact: true,
56
+ reason: `Run count (${session.runCount}) reached threshold (${policy.maxRuns})`,
57
+ handoff: '',
58
+ };
59
+ }
60
+
61
+ // Check maxInputTokens
62
+ if (policy.maxInputTokens !== undefined && session.inputTokens >= policy.maxInputTokens) {
63
+ return {
64
+ compact: true,
65
+ reason: `Input tokens (${session.inputTokens}) reached threshold (${policy.maxInputTokens})`,
66
+ handoff: '',
67
+ };
68
+ }
69
+
70
+ // Check maxAge
71
+ if (policy.maxAge !== undefined) {
72
+ const maxMs = parseDuration(policy.maxAge);
73
+ if (maxMs !== undefined) {
74
+ const startedAt = new Date(session.startedAt).getTime();
75
+ const elapsed = now.getTime() - startedAt;
76
+ if (elapsed >= maxMs) {
77
+ return {
78
+ compact: true,
79
+ reason: `Session age (${Math.round(elapsed / 60_000)}m) reached threshold (${policy.maxAge})`,
80
+ handoff: '',
81
+ };
82
+ }
83
+ }
84
+ }
85
+
86
+ return noCompact;
87
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Session Compaction Policy — types and defaults for session rotation.
3
+ *
4
+ * Three thresholds determine when a session should be compacted:
5
+ * - maxRuns: tool call / interaction count
6
+ * - maxInputTokens: cumulative input token count
7
+ * - maxAge: wall-clock duration (ISO 8601 duration string, e.g. '72h')
8
+ */
9
+
10
+ // =============================================================================
11
+ // TYPES
12
+ // =============================================================================
13
+
14
+ /** Policy thresholds — all optional, merged from three levels. */
15
+ export interface CompactionPolicy {
16
+ /** Maximum number of runs (tool calls / interactions) before compaction. */
17
+ maxRuns?: number;
18
+ /** Maximum cumulative input tokens before compaction. */
19
+ maxInputTokens?: number;
20
+ /** Maximum wall-clock age as an ISO 8601-ish duration string (e.g. '72h', '30m', '7d'). */
21
+ maxAge?: string;
22
+ }
23
+
24
+ /** Result of evaluating whether compaction is needed. */
25
+ export interface CompactionResult {
26
+ /** Whether compaction should happen. */
27
+ compact: boolean;
28
+ /** Human-readable reason (empty string when compact is false). */
29
+ reason: string;
30
+ /** Pre-rendered handoff markdown (empty string when compact is false). */
31
+ handoff: string;
32
+ }
33
+
34
+ /** State snapshot of the current session, used for evaluation. */
35
+ export interface SessionState {
36
+ /** Number of runs (tool calls) so far. */
37
+ runCount: number;
38
+ /** Cumulative input tokens consumed. */
39
+ inputTokens: number;
40
+ /** ISO 8601 timestamp when the session started. */
41
+ startedAt: string;
42
+ }
43
+
44
+ /** Structured handoff note persisted on rotation. */
45
+ export interface HandoffNote {
46
+ /** ISO 8601 timestamp when the session was rotated. */
47
+ rotatedAt: string;
48
+ /** Why the session was rotated. */
49
+ reason: string;
50
+ /** Description of work in progress at rotation time. */
51
+ inProgress: string;
52
+ /** Key decisions made during the session. */
53
+ keyDecisions: string[];
54
+ /** Files modified during the session. */
55
+ filesModified: string[];
56
+ }
57
+
58
+ // =============================================================================
59
+ // ENGINE DEFAULTS
60
+ // =============================================================================
61
+
62
+ export const ENGINE_DEFAULTS: Required<CompactionPolicy> = {
63
+ maxRuns: 200,
64
+ maxInputTokens: 2_000_000,
65
+ maxAge: '72h',
66
+ };