@soleri/core 9.7.2 → 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.
Files changed (91) hide show
  1. package/dist/brain/intelligence.d.ts.map +1 -1
  2. package/dist/brain/intelligence.js +11 -2
  3. package/dist/brain/intelligence.js.map +1 -1
  4. package/dist/brain/types.d.ts +1 -0
  5. package/dist/brain/types.d.ts.map +1 -1
  6. package/dist/enforcement/adapters/index.d.ts +15 -0
  7. package/dist/enforcement/adapters/index.d.ts.map +1 -1
  8. package/dist/enforcement/adapters/index.js +38 -0
  9. package/dist/enforcement/adapters/index.js.map +1 -1
  10. package/dist/enforcement/adapters/opencode.d.ts +21 -0
  11. package/dist/enforcement/adapters/opencode.d.ts.map +1 -0
  12. package/dist/enforcement/adapters/opencode.js +115 -0
  13. package/dist/enforcement/adapters/opencode.js.map +1 -0
  14. package/dist/index.d.ts +4 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +5 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/paths.d.ts +2 -0
  19. package/dist/paths.d.ts.map +1 -1
  20. package/dist/paths.js +4 -0
  21. package/dist/paths.js.map +1 -1
  22. package/dist/planning/evidence-collector.d.ts +2 -0
  23. package/dist/planning/evidence-collector.d.ts.map +1 -1
  24. package/dist/planning/evidence-collector.js +7 -2
  25. package/dist/planning/evidence-collector.js.map +1 -1
  26. package/dist/planning/gap-patterns.d.ts.map +1 -1
  27. package/dist/planning/gap-patterns.js +4 -1
  28. package/dist/planning/gap-patterns.js.map +1 -1
  29. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  30. package/dist/planning/plan-lifecycle.js +5 -0
  31. package/dist/planning/plan-lifecycle.js.map +1 -1
  32. package/dist/planning/planner-types.d.ts +2 -0
  33. package/dist/planning/planner-types.d.ts.map +1 -1
  34. package/dist/runtime/capture-ops.d.ts.map +1 -1
  35. package/dist/runtime/capture-ops.js +14 -6
  36. package/dist/runtime/capture-ops.js.map +1 -1
  37. package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
  38. package/dist/runtime/facades/curator-facade.js +52 -4
  39. package/dist/runtime/facades/curator-facade.js.map +1 -1
  40. package/dist/runtime/orchestrate-ops.d.ts +12 -0
  41. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  42. package/dist/runtime/orchestrate-ops.js +141 -1
  43. package/dist/runtime/orchestrate-ops.js.map +1 -1
  44. package/dist/runtime/quality-signals.d.ts +42 -0
  45. package/dist/runtime/quality-signals.d.ts.map +1 -0
  46. package/dist/runtime/quality-signals.js +124 -0
  47. package/dist/runtime/quality-signals.js.map +1 -0
  48. package/dist/skills/trust-classifier.js +1 -1
  49. package/dist/skills/trust-classifier.js.map +1 -1
  50. package/dist/vault/vault-markdown-sync.d.ts +5 -2
  51. package/dist/vault/vault-markdown-sync.d.ts.map +1 -1
  52. package/dist/vault/vault-markdown-sync.js +13 -2
  53. package/dist/vault/vault-markdown-sync.js.map +1 -1
  54. package/dist/workflows/index.d.ts +6 -0
  55. package/dist/workflows/index.d.ts.map +1 -0
  56. package/dist/workflows/index.js +5 -0
  57. package/dist/workflows/index.js.map +1 -0
  58. package/dist/workflows/workflow-loader.d.ts +83 -0
  59. package/dist/workflows/workflow-loader.d.ts.map +1 -0
  60. package/dist/workflows/workflow-loader.js +207 -0
  61. package/dist/workflows/workflow-loader.js.map +1 -0
  62. package/package.json +1 -1
  63. package/src/brain/intelligence.ts +15 -2
  64. package/src/brain/types.ts +1 -0
  65. package/src/enforcement/adapters/index.ts +45 -0
  66. package/src/enforcement/adapters/opencode.test.ts +406 -0
  67. package/src/enforcement/adapters/opencode.ts +153 -0
  68. package/src/index.ts +19 -0
  69. package/src/paths.ts +5 -0
  70. package/src/planning/evidence-collector.test.ts +95 -0
  71. package/src/planning/evidence-collector.ts +11 -0
  72. package/src/planning/gap-patterns.ts +7 -3
  73. package/src/planning/plan-lifecycle.test.ts +49 -0
  74. package/src/planning/plan-lifecycle.ts +5 -0
  75. package/src/planning/planner-types.ts +2 -0
  76. package/src/runtime/capture-ops.test.ts +58 -1
  77. package/src/runtime/capture-ops.ts +15 -4
  78. package/src/runtime/facades/curator-facade.test.ts +87 -9
  79. package/src/runtime/facades/curator-facade.ts +60 -4
  80. package/src/runtime/orchestrate-ops.test.ts +78 -1
  81. package/src/runtime/orchestrate-ops.ts +175 -1
  82. package/src/runtime/orchestrate-status-readiness.test.ts +162 -0
  83. package/src/runtime/quality-signals.test.ts +312 -0
  84. package/src/runtime/quality-signals.ts +169 -0
  85. package/src/skills/trust-classifier.ts +1 -1
  86. package/src/vault/vault-markdown-sync.test.ts +40 -0
  87. package/src/vault/vault-markdown-sync.ts +16 -3
  88. package/src/workflows/index.ts +12 -0
  89. package/src/workflows/orchestrate-integration.test.ts +166 -0
  90. package/src/workflows/workflow-loader.test.ts +149 -0
  91. package/src/workflows/workflow-loader.ts +238 -0
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { applyWorkflowOverride } from '../runtime/orchestrate-ops.js';
3
+ import type { OrchestrationPlan, PlanStep } from '../flows/types.js';
4
+ import type { WorkflowOverride } from './workflow-loader.js';
5
+
6
+ function makePlan(steps: PlanStep[]): OrchestrationPlan {
7
+ return {
8
+ planId: 'test-plan-1',
9
+ intent: 'BUILD',
10
+ flowId: 'BUILD-flow',
11
+ steps,
12
+ skipped: [],
13
+ epilogue: ['capture_knowledge'],
14
+ warnings: [],
15
+ summary: 'Test plan',
16
+ estimatedTools: steps.reduce((acc, s) => acc + s.tools.length, 0),
17
+ context: {
18
+ intent: 'BUILD',
19
+ probes: {
20
+ vault: true,
21
+ brain: false,
22
+ designSystem: false,
23
+ sessionStore: true,
24
+ projectRules: false,
25
+ active: true,
26
+ },
27
+ entities: { components: [], actions: [] },
28
+ projectPath: '.',
29
+ },
30
+ };
31
+ }
32
+
33
+ function makeStep(id: string, tools: string[] = []): PlanStep {
34
+ return {
35
+ id,
36
+ name: id,
37
+ tools,
38
+ parallel: false,
39
+ requires: [],
40
+ status: 'pending',
41
+ };
42
+ }
43
+
44
+ describe('applyWorkflowOverride', () => {
45
+ it('merges gates into matching plan steps', () => {
46
+ const plan = makePlan([
47
+ makeStep('pre-execution-vault-search', ['vault_search']),
48
+ makeStep('completion-capture', ['capture_knowledge']),
49
+ ]);
50
+
51
+ const override: WorkflowOverride = {
52
+ name: 'feature-dev',
53
+ gates: [
54
+ {
55
+ phase: 'pre-execution',
56
+ requirement: 'Plan approved by user',
57
+ check: 'plan-approved',
58
+ },
59
+ {
60
+ phase: 'completion',
61
+ requirement: 'Knowledge captured',
62
+ check: 'knowledge-captured',
63
+ },
64
+ ],
65
+ tools: [],
66
+ };
67
+
68
+ applyWorkflowOverride(plan, override);
69
+
70
+ // Gates should be attached to matching steps
71
+ expect(plan.steps[0].gate).toBeDefined();
72
+ expect(plan.steps[0].gate!.type).toBe('GATE');
73
+ expect(plan.steps[0].gate!.condition).toBe('Plan approved by user');
74
+
75
+ expect(plan.steps[1].gate).toBeDefined();
76
+ expect(plan.steps[1].gate!.condition).toBe('Knowledge captured');
77
+ });
78
+
79
+ it('appends unmatched gates as new steps', () => {
80
+ const plan = makePlan([makeStep('vault-search', ['vault_search'])]);
81
+
82
+ const override: WorkflowOverride = {
83
+ name: 'bug-fix',
84
+ gates: [
85
+ {
86
+ phase: 'post-task',
87
+ requirement: 'All tests pass',
88
+ check: 'tests-pass',
89
+ },
90
+ ],
91
+ tools: [],
92
+ };
93
+
94
+ applyWorkflowOverride(plan, override);
95
+
96
+ // Original step untouched
97
+ expect(plan.steps[0].gate).toBeUndefined();
98
+
99
+ // New gate step appended
100
+ expect(plan.steps).toHaveLength(2);
101
+ expect(plan.steps[1].id).toBe('workflow-gate-post-task');
102
+ expect(plan.steps[1].gate!.condition).toBe('All tests pass');
103
+ });
104
+
105
+ it('merges tools into all plan steps (deduplicated)', () => {
106
+ const plan = makePlan([makeStep('step1', ['existing_tool']), makeStep('step2', [])]);
107
+
108
+ const override: WorkflowOverride = {
109
+ name: 'feature-dev',
110
+ gates: [],
111
+ tools: ['soleri_vault op:search_intelligent', 'existing_tool'],
112
+ };
113
+
114
+ applyWorkflowOverride(plan, override);
115
+
116
+ // step1 already had existing_tool — should not duplicate
117
+ expect(plan.steps[0].tools).toEqual(['existing_tool', 'soleri_vault op:search_intelligent']);
118
+ // step2 gets the tools
119
+ expect(plan.steps[1].tools).toEqual(['soleri_vault op:search_intelligent', 'existing_tool']);
120
+ // estimatedTools updated
121
+ expect(plan.estimatedTools).toBe(plan.steps.reduce((acc, s) => acc + s.tools.length, 0));
122
+ });
123
+
124
+ it('does nothing when override has no gates or tools', () => {
125
+ const plan = makePlan([makeStep('step1', ['t1'])]);
126
+ const originalSteps = plan.steps.length;
127
+ const originalTools = plan.steps[0].tools.length;
128
+
129
+ const override: WorkflowOverride = {
130
+ name: 'empty',
131
+ gates: [],
132
+ tools: [],
133
+ };
134
+
135
+ applyWorkflowOverride(plan, override);
136
+
137
+ expect(plan.steps).toHaveLength(originalSteps);
138
+ expect(plan.steps[0].tools).toHaveLength(originalTools);
139
+ // Warning still added
140
+ expect(plan.warnings).toContain('Workflow override "empty" applied (0 gate(s), 0 tool(s)).');
141
+ });
142
+
143
+ it('plan remains unchanged when no workflow matches', () => {
144
+ // This tests the calling code path — if getWorkflowForIntent returns null,
145
+ // applyWorkflowOverride is never called
146
+ const plan = makePlan([makeStep('step1', ['t1'])]);
147
+ expect(plan.warnings).toHaveLength(0);
148
+ expect(plan.steps[0].gate).toBeUndefined();
149
+ });
150
+
151
+ it('adds info warning about applied override', () => {
152
+ const plan = makePlan([makeStep('step1')]);
153
+
154
+ const override: WorkflowOverride = {
155
+ name: 'feature-dev',
156
+ gates: [{ phase: 'pre', requirement: 'ok', check: 'go' }],
157
+ tools: ['tool1', 'tool2'],
158
+ };
159
+
160
+ applyWorkflowOverride(plan, override);
161
+
162
+ expect(plan.warnings).toContain(
163
+ 'Workflow override "feature-dev" applied (1 gate(s), 2 tool(s)).',
164
+ );
165
+ });
166
+ });
@@ -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
+ }