open-multi-agent-kit 0.90.3 → 0.90.5

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 (184) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +16 -14
  3. package/dist/config.d.ts +3 -6
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +27 -19
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/adaptorch-bridge.d.ts +250 -0
  8. package/dist/core/adaptorch-bridge.d.ts.map +1 -0
  9. package/dist/core/adaptorch-bridge.js +466 -0
  10. package/dist/core/adaptorch-bridge.js.map +1 -0
  11. package/dist/core/agent-session.d.ts +97 -0
  12. package/dist/core/agent-session.d.ts.map +1 -1
  13. package/dist/core/agent-session.js +194 -0
  14. package/dist/core/agent-session.js.map +1 -1
  15. package/dist/core/bang-skill-invocation.d.ts.map +1 -1
  16. package/dist/core/bang-skill-invocation.js +149 -0
  17. package/dist/core/bang-skill-invocation.js.map +1 -1
  18. package/dist/core/compaction/utils.d.ts.map +1 -1
  19. package/dist/core/compaction/utils.js +15 -2
  20. package/dist/core/compaction/utils.js.map +1 -1
  21. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  22. package/dist/core/context-budget-v2-planner.js +10 -2
  23. package/dist/core/context-budget-v2-planner.js.map +1 -1
  24. package/dist/core/context-budget-v2-types.d.ts +7 -1
  25. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  26. package/dist/core/context-budget-v2-types.js.map +1 -1
  27. package/dist/core/domain-loadouts.d.ts.map +1 -1
  28. package/dist/core/domain-loadouts.js +1 -1
  29. package/dist/core/domain-loadouts.js.map +1 -1
  30. package/dist/core/reasoning-router-bias.d.ts +107 -0
  31. package/dist/core/reasoning-router-bias.d.ts.map +1 -0
  32. package/dist/core/reasoning-router-bias.js +278 -0
  33. package/dist/core/reasoning-router-bias.js.map +1 -0
  34. package/dist/core/reasoning-router-resolver.d.ts +44 -0
  35. package/dist/core/reasoning-router-resolver.d.ts.map +1 -0
  36. package/dist/core/reasoning-router-resolver.js +79 -0
  37. package/dist/core/reasoning-router-resolver.js.map +1 -0
  38. package/dist/core/reasoning-router-v4-weights.d.ts +80 -0
  39. package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -0
  40. package/dist/core/reasoning-router-v4-weights.js +68 -0
  41. package/dist/core/reasoning-router-v4-weights.js.map +1 -0
  42. package/dist/core/reasoning-router-v4.d.ts +118 -0
  43. package/dist/core/reasoning-router-v4.d.ts.map +1 -0
  44. package/dist/core/reasoning-router-v4.js +585 -0
  45. package/dist/core/reasoning-router-v4.js.map +1 -0
  46. package/dist/core/router-feedback-collector.d.ts +98 -0
  47. package/dist/core/router-feedback-collector.d.ts.map +1 -0
  48. package/dist/core/router-feedback-collector.js +236 -0
  49. package/dist/core/router-feedback-collector.js.map +1 -0
  50. package/dist/core/settings-manager.d.ts +28 -0
  51. package/dist/core/settings-manager.d.ts.map +1 -1
  52. package/dist/core/settings-manager.js +16 -0
  53. package/dist/core/settings-manager.js.map +1 -1
  54. package/dist/core/slash-commands.d.ts.map +1 -1
  55. package/dist/core/slash-commands.js +4 -1
  56. package/dist/core/slash-commands.js.map +1 -1
  57. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts +15 -0
  58. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts.map +1 -1
  59. package/dist/modes/interactive/components/control-panel-runtime-status.js +95 -5
  60. package/dist/modes/interactive/components/control-panel-runtime-status.js.map +1 -1
  61. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  62. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  63. package/dist/modes/interactive/interactive-mode.js +60 -17
  64. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  65. package/docs/loadout-domains/README.md +8 -6
  66. package/docs/loadout-domains/ai-agent-ops.md +3 -2
  67. package/docs/loadout-domains/backend-api.md +3 -2
  68. package/docs/loadout-domains/data-science.md +3 -2
  69. package/docs/loadout-domains/devops-infra.md +2 -1
  70. package/docs/loadout-domains/docs-writing.md +7 -2
  71. package/docs/loadout-domains/frontend-ui.md +42 -11
  72. package/docs/loadout-domains/korean-document.md +83 -0
  73. package/docs/loadout-domains/mobile.md +1 -1
  74. package/docs/loadout-domains/qa-testing.md +4 -2
  75. package/docs/loadout-domains/security-audit.md +2 -1
  76. package/docs/loadout-domains/visual-qa.md +94 -0
  77. package/docs/skills.md +4 -1
  78. package/docs/usage.md +36 -3
  79. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  80. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  81. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  82. package/examples/extensions/gondolin/package-lock.json +2 -2
  83. package/examples/extensions/gondolin/package.json +1 -1
  84. package/examples/extensions/sandbox/package-lock.json +2 -2
  85. package/examples/extensions/sandbox/package.json +1 -1
  86. package/examples/extensions/subagent/agent-capability-router.test.ts +123 -0
  87. package/examples/extensions/subagent/agent-capability-router.ts +274 -0
  88. package/examples/extensions/subagent/agents/planner.md +3 -0
  89. package/examples/extensions/subagent/agents/reviewer.md +3 -0
  90. package/examples/extensions/subagent/agents/scout.md +3 -0
  91. package/examples/extensions/subagent/agents/worker.md +2 -0
  92. package/examples/extensions/subagent/agents.ts +14 -0
  93. package/examples/extensions/subagent/capabilities.ts +368 -0
  94. package/examples/extensions/subagent/domain-profiles.ts +1089 -0
  95. package/examples/extensions/subagent/index.ts +71 -3
  96. package/examples/extensions/with-deps/package-lock.json +2 -2
  97. package/examples/extensions/with-deps/package.json +1 -1
  98. package/examples/sdk/12-full-control.ts +1 -1
  99. package/npm-shrinkwrap.json +12 -12
  100. package/package.json +4 -4
  101. package/dist/agents/jailbreak/index.d.ts +0 -7
  102. package/dist/agents/jailbreak/index.d.ts.map +0 -1
  103. package/dist/agents/jailbreak/index.js +0 -7
  104. package/dist/agents/jailbreak/index.js.map +0 -1
  105. package/dist/agents/jailbreak/subagent-f.d.ts +0 -44
  106. package/dist/agents/jailbreak/subagent-f.d.ts.map +0 -1
  107. package/dist/agents/jailbreak/subagent-f.js +0 -60
  108. package/dist/agents/jailbreak/subagent-f.js.map +0 -1
  109. package/dist/cli/jailbreak-args.d.ts +0 -26
  110. package/dist/cli/jailbreak-args.d.ts.map +0 -1
  111. package/dist/cli/jailbreak-args.js +0 -86
  112. package/dist/cli/jailbreak-args.js.map +0 -1
  113. package/dist/commands/jailbreak.d.ts +0 -49
  114. package/dist/commands/jailbreak.d.ts.map +0 -1
  115. package/dist/commands/jailbreak.js +0 -201
  116. package/dist/commands/jailbreak.js.map +0 -1
  117. package/dist/core/compactor.d.ts +0 -42
  118. package/dist/core/compactor.d.ts.map +0 -1
  119. package/dist/core/compactor.js +0 -128
  120. package/dist/core/compactor.js.map +0 -1
  121. package/dist/core/token-optimizer.d.ts +0 -58
  122. package/dist/core/token-optimizer.d.ts.map +0 -1
  123. package/dist/core/token-optimizer.js +0 -179
  124. package/dist/core/token-optimizer.js.map +0 -1
  125. package/dist/encoding/index.d.ts +0 -6
  126. package/dist/encoding/index.d.ts.map +0 -1
  127. package/dist/encoding/index.js +0 -6
  128. package/dist/encoding/index.js.map +0 -1
  129. package/dist/encoding/l4-encoder.d.ts +0 -96
  130. package/dist/encoding/l4-encoder.d.ts.map +0 -1
  131. package/dist/encoding/l4-encoder.js +0 -474
  132. package/dist/encoding/l4-encoder.js.map +0 -1
  133. package/dist/fuzzing/index.d.ts +0 -6
  134. package/dist/fuzzing/index.d.ts.map +0 -1
  135. package/dist/fuzzing/index.js +0 -6
  136. package/dist/fuzzing/index.js.map +0 -1
  137. package/dist/fuzzing/mutation-engine.d.ts +0 -93
  138. package/dist/fuzzing/mutation-engine.d.ts.map +0 -1
  139. package/dist/fuzzing/mutation-engine.js +0 -244
  140. package/dist/fuzzing/mutation-engine.js.map +0 -1
  141. package/dist/jailbreak-extension.d.ts +0 -48
  142. package/dist/jailbreak-extension.d.ts.map +0 -1
  143. package/dist/jailbreak-extension.js +0 -47
  144. package/dist/jailbreak-extension.js.map +0 -1
  145. package/dist/modules/index.d.ts +0 -8
  146. package/dist/modules/index.d.ts.map +0 -1
  147. package/dist/modules/index.js +0 -7
  148. package/dist/modules/index.js.map +0 -1
  149. package/dist/modules/lrl-bypass.d.ts +0 -40
  150. package/dist/modules/lrl-bypass.d.ts.map +0 -1
  151. package/dist/modules/lrl-bypass.js +0 -84
  152. package/dist/modules/lrl-bypass.js.map +0 -1
  153. package/dist/modules/lrl-deps.d.ts +0 -38
  154. package/dist/modules/lrl-deps.d.ts.map +0 -1
  155. package/dist/modules/lrl-deps.js +0 -128
  156. package/dist/modules/lrl-deps.js.map +0 -1
  157. package/dist/multiturn/easl-chain.d.ts +0 -113
  158. package/dist/multiturn/easl-chain.d.ts.map +0 -1
  159. package/dist/multiturn/easl-chain.js +0 -351
  160. package/dist/multiturn/easl-chain.js.map +0 -1
  161. package/dist/multiturn/index.d.ts +0 -6
  162. package/dist/multiturn/index.d.ts.map +0 -1
  163. package/dist/multiturn/index.js +0 -6
  164. package/dist/multiturn/index.js.map +0 -1
  165. package/dist/routing/fallback-router.d.ts +0 -123
  166. package/dist/routing/fallback-router.d.ts.map +0 -1
  167. package/dist/routing/fallback-router.js +0 -355
  168. package/dist/routing/fallback-router.js.map +0 -1
  169. package/dist/routing/index.d.ts +0 -6
  170. package/dist/routing/index.d.ts.map +0 -1
  171. package/dist/routing/index.js +0 -6
  172. package/dist/routing/index.js.map +0 -1
  173. package/dist/types/jailbreak.d.ts +0 -246
  174. package/dist/types/jailbreak.d.ts.map +0 -1
  175. package/dist/types/jailbreak.js +0 -8
  176. package/dist/types/jailbreak.js.map +0 -1
  177. package/dist/utils/jailbreak/common.d.ts +0 -63
  178. package/dist/utils/jailbreak/common.d.ts.map +0 -1
  179. package/dist/utils/jailbreak/common.js +0 -130
  180. package/dist/utils/jailbreak/common.js.map +0 -1
  181. package/dist/utils/jailbreak/index.d.ts +0 -7
  182. package/dist/utils/jailbreak/index.d.ts.map +0 -1
  183. package/dist/utils/jailbreak/index.js +0 -7
  184. package/dist/utils/jailbreak/index.js.map +0 -1
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Deterministic capability-routing / validation layer for the subagent dispatcher.
3
+ *
4
+ * Parses optional `skills` / `mcp` / `hooks` declared in an agent file's
5
+ * frontmatter, validates them against the live skill catalog on disk (pruning
6
+ * unknown names — anti-hallucination), resolves skill names to absolute
7
+ * `SKILL.md` paths, and produces:
8
+ * (a) a deterministic preamble string describing the granted capabilities,
9
+ * (b) CLI args that enforce catalog restriction via the existing
10
+ * `--no-skills` + `--skill <path>` flags (no core changes required).
11
+ *
12
+ * Self-contained: depends only on `node:fs`, `node:path`, `node:os`. No
13
+ * external packages, no imports from other project files.
14
+ *
15
+ * @module capabilities
16
+ */
17
+
18
+ import * as fs from "node:fs";
19
+ import * as os from "node:os";
20
+ import * as path from "node:path";
21
+
22
+ /** Skills / MCP / hooks declared in an agent file's frontmatter, normalized and deduped. */
23
+ export interface AgentCapabilities {
24
+ readonly skills: readonly string[];
25
+ readonly mcp: readonly string[];
26
+ readonly hooks: readonly string[];
27
+ }
28
+
29
+ /** The live catalog to validate declared capabilities against. */
30
+ export interface CapabilityCatalog {
31
+ /** Lowercased skill name -> absolute SKILL.md path. */
32
+ readonly skills: ReadonlyMap<string, string>;
33
+ readonly hooks: ReadonlySet<string>;
34
+ readonly mcp: ReadonlySet<string>;
35
+ }
36
+
37
+ /** A capabilities declaration split into valid (catalog-known) vs unknown names. */
38
+ export interface ValidatedCapabilities {
39
+ readonly skills: readonly string[]; // valid names, order preserved, deduped
40
+ readonly mcp: readonly string[];
41
+ readonly hooks: readonly string[];
42
+ readonly unknownSkills: readonly string[];
43
+ readonly unknownMcp: readonly string[];
44
+ readonly unknownHooks: readonly string[];
45
+ }
46
+
47
+ /**
48
+ * Compiled-in hook set — the 16 always-on hooks sourced from
49
+ * `packages/coding-agent/src/core/hook-inventory.ts`. These are compiled into
50
+ * the OMK binary. This list MUST be updated if the runtime hook set changes.
51
+ */
52
+ export const VALID_HOOKS: readonly string[] = [
53
+ "awesome-agent-skills-router",
54
+ "branch-diff-snapshot",
55
+ "eslint-after-edit",
56
+ "notify-sound-on-stop",
57
+ "npm-audit-summary",
58
+ "post-format",
59
+ "post-init-mcp",
60
+ "pre-shell-guard",
61
+ "precompact-checkpoint",
62
+ "protect-secrets",
63
+ "release-check-before-stop",
64
+ "session-context",
65
+ "stop-verify",
66
+ "subagent-stop-audit",
67
+ "typecheck-after-edit",
68
+ "worktree-create-guard",
69
+ ];
70
+
71
+ /**
72
+ * Compiled-in MCP server set — the 18 configured servers sourced from the MCP
73
+ * configuration. This list MUST be updated if the runtime MCP set changes.
74
+ */
75
+ export const VALID_MCP: readonly string[] = [
76
+ "adaptorch",
77
+ "chrome-devtools",
78
+ "context7",
79
+ "fetch",
80
+ "filesystem",
81
+ "firecrawl",
82
+ "github",
83
+ "lean-ctx",
84
+ "memory",
85
+ "obsidian",
86
+ "ouroboros",
87
+ "playwright",
88
+ "serena",
89
+ "supermemory",
90
+ "understand-anything",
91
+ "zai-reader",
92
+ "zai-vision",
93
+ "zai-zread",
94
+ ];
95
+
96
+ const SKILL_FILE_NAME = "SKILL.md";
97
+
98
+ /** True for non-empty strings. */
99
+ function isNonEmptyString(value: unknown): value is string {
100
+ return typeof value === "string" && value.length > 0;
101
+ }
102
+
103
+ /**
104
+ * Coerce a frontmatter value (a comma-separated string OR an array of strings)
105
+ * into a clean list: trim every element, drop empties, dedupe while preserving
106
+ * first-seen order.
107
+ */
108
+ function normalizeList(value: unknown): string[] {
109
+ const raw: readonly unknown[] = typeof value === "string" ? value.split(",") : Array.isArray(value) ? value : [];
110
+ const seen = new Set<string>();
111
+ const out: string[] = [];
112
+ for (const item of raw) {
113
+ const coerced = typeof item === "string" ? item : String(item);
114
+ const trimmed = coerced.trim();
115
+ if (trimmed.length === 0) continue;
116
+ if (seen.has(trimmed)) continue;
117
+ seen.add(trimmed);
118
+ out.push(trimmed);
119
+ }
120
+ return out;
121
+ }
122
+
123
+ /**
124
+ * Read `frontmatter.skills` / `frontmatter.mcp` / `frontmatter.hooks`. Each may
125
+ * be a comma-separated string or an array of strings; values are trimmed,
126
+ * empties dropped, and order preserved with dedupe. Returns `undefined` when
127
+ * all three are empty/absent.
128
+ */
129
+ export function parseCapabilities(frontmatter: Record<string, unknown>): AgentCapabilities | undefined {
130
+ const skills = normalizeList(frontmatter.skills);
131
+ const mcp = normalizeList(frontmatter.mcp);
132
+ const hooks = normalizeList(frontmatter.hooks);
133
+ if (skills.length === 0 && mcp.length === 0 && hooks.length === 0) {
134
+ return undefined;
135
+ }
136
+ return { skills, mcp, hooks };
137
+ }
138
+
139
+ /** Strip a single layer of matching surrounding single or double quotes. */
140
+ function stripQuotes(value: string): string {
141
+ const len = value.length;
142
+ if (len >= 2) {
143
+ const first = value[0];
144
+ const last = value[len - 1];
145
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
146
+ return value.slice(1, -1).trim();
147
+ }
148
+ }
149
+ return value;
150
+ }
151
+
152
+ /**
153
+ * Extract the top-level `name:` value from a `SKILL.md` YAML frontmatter block.
154
+ * Returns `null` when no frontmatter or no top-level `name:` key is present.
155
+ * Nested (indented) `name:` keys are intentionally ignored.
156
+ */
157
+ function parseSkillName(content: string): string | null {
158
+ const lines = content.split("\n");
159
+ if (lines.length === 0 || lines[0].trim() !== "---") return null;
160
+ for (let i = 1; i < lines.length; i++) {
161
+ const line = lines[i];
162
+ if (line.trim() === "---") return null; // closing delimiter before any name
163
+ if (!line.startsWith("name:")) continue;
164
+ const raw = line.slice("name:".length).trim();
165
+ if (raw.length === 0) continue;
166
+ return stripQuotes(raw);
167
+ }
168
+ return null;
169
+ }
170
+
171
+ /** Scan one catalog root for `SKILL.md` files and register them into `skills`. Missing/unreadable roots are skipped. */
172
+ function addSkillsFromRoot(root: string, skills: Map<string, string>): void {
173
+ let entries: string[];
174
+ try {
175
+ entries = fs.readdirSync(root, { recursive: true, encoding: "utf8" });
176
+ } catch {
177
+ return;
178
+ }
179
+ // Sort for deterministic "FIRST wins on collision" ordering within a root.
180
+ entries.sort();
181
+ for (const rel of entries) {
182
+ if (path.basename(rel) !== SKILL_FILE_NAME) continue;
183
+ const abs = path.resolve(root, rel);
184
+ let stat: fs.Stats;
185
+ try {
186
+ stat = fs.statSync(abs);
187
+ } catch {
188
+ continue;
189
+ }
190
+ if (!stat.isFile()) continue;
191
+ let content: string;
192
+ try {
193
+ content = fs.readFileSync(abs, "utf-8");
194
+ } catch {
195
+ continue;
196
+ }
197
+ const parsed = parseSkillName(content);
198
+ const name = (parsed ?? path.basename(path.dirname(abs))).toLowerCase();
199
+ if (!isNonEmptyString(name)) continue;
200
+ if (!skills.has(name)) skills.set(name, abs); // FIRST wins, deterministic
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Build the live capability catalog by scanning `SKILL.md` files under the
206
+ * standard agent skill roots plus the user skills directory. For each
207
+ * `SKILL.md`: the name is the frontmatter `name:` value if present, else the
208
+ * parent directory name; lowercased; mapped to its absolute path (first wins
209
+ * on collision). Hooks/MCP sets are the compiled-in `VALID_HOOKS` / `VALID_MCP`.
210
+ *
211
+ * Roots scanned (each wrapped so a missing dir is skipped):
212
+ * `<agentDir>/skills`, `<agentDir>/omk-ui`, `<agentDir>/plugins`,
213
+ * `<agentDir>/packages`, `<agentDir>/googleworkspace-cli`, `<agentDir>/git`,
214
+ * and `userSkillsDir` (default `~/.agents/skills`).
215
+ */
216
+ export function buildCapabilityCatalog(opts: { agentDir: string; userSkillsDir?: string }): CapabilityCatalog {
217
+ const skills = new Map<string, string>();
218
+ const roots: readonly string[] = [
219
+ path.join(opts.agentDir, "skills"),
220
+ path.join(opts.agentDir, "omk-ui"),
221
+ path.join(opts.agentDir, "plugins"),
222
+ path.join(opts.agentDir, "packages"),
223
+ path.join(opts.agentDir, "googleworkspace-cli"),
224
+ path.join(opts.agentDir, "git"),
225
+ opts.userSkillsDir ?? path.join(os.homedir(), ".agents", "skills"),
226
+ ];
227
+ for (const root of roots) addSkillsFromRoot(root, skills);
228
+ return {
229
+ skills,
230
+ hooks: new Set(VALID_HOOKS),
231
+ mcp: new Set(VALID_MCP),
232
+ };
233
+ }
234
+
235
+ /** Partition + dedupe a list into kept (valid) vs dropped (unknown), preserving first-seen order. */
236
+ function partitionDedupe(
237
+ items: readonly string[],
238
+ isValid: (name: string) => boolean,
239
+ ): { readonly kept: string[]; readonly dropped: string[] } {
240
+ const seen = new Set<string>();
241
+ const kept: string[] = [];
242
+ const dropped: string[] = [];
243
+ for (const item of items) {
244
+ if (seen.has(item)) continue;
245
+ seen.add(item);
246
+ if (isValid(item)) kept.push(item);
247
+ else dropped.push(item);
248
+ }
249
+ return { kept, dropped };
250
+ }
251
+
252
+ /**
253
+ * Partition declared capabilities into valid (catalog-known) vs unknown.
254
+ * Skills match case-insensitively (catalog keys are lowercased); hooks and MCP
255
+ * match exactly. Order is preserved and each side is deduped.
256
+ */
257
+ export function validateCapabilities(caps: AgentCapabilities, catalog: CapabilityCatalog): ValidatedCapabilities {
258
+ const skills = partitionDedupe(caps.skills, (name) => catalog.skills.has(name.toLowerCase()));
259
+ const mcp = partitionDedupe(caps.mcp, (name) => catalog.mcp.has(name));
260
+ const hooks = partitionDedupe(caps.hooks, (name) => catalog.hooks.has(name));
261
+ return {
262
+ skills: skills.kept,
263
+ unknownSkills: skills.dropped,
264
+ mcp: mcp.kept,
265
+ unknownMcp: mcp.dropped,
266
+ hooks: hooks.kept,
267
+ unknownHooks: hooks.dropped,
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Resolve valid (catalog-known) skill names to their absolute `SKILL.md` paths.
273
+ * Names with no catalog entry are dropped. Order preserved.
274
+ */
275
+ export function resolveSkillPaths(names: readonly string[], catalog: CapabilityCatalog): string[] {
276
+ const out: string[] = [];
277
+ for (const name of names) {
278
+ const resolved = catalog.skills.get(name.toLowerCase());
279
+ if (resolved !== undefined) out.push(resolved);
280
+ }
281
+ return out;
282
+ }
283
+
284
+ /**
285
+ * Build the CLI args that enforce catalog-only skill loading via the existing
286
+ * `--no-skills` + `--skill <path>` flags. Returns `[]` when no skills are
287
+ * granted (the caller simply omits enforcement in that case). Otherwise:
288
+ * `['--no-skills', '--skill', p1, '--skill', p2, ...]`.
289
+ */
290
+ export function buildEnforcementArgs(validated: ValidatedCapabilities, catalog: CapabilityCatalog): string[] {
291
+ if (validated.skills.length === 0) return [];
292
+ const args: string[] = ["--no-skills"];
293
+ for (const skillPath of resolveSkillPaths(validated.skills, catalog)) {
294
+ args.push("--skill", skillPath);
295
+ }
296
+ return args;
297
+ }
298
+
299
+ /**
300
+ * Build a deterministic, multi-line preamble describing the granted
301
+ * capabilities. Lines: `Allowed skills: ...`, `Allowed MCP: ...`,
302
+ * `Relevant hooks: ...` (each `none` when empty). If any unknown* arrays are
303
+ * non-empty, appends one warning line listing them.
304
+ */
305
+ export function buildCapabilitiesPreamble(validated: ValidatedCapabilities): string {
306
+ const lines: string[] = [
307
+ `Allowed skills: ${validated.skills.length > 0 ? validated.skills.join(", ") : "none"}`,
308
+ `Allowed MCP: ${validated.mcp.length > 0 ? validated.mcp.join(", ") : "none"}`,
309
+ `Relevant hooks: ${validated.hooks.length > 0 ? validated.hooks.join(", ") : "none"}`,
310
+ ];
311
+
312
+ const unknownParts: string[] = [];
313
+ if (validated.unknownSkills.length > 0) unknownParts.push(`skills: [${validated.unknownSkills.join(", ")}]`);
314
+ if (validated.unknownMcp.length > 0) unknownParts.push(`mcp: [${validated.unknownMcp.join(", ")}]`);
315
+ if (validated.unknownHooks.length > 0) unknownParts.push(`hooks: [${validated.unknownHooks.join(", ")}]`);
316
+ if (unknownParts.length > 0) {
317
+ lines.push(`WARNING: ignored unknown capabilities — ${unknownParts.join("; ")}`);
318
+ }
319
+
320
+ return lines.join("\n");
321
+ }
322
+
323
+ const CAPABILITY_SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
324
+
325
+ /**
326
+ * Extract a comma/sentence-separated slug list from the first line under the
327
+ * embedded "Assigned capabilities" section that matches `labelRe`. Parenthetical
328
+ * descriptions `(...)` are stripped first; tokens are then split on comma,
329
+ * semicolon, period, and em-dash, lowercased, and kept only if they look like a
330
+ * bare slug. A line whose value contains "No direct OMK skill match" yields no
331
+ * slugs (it is the explicit no-match marker, not a citation list).
332
+ */
333
+ function extractCapabilitySlugs(segment: string, labelRe: RegExp): readonly string[] {
334
+ const out: string[] = [];
335
+ for (const line of segment.split("\n")) {
336
+ const m = line.match(labelRe);
337
+ if (!m || m.index === undefined) continue;
338
+ let rest = line.slice(m.index + m[0].length);
339
+ if (/no direct omk skill match/i.test(rest)) return out;
340
+ rest = rest.replace(/\([^)]*\)/g, "");
341
+ for (const raw of rest.split(/[,;.\u2014]/)) {
342
+ const slug = raw.trim().toLowerCase();
343
+ if (slug !== "" && CAPABILITY_SLUG_RE.test(slug) && !out.includes(slug)) out.push(slug);
344
+ }
345
+ return out;
346
+ }
347
+ return out;
348
+ }
349
+
350
+ /**
351
+ * Parse the deterministic "Assigned capabilities (verified ...)" section that
352
+ * the broad-catalog agent files carry in their body (not frontmatter). Returns
353
+ * the cited skills/mcp/hooks, or `undefined` when the section is absent or all
354
+ * three lists are empty. This lets the ~233 agents that declare capabilities
355
+ * in prose be validated and enforced at spawn time without a frontmatter
356
+ * migration. Frontmatter declarations (via `parseCapabilities`) remain the
357
+ * canonical source; this is the fallback for the existing embedded form.
358
+ */
359
+ export function parseEmbeddedCapabilities(systemPrompt: string): AgentCapabilities | undefined {
360
+ const idx = systemPrompt.indexOf("Assigned capabilities");
361
+ if (idx < 0) return undefined;
362
+ const segment = systemPrompt.slice(idx);
363
+ const skills = extractCapabilitySlugs(segment, /^-\s*Skills\s*:\s*/i);
364
+ const mcp = extractCapabilitySlugs(segment, /^-\s*MCP to request[^:]*:\s*/i);
365
+ const hooks = extractCapabilitySlugs(segment, /^-\s*Hooks relevant[^:]*:\s*/i);
366
+ if (skills.length === 0 && mcp.length === 0 && hooks.length === 0) return undefined;
367
+ return { skills, mcp, hooks };
368
+ }