@soleri/core 9.8.0 → 9.9.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/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +11 -2
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/types.d.ts +1 -0
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +2 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +4 -0
- package/dist/paths.js.map +1 -1
- package/dist/planning/gap-patterns.d.ts.map +1 -1
- package/dist/planning/gap-patterns.js +4 -1
- package/dist/planning/gap-patterns.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +14 -6
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
- package/dist/runtime/facades/curator-facade.js +52 -4
- package/dist/runtime/facades/curator-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts +12 -0
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +76 -0
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/vault/vault-markdown-sync.d.ts +5 -2
- package/dist/vault/vault-markdown-sync.d.ts.map +1 -1
- package/dist/vault/vault-markdown-sync.js +13 -2
- package/dist/vault/vault-markdown-sync.js.map +1 -1
- package/dist/workflows/index.d.ts +6 -0
- package/dist/workflows/index.d.ts.map +1 -0
- package/dist/workflows/index.js +5 -0
- package/dist/workflows/index.js.map +1 -0
- package/dist/workflows/workflow-loader.d.ts +83 -0
- package/dist/workflows/workflow-loader.d.ts.map +1 -0
- package/dist/workflows/workflow-loader.js +207 -0
- package/dist/workflows/workflow-loader.js.map +1 -0
- package/package.json +1 -1
- package/src/brain/intelligence.ts +15 -2
- package/src/brain/types.ts +1 -0
- package/src/enforcement/adapters/opencode.test.ts +4 -2
- package/src/index.ts +19 -0
- package/src/paths.ts +5 -0
- package/src/planning/gap-patterns.ts +7 -3
- package/src/runtime/capture-ops.test.ts +58 -1
- package/src/runtime/capture-ops.ts +15 -4
- package/src/runtime/facades/curator-facade.test.ts +87 -9
- package/src/runtime/facades/curator-facade.ts +60 -4
- package/src/runtime/orchestrate-ops.ts +84 -0
- package/src/vault/vault-markdown-sync.test.ts +40 -0
- package/src/vault/vault-markdown-sync.ts +16 -3
- package/src/workflows/index.ts +12 -0
- package/src/workflows/orchestrate-integration.test.ts +166 -0
- package/src/workflows/workflow-loader.test.ts +149 -0
- package/src/workflows/workflow-loader.ts +238 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { loadAgentWorkflows, getWorkflowForIntent, WORKFLOW_TO_INTENT } from './workflow-loader.js';
|
|
4
|
+
import type { WorkflowOverride } from './workflow-loader.js';
|
|
5
|
+
|
|
6
|
+
vi.mock('node:fs', () => ({
|
|
7
|
+
default: {
|
|
8
|
+
readdirSync: vi.fn(),
|
|
9
|
+
statSync: vi.fn(),
|
|
10
|
+
readFileSync: vi.fn(),
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe('workflow-loader', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.resetAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('loadAgentWorkflows', () => {
|
|
20
|
+
it('returns empty map when directory does not exist', () => {
|
|
21
|
+
(fs.readdirSync as ReturnType<typeof vi.fn>).mockImplementation(() => {
|
|
22
|
+
throw new Error('ENOENT');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const result = loadAgentWorkflows('/nonexistent/workflows');
|
|
26
|
+
expect(result.size).toBe(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('loads gates and tools from workflow folder', () => {
|
|
30
|
+
(fs.readdirSync as ReturnType<typeof vi.fn>).mockReturnValue(['feature-dev']);
|
|
31
|
+
(fs.statSync as ReturnType<typeof vi.fn>).mockReturnValue({ isDirectory: () => true });
|
|
32
|
+
(fs.readFileSync as ReturnType<typeof vi.fn>).mockImplementation((filePath: string) => {
|
|
33
|
+
if (filePath.endsWith('prompt.md')) {
|
|
34
|
+
return '# Feature Dev\nBuild new features.';
|
|
35
|
+
}
|
|
36
|
+
if (filePath.endsWith('gates.yaml')) {
|
|
37
|
+
return `gates:
|
|
38
|
+
- phase: pre-execution
|
|
39
|
+
requirement: Plan approved
|
|
40
|
+
check: plan-approved
|
|
41
|
+
- phase: completion
|
|
42
|
+
requirement: Tests pass
|
|
43
|
+
check: tests-pass
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
if (filePath.endsWith('tools.yaml')) {
|
|
47
|
+
return `tools:
|
|
48
|
+
- soleri_vault op:search_intelligent
|
|
49
|
+
- soleri_plan op:create_plan
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
throw new Error('ENOENT');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const result = loadAgentWorkflows('/agent/workflows');
|
|
56
|
+
expect(result.size).toBe(1);
|
|
57
|
+
|
|
58
|
+
const workflow = result.get('feature-dev')!;
|
|
59
|
+
expect(workflow.name).toBe('feature-dev');
|
|
60
|
+
expect(workflow.prompt).toBe('# Feature Dev\nBuild new features.');
|
|
61
|
+
expect(workflow.gates).toHaveLength(2);
|
|
62
|
+
expect(workflow.gates[0]).toEqual({
|
|
63
|
+
phase: 'pre-execution',
|
|
64
|
+
requirement: 'Plan approved',
|
|
65
|
+
check: 'plan-approved',
|
|
66
|
+
});
|
|
67
|
+
expect(workflow.gates[1]).toEqual({
|
|
68
|
+
phase: 'completion',
|
|
69
|
+
requirement: 'Tests pass',
|
|
70
|
+
check: 'tests-pass',
|
|
71
|
+
});
|
|
72
|
+
expect(workflow.tools).toEqual([
|
|
73
|
+
'soleri_vault op:search_intelligent',
|
|
74
|
+
'soleri_plan op:create_plan',
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('skips non-directory entries', () => {
|
|
79
|
+
(fs.readdirSync as ReturnType<typeof vi.fn>).mockReturnValue(['README.md']);
|
|
80
|
+
(fs.statSync as ReturnType<typeof vi.fn>).mockReturnValue({ isDirectory: () => false });
|
|
81
|
+
|
|
82
|
+
const result = loadAgentWorkflows('/agent/workflows');
|
|
83
|
+
expect(result.size).toBe(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('skips workflow folders with no content', () => {
|
|
87
|
+
(fs.readdirSync as ReturnType<typeof vi.fn>).mockReturnValue(['empty-workflow']);
|
|
88
|
+
(fs.statSync as ReturnType<typeof vi.fn>).mockReturnValue({ isDirectory: () => true });
|
|
89
|
+
(fs.readFileSync as ReturnType<typeof vi.fn>).mockImplementation(() => {
|
|
90
|
+
throw new Error('ENOENT');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const result = loadAgentWorkflows('/agent/workflows');
|
|
94
|
+
expect(result.size).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('getWorkflowForIntent', () => {
|
|
99
|
+
it('returns matching workflow for BUILD intent', () => {
|
|
100
|
+
const workflows = new Map<string, WorkflowOverride>([
|
|
101
|
+
['feature-dev', { name: 'feature-dev', gates: [], tools: ['tool1'] }],
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const result = getWorkflowForIntent(workflows, 'BUILD');
|
|
105
|
+
expect(result).not.toBeNull();
|
|
106
|
+
expect(result!.name).toBe('feature-dev');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('returns null when no matching workflow', () => {
|
|
110
|
+
const workflows = new Map<string, WorkflowOverride>([
|
|
111
|
+
['feature-dev', { name: 'feature-dev', gates: [], tools: ['tool1'] }],
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
const result = getWorkflowForIntent(workflows, 'EXPLORE');
|
|
115
|
+
expect(result).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('uses custom mapping when provided', () => {
|
|
119
|
+
const workflows = new Map<string, WorkflowOverride>([
|
|
120
|
+
['my-custom', { name: 'my-custom', gates: [], tools: ['t1'] }],
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
const result = getWorkflowForIntent(workflows, 'DESIGN', {
|
|
124
|
+
'my-custom': 'DESIGN',
|
|
125
|
+
});
|
|
126
|
+
expect(result).not.toBeNull();
|
|
127
|
+
expect(result!.name).toBe('my-custom');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('is case-insensitive for intent', () => {
|
|
131
|
+
const workflows = new Map<string, WorkflowOverride>([
|
|
132
|
+
['bug-fix', { name: 'bug-fix', gates: [], tools: [] }],
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
// bug-fix maps to FIX in WORKFLOW_TO_INTENT
|
|
136
|
+
const result = getWorkflowForIntent(workflows, 'fix');
|
|
137
|
+
expect(result).not.toBeNull();
|
|
138
|
+
expect(result!.name).toBe('bug-fix');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('WORKFLOW_TO_INTENT', () => {
|
|
143
|
+
it('maps known workflow names to intents', () => {
|
|
144
|
+
expect(WORKFLOW_TO_INTENT['feature-dev']).toBe('BUILD');
|
|
145
|
+
expect(WORKFLOW_TO_INTENT['bug-fix']).toBe('FIX');
|
|
146
|
+
expect(WORKFLOW_TO_INTENT['code-review']).toBe('REVIEW');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow loader — reads agent workflow overrides from the file tree.
|
|
3
|
+
*
|
|
4
|
+
* Each workflow is a folder under `workflows/` containing:
|
|
5
|
+
* - `prompt.md` — system prompt for the workflow (optional)
|
|
6
|
+
* - `gates.yaml` — gate definitions (optional)
|
|
7
|
+
* - `tools.yaml` — tool allowlist (optional)
|
|
8
|
+
*
|
|
9
|
+
* These overrides are merged into the OrchestrationPlan when
|
|
10
|
+
* the detected intent matches a workflow via WORKFLOW_TO_INTENT.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Schemas
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export const WorkflowGateSchema = z.object({
|
|
22
|
+
phase: z.string(),
|
|
23
|
+
requirement: z.string(),
|
|
24
|
+
check: z.string(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const WorkflowOverrideSchema = z.object({
|
|
28
|
+
name: z.string(),
|
|
29
|
+
prompt: z.string().optional(),
|
|
30
|
+
gates: z.array(WorkflowGateSchema).default([]),
|
|
31
|
+
tools: z.array(z.string()).default([]),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export type WorkflowGate = z.infer<typeof WorkflowGateSchema>;
|
|
35
|
+
export type WorkflowOverride = z.infer<typeof WorkflowOverrideSchema>;
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Workflow → Intent mapping
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Maps workflow folder names to intent strings.
|
|
43
|
+
* Used by `getWorkflowForIntent()` to find a matching workflow.
|
|
44
|
+
*/
|
|
45
|
+
export const WORKFLOW_TO_INTENT: Record<string, string> = {
|
|
46
|
+
'feature-dev': 'BUILD',
|
|
47
|
+
'bug-fix': 'FIX',
|
|
48
|
+
'code-review': 'REVIEW',
|
|
49
|
+
'component-build': 'BUILD',
|
|
50
|
+
'token-migration': 'ENHANCE',
|
|
51
|
+
'a11y-remediation': 'FIX',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Loader
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Load all workflow overrides from an agent's `workflows/` directory.
|
|
60
|
+
*
|
|
61
|
+
* Returns an empty Map if the directory doesn't exist or can't be read
|
|
62
|
+
* (graceful degradation — no throw).
|
|
63
|
+
*/
|
|
64
|
+
export function loadAgentWorkflows(workflowsDir: string): Map<string, WorkflowOverride> {
|
|
65
|
+
const workflows = new Map<string, WorkflowOverride>();
|
|
66
|
+
|
|
67
|
+
let entries: string[];
|
|
68
|
+
try {
|
|
69
|
+
entries = fs.readdirSync(workflowsDir);
|
|
70
|
+
} catch {
|
|
71
|
+
// Directory doesn't exist or can't be read — that's fine
|
|
72
|
+
return workflows;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const fullPath = path.join(workflowsDir, entry);
|
|
77
|
+
let stat: fs.Stats;
|
|
78
|
+
try {
|
|
79
|
+
stat = fs.statSync(fullPath);
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!stat.isDirectory()) continue;
|
|
84
|
+
|
|
85
|
+
const override: WorkflowOverride = { name: entry, gates: [], tools: [] };
|
|
86
|
+
|
|
87
|
+
// Read prompt.md
|
|
88
|
+
const promptPath = path.join(fullPath, 'prompt.md');
|
|
89
|
+
try {
|
|
90
|
+
override.prompt = fs.readFileSync(promptPath, 'utf-8').trim();
|
|
91
|
+
} catch {
|
|
92
|
+
// No prompt — that's fine
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Read gates.yaml
|
|
96
|
+
const gatesPath = path.join(fullPath, 'gates.yaml');
|
|
97
|
+
try {
|
|
98
|
+
const raw = fs.readFileSync(gatesPath, 'utf-8');
|
|
99
|
+
// Simple YAML parsing for the gates structure
|
|
100
|
+
const gates = parseGatesYaml(raw);
|
|
101
|
+
override.gates = gates;
|
|
102
|
+
} catch {
|
|
103
|
+
// No gates — that's fine
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Read tools.yaml
|
|
107
|
+
const toolsPath = path.join(fullPath, 'tools.yaml');
|
|
108
|
+
try {
|
|
109
|
+
const raw = fs.readFileSync(toolsPath, 'utf-8');
|
|
110
|
+
const tools = parseToolsYaml(raw);
|
|
111
|
+
override.tools = tools;
|
|
112
|
+
} catch {
|
|
113
|
+
// No tools — that's fine
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Only store if we got something useful
|
|
117
|
+
if (override.prompt || override.gates.length > 0 || override.tools.length > 0) {
|
|
118
|
+
workflows.set(entry, override);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return workflows;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Intent matching
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Find a workflow override that matches the given intent.
|
|
131
|
+
*
|
|
132
|
+
* Uses WORKFLOW_TO_INTENT mapping, optionally overridden by customMapping.
|
|
133
|
+
* Returns null if no matching workflow is found.
|
|
134
|
+
*/
|
|
135
|
+
export function getWorkflowForIntent(
|
|
136
|
+
workflows: Map<string, WorkflowOverride>,
|
|
137
|
+
intent: string,
|
|
138
|
+
customMapping?: Record<string, string>,
|
|
139
|
+
): WorkflowOverride | null {
|
|
140
|
+
const mapping = customMapping ?? WORKFLOW_TO_INTENT;
|
|
141
|
+
const normalizedIntent = intent.toUpperCase();
|
|
142
|
+
|
|
143
|
+
for (const [workflowName, mappedIntent] of Object.entries(mapping)) {
|
|
144
|
+
if (mappedIntent.toUpperCase() === normalizedIntent && workflows.has(workflowName)) {
|
|
145
|
+
return workflows.get(workflowName)!;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Minimal YAML parsers (no external dependency)
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Parse a simple gates.yaml file. Expected format:
|
|
158
|
+
*
|
|
159
|
+
* ```yaml
|
|
160
|
+
* gates:
|
|
161
|
+
* - phase: brainstorming
|
|
162
|
+
* requirement: Requirements are clear
|
|
163
|
+
* check: user-approval
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
function parseGatesYaml(raw: string): WorkflowGate[] {
|
|
167
|
+
const gates: WorkflowGate[] = [];
|
|
168
|
+
const lines = raw.split('\n');
|
|
169
|
+
|
|
170
|
+
let current: Partial<WorkflowGate> | null = null;
|
|
171
|
+
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
const trimmed = line.trim();
|
|
174
|
+
|
|
175
|
+
// Skip empty lines and the root "gates:" key
|
|
176
|
+
if (!trimmed || trimmed === 'gates:') continue;
|
|
177
|
+
|
|
178
|
+
// New list item
|
|
179
|
+
if (trimmed.startsWith('- ')) {
|
|
180
|
+
if (current && current.phase && current.requirement && current.check) {
|
|
181
|
+
gates.push(current as WorkflowGate);
|
|
182
|
+
}
|
|
183
|
+
current = {};
|
|
184
|
+
// Parse inline key from "- phase: value"
|
|
185
|
+
const inlineMatch = trimmed.match(/^-\s+(\w+):\s*(.+)$/);
|
|
186
|
+
if (inlineMatch) {
|
|
187
|
+
const [, key, value] = inlineMatch;
|
|
188
|
+
if (key === 'phase' || key === 'requirement' || key === 'check') {
|
|
189
|
+
current[key] = value.trim();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Continuation key: " requirement: value"
|
|
196
|
+
if (current) {
|
|
197
|
+
const kvMatch = trimmed.match(/^(\w+):\s*(.+)$/);
|
|
198
|
+
if (kvMatch) {
|
|
199
|
+
const [, key, value] = kvMatch;
|
|
200
|
+
if (key === 'phase' || key === 'requirement' || key === 'check') {
|
|
201
|
+
current[key] = value.trim();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Flush last entry
|
|
208
|
+
if (current && current.phase && current.requirement && current.check) {
|
|
209
|
+
gates.push(current as WorkflowGate);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return gates;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Parse a simple tools.yaml file. Expected format:
|
|
217
|
+
*
|
|
218
|
+
* ```yaml
|
|
219
|
+
* tools:
|
|
220
|
+
* - soleri_vault op:search_intelligent
|
|
221
|
+
* - soleri_plan op:create_plan
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
function parseToolsYaml(raw: string): string[] {
|
|
225
|
+
const tools: string[] = [];
|
|
226
|
+
const lines = raw.split('\n');
|
|
227
|
+
|
|
228
|
+
for (const line of lines) {
|
|
229
|
+
const trimmed = line.trim();
|
|
230
|
+
if (!trimmed || trimmed === 'tools:') continue;
|
|
231
|
+
|
|
232
|
+
if (trimmed.startsWith('- ')) {
|
|
233
|
+
tools.push(trimmed.slice(2).trim());
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return tools;
|
|
238
|
+
}
|