@jaggerxtrm/specialists 3.0.2 → 3.2.1

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/dist/index.js CHANGED
@@ -8531,6 +8531,7 @@ var require_limitLength = __commonJS((exports) => {
8531
8531
  var require_pattern = __commonJS((exports) => {
8532
8532
  Object.defineProperty(exports, "__esModule", { value: true });
8533
8533
  var code_1 = require_code2();
8534
+ var util_1 = require_util();
8534
8535
  var codegen_1 = require_codegen();
8535
8536
  var error2 = {
8536
8537
  message: ({ schemaCode }) => (0, codegen_1.str)`must match pattern "${schemaCode}"`,
@@ -8543,10 +8544,18 @@ var require_pattern = __commonJS((exports) => {
8543
8544
  $data: true,
8544
8545
  error: error2,
8545
8546
  code(cxt) {
8546
- const { data, $data, schema, schemaCode, it } = cxt;
8547
+ const { gen, data, $data, schema, schemaCode, it } = cxt;
8547
8548
  const u = it.opts.unicodeRegExp ? "u" : "";
8548
- const regExp = $data ? (0, codegen_1._)`(new RegExp(${schemaCode}, ${u}))` : (0, code_1.usePattern)(cxt, schema);
8549
- cxt.fail$data((0, codegen_1._)`!${regExp}.test(${data})`);
8549
+ if ($data) {
8550
+ const { regExp } = it.opts.code;
8551
+ const regExpCode = regExp.code === "new RegExp" ? (0, codegen_1._)`new RegExp` : (0, util_1.useFunc)(gen, regExp);
8552
+ const valid = gen.let("valid");
8553
+ gen.try(() => gen.assign(valid, (0, codegen_1._)`${regExpCode}(${schemaCode}, ${u}).test(${data})`), () => gen.assign(valid, false));
8554
+ cxt.fail$data((0, codegen_1._)`!${valid}`);
8555
+ } else {
8556
+ const regExp = (0, code_1.usePattern)(cxt, schema);
8557
+ cxt.fail$data((0, codegen_1._)`!${regExp}.test(${data})`);
8558
+ }
8550
8559
  }
8551
8560
  };
8552
8561
  exports.default = def;
@@ -17524,12 +17533,13 @@ class SpecialistLoader {
17524
17533
  this.userDir = options.userDir ?? join(homedir(), ".agents", "specialists");
17525
17534
  }
17526
17535
  getScanDirs() {
17527
- return [
17536
+ const dirs = [
17528
17537
  { path: join(this.projectDir, "specialists"), scope: "project" },
17529
17538
  { path: join(this.projectDir, ".claude", "specialists"), scope: "project" },
17530
17539
  { path: join(this.projectDir, ".agent-forge", "specialists"), scope: "project" },
17531
17540
  { path: this.userDir, scope: "user" }
17532
- ].filter((d) => existsSync(d.path));
17541
+ ];
17542
+ return dirs.filter((d) => existsSync(d.path));
17533
17543
  }
17534
17544
  async list(category) {
17535
17545
  const results = [];
@@ -17640,6 +17650,9 @@ var init_backendMap = __esm(() => {
17640
17650
 
17641
17651
  // src/pi/session.ts
17642
17652
  import { spawn } from "node:child_process";
17653
+ import { existsSync as existsSync2 } from "node:fs";
17654
+ import { homedir as homedir2 } from "node:os";
17655
+ import { join as join2 } from "node:path";
17643
17656
  function mapPermissionToTools(level) {
17644
17657
  switch (level?.toUpperCase()) {
17645
17658
  case "READ_ONLY":
@@ -17659,6 +17672,7 @@ class PiAgentSession {
17659
17672
  options;
17660
17673
  proc;
17661
17674
  _lastOutput = "";
17675
+ _donePromise;
17662
17676
  _doneResolve;
17663
17677
  _doneReject;
17664
17678
  _agentEndReceived = false;
@@ -17686,6 +17700,7 @@ class PiAgentSession {
17686
17700
  const args = [
17687
17701
  "--mode",
17688
17702
  "rpc",
17703
+ "--no-extensions",
17689
17704
  ...providerArgs,
17690
17705
  "--no-session",
17691
17706
  ...extraArgs
@@ -17693,11 +17708,22 @@ class PiAgentSession {
17693
17708
  const toolsFlag = mapPermissionToTools(this.options.permissionLevel);
17694
17709
  if (toolsFlag)
17695
17710
  args.push("--tools", toolsFlag);
17711
+ const piExtDir = join2(homedir2(), ".pi", "agent", "extensions");
17712
+ const permLevel = (this.options.permissionLevel ?? "").toUpperCase();
17713
+ if (permLevel !== "READ_ONLY") {
17714
+ const qgPath = join2(piExtDir, "quality-gates");
17715
+ if (existsSync2(qgPath))
17716
+ args.push("-e", qgPath);
17717
+ }
17718
+ const ssPath = join2(piExtDir, "service-skills");
17719
+ if (existsSync2(ssPath))
17720
+ args.push("-e", ssPath);
17696
17721
  if (this.options.systemPrompt) {
17697
17722
  args.push("--append-system-prompt", this.options.systemPrompt);
17698
17723
  }
17699
17724
  this.proc = spawn("pi", args, {
17700
- stdio: ["pipe", "pipe", "inherit"]
17725
+ stdio: ["pipe", "pipe", "inherit"],
17726
+ cwd: this.options.cwd
17701
17727
  });
17702
17728
  const donePromise = new Promise((resolve, reject) => {
17703
17729
  this._doneResolve = resolve;
@@ -17759,34 +17785,11 @@ class PiAgentSession {
17759
17785
  this._lastOutput = last.content.filter((c) => c.type === "text").map((c) => c.text).join("");
17760
17786
  }
17761
17787
  this._agentEndReceived = true;
17762
- this.options.onEvent?.("done");
17788
+ this.options.onEvent?.("agent_end");
17763
17789
  this._doneResolve?.();
17764
17790
  return;
17765
17791
  }
17766
- if (type === "thinking_start") {
17767
- this.options.onEvent?.("thinking");
17768
- return;
17769
- }
17770
- if (type === "thinking_delta") {
17771
- if (event.delta)
17772
- this.options.onThinking?.(event.delta);
17773
- this.options.onEvent?.("thinking");
17774
- return;
17775
- }
17776
- if (type === "thinking_end") {
17777
- return;
17778
- }
17779
- if (type === "toolcall_start") {
17780
- this.options.onToolStart?.(event.name ?? event.toolName ?? "tool");
17781
- this.options.onEvent?.("toolcall");
17782
- return;
17783
- }
17784
- if (type === "toolcall_end") {
17785
- this.options.onEvent?.("toolcall");
17786
- return;
17787
- }
17788
17792
  if (type === "tool_execution_start") {
17789
- this.options.onToolStart?.(event.name ?? event.toolName ?? "tool");
17790
17793
  this.options.onEvent?.("tool_execution");
17791
17794
  return;
17792
17795
  }
@@ -17795,7 +17798,7 @@ class PiAgentSession {
17795
17798
  return;
17796
17799
  }
17797
17800
  if (type === "tool_execution_end") {
17798
- this.options.onToolEnd?.(event.name ?? event.toolName ?? "tool");
17801
+ this.options.onToolEnd?.(event.toolName ?? event.name ?? "tool");
17799
17802
  this.options.onEvent?.("tool_execution_end");
17800
17803
  return;
17801
17804
  }
@@ -17803,9 +17806,30 @@ class PiAgentSession {
17803
17806
  const ae = event.assistantMessageEvent;
17804
17807
  if (!ae)
17805
17808
  return;
17806
- if (ae.type === "text_delta" && ae.delta) {
17807
- this.options.onToken?.(ae.delta);
17808
- this.options.onEvent?.("text");
17809
+ switch (ae.type) {
17810
+ case "text_delta":
17811
+ if (ae.delta)
17812
+ this.options.onToken?.(ae.delta);
17813
+ this.options.onEvent?.("text");
17814
+ break;
17815
+ case "thinking_start":
17816
+ this.options.onEvent?.("thinking");
17817
+ break;
17818
+ case "thinking_delta":
17819
+ if (ae.delta)
17820
+ this.options.onThinking?.(ae.delta);
17821
+ this.options.onEvent?.("thinking");
17822
+ break;
17823
+ case "toolcall_start":
17824
+ this.options.onToolStart?.(ae.name ?? ae.toolName ?? "tool");
17825
+ this.options.onEvent?.("toolcall");
17826
+ break;
17827
+ case "toolcall_end":
17828
+ this.options.onEvent?.("toolcall");
17829
+ break;
17830
+ case "done":
17831
+ this.options.onEvent?.("message_done");
17832
+ break;
17809
17833
  }
17810
17834
  }
17811
17835
  }
@@ -17831,7 +17855,7 @@ class PiAgentSession {
17831
17855
  this.proc?.stdin?.write(msg);
17832
17856
  }
17833
17857
  async waitForDone(timeout) {
17834
- const donePromise = this._donePromise;
17858
+ const donePromise = this._donePromise ?? Promise.resolve();
17835
17859
  if (!timeout)
17836
17860
  return donePromise;
17837
17861
  return Promise.race([
@@ -17868,7 +17892,7 @@ class PiAgentSession {
17868
17892
  if (this._killed)
17869
17893
  return;
17870
17894
  this.proc?.stdin?.end();
17871
- await this._donePromise.catch(() => {});
17895
+ await this._donePromise?.catch(() => {});
17872
17896
  }
17873
17897
  kill() {
17874
17898
  if (this._killed)
@@ -17892,6 +17916,29 @@ var init_session = __esm(() => {
17892
17916
 
17893
17917
  // src/specialist/beads.ts
17894
17918
  import { spawnSync } from "node:child_process";
17919
+ function buildBeadContext(bead, completedBlockers = []) {
17920
+ const lines = [`# Task: ${bead.title}`];
17921
+ if (bead.description?.trim()) {
17922
+ lines.push(bead.description.trim());
17923
+ }
17924
+ if (bead.notes?.trim()) {
17925
+ lines.push("", "## Notes", bead.notes.trim());
17926
+ }
17927
+ if (completedBlockers.length > 0) {
17928
+ lines.push("", "## Context from completed dependencies:");
17929
+ for (const blocker of completedBlockers) {
17930
+ lines.push("", `### ${blocker.title} (${blocker.id})`);
17931
+ if (blocker.description?.trim()) {
17932
+ lines.push(blocker.description.trim());
17933
+ }
17934
+ if (blocker.notes?.trim()) {
17935
+ lines.push("", blocker.notes.trim());
17936
+ }
17937
+ }
17938
+ }
17939
+ return lines.join(`
17940
+ `).trim();
17941
+ }
17895
17942
 
17896
17943
  class BeadsClient {
17897
17944
  available;
@@ -17917,12 +17964,66 @@ class BeadsClient {
17917
17964
  const id = result.stdout?.trim();
17918
17965
  return id || null;
17919
17966
  }
17967
+ readBead(id) {
17968
+ if (!this.available || !id)
17969
+ return null;
17970
+ const result = spawnSync("bd", ["show", id, "--json"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 });
17971
+ if (result.error || result.status !== 0 || !result.stdout?.trim())
17972
+ return null;
17973
+ try {
17974
+ const parsed = JSON.parse(result.stdout);
17975
+ const bead = Array.isArray(parsed) ? parsed[0] : parsed;
17976
+ if (!bead || typeof bead !== "object" || typeof bead.id !== "string" || typeof bead.title !== "string")
17977
+ return null;
17978
+ return bead;
17979
+ } catch (err) {
17980
+ console.warn(`[specialists] readBead: JSON parse failed for id=${id}: ${err}`);
17981
+ return null;
17982
+ }
17983
+ }
17984
+ getCompletedBlockers(id, depth = 1) {
17985
+ if (!this.available || !id || depth < 1)
17986
+ return [];
17987
+ const result = spawnSync("bd", ["dep", "list", id, "--json"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 });
17988
+ if (result.error || result.status !== 0 || !result.stdout?.trim())
17989
+ return [];
17990
+ let deps;
17991
+ try {
17992
+ deps = JSON.parse(result.stdout);
17993
+ if (!Array.isArray(deps))
17994
+ return [];
17995
+ } catch {
17996
+ return [];
17997
+ }
17998
+ const blockers = deps.filter((d) => d.dependency_type === "blocks" && d.status === "closed");
17999
+ const records = [];
18000
+ for (const dep of blockers) {
18001
+ const record3 = this.readBead(dep.id);
18002
+ if (record3) {
18003
+ records.push(record3);
18004
+ if (depth > 1) {
18005
+ records.push(...this.getCompletedBlockers(dep.id, depth - 1));
18006
+ }
18007
+ }
18008
+ }
18009
+ return records;
18010
+ }
18011
+ addDependency(trackingBeadId, inputBeadId) {
18012
+ if (!this.available || !trackingBeadId || !inputBeadId)
18013
+ return;
18014
+ spawnSync("bd", ["dep", "add", trackingBeadId, inputBeadId], { stdio: "ignore" });
18015
+ }
17920
18016
  closeBead(id, status, durationMs, model) {
17921
18017
  if (!this.available || !id)
17922
18018
  return;
17923
18019
  const reason = `${status}, ${Math.round(durationMs)}ms, ${model}`;
17924
18020
  spawnSync("bd", ["close", id, "-r", reason], { stdio: "ignore" });
17925
18021
  }
18022
+ updateBeadNotes(id, notes) {
18023
+ if (!this.available || !id || !notes)
18024
+ return;
18025
+ spawnSync("bd", ["update", id, "--notes", notes], { stdio: "ignore" });
18026
+ }
17926
18027
  auditBead(id, toolName, model, exitCode) {
17927
18028
  if (!this.available || !id)
17928
18029
  return;
@@ -18006,7 +18107,14 @@ class SpecialistRunner {
18006
18107
  const preScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "pre") ?? [];
18007
18108
  const preResults = preScripts.map((s) => runScript(s.path)).filter((_, i) => preScripts[i].inject_output);
18008
18109
  const preScriptOutput = formatScriptOutput(preResults);
18009
- const variables = { prompt: options.prompt, pre_script_output: preScriptOutput, ...options.variables };
18110
+ const beadVariables = options.inputBeadId ? { bead_context: options.prompt, bead_id: options.inputBeadId } : {};
18111
+ const variables = {
18112
+ prompt: options.prompt,
18113
+ cwd: process.cwd(),
18114
+ pre_script_output: preScriptOutput,
18115
+ ...options.variables ?? {},
18116
+ ...beadVariables
18117
+ };
18010
18118
  const renderedTask = renderTemplate(prompt.task_template, variables);
18011
18119
  const promptHash = createHash("sha256").update(renderedTask).digest("hex").slice(0, 16);
18012
18120
  await hooks.emit("post_render", invocationId, metadata.name, metadata.version, {
@@ -18059,10 +18167,15 @@ You have access via Bash:
18059
18167
  });
18060
18168
  const beadsIntegration = spec.specialist.beads_integration ?? "auto";
18061
18169
  let beadId;
18062
- if (beadsClient && shouldCreateBead(beadsIntegration, execution.permission_required)) {
18170
+ let ownsBead = false;
18171
+ if (options.inputBeadId) {
18172
+ beadId = options.inputBeadId;
18173
+ } else if (beadsClient && shouldCreateBead(beadsIntegration, execution.permission_required)) {
18063
18174
  beadId = beadsClient.createBead(metadata.name) ?? undefined;
18064
- if (beadId)
18175
+ if (beadId) {
18176
+ ownsBead = true;
18065
18177
  onBeadCreated?.(beadId);
18178
+ }
18066
18179
  }
18067
18180
  let output;
18068
18181
  let sessionBackend = model;
@@ -18072,6 +18185,7 @@ You have access via Bash:
18072
18185
  model,
18073
18186
  systemPrompt: agentsMd || undefined,
18074
18187
  permissionLevel,
18188
+ cwd: process.cwd(),
18075
18189
  onToken: (delta) => onProgress?.(delta),
18076
18190
  onThinking: (delta) => onProgress?.(`\uD83D\uDCAD ${delta}`),
18077
18191
  onToolStart: (tool) => onProgress?.(`
@@ -18100,7 +18214,8 @@ You have access via Bash:
18100
18214
  }
18101
18215
  const beadStatus = isCancelled ? "CANCELLED" : "ERROR";
18102
18216
  if (beadId) {
18103
- beadsClient?.closeBead(beadId, beadStatus, Date.now() - start, model);
18217
+ if (ownsBead)
18218
+ beadsClient?.closeBead(beadId, beadStatus, Date.now() - start, model);
18104
18219
  beadsClient?.auditBead(beadId, metadata.name, model, 1);
18105
18220
  }
18106
18221
  await hooks.emit("post_execute", invocationId, metadata.name, metadata.version, {
@@ -18123,7 +18238,8 @@ You have access via Bash:
18123
18238
  output_valid: true
18124
18239
  });
18125
18240
  if (beadId) {
18126
- beadsClient?.closeBead(beadId, "COMPLETE", durationMs, model);
18241
+ if (ownsBead)
18242
+ beadsClient?.closeBead(beadId, "COMPLETE", durationMs, model);
18127
18243
  beadsClient?.auditBead(beadId, metadata.name, model, 0);
18128
18244
  }
18129
18245
  return {
@@ -18132,6 +18248,7 @@ You have access via Bash:
18132
18248
  model,
18133
18249
  durationMs,
18134
18250
  specialistVersion: metadata.version,
18251
+ promptHash,
18135
18252
  beadId
18136
18253
  };
18137
18254
  }
@@ -18234,9 +18351,9 @@ __export(exports_install, {
18234
18351
  });
18235
18352
  import { execFileSync } from "node:child_process";
18236
18353
  import { fileURLToPath } from "node:url";
18237
- import { dirname as dirname2, join as join4 } from "node:path";
18354
+ import { dirname as dirname2, join as join5 } from "node:path";
18238
18355
  async function run() {
18239
- const installerPath = join4(dirname2(fileURLToPath(import.meta.url)), "..", "bin", "install.js");
18356
+ const installerPath = join5(dirname2(fileURLToPath(import.meta.url)), "..", "bin", "install.js");
18240
18357
  execFileSync(process.execPath, [installerPath], { stdio: "inherit" });
18241
18358
  }
18242
18359
  var init_install = () => {};
@@ -18281,6 +18398,10 @@ function parseArgs(argv) {
18281
18398
  result.scope = value;
18282
18399
  continue;
18283
18400
  }
18401
+ if (token === "--json") {
18402
+ result.json = true;
18403
+ continue;
18404
+ }
18284
18405
  }
18285
18406
  return result;
18286
18407
  }
@@ -18300,6 +18421,10 @@ async function run3() {
18300
18421
  if (args.scope) {
18301
18422
  specialists = specialists.filter((s) => s.scope === args.scope);
18302
18423
  }
18424
+ if (args.json) {
18425
+ console.log(JSON.stringify(specialists, null, 2));
18426
+ return;
18427
+ }
18303
18428
  if (specialists.length === 0) {
18304
18429
  console.log("No specialists found.");
18305
18430
  return;
@@ -18383,7 +18508,7 @@ async function run4() {
18383
18508
  }
18384
18509
  const allModels = parsePiModels();
18385
18510
  if (!allModels) {
18386
- console.error("pi not found or failed — run specialists install");
18511
+ console.error("pi not found or failed — install and configure pi first");
18387
18512
  process.exit(1);
18388
18513
  }
18389
18514
  let models = allModels;
@@ -18439,36 +18564,62 @@ var exports_init = {};
18439
18564
  __export(exports_init, {
18440
18565
  run: () => run5
18441
18566
  });
18442
- import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18443
- import { join as join5 } from "node:path";
18567
+ import { existsSync as existsSync4, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18568
+ import { join as join6 } from "node:path";
18444
18569
  function ok(msg) {
18445
18570
  console.log(` ${green2("✓")} ${msg}`);
18446
18571
  }
18447
18572
  function skip(msg) {
18448
18573
  console.log(` ${yellow3("○")} ${msg}`);
18449
18574
  }
18575
+ function loadJson(path, fallback) {
18576
+ if (!existsSync4(path))
18577
+ return structuredClone(fallback);
18578
+ try {
18579
+ return JSON.parse(readFileSync(path, "utf-8"));
18580
+ } catch {
18581
+ return structuredClone(fallback);
18582
+ }
18583
+ }
18584
+ function saveJson(path, value) {
18585
+ writeFileSync(path, JSON.stringify(value, null, 2) + `
18586
+ `, "utf-8");
18587
+ }
18588
+ function ensureProjectMcp(cwd) {
18589
+ const mcpPath = join6(cwd, MCP_FILE);
18590
+ const mcp = loadJson(mcpPath, { mcpServers: {} });
18591
+ mcp.mcpServers ??= {};
18592
+ const existing = mcp.mcpServers[MCP_SERVER_NAME];
18593
+ if (existing && existing.command === MCP_SERVER_CONFIG.command && Array.isArray(existing.args) && existing.args.length === MCP_SERVER_CONFIG.args.length) {
18594
+ skip(".mcp.json already registers specialists");
18595
+ return;
18596
+ }
18597
+ mcp.mcpServers[MCP_SERVER_NAME] = MCP_SERVER_CONFIG;
18598
+ saveJson(mcpPath, mcp);
18599
+ ok("registered specialists in project .mcp.json");
18600
+ }
18450
18601
  async function run5() {
18451
18602
  const cwd = process.cwd();
18452
18603
  console.log(`
18453
18604
  ${bold3("specialists init")}
18454
18605
  `);
18455
- const specialistsDir = join5(cwd, "specialists");
18456
- if (existsSync3(specialistsDir)) {
18606
+ const specialistsDir = join6(cwd, "specialists");
18607
+ if (existsSync4(specialistsDir)) {
18457
18608
  skip("specialists/ already exists");
18458
18609
  } else {
18459
18610
  mkdirSync(specialistsDir, { recursive: true });
18460
18611
  ok("created specialists/");
18461
18612
  }
18462
- const runtimeDir = join5(cwd, ".specialists");
18463
- if (existsSync3(runtimeDir)) {
18613
+ const runtimeDir = join6(cwd, ".specialists");
18614
+ if (existsSync4(runtimeDir)) {
18464
18615
  skip(".specialists/ already exists");
18465
18616
  } else {
18466
- mkdirSync(join5(runtimeDir, "jobs"), { recursive: true });
18467
- mkdirSync(join5(runtimeDir, "ready"), { recursive: true });
18617
+ mkdirSync(join6(runtimeDir, "jobs"), { recursive: true });
18618
+ mkdirSync(join6(runtimeDir, "ready"), { recursive: true });
18468
18619
  ok("created .specialists/ (jobs/, ready/)");
18469
18620
  }
18470
- const gitignorePath = join5(cwd, ".gitignore");
18471
- if (existsSync3(gitignorePath)) {
18621
+ const gitignorePath = join6(cwd, ".gitignore");
18622
+ if (existsSync4(gitignorePath)) {
18472
18623
  const existing = readFileSync(gitignorePath, "utf-8");
18473
18624
  if (existing.includes(GITIGNORE_ENTRY)) {
18474
18625
  skip(".gitignore already has .specialists/ entry");
@@ -18485,8 +18636,8 @@ ${bold3("specialists init")}
18485
18636
  `, "utf-8");
18486
18637
  ok("created .gitignore with .specialists/ entry");
18487
18638
  }
18488
- const agentsPath = join5(cwd, "AGENTS.md");
18489
- if (existsSync3(agentsPath)) {
18639
+ const agentsPath = join6(cwd, "AGENTS.md");
18640
+ if (existsSync4(agentsPath)) {
18490
18641
  const existing = readFileSync(agentsPath, "utf-8");
18491
18642
  if (existing.includes(AGENTS_MARKER)) {
18492
18643
  skip("AGENTS.md already has Specialists section");
@@ -18500,16 +18651,17 @@ ${bold3("specialists init")}
18500
18651
  writeFileSync(agentsPath, AGENTS_BLOCK, "utf-8");
18501
18652
  ok("created AGENTS.md with Specialists section");
18502
18653
  }
18654
+ ensureProjectMcp(cwd);
18503
18655
  console.log(`
18504
18656
  ${bold3("Done!")}
18505
18657
  `);
18506
18658
  console.log(` ${dim3("Next steps:")}`);
18507
18659
  console.log(` 1. Add your specialists to ${yellow3("specialists/")}`);
18508
18660
  console.log(` 2. Run ${yellow3("specialists list")} to verify they are discovered`);
18509
- console.log(` 3. Restart Claude Code to pick up AGENTS.md changes
18661
+ console.log(` 3. Restart Claude Code to pick up AGENTS.md / .mcp.json changes
18510
18662
  `);
18511
18663
  }
18512
- var bold3 = (s) => `\x1B[1m${s}\x1B[0m`, green2 = (s) => `\x1B[32m${s}\x1B[0m`, yellow3 = (s) => `\x1B[33m${s}\x1B[0m`, dim3 = (s) => `\x1B[2m${s}\x1B[0m`, AGENTS_BLOCK, AGENTS_MARKER = "## Specialists", GITIGNORE_ENTRY = ".specialists/";
18664
+ var bold3 = (s) => `\x1B[1m${s}\x1B[0m`, green2 = (s) => `\x1B[32m${s}\x1B[0m`, yellow3 = (s) => `\x1B[33m${s}\x1B[0m`, dim3 = (s) => `\x1B[2m${s}\x1B[0m`, AGENTS_BLOCK, AGENTS_MARKER = "## Specialists", GITIGNORE_ENTRY = ".specialists/", MCP_FILE = ".mcp.json", MCP_SERVER_NAME = "specialists", MCP_SERVER_CONFIG;
18513
18665
  var init_init = __esm(() => {
18514
18666
  AGENTS_BLOCK = `
18515
18667
  ## Specialists
@@ -18519,6 +18671,7 @@ see available specialists. Use \`use_specialist\` or \`start_specialist\` to
18519
18671
  delegate heavy tasks (code review, bug hunting, deep reasoning) to the right
18520
18672
  specialist without user intervention.
18521
18673
  `.trimStart();
18674
+ MCP_SERVER_CONFIG = { command: "specialists", args: [] };
18522
18675
  });
18523
18676
 
18524
18677
  // src/cli/edit.ts
@@ -18651,10 +18804,119 @@ var init_edit = __esm(() => {
18651
18804
  VALID_PERMISSIONS = ["READ_ONLY", "LOW", "MEDIUM", "HIGH"];
18652
18805
  });
18653
18806
 
18807
+ // src/specialist/timeline-events.ts
18808
+ function mapCallbackEventToTimelineEvent(callbackEvent, context) {
18809
+ const t = Date.now();
18810
+ switch (callbackEvent) {
18811
+ case "thinking":
18812
+ return { t, type: TIMELINE_EVENT_TYPES.THINKING };
18813
+ case "toolcall":
18814
+ return {
18815
+ t,
18816
+ type: TIMELINE_EVENT_TYPES.TOOL,
18817
+ tool: context.tool ?? "unknown",
18818
+ phase: "start",
18819
+ tool_call_id: context.toolCallId
18820
+ };
18821
+ case "tool_execution_end":
18822
+ return {
18823
+ t,
18824
+ type: TIMELINE_EVENT_TYPES.TOOL,
18825
+ tool: context.tool ?? "unknown",
18826
+ phase: "end",
18827
+ tool_call_id: context.toolCallId,
18828
+ is_error: context.isError
18829
+ };
18830
+ case "text":
18831
+ return { t, type: TIMELINE_EVENT_TYPES.TEXT };
18832
+ case "agent_end":
18833
+ case "message_done":
18834
+ case "done":
18835
+ return null;
18836
+ default:
18837
+ return null;
18838
+ }
18839
+ }
18840
+ function createRunStartEvent(specialist, beadId) {
18841
+ return {
18842
+ t: Date.now(),
18843
+ type: TIMELINE_EVENT_TYPES.RUN_START,
18844
+ specialist,
18845
+ bead_id: beadId
18846
+ };
18847
+ }
18848
+ function createMetaEvent(model, backend) {
18849
+ return {
18850
+ t: Date.now(),
18851
+ type: TIMELINE_EVENT_TYPES.META,
18852
+ model,
18853
+ backend
18854
+ };
18855
+ }
18856
+ function createRunCompleteEvent(status, elapsed_s, options) {
18857
+ return {
18858
+ t: Date.now(),
18859
+ type: TIMELINE_EVENT_TYPES.RUN_COMPLETE,
18860
+ status,
18861
+ elapsed_s,
18862
+ ...options
18863
+ };
18864
+ }
18865
+ function parseTimelineEvent(line) {
18866
+ try {
18867
+ const parsed = JSON.parse(line);
18868
+ if (!parsed || typeof parsed !== "object")
18869
+ return null;
18870
+ if (typeof parsed.t !== "number")
18871
+ return null;
18872
+ if (typeof parsed.type !== "string")
18873
+ return null;
18874
+ if (parsed.type === TIMELINE_EVENT_TYPES.DONE) {
18875
+ return {
18876
+ t: parsed.t,
18877
+ type: TIMELINE_EVENT_TYPES.DONE,
18878
+ elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
18879
+ };
18880
+ }
18881
+ if (parsed.type === TIMELINE_EVENT_TYPES.AGENT_END) {
18882
+ return {
18883
+ t: parsed.t,
18884
+ type: TIMELINE_EVENT_TYPES.AGENT_END,
18885
+ elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
18886
+ };
18887
+ }
18888
+ const knownTypes = Object.values(TIMELINE_EVENT_TYPES).filter((type) => type !== TIMELINE_EVENT_TYPES.DONE && type !== TIMELINE_EVENT_TYPES.AGENT_END);
18889
+ if (!knownTypes.includes(parsed.type))
18890
+ return null;
18891
+ return parsed;
18892
+ } catch {
18893
+ return null;
18894
+ }
18895
+ }
18896
+ function isRunCompleteEvent(event) {
18897
+ return event.type === TIMELINE_EVENT_TYPES.RUN_COMPLETE;
18898
+ }
18899
+ function compareTimelineEvents(a, b) {
18900
+ return a.t - b.t;
18901
+ }
18902
+ var TIMELINE_EVENT_TYPES;
18903
+ var init_timeline_events = __esm(() => {
18904
+ TIMELINE_EVENT_TYPES = {
18905
+ RUN_START: "run_start",
18906
+ META: "meta",
18907
+ THINKING: "thinking",
18908
+ TOOL: "tool",
18909
+ TEXT: "text",
18910
+ RUN_COMPLETE: "run_complete",
18911
+ DONE: "done",
18912
+ AGENT_END: "agent_end"
18913
+ };
18914
+ });
18915
+
18654
18916
  // src/specialist/supervisor.ts
18655
18917
  import {
18656
18918
  closeSync,
18657
- existsSync as existsSync4,
18919
+ existsSync as existsSync5,
18658
18920
  mkdirSync as mkdirSync2,
18659
18921
  openSync,
18660
18922
  readdirSync,
@@ -18665,7 +18927,32 @@ import {
18665
18927
  writeFileSync as writeFileSync3,
18666
18928
  writeSync
18667
18929
  } from "node:fs";
18668
- import { join as join6 } from "node:path";
18930
+ import { join as join7 } from "node:path";
18931
+ import { spawnSync as spawnSync4 } from "node:child_process";
18932
+ function getCurrentGitSha() {
18933
+ const result = spawnSync4("git", ["rev-parse", "HEAD"], {
18934
+ encoding: "utf-8",
18935
+ stdio: ["ignore", "pipe", "ignore"]
18936
+ });
18937
+ if (result.status !== 0)
18938
+ return;
18939
+ const sha = result.stdout?.trim();
18940
+ return sha || undefined;
18941
+ }
18942
+ function formatBeadNotes(result) {
18943
+ const metadata = [
18944
+ `prompt_hash=${result.promptHash}`,
18945
+ `git_sha=${getCurrentGitSha() ?? "unknown"}`,
18946
+ `elapsed_ms=${Math.round(result.durationMs)}`,
18947
+ `model=${result.model}`,
18948
+ `backend=${result.backend}`
18949
+ ].join(`
18950
+ `);
18951
+ return `${result.output}
18952
+
18953
+ ---
18954
+ ${metadata}`;
18955
+ }
18669
18956
 
18670
18957
  class Supervisor {
18671
18958
  opts;
@@ -18673,23 +18960,23 @@ class Supervisor {
18673
18960
  this.opts = opts;
18674
18961
  }
18675
18962
  jobDir(id) {
18676
- return join6(this.opts.jobsDir, id);
18963
+ return join7(this.opts.jobsDir, id);
18677
18964
  }
18678
18965
  statusPath(id) {
18679
- return join6(this.jobDir(id), "status.json");
18966
+ return join7(this.jobDir(id), "status.json");
18680
18967
  }
18681
18968
  resultPath(id) {
18682
- return join6(this.jobDir(id), "result.txt");
18969
+ return join7(this.jobDir(id), "result.txt");
18683
18970
  }
18684
18971
  eventsPath(id) {
18685
- return join6(this.jobDir(id), "events.jsonl");
18972
+ return join7(this.jobDir(id), "events.jsonl");
18686
18973
  }
18687
18974
  readyDir() {
18688
- return join6(this.opts.jobsDir, "..", "ready");
18975
+ return join7(this.opts.jobsDir, "..", "ready");
18689
18976
  }
18690
18977
  readStatus(id) {
18691
18978
  const path = this.statusPath(id);
18692
- if (!existsSync4(path))
18979
+ if (!existsSync5(path))
18693
18980
  return null;
18694
18981
  try {
18695
18982
  return JSON.parse(readFileSync3(path, "utf-8"));
@@ -18698,12 +18985,12 @@ class Supervisor {
18698
18985
  }
18699
18986
  }
18700
18987
  listJobs() {
18701
- if (!existsSync4(this.opts.jobsDir))
18988
+ if (!existsSync5(this.opts.jobsDir))
18702
18989
  return [];
18703
18990
  const jobs = [];
18704
18991
  for (const entry of readdirSync(this.opts.jobsDir)) {
18705
- const path = join6(this.opts.jobsDir, entry, "status.json");
18706
- if (!existsSync4(path))
18992
+ const path = join7(this.opts.jobsDir, entry, "status.json");
18993
+ if (!existsSync5(path))
18707
18994
  continue;
18708
18995
  try {
18709
18996
  jobs.push(JSON.parse(readFileSync3(path, "utf-8")));
@@ -18724,11 +19011,11 @@ class Supervisor {
18724
19011
  this.writeStatusFile(id, { ...current, ...updates });
18725
19012
  }
18726
19013
  gc() {
18727
- if (!existsSync4(this.opts.jobsDir))
19014
+ if (!existsSync5(this.opts.jobsDir))
18728
19015
  return;
18729
19016
  const cutoff = Date.now() - JOB_TTL_DAYS * 86400000;
18730
19017
  for (const entry of readdirSync(this.opts.jobsDir)) {
18731
- const dir = join6(this.opts.jobsDir, entry);
19018
+ const dir = join7(this.opts.jobsDir, entry);
18732
19019
  try {
18733
19020
  const stat2 = statSync(dir);
18734
19021
  if (!stat2.isDirectory())
@@ -18739,11 +19026,11 @@ class Supervisor {
18739
19026
  }
18740
19027
  }
18741
19028
  crashRecovery() {
18742
- if (!existsSync4(this.opts.jobsDir))
19029
+ if (!existsSync5(this.opts.jobsDir))
18743
19030
  return;
18744
19031
  for (const entry of readdirSync(this.opts.jobsDir)) {
18745
- const statusPath = join6(this.opts.jobsDir, entry, "status.json");
18746
- if (!existsSync4(statusPath))
19032
+ const statusPath = join7(this.opts.jobsDir, entry, "status.json");
19033
+ if (!existsSync5(statusPath))
18747
19034
  continue;
18748
19035
  try {
18749
19036
  const s = JSON.parse(readFileSync3(statusPath, "utf-8"));
@@ -18780,14 +19067,19 @@ class Supervisor {
18780
19067
  };
18781
19068
  this.writeStatusFile(id, initialStatus);
18782
19069
  const eventsFd = openSync(this.eventsPath(id), "a");
18783
- const appendEvent = (obj) => {
19070
+ const appendTimelineEvent = (event) => {
18784
19071
  try {
18785
- writeSync(eventsFd, JSON.stringify({ t: Date.now(), ...obj }) + `
19072
+ writeSync(eventsFd, JSON.stringify(event) + `
18786
19073
  `);
18787
19074
  } catch {}
18788
19075
  };
19076
+ appendTimelineEvent(createRunStartEvent(runOptions.name));
18789
19077
  let textLogged = false;
18790
19078
  let currentTool = "";
19079
+ let currentToolCallId = "";
19080
+ let killFn;
19081
+ const sigtermHandler = () => killFn?.();
19082
+ process.once("SIGTERM", sigtermHandler);
18791
19083
  try {
18792
19084
  const result = await runner.run(runOptions, (delta) => {
18793
19085
  const toolMatch = delta.match(/⚙ (.+?)…/);
@@ -18803,21 +19095,29 @@ class Supervisor {
18803
19095
  last_event_at_ms: now,
18804
19096
  elapsed_s: Math.round((now - startedAtMs) / 1000)
18805
19097
  });
18806
- if (LOGGED_EVENTS.has(eventType)) {
18807
- const tool = eventType === "toolcall" || eventType === "tool_execution_end" ? currentTool : undefined;
18808
- appendEvent({ type: eventType, ...tool ? { tool } : {} });
19098
+ const timelineEvent = mapCallbackEventToTimelineEvent(eventType, {
19099
+ tool: currentTool,
19100
+ toolCallId: currentToolCallId || undefined
19101
+ });
19102
+ if (timelineEvent) {
19103
+ appendTimelineEvent(timelineEvent);
18809
19104
  } else if (eventType === "text" && !textLogged) {
18810
19105
  textLogged = true;
18811
- appendEvent({ type: "text" });
19106
+ appendTimelineEvent({ t: Date.now(), type: TIMELINE_EVENT_TYPES.TEXT });
18812
19107
  }
18813
19108
  }, (meta) => {
18814
19109
  this.updateStatus(id, { model: meta.model, backend: meta.backend });
18815
- appendEvent({ type: "meta", model: meta.model, backend: meta.backend });
18816
- }, (_killFn) => {}, (beadId) => {
19110
+ appendTimelineEvent(createMetaEvent(meta.model, meta.backend));
19111
+ }, (fn) => {
19112
+ killFn = fn;
19113
+ }, (beadId) => {
18817
19114
  this.updateStatus(id, { bead_id: beadId });
18818
19115
  });
18819
19116
  const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
18820
19117
  writeFileSync3(this.resultPath(id), result.output, "utf-8");
19118
+ if (result.beadId) {
19119
+ this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
19120
+ }
18821
19121
  this.updateStatus(id, {
18822
19122
  status: "done",
18823
19123
  elapsed_s: elapsed,
@@ -18826,27 +19126,35 @@ class Supervisor {
18826
19126
  backend: result.backend,
18827
19127
  bead_id: result.beadId
18828
19128
  });
18829
- appendEvent({ type: "agent_end", elapsed_s: elapsed });
18830
- writeFileSync3(join6(this.readyDir(), id), "", "utf-8");
19129
+ appendTimelineEvent(createRunCompleteEvent("COMPLETE", elapsed, {
19130
+ model: result.model,
19131
+ backend: result.backend,
19132
+ bead_id: result.beadId
19133
+ }));
19134
+ writeFileSync3(join7(this.readyDir(), id), "", "utf-8");
18831
19135
  return id;
18832
19136
  } catch (err) {
18833
19137
  const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
19138
+ const errorMsg = err?.message ?? String(err);
18834
19139
  this.updateStatus(id, {
18835
19140
  status: "error",
18836
19141
  elapsed_s: elapsed,
18837
- error: err?.message ?? String(err)
19142
+ error: errorMsg
18838
19143
  });
18839
- appendEvent({ type: "error", message: err?.message ?? String(err) });
19144
+ appendTimelineEvent(createRunCompleteEvent("ERROR", elapsed, {
19145
+ error: errorMsg
19146
+ }));
18840
19147
  throw err;
18841
19148
  } finally {
19149
+ process.removeListener("SIGTERM", sigtermHandler);
18842
19150
  closeSync(eventsFd);
18843
19151
  }
18844
19152
  }
18845
19153
  }
18846
- var JOB_TTL_DAYS, LOGGED_EVENTS;
19154
+ var JOB_TTL_DAYS;
18847
19155
  var init_supervisor = __esm(() => {
19156
+ init_timeline_events();
18848
19157
  JOB_TTL_DAYS = Number(process.env.SPECIALISTS_JOB_TTL_DAYS ?? 7);
18849
- LOGGED_EVENTS = new Set(["thinking", "toolcall", "tool_execution_end", "done"]);
18850
19158
  });
18851
19159
 
18852
19160
  // src/cli/run.ts
@@ -18854,27 +19162,37 @@ var exports_run = {};
18854
19162
  __export(exports_run, {
18855
19163
  run: () => run7
18856
19164
  });
18857
- import { join as join7 } from "node:path";
19165
+ import { join as join8 } from "node:path";
18858
19166
  async function parseArgs4(argv) {
18859
19167
  const name = argv[0];
18860
19168
  if (!name || name.startsWith("--")) {
18861
- console.error('Usage: specialists run <name> [--prompt "..."] [--model <model>] [--no-beads] [--background]');
19169
+ console.error('Usage: specialists run <name> [--prompt "..."] [--bead <id>] [--context-depth <n>] [--model <model>] [--no-beads] [--background]');
18862
19170
  process.exit(1);
18863
19171
  }
18864
19172
  let prompt = "";
19173
+ let beadId;
18865
19174
  let model;
18866
19175
  let noBeads = false;
18867
19176
  let background = false;
19177
+ let contextDepth = 1;
18868
19178
  for (let i = 1;i < argv.length; i++) {
18869
19179
  const token = argv[i];
18870
19180
  if (token === "--prompt" && argv[i + 1]) {
18871
19181
  prompt = argv[++i];
18872
19182
  continue;
18873
19183
  }
19184
+ if (token === "--bead" && argv[i + 1]) {
19185
+ beadId = argv[++i];
19186
+ continue;
19187
+ }
18874
19188
  if (token === "--model" && argv[i + 1]) {
18875
19189
  model = argv[++i];
18876
19190
  continue;
18877
19191
  }
19192
+ if (token === "--context-depth" && argv[i + 1]) {
19193
+ contextDepth = parseInt(argv[++i], 10) || 0;
19194
+ continue;
19195
+ }
18878
19196
  if (token === "--no-beads") {
18879
19197
  noBeads = true;
18880
19198
  continue;
@@ -18884,10 +19202,11 @@ async function parseArgs4(argv) {
18884
19202
  continue;
18885
19203
  }
18886
19204
  }
18887
- if (!prompt) {
18888
- if (process.stdin.isTTY) {
18889
- process.stderr.write(dim5("Prompt (Ctrl+D when done): "));
18890
- }
19205
+ if (prompt && beadId) {
19206
+ console.error("Error: use either --prompt or --bead, not both.");
19207
+ process.exit(1);
19208
+ }
19209
+ if (!prompt && !beadId && !process.stdin.isTTY) {
18891
19210
  prompt = await new Promise((resolve) => {
18892
19211
  let buf = "";
18893
19212
  process.stdin.setEncoding("utf-8");
@@ -18897,26 +19216,58 @@ async function parseArgs4(argv) {
18897
19216
  process.stdin.on("end", () => resolve(buf.trim()));
18898
19217
  });
18899
19218
  }
18900
- return { name, prompt, model, noBeads, background };
19219
+ if (!prompt && !beadId) {
19220
+ console.error("Error: provide --prompt, pipe stdin, or use --bead <id>.");
19221
+ process.exit(1);
19222
+ }
19223
+ return { name, prompt, beadId, model, noBeads, background, contextDepth };
18901
19224
  }
18902
19225
  async function run7() {
18903
19226
  const args = await parseArgs4(process.argv.slice(3));
18904
19227
  const loader = new SpecialistLoader;
18905
19228
  const circuitBreaker = new CircuitBreaker;
18906
- const hooks = new HookEmitter({ tracePath: join7(process.cwd(), ".specialists", "trace.jsonl") });
18907
- const beadsClient = args.noBeads ? null : new BeadsClient;
19229
+ const hooks = new HookEmitter({ tracePath: join8(process.cwd(), ".specialists", "trace.jsonl") });
19230
+ const beadsClient = args.noBeads ? undefined : new BeadsClient;
19231
+ const beadReader = beadsClient ?? new BeadsClient;
19232
+ let prompt = args.prompt;
19233
+ let variables;
19234
+ if (args.beadId) {
19235
+ const bead = beadReader.readBead(args.beadId);
19236
+ if (!bead) {
19237
+ throw new Error(`Unable to read bead '${args.beadId}' via bd show --json`);
19238
+ }
19239
+ const blockers = args.contextDepth > 0 ? beadReader.getCompletedBlockers(args.beadId, args.contextDepth) : [];
19240
+ if (blockers.length > 0) {
19241
+ process.stderr.write(dim5(`
19242
+ [context: ${blockers.length} completed dep${blockers.length > 1 ? "s" : ""} injected]
19243
+ `));
19244
+ }
19245
+ const beadContext = buildBeadContext(bead, blockers);
19246
+ prompt = beadContext;
19247
+ variables = {
19248
+ bead_context: beadContext,
19249
+ bead_id: args.beadId
19250
+ };
19251
+ }
18908
19252
  const runner = new SpecialistRunner({
18909
19253
  loader,
18910
19254
  hooks,
18911
19255
  circuitBreaker,
18912
- beadsClient: beadsClient ?? undefined
19256
+ beadsClient
18913
19257
  });
18914
19258
  if (args.background) {
18915
- const jobsDir = join7(process.cwd(), ".specialists", "jobs");
19259
+ const jobsDir = join8(process.cwd(), ".specialists", "jobs");
18916
19260
  const supervisor = new Supervisor({
18917
19261
  runner,
18918
- runOptions: { name: args.name, prompt: args.prompt, backendOverride: args.model },
18919
- jobsDir
19262
+ runOptions: {
19263
+ name: args.name,
19264
+ prompt,
19265
+ variables,
19266
+ backendOverride: args.model,
19267
+ inputBeadId: args.beadId
19268
+ },
19269
+ jobsDir,
19270
+ beadsClient
18920
19271
  });
18921
19272
  try {
18922
19273
  const jobId = await supervisor.run();
@@ -18933,11 +19284,13 @@ async function run7() {
18933
19284
  ${bold5(`Running ${cyan3(args.name)}`)}
18934
19285
 
18935
19286
  `);
18936
- let beadId;
19287
+ let trackingBeadId;
18937
19288
  const result = await runner.run({
18938
19289
  name: args.name,
18939
- prompt: args.prompt,
18940
- backendOverride: args.model
19290
+ prompt,
19291
+ variables,
19292
+ backendOverride: args.model,
19293
+ inputBeadId: args.beadId
18941
19294
  }, (delta) => process.stdout.write(delta), undefined, (meta) => process.stderr.write(dim5(`
18942
19295
  [${meta.backend} / ${meta.model}]
18943
19296
 
@@ -18950,10 +19303,10 @@ Interrupted.
18950
19303
  killFn();
18951
19304
  process.exit(130);
18952
19305
  });
18953
- }, (id) => {
18954
- beadId = id;
19306
+ }, (beadId) => {
19307
+ trackingBeadId = beadId;
18955
19308
  process.stderr.write(dim5(`
18956
- [bead: ${id}]
19309
+ [bead: ${beadId}]
18957
19310
  `));
18958
19311
  });
18959
19312
  if (result.output && !result.output.endsWith(`
@@ -18961,8 +19314,9 @@ Interrupted.
18961
19314
  process.stdout.write(`
18962
19315
  `);
18963
19316
  const secs = (result.durationMs / 1000).toFixed(1);
19317
+ const effectiveBeadId = args.beadId ?? trackingBeadId;
18964
19318
  const footer = [
18965
- beadId ? `bead ${beadId}` : "",
19319
+ effectiveBeadId ? `bead ${effectiveBeadId}` : "",
18966
19320
  `${secs}s`,
18967
19321
  dim5(result.model)
18968
19322
  ].filter(Boolean).join(" ");
@@ -18980,14 +19334,103 @@ var init_run = __esm(() => {
18980
19334
  init_supervisor();
18981
19335
  });
18982
19336
 
19337
+ // src/cli/format-helpers.ts
19338
+ function formatTime(t) {
19339
+ return new Date(t).toISOString().slice(11, 19);
19340
+ }
19341
+ function formatElapsed(seconds) {
19342
+ if (seconds < 60)
19343
+ return `${seconds}s`;
19344
+ const m = Math.floor(seconds / 60);
19345
+ const s = seconds % 60;
19346
+ return s > 0 ? `${m}m ${s}s` : `${m}m`;
19347
+ }
19348
+ function getEventLabel(type) {
19349
+ return EVENT_LABELS[type] ?? type.slice(0, 5).toUpperCase();
19350
+ }
19351
+
19352
+ class JobColorMap {
19353
+ colors = new Map;
19354
+ nextIdx = 0;
19355
+ getColor(jobId) {
19356
+ let color = this.colors.get(jobId);
19357
+ if (!color) {
19358
+ color = JOB_COLORS[this.nextIdx % JOB_COLORS.length];
19359
+ this.colors.set(jobId, color);
19360
+ this.nextIdx++;
19361
+ }
19362
+ return color;
19363
+ }
19364
+ get(jobId) {
19365
+ return this.getColor(jobId);
19366
+ }
19367
+ has(jobId) {
19368
+ return this.colors.has(jobId);
19369
+ }
19370
+ get size() {
19371
+ return this.colors.size;
19372
+ }
19373
+ }
19374
+ function formatEventLine(event, options) {
19375
+ const ts = dim6(formatTime(event.t));
19376
+ const label = options.colorize(bold6(getEventLabel(event.type).padEnd(5)));
19377
+ const prefix = `${options.colorize(`[${options.jobId}]`)} ${options.specialist}${options.beadId ? ` ${dim6(`[${options.beadId}]`)}` : ""}`;
19378
+ const detailParts = [];
19379
+ if (event.type === "meta") {
19380
+ detailParts.push(`model=${event.model}`);
19381
+ detailParts.push(`backend=${event.backend}`);
19382
+ } else if (event.type === "tool") {
19383
+ detailParts.push(`tool=${event.tool}`);
19384
+ detailParts.push(`phase=${event.phase}`);
19385
+ if (event.phase === "end") {
19386
+ detailParts.push(`ok=${event.is_error ? "false" : "true"}`);
19387
+ }
19388
+ } else if (event.type === "run_complete") {
19389
+ detailParts.push(`status=${event.status}`);
19390
+ detailParts.push(`elapsed=${formatElapsed(event.elapsed_s)}`);
19391
+ if (event.error) {
19392
+ detailParts.push(`error=${event.error}`);
19393
+ }
19394
+ } else if (event.type === "done" || event.type === "agent_end") {
19395
+ detailParts.push("status=COMPLETE");
19396
+ detailParts.push(`elapsed=${formatElapsed(event.elapsed_s ?? 0)}`);
19397
+ } else if (event.type === "run_start") {
19398
+ detailParts.push(`specialist=${event.specialist}`);
19399
+ if (event.bead_id) {
19400
+ detailParts.push(`bead=${event.bead_id}`);
19401
+ }
19402
+ } else if (event.type === "text") {
19403
+ detailParts.push("kind=assistant");
19404
+ } else if (event.type === "thinking") {
19405
+ detailParts.push("kind=model");
19406
+ }
19407
+ const detail = detailParts.length > 0 ? dim6(detailParts.join(" ")) : "";
19408
+ return `${ts} ${prefix} ${label}${detail ? ` ${detail}` : ""}`.trimEnd();
19409
+ }
19410
+ var dim6 = (s) => `\x1B[2m${s}\x1B[0m`, bold6 = (s) => `\x1B[1m${s}\x1B[0m`, cyan4 = (s) => `\x1B[36m${s}\x1B[0m`, yellow5 = (s) => `\x1B[33m${s}\x1B[0m`, red = (s) => `\x1B[31m${s}\x1B[0m`, green5 = (s) => `\x1B[32m${s}\x1B[0m`, blue = (s) => `\x1B[34m${s}\x1B[0m`, magenta = (s) => `\x1B[35m${s}\x1B[0m`, JOB_COLORS, EVENT_LABELS;
19411
+ var init_format_helpers = __esm(() => {
19412
+ JOB_COLORS = [cyan4, yellow5, magenta, green5, blue, red];
19413
+ EVENT_LABELS = {
19414
+ run_start: "START",
19415
+ meta: "META",
19416
+ thinking: "THINK",
19417
+ tool: "TOOL",
19418
+ text: "TEXT",
19419
+ run_complete: "DONE",
19420
+ done: "DONE",
19421
+ agent_end: "DONE",
19422
+ error: "ERR"
19423
+ };
19424
+ });
19425
+
18983
19426
  // src/cli/status.ts
18984
19427
  var exports_status = {};
18985
19428
  __export(exports_status, {
18986
19429
  run: () => run8
18987
19430
  });
18988
- import { spawnSync as spawnSync4 } from "node:child_process";
18989
- import { existsSync as existsSync5 } from "node:fs";
18990
- import { join as join8 } from "node:path";
19431
+ import { spawnSync as spawnSync5 } from "node:child_process";
19432
+ import { existsSync as existsSync6 } from "node:fs";
19433
+ import { join as join9 } from "node:path";
18991
19434
  function ok2(msg) {
18992
19435
  console.log(` ${green5("✓")} ${msg}`);
18993
19436
  }
@@ -18995,7 +19438,7 @@ function warn(msg) {
18995
19438
  console.log(` ${yellow5("○")} ${msg}`);
18996
19439
  }
18997
19440
  function fail(msg) {
18998
- console.log(` ${red("✗")} ${msg}`);
19441
+ console.log(` ${red2("✗")} ${msg}`);
18999
19442
  }
19000
19443
  function info(msg) {
19001
19444
  console.log(` ${dim6(msg)}`);
@@ -19006,7 +19449,7 @@ function section(label) {
19006
19449
  ${bold6(`── ${label} ${line}`)}`);
19007
19450
  }
19008
19451
  function cmd(bin, args) {
19009
- const r = spawnSync4(bin, args, {
19452
+ const r = spawnSync5(bin, args, {
19010
19453
  encoding: "utf8",
19011
19454
  stdio: "pipe",
19012
19455
  timeout: 5000
@@ -19014,9 +19457,9 @@ function cmd(bin, args) {
19014
19457
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
19015
19458
  }
19016
19459
  function isInstalled(bin) {
19017
- return spawnSync4("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
19460
+ return spawnSync5("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
19018
19461
  }
19019
- function formatElapsed(s) {
19462
+ function formatElapsed2(s) {
19020
19463
  if (s.elapsed_s === undefined)
19021
19464
  return "...";
19022
19465
  const m = Math.floor(s.elapsed_s / 60);
@@ -19026,11 +19469,11 @@ function formatElapsed(s) {
19026
19469
  function statusColor(status) {
19027
19470
  switch (status) {
19028
19471
  case "running":
19029
- return cyan4(status);
19472
+ return cyan5(status);
19030
19473
  case "done":
19031
19474
  return green5(status);
19032
19475
  case "error":
19033
- return red(status);
19476
+ return red2(status);
19034
19477
  case "starting":
19035
19478
  return yellow5(status);
19036
19479
  default:
@@ -19038,56 +19481,113 @@ function statusColor(status) {
19038
19481
  }
19039
19482
  }
19040
19483
  async function run8() {
19484
+ const argv = process.argv.slice(3);
19485
+ const jsonMode = argv.includes("--json");
19486
+ const loader = new SpecialistLoader;
19487
+ const allSpecialists = await loader.list();
19488
+ const piInstalled = isInstalled("pi");
19489
+ const piVersion = piInstalled ? cmd("pi", ["--version"]) : null;
19490
+ const piModels = piInstalled ? cmd("pi", ["--list-models"]) : null;
19491
+ const piProviders = piModels ? new Set(piModels.stdout.split(`
19492
+ `).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean)) : new Set;
19493
+ const bdInstalled = isInstalled("bd");
19494
+ const bdVersion = bdInstalled ? cmd("bd", ["--version"]) : null;
19495
+ const beadsPresent = existsSync6(join9(process.cwd(), ".beads"));
19496
+ const specialistsBin = cmd("which", ["specialists"]);
19497
+ const jobsDir = join9(process.cwd(), ".specialists", "jobs");
19498
+ let jobs = [];
19499
+ if (existsSync6(jobsDir)) {
19500
+ const supervisor = new Supervisor({
19501
+ runner: null,
19502
+ runOptions: null,
19503
+ jobsDir
19504
+ });
19505
+ jobs = supervisor.listJobs();
19506
+ }
19507
+ const stalenessMap = {};
19508
+ for (const s of allSpecialists) {
19509
+ stalenessMap[s.name] = await checkStaleness(s);
19510
+ }
19511
+ if (jsonMode) {
19512
+ const output = {
19513
+ specialists: {
19514
+ count: allSpecialists.length,
19515
+ items: allSpecialists.map((s) => ({
19516
+ name: s.name,
19517
+ scope: s.scope,
19518
+ model: s.model,
19519
+ description: s.description,
19520
+ staleness: stalenessMap[s.name]
19521
+ }))
19522
+ },
19523
+ pi: {
19524
+ installed: piInstalled,
19525
+ version: piVersion?.stdout ?? null,
19526
+ providers: [...piProviders]
19527
+ },
19528
+ beads: {
19529
+ installed: bdInstalled,
19530
+ version: bdVersion?.stdout ?? null,
19531
+ initialized: beadsPresent
19532
+ },
19533
+ mcp: {
19534
+ specialists_installed: specialistsBin.ok,
19535
+ binary_path: specialistsBin.ok ? specialistsBin.stdout : null
19536
+ },
19537
+ jobs: jobs.map((j) => ({
19538
+ id: j.id,
19539
+ specialist: j.specialist,
19540
+ status: j.status,
19541
+ elapsed_s: j.elapsed_s,
19542
+ current_tool: j.current_tool ?? null,
19543
+ error: j.error ?? null
19544
+ }))
19545
+ };
19546
+ console.log(JSON.stringify(output, null, 2));
19547
+ return;
19548
+ }
19041
19549
  console.log(`
19042
19550
  ${bold6("specialists status")}
19043
19551
  `);
19044
19552
  section("Specialists");
19045
- const loader = new SpecialistLoader;
19046
- const all = await loader.list();
19047
- if (all.length === 0) {
19553
+ if (allSpecialists.length === 0) {
19048
19554
  warn(`no specialists found — run ${yellow5("specialists init")} to scaffold`);
19049
19555
  } else {
19050
- const byScope = all.reduce((acc, s) => {
19556
+ const byScope = allSpecialists.reduce((acc, s) => {
19051
19557
  acc[s.scope] = (acc[s.scope] ?? 0) + 1;
19052
19558
  return acc;
19053
19559
  }, {});
19054
19560
  const scopeSummary = Object.entries(byScope).map(([scope, n]) => `${n} ${scope}`).join(", ");
19055
- ok2(`${all.length} found ${dim6(`(${scopeSummary})`)}`);
19056
- for (const s of all) {
19057
- const staleness = await checkStaleness(s);
19561
+ ok2(`${allSpecialists.length} found ${dim6(`(${scopeSummary})`)}`);
19562
+ for (const s of allSpecialists) {
19563
+ const staleness = stalenessMap[s.name];
19058
19564
  if (staleness === "AGED") {
19059
- warn(`${s.name} ${red("AGED")} ${dim6(s.scope)}`);
19565
+ warn(`${s.name} ${red2("AGED")} ${dim6(s.scope)}`);
19060
19566
  } else if (staleness === "STALE") {
19061
19567
  warn(`${s.name} ${yellow5("STALE")} ${dim6(s.scope)}`);
19062
19568
  }
19063
19569
  }
19064
19570
  }
19065
19571
  section("pi (coding agent runtime)");
19066
- if (!isInstalled("pi")) {
19067
- fail(`pi not installed — run ${yellow5("specialists install")}`);
19572
+ if (!piInstalled) {
19573
+ fail(`pi not installed — install ${yellow5("pi")} first`);
19068
19574
  } else {
19069
- const version2 = cmd("pi", ["--version"]);
19070
- const models = cmd("pi", ["--list-models"]);
19071
- const providers = new Set(models.stdout.split(`
19072
- `).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean));
19073
- const vStr = version2.ok ? `v${version2.stdout}` : "unknown version";
19074
- const pStr = providers.size > 0 ? `${providers.size} provider${providers.size > 1 ? "s" : ""} active ${dim6(`(${[...providers].join(", ")})`)} ` : yellow5("no providers configured — run pi config");
19575
+ const vStr = piVersion?.ok ? `v${piVersion.stdout}` : "unknown version";
19576
+ const pStr = piProviders.size > 0 ? `${piProviders.size} provider${piProviders.size > 1 ? "s" : ""} active ${dim6(`(${[...piProviders].join(", ")})`)} ` : yellow5("no providers configured — run pi config");
19075
19577
  ok2(`${vStr} — ${pStr}`);
19076
19578
  }
19077
19579
  section("beads (issue tracker)");
19078
- if (!isInstalled("bd")) {
19079
- fail(`bd not installed — run ${yellow5("specialists install")}`);
19580
+ if (!bdInstalled) {
19581
+ fail(`bd not installed — install ${yellow5("bd")} first`);
19080
19582
  } else {
19081
- const bdVersion = cmd("bd", ["--version"]);
19082
- ok2(`bd installed${bdVersion.ok ? ` ${dim6(bdVersion.stdout)}` : ""}`);
19083
- if (existsSync5(join8(process.cwd(), ".beads"))) {
19583
+ ok2(`bd installed${bdVersion?.ok ? ` ${dim6(bdVersion.stdout)}` : ""}`);
19584
+ if (beadsPresent) {
19084
19585
  ok2(".beads/ present in project");
19085
19586
  } else {
19086
19587
  warn(`.beads/ not found — run ${yellow5("bd init")} to enable issue tracking`);
19087
19588
  }
19088
19589
  }
19089
19590
  section("MCP");
19090
- const specialistsBin = cmd("which", ["specialists"]);
19091
19591
  if (!specialistsBin.ok) {
19092
19592
  fail(`specialists not installed globally — run ${yellow5("npm install -g @jaggerxtrm/specialists")}`);
19093
19593
  } else {
@@ -19095,29 +19595,21 @@ ${bold6("specialists status")}
19095
19595
  info(`verify registration: claude mcp get specialists`);
19096
19596
  info(`re-register: specialists install`);
19097
19597
  }
19098
- const jobsDir = join8(process.cwd(), ".specialists", "jobs");
19099
- if (existsSync5(jobsDir)) {
19100
- const supervisor = new Supervisor({
19101
- runner: null,
19102
- runOptions: null,
19103
- jobsDir
19104
- });
19105
- const jobs = supervisor.listJobs();
19106
- if (jobs.length > 0) {
19107
- section("Active Jobs");
19108
- for (const job of jobs) {
19109
- const elapsed = formatElapsed(job);
19110
- const detail = job.status === "error" ? red(job.error?.slice(0, 40) ?? "error") : job.current_tool ? dim6(`tool: ${job.current_tool}`) : dim6(job.current_event ?? "");
19111
- console.log(` ${dim6(job.id)} ${job.specialist.padEnd(20)} ${statusColor(job.status).padEnd(7)} ${elapsed.padStart(6)} ${detail}`);
19112
- }
19598
+ if (jobs.length > 0) {
19599
+ section("Active Jobs");
19600
+ for (const job of jobs) {
19601
+ const elapsed = formatElapsed2(job);
19602
+ const detail = job.status === "error" ? red2(job.error?.slice(0, 40) ?? "error") : job.current_tool ? dim6(`tool: ${job.current_tool}`) : dim6(job.current_event ?? "");
19603
+ console.log(` ${dim6(job.id)} ${job.specialist.padEnd(20)} ${statusColor(job.status).padEnd(7)} ${elapsed.padStart(6)} ${detail}`);
19113
19604
  }
19114
19605
  }
19115
19606
  console.log();
19116
19607
  }
19117
- var bold6 = (s) => `\x1B[1m${s}\x1B[0m`, dim6 = (s) => `\x1B[2m${s}\x1B[0m`, green5 = (s) => `\x1B[32m${s}\x1B[0m`, yellow5 = (s) => `\x1B[33m${s}\x1B[0m`, red = (s) => `\x1B[31m${s}\x1B[0m`, cyan4 = (s) => `\x1B[36m${s}\x1B[0m`;
19608
+ var red2 = (s) => `\x1B[31m${s}\x1B[0m`, cyan5 = (s) => `\x1B[36m${s}\x1B[0m`;
19118
19609
  var init_status = __esm(() => {
19119
19610
  init_loader();
19120
19611
  init_supervisor();
19612
+ init_format_helpers();
19121
19613
  });
19122
19614
 
19123
19615
  // src/cli/result.ts
@@ -19125,15 +19617,15 @@ var exports_result = {};
19125
19617
  __export(exports_result, {
19126
19618
  run: () => run9
19127
19619
  });
19128
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
19129
- import { join as join9 } from "node:path";
19620
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
19621
+ import { join as join10 } from "node:path";
19130
19622
  async function run9() {
19131
19623
  const jobId = process.argv[3];
19132
19624
  if (!jobId) {
19133
19625
  console.error("Usage: specialists result <job-id>");
19134
19626
  process.exit(1);
19135
19627
  }
19136
- const jobsDir = join9(process.cwd(), ".specialists", "jobs");
19628
+ const jobsDir = join10(process.cwd(), ".specialists", "jobs");
19137
19629
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19138
19630
  const status = supervisor.readStatus(jobId);
19139
19631
  if (!status) {
@@ -19146,107 +19638,340 @@ async function run9() {
19146
19638
  process.exit(1);
19147
19639
  }
19148
19640
  if (status.status === "error") {
19149
- process.stderr.write(`${red2(`Job ${jobId} failed:`)} ${status.error ?? "unknown error"}
19641
+ process.stderr.write(`${red3(`Job ${jobId} failed:`)} ${status.error ?? "unknown error"}
19150
19642
  `);
19151
19643
  process.exit(1);
19152
19644
  }
19153
- const resultPath = join9(jobsDir, jobId, "result.txt");
19154
- if (!existsSync6(resultPath)) {
19645
+ const resultPath = join10(jobsDir, jobId, "result.txt");
19646
+ if (!existsSync7(resultPath)) {
19155
19647
  console.error(`Result file not found for job ${jobId}`);
19156
19648
  process.exit(1);
19157
19649
  }
19158
19650
  process.stdout.write(readFileSync4(resultPath, "utf-8"));
19159
19651
  }
19160
- var dim7 = (s) => `\x1B[2m${s}\x1B[0m`, red2 = (s) => `\x1B[31m${s}\x1B[0m`;
19652
+ var dim7 = (s) => `\x1B[2m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
19161
19653
  var init_result = __esm(() => {
19162
19654
  init_supervisor();
19163
19655
  });
19164
19656
 
19657
+ // src/specialist/timeline-query.ts
19658
+ import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
19659
+ import { join as join11 } from "node:path";
19660
+ function readJobEvents(jobDir) {
19661
+ const eventsPath = join11(jobDir, "events.jsonl");
19662
+ if (!existsSync8(eventsPath))
19663
+ return [];
19664
+ const content = readFileSync5(eventsPath, "utf-8");
19665
+ const lines = content.split(`
19666
+ `).filter(Boolean);
19667
+ const events = [];
19668
+ for (const line of lines) {
19669
+ const event = parseTimelineEvent(line);
19670
+ if (event)
19671
+ events.push(event);
19672
+ }
19673
+ events.sort(compareTimelineEvents);
19674
+ return events;
19675
+ }
19676
+ function readAllJobEvents(jobsDir) {
19677
+ if (!existsSync8(jobsDir))
19678
+ return [];
19679
+ const batches = [];
19680
+ const entries = readdirSync2(jobsDir);
19681
+ for (const entry of entries) {
19682
+ const jobDir = join11(jobsDir, entry);
19683
+ try {
19684
+ const stat2 = __require("node:fs").statSync(jobDir);
19685
+ if (!stat2.isDirectory())
19686
+ continue;
19687
+ } catch {
19688
+ continue;
19689
+ }
19690
+ const jobId = entry;
19691
+ const statusPath = join11(jobDir, "status.json");
19692
+ let specialist = "unknown";
19693
+ let beadId;
19694
+ if (existsSync8(statusPath)) {
19695
+ try {
19696
+ const status = JSON.parse(readFileSync5(statusPath, "utf-8"));
19697
+ specialist = status.specialist ?? "unknown";
19698
+ beadId = status.bead_id;
19699
+ } catch {}
19700
+ }
19701
+ const events = readJobEvents(jobDir);
19702
+ if (events.length > 0) {
19703
+ batches.push({ jobId, specialist, beadId, events });
19704
+ }
19705
+ }
19706
+ return batches;
19707
+ }
19708
+ function mergeTimelineEvents(batches) {
19709
+ const merged = [];
19710
+ for (const batch of batches) {
19711
+ for (const event of batch.events) {
19712
+ merged.push({
19713
+ jobId: batch.jobId,
19714
+ specialist: batch.specialist,
19715
+ beadId: batch.beadId,
19716
+ event
19717
+ });
19718
+ }
19719
+ }
19720
+ merged.sort((a, b) => compareTimelineEvents(a.event, b.event));
19721
+ return merged;
19722
+ }
19723
+ function filterTimelineEvents(merged, filter) {
19724
+ let result = merged;
19725
+ if (filter.since !== undefined) {
19726
+ result = result.filter(({ event }) => event.t >= filter.since);
19727
+ }
19728
+ if (filter.jobId !== undefined) {
19729
+ result = result.filter(({ jobId }) => jobId === filter.jobId);
19730
+ }
19731
+ if (filter.specialist !== undefined) {
19732
+ result = result.filter(({ specialist }) => specialist === filter.specialist);
19733
+ }
19734
+ if (filter.limit !== undefined && filter.limit > 0) {
19735
+ result = result.slice(0, filter.limit);
19736
+ }
19737
+ return result;
19738
+ }
19739
+ function queryTimeline(jobsDir, filter = {}) {
19740
+ let batches = readAllJobEvents(jobsDir);
19741
+ if (filter.jobId !== undefined) {
19742
+ batches = batches.filter((b) => b.jobId === filter.jobId);
19743
+ }
19744
+ if (filter.specialist !== undefined) {
19745
+ batches = batches.filter((b) => b.specialist === filter.specialist);
19746
+ }
19747
+ const merged = mergeTimelineEvents(batches);
19748
+ return filterTimelineEvents(merged, filter);
19749
+ }
19750
+ var init_timeline_query = __esm(() => {
19751
+ init_timeline_events();
19752
+ });
19753
+
19165
19754
  // src/cli/feed.ts
19166
19755
  var exports_feed = {};
19167
19756
  __export(exports_feed, {
19168
19757
  run: () => run10
19169
19758
  });
19170
- import { existsSync as existsSync7, readFileSync as readFileSync5, watchFile } from "node:fs";
19171
- import { join as join10 } from "node:path";
19172
- function formatEvent(line) {
19173
- try {
19174
- const e = JSON.parse(line);
19175
- const ts = new Date(e.t).toISOString().slice(11, 19);
19176
- const type = e.type ?? "?";
19177
- const extra = e.tool ? ` ${cyan5(e.tool)}` : e.model ? ` ${dim8(e.model)}` : e.message ? ` ${red3(e.message)}` : "";
19178
- return `${dim8(ts)} ${type}${extra}`;
19179
- } catch {
19180
- return line;
19759
+ import { existsSync as existsSync9 } from "node:fs";
19760
+ import { join as join12 } from "node:path";
19761
+ function getHumanEventKey(event) {
19762
+ switch (event.type) {
19763
+ case "meta":
19764
+ return `meta:${event.backend}:${event.model}`;
19765
+ case "tool":
19766
+ return `tool:${event.tool}:${event.phase}:${event.is_error ? "error" : "ok"}`;
19767
+ case "text":
19768
+ return "text";
19769
+ case "thinking":
19770
+ return "thinking";
19771
+ case "run_start":
19772
+ return `run_start:${event.specialist}:${event.bead_id ?? ""}`;
19773
+ case "run_complete":
19774
+ return `run_complete:${event.status}:${event.error ?? ""}`;
19775
+ case "done":
19776
+ case "agent_end":
19777
+ return `complete:${event.type}`;
19778
+ default:
19779
+ return event.type;
19181
19780
  }
19182
19781
  }
19183
- function printLines(content, from) {
19184
- const lines = content.split(`
19185
- `).filter(Boolean);
19186
- for (let i = from;i < lines.length; i++) {
19187
- console.log(formatEvent(lines[i]));
19782
+ function shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey) {
19783
+ if (event.type === "meta") {
19784
+ const metaKey = `${event.backend}:${event.model}`;
19785
+ if (seenMetaKey.get(jobId) === metaKey)
19786
+ return true;
19787
+ seenMetaKey.set(jobId, metaKey);
19188
19788
  }
19189
- return lines.length;
19789
+ const key = getHumanEventKey(event);
19790
+ if (lastPrintedEventKey.get(jobId) === key)
19791
+ return true;
19792
+ lastPrintedEventKey.set(jobId, key);
19793
+ return false;
19190
19794
  }
19191
- async function run10() {
19192
- const argv = process.argv.slice(3);
19795
+ function parseSince(value) {
19796
+ if (value.includes("T") || value.includes("-")) {
19797
+ return new Date(value).getTime();
19798
+ }
19799
+ const match = value.match(/^(\d+)([smhd])$/);
19800
+ if (match) {
19801
+ const num = parseInt(match[1], 10);
19802
+ const unit = match[2];
19803
+ const multipliers = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
19804
+ return Date.now() - num * multipliers[unit];
19805
+ }
19806
+ return;
19807
+ }
19808
+ function parseArgs5(argv) {
19193
19809
  let jobId;
19810
+ let specialist;
19811
+ let since;
19812
+ let limit = 100;
19194
19813
  let follow = false;
19814
+ let forever = false;
19815
+ let json = false;
19195
19816
  for (let i = 0;i < argv.length; i++) {
19196
19817
  if (argv[i] === "--job" && argv[i + 1]) {
19197
19818
  jobId = argv[++i];
19198
19819
  continue;
19199
19820
  }
19821
+ if (argv[i] === "--specialist" && argv[i + 1]) {
19822
+ specialist = argv[++i];
19823
+ continue;
19824
+ }
19825
+ if (argv[i] === "--since" && argv[i + 1]) {
19826
+ since = parseSince(argv[++i]);
19827
+ continue;
19828
+ }
19829
+ if (argv[i] === "--limit" && argv[i + 1]) {
19830
+ limit = parseInt(argv[++i], 10);
19831
+ continue;
19832
+ }
19200
19833
  if (argv[i] === "--follow" || argv[i] === "-f") {
19201
19834
  follow = true;
19202
19835
  continue;
19203
19836
  }
19837
+ if (argv[i] === "--forever") {
19838
+ forever = true;
19839
+ continue;
19840
+ }
19841
+ if (argv[i] === "--json") {
19842
+ json = true;
19843
+ continue;
19844
+ }
19204
19845
  if (!jobId && !argv[i].startsWith("--"))
19205
19846
  jobId = argv[i];
19206
19847
  }
19207
- if (!jobId) {
19208
- console.error("Usage: specialists feed --job <job-id> [--follow]");
19209
- process.exit(1);
19848
+ return { jobId, specialist, since, limit, follow, forever, json };
19849
+ }
19850
+ function printSnapshot(merged, options) {
19851
+ if (merged.length === 0) {
19852
+ if (!options.json)
19853
+ console.log(dim6("No events found."));
19854
+ return;
19210
19855
  }
19211
- const jobsDir = join10(process.cwd(), ".specialists", "jobs");
19212
- const eventsPath = join10(jobsDir, jobId, "events.jsonl");
19213
- if (!existsSync7(eventsPath)) {
19214
- const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19215
- if (!supervisor.readStatus(jobId)) {
19216
- console.error(`No job found: ${jobId}`);
19217
- process.exit(1);
19856
+ const colorMap = new JobColorMap;
19857
+ if (options.json) {
19858
+ for (const { jobId, specialist, beadId, event } of merged) {
19859
+ console.log(JSON.stringify({ jobId, specialist, beadId, ...event }));
19218
19860
  }
19219
- console.log(dim8("No events yet."));
19220
19861
  return;
19221
19862
  }
19222
- const content = readFileSync5(eventsPath, "utf-8");
19223
- let linesRead = printLines(content, 0);
19224
- if (!follow)
19225
- return;
19226
- process.stderr.write(dim8(`Following ${jobId}... (Ctrl+C to stop)
19227
- `));
19863
+ const lastPrintedEventKey = new Map;
19864
+ const seenMetaKey = new Map;
19865
+ for (const { jobId, specialist, beadId, event } of merged) {
19866
+ if (shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey))
19867
+ continue;
19868
+ const colorize = colorMap.get(jobId);
19869
+ console.log(formatEventLine(event, { jobId, specialist, beadId, colorize }));
19870
+ }
19871
+ }
19872
+ function isCompletionEvent(event) {
19873
+ return isRunCompleteEvent(event) || event.type === "done" || event.type === "agent_end";
19874
+ }
19875
+ async function followMerged(jobsDir, options) {
19876
+ const colorMap = new JobColorMap;
19877
+ const lastSeenT = new Map;
19878
+ const completedJobs = new Set;
19879
+ const filteredBatches = () => readAllJobEvents(jobsDir).filter((batch) => !options.jobId || batch.jobId === options.jobId).filter((batch) => !options.specialist || batch.specialist === options.specialist);
19880
+ const initial = queryTimeline(jobsDir, {
19881
+ jobId: options.jobId,
19882
+ specialist: options.specialist,
19883
+ since: options.since,
19884
+ limit: options.limit
19885
+ });
19886
+ printSnapshot(initial, { ...options, json: options.json });
19887
+ for (const batch of filteredBatches()) {
19888
+ if (batch.events.length > 0) {
19889
+ const maxT = Math.max(...batch.events.map((event) => event.t));
19890
+ lastSeenT.set(batch.jobId, maxT);
19891
+ }
19892
+ if (batch.events.some(isCompletionEvent)) {
19893
+ completedJobs.add(batch.jobId);
19894
+ }
19895
+ }
19896
+ const initialBatchCount = filteredBatches().length;
19897
+ if (!options.forever && initialBatchCount > 0 && completedJobs.size === initialBatchCount) {
19898
+ if (!options.json) {
19899
+ process.stderr.write(dim6(`All jobs complete.
19900
+ `));
19901
+ }
19902
+ return;
19903
+ }
19904
+ if (!options.json) {
19905
+ process.stderr.write(dim6(`Following... (Ctrl+C to stop)
19906
+ `));
19907
+ }
19908
+ const lastPrintedEventKey = new Map;
19909
+ const seenMetaKey = new Map;
19228
19910
  await new Promise((resolve) => {
19229
- watchFile(eventsPath, { interval: 500 }, () => {
19230
- try {
19231
- const updated = readFileSync5(eventsPath, "utf-8");
19232
- linesRead = printLines(updated, linesRead);
19233
- const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19234
- const status = supervisor.readStatus(jobId);
19235
- if (status && status.status !== "running" && status.status !== "starting") {
19236
- const finalMsg = status.status === "done" ? `
19237
- ${yellow6("Job complete.")} Run: specialists result ${jobId}` : `
19238
- ${red3(`Job ${status.status}.`)} ${status.error ?? ""}`;
19239
- process.stderr.write(finalMsg + `
19240
- `);
19241
- resolve();
19911
+ const interval = setInterval(() => {
19912
+ const batches = filteredBatches();
19913
+ const newEvents = [];
19914
+ for (const batch of batches) {
19915
+ const lastT = lastSeenT.get(batch.jobId) ?? 0;
19916
+ for (const event of batch.events) {
19917
+ if (event.t > lastT) {
19918
+ newEvents.push({
19919
+ jobId: batch.jobId,
19920
+ specialist: batch.specialist,
19921
+ beadId: batch.beadId,
19922
+ event
19923
+ });
19924
+ }
19242
19925
  }
19243
- } catch {}
19244
- });
19926
+ if (batch.events.length > 0) {
19927
+ const maxT = Math.max(...batch.events.map((e) => e.t));
19928
+ lastSeenT.set(batch.jobId, maxT);
19929
+ }
19930
+ if (batch.events.some(isCompletionEvent)) {
19931
+ completedJobs.add(batch.jobId);
19932
+ }
19933
+ }
19934
+ newEvents.sort((a, b) => a.event.t - b.event.t);
19935
+ for (const { jobId, specialist, beadId, event } of newEvents) {
19936
+ if (options.json) {
19937
+ console.log(JSON.stringify({ jobId, specialist, beadId, ...event }));
19938
+ } else {
19939
+ if (shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey))
19940
+ continue;
19941
+ const colorize = colorMap.get(jobId);
19942
+ console.log(formatEventLine(event, { jobId, specialist, beadId, colorize }));
19943
+ }
19944
+ }
19945
+ if (!options.forever && batches.length > 0 && completedJobs.size === batches.length) {
19946
+ clearInterval(interval);
19947
+ resolve();
19948
+ }
19949
+ }, 500);
19245
19950
  });
19246
19951
  }
19247
- var dim8 = (s) => `\x1B[2m${s}\x1B[0m`, cyan5 = (s) => `\x1B[36m${s}\x1B[0m`, yellow6 = (s) => `\x1B[33m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
19952
+ async function run10() {
19953
+ const options = parseArgs5(process.argv.slice(3));
19954
+ const jobsDir = join12(process.cwd(), ".specialists", "jobs");
19955
+ if (!existsSync9(jobsDir)) {
19956
+ console.log(dim6("No jobs directory found."));
19957
+ return;
19958
+ }
19959
+ if (options.follow) {
19960
+ await followMerged(jobsDir, options);
19961
+ return;
19962
+ }
19963
+ const merged = queryTimeline(jobsDir, {
19964
+ jobId: options.jobId,
19965
+ specialist: options.specialist,
19966
+ since: options.since,
19967
+ limit: options.limit
19968
+ });
19969
+ printSnapshot(merged, options);
19970
+ }
19248
19971
  var init_feed = __esm(() => {
19249
- init_supervisor();
19972
+ init_timeline_events();
19973
+ init_timeline_query();
19974
+ init_format_helpers();
19250
19975
  });
19251
19976
 
19252
19977
  // src/cli/stop.ts
@@ -19254,14 +19979,14 @@ var exports_stop = {};
19254
19979
  __export(exports_stop, {
19255
19980
  run: () => run11
19256
19981
  });
19257
- import { join as join11 } from "node:path";
19982
+ import { join as join13 } from "node:path";
19258
19983
  async function run11() {
19259
19984
  const jobId = process.argv[3];
19260
19985
  if (!jobId) {
19261
19986
  console.error("Usage: specialists stop <job-id>");
19262
19987
  process.exit(1);
19263
19988
  }
19264
- const jobsDir = join11(process.cwd(), ".specialists", "jobs");
19989
+ const jobsDir = join13(process.cwd(), ".specialists", "jobs");
19265
19990
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19266
19991
  const status = supervisor.readStatus(jobId);
19267
19992
  if (!status) {
@@ -19269,71 +19994,742 @@ async function run11() {
19269
19994
  process.exit(1);
19270
19995
  }
19271
19996
  if (status.status === "done" || status.status === "error") {
19272
- process.stderr.write(`${dim9(`Job ${jobId} is already ${status.status}.`)}
19997
+ process.stderr.write(`${dim8(`Job ${jobId} is already ${status.status}.`)}
19273
19998
  `);
19274
19999
  return;
19275
20000
  }
19276
20001
  if (!status.pid) {
19277
- process.stderr.write(`${red4(`No PID recorded for job ${jobId}.`)}
20002
+ process.stderr.write(`${red5(`No PID recorded for job ${jobId}.`)}
19278
20003
  `);
19279
20004
  process.exit(1);
19280
20005
  }
19281
20006
  try {
19282
20007
  process.kill(status.pid, "SIGTERM");
19283
- process.stdout.write(`${green6("✓")} Sent SIGTERM to PID ${status.pid} (job ${jobId})
20008
+ process.stdout.write(`${green7("✓")} Sent SIGTERM to PID ${status.pid} (job ${jobId})
19284
20009
  `);
19285
20010
  } catch (err) {
19286
20011
  if (err.code === "ESRCH") {
19287
- process.stderr.write(`${red4(`Process ${status.pid} not found.`)} Job may have already completed.
20012
+ process.stderr.write(`${red5(`Process ${status.pid} not found.`)} Job may have already completed.
19288
20013
  `);
19289
20014
  } else {
19290
- process.stderr.write(`${red4("Error:")} ${err.message}
20015
+ process.stderr.write(`${red5("Error:")} ${err.message}
19291
20016
  `);
19292
20017
  process.exit(1);
19293
20018
  }
19294
20019
  }
19295
20020
  }
19296
- var green6 = (s) => `\x1B[32m${s}\x1B[0m`, red4 = (s) => `\x1B[31m${s}\x1B[0m`, dim9 = (s) => `\x1B[2m${s}\x1B[0m`;
20021
+ var green7 = (s) => `\x1B[32m${s}\x1B[0m`, red5 = (s) => `\x1B[31m${s}\x1B[0m`, dim8 = (s) => `\x1B[2m${s}\x1B[0m`;
19297
20022
  var init_stop = __esm(() => {
19298
20023
  init_supervisor();
19299
20024
  });
19300
20025
 
20026
+ // src/cli/quickstart.ts
20027
+ var exports_quickstart = {};
20028
+ __export(exports_quickstart, {
20029
+ run: () => run12
20030
+ });
20031
+ function section2(title) {
20032
+ const bar = "─".repeat(60);
20033
+ return `
20034
+ ${bold7(cyan7(title))}
20035
+ ${dim9(bar)}`;
20036
+ }
20037
+ function cmd2(s) {
20038
+ return yellow6(s);
20039
+ }
20040
+ function flag(s) {
20041
+ return green8(s);
20042
+ }
20043
+ async function run12() {
20044
+ const lines = [
20045
+ "",
20046
+ bold7("specialists · Quick Start Guide"),
20047
+ dim9("One MCP server. Multiple AI backends. Intelligent orchestration."),
20048
+ ""
20049
+ ];
20050
+ lines.push(section2("1. Installation"));
20051
+ lines.push("");
20052
+ lines.push(` ${cmd2("npm install -g @jaggerxtrm/specialists")} # install globally`);
20053
+ lines.push(` ${cmd2("specialists install")} # project setup:`);
20054
+ lines.push(` ${dim9(" # checks pi · bd · xt, then wires MCP + hooks")}`);
20055
+ lines.push("");
20056
+ lines.push(` Verify everything is healthy:`);
20057
+ lines.push(` ${cmd2("specialists status")} # shows pi, beads, MCP, active jobs`);
20058
+ lines.push("");
20059
+ lines.push(section2("2. Initialize a Project"));
20060
+ lines.push("");
20061
+ lines.push(` Run once per project root:`);
20062
+ lines.push(` ${cmd2("specialists init")} # creates specialists/, .specialists/, AGENTS.md`);
20063
+ lines.push("");
20064
+ lines.push(` What this creates:`);
20065
+ lines.push(` ${dim9("specialists/")} — put your .specialist.yaml files here`);
20066
+ lines.push(` ${dim9(".specialists/")} — runtime data (jobs/, ready/) — gitignored`);
20067
+ lines.push(` ${dim9("AGENTS.md")} — context block injected into Claude sessions`);
20068
+ lines.push("");
20069
+ lines.push(section2("3. Discover Specialists"));
20070
+ lines.push("");
20071
+ lines.push(` ${cmd2("specialists list")} # all specialists (project + user)`);
20072
+ lines.push(` ${cmd2("specialists list")} ${flag("--scope project")} # project-scoped only`);
20073
+ lines.push(` ${cmd2("specialists list")} ${flag("--scope user")} # user-scoped (~/.specialists/)`);
20074
+ lines.push(` ${cmd2("specialists list")} ${flag("--category analysis")} # filter by category`);
20075
+ lines.push(` ${cmd2("specialists list")} ${flag("--json")} # machine-readable JSON`);
20076
+ lines.push("");
20077
+ lines.push(` Scopes (searched in order):`);
20078
+ lines.push(` ${blue2("project")} ./specialists/*.specialist.yaml`);
20079
+ lines.push(` ${blue2("user")} ~/.specialists/*.specialist.yaml`);
20080
+ lines.push(` ${blue2("system")} bundled specialists (shipped with the package)`);
20081
+ lines.push("");
20082
+ lines.push(section2("4. Running a Specialist"));
20083
+ lines.push("");
20084
+ lines.push(` ${bold7("Foreground")} (streams output to stdout):`);
20085
+ lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${dim9('"Review src/api.ts for security issues"')}`);
20086
+ lines.push("");
20087
+ lines.push(` ${bold7("Background")} (returns a job ID immediately):`);
20088
+ lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${dim9('"..."')} ${flag("--background")}`);
20089
+ lines.push(` ${dim9(" # → Job started: job_a1b2c3d4")}`);
20090
+ lines.push("");
20091
+ lines.push(` Override model for one run:`);
20092
+ lines.push(` ${cmd2("specialists run code-review")} ${flag("--model")} ${dim9("anthropic/claude-opus-4-6")} ${flag("--prompt")} ${dim9('"..."')}`);
20093
+ lines.push("");
20094
+ lines.push(` Run without beads issue tracking:`);
20095
+ lines.push(` ${cmd2("specialists run code-review")} ${flag("--no-beads")} ${flag("--prompt")} ${dim9('"..."')}`);
20096
+ lines.push("");
20097
+ lines.push(` Pipe a prompt from stdin:`);
20098
+ lines.push(` ${cmd2("cat my-brief.md | specialists run code-review")}`);
20099
+ lines.push("");
20100
+ lines.push(section2("5. Background Job Lifecycle"));
20101
+ lines.push("");
20102
+ lines.push(` ${bold7("Watch progress")} — stream events as they arrive:`);
20103
+ lines.push(` ${cmd2("specialists feed job_a1b2c3d4")} # print events so far`);
20104
+ lines.push(` ${cmd2("specialists feed job_a1b2c3d4")} ${flag("--follow")} # tail and stream live updates`);
20105
+ lines.push("");
20106
+ lines.push(` ${bold7("Read results")} — print the final output:`);
20107
+ lines.push(` ${cmd2("specialists result job_a1b2c3d4")} # exits 1 if still running`);
20108
+ lines.push("");
20109
+ lines.push(` ${bold7("Cancel a job")}:`);
20110
+ lines.push(` ${cmd2("specialists stop job_a1b2c3d4")} # sends SIGTERM to the agent process`);
20111
+ lines.push("");
20112
+ lines.push(` ${bold7("Job files")} in ${dim9(".specialists/jobs/<job-id>/")}:`);
20113
+ lines.push(` ${dim9("status.json")} — id, specialist, status, pid, started_at, elapsed_s, current_tool`);
20114
+ lines.push(` ${dim9("events.jsonl")} — one JSON event per line (tool_use, text, agent_end, error …)`);
20115
+ lines.push(` ${dim9("result.txt")} — final output (written when status=done)`);
20116
+ lines.push("");
20117
+ lines.push(section2("6. Editing Specialists"));
20118
+ lines.push("");
20119
+ lines.push(` Change a field without opening the YAML manually:`);
20120
+ lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${dim9("anthropic/claude-sonnet-4-6")}`);
20121
+ lines.push(` ${cmd2("specialists edit code-review")} ${flag("--description")} ${dim9('"Updated description"')}`);
20122
+ lines.push(` ${cmd2("specialists edit code-review")} ${flag("--timeout")} ${dim9("120000")}`);
20123
+ lines.push(` ${cmd2("specialists edit code-review")} ${flag("--permission")} ${dim9("HIGH")}`);
20124
+ lines.push(` ${cmd2("specialists edit code-review")} ${flag("--tags")} ${dim9("analysis,security,review")}`);
20125
+ lines.push("");
20126
+ lines.push(` Preview without writing:`);
20127
+ lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${dim9("...")} ${flag("--dry-run")}`);
20128
+ lines.push("");
20129
+ lines.push(section2("7. .specialist.yaml Schema"));
20130
+ lines.push("");
20131
+ lines.push(` Full annotated example:`);
20132
+ lines.push("");
20133
+ const schemaLines = [
20134
+ "specialist:",
20135
+ " metadata:",
20136
+ ' name: my-specialist # required · used in "specialists run <name>"',
20137
+ " version: 1.0.0 # semver, for staleness detection",
20138
+ ' description: "What it does" # shown in specialists list',
20139
+ " category: analysis # free-form tag for --category filter",
20140
+ " tags: [review, security] # array of labels",
20141
+ ' updated: "2026-03-11" # ISO date — used for staleness check',
20142
+ "",
20143
+ " execution:",
20144
+ " mode: tool # tool (default) | chat",
20145
+ " model: anthropic/claude-sonnet-4-6 # primary model",
20146
+ " fallback_model: qwen-cli/qwen3-coder # if primary circuit-breaks",
20147
+ " timeout_ms: 120000 # ms before job is killed (default: 120000)",
20148
+ " stall_timeout_ms: 30000 # ms of silence before stall-detection fires",
20149
+ " response_format: markdown # markdown | json | text",
20150
+ " permission_required: MEDIUM # READ_ONLY | LOW | MEDIUM | HIGH",
20151
+ "",
20152
+ " prompt:",
20153
+ " system: | # system prompt (multiline YAML literal block)",
20154
+ " You are …",
20155
+ " user_template: | # optional; $prompt and $context are substituted",
20156
+ " Task: $prompt",
20157
+ " Context: $context",
20158
+ "",
20159
+ " skills:",
20160
+ " paths: # extra skill dirs searched at runtime",
20161
+ " - ./specialists/skills",
20162
+ " - ~/.specialists/skills",
20163
+ "",
20164
+ " capabilities:",
20165
+ " web_search: false # allow web search tool",
20166
+ " file_write: true # allow file writes",
20167
+ "",
20168
+ " beads_integration:",
20169
+ " auto_create: true # create a beads issue per run",
20170
+ " issue_type: task # task | bug | feature",
20171
+ " priority: 2 # 0=critical … 4=backlog"
20172
+ ];
20173
+ for (const l of schemaLines) {
20174
+ lines.push(` ${dim9(l)}`);
20175
+ }
20176
+ lines.push("");
20177
+ lines.push(section2("8. Hook System"));
20178
+ lines.push("");
20179
+ lines.push(` Specialists emits lifecycle events to ${dim9(".specialists/trace.jsonl")}:`);
20180
+ lines.push("");
20181
+ lines.push(` ${bold7("Hook point")} ${bold7("When fired")}`);
20182
+ lines.push(` ${yellow6("specialist:start")} before the agent session begins`);
20183
+ lines.push(` ${yellow6("specialist:token")} on each streamed token (delta)`);
20184
+ lines.push(` ${yellow6("specialist:done")} after successful completion`);
20185
+ lines.push(` ${yellow6("specialist:error")} on failure or timeout`);
20186
+ lines.push("");
20187
+ lines.push(` Each event line in trace.jsonl:`);
20188
+ lines.push(` ${dim9('{"t":"<ISO>","hook":"specialist:done","specialist":"code-review","durationMs":4120}')}`);
20189
+ lines.push("");
20190
+ lines.push(` Tail the trace file to observe all activity:`);
20191
+ lines.push(` ${cmd2("tail -f .specialists/trace.jsonl | jq .")}`);
20192
+ lines.push("");
20193
+ lines.push(section2("9. MCP Integration (Claude Code)"));
20194
+ lines.push("");
20195
+ lines.push(` After ${cmd2("specialists install")}, these MCP tools are available to Claude:`);
20196
+ lines.push("");
20197
+ lines.push(` ${bold7("specialist_init")} — bootstrap: bd init + list specialists`);
20198
+ lines.push(` ${bold7("list_specialists")} — discover specialists (project/user/system)`);
20199
+ lines.push(` ${bold7("use_specialist")} — full lifecycle: load → agents.md → run → output`);
20200
+ lines.push(` ${bold7("run_parallel")} — concurrent or pipeline execution`);
20201
+ lines.push(` ${bold7("start_specialist")} — async job start, returns job ID`);
20202
+ lines.push(` ${bold7("poll_specialist")} — poll job status/output by ID`);
20203
+ lines.push(` ${bold7("stop_specialist")} — cancel a running job by ID`);
20204
+ lines.push(` ${bold7("specialist_status")} — circuit breaker health + staleness`);
20205
+ lines.push("");
20206
+ lines.push(section2("10. Common Workflows"));
20207
+ lines.push("");
20208
+ lines.push(` ${bold7("Foreground review, save to file:")}`);
20209
+ lines.push(` ${cmd2('specialists run code-review --prompt "Audit src/" > review.md')}`);
20210
+ lines.push("");
20211
+ lines.push(` ${bold7("Fire-and-forget, check later:")}`);
20212
+ lines.push(` ${cmd2('specialists run deep-analysis --prompt "..." --background')}`);
20213
+ lines.push(` ${cmd2("specialists feed <job-id> --follow")}`);
20214
+ lines.push(` ${cmd2("specialists result <job-id> > analysis.md")}`);
20215
+ lines.push("");
20216
+ lines.push(` ${bold7("Override model for a single run:")}`);
20217
+ lines.push(` ${cmd2('specialists run code-review --model anthropic/claude-opus-4-6 --prompt "..."')}`);
20218
+ lines.push("");
20219
+ lines.push(dim9("─".repeat(62)));
20220
+ lines.push(` ${dim9("specialists help")} command list ${dim9("specialists <cmd> --help")} per-command flags`);
20221
+ lines.push(` ${dim9("specialists status")} health check ${dim9("specialists models")} available models`);
20222
+ lines.push("");
20223
+ console.log(lines.join(`
20224
+ `));
20225
+ }
20226
+ var bold7 = (s) => `\x1B[1m${s}\x1B[0m`, dim9 = (s) => `\x1B[2m${s}\x1B[0m`, yellow6 = (s) => `\x1B[33m${s}\x1B[0m`, cyan7 = (s) => `\x1B[36m${s}\x1B[0m`, blue2 = (s) => `\x1B[34m${s}\x1B[0m`, green8 = (s) => `\x1B[32m${s}\x1B[0m`;
20227
+
20228
+ // src/cli/doctor.ts
20229
+ var exports_doctor = {};
20230
+ __export(exports_doctor, {
20231
+ run: () => run13
20232
+ });
20233
+ import { spawnSync as spawnSync6 } from "node:child_process";
20234
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "node:fs";
20235
+ import { join as join14 } from "node:path";
20236
+ function ok3(msg) {
20237
+ console.log(` ${green9("✓")} ${msg}`);
20238
+ }
20239
+ function warn2(msg) {
20240
+ console.log(` ${yellow7("○")} ${msg}`);
20241
+ }
20242
+ function fail2(msg) {
20243
+ console.log(` ${red6("✗")} ${msg}`);
20244
+ }
20245
+ function fix(msg) {
20246
+ console.log(` ${dim10("→ fix:")} ${yellow7(msg)}`);
20247
+ }
20248
+ function hint(msg) {
20249
+ console.log(` ${dim10(msg)}`);
20250
+ }
20251
+ function section3(label) {
20252
+ const line = "─".repeat(Math.max(0, 38 - label.length));
20253
+ console.log(`
20254
+ ${bold8(`── ${label} ${line}`)}`);
20255
+ }
20256
+ function sp(bin, args) {
20257
+ const r = spawnSync6(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
20258
+ return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
20259
+ }
20260
+ function isInstalled2(bin) {
20261
+ return spawnSync6("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
20262
+ }
20263
+ function loadJson2(path) {
20264
+ if (!existsSync10(path))
20265
+ return null;
20266
+ try {
20267
+ return JSON.parse(readFileSync6(path, "utf8"));
20268
+ } catch {
20269
+ return null;
20270
+ }
20271
+ }
20272
+ function checkPi() {
20273
+ section3("pi (coding agent runtime)");
20274
+ if (!isInstalled2("pi")) {
20275
+ fail2("pi not installed");
20276
+ fix("install pi first");
20277
+ return false;
20278
+ }
20279
+ const version2 = sp("pi", ["--version"]);
20280
+ const models = sp("pi", ["--list-models"]);
20281
+ const providers = models.ok ? new Set(models.stdout.split(`
20282
+ `).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean)) : new Set;
20283
+ const vStr = version2.ok ? `v${version2.stdout}` : "unknown version";
20284
+ if (providers.size === 0) {
20285
+ warn2(`pi ${vStr} installed but no active providers`);
20286
+ fix("pi config (add at least one API key)");
20287
+ return false;
20288
+ }
20289
+ ok3(`pi ${vStr} — ${providers.size} provider${providers.size > 1 ? "s" : ""} active ${dim10(`(${[...providers].join(", ")})`)}`);
20290
+ return true;
20291
+ }
20292
+ function checkBd() {
20293
+ section3("beads (issue tracker)");
20294
+ if (!isInstalled2("bd")) {
20295
+ fail2("bd not installed");
20296
+ fix("install beads (bd) first");
20297
+ return false;
20298
+ }
20299
+ ok3(`bd installed ${dim10(sp("bd", ["--version"]).stdout || "")}`);
20300
+ if (existsSync10(join14(CWD, ".beads")))
20301
+ ok3(".beads/ present in project");
20302
+ else
20303
+ warn2(".beads/ not found in project");
20304
+ return true;
20305
+ }
20306
+ function checkXt() {
20307
+ section3("xtrm-tools");
20308
+ if (!isInstalled2("xt")) {
20309
+ fail2("xt not installed");
20310
+ fix("install xtrm-tools first");
20311
+ return false;
20312
+ }
20313
+ ok3(`xt installed ${dim10(sp("xt", ["--version"]).stdout || "")}`);
20314
+ return true;
20315
+ }
20316
+ function checkHooks() {
20317
+ section3("Claude Code hooks (2 expected)");
20318
+ let allPresent = true;
20319
+ for (const name of HOOK_NAMES) {
20320
+ const dest = join14(HOOKS_DIR, name);
20321
+ if (!existsSync10(dest)) {
20322
+ fail2(`${name} ${red6("missing")}`);
20323
+ fix("specialists install");
20324
+ allPresent = false;
20325
+ } else {
20326
+ ok3(name);
20327
+ }
20328
+ }
20329
+ const settings = loadJson2(SETTINGS_FILE);
20330
+ if (!settings) {
20331
+ warn2(`Could not read ${SETTINGS_FILE}`);
20332
+ fix("specialists install");
20333
+ return false;
20334
+ }
20335
+ const hooks = settings.hooks ?? {};
20336
+ const wiredCommands = new Set([
20337
+ ...hooks.UserPromptSubmit ?? [],
20338
+ ...hooks.SessionStart ?? []
20339
+ ].flatMap((entry) => (entry.hooks ?? []).map((h) => h.command ?? "")));
20340
+ for (const name of HOOK_NAMES) {
20341
+ const expected = join14(HOOKS_DIR, name);
20342
+ if (!wiredCommands.has(expected)) {
20343
+ warn2(`${name} not wired in settings.json`);
20344
+ fix("specialists install");
20345
+ allPresent = false;
20346
+ }
20347
+ }
20348
+ if (allPresent)
20349
+ hint(`Hooks wired in ${SETTINGS_FILE}`);
20350
+ return allPresent;
20351
+ }
20352
+ function checkMCP() {
20353
+ section3("MCP registration");
20354
+ const mcp = loadJson2(MCP_FILE2);
20355
+ const spec = mcp?.mcpServers?.specialists;
20356
+ if (!spec || spec.command !== "specialists") {
20357
+ fail2(`MCP server 'specialists' not registered in .mcp.json`);
20358
+ fix("specialists install");
20359
+ return false;
20360
+ }
20361
+ ok3(`MCP server 'specialists' registered in ${MCP_FILE2}`);
20362
+ return true;
20363
+ }
20364
+ function checkRuntimeDirs() {
20365
+ section3(".specialists/ runtime directories");
20366
+ const rootDir = join14(CWD, ".specialists");
20367
+ const jobsDir = join14(rootDir, "jobs");
20368
+ const readyDir = join14(rootDir, "ready");
20369
+ let allOk = true;
20370
+ if (!existsSync10(rootDir)) {
20371
+ warn2(".specialists/ not found in current project");
20372
+ fix("specialists init");
20373
+ allOk = false;
20374
+ } else {
20375
+ ok3(".specialists/ present");
20376
+ for (const [subDir, label] of [[jobsDir, "jobs"], [readyDir, "ready"]]) {
20377
+ if (!existsSync10(subDir)) {
20378
+ warn2(`.specialists/${label}/ missing — auto-creating`);
20379
+ mkdirSync3(subDir, { recursive: true });
20380
+ ok3(`.specialists/${label}/ created`);
20381
+ } else {
20382
+ ok3(`.specialists/${label}/ present`);
20383
+ }
20384
+ }
20385
+ }
20386
+ return allOk;
20387
+ }
20388
+ function checkZombieJobs() {
20389
+ section3("Background jobs");
20390
+ const jobsDir = join14(CWD, ".specialists", "jobs");
20391
+ if (!existsSync10(jobsDir)) {
20392
+ hint("No .specialists/jobs/ — skipping");
20393
+ return true;
20394
+ }
20395
+ let entries;
20396
+ try {
20397
+ entries = readdirSync3(jobsDir);
20398
+ } catch {
20399
+ entries = [];
20400
+ }
20401
+ if (entries.length === 0) {
20402
+ ok3("No jobs found");
20403
+ return true;
20404
+ }
20405
+ let zombies = 0;
20406
+ let total = 0;
20407
+ let running = 0;
20408
+ for (const jobId of entries) {
20409
+ const statusPath = join14(jobsDir, jobId, "status.json");
20410
+ if (!existsSync10(statusPath))
20411
+ continue;
20412
+ try {
20413
+ const status = JSON.parse(readFileSync6(statusPath, "utf8"));
20414
+ total++;
20415
+ if (status.status === "running" || status.status === "starting") {
20416
+ const pid = status.pid;
20417
+ if (pid) {
20418
+ let alive = false;
20419
+ try {
20420
+ process.kill(pid, 0);
20421
+ alive = true;
20422
+ } catch {}
20423
+ if (alive)
20424
+ running++;
20425
+ else {
20426
+ zombies++;
20427
+ warn2(`${jobId} ${yellow7("ZOMBIE")} ${dim10(`pid ${pid} not found, status=${status.status}`)}`);
20428
+ fix(`Edit .specialists/jobs/${jobId}/status.json → set "status": "error"`);
20429
+ }
20430
+ }
20431
+ }
20432
+ } catch {}
20433
+ }
20434
+ if (zombies === 0) {
20435
+ const detail = running > 0 ? `, ${running} currently running` : ", none currently running";
20436
+ ok3(`${total} job${total !== 1 ? "s" : ""} checked${detail}`);
20437
+ }
20438
+ return zombies === 0;
20439
+ }
20440
+ async function run13() {
20441
+ console.log(`
20442
+ ${bold8("specialists doctor")}
20443
+ `);
20444
+ const piOk = checkPi();
20445
+ const bdOk = checkBd();
20446
+ const xtOk = checkXt();
20447
+ const hooksOk = checkHooks();
20448
+ const mcpOk = checkMCP();
20449
+ const dirsOk = checkRuntimeDirs();
20450
+ const jobsOk = checkZombieJobs();
20451
+ const allOk = piOk && bdOk && xtOk && hooksOk && mcpOk && dirsOk && jobsOk;
20452
+ console.log("");
20453
+ if (allOk) {
20454
+ console.log(` ${green9("✓")} ${bold8("All checks passed")} — specialists is healthy`);
20455
+ } else {
20456
+ console.log(` ${yellow7("○")} ${bold8("Some checks failed")} — follow the fix hints above`);
20457
+ console.log(` ${dim10("specialists install fixes hook + MCP registration; pi, bd, and xt must be installed separately.")}`);
20458
+ }
20459
+ console.log("");
20460
+ }
20461
+ var bold8 = (s) => `\x1B[1m${s}\x1B[0m`, dim10 = (s) => `\x1B[2m${s}\x1B[0m`, green9 = (s) => `\x1B[32m${s}\x1B[0m`, yellow7 = (s) => `\x1B[33m${s}\x1B[0m`, red6 = (s) => `\x1B[31m${s}\x1B[0m`, CWD, CLAUDE_DIR, HOOKS_DIR, SETTINGS_FILE, MCP_FILE2, HOOK_NAMES;
20462
+ var init_doctor = __esm(() => {
20463
+ CWD = process.cwd();
20464
+ CLAUDE_DIR = join14(CWD, ".claude");
20465
+ HOOKS_DIR = join14(CLAUDE_DIR, "hooks");
20466
+ SETTINGS_FILE = join14(CLAUDE_DIR, "settings.json");
20467
+ MCP_FILE2 = join14(CWD, ".mcp.json");
20468
+ HOOK_NAMES = [
20469
+ "specialists-complete.mjs",
20470
+ "specialists-session-start.mjs"
20471
+ ];
20472
+ });
20473
+
20474
+ // src/cli/setup.ts
20475
+ var exports_setup = {};
20476
+ __export(exports_setup, {
20477
+ run: () => run14
20478
+ });
20479
+ import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
20480
+ import { homedir as homedir3 } from "node:os";
20481
+ import { join as join15, resolve } from "node:path";
20482
+ function ok4(msg) {
20483
+ console.log(` ${green10("✓")} ${msg}`);
20484
+ }
20485
+ function skip2(msg) {
20486
+ console.log(` ${yellow8("○")} ${msg}`);
20487
+ }
20488
+ function resolveTarget(target) {
20489
+ switch (target) {
20490
+ case "global":
20491
+ return join15(homedir3(), ".claude", "CLAUDE.md");
20492
+ case "agents":
20493
+ return join15(process.cwd(), "AGENTS.md");
20494
+ case "project":
20495
+ default:
20496
+ return join15(process.cwd(), "CLAUDE.md");
20497
+ }
20498
+ }
20499
+ function parseArgs6() {
20500
+ const argv = process.argv.slice(3);
20501
+ let target = "project";
20502
+ let dryRun = false;
20503
+ for (let i = 0;i < argv.length; i++) {
20504
+ const token = argv[i];
20505
+ if (token === "--global" || token === "-g") {
20506
+ target = "global";
20507
+ continue;
20508
+ }
20509
+ if (token === "--agents" || token === "-a") {
20510
+ target = "agents";
20511
+ continue;
20512
+ }
20513
+ if (token === "--project" || token === "-p") {
20514
+ target = "project";
20515
+ continue;
20516
+ }
20517
+ if (token === "--dry-run") {
20518
+ dryRun = true;
20519
+ continue;
20520
+ }
20521
+ }
20522
+ return { target, dryRun };
20523
+ }
20524
+ async function run14() {
20525
+ const { target, dryRun } = parseArgs6();
20526
+ const filePath = resolve(resolveTarget(target));
20527
+ const label = target === "global" ? "~/.claude/CLAUDE.md" : filePath.replace(process.cwd() + "/", "");
20528
+ console.log(`
20529
+ ${bold9("specialists setup")}
20530
+ `);
20531
+ console.log(` Target: ${yellow8(label)}${dryRun ? dim11(" (dry-run)") : ""}
20532
+ `);
20533
+ if (existsSync11(filePath)) {
20534
+ const existing = readFileSync7(filePath, "utf8");
20535
+ if (existing.includes(MARKER)) {
20536
+ skip2(`${label} already contains Specialists Workflow section`);
20537
+ console.log(`
20538
+ ${dim11("To force-update, remove the ## Specialists Workflow section and re-run.")}
20539
+ `);
20540
+ return;
20541
+ }
20542
+ if (dryRun) {
20543
+ console.log(dim11("─".repeat(60)));
20544
+ console.log(dim11("Would append to existing file:"));
20545
+ console.log("");
20546
+ console.log(WORKFLOW_BLOCK);
20547
+ console.log(dim11("─".repeat(60)));
20548
+ return;
20549
+ }
20550
+ const separator = existing.trimEnd().endsWith(`
20551
+ `) ? `
20552
+ ` : `
20553
+
20554
+ `;
20555
+ writeFileSync4(filePath, existing.trimEnd() + separator + WORKFLOW_BLOCK, "utf8");
20556
+ ok4(`Appended Specialists Workflow section to ${label}`);
20557
+ } else {
20558
+ if (dryRun) {
20559
+ console.log(dim11("─".repeat(60)));
20560
+ console.log(dim11(`Would create ${label}:`));
20561
+ console.log("");
20562
+ console.log(WORKFLOW_BLOCK);
20563
+ console.log(dim11("─".repeat(60)));
20564
+ return;
20565
+ }
20566
+ writeFileSync4(filePath, WORKFLOW_BLOCK, "utf8");
20567
+ ok4(`Created ${label} with Specialists Workflow section`);
20568
+ }
20569
+ console.log("");
20570
+ console.log(` ${dim11("Next steps:")}`);
20571
+ console.log(` • Restart Claude Code to pick up the new context`);
20572
+ console.log(` • Run ${yellow8("specialists list")} to see available specialists`);
20573
+ console.log(` • Run ${yellow8("specialist_init")} in a new session to bootstrap context`);
20574
+ console.log("");
20575
+ }
20576
+ var bold9 = (s) => `\x1B[1m${s}\x1B[0m`, dim11 = (s) => `\x1B[2m${s}\x1B[0m`, green10 = (s) => `\x1B[32m${s}\x1B[0m`, yellow8 = (s) => `\x1B[33m${s}\x1B[0m`, MARKER = "## Specialists Workflow", WORKFLOW_BLOCK = `## Specialists Workflow
20577
+
20578
+ > Injected by \`specialists setup\`. Keep this section — agents use it for context.
20579
+
20580
+ ### When to use specialists
20581
+
20582
+ Specialists are autonomous AI agents (running via the \`specialists\` MCP server)
20583
+ optimised for heavy tasks: code review, deep bug analysis, test generation,
20584
+ architecture design. Use them instead of doing the work yourself when the task
20585
+ would benefit from a fresh perspective, a second opinion, or a different model.
20586
+
20587
+ ### Quick reference
20588
+
20589
+ \`\`\`
20590
+ # List available specialists
20591
+ specialists list # all scopes
20592
+ specialists list --scope project # this project only
20593
+
20594
+ # Run a specialist (foreground — streams output)
20595
+ specialists run <name> --prompt "..."
20596
+
20597
+ # Run async (background — immediate job ID)
20598
+ specialists run <name> --prompt "..." --background
20599
+ → Job started: job_a1b2c3d4
20600
+
20601
+ # Watch / get results
20602
+ specialists feed job_a1b2c3d4 --follow # tail live events
20603
+ specialists result job_a1b2c3d4 # read final output
20604
+ specialists stop job_a1b2c3d4 # cancel if needed
20605
+ \`\`\`
20606
+
20607
+ ### MCP tools (available in this session)
20608
+
20609
+ | Tool | Purpose |
20610
+ |------|---------|
20611
+ | \`specialist_init\` | Bootstrap: bd init + list specialists |
20612
+ | \`list_specialists\` | Discover specialists across scopes |
20613
+ | \`use_specialist\` | Run foreground: load → inject context → execute → output |
20614
+ | \`start_specialist\` | Start async: returns job ID immediately |
20615
+ | \`poll_specialist\` | Poll job status + delta output by ID |
20616
+ | \`stop_specialist\` | Cancel a running job |
20617
+ | \`run_parallel\` | Run multiple specialists concurrently or as a pipeline |
20618
+ | \`specialist_status\` | Circuit breaker health + staleness |
20619
+
20620
+ ### Completion banner format
20621
+
20622
+ When a specialist finishes, you may see:
20623
+
20624
+ \`\`\`
20625
+ ✓ bead unitAI-xxx 4.1s anthropic/claude-sonnet-4-6
20626
+ \`\`\`
20627
+
20628
+ This means:
20629
+ - The specialist completed successfully
20630
+ - A beads issue (\`unitAI-xxx\`) was created to track the run
20631
+ - The result can be fetched with \`specialists result <job-id>\`
20632
+
20633
+ ### When NOT to use specialists
20634
+
20635
+ - Simple single-file edits — just do it directly
20636
+ - Tasks that need interactive back-and-forth — use foreground mode or work yourself
20637
+ - Short read-only queries — faster to answer directly
20638
+ `;
20639
+ var init_setup = () => {};
20640
+
19301
20641
  // src/cli/help.ts
19302
20642
  var exports_help = {};
19303
20643
  __export(exports_help, {
19304
- run: () => run12
20644
+ run: () => run15
19305
20645
  });
19306
- async function run12() {
20646
+ function formatCommands(entries) {
20647
+ const width = Math.max(...entries.map(([cmd3]) => cmd3.length));
20648
+ return entries.map(([cmd3, desc]) => ` ${cmd3.padEnd(width)} ${desc}`);
20649
+ }
20650
+ async function run15() {
19307
20651
  const lines = [
19308
20652
  "",
19309
- bold7("specialists <command>"),
20653
+ "Specialists lets you run project-scoped specialist agents with a bead-first workflow.",
20654
+ "",
20655
+ bold10("Usage:"),
20656
+ " specialists [command]",
20657
+ " specialists [command] --help",
20658
+ "",
20659
+ bold10("Common flows:"),
20660
+ "",
20661
+ " Tracked work (primary)",
20662
+ ' bd create "Task title" -t task -p 1 --json',
20663
+ " specialists run <name> --bead <id> [--context-depth N] [--background]",
20664
+ " specialists feed -f",
20665
+ ' bd close <id> --reason "Done"',
20666
+ "",
20667
+ " Ad-hoc work",
20668
+ ' specialists run <name> --prompt "..."',
20669
+ "",
20670
+ " Rules",
20671
+ " --bead is for tracked work",
20672
+ " --prompt is for quick untracked work",
20673
+ " --context-depth defaults to 1 with --bead",
20674
+ " --no-beads does not disable bead reading",
20675
+ "",
20676
+ bold10("Core commands:"),
20677
+ ...formatCommands(CORE_COMMANDS),
20678
+ "",
20679
+ bold10("Extended commands:"),
20680
+ ...formatCommands(EXTENDED_COMMANDS),
20681
+ "",
20682
+ bold10("xtrm worktree commands:"),
20683
+ ...formatCommands(WORKTREE_COMMANDS),
19310
20684
  "",
19311
- "Commands:",
19312
- ...COMMANDS.map(([cmd2, desc]) => ` ${cmd2.padEnd(COL_WIDTH)} ${dim10(desc)}`),
20685
+ bold10("Examples:"),
20686
+ " specialists init",
20687
+ " specialists list",
20688
+ " specialists run bug-hunt --bead unitAI-123 --background",
20689
+ ' specialists run codebase-explorer --prompt "Map the CLI architecture"',
20690
+ " specialists feed -f",
20691
+ " specialists result <job-id>",
19313
20692
  "",
19314
- dim10("Run 'specialists <command> --help' for command-specific options."),
20693
+ bold10("More help:"),
20694
+ " specialists quickstart Full guide and workflow reference",
20695
+ " specialists run --help Run command details and flags",
20696
+ " specialists init --help Bootstrap behavior and workflow injection",
20697
+ " specialists feed --help Background job monitoring details",
20698
+ "",
20699
+ dim12("Project model: specialists are project-only; user-scope discovery is deprecated."),
19315
20700
  ""
19316
20701
  ];
19317
20702
  console.log(lines.join(`
19318
20703
  `));
19319
20704
  }
19320
- var bold7 = (s) => `\x1B[1m${s}\x1B[0m`, dim10 = (s) => `\x1B[2m${s}\x1B[0m`, COMMANDS, COL_WIDTH;
20705
+ var bold10 = (s) => `\x1B[1m${s}\x1B[0m`, dim12 = (s) => `\x1B[2m${s}\x1B[0m`, CORE_COMMANDS, EXTENDED_COMMANDS, WORKTREE_COMMANDS;
19321
20706
  var init_help = __esm(() => {
19322
- COMMANDS = [
19323
- ["install", "Full-stack installer: pi, beads, dolt, MCP registration, hooks"],
19324
- ["list", "List available specialists with model and description"],
19325
- ["models", "List models available on pi, flagged with thinking/images support"],
20707
+ CORE_COMMANDS = [
20708
+ ["init", "Bootstrap a project: dirs, workflow injection, project MCP registration"],
20709
+ ["list", "List specialists in this project"],
20710
+ ["run", "Run a specialist with --bead for tracked work or --prompt for ad-hoc work"],
20711
+ ["feed", "Tail job events; use -f to follow all jobs"],
20712
+ ["result", "Print final output of a completed background job"],
20713
+ ["stop", "Stop a running background job"],
20714
+ ["status", "Show health, MCP state, and active jobs"],
20715
+ ["doctor", "Diagnose installation/runtime problems"],
20716
+ ["quickstart", "Full getting-started guide"],
20717
+ ["help", "Show this help"]
20718
+ ];
20719
+ EXTENDED_COMMANDS = [
20720
+ ["edit", "Edit a specialist field such as model or description"],
20721
+ ["models", "List models available on pi"],
19326
20722
  ["version", "Print installed version"],
19327
- ["init", "Initialize specialists in the current project"],
19328
- ["edit", "Edit a specialist field (e.g. --model, --description)"],
19329
- ["run", "Run a specialist with a prompt (--background for async)"],
19330
- ["result", "Print result of a background job"],
19331
- ["feed", "Tail events for a background job (--follow to stream)"],
19332
- ["stop", "Send SIGTERM to a running background job"],
19333
- ["status", "Show system health (pi, beads, MCP, jobs)"],
19334
- ["help", "Show this help message"]
20723
+ ["setup", "[deprecated] Use specialists init instead"],
20724
+ ["install", "[deprecated] Use specialists init instead"]
20725
+ ];
20726
+ WORKTREE_COMMANDS = [
20727
+ ["xt pi [name]", "Start a Pi session in a sandboxed xt worktree"],
20728
+ ["xt claude [name]", "Start a Claude session in a sandboxed xt worktree"],
20729
+ ["xt attach [slug]", "Resume an existing xt worktree session"],
20730
+ ["xt worktree list", "List worktrees with runtime and activity"],
20731
+ ["xt end", "Close session, push, PR, cleanup"]
19335
20732
  ];
19336
- COL_WIDTH = Math.max(...COMMANDS.map(([cmd2]) => cmd2.length));
19337
20733
  });
19338
20734
 
19339
20735
  // node_modules/zod/v4/core/core.js
@@ -25362,6 +26758,9 @@ class Protocol {
25362
26758
  }
25363
26759
  }
25364
26760
  async connect(transport) {
26761
+ if (this._transport) {
26762
+ throw new Error("Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.");
26763
+ }
25365
26764
  this._transport = transport;
25366
26765
  const _onclose = this.transport?.onclose;
25367
26766
  this._transport.onclose = () => {
@@ -25394,6 +26793,10 @@ class Protocol {
25394
26793
  this._progressHandlers.clear();
25395
26794
  this._taskProgressTokens.clear();
25396
26795
  this._pendingDebouncedNotifications.clear();
26796
+ for (const controller of this._requestHandlerAbortControllers.values()) {
26797
+ controller.abort();
26798
+ }
26799
+ this._requestHandlerAbortControllers.clear();
25397
26800
  const error2 = McpError.fromError(ErrorCode.ConnectionClosed, "Connection closed");
25398
26801
  this._transport = undefined;
25399
26802
  this.onclose?.();
@@ -25444,6 +26847,8 @@ class Protocol {
25444
26847
  sessionId: capturedTransport?.sessionId,
25445
26848
  _meta: request.params?._meta,
25446
26849
  sendNotification: async (notification) => {
26850
+ if (abortController.signal.aborted)
26851
+ return;
25447
26852
  const notificationOptions = { relatedRequestId: request.id };
25448
26853
  if (relatedTaskId) {
25449
26854
  notificationOptions.relatedTask = { taskId: relatedTaskId };
@@ -25451,6 +26856,9 @@ class Protocol {
25451
26856
  await this.notification(notification, notificationOptions);
25452
26857
  },
25453
26858
  sendRequest: async (r, resultSchema, options) => {
26859
+ if (abortController.signal.aborted) {
26860
+ throw new McpError(ErrorCode.ConnectionClosed, "Request was cancelled");
26861
+ }
25454
26862
  const requestOptions = { ...options, relatedRequestId: request.id };
25455
26863
  if (relatedTaskId && !requestOptions.relatedTask) {
25456
26864
  requestOptions.relatedTask = { taskId: relatedTaskId };
@@ -26064,6 +27472,62 @@ class ExperimentalServerTasks {
26064
27472
  requestStream(request, resultSchema, options) {
26065
27473
  return this._server.requestStream(request, resultSchema, options);
26066
27474
  }
27475
+ createMessageStream(params, options) {
27476
+ const clientCapabilities = this._server.getClientCapabilities();
27477
+ if ((params.tools || params.toolChoice) && !clientCapabilities?.sampling?.tools) {
27478
+ throw new Error("Client does not support sampling tools capability.");
27479
+ }
27480
+ if (params.messages.length > 0) {
27481
+ const lastMessage = params.messages[params.messages.length - 1];
27482
+ const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
27483
+ const hasToolResults = lastContent.some((c) => c.type === "tool_result");
27484
+ const previousMessage = params.messages.length > 1 ? params.messages[params.messages.length - 2] : undefined;
27485
+ const previousContent = previousMessage ? Array.isArray(previousMessage.content) ? previousMessage.content : [previousMessage.content] : [];
27486
+ const hasPreviousToolUse = previousContent.some((c) => c.type === "tool_use");
27487
+ if (hasToolResults) {
27488
+ if (lastContent.some((c) => c.type !== "tool_result")) {
27489
+ throw new Error("The last message must contain only tool_result content if any is present");
27490
+ }
27491
+ if (!hasPreviousToolUse) {
27492
+ throw new Error("tool_result blocks are not matching any tool_use from the previous message");
27493
+ }
27494
+ }
27495
+ if (hasPreviousToolUse) {
27496
+ const toolUseIds = new Set(previousContent.filter((c) => c.type === "tool_use").map((c) => c.id));
27497
+ const toolResultIds = new Set(lastContent.filter((c) => c.type === "tool_result").map((c) => c.toolUseId));
27498
+ if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every((id) => toolResultIds.has(id))) {
27499
+ throw new Error("ids of tool_result blocks and tool_use blocks from previous message do not match");
27500
+ }
27501
+ }
27502
+ }
27503
+ return this.requestStream({
27504
+ method: "sampling/createMessage",
27505
+ params
27506
+ }, CreateMessageResultSchema, options);
27507
+ }
27508
+ elicitInputStream(params, options) {
27509
+ const clientCapabilities = this._server.getClientCapabilities();
27510
+ const mode = params.mode ?? "form";
27511
+ switch (mode) {
27512
+ case "url": {
27513
+ if (!clientCapabilities?.elicitation?.url) {
27514
+ throw new Error("Client does not support url elicitation.");
27515
+ }
27516
+ break;
27517
+ }
27518
+ case "form": {
27519
+ if (!clientCapabilities?.elicitation?.form) {
27520
+ throw new Error("Client does not support form elicitation.");
27521
+ }
27522
+ break;
27523
+ }
27524
+ }
27525
+ const normalizedParams = mode === "form" && params.mode === undefined ? { ...params, mode: "form" } : params;
27526
+ return this.requestStream({
27527
+ method: "elicitation/create",
27528
+ params: normalizedParams
27529
+ }, ElicitResultSchema, options);
27530
+ }
26067
27531
  async getTask(taskId, options) {
26068
27532
  return this._server.getTask({ taskId }, options);
26069
27533
  }
@@ -26538,7 +28002,7 @@ class StdioServerTransport {
26538
28002
  }
26539
28003
 
26540
28004
  // src/server.ts
26541
- import { join as join3 } from "node:path";
28005
+ import { join as join4 } from "node:path";
26542
28006
 
26543
28007
  // src/constants.ts
26544
28008
  var LOG_PREFIX = "[specialists]";
@@ -26627,25 +28091,49 @@ function createListSpecialistsTool(loader) {
26627
28091
 
26628
28092
  // src/tools/specialist/use_specialist.tool.ts
26629
28093
  init_zod();
28094
+ init_beads();
26630
28095
  var useSpecialistSchema = exports_external.object({
26631
28096
  name: exports_external.string().describe("Specialist identifier (e.g. codebase-explorer)"),
26632
- prompt: exports_external.string().describe("The task or question for the specialist"),
28097
+ prompt: exports_external.string().optional().describe("The task or question for the specialist"),
28098
+ bead_id: exports_external.string().optional().describe("Use an existing bead as the specialist prompt"),
26633
28099
  variables: exports_external.record(exports_external.string()).optional().describe("Additional $variable substitutions"),
26634
28100
  backend_override: exports_external.string().optional().describe("Force a specific backend (gemini, qwen, anthropic)"),
26635
- autonomy_level: exports_external.string().optional().describe("Override permission level for this invocation")
28101
+ autonomy_level: exports_external.string().optional().describe("Override permission level for this invocation"),
28102
+ context_depth: exports_external.number().optional().describe("Depth of blocker context injection (0 = none, 1 = immediate blockers, etc.)")
28103
+ }).refine((input) => Boolean(input.prompt?.trim() || input.bead_id), {
28104
+ message: "Either prompt or bead_id is required",
28105
+ path: ["prompt"]
26636
28106
  });
26637
28107
  function createUseSpecialistTool(runner) {
26638
28108
  return {
26639
28109
  name: "use_specialist",
26640
- description: "Run a specialist synchronously and wait for the result. " + "Full lifecycle: load → agents.md → pi session → output. " + "Response includes output, model, durationMs, and beadId (string | undefined). " + "beadId is set when the specialist's beads_integration policy triggered bead creation " + "(default: auto — creates for LOW/MEDIUM/HIGH permission, skips for READ_ONLY). " + "If beadId is present, use `bd update <beadId> --notes` to attach findings or " + "`bd remember` to persist key discoveries for future sessions.",
28110
+ description: "Run a specialist synchronously and wait for the result. " + "Full lifecycle: load → agents.md → pi session → output. " + "Response includes output, model, durationMs, and beadId (string | undefined). " + "beadId is set when the specialist's beads_integration policy triggered bead creation " + "(default: auto — creates for LOW/MEDIUM/HIGH permission, skips for READ_ONLY). " + "If beadId is present, use `bd update <beadId> --notes` to attach findings or " + "`bd remember` to persist key discoveries for future sessions. " + "When bead_id is provided, the source bead becomes the specialist prompt and the tracking bead links back to it. " + "Use context_depth to inject outputs from completed blocking dependencies (depth 1 = immediate blockers, 2 = include their blockers too).",
26641
28111
  inputSchema: useSpecialistSchema,
26642
28112
  async execute(input, onProgress) {
28113
+ let prompt = input.prompt?.trim() ?? "";
28114
+ let variables = input.variables;
28115
+ if (input.bead_id) {
28116
+ const beadsClient = new BeadsClient;
28117
+ const bead = beadsClient.readBead(input.bead_id);
28118
+ if (!bead) {
28119
+ throw new Error(`Unable to read bead '${input.bead_id}' via bd show --json`);
28120
+ }
28121
+ const blockers = input.context_depth && input.context_depth > 0 ? beadsClient.getBlockers(input.bead_id, input.context_depth) : [];
28122
+ const beadContext = buildBeadContext(bead, { blockers, depth: input.context_depth ?? 0 });
28123
+ prompt = beadContext;
28124
+ variables = {
28125
+ ...input.variables ?? {},
28126
+ bead_context: beadContext,
28127
+ bead_id: input.bead_id
28128
+ };
28129
+ }
26643
28130
  return runner.run({
26644
28131
  name: input.name,
26645
- prompt: input.prompt,
26646
- variables: input.variables,
28132
+ prompt,
28133
+ variables,
26647
28134
  backendOverride: input.backend_override,
26648
- autonomyLevel: input.autonomy_level
28135
+ autonomyLevel: input.autonomy_level,
28136
+ inputBeadId: input.bead_id
26649
28137
  }, onProgress);
26650
28138
  }
26651
28139
  };
@@ -26751,14 +28239,14 @@ function createSpecialistStatusTool(loader, circuitBreaker) {
26751
28239
  async execute(_) {
26752
28240
  const list = await loader.list();
26753
28241
  const stalenessResults = await Promise.all(list.map((s) => checkStaleness(s)));
26754
- const { existsSync: existsSync2, readdirSync, readFileSync } = await import("node:fs");
26755
- const { join: join2 } = await import("node:path");
26756
- const jobsDir = join2(process.cwd(), ".specialists", "jobs");
28242
+ const { existsSync: existsSync3, readdirSync, readFileSync } = await import("node:fs");
28243
+ const { join: join3 } = await import("node:path");
28244
+ const jobsDir = join3(process.cwd(), ".specialists", "jobs");
26757
28245
  const jobs = [];
26758
- if (existsSync2(jobsDir)) {
28246
+ if (existsSync3(jobsDir)) {
26759
28247
  for (const entry of readdirSync(jobsDir)) {
26760
- const statusPath = join2(jobsDir, entry, "status.json");
26761
- if (!existsSync2(statusPath))
28248
+ const statusPath = join3(jobsDir, entry, "status.json");
28249
+ if (!existsSync3(statusPath))
26762
28250
  continue;
26763
28251
  try {
26764
28252
  jobs.push(JSON.parse(readFileSync(statusPath, "utf-8")));
@@ -26970,13 +28458,13 @@ init_zod();
26970
28458
  // src/tools/specialist/specialist_init.tool.ts
26971
28459
  init_zod();
26972
28460
  import { spawnSync as spawnSync2 } from "node:child_process";
26973
- import { existsSync as existsSync2 } from "node:fs";
26974
- import { join as join2 } from "node:path";
28461
+ import { existsSync as existsSync3 } from "node:fs";
28462
+ import { join as join3 } from "node:path";
26975
28463
  var specialistInitSchema = objectType({});
26976
28464
  function createSpecialistInitTool(loader, deps) {
26977
28465
  const resolved = deps ?? {
26978
28466
  bdAvailable: () => spawnSync2("bd", ["--version"], { stdio: "ignore" }).status === 0,
26979
- beadsExists: () => existsSync2(join2(process.cwd(), ".beads")),
28467
+ beadsExists: () => existsSync3(join3(process.cwd(), ".beads")),
26980
28468
  bdInit: () => spawnSync2("bd", ["init"], { stdio: "ignore" })
26981
28469
  };
26982
28470
  return {
@@ -27011,7 +28499,7 @@ class SpecialistsServer {
27011
28499
  const circuitBreaker = new CircuitBreaker;
27012
28500
  const loader = new SpecialistLoader;
27013
28501
  const hooks = new HookEmitter({
27014
- tracePath: join3(process.cwd(), ".specialists", "trace.jsonl")
28502
+ tracePath: join4(process.cwd(), ".specialists", "trace.jsonl")
27015
28503
  });
27016
28504
  const beadsClient = new BeadsClient;
27017
28505
  const runner = new SpecialistRunner({ loader, hooks, circuitBreaker, beadsClient });
@@ -27107,8 +28595,26 @@ class SpecialistsServer {
27107
28595
 
27108
28596
  // src/index.ts
27109
28597
  var sub = process.argv[2];
27110
- async function run13() {
28598
+ var next = process.argv[3];
28599
+ function wantsHelp() {
28600
+ return next === "--help" || next === "-h";
28601
+ }
28602
+ async function run16() {
27111
28603
  if (sub === "install") {
28604
+ if (wantsHelp()) {
28605
+ console.log([
28606
+ "",
28607
+ "Usage: specialists install",
28608
+ "",
28609
+ "Project setup: checks pi/bd/xt prerequisites, registers the MCP server,",
28610
+ "and installs specialists-specific project hooks.",
28611
+ "",
28612
+ "No flags — just run it.",
28613
+ ""
28614
+ ].join(`
28615
+ `));
28616
+ return;
28617
+ }
27112
28618
  const { run: handler } = await Promise.resolve().then(() => (init_install(), exports_install));
27113
28619
  return handler();
27114
28620
  }
@@ -27117,41 +28623,322 @@ async function run13() {
27117
28623
  return handler();
27118
28624
  }
27119
28625
  if (sub === "list") {
28626
+ if (wantsHelp()) {
28627
+ console.log([
28628
+ "",
28629
+ "Usage: specialists list [options]",
28630
+ "",
28631
+ "List specialists in the current project.",
28632
+ "",
28633
+ "What it shows:",
28634
+ " - specialist name",
28635
+ " - model",
28636
+ " - short description",
28637
+ "",
28638
+ "Options:",
28639
+ " --category <name> Filter by category tag",
28640
+ " --json Output as JSON array",
28641
+ "",
28642
+ "Examples:",
28643
+ " specialists list",
28644
+ " specialists list --category analysis",
28645
+ " specialists list --json",
28646
+ "",
28647
+ "Project model:",
28648
+ " Specialists are project-only. User-scope discovery is deprecated.",
28649
+ ""
28650
+ ].join(`
28651
+ `));
28652
+ return;
28653
+ }
27120
28654
  const { run: handler } = await Promise.resolve().then(() => (init_list(), exports_list));
27121
28655
  return handler();
27122
28656
  }
27123
28657
  if (sub === "models") {
28658
+ if (wantsHelp()) {
28659
+ console.log([
28660
+ "",
28661
+ "Usage: specialists models",
28662
+ "",
28663
+ "List all models available on pi, with thinking and image support flags.",
28664
+ "",
28665
+ "No flags.",
28666
+ ""
28667
+ ].join(`
28668
+ `));
28669
+ return;
28670
+ }
27124
28671
  const { run: handler } = await Promise.resolve().then(() => (init_models(), exports_models));
27125
28672
  return handler();
27126
28673
  }
27127
28674
  if (sub === "init") {
28675
+ if (wantsHelp()) {
28676
+ console.log([
28677
+ "",
28678
+ "Usage: specialists init [--force-workflow]",
28679
+ "",
28680
+ "Bootstrap a project for specialists. This is the sole onboarding command.",
28681
+ "",
28682
+ "What it does:",
28683
+ " • creates specialists/ for project .specialist.yaml files",
28684
+ " • creates .specialists/ runtime dirs (jobs/, ready/)",
28685
+ " • adds .specialists/ to .gitignore",
28686
+ " • injects the managed workflow block into AGENTS.md and CLAUDE.md",
28687
+ " • registers the Specialists MCP server at project scope",
28688
+ "",
28689
+ "Options:",
28690
+ " --force-workflow Overwrite existing managed workflow blocks",
28691
+ "",
28692
+ "Examples:",
28693
+ " specialists init",
28694
+ " specialists init --force-workflow",
28695
+ "",
28696
+ "Notes:",
28697
+ " setup and install are deprecated; use specialists init.",
28698
+ " Safe to run again; existing project state is preserved where possible.",
28699
+ ""
28700
+ ].join(`
28701
+ `));
28702
+ return;
28703
+ }
27128
28704
  const { run: handler } = await Promise.resolve().then(() => (init_init(), exports_init));
27129
28705
  return handler();
27130
28706
  }
27131
28707
  if (sub === "edit") {
28708
+ if (wantsHelp()) {
28709
+ console.log([
28710
+ "",
28711
+ "Usage: specialists edit <name> --<field> <value> [options]",
28712
+ "",
28713
+ "Edit a field in a .specialist.yaml without opening the file.",
28714
+ "",
28715
+ "Editable fields:",
28716
+ " --model <value> Primary execution model",
28717
+ " --fallback-model <value> Fallback model (used on circuit-break)",
28718
+ " --description <value> One-line description",
28719
+ " --permission <value> READ_ONLY | LOW | MEDIUM | HIGH",
28720
+ " --timeout <ms> Timeout in milliseconds",
28721
+ " --tags <a,b,c> Comma-separated list of tags",
28722
+ "",
28723
+ "Options:",
28724
+ " --dry-run Preview the change without writing",
28725
+ " --scope <project|user> Disambiguate if same name exists in multiple scopes",
28726
+ "",
28727
+ "Examples:",
28728
+ " specialists edit code-review --model anthropic/claude-opus-4-6",
28729
+ " specialists edit code-review --permission HIGH --dry-run",
28730
+ " specialists edit code-review --tags analysis,security",
28731
+ ""
28732
+ ].join(`
28733
+ `));
28734
+ return;
28735
+ }
27132
28736
  const { run: handler } = await Promise.resolve().then(() => (init_edit(), exports_edit));
27133
28737
  return handler();
27134
28738
  }
27135
28739
  if (sub === "run") {
28740
+ if (wantsHelp()) {
28741
+ console.log([
28742
+ "",
28743
+ "Usage: specialists run <name> [options]",
28744
+ "",
28745
+ "Run a specialist in foreground or background.",
28746
+ "",
28747
+ "Primary modes:",
28748
+ " tracked: specialists run <name> --bead <id>",
28749
+ ' ad-hoc: specialists run <name> --prompt "..."',
28750
+ "",
28751
+ "Options:",
28752
+ " --bead <id> Use an existing bead as the prompt source",
28753
+ " --prompt <text> Ad-hoc prompt for untracked work",
28754
+ " --context-depth <n> Dependency context depth when using --bead (default: 1)",
28755
+ " --no-beads Do not create a new tracking bead (does not disable bead reading)",
28756
+ " --background Start async and return a job id",
28757
+ " --model <model> Override the configured model for this run",
28758
+ "",
28759
+ "Examples:",
28760
+ " specialists run bug-hunt --bead unitAI-55d",
28761
+ " specialists run bug-hunt --bead unitAI-55d --context-depth 2 --background",
28762
+ ' specialists run code-review --prompt "Audit src/api.ts"',
28763
+ " cat brief.md | specialists run report-generator",
28764
+ "",
28765
+ "Rules:",
28766
+ " Use --bead for tracked work.",
28767
+ " Use --prompt for quick ad-hoc work.",
28768
+ ""
28769
+ ].join(`
28770
+ `));
28771
+ return;
28772
+ }
27136
28773
  const { run: handler } = await Promise.resolve().then(() => (init_run(), exports_run));
27137
28774
  return handler();
27138
28775
  }
27139
28776
  if (sub === "status") {
28777
+ if (wantsHelp()) {
28778
+ console.log([
28779
+ "",
28780
+ "Usage: specialists status [options]",
28781
+ "",
28782
+ "Show current runtime state.",
28783
+ "",
28784
+ "Sections include:",
28785
+ " - discovered specialists",
28786
+ " - pi provider/runtime health",
28787
+ " - beads availability",
28788
+ " - MCP registration hints",
28789
+ " - active background jobs",
28790
+ "",
28791
+ "Options:",
28792
+ " --json Output machine-readable JSON",
28793
+ "",
28794
+ "Examples:",
28795
+ " specialists status",
28796
+ " specialists status --json",
28797
+ ""
28798
+ ].join(`
28799
+ `));
28800
+ return;
28801
+ }
27140
28802
  const { run: handler } = await Promise.resolve().then(() => (init_status(), exports_status));
27141
28803
  return handler();
27142
28804
  }
27143
28805
  if (sub === "result") {
28806
+ if (wantsHelp()) {
28807
+ console.log([
28808
+ "",
28809
+ "Usage: specialists result <job-id>",
28810
+ "",
28811
+ "Print the final output of a completed background job.",
28812
+ "Exits with code 1 if the job is still running or failed.",
28813
+ "",
28814
+ "Examples:",
28815
+ " specialists result job_a1b2c3d4",
28816
+ " specialists result job_a1b2c3d4 > output.md",
28817
+ "",
28818
+ "See also:",
28819
+ " specialists feed <job-id> --follow (stream live events)",
28820
+ " specialists status (list all active jobs)",
28821
+ ""
28822
+ ].join(`
28823
+ `));
28824
+ return;
28825
+ }
27144
28826
  const { run: handler } = await Promise.resolve().then(() => (init_result(), exports_result));
27145
28827
  return handler();
27146
28828
  }
27147
28829
  if (sub === "feed") {
28830
+ if (wantsHelp()) {
28831
+ console.log([
28832
+ "",
28833
+ "Usage: specialists feed <job-id> [options]",
28834
+ " specialists feed -f [--forever]",
28835
+ "",
28836
+ "Read background job events.",
28837
+ "",
28838
+ "Modes:",
28839
+ " specialists feed <job-id> Replay events for one job",
28840
+ " specialists feed <job-id> -f Follow one job until completion",
28841
+ " specialists feed -f Follow all jobs globally",
28842
+ "",
28843
+ "Options:",
28844
+ " -f, --follow Follow live updates",
28845
+ " --forever Keep following in global mode even when all jobs complete",
28846
+ "",
28847
+ "Examples:",
28848
+ " specialists feed 49adda",
28849
+ " specialists feed 49adda --follow",
28850
+ " specialists feed -f",
28851
+ " specialists feed -f --forever",
28852
+ ""
28853
+ ].join(`
28854
+ `));
28855
+ return;
28856
+ }
27148
28857
  const { run: handler } = await Promise.resolve().then(() => (init_feed(), exports_feed));
27149
28858
  return handler();
27150
28859
  }
27151
28860
  if (sub === "stop") {
28861
+ if (wantsHelp()) {
28862
+ console.log([
28863
+ "",
28864
+ "Usage: specialists stop <job-id>",
28865
+ "",
28866
+ "Send SIGTERM to the agent process for a running background job.",
28867
+ "Has no effect if the job is already done or errored.",
28868
+ "",
28869
+ "Examples:",
28870
+ " specialists stop job_a1b2c3d4",
28871
+ ""
28872
+ ].join(`
28873
+ `));
28874
+ return;
28875
+ }
27152
28876
  const { run: handler } = await Promise.resolve().then(() => (init_stop(), exports_stop));
27153
28877
  return handler();
27154
28878
  }
28879
+ if (sub === "quickstart") {
28880
+ const { run: handler } = await Promise.resolve().then(() => exports_quickstart);
28881
+ return handler();
28882
+ }
28883
+ if (sub === "doctor") {
28884
+ if (wantsHelp()) {
28885
+ console.log([
28886
+ "",
28887
+ "Usage: specialists doctor",
28888
+ "",
28889
+ "Diagnose bootstrap and runtime problems.",
28890
+ "",
28891
+ "Checks:",
28892
+ " 1. pi installed and has active providers",
28893
+ " 2. beads installed and .beads/ present",
28894
+ " 3. xtrm-tools availability",
28895
+ " 4. Specialists MCP registration in .mcp.json",
28896
+ " 5. .specialists/ runtime directories",
28897
+ " 6. hook wiring expectations",
28898
+ " 7. zombie job detection",
28899
+ "",
28900
+ "Behavior:",
28901
+ " - prints fix hints for failing checks",
28902
+ " - auto-creates missing runtime directories when possible",
28903
+ "",
28904
+ "Examples:",
28905
+ " specialists doctor",
28906
+ ""
28907
+ ].join(`
28908
+ `));
28909
+ return;
28910
+ }
28911
+ const { run: handler } = await Promise.resolve().then(() => (init_doctor(), exports_doctor));
28912
+ return handler();
28913
+ }
28914
+ if (sub === "setup") {
28915
+ if (wantsHelp()) {
28916
+ console.log([
28917
+ "",
28918
+ "Usage: specialists setup [options]",
28919
+ "",
28920
+ "Inject the Specialists Workflow context block into AGENTS.md or CLAUDE.md.",
28921
+ "This teaches agents in that project how to use specialists.",
28922
+ "",
28923
+ "Options:",
28924
+ " --project, -p Write to ./CLAUDE.md (default)",
28925
+ " --agents, -a Write to ./AGENTS.md",
28926
+ " --global, -g Write to ~/.claude/CLAUDE.md",
28927
+ " --dry-run Preview the block without writing",
28928
+ "",
28929
+ "Examples:",
28930
+ " specialists setup # → ./CLAUDE.md",
28931
+ " specialists setup --agents # → ./AGENTS.md",
28932
+ " specialists setup --global # → ~/.claude/CLAUDE.md",
28933
+ " specialists setup --dry-run # preview only",
28934
+ ""
28935
+ ].join(`
28936
+ `));
28937
+ return;
28938
+ }
28939
+ const { run: handler } = await Promise.resolve().then(() => (init_setup(), exports_setup));
28940
+ return handler();
28941
+ }
27155
28942
  if (sub === "help" || sub === "--help" || sub === "-h") {
27156
28943
  const { run: handler } = await Promise.resolve().then(() => (init_help(), exports_help));
27157
28944
  return handler();
@@ -27165,7 +28952,7 @@ Run 'specialists help' to see available commands.`);
27165
28952
  const server = new SpecialistsServer;
27166
28953
  await server.start();
27167
28954
  }
27168
- run13().catch((error2) => {
28955
+ run16().catch((error2) => {
27169
28956
  logger.error(`Fatal error: ${error2}`);
27170
28957
  process.exit(1);
27171
28958
  });