@jaggerxtrm/specialists 2.0.1 → 2.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 (3) hide show
  1. package/README.md +60 -12
  2. package/dist/index.js +82 -14
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # OmniSpecialist
1
+ # Specialists
2
2
 
3
3
  **One MCP Server. Many Specialists. Real AI Agents.**
4
4
 
@@ -6,7 +6,7 @@
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg)](https://www.typescriptlang.org/)
8
8
 
9
- OmniSpecialist is a **Model Context Protocol (MCP) server** that lets Claude (and other AI agents) discover and run specialist agents — each a full autonomous coding agent powered by [pi](https://github.com/mariozechner/pi), scoped to a specific task.
9
+ **Specialists** is a **Model Context Protocol (MCP) server** that lets Claude (and other AI agents) discover and run specialist agents — each a full autonomous coding agent powered by [pi](https://github.com/mariozechner/pi), scoped to a specific task.
10
10
 
11
11
  **Designed for agents, not just users.** Claude can autonomously route heavy tasks (code review, bug hunting, deep reasoning, session init) to the right specialist without user intervention. Specialists run in the background while Claude continues working.
12
12
 
@@ -14,26 +14,27 @@ OmniSpecialist is a **Model Context Protocol (MCP) server** that lets Claude (an
14
14
 
15
15
  ## How it works
16
16
 
17
- Specialists are `.specialist.yaml` files that define an autonomous agent: its model, system prompt, task template, and permission tier. OmniSpecialist discovers them across three scopes:
17
+ Specialists are `.specialist.yaml` files that define an autonomous agent: its model, system prompt, task template, and permission tier. The server discovers them across three scopes:
18
18
 
19
19
  | Scope | Location | Purpose |
20
20
  |-------|----------|---------|
21
21
  | **project** | `./specialists/` | Per-project specialists |
22
22
  | **user** | `~/.agents/specialists/` | Personal specialists |
23
- | **system** | bundled with OmniSpecialist | Built-in defaults |
23
+ | **system** | bundled with the package | Built-in defaults |
24
24
 
25
- When a specialist runs, OmniSpecialist spawns a `pi` subprocess with the right model, tools, and system prompt injected. Output streams back in real time via cursor-based polling.
25
+ When a specialist runs, the server spawns a `pi` subprocess with the right model, tools, and system prompt injected. Output streams back in real time via cursor-based polling.
26
26
 
27
27
  ---
28
28
 
29
- ## MCP Tools (7)
29
+ ## MCP Tools (8)
30
30
 
31
31
  | Tool | Description |
32
32
  |------|-------------|
33
+ | `specialist_init` | Session bootstrap: init beads if needed, return available specialists |
33
34
  | `list_specialists` | Discover all available specialists across scopes |
34
35
  | `use_specialist` | Run a specialist synchronously and return the result |
35
36
  | `start_specialist` | Fire-and-forget: start a specialist job, returns `job_id` |
36
- | `poll_specialist` | Poll a running job; returns delta since last cursor |
37
+ | `poll_specialist` | Poll a running job; returns delta since last cursor + `beadId` |
37
38
  | `stop_specialist` | Cancel a running job |
38
39
  | `run_parallel` | Run multiple specialists concurrently or as a pipeline |
39
40
  | `specialist_status` | Circuit breaker health + job status |
@@ -58,7 +59,7 @@ When a specialist runs, OmniSpecialist spawns a `pi` subprocess with the right m
58
59
 
59
60
  ## Permission Tiers
60
61
 
61
- Specialists declare their required permission level. OmniSpecialist enforces this at spawn time via `pi --tools`:
62
+ Specialists declare their required permission level, enforced at spawn time via `pi --tools`:
62
63
 
63
64
  | Tier | Allowed tools | Use case |
64
65
  |------|--------------|----------|
@@ -69,13 +70,60 @@ Specialists declare their required permission level. OmniSpecialist enforces thi
69
70
 
70
71
  ---
71
72
 
73
+ ## Beads Integration
74
+
75
+ Specialists with write permissions (`LOW`/`MEDIUM`/`HIGH`) automatically create a [beads](https://github.com/beads/bd) issue when they run and close it on completion. Control this per-specialist with `beads_integration`:
76
+
77
+ ```yaml
78
+ beads_integration: auto # default — create for LOW/MEDIUM/HIGH, skip for READ_ONLY
79
+ beads_integration: always # always create, regardless of permission tier
80
+ beads_integration: never # never create
81
+ ```
82
+
83
+ The orchestrating agent can retrieve the `beadId` from `poll_specialist` output to link the issue for follow-up (`bd remember`, `bd update --notes`, etc.).
84
+
85
+ ---
86
+
72
87
  ## Installation
73
88
 
89
+ ### One-line installer (recommended)
90
+
74
91
  ```bash
75
- node <(curl -fsSL https://raw.githubusercontent.com/Jaggerxtrm/unit.ai-specialists/master/bin/install.js)
92
+ npx --package=@jaggerxtrm/specialists install
76
93
  ```
77
94
 
78
- Clones the repo to `~/.agents/omnispecialist/`, installs dependencies (bun if available, else npm), registers the MCP as a direct `node` call no npx overhead on every startup. Also installs **pi** and **beads** if missing.
95
+ Installs: **pi** (`@mariozechner/pi-coding-agent`), **beads** (`@beads/bd`), **dolt** (interactive sudo on Linux / brew on macOS), registers the `specialists` MCP at user scope, scaffolds `~/.agents/specialists/`.
96
+
97
+ After running, **restart Claude Code** to load the MCP.
98
+
99
+ ---
100
+
101
+ ### Manual installation
102
+
103
+ **1. pi** — coding agent runtime:
104
+ ```bash
105
+ npm install -g @mariozechner/pi-coding-agent
106
+ ```
107
+ Run `pi` once, then `pi config` to enable your model providers (Anthropic, Google, etc.).
108
+
109
+ **2. beads** — issue tracker:
110
+ ```bash
111
+ npm install -g @beads/bd
112
+ ```
113
+
114
+ **3. dolt** — beads sync backend:
115
+ ```bash
116
+ # Linux
117
+ sudo bash -c 'curl -L https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash'
118
+ # macOS
119
+ brew install dolt
120
+ ```
121
+
122
+ **4. specialists + MCP:**
123
+ ```bash
124
+ npm install -g @jaggerxtrm/specialists
125
+ claude mcp add --scope user specialists -- specialists
126
+ ```
79
127
 
80
128
  Then **restart Claude Code**.
81
129
 
@@ -93,7 +141,7 @@ specialist:
93
141
  description: "What this specialist does."
94
142
  category: analysis
95
143
  tags: [analysis, example]
96
- updated: "2026-03-07"
144
+ updated: "2026-03-09"
97
145
 
98
146
  execution:
99
147
  mode: tool
@@ -132,7 +180,7 @@ bun test
132
180
  ```
133
181
 
134
182
  - **Build**: `bun build src/index.ts --target=node --outfile=dist/index.js`
135
- - **Test**: `bun --bun vitest run` (40 unit tests)
183
+ - **Test**: `bun --bun vitest run` (67 unit tests)
136
184
  - **Lint**: `tsc --noEmit`
137
185
 
138
186
  See [CLAUDE.md](CLAUDE.md) for the full architecture guide and [ROADMAP.md](ROADMAP.md) for planned features.
package/dist/index.js CHANGED
@@ -24568,12 +24568,12 @@ class StdioServerTransport {
24568
24568
  }
24569
24569
 
24570
24570
  // src/server.ts
24571
- import { join as join2 } from "node:path";
24571
+ import { join as join3 } from "node:path";
24572
24572
 
24573
24573
  // src/constants.ts
24574
- var LOG_PREFIX = "[UAI-MCP]";
24574
+ var LOG_PREFIX = "[specialists]";
24575
24575
  var MCP_CONFIG = {
24576
- SERVER_NAME: "unitAI",
24576
+ SERVER_NAME: "specialists",
24577
24577
  VERSION: "1.0.0",
24578
24578
  CAPABILITIES: {
24579
24579
  tools: {},
@@ -24748,6 +24748,7 @@ var SpecialistSchema = objectType({
24748
24748
  capabilities: CapabilitiesSchema,
24749
24749
  communication: CommunicationSchema,
24750
24750
  validation: ValidationSchema,
24751
+ beads_integration: enumType(["auto", "always", "never"]).default("auto"),
24751
24752
  heartbeat: unknownType().optional()
24752
24753
  })
24753
24754
  });
@@ -25078,6 +25079,15 @@ class PiAgentSession {
25078
25079
  }
25079
25080
  }
25080
25081
 
25082
+ // src/specialist/beads.ts
25083
+ function shouldCreateBead(beadsIntegration, permissionRequired) {
25084
+ if (beadsIntegration === "never")
25085
+ return false;
25086
+ if (beadsIntegration === "always")
25087
+ return true;
25088
+ return permissionRequired !== "READ_ONLY";
25089
+ }
25090
+
25081
25091
  // src/specialist/runner.ts
25082
25092
  class SpecialistRunner {
25083
25093
  deps;
@@ -25087,7 +25097,7 @@ class SpecialistRunner {
25087
25097
  this.sessionFactory = deps.sessionFactory ?? PiAgentSession.create.bind(PiAgentSession);
25088
25098
  }
25089
25099
  async run(options, onProgress, onEvent, onMeta, onKillRegistered) {
25090
- const { loader, hooks, circuitBreaker } = this.deps;
25100
+ const { loader, hooks, circuitBreaker, beadsClient } = this.deps;
25091
25101
  const invocationId = crypto.randomUUID();
25092
25102
  const start = Date.now();
25093
25103
  const spec = await loader.get(options.name);
@@ -25142,6 +25152,11 @@ You have access via Bash:
25142
25152
  timeout_ms: execution.timeout_ms,
25143
25153
  permission_level: permissionLevel
25144
25154
  });
25155
+ const beadsIntegration = spec.specialist.beads_integration ?? "auto";
25156
+ let beadId;
25157
+ if (beadsClient && shouldCreateBead(beadsIntegration, execution.permission_required)) {
25158
+ beadId = beadsClient.createBead(metadata.name) ?? undefined;
25159
+ }
25145
25160
  let output;
25146
25161
  let session;
25147
25162
  try {
@@ -25179,6 +25194,8 @@ You have access via Bash:
25179
25194
  circuitBreaker.recordSuccess(model);
25180
25195
  } catch (err) {
25181
25196
  circuitBreaker.recordFailure(model);
25197
+ if (beadId)
25198
+ beadsClient?.closeBead(beadId, "ERROR", Date.now() - start, model);
25182
25199
  await hooks.emit("post_execute", invocationId, metadata.name, metadata.version, {
25183
25200
  status: "ERROR",
25184
25201
  duration_ms: Date.now() - start,
@@ -25198,12 +25215,17 @@ You have access via Bash:
25198
25215
  duration_ms: durationMs,
25199
25216
  output_valid: true
25200
25217
  });
25218
+ if (beadId) {
25219
+ beadsClient?.closeBead(beadId, "COMPLETE", durationMs, model);
25220
+ beadsClient?.auditBead(beadId, metadata.name, model, 0);
25221
+ }
25201
25222
  return {
25202
25223
  output,
25203
25224
  backend: session.meta.backend,
25204
25225
  model,
25205
25226
  durationMs,
25206
- specialistVersion: metadata.version
25227
+ specialistVersion: metadata.version,
25228
+ beadId
25207
25229
  };
25208
25230
  }
25209
25231
  async startAsync(options, registry2) {
@@ -25478,6 +25500,12 @@ class JobRegistry {
25478
25500
  if (meta.model)
25479
25501
  job.model = meta.model;
25480
25502
  }
25503
+ setBeadId(id, beadId) {
25504
+ const job = this.jobs.get(id);
25505
+ if (!job)
25506
+ return;
25507
+ job.beadId = beadId;
25508
+ }
25481
25509
  setKillFn(id, killFn) {
25482
25510
  const job = this.jobs.get(id);
25483
25511
  if (!job)
@@ -25499,6 +25527,8 @@ class JobRegistry {
25499
25527
  job.model = result.model;
25500
25528
  job.specialistVersion = result.specialistVersion;
25501
25529
  job.endedAtMs = Date.now();
25530
+ if (result.beadId)
25531
+ job.beadId = result.beadId;
25502
25532
  }
25503
25533
  fail(id, err) {
25504
25534
  const job = this.jobs.get(id);
@@ -25535,7 +25565,8 @@ class JobRegistry {
25535
25565
  model: job.model,
25536
25566
  specialist_version: job.specialistVersion,
25537
25567
  duration_ms: (job.endedAtMs ?? Date.now()) - job.startedAtMs,
25538
- error: job.error
25568
+ error: job.error,
25569
+ beadId: job.beadId
25539
25570
  };
25540
25571
  }
25541
25572
  delete(id) {
@@ -25606,15 +25637,50 @@ function createStopSpecialistTool(registry2) {
25606
25637
  };
25607
25638
  }
25608
25639
 
25640
+ // src/tools/specialist/specialist_init.tool.ts
25641
+ import { spawnSync } from "node:child_process";
25642
+ import { existsSync as existsSync2 } from "node:fs";
25643
+ import { join as join2 } from "node:path";
25644
+ var specialistInitSchema = exports_external.object({});
25645
+ function createSpecialistInitTool(loader, deps) {
25646
+ const resolved = deps ?? {
25647
+ bdAvailable: () => spawnSync("bd", ["--version"], { stdio: "ignore" }).status === 0,
25648
+ beadsExists: () => existsSync2(join2(process.cwd(), ".beads")),
25649
+ bdInit: () => spawnSync("bd", ["init"], { stdio: "ignore" })
25650
+ };
25651
+ return {
25652
+ name: "specialist_init",
25653
+ description: "Session bootstrap: initializes beads in the project if not already set up, " + "then returns available specialists. Call at session start for orientation.",
25654
+ inputSchema: specialistInitSchema,
25655
+ async execute(_input) {
25656
+ const available = resolved.bdAvailable();
25657
+ let initialized = false;
25658
+ if (available) {
25659
+ if (resolved.beadsExists()) {
25660
+ initialized = true;
25661
+ } else {
25662
+ const result = resolved.bdInit();
25663
+ initialized = result.status === 0;
25664
+ }
25665
+ }
25666
+ const specialists = await loader.list();
25667
+ return {
25668
+ specialists,
25669
+ beads: { available, initialized }
25670
+ };
25671
+ }
25672
+ };
25673
+ }
25674
+
25609
25675
  // src/server.ts
25610
- class UnitAIServer {
25676
+ class SpecialistsServer {
25611
25677
  server;
25612
25678
  tools;
25613
25679
  constructor() {
25614
25680
  const circuitBreaker = new CircuitBreaker;
25615
25681
  const loader = new SpecialistLoader;
25616
25682
  const hooks = new HookEmitter({
25617
- tracePath: join2(process.cwd(), ".unitai", "trace.jsonl")
25683
+ tracePath: join3(process.cwd(), ".specialists", "trace.jsonl")
25618
25684
  });
25619
25685
  const runner = new SpecialistRunner({ loader, hooks, circuitBreaker });
25620
25686
  const registry2 = new JobRegistry;
@@ -25625,7 +25691,8 @@ class UnitAIServer {
25625
25691
  createSpecialistStatusTool(loader, circuitBreaker),
25626
25692
  createStartSpecialistTool(runner, registry2),
25627
25693
  createPollSpecialistTool(registry2),
25628
- createStopSpecialistTool(registry2)
25694
+ createStopSpecialistTool(registry2),
25695
+ createSpecialistInitTool(loader)
25629
25696
  ];
25630
25697
  this.server = new Server({ name: MCP_CONFIG.SERVER_NAME, version: MCP_CONFIG.VERSION }, { capabilities: MCP_CONFIG.CAPABILITIES });
25631
25698
  this.setupHandlers();
@@ -25639,7 +25706,8 @@ class UnitAIServer {
25639
25706
  specialist_status: exports_external.object({}),
25640
25707
  start_specialist: startSpecialistSchema,
25641
25708
  poll_specialist: pollSpecialistSchema,
25642
- stop_specialist: stopSpecialistSchema
25709
+ stop_specialist: stopSpecialistSchema,
25710
+ specialist_init: specialistInitSchema
25643
25711
  };
25644
25712
  this.toolSchemas = schemaMap;
25645
25713
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -25665,7 +25733,7 @@ class UnitAIServer {
25665
25733
  const onProgress = (msg) => {
25666
25734
  this.server.notification({
25667
25735
  method: "notifications/message",
25668
- params: { level: "info", logger: "unitai", data: msg }
25736
+ params: { level: "info", logger: "specialists", data: msg }
25669
25737
  }).catch(() => {});
25670
25738
  };
25671
25739
  try {
@@ -25689,7 +25757,7 @@ class UnitAIServer {
25689
25757
  try {
25690
25758
  const transport = new StdioServerTransport;
25691
25759
  await this.server.connect(transport);
25692
- logger.info(`UnitAI MCP Server v2 started — ${this.tools.length} tools registered`);
25760
+ logger.info(`Specialists MCP Server v2 started — ${this.tools.length} tools registered`);
25693
25761
  } catch (error2) {
25694
25762
  logger.error("Failed to start server", error2);
25695
25763
  process.exit(1);
@@ -25702,8 +25770,8 @@ class UnitAIServer {
25702
25770
 
25703
25771
  // src/index.ts
25704
25772
  async function main() {
25705
- logger.info("Starting Unified AI MCP Tool server...");
25706
- const server = new UnitAIServer;
25773
+ logger.info("Starting Specialists MCP Server...");
25774
+ const server = new SpecialistsServer;
25707
25775
  await server.start();
25708
25776
  }
25709
25777
  main().catch((error2) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jaggerxtrm/specialists",
3
- "version": "2.0.1",
4
- "description": "OmniSpecialist \u2014 7-tool MCP orchestration layer powered by the Specialist System. Discover and execute .specialist.yaml files across project/user/system scopes via pi.",
3
+ "version": "2.1.0",
4
+ "description": "OmniSpecialist 7-tool MCP orchestration layer powered by the Specialist System. Discover and execute .specialist.yaml files across project/user/system scopes via pi.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
7
7
  "files": [