@omnidev-ai/core 0.1.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 (59) hide show
  1. package/package.json +31 -0
  2. package/src/capability/AGENTS.md +58 -0
  3. package/src/capability/commands.test.ts +414 -0
  4. package/src/capability/commands.ts +70 -0
  5. package/src/capability/docs.test.ts +199 -0
  6. package/src/capability/docs.ts +46 -0
  7. package/src/capability/index.ts +20 -0
  8. package/src/capability/loader.test.ts +815 -0
  9. package/src/capability/loader.ts +492 -0
  10. package/src/capability/registry.test.ts +473 -0
  11. package/src/capability/registry.ts +55 -0
  12. package/src/capability/rules.test.ts +145 -0
  13. package/src/capability/rules.ts +133 -0
  14. package/src/capability/skills.test.ts +316 -0
  15. package/src/capability/skills.ts +56 -0
  16. package/src/capability/sources.test.ts +338 -0
  17. package/src/capability/sources.ts +966 -0
  18. package/src/capability/subagents.test.ts +478 -0
  19. package/src/capability/subagents.ts +103 -0
  20. package/src/capability/yaml-parser.ts +81 -0
  21. package/src/config/AGENTS.md +46 -0
  22. package/src/config/capabilities.ts +82 -0
  23. package/src/config/env.test.ts +286 -0
  24. package/src/config/env.ts +96 -0
  25. package/src/config/index.ts +6 -0
  26. package/src/config/loader.test.ts +282 -0
  27. package/src/config/loader.ts +137 -0
  28. package/src/config/parser.test.ts +281 -0
  29. package/src/config/parser.ts +55 -0
  30. package/src/config/profiles.test.ts +259 -0
  31. package/src/config/profiles.ts +75 -0
  32. package/src/config/provider.test.ts +79 -0
  33. package/src/config/provider.ts +55 -0
  34. package/src/debug.ts +20 -0
  35. package/src/gitignore/manager.test.ts +219 -0
  36. package/src/gitignore/manager.ts +167 -0
  37. package/src/index.test.ts +26 -0
  38. package/src/index.ts +39 -0
  39. package/src/mcp-json/index.ts +1 -0
  40. package/src/mcp-json/manager.test.ts +415 -0
  41. package/src/mcp-json/manager.ts +118 -0
  42. package/src/state/active-profile.test.ts +131 -0
  43. package/src/state/active-profile.ts +41 -0
  44. package/src/state/index.ts +2 -0
  45. package/src/state/manifest.test.ts +548 -0
  46. package/src/state/manifest.ts +164 -0
  47. package/src/sync.ts +213 -0
  48. package/src/templates/agents.test.ts +23 -0
  49. package/src/templates/agents.ts +14 -0
  50. package/src/templates/claude.test.ts +48 -0
  51. package/src/templates/claude.ts +122 -0
  52. package/src/test-utils/helpers.test.ts +196 -0
  53. package/src/test-utils/helpers.ts +187 -0
  54. package/src/test-utils/index.ts +30 -0
  55. package/src/test-utils/mocks.test.ts +83 -0
  56. package/src/test-utils/mocks.ts +101 -0
  57. package/src/types/capability-export.ts +234 -0
  58. package/src/types/index.test.ts +28 -0
  59. package/src/types/index.ts +270 -0
@@ -0,0 +1,478 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { loadSubagents } from "./subagents";
5
+
6
+ describe("loadSubagents", () => {
7
+ const testDir = join(process.cwd(), "test-subagents-temp");
8
+ const capabilityPath = join(testDir, "test-capability");
9
+
10
+ beforeEach(() => {
11
+ mkdirSync(capabilityPath, { recursive: true });
12
+ });
13
+
14
+ afterEach(() => {
15
+ if (testDir) {
16
+ rmSync(testDir, { recursive: true, force: true });
17
+ }
18
+ });
19
+
20
+ test("returns empty array when subagents directory does not exist", async () => {
21
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
22
+ expect(subagents).toEqual([]);
23
+ });
24
+
25
+ test("returns empty array when subagents directory is empty", async () => {
26
+ mkdirSync(join(capabilityPath, "subagents"), { recursive: true });
27
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
28
+ expect(subagents).toEqual([]);
29
+ });
30
+
31
+ test("loads single subagent with valid frontmatter and system prompt", async () => {
32
+ const subagentDir = join(capabilityPath, "subagents", "test-subagent");
33
+ mkdirSync(subagentDir, { recursive: true });
34
+
35
+ const subagentContent = `---
36
+ name: test-subagent
37
+ description: A test subagent
38
+ ---
39
+
40
+ You are a test subagent.
41
+
42
+ ## Instructions
43
+
44
+ Do testing things.`;
45
+
46
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
47
+
48
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
49
+
50
+ expect(subagents).toHaveLength(1);
51
+ expect(subagents[0]).toEqual({
52
+ name: "test-subagent",
53
+ description: "A test subagent",
54
+ systemPrompt: "You are a test subagent.\n\n## Instructions\n\nDo testing things.",
55
+ capabilityId: "test-cap",
56
+ });
57
+ });
58
+
59
+ test("loads subagent with tools field", async () => {
60
+ const subagentDir = join(capabilityPath, "subagents", "tools-subagent");
61
+ mkdirSync(subagentDir, { recursive: true });
62
+
63
+ const subagentContent = `---
64
+ name: tools-subagent
65
+ description: Subagent with tools
66
+ tools: Read, Glob, Grep
67
+ ---
68
+
69
+ System prompt here.`;
70
+
71
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
72
+
73
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
74
+
75
+ expect(subagents).toHaveLength(1);
76
+ expect(subagents[0]?.tools).toEqual(["Read", "Glob", "Grep"]);
77
+ });
78
+
79
+ test("loads subagent with disallowedTools field", async () => {
80
+ const subagentDir = join(capabilityPath, "subagents", "disallowed-subagent");
81
+ mkdirSync(subagentDir, { recursive: true });
82
+
83
+ const subagentContent = `---
84
+ name: disallowed-subagent
85
+ description: Subagent with disallowed tools
86
+ disallowedTools: Write, Edit
87
+ ---
88
+
89
+ System prompt here.`;
90
+
91
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
92
+
93
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
94
+
95
+ expect(subagents).toHaveLength(1);
96
+ expect(subagents[0]?.disallowedTools).toEqual(["Write", "Edit"]);
97
+ });
98
+
99
+ test("loads subagent with model field", async () => {
100
+ const subagentDir = join(capabilityPath, "subagents", "model-subagent");
101
+ mkdirSync(subagentDir, { recursive: true });
102
+
103
+ const subagentContent = `---
104
+ name: model-subagent
105
+ description: Subagent with model
106
+ model: haiku
107
+ ---
108
+
109
+ System prompt here.`;
110
+
111
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
112
+
113
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
114
+
115
+ expect(subagents).toHaveLength(1);
116
+ expect(subagents[0]?.model).toBe("haiku");
117
+ });
118
+
119
+ test("loads subagent with permissionMode field", async () => {
120
+ const subagentDir = join(capabilityPath, "subagents", "permission-subagent");
121
+ mkdirSync(subagentDir, { recursive: true });
122
+
123
+ const subagentContent = `---
124
+ name: permission-subagent
125
+ description: Subagent with permission mode
126
+ permissionMode: dontAsk
127
+ ---
128
+
129
+ System prompt here.`;
130
+
131
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
132
+
133
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
134
+
135
+ expect(subagents).toHaveLength(1);
136
+ expect(subagents[0]?.permissionMode).toBe("dontAsk");
137
+ });
138
+
139
+ test("loads subagent with skills field", async () => {
140
+ const subagentDir = join(capabilityPath, "subagents", "skills-subagent");
141
+ mkdirSync(subagentDir, { recursive: true });
142
+
143
+ const subagentContent = `---
144
+ name: skills-subagent
145
+ description: Subagent with skills
146
+ skills: prd, ralph
147
+ ---
148
+
149
+ System prompt here.`;
150
+
151
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
152
+
153
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
154
+
155
+ expect(subagents).toHaveLength(1);
156
+ expect(subagents[0]?.skills).toEqual(["prd", "ralph"]);
157
+ });
158
+
159
+ test("loads subagent with all optional fields", async () => {
160
+ const subagentDir = join(capabilityPath, "subagents", "full-subagent");
161
+ mkdirSync(subagentDir, { recursive: true });
162
+
163
+ const subagentContent = `---
164
+ name: full-subagent
165
+ description: Subagent with all fields
166
+ tools: Read, Glob, Grep, Bash
167
+ disallowedTools: Write, Edit
168
+ model: sonnet
169
+ permissionMode: acceptEdits
170
+ skills: prd, ralph, mcp-builder
171
+ ---
172
+
173
+ You are a fully configured subagent.`;
174
+
175
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
176
+
177
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
178
+
179
+ expect(subagents).toHaveLength(1);
180
+ expect(subagents[0]?.name).toBe("full-subagent");
181
+ expect(subagents[0]?.description).toBe("Subagent with all fields");
182
+ expect(subagents[0]?.tools).toEqual(["Read", "Glob", "Grep", "Bash"]);
183
+ expect(subagents[0]?.disallowedTools).toEqual(["Write", "Edit"]);
184
+ expect(subagents[0]?.model).toBe("sonnet");
185
+ expect(subagents[0]?.permissionMode).toBe("acceptEdits");
186
+ expect(subagents[0]?.skills).toEqual(["prd", "ralph", "mcp-builder"]);
187
+ expect(subagents[0]?.systemPrompt).toBe("You are a fully configured subagent.");
188
+ });
189
+
190
+ test("loads multiple subagents from different directories", async () => {
191
+ const subagent1Dir = join(capabilityPath, "subagents", "subagent-1");
192
+ const subagent2Dir = join(capabilityPath, "subagents", "subagent-2");
193
+ mkdirSync(subagent1Dir, { recursive: true });
194
+ mkdirSync(subagent2Dir, { recursive: true });
195
+
196
+ writeFileSync(
197
+ join(subagent1Dir, "SUBAGENT.md"),
198
+ `---
199
+ name: subagent-1
200
+ description: First subagent
201
+ ---
202
+
203
+ First subagent prompt.`,
204
+ );
205
+
206
+ writeFileSync(
207
+ join(subagent2Dir, "SUBAGENT.md"),
208
+ `---
209
+ name: subagent-2
210
+ description: Second subagent
211
+ ---
212
+
213
+ Second subagent prompt.`,
214
+ );
215
+
216
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
217
+
218
+ expect(subagents).toHaveLength(2);
219
+ expect(subagents[0]?.name).toBe("subagent-1");
220
+ expect(subagents[1]?.name).toBe("subagent-2");
221
+ });
222
+
223
+ test("skips subagent directories without SUBAGENT.md file", async () => {
224
+ const validDir = join(capabilityPath, "subagents", "valid-subagent");
225
+ const invalidDir = join(capabilityPath, "subagents", "no-file");
226
+ mkdirSync(validDir, { recursive: true });
227
+ mkdirSync(invalidDir, { recursive: true });
228
+
229
+ writeFileSync(
230
+ join(validDir, "SUBAGENT.md"),
231
+ `---
232
+ name: valid-subagent
233
+ description: Valid subagent
234
+ ---
235
+
236
+ Valid prompt.`,
237
+ );
238
+
239
+ // No SUBAGENT.md in invalidDir
240
+
241
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
242
+
243
+ expect(subagents).toHaveLength(1);
244
+ expect(subagents[0]?.name).toBe("valid-subagent");
245
+ });
246
+
247
+ test("handles YAML frontmatter with quoted values", async () => {
248
+ const subagentDir = join(capabilityPath, "subagents", "quoted-subagent");
249
+ mkdirSync(subagentDir, { recursive: true });
250
+
251
+ const subagentContent = `---
252
+ name: "quoted-subagent"
253
+ description: "A subagent with quoted values"
254
+ ---
255
+
256
+ System prompt here.`;
257
+
258
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
259
+
260
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
261
+
262
+ expect(subagents).toHaveLength(1);
263
+ expect(subagents[0]?.name).toBe("quoted-subagent");
264
+ expect(subagents[0]?.description).toBe("A subagent with quoted values");
265
+ });
266
+
267
+ test("trims whitespace from system prompt", async () => {
268
+ const subagentDir = join(capabilityPath, "subagents", "whitespace-subagent");
269
+ mkdirSync(subagentDir, { recursive: true });
270
+
271
+ const subagentContent = `---
272
+ name: whitespace-subagent
273
+ description: Test whitespace trimming
274
+ ---
275
+
276
+
277
+ System prompt with leading/trailing whitespace.
278
+
279
+ `;
280
+
281
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
282
+
283
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
284
+
285
+ expect(subagents).toHaveLength(1);
286
+ expect(subagents[0]?.systemPrompt).toBe("System prompt with leading/trailing whitespace.");
287
+ });
288
+
289
+ test("throws error when SUBAGENT.md has no frontmatter", async () => {
290
+ const subagentDir = join(capabilityPath, "subagents", "no-frontmatter");
291
+ mkdirSync(subagentDir, { recursive: true });
292
+
293
+ const subagentContent = `# Just Instructions
294
+
295
+ No frontmatter here.`;
296
+
297
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
298
+
299
+ await expect(loadSubagents(capabilityPath, "test-cap")).rejects.toThrow(
300
+ /Invalid SUBAGENT\.md format.*missing YAML frontmatter/,
301
+ );
302
+ });
303
+
304
+ test("throws error when SUBAGENT.md is missing name field", async () => {
305
+ const subagentDir = join(capabilityPath, "subagents", "missing-name");
306
+ mkdirSync(subagentDir, { recursive: true });
307
+
308
+ const subagentContent = `---
309
+ description: Missing name field
310
+ ---
311
+
312
+ System prompt here.`;
313
+
314
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
315
+
316
+ await expect(loadSubagents(capabilityPath, "test-cap")).rejects.toThrow(
317
+ /name and description required/,
318
+ );
319
+ });
320
+
321
+ test("throws error when SUBAGENT.md is missing description field", async () => {
322
+ const subagentDir = join(capabilityPath, "subagents", "missing-description");
323
+ mkdirSync(subagentDir, { recursive: true });
324
+
325
+ const subagentContent = `---
326
+ name: missing-description
327
+ ---
328
+
329
+ System prompt here.`;
330
+
331
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
332
+
333
+ await expect(loadSubagents(capabilityPath, "test-cap")).rejects.toThrow(
334
+ /name and description required/,
335
+ );
336
+ });
337
+
338
+ test("handles empty system prompt after frontmatter", async () => {
339
+ const subagentDir = join(capabilityPath, "subagents", "empty-prompt");
340
+ mkdirSync(subagentDir, { recursive: true });
341
+
342
+ const subagentContent = `---
343
+ name: empty-prompt
344
+ description: Subagent with no prompt
345
+ ---
346
+ `;
347
+
348
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
349
+
350
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
351
+
352
+ expect(subagents).toHaveLength(1);
353
+ expect(subagents[0]?.systemPrompt).toBe("");
354
+ });
355
+
356
+ test("preserves markdown formatting in system prompt", async () => {
357
+ const subagentDir = join(capabilityPath, "subagents", "markdown-subagent");
358
+ mkdirSync(subagentDir, { recursive: true });
359
+
360
+ const subagentContent = `---
361
+ name: markdown-subagent
362
+ description: Subagent with markdown
363
+ ---
364
+
365
+ # Header
366
+
367
+ - List item 1
368
+ - List item 2
369
+
370
+ **Bold text** and *italic text*.
371
+
372
+ \`\`\`typescript
373
+ const code = "example";
374
+ \`\`\``;
375
+
376
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
377
+
378
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
379
+
380
+ expect(subagents).toHaveLength(1);
381
+ expect(subagents[0]?.systemPrompt).toContain("# Header");
382
+ expect(subagents[0]?.systemPrompt).toContain("- List item 1");
383
+ expect(subagents[0]?.systemPrompt).toContain("**Bold text**");
384
+ expect(subagents[0]?.systemPrompt).toContain("```typescript");
385
+ });
386
+
387
+ test("ignores non-directory entries in subagents folder", async () => {
388
+ const subagentsDir = join(capabilityPath, "subagents");
389
+ const validDir = join(subagentsDir, "valid-subagent");
390
+ mkdirSync(validDir, { recursive: true });
391
+
392
+ writeFileSync(
393
+ join(validDir, "SUBAGENT.md"),
394
+ `---
395
+ name: valid-subagent
396
+ description: Valid subagent
397
+ ---
398
+
399
+ Valid prompt.`,
400
+ );
401
+
402
+ // Create a file directly in subagents/ directory (should be ignored)
403
+ writeFileSync(join(subagentsDir, "README.md"), "This should be ignored");
404
+
405
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
406
+
407
+ expect(subagents).toHaveLength(1);
408
+ expect(subagents[0]?.name).toBe("valid-subagent");
409
+ });
410
+
411
+ test("associates subagents with correct capability ID", async () => {
412
+ const subagentDir = join(capabilityPath, "subagents", "test-subagent");
413
+ mkdirSync(subagentDir, { recursive: true });
414
+
415
+ writeFileSync(
416
+ join(subagentDir, "SUBAGENT.md"),
417
+ `---
418
+ name: test-subagent
419
+ description: Test subagent
420
+ ---
421
+
422
+ System prompt.`,
423
+ );
424
+
425
+ const subagents = await loadSubagents(capabilityPath, "my-capability");
426
+
427
+ expect(subagents).toHaveLength(1);
428
+ expect(subagents[0]?.capabilityId).toBe("my-capability");
429
+ });
430
+
431
+ test("model field accepts inherit value", async () => {
432
+ const subagentDir = join(capabilityPath, "subagents", "inherit-model");
433
+ mkdirSync(subagentDir, { recursive: true });
434
+
435
+ const subagentContent = `---
436
+ name: inherit-model
437
+ description: Subagent with inherit model
438
+ model: inherit
439
+ ---
440
+
441
+ System prompt here.`;
442
+
443
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
444
+
445
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
446
+
447
+ expect(subagents).toHaveLength(1);
448
+ expect(subagents[0]?.model).toBe("inherit");
449
+ });
450
+
451
+ test("permissionMode field accepts all valid values", async () => {
452
+ const modes = ["default", "acceptEdits", "dontAsk", "bypassPermissions", "plan"] as const;
453
+
454
+ for (const mode of modes) {
455
+ // Clean up from previous iteration
456
+ const subagentsDir = join(capabilityPath, "subagents");
457
+ rmSync(subagentsDir, { recursive: true, force: true });
458
+
459
+ const subagentDir = join(capabilityPath, "subagents", `${mode}-subagent`);
460
+ mkdirSync(subagentDir, { recursive: true });
461
+
462
+ const subagentContent = `---
463
+ name: ${mode}-subagent
464
+ description: Subagent with ${mode} permission mode
465
+ permissionMode: ${mode}
466
+ ---
467
+
468
+ System prompt here.`;
469
+
470
+ writeFileSync(join(subagentDir, "SUBAGENT.md"), subagentContent);
471
+
472
+ const subagents = await loadSubagents(capabilityPath, "test-cap");
473
+
474
+ expect(subagents).toHaveLength(1);
475
+ expect(subagents[0]?.permissionMode).toBe(mode);
476
+ }
477
+ });
478
+ });
@@ -0,0 +1,103 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { Subagent, SubagentHooks, SubagentModel, SubagentPermissionMode } from "../types";
4
+ import { parseFrontmatterWithMarkdown } from "./yaml-parser";
5
+
6
+ interface SubagentFrontmatter {
7
+ name: string;
8
+ description: string;
9
+ tools?: string;
10
+ disallowedTools?: string;
11
+ model?: SubagentModel;
12
+ permissionMode?: SubagentPermissionMode;
13
+ skills?: string;
14
+ hooks?: SubagentHooks;
15
+ }
16
+
17
+ /**
18
+ * Load subagents from the subagents/ directory of a capability.
19
+ * Each subagent is a SUBAGENT.md file in its own subdirectory.
20
+ */
21
+ export async function loadSubagents(
22
+ capabilityPath: string,
23
+ capabilityId: string,
24
+ ): Promise<Subagent[]> {
25
+ const subagentsDir = join(capabilityPath, "subagents");
26
+
27
+ if (!existsSync(subagentsDir)) {
28
+ return [];
29
+ }
30
+
31
+ const subagents: Subagent[] = [];
32
+ const entries = readdirSync(subagentsDir, { withFileTypes: true });
33
+
34
+ for (const entry of entries) {
35
+ if (entry.isDirectory()) {
36
+ const subagentPath = join(subagentsDir, entry.name, "SUBAGENT.md");
37
+ if (existsSync(subagentPath)) {
38
+ const subagent = await parseSubagentFile(subagentPath, capabilityId);
39
+ subagents.push(subagent);
40
+ }
41
+ }
42
+ }
43
+
44
+ return subagents;
45
+ }
46
+
47
+ async function parseSubagentFile(filePath: string, capabilityId: string): Promise<Subagent> {
48
+ const content = await Bun.file(filePath).text();
49
+
50
+ const parsed = parseFrontmatterWithMarkdown<SubagentFrontmatter>(content);
51
+
52
+ if (!parsed) {
53
+ throw new Error(`Invalid SUBAGENT.md format at ${filePath}: missing YAML frontmatter`);
54
+ }
55
+
56
+ const frontmatter = parsed.frontmatter;
57
+ const systemPrompt = parsed.markdown;
58
+
59
+ if (!frontmatter.name || !frontmatter.description) {
60
+ throw new Error(`Invalid SUBAGENT.md at ${filePath}: name and description required`);
61
+ }
62
+
63
+ const result: Subagent = {
64
+ name: frontmatter.name,
65
+ description: frontmatter.description,
66
+ systemPrompt: systemPrompt.trim(),
67
+ capabilityId,
68
+ };
69
+
70
+ // Add optional fields if present
71
+ if (frontmatter.tools) {
72
+ result.tools = parseCommaSeparatedList(frontmatter.tools);
73
+ }
74
+
75
+ if (frontmatter.disallowedTools) {
76
+ result.disallowedTools = parseCommaSeparatedList(frontmatter.disallowedTools);
77
+ }
78
+
79
+ if (frontmatter.model) {
80
+ result.model = frontmatter.model;
81
+ }
82
+
83
+ if (frontmatter.permissionMode) {
84
+ result.permissionMode = frontmatter.permissionMode;
85
+ }
86
+
87
+ if (frontmatter.skills) {
88
+ result.skills = parseCommaSeparatedList(frontmatter.skills);
89
+ }
90
+
91
+ if (frontmatter.hooks) {
92
+ result.hooks = frontmatter.hooks;
93
+ }
94
+
95
+ return result;
96
+ }
97
+
98
+ function parseCommaSeparatedList(value: string): string[] {
99
+ return value
100
+ .split(",")
101
+ .map((item) => item.trim())
102
+ .filter(Boolean);
103
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * YAML Frontmatter Parser Utility
3
+ *
4
+ * Consolidates YAML frontmatter parsing logic that was duplicated across
5
+ * skills.ts, commands.ts, and subagents.ts.
6
+ *
7
+ * This utility provides:
8
+ * - parseSimpleYamlFrontmatter: Parse key: value pairs from YAML
9
+ * - parseFrontmatterWithMarkdown: Extract frontmatter + markdown content
10
+ */
11
+
12
+ /**
13
+ * Parse simple YAML key: value pairs
14
+ * Supports basic key: value syntax (not full YAML spec)
15
+ * Handles quoted and unquoted values
16
+ *
17
+ * @param yaml - YAML content string to parse
18
+ * @returns Record of key-value pairs
19
+ */
20
+ export function parseSimpleYamlFrontmatter<T>(yaml: string): T {
21
+ const result: Record<string, string> = {};
22
+
23
+ for (const line of yaml.split("\n")) {
24
+ const trimmed = line.trim();
25
+ if (!trimmed || trimmed.startsWith("#")) {
26
+ continue;
27
+ }
28
+
29
+ // Support: key: value and key: "quoted value"
30
+ const colonIndex = trimmed.indexOf(":");
31
+ if (colonIndex === -1) {
32
+ continue;
33
+ }
34
+
35
+ const rawKey = trimmed.slice(0, colonIndex).trim();
36
+ let value = trimmed.slice(colonIndex + 1).trim();
37
+
38
+ // Remove quotes if present (double or single)
39
+ if (
40
+ (value.startsWith('"') && value.endsWith('"')) ||
41
+ (value.startsWith("'") && value.endsWith("'"))
42
+ ) {
43
+ value = value.slice(1, -1);
44
+ }
45
+
46
+ // Convert kebab-case to camelCase
47
+ const key = rawKey.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
48
+
49
+ result[key] = value;
50
+ }
51
+
52
+ return result as unknown as T;
53
+ }
54
+
55
+ /**
56
+ * Parse content with YAML frontmatter separated from markdown
57
+ * Expected format:
58
+ * ```
59
+ * ---
60
+ * key: value
61
+ * ---
62
+ * Markdown content here
63
+ * ```
64
+ *
65
+ * @param content - Full content with frontmatter and markdown
66
+ * @returns Object with frontmatter (parsed) and markdown (remaining) or null if no frontmatter
67
+ */
68
+ export function parseFrontmatterWithMarkdown<T>(
69
+ content: string,
70
+ ): { frontmatter: T; markdown: string } | null {
71
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/s);
72
+
73
+ if (!match?.[1] || match[2] === undefined) {
74
+ return null;
75
+ }
76
+
77
+ const frontmatter = parseSimpleYamlFrontmatter<T>(match[1]);
78
+ const markdown = match[2];
79
+
80
+ return { frontmatter, markdown };
81
+ }
@@ -0,0 +1,46 @@
1
+ # CONFIGURATION SUBSYSTEM
2
+
3
+ **Generated:** 2026-01-12T10:36:46
4
+ **Commit:** (not specified)
5
+ **Branch:** (not specified)
6
+
7
+ ## OVERVIEW
8
+ Configuration loading and parsing system using TOML format with profile-based capability management.
9
+
10
+ ## WHERE TO LOOK
11
+ | Task | Location | Notes |
12
+ |------|----------|-------|
13
+ | Config merge logic | loader.ts | Merges config.toml + config.local.toml |
14
+ | Environment loading | env.ts | Loads .omni/.env, validates declarations |
15
+ | TOML parsing | parser.ts | Uses smol-toml, validates capability.toml |
16
+ | Provider selection | provider.ts | Loads provider.toml, parses CLI flags |
17
+ | Profile management | profiles.ts | Active profile tracking, resolves capabilities |
18
+ | Capability enable/disable | capabilities.ts | Updates profiles + gitignore patterns |
19
+
20
+ ## CONVENTIONS
21
+
22
+ **Config File Locations:**
23
+ - `.omni/config.toml` - main config (project name, default profile, profiles)
24
+ - `.omni/config.local.toml` - local overrides (gitignored)
25
+ - `.omni/provider.toml` - provider selection (claude/codex/both)
26
+ - `.omni/.env` - secrets, always gitignored
27
+
28
+ **Profile-Based Capability Management:**
29
+ - Profiles define capability sets in `[profiles.name].capabilities`
30
+ - `active_profile` in config.toml selects current profile
31
+ - `always_enabled_capabilities` list merged with profile capabilities
32
+ - Use `enableCapability()` / `disableCapability()` to update active profile
33
+
34
+ **Environment Variable Handling:**
35
+ - capability.toml `[env]` section declares requirements
36
+ - `required = true` → must be present or default
37
+ - `secret = true` → masked in logs/error messages
38
+ - `default = "value"` → optional with fallback
39
+
40
+ ## ANTI-PATTERNS (THIS SUBSYSTEM)
41
+
42
+ - **NEVER** edit config.toml directly for capability changes - use enableCapability()/disableCapability()
43
+ - **NEVER** commit .omni/.env or config.local.toml - both gitignored
44
+ - **NEVER** parse TOML manually - use parseOmniConfig() / parseCapabilityConfig()
45
+ - **NEVER** ignore validateEnv() errors - required env vars block capability load
46
+ - **NEVER** assume process.env is complete - merge with .omni/.env via loadEnvironment()