@soleri/core 9.6.0 → 9.7.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/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/packs/index.d.ts +1 -1
- package/dist/packs/index.d.ts.map +1 -1
- package/dist/packs/index.js.map +1 -1
- package/dist/packs/types.d.ts +69 -42
- package/dist/packs/types.d.ts.map +1 -1
- package/dist/packs/types.js.map +1 -1
- package/dist/planning/github-projection.d.ts +3 -1
- package/dist/planning/github-projection.d.ts.map +1 -1
- package/dist/planning/github-projection.js +5 -1
- package/dist/planning/github-projection.js.map +1 -1
- package/dist/planning/goal-ancestry.d.ts +72 -0
- package/dist/planning/goal-ancestry.d.ts.map +1 -0
- package/dist/planning/goal-ancestry.js +137 -0
- package/dist/planning/goal-ancestry.js.map +1 -0
- package/dist/planning/plan-lifecycle.d.ts +2 -0
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +1 -0
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/planning/planner-types.d.ts +2 -0
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/runtime/context-health.d.ts +14 -1
- package/dist/runtime/context-health.d.ts.map +1 -1
- package/dist/runtime/context-health.js +30 -2
- package/dist/runtime/context-health.js.map +1 -1
- package/dist/session/compaction-evaluator.d.ts +20 -0
- package/dist/session/compaction-evaluator.d.ts.map +1 -0
- package/dist/session/compaction-evaluator.js +73 -0
- package/dist/session/compaction-evaluator.js.map +1 -0
- package/dist/session/compaction-policy.d.ts +50 -0
- package/dist/session/compaction-policy.d.ts.map +1 -0
- package/dist/session/compaction-policy.js +17 -0
- package/dist/session/compaction-policy.js.map +1 -0
- package/dist/session/handoff-renderer.d.ts +22 -0
- package/dist/session/handoff-renderer.d.ts.map +1 -0
- package/dist/session/handoff-renderer.js +49 -0
- package/dist/session/handoff-renderer.js.map +1 -0
- package/dist/session/index.d.ts +6 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +5 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/policy-resolver.d.ts +20 -0
- package/dist/session/policy-resolver.d.ts.map +1 -0
- package/dist/session/policy-resolver.js +28 -0
- package/dist/session/policy-resolver.js.map +1 -0
- package/dist/skills/sync-skills.d.ts +27 -0
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +92 -1
- package/dist/skills/sync-skills.js.map +1 -1
- package/dist/skills/trust-classifier.d.ts +32 -0
- package/dist/skills/trust-classifier.d.ts.map +1 -0
- package/dist/skills/trust-classifier.js +109 -0
- package/dist/skills/trust-classifier.js.map +1 -0
- package/dist/subagent/dispatcher.d.ts +4 -0
- package/dist/subagent/dispatcher.d.ts.map +1 -1
- package/dist/subagent/dispatcher.js +14 -2
- package/dist/subagent/dispatcher.js.map +1 -1
- package/package.json +1 -4
- package/src/index.ts +40 -0
- package/src/packs/index.ts +4 -0
- package/src/packs/types.ts +32 -0
- package/src/planning/github-projection.ts +6 -0
- package/src/planning/goal-ancestry.test.ts +427 -0
- package/src/planning/goal-ancestry.ts +187 -0
- package/src/planning/plan-lifecycle.ts +3 -0
- package/src/planning/planner-types.ts +2 -0
- package/src/runtime/context-health.ts +42 -2
- package/src/session/compaction-evaluator.ts +87 -0
- package/src/session/compaction-policy.ts +66 -0
- package/src/session/compaction.test.ts +259 -0
- package/src/session/handoff-renderer.ts +56 -0
- package/src/session/index.ts +12 -0
- package/src/session/policy-resolver.ts +34 -0
- package/src/skills/sync-skills.ts +114 -1
- package/src/skills/trust-classifier.test.ts +252 -0
- package/src/skills/trust-classifier.ts +127 -0
- package/src/subagent/dispatcher.ts +18 -2
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { classifyTrust } from './trust-classifier.js';
|
|
6
|
+
import { classifySkills, checkSkillCompatibility, ApprovalRequiredError } from './sync-skills.js';
|
|
7
|
+
import type { SkillEntry } from './sync-skills.js';
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// HELPERS
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
let testDir: string;
|
|
14
|
+
|
|
15
|
+
function setup(): string {
|
|
16
|
+
testDir = join(tmpdir(), `soleri-trust-test-${Date.now()}`);
|
|
17
|
+
mkdirSync(testDir, { recursive: true });
|
|
18
|
+
return testDir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createSkillDir(parentDir: string, name: string, files: Record<string, string>): string {
|
|
22
|
+
const dir = join(parentDir, name);
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
25
|
+
const fullPath = join(dir, filePath);
|
|
26
|
+
mkdirSync(join(fullPath, '..'), { recursive: true });
|
|
27
|
+
writeFileSync(fullPath, content);
|
|
28
|
+
}
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// TrustClassifier
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
describe('TrustClassifier', () => {
|
|
37
|
+
beforeEach(() => setup());
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
if (testDir) rmSync(testDir, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('classifies markdown-only directory', () => {
|
|
43
|
+
const dir = createSkillDir(testDir, 'md-skill', {
|
|
44
|
+
'SKILL.md': '---\nname: test\n---\n# Test Skill',
|
|
45
|
+
'reference.md': '# Reference doc',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const result = classifyTrust(dir);
|
|
49
|
+
|
|
50
|
+
expect(result.trust).toBe('markdown_only');
|
|
51
|
+
expect(result.inventory).toHaveLength(2);
|
|
52
|
+
expect(result.inventory.find((i) => i.path === 'SKILL.md')?.kind).toBe('skill');
|
|
53
|
+
expect(result.inventory.find((i) => i.path === 'reference.md')?.kind).toBe('reference');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('classifies directory with assets', () => {
|
|
57
|
+
const dir = createSkillDir(testDir, 'asset-skill', {
|
|
58
|
+
'SKILL.md': '# Skill',
|
|
59
|
+
'logo.png': 'fake-png-data',
|
|
60
|
+
'config.json': '{}',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const result = classifyTrust(dir);
|
|
64
|
+
|
|
65
|
+
expect(result.trust).toBe('assets');
|
|
66
|
+
expect(result.inventory.find((i) => i.path === 'logo.png')?.kind).toBe('asset');
|
|
67
|
+
expect(result.inventory.find((i) => i.path === 'config.json')?.kind).toBe('asset');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('classifies directory with scripts', () => {
|
|
71
|
+
const dir = createSkillDir(testDir, 'script-skill', {
|
|
72
|
+
'SKILL.md': '# Skill',
|
|
73
|
+
'setup.sh': '#!/bin/bash\necho hi',
|
|
74
|
+
'helper.ts': 'export const x = 1;',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const result = classifyTrust(dir);
|
|
78
|
+
|
|
79
|
+
expect(result.trust).toBe('scripts');
|
|
80
|
+
expect(result.inventory.filter((i) => i.kind === 'script')).toHaveLength(2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('treats .d.ts files as reference, not scripts', () => {
|
|
84
|
+
const dir = createSkillDir(testDir, 'decl-skill', {
|
|
85
|
+
'SKILL.md': '# Skill',
|
|
86
|
+
'types.d.ts': 'export type Foo = string;',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = classifyTrust(dir);
|
|
90
|
+
|
|
91
|
+
expect(result.trust).toBe('markdown_only');
|
|
92
|
+
expect(result.inventory.find((i) => i.path === 'types.d.ts')?.kind).toBe('reference');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('returns markdown_only for empty directory', () => {
|
|
96
|
+
const dir = join(testDir, 'empty-skill');
|
|
97
|
+
mkdirSync(dir, { recursive: true });
|
|
98
|
+
|
|
99
|
+
const result = classifyTrust(dir);
|
|
100
|
+
|
|
101
|
+
expect(result.trust).toBe('markdown_only');
|
|
102
|
+
expect(result.inventory).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('returns markdown_only for non-existent directory', () => {
|
|
106
|
+
const result = classifyTrust(join(testDir, 'nonexistent'));
|
|
107
|
+
|
|
108
|
+
expect(result.trust).toBe('markdown_only');
|
|
109
|
+
expect(result.inventory).toHaveLength(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('handles nested directories', () => {
|
|
113
|
+
const dir = createSkillDir(testDir, 'nested-skill', {
|
|
114
|
+
'SKILL.md': '# Skill',
|
|
115
|
+
'sub/helper.js': 'module.exports = {};',
|
|
116
|
+
'sub/deep/readme.md': '# Deep',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const result = classifyTrust(dir);
|
|
120
|
+
|
|
121
|
+
expect(result.trust).toBe('scripts');
|
|
122
|
+
expect(result.inventory).toHaveLength(3);
|
|
123
|
+
expect(result.inventory.find((i) => i.path === 'sub/helper.js')?.kind).toBe('script');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('skips hidden directories', () => {
|
|
127
|
+
const dir = createSkillDir(testDir, 'hidden-skill', {
|
|
128
|
+
'SKILL.md': '# Skill',
|
|
129
|
+
'.git/config': 'gitconfig',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const result = classifyTrust(dir);
|
|
133
|
+
|
|
134
|
+
expect(result.inventory.some((i) => i.path.includes('.git'))).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// =============================================================================
|
|
139
|
+
// checkSkillCompatibility
|
|
140
|
+
// =============================================================================
|
|
141
|
+
|
|
142
|
+
describe('checkSkillCompatibility', () => {
|
|
143
|
+
it('returns unknown when no engine version specified', () => {
|
|
144
|
+
expect(checkSkillCompatibility(undefined, '9.6.0')).toBe('unknown');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('returns unknown when no current version available', () => {
|
|
148
|
+
expect(checkSkillCompatibility('>=9.0.0', undefined)).toBe('unknown');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('returns compatible for matching version', () => {
|
|
152
|
+
expect(checkSkillCompatibility('>=9.0.0', '9.6.0')).toBe('compatible');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('returns invalid for incompatible version', () => {
|
|
156
|
+
expect(checkSkillCompatibility('>=10.0.0', '9.6.0')).toBe('invalid');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('returns compatible for caret range', () => {
|
|
160
|
+
expect(checkSkillCompatibility('^9.0.0', '9.6.0')).toBe('compatible');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('returns invalid for caret range with major mismatch', () => {
|
|
164
|
+
expect(checkSkillCompatibility('^10.0.0', '9.6.0')).toBe('invalid');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// classifySkills (integration with approval gate)
|
|
170
|
+
// =============================================================================
|
|
171
|
+
|
|
172
|
+
describe('classifySkills', () => {
|
|
173
|
+
beforeEach(() => setup());
|
|
174
|
+
afterEach(() => {
|
|
175
|
+
if (testDir) rmSync(testDir, { recursive: true, force: true });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('classifies markdown-only skills without error', () => {
|
|
179
|
+
createSkillDir(testDir, 'safe-skill', {
|
|
180
|
+
'SKILL.md': '---\nname: safe\n---\n# Safe Skill',
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const skills: SkillEntry[] = [
|
|
184
|
+
{ name: 'safe-skill', sourcePath: join(testDir, 'safe-skill', 'SKILL.md') },
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
const result = classifySkills(skills);
|
|
188
|
+
|
|
189
|
+
expect(result).toHaveLength(1);
|
|
190
|
+
expect(result[0].metadata?.trust).toBe('markdown_only');
|
|
191
|
+
expect(result[0].metadata?.compatibility).toBe('unknown');
|
|
192
|
+
expect(result[0].metadata?.source.type).toBe('local');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('throws ApprovalRequiredError for scripts without approval', () => {
|
|
196
|
+
createSkillDir(testDir, 'risky-skill', {
|
|
197
|
+
'SKILL.md': '# Risky',
|
|
198
|
+
'run.sh': '#!/bin/bash\nrm -rf /',
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const skills: SkillEntry[] = [
|
|
202
|
+
{ name: 'risky-skill', sourcePath: join(testDir, 'risky-skill', 'SKILL.md') },
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
expect(() => classifySkills(skills)).toThrow(ApprovalRequiredError);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('allows scripts when explicitly approved', () => {
|
|
209
|
+
createSkillDir(testDir, 'approved-skill', {
|
|
210
|
+
'SKILL.md': '# Approved',
|
|
211
|
+
'setup.sh': '#!/bin/bash\necho ok',
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const skills: SkillEntry[] = [
|
|
215
|
+
{ name: 'approved-skill', sourcePath: join(testDir, 'approved-skill', 'SKILL.md') },
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
const result = classifySkills(skills, {
|
|
219
|
+
approvedScripts: new Set(['approved-skill']),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(result).toHaveLength(1);
|
|
223
|
+
expect(result[0].metadata?.trust).toBe('scripts');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('reads engine version from SKILL.md frontmatter', () => {
|
|
227
|
+
createSkillDir(testDir, 'versioned-skill', {
|
|
228
|
+
'SKILL.md': '---\nname: versioned\nengineVersion: ">=9.0.0"\n---\n# Versioned',
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const skills: SkillEntry[] = [
|
|
232
|
+
{ name: 'versioned-skill', sourcePath: join(testDir, 'versioned-skill', 'SKILL.md') },
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
const result = classifySkills(skills, { currentEngineVersion: '9.6.0' });
|
|
236
|
+
|
|
237
|
+
expect(result[0].metadata?.engineVersion).toBe('>=9.0.0');
|
|
238
|
+
expect(result[0].metadata?.compatibility).toBe('compatible');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('detects npm source type from node_modules path', () => {
|
|
242
|
+
const npmDir = join(testDir, 'node_modules', '@soleri', 'pack-test');
|
|
243
|
+
mkdirSync(npmDir, { recursive: true });
|
|
244
|
+
writeFileSync(join(npmDir, 'SKILL.md'), '---\nname: npm-skill\n---\n# NPM Skill');
|
|
245
|
+
|
|
246
|
+
const skills: SkillEntry[] = [{ name: 'npm-skill', sourcePath: join(npmDir, 'SKILL.md') }];
|
|
247
|
+
|
|
248
|
+
const result = classifySkills(skills);
|
|
249
|
+
|
|
250
|
+
expect(result[0].metadata?.source.type).toBe('npm');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust Classifier — scans a skill directory and determines its trust level.
|
|
3
|
+
*
|
|
4
|
+
* Classification rules:
|
|
5
|
+
* - `.sh`, `.ts`, `.js` files (non-declaration) -> `scripts`
|
|
6
|
+
* - Non-`.md` files (images, JSON, etc.) -> `assets`
|
|
7
|
+
* - `.md` files only -> `markdown_only`
|
|
8
|
+
*
|
|
9
|
+
* Also builds a full inventory of all files with their classified kind.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
13
|
+
import { join, extname, relative } from 'node:path';
|
|
14
|
+
import type { TrustLevel, SkillInventoryItem } from '../packs/types.js';
|
|
15
|
+
|
|
16
|
+
/** File extensions that indicate executable scripts */
|
|
17
|
+
const SCRIPT_EXTENSIONS = new Set(['.sh', '.ts', '.js', '.mjs', '.cjs', '.py', '.rb', '.bash']);
|
|
18
|
+
|
|
19
|
+
/** File extensions considered markdown/documentation */
|
|
20
|
+
const MARKDOWN_EXTENSIONS = new Set(['.md', '.mdx']);
|
|
21
|
+
|
|
22
|
+
/** File extensions for TypeScript declaration files (not executable) */
|
|
23
|
+
const DECLARATION_PATTERN = /\.d\.[mc]?ts$/;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Classify a skill directory and return its trust level and inventory.
|
|
27
|
+
*
|
|
28
|
+
* @param dirPath - Absolute path to the skill directory
|
|
29
|
+
* @returns Trust level and full file inventory
|
|
30
|
+
*/
|
|
31
|
+
export function classifyTrust(dirPath: string): {
|
|
32
|
+
trust: TrustLevel;
|
|
33
|
+
inventory: SkillInventoryItem[];
|
|
34
|
+
} {
|
|
35
|
+
if (!existsSync(dirPath)) {
|
|
36
|
+
return { trust: 'markdown_only', inventory: [] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const inventory: SkillInventoryItem[] = [];
|
|
40
|
+
walkDir(dirPath, dirPath, inventory);
|
|
41
|
+
|
|
42
|
+
// Determine trust level from inventory
|
|
43
|
+
const hasScripts = inventory.some((item) => item.kind === 'script');
|
|
44
|
+
const hasAssets = inventory.some((item) => item.kind === 'asset');
|
|
45
|
+
|
|
46
|
+
let trust: TrustLevel;
|
|
47
|
+
if (hasScripts) {
|
|
48
|
+
trust = 'scripts';
|
|
49
|
+
} else if (hasAssets) {
|
|
50
|
+
trust = 'assets';
|
|
51
|
+
} else {
|
|
52
|
+
trust = 'markdown_only';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { trust, inventory };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Namespace object for backward compatibility and namespaced access.
|
|
60
|
+
* Delegates to standalone `classifyTrust` function.
|
|
61
|
+
*/
|
|
62
|
+
export const TrustClassifier = {
|
|
63
|
+
classify(dirPath: string): Promise<{ trust: TrustLevel; inventory: SkillInventoryItem[] }> {
|
|
64
|
+
return Promise.resolve(classifyTrust(dirPath));
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Recursively walk a directory and classify all files */
|
|
69
|
+
function walkDir(rootDir: string, currentDir: string, inventory: SkillInventoryItem[]): void {
|
|
70
|
+
let names: string[];
|
|
71
|
+
try {
|
|
72
|
+
names = readdirSync(currentDir);
|
|
73
|
+
} catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const name of names) {
|
|
78
|
+
const fullPath = join(currentDir, name);
|
|
79
|
+
let stat;
|
|
80
|
+
try {
|
|
81
|
+
stat = statSync(fullPath);
|
|
82
|
+
} catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (stat.isDirectory()) {
|
|
87
|
+
// Skip hidden directories and node_modules
|
|
88
|
+
if (name.startsWith('.') || name === 'node_modules') continue;
|
|
89
|
+
walkDir(rootDir, fullPath, inventory);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!stat.isFile()) continue;
|
|
94
|
+
|
|
95
|
+
const relPath = relative(rootDir, fullPath);
|
|
96
|
+
const ext = extname(name).toLowerCase();
|
|
97
|
+
const kind = classifyFile(name, ext);
|
|
98
|
+
|
|
99
|
+
inventory.push({ path: relPath, kind });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Classify a single file by its extension and name */
|
|
104
|
+
function classifyFile(fileName: string, ext: string): SkillInventoryItem['kind'] {
|
|
105
|
+
// SKILL.md is the primary skill definition
|
|
106
|
+
if (fileName === 'SKILL.md' || fileName === 'skill.md') {
|
|
107
|
+
return 'skill';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Declaration files are not executable
|
|
111
|
+
if (DECLARATION_PATTERN.test(fileName)) {
|
|
112
|
+
return 'reference';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Script files
|
|
116
|
+
if (SCRIPT_EXTENSIONS.has(ext)) {
|
|
117
|
+
return 'script';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Markdown files are references
|
|
121
|
+
if (MARKDOWN_EXTENSIONS.has(ext)) {
|
|
122
|
+
return 'reference';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Everything else is an asset
|
|
126
|
+
return 'asset';
|
|
127
|
+
}
|
|
@@ -19,6 +19,8 @@ import { WorkspaceResolver } from './workspace-resolver.js';
|
|
|
19
19
|
import { ConcurrencyManager } from './concurrency-manager.js';
|
|
20
20
|
import { OrphanReaper } from './orphan-reaper.js';
|
|
21
21
|
import { aggregate } from './result-aggregator.js';
|
|
22
|
+
import type { GoalRepository } from '../planning/goal-ancestry.js';
|
|
23
|
+
import { GoalAncestry } from '../planning/goal-ancestry.js';
|
|
22
24
|
|
|
23
25
|
const DEFAULT_TIMEOUT = 300_000; // 5 minutes
|
|
24
26
|
const DEFAULT_MAX_CONCURRENT = 3;
|
|
@@ -28,6 +30,8 @@ export interface SubagentDispatcherConfig {
|
|
|
28
30
|
adapterRegistry: RuntimeAdapterRegistry;
|
|
29
31
|
/** Base directory for git worktree isolation */
|
|
30
32
|
baseDir?: string;
|
|
33
|
+
/** Optional goal repository for injecting goal ancestry context */
|
|
34
|
+
goalRepository?: GoalRepository;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export class SubagentDispatcher {
|
|
@@ -36,9 +40,13 @@ export class SubagentDispatcher {
|
|
|
36
40
|
private readonly concurrency = new ConcurrencyManager();
|
|
37
41
|
private readonly reaper: OrphanReaper;
|
|
38
42
|
private readonly adapterRegistry: RuntimeAdapterRegistry;
|
|
43
|
+
private readonly goalAncestry?: GoalAncestry;
|
|
39
44
|
|
|
40
45
|
constructor(config: SubagentDispatcherConfig) {
|
|
41
46
|
this.adapterRegistry = config.adapterRegistry;
|
|
47
|
+
if (config.goalRepository) {
|
|
48
|
+
this.goalAncestry = new GoalAncestry(config.goalRepository);
|
|
49
|
+
}
|
|
42
50
|
this.workspace = new WorkspaceResolver(config.baseDir ?? process.cwd());
|
|
43
51
|
this.reaper = new OrphanReaper((taskId) => {
|
|
44
52
|
// On orphan: release the task claim and clean up workspace
|
|
@@ -245,13 +253,21 @@ export class SubagentDispatcher {
|
|
|
245
253
|
};
|
|
246
254
|
}
|
|
247
255
|
|
|
248
|
-
// 4.
|
|
256
|
+
// 4. Inject goal ancestry context if available
|
|
257
|
+
let enrichedConfig: Record<string, unknown> = { ...task.config, timeout };
|
|
258
|
+
const goalId = task.config?.goalId as string | undefined;
|
|
259
|
+
if (goalId && this.goalAncestry) {
|
|
260
|
+
enrichedConfig =
|
|
261
|
+
this.goalAncestry.inject({ config: enrichedConfig }, goalId).config ?? enrichedConfig;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 5. Execute with timeout
|
|
249
265
|
try {
|
|
250
266
|
const resultPromise = adapter.execute({
|
|
251
267
|
runId: `subagent-${task.taskId}-${Date.now()}`,
|
|
252
268
|
prompt: task.prompt,
|
|
253
269
|
workspace,
|
|
254
|
-
config:
|
|
270
|
+
config: enrichedConfig,
|
|
255
271
|
});
|
|
256
272
|
|
|
257
273
|
const timeoutPromise = new Promise<never>((_, reject) => {
|