@soleri/core 9.11.0 → 9.13.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 (234) 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/dream/cron-manager.d.ts +10 -0
  8. package/dist/dream/cron-manager.d.ts.map +1 -0
  9. package/dist/dream/cron-manager.js +122 -0
  10. package/dist/dream/cron-manager.js.map +1 -0
  11. package/dist/dream/dream-engine.d.ts +34 -0
  12. package/dist/dream/dream-engine.d.ts.map +1 -0
  13. package/dist/dream/dream-engine.js +88 -0
  14. package/dist/dream/dream-engine.js.map +1 -0
  15. package/dist/dream/dream-ops.d.ts +8 -0
  16. package/dist/dream/dream-ops.d.ts.map +1 -0
  17. package/dist/dream/dream-ops.js +49 -0
  18. package/dist/dream/dream-ops.js.map +1 -0
  19. package/dist/dream/index.d.ts +7 -0
  20. package/dist/dream/index.d.ts.map +1 -0
  21. package/dist/dream/index.js +5 -0
  22. package/dist/dream/index.js.map +1 -0
  23. package/dist/dream/schema.d.ts +3 -0
  24. package/dist/dream/schema.d.ts.map +1 -0
  25. package/dist/dream/schema.js +16 -0
  26. package/dist/dream/schema.js.map +1 -0
  27. package/dist/embeddings/index.d.ts +5 -0
  28. package/dist/embeddings/index.d.ts.map +1 -0
  29. package/dist/embeddings/index.js +3 -0
  30. package/dist/embeddings/index.js.map +1 -0
  31. package/dist/embeddings/openai-provider.d.ts +31 -0
  32. package/dist/embeddings/openai-provider.d.ts.map +1 -0
  33. package/dist/embeddings/openai-provider.js +120 -0
  34. package/dist/embeddings/openai-provider.js.map +1 -0
  35. package/dist/embeddings/pipeline.d.ts +36 -0
  36. package/dist/embeddings/pipeline.d.ts.map +1 -0
  37. package/dist/embeddings/pipeline.js +78 -0
  38. package/dist/embeddings/pipeline.js.map +1 -0
  39. package/dist/embeddings/types.d.ts +62 -0
  40. package/dist/embeddings/types.d.ts.map +1 -0
  41. package/dist/embeddings/types.js +3 -0
  42. package/dist/embeddings/types.js.map +1 -0
  43. package/dist/engine/bin/soleri-engine.js +4 -1
  44. package/dist/engine/bin/soleri-engine.js.map +1 -1
  45. package/dist/engine/module-manifest.d.ts.map +1 -1
  46. package/dist/engine/module-manifest.js +20 -0
  47. package/dist/engine/module-manifest.js.map +1 -1
  48. package/dist/engine/register-engine.d.ts.map +1 -1
  49. package/dist/engine/register-engine.js +12 -0
  50. package/dist/engine/register-engine.js.map +1 -1
  51. package/dist/flows/chain-types.d.ts +8 -8
  52. package/dist/flows/dispatch-registry.d.ts +15 -1
  53. package/dist/flows/dispatch-registry.d.ts.map +1 -1
  54. package/dist/flows/dispatch-registry.js +28 -1
  55. package/dist/flows/dispatch-registry.js.map +1 -1
  56. package/dist/flows/executor.d.ts +20 -2
  57. package/dist/flows/executor.d.ts.map +1 -1
  58. package/dist/flows/executor.js +79 -1
  59. package/dist/flows/executor.js.map +1 -1
  60. package/dist/flows/index.d.ts +2 -1
  61. package/dist/flows/index.d.ts.map +1 -1
  62. package/dist/flows/index.js.map +1 -1
  63. package/dist/flows/types.d.ts +43 -21
  64. package/dist/flows/types.d.ts.map +1 -1
  65. package/dist/index.d.ts +6 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +4 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/persona/defaults.d.ts +8 -0
  70. package/dist/persona/defaults.d.ts.map +1 -1
  71. package/dist/persona/defaults.js +49 -0
  72. package/dist/persona/defaults.js.map +1 -1
  73. package/dist/plugins/types.d.ts +31 -31
  74. package/dist/runtime/admin-ops.d.ts.map +1 -1
  75. package/dist/runtime/admin-ops.js +15 -0
  76. package/dist/runtime/admin-ops.js.map +1 -1
  77. package/dist/runtime/admin-setup-ops.js +2 -2
  78. package/dist/runtime/admin-setup-ops.js.map +1 -1
  79. package/dist/runtime/embedding-ops.d.ts +12 -0
  80. package/dist/runtime/embedding-ops.d.ts.map +1 -0
  81. package/dist/runtime/embedding-ops.js +96 -0
  82. package/dist/runtime/embedding-ops.js.map +1 -0
  83. package/dist/runtime/facades/embedding-facade.d.ts +7 -0
  84. package/dist/runtime/facades/embedding-facade.d.ts.map +1 -0
  85. package/dist/runtime/facades/embedding-facade.js +8 -0
  86. package/dist/runtime/facades/embedding-facade.js.map +1 -0
  87. package/dist/runtime/facades/index.d.ts.map +1 -1
  88. package/dist/runtime/facades/index.js +12 -0
  89. package/dist/runtime/facades/index.js.map +1 -1
  90. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  91. package/dist/runtime/facades/orchestrate-facade.js +120 -0
  92. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  93. package/dist/runtime/feature-flags.d.ts.map +1 -1
  94. package/dist/runtime/feature-flags.js +4 -0
  95. package/dist/runtime/feature-flags.js.map +1 -1
  96. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  97. package/dist/runtime/orchestrate-ops.js +140 -9
  98. package/dist/runtime/orchestrate-ops.js.map +1 -1
  99. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  100. package/dist/runtime/planning-extra-ops.js +51 -0
  101. package/dist/runtime/planning-extra-ops.js.map +1 -1
  102. package/dist/runtime/preflight.d.ts +32 -0
  103. package/dist/runtime/preflight.d.ts.map +1 -0
  104. package/dist/runtime/preflight.js +29 -0
  105. package/dist/runtime/preflight.js.map +1 -0
  106. package/dist/runtime/runtime.d.ts.map +1 -1
  107. package/dist/runtime/runtime.js +33 -2
  108. package/dist/runtime/runtime.js.map +1 -1
  109. package/dist/runtime/types.d.ts +27 -0
  110. package/dist/runtime/types.d.ts.map +1 -1
  111. package/dist/skills/step-tracker.d.ts +39 -0
  112. package/dist/skills/step-tracker.d.ts.map +1 -0
  113. package/dist/skills/step-tracker.js +105 -0
  114. package/dist/skills/step-tracker.js.map +1 -0
  115. package/dist/skills/sync-skills.d.ts +3 -2
  116. package/dist/skills/sync-skills.d.ts.map +1 -1
  117. package/dist/skills/sync-skills.js +42 -8
  118. package/dist/skills/sync-skills.js.map +1 -1
  119. package/dist/subagent/dispatcher.d.ts +4 -3
  120. package/dist/subagent/dispatcher.d.ts.map +1 -1
  121. package/dist/subagent/dispatcher.js +57 -35
  122. package/dist/subagent/dispatcher.js.map +1 -1
  123. package/dist/subagent/index.d.ts +1 -0
  124. package/dist/subagent/index.d.ts.map +1 -1
  125. package/dist/subagent/index.js.map +1 -1
  126. package/dist/subagent/orphan-reaper.d.ts +51 -4
  127. package/dist/subagent/orphan-reaper.d.ts.map +1 -1
  128. package/dist/subagent/orphan-reaper.js +103 -3
  129. package/dist/subagent/orphan-reaper.js.map +1 -1
  130. package/dist/subagent/types.d.ts +7 -0
  131. package/dist/subagent/types.d.ts.map +1 -1
  132. package/dist/subagent/workspace-resolver.d.ts +2 -0
  133. package/dist/subagent/workspace-resolver.d.ts.map +1 -1
  134. package/dist/subagent/workspace-resolver.js +3 -1
  135. package/dist/subagent/workspace-resolver.js.map +1 -1
  136. package/dist/vault/vault-entries.d.ts +18 -0
  137. package/dist/vault/vault-entries.d.ts.map +1 -1
  138. package/dist/vault/vault-entries.js +73 -0
  139. package/dist/vault/vault-entries.js.map +1 -1
  140. package/dist/vault/vault-manager.d.ts.map +1 -1
  141. package/dist/vault/vault-manager.js +1 -0
  142. package/dist/vault/vault-manager.js.map +1 -1
  143. package/dist/vault/vault-schema.d.ts.map +1 -1
  144. package/dist/vault/vault-schema.js +14 -0
  145. package/dist/vault/vault-schema.js.map +1 -1
  146. package/dist/vault/vault.d.ts +1 -0
  147. package/dist/vault/vault.d.ts.map +1 -1
  148. package/dist/vault/vault.js.map +1 -1
  149. package/package.json +3 -5
  150. package/src/__tests__/cron-manager.test.ts +132 -0
  151. package/src/__tests__/deviation-detection.test.ts +234 -0
  152. package/src/__tests__/embeddings.test.ts +536 -0
  153. package/src/__tests__/preflight.test.ts +97 -0
  154. package/src/__tests__/step-persistence.test.ts +324 -0
  155. package/src/__tests__/step-tracker.test.ts +260 -0
  156. package/src/__tests__/subagent/dispatcher.test.ts +122 -4
  157. package/src/__tests__/subagent/orphan-reaper.test.ts +148 -12
  158. package/src/__tests__/subagent/process-lifecycle.test.ts +422 -0
  159. package/src/__tests__/subagent/workspace-resolver.test.ts +6 -1
  160. package/src/adapters/types.ts +2 -0
  161. package/src/brain/brain.ts +117 -9
  162. package/src/dream/cron-manager.ts +137 -0
  163. package/src/dream/dream-engine.ts +119 -0
  164. package/src/dream/dream-ops.ts +56 -0
  165. package/src/dream/dream.test.ts +182 -0
  166. package/src/dream/index.ts +6 -0
  167. package/src/dream/schema.ts +17 -0
  168. package/src/embeddings/openai-provider.ts +158 -0
  169. package/src/embeddings/pipeline.ts +126 -0
  170. package/src/embeddings/types.ts +67 -0
  171. package/src/engine/bin/soleri-engine.ts +4 -1
  172. package/src/engine/module-manifest.test.ts +4 -4
  173. package/src/engine/module-manifest.ts +20 -0
  174. package/src/engine/register-engine.ts +12 -0
  175. package/src/flows/dispatch-registry.ts +44 -1
  176. package/src/flows/executor.ts +93 -2
  177. package/src/flows/index.ts +2 -0
  178. package/src/flows/types.ts +39 -1
  179. package/src/index.ts +12 -0
  180. package/src/persona/defaults.test.ts +39 -1
  181. package/src/persona/defaults.ts +65 -0
  182. package/src/planning/goal-ancestry.test.ts +3 -5
  183. package/src/planning/planner.test.ts +2 -3
  184. package/src/runtime/admin-ops.test.ts +2 -2
  185. package/src/runtime/admin-ops.ts +17 -0
  186. package/src/runtime/admin-setup-ops.ts +2 -2
  187. package/src/runtime/embedding-ops.ts +116 -0
  188. package/src/runtime/facades/admin-facade.test.ts +31 -0
  189. package/src/runtime/facades/embedding-facade.ts +11 -0
  190. package/src/runtime/facades/index.ts +12 -0
  191. package/src/runtime/facades/orchestrate-facade.test.ts +16 -0
  192. package/src/runtime/facades/orchestrate-facade.ts +146 -0
  193. package/src/runtime/feature-flags.ts +4 -0
  194. package/src/runtime/orchestrate-ops.test.ts +131 -0
  195. package/src/runtime/orchestrate-ops.ts +158 -10
  196. package/src/runtime/planning-extra-ops.ts +77 -0
  197. package/src/runtime/preflight.ts +53 -0
  198. package/src/runtime/runtime.ts +41 -2
  199. package/src/runtime/types.ts +20 -0
  200. package/src/skills/__tests__/sync-skills.test.ts +132 -0
  201. package/src/skills/step-tracker.ts +162 -0
  202. package/src/skills/sync-skills.ts +54 -9
  203. package/src/subagent/dispatcher.ts +62 -39
  204. package/src/subagent/index.ts +1 -0
  205. package/src/subagent/orphan-reaper.test.ts +135 -0
  206. package/src/subagent/orphan-reaper.ts +130 -7
  207. package/src/subagent/types.ts +10 -0
  208. package/src/subagent/workspace-resolver.ts +3 -1
  209. package/src/vault/vault-entries.ts +112 -0
  210. package/src/vault/vault-manager.ts +1 -0
  211. package/src/vault/vault-scaling.test.ts +3 -2
  212. package/src/vault/vault-schema.ts +15 -0
  213. package/src/vault/vault.ts +1 -0
  214. package/vitest.config.ts +2 -1
  215. package/dist/brain/strength-scorer.d.ts +0 -31
  216. package/dist/brain/strength-scorer.d.ts.map +0 -1
  217. package/dist/brain/strength-scorer.js +0 -264
  218. package/dist/brain/strength-scorer.js.map +0 -1
  219. package/dist/engine/index.d.ts +0 -21
  220. package/dist/engine/index.d.ts.map +0 -1
  221. package/dist/engine/index.js +0 -18
  222. package/dist/engine/index.js.map +0 -1
  223. package/dist/hooks/index.d.ts +0 -2
  224. package/dist/hooks/index.d.ts.map +0 -1
  225. package/dist/hooks/index.js +0 -2
  226. package/dist/hooks/index.js.map +0 -1
  227. package/dist/persona/index.d.ts +0 -5
  228. package/dist/persona/index.d.ts.map +0 -1
  229. package/dist/persona/index.js +0 -4
  230. package/dist/persona/index.js.map +0 -1
  231. package/dist/vault/vault-interfaces.d.ts +0 -153
  232. package/dist/vault/vault-interfaces.d.ts.map +0 -1
  233. package/dist/vault/vault-interfaces.js +0 -2
  234. package/dist/vault/vault-interfaces.js.map +0 -1
@@ -0,0 +1,132 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+
6
+ // =============================================================================
7
+ // HELPERS
8
+ // =============================================================================
9
+
10
+ let sourceDir: string;
11
+ let fakeHome: string;
12
+
13
+ function setup(): void {
14
+ const base = join(tmpdir(), `soleri-sync-test-${Date.now()}`);
15
+ mkdirSync(base, { recursive: true });
16
+
17
+ sourceDir = join(base, 'source-skills');
18
+ mkdirSync(sourceDir, { recursive: true });
19
+
20
+ fakeHome = join(base, 'fake-home');
21
+ mkdirSync(join(fakeHome, '.claude', 'skills'), { recursive: true });
22
+ }
23
+
24
+ function teardown(): void {
25
+ if (fakeHome) {
26
+ const base = join(fakeHome, '..');
27
+ rmSync(base, { recursive: true, force: true });
28
+ }
29
+ }
30
+
31
+ /** Create a source skill directory with a minimal SKILL.md */
32
+ function createSourceSkill(name: string, content?: string): string {
33
+ const dir = join(sourceDir, name);
34
+ mkdirSync(dir, { recursive: true });
35
+ writeFileSync(
36
+ join(dir, 'SKILL.md'),
37
+ content ?? `---\nname: ${name}\n---\n\n# ${name}\n\nA test skill.\n`,
38
+ );
39
+ return dir;
40
+ }
41
+
42
+ /** Create a directory in the fake ~/.claude/skills/ target */
43
+ function createTargetSkillDir(name: string): string {
44
+ const dir = join(fakeHome, '.claude', 'skills', name);
45
+ mkdirSync(dir, { recursive: true });
46
+ writeFileSync(join(dir, 'SKILL.md'), `---\nname: ${name}\n---\n\nStale skill.\n`);
47
+ return dir;
48
+ }
49
+
50
+ function targetSkillsDir(): string {
51
+ return join(fakeHome, '.claude', 'skills');
52
+ }
53
+
54
+ function targetDirExists(name: string): boolean {
55
+ return existsSync(join(targetSkillsDir(), name));
56
+ }
57
+
58
+ // =============================================================================
59
+ // TESTS
60
+ // =============================================================================
61
+
62
+ describe('syncSkillsToClaudeCode — orphan cleanup', () => {
63
+ beforeEach(() => {
64
+ setup();
65
+ // Mock homedir() so syncSkillsToClaudeCode writes to our temp directory
66
+ vi.mock('node:os', async (importOriginal) => {
67
+ const original = await importOriginal<typeof import('node:os')>();
68
+ return {
69
+ ...original,
70
+ homedir: () => fakeHome,
71
+ };
72
+ });
73
+ });
74
+
75
+ afterEach(() => {
76
+ vi.restoreAllMocks();
77
+ teardown();
78
+ });
79
+
80
+ it('removes orphan directories that match the agent prefix', async () => {
81
+ // Source has "my-skill", target has stale "test-agent-old-skill"
82
+ createSourceSkill('my-skill');
83
+ createTargetSkillDir('test-agent-old-skill');
84
+
85
+ const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
86
+ const result = syncSkillsToClaudeCode([sourceDir], 'Test Agent');
87
+
88
+ // The orphan should be reported as removed
89
+ expect(result.removed).toContain('test-agent-old-skill');
90
+ // The orphan directory should be gone
91
+ expect(targetDirExists('test-agent-old-skill')).toBe(false);
92
+ });
93
+
94
+ it('does NOT remove directories that do not match the agent prefix', async () => {
95
+ createSourceSkill('my-skill');
96
+ // "other-agent-skill" does NOT start with "test-agent-"
97
+ createTargetSkillDir('other-agent-skill');
98
+
99
+ const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
100
+ const result = syncSkillsToClaudeCode([sourceDir], 'Test Agent');
101
+
102
+ // Should still exist — not our prefix
103
+ expect(targetDirExists('other-agent-skill')).toBe(true);
104
+ expect(result.removed).not.toContain('other-agent-skill');
105
+ });
106
+
107
+ it('does NOT remove a skill directory that was just synced', async () => {
108
+ createSourceSkill('active-skill');
109
+ // This directory matches the prefix AND is a current skill
110
+ createTargetSkillDir('test-agent-active-skill');
111
+
112
+ const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
113
+ const result = syncSkillsToClaudeCode([sourceDir], 'Test Agent');
114
+
115
+ // "active-skill" should be synced (installed/updated/skipped), not removed
116
+ const synced = [...result.installed, ...result.updated, ...result.skipped];
117
+ expect(synced).toContain('active-skill');
118
+ expect(result.removed).not.toContain('test-agent-active-skill');
119
+ expect(targetDirExists('test-agent-active-skill')).toBe(true);
120
+ });
121
+
122
+ it('returns an empty removed array when there are no orphans', async () => {
123
+ createSourceSkill('only-skill');
124
+
125
+ const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
126
+ const result = syncSkillsToClaudeCode([sourceDir], 'Test Agent');
127
+
128
+ expect(result.removed).toBeDefined();
129
+ expect(Array.isArray(result.removed)).toBe(true);
130
+ expect(result.removed).toHaveLength(0);
131
+ });
132
+ });
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Skill step tracker — converts skills from suggestions to enforceable protocols.
3
+ * Persists step state to .soleri/skill-runs/ for context compaction survival.
4
+ */
5
+
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { homedir } from 'node:os';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Types
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export type EvidenceType = 'tool_called' | 'file_exists';
15
+
16
+ export interface SkillStep {
17
+ id: string;
18
+ description: string;
19
+ evidence: EvidenceType;
20
+ }
21
+
22
+ export interface StepEvidence {
23
+ type: EvidenceType;
24
+ value: string;
25
+ timestamp: string;
26
+ verified: boolean;
27
+ }
28
+
29
+ export interface SkillStepTracker {
30
+ skillName: string;
31
+ runId: string;
32
+ steps: SkillStep[];
33
+ currentStep: number;
34
+ startedAt: string;
35
+ evidence: Record<string, StepEvidence>;
36
+ completedAt?: string;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Factory
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /** Sanitize a string for safe use in file paths. */
44
+ function sanitizeForPath(name: string): string {
45
+ return name.replace(/[/\\:*?"<>|.]/g, '_');
46
+ }
47
+
48
+ export function createTracker(skillName: string, steps: SkillStep[]): SkillStepTracker {
49
+ return {
50
+ skillName,
51
+ runId: `${sanitizeForPath(skillName)}-${Date.now()}`,
52
+ steps,
53
+ currentStep: 0,
54
+ startedAt: new Date().toISOString(),
55
+ evidence: {},
56
+ };
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Operations
61
+ // ---------------------------------------------------------------------------
62
+
63
+ export function advanceStep(tracker: SkillStepTracker): SkillStepTracker {
64
+ if (tracker.currentStep < tracker.steps.length - 1) {
65
+ return { ...tracker, currentStep: tracker.currentStep + 1 };
66
+ }
67
+ return { ...tracker, completedAt: new Date().toISOString() };
68
+ }
69
+
70
+ export function recordEvidence(
71
+ tracker: SkillStepTracker,
72
+ stepId: string,
73
+ value: string,
74
+ verified: boolean = true,
75
+ ): SkillStepTracker {
76
+ const step = tracker.steps.find((s) => s.id === stepId);
77
+ if (!step) return tracker;
78
+
79
+ return {
80
+ ...tracker,
81
+ evidence: {
82
+ ...tracker.evidence,
83
+ [stepId]: {
84
+ type: step.evidence,
85
+ value,
86
+ timestamp: new Date().toISOString(),
87
+ verified,
88
+ },
89
+ },
90
+ };
91
+ }
92
+
93
+ export function generateCheckpoint(tracker: SkillStepTracker): string {
94
+ const completed = tracker.steps
95
+ .filter((s) => tracker.evidence[s.id]?.verified)
96
+ .map((s) => `${s.id} ✓`);
97
+
98
+ const current = tracker.steps[tracker.currentStep];
99
+ const total = tracker.steps.length;
100
+ const completedCount = completed.length;
101
+
102
+ const lines = [
103
+ `--- Skill Checkpoint: ${tracker.skillName} ---`,
104
+ `Completed: ${completed.length > 0 ? completed.join(', ') : 'none'}`,
105
+ `Current: ${current ? `${current.id} (step ${tracker.currentStep + 1} of ${total})` : 'all done'}`,
106
+ ];
107
+
108
+ if (current) {
109
+ lines.push(`Evidence required: ${current.evidence} → ${current.description}`);
110
+ }
111
+
112
+ lines.push(`Progress: ${completedCount}/${total}`, '---');
113
+ return lines.join('\n');
114
+ }
115
+
116
+ export interface CompletionResult {
117
+ complete: boolean;
118
+ skippedSteps: string[];
119
+ evidenceCount: number;
120
+ totalSteps: number;
121
+ }
122
+
123
+ export function validateCompletion(tracker: SkillStepTracker): CompletionResult {
124
+ const skippedSteps = tracker.steps
125
+ .filter((s) => !tracker.evidence[s.id]?.verified)
126
+ .map((s) => s.id);
127
+
128
+ return {
129
+ complete: skippedSteps.length === 0,
130
+ skippedSteps,
131
+ evidenceCount: Object.keys(tracker.evidence).length,
132
+ totalSteps: tracker.steps.length,
133
+ };
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Persistence
138
+ // ---------------------------------------------------------------------------
139
+
140
+ function getRunsDir(): string {
141
+ return join(homedir(), '.soleri', 'skill-runs');
142
+ }
143
+
144
+ export function persistTracker(tracker: SkillStepTracker): string {
145
+ const dir = getRunsDir();
146
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
147
+
148
+ const filePath = join(dir, `${tracker.runId}.json`);
149
+ writeFileSync(filePath, JSON.stringify(tracker, null, 2), 'utf-8');
150
+ return filePath;
151
+ }
152
+
153
+ export function loadTracker(runId: string): SkillStepTracker | null {
154
+ const filePath = join(getRunsDir(), `${runId}.json`);
155
+ if (!existsSync(filePath)) return null;
156
+
157
+ try {
158
+ return JSON.parse(readFileSync(filePath, 'utf-8')) as SkillStepTracker;
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
@@ -1,12 +1,21 @@
1
1
  /**
2
2
  * Skill sync — discovers SKILL.md files in agent skills directories
3
- * and copies them to ~/.claude/commands/ for Claude Code discovery.
3
+ * and copies them to ~/.claude/skills/ for Claude Code discovery.
4
4
  *
5
5
  * Injects agent branding so users know which agent owns the skill.
6
6
  * Called automatically at engine startup and by admin_setup_global.
7
7
  */
8
8
 
9
- import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
9
+ import {
10
+ cpSync,
11
+ existsSync,
12
+ mkdirSync,
13
+ readdirSync,
14
+ readFileSync,
15
+ rmSync,
16
+ statSync,
17
+ writeFileSync,
18
+ } from 'node:fs';
10
19
  import { join, dirname } from 'node:path';
11
20
  import { homedir } from 'node:os';
12
21
  import type { SkillMetadata, SourceType } from '../packs/types.js';
@@ -25,6 +34,7 @@ export interface SyncResult {
25
34
  updated: string[];
26
35
  skipped: string[];
27
36
  failed: string[];
37
+ removed: string[];
28
38
  }
29
39
 
30
40
  /** Error thrown when a skill requires approval due to scripts trust level */
@@ -86,30 +96,31 @@ function brandSkillContent(content: string, agentName: string, prefixedName?: st
86
96
  }
87
97
 
88
98
  /**
89
- * Sync skills from agent directory to ~/.claude/commands/.
99
+ * Sync skills from agent directory to ~/.claude/skills/.
90
100
  * - New skills are installed with agent branding
91
101
  * - Changed skills are overwritten (compared by mtime)
92
102
  * - Missing source skills leave target untouched (other agents may own them)
93
103
  */
94
104
  export function syncSkillsToClaudeCode(skillsDirs: string[], agentName?: string): SyncResult {
95
- const commandsDir = join(homedir(), '.claude', 'commands');
105
+ const skillsDir = join(homedir(), '.claude', 'skills');
96
106
  const skills = discoverSkills(skillsDirs);
97
- const result: SyncResult = { installed: [], updated: [], skipped: [], failed: [] };
107
+ const result: SyncResult = { installed: [], updated: [], skipped: [], failed: [], removed: [] };
98
108
 
99
109
  if (skills.length === 0) return result;
100
110
 
101
- mkdirSync(commandsDir, { recursive: true });
102
-
103
111
  for (const skill of skills) {
104
112
  const prefix = agentName ? `${agentName.toLowerCase().replace(/\s+/g, '-')}-` : '';
105
- const targetPath = join(commandsDir, `${prefix}${skill.name}.md`);
113
+ const skillName = `${prefix}${skill.name}`;
114
+ const targetDir = join(skillsDir, skillName);
115
+ const targetPath = join(targetDir, 'SKILL.md');
106
116
  try {
107
117
  const sourceContent = readFileSync(skill.sourcePath, 'utf-8');
108
118
  const branded = agentName
109
- ? brandSkillContent(sourceContent, agentName, `${prefix}${skill.name}`)
119
+ ? brandSkillContent(sourceContent, agentName, skillName)
110
120
  : sourceContent;
111
121
 
112
122
  if (!existsSync(targetPath)) {
123
+ mkdirSync(targetDir, { recursive: true });
113
124
  writeFileSync(targetPath, branded);
114
125
  result.installed.push(skill.name);
115
126
  } else {
@@ -127,6 +138,40 @@ export function syncSkillsToClaudeCode(skillsDirs: string[], agentName?: string)
127
138
  }
128
139
  }
129
140
 
141
+ // Orphan cleanup: remove skills that belong to this agent but are no longer in source
142
+ if (agentName) {
143
+ const prefix = `${agentName.toLowerCase().replace(/\s+/g, '-')}-`;
144
+ const syncedNames = new Set<string>(
145
+ [...result.installed, ...result.updated, ...result.skipped, ...result.failed].map(
146
+ (name) => `${prefix}${name}`,
147
+ ),
148
+ );
149
+
150
+ try {
151
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
152
+ for (const entry of entries) {
153
+ if (!entry.isDirectory()) continue;
154
+ if (!entry.name.startsWith(prefix)) continue;
155
+ if (syncedNames.has(entry.name)) continue;
156
+
157
+ // Orphan detected — stage backup then remove
158
+ const orphanPath = join(skillsDir, entry.name);
159
+ try {
160
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
161
+ const stagingDir = join(homedir(), '.soleri', 'staging', timestamp);
162
+ mkdirSync(stagingDir, { recursive: true });
163
+ cpSync(orphanPath, join(stagingDir, entry.name), { recursive: true });
164
+ rmSync(orphanPath, { recursive: true, force: true });
165
+ result.removed.push(entry.name);
166
+ } catch {
167
+ result.failed.push(entry.name);
168
+ }
169
+ }
170
+ } catch {
171
+ // Skills directory doesn't exist or is unreadable — nothing to clean
172
+ }
173
+ }
174
+
130
175
  return result;
131
176
  }
132
177
 
@@ -18,6 +18,7 @@ import { TaskCheckout } from './task-checkout.js';
18
18
  import { WorkspaceResolver } from './workspace-resolver.js';
19
19
  import { ConcurrencyManager } from './concurrency-manager.js';
20
20
  import { OrphanReaper } from './orphan-reaper.js';
21
+ import type { ReapResult } from './orphan-reaper.js';
21
22
  import { aggregate } from './result-aggregator.js';
22
23
  import type { GoalRepository } from '../planning/goal-ancestry.js';
23
24
  import { GoalAncestry } from '../planning/goal-ancestry.js';
@@ -48,8 +49,9 @@ export class SubagentDispatcher {
48
49
  this.goalAncestry = new GoalAncestry(config.goalRepository);
49
50
  }
50
51
  this.workspace = new WorkspaceResolver(config.baseDir ?? process.cwd());
51
- this.reaper = new OrphanReaper((taskId) => {
52
- // On orphan: release the task claim and clean up workspace
52
+ this.reaper = new OrphanReaper((taskId, pid) => {
53
+ // On orphan: kill the process group, release the task claim, and clean up workspace
54
+ this.reaper.killProcessGroup(pid);
53
55
  this.checkout.release(taskId);
54
56
  this.workspace.cleanup(taskId);
55
57
  });
@@ -78,52 +80,49 @@ export class SubagentDispatcher {
78
80
  // Resolve dependency order
79
81
  const ordered = this.resolveDependencies(tasks);
80
82
 
81
- if (parallel) {
82
- // Run independent tasks in parallel, respecting dependencies
83
- const results = await this.dispatchParallel(ordered, {
84
- maxConcurrent,
85
- worktreeIsolation,
86
- timeout,
87
- onTaskUpdate,
88
- });
89
- return aggregate(results);
90
- }
83
+ try {
84
+ if (parallel) {
85
+ // Run independent tasks in parallel, respecting dependencies
86
+ const results = await this.dispatchParallel(ordered, {
87
+ maxConcurrent,
88
+ worktreeIsolation,
89
+ timeout,
90
+ onTaskUpdate,
91
+ });
92
+ return aggregate(results);
93
+ }
91
94
 
92
- // Sequential dispatch — await in loop is intentional (tasks must run one at a time)
93
- const results: SubagentResult[] = [];
94
- for (const task of ordered) {
95
- // eslint-disable-line no-await-in-loop
96
- onTaskUpdate?.(task.taskId, 'running');
97
- const result = await this.executeTask(task, worktreeIsolation, timeout);
98
- results.push(result);
99
- onTaskUpdate?.(task.taskId, result.status);
100
-
101
- // Stop on failure in sequential mode
102
- if (result.exitCode !== 0) break;
103
- }
95
+ // Sequential dispatch — await in loop is intentional (tasks must run one at a time)
96
+ const results: SubagentResult[] = [];
97
+ for (const task of ordered) {
98
+ // eslint-disable-line no-await-in-loop
99
+ onTaskUpdate?.(task.taskId, 'running');
100
+ const result = await this.executeTask(task, worktreeIsolation, timeout);
101
+ results.push(result);
102
+ onTaskUpdate?.(task.taskId, result.status);
103
+
104
+ // Stop on failure in sequential mode
105
+ if (result.exitCode !== 0) break;
106
+ }
104
107
 
105
- return aggregate(results);
108
+ return aggregate(results);
109
+ } finally {
110
+ // Event-driven orphan reaping: sweep after every dispatch cycle
111
+ this.reaper.reap();
112
+ }
106
113
  }
107
114
 
108
- /** Clean up all resources (worktrees, claims, concurrency) */
115
+ /** Clean up all resources — kills tracked process groups, then cleans worktrees, claims, concurrency */
109
116
  cleanup(): void {
117
+ this.reaper.killAll();
110
118
  this.workspace.cleanupAll();
111
119
  this.checkout.releaseAll();
112
120
  this.concurrency.reset();
113
- this.reaper.clear();
114
121
  }
115
122
 
116
123
  /** Run orphan detection and cleanup */
117
- reapOrphans(): SubagentResult[] {
118
- const orphaned = this.reaper.reap();
119
- return orphaned.map((p) => ({
120
- taskId: p.taskId,
121
- status: 'orphaned' as const,
122
- exitCode: 1,
123
- error: `Process ${p.pid} died unexpectedly`,
124
- durationMs: Date.now() - p.registeredAt,
125
- pid: p.pid,
126
- }));
124
+ reapOrphans(): ReapResult {
125
+ return this.reaper.reap();
127
126
  }
128
127
 
129
128
  // ── Internal ──────────────────────────────────────────────────────
@@ -261,21 +260,43 @@ export class SubagentDispatcher {
261
260
  this.goalAncestry.inject({ config: enrichedConfig }, goalId).config ?? enrichedConfig;
262
261
  }
263
262
 
264
- // 5. Execute with timeout
263
+ // 5. Execute with timeout and active process killing
264
+ let childPid: number | undefined;
265
265
  try {
266
266
  const resultPromise = adapter.execute({
267
267
  runId: `subagent-${task.taskId}-${Date.now()}`,
268
268
  prompt: task.prompt,
269
269
  workspace,
270
270
  config: enrichedConfig,
271
+ onMeta: (meta) => {
272
+ // Adapters report their child PID via onMeta({ pid })
273
+ if (typeof meta.pid === 'number') {
274
+ childPid = meta.pid;
275
+ this.reaper.register(childPid, task.taskId);
276
+ }
277
+ },
271
278
  });
272
279
 
273
280
  const timeoutPromise = new Promise<never>((_, reject) => {
274
- setTimeout(() => reject(new Error('Task timed out')), timeout);
281
+ setTimeout(() => {
282
+ reject(new Error('Task timed out'));
283
+ // Kill the child process if we have a PID
284
+ if (childPid !== undefined) {
285
+ // Fire-and-forget: kill with escalation (SIGTERM → wait 5s → SIGKILL)
286
+ void this.reaper.killProcess(childPid, true).then(() => {
287
+ this.reaper.unregister(childPid!);
288
+ });
289
+ }
290
+ }, timeout);
275
291
  });
276
292
 
277
293
  const adapterResult = await Promise.race([resultPromise, timeoutPromise]);
278
294
 
295
+ // Normal completion — unregister from reaper
296
+ if (childPid !== undefined) {
297
+ this.reaper.unregister(childPid);
298
+ }
299
+
279
300
  return {
280
301
  taskId: task.taskId,
281
302
  status: adapterResult.exitCode === 0 ? 'completed' : 'failed',
@@ -284,6 +305,7 @@ export class SubagentDispatcher {
284
305
  usage: adapterResult.usage,
285
306
  sessionState: adapterResult.sessionState,
286
307
  durationMs: Date.now() - startTime,
308
+ pid: childPid ?? adapterResult.pid,
287
309
  };
288
310
  } catch (err) {
289
311
  return {
@@ -292,6 +314,7 @@ export class SubagentDispatcher {
292
314
  exitCode: 1,
293
315
  error: err instanceof Error ? err.message : String(err),
294
316
  durationMs: Date.now() - startTime,
317
+ pid: childPid,
295
318
  };
296
319
  } finally {
297
320
  // Cleanup
@@ -22,6 +22,7 @@ export { TaskCheckout } from './task-checkout.js';
22
22
  export { WorkspaceResolver } from './workspace-resolver.js';
23
23
  export { ConcurrencyManager } from './concurrency-manager.js';
24
24
  export { OrphanReaper } from './orphan-reaper.js';
25
+ export type { ReapResult, ProcessGroupKillResult } from './orphan-reaper.js';
25
26
  export { aggregate as aggregateResults } from './result-aggregator.js';
26
27
 
27
28
  // Dispatcher