open-multi-agent-kit 0.90.4 → 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 (89) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +8 -8
  3. package/dist/core/adaptorch-bridge.d.ts +14 -21
  4. package/dist/core/adaptorch-bridge.d.ts.map +1 -1
  5. package/dist/core/adaptorch-bridge.js +11 -18
  6. package/dist/core/adaptorch-bridge.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +6 -39
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +7 -99
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  12. package/dist/core/context-budget-v2-planner.js +10 -2
  13. package/dist/core/context-budget-v2-planner.js.map +1 -1
  14. package/dist/core/context-budget-v2-types.d.ts +7 -1
  15. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  16. package/dist/core/context-budget-v2-types.js.map +1 -1
  17. package/dist/core/reasoning-router-bias.d.ts +11 -23
  18. package/dist/core/reasoning-router-bias.d.ts.map +1 -1
  19. package/dist/core/reasoning-router-bias.js +9 -18
  20. package/dist/core/reasoning-router-bias.js.map +1 -1
  21. package/dist/core/reasoning-router-resolver.d.ts +44 -0
  22. package/dist/core/reasoning-router-resolver.d.ts.map +1 -0
  23. package/dist/core/reasoning-router-resolver.js +79 -0
  24. package/dist/core/reasoning-router-resolver.js.map +1 -0
  25. package/dist/core/reasoning-router-v4-weights.d.ts +28 -72
  26. package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
  27. package/dist/core/reasoning-router-v4-weights.js +16 -60
  28. package/dist/core/reasoning-router-v4-weights.js.map +1 -1
  29. package/dist/core/reasoning-router-v4.d.ts +25 -70
  30. package/dist/core/reasoning-router-v4.d.ts.map +1 -1
  31. package/dist/core/reasoning-router-v4.js +46 -144
  32. package/dist/core/reasoning-router-v4.js.map +1 -1
  33. package/dist/core/router-feedback-collector.d.ts +13 -16
  34. package/dist/core/router-feedback-collector.d.ts.map +1 -1
  35. package/dist/core/router-feedback-collector.js +5 -8
  36. package/dist/core/router-feedback-collector.js.map +1 -1
  37. package/dist/core/slash-commands.d.ts.map +1 -1
  38. package/dist/core/slash-commands.js +1 -1
  39. package/dist/core/slash-commands.js.map +1 -1
  40. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  41. package/dist/modes/interactive/interactive-mode.js +16 -98
  42. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  43. package/docs/usage.md +11 -12
  44. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  45. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  46. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  47. package/examples/extensions/gondolin/package-lock.json +2 -2
  48. package/examples/extensions/gondolin/package.json +1 -1
  49. package/examples/extensions/sandbox/package-lock.json +2 -2
  50. package/examples/extensions/sandbox/package.json +1 -1
  51. package/examples/extensions/subagent/agent-capability-router.test.ts +123 -0
  52. package/examples/extensions/subagent/agent-capability-router.ts +274 -0
  53. package/examples/extensions/subagent/agents/planner.md +3 -0
  54. package/examples/extensions/subagent/agents/reviewer.md +3 -0
  55. package/examples/extensions/subagent/agents/scout.md +3 -0
  56. package/examples/extensions/subagent/agents/worker.md +2 -0
  57. package/examples/extensions/subagent/agents.ts +14 -0
  58. package/examples/extensions/subagent/capabilities.ts +368 -0
  59. package/examples/extensions/subagent/domain-profiles.ts +1089 -0
  60. package/examples/extensions/subagent/index.ts +71 -3
  61. package/examples/extensions/with-deps/package-lock.json +2 -2
  62. package/examples/extensions/with-deps/package.json +1 -1
  63. package/examples/sdk/12-full-control.ts +1 -1
  64. package/npm-shrinkwrap.json +12 -12
  65. package/package.json +4 -4
  66. package/dist/core/compactor.d.ts +0 -42
  67. package/dist/core/compactor.d.ts.map +0 -1
  68. package/dist/core/compactor.js +0 -128
  69. package/dist/core/compactor.js.map +0 -1
  70. package/dist/core/reasoning-router-v2.d.ts +0 -94
  71. package/dist/core/reasoning-router-v2.d.ts.map +0 -1
  72. package/dist/core/reasoning-router-v2.js +0 -288
  73. package/dist/core/reasoning-router-v2.js.map +0 -1
  74. package/dist/core/reasoning-router-v3.d.ts +0 -15
  75. package/dist/core/reasoning-router-v3.d.ts.map +0 -1
  76. package/dist/core/reasoning-router-v3.js +0 -193
  77. package/dist/core/reasoning-router-v3.js.map +0 -1
  78. package/dist/core/reasoning-router-weights.d.ts +0 -155
  79. package/dist/core/reasoning-router-weights.d.ts.map +0 -1
  80. package/dist/core/reasoning-router-weights.js +0 -204
  81. package/dist/core/reasoning-router-weights.js.map +0 -1
  82. package/dist/core/reasoning-router.d.ts +0 -44
  83. package/dist/core/reasoning-router.d.ts.map +0 -1
  84. package/dist/core/reasoning-router.js +0 -138
  85. package/dist/core/reasoning-router.js.map +0 -1
  86. package/dist/core/token-optimizer.d.ts +0 -58
  87. package/dist/core/token-optimizer.d.ts.map +0 -1
  88. package/dist/core/token-optimizer.js +0 -179
  89. package/dist/core/token-optimizer.js.map +0 -1
package/docs/usage.md CHANGED
@@ -38,7 +38,7 @@ Type `/` in the editor to open command completion. Extensions can register custo
38
38
  |---------|-------------|
39
39
  | `/login`, `/logout` | Manage OAuth or API-key credentials |
40
40
  | `/model` | Switch models, then choose thinking level |
41
- | `/think` | Choose thinking level, or `auto`/`auto-v2`/`auto-v3`/`auto-v4` to route per task |
41
+ | `/think` | Choose thinking level, or `auto` to route per task through v4 |
42
42
  | `/scoped-models` | Enable/disable models for Ctrl+P cycling |
43
43
  | `/settings` | Thinking level, theme, message delivery, transport |
44
44
  | `/resume` | Pick from previous sessions |
@@ -61,35 +61,34 @@ Type `/` in the editor to open command completion. Extensions can register custo
61
61
 
62
62
  `/think <level>` sets the thinking level manually. Manual levels always win: choosing a concrete level leaves auto routing, so no auto router runs.
63
63
 
64
- `/think auto` remains the historical v1 local classifier and rule table (no network calls). `/think auto-v2`, `/think auto v2`, or `/think auto:v2` enables the calibrated v2 weighted router. `/think auto-v3`, `/think auto v3`, or `/think auto:v3` enables the contextual v3 router. `/think auto-v4`, `/think auto v4`, or `/think auto:v4` enables the confidence-bearing v4 router. Use `/think auto-v1`, `/think auto v1`, or `/think auto:v1` to return explicitly to v1.
64
+ `/think auto` enables the deterministic local v4 router (no network calls). Versioned auto commands are no longer accepted; choose `/think auto` for automatic routing or `/think <level>` for a manual override.
65
65
 
66
- The active auto routers classify each prompt into a task class (trivial, simple edit, code generation, debug, refactor, review, plan) and map it to a recommended level, from `minimal` for trivial prompts up to `xhigh` for planning work.
66
+ The auto router classifies each prompt into a task class (trivial, simple edit, code generation, debug, refactor, review, plan) and maps it to a recommended level, from `minimal` for trivial prompts up to `xhigh` for planning work.
67
67
 
68
68
  The routing core is deterministic and local. It looks only at bounded turn signals such as:
69
69
 
70
70
  - prompt length
71
71
  - presence of code fences or diff markers
72
- - keyword families; v2 separates strong signals from ambiguous weak signals such as `fix`
73
- - leading intent, localized edit objects, diagnostic evidence, review scope, plan briefs, refactor cues, and implementation objects, when v3 or v4 is selected
74
- - bounded negation of those whole-prompt matches and short-range compound-intent detection across a leading conjunction, when v4 is selected
75
- - recent auto-router task history in the session, when v2 is selected
76
- - context pressure buckets, when v2 is selected
72
+ - keyword families
73
+ - leading intent, localized edit objects, diagnostic evidence, review scope, plan briefs, refactor cues, and implementation objects
74
+ - bounded negation of whole-prompt matches and short-range compound-intent detection across a leading conjunction
75
+ - recent auto-router task history in the session
76
+ - context pressure buckets
77
77
  - the subagent lane type, when one is set
78
78
 
79
- v4 additionally reports a confidence band (`high`, `medium`, `low`), the score margin between the top two task classes, and a fallback reason on every turn, none of which carry prompt text. When confidence is low or no weighted signal decided the class, v4 raises the resolved level by one step above what the same task class would otherwise resolve to; confidence never lowers it. v4 is checked against a gold-set evaluation harness with fixed train/dev/holdout splits and accuracy, macro-F1, severe-under-allocation, and class-flip/McNemar checks, which confirmed v4 classifies identically to v3 across the full gold set, including the rows held out from tuning.
79
+ v4 reports a confidence band (`high`, `medium`, `low`), the score margin between the top two task classes, and a fallback reason on every turn, none of which carry prompt text. When confidence is low or no weighted signal decided the class, v4 raises the resolved level by one step above what the same task class would otherwise resolve to; confidence never lowers it. v4 is checked against a gold-set evaluation harness with fixed train/dev/holdout splits and accuracy, macro-F1, severe-under-allocation, and class-flip/McNemar checks.
80
80
 
81
81
  Precedence:
82
82
 
83
83
  - Manual `/think <level>` always wins.
84
84
  - The router only resolves levels while `auto` mode is active.
85
85
  - Auto-resolved levels apply per turn only and never overwrite the persisted default thinking level in settings.
86
- - v1 remains the default auto router; v2, v3, and v4 must be selected explicitly for the current session.
87
86
 
88
87
  Resolved levels are clamped to the model's capabilities: models without `xhigh`/`max` are capped at their highest supported level, and models without reasoning support bypass the router entirely.
89
88
 
90
- The v4 learning path is available only through the global `reasoningRouterLearning` setting and is off by default. When `reasoningRouterLearning.enabled` is `true`, auto-v4 loads one validated bias snapshot for the session, applies a bounded `-2..2` ladder-step bias, and appends a privacy-safe feedback record containing only bounded enums, booleans, and buckets. Project-local settings cannot enable or redirect this feature, and the ledger never stores raw prompts, file paths, diffs, session identifiers, model/provider payloads, tool output, or hook output.
89
+ The v4 learning path is available only through the global `reasoningRouterLearning` setting and is off by default. When `reasoningRouterLearning.enabled` is `true`, `/think auto` loads one validated bias snapshot for the session, applies a bounded `-2..2` ladder-step bias, and appends a privacy-safe feedback record containing only bounded enums, booleans, and buckets. Project-local settings cannot enable or redirect this feature, and the ledger never stores raw prompts, file paths, diffs, session identifiers, model/provider payloads, tool output, or hook output.
91
90
 
92
- The Adaptorch advisory bridge module still ships as default-off groundwork only. It has no settings key, command, transport, or session call site yet, so it does not affect `/think auto-v4` until a future transport and security review explicitly wire it.
91
+ The Adaptorch advisory bridge module still ships as default-off groundwork only. It has no settings key, command, transport, or session call site yet, so it does not affect `/think auto` until a future transport and security review explicitly wire it.
93
92
 
94
93
  ## Message Queue
95
94
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omk-extension-custom-provider",
3
- "version": "0.90.4",
3
+ "version": "0.90.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omk-extension-custom-provider",
9
- "version": "0.90.4",
9
+ "version": "0.90.5",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.52.0"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "omk-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "0.90.4",
4
+ "version": "0.90.5",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "omk-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "0.90.4",
4
+ "version": "0.90.5",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omk-extension-gondolin",
3
- "version": "0.90.4",
3
+ "version": "0.90.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omk-extension-gondolin",
9
- "version": "0.90.4",
9
+ "version": "0.90.5",
10
10
  "dependencies": {
11
11
  "@earendil-works/gondolin": "0.12.0"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "omk-extension-gondolin",
3
3
  "private": true,
4
- "version": "0.90.4",
4
+ "version": "0.90.5",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omk-extension-sandbox",
3
- "version": "0.90.4",
3
+ "version": "0.90.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omk-extension-sandbox",
9
- "version": "0.90.4",
9
+ "version": "0.90.5",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sandbox-runtime": "0.0.26"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "omk-extension-sandbox",
3
3
  "private": true,
4
- "version": "0.90.4",
4
+ "version": "0.90.5",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Deterministic unit tests for the agent → capability router.
3
+ *
4
+ * The router is fully deterministic (no LLM, no randomness, no I/O inside the
5
+ * router itself), so every assertion here is an exact expected value. The only
6
+ * I/O is the one-time module-scope catalog build, which scans the live skill
7
+ * tree under the agent directory.
8
+ */
9
+ import { describe, expect, it } from "vitest";
10
+ import { auditCapabilities, classifyAgent, deriveCapabilities } from "./agent-capability-router.ts";
11
+ import { buildCapabilityCatalog } from "./capabilities.ts";
12
+
13
+ // Built once at module scope from the live skill tree under the agent
14
+ // directory. This is populated during local verification but empty in CI (the
15
+ // agent skill catalog is not checked out on the runner), so the skill-catalog
16
+ // -dependent assertions below are guarded with `skipIf(!hasSkillCatalog)`. The
17
+ // MCP/hooks sets are a fixed OMK list and stay populated everywhere.
18
+ const catalog = buildCapabilityCatalog({ agentDir: `${process.env.HOME ?? "/home/yu"}/.omk/agent` });
19
+ const hasSkillCatalog = catalog.skills.size > 0;
20
+
21
+ describe("classifyAgent", () => {
22
+ it("classifies by name token: seo-specialist → marketing-content", () => {
23
+ expect(classifyAgent("seo-specialist", "x")?.id).toBe("marketing-content");
24
+ });
25
+
26
+ it("classifies smart-contract-auditor → security", () => {
27
+ expect(classifyAgent("smart-contract-auditor", "x")?.id).toBe("security");
28
+ });
29
+
30
+ it("classifies react-developer → frontend-web", () => {
31
+ expect(classifyAgent("react-developer", "x")?.id).toBe("frontend-web");
32
+ });
33
+
34
+ it("guards the gin→engineer substring regression: ml-engineer is NOT backend-api", () => {
35
+ // "engineer" contains "gin"; a naive first-match classifier would route
36
+ // this to backend-api. Boundary-anchored voting must not.
37
+ expect(classifyAgent("ml-engineer", "AI/ML")?.id).not.toBe("backend-api");
38
+ });
39
+
40
+ it("guards the gis→strategist substring regression: account-strategist is NOT gis-spatial", () => {
41
+ // "strategist" contains "gis"; must not flip into the GIS domain.
42
+ expect(classifyAgent("account-strategist", "x")?.id).not.toBe("gis-spatial");
43
+ });
44
+
45
+ it("returns null when no domain matches", () => {
46
+ expect(classifyAgent("zzz-nonexistent-xyz", "nothing relevant here")).toBeNull();
47
+ });
48
+ });
49
+
50
+ describe("deriveCapabilities", () => {
51
+ it.skipIf(!hasSkillCatalog)(
52
+ "weights the name token: react-developer's first derived skill is react-patterns",
53
+ () => {
54
+ const { skills } = deriveCapabilities("react-developer", "React frontend", catalog);
55
+ expect(skills[0]).toBe("react-patterns");
56
+ },
57
+ );
58
+
59
+ it.skipIf(!hasSkillCatalog)("includes the literal domain skill for seo-specialist", () => {
60
+ const { skills } = deriveCapabilities("seo-specialist", "SEO", catalog);
61
+ expect(skills).toContain("seo");
62
+ });
63
+
64
+ it("returns empty skills for an unmatched domain", () => {
65
+ const { skills } = deriveCapabilities("zzz-unknown", "x", catalog);
66
+ expect(skills.length).toBe(0);
67
+ });
68
+
69
+ it.skipIf(!hasSkillCatalog)("only emits skills that exist in the live catalog", () => {
70
+ const { skills } = deriveCapabilities("react-developer", "React frontend", catalog);
71
+ expect(skills.length).toBeGreaterThan(0);
72
+ for (const skill of skills) {
73
+ expect(catalog.skills.has(skill)).toBe(true);
74
+ }
75
+ });
76
+
77
+ it("derived MCP is a subset of the catalog MCP set", () => {
78
+ const { mcp } = deriveCapabilities("devops-automator", "kubernetes", catalog);
79
+ expect(mcp.length).toBeGreaterThan(0);
80
+ for (const server of mcp) {
81
+ expect(catalog.mcp.has(server)).toBe(true);
82
+ }
83
+ });
84
+ });
85
+
86
+ describe("auditCapabilities", () => {
87
+ it("identical sets → jaccard 1.0 and verdict match", () => {
88
+ const caps = { skills: ["react-patterns"], mcp: [], hooks: [] };
89
+ const result = auditCapabilities(caps, caps, catalog);
90
+ expect(result.jaccard).toBe(1.0);
91
+ expect(result.verdict).toBe("match");
92
+ });
93
+
94
+ it("disjoint sets → jaccard 0 and verdict divergent", () => {
95
+ const declared = { skills: ["react-patterns"], mcp: [], hooks: [] };
96
+ const derived = { skills: ["seo"], mcp: [], hooks: [] };
97
+ const result = auditCapabilities(declared, derived, catalog);
98
+ expect(result.jaccard).toBe(0);
99
+ expect(result.verdict).toBe("divergent");
100
+ });
101
+
102
+ it("half overlap → jaccard within [0,1] and verdict drift or match", () => {
103
+ const declared = { skills: ["react-patterns", "seo"], mcp: [], hooks: [] };
104
+ const derived = { skills: ["react-patterns"], mcp: [], hooks: [] };
105
+ const result = auditCapabilities(declared, derived, catalog);
106
+ expect(result.jaccard).toBeGreaterThanOrEqual(0);
107
+ expect(result.jaccard).toBeLessThanOrEqual(1);
108
+ expect(["drift", "match"]).toContain(result.verdict);
109
+ });
110
+
111
+ it("surfaces declared-but-unknown skills in declaredUnknownSkills", () => {
112
+ const declared = { skills: ["fake-skill-xyz"], mcp: [], hooks: [] };
113
+ const derived = { skills: [], mcp: [], hooks: [] };
114
+ const result = auditCapabilities(declared, derived, catalog);
115
+ expect(result.declaredUnknownSkills).toContain("fake-skill-xyz");
116
+ });
117
+
118
+ it("both-empty skills → jaccard 1.0", () => {
119
+ const empty = { skills: [], mcp: [], hooks: [] };
120
+ const result = auditCapabilities(empty, empty, catalog);
121
+ expect(result.jaccard).toBe(1.0);
122
+ });
123
+ });
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Deterministic agent → capability router.
3
+ *
4
+ * Classifies an agent (by name + description) into a {@link DomainProfile},
5
+ * derives a baseline capability set (skills / MCP / hooks) from the live
6
+ * {@link CapabilityCatalog}, and audits a declared capability set against the
7
+ * deterministic derivation (Jaccard overlap + drift verdict).
8
+ *
9
+ * Fully deterministic: same inputs always yield same outputs. No LLM calls,
10
+ * no randomness, no I/O. Depends only on {@link DOMAIN_PROFILES} and the
11
+ * passed-in catalog.
12
+ *
13
+ * Classification is **name-weighted vote scoring**: a domain's keywords are
14
+ * tested against the agent name and description; matches inside the name (the
15
+ * strongest identity signal) count triple. The domain with the highest score
16
+ * wins (ties resolved by array order, so specific domains should come first).
17
+ * This is robust to the substring false positives that plague first-match
18
+ * classification (e.g. `chem` matching "s**chem**a", `gis` matching
19
+ * "strate**gis**t"): a single accidental description hit cannot outvote a
20
+ * domain that matches the name.
21
+ *
22
+ * @module agent-capability-router
23
+ */
24
+
25
+ import type { AgentCapabilities, CapabilityCatalog } from "./capabilities.ts";
26
+ import { validateCapabilities } from "./capabilities.ts";
27
+ import type { DomainProfile } from "./domain-profiles.ts";
28
+ import { DOMAIN_PROFILES } from "./domain-profiles.ts";
29
+
30
+ /** Maximum number of scored skills to grant. */
31
+ const MAX_SKILLS = 6;
32
+ /** Number of pool skills granted as a fallback when nothing scores above zero. */
33
+ const FALLBACK_SKILLS = 4;
34
+ /** Jaccard threshold (inclusive) for a "match" verdict. */
35
+ const JACCARD_MATCH = 0.5;
36
+ /** Jaccard threshold (inclusive) for a "drift" verdict. */
37
+ const JACCARD_DRIFT = 0.15;
38
+ /** Weight multiplier when a keyword or skill token matches an agent *name* token. */
39
+ const NAME_WEIGHT = 3;
40
+
41
+ /**
42
+ * Generic role-suffix tokens that carry no domain signal and must not be
43
+ * counted as matches (otherwise e.g. "react-developer" would tie
44
+ * "angular-developer" on the "developer" token). These are stripped before
45
+ * both classification voting and derive scoring.
46
+ */
47
+ const GENERIC_ROLE_TOKENS: ReadonlySet<string> = new Set([
48
+ "agent",
49
+ "analyst",
50
+ "architect",
51
+ "auditor",
52
+ "builder",
53
+ "coach",
54
+ "consultant",
55
+ "coordinator",
56
+ "creator",
57
+ "designer",
58
+ "developer",
59
+ "director",
60
+ "editor",
61
+ "engineer",
62
+ "expert",
63
+ "guardian",
64
+ "lead",
65
+ "manager",
66
+ "master",
67
+ "mentor",
68
+ "navigator",
69
+ "officer",
70
+ "operator",
71
+ "planner",
72
+ "producer",
73
+ "reviewer",
74
+ "specialist",
75
+ "steward",
76
+ "strategist",
77
+ "tester",
78
+ "writer",
79
+ "engineers",
80
+ "specialists",
81
+ "managers",
82
+ "developers",
83
+ ]);
84
+
85
+ /**
86
+ * Classify an agent by name + description against {@link DOMAIN_PROFILES} using
87
+ * name-weighted vote scoring. Returns the highest-scoring domain (ties resolve
88
+ * to the earliest in {@link DOMAIN_PROFILES}), or `null` when every domain
89
+ * scores zero — the explicit "No direct OMK skill match" sentinel.
90
+ */
91
+ export function classifyAgent(name: string, description: string): DomainProfile | null {
92
+ const nameLower = name.toLowerCase();
93
+ const text = `${name} ${description}`.toLowerCase();
94
+
95
+ let best: DomainProfile | null = null;
96
+ let bestScore = 0;
97
+ for (const domain of DOMAIN_PROFILES) {
98
+ let score = 0;
99
+ for (const re of domain.keywords) {
100
+ // Boundary-anchored match: a keyword only counts when it lands on a
101
+ // word edge, so bare short keywords ("gin", "gis", "chem") cannot fire
102
+ // inside unrelated words ("engineer", "strategist", "schema").
103
+ if (!keywordHits(re, text)) continue;
104
+ // A keyword that also matches the agent name is a strong identity
105
+ // signal (the name is the most reliable classifier); otherwise it
106
+ // is a weaker description-only hit.
107
+ score += keywordHits(re, nameLower) ? NAME_WEIGHT : 1;
108
+ }
109
+ if (score > bestScore) {
110
+ bestScore = score;
111
+ best = domain;
112
+ }
113
+ }
114
+ return best;
115
+ }
116
+
117
+ /** Split lowercased text into identity tokens: non-word boundaries, length >= 2. */
118
+ function tokenize(lowerText: string): string[] {
119
+ const tokens: string[] = [];
120
+ for (const raw of lowerText.split(/[^a-z0-9+#]+/)) {
121
+ if (raw.length >= 2) tokens.push(raw);
122
+ }
123
+ return tokens;
124
+ }
125
+
126
+ /** Identity tokens with generic role suffixes removed. */
127
+ function identityTokens(lowerText: string): string[] {
128
+ return tokenize(lowerText).filter((t) => !GENERIC_ROLE_TOKENS.has(t));
129
+ }
130
+
131
+ /**
132
+ * Boundary-anchored keyword test. A keyword must start at a left word edge
133
+ * (start-of-text or a non-`[a-z0-9]` neighbor). A **right** boundary is added
134
+ * only for short, all-alphabetic keywords (length <= 4, e.g. "gin", "gis",
135
+ * "chem", "seo"): those are the ones that cause substring false positives
136
+ * ("gin" -> "engineer", "gis" -> "strategist", "chem" -> "schema"). Longer and
137
+ * pattern keywords ("react", "3d-", "smart-?contract", "document-?generat")
138
+ * use prefix semantics (left edge only) so intentional prefix/partial matches
139
+ * still fire. Node >= 10 supports lookbehind.
140
+ */
141
+ function keywordHits(re: RegExp, text: string): boolean {
142
+ const src = re.source;
143
+ const shortBare = src.length <= 4 && /^[a-z]+$/i.test(src);
144
+ const right = shortBare ? "(?![a-z0-9])" : "";
145
+ return new RegExp(`(?<![a-z0-9])${src}${right}`, "i").test(text);
146
+ }
147
+
148
+ /**
149
+ * Score a candidate skill against the agent's name and description tokens.
150
+ * The skill's **first** `-`-split token is the technology word (e.g. "react"
151
+ * in "react-patterns") and is weighted heaviest; a match in the agent name is
152
+ * worth {@link NAME_WEIGHT}x a description match. Generic tokens are skipped.
153
+ */
154
+ function skillMatchScore(
155
+ skill: string,
156
+ nameTokens: ReadonlySet<string>,
157
+ descTokens: ReadonlySet<string>,
158
+ nameLower: string,
159
+ descLower: string,
160
+ ): number {
161
+ const parts = skill.split("-");
162
+ let score = 0;
163
+ for (let i = 0; i < parts.length; i++) {
164
+ const part = parts[i];
165
+ if (part.length === 0 || GENERIC_ROLE_TOKENS.has(part)) continue;
166
+ const isTech = i === 0; // first token = the technology word
167
+ const nameHit = nameTokens.has(part);
168
+ const descHit = descTokens.has(part);
169
+ if (nameHit) score += (isTech ? 5 : 2) * NAME_WEIGHT;
170
+ else if (descHit) score += isTech ? 5 : 2;
171
+ }
172
+ // Full skill string appearing verbatim in name or description is a strong signal.
173
+ if (nameLower.includes(skill)) score += 3 * NAME_WEIGHT;
174
+ else if (descLower.includes(skill)) score += 3;
175
+ return score;
176
+ }
177
+
178
+ /** Dedupe a list preserving first-seen order. */
179
+ function dedupe(items: readonly string[]): string[] {
180
+ const seen = new Set<string>();
181
+ const out: string[] = [];
182
+ for (const item of items) {
183
+ if (seen.has(item)) continue;
184
+ seen.add(item);
185
+ out.push(item);
186
+ }
187
+ return out;
188
+ }
189
+
190
+ /**
191
+ * Deterministically derive a baseline capability set for an agent. When no
192
+ * domain matches, all three lists are empty. Otherwise skills are picked from
193
+ * the domain's pool by name/description relevance (top {@link MAX_SKILLS} with
194
+ * a positive score, falling back to the first {@link FALLBACK_SKILLS} pool
195
+ * entries when nothing scores above zero), and MCP/hooks are the domain
196
+ * defaults intersected with the live catalog.
197
+ */
198
+ export function deriveCapabilities(name: string, description: string, catalog: CapabilityCatalog): AgentCapabilities {
199
+ const domain = classifyAgent(name, description);
200
+ if (domain === null) return { skills: [], mcp: [], hooks: [] };
201
+
202
+ const nameLower = name.toLowerCase();
203
+ const descLower = description.toLowerCase();
204
+ const nameTokens = new Set(identityTokens(nameLower));
205
+ const descTokens = new Set(identityTokens(`${nameLower} ${descLower}`));
206
+
207
+ const candidates = domain.skillPool.filter((s) => catalog.skills.has(s));
208
+ const scored = candidates.map((skill) => ({
209
+ skill,
210
+ score: skillMatchScore(skill, nameTokens, descTokens, nameLower, descLower),
211
+ }));
212
+ const positive = scored.filter((x) => x.score > 0);
213
+
214
+ let selected: string[];
215
+ if (positive.length > 0) {
216
+ positive.sort((a, b) => {
217
+ if (a.score !== b.score) return b.score - a.score;
218
+ if (a.skill < b.skill) return -1;
219
+ if (a.skill > b.skill) return 1;
220
+ return 0;
221
+ });
222
+ selected = positive.slice(0, MAX_SKILLS).map((x) => x.skill);
223
+ } else {
224
+ selected = candidates.slice(0, FALLBACK_SKILLS);
225
+ }
226
+
227
+ const mcp = domain.defaultMcp.filter((m) => catalog.mcp.has(m));
228
+ const hooks = domain.defaultHooks.filter((h) => catalog.hooks.has(h));
229
+
230
+ return { skills: dedupe(selected), mcp, hooks };
231
+ }
232
+
233
+ /** Result of comparing a declared capability set against the deterministic derivation. */
234
+ export interface CapabilityAudit {
235
+ /** Jaccard overlap of declared vs derived skill sets, 0..1 (1.0 when both empty). */
236
+ readonly jaccard: number;
237
+ /** Skills declared but not in the live catalog (from {@link validateCapabilities}). */
238
+ readonly declaredUnknownSkills: readonly string[];
239
+ readonly skillsOnlyInDeclared: readonly string[];
240
+ readonly skillsOnlyInDerived: readonly string[];
241
+ /** "match" (jaccard >= 0.5), "drift" (0.15 <= jaccard < 0.5), "divergent" (jaccard < 0.15). */
242
+ readonly verdict: "match" | "drift" | "divergent";
243
+ }
244
+
245
+ /**
246
+ * Compare a declared (e.g. LLM-assigned or embedded) capability set against the
247
+ * deterministic derivation. The Jaccard coefficient is over the two skill lists
248
+ * treated as sets; both-empty is defined as 1.0.
249
+ */
250
+ export function auditCapabilities(
251
+ declared: AgentCapabilities,
252
+ derived: AgentCapabilities,
253
+ catalog: CapabilityCatalog,
254
+ ): CapabilityAudit {
255
+ const declaredSet = new Set(declared.skills);
256
+ const derivedSet = new Set(derived.skills);
257
+
258
+ let intersection = 0;
259
+ for (const s of declaredSet) if (derivedSet.has(s)) intersection++;
260
+ const unionSize = new Set([...declaredSet, ...derivedSet]).size;
261
+ const jaccard = unionSize === 0 ? 1.0 : intersection / unionSize;
262
+
263
+ const declaredUnknownSkills = validateCapabilities(declared, catalog).unknownSkills;
264
+
265
+ const skillsOnlyInDeclared: string[] = [];
266
+ for (const s of declaredSet) if (!derivedSet.has(s)) skillsOnlyInDeclared.push(s);
267
+ const skillsOnlyInDerived: string[] = [];
268
+ for (const s of derivedSet) if (!declaredSet.has(s)) skillsOnlyInDerived.push(s);
269
+
270
+ const verdict: "match" | "drift" | "divergent" =
271
+ jaccard >= JACCARD_MATCH ? "match" : jaccard >= JACCARD_DRIFT ? "drift" : "divergent";
272
+
273
+ return { jaccard, declaredUnknownSkills, skillsOnlyInDeclared, skillsOnlyInDerived, verdict };
274
+ }
@@ -9,6 +9,9 @@ You are a planning specialist. You receive context (from a scout) and requiremen
9
9
 
10
10
  You must NOT make any changes. Only read, analyze, and plan.
11
11
 
12
+ Assigned capabilities (see ~/.omk/agent/SUBAGENTS.md for the full routing reference):
13
+ - Skills: ulw-plan (explore-first planning, decision-complete plans), packages (if the codebase spans multiple packages), deep-interview (only if requirements are still fuzzy after reading the scout's context). Load only what the task needs; do not bulk-load the catalog.
14
+
12
15
  Input format you'll receive:
13
16
  - Context/findings from a scout agent
14
17
  - Original query or requirements
@@ -10,6 +10,9 @@ You are a senior code reviewer. Analyze code for quality, security, and maintain
10
10
  Bash is for read-only commands only: `git diff`, `git log`, `git show`. Do NOT modify files or run builds.
11
11
  Assume tool permissions are not perfectly enforceable; keep all bash usage strictly read-only.
12
12
 
13
+ Assigned capabilities (see ~/.omk/agent/SUBAGENTS.md for the full routing reference):
14
+ - Skills: review-work (structured multi-angle review pass), security-review (when the diff touches auth, secrets, or user input). Load only what the task needs.
15
+
13
16
  Strategy:
14
17
  1. Run `git diff` to see recent changes (if applicable)
15
18
  2. Read the modified files
@@ -9,6 +9,9 @@ You are a scout. Quickly investigate a codebase and return structured findings t
9
9
 
10
10
  Your output will be passed to an agent who has NOT seen the files you explored.
11
11
 
12
+ Assigned capabilities (see ~/.omk/agent/SUBAGENTS.md for the full routing reference):
13
+ - Skills: understand-anything (if a knowledge-graph/dependency question, not just a file lookup), ast-grep (structural search when grep-by-text isn't precise enough), headroom (if findings would otherwise be too large to hand off compactly). Load only what the task needs.
14
+
12
15
  Thoroughness (infer from task, default medium):
13
16
  - Quick: Targeted lookups, key files only
14
17
  - Medium: Follow imports, read critical sections
@@ -8,6 +8,8 @@ You are a worker agent with full capabilities. You operate in an isolated contex
8
8
 
9
9
  Work autonomously to complete the assigned task. Use all available tools as needed.
10
10
 
11
+ Assigned capabilities (see ~/.omk/agent/SUBAGENTS.md for the full routing reference): no fixed skill set — you have the full catalog. Self-route: classify the task, pick the most specific matching skill, load only that one, and never browse the catalog speculatively. For general coding work the most common picks are programming, debugging, git-master, ast-grep, and lsp; for anything else, match by the skill's own description.
12
+
11
13
  Output format when finished:
12
14
 
13
15
  ## Completed
@@ -5,6 +5,7 @@
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import { getAgentDir, parseFrontmatter } from "open-multi-agent-kit";
8
+ import { type AgentCapabilities, parseCapabilities } from "./capabilities.ts";
8
9
 
9
10
  export type AgentScope = "user" | "project" | "both";
10
11
 
@@ -13,6 +14,11 @@ export interface AgentConfig {
13
14
  description: string;
14
15
  tools?: string[];
15
16
  model?: string;
17
+ /** Optional capabilities declared in frontmatter (canonical source). */
18
+ capabilities?: AgentCapabilities;
19
+ /** When true, the dispatcher restricts the spawned subprocess to the declared
20
+ * skills via `--no-skills` + `--skill <path>`. Default off (guidance only). */
21
+ enforceCapabilities?: boolean;
16
22
  systemPrompt: string;
17
23
  source: "user" | "project";
18
24
  filePath: string;
@@ -60,11 +66,19 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
60
66
  .map((t: string) => t.trim())
61
67
  .filter(Boolean);
62
68
 
69
+ const capabilities = parseCapabilities(frontmatter);
70
+ const enforceCapabilities =
71
+ typeof frontmatter.enforceCapabilities === "string"
72
+ ? frontmatter.enforceCapabilities.trim().toLowerCase() === "true"
73
+ : undefined;
74
+
63
75
  agents.push({
64
76
  name: frontmatter.name,
65
77
  description: frontmatter.description,
66
78
  tools: tools && tools.length > 0 ? tools : undefined,
67
79
  model: frontmatter.model,
80
+ capabilities,
81
+ enforceCapabilities,
68
82
  systemPrompt: body,
69
83
  source,
70
84
  filePath,