@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,406 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { OpenCodeAdapter } from './opencode.js';
3
+ import type { EnforcementAction, EnforcementRule } from '../types.js';
4
+
5
+ // Mock node:fs so detectHost() doesn't hit real filesystem
6
+ vi.mock('node:fs', async (importOriginal) => {
7
+ const actual = await importOriginal<typeof import('node:fs')>();
8
+ return { ...actual, existsSync: vi.fn(() => false) };
9
+ });
10
+
11
+ import { existsSync } from 'node:fs';
12
+ import { detectHost, createHostAdapter } from './index.js';
13
+
14
+ const mockedExistsSync = vi.mocked(existsSync);
15
+
16
+ // ─── Helpers ──────────────────────────────────────────────────────
17
+
18
+ function makeRule(overrides: Partial<EnforcementRule> = {}): EnforcementRule {
19
+ return {
20
+ id: 'test-rule',
21
+ description: 'Test rule',
22
+ trigger: 'pre-tool-use',
23
+ action: 'block',
24
+ message: 'Blocked',
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ // ─── OpenCodeAdapter ──────────────────────────────────────────────
30
+
31
+ describe('OpenCodeAdapter', () => {
32
+ const adapter = new OpenCodeAdapter();
33
+
34
+ // ─── supports() ─────────────────────────────────────────────────
35
+
36
+ describe('supports', () => {
37
+ it('returns true for pre-tool-use', () => {
38
+ expect(adapter.supports('pre-tool-use')).toBe(true);
39
+ });
40
+
41
+ it('returns true for post-tool-use', () => {
42
+ expect(adapter.supports('post-tool-use')).toBe(true);
43
+ });
44
+
45
+ it('returns true for pre-compact', () => {
46
+ expect(adapter.supports('pre-compact')).toBe(true);
47
+ });
48
+
49
+ it('returns true for session-start', () => {
50
+ expect(adapter.supports('session-start')).toBe(true);
51
+ });
52
+
53
+ it('returns false for pre-commit', () => {
54
+ expect(adapter.supports('pre-commit')).toBe(false);
55
+ });
56
+
57
+ it('returns false for on-save', () => {
58
+ expect(adapter.supports('on-save')).toBe(false);
59
+ });
60
+ });
61
+
62
+ // ─── translate() — empty config ─────────────────────────────────
63
+
64
+ describe('translate with empty config', () => {
65
+ it('returns empty files array when no rules provided', () => {
66
+ const result = adapter.translate({ rules: [] });
67
+ expect(result.host).toBe('opencode');
68
+ expect(result.files).toHaveLength(0);
69
+ expect(result.skipped).toHaveLength(0);
70
+ });
71
+ });
72
+
73
+ // ─── translate() — config generation format ─────────────────────
74
+
75
+ describe('translate config generation', () => {
76
+ it('generates plugin file at .opencode/plugins/soleri-enforcement.ts', () => {
77
+ const result = adapter.translate({
78
+ rules: [makeRule({ id: 'r1', trigger: 'pre-tool-use', pattern: 'test' })],
79
+ });
80
+
81
+ expect(result.files).toHaveLength(1);
82
+ expect(result.files[0].path).toBe('.opencode/plugins/soleri-enforcement.ts');
83
+ });
84
+
85
+ it('includes auto-generated header comment', () => {
86
+ const result = adapter.translate({
87
+ rules: [makeRule({ id: 'r1', trigger: 'pre-tool-use', pattern: 'test' })],
88
+ });
89
+
90
+ expect(result.files[0].content).toContain('Auto-generated');
91
+ expect(result.files[0].content).toContain('do not edit manually');
92
+ });
93
+
94
+ it('exports a default object with hooks', () => {
95
+ const result = adapter.translate({
96
+ rules: [makeRule({ id: 'r1', trigger: 'pre-tool-use', pattern: 'test' })],
97
+ });
98
+
99
+ const content = result.files[0].content;
100
+ expect(content).toContain('export default {');
101
+ expect(content).toContain('hooks: {');
102
+ });
103
+
104
+ it('maps pre-tool-use to tool.execute.before event', () => {
105
+ const result = adapter.translate({
106
+ rules: [makeRule({ trigger: 'pre-tool-use', pattern: 'test' })],
107
+ });
108
+
109
+ expect(result.files[0].content).toContain("'tool.execute.before'");
110
+ });
111
+
112
+ it('maps post-tool-use to tool.execute.after event', () => {
113
+ const result = adapter.translate({
114
+ rules: [makeRule({ trigger: 'post-tool-use', pattern: 'test' })],
115
+ });
116
+
117
+ expect(result.files[0].content).toContain("'tool.execute.after'");
118
+ });
119
+
120
+ it('maps pre-compact to session.compacted event', () => {
121
+ const result = adapter.translate({
122
+ rules: [makeRule({ trigger: 'pre-compact', pattern: 'test' })],
123
+ });
124
+
125
+ expect(result.files[0].content).toContain("'session.compacted'");
126
+ });
127
+
128
+ it('maps session-start to session.created event', () => {
129
+ const result = adapter.translate({
130
+ rules: [makeRule({ trigger: 'session-start', pattern: 'test' })],
131
+ });
132
+
133
+ expect(result.files[0].content).toContain("'session.created'");
134
+ });
135
+
136
+ it('groups handlers by event when multiple rules share a trigger', () => {
137
+ const result = adapter.translate({
138
+ rules: [
139
+ makeRule({
140
+ id: 'r1',
141
+ trigger: 'pre-tool-use',
142
+ pattern: 'foo',
143
+ action: 'block',
144
+ message: 'No foo',
145
+ }),
146
+ makeRule({
147
+ id: 'r2',
148
+ trigger: 'pre-tool-use',
149
+ pattern: 'bar',
150
+ action: 'warn',
151
+ message: 'No bar',
152
+ }),
153
+ ],
154
+ });
155
+
156
+ const content = result.files[0].content;
157
+ // Should have only one 'tool.execute.before' event entry with both checks
158
+ const eventMatches = content.match(/tool\.execute\.before/g);
159
+ expect(eventMatches).toHaveLength(1);
160
+ expect(content).toContain('r1');
161
+ expect(content).toContain('r2');
162
+ });
163
+ });
164
+
165
+ // ─── translate() — block/warn/suggest actions ───────────────────
166
+
167
+ describe('action code generation', () => {
168
+ it('block action generates throw new Error', () => {
169
+ const result = adapter.translate({
170
+ rules: [
171
+ makeRule({ id: 'no-exec', action: 'block', message: 'Do not execute', pattern: 'exec' }),
172
+ ],
173
+ });
174
+
175
+ const content = result.files[0].content;
176
+ expect(content).toContain('throw new Error');
177
+ expect(content).toContain('[no-exec] BLOCKED: Do not execute');
178
+ });
179
+
180
+ it('warn action generates console.warn', () => {
181
+ const result = adapter.translate({
182
+ rules: [
183
+ makeRule({ id: 'risky', action: 'warn', message: 'Risky operation', pattern: 'risk' }),
184
+ ],
185
+ });
186
+
187
+ const content = result.files[0].content;
188
+ expect(content).toContain('console.warn');
189
+ expect(content).toContain('[risky] WARNING: Risky operation');
190
+ });
191
+
192
+ it('suggest action generates console.info', () => {
193
+ const result = adapter.translate({
194
+ rules: [
195
+ makeRule({ id: 'tip', action: 'suggest', message: 'Consider this', pattern: 'maybe' }),
196
+ ],
197
+ });
198
+
199
+ const content = result.files[0].content;
200
+ expect(content).toContain('console.info');
201
+ expect(content).toContain('[tip] SUGGESTION: Consider this');
202
+ });
203
+
204
+ it('unknown action falls back to console.warn', () => {
205
+ const result = adapter.translate({
206
+ rules: [
207
+ makeRule({
208
+ id: 'unk',
209
+ action: 'unknown' as unknown as EnforcementAction,
210
+ message: 'Fallback msg',
211
+ pattern: 'x',
212
+ }),
213
+ ],
214
+ });
215
+
216
+ const content = result.files[0].content;
217
+ expect(content).toContain('console.warn');
218
+ expect(content).toContain('[unk] Fallback msg');
219
+ });
220
+
221
+ it('rules without pattern generate action code without regex test', () => {
222
+ const result = adapter.translate({
223
+ rules: [makeRule({ id: 'always', action: 'block', message: 'Always block' })],
224
+ });
225
+
226
+ const content = result.files[0].content;
227
+ expect(content).toContain('throw new Error');
228
+ expect(content).not.toContain('.test(');
229
+ });
230
+
231
+ it('rules with pattern generate regex test against ctx.input', () => {
232
+ const result = adapter.translate({
233
+ rules: [makeRule({ id: 'pat', action: 'warn', message: 'Match found', pattern: 'danger' })],
234
+ });
235
+
236
+ const content = result.files[0].content;
237
+ expect(content).toContain('/danger/.test(JSON.stringify(ctx.input');
238
+ });
239
+ });
240
+
241
+ // ─── translate() — skipped triggers ─────────────────────────────
242
+
243
+ describe('skipped triggers', () => {
244
+ it('skips pre-commit with reason', () => {
245
+ const result = adapter.translate({
246
+ rules: [makeRule({ id: 'commit-check', trigger: 'pre-commit' })],
247
+ });
248
+
249
+ expect(result.files).toHaveLength(0);
250
+ expect(result.skipped).toHaveLength(1);
251
+ expect(result.skipped[0].ruleId).toBe('commit-check');
252
+ expect(result.skipped[0].reason).toContain('not supported by OpenCode');
253
+ });
254
+
255
+ it('skips on-save with reason', () => {
256
+ const result = adapter.translate({
257
+ rules: [makeRule({ id: 'save-check', trigger: 'on-save' })],
258
+ });
259
+
260
+ expect(result.files).toHaveLength(0);
261
+ expect(result.skipped).toHaveLength(1);
262
+ expect(result.skipped[0].ruleId).toBe('save-check');
263
+ expect(result.skipped[0].reason).toContain('not supported by OpenCode');
264
+ });
265
+
266
+ it('handles mix of supported and unsupported triggers', () => {
267
+ const result = adapter.translate({
268
+ rules: [
269
+ makeRule({ id: 'ok', trigger: 'pre-tool-use', pattern: 'test' }),
270
+ makeRule({ id: 'skip1', trigger: 'pre-commit' }),
271
+ makeRule({ id: 'skip2', trigger: 'on-save' }),
272
+ ],
273
+ });
274
+
275
+ expect(result.files).toHaveLength(1);
276
+ expect(result.skipped).toHaveLength(2);
277
+ expect(result.skipped.map((s) => s.ruleId)).toEqual(['skip1', 'skip2']);
278
+ });
279
+
280
+ it('returns only skipped items when all rules are unsupported', () => {
281
+ const result = adapter.translate({
282
+ rules: [
283
+ makeRule({ id: 's1', trigger: 'pre-commit' }),
284
+ makeRule({ id: 's2', trigger: 'on-save' }),
285
+ ],
286
+ });
287
+
288
+ expect(result.files).toHaveLength(0);
289
+ expect(result.skipped).toHaveLength(2);
290
+ });
291
+ });
292
+
293
+ // ─── host property ──────────────────────────────────────────────
294
+
295
+ describe('host', () => {
296
+ it('identifies as opencode', () => {
297
+ expect(adapter.host).toBe('opencode');
298
+ });
299
+ });
300
+ });
301
+
302
+ // ─── detectHost() ─────────────────────────────────────────────────
303
+
304
+ describe('detectHost', () => {
305
+ const originalEnv = { ...process.env };
306
+
307
+ beforeEach(() => {
308
+ // Clear relevant env vars before each test
309
+ delete process.env.OPENCODE;
310
+ delete process.env.OPENCODE_SESSION;
311
+ delete process.env.CLAUDE_CODE;
312
+ mockedExistsSync.mockReset().mockReturnValue(false);
313
+ });
314
+
315
+ afterEach(() => {
316
+ process.env = { ...originalEnv };
317
+ });
318
+
319
+ it('returns opencode when OPENCODE env var is set and no Claude indicators', () => {
320
+ process.env.OPENCODE = '1';
321
+ mockedExistsSync.mockReturnValue(false);
322
+
323
+ expect(detectHost()).toBe('opencode');
324
+ });
325
+
326
+ it('returns opencode when OPENCODE_SESSION env var is set and no Claude indicators', () => {
327
+ process.env.OPENCODE_SESSION = 'abc123';
328
+ mockedExistsSync.mockReturnValue(false);
329
+
330
+ expect(detectHost()).toBe('opencode');
331
+ });
332
+
333
+ it('returns claude-code when CLAUDE_CODE env var is set and no OpenCode indicators', () => {
334
+ process.env.CLAUDE_CODE = '1';
335
+ mockedExistsSync.mockReturnValue(false);
336
+
337
+ expect(detectHost()).toBe('claude-code');
338
+ });
339
+
340
+ it('returns claude-code when both OpenCode and Claude indicators present', () => {
341
+ process.env.OPENCODE = '1';
342
+ process.env.CLAUDE_CODE = '1';
343
+ mockedExistsSync.mockReturnValue(false);
344
+
345
+ expect(detectHost()).toBe('claude-code');
346
+ });
347
+
348
+ it('returns claude-code when neither host is detected (default)', () => {
349
+ mockedExistsSync.mockReturnValue(false);
350
+
351
+ expect(detectHost()).toBe('claude-code');
352
+ });
353
+
354
+ it('detects opencode via filesystem config when env vars absent', () => {
355
+ mockedExistsSync.mockImplementation((p: unknown) => {
356
+ // Normalize to forward slashes so the check works on Windows too
357
+ const path = String(p).replace(/\\/g, '/');
358
+ if (path.includes('opencode/opencode.json')) return true;
359
+ if (path.includes('.claude')) return false;
360
+ return false;
361
+ });
362
+
363
+ expect(detectHost()).toBe('opencode');
364
+ });
365
+
366
+ it('detects claude-code via filesystem when .claude dir exists', () => {
367
+ mockedExistsSync.mockImplementation((p: unknown) => {
368
+ // Normalize to forward slashes so the check works on Windows too
369
+ const path = String(p).replace(/\\/g, '/');
370
+ if (path.includes('.claude')) return true;
371
+ return false;
372
+ });
373
+
374
+ expect(detectHost()).toBe('claude-code');
375
+ });
376
+ });
377
+
378
+ // ─── createHostAdapter() ──────────────────────────────────────────
379
+
380
+ describe('createHostAdapter', () => {
381
+ const originalEnv = { ...process.env };
382
+
383
+ beforeEach(() => {
384
+ delete process.env.OPENCODE;
385
+ delete process.env.OPENCODE_SESSION;
386
+ delete process.env.CLAUDE_CODE;
387
+ mockedExistsSync.mockReset().mockReturnValue(false);
388
+ });
389
+
390
+ afterEach(() => {
391
+ process.env = { ...originalEnv };
392
+ });
393
+
394
+ it('returns OpenCodeAdapter when opencode is detected', () => {
395
+ process.env.OPENCODE = '1';
396
+
397
+ const adapter = createHostAdapter();
398
+ expect(adapter.host).toBe('opencode');
399
+ expect(adapter).toBeInstanceOf(OpenCodeAdapter);
400
+ });
401
+
402
+ it('returns ClaudeCodeAdapter by default', () => {
403
+ const adapter = createHostAdapter();
404
+ expect(adapter.host).toBe('claude-code');
405
+ });
406
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * OpenCode host adapter — translates enforcement rules to OpenCode plugin config.
3
+ *
4
+ * Maps:
5
+ * - pre-tool-use → tool.execute.before
6
+ * - post-tool-use → tool.execute.after
7
+ * - pre-compact → session.compacted
8
+ * - session-start → session.created
9
+ *
10
+ * Unsupported: pre-commit, on-save (no OpenCode hook equivalents).
11
+ */
12
+
13
+ import type {
14
+ EnforcementConfig,
15
+ EnforcementTrigger,
16
+ HostAdapter,
17
+ HostAdapterResult,
18
+ } from '../types.js';
19
+
20
+ const TRIGGER_TO_EVENT: Partial<Record<EnforcementTrigger, string>> = {
21
+ 'pre-tool-use': 'tool.execute.before',
22
+ 'post-tool-use': 'tool.execute.after',
23
+ 'pre-compact': 'session.compacted',
24
+ 'session-start': 'session.created',
25
+ };
26
+
27
+ interface HookHandler {
28
+ event: string;
29
+ ruleId: string;
30
+ pattern?: string;
31
+ action: string;
32
+ message: string;
33
+ }
34
+
35
+ export class OpenCodeAdapter implements HostAdapter {
36
+ readonly host = 'opencode';
37
+
38
+ supports(trigger: EnforcementTrigger): boolean {
39
+ return trigger in TRIGGER_TO_EVENT;
40
+ }
41
+
42
+ translate(config: EnforcementConfig): HostAdapterResult {
43
+ const handlers: HookHandler[] = [];
44
+ const skipped: Array<{ ruleId: string; reason: string }> = [];
45
+
46
+ for (const rule of config.rules) {
47
+ if (!this.supports(rule.trigger)) {
48
+ skipped.push({
49
+ ruleId: rule.id,
50
+ reason: `Trigger '${rule.trigger}' not supported by OpenCode`,
51
+ });
52
+ continue;
53
+ }
54
+
55
+ const event = TRIGGER_TO_EVENT[rule.trigger];
56
+ if (!event) {
57
+ skipped.push({
58
+ ruleId: rule.id,
59
+ reason: `No event mapping for '${rule.trigger}'`,
60
+ });
61
+ continue;
62
+ }
63
+
64
+ handlers.push({
65
+ event,
66
+ ruleId: rule.id,
67
+ pattern: rule.pattern,
68
+ action: rule.action,
69
+ message: rule.message,
70
+ });
71
+ }
72
+
73
+ const files: Array<{ path: string; content: string }> = [];
74
+
75
+ if (handlers.length > 0) {
76
+ files.push({
77
+ path: '.opencode/plugins/soleri-enforcement.ts',
78
+ content: this.generatePluginFile(handlers),
79
+ });
80
+ }
81
+
82
+ return { host: this.host, files, skipped };
83
+ }
84
+
85
+ private generatePluginFile(handlers: HookHandler[]): string {
86
+ // Group handlers by event
87
+ const byEvent = new Map<string, HookHandler[]>();
88
+ for (const h of handlers) {
89
+ const existing = byEvent.get(h.event) ?? [];
90
+ existing.push(h);
91
+ byEvent.set(h.event, existing);
92
+ }
93
+
94
+ const hookEntries: string[] = [];
95
+
96
+ for (const [event, eventHandlers] of Array.from(byEvent.entries())) {
97
+ const checks = eventHandlers.map((h) => this.generateCheck(h)).join('\n');
98
+ hookEntries.push(` '${event}': (ctx) => {\n${checks}\n }`);
99
+ }
100
+
101
+ const lines = [
102
+ '/**',
103
+ ' * Soleri enforcement plugin for OpenCode.',
104
+ ' * Auto-generated — do not edit manually.',
105
+ ' */',
106
+ '',
107
+ 'export default {',
108
+ ' hooks: {',
109
+ hookEntries.join(',\n'),
110
+ ' },',
111
+ '};',
112
+ '',
113
+ ];
114
+
115
+ return lines.join('\n');
116
+ }
117
+
118
+ private generateCheck(handler: HookHandler): string {
119
+ const indent = ' ';
120
+
121
+ if (!handler.pattern) {
122
+ // No pattern — always fires
123
+ return this.generateActionCode(indent, handler.ruleId, handler.action, handler.message);
124
+ }
125
+
126
+ // Pattern-based check
127
+ const lines = [
128
+ `${indent}// Rule: ${handler.ruleId}`,
129
+ `${indent}if (/${handler.pattern}/.test(JSON.stringify(ctx.input ?? ''))) {`,
130
+ this.generateActionCode(`${indent} `, handler.ruleId, handler.action, handler.message),
131
+ `${indent}}`,
132
+ ];
133
+ return lines.join('\n');
134
+ }
135
+
136
+ private generateActionCode(
137
+ indent: string,
138
+ ruleId: string,
139
+ action: string,
140
+ message: string,
141
+ ): string {
142
+ switch (action) {
143
+ case 'block':
144
+ return `${indent}throw new Error('[${ruleId}] BLOCKED: ${message}');`;
145
+ case 'warn':
146
+ return `${indent}console.warn('[${ruleId}] WARNING: ${message}');`;
147
+ case 'suggest':
148
+ return `${indent}console.info('[${ruleId}] SUGGESTION: ${message}');`;
149
+ default:
150
+ return `${indent}console.warn('[${ruleId}] ${message}');`;
151
+ }
152
+ }
153
+ }
package/src/index.ts CHANGED
@@ -40,9 +40,18 @@ export {
40
40
  agentKeysPath,
41
41
  agentTemplatesDir,
42
42
  agentFlagsPath,
43
+ agentKnowledgeDir,
44
+ projectKnowledgeDir,
43
45
  sharedVaultPath,
44
46
  } from './paths.js';
45
47
 
48
+ // ─── Vault Markdown Sync ───────────────────────────────────────────
49
+ export {
50
+ syncAllToMarkdown,
51
+ syncEntryToMarkdown,
52
+ entryToMarkdown,
53
+ } from './vault/vault-markdown-sync.js';
54
+
46
55
  // ─── Intelligence ────────────────────────────────────────────────────
47
56
  export type {
48
57
  IntelligenceEntry,
@@ -901,6 +910,16 @@ export type {
901
910
  DeclinedCategory,
902
911
  } from './operator/operator-context-types.js';
903
912
 
913
+ // ─── Workflows ─────────────────────────────────────────────────────────
914
+ export {
915
+ WorkflowGateSchema,
916
+ WorkflowOverrideSchema,
917
+ WORKFLOW_TO_INTENT,
918
+ loadAgentWorkflows,
919
+ getWorkflowForIntent,
920
+ } from './workflows/index.js';
921
+ export type { WorkflowGate, WorkflowOverride } from './workflows/index.js';
922
+
904
923
  // ─── Update Check ────────────────────────────────────────────────────
905
924
  export { checkForUpdate, buildChangelogUrl, detectBreakingChanges } from './update-check.js';
906
925
  export type { UpdateInfo } from './update-check.js';
package/src/paths.ts CHANGED
@@ -109,6 +109,11 @@ export function agentKnowledgeDir(agentId: string): string {
109
109
  return join(agentHome(agentId), 'knowledge');
110
110
  }
111
111
 
112
+ /** Project-local knowledge directory for browsable markdown sync. */
113
+ export function projectKnowledgeDir(projectPath: string): string {
114
+ return join(projectPath, 'knowledge');
115
+ }
116
+
112
117
  /** Shared vault path: ~/.soleri/vault.db (cross-agent intelligence) */
113
118
  export function sharedVaultPath(): string {
114
119
  return join(SOLERI_HOME, 'vault.db');