@soleri/core 9.10.0 → 9.12.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 (248) hide show
  1. package/dist/adapters/types.d.ts +2 -0
  2. package/dist/adapters/types.d.ts.map +1 -1
  3. package/dist/brain/brain.d.ts +5 -1
  4. package/dist/brain/brain.d.ts.map +1 -1
  5. package/dist/brain/brain.js +97 -10
  6. package/dist/brain/brain.js.map +1 -1
  7. package/dist/brain/intelligence.d.ts.map +1 -1
  8. package/dist/brain/intelligence.js +4 -0
  9. package/dist/brain/intelligence.js.map +1 -1
  10. package/dist/brain/types.d.ts +1 -1
  11. package/dist/brain/types.d.ts.map +1 -1
  12. package/dist/dream/cron-manager.d.ts +10 -0
  13. package/dist/dream/cron-manager.d.ts.map +1 -0
  14. package/dist/dream/cron-manager.js +122 -0
  15. package/dist/dream/cron-manager.js.map +1 -0
  16. package/dist/dream/dream-engine.d.ts +34 -0
  17. package/dist/dream/dream-engine.d.ts.map +1 -0
  18. package/dist/dream/dream-engine.js +88 -0
  19. package/dist/dream/dream-engine.js.map +1 -0
  20. package/dist/dream/dream-ops.d.ts +8 -0
  21. package/dist/dream/dream-ops.d.ts.map +1 -0
  22. package/dist/dream/dream-ops.js +49 -0
  23. package/dist/dream/dream-ops.js.map +1 -0
  24. package/dist/dream/index.d.ts +7 -0
  25. package/dist/dream/index.d.ts.map +1 -0
  26. package/dist/dream/index.js +5 -0
  27. package/dist/dream/index.js.map +1 -0
  28. package/dist/dream/schema.d.ts +3 -0
  29. package/dist/dream/schema.d.ts.map +1 -0
  30. package/dist/dream/schema.js +16 -0
  31. package/dist/dream/schema.js.map +1 -0
  32. package/dist/embeddings/index.d.ts +5 -0
  33. package/dist/embeddings/index.d.ts.map +1 -0
  34. package/dist/embeddings/index.js +3 -0
  35. package/dist/embeddings/index.js.map +1 -0
  36. package/dist/embeddings/openai-provider.d.ts +31 -0
  37. package/dist/embeddings/openai-provider.d.ts.map +1 -0
  38. package/dist/embeddings/openai-provider.js +120 -0
  39. package/dist/embeddings/openai-provider.js.map +1 -0
  40. package/dist/embeddings/pipeline.d.ts +36 -0
  41. package/dist/embeddings/pipeline.d.ts.map +1 -0
  42. package/dist/embeddings/pipeline.js +78 -0
  43. package/dist/embeddings/pipeline.js.map +1 -0
  44. package/dist/embeddings/types.d.ts +62 -0
  45. package/dist/embeddings/types.d.ts.map +1 -0
  46. package/dist/embeddings/types.js +3 -0
  47. package/dist/embeddings/types.js.map +1 -0
  48. package/dist/engine/bin/soleri-engine.js +4 -1
  49. package/dist/engine/bin/soleri-engine.js.map +1 -1
  50. package/dist/engine/module-manifest.d.ts.map +1 -1
  51. package/dist/engine/module-manifest.js +20 -0
  52. package/dist/engine/module-manifest.js.map +1 -1
  53. package/dist/engine/register-engine.d.ts.map +1 -1
  54. package/dist/engine/register-engine.js +12 -0
  55. package/dist/engine/register-engine.js.map +1 -1
  56. package/dist/flows/chain-types.d.ts +8 -8
  57. package/dist/flows/dispatch-registry.d.ts +15 -1
  58. package/dist/flows/dispatch-registry.d.ts.map +1 -1
  59. package/dist/flows/dispatch-registry.js +28 -1
  60. package/dist/flows/dispatch-registry.js.map +1 -1
  61. package/dist/flows/executor.d.ts +20 -2
  62. package/dist/flows/executor.d.ts.map +1 -1
  63. package/dist/flows/executor.js +79 -1
  64. package/dist/flows/executor.js.map +1 -1
  65. package/dist/flows/index.d.ts +2 -1
  66. package/dist/flows/index.d.ts.map +1 -1
  67. package/dist/flows/index.js.map +1 -1
  68. package/dist/flows/types.d.ts +43 -21
  69. package/dist/flows/types.d.ts.map +1 -1
  70. package/dist/index.d.ts +5 -0
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +3 -0
  73. package/dist/index.js.map +1 -1
  74. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  75. package/dist/planning/plan-lifecycle.js +4 -2
  76. package/dist/planning/plan-lifecycle.js.map +1 -1
  77. package/dist/planning/planner-types.d.ts +1 -1
  78. package/dist/planning/planner-types.d.ts.map +1 -1
  79. package/dist/plugins/types.d.ts +31 -31
  80. package/dist/runtime/admin-ops.d.ts.map +1 -1
  81. package/dist/runtime/admin-ops.js +15 -0
  82. package/dist/runtime/admin-ops.js.map +1 -1
  83. package/dist/runtime/admin-setup-ops.js +2 -2
  84. package/dist/runtime/admin-setup-ops.js.map +1 -1
  85. package/dist/runtime/embedding-ops.d.ts +12 -0
  86. package/dist/runtime/embedding-ops.d.ts.map +1 -0
  87. package/dist/runtime/embedding-ops.js +96 -0
  88. package/dist/runtime/embedding-ops.js.map +1 -0
  89. package/dist/runtime/facades/embedding-facade.d.ts +7 -0
  90. package/dist/runtime/facades/embedding-facade.d.ts.map +1 -0
  91. package/dist/runtime/facades/embedding-facade.js +8 -0
  92. package/dist/runtime/facades/embedding-facade.js.map +1 -0
  93. package/dist/runtime/facades/index.d.ts.map +1 -1
  94. package/dist/runtime/facades/index.js +12 -0
  95. package/dist/runtime/facades/index.js.map +1 -1
  96. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  97. package/dist/runtime/facades/orchestrate-facade.js +120 -0
  98. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  99. package/dist/runtime/feature-flags.d.ts.map +1 -1
  100. package/dist/runtime/feature-flags.js +4 -0
  101. package/dist/runtime/feature-flags.js.map +1 -1
  102. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  103. package/dist/runtime/orchestrate-ops.js +146 -12
  104. package/dist/runtime/orchestrate-ops.js.map +1 -1
  105. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  106. package/dist/runtime/planning-extra-ops.js +51 -0
  107. package/dist/runtime/planning-extra-ops.js.map +1 -1
  108. package/dist/runtime/preflight.d.ts +32 -0
  109. package/dist/runtime/preflight.d.ts.map +1 -0
  110. package/dist/runtime/preflight.js +29 -0
  111. package/dist/runtime/preflight.js.map +1 -0
  112. package/dist/runtime/quality-signals.d.ts +6 -1
  113. package/dist/runtime/quality-signals.d.ts.map +1 -1
  114. package/dist/runtime/quality-signals.js +41 -5
  115. package/dist/runtime/quality-signals.js.map +1 -1
  116. package/dist/runtime/runtime.d.ts.map +1 -1
  117. package/dist/runtime/runtime.js +33 -2
  118. package/dist/runtime/runtime.js.map +1 -1
  119. package/dist/runtime/types.d.ts +27 -0
  120. package/dist/runtime/types.d.ts.map +1 -1
  121. package/dist/skills/step-tracker.d.ts +39 -0
  122. package/dist/skills/step-tracker.d.ts.map +1 -0
  123. package/dist/skills/step-tracker.js +105 -0
  124. package/dist/skills/step-tracker.js.map +1 -0
  125. package/dist/skills/sync-skills.d.ts +3 -2
  126. package/dist/skills/sync-skills.d.ts.map +1 -1
  127. package/dist/skills/sync-skills.js +42 -8
  128. package/dist/skills/sync-skills.js.map +1 -1
  129. package/dist/subagent/dispatcher.d.ts +4 -3
  130. package/dist/subagent/dispatcher.d.ts.map +1 -1
  131. package/dist/subagent/dispatcher.js +57 -35
  132. package/dist/subagent/dispatcher.js.map +1 -1
  133. package/dist/subagent/index.d.ts +1 -0
  134. package/dist/subagent/index.d.ts.map +1 -1
  135. package/dist/subagent/index.js.map +1 -1
  136. package/dist/subagent/orphan-reaper.d.ts +51 -4
  137. package/dist/subagent/orphan-reaper.d.ts.map +1 -1
  138. package/dist/subagent/orphan-reaper.js +103 -3
  139. package/dist/subagent/orphan-reaper.js.map +1 -1
  140. package/dist/subagent/types.d.ts +7 -0
  141. package/dist/subagent/types.d.ts.map +1 -1
  142. package/dist/subagent/workspace-resolver.d.ts +2 -0
  143. package/dist/subagent/workspace-resolver.d.ts.map +1 -1
  144. package/dist/subagent/workspace-resolver.js +3 -1
  145. package/dist/subagent/workspace-resolver.js.map +1 -1
  146. package/dist/vault/vault-entries.d.ts +18 -0
  147. package/dist/vault/vault-entries.d.ts.map +1 -1
  148. package/dist/vault/vault-entries.js +73 -0
  149. package/dist/vault/vault-entries.js.map +1 -1
  150. package/dist/vault/vault-manager.d.ts.map +1 -1
  151. package/dist/vault/vault-manager.js +1 -0
  152. package/dist/vault/vault-manager.js.map +1 -1
  153. package/dist/vault/vault-schema.d.ts.map +1 -1
  154. package/dist/vault/vault-schema.js +14 -0
  155. package/dist/vault/vault-schema.js.map +1 -1
  156. package/dist/vault/vault.d.ts +1 -0
  157. package/dist/vault/vault.d.ts.map +1 -1
  158. package/dist/vault/vault.js.map +1 -1
  159. package/package.json +3 -5
  160. package/src/__tests__/cron-manager.test.ts +132 -0
  161. package/src/__tests__/deviation-detection.test.ts +234 -0
  162. package/src/__tests__/embeddings.test.ts +536 -0
  163. package/src/__tests__/preflight.test.ts +97 -0
  164. package/src/__tests__/step-persistence.test.ts +324 -0
  165. package/src/__tests__/step-tracker.test.ts +260 -0
  166. package/src/__tests__/subagent/dispatcher.test.ts +122 -4
  167. package/src/__tests__/subagent/orphan-reaper.test.ts +148 -12
  168. package/src/__tests__/subagent/process-lifecycle.test.ts +422 -0
  169. package/src/__tests__/subagent/workspace-resolver.test.ts +6 -1
  170. package/src/adapters/types.ts +2 -0
  171. package/src/brain/brain.ts +117 -9
  172. package/src/brain/intelligence.ts +4 -0
  173. package/src/brain/types.ts +6 -1
  174. package/src/dream/cron-manager.ts +137 -0
  175. package/src/dream/dream-engine.ts +119 -0
  176. package/src/dream/dream-ops.ts +56 -0
  177. package/src/dream/dream.test.ts +182 -0
  178. package/src/dream/index.ts +6 -0
  179. package/src/dream/schema.ts +17 -0
  180. package/src/embeddings/openai-provider.ts +158 -0
  181. package/src/embeddings/pipeline.ts +126 -0
  182. package/src/embeddings/types.ts +67 -0
  183. package/src/engine/bin/soleri-engine.ts +4 -1
  184. package/src/engine/module-manifest.test.ts +4 -4
  185. package/src/engine/module-manifest.ts +20 -0
  186. package/src/engine/register-engine.ts +12 -0
  187. package/src/flows/dispatch-registry.ts +44 -1
  188. package/src/flows/executor.ts +93 -2
  189. package/src/flows/index.ts +2 -0
  190. package/src/flows/types.ts +39 -1
  191. package/src/index.ts +11 -0
  192. package/src/planning/goal-ancestry.test.ts +3 -5
  193. package/src/planning/plan-lifecycle.ts +5 -2
  194. package/src/planning/planner-types.ts +1 -1
  195. package/src/planning/planner.test.ts +73 -3
  196. package/src/runtime/admin-ops.test.ts +2 -2
  197. package/src/runtime/admin-ops.ts +17 -0
  198. package/src/runtime/admin-setup-ops.ts +2 -2
  199. package/src/runtime/embedding-ops.ts +116 -0
  200. package/src/runtime/facades/admin-facade.test.ts +31 -0
  201. package/src/runtime/facades/embedding-facade.ts +11 -0
  202. package/src/runtime/facades/index.ts +12 -0
  203. package/src/runtime/facades/orchestrate-facade.test.ts +16 -0
  204. package/src/runtime/facades/orchestrate-facade.ts +146 -0
  205. package/src/runtime/feature-flags.ts +4 -0
  206. package/src/runtime/orchestrate-ops.test.ts +182 -2
  207. package/src/runtime/orchestrate-ops.ts +170 -13
  208. package/src/runtime/planning-extra-ops.ts +77 -0
  209. package/src/runtime/preflight.ts +53 -0
  210. package/src/runtime/quality-signals.test.ts +182 -8
  211. package/src/runtime/quality-signals.ts +44 -5
  212. package/src/runtime/runtime.ts +41 -2
  213. package/src/runtime/types.ts +20 -0
  214. package/src/skills/__tests__/sync-skills.test.ts +132 -0
  215. package/src/skills/step-tracker.ts +162 -0
  216. package/src/skills/sync-skills.ts +54 -9
  217. package/src/subagent/dispatcher.ts +62 -39
  218. package/src/subagent/index.ts +1 -0
  219. package/src/subagent/orphan-reaper.test.ts +135 -0
  220. package/src/subagent/orphan-reaper.ts +130 -7
  221. package/src/subagent/types.ts +10 -0
  222. package/src/subagent/workspace-resolver.ts +3 -1
  223. package/src/vault/vault-entries.ts +112 -0
  224. package/src/vault/vault-manager.ts +1 -0
  225. package/src/vault/vault-scaling.test.ts +3 -2
  226. package/src/vault/vault-schema.ts +15 -0
  227. package/src/vault/vault.ts +1 -0
  228. package/vitest.config.ts +2 -1
  229. package/dist/brain/strength-scorer.d.ts +0 -31
  230. package/dist/brain/strength-scorer.d.ts.map +0 -1
  231. package/dist/brain/strength-scorer.js +0 -264
  232. package/dist/brain/strength-scorer.js.map +0 -1
  233. package/dist/engine/index.d.ts +0 -21
  234. package/dist/engine/index.d.ts.map +0 -1
  235. package/dist/engine/index.js +0 -18
  236. package/dist/engine/index.js.map +0 -1
  237. package/dist/hooks/index.d.ts +0 -2
  238. package/dist/hooks/index.d.ts.map +0 -1
  239. package/dist/hooks/index.js +0 -2
  240. package/dist/hooks/index.js.map +0 -1
  241. package/dist/persona/index.d.ts +0 -5
  242. package/dist/persona/index.d.ts.map +0 -1
  243. package/dist/persona/index.js +0 -4
  244. package/dist/persona/index.js.map +0 -1
  245. package/dist/vault/vault-interfaces.d.ts +0 -153
  246. package/dist/vault/vault-interfaces.d.ts.map +0 -1
  247. package/dist/vault/vault-interfaces.js +0 -2
  248. package/dist/vault/vault-interfaces.js.map +0 -1
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Step Persistence — Unit Tests
3
+ *
4
+ * Tests for the incremental correction protocol:
5
+ * - Step output persistence to disk
6
+ * - Manifest management (load/save/create)
7
+ * - Rerun marking and staleness propagation
8
+ * - cascadeTo behavior
9
+ * - rerunCount incrementing
10
+ */
11
+
12
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import os from 'node:os';
16
+ import { getPlanRunDir, loadManifest, saveManifest, persistStepOutput } from '../flows/executor.js';
17
+ import type { PlanRunManifest, StepState } from '../flows/types.js';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Helpers
21
+ // ---------------------------------------------------------------------------
22
+
23
+ let tmpDir: string;
24
+
25
+ function createTmpDir(): string {
26
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'soleri-step-persist-'));
27
+ }
28
+
29
+ function cleanTmpDir(dir: string): void {
30
+ fs.rmSync(dir, { recursive: true, force: true });
31
+ }
32
+
33
+ /** Build a manifest with N completed steps for testing. */
34
+ function buildManifest(planId: string, stepCount: number): PlanRunManifest {
35
+ const now = new Date().toISOString();
36
+ const steps: Record<string, StepState> = {};
37
+ for (let i = 0; i < stepCount; i++) {
38
+ steps[`step-${i}`] = {
39
+ status: 'completed',
40
+ output: { result: `output-${i}` },
41
+ timestamp: now,
42
+ rerunCount: 0,
43
+ };
44
+ }
45
+ return { planId, steps, lastRun: now, createdAt: now };
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Tests
50
+ // ---------------------------------------------------------------------------
51
+
52
+ describe('Step Persistence', () => {
53
+ beforeEach(() => {
54
+ tmpDir = createTmpDir();
55
+ });
56
+
57
+ afterEach(() => {
58
+ cleanTmpDir(tmpDir);
59
+ });
60
+
61
+ // ─── getPlanRunDir ─────────────────────────────────────────────
62
+
63
+ describe('getPlanRunDir', () => {
64
+ it('returns the correct path structure', () => {
65
+ const dir = getPlanRunDir('/project', 'plan-123');
66
+ expect(dir).toBe(path.join('/project', '.soleri', 'plan-runs', 'plan-123'));
67
+ });
68
+
69
+ it('handles plan IDs with special characters', () => {
70
+ const dir = getPlanRunDir('/project', 'plan_abc-def');
71
+ expect(dir).toContain('plan_abc-def');
72
+ });
73
+ });
74
+
75
+ // ─── loadManifest ──────────────────────────────────────────────
76
+
77
+ describe('loadManifest', () => {
78
+ it('creates a fresh manifest when none exists', () => {
79
+ const runDir = path.join(tmpDir, 'nonexistent');
80
+ const manifest = loadManifest(runDir, 'plan-new');
81
+
82
+ expect(manifest.planId).toBe('plan-new');
83
+ expect(manifest.steps).toEqual({});
84
+ expect(manifest.createdAt).toBeTruthy();
85
+ expect(manifest.lastRun).toBeTruthy();
86
+ });
87
+
88
+ it('loads existing manifest from disk', () => {
89
+ const runDir = path.join(tmpDir, 'existing');
90
+ const original = buildManifest('plan-load', 3);
91
+ fs.mkdirSync(runDir, { recursive: true });
92
+ fs.writeFileSync(path.join(runDir, 'manifest.json'), JSON.stringify(original));
93
+
94
+ const loaded = loadManifest(runDir, 'plan-load');
95
+ expect(loaded.planId).toBe('plan-load');
96
+ expect(Object.keys(loaded.steps)).toHaveLength(3);
97
+ expect(loaded.steps['step-0'].status).toBe('completed');
98
+ });
99
+ });
100
+
101
+ // ─── saveManifest ──────────────────────────────────────────────
102
+
103
+ describe('saveManifest', () => {
104
+ it('creates directories and writes manifest', () => {
105
+ const runDir = path.join(tmpDir, 'deep', 'nested', 'dir');
106
+ const manifest = buildManifest('plan-save', 2);
107
+
108
+ saveManifest(runDir, manifest);
109
+
110
+ const manifestPath = path.join(runDir, 'manifest.json');
111
+ expect(fs.existsSync(manifestPath)).toBe(true);
112
+
113
+ const written = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
114
+ expect(written.planId).toBe('plan-save');
115
+ expect(Object.keys(written.steps)).toHaveLength(2);
116
+ });
117
+
118
+ it('overwrites existing manifest', () => {
119
+ const runDir = path.join(tmpDir, 'overwrite');
120
+ const manifest1 = buildManifest('plan-ow', 1);
121
+ saveManifest(runDir, manifest1);
122
+
123
+ const manifest2 = buildManifest('plan-ow', 3);
124
+ saveManifest(runDir, manifest2);
125
+
126
+ const loaded = loadManifest(runDir, 'plan-ow');
127
+ expect(Object.keys(loaded.steps)).toHaveLength(3);
128
+ });
129
+ });
130
+
131
+ // ─── persistStepOutput ─────────────────────────────────────────
132
+
133
+ describe('persistStepOutput', () => {
134
+ it('persists step output file and updates manifest', () => {
135
+ const runDir = path.join(tmpDir, 'persist-step');
136
+ const manifest = buildManifest('plan-ps', 0);
137
+
138
+ persistStepOutput(runDir, manifest, 0, 'init', { data: 'hello' });
139
+
140
+ // Step file should exist
141
+ const stepFile = path.join(runDir, 'step-0-init.json');
142
+ expect(fs.existsSync(stepFile)).toBe(true);
143
+ const stepData = JSON.parse(fs.readFileSync(stepFile, 'utf-8'));
144
+ expect(stepData.data).toBe('hello');
145
+
146
+ // Manifest should be updated
147
+ expect(manifest.steps['init']).toBeDefined();
148
+ expect(manifest.steps['init'].status).toBe('completed');
149
+ expect(manifest.steps['init'].rerunCount).toBe(0);
150
+
151
+ // Manifest file should exist on disk
152
+ const manifestFile = path.join(runDir, 'manifest.json');
153
+ expect(fs.existsSync(manifestFile)).toBe(true);
154
+ });
155
+
156
+ it('increments rerunCount when step already exists', () => {
157
+ const runDir = path.join(tmpDir, 'persist-rerun');
158
+ const manifest = buildManifest('plan-pr', 0);
159
+
160
+ // First run
161
+ persistStepOutput(runDir, manifest, 0, 'step-a', { v: 1 });
162
+ expect(manifest.steps['step-a'].rerunCount).toBe(0);
163
+
164
+ // Second run — rerunCount should increment
165
+ persistStepOutput(runDir, manifest, 0, 'step-a', { v: 2 });
166
+ expect(manifest.steps['step-a'].rerunCount).toBe(1);
167
+
168
+ // Third run
169
+ persistStepOutput(runDir, manifest, 0, 'step-a', { v: 3 });
170
+ expect(manifest.steps['step-a'].rerunCount).toBe(2);
171
+ });
172
+
173
+ it('preserves rerunReason from previous state', () => {
174
+ const runDir = path.join(tmpDir, 'persist-reason');
175
+ const manifest = buildManifest('plan-reason', 0);
176
+
177
+ // Manually set a step with a rerun reason
178
+ manifest.steps['step-x'] = {
179
+ status: 'rerun',
180
+ output: null,
181
+ timestamp: new Date().toISOString(),
182
+ rerunCount: 1,
183
+ rerunReason: 'gate failed',
184
+ };
185
+
186
+ persistStepOutput(runDir, manifest, 0, 'step-x', { fixed: true });
187
+ expect(manifest.steps['step-x'].rerunReason).toBe('gate failed');
188
+ expect(manifest.steps['step-x'].rerunCount).toBe(2);
189
+ expect(manifest.steps['step-x'].status).toBe('completed');
190
+ });
191
+ });
192
+
193
+ // ─── Rerun marking and staleness propagation ───────────────────
194
+
195
+ describe('rerun marking and staleness propagation', () => {
196
+ it('marks target step as rerun and downstream as stale', () => {
197
+ const manifest = buildManifest('plan-stale', 5);
198
+
199
+ // Simulate what orchestrate_rerun_step does
200
+ const stepNumber = 1;
201
+ const reason = 'output was wrong';
202
+ const now = new Date().toISOString();
203
+ const sortedStepIds = Object.keys(manifest.steps);
204
+
205
+ for (let i = 0; i < sortedStepIds.length; i++) {
206
+ const sid = sortedStepIds[i];
207
+ const state = manifest.steps[sid];
208
+
209
+ if (i === stepNumber) {
210
+ state.status = 'rerun';
211
+ state.rerunCount += 1;
212
+ state.rerunReason = reason;
213
+ state.timestamp = now;
214
+ } else if (i > stepNumber) {
215
+ state.status = 'stale';
216
+ state.timestamp = now;
217
+ }
218
+ }
219
+
220
+ expect(manifest.steps['step-0'].status).toBe('completed');
221
+ expect(manifest.steps['step-1'].status).toBe('rerun');
222
+ expect(manifest.steps['step-1'].rerunCount).toBe(1);
223
+ expect(manifest.steps['step-1'].rerunReason).toBe('output was wrong');
224
+ expect(manifest.steps['step-2'].status).toBe('stale');
225
+ expect(manifest.steps['step-3'].status).toBe('stale');
226
+ expect(manifest.steps['step-4'].status).toBe('stale');
227
+ });
228
+
229
+ it('cascadeTo marks a range as rerun instead of stale', () => {
230
+ const manifest = buildManifest('plan-cascade', 5);
231
+
232
+ const stepNumber = 1;
233
+ const cascadeTo = 4; // steps 1,2,3 → rerun; step 4 → stale
234
+ const reason = 'dependency changed';
235
+ const now = new Date().toISOString();
236
+ const sortedStepIds = Object.keys(manifest.steps);
237
+
238
+ for (let i = 0; i < sortedStepIds.length; i++) {
239
+ const sid = sortedStepIds[i];
240
+ const state = manifest.steps[sid];
241
+
242
+ if (i === stepNumber) {
243
+ state.status = 'rerun';
244
+ state.rerunCount += 1;
245
+ state.rerunReason = reason;
246
+ state.timestamp = now;
247
+ } else if (i > stepNumber) {
248
+ if (i < cascadeTo) {
249
+ state.status = 'rerun';
250
+ state.rerunCount += 1;
251
+ state.rerunReason = `Cascade from step ${stepNumber}: ${reason}`;
252
+ state.timestamp = now;
253
+ } else {
254
+ state.status = 'stale';
255
+ state.timestamp = now;
256
+ }
257
+ }
258
+ }
259
+
260
+ expect(manifest.steps['step-0'].status).toBe('completed');
261
+ expect(manifest.steps['step-1'].status).toBe('rerun');
262
+ expect(manifest.steps['step-1'].rerunCount).toBe(1);
263
+ expect(manifest.steps['step-2'].status).toBe('rerun');
264
+ expect(manifest.steps['step-2'].rerunCount).toBe(1);
265
+ expect(manifest.steps['step-3'].status).toBe('rerun');
266
+ expect(manifest.steps['step-3'].rerunCount).toBe(1);
267
+ expect(manifest.steps['step-4'].status).toBe('stale');
268
+ expect(manifest.steps['step-4'].rerunCount).toBe(0);
269
+ });
270
+
271
+ it('rerunCount accumulates across multiple reruns', () => {
272
+ const manifest = buildManifest('plan-multi', 3);
273
+
274
+ // First rerun of step 1
275
+ manifest.steps['step-1'].status = 'rerun';
276
+ manifest.steps['step-1'].rerunCount += 1;
277
+
278
+ // Second rerun of step 1
279
+ manifest.steps['step-1'].rerunCount += 1;
280
+
281
+ // Third rerun
282
+ manifest.steps['step-1'].rerunCount += 1;
283
+
284
+ expect(manifest.steps['step-1'].rerunCount).toBe(3);
285
+ });
286
+ });
287
+
288
+ // ─── Round-trip persistence ────────────────────────────────────
289
+
290
+ describe('round-trip persistence', () => {
291
+ it('manifest survives save and load cycle', () => {
292
+ const runDir = path.join(tmpDir, 'roundtrip');
293
+ const manifest = buildManifest('plan-rt', 3);
294
+
295
+ // Mark step 1 as rerun
296
+ manifest.steps['step-1'].status = 'rerun';
297
+ manifest.steps['step-1'].rerunCount = 2;
298
+ manifest.steps['step-1'].rerunReason = 'test reason';
299
+
300
+ saveManifest(runDir, manifest);
301
+ const loaded = loadManifest(runDir, 'plan-rt');
302
+
303
+ expect(loaded.steps['step-1'].status).toBe('rerun');
304
+ expect(loaded.steps['step-1'].rerunCount).toBe(2);
305
+ expect(loaded.steps['step-1'].rerunReason).toBe('test reason');
306
+ expect(loaded.steps['step-0'].status).toBe('completed');
307
+ });
308
+
309
+ it('step output files persist alongside manifest', () => {
310
+ const runDir = path.join(tmpDir, 'files');
311
+ const manifest = buildManifest('plan-files', 0);
312
+
313
+ persistStepOutput(runDir, manifest, 0, 'alpha', { x: 1 });
314
+ persistStepOutput(runDir, manifest, 1, 'beta', { x: 2 });
315
+ persistStepOutput(runDir, manifest, 2, 'gamma', { x: 3 });
316
+
317
+ const files = fs.readdirSync(runDir).sort();
318
+ expect(files).toContain('manifest.json');
319
+ expect(files).toContain('step-0-alpha.json');
320
+ expect(files).toContain('step-1-beta.json');
321
+ expect(files).toContain('step-2-gamma.json');
322
+ });
323
+ });
324
+ });
@@ -0,0 +1,260 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import {
6
+ createTracker,
7
+ advanceStep,
8
+ recordEvidence,
9
+ generateCheckpoint,
10
+ validateCompletion,
11
+ persistTracker,
12
+ loadTracker,
13
+ } from '../skills/step-tracker.js';
14
+ import type { SkillStep, SkillStepTracker } from '../skills/step-tracker.js';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Fixtures
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const SAMPLE_STEPS: SkillStep[] = [
21
+ { id: 'vault-query', description: 'Query vault for relevant patterns', evidence: 'tool_called' },
22
+ { id: 'analysis', description: 'Run analysis', evidence: 'file_exists' },
23
+ { id: 'apply-fix', description: 'Apply the fix', evidence: 'tool_called' },
24
+ ];
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // createTracker
28
+ // ---------------------------------------------------------------------------
29
+
30
+ describe('createTracker', () => {
31
+ it('creates tracker with correct initial state', () => {
32
+ const tracker = createTracker('test-skill', SAMPLE_STEPS);
33
+
34
+ expect(tracker.skillName).toBe('test-skill');
35
+ expect(tracker.runId).toMatch(/^test-skill-\d+$/);
36
+ expect(tracker.steps).toEqual(SAMPLE_STEPS);
37
+ expect(tracker.currentStep).toBe(0);
38
+ expect(tracker.startedAt).toBeTruthy();
39
+ expect(tracker.evidence).toEqual({});
40
+ expect(tracker.completedAt).toBeUndefined();
41
+ });
42
+ });
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // advanceStep
46
+ // ---------------------------------------------------------------------------
47
+
48
+ describe('advanceStep', () => {
49
+ it('increments currentStep', () => {
50
+ const tracker = createTracker('test', SAMPLE_STEPS);
51
+ const advanced = advanceStep(tracker);
52
+
53
+ expect(advanced.currentStep).toBe(1);
54
+ expect(advanced.completedAt).toBeUndefined();
55
+ });
56
+
57
+ it('on last step sets completedAt', () => {
58
+ let tracker = createTracker('test', SAMPLE_STEPS);
59
+ // Advance to last step (index 2)
60
+ tracker = { ...tracker, currentStep: SAMPLE_STEPS.length - 1 };
61
+ const completed = advanceStep(tracker);
62
+
63
+ expect(completed.completedAt).toBeTruthy();
64
+ // currentStep stays at last index
65
+ expect(completed.currentStep).toBe(SAMPLE_STEPS.length - 1);
66
+ });
67
+
68
+ it('does not mutate original tracker', () => {
69
+ const tracker = createTracker('test', SAMPLE_STEPS);
70
+ advanceStep(tracker);
71
+
72
+ expect(tracker.currentStep).toBe(0);
73
+ });
74
+ });
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // recordEvidence
78
+ // ---------------------------------------------------------------------------
79
+
80
+ describe('recordEvidence', () => {
81
+ it('stores evidence for a step', () => {
82
+ const tracker = createTracker('test', SAMPLE_STEPS);
83
+ const updated = recordEvidence(tracker, 'vault-query', 'vault.search called');
84
+
85
+ expect(updated.evidence['vault-query']).toBeDefined();
86
+ expect(updated.evidence['vault-query'].type).toBe('tool_called');
87
+ expect(updated.evidence['vault-query'].value).toBe('vault.search called');
88
+ expect(updated.evidence['vault-query'].verified).toBe(true);
89
+ expect(updated.evidence['vault-query'].timestamp).toBeTruthy();
90
+ });
91
+
92
+ it('ignores unknown step IDs', () => {
93
+ const tracker = createTracker('test', SAMPLE_STEPS);
94
+ const updated = recordEvidence(tracker, 'nonexistent', 'some value');
95
+
96
+ expect(updated).toBe(tracker); // same reference — no change
97
+ expect(Object.keys(updated.evidence)).toHaveLength(0);
98
+ });
99
+
100
+ it('respects verified parameter', () => {
101
+ const tracker = createTracker('test', SAMPLE_STEPS);
102
+ const updated = recordEvidence(tracker, 'vault-query', 'value', false);
103
+
104
+ expect(updated.evidence['vault-query'].verified).toBe(false);
105
+ });
106
+ });
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // generateCheckpoint
110
+ // ---------------------------------------------------------------------------
111
+
112
+ describe('generateCheckpoint', () => {
113
+ it('formats correctly with 0 completions', () => {
114
+ const tracker = createTracker('my-skill', SAMPLE_STEPS);
115
+ const cp = generateCheckpoint(tracker);
116
+
117
+ expect(cp).toContain('--- Skill Checkpoint: my-skill ---');
118
+ expect(cp).toContain('Completed: none');
119
+ expect(cp).toContain('Current: vault-query (step 1 of 3)');
120
+ expect(cp).toContain('Evidence required: tool_called');
121
+ expect(cp).toContain('Progress: 0/3');
122
+ expect(cp).toContain('---');
123
+ });
124
+
125
+ it('formats correctly with partial completion', () => {
126
+ let tracker = createTracker('my-skill', SAMPLE_STEPS);
127
+ tracker = recordEvidence(tracker, 'vault-query', 'called');
128
+ tracker = advanceStep(tracker);
129
+ const cp = generateCheckpoint(tracker);
130
+
131
+ expect(cp).toContain('vault-query ✓');
132
+ expect(cp).toContain('Current: analysis (step 2 of 3)');
133
+ expect(cp).toContain('Progress: 1/3');
134
+ });
135
+
136
+ it('formats correctly with full completion', () => {
137
+ let tracker = createTracker('my-skill', SAMPLE_STEPS);
138
+ tracker = recordEvidence(tracker, 'vault-query', 'called');
139
+ tracker = recordEvidence(tracker, 'analysis', '/tmp/result.json');
140
+ tracker = recordEvidence(tracker, 'apply-fix', 'fix.apply called');
141
+ // Advance past last step
142
+ tracker = { ...tracker, currentStep: SAMPLE_STEPS.length - 1 };
143
+ tracker = advanceStep(tracker);
144
+ const cp = generateCheckpoint(tracker);
145
+
146
+ expect(cp).toContain('vault-query ✓, analysis ✓, apply-fix ✓');
147
+ expect(cp).toContain('Progress: 3/3');
148
+ });
149
+ });
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // validateCompletion
153
+ // ---------------------------------------------------------------------------
154
+
155
+ describe('validateCompletion', () => {
156
+ it('returns correct skipped steps when none completed', () => {
157
+ const tracker = createTracker('test', SAMPLE_STEPS);
158
+ const result = validateCompletion(tracker);
159
+
160
+ expect(result.complete).toBe(false);
161
+ expect(result.skippedSteps).toEqual(['vault-query', 'analysis', 'apply-fix']);
162
+ expect(result.evidenceCount).toBe(0);
163
+ expect(result.totalSteps).toBe(3);
164
+ });
165
+
166
+ it('returns complete=true when all steps have verified evidence', () => {
167
+ let tracker = createTracker('test', SAMPLE_STEPS);
168
+ tracker = recordEvidence(tracker, 'vault-query', 'v');
169
+ tracker = recordEvidence(tracker, 'analysis', 'a');
170
+ tracker = recordEvidence(tracker, 'apply-fix', 'f');
171
+ const result = validateCompletion(tracker);
172
+
173
+ expect(result.complete).toBe(true);
174
+ expect(result.skippedSteps).toEqual([]);
175
+ expect(result.evidenceCount).toBe(3);
176
+ expect(result.totalSteps).toBe(3);
177
+ });
178
+
179
+ it('does not count unverified evidence as complete', () => {
180
+ let tracker = createTracker('test', SAMPLE_STEPS);
181
+ tracker = recordEvidence(tracker, 'vault-query', 'v', false);
182
+ tracker = recordEvidence(tracker, 'analysis', 'a', true);
183
+ tracker = recordEvidence(tracker, 'apply-fix', 'f', true);
184
+ const result = validateCompletion(tracker);
185
+
186
+ expect(result.complete).toBe(false);
187
+ expect(result.skippedSteps).toEqual(['vault-query']);
188
+ });
189
+ });
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // persistTracker / loadTracker
193
+ // ---------------------------------------------------------------------------
194
+
195
+ describe('persistence', () => {
196
+ let tmpDir: string;
197
+ let originalHome: string | undefined;
198
+ let originalUserProfile: string | undefined;
199
+
200
+ beforeEach(() => {
201
+ tmpDir = join(tmpdir(), `step-tracker-test-${Date.now()}`);
202
+ mkdirSync(tmpDir, { recursive: true });
203
+ originalHome = process.env.HOME;
204
+ originalUserProfile = process.env.USERPROFILE;
205
+ // Override HOME (Unix) and USERPROFILE (Windows) so getRunsDir() writes to our temp dir
206
+ process.env.HOME = tmpDir;
207
+ process.env.USERPROFILE = tmpDir;
208
+ });
209
+
210
+ afterEach(() => {
211
+ process.env.HOME = originalHome;
212
+ process.env.USERPROFILE = originalUserProfile;
213
+ try {
214
+ rmSync(tmpDir, { recursive: true, force: true });
215
+ } catch {
216
+ // cleanup best-effort
217
+ }
218
+ });
219
+
220
+ it('round-trips correctly', () => {
221
+ let tracker = createTracker('persist-test', SAMPLE_STEPS);
222
+ tracker = recordEvidence(tracker, 'vault-query', 'called');
223
+ tracker = advanceStep(tracker);
224
+
225
+ const filePath = persistTracker(tracker);
226
+
227
+ expect(existsSync(filePath)).toBe(true);
228
+
229
+ const loaded = loadTracker(tracker.runId);
230
+ expect(loaded).not.toBeNull();
231
+ expect(loaded!.skillName).toBe('persist-test');
232
+ expect(loaded!.currentStep).toBe(1);
233
+ expect(loaded!.evidence['vault-query'].value).toBe('called');
234
+ expect(loaded!.steps).toEqual(SAMPLE_STEPS);
235
+ });
236
+
237
+ it('returns null for non-existent runId', () => {
238
+ const result = loadTracker('does-not-exist-12345');
239
+ expect(result).toBeNull();
240
+ });
241
+
242
+ it('creates skill-runs directory if missing', () => {
243
+ const runsDir = join(tmpDir, '.soleri', 'skill-runs');
244
+ expect(existsSync(runsDir)).toBe(false);
245
+
246
+ const tracker = createTracker('dir-test', SAMPLE_STEPS);
247
+ persistTracker(tracker);
248
+
249
+ expect(existsSync(runsDir)).toBe(true);
250
+ });
251
+
252
+ it('returns null for corrupted JSON', () => {
253
+ const runsDir = join(tmpDir, '.soleri', 'skill-runs');
254
+ mkdirSync(runsDir, { recursive: true });
255
+ writeFileSync(join(runsDir, 'bad-run.json'), '{ invalid json', 'utf-8');
256
+
257
+ const result = loadTracker('bad-run');
258
+ expect(result).toBeNull();
259
+ });
260
+ });