@jaggerxtrm/specialists 3.2.0 → 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 = () => {};
@@ -18391,7 +18508,7 @@ async function run4() {
18391
18508
  }
18392
18509
  const allModels = parsePiModels();
18393
18510
  if (!allModels) {
18394
- console.error("pi not found or failed — run specialists install");
18511
+ console.error("pi not found or failed — install and configure pi first");
18395
18512
  process.exit(1);
18396
18513
  }
18397
18514
  let models = allModels;
@@ -18447,36 +18564,62 @@ var exports_init = {};
18447
18564
  __export(exports_init, {
18448
18565
  run: () => run5
18449
18566
  });
18450
- import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18451
- 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";
18452
18569
  function ok(msg) {
18453
18570
  console.log(` ${green2("✓")} ${msg}`);
18454
18571
  }
18455
18572
  function skip(msg) {
18456
18573
  console.log(` ${yellow3("○")} ${msg}`);
18457
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
+ }
18458
18601
  async function run5() {
18459
18602
  const cwd = process.cwd();
18460
18603
  console.log(`
18461
18604
  ${bold3("specialists init")}
18462
18605
  `);
18463
- const specialistsDir = join5(cwd, "specialists");
18464
- if (existsSync3(specialistsDir)) {
18606
+ const specialistsDir = join6(cwd, "specialists");
18607
+ if (existsSync4(specialistsDir)) {
18465
18608
  skip("specialists/ already exists");
18466
18609
  } else {
18467
18610
  mkdirSync(specialistsDir, { recursive: true });
18468
18611
  ok("created specialists/");
18469
18612
  }
18470
- const runtimeDir = join5(cwd, ".specialists");
18471
- if (existsSync3(runtimeDir)) {
18613
+ const runtimeDir = join6(cwd, ".specialists");
18614
+ if (existsSync4(runtimeDir)) {
18472
18615
  skip(".specialists/ already exists");
18473
18616
  } else {
18474
- mkdirSync(join5(runtimeDir, "jobs"), { recursive: true });
18475
- mkdirSync(join5(runtimeDir, "ready"), { recursive: true });
18617
+ mkdirSync(join6(runtimeDir, "jobs"), { recursive: true });
18618
+ mkdirSync(join6(runtimeDir, "ready"), { recursive: true });
18476
18619
  ok("created .specialists/ (jobs/, ready/)");
18477
18620
  }
18478
- const gitignorePath = join5(cwd, ".gitignore");
18479
- if (existsSync3(gitignorePath)) {
18621
+ const gitignorePath = join6(cwd, ".gitignore");
18622
+ if (existsSync4(gitignorePath)) {
18480
18623
  const existing = readFileSync(gitignorePath, "utf-8");
18481
18624
  if (existing.includes(GITIGNORE_ENTRY)) {
18482
18625
  skip(".gitignore already has .specialists/ entry");
@@ -18493,8 +18636,8 @@ ${bold3("specialists init")}
18493
18636
  `, "utf-8");
18494
18637
  ok("created .gitignore with .specialists/ entry");
18495
18638
  }
18496
- const agentsPath = join5(cwd, "AGENTS.md");
18497
- if (existsSync3(agentsPath)) {
18639
+ const agentsPath = join6(cwd, "AGENTS.md");
18640
+ if (existsSync4(agentsPath)) {
18498
18641
  const existing = readFileSync(agentsPath, "utf-8");
18499
18642
  if (existing.includes(AGENTS_MARKER)) {
18500
18643
  skip("AGENTS.md already has Specialists section");
@@ -18508,16 +18651,17 @@ ${bold3("specialists init")}
18508
18651
  writeFileSync(agentsPath, AGENTS_BLOCK, "utf-8");
18509
18652
  ok("created AGENTS.md with Specialists section");
18510
18653
  }
18654
+ ensureProjectMcp(cwd);
18511
18655
  console.log(`
18512
18656
  ${bold3("Done!")}
18513
18657
  `);
18514
18658
  console.log(` ${dim3("Next steps:")}`);
18515
18659
  console.log(` 1. Add your specialists to ${yellow3("specialists/")}`);
18516
18660
  console.log(` 2. Run ${yellow3("specialists list")} to verify they are discovered`);
18517
- 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
18518
18662
  `);
18519
18663
  }
18520
- 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;
18521
18665
  var init_init = __esm(() => {
18522
18666
  AGENTS_BLOCK = `
18523
18667
  ## Specialists
@@ -18527,6 +18671,7 @@ see available specialists. Use \`use_specialist\` or \`start_specialist\` to
18527
18671
  delegate heavy tasks (code review, bug hunting, deep reasoning) to the right
18528
18672
  specialist without user intervention.
18529
18673
  `.trimStart();
18674
+ MCP_SERVER_CONFIG = { command: "specialists", args: [] };
18530
18675
  });
18531
18676
 
18532
18677
  // src/cli/edit.ts
@@ -18659,10 +18804,119 @@ var init_edit = __esm(() => {
18659
18804
  VALID_PERMISSIONS = ["READ_ONLY", "LOW", "MEDIUM", "HIGH"];
18660
18805
  });
18661
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
+
18662
18916
  // src/specialist/supervisor.ts
18663
18917
  import {
18664
18918
  closeSync,
18665
- existsSync as existsSync4,
18919
+ existsSync as existsSync5,
18666
18920
  mkdirSync as mkdirSync2,
18667
18921
  openSync,
18668
18922
  readdirSync,
@@ -18673,7 +18927,32 @@ import {
18673
18927
  writeFileSync as writeFileSync3,
18674
18928
  writeSync
18675
18929
  } from "node:fs";
18676
- 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
+ }
18677
18956
 
18678
18957
  class Supervisor {
18679
18958
  opts;
@@ -18681,23 +18960,23 @@ class Supervisor {
18681
18960
  this.opts = opts;
18682
18961
  }
18683
18962
  jobDir(id) {
18684
- return join6(this.opts.jobsDir, id);
18963
+ return join7(this.opts.jobsDir, id);
18685
18964
  }
18686
18965
  statusPath(id) {
18687
- return join6(this.jobDir(id), "status.json");
18966
+ return join7(this.jobDir(id), "status.json");
18688
18967
  }
18689
18968
  resultPath(id) {
18690
- return join6(this.jobDir(id), "result.txt");
18969
+ return join7(this.jobDir(id), "result.txt");
18691
18970
  }
18692
18971
  eventsPath(id) {
18693
- return join6(this.jobDir(id), "events.jsonl");
18972
+ return join7(this.jobDir(id), "events.jsonl");
18694
18973
  }
18695
18974
  readyDir() {
18696
- return join6(this.opts.jobsDir, "..", "ready");
18975
+ return join7(this.opts.jobsDir, "..", "ready");
18697
18976
  }
18698
18977
  readStatus(id) {
18699
18978
  const path = this.statusPath(id);
18700
- if (!existsSync4(path))
18979
+ if (!existsSync5(path))
18701
18980
  return null;
18702
18981
  try {
18703
18982
  return JSON.parse(readFileSync3(path, "utf-8"));
@@ -18706,12 +18985,12 @@ class Supervisor {
18706
18985
  }
18707
18986
  }
18708
18987
  listJobs() {
18709
- if (!existsSync4(this.opts.jobsDir))
18988
+ if (!existsSync5(this.opts.jobsDir))
18710
18989
  return [];
18711
18990
  const jobs = [];
18712
18991
  for (const entry of readdirSync(this.opts.jobsDir)) {
18713
- const path = join6(this.opts.jobsDir, entry, "status.json");
18714
- if (!existsSync4(path))
18992
+ const path = join7(this.opts.jobsDir, entry, "status.json");
18993
+ if (!existsSync5(path))
18715
18994
  continue;
18716
18995
  try {
18717
18996
  jobs.push(JSON.parse(readFileSync3(path, "utf-8")));
@@ -18732,11 +19011,11 @@ class Supervisor {
18732
19011
  this.writeStatusFile(id, { ...current, ...updates });
18733
19012
  }
18734
19013
  gc() {
18735
- if (!existsSync4(this.opts.jobsDir))
19014
+ if (!existsSync5(this.opts.jobsDir))
18736
19015
  return;
18737
19016
  const cutoff = Date.now() - JOB_TTL_DAYS * 86400000;
18738
19017
  for (const entry of readdirSync(this.opts.jobsDir)) {
18739
- const dir = join6(this.opts.jobsDir, entry);
19018
+ const dir = join7(this.opts.jobsDir, entry);
18740
19019
  try {
18741
19020
  const stat2 = statSync(dir);
18742
19021
  if (!stat2.isDirectory())
@@ -18747,11 +19026,11 @@ class Supervisor {
18747
19026
  }
18748
19027
  }
18749
19028
  crashRecovery() {
18750
- if (!existsSync4(this.opts.jobsDir))
19029
+ if (!existsSync5(this.opts.jobsDir))
18751
19030
  return;
18752
19031
  for (const entry of readdirSync(this.opts.jobsDir)) {
18753
- const statusPath = join6(this.opts.jobsDir, entry, "status.json");
18754
- if (!existsSync4(statusPath))
19032
+ const statusPath = join7(this.opts.jobsDir, entry, "status.json");
19033
+ if (!existsSync5(statusPath))
18755
19034
  continue;
18756
19035
  try {
18757
19036
  const s = JSON.parse(readFileSync3(statusPath, "utf-8"));
@@ -18788,14 +19067,19 @@ class Supervisor {
18788
19067
  };
18789
19068
  this.writeStatusFile(id, initialStatus);
18790
19069
  const eventsFd = openSync(this.eventsPath(id), "a");
18791
- const appendEvent = (obj) => {
19070
+ const appendTimelineEvent = (event) => {
18792
19071
  try {
18793
- writeSync(eventsFd, JSON.stringify({ t: Date.now(), ...obj }) + `
19072
+ writeSync(eventsFd, JSON.stringify(event) + `
18794
19073
  `);
18795
19074
  } catch {}
18796
19075
  };
19076
+ appendTimelineEvent(createRunStartEvent(runOptions.name));
18797
19077
  let textLogged = false;
18798
19078
  let currentTool = "";
19079
+ let currentToolCallId = "";
19080
+ let killFn;
19081
+ const sigtermHandler = () => killFn?.();
19082
+ process.once("SIGTERM", sigtermHandler);
18799
19083
  try {
18800
19084
  const result = await runner.run(runOptions, (delta) => {
18801
19085
  const toolMatch = delta.match(/⚙ (.+?)…/);
@@ -18811,21 +19095,29 @@ class Supervisor {
18811
19095
  last_event_at_ms: now,
18812
19096
  elapsed_s: Math.round((now - startedAtMs) / 1000)
18813
19097
  });
18814
- if (LOGGED_EVENTS.has(eventType)) {
18815
- const tool = eventType === "toolcall" || eventType === "tool_execution_end" ? currentTool : undefined;
18816
- appendEvent({ type: eventType, ...tool ? { tool } : {} });
19098
+ const timelineEvent = mapCallbackEventToTimelineEvent(eventType, {
19099
+ tool: currentTool,
19100
+ toolCallId: currentToolCallId || undefined
19101
+ });
19102
+ if (timelineEvent) {
19103
+ appendTimelineEvent(timelineEvent);
18817
19104
  } else if (eventType === "text" && !textLogged) {
18818
19105
  textLogged = true;
18819
- appendEvent({ type: "text" });
19106
+ appendTimelineEvent({ t: Date.now(), type: TIMELINE_EVENT_TYPES.TEXT });
18820
19107
  }
18821
19108
  }, (meta) => {
18822
19109
  this.updateStatus(id, { model: meta.model, backend: meta.backend });
18823
- appendEvent({ type: "meta", model: meta.model, backend: meta.backend });
18824
- }, (_killFn) => {}, (beadId) => {
19110
+ appendTimelineEvent(createMetaEvent(meta.model, meta.backend));
19111
+ }, (fn) => {
19112
+ killFn = fn;
19113
+ }, (beadId) => {
18825
19114
  this.updateStatus(id, { bead_id: beadId });
18826
19115
  });
18827
19116
  const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
18828
19117
  writeFileSync3(this.resultPath(id), result.output, "utf-8");
19118
+ if (result.beadId) {
19119
+ this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
19120
+ }
18829
19121
  this.updateStatus(id, {
18830
19122
  status: "done",
18831
19123
  elapsed_s: elapsed,
@@ -18834,27 +19126,35 @@ class Supervisor {
18834
19126
  backend: result.backend,
18835
19127
  bead_id: result.beadId
18836
19128
  });
18837
- appendEvent({ type: "agent_end", elapsed_s: elapsed });
18838
- 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");
18839
19135
  return id;
18840
19136
  } catch (err) {
18841
19137
  const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
19138
+ const errorMsg = err?.message ?? String(err);
18842
19139
  this.updateStatus(id, {
18843
19140
  status: "error",
18844
19141
  elapsed_s: elapsed,
18845
- error: err?.message ?? String(err)
19142
+ error: errorMsg
18846
19143
  });
18847
- appendEvent({ type: "error", message: err?.message ?? String(err) });
19144
+ appendTimelineEvent(createRunCompleteEvent("ERROR", elapsed, {
19145
+ error: errorMsg
19146
+ }));
18848
19147
  throw err;
18849
19148
  } finally {
19149
+ process.removeListener("SIGTERM", sigtermHandler);
18850
19150
  closeSync(eventsFd);
18851
19151
  }
18852
19152
  }
18853
19153
  }
18854
- var JOB_TTL_DAYS, LOGGED_EVENTS;
19154
+ var JOB_TTL_DAYS;
18855
19155
  var init_supervisor = __esm(() => {
19156
+ init_timeline_events();
18856
19157
  JOB_TTL_DAYS = Number(process.env.SPECIALISTS_JOB_TTL_DAYS ?? 7);
18857
- LOGGED_EVENTS = new Set(["thinking", "toolcall", "tool_execution_end", "done"]);
18858
19158
  });
18859
19159
 
18860
19160
  // src/cli/run.ts
@@ -18862,27 +19162,37 @@ var exports_run = {};
18862
19162
  __export(exports_run, {
18863
19163
  run: () => run7
18864
19164
  });
18865
- import { join as join7 } from "node:path";
19165
+ import { join as join8 } from "node:path";
18866
19166
  async function parseArgs4(argv) {
18867
19167
  const name = argv[0];
18868
19168
  if (!name || name.startsWith("--")) {
18869
- 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]');
18870
19170
  process.exit(1);
18871
19171
  }
18872
19172
  let prompt = "";
19173
+ let beadId;
18873
19174
  let model;
18874
19175
  let noBeads = false;
18875
19176
  let background = false;
19177
+ let contextDepth = 1;
18876
19178
  for (let i = 1;i < argv.length; i++) {
18877
19179
  const token = argv[i];
18878
19180
  if (token === "--prompt" && argv[i + 1]) {
18879
19181
  prompt = argv[++i];
18880
19182
  continue;
18881
19183
  }
19184
+ if (token === "--bead" && argv[i + 1]) {
19185
+ beadId = argv[++i];
19186
+ continue;
19187
+ }
18882
19188
  if (token === "--model" && argv[i + 1]) {
18883
19189
  model = argv[++i];
18884
19190
  continue;
18885
19191
  }
19192
+ if (token === "--context-depth" && argv[i + 1]) {
19193
+ contextDepth = parseInt(argv[++i], 10) || 0;
19194
+ continue;
19195
+ }
18886
19196
  if (token === "--no-beads") {
18887
19197
  noBeads = true;
18888
19198
  continue;
@@ -18892,10 +19202,11 @@ async function parseArgs4(argv) {
18892
19202
  continue;
18893
19203
  }
18894
19204
  }
18895
- if (!prompt) {
18896
- if (process.stdin.isTTY) {
18897
- process.stderr.write(dim5("Prompt (Ctrl+D when done): "));
18898
- }
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) {
18899
19210
  prompt = await new Promise((resolve) => {
18900
19211
  let buf = "";
18901
19212
  process.stdin.setEncoding("utf-8");
@@ -18905,26 +19216,58 @@ async function parseArgs4(argv) {
18905
19216
  process.stdin.on("end", () => resolve(buf.trim()));
18906
19217
  });
18907
19218
  }
18908
- 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 };
18909
19224
  }
18910
19225
  async function run7() {
18911
19226
  const args = await parseArgs4(process.argv.slice(3));
18912
19227
  const loader = new SpecialistLoader;
18913
19228
  const circuitBreaker = new CircuitBreaker;
18914
- const hooks = new HookEmitter({ tracePath: join7(process.cwd(), ".specialists", "trace.jsonl") });
18915
- 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
+ }
18916
19252
  const runner = new SpecialistRunner({
18917
19253
  loader,
18918
19254
  hooks,
18919
19255
  circuitBreaker,
18920
- beadsClient: beadsClient ?? undefined
19256
+ beadsClient
18921
19257
  });
18922
19258
  if (args.background) {
18923
- const jobsDir = join7(process.cwd(), ".specialists", "jobs");
19259
+ const jobsDir = join8(process.cwd(), ".specialists", "jobs");
18924
19260
  const supervisor = new Supervisor({
18925
19261
  runner,
18926
- runOptions: { name: args.name, prompt: args.prompt, backendOverride: args.model },
18927
- jobsDir
19262
+ runOptions: {
19263
+ name: args.name,
19264
+ prompt,
19265
+ variables,
19266
+ backendOverride: args.model,
19267
+ inputBeadId: args.beadId
19268
+ },
19269
+ jobsDir,
19270
+ beadsClient
18928
19271
  });
18929
19272
  try {
18930
19273
  const jobId = await supervisor.run();
@@ -18941,11 +19284,13 @@ async function run7() {
18941
19284
  ${bold5(`Running ${cyan3(args.name)}`)}
18942
19285
 
18943
19286
  `);
18944
- let beadId;
19287
+ let trackingBeadId;
18945
19288
  const result = await runner.run({
18946
19289
  name: args.name,
18947
- prompt: args.prompt,
18948
- backendOverride: args.model
19290
+ prompt,
19291
+ variables,
19292
+ backendOverride: args.model,
19293
+ inputBeadId: args.beadId
18949
19294
  }, (delta) => process.stdout.write(delta), undefined, (meta) => process.stderr.write(dim5(`
18950
19295
  [${meta.backend} / ${meta.model}]
18951
19296
 
@@ -18958,10 +19303,10 @@ Interrupted.
18958
19303
  killFn();
18959
19304
  process.exit(130);
18960
19305
  });
18961
- }, (id) => {
18962
- beadId = id;
19306
+ }, (beadId) => {
19307
+ trackingBeadId = beadId;
18963
19308
  process.stderr.write(dim5(`
18964
- [bead: ${id}]
19309
+ [bead: ${beadId}]
18965
19310
  `));
18966
19311
  });
18967
19312
  if (result.output && !result.output.endsWith(`
@@ -18969,8 +19314,9 @@ Interrupted.
18969
19314
  process.stdout.write(`
18970
19315
  `);
18971
19316
  const secs = (result.durationMs / 1000).toFixed(1);
19317
+ const effectiveBeadId = args.beadId ?? trackingBeadId;
18972
19318
  const footer = [
18973
- beadId ? `bead ${beadId}` : "",
19319
+ effectiveBeadId ? `bead ${effectiveBeadId}` : "",
18974
19320
  `${secs}s`,
18975
19321
  dim5(result.model)
18976
19322
  ].filter(Boolean).join(" ");
@@ -18988,14 +19334,103 @@ var init_run = __esm(() => {
18988
19334
  init_supervisor();
18989
19335
  });
18990
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
+
18991
19426
  // src/cli/status.ts
18992
19427
  var exports_status = {};
18993
19428
  __export(exports_status, {
18994
19429
  run: () => run8
18995
19430
  });
18996
- import { spawnSync as spawnSync4 } from "node:child_process";
18997
- import { existsSync as existsSync5 } from "node:fs";
18998
- 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";
18999
19434
  function ok2(msg) {
19000
19435
  console.log(` ${green5("✓")} ${msg}`);
19001
19436
  }
@@ -19003,7 +19438,7 @@ function warn(msg) {
19003
19438
  console.log(` ${yellow5("○")} ${msg}`);
19004
19439
  }
19005
19440
  function fail(msg) {
19006
- console.log(` ${red("✗")} ${msg}`);
19441
+ console.log(` ${red2("✗")} ${msg}`);
19007
19442
  }
19008
19443
  function info(msg) {
19009
19444
  console.log(` ${dim6(msg)}`);
@@ -19014,7 +19449,7 @@ function section(label) {
19014
19449
  ${bold6(`── ${label} ${line}`)}`);
19015
19450
  }
19016
19451
  function cmd(bin, args) {
19017
- const r = spawnSync4(bin, args, {
19452
+ const r = spawnSync5(bin, args, {
19018
19453
  encoding: "utf8",
19019
19454
  stdio: "pipe",
19020
19455
  timeout: 5000
@@ -19022,9 +19457,9 @@ function cmd(bin, args) {
19022
19457
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
19023
19458
  }
19024
19459
  function isInstalled(bin) {
19025
- return spawnSync4("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
19460
+ return spawnSync5("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
19026
19461
  }
19027
- function formatElapsed(s) {
19462
+ function formatElapsed2(s) {
19028
19463
  if (s.elapsed_s === undefined)
19029
19464
  return "...";
19030
19465
  const m = Math.floor(s.elapsed_s / 60);
@@ -19034,11 +19469,11 @@ function formatElapsed(s) {
19034
19469
  function statusColor(status) {
19035
19470
  switch (status) {
19036
19471
  case "running":
19037
- return cyan4(status);
19472
+ return cyan5(status);
19038
19473
  case "done":
19039
19474
  return green5(status);
19040
19475
  case "error":
19041
- return red(status);
19476
+ return red2(status);
19042
19477
  case "starting":
19043
19478
  return yellow5(status);
19044
19479
  default:
@@ -19057,11 +19492,11 @@ async function run8() {
19057
19492
  `).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean)) : new Set;
19058
19493
  const bdInstalled = isInstalled("bd");
19059
19494
  const bdVersion = bdInstalled ? cmd("bd", ["--version"]) : null;
19060
- const beadsPresent = existsSync5(join8(process.cwd(), ".beads"));
19495
+ const beadsPresent = existsSync6(join9(process.cwd(), ".beads"));
19061
19496
  const specialistsBin = cmd("which", ["specialists"]);
19062
- const jobsDir = join8(process.cwd(), ".specialists", "jobs");
19497
+ const jobsDir = join9(process.cwd(), ".specialists", "jobs");
19063
19498
  let jobs = [];
19064
- if (existsSync5(jobsDir)) {
19499
+ if (existsSync6(jobsDir)) {
19065
19500
  const supervisor = new Supervisor({
19066
19501
  runner: null,
19067
19502
  runOptions: null,
@@ -19127,7 +19562,7 @@ ${bold6("specialists status")}
19127
19562
  for (const s of allSpecialists) {
19128
19563
  const staleness = stalenessMap[s.name];
19129
19564
  if (staleness === "AGED") {
19130
- warn(`${s.name} ${red("AGED")} ${dim6(s.scope)}`);
19565
+ warn(`${s.name} ${red2("AGED")} ${dim6(s.scope)}`);
19131
19566
  } else if (staleness === "STALE") {
19132
19567
  warn(`${s.name} ${yellow5("STALE")} ${dim6(s.scope)}`);
19133
19568
  }
@@ -19135,7 +19570,7 @@ ${bold6("specialists status")}
19135
19570
  }
19136
19571
  section("pi (coding agent runtime)");
19137
19572
  if (!piInstalled) {
19138
- fail(`pi not installed — run ${yellow5("specialists install")}`);
19573
+ fail(`pi not installed — install ${yellow5("pi")} first`);
19139
19574
  } else {
19140
19575
  const vStr = piVersion?.ok ? `v${piVersion.stdout}` : "unknown version";
19141
19576
  const pStr = piProviders.size > 0 ? `${piProviders.size} provider${piProviders.size > 1 ? "s" : ""} active ${dim6(`(${[...piProviders].join(", ")})`)} ` : yellow5("no providers configured — run pi config");
@@ -19143,7 +19578,7 @@ ${bold6("specialists status")}
19143
19578
  }
19144
19579
  section("beads (issue tracker)");
19145
19580
  if (!bdInstalled) {
19146
- fail(`bd not installed — run ${yellow5("specialists install")}`);
19581
+ fail(`bd not installed — install ${yellow5("bd")} first`);
19147
19582
  } else {
19148
19583
  ok2(`bd installed${bdVersion?.ok ? ` ${dim6(bdVersion.stdout)}` : ""}`);
19149
19584
  if (beadsPresent) {
@@ -19163,17 +19598,18 @@ ${bold6("specialists status")}
19163
19598
  if (jobs.length > 0) {
19164
19599
  section("Active Jobs");
19165
19600
  for (const job of jobs) {
19166
- const elapsed = formatElapsed(job);
19167
- const detail = job.status === "error" ? red(job.error?.slice(0, 40) ?? "error") : job.current_tool ? dim6(`tool: ${job.current_tool}`) : dim6(job.current_event ?? "");
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 ?? "");
19168
19603
  console.log(` ${dim6(job.id)} ${job.specialist.padEnd(20)} ${statusColor(job.status).padEnd(7)} ${elapsed.padStart(6)} ${detail}`);
19169
19604
  }
19170
19605
  }
19171
19606
  console.log();
19172
19607
  }
19173
- 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`;
19174
19609
  var init_status = __esm(() => {
19175
19610
  init_loader();
19176
19611
  init_supervisor();
19612
+ init_format_helpers();
19177
19613
  });
19178
19614
 
19179
19615
  // src/cli/result.ts
@@ -19181,15 +19617,15 @@ var exports_result = {};
19181
19617
  __export(exports_result, {
19182
19618
  run: () => run9
19183
19619
  });
19184
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
19185
- 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";
19186
19622
  async function run9() {
19187
19623
  const jobId = process.argv[3];
19188
19624
  if (!jobId) {
19189
19625
  console.error("Usage: specialists result <job-id>");
19190
19626
  process.exit(1);
19191
19627
  }
19192
- const jobsDir = join9(process.cwd(), ".specialists", "jobs");
19628
+ const jobsDir = join10(process.cwd(), ".specialists", "jobs");
19193
19629
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19194
19630
  const status = supervisor.readStatus(jobId);
19195
19631
  if (!status) {
@@ -19202,107 +19638,340 @@ async function run9() {
19202
19638
  process.exit(1);
19203
19639
  }
19204
19640
  if (status.status === "error") {
19205
- process.stderr.write(`${red2(`Job ${jobId} failed:`)} ${status.error ?? "unknown error"}
19641
+ process.stderr.write(`${red3(`Job ${jobId} failed:`)} ${status.error ?? "unknown error"}
19206
19642
  `);
19207
19643
  process.exit(1);
19208
19644
  }
19209
- const resultPath = join9(jobsDir, jobId, "result.txt");
19210
- if (!existsSync6(resultPath)) {
19645
+ const resultPath = join10(jobsDir, jobId, "result.txt");
19646
+ if (!existsSync7(resultPath)) {
19211
19647
  console.error(`Result file not found for job ${jobId}`);
19212
19648
  process.exit(1);
19213
19649
  }
19214
19650
  process.stdout.write(readFileSync4(resultPath, "utf-8"));
19215
19651
  }
19216
- 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`;
19217
19653
  var init_result = __esm(() => {
19218
19654
  init_supervisor();
19219
19655
  });
19220
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
+
19221
19754
  // src/cli/feed.ts
19222
19755
  var exports_feed = {};
19223
19756
  __export(exports_feed, {
19224
19757
  run: () => run10
19225
19758
  });
19226
- import { existsSync as existsSync7, readFileSync as readFileSync5, watchFile } from "node:fs";
19227
- import { join as join10 } from "node:path";
19228
- function formatEvent(line) {
19229
- try {
19230
- const e = JSON.parse(line);
19231
- const ts = new Date(e.t).toISOString().slice(11, 19);
19232
- const type = e.type ?? "?";
19233
- const extra = e.tool ? ` ${cyan5(e.tool)}` : e.model ? ` ${dim8(e.model)}` : e.message ? ` ${red3(e.message)}` : "";
19234
- return `${dim8(ts)} ${type}${extra}`;
19235
- } catch {
19236
- 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;
19237
19780
  }
19238
19781
  }
19239
- function printLines(content, from) {
19240
- const lines = content.split(`
19241
- `).filter(Boolean);
19242
- for (let i = from;i < lines.length; i++) {
19243
- 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);
19788
+ }
19789
+ const key = getHumanEventKey(event);
19790
+ if (lastPrintedEventKey.get(jobId) === key)
19791
+ return true;
19792
+ lastPrintedEventKey.set(jobId, key);
19793
+ return false;
19794
+ }
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];
19244
19805
  }
19245
- return lines.length;
19806
+ return;
19246
19807
  }
19247
- async function run10() {
19248
- const argv = process.argv.slice(3);
19808
+ function parseArgs5(argv) {
19249
19809
  let jobId;
19810
+ let specialist;
19811
+ let since;
19812
+ let limit = 100;
19250
19813
  let follow = false;
19814
+ let forever = false;
19815
+ let json = false;
19251
19816
  for (let i = 0;i < argv.length; i++) {
19252
19817
  if (argv[i] === "--job" && argv[i + 1]) {
19253
19818
  jobId = argv[++i];
19254
19819
  continue;
19255
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
+ }
19256
19833
  if (argv[i] === "--follow" || argv[i] === "-f") {
19257
19834
  follow = true;
19258
19835
  continue;
19259
19836
  }
19837
+ if (argv[i] === "--forever") {
19838
+ forever = true;
19839
+ continue;
19840
+ }
19841
+ if (argv[i] === "--json") {
19842
+ json = true;
19843
+ continue;
19844
+ }
19260
19845
  if (!jobId && !argv[i].startsWith("--"))
19261
19846
  jobId = argv[i];
19262
19847
  }
19263
- if (!jobId) {
19264
- console.error("Usage: specialists feed --job <job-id> [--follow]");
19265
- 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;
19266
19855
  }
19267
- const jobsDir = join10(process.cwd(), ".specialists", "jobs");
19268
- const eventsPath = join10(jobsDir, jobId, "events.jsonl");
19269
- if (!existsSync7(eventsPath)) {
19270
- const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19271
- if (!supervisor.readStatus(jobId)) {
19272
- console.error(`No job found: ${jobId}`);
19273
- 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 }));
19274
19860
  }
19275
- console.log(dim8("No events yet."));
19276
19861
  return;
19277
19862
  }
19278
- const content = readFileSync5(eventsPath, "utf-8");
19279
- let linesRead = printLines(content, 0);
19280
- if (!follow)
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
+ }
19281
19902
  return;
19282
- process.stderr.write(dim8(`Following ${jobId}... (Ctrl+C to stop)
19903
+ }
19904
+ if (!options.json) {
19905
+ process.stderr.write(dim6(`Following... (Ctrl+C to stop)
19283
19906
  `));
19907
+ }
19908
+ const lastPrintedEventKey = new Map;
19909
+ const seenMetaKey = new Map;
19284
19910
  await new Promise((resolve) => {
19285
- watchFile(eventsPath, { interval: 500 }, () => {
19286
- try {
19287
- const updated = readFileSync5(eventsPath, "utf-8");
19288
- linesRead = printLines(updated, linesRead);
19289
- const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19290
- const status = supervisor.readStatus(jobId);
19291
- if (status && status.status !== "running" && status.status !== "starting") {
19292
- const finalMsg = status.status === "done" ? `
19293
- ${yellow6("Job complete.")} Run: specialists result ${jobId}` : `
19294
- ${red3(`Job ${status.status}.`)} ${status.error ?? ""}`;
19295
- process.stderr.write(finalMsg + `
19296
- `);
19297
- 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
+ }
19298
19925
  }
19299
- } catch {}
19300
- });
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);
19950
+ });
19951
+ }
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
19301
19968
  });
19969
+ printSnapshot(merged, options);
19302
19970
  }
19303
- 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`;
19304
19971
  var init_feed = __esm(() => {
19305
- init_supervisor();
19972
+ init_timeline_events();
19973
+ init_timeline_query();
19974
+ init_format_helpers();
19306
19975
  });
19307
19976
 
19308
19977
  // src/cli/stop.ts
@@ -19310,14 +19979,14 @@ var exports_stop = {};
19310
19979
  __export(exports_stop, {
19311
19980
  run: () => run11
19312
19981
  });
19313
- import { join as join11 } from "node:path";
19982
+ import { join as join13 } from "node:path";
19314
19983
  async function run11() {
19315
19984
  const jobId = process.argv[3];
19316
19985
  if (!jobId) {
19317
19986
  console.error("Usage: specialists stop <job-id>");
19318
19987
  process.exit(1);
19319
19988
  }
19320
- const jobsDir = join11(process.cwd(), ".specialists", "jobs");
19989
+ const jobsDir = join13(process.cwd(), ".specialists", "jobs");
19321
19990
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19322
19991
  const status = supervisor.readStatus(jobId);
19323
19992
  if (!status) {
@@ -19325,31 +19994,31 @@ async function run11() {
19325
19994
  process.exit(1);
19326
19995
  }
19327
19996
  if (status.status === "done" || status.status === "error") {
19328
- process.stderr.write(`${dim9(`Job ${jobId} is already ${status.status}.`)}
19997
+ process.stderr.write(`${dim8(`Job ${jobId} is already ${status.status}.`)}
19329
19998
  `);
19330
19999
  return;
19331
20000
  }
19332
20001
  if (!status.pid) {
19333
- process.stderr.write(`${red4(`No PID recorded for job ${jobId}.`)}
20002
+ process.stderr.write(`${red5(`No PID recorded for job ${jobId}.`)}
19334
20003
  `);
19335
20004
  process.exit(1);
19336
20005
  }
19337
20006
  try {
19338
20007
  process.kill(status.pid, "SIGTERM");
19339
- 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})
19340
20009
  `);
19341
20010
  } catch (err) {
19342
20011
  if (err.code === "ESRCH") {
19343
- 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.
19344
20013
  `);
19345
20014
  } else {
19346
- process.stderr.write(`${red4("Error:")} ${err.message}
20015
+ process.stderr.write(`${red5("Error:")} ${err.message}
19347
20016
  `);
19348
20017
  process.exit(1);
19349
20018
  }
19350
20019
  }
19351
20020
  }
19352
- 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`;
19353
20022
  var init_stop = __esm(() => {
19354
20023
  init_supervisor();
19355
20024
  });
@@ -19362,27 +20031,27 @@ __export(exports_quickstart, {
19362
20031
  function section2(title) {
19363
20032
  const bar = "─".repeat(60);
19364
20033
  return `
19365
- ${bold7(cyan6(title))}
19366
- ${dim10(bar)}`;
20034
+ ${bold7(cyan7(title))}
20035
+ ${dim9(bar)}`;
19367
20036
  }
19368
20037
  function cmd2(s) {
19369
- return yellow7(s);
20038
+ return yellow6(s);
19370
20039
  }
19371
20040
  function flag(s) {
19372
- return green7(s);
20041
+ return green8(s);
19373
20042
  }
19374
20043
  async function run12() {
19375
20044
  const lines = [
19376
20045
  "",
19377
20046
  bold7("specialists · Quick Start Guide"),
19378
- dim10("One MCP server. Multiple AI backends. Intelligent orchestration."),
20047
+ dim9("One MCP server. Multiple AI backends. Intelligent orchestration."),
19379
20048
  ""
19380
20049
  ];
19381
20050
  lines.push(section2("1. Installation"));
19382
20051
  lines.push("");
19383
20052
  lines.push(` ${cmd2("npm install -g @jaggerxtrm/specialists")} # install globally`);
19384
- lines.push(` ${cmd2("specialists install")} # full-stack setup:`);
19385
- lines.push(` ${dim10(" # pi · beads · dolt · MCP · hooks")}`);
20053
+ lines.push(` ${cmd2("specialists install")} # project setup:`);
20054
+ lines.push(` ${dim9(" # checks pi · bd · xt, then wires MCP + hooks")}`);
19386
20055
  lines.push("");
19387
20056
  lines.push(` Verify everything is healthy:`);
19388
20057
  lines.push(` ${cmd2("specialists status")} # shows pi, beads, MCP, active jobs`);
@@ -19393,9 +20062,9 @@ async function run12() {
19393
20062
  lines.push(` ${cmd2("specialists init")} # creates specialists/, .specialists/, AGENTS.md`);
19394
20063
  lines.push("");
19395
20064
  lines.push(` What this creates:`);
19396
- lines.push(` ${dim10("specialists/")} — put your .specialist.yaml files here`);
19397
- lines.push(` ${dim10(".specialists/")} — runtime data (jobs/, ready/) — gitignored`);
19398
- lines.push(` ${dim10("AGENTS.md")} — context block injected into Claude sessions`);
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`);
19399
20068
  lines.push("");
19400
20069
  lines.push(section2("3. Discover Specialists"));
19401
20070
  lines.push("");
@@ -19406,24 +20075,24 @@ async function run12() {
19406
20075
  lines.push(` ${cmd2("specialists list")} ${flag("--json")} # machine-readable JSON`);
19407
20076
  lines.push("");
19408
20077
  lines.push(` Scopes (searched in order):`);
19409
- lines.push(` ${blue("project")} ./specialists/*.specialist.yaml`);
19410
- lines.push(` ${blue("user")} ~/.specialists/*.specialist.yaml`);
19411
- lines.push(` ${blue("system")} bundled specialists (shipped with the package)`);
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)`);
19412
20081
  lines.push("");
19413
20082
  lines.push(section2("4. Running a Specialist"));
19414
20083
  lines.push("");
19415
20084
  lines.push(` ${bold7("Foreground")} (streams output to stdout):`);
19416
- lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${dim10('"Review src/api.ts for security issues"')}`);
20085
+ lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${dim9('"Review src/api.ts for security issues"')}`);
19417
20086
  lines.push("");
19418
20087
  lines.push(` ${bold7("Background")} (returns a job ID immediately):`);
19419
- lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${dim10('"..."')} ${flag("--background")}`);
19420
- lines.push(` ${dim10(" # → Job started: job_a1b2c3d4")}`);
20088
+ lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${dim9('"..."')} ${flag("--background")}`);
20089
+ lines.push(` ${dim9(" # → Job started: job_a1b2c3d4")}`);
19421
20090
  lines.push("");
19422
20091
  lines.push(` Override model for one run:`);
19423
- lines.push(` ${cmd2("specialists run code-review")} ${flag("--model")} ${dim10("anthropic/claude-opus-4-6")} ${flag("--prompt")} ${dim10('"..."')}`);
20092
+ lines.push(` ${cmd2("specialists run code-review")} ${flag("--model")} ${dim9("anthropic/claude-opus-4-6")} ${flag("--prompt")} ${dim9('"..."')}`);
19424
20093
  lines.push("");
19425
20094
  lines.push(` Run without beads issue tracking:`);
19426
- lines.push(` ${cmd2("specialists run code-review")} ${flag("--no-beads")} ${flag("--prompt")} ${dim10('"..."')}`);
20095
+ lines.push(` ${cmd2("specialists run code-review")} ${flag("--no-beads")} ${flag("--prompt")} ${dim9('"..."')}`);
19427
20096
  lines.push("");
19428
20097
  lines.push(` Pipe a prompt from stdin:`);
19429
20098
  lines.push(` ${cmd2("cat my-brief.md | specialists run code-review")}`);
@@ -19440,22 +20109,22 @@ async function run12() {
19440
20109
  lines.push(` ${bold7("Cancel a job")}:`);
19441
20110
  lines.push(` ${cmd2("specialists stop job_a1b2c3d4")} # sends SIGTERM to the agent process`);
19442
20111
  lines.push("");
19443
- lines.push(` ${bold7("Job files")} in ${dim10(".specialists/jobs/<job-id>/")}:`);
19444
- lines.push(` ${dim10("status.json")} — id, specialist, status, pid, started_at, elapsed_s, current_tool`);
19445
- lines.push(` ${dim10("events.jsonl")} — one JSON event per line (tool_use, text, agent_end, error …)`);
19446
- lines.push(` ${dim10("result.txt")} — final output (written when status=done)`);
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)`);
19447
20116
  lines.push("");
19448
20117
  lines.push(section2("6. Editing Specialists"));
19449
20118
  lines.push("");
19450
20119
  lines.push(` Change a field without opening the YAML manually:`);
19451
- lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${dim10("anthropic/claude-sonnet-4-6")}`);
19452
- lines.push(` ${cmd2("specialists edit code-review")} ${flag("--description")} ${dim10('"Updated description"')}`);
19453
- lines.push(` ${cmd2("specialists edit code-review")} ${flag("--timeout")} ${dim10("120000")}`);
19454
- lines.push(` ${cmd2("specialists edit code-review")} ${flag("--permission")} ${dim10("HIGH")}`);
19455
- lines.push(` ${cmd2("specialists edit code-review")} ${flag("--tags")} ${dim10("analysis,security,review")}`);
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")}`);
19456
20125
  lines.push("");
19457
20126
  lines.push(` Preview without writing:`);
19458
- lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${dim10("...")} ${flag("--dry-run")}`);
20127
+ lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${dim9("...")} ${flag("--dry-run")}`);
19459
20128
  lines.push("");
19460
20129
  lines.push(section2("7. .specialist.yaml Schema"));
19461
20130
  lines.push("");
@@ -19502,21 +20171,21 @@ async function run12() {
19502
20171
  " priority: 2 # 0=critical … 4=backlog"
19503
20172
  ];
19504
20173
  for (const l of schemaLines) {
19505
- lines.push(` ${dim10(l)}`);
20174
+ lines.push(` ${dim9(l)}`);
19506
20175
  }
19507
20176
  lines.push("");
19508
20177
  lines.push(section2("8. Hook System"));
19509
20178
  lines.push("");
19510
- lines.push(` Specialists emits lifecycle events to ${dim10(".specialists/trace.jsonl")}:`);
20179
+ lines.push(` Specialists emits lifecycle events to ${dim9(".specialists/trace.jsonl")}:`);
19511
20180
  lines.push("");
19512
20181
  lines.push(` ${bold7("Hook point")} ${bold7("When fired")}`);
19513
- lines.push(` ${yellow7("specialist:start")} before the agent session begins`);
19514
- lines.push(` ${yellow7("specialist:token")} on each streamed token (delta)`);
19515
- lines.push(` ${yellow7("specialist:done")} after successful completion`);
19516
- lines.push(` ${yellow7("specialist:error")} on failure or timeout`);
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`);
19517
20186
  lines.push("");
19518
20187
  lines.push(` Each event line in trace.jsonl:`);
19519
- lines.push(` ${dim10('{"t":"<ISO>","hook":"specialist:done","specialist":"code-review","durationMs":4120}')}`);
20188
+ lines.push(` ${dim9('{"t":"<ISO>","hook":"specialist:done","specialist":"code-review","durationMs":4120}')}`);
19520
20189
  lines.push("");
19521
20190
  lines.push(` Tail the trace file to observe all activity:`);
19522
20191
  lines.push(` ${cmd2("tail -f .specialists/trace.jsonl | jq .")}`);
@@ -19547,38 +20216,37 @@ async function run12() {
19547
20216
  lines.push(` ${bold7("Override model for a single run:")}`);
19548
20217
  lines.push(` ${cmd2('specialists run code-review --model anthropic/claude-opus-4-6 --prompt "..."')}`);
19549
20218
  lines.push("");
19550
- lines.push(dim10("─".repeat(62)));
19551
- lines.push(` ${dim10("specialists help")} command list ${dim10("specialists <cmd> --help")} per-command flags`);
19552
- lines.push(` ${dim10("specialists status")} health check ${dim10("specialists models")} available models`);
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`);
19553
20222
  lines.push("");
19554
20223
  console.log(lines.join(`
19555
20224
  `));
19556
20225
  }
19557
- var bold7 = (s) => `\x1B[1m${s}\x1B[0m`, dim10 = (s) => `\x1B[2m${s}\x1B[0m`, yellow7 = (s) => `\x1B[33m${s}\x1B[0m`, cyan6 = (s) => `\x1B[36m${s}\x1B[0m`, blue = (s) => `\x1B[34m${s}\x1B[0m`, green7 = (s) => `\x1B[32m${s}\x1B[0m`;
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`;
19558
20227
 
19559
20228
  // src/cli/doctor.ts
19560
20229
  var exports_doctor = {};
19561
20230
  __export(exports_doctor, {
19562
20231
  run: () => run13
19563
20232
  });
19564
- import { spawnSync as spawnSync5 } from "node:child_process";
19565
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync2 } from "node:fs";
19566
- import { homedir as homedir2 } from "node:os";
19567
- import { join as join12 } from "node:path";
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";
19568
20236
  function ok3(msg) {
19569
- console.log(` ${green8("✓")} ${msg}`);
20237
+ console.log(` ${green9("✓")} ${msg}`);
19570
20238
  }
19571
20239
  function warn2(msg) {
19572
- console.log(` ${yellow8("○")} ${msg}`);
20240
+ console.log(` ${yellow7("○")} ${msg}`);
19573
20241
  }
19574
20242
  function fail2(msg) {
19575
- console.log(` ${red5("✗")} ${msg}`);
20243
+ console.log(` ${red6("✗")} ${msg}`);
19576
20244
  }
19577
20245
  function fix(msg) {
19578
- console.log(` ${dim11("→ fix:")} ${yellow8(msg)}`);
20246
+ console.log(` ${dim10("→ fix:")} ${yellow7(msg)}`);
19579
20247
  }
19580
20248
  function hint(msg) {
19581
- console.log(` ${dim11(msg)}`);
20249
+ console.log(` ${dim10(msg)}`);
19582
20250
  }
19583
20251
  function section3(label) {
19584
20252
  const line = "─".repeat(Math.max(0, 38 - label.length));
@@ -19586,17 +20254,26 @@ function section3(label) {
19586
20254
  ${bold8(`── ${label} ${line}`)}`);
19587
20255
  }
19588
20256
  function sp(bin, args) {
19589
- const r = spawnSync5(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
20257
+ const r = spawnSync6(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
19590
20258
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
19591
20259
  }
19592
20260
  function isInstalled2(bin) {
19593
- return spawnSync5("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
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
+ }
19594
20271
  }
19595
20272
  function checkPi() {
19596
20273
  section3("pi (coding agent runtime)");
19597
20274
  if (!isInstalled2("pi")) {
19598
20275
  fail2("pi not installed");
19599
- fix("specialists install");
20276
+ fix("install pi first");
19600
20277
  return false;
19601
20278
  }
19602
20279
  const version2 = sp("pi", ["--version"]);
@@ -19609,75 +20286,95 @@ function checkPi() {
19609
20286
  fix("pi config (add at least one API key)");
19610
20287
  return false;
19611
20288
  }
19612
- ok3(`pi ${vStr} — ${providers.size} provider${providers.size > 1 ? "s" : ""} active ${dim11(`(${[...providers].join(", ")})`)} `);
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 || "")}`);
19613
20314
  return true;
19614
20315
  }
19615
20316
  function checkHooks() {
19616
- section3("Claude Code hooks (7 expected)");
20317
+ section3("Claude Code hooks (2 expected)");
19617
20318
  let allPresent = true;
19618
20319
  for (const name of HOOK_NAMES) {
19619
- const dest = join12(HOOKS_DIR, name);
19620
- if (!existsSync8(dest)) {
19621
- fail2(`${name} ${red5("missing")}`);
19622
- fix("specialists install (reinstalls all hooks)");
20320
+ const dest = join14(HOOKS_DIR, name);
20321
+ if (!existsSync10(dest)) {
20322
+ fail2(`${name} ${red6("missing")}`);
20323
+ fix("specialists install");
19623
20324
  allPresent = false;
19624
20325
  } else {
19625
20326
  ok3(name);
19626
20327
  }
19627
20328
  }
19628
- if (allPresent && existsSync8(SETTINGS_FILE)) {
19629
- try {
19630
- const settings = JSON.parse(readFileSync6(SETTINGS_FILE, "utf8"));
19631
- const hooks = settings.hooks ?? {};
19632
- const allEntries = [
19633
- ...hooks.PreToolUse ?? [],
19634
- ...hooks.PostToolUse ?? [],
19635
- ...hooks.Stop ?? [],
19636
- ...hooks.UserPromptSubmit ?? [],
19637
- ...hooks.SessionStart ?? []
19638
- ];
19639
- const wiredCommands = new Set(allEntries.flatMap((e) => (e.hooks ?? []).map((h) => h.command ?? "")));
19640
- const guardWired = [...wiredCommands].some((c) => c.includes("specialists-main-guard"));
19641
- if (!guardWired) {
19642
- warn2("specialists-main-guard not wired in settings.json");
19643
- fix("specialists install (rewires hooks in settings.json)");
19644
- allPresent = false;
19645
- } else {
19646
- hint(`Hooks wired in ${SETTINGS_FILE}`);
19647
- }
19648
- } catch {
19649
- warn2(`Could not parse ${SETTINGS_FILE}`);
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");
19650
20345
  allPresent = false;
19651
20346
  }
19652
20347
  }
20348
+ if (allPresent)
20349
+ hint(`Hooks wired in ${SETTINGS_FILE}`);
19653
20350
  return allPresent;
19654
20351
  }
19655
20352
  function checkMCP() {
19656
20353
  section3("MCP registration");
19657
- const check2 = sp("claude", ["mcp", "get", MCP_NAME]);
19658
- if (!check2.ok) {
19659
- fail2(`MCP server '${MCP_NAME}' not registered`);
19660
- fix(`specialists install (or: claude mcp add --scope user ${MCP_NAME} -- specialists)`);
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");
19661
20359
  return false;
19662
20360
  }
19663
- ok3(`MCP server '${MCP_NAME}' registered`);
20361
+ ok3(`MCP server 'specialists' registered in ${MCP_FILE2}`);
19664
20362
  return true;
19665
20363
  }
19666
20364
  function checkRuntimeDirs() {
19667
20365
  section3(".specialists/ runtime directories");
19668
- const cwd = process.cwd();
19669
- const rootDir = join12(cwd, ".specialists");
19670
- const jobsDir = join12(rootDir, "jobs");
19671
- const readyDir = join12(rootDir, "ready");
20366
+ const rootDir = join14(CWD, ".specialists");
20367
+ const jobsDir = join14(rootDir, "jobs");
20368
+ const readyDir = join14(rootDir, "ready");
19672
20369
  let allOk = true;
19673
- if (!existsSync8(rootDir)) {
20370
+ if (!existsSync10(rootDir)) {
19674
20371
  warn2(".specialists/ not found in current project");
19675
20372
  fix("specialists init");
19676
20373
  allOk = false;
19677
20374
  } else {
19678
20375
  ok3(".specialists/ present");
19679
20376
  for (const [subDir, label] of [[jobsDir, "jobs"], [readyDir, "ready"]]) {
19680
- if (!existsSync8(subDir)) {
20377
+ if (!existsSync10(subDir)) {
19681
20378
  warn2(`.specialists/${label}/ missing — auto-creating`);
19682
20379
  mkdirSync3(subDir, { recursive: true });
19683
20380
  ok3(`.specialists/${label}/ created`);
@@ -19690,14 +20387,14 @@ function checkRuntimeDirs() {
19690
20387
  }
19691
20388
  function checkZombieJobs() {
19692
20389
  section3("Background jobs");
19693
- const jobsDir = join12(process.cwd(), ".specialists", "jobs");
19694
- if (!existsSync8(jobsDir)) {
20390
+ const jobsDir = join14(CWD, ".specialists", "jobs");
20391
+ if (!existsSync10(jobsDir)) {
19695
20392
  hint("No .specialists/jobs/ — skipping");
19696
20393
  return true;
19697
20394
  }
19698
20395
  let entries;
19699
20396
  try {
19700
- entries = readdirSync2(jobsDir);
20397
+ entries = readdirSync3(jobsDir);
19701
20398
  } catch {
19702
20399
  entries = [];
19703
20400
  }
@@ -19709,8 +20406,8 @@ function checkZombieJobs() {
19709
20406
  let total = 0;
19710
20407
  let running = 0;
19711
20408
  for (const jobId of entries) {
19712
- const statusPath = join12(jobsDir, jobId, "status.json");
19713
- if (!existsSync8(statusPath))
20409
+ const statusPath = join14(jobsDir, jobId, "status.json");
20410
+ if (!existsSync10(statusPath))
19714
20411
  continue;
19715
20412
  try {
19716
20413
  const status = JSON.parse(readFileSync6(statusPath, "utf8"));
@@ -19723,11 +20420,11 @@ function checkZombieJobs() {
19723
20420
  process.kill(pid, 0);
19724
20421
  alive = true;
19725
20422
  } catch {}
19726
- if (alive) {
20423
+ if (alive)
19727
20424
  running++;
19728
- } else {
20425
+ else {
19729
20426
  zombies++;
19730
- warn2(`${jobId} ${yellow8("ZOMBIE")} ${dim11(`pid ${pid} not found, status=${status.status}`)}`);
20427
+ warn2(`${jobId} ${yellow7("ZOMBIE")} ${dim10(`pid ${pid} not found, status=${status.status}`)}`);
19731
20428
  fix(`Edit .specialists/jobs/${jobId}/status.json → set "status": "error"`);
19732
20429
  }
19733
20430
  }
@@ -19745,32 +20442,30 @@ async function run13() {
19745
20442
  ${bold8("specialists doctor")}
19746
20443
  `);
19747
20444
  const piOk = checkPi();
20445
+ const bdOk = checkBd();
20446
+ const xtOk = checkXt();
19748
20447
  const hooksOk = checkHooks();
19749
20448
  const mcpOk = checkMCP();
19750
20449
  const dirsOk = checkRuntimeDirs();
19751
20450
  const jobsOk = checkZombieJobs();
19752
- const allOk = piOk && hooksOk && mcpOk && dirsOk && jobsOk;
20451
+ const allOk = piOk && bdOk && xtOk && hooksOk && mcpOk && dirsOk && jobsOk;
19753
20452
  console.log("");
19754
20453
  if (allOk) {
19755
- console.log(` ${green8("✓")} ${bold8("All checks passed")} — specialists is healthy`);
20454
+ console.log(` ${green9("✓")} ${bold8("All checks passed")} — specialists is healthy`);
19756
20455
  } else {
19757
- console.log(` ${yellow8("○")} ${bold8("Some checks failed")} — follow the fix hints above`);
19758
- console.log(` ${dim11("specialists install fixes most issues automatically.")}`);
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.")}`);
19759
20458
  }
19760
20459
  console.log("");
19761
20460
  }
19762
- var bold8 = (s) => `\x1B[1m${s}\x1B[0m`, dim11 = (s) => `\x1B[2m${s}\x1B[0m`, green8 = (s) => `\x1B[32m${s}\x1B[0m`, yellow8 = (s) => `\x1B[33m${s}\x1B[0m`, red5 = (s) => `\x1B[31m${s}\x1B[0m`, HOME, CLAUDE_DIR, HOOKS_DIR, SETTINGS_FILE, MCP_NAME = "specialists", HOOK_NAMES;
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;
19763
20462
  var init_doctor = __esm(() => {
19764
- HOME = homedir2();
19765
- CLAUDE_DIR = join12(HOME, ".claude");
19766
- HOOKS_DIR = join12(CLAUDE_DIR, "hooks");
19767
- SETTINGS_FILE = join12(CLAUDE_DIR, "settings.json");
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");
19768
20468
  HOOK_NAMES = [
19769
- "specialists-main-guard.mjs",
19770
- "beads-edit-gate.mjs",
19771
- "beads-commit-gate.mjs",
19772
- "beads-stop-gate.mjs",
19773
- "beads-close-memory-prompt.mjs",
19774
20469
  "specialists-complete.mjs",
19775
20470
  "specialists-session-start.mjs"
19776
20471
  ];
@@ -19781,27 +20476,27 @@ var exports_setup = {};
19781
20476
  __export(exports_setup, {
19782
20477
  run: () => run14
19783
20478
  });
19784
- import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
20479
+ import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
19785
20480
  import { homedir as homedir3 } from "node:os";
19786
- import { join as join13, resolve } from "node:path";
20481
+ import { join as join15, resolve } from "node:path";
19787
20482
  function ok4(msg) {
19788
- console.log(` ${green9("✓")} ${msg}`);
20483
+ console.log(` ${green10("✓")} ${msg}`);
19789
20484
  }
19790
20485
  function skip2(msg) {
19791
- console.log(` ${yellow9("○")} ${msg}`);
20486
+ console.log(` ${yellow8("○")} ${msg}`);
19792
20487
  }
19793
20488
  function resolveTarget(target) {
19794
20489
  switch (target) {
19795
20490
  case "global":
19796
- return join13(homedir3(), ".claude", "CLAUDE.md");
20491
+ return join15(homedir3(), ".claude", "CLAUDE.md");
19797
20492
  case "agents":
19798
- return join13(process.cwd(), "AGENTS.md");
20493
+ return join15(process.cwd(), "AGENTS.md");
19799
20494
  case "project":
19800
20495
  default:
19801
- return join13(process.cwd(), "CLAUDE.md");
20496
+ return join15(process.cwd(), "CLAUDE.md");
19802
20497
  }
19803
20498
  }
19804
- function parseArgs5() {
20499
+ function parseArgs6() {
19805
20500
  const argv = process.argv.slice(3);
19806
20501
  let target = "project";
19807
20502
  let dryRun = false;
@@ -19827,29 +20522,29 @@ function parseArgs5() {
19827
20522
  return { target, dryRun };
19828
20523
  }
19829
20524
  async function run14() {
19830
- const { target, dryRun } = parseArgs5();
20525
+ const { target, dryRun } = parseArgs6();
19831
20526
  const filePath = resolve(resolveTarget(target));
19832
20527
  const label = target === "global" ? "~/.claude/CLAUDE.md" : filePath.replace(process.cwd() + "/", "");
19833
20528
  console.log(`
19834
20529
  ${bold9("specialists setup")}
19835
20530
  `);
19836
- console.log(` Target: ${yellow9(label)}${dryRun ? dim12(" (dry-run)") : ""}
20531
+ console.log(` Target: ${yellow8(label)}${dryRun ? dim11(" (dry-run)") : ""}
19837
20532
  `);
19838
- if (existsSync9(filePath)) {
20533
+ if (existsSync11(filePath)) {
19839
20534
  const existing = readFileSync7(filePath, "utf8");
19840
20535
  if (existing.includes(MARKER)) {
19841
20536
  skip2(`${label} already contains Specialists Workflow section`);
19842
20537
  console.log(`
19843
- ${dim12("To force-update, remove the ## Specialists Workflow section and re-run.")}
20538
+ ${dim11("To force-update, remove the ## Specialists Workflow section and re-run.")}
19844
20539
  `);
19845
20540
  return;
19846
20541
  }
19847
20542
  if (dryRun) {
19848
- console.log(dim12("─".repeat(60)));
19849
- console.log(dim12("Would append to existing file:"));
20543
+ console.log(dim11("─".repeat(60)));
20544
+ console.log(dim11("Would append to existing file:"));
19850
20545
  console.log("");
19851
20546
  console.log(WORKFLOW_BLOCK);
19852
- console.log(dim12("─".repeat(60)));
20547
+ console.log(dim11("─".repeat(60)));
19853
20548
  return;
19854
20549
  }
19855
20550
  const separator = existing.trimEnd().endsWith(`
@@ -19861,24 +20556,24 @@ ${bold9("specialists setup")}
19861
20556
  ok4(`Appended Specialists Workflow section to ${label}`);
19862
20557
  } else {
19863
20558
  if (dryRun) {
19864
- console.log(dim12("─".repeat(60)));
19865
- console.log(dim12(`Would create ${label}:`));
20559
+ console.log(dim11("─".repeat(60)));
20560
+ console.log(dim11(`Would create ${label}:`));
19866
20561
  console.log("");
19867
20562
  console.log(WORKFLOW_BLOCK);
19868
- console.log(dim12("─".repeat(60)));
20563
+ console.log(dim11("─".repeat(60)));
19869
20564
  return;
19870
20565
  }
19871
20566
  writeFileSync4(filePath, WORKFLOW_BLOCK, "utf8");
19872
20567
  ok4(`Created ${label} with Specialists Workflow section`);
19873
20568
  }
19874
20569
  console.log("");
19875
- console.log(` ${dim12("Next steps:")}`);
20570
+ console.log(` ${dim11("Next steps:")}`);
19876
20571
  console.log(` • Restart Claude Code to pick up the new context`);
19877
- console.log(` • Run ${yellow9("specialists list")} to see available specialists`);
19878
- console.log(` • Run ${yellow9("specialist_init")} in a new session to bootstrap 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`);
19879
20574
  console.log("");
19880
20575
  }
19881
- var bold9 = (s) => `\x1B[1m${s}\x1B[0m`, dim12 = (s) => `\x1B[2m${s}\x1B[0m`, green9 = (s) => `\x1B[32m${s}\x1B[0m`, yellow9 = (s) => `\x1B[33m${s}\x1B[0m`, MARKER = "## Specialists Workflow", WORKFLOW_BLOCK = `## Specialists Workflow
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
19882
20577
 
19883
20578
  > Injected by \`specialists setup\`. Keep this section — agents use it for context.
19884
20579
 
@@ -19948,59 +20643,92 @@ var exports_help = {};
19948
20643
  __export(exports_help, {
19949
20644
  run: () => run15
19950
20645
  });
19951
- function formatGroup(label, entries) {
19952
- const colWidth = Math.max(...entries.map(([cmd3]) => cmd3.length));
19953
- return [
19954
- "",
19955
- bold10(cyan7(label)),
19956
- ...entries.map(([cmd3, desc]) => ` ${cmd3.padEnd(colWidth)} ${dim13(desc)}`)
19957
- ];
20646
+ function formatCommands(entries) {
20647
+ const width = Math.max(...entries.map(([cmd3]) => cmd3.length));
20648
+ return entries.map(([cmd3, desc]) => ` ${cmd3.padEnd(width)} ${desc}`);
19958
20649
  }
19959
20650
  async function run15() {
19960
20651
  const lines = [
19961
20652
  "",
19962
- bold10("specialists <command> [options]"),
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 "..."',
19963
20669
  "",
19964
- dim13("One MCP server. Multiple AI backends. Intelligent orchestration."),
19965
- ...formatGroup("Setup", SETUP),
19966
- ...formatGroup("Discovery", DISCOVERY),
19967
- ...formatGroup("Running", RUNNING),
19968
- ...formatGroup("Jobs", JOBS),
19969
- ...formatGroup("Other", OTHER),
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",
19970
20675
  "",
19971
- dim13("Run 'specialists <command> --help' for command-specific options."),
19972
- dim13("Run 'specialists quickstart' for a full getting-started guide."),
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),
20684
+ "",
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>",
20692
+ "",
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."),
19973
20700
  ""
19974
20701
  ];
19975
20702
  console.log(lines.join(`
19976
20703
  `));
19977
20704
  }
19978
- var bold10 = (s) => `\x1B[1m${s}\x1B[0m`, dim13 = (s) => `\x1B[2m${s}\x1B[0m`, cyan7 = (s) => `\x1B[36m${s}\x1B[0m`, SETUP, DISCOVERY, RUNNING, JOBS, OTHER;
20705
+ var bold10 = (s) => `\x1B[1m${s}\x1B[0m`, dim12 = (s) => `\x1B[2m${s}\x1B[0m`, CORE_COMMANDS, EXTENDED_COMMANDS, WORKTREE_COMMANDS;
19979
20706
  var init_help = __esm(() => {
19980
- SETUP = [
19981
- ["install", "Full-stack installer: pi, beads, dolt, MCP registration, hooks"],
19982
- ["init", "Scaffold specialists/, .specialists/, AGENTS.md in current project"],
19983
- ["setup", "Inject workflow context block into CLAUDE.md or AGENTS.md"],
19984
- ["quickstart", "Rich getting-started guide with examples and YAML schema reference"],
19985
- ["doctor", "Health check: pi, hooks, MCP registration, dirs, zombie jobs"]
19986
- ];
19987
- DISCOVERY = [
19988
- ["list", "List available specialists with model and description"],
19989
- ["models", "List models available on pi, flagged with thinking/images support"],
19990
- ["status", "Show system health (pi, beads, MCP, jobs)"]
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"]
19991
20718
  ];
19992
- RUNNING = [
19993
- ["run", "Run a specialist with a prompt (--background for async)"],
19994
- ["edit", "Edit a specialist field (e.g. --model, --description)"]
19995
- ];
19996
- JOBS = [
19997
- ["feed", "Tail events for a background job (--follow to stream)"],
19998
- ["result", "Print result of a background job"],
19999
- ["stop", "Send SIGTERM to a running background job"]
20000
- ];
20001
- OTHER = [
20719
+ EXTENDED_COMMANDS = [
20720
+ ["edit", "Edit a specialist field such as model or description"],
20721
+ ["models", "List models available on pi"],
20002
20722
  ["version", "Print installed version"],
20003
- ["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"]
20004
20732
  ];
20005
20733
  });
20006
20734
 
@@ -26030,6 +26758,9 @@ class Protocol {
26030
26758
  }
26031
26759
  }
26032
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
+ }
26033
26764
  this._transport = transport;
26034
26765
  const _onclose = this.transport?.onclose;
26035
26766
  this._transport.onclose = () => {
@@ -26062,6 +26793,10 @@ class Protocol {
26062
26793
  this._progressHandlers.clear();
26063
26794
  this._taskProgressTokens.clear();
26064
26795
  this._pendingDebouncedNotifications.clear();
26796
+ for (const controller of this._requestHandlerAbortControllers.values()) {
26797
+ controller.abort();
26798
+ }
26799
+ this._requestHandlerAbortControllers.clear();
26065
26800
  const error2 = McpError.fromError(ErrorCode.ConnectionClosed, "Connection closed");
26066
26801
  this._transport = undefined;
26067
26802
  this.onclose?.();
@@ -26112,6 +26847,8 @@ class Protocol {
26112
26847
  sessionId: capturedTransport?.sessionId,
26113
26848
  _meta: request.params?._meta,
26114
26849
  sendNotification: async (notification) => {
26850
+ if (abortController.signal.aborted)
26851
+ return;
26115
26852
  const notificationOptions = { relatedRequestId: request.id };
26116
26853
  if (relatedTaskId) {
26117
26854
  notificationOptions.relatedTask = { taskId: relatedTaskId };
@@ -26119,6 +26856,9 @@ class Protocol {
26119
26856
  await this.notification(notification, notificationOptions);
26120
26857
  },
26121
26858
  sendRequest: async (r, resultSchema, options) => {
26859
+ if (abortController.signal.aborted) {
26860
+ throw new McpError(ErrorCode.ConnectionClosed, "Request was cancelled");
26861
+ }
26122
26862
  const requestOptions = { ...options, relatedRequestId: request.id };
26123
26863
  if (relatedTaskId && !requestOptions.relatedTask) {
26124
26864
  requestOptions.relatedTask = { taskId: relatedTaskId };
@@ -26732,6 +27472,62 @@ class ExperimentalServerTasks {
26732
27472
  requestStream(request, resultSchema, options) {
26733
27473
  return this._server.requestStream(request, resultSchema, options);
26734
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
+ }
26735
27531
  async getTask(taskId, options) {
26736
27532
  return this._server.getTask({ taskId }, options);
26737
27533
  }
@@ -27206,7 +28002,7 @@ class StdioServerTransport {
27206
28002
  }
27207
28003
 
27208
28004
  // src/server.ts
27209
- import { join as join3 } from "node:path";
28005
+ import { join as join4 } from "node:path";
27210
28006
 
27211
28007
  // src/constants.ts
27212
28008
  var LOG_PREFIX = "[specialists]";
@@ -27295,25 +28091,49 @@ function createListSpecialistsTool(loader) {
27295
28091
 
27296
28092
  // src/tools/specialist/use_specialist.tool.ts
27297
28093
  init_zod();
28094
+ init_beads();
27298
28095
  var useSpecialistSchema = exports_external.object({
27299
28096
  name: exports_external.string().describe("Specialist identifier (e.g. codebase-explorer)"),
27300
- 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"),
27301
28099
  variables: exports_external.record(exports_external.string()).optional().describe("Additional $variable substitutions"),
27302
28100
  backend_override: exports_external.string().optional().describe("Force a specific backend (gemini, qwen, anthropic)"),
27303
- 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"]
27304
28106
  });
27305
28107
  function createUseSpecialistTool(runner) {
27306
28108
  return {
27307
28109
  name: "use_specialist",
27308
- 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).",
27309
28111
  inputSchema: useSpecialistSchema,
27310
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
+ }
27311
28130
  return runner.run({
27312
28131
  name: input.name,
27313
- prompt: input.prompt,
27314
- variables: input.variables,
28132
+ prompt,
28133
+ variables,
27315
28134
  backendOverride: input.backend_override,
27316
- autonomyLevel: input.autonomy_level
28135
+ autonomyLevel: input.autonomy_level,
28136
+ inputBeadId: input.bead_id
27317
28137
  }, onProgress);
27318
28138
  }
27319
28139
  };
@@ -27419,14 +28239,14 @@ function createSpecialistStatusTool(loader, circuitBreaker) {
27419
28239
  async execute(_) {
27420
28240
  const list = await loader.list();
27421
28241
  const stalenessResults = await Promise.all(list.map((s) => checkStaleness(s)));
27422
- const { existsSync: existsSync2, readdirSync, readFileSync } = await import("node:fs");
27423
- const { join: join2 } = await import("node:path");
27424
- 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");
27425
28245
  const jobs = [];
27426
- if (existsSync2(jobsDir)) {
28246
+ if (existsSync3(jobsDir)) {
27427
28247
  for (const entry of readdirSync(jobsDir)) {
27428
- const statusPath = join2(jobsDir, entry, "status.json");
27429
- if (!existsSync2(statusPath))
28248
+ const statusPath = join3(jobsDir, entry, "status.json");
28249
+ if (!existsSync3(statusPath))
27430
28250
  continue;
27431
28251
  try {
27432
28252
  jobs.push(JSON.parse(readFileSync(statusPath, "utf-8")));
@@ -27638,13 +28458,13 @@ init_zod();
27638
28458
  // src/tools/specialist/specialist_init.tool.ts
27639
28459
  init_zod();
27640
28460
  import { spawnSync as spawnSync2 } from "node:child_process";
27641
- import { existsSync as existsSync2 } from "node:fs";
27642
- import { join as join2 } from "node:path";
28461
+ import { existsSync as existsSync3 } from "node:fs";
28462
+ import { join as join3 } from "node:path";
27643
28463
  var specialistInitSchema = objectType({});
27644
28464
  function createSpecialistInitTool(loader, deps) {
27645
28465
  const resolved = deps ?? {
27646
28466
  bdAvailable: () => spawnSync2("bd", ["--version"], { stdio: "ignore" }).status === 0,
27647
- beadsExists: () => existsSync2(join2(process.cwd(), ".beads")),
28467
+ beadsExists: () => existsSync3(join3(process.cwd(), ".beads")),
27648
28468
  bdInit: () => spawnSync2("bd", ["init"], { stdio: "ignore" })
27649
28469
  };
27650
28470
  return {
@@ -27679,7 +28499,7 @@ class SpecialistsServer {
27679
28499
  const circuitBreaker = new CircuitBreaker;
27680
28500
  const loader = new SpecialistLoader;
27681
28501
  const hooks = new HookEmitter({
27682
- tracePath: join3(process.cwd(), ".specialists", "trace.jsonl")
28502
+ tracePath: join4(process.cwd(), ".specialists", "trace.jsonl")
27683
28503
  });
27684
28504
  const beadsClient = new BeadsClient;
27685
28505
  const runner = new SpecialistRunner({ loader, hooks, circuitBreaker, beadsClient });
@@ -27786,8 +28606,8 @@ async function run16() {
27786
28606
  "",
27787
28607
  "Usage: specialists install",
27788
28608
  "",
27789
- "Full-stack setup: installs pi, beads, dolt, registers the MCP server,",
27790
- "and installs session hooks and skills for Claude Code.",
28609
+ "Project setup: checks pi/bd/xt prerequisites, registers the MCP server,",
28610
+ "and installs specialists-specific project hooks.",
27791
28611
  "",
27792
28612
  "No flags — just run it.",
27793
28613
  ""
@@ -27808,18 +28628,24 @@ async function run16() {
27808
28628
  "",
27809
28629
  "Usage: specialists list [options]",
27810
28630
  "",
27811
- "List available specialists across all scopes.",
28631
+ "List specialists in the current project.",
28632
+ "",
28633
+ "What it shows:",
28634
+ " - specialist name",
28635
+ " - model",
28636
+ " - short description",
27812
28637
  "",
27813
28638
  "Options:",
27814
- " --scope <project|user> Filter by scope",
27815
- " --category <name> Filter by category tag",
27816
- " --json Output as JSON array",
28639
+ " --category <name> Filter by category tag",
28640
+ " --json Output as JSON array",
27817
28641
  "",
27818
28642
  "Examples:",
27819
28643
  " specialists list",
27820
- " specialists list --scope project",
27821
28644
  " specialists list --category analysis",
27822
28645
  " specialists list --json",
28646
+ "",
28647
+ "Project model:",
28648
+ " Specialists are project-only. User-scope discovery is deprecated.",
27823
28649
  ""
27824
28650
  ].join(`
27825
28651
  `));
@@ -27849,15 +28675,27 @@ async function run16() {
27849
28675
  if (wantsHelp()) {
27850
28676
  console.log([
27851
28677
  "",
27852
- "Usage: specialists init",
28678
+ "Usage: specialists init [--force-workflow]",
27853
28679
  "",
27854
- "Initialize specialists in the current project:",
27855
- " • Creates specialists/ — put .specialist.yaml files here",
27856
- " • Creates .specialists/ — runtime data (gitignored)",
27857
- " • Adds .specialists/ to .gitignore",
27858
- " • Scaffolds AGENTS.md — context injected into Claude sessions",
28680
+ "Bootstrap a project for specialists. This is the sole onboarding command.",
27859
28681
  "",
27860
- "Safe to run on an existing project (skips already-present items).",
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.",
27861
28699
  ""
27862
28700
  ].join(`
27863
28701
  `));
@@ -27904,24 +28742,29 @@ async function run16() {
27904
28742
  "",
27905
28743
  "Usage: specialists run <name> [options]",
27906
28744
  "",
27907
- "Run a specialist. Streams output to stdout by default.",
27908
- "Reads prompt from stdin if --prompt is not provided.",
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 "..."',
27909
28750
  "",
27910
28751
  "Options:",
27911
- " --prompt <text> Prompt to send to the specialist (required unless piped)",
27912
- " --model <model> Override the model for this run only",
27913
- " --background Run async; prints job ID and exits immediately",
27914
- " --no-beads Skip creating a beads issue for this run",
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",
27915
28758
  "",
27916
28759
  "Examples:",
28760
+ " specialists run bug-hunt --bead unitAI-55d",
28761
+ " specialists run bug-hunt --bead unitAI-55d --context-depth 2 --background",
27917
28762
  ' specialists run code-review --prompt "Audit src/api.ts"',
27918
- ' specialists run code-review --prompt "..." --background',
27919
- " cat brief.md | specialists run deep-analysis",
27920
- ' specialists run code-review --model anthropic/claude-opus-4-6 --prompt "..."',
28763
+ " cat brief.md | specialists run report-generator",
27921
28764
  "",
27922
- "See also:",
27923
- " specialists feed --help (tail events for a background job)",
27924
- " specialists result --help (read background job output)",
28765
+ "Rules:",
28766
+ " Use --bead for tracked work.",
28767
+ " Use --prompt for quick ad-hoc work.",
27925
28768
  ""
27926
28769
  ].join(`
27927
28770
  `));
@@ -27936,11 +28779,17 @@ async function run16() {
27936
28779
  "",
27937
28780
  "Usage: specialists status [options]",
27938
28781
  "",
27939
- "Show system health: pi runtime, beads installation, MCP registration,",
27940
- "and all active background jobs.",
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",
27941
28790
  "",
27942
28791
  "Options:",
27943
- " --json Output as JSON",
28792
+ " --json Output machine-readable JSON",
27944
28793
  "",
27945
28794
  "Examples:",
27946
28795
  " specialists status",
@@ -27982,20 +28831,24 @@ async function run16() {
27982
28831
  console.log([
27983
28832
  "",
27984
28833
  "Usage: specialists feed <job-id> [options]",
27985
- " specialists feed --job <job-id> [options]",
28834
+ " specialists feed -f [--forever]",
28835
+ "",
28836
+ "Read background job events.",
27986
28837
  "",
27987
- "Print events emitted by a background job.",
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",
27988
28842
  "",
27989
28843
  "Options:",
27990
- " --follow, -f Stay open and stream new events as they arrive",
27991
- " (exits automatically when job completes)",
28844
+ " -f, --follow Follow live updates",
28845
+ " --forever Keep following in global mode even when all jobs complete",
27992
28846
  "",
27993
28847
  "Examples:",
27994
- " specialists feed job_a1b2c3d4",
27995
- " specialists feed job_a1b2c3d4 --follow",
27996
- " specialists feed --job job_a1b2c3d4 -f",
27997
- "",
27998
- "Event types: tool_use · tool_result · text · agent_end · error",
28848
+ " specialists feed 49adda",
28849
+ " specialists feed 49adda --follow",
28850
+ " specialists feed -f",
28851
+ " specialists feed -f --forever",
27999
28852
  ""
28000
28853
  ].join(`
28001
28854
  `));
@@ -28033,15 +28886,20 @@ async function run16() {
28033
28886
  "",
28034
28887
  "Usage: specialists doctor",
28035
28888
  "",
28036
- "Health check for your specialists installation:",
28037
- " 1. pi installed and has at least one active provider",
28038
- " 2. All 7 Claude Code hooks present and wired in settings.json",
28039
- " 3. MCP server registered (claude mcp get specialists)",
28040
- " 4. .specialists/jobs/ and .specialists/ready/ dirs exist",
28041
- " 5. No zombie jobs (running status but dead PID)",
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",
28042
28899
  "",
28043
- "Prints fix hints for each failure.",
28044
- "Auto-creates missing runtime directories.",
28900
+ "Behavior:",
28901
+ " - prints fix hints for failing checks",
28902
+ " - auto-creates missing runtime directories when possible",
28045
28903
  "",
28046
28904
  "Examples:",
28047
28905
  " specialists doctor",