@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.
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/brain/brain.d.ts +5 -1
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +97 -10
- package/dist/brain/brain.js.map +1 -1
- package/dist/dream/cron-manager.d.ts +10 -0
- package/dist/dream/cron-manager.d.ts.map +1 -0
- package/dist/dream/cron-manager.js +122 -0
- package/dist/dream/cron-manager.js.map +1 -0
- package/dist/dream/dream-engine.d.ts +34 -0
- package/dist/dream/dream-engine.d.ts.map +1 -0
- package/dist/dream/dream-engine.js +88 -0
- package/dist/dream/dream-engine.js.map +1 -0
- package/dist/dream/dream-ops.d.ts +8 -0
- package/dist/dream/dream-ops.d.ts.map +1 -0
- package/dist/dream/dream-ops.js +49 -0
- package/dist/dream/dream-ops.js.map +1 -0
- package/dist/dream/index.d.ts +7 -0
- package/dist/dream/index.d.ts.map +1 -0
- package/dist/dream/index.js +5 -0
- package/dist/dream/index.js.map +1 -0
- package/dist/dream/schema.d.ts +3 -0
- package/dist/dream/schema.d.ts.map +1 -0
- package/dist/dream/schema.js +16 -0
- package/dist/dream/schema.js.map +1 -0
- package/dist/embeddings/index.d.ts +5 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +3 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/openai-provider.d.ts +31 -0
- package/dist/embeddings/openai-provider.d.ts.map +1 -0
- package/dist/embeddings/openai-provider.js +120 -0
- package/dist/embeddings/openai-provider.js.map +1 -0
- package/dist/embeddings/pipeline.d.ts +36 -0
- package/dist/embeddings/pipeline.d.ts.map +1 -0
- package/dist/embeddings/pipeline.js +78 -0
- package/dist/embeddings/pipeline.js.map +1 -0
- package/dist/embeddings/types.d.ts +62 -0
- package/dist/embeddings/types.d.ts.map +1 -0
- package/dist/embeddings/types.js +3 -0
- package/dist/embeddings/types.js.map +1 -0
- package/dist/engine/bin/soleri-engine.js +4 -1
- package/dist/engine/bin/soleri-engine.js.map +1 -1
- package/dist/engine/module-manifest.d.ts.map +1 -1
- package/dist/engine/module-manifest.js +20 -0
- package/dist/engine/module-manifest.js.map +1 -1
- package/dist/engine/register-engine.d.ts.map +1 -1
- package/dist/engine/register-engine.js +12 -0
- package/dist/engine/register-engine.js.map +1 -1
- package/dist/flows/chain-types.d.ts +8 -8
- package/dist/flows/dispatch-registry.d.ts +15 -1
- package/dist/flows/dispatch-registry.d.ts.map +1 -1
- package/dist/flows/dispatch-registry.js +28 -1
- package/dist/flows/dispatch-registry.js.map +1 -1
- package/dist/flows/executor.d.ts +20 -2
- package/dist/flows/executor.d.ts.map +1 -1
- package/dist/flows/executor.js +79 -1
- package/dist/flows/executor.js.map +1 -1
- package/dist/flows/index.d.ts +2 -1
- package/dist/flows/index.d.ts.map +1 -1
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/types.d.ts +43 -21
- package/dist/flows/types.d.ts.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/persona/defaults.d.ts +8 -0
- package/dist/persona/defaults.d.ts.map +1 -1
- package/dist/persona/defaults.js +49 -0
- package/dist/persona/defaults.js.map +1 -1
- package/dist/plugins/types.d.ts +31 -31
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +15 -0
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.js +2 -2
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/embedding-ops.d.ts +12 -0
- package/dist/runtime/embedding-ops.d.ts.map +1 -0
- package/dist/runtime/embedding-ops.js +96 -0
- package/dist/runtime/embedding-ops.js.map +1 -0
- package/dist/runtime/facades/embedding-facade.d.ts +7 -0
- package/dist/runtime/facades/embedding-facade.d.ts.map +1 -0
- package/dist/runtime/facades/embedding-facade.js +8 -0
- package/dist/runtime/facades/embedding-facade.js.map +1 -0
- package/dist/runtime/facades/index.d.ts.map +1 -1
- package/dist/runtime/facades/index.js +12 -0
- package/dist/runtime/facades/index.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +120 -0
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/feature-flags.d.ts.map +1 -1
- package/dist/runtime/feature-flags.js +4 -0
- package/dist/runtime/feature-flags.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +140 -9
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +51 -0
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/preflight.d.ts +32 -0
- package/dist/runtime/preflight.d.ts.map +1 -0
- package/dist/runtime/preflight.js +29 -0
- package/dist/runtime/preflight.js.map +1 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +33 -2
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +27 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/skills/step-tracker.d.ts +39 -0
- package/dist/skills/step-tracker.d.ts.map +1 -0
- package/dist/skills/step-tracker.js +105 -0
- package/dist/skills/step-tracker.js.map +1 -0
- package/dist/skills/sync-skills.d.ts +3 -2
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +42 -8
- package/dist/skills/sync-skills.js.map +1 -1
- package/dist/subagent/dispatcher.d.ts +4 -3
- package/dist/subagent/dispatcher.d.ts.map +1 -1
- package/dist/subagent/dispatcher.js +57 -35
- package/dist/subagent/dispatcher.js.map +1 -1
- package/dist/subagent/index.d.ts +1 -0
- package/dist/subagent/index.d.ts.map +1 -1
- package/dist/subagent/index.js.map +1 -1
- package/dist/subagent/orphan-reaper.d.ts +51 -4
- package/dist/subagent/orphan-reaper.d.ts.map +1 -1
- package/dist/subagent/orphan-reaper.js +103 -3
- package/dist/subagent/orphan-reaper.js.map +1 -1
- package/dist/subagent/types.d.ts +7 -0
- package/dist/subagent/types.d.ts.map +1 -1
- package/dist/subagent/workspace-resolver.d.ts +2 -0
- package/dist/subagent/workspace-resolver.d.ts.map +1 -1
- package/dist/subagent/workspace-resolver.js +3 -1
- package/dist/subagent/workspace-resolver.js.map +1 -1
- package/dist/vault/vault-entries.d.ts +18 -0
- package/dist/vault/vault-entries.d.ts.map +1 -1
- package/dist/vault/vault-entries.js +73 -0
- package/dist/vault/vault-entries.js.map +1 -1
- package/dist/vault/vault-manager.d.ts.map +1 -1
- package/dist/vault/vault-manager.js +1 -0
- package/dist/vault/vault-manager.js.map +1 -1
- package/dist/vault/vault-schema.d.ts.map +1 -1
- package/dist/vault/vault-schema.js +14 -0
- package/dist/vault/vault-schema.js.map +1 -1
- package/dist/vault/vault.d.ts +1 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js.map +1 -1
- package/package.json +3 -5
- package/src/__tests__/cron-manager.test.ts +132 -0
- package/src/__tests__/deviation-detection.test.ts +234 -0
- package/src/__tests__/embeddings.test.ts +536 -0
- package/src/__tests__/preflight.test.ts +97 -0
- package/src/__tests__/step-persistence.test.ts +324 -0
- package/src/__tests__/step-tracker.test.ts +260 -0
- package/src/__tests__/subagent/dispatcher.test.ts +122 -4
- package/src/__tests__/subagent/orphan-reaper.test.ts +148 -12
- package/src/__tests__/subagent/process-lifecycle.test.ts +422 -0
- package/src/__tests__/subagent/workspace-resolver.test.ts +6 -1
- package/src/adapters/types.ts +2 -0
- package/src/brain/brain.ts +117 -9
- package/src/dream/cron-manager.ts +137 -0
- package/src/dream/dream-engine.ts +119 -0
- package/src/dream/dream-ops.ts +56 -0
- package/src/dream/dream.test.ts +182 -0
- package/src/dream/index.ts +6 -0
- package/src/dream/schema.ts +17 -0
- package/src/embeddings/openai-provider.ts +158 -0
- package/src/embeddings/pipeline.ts +126 -0
- package/src/embeddings/types.ts +67 -0
- package/src/engine/bin/soleri-engine.ts +4 -1
- package/src/engine/module-manifest.test.ts +4 -4
- package/src/engine/module-manifest.ts +20 -0
- package/src/engine/register-engine.ts +12 -0
- package/src/flows/dispatch-registry.ts +44 -1
- package/src/flows/executor.ts +93 -2
- package/src/flows/index.ts +2 -0
- package/src/flows/types.ts +39 -1
- package/src/index.ts +12 -0
- package/src/persona/defaults.test.ts +39 -1
- package/src/persona/defaults.ts +65 -0
- package/src/planning/goal-ancestry.test.ts +3 -5
- package/src/planning/planner.test.ts +2 -3
- package/src/runtime/admin-ops.test.ts +2 -2
- package/src/runtime/admin-ops.ts +17 -0
- package/src/runtime/admin-setup-ops.ts +2 -2
- package/src/runtime/embedding-ops.ts +116 -0
- package/src/runtime/facades/admin-facade.test.ts +31 -0
- package/src/runtime/facades/embedding-facade.ts +11 -0
- package/src/runtime/facades/index.ts +12 -0
- package/src/runtime/facades/orchestrate-facade.test.ts +16 -0
- package/src/runtime/facades/orchestrate-facade.ts +146 -0
- package/src/runtime/feature-flags.ts +4 -0
- package/src/runtime/orchestrate-ops.test.ts +131 -0
- package/src/runtime/orchestrate-ops.ts +158 -10
- package/src/runtime/planning-extra-ops.ts +77 -0
- package/src/runtime/preflight.ts +53 -0
- package/src/runtime/runtime.ts +41 -2
- package/src/runtime/types.ts +20 -0
- package/src/skills/__tests__/sync-skills.test.ts +132 -0
- package/src/skills/step-tracker.ts +162 -0
- package/src/skills/sync-skills.ts +54 -9
- package/src/subagent/dispatcher.ts +62 -39
- package/src/subagent/index.ts +1 -0
- package/src/subagent/orphan-reaper.test.ts +135 -0
- package/src/subagent/orphan-reaper.ts +130 -7
- package/src/subagent/types.ts +10 -0
- package/src/subagent/workspace-resolver.ts +3 -1
- package/src/vault/vault-entries.ts +112 -0
- package/src/vault/vault-manager.ts +1 -0
- package/src/vault/vault-scaling.test.ts +3 -2
- package/src/vault/vault-schema.ts +15 -0
- package/src/vault/vault.ts +1 -0
- package/vitest.config.ts +2 -1
- package/dist/brain/strength-scorer.d.ts +0 -31
- package/dist/brain/strength-scorer.d.ts.map +0 -1
- package/dist/brain/strength-scorer.js +0 -264
- package/dist/brain/strength-scorer.js.map +0 -1
- package/dist/engine/index.d.ts +0 -21
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -18
- package/dist/engine/index.js.map +0 -1
- package/dist/hooks/index.d.ts +0 -2
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js +0 -2
- package/dist/hooks/index.js.map +0 -1
- package/dist/persona/index.d.ts +0 -5
- package/dist/persona/index.d.ts.map +0 -1
- package/dist/persona/index.js +0 -4
- package/dist/persona/index.js.map +0 -1
- package/dist/vault/vault-interfaces.d.ts +0 -153
- package/dist/vault/vault-interfaces.d.ts.map +0 -1
- package/dist/vault/vault-interfaces.js +0 -2
- 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/
|
|
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 {
|
|
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/
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
|
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():
|
|
118
|
-
|
|
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(() =>
|
|
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
|
package/src/subagent/index.ts
CHANGED
|
@@ -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
|