@jaggerxtrm/specialists 2.1.7 → 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/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
@@ -24894,6 +24894,12 @@ function getProviderArgs(model) {
24894
24894
  }
24895
24895
 
24896
24896
  // src/pi/session.ts
24897
+ class SessionKilledError extends Error {
24898
+ constructor() {
24899
+ super("Session was killed");
24900
+ this.name = "SessionKilledError";
24901
+ }
24902
+ }
24897
24903
  function mapPermissionToTools(level) {
24898
24904
  switch (level?.toUpperCase()) {
24899
24905
  case "READ_ONLY":
@@ -24952,6 +24958,7 @@ class PiAgentSession {
24952
24958
  this._doneResolve = resolve;
24953
24959
  this._doneReject = reject;
24954
24960
  });
24961
+ donePromise.catch(() => {});
24955
24962
  this._donePromise = donePromise;
24956
24963
  this.proc.stdout?.on("data", (chunk) => {
24957
24964
  this._lineBuffer += chunk.toString();
@@ -25064,10 +25071,12 @@ class PiAgentSession {
25064
25071
  return this._lastOutput;
25065
25072
  }
25066
25073
  kill() {
25074
+ if (this._killed)
25075
+ return;
25067
25076
  this._killed = true;
25068
25077
  this.proc?.kill();
25069
25078
  this.proc = undefined;
25070
- this._doneResolve?.();
25079
+ this._doneReject?.(new SessionKilledError);
25071
25080
  }
25072
25081
  }
25073
25082
 
@@ -25232,6 +25241,7 @@ You have access via Bash:
25232
25241
  onBeadCreated?.(beadId);
25233
25242
  }
25234
25243
  let output;
25244
+ let sessionBackend = model;
25235
25245
  let session;
25236
25246
  try {
25237
25247
  session = await this.sessionFactory({
@@ -25251,20 +25261,28 @@ You have access via Bash:
25251
25261
  onKillRegistered?.(session.kill.bind(session));
25252
25262
  await session.prompt(renderedTask);
25253
25263
  await session.waitForDone();
25264
+ sessionBackend = session.meta.backend;
25254
25265
  output = await session.getLastOutput();
25266
+ sessionBackend = session.meta.backend;
25255
25267
  const postScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "post") ?? [];
25256
25268
  for (const script of postScripts)
25257
25269
  runScript(script.path);
25258
25270
  circuitBreaker.recordSuccess(model);
25259
25271
  } catch (err) {
25260
- circuitBreaker.recordFailure(model);
25261
- if (beadId)
25262
- 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
+ }
25263
25281
  await hooks.emit("post_execute", invocationId, metadata.name, metadata.version, {
25264
- status: "ERROR",
25282
+ status: isCancelled ? "CANCELLED" : "ERROR",
25265
25283
  duration_ms: Date.now() - start,
25266
25284
  output_valid: false,
25267
- error: { type: "backend_error", message: err.message }
25285
+ error: { type: isCancelled ? "cancelled" : "backend_error", message: err.message }
25268
25286
  });
25269
25287
  throw err;
25270
25288
  } finally {
@@ -25285,7 +25303,7 @@ You have access via Bash:
25285
25303
  }
25286
25304
  return {
25287
25305
  output,
25288
- backend: session.meta.backend,
25306
+ backend: sessionBackend,
25289
25307
  model,
25290
25308
  durationMs,
25291
25309
  specialistVersion: metadata.version,
@@ -25460,16 +25478,16 @@ async function runPipeline(steps, runner, onProgress) {
25460
25478
  }
25461
25479
 
25462
25480
  // src/tools/specialist/run_parallel.tool.ts
25463
- var InvocationSchema = exports_external.object({
25464
- name: exports_external.string(),
25465
- prompt: exports_external.string(),
25466
- variables: exports_external.record(exports_external.string()).optional(),
25467
- 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()
25468
25486
  });
25469
- var runParallelSchema = exports_external.object({
25470
- specialists: exports_external.array(InvocationSchema).min(1),
25471
- merge_strategy: exports_external.enum(["collect", "synthesize", "vote", "pipeline"]).default("collect"),
25472
- 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)
25473
25491
  });
25474
25492
  function createRunParallelTool(runner) {
25475
25493
  return {
@@ -25499,6 +25517,7 @@ function createRunParallelTool(runner) {
25499
25517
  status: r.status,
25500
25518
  output: r.status === "fulfilled" ? r.value.output : null,
25501
25519
  durationMs: r.status === "fulfilled" ? r.value.durationMs : null,
25520
+ beadId: r.status === "fulfilled" ? r.value.beadId : undefined,
25502
25521
  error: r.status === "rejected" ? String(r.reason?.message) : null
25503
25522
  }));
25504
25523
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaggerxtrm/specialists",
3
- "version": "2.1.7",
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",