@jaggerxtrm/specialists 2.1.6 → 2.1.8

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.
package/README.md CHANGED
@@ -14,13 +14,12 @@
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. The server 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 two scopes:
18
18
 
19
19
  | Scope | Location | Purpose |
20
20
  |-------|----------|---------|
21
21
  | **project** | `./specialists/` | Per-project specialists |
22
22
  | **user** | `~/.agents/specialists/` | Built-in defaults (copied on install) + your own |
23
- | **system** | bundled with the package | Fallback if user scope is empty |
24
23
 
25
24
  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
25
 
package/bin/install.js CHANGED
@@ -138,24 +138,220 @@ const HOOK_ENTRY = {
138
138
  hooks: [{ type: 'command', command: HOOK_FILE }],
139
139
  };
140
140
 
141
+
142
+ const BEADS_EDIT_GATE_FILE = join(HOOKS_DIR, 'beads-edit-gate.mjs');
143
+ const BEADS_COMMIT_GATE_FILE = join(HOOKS_DIR, 'beads-commit-gate.mjs');
144
+ const BEADS_STOP_GATE_FILE = join(HOOKS_DIR, 'beads-stop-gate.mjs');
145
+
146
+ const BEADS_EDIT_GATE_SCRIPT = `#!/usr/bin/env node
147
+ // beads-edit-gate — Claude Code PreToolUse hook
148
+ // Blocks file edits when no beads issue is in_progress.
149
+ // Only active in projects with a .beads/ directory.
150
+ // Exit 0: allow | Exit 2: block (stderr shown to Claude)
151
+ //
152
+ // Installed by: npx --package=@jaggerxtrm/specialists install
153
+
154
+ import { execSync } from 'node:child_process';
155
+ import { readFileSync, existsSync } from 'node:fs';
156
+ import { join } from 'node:path';
157
+
158
+ let input;
159
+ try {
160
+ input = JSON.parse(readFileSync(0, 'utf8'));
161
+ } catch {
162
+ process.exit(0);
163
+ }
164
+
165
+ const cwd = input.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
166
+ if (!existsSync(join(cwd, '.beads'))) process.exit(0);
167
+
168
+ let inProgress = 0;
169
+ try {
170
+ const output = execSync('bd list --status=in_progress', {
171
+ encoding: 'utf8',
172
+ cwd,
173
+ stdio: ['pipe', 'pipe', 'pipe'],
174
+ timeout: 8000,
175
+ });
176
+ inProgress = (output.match(/in_progress/g) ?? []).length;
177
+ } catch {
178
+ process.exit(0);
179
+ }
180
+
181
+ if (inProgress === 0) {
182
+ process.stderr.write(
183
+ '\\u{1F6AB} BEADS GATE: No in_progress issue tracked.\\n' +
184
+ 'You MUST create and claim a beads issue BEFORE editing any file:\\n\\n' +
185
+ ' bd create --title="<task summary>" --type=task --priority=2\\n' +
186
+ ' bd update <id> --status=in_progress\\n\\n' +
187
+ 'No exceptions. Momentum is not an excuse.\\n'
188
+ );
189
+ process.exit(2);
190
+ }
191
+
192
+ process.exit(0);
193
+ `;
194
+
195
+ const BEADS_COMMIT_GATE_SCRIPT = `#!/usr/bin/env node
196
+ // beads-commit-gate — Claude Code PreToolUse hook
197
+ // Blocks \`git commit\` when in_progress beads issues still exist.
198
+ // Forces: close issues first, THEN commit.
199
+ // Exit 0: allow | Exit 2: block (stderr shown to Claude)
200
+ //
201
+ // Installed by: npx --package=@jaggerxtrm/specialists install
202
+
203
+ import { execSync } from 'node:child_process';
204
+ import { readFileSync, existsSync } from 'node:fs';
205
+ import { join } from 'node:path';
206
+
207
+ let input;
208
+ try {
209
+ input = JSON.parse(readFileSync(0, 'utf8'));
210
+ } catch {
211
+ process.exit(0);
212
+ }
213
+
214
+ const tool = input.tool_name ?? '';
215
+ if (tool !== 'Bash') process.exit(0);
216
+
217
+ const cmd = input.tool_input?.command ?? '';
218
+ if (!/\\bgit\\s+commit\\b/.test(cmd)) process.exit(0);
219
+
220
+ const cwd = input.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
221
+ if (!existsSync(join(cwd, '.beads'))) process.exit(0);
222
+
223
+ let inProgress = 0;
224
+ let summary = '';
225
+ try {
226
+ const output = execSync('bd list --status=in_progress', {
227
+ encoding: 'utf8',
228
+ cwd,
229
+ stdio: ['pipe', 'pipe', 'pipe'],
230
+ timeout: 8000,
231
+ });
232
+ inProgress = (output.match(/in_progress/g) ?? []).length;
233
+ summary = output.trim();
234
+ } catch {
235
+ process.exit(0);
236
+ }
237
+
238
+ if (inProgress > 0) {
239
+ process.stderr.write(
240
+ '\\u{1F6AB} BEADS GATE: Cannot commit with open in_progress issues.\\n' +
241
+ 'Close them first, THEN commit:\\n\\n' +
242
+ ' bd close <id1> <id2> ...\\n' +
243
+ ' git add <files> && git commit -m "..."\\n\\n' +
244
+ \`Open issues:\\n\${summary}\\n\`
245
+ );
246
+ process.exit(2);
247
+ }
248
+
249
+ process.exit(0);
250
+ `;
251
+
252
+ const BEADS_STOP_GATE_SCRIPT = `#!/usr/bin/env node
253
+ // beads-stop-gate — Claude Code Stop hook
254
+ // Blocks the agent from stopping when in_progress beads issues remain.
255
+ // Forces the session close protocol before declaring done.
256
+ // Exit 0: allow stop | Exit 2: block stop (stderr shown to Claude)
257
+ //
258
+ // Installed by: npx --package=@jaggerxtrm/specialists install
259
+
260
+ import { execSync } from 'node:child_process';
261
+ import { readFileSync, existsSync } from 'node:fs';
262
+ import { join } from 'node:path';
263
+
264
+ let input;
265
+ try {
266
+ input = JSON.parse(readFileSync(0, 'utf8'));
267
+ } catch {
268
+ process.exit(0);
269
+ }
270
+
271
+ const cwd = input.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
272
+ if (!existsSync(join(cwd, '.beads'))) process.exit(0);
273
+
274
+ let inProgress = 0;
275
+ let summary = '';
276
+ try {
277
+ const output = execSync('bd list --status=in_progress', {
278
+ encoding: 'utf8',
279
+ cwd,
280
+ stdio: ['pipe', 'pipe', 'pipe'],
281
+ timeout: 8000,
282
+ });
283
+ inProgress = (output.match(/in_progress/g) ?? []).length;
284
+ summary = output.trim();
285
+ } catch {
286
+ process.exit(0);
287
+ }
288
+
289
+ if (inProgress > 0) {
290
+ process.stderr.write(
291
+ '\\u{1F6AB} BEADS STOP GATE: Cannot stop with unresolved in_progress issues.\\n' +
292
+ 'Complete the session close protocol:\\n\\n' +
293
+ ' bd close <id1> <id2> ...\\n' +
294
+ ' git add <files> && git commit -m "..."\\n' +
295
+ ' git push\\n\\n' +
296
+ \`Open issues:\\n\${summary}\\n\`
297
+ );
298
+ process.exit(2);
299
+ }
300
+
301
+ process.exit(0);
302
+ `;
303
+
304
+ const BEADS_EDIT_GATE_ENTRY = {
305
+ matcher: 'Edit|Write|MultiEdit|NotebookEdit|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol',
306
+ hooks: [{ type: 'command', command: BEADS_EDIT_GATE_FILE, timeout: 10 }],
307
+ };
308
+ const BEADS_COMMIT_GATE_ENTRY = {
309
+ matcher: 'Bash',
310
+ hooks: [{ type: 'command', command: BEADS_COMMIT_GATE_FILE, timeout: 10 }],
311
+ };
312
+ const BEADS_STOP_GATE_ENTRY = {
313
+ hooks: [{ type: 'command', command: BEADS_STOP_GATE_FILE, timeout: 10 }],
314
+ };
315
+
141
316
  function installHook() {
142
317
  mkdirSync(HOOKS_DIR, { recursive: true });
318
+
319
+ // Write all hook files
143
320
  writeFileSync(HOOK_FILE, HOOK_SCRIPT, 'utf8');
144
321
  chmodSync(HOOK_FILE, 0o755);
322
+ writeFileSync(BEADS_EDIT_GATE_FILE, BEADS_EDIT_GATE_SCRIPT, 'utf8');
323
+ chmodSync(BEADS_EDIT_GATE_FILE, 0o755);
324
+ writeFileSync(BEADS_COMMIT_GATE_FILE, BEADS_COMMIT_GATE_SCRIPT, 'utf8');
325
+ chmodSync(BEADS_COMMIT_GATE_FILE, 0o755);
326
+ writeFileSync(BEADS_STOP_GATE_FILE, BEADS_STOP_GATE_SCRIPT, 'utf8');
327
+ chmodSync(BEADS_STOP_GATE_FILE, 0o755);
145
328
 
146
329
  let settings = {};
147
330
  if (existsSync(SETTINGS_FILE)) {
148
331
  try { settings = JSON.parse(readFileSync(SETTINGS_FILE, 'utf8')); } catch {}
149
332
  }
150
333
 
151
- if (!Array.isArray(settings.hooks?.PreToolUse)) {
152
- settings.hooks = settings.hooks ?? {};
153
- settings.hooks.PreToolUse = [];
154
- }
155
-
156
- settings.hooks.PreToolUse = settings.hooks.PreToolUse
157
- .filter(e => !e.hooks?.some(h => h.command?.includes('specialists-main-guard')));
334
+ settings.hooks = settings.hooks ?? {};
335
+
336
+ // PreToolUse replace any existing specialists-managed entries
337
+ if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];
338
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(e =>
339
+ !e.hooks?.some(h =>
340
+ h.command?.includes('specialists-main-guard') ||
341
+ h.command?.includes('beads-edit-gate') ||
342
+ h.command?.includes('beads-commit-gate')
343
+ )
344
+ );
158
345
  settings.hooks.PreToolUse.push(HOOK_ENTRY);
346
+ settings.hooks.PreToolUse.push(BEADS_EDIT_GATE_ENTRY);
347
+ settings.hooks.PreToolUse.push(BEADS_COMMIT_GATE_ENTRY);
348
+
349
+ // Stop — replace any existing beads-stop-gate entry
350
+ if (!Array.isArray(settings.hooks.Stop)) settings.hooks.Stop = [];
351
+ settings.hooks.Stop = settings.hooks.Stop.filter(e =>
352
+ !e.hooks?.some(h => h.command?.includes('beads-stop-gate'))
353
+ );
354
+ settings.hooks.Stop.push(BEADS_STOP_GATE_ENTRY);
159
355
 
160
356
  mkdirSync(CLAUDE_DIR, { recursive: true });
161
357
  writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + '\n', 'utf8');
@@ -229,9 +425,12 @@ section('Claude Code hooks');
229
425
  const hookExisted = existsSync(HOOK_FILE);
230
426
  installHook();
231
427
  hookExisted
232
- ? ok('main-guard hook updated')
233
- : ok('main-guard hook installed → ~/.claude/hooks/specialists-main-guard.mjs');
234
- info('Blocks Edit/Write/git commit/push on main or master branch');
428
+ ? ok('hooks updated (main-guard + beads gates)')
429
+ : ok('hooks installed → ~/.claude/hooks/');
430
+ info('main-guard: blocks Edit/Write/git commit/push on main or master branch');
431
+ info('beads-edit-gate: requires in_progress bead before editing files');
432
+ info('beads-commit-gate: requires issues closed before git commit');
433
+ info('beads-stop-gate: requires issues closed before session end');
235
434
 
236
435
  // 7. Health check
237
436
  section('Health check');
package/dist/index.js CHANGED
@@ -24786,19 +24786,16 @@ class SpecialistLoader {
24786
24786
  cache = new Map;
24787
24787
  projectDir;
24788
24788
  userDir;
24789
- systemDir;
24790
24789
  constructor(options = {}) {
24791
24790
  this.projectDir = options.projectDir ?? process.cwd();
24792
24791
  this.userDir = options.userDir ?? join(homedir(), ".agents", "specialists");
24793
- this.systemDir = options.systemDir ?? join(new URL(import.meta.url).pathname, "..", "..", "specialists");
24794
24792
  }
24795
24793
  getScanDirs() {
24796
24794
  return [
24797
24795
  { path: join(this.projectDir, "specialists"), scope: "project" },
24798
24796
  { path: join(this.projectDir, ".claude", "specialists"), scope: "project" },
24799
24797
  { path: join(this.projectDir, ".agent-forge", "specialists"), scope: "project" },
24800
- { path: this.userDir, scope: "user" },
24801
- { path: this.systemDir, scope: "system" }
24798
+ { path: this.userDir, scope: "user" }
24802
24799
  ].filter((d) => existsSync(d.path));
24803
24800
  }
24804
24801
  async list(category) {
@@ -24897,6 +24894,12 @@ function getProviderArgs(model) {
24897
24894
  }
24898
24895
 
24899
24896
  // src/pi/session.ts
24897
+ class SessionKilledError extends Error {
24898
+ constructor() {
24899
+ super("Session was killed");
24900
+ this.name = "SessionKilledError";
24901
+ }
24902
+ }
24900
24903
  function mapPermissionToTools(level) {
24901
24904
  switch (level?.toUpperCase()) {
24902
24905
  case "READ_ONLY":
@@ -24955,6 +24958,7 @@ class PiAgentSession {
24955
24958
  this._doneResolve = resolve;
24956
24959
  this._doneReject = reject;
24957
24960
  });
24961
+ donePromise.catch(() => {});
24958
24962
  this._donePromise = donePromise;
24959
24963
  this.proc.stdout?.on("data", (chunk) => {
24960
24964
  this._lineBuffer += chunk.toString();
@@ -25067,10 +25071,12 @@ class PiAgentSession {
25067
25071
  return this._lastOutput;
25068
25072
  }
25069
25073
  kill() {
25074
+ if (this._killed)
25075
+ return;
25070
25076
  this._killed = true;
25071
25077
  this.proc?.kill();
25072
25078
  this.proc = undefined;
25073
- this._doneResolve?.();
25079
+ this._doneReject?.(new SessionKilledError);
25074
25080
  }
25075
25081
  }
25076
25082
 
@@ -25235,6 +25241,7 @@ You have access via Bash:
25235
25241
  onBeadCreated?.(beadId);
25236
25242
  }
25237
25243
  let output;
25244
+ let sessionBackend = model;
25238
25245
  let session;
25239
25246
  try {
25240
25247
  session = await this.sessionFactory({
@@ -25254,20 +25261,28 @@ You have access via Bash:
25254
25261
  onKillRegistered?.(session.kill.bind(session));
25255
25262
  await session.prompt(renderedTask);
25256
25263
  await session.waitForDone();
25264
+ sessionBackend = session.meta.backend;
25257
25265
  output = await session.getLastOutput();
25266
+ sessionBackend = session.meta.backend;
25258
25267
  const postScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "post") ?? [];
25259
25268
  for (const script of postScripts)
25260
25269
  runScript(script.path);
25261
25270
  circuitBreaker.recordSuccess(model);
25262
25271
  } catch (err) {
25263
- circuitBreaker.recordFailure(model);
25264
- if (beadId)
25265
- beadsClient?.closeBead(beadId, "ERROR", Date.now() - start, model);
25272
+ const isCancelled = err instanceof SessionKilledError;
25273
+ if (!isCancelled) {
25274
+ circuitBreaker.recordFailure(model);
25275
+ }
25276
+ const beadStatus = isCancelled ? "CANCELLED" : "ERROR";
25277
+ if (beadId) {
25278
+ beadsClient?.closeBead(beadId, beadStatus, Date.now() - start, model);
25279
+ beadsClient?.auditBead(beadId, metadata.name, model, 1);
25280
+ }
25266
25281
  await hooks.emit("post_execute", invocationId, metadata.name, metadata.version, {
25267
- status: "ERROR",
25282
+ status: isCancelled ? "CANCELLED" : "ERROR",
25268
25283
  duration_ms: Date.now() - start,
25269
25284
  output_valid: false,
25270
- error: { type: "backend_error", message: err.message }
25285
+ error: { type: isCancelled ? "cancelled" : "backend_error", message: err.message }
25271
25286
  });
25272
25287
  throw err;
25273
25288
  } finally {
@@ -25288,7 +25303,7 @@ You have access via Bash:
25288
25303
  }
25289
25304
  return {
25290
25305
  output,
25291
- backend: session.meta.backend,
25306
+ backend: sessionBackend,
25292
25307
  model,
25293
25308
  durationMs,
25294
25309
  specialistVersion: metadata.version,
@@ -25463,16 +25478,16 @@ async function runPipeline(steps, runner, onProgress) {
25463
25478
  }
25464
25479
 
25465
25480
  // src/tools/specialist/run_parallel.tool.ts
25466
- var InvocationSchema = exports_external.object({
25467
- name: exports_external.string(),
25468
- prompt: exports_external.string(),
25469
- variables: exports_external.record(exports_external.string()).optional(),
25470
- backend_override: exports_external.string().optional()
25481
+ var InvocationSchema = objectType({
25482
+ name: stringType(),
25483
+ prompt: stringType(),
25484
+ variables: recordType(stringType()).optional(),
25485
+ backend_override: stringType().optional()
25471
25486
  });
25472
- var runParallelSchema = exports_external.object({
25473
- specialists: exports_external.array(InvocationSchema).min(1),
25474
- merge_strategy: exports_external.enum(["collect", "synthesize", "vote", "pipeline"]).default("collect"),
25475
- timeout_ms: exports_external.number().default(120000)
25487
+ var runParallelSchema = objectType({
25488
+ specialists: arrayType(InvocationSchema).min(1),
25489
+ merge_strategy: enumType(["collect", "synthesize", "vote", "pipeline"]).default("collect"),
25490
+ timeout_ms: numberType().default(120000)
25476
25491
  });
25477
25492
  function createRunParallelTool(runner) {
25478
25493
  return {
@@ -25502,6 +25517,7 @@ function createRunParallelTool(runner) {
25502
25517
  status: r.status,
25503
25518
  output: r.status === "fulfilled" ? r.value.output : null,
25504
25519
  durationMs: r.status === "fulfilled" ? r.value.durationMs : null,
25520
+ beadId: r.status === "fulfilled" ? r.value.beadId : undefined,
25505
25521
  error: r.status === "rejected" ? String(r.reason?.message) : null
25506
25522
  }));
25507
25523
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaggerxtrm/specialists",
3
- "version": "2.1.6",
3
+ "version": "2.1.8",
4
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",