@jaggerxtrm/specialists 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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;
@@ -17414,7 +17423,7 @@ async function parseSpecialist(yamlContent) {
17414
17423
  const raw = $parse(yamlContent);
17415
17424
  return SpecialistSchema.parseAsync(raw);
17416
17425
  }
17417
- var KebabCase, Semver, MetadataSchema, ExecutionSchema, PromptSchema2, SkillsSchema, CapabilitiesSchema, CommunicationSchema, ValidationSchema, SpecialistSchema;
17426
+ var KebabCase, Semver, MetadataSchema, ExecutionSchema, PromptSchema2, ScriptEntrySchema, SkillsSchema, CapabilitiesSchema, CommunicationSchema, ValidationSchema, SpecialistSchema;
17418
17427
  var init_schema = __esm(() => {
17419
17428
  init_zod();
17420
17429
  init_dist();
@@ -17438,6 +17447,7 @@ var init_schema = __esm(() => {
17438
17447
  stall_timeout_ms: numberType().optional(),
17439
17448
  response_format: enumType(["text", "json", "markdown"]).default("text"),
17440
17449
  permission_required: enumType(["READ_ONLY", "LOW", "MEDIUM", "HIGH"]).default("READ_ONLY"),
17450
+ thinking_level: enumType(["off", "minimal", "low", "medium", "high", "xhigh"]).optional(),
17441
17451
  preferred_profile: stringType().optional(),
17442
17452
  approval_mode: stringType().optional()
17443
17453
  });
@@ -17449,27 +17459,26 @@ var init_schema = __esm(() => {
17449
17459
  examples: arrayType(unknownType()).optional(),
17450
17460
  skill_inherit: stringType().optional()
17451
17461
  });
17462
+ ScriptEntrySchema = objectType({
17463
+ run: stringType().optional(),
17464
+ path: stringType().optional(),
17465
+ phase: enumType(["pre", "post"]),
17466
+ inject_output: booleanType().default(false)
17467
+ }).transform((s) => ({
17468
+ run: s.run ?? s.path ?? "",
17469
+ phase: s.phase,
17470
+ inject_output: s.inject_output
17471
+ }));
17452
17472
  SkillsSchema = objectType({
17453
- scripts: arrayType(objectType({
17454
- path: stringType(),
17455
- phase: enumType(["pre", "post"]),
17456
- inject_output: booleanType().default(false)
17457
- })).optional(),
17458
- references: arrayType(unknownType()).optional(),
17459
- tools: arrayType(stringType()).optional(),
17460
- paths: arrayType(stringType()).optional()
17473
+ paths: arrayType(stringType()).optional(),
17474
+ scripts: arrayType(ScriptEntrySchema).optional()
17461
17475
  }).optional();
17462
17476
  CapabilitiesSchema = objectType({
17463
- file_scope: arrayType(stringType()).optional(),
17464
- blocked_tools: arrayType(stringType()).optional(),
17465
- can_spawn: booleanType().optional(),
17466
- tools: arrayType(objectType({ name: stringType(), purpose: stringType() })).optional(),
17467
- diagnostic_scripts: arrayType(stringType()).optional()
17477
+ required_tools: arrayType(stringType()).optional(),
17478
+ external_commands: arrayType(stringType()).optional()
17468
17479
  }).optional();
17469
17480
  CommunicationSchema = objectType({
17470
- publishes: arrayType(stringType()).optional(),
17471
- subscribes: arrayType(stringType()).optional(),
17472
- output_to: stringType().optional()
17481
+ next_specialists: unionType([stringType(), arrayType(stringType())]).optional()
17473
17482
  }).optional();
17474
17483
  ValidationSchema = objectType({
17475
17484
  files_to_watch: arrayType(stringType()).optional(),
@@ -17485,6 +17494,7 @@ var init_schema = __esm(() => {
17485
17494
  capabilities: CapabilitiesSchema,
17486
17495
  communication: CommunicationSchema,
17487
17496
  validation: ValidationSchema,
17497
+ output_file: stringType().optional(),
17488
17498
  beads_integration: enumType(["auto", "always", "never"]).default("auto"),
17489
17499
  heartbeat: unknownType().optional()
17490
17500
  })
@@ -17494,7 +17504,6 @@ var init_schema = __esm(() => {
17494
17504
  // src/specialist/loader.ts
17495
17505
  import { readdir, readFile, stat } from "node:fs/promises";
17496
17506
  import { join } from "node:path";
17497
- import { homedir } from "node:os";
17498
17507
  import { existsSync } from "node:fs";
17499
17508
  async function checkStaleness(summary) {
17500
17509
  if (!summary.filestoWatch?.length || !summary.updated)
@@ -17518,18 +17527,15 @@ async function checkStaleness(summary) {
17518
17527
  class SpecialistLoader {
17519
17528
  cache = new Map;
17520
17529
  projectDir;
17521
- userDir;
17522
17530
  constructor(options = {}) {
17523
17531
  this.projectDir = options.projectDir ?? process.cwd();
17524
- this.userDir = options.userDir ?? join(homedir(), ".agents", "specialists");
17525
17532
  }
17526
17533
  getScanDirs() {
17527
- return [
17528
- { path: join(this.projectDir, "specialists"), scope: "project" },
17529
- { path: join(this.projectDir, ".claude", "specialists"), scope: "project" },
17530
- { path: join(this.projectDir, ".agent-forge", "specialists"), scope: "project" },
17531
- { path: this.userDir, scope: "user" }
17532
- ].filter((d) => existsSync(d.path));
17534
+ const dirs = [
17535
+ { path: join(this.projectDir, ".specialists", "user", "specialists"), scope: "user" },
17536
+ { path: join(this.projectDir, ".specialists", "default", "specialists"), scope: "default" }
17537
+ ];
17538
+ return dirs.filter((d) => existsSync(d.path));
17533
17539
  }
17534
17540
  async list(category) {
17535
17541
  const results = [];
@@ -17559,7 +17565,11 @@ class SpecialistLoader {
17559
17565
  filestoWatch: spec.specialist.validation?.files_to_watch,
17560
17566
  staleThresholdDays: spec.specialist.validation?.stale_threshold_days
17561
17567
  });
17562
- } catch {}
17568
+ } catch (e) {
17569
+ const reason = e instanceof Error ? e.message : String(e);
17570
+ process.stderr.write(`[specialists] skipping ${filePath}: ${reason}
17571
+ `);
17572
+ }
17563
17573
  }
17564
17574
  }
17565
17575
  return results;
@@ -17574,11 +17584,10 @@ class SpecialistLoader {
17574
17584
  const spec = await parseSpecialist(content);
17575
17585
  const rawPaths = spec.specialist.skills?.paths;
17576
17586
  if (rawPaths?.length) {
17577
- const home = homedir();
17578
17587
  const fileDir = dir.path;
17579
17588
  const resolved = rawPaths.map((p) => {
17580
17589
  if (p.startsWith("~/"))
17581
- return join(home, p.slice(2));
17590
+ return join(process.env.HOME || "", p.slice(2));
17582
17591
  if (p.startsWith("./"))
17583
17592
  return join(fileDir, p.slice(2));
17584
17593
  return p;
@@ -17640,14 +17649,17 @@ var init_backendMap = __esm(() => {
17640
17649
 
17641
17650
  // src/pi/session.ts
17642
17651
  import { spawn } from "node:child_process";
17652
+ import { existsSync as existsSync2 } from "node:fs";
17653
+ import { homedir } from "node:os";
17654
+ import { join as join2 } from "node:path";
17643
17655
  function mapPermissionToTools(level) {
17644
17656
  switch (level?.toUpperCase()) {
17645
17657
  case "READ_ONLY":
17646
- return "read,bash,grep,find,ls";
17647
- case "BASH_ONLY":
17648
- return "bash";
17658
+ return "read,grep,find,ls";
17649
17659
  case "LOW":
17660
+ return "read,bash,grep,find,ls";
17650
17661
  case "MEDIUM":
17662
+ return "read,bash,edit,grep,find,ls";
17651
17663
  case "HIGH":
17652
17664
  return "read,bash,edit,write,grep,find,ls";
17653
17665
  default:
@@ -17659,6 +17671,7 @@ class PiAgentSession {
17659
17671
  options;
17660
17672
  proc;
17661
17673
  _lastOutput = "";
17674
+ _donePromise;
17662
17675
  _doneResolve;
17663
17676
  _doneReject;
17664
17677
  _agentEndReceived = false;
@@ -17686,6 +17699,7 @@ class PiAgentSession {
17686
17699
  const args = [
17687
17700
  "--mode",
17688
17701
  "rpc",
17702
+ "--no-extensions",
17689
17703
  ...providerArgs,
17690
17704
  "--no-session",
17691
17705
  ...extraArgs
@@ -17693,11 +17707,28 @@ class PiAgentSession {
17693
17707
  const toolsFlag = mapPermissionToTools(this.options.permissionLevel);
17694
17708
  if (toolsFlag)
17695
17709
  args.push("--tools", toolsFlag);
17710
+ if (this.options.thinkingLevel) {
17711
+ args.push("--thinking", this.options.thinkingLevel);
17712
+ }
17713
+ for (const skillPath of this.options.skillPaths ?? []) {
17714
+ args.push("--skill", skillPath);
17715
+ }
17716
+ const piExtDir = join2(homedir(), ".pi", "agent", "extensions");
17717
+ const permLevel = (this.options.permissionLevel ?? "").toUpperCase();
17718
+ if (permLevel !== "READ_ONLY") {
17719
+ const qgPath = join2(piExtDir, "quality-gates");
17720
+ if (existsSync2(qgPath))
17721
+ args.push("-e", qgPath);
17722
+ }
17723
+ const ssPath = join2(piExtDir, "service-skills");
17724
+ if (existsSync2(ssPath))
17725
+ args.push("-e", ssPath);
17696
17726
  if (this.options.systemPrompt) {
17697
17727
  args.push("--append-system-prompt", this.options.systemPrompt);
17698
17728
  }
17699
17729
  this.proc = spawn("pi", args, {
17700
- stdio: ["pipe", "pipe", "inherit"]
17730
+ stdio: ["pipe", "pipe", "inherit"],
17731
+ cwd: this.options.cwd
17701
17732
  });
17702
17733
  const donePromise = new Promise((resolve, reject) => {
17703
17734
  this._doneResolve = resolve;
@@ -17759,34 +17790,11 @@ class PiAgentSession {
17759
17790
  this._lastOutput = last.content.filter((c) => c.type === "text").map((c) => c.text).join("");
17760
17791
  }
17761
17792
  this._agentEndReceived = true;
17762
- this.options.onEvent?.("done");
17793
+ this.options.onEvent?.("agent_end");
17763
17794
  this._doneResolve?.();
17764
17795
  return;
17765
17796
  }
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
17797
  if (type === "tool_execution_start") {
17789
- this.options.onToolStart?.(event.name ?? event.toolName ?? "tool");
17790
17798
  this.options.onEvent?.("tool_execution");
17791
17799
  return;
17792
17800
  }
@@ -17795,7 +17803,7 @@ class PiAgentSession {
17795
17803
  return;
17796
17804
  }
17797
17805
  if (type === "tool_execution_end") {
17798
- this.options.onToolEnd?.(event.name ?? event.toolName ?? "tool");
17806
+ this.options.onToolEnd?.(event.toolName ?? event.name ?? "tool");
17799
17807
  this.options.onEvent?.("tool_execution_end");
17800
17808
  return;
17801
17809
  }
@@ -17803,9 +17811,30 @@ class PiAgentSession {
17803
17811
  const ae = event.assistantMessageEvent;
17804
17812
  if (!ae)
17805
17813
  return;
17806
- if (ae.type === "text_delta" && ae.delta) {
17807
- this.options.onToken?.(ae.delta);
17808
- this.options.onEvent?.("text");
17814
+ switch (ae.type) {
17815
+ case "text_delta":
17816
+ if (ae.delta)
17817
+ this.options.onToken?.(ae.delta);
17818
+ this.options.onEvent?.("text");
17819
+ break;
17820
+ case "thinking_start":
17821
+ this.options.onEvent?.("thinking");
17822
+ break;
17823
+ case "thinking_delta":
17824
+ if (ae.delta)
17825
+ this.options.onThinking?.(ae.delta);
17826
+ this.options.onEvent?.("thinking");
17827
+ break;
17828
+ case "toolcall_start":
17829
+ this.options.onToolStart?.(ae.name ?? ae.toolName ?? "tool");
17830
+ this.options.onEvent?.("toolcall");
17831
+ break;
17832
+ case "toolcall_end":
17833
+ this.options.onEvent?.("toolcall");
17834
+ break;
17835
+ case "done":
17836
+ this.options.onEvent?.("message_done");
17837
+ break;
17809
17838
  }
17810
17839
  }
17811
17840
  }
@@ -17831,7 +17860,7 @@ class PiAgentSession {
17831
17860
  this.proc?.stdin?.write(msg);
17832
17861
  }
17833
17862
  async waitForDone(timeout) {
17834
- const donePromise = this._donePromise;
17863
+ const donePromise = this._donePromise ?? Promise.resolve();
17835
17864
  if (!timeout)
17836
17865
  return donePromise;
17837
17866
  return Promise.race([
@@ -17868,7 +17897,7 @@ class PiAgentSession {
17868
17897
  if (this._killed)
17869
17898
  return;
17870
17899
  this.proc?.stdin?.end();
17871
- await this._donePromise.catch(() => {});
17900
+ await this._donePromise?.catch(() => {});
17872
17901
  }
17873
17902
  kill() {
17874
17903
  if (this._killed)
@@ -17878,6 +17907,30 @@ class PiAgentSession {
17878
17907
  this.proc = undefined;
17879
17908
  this._doneReject?.(new SessionKilledError);
17880
17909
  }
17910
+ async steer(message) {
17911
+ if (this._killed || !this.proc?.stdin) {
17912
+ throw new Error("Session is not active");
17913
+ }
17914
+ const cmd = JSON.stringify({ type: "steer", message }) + `
17915
+ `;
17916
+ await new Promise((resolve, reject) => {
17917
+ this.proc.stdin.write(cmd, (err) => err ? reject(err) : resolve());
17918
+ });
17919
+ }
17920
+ async resume(task, timeout) {
17921
+ if (this._killed || !this.proc?.stdin) {
17922
+ throw new Error("Session is not active");
17923
+ }
17924
+ this._agentEndReceived = false;
17925
+ const donePromise = new Promise((resolve, reject) => {
17926
+ this._doneResolve = resolve;
17927
+ this._doneReject = reject;
17928
+ });
17929
+ donePromise.catch(() => {});
17930
+ this._donePromise = donePromise;
17931
+ await this.prompt(task);
17932
+ await this.waitForDone(timeout);
17933
+ }
17881
17934
  }
17882
17935
  var SessionKilledError;
17883
17936
  var init_session = __esm(() => {
@@ -17892,6 +17945,29 @@ var init_session = __esm(() => {
17892
17945
 
17893
17946
  // src/specialist/beads.ts
17894
17947
  import { spawnSync } from "node:child_process";
17948
+ function buildBeadContext(bead, completedBlockers = []) {
17949
+ const lines = [`# Task: ${bead.title}`];
17950
+ if (bead.description?.trim()) {
17951
+ lines.push(bead.description.trim());
17952
+ }
17953
+ if (bead.notes?.trim()) {
17954
+ lines.push("", "## Notes", bead.notes.trim());
17955
+ }
17956
+ if (completedBlockers.length > 0) {
17957
+ lines.push("", "## Context from completed dependencies:");
17958
+ for (const blocker of completedBlockers) {
17959
+ lines.push("", `### ${blocker.title} (${blocker.id})`);
17960
+ if (blocker.description?.trim()) {
17961
+ lines.push(blocker.description.trim());
17962
+ }
17963
+ if (blocker.notes?.trim()) {
17964
+ lines.push("", blocker.notes.trim());
17965
+ }
17966
+ }
17967
+ }
17968
+ return lines.join(`
17969
+ `).trim();
17970
+ }
17895
17971
 
17896
17972
  class BeadsClient {
17897
17973
  available;
@@ -17917,12 +17993,66 @@ class BeadsClient {
17917
17993
  const id = result.stdout?.trim();
17918
17994
  return id || null;
17919
17995
  }
17996
+ readBead(id) {
17997
+ if (!this.available || !id)
17998
+ return null;
17999
+ const result = spawnSync("bd", ["show", id, "--json"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 });
18000
+ if (result.error || result.status !== 0 || !result.stdout?.trim())
18001
+ return null;
18002
+ try {
18003
+ const parsed = JSON.parse(result.stdout);
18004
+ const bead = Array.isArray(parsed) ? parsed[0] : parsed;
18005
+ if (!bead || typeof bead !== "object" || typeof bead.id !== "string" || typeof bead.title !== "string")
18006
+ return null;
18007
+ return bead;
18008
+ } catch (err) {
18009
+ console.warn(`[specialists] readBead: JSON parse failed for id=${id}: ${err}`);
18010
+ return null;
18011
+ }
18012
+ }
18013
+ getCompletedBlockers(id, depth = 1) {
18014
+ if (!this.available || !id || depth < 1)
18015
+ return [];
18016
+ const result = spawnSync("bd", ["dep", "list", id, "--json"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 });
18017
+ if (result.error || result.status !== 0 || !result.stdout?.trim())
18018
+ return [];
18019
+ let deps;
18020
+ try {
18021
+ deps = JSON.parse(result.stdout);
18022
+ if (!Array.isArray(deps))
18023
+ return [];
18024
+ } catch {
18025
+ return [];
18026
+ }
18027
+ const blockers = deps.filter((d) => d.dependency_type === "blocks" && d.status === "closed");
18028
+ const records = [];
18029
+ for (const dep of blockers) {
18030
+ const record3 = this.readBead(dep.id);
18031
+ if (record3) {
18032
+ records.push(record3);
18033
+ if (depth > 1) {
18034
+ records.push(...this.getCompletedBlockers(dep.id, depth - 1));
18035
+ }
18036
+ }
18037
+ }
18038
+ return records;
18039
+ }
18040
+ addDependency(trackingBeadId, inputBeadId) {
18041
+ if (!this.available || !trackingBeadId || !inputBeadId)
18042
+ return;
18043
+ spawnSync("bd", ["dep", "add", trackingBeadId, inputBeadId], { stdio: "ignore" });
18044
+ }
17920
18045
  closeBead(id, status, durationMs, model) {
17921
18046
  if (!this.available || !id)
17922
18047
  return;
17923
18048
  const reason = `${status}, ${Math.round(durationMs)}ms, ${model}`;
17924
18049
  spawnSync("bd", ["close", id, "-r", reason], { stdio: "ignore" });
17925
18050
  }
18051
+ updateBeadNotes(id, notes) {
18052
+ if (!this.available || !id || !notes)
18053
+ return;
18054
+ spawnSync("bd", ["update", id, "--notes", notes], { stdio: "ignore" });
18055
+ }
17926
18056
  auditBead(id, toolName, model, exitCode) {
17927
18057
  if (!this.available || !id)
17928
18058
  return;
@@ -17954,14 +18084,16 @@ var init_beads = () => {};
17954
18084
  // src/specialist/runner.ts
17955
18085
  import { createHash } from "node:crypto";
17956
18086
  import { writeFile } from "node:fs/promises";
17957
- import { execSync } from "node:child_process";
17958
- import { basename } from "node:path";
17959
- function runScript(scriptPath) {
18087
+ import { execSync, spawnSync as spawnSync2 } from "node:child_process";
18088
+ import { existsSync as existsSync3, readFileSync } from "node:fs";
18089
+ import { basename, resolve } from "node:path";
18090
+ import { homedir as homedir2 } from "node:os";
18091
+ function runScript(run) {
17960
18092
  try {
17961
- const output = execSync(scriptPath, { encoding: "utf8", timeout: 30000 });
17962
- return { name: basename(scriptPath), output, exitCode: 0 };
18093
+ const output = execSync(run, { encoding: "utf8", timeout: 30000 });
18094
+ return { name: basename(run.split(" ")[0]), output, exitCode: 0 };
17963
18095
  } catch (e) {
17964
- return { name: basename(scriptPath), output: e.stdout ?? e.message ?? "", exitCode: e.status ?? 1 };
18096
+ return { name: basename(run.split(" ")[0]), output: e.stdout ?? e.message ?? "", exitCode: e.status ?? 1 };
17965
18097
  }
17966
18098
  }
17967
18099
  function formatScriptOutput(results) {
@@ -17979,6 +18111,77 @@ ${r.output.trim()}
17979
18111
  ${blocks}
17980
18112
  </pre_flight_context>`;
17981
18113
  }
18114
+ function resolvePath(p) {
18115
+ return p.startsWith("~/") ? resolve(homedir2(), p.slice(2)) : resolve(p);
18116
+ }
18117
+ function commandExists(cmd) {
18118
+ const result = spawnSync2("which", [cmd], { stdio: "ignore" });
18119
+ return result.status === 0;
18120
+ }
18121
+ function validateShebang(filePath, errors5) {
18122
+ try {
18123
+ const head = readFileSync(filePath, "utf-8").slice(0, 120);
18124
+ if (!head.startsWith("#!"))
18125
+ return;
18126
+ const shebang = head.split(`
18127
+ `)[0].toLowerCase();
18128
+ const typos = [
18129
+ [/pytho[^n]|pyton|pyhon/, "python"],
18130
+ [/nod[^e]b/, "node"],
18131
+ [/bsh$|bas$/, "bash"],
18132
+ [/rub[^y]/, "ruby"]
18133
+ ];
18134
+ for (const [pattern, correct] of typos) {
18135
+ if (pattern.test(shebang)) {
18136
+ errors5.push(` ✗ ${filePath}: shebang looks wrong — did you mean '${correct}'? (got: ${shebang})`);
18137
+ }
18138
+ }
18139
+ } catch {}
18140
+ }
18141
+ function validateBeforeRun(spec) {
18142
+ const errors5 = [];
18143
+ const warnings = [];
18144
+ for (const p of spec.specialist.skills?.paths ?? []) {
18145
+ const abs = resolvePath(p);
18146
+ if (!existsSync3(abs))
18147
+ warnings.push(` ⚠ skills.paths: file not found: ${p}`);
18148
+ }
18149
+ for (const script of spec.specialist.skills?.scripts ?? []) {
18150
+ const { run } = script;
18151
+ if (!run)
18152
+ continue;
18153
+ const isFilePath = run.startsWith("./") || run.startsWith("../") || run.startsWith("/") || run.startsWith("~/");
18154
+ if (isFilePath) {
18155
+ const abs = resolvePath(run);
18156
+ if (!existsSync3(abs)) {
18157
+ errors5.push(` ✗ skills.scripts: script not found: ${run}`);
18158
+ } else {
18159
+ validateShebang(abs, errors5);
18160
+ }
18161
+ } else {
18162
+ const binary = run.split(" ")[0];
18163
+ if (!commandExists(binary)) {
18164
+ errors5.push(` ✗ skills.scripts: command not found on PATH: ${binary}`);
18165
+ }
18166
+ }
18167
+ }
18168
+ for (const cmd of spec.specialist.capabilities?.external_commands ?? []) {
18169
+ if (!commandExists(cmd)) {
18170
+ errors5.push(` ✗ capabilities.external_commands: not found on PATH: ${cmd}`);
18171
+ }
18172
+ }
18173
+ if (warnings.length > 0) {
18174
+ process.stderr.write(`[specialists] pre-run warnings:
18175
+ ${warnings.join(`
18176
+ `)}
18177
+ `);
18178
+ }
18179
+ if (errors5.length > 0) {
18180
+ throw new Error(`Specialist pre-run validation failed:
18181
+ ${errors5.join(`
18182
+ `)}`);
18183
+ }
18184
+ }
17982
18185
 
17983
18186
  class SpecialistRunner {
17984
18187
  deps;
@@ -17987,12 +18190,12 @@ class SpecialistRunner {
17987
18190
  this.deps = deps;
17988
18191
  this.sessionFactory = deps.sessionFactory ?? PiAgentSession.create.bind(PiAgentSession);
17989
18192
  }
17990
- async run(options, onProgress, onEvent, onMeta, onKillRegistered, onBeadCreated) {
18193
+ async run(options, onProgress, onEvent, onMeta, onKillRegistered, onBeadCreated, onSteerRegistered, onResumeReady) {
17991
18194
  const { loader, hooks, circuitBreaker, beadsClient } = this.deps;
17992
18195
  const invocationId = crypto.randomUUID();
17993
18196
  const start = Date.now();
17994
18197
  const spec = await loader.get(options.name);
17995
- const { metadata, execution, prompt, communication } = spec.specialist;
18198
+ const { metadata, execution, prompt, output_file } = spec.specialist;
17996
18199
  const primaryModel = options.backendOverride ?? execution.model;
17997
18200
  const model = circuitBreaker.isAvailable(primaryModel) ? primaryModel : execution.fallback_model ?? primaryModel;
17998
18201
  const fallbackUsed = model !== primaryModel;
@@ -18003,10 +18206,18 @@ class SpecialistRunner {
18003
18206
  circuit_breaker_state: circuitBreaker.getState(model),
18004
18207
  scope: "project"
18005
18208
  });
18209
+ validateBeforeRun(spec);
18006
18210
  const preScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "pre") ?? [];
18007
- const preResults = preScripts.map((s) => runScript(s.path)).filter((_, i) => preScripts[i].inject_output);
18211
+ const preResults = preScripts.map((s) => runScript(s.run)).filter((_, i) => preScripts[i].inject_output);
18008
18212
  const preScriptOutput = formatScriptOutput(preResults);
18009
- const variables = { prompt: options.prompt, pre_script_output: preScriptOutput, ...options.variables };
18213
+ const beadVariables = options.inputBeadId ? { bead_context: options.prompt, bead_id: options.inputBeadId } : {};
18214
+ const variables = {
18215
+ prompt: options.prompt,
18216
+ cwd: process.cwd(),
18217
+ pre_script_output: preScriptOutput,
18218
+ ...options.variables ?? {},
18219
+ ...beadVariables
18220
+ };
18010
18221
  const renderedTask = renderTemplate(prompt.task_template, variables);
18011
18222
  const promptHash = createHash("sha256").update(renderedTask).digest("hex").slice(0, 16);
18012
18223
  await hooks.emit("post_render", invocationId, metadata.name, metadata.version, {
@@ -18015,40 +18226,32 @@ class SpecialistRunner {
18015
18226
  estimated_tokens: Math.ceil(renderedTask.length / 4),
18016
18227
  system_prompt_present: !!prompt.system
18017
18228
  });
18018
- const { readFile: readFile2 } = await import("node:fs/promises");
18019
- let agentsMd = prompt.system ?? "";
18020
- if (prompt.skill_inherit) {
18021
- const skillContent = await readFile2(prompt.skill_inherit, "utf-8").catch(() => "");
18022
- if (skillContent)
18023
- agentsMd += `
18024
-
18025
- ---
18026
- # Service Knowledge
18027
-
18028
- ${skillContent}`;
18029
- }
18030
- const skillPaths = spec.specialist.skills?.paths ?? [];
18031
- for (const skillPath of skillPaths) {
18032
- const skillContent = await readFile2(skillPath, "utf-8").catch(() => "");
18033
- if (skillContent)
18034
- agentsMd += `
18035
-
18036
- ---
18037
- # Skill: ${skillPath}
18038
-
18039
- ${skillContent}`;
18040
- }
18041
- if (spec.specialist.capabilities?.diagnostic_scripts?.length) {
18042
- agentsMd += `
18043
-
18044
- ---
18045
- # Diagnostic Scripts
18046
- You have access via Bash:
18047
- `;
18048
- for (const s of spec.specialist.capabilities.diagnostic_scripts) {
18049
- agentsMd += `- \`${s}\`
18050
- `;
18229
+ const agentsMd = prompt.system ?? "";
18230
+ const skillPaths = [];
18231
+ if (prompt.skill_inherit)
18232
+ skillPaths.push(prompt.skill_inherit);
18233
+ skillPaths.push(...spec.specialist.skills?.paths ?? []);
18234
+ if (skillPaths.length > 0 || preScripts.length > 0) {
18235
+ const line = "━".repeat(56);
18236
+ onProgress?.(`
18237
+ ${line}
18238
+ ◆ AUTO INJECTED
18239
+ `);
18240
+ if (skillPaths.length > 0) {
18241
+ onProgress?.(` skills (--skill):
18242
+ ${skillPaths.map((p) => ` • ${p}`).join(`
18243
+ `)}
18244
+ `);
18051
18245
  }
18246
+ if (preScripts.length > 0) {
18247
+ onProgress?.(` pre scripts/commands:
18248
+ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_output" : ""}`).join(`
18249
+ `)}
18250
+ `);
18251
+ }
18252
+ onProgress?.(`${line}
18253
+
18254
+ `);
18052
18255
  }
18053
18256
  const permissionLevel = options.autonomyLevel ?? execution.permission_required;
18054
18257
  await hooks.emit("pre_execute", invocationId, metadata.name, metadata.version, {
@@ -18059,19 +18262,28 @@ You have access via Bash:
18059
18262
  });
18060
18263
  const beadsIntegration = spec.specialist.beads_integration ?? "auto";
18061
18264
  let beadId;
18062
- if (beadsClient && shouldCreateBead(beadsIntegration, execution.permission_required)) {
18265
+ let ownsBead = false;
18266
+ if (options.inputBeadId) {
18267
+ beadId = options.inputBeadId;
18268
+ } else if (beadsClient && shouldCreateBead(beadsIntegration, execution.permission_required)) {
18063
18269
  beadId = beadsClient.createBead(metadata.name) ?? undefined;
18064
- if (beadId)
18270
+ if (beadId) {
18271
+ ownsBead = true;
18065
18272
  onBeadCreated?.(beadId);
18273
+ }
18066
18274
  }
18067
18275
  let output;
18068
18276
  let sessionBackend = model;
18069
18277
  let session;
18278
+ let keepAliveActive = false;
18070
18279
  try {
18071
18280
  session = await this.sessionFactory({
18072
18281
  model,
18073
18282
  systemPrompt: agentsMd || undefined,
18283
+ skillPaths: skillPaths.length > 0 ? skillPaths : undefined,
18284
+ thinkingLevel: execution.thinking_level,
18074
18285
  permissionLevel,
18286
+ cwd: process.cwd(),
18075
18287
  onToken: (delta) => onProgress?.(delta),
18076
18288
  onThinking: (delta) => onProgress?.(`\uD83D\uDCAD ${delta}`),
18077
18289
  onToolStart: (tool) => onProgress?.(`
@@ -18083,15 +18295,29 @@ You have access via Bash:
18083
18295
  });
18084
18296
  await session.start();
18085
18297
  onKillRegistered?.(session.kill.bind(session));
18298
+ onSteerRegistered?.((msg) => session.steer(msg));
18086
18299
  await session.prompt(renderedTask);
18087
18300
  await session.waitForDone(execution.timeout_ms);
18088
18301
  sessionBackend = session.meta.backend;
18089
18302
  output = await session.getLastOutput();
18090
18303
  sessionBackend = session.meta.backend;
18091
- await session.close();
18304
+ if (options.keepAlive && onResumeReady) {
18305
+ keepAliveActive = true;
18306
+ const resumeFn = async (msg) => {
18307
+ await session.resume(msg, execution.timeout_ms);
18308
+ return session.getLastOutput();
18309
+ };
18310
+ const closeFn = async () => {
18311
+ keepAliveActive = false;
18312
+ await session.close();
18313
+ };
18314
+ onResumeReady(resumeFn, closeFn);
18315
+ } else {
18316
+ await session.close();
18317
+ }
18092
18318
  const postScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "post") ?? [];
18093
18319
  for (const script of postScripts)
18094
- runScript(script.path);
18320
+ runScript(script.run);
18095
18321
  circuitBreaker.recordSuccess(model);
18096
18322
  } catch (err) {
18097
18323
  const isCancelled = err instanceof SessionKilledError;
@@ -18100,7 +18326,8 @@ You have access via Bash:
18100
18326
  }
18101
18327
  const beadStatus = isCancelled ? "CANCELLED" : "ERROR";
18102
18328
  if (beadId) {
18103
- beadsClient?.closeBead(beadId, beadStatus, Date.now() - start, model);
18329
+ if (ownsBead)
18330
+ beadsClient?.closeBead(beadId, beadStatus, Date.now() - start, model);
18104
18331
  beadsClient?.auditBead(beadId, metadata.name, model, 1);
18105
18332
  }
18106
18333
  await hooks.emit("post_execute", invocationId, metadata.name, metadata.version, {
@@ -18111,11 +18338,13 @@ You have access via Bash:
18111
18338
  });
18112
18339
  throw err;
18113
18340
  } finally {
18114
- session?.kill();
18341
+ if (!keepAliveActive) {
18342
+ session?.kill();
18343
+ }
18115
18344
  }
18116
18345
  const durationMs = Date.now() - start;
18117
- if (communication?.output_to) {
18118
- await writeFile(communication.output_to, output, "utf-8").catch(() => {});
18346
+ if (output_file) {
18347
+ await writeFile(output_file, output, "utf-8").catch(() => {});
18119
18348
  }
18120
18349
  await hooks.emit("post_execute", invocationId, metadata.name, metadata.version, {
18121
18350
  status: "COMPLETE",
@@ -18123,7 +18352,8 @@ You have access via Bash:
18123
18352
  output_valid: true
18124
18353
  });
18125
18354
  if (beadId) {
18126
- beadsClient?.closeBead(beadId, "COMPLETE", durationMs, model);
18355
+ if (ownsBead)
18356
+ beadsClient?.closeBead(beadId, "COMPLETE", durationMs, model);
18127
18357
  beadsClient?.auditBead(beadId, metadata.name, model, 0);
18128
18358
  }
18129
18359
  return {
@@ -18132,6 +18362,7 @@ You have access via Bash:
18132
18362
  model,
18133
18363
  durationMs,
18134
18364
  specialistVersion: metadata.version,
18365
+ promptHash,
18135
18366
  beadId
18136
18367
  };
18137
18368
  }
@@ -18147,7 +18378,7 @@ You have access via Bash:
18147
18378
  model: "?",
18148
18379
  specialistVersion
18149
18380
  });
18150
- this.run(options, (text) => registry2.appendOutput(jobId, text), (eventType) => registry2.setCurrentEvent(jobId, eventType), (meta) => registry2.setMeta(jobId, meta), (killFn) => registry2.setKillFn(jobId, killFn), (beadId) => registry2.setBeadId(jobId, beadId)).then((result) => registry2.complete(jobId, result)).catch((err) => registry2.fail(jobId, err));
18381
+ this.run(options, (text) => registry2.appendOutput(jobId, text), (eventType) => registry2.setCurrentEvent(jobId, eventType), (meta) => registry2.setMeta(jobId, meta), (killFn) => registry2.setKillFn(jobId, killFn), (beadId) => registry2.setBeadId(jobId, beadId), (steerFn) => registry2.setSteerFn(jobId, steerFn), (resumeFn, closeFn) => registry2.setResumeFn(jobId, resumeFn, closeFn)).then((result) => registry2.complete(jobId, result)).catch((err) => registry2.fail(jobId, err));
18151
18382
  return jobId;
18152
18383
  }
18153
18384
  }
@@ -18227,129 +18458,531 @@ class CircuitBreaker {
18227
18458
  }
18228
18459
  }
18229
18460
 
18230
- // src/cli/install.ts
18231
- var exports_install = {};
18232
- __export(exports_install, {
18233
- run: () => run
18234
- });
18235
- import { execFileSync } from "node:child_process";
18236
- import { fileURLToPath } from "node:url";
18237
- import { dirname as dirname2, join as join4 } from "node:path";
18238
- async function run() {
18239
- const installerPath = join4(dirname2(fileURLToPath(import.meta.url)), "..", "bin", "install.js");
18240
- execFileSync(process.execPath, [installerPath], { stdio: "inherit" });
18461
+ // src/specialist/timeline-events.ts
18462
+ function mapCallbackEventToTimelineEvent(callbackEvent, context) {
18463
+ const t = Date.now();
18464
+ switch (callbackEvent) {
18465
+ case "thinking":
18466
+ return { t, type: TIMELINE_EVENT_TYPES.THINKING };
18467
+ case "toolcall":
18468
+ return {
18469
+ t,
18470
+ type: TIMELINE_EVENT_TYPES.TOOL,
18471
+ tool: context.tool ?? "unknown",
18472
+ phase: "start",
18473
+ tool_call_id: context.toolCallId
18474
+ };
18475
+ case "tool_execution_end":
18476
+ return {
18477
+ t,
18478
+ type: TIMELINE_EVENT_TYPES.TOOL,
18479
+ tool: context.tool ?? "unknown",
18480
+ phase: "end",
18481
+ tool_call_id: context.toolCallId,
18482
+ is_error: context.isError
18483
+ };
18484
+ case "text":
18485
+ return { t, type: TIMELINE_EVENT_TYPES.TEXT };
18486
+ case "agent_end":
18487
+ case "message_done":
18488
+ case "done":
18489
+ return null;
18490
+ default:
18491
+ return null;
18492
+ }
18241
18493
  }
18242
- var init_install = () => {};
18243
-
18244
- // src/cli/version.ts
18245
- var exports_version = {};
18246
- __export(exports_version, {
18247
- run: () => run2
18248
- });
18249
- import { createRequire as createRequire2 } from "node:module";
18250
- async function run2() {
18251
- const req = createRequire2(import.meta.url);
18252
- const pkg = req("../package.json");
18253
- console.log(`${pkg.name} v${pkg.version}`);
18494
+ function createRunStartEvent(specialist, beadId) {
18495
+ return {
18496
+ t: Date.now(),
18497
+ type: TIMELINE_EVENT_TYPES.RUN_START,
18498
+ specialist,
18499
+ bead_id: beadId
18500
+ };
18254
18501
  }
18255
- var init_version = () => {};
18256
-
18257
- // src/cli/list.ts
18258
- var exports_list = {};
18259
- __export(exports_list, {
18260
- run: () => run3,
18261
- parseArgs: () => parseArgs,
18262
- ArgParseError: () => ArgParseError
18263
- });
18264
- function parseArgs(argv) {
18265
- const result = {};
18266
- for (let i = 0;i < argv.length; i++) {
18267
- const token = argv[i];
18268
- if (token === "--category") {
18269
- const value = argv[++i];
18270
- if (!value || value.startsWith("--")) {
18271
- throw new ArgParseError("--category requires a value");
18272
- }
18273
- result.category = value;
18274
- continue;
18275
- }
18276
- if (token === "--scope") {
18277
- const value = argv[++i];
18278
- if (value !== "project" && value !== "user") {
18279
- throw new ArgParseError(`--scope must be "project" or "user", got: "${value ?? ""}"`);
18280
- }
18281
- result.scope = value;
18282
- continue;
18283
- }
18284
- if (token === "--json") {
18285
- result.json = true;
18286
- continue;
18287
- }
18288
- }
18289
- return result;
18502
+ function createMetaEvent(model, backend) {
18503
+ return {
18504
+ t: Date.now(),
18505
+ type: TIMELINE_EVENT_TYPES.META,
18506
+ model,
18507
+ backend
18508
+ };
18290
18509
  }
18291
- async function run3() {
18292
- let args;
18510
+ function createRunCompleteEvent(status, elapsed_s, options) {
18511
+ return {
18512
+ t: Date.now(),
18513
+ type: TIMELINE_EVENT_TYPES.RUN_COMPLETE,
18514
+ status,
18515
+ elapsed_s,
18516
+ ...options
18517
+ };
18518
+ }
18519
+ function parseTimelineEvent(line) {
18293
18520
  try {
18294
- args = parseArgs(process.argv.slice(3));
18295
- } catch (err) {
18296
- if (err instanceof ArgParseError) {
18297
- console.error(`Error: ${err.message}`);
18298
- process.exit(1);
18521
+ const parsed = JSON.parse(line);
18522
+ if (!parsed || typeof parsed !== "object")
18523
+ return null;
18524
+ if (typeof parsed.t !== "number")
18525
+ return null;
18526
+ if (typeof parsed.type !== "string")
18527
+ return null;
18528
+ if (parsed.type === TIMELINE_EVENT_TYPES.DONE) {
18529
+ return {
18530
+ t: parsed.t,
18531
+ type: TIMELINE_EVENT_TYPES.DONE,
18532
+ elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
18533
+ };
18299
18534
  }
18300
- throw err;
18301
- }
18302
- const loader = new SpecialistLoader;
18303
- let specialists = await loader.list(args.category);
18304
- if (args.scope) {
18305
- specialists = specialists.filter((s) => s.scope === args.scope);
18306
- }
18307
- if (args.json) {
18308
- console.log(JSON.stringify(specialists, null, 2));
18309
- return;
18310
- }
18311
- if (specialists.length === 0) {
18312
- console.log("No specialists found.");
18313
- return;
18314
- }
18315
- const nameWidth = Math.max(...specialists.map((s) => s.name.length), 4);
18316
- console.log(`
18317
- ${bold(`Specialists (${specialists.length})`)}
18318
- `);
18319
- for (const s of specialists) {
18320
- const name = cyan(s.name.padEnd(nameWidth));
18321
- const scopeTag = yellow(`[${s.scope}]`);
18322
- const model = dim(s.model);
18323
- const desc = s.description.length > 80 ? s.description.slice(0, 79) + "…" : s.description;
18324
- console.log(` ${name} ${scopeTag} ${model}`);
18325
- console.log(` ${" ".repeat(nameWidth)} ${dim(desc)}`);
18326
- console.log();
18535
+ if (parsed.type === TIMELINE_EVENT_TYPES.AGENT_END) {
18536
+ return {
18537
+ t: parsed.t,
18538
+ type: TIMELINE_EVENT_TYPES.AGENT_END,
18539
+ elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
18540
+ };
18541
+ }
18542
+ const knownTypes = Object.values(TIMELINE_EVENT_TYPES).filter((type) => type !== TIMELINE_EVENT_TYPES.DONE && type !== TIMELINE_EVENT_TYPES.AGENT_END);
18543
+ if (!knownTypes.includes(parsed.type))
18544
+ return null;
18545
+ return parsed;
18546
+ } catch {
18547
+ return null;
18327
18548
  }
18328
18549
  }
18329
- var dim = (s) => `\x1B[2m${s}\x1B[0m`, bold = (s) => `\x1B[1m${s}\x1B[0m`, cyan = (s) => `\x1B[36m${s}\x1B[0m`, yellow = (s) => `\x1B[33m${s}\x1B[0m`, ArgParseError;
18330
- var init_list = __esm(() => {
18331
- init_loader();
18332
- ArgParseError = class ArgParseError extends Error {
18333
- constructor(message) {
18334
- super(message);
18335
- this.name = "ArgParseError";
18336
- }
18550
+ function isRunCompleteEvent(event) {
18551
+ return event.type === TIMELINE_EVENT_TYPES.RUN_COMPLETE;
18552
+ }
18553
+ function compareTimelineEvents(a, b) {
18554
+ return a.t - b.t;
18555
+ }
18556
+ var TIMELINE_EVENT_TYPES;
18557
+ var init_timeline_events = __esm(() => {
18558
+ TIMELINE_EVENT_TYPES = {
18559
+ RUN_START: "run_start",
18560
+ META: "meta",
18561
+ THINKING: "thinking",
18562
+ TOOL: "tool",
18563
+ TEXT: "text",
18564
+ RUN_COMPLETE: "run_complete",
18565
+ DONE: "done",
18566
+ AGENT_END: "agent_end"
18337
18567
  };
18338
18568
  });
18339
18569
 
18340
- // src/cli/models.ts
18341
- var exports_models = {};
18342
- __export(exports_models, {
18343
- run: () => run4
18344
- });
18345
- import { spawnSync as spawnSync3 } from "node:child_process";
18346
- function parsePiModels() {
18347
- const r = spawnSync3("pi", ["--list-models"], {
18348
- encoding: "utf8",
18349
- stdio: "pipe",
18350
- timeout: 8000
18351
- });
18352
- if (r.status !== 0 || r.error)
18570
+ // src/specialist/supervisor.ts
18571
+ import {
18572
+ closeSync,
18573
+ existsSync as existsSync4,
18574
+ mkdirSync,
18575
+ openSync,
18576
+ readdirSync,
18577
+ readFileSync as readFileSync2,
18578
+ renameSync,
18579
+ rmSync,
18580
+ statSync,
18581
+ writeFileSync,
18582
+ writeSync
18583
+ } from "node:fs";
18584
+ import { join as join3 } from "node:path";
18585
+ import { createInterface } from "node:readline";
18586
+ import { createReadStream } from "node:fs";
18587
+ import { spawnSync as spawnSync3, execFileSync } from "node:child_process";
18588
+ function getCurrentGitSha() {
18589
+ const result = spawnSync3("git", ["rev-parse", "HEAD"], {
18590
+ encoding: "utf-8",
18591
+ stdio: ["ignore", "pipe", "ignore"]
18592
+ });
18593
+ if (result.status !== 0)
18594
+ return;
18595
+ const sha = result.stdout?.trim();
18596
+ return sha || undefined;
18597
+ }
18598
+ function formatBeadNotes(result) {
18599
+ const metadata = [
18600
+ `prompt_hash=${result.promptHash}`,
18601
+ `git_sha=${getCurrentGitSha() ?? "unknown"}`,
18602
+ `elapsed_ms=${Math.round(result.durationMs)}`,
18603
+ `model=${result.model}`,
18604
+ `backend=${result.backend}`
18605
+ ].join(`
18606
+ `);
18607
+ return `${result.output}
18608
+
18609
+ ---
18610
+ ${metadata}`;
18611
+ }
18612
+
18613
+ class Supervisor {
18614
+ opts;
18615
+ constructor(opts) {
18616
+ this.opts = opts;
18617
+ }
18618
+ jobDir(id) {
18619
+ return join3(this.opts.jobsDir, id);
18620
+ }
18621
+ statusPath(id) {
18622
+ return join3(this.jobDir(id), "status.json");
18623
+ }
18624
+ resultPath(id) {
18625
+ return join3(this.jobDir(id), "result.txt");
18626
+ }
18627
+ eventsPath(id) {
18628
+ return join3(this.jobDir(id), "events.jsonl");
18629
+ }
18630
+ readyDir() {
18631
+ return join3(this.opts.jobsDir, "..", "ready");
18632
+ }
18633
+ readStatus(id) {
18634
+ const path = this.statusPath(id);
18635
+ if (!existsSync4(path))
18636
+ return null;
18637
+ try {
18638
+ return JSON.parse(readFileSync2(path, "utf-8"));
18639
+ } catch {
18640
+ return null;
18641
+ }
18642
+ }
18643
+ listJobs() {
18644
+ if (!existsSync4(this.opts.jobsDir))
18645
+ return [];
18646
+ const jobs = [];
18647
+ for (const entry of readdirSync(this.opts.jobsDir)) {
18648
+ const path = join3(this.opts.jobsDir, entry, "status.json");
18649
+ if (!existsSync4(path))
18650
+ continue;
18651
+ try {
18652
+ jobs.push(JSON.parse(readFileSync2(path, "utf-8")));
18653
+ } catch {}
18654
+ }
18655
+ return jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
18656
+ }
18657
+ writeStatusFile(id, data) {
18658
+ const path = this.statusPath(id);
18659
+ const tmp = path + ".tmp";
18660
+ writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
18661
+ renameSync(tmp, path);
18662
+ }
18663
+ updateStatus(id, updates) {
18664
+ const current = this.readStatus(id);
18665
+ if (!current)
18666
+ return;
18667
+ this.writeStatusFile(id, { ...current, ...updates });
18668
+ }
18669
+ gc() {
18670
+ if (!existsSync4(this.opts.jobsDir))
18671
+ return;
18672
+ const cutoff = Date.now() - JOB_TTL_DAYS * 86400000;
18673
+ for (const entry of readdirSync(this.opts.jobsDir)) {
18674
+ const dir = join3(this.opts.jobsDir, entry);
18675
+ try {
18676
+ const stat2 = statSync(dir);
18677
+ if (!stat2.isDirectory())
18678
+ continue;
18679
+ if (stat2.mtimeMs < cutoff)
18680
+ rmSync(dir, { recursive: true, force: true });
18681
+ } catch {}
18682
+ }
18683
+ }
18684
+ crashRecovery() {
18685
+ if (!existsSync4(this.opts.jobsDir))
18686
+ return;
18687
+ for (const entry of readdirSync(this.opts.jobsDir)) {
18688
+ const statusPath = join3(this.opts.jobsDir, entry, "status.json");
18689
+ if (!existsSync4(statusPath))
18690
+ continue;
18691
+ try {
18692
+ const s = JSON.parse(readFileSync2(statusPath, "utf-8"));
18693
+ if (s.status !== "running" && s.status !== "starting")
18694
+ continue;
18695
+ if (!s.pid)
18696
+ continue;
18697
+ try {
18698
+ process.kill(s.pid, 0);
18699
+ } catch {
18700
+ const tmp = statusPath + ".tmp";
18701
+ const updated = { ...s, status: "error", error: "Process crashed or was killed" };
18702
+ writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
18703
+ renameSync(tmp, statusPath);
18704
+ }
18705
+ } catch {}
18706
+ }
18707
+ }
18708
+ async run() {
18709
+ const { runner, runOptions, jobsDir } = this.opts;
18710
+ this.gc();
18711
+ this.crashRecovery();
18712
+ const id = crypto.randomUUID().slice(0, 6);
18713
+ const dir = this.jobDir(id);
18714
+ const startedAtMs = Date.now();
18715
+ mkdirSync(dir, { recursive: true });
18716
+ mkdirSync(this.readyDir(), { recursive: true });
18717
+ const initialStatus = {
18718
+ id,
18719
+ specialist: runOptions.name,
18720
+ status: "starting",
18721
+ started_at_ms: startedAtMs,
18722
+ pid: process.pid
18723
+ };
18724
+ this.writeStatusFile(id, initialStatus);
18725
+ const eventsFd = openSync(this.eventsPath(id), "a");
18726
+ const appendTimelineEvent = (event) => {
18727
+ try {
18728
+ writeSync(eventsFd, JSON.stringify(event) + `
18729
+ `);
18730
+ } catch {}
18731
+ };
18732
+ appendTimelineEvent(createRunStartEvent(runOptions.name));
18733
+ const fifoPath = join3(dir, "steer.pipe");
18734
+ try {
18735
+ execFileSync("mkfifo", [fifoPath]);
18736
+ this.updateStatus(id, { fifo_path: fifoPath });
18737
+ } catch {}
18738
+ let textLogged = false;
18739
+ let currentTool = "";
18740
+ let currentToolCallId = "";
18741
+ let killFn;
18742
+ let steerFn;
18743
+ let resumeFn;
18744
+ let closeFn;
18745
+ const sigtermHandler = () => killFn?.();
18746
+ process.once("SIGTERM", sigtermHandler);
18747
+ try {
18748
+ const result = await runner.run(runOptions, (delta) => {
18749
+ const toolMatch = delta.match(/⚙ (.+?)…/);
18750
+ if (toolMatch) {
18751
+ currentTool = toolMatch[1];
18752
+ this.updateStatus(id, { current_tool: currentTool });
18753
+ }
18754
+ }, (eventType) => {
18755
+ const now = Date.now();
18756
+ this.updateStatus(id, {
18757
+ status: "running",
18758
+ current_event: eventType,
18759
+ last_event_at_ms: now,
18760
+ elapsed_s: Math.round((now - startedAtMs) / 1000)
18761
+ });
18762
+ const timelineEvent = mapCallbackEventToTimelineEvent(eventType, {
18763
+ tool: currentTool,
18764
+ toolCallId: currentToolCallId || undefined
18765
+ });
18766
+ if (timelineEvent) {
18767
+ appendTimelineEvent(timelineEvent);
18768
+ } else if (eventType === "text" && !textLogged) {
18769
+ textLogged = true;
18770
+ appendTimelineEvent({ t: Date.now(), type: TIMELINE_EVENT_TYPES.TEXT });
18771
+ }
18772
+ }, (meta) => {
18773
+ this.updateStatus(id, { model: meta.model, backend: meta.backend });
18774
+ appendTimelineEvent(createMetaEvent(meta.model, meta.backend));
18775
+ }, (fn) => {
18776
+ killFn = fn;
18777
+ }, (beadId) => {
18778
+ this.updateStatus(id, { bead_id: beadId });
18779
+ }, (fn) => {
18780
+ steerFn = fn;
18781
+ if (existsSync4(fifoPath)) {
18782
+ const rl = createInterface({ input: createReadStream(fifoPath, { flags: "r+" }) });
18783
+ rl.on("line", (line) => {
18784
+ try {
18785
+ const parsed = JSON.parse(line);
18786
+ if (parsed?.type === "steer" && typeof parsed.message === "string") {
18787
+ steerFn?.(parsed.message).catch(() => {});
18788
+ } else if (parsed?.type === "prompt" && typeof parsed.message === "string") {
18789
+ if (resumeFn) {
18790
+ this.updateStatus(id, { status: "running", current_event: "starting" });
18791
+ resumeFn(parsed.message).then((output) => {
18792
+ writeFileSync(this.resultPath(id), output, "utf-8");
18793
+ this.updateStatus(id, {
18794
+ status: "waiting",
18795
+ current_event: "waiting",
18796
+ elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
18797
+ last_event_at_ms: Date.now()
18798
+ });
18799
+ }).catch((err) => {
18800
+ this.updateStatus(id, { status: "error", error: err?.message ?? String(err) });
18801
+ });
18802
+ }
18803
+ } else if (parsed?.type === "close") {
18804
+ closeFn?.().catch(() => {});
18805
+ }
18806
+ } catch {}
18807
+ });
18808
+ rl.on("error", () => {});
18809
+ }
18810
+ }, (rFn, cFn) => {
18811
+ resumeFn = rFn;
18812
+ closeFn = cFn;
18813
+ this.updateStatus(id, { status: "waiting", current_event: "waiting" });
18814
+ });
18815
+ const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
18816
+ writeFileSync(this.resultPath(id), result.output, "utf-8");
18817
+ if (result.beadId) {
18818
+ this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
18819
+ }
18820
+ this.updateStatus(id, {
18821
+ status: "done",
18822
+ elapsed_s: elapsed,
18823
+ last_event_at_ms: Date.now(),
18824
+ model: result.model,
18825
+ backend: result.backend,
18826
+ bead_id: result.beadId
18827
+ });
18828
+ appendTimelineEvent(createRunCompleteEvent("COMPLETE", elapsed, {
18829
+ model: result.model,
18830
+ backend: result.backend,
18831
+ bead_id: result.beadId
18832
+ }));
18833
+ writeFileSync(join3(this.readyDir(), id), "", "utf-8");
18834
+ return id;
18835
+ } catch (err) {
18836
+ const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
18837
+ const errorMsg = err?.message ?? String(err);
18838
+ this.updateStatus(id, {
18839
+ status: "error",
18840
+ elapsed_s: elapsed,
18841
+ error: errorMsg
18842
+ });
18843
+ appendTimelineEvent(createRunCompleteEvent("ERROR", elapsed, {
18844
+ error: errorMsg
18845
+ }));
18846
+ throw err;
18847
+ } finally {
18848
+ process.removeListener("SIGTERM", sigtermHandler);
18849
+ closeSync(eventsFd);
18850
+ try {
18851
+ if (existsSync4(fifoPath))
18852
+ rmSync(fifoPath);
18853
+ } catch {}
18854
+ }
18855
+ }
18856
+ }
18857
+ var JOB_TTL_DAYS;
18858
+ var init_supervisor = __esm(() => {
18859
+ init_timeline_events();
18860
+ JOB_TTL_DAYS = Number(process.env.SPECIALISTS_JOB_TTL_DAYS ?? 7);
18861
+ });
18862
+
18863
+ // src/cli/install.ts
18864
+ var exports_install = {};
18865
+ __export(exports_install, {
18866
+ run: () => run
18867
+ });
18868
+ import { execFileSync as execFileSync2 } from "node:child_process";
18869
+ import { fileURLToPath } from "node:url";
18870
+ import { dirname as dirname2, join as join8 } from "node:path";
18871
+ async function run() {
18872
+ const installerPath = join8(dirname2(fileURLToPath(import.meta.url)), "..", "bin", "install.js");
18873
+ execFileSync2(process.execPath, [installerPath], { stdio: "inherit" });
18874
+ }
18875
+ var init_install = () => {};
18876
+
18877
+ // src/cli/version.ts
18878
+ var exports_version = {};
18879
+ __export(exports_version, {
18880
+ run: () => run2
18881
+ });
18882
+ import { createRequire as createRequire2 } from "node:module";
18883
+ async function run2() {
18884
+ const req = createRequire2(import.meta.url);
18885
+ const pkg = req("../package.json");
18886
+ console.log(`${pkg.name} v${pkg.version}`);
18887
+ }
18888
+ var init_version = () => {};
18889
+
18890
+ // src/cli/list.ts
18891
+ var exports_list = {};
18892
+ __export(exports_list, {
18893
+ run: () => run3,
18894
+ parseArgs: () => parseArgs,
18895
+ ArgParseError: () => ArgParseError
18896
+ });
18897
+ function parseArgs(argv) {
18898
+ const result = {};
18899
+ for (let i = 0;i < argv.length; i++) {
18900
+ const token = argv[i];
18901
+ if (token === "--category") {
18902
+ const value = argv[++i];
18903
+ if (!value || value.startsWith("--")) {
18904
+ throw new ArgParseError("--category requires a value");
18905
+ }
18906
+ result.category = value;
18907
+ continue;
18908
+ }
18909
+ if (token === "--scope") {
18910
+ const value = argv[++i];
18911
+ if (value !== "default" && value !== "user") {
18912
+ throw new ArgParseError(`--scope must be "default" or "user", got: "${value ?? ""}"`);
18913
+ }
18914
+ result.scope = value;
18915
+ continue;
18916
+ }
18917
+ if (token === "--json") {
18918
+ result.json = true;
18919
+ continue;
18920
+ }
18921
+ }
18922
+ return result;
18923
+ }
18924
+ async function run3() {
18925
+ let args;
18926
+ try {
18927
+ args = parseArgs(process.argv.slice(3));
18928
+ } catch (err) {
18929
+ if (err instanceof ArgParseError) {
18930
+ console.error(`Error: ${err.message}`);
18931
+ process.exit(1);
18932
+ }
18933
+ throw err;
18934
+ }
18935
+ const loader = new SpecialistLoader;
18936
+ let specialists = await loader.list(args.category);
18937
+ if (args.scope) {
18938
+ specialists = specialists.filter((s) => s.scope === args.scope);
18939
+ }
18940
+ if (args.json) {
18941
+ console.log(JSON.stringify(specialists, null, 2));
18942
+ return;
18943
+ }
18944
+ if (specialists.length === 0) {
18945
+ console.log("No specialists found.");
18946
+ return;
18947
+ }
18948
+ const nameWidth = Math.max(...specialists.map((s) => s.name.length), 4);
18949
+ console.log(`
18950
+ ${bold(`Specialists (${specialists.length})`)}
18951
+ `);
18952
+ for (const s of specialists) {
18953
+ const name = cyan(s.name.padEnd(nameWidth));
18954
+ const scopeTag = s.scope === "default" ? green("[default]") : yellow("[user]");
18955
+ const model = dim(s.model);
18956
+ const desc = s.description.length > 80 ? s.description.slice(0, 79) + "…" : s.description;
18957
+ console.log(` ${name} ${scopeTag} ${model}`);
18958
+ console.log(` ${" ".repeat(nameWidth)} ${dim(desc)}`);
18959
+ console.log();
18960
+ }
18961
+ }
18962
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`, bold = (s) => `\x1B[1m${s}\x1B[0m`, cyan = (s) => `\x1B[36m${s}\x1B[0m`, green = (s) => `\x1B[32m${s}\x1B[0m`, yellow = (s) => `\x1B[33m${s}\x1B[0m`, ArgParseError;
18963
+ var init_list = __esm(() => {
18964
+ init_loader();
18965
+ ArgParseError = class ArgParseError extends Error {
18966
+ constructor(message) {
18967
+ super(message);
18968
+ this.name = "ArgParseError";
18969
+ }
18970
+ };
18971
+ });
18972
+
18973
+ // src/cli/models.ts
18974
+ var exports_models = {};
18975
+ __export(exports_models, {
18976
+ run: () => run4
18977
+ });
18978
+ import { spawnSync as spawnSync5 } from "node:child_process";
18979
+ function parsePiModels() {
18980
+ const r = spawnSync5("pi", ["--list-models"], {
18981
+ encoding: "utf8",
18982
+ stdio: "pipe",
18983
+ timeout: 8000
18984
+ });
18985
+ if (r.status !== 0 || r.error)
18353
18986
  return null;
18354
18987
  return r.stdout.split(`
18355
18988
  `).slice(1).map((line) => line.trim()).filter(Boolean).map((line) => {
@@ -18391,7 +19024,7 @@ async function run4() {
18391
19024
  }
18392
19025
  const allModels = parsePiModels();
18393
19026
  if (!allModels) {
18394
- console.error("pi not found or failed — run specialists install");
19027
+ console.error("pi not found or failed — install and configure pi first");
18395
19028
  process.exit(1);
18396
19029
  }
18397
19030
  let models = allModels;
@@ -18422,7 +19055,7 @@ ${bold2(`Models on pi`)} ${dim2(`(${total} total)`)}
18422
19055
  const key = `${m.provider}/${m.model}`;
18423
19056
  const inUse = usedBy.get(key);
18424
19057
  const flags = [
18425
- m.thinking ? green("thinking") : dim2("·"),
19058
+ m.thinking ? green2("thinking") : dim2("·"),
18426
19059
  m.images ? dim2("images") : ""
18427
19060
  ].filter(Boolean).join(" ");
18428
19061
  const ctx = dim2(`ctx ${m.context}`);
@@ -18437,7 +19070,7 @@ ${bold2(`Models on pi`)} ${dim2(`(${total} total)`)}
18437
19070
  console.log();
18438
19071
  }
18439
19072
  }
18440
- var bold2 = (s) => `\x1B[1m${s}\x1B[0m`, dim2 = (s) => `\x1B[2m${s}\x1B[0m`, cyan2 = (s) => `\x1B[36m${s}\x1B[0m`, yellow2 = (s) => `\x1B[33m${s}\x1B[0m`, green = (s) => `\x1B[32m${s}\x1B[0m`;
19073
+ var bold2 = (s) => `\x1B[1m${s}\x1B[0m`, dim2 = (s) => `\x1B[2m${s}\x1B[0m`, cyan2 = (s) => `\x1B[36m${s}\x1B[0m`, yellow2 = (s) => `\x1B[33m${s}\x1B[0m`, green2 = (s) => `\x1B[32m${s}\x1B[0m`;
18441
19074
  var init_models = __esm(() => {
18442
19075
  init_loader();
18443
19076
  });
@@ -18447,77 +19080,287 @@ var exports_init = {};
18447
19080
  __export(exports_init, {
18448
19081
  run: () => run5
18449
19082
  });
18450
- import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18451
- import { join as join5 } from "node:path";
19083
+ import { copyFileSync, cpSync, existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
19084
+ import { join as join9 } from "node:path";
19085
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
18452
19086
  function ok(msg) {
18453
- console.log(` ${green2("✓")} ${msg}`);
19087
+ console.log(` ${green3("✓")} ${msg}`);
19088
+ }
19089
+ function skip(msg) {
19090
+ console.log(` ${yellow3("○")} ${msg}`);
19091
+ }
19092
+ function loadJson(path, fallback) {
19093
+ if (!existsSync6(path))
19094
+ return structuredClone(fallback);
19095
+ try {
19096
+ return JSON.parse(readFileSync3(path, "utf-8"));
19097
+ } catch {
19098
+ return structuredClone(fallback);
19099
+ }
19100
+ }
19101
+ function saveJson(path, value) {
19102
+ writeFileSync4(path, JSON.stringify(value, null, 2) + `
19103
+ `, "utf-8");
19104
+ }
19105
+ function resolvePackagePath(relativePath) {
19106
+ const configPath = `config/${relativePath}`;
19107
+ let resolved = fileURLToPath2(new URL(`../${configPath}`, import.meta.url));
19108
+ if (existsSync6(resolved))
19109
+ return resolved;
19110
+ resolved = fileURLToPath2(new URL(`../../${configPath}`, import.meta.url));
19111
+ if (existsSync6(resolved))
19112
+ return resolved;
19113
+ return null;
19114
+ }
19115
+ function copyCanonicalSpecialists(cwd) {
19116
+ const sourceDir = resolvePackagePath("specialists");
19117
+ if (!sourceDir) {
19118
+ skip("no canonical specialists found in package");
19119
+ return;
19120
+ }
19121
+ const targetDir = join9(cwd, ".specialists", "default", "specialists");
19122
+ const files = readdirSync2(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
19123
+ if (files.length === 0) {
19124
+ skip("no specialist files found in package");
19125
+ return;
19126
+ }
19127
+ if (!existsSync6(targetDir)) {
19128
+ mkdirSync2(targetDir, { recursive: true });
19129
+ }
19130
+ let copied = 0;
19131
+ let skipped = 0;
19132
+ for (const file of files) {
19133
+ const src = join9(sourceDir, file);
19134
+ const dest = join9(targetDir, file);
19135
+ if (existsSync6(dest)) {
19136
+ skipped++;
19137
+ } else {
19138
+ copyFileSync(src, dest);
19139
+ copied++;
19140
+ }
19141
+ }
19142
+ if (copied > 0) {
19143
+ ok(`copied ${copied} canonical specialist${copied === 1 ? "" : "s"} to .specialists/default/specialists/`);
19144
+ }
19145
+ if (skipped > 0) {
19146
+ skip(`${skipped} specialist${skipped === 1 ? "" : "s"} already exist (not overwritten)`);
19147
+ }
19148
+ }
19149
+ function copyCanonicalHooks(cwd) {
19150
+ const sourceDir = resolvePackagePath("hooks");
19151
+ if (!sourceDir) {
19152
+ skip("no canonical hooks found in package");
19153
+ return;
19154
+ }
19155
+ const targetDir = join9(cwd, ".specialists", "default", "hooks");
19156
+ const hooks = readdirSync2(sourceDir).filter((f) => f.endsWith(".mjs"));
19157
+ if (hooks.length === 0) {
19158
+ skip("no hook files found in package");
19159
+ return;
19160
+ }
19161
+ if (!existsSync6(targetDir)) {
19162
+ mkdirSync2(targetDir, { recursive: true });
19163
+ }
19164
+ let copied = 0;
19165
+ let skipped = 0;
19166
+ for (const file of hooks) {
19167
+ const src = join9(sourceDir, file);
19168
+ const dest = join9(targetDir, file);
19169
+ if (existsSync6(dest)) {
19170
+ skipped++;
19171
+ } else {
19172
+ copyFileSync(src, dest);
19173
+ copied++;
19174
+ }
19175
+ }
19176
+ if (copied > 0) {
19177
+ ok(`copied ${copied} hook${copied === 1 ? "" : "s"} to .specialists/default/hooks/`);
19178
+ }
19179
+ if (skipped > 0) {
19180
+ skip(`${skipped} hook${skipped === 1 ? "" : "s"} already exist (not overwritten)`);
19181
+ }
19182
+ }
19183
+ function ensureProjectHooks(cwd) {
19184
+ const settingsPath = join9(cwd, ".claude", "settings.json");
19185
+ const settingsDir = join9(cwd, ".claude");
19186
+ if (!existsSync6(settingsDir)) {
19187
+ mkdirSync2(settingsDir, { recursive: true });
19188
+ }
19189
+ const settings = loadJson(settingsPath, {});
19190
+ let changed = false;
19191
+ function addHook(event, command) {
19192
+ const eventList = settings[event] ?? [];
19193
+ settings[event] = eventList;
19194
+ const alreadyWired = eventList.some((entry) => entry?.hooks?.some?.((h) => h?.command === command));
19195
+ if (!alreadyWired) {
19196
+ eventList.push({ matcher: "", hooks: [{ type: "command", command }] });
19197
+ changed = true;
19198
+ }
19199
+ }
19200
+ addHook("UserPromptSubmit", "node .specialists/default/hooks/specialists-complete.mjs");
19201
+ addHook("SessionStart", "node .specialists/default/hooks/specialists-session-start.mjs");
19202
+ if (changed) {
19203
+ saveJson(settingsPath, settings);
19204
+ ok("wired specialists hooks in .claude/settings.json");
19205
+ } else {
19206
+ skip(".claude/settings.json already has specialists hooks");
19207
+ }
19208
+ }
19209
+ function copyCanonicalSkills(cwd) {
19210
+ const sourceDir = resolvePackagePath("skills");
19211
+ if (!sourceDir) {
19212
+ skip("no canonical skills found in package");
19213
+ return;
19214
+ }
19215
+ const skills = readdirSync2(sourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
19216
+ if (skills.length === 0) {
19217
+ skip("no skill directories found in package");
19218
+ return;
19219
+ }
19220
+ const targetDir = join9(cwd, ".specialists", "default", "skills");
19221
+ if (!existsSync6(targetDir)) {
19222
+ mkdirSync2(targetDir, { recursive: true });
19223
+ }
19224
+ let copied = 0;
19225
+ let skipped = 0;
19226
+ for (const skill of skills) {
19227
+ const src = join9(sourceDir, skill);
19228
+ const dest = join9(targetDir, skill);
19229
+ if (existsSync6(dest)) {
19230
+ skipped++;
19231
+ } else {
19232
+ cpSync(src, dest, { recursive: true });
19233
+ copied++;
19234
+ }
19235
+ }
19236
+ if (copied > 0) {
19237
+ ok(`copied ${copied} skill${copied === 1 ? "" : "s"} to .specialists/default/skills/`);
19238
+ }
19239
+ if (skipped > 0) {
19240
+ skip(`${skipped} skill${skipped === 1 ? "" : "s"} already exist (not overwritten)`);
19241
+ }
19242
+ }
19243
+ function createUserDirs(cwd) {
19244
+ const userDirs = [
19245
+ join9(cwd, ".specialists", "user", "specialists"),
19246
+ join9(cwd, ".specialists", "user", "hooks"),
19247
+ join9(cwd, ".specialists", "user", "skills")
19248
+ ];
19249
+ let created = 0;
19250
+ for (const dir of userDirs) {
19251
+ if (!existsSync6(dir)) {
19252
+ mkdirSync2(dir, { recursive: true });
19253
+ created++;
19254
+ }
19255
+ }
19256
+ if (created > 0) {
19257
+ ok("created .specialists/user/ directories for custom assets");
19258
+ }
18454
19259
  }
18455
- function skip(msg) {
18456
- console.log(` ${yellow3("○")} ${msg}`);
19260
+ function createRuntimeDirs(cwd) {
19261
+ const runtimeDirs = [
19262
+ join9(cwd, ".specialists", "jobs"),
19263
+ join9(cwd, ".specialists", "ready")
19264
+ ];
19265
+ let created = 0;
19266
+ for (const dir of runtimeDirs) {
19267
+ if (!existsSync6(dir)) {
19268
+ mkdirSync2(dir, { recursive: true });
19269
+ created++;
19270
+ }
19271
+ }
19272
+ if (created > 0) {
19273
+ ok("created .specialists/jobs/ and .specialists/ready/");
19274
+ }
18457
19275
  }
18458
- async function run5() {
18459
- const cwd = process.cwd();
18460
- console.log(`
18461
- ${bold3("specialists init")}
19276
+ function ensureProjectMcp(cwd) {
19277
+ const mcpPath = join9(cwd, MCP_FILE);
19278
+ const mcp = loadJson(mcpPath, { mcpServers: {} });
19279
+ mcp.mcpServers ??= {};
19280
+ const existing = mcp.mcpServers[MCP_SERVER_NAME];
19281
+ if (existing && existing.command === MCP_SERVER_CONFIG.command && Array.isArray(existing.args) && existing.args.length === MCP_SERVER_CONFIG.args.length) {
19282
+ skip(".mcp.json already registers specialists");
19283
+ return;
19284
+ }
19285
+ mcp.mcpServers[MCP_SERVER_NAME] = MCP_SERVER_CONFIG;
19286
+ saveJson(mcpPath, mcp);
19287
+ ok("registered specialists in project .mcp.json");
19288
+ }
19289
+ function ensureGitignore(cwd) {
19290
+ const gitignorePath = join9(cwd, ".gitignore");
19291
+ const existing = existsSync6(gitignorePath) ? readFileSync3(gitignorePath, "utf-8") : "";
19292
+ let added = 0;
19293
+ const lines = existing.split(`
18462
19294
  `);
18463
- const specialistsDir = join5(cwd, "specialists");
18464
- if (existsSync3(specialistsDir)) {
18465
- skip("specialists/ already exists");
18466
- } else {
18467
- mkdirSync(specialistsDir, { recursive: true });
18468
- ok("created specialists/");
19295
+ for (const entry of GITIGNORE_ENTRIES) {
19296
+ if (!lines.includes(entry)) {
19297
+ lines.push(entry);
19298
+ added++;
19299
+ }
18469
19300
  }
18470
- const runtimeDir = join5(cwd, ".specialists");
18471
- if (existsSync3(runtimeDir)) {
18472
- skip(".specialists/ already exists");
18473
- } else {
18474
- mkdirSync(join5(runtimeDir, "jobs"), { recursive: true });
18475
- mkdirSync(join5(runtimeDir, "ready"), { recursive: true });
18476
- ok("created .specialists/ (jobs/, ready/)");
18477
- }
18478
- const gitignorePath = join5(cwd, ".gitignore");
18479
- if (existsSync3(gitignorePath)) {
18480
- const existing = readFileSync(gitignorePath, "utf-8");
18481
- if (existing.includes(GITIGNORE_ENTRY)) {
18482
- skip(".gitignore already has .specialists/ entry");
18483
- } else {
18484
- const separator = existing.endsWith(`
18485
- `) ? "" : `
18486
- `;
18487
- writeFileSync(gitignorePath, existing + separator + GITIGNORE_ENTRY + `
19301
+ if (added > 0) {
19302
+ writeFileSync4(gitignorePath, lines.join(`
19303
+ `) + `
18488
19304
  `, "utf-8");
18489
- ok("added .specialists/ to .gitignore");
18490
- }
19305
+ ok("added .specialists/jobs/ and .specialists/ready/ to .gitignore");
18491
19306
  } else {
18492
- writeFileSync(gitignorePath, GITIGNORE_ENTRY + `
18493
- `, "utf-8");
18494
- ok("created .gitignore with .specialists/ entry");
19307
+ skip(".gitignore already has runtime entries");
18495
19308
  }
18496
- const agentsPath = join5(cwd, "AGENTS.md");
18497
- if (existsSync3(agentsPath)) {
18498
- const existing = readFileSync(agentsPath, "utf-8");
19309
+ }
19310
+ function ensureAgentsMd(cwd) {
19311
+ const agentsPath = join9(cwd, "AGENTS.md");
19312
+ if (existsSync6(agentsPath)) {
19313
+ const existing = readFileSync3(agentsPath, "utf-8");
18499
19314
  if (existing.includes(AGENTS_MARKER)) {
18500
19315
  skip("AGENTS.md already has Specialists section");
18501
19316
  } else {
18502
- writeFileSync(agentsPath, existing.trimEnd() + `
19317
+ writeFileSync4(agentsPath, existing.trimEnd() + `
18503
19318
 
18504
19319
  ` + AGENTS_BLOCK, "utf-8");
18505
19320
  ok("appended Specialists section to AGENTS.md");
18506
19321
  }
18507
19322
  } else {
18508
- writeFileSync(agentsPath, AGENTS_BLOCK, "utf-8");
19323
+ writeFileSync4(agentsPath, AGENTS_BLOCK, "utf-8");
18509
19324
  ok("created AGENTS.md with Specialists section");
18510
19325
  }
19326
+ }
19327
+ async function run5() {
19328
+ const cwd = process.cwd();
19329
+ console.log(`
19330
+ ${bold3("specialists init")}
19331
+ `);
19332
+ copyCanonicalSpecialists(cwd);
19333
+ copyCanonicalHooks(cwd);
19334
+ copyCanonicalSkills(cwd);
19335
+ createUserDirs(cwd);
19336
+ createRuntimeDirs(cwd);
19337
+ ensureGitignore(cwd);
19338
+ ensureAgentsMd(cwd);
19339
+ ensureProjectMcp(cwd);
19340
+ ensureProjectHooks(cwd);
18511
19341
  console.log(`
18512
19342
  ${bold3("Done!")}
18513
19343
  `);
18514
- console.log(` ${dim3("Next steps:")}`);
18515
- console.log(` 1. Add your specialists to ${yellow3("specialists/")}`);
18516
- 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
19344
+ console.log(` ${dim3("Directory structure:")}`);
19345
+ console.log(` .specialists/`);
19346
+ console.log(` ├── default/ ${dim3("# canonical assets (from init)")}`);
19347
+ console.log(` │ ├── specialists/`);
19348
+ console.log(` │ ├── hooks/`);
19349
+ console.log(` │ └── skills/`);
19350
+ console.log(` ├── user/ ${dim3("# your custom additions")}`);
19351
+ console.log(` │ ├── specialists/`);
19352
+ console.log(` │ ├── hooks/`);
19353
+ console.log(` │ └── skills/`);
19354
+ console.log(` ├── jobs/ ${dim3("# runtime (gitignored)")}`);
19355
+ console.log(` └── ready/ ${dim3("# runtime (gitignored)")}`);
19356
+ console.log(`
19357
+ ${dim3("Next steps:")}`);
19358
+ console.log(` 1. Run ${yellow3("specialists list")} to see available specialists`);
19359
+ console.log(` 2. Add custom specialists to ${yellow3(".specialists/user/specialists/")}`);
19360
+ console.log(` 3. Restart Claude Code to pick up changes
18518
19361
  `);
18519
19362
  }
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/";
19363
+ var bold3 = (s) => `\x1B[1m${s}\x1B[0m`, green3 = (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_ENTRIES, MCP_FILE = ".mcp.json", MCP_SERVER_NAME = "specialists", MCP_SERVER_CONFIG;
18521
19364
  var init_init = __esm(() => {
18522
19365
  AGENTS_BLOCK = `
18523
19366
  ## Specialists
@@ -18526,7 +19369,11 @@ Call \`specialist_init\` at the start of every session to bootstrap context and
18526
19369
  see available specialists. Use \`use_specialist\` or \`start_specialist\` to
18527
19370
  delegate heavy tasks (code review, bug hunting, deep reasoning) to the right
18528
19371
  specialist without user intervention.
19372
+
19373
+ Add custom specialists to \`.specialists/user/specialists/\` to extend the defaults.
18529
19374
  `.trimStart();
19375
+ GITIGNORE_ENTRIES = [".specialists/jobs/", ".specialists/ready/"];
19376
+ MCP_SERVER_CONFIG = { command: "specialists", args: [] };
18530
19377
  });
18531
19378
 
18532
19379
  // src/cli/edit.ts
@@ -18534,11 +19381,11 @@ var exports_edit = {};
18534
19381
  __export(exports_edit, {
18535
19382
  run: () => run6
18536
19383
  });
18537
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
19384
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
18538
19385
  function parseArgs3(argv) {
18539
19386
  const name = argv[0];
18540
19387
  if (!name || name.startsWith("--")) {
18541
- console.error("Usage: specialists edit <name> --<field> <value> [--dry-run]");
19388
+ console.error("Usage: specialists|sp edit <name> --<field> <value> [--dry-run]");
18542
19389
  console.error(` Fields: ${Object.keys(FIELD_MAP).join(", ")}`);
18543
19390
  process.exit(1);
18544
19391
  }
@@ -18588,273 +19435,75 @@ function parseArgs3(argv) {
18588
19435
  function setIn(doc2, path, value) {
18589
19436
  let node = doc2;
18590
19437
  for (let i = 0;i < path.length - 1; i++) {
18591
- node = node.get(path[i], true);
18592
- }
18593
- const leaf = path[path.length - 1];
18594
- if (Array.isArray(value)) {
18595
- node.set(leaf, value);
18596
- } else {
18597
- node.set(leaf, value);
18598
- }
18599
- }
18600
- async function run6() {
18601
- const args = parseArgs3(process.argv.slice(3));
18602
- const { name, field, value, dryRun, scope } = args;
18603
- const loader = new SpecialistLoader;
18604
- const all = await loader.list();
18605
- const match = all.find((s) => s.name === name && (scope === undefined || s.scope === scope));
18606
- if (!match) {
18607
- const hint = scope ? ` (scope: ${scope})` : "";
18608
- console.error(`Error: specialist "${name}" not found${hint}`);
18609
- console.error(` Run ${yellow4("specialists list")} to see available specialists`);
18610
- process.exit(1);
18611
- }
18612
- const raw = readFileSync2(match.filePath, "utf-8");
18613
- const doc2 = $parseDocument(raw);
18614
- const yamlPath = FIELD_MAP[field];
18615
- let typedValue = value;
18616
- if (field === "timeout") {
18617
- typedValue = parseInt(value, 10);
18618
- } else if (field === "tags") {
18619
- typedValue = value.split(",").map((t) => t.trim()).filter(Boolean);
18620
- }
18621
- setIn(doc2, yamlPath, typedValue);
18622
- const updated = doc2.toString();
18623
- if (dryRun) {
18624
- console.log(`
18625
- ${bold4(`[dry-run] ${match.filePath}`)}
18626
- `);
18627
- console.log(dim4("--- current"));
18628
- console.log(dim4(`+++ updated`));
18629
- const oldLines = raw.split(`
18630
- `);
18631
- const newLines = updated.split(`
18632
- `);
18633
- newLines.forEach((line, i) => {
18634
- if (line !== oldLines[i]) {
18635
- if (oldLines[i] !== undefined)
18636
- console.log(dim4(`- ${oldLines[i]}`));
18637
- console.log(green3(`+ ${line}`));
18638
- }
18639
- });
18640
- console.log();
18641
- return;
18642
- }
18643
- writeFileSync2(match.filePath, updated, "utf-8");
18644
- const displayValue = field === "tags" ? `[${typedValue.join(", ")}]` : String(typedValue);
18645
- console.log(`${green3("✓")} ${bold4(name)}: ${yellow4(field)} = ${displayValue}` + dim4(` (${match.filePath})`));
18646
- }
18647
- var bold4 = (s) => `\x1B[1m${s}\x1B[0m`, green3 = (s) => `\x1B[32m${s}\x1B[0m`, yellow4 = (s) => `\x1B[33m${s}\x1B[0m`, dim4 = (s) => `\x1B[2m${s}\x1B[0m`, FIELD_MAP, VALID_PERMISSIONS;
18648
- var init_edit = __esm(() => {
18649
- init_dist();
18650
- init_loader();
18651
- FIELD_MAP = {
18652
- model: ["specialist", "execution", "model"],
18653
- "fallback-model": ["specialist", "execution", "fallback_model"],
18654
- description: ["specialist", "metadata", "description"],
18655
- permission: ["specialist", "execution", "permission_required"],
18656
- timeout: ["specialist", "execution", "timeout_ms"],
18657
- tags: ["specialist", "metadata", "tags"]
18658
- };
18659
- VALID_PERMISSIONS = ["READ_ONLY", "LOW", "MEDIUM", "HIGH"];
18660
- });
18661
-
18662
- // src/specialist/supervisor.ts
18663
- import {
18664
- closeSync,
18665
- existsSync as existsSync4,
18666
- mkdirSync as mkdirSync2,
18667
- openSync,
18668
- readdirSync,
18669
- readFileSync as readFileSync3,
18670
- renameSync,
18671
- rmSync,
18672
- statSync,
18673
- writeFileSync as writeFileSync3,
18674
- writeSync
18675
- } from "node:fs";
18676
- import { join as join6 } from "node:path";
18677
-
18678
- class Supervisor {
18679
- opts;
18680
- constructor(opts) {
18681
- this.opts = opts;
18682
- }
18683
- jobDir(id) {
18684
- return join6(this.opts.jobsDir, id);
18685
- }
18686
- statusPath(id) {
18687
- return join6(this.jobDir(id), "status.json");
18688
- }
18689
- resultPath(id) {
18690
- return join6(this.jobDir(id), "result.txt");
18691
- }
18692
- eventsPath(id) {
18693
- return join6(this.jobDir(id), "events.jsonl");
18694
- }
18695
- readyDir() {
18696
- return join6(this.opts.jobsDir, "..", "ready");
18697
- }
18698
- readStatus(id) {
18699
- const path = this.statusPath(id);
18700
- if (!existsSync4(path))
18701
- return null;
18702
- try {
18703
- return JSON.parse(readFileSync3(path, "utf-8"));
18704
- } catch {
18705
- return null;
18706
- }
18707
- }
18708
- listJobs() {
18709
- if (!existsSync4(this.opts.jobsDir))
18710
- return [];
18711
- const jobs = [];
18712
- for (const entry of readdirSync(this.opts.jobsDir)) {
18713
- const path = join6(this.opts.jobsDir, entry, "status.json");
18714
- if (!existsSync4(path))
18715
- continue;
18716
- try {
18717
- jobs.push(JSON.parse(readFileSync3(path, "utf-8")));
18718
- } catch {}
18719
- }
18720
- return jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
18721
- }
18722
- writeStatusFile(id, data) {
18723
- const path = this.statusPath(id);
18724
- const tmp = path + ".tmp";
18725
- writeFileSync3(tmp, JSON.stringify(data, null, 2), "utf-8");
18726
- renameSync(tmp, path);
18727
- }
18728
- updateStatus(id, updates) {
18729
- const current = this.readStatus(id);
18730
- if (!current)
18731
- return;
18732
- this.writeStatusFile(id, { ...current, ...updates });
18733
- }
18734
- gc() {
18735
- if (!existsSync4(this.opts.jobsDir))
18736
- return;
18737
- const cutoff = Date.now() - JOB_TTL_DAYS * 86400000;
18738
- for (const entry of readdirSync(this.opts.jobsDir)) {
18739
- const dir = join6(this.opts.jobsDir, entry);
18740
- try {
18741
- const stat2 = statSync(dir);
18742
- if (!stat2.isDirectory())
18743
- continue;
18744
- if (stat2.mtimeMs < cutoff)
18745
- rmSync(dir, { recursive: true, force: true });
18746
- } catch {}
18747
- }
18748
- }
18749
- crashRecovery() {
18750
- if (!existsSync4(this.opts.jobsDir))
18751
- return;
18752
- for (const entry of readdirSync(this.opts.jobsDir)) {
18753
- const statusPath = join6(this.opts.jobsDir, entry, "status.json");
18754
- if (!existsSync4(statusPath))
18755
- continue;
18756
- try {
18757
- const s = JSON.parse(readFileSync3(statusPath, "utf-8"));
18758
- if (s.status !== "running" && s.status !== "starting")
18759
- continue;
18760
- if (!s.pid)
18761
- continue;
18762
- try {
18763
- process.kill(s.pid, 0);
18764
- } catch {
18765
- const tmp = statusPath + ".tmp";
18766
- const updated = { ...s, status: "error", error: "Process crashed or was killed" };
18767
- writeFileSync3(tmp, JSON.stringify(updated, null, 2), "utf-8");
18768
- renameSync(tmp, statusPath);
18769
- }
18770
- } catch {}
18771
- }
18772
- }
18773
- async run() {
18774
- const { runner, runOptions, jobsDir } = this.opts;
18775
- this.gc();
18776
- this.crashRecovery();
18777
- const id = crypto.randomUUID().slice(0, 6);
18778
- const dir = this.jobDir(id);
18779
- const startedAtMs = Date.now();
18780
- mkdirSync2(dir, { recursive: true });
18781
- mkdirSync2(this.readyDir(), { recursive: true });
18782
- const initialStatus = {
18783
- id,
18784
- specialist: runOptions.name,
18785
- status: "starting",
18786
- started_at_ms: startedAtMs,
18787
- pid: process.pid
18788
- };
18789
- this.writeStatusFile(id, initialStatus);
18790
- const eventsFd = openSync(this.eventsPath(id), "a");
18791
- const appendEvent = (obj) => {
18792
- try {
18793
- writeSync(eventsFd, JSON.stringify({ t: Date.now(), ...obj }) + `
18794
- `);
18795
- } catch {}
18796
- };
18797
- let textLogged = false;
18798
- let currentTool = "";
18799
- try {
18800
- const result = await runner.run(runOptions, (delta) => {
18801
- const toolMatch = delta.match(/⚙ (.+?)…/);
18802
- if (toolMatch) {
18803
- currentTool = toolMatch[1];
18804
- this.updateStatus(id, { current_tool: currentTool });
18805
- }
18806
- }, (eventType) => {
18807
- const now = Date.now();
18808
- this.updateStatus(id, {
18809
- status: "running",
18810
- current_event: eventType,
18811
- last_event_at_ms: now,
18812
- elapsed_s: Math.round((now - startedAtMs) / 1000)
18813
- });
18814
- if (LOGGED_EVENTS.has(eventType)) {
18815
- const tool = eventType === "toolcall" || eventType === "tool_execution_end" ? currentTool : undefined;
18816
- appendEvent({ type: eventType, ...tool ? { tool } : {} });
18817
- } else if (eventType === "text" && !textLogged) {
18818
- textLogged = true;
18819
- appendEvent({ type: "text" });
18820
- }
18821
- }, (meta) => {
18822
- this.updateStatus(id, { model: meta.model, backend: meta.backend });
18823
- appendEvent({ type: "meta", model: meta.model, backend: meta.backend });
18824
- }, (_killFn) => {}, (beadId) => {
18825
- this.updateStatus(id, { bead_id: beadId });
18826
- });
18827
- const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
18828
- writeFileSync3(this.resultPath(id), result.output, "utf-8");
18829
- this.updateStatus(id, {
18830
- status: "done",
18831
- elapsed_s: elapsed,
18832
- last_event_at_ms: Date.now(),
18833
- model: result.model,
18834
- backend: result.backend,
18835
- bead_id: result.beadId
18836
- });
18837
- appendEvent({ type: "agent_end", elapsed_s: elapsed });
18838
- writeFileSync3(join6(this.readyDir(), id), "", "utf-8");
18839
- return id;
18840
- } catch (err) {
18841
- const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
18842
- this.updateStatus(id, {
18843
- status: "error",
18844
- elapsed_s: elapsed,
18845
- error: err?.message ?? String(err)
18846
- });
18847
- appendEvent({ type: "error", message: err?.message ?? String(err) });
18848
- throw err;
18849
- } finally {
18850
- closeSync(eventsFd);
18851
- }
19438
+ node = node.get(path[i], true);
19439
+ }
19440
+ const leaf = path[path.length - 1];
19441
+ if (Array.isArray(value)) {
19442
+ node.set(leaf, value);
19443
+ } else {
19444
+ node.set(leaf, value);
18852
19445
  }
18853
19446
  }
18854
- var JOB_TTL_DAYS, LOGGED_EVENTS;
18855
- var init_supervisor = __esm(() => {
18856
- JOB_TTL_DAYS = Number(process.env.SPECIALISTS_JOB_TTL_DAYS ?? 7);
18857
- LOGGED_EVENTS = new Set(["thinking", "toolcall", "tool_execution_end", "done"]);
19447
+ async function run6() {
19448
+ const args = parseArgs3(process.argv.slice(3));
19449
+ const { name, field, value, dryRun, scope } = args;
19450
+ const loader = new SpecialistLoader;
19451
+ const all = await loader.list();
19452
+ const match = all.find((s) => s.name === name && (scope === undefined || s.scope === scope));
19453
+ if (!match) {
19454
+ const hint = scope ? ` (scope: ${scope})` : "";
19455
+ console.error(`Error: specialist "${name}" not found${hint}`);
19456
+ console.error(` Run ${yellow4("specialists list")} to see available specialists`);
19457
+ process.exit(1);
19458
+ }
19459
+ const raw = readFileSync4(match.filePath, "utf-8");
19460
+ const doc2 = $parseDocument(raw);
19461
+ const yamlPath = FIELD_MAP[field];
19462
+ let typedValue = value;
19463
+ if (field === "timeout") {
19464
+ typedValue = parseInt(value, 10);
19465
+ } else if (field === "tags") {
19466
+ typedValue = value.split(",").map((t) => t.trim()).filter(Boolean);
19467
+ }
19468
+ setIn(doc2, yamlPath, typedValue);
19469
+ const updated = doc2.toString();
19470
+ if (dryRun) {
19471
+ console.log(`
19472
+ ${bold4(`[dry-run] ${match.filePath}`)}
19473
+ `);
19474
+ console.log(dim4("--- current"));
19475
+ console.log(dim4(`+++ updated`));
19476
+ const oldLines = raw.split(`
19477
+ `);
19478
+ const newLines = updated.split(`
19479
+ `);
19480
+ newLines.forEach((line, i) => {
19481
+ if (line !== oldLines[i]) {
19482
+ if (oldLines[i] !== undefined)
19483
+ console.log(dim4(`- ${oldLines[i]}`));
19484
+ console.log(green4(`+ ${line}`));
19485
+ }
19486
+ });
19487
+ console.log();
19488
+ return;
19489
+ }
19490
+ writeFileSync5(match.filePath, updated, "utf-8");
19491
+ const displayValue = field === "tags" ? `[${typedValue.join(", ")}]` : String(typedValue);
19492
+ console.log(`${green4("✓")} ${bold4(name)}: ${yellow4(field)} = ${displayValue}` + dim4(` (${match.filePath})`));
19493
+ }
19494
+ var bold4 = (s) => `\x1B[1m${s}\x1B[0m`, green4 = (s) => `\x1B[32m${s}\x1B[0m`, yellow4 = (s) => `\x1B[33m${s}\x1B[0m`, dim4 = (s) => `\x1B[2m${s}\x1B[0m`, FIELD_MAP, VALID_PERMISSIONS;
19495
+ var init_edit = __esm(() => {
19496
+ init_dist();
19497
+ init_loader();
19498
+ FIELD_MAP = {
19499
+ model: ["specialist", "execution", "model"],
19500
+ "fallback-model": ["specialist", "execution", "fallback_model"],
19501
+ description: ["specialist", "metadata", "description"],
19502
+ permission: ["specialist", "execution", "permission_required"],
19503
+ timeout: ["specialist", "execution", "timeout_ms"],
19504
+ tags: ["specialist", "metadata", "tags"]
19505
+ };
19506
+ VALID_PERMISSIONS = ["READ_ONLY", "LOW", "MEDIUM", "HIGH"];
18858
19507
  });
18859
19508
 
18860
19509
  // src/cli/run.ts
@@ -18862,27 +19511,40 @@ var exports_run = {};
18862
19511
  __export(exports_run, {
18863
19512
  run: () => run7
18864
19513
  });
18865
- import { join as join7 } from "node:path";
19514
+ import { spawn as spawn2 } from "node:child_process";
19515
+ import { join as join10 } from "node:path";
18866
19516
  async function parseArgs4(argv) {
18867
19517
  const name = argv[0];
18868
19518
  if (!name || name.startsWith("--")) {
18869
- console.error('Usage: specialists run <name> [--prompt "..."] [--model <model>] [--no-beads] [--background]');
19519
+ console.error('Usage: specialists|sp run <name> [--prompt "..."] [--bead <id>] [--context-depth <n>] [--model <model>] [--no-beads] [--background] [--follow]');
18870
19520
  process.exit(1);
18871
19521
  }
18872
19522
  let prompt = "";
19523
+ let beadId;
18873
19524
  let model;
18874
19525
  let noBeads = false;
18875
19526
  let background = false;
19527
+ let follow = false;
19528
+ let keepAlive = false;
19529
+ let contextDepth = 1;
18876
19530
  for (let i = 1;i < argv.length; i++) {
18877
19531
  const token = argv[i];
18878
19532
  if (token === "--prompt" && argv[i + 1]) {
18879
19533
  prompt = argv[++i];
18880
19534
  continue;
18881
19535
  }
19536
+ if (token === "--bead" && argv[i + 1]) {
19537
+ beadId = argv[++i];
19538
+ continue;
19539
+ }
18882
19540
  if (token === "--model" && argv[i + 1]) {
18883
19541
  model = argv[++i];
18884
19542
  continue;
18885
19543
  }
19544
+ if (token === "--context-depth" && argv[i + 1]) {
19545
+ contextDepth = parseInt(argv[++i], 10) || 0;
19546
+ continue;
19547
+ }
18886
19548
  if (token === "--no-beads") {
18887
19549
  noBeads = true;
18888
19550
  continue;
@@ -18891,61 +19553,130 @@ async function parseArgs4(argv) {
18891
19553
  background = true;
18892
19554
  continue;
18893
19555
  }
18894
- }
18895
- if (!prompt) {
18896
- if (process.stdin.isTTY) {
18897
- process.stderr.write(dim5("Prompt (Ctrl+D when done): "));
19556
+ if (token === "--follow") {
19557
+ follow = true;
19558
+ continue;
19559
+ }
19560
+ if (token === "--keep-alive") {
19561
+ keepAlive = true;
19562
+ continue;
18898
19563
  }
18899
- prompt = await new Promise((resolve) => {
19564
+ }
19565
+ if (prompt && beadId) {
19566
+ console.error("Error: use either --prompt or --bead, not both.");
19567
+ process.exit(1);
19568
+ }
19569
+ if (!prompt && !beadId && !process.stdin.isTTY) {
19570
+ prompt = await new Promise((resolve2) => {
18900
19571
  let buf = "";
18901
19572
  process.stdin.setEncoding("utf-8");
18902
19573
  process.stdin.on("data", (chunk) => {
18903
19574
  buf += chunk;
18904
19575
  });
18905
- process.stdin.on("end", () => resolve(buf.trim()));
19576
+ process.stdin.on("end", () => resolve2(buf.trim()));
18906
19577
  });
18907
19578
  }
18908
- return { name, prompt, model, noBeads, background };
19579
+ if (!prompt && !beadId) {
19580
+ console.error("Error: provide --prompt, pipe stdin, or use --bead <id>.");
19581
+ process.exit(1);
19582
+ }
19583
+ return { name, prompt, beadId, model, noBeads, background, follow, keepAlive, contextDepth };
18909
19584
  }
18910
19585
  async function run7() {
18911
19586
  const args = await parseArgs4(process.argv.slice(3));
18912
19587
  const loader = new SpecialistLoader;
18913
19588
  const circuitBreaker = new CircuitBreaker;
18914
- const hooks = new HookEmitter({ tracePath: join7(process.cwd(), ".specialists", "trace.jsonl") });
18915
- const beadsClient = args.noBeads ? null : new BeadsClient;
19589
+ const hooks = new HookEmitter({ tracePath: join10(process.cwd(), ".specialists", "trace.jsonl") });
19590
+ const beadsClient = args.noBeads ? undefined : new BeadsClient;
19591
+ const beadReader = beadsClient ?? new BeadsClient;
19592
+ let prompt = args.prompt;
19593
+ let variables;
19594
+ if (args.beadId) {
19595
+ const bead = beadReader.readBead(args.beadId);
19596
+ if (!bead) {
19597
+ throw new Error(`Unable to read bead '${args.beadId}' via bd show --json`);
19598
+ }
19599
+ const blockers = args.contextDepth > 0 ? beadReader.getCompletedBlockers(args.beadId, args.contextDepth) : [];
19600
+ if (blockers.length > 0) {
19601
+ process.stderr.write(dim5(`
19602
+ [context: ${blockers.length} completed dep${blockers.length > 1 ? "s" : ""} injected]
19603
+ `));
19604
+ }
19605
+ const beadContext = buildBeadContext(bead, blockers);
19606
+ prompt = beadContext;
19607
+ variables = {
19608
+ bead_context: beadContext,
19609
+ bead_id: args.beadId
19610
+ };
19611
+ }
18916
19612
  const runner = new SpecialistRunner({
18917
19613
  loader,
18918
19614
  hooks,
18919
19615
  circuitBreaker,
18920
- beadsClient: beadsClient ?? undefined
19616
+ beadsClient
18921
19617
  });
18922
- if (args.background) {
18923
- const jobsDir = join7(process.cwd(), ".specialists", "jobs");
19618
+ if (args.background || args.follow) {
19619
+ const jobsDir = join10(process.cwd(), ".specialists", "jobs");
18924
19620
  const supervisor = new Supervisor({
18925
19621
  runner,
18926
- runOptions: { name: args.name, prompt: args.prompt, backendOverride: args.model },
18927
- jobsDir
19622
+ runOptions: {
19623
+ name: args.name,
19624
+ prompt,
19625
+ variables,
19626
+ backendOverride: args.model,
19627
+ inputBeadId: args.beadId,
19628
+ keepAlive: args.keepAlive
19629
+ },
19630
+ jobsDir,
19631
+ beadsClient
18928
19632
  });
19633
+ let jobId;
18929
19634
  try {
18930
- const jobId = await supervisor.run();
18931
- process.stdout.write(`Job started: ${jobId}
19635
+ jobId = await supervisor.run();
19636
+ if (!args.follow) {
19637
+ process.stdout.write(`Job started: ${jobId}
18932
19638
  `);
19639
+ }
18933
19640
  } catch (err) {
18934
19641
  process.stderr.write(`Error: ${err?.message ?? err}
18935
19642
  `);
18936
19643
  process.exit(1);
18937
19644
  }
19645
+ if (args.follow) {
19646
+ await new Promise((resolve2, reject) => {
19647
+ const feed = spawn2("specialists", ["feed", "--job", jobId, "--follow"], {
19648
+ cwd: process.cwd(),
19649
+ stdio: "inherit"
19650
+ });
19651
+ feed.on("close", (code) => {
19652
+ if (code === 0) {
19653
+ resolve2();
19654
+ } else {
19655
+ reject(new Error(`Feed exited with code ${code}`));
19656
+ }
19657
+ });
19658
+ feed.on("error", (err) => {
19659
+ reject(err);
19660
+ });
19661
+ }).catch((err) => {
19662
+ process.stderr.write(`Error: ${err.message}
19663
+ `);
19664
+ process.exit(1);
19665
+ });
19666
+ }
18938
19667
  return;
18939
19668
  }
18940
19669
  process.stderr.write(`
18941
19670
  ${bold5(`Running ${cyan3(args.name)}`)}
18942
19671
 
18943
19672
  `);
18944
- let beadId;
19673
+ let trackingBeadId;
18945
19674
  const result = await runner.run({
18946
19675
  name: args.name,
18947
- prompt: args.prompt,
18948
- backendOverride: args.model
19676
+ prompt,
19677
+ variables,
19678
+ backendOverride: args.model,
19679
+ inputBeadId: args.beadId
18949
19680
  }, (delta) => process.stdout.write(delta), undefined, (meta) => process.stderr.write(dim5(`
18950
19681
  [${meta.backend} / ${meta.model}]
18951
19682
 
@@ -18958,10 +19689,10 @@ Interrupted.
18958
19689
  killFn();
18959
19690
  process.exit(130);
18960
19691
  });
18961
- }, (id) => {
18962
- beadId = id;
19692
+ }, (beadId) => {
19693
+ trackingBeadId = beadId;
18963
19694
  process.stderr.write(dim5(`
18964
- [bead: ${id}]
19695
+ [bead: ${beadId}]
18965
19696
  `));
18966
19697
  });
18967
19698
  if (result.output && !result.output.endsWith(`
@@ -18969,17 +19700,18 @@ Interrupted.
18969
19700
  process.stdout.write(`
18970
19701
  `);
18971
19702
  const secs = (result.durationMs / 1000).toFixed(1);
19703
+ const effectiveBeadId = args.beadId ?? trackingBeadId;
18972
19704
  const footer = [
18973
- beadId ? `bead ${beadId}` : "",
19705
+ effectiveBeadId ? `bead ${effectiveBeadId}` : "",
18974
19706
  `${secs}s`,
18975
19707
  dim5(result.model)
18976
19708
  ].filter(Boolean).join(" ");
18977
19709
  process.stderr.write(`
18978
- ${green4("✓")} ${footer}
19710
+ ${green5("✓")} ${footer}
18979
19711
 
18980
19712
  `);
18981
19713
  }
18982
- var bold5 = (s) => `\x1B[1m${s}\x1B[0m`, dim5 = (s) => `\x1B[2m${s}\x1B[0m`, green4 = (s) => `\x1B[32m${s}\x1B[0m`, cyan3 = (s) => `\x1B[36m${s}\x1B[0m`;
19714
+ var bold5 = (s) => `\x1B[1m${s}\x1B[0m`, dim5 = (s) => `\x1B[2m${s}\x1B[0m`, green5 = (s) => `\x1B[32m${s}\x1B[0m`, cyan3 = (s) => `\x1B[36m${s}\x1B[0m`;
18983
19715
  var init_run = __esm(() => {
18984
19716
  init_loader();
18985
19717
  init_runner();
@@ -18988,16 +19720,105 @@ var init_run = __esm(() => {
18988
19720
  init_supervisor();
18989
19721
  });
18990
19722
 
19723
+ // src/cli/format-helpers.ts
19724
+ function formatTime(t) {
19725
+ return new Date(t).toISOString().slice(11, 19);
19726
+ }
19727
+ function formatElapsed(seconds) {
19728
+ if (seconds < 60)
19729
+ return `${seconds}s`;
19730
+ const m = Math.floor(seconds / 60);
19731
+ const s = seconds % 60;
19732
+ return s > 0 ? `${m}m ${s}s` : `${m}m`;
19733
+ }
19734
+ function getEventLabel(type) {
19735
+ return EVENT_LABELS[type] ?? type.slice(0, 5).toUpperCase();
19736
+ }
19737
+
19738
+ class JobColorMap {
19739
+ colors = new Map;
19740
+ nextIdx = 0;
19741
+ getColor(jobId) {
19742
+ let color = this.colors.get(jobId);
19743
+ if (!color) {
19744
+ color = JOB_COLORS[this.nextIdx % JOB_COLORS.length];
19745
+ this.colors.set(jobId, color);
19746
+ this.nextIdx++;
19747
+ }
19748
+ return color;
19749
+ }
19750
+ get(jobId) {
19751
+ return this.getColor(jobId);
19752
+ }
19753
+ has(jobId) {
19754
+ return this.colors.has(jobId);
19755
+ }
19756
+ get size() {
19757
+ return this.colors.size;
19758
+ }
19759
+ }
19760
+ function formatEventLine(event, options) {
19761
+ const ts = dim6(formatTime(event.t));
19762
+ const label = options.colorize(bold6(getEventLabel(event.type).padEnd(5)));
19763
+ const prefix = `${options.colorize(`[${options.jobId}]`)} ${options.specialist}${options.beadId ? ` ${dim6(`[${options.beadId}]`)}` : ""}`;
19764
+ const detailParts = [];
19765
+ if (event.type === "meta") {
19766
+ detailParts.push(`model=${event.model}`);
19767
+ detailParts.push(`backend=${event.backend}`);
19768
+ } else if (event.type === "tool") {
19769
+ detailParts.push(`tool=${event.tool}`);
19770
+ detailParts.push(`phase=${event.phase}`);
19771
+ if (event.phase === "end") {
19772
+ detailParts.push(`ok=${event.is_error ? "false" : "true"}`);
19773
+ }
19774
+ } else if (event.type === "run_complete") {
19775
+ detailParts.push(`status=${event.status}`);
19776
+ detailParts.push(`elapsed=${formatElapsed(event.elapsed_s)}`);
19777
+ if (event.error) {
19778
+ detailParts.push(`error=${event.error}`);
19779
+ }
19780
+ } else if (event.type === "done" || event.type === "agent_end") {
19781
+ detailParts.push("status=COMPLETE");
19782
+ detailParts.push(`elapsed=${formatElapsed(event.elapsed_s ?? 0)}`);
19783
+ } else if (event.type === "run_start") {
19784
+ detailParts.push(`specialist=${event.specialist}`);
19785
+ if (event.bead_id) {
19786
+ detailParts.push(`bead=${event.bead_id}`);
19787
+ }
19788
+ } else if (event.type === "text") {
19789
+ detailParts.push("kind=assistant");
19790
+ } else if (event.type === "thinking") {
19791
+ detailParts.push("kind=model");
19792
+ }
19793
+ const detail = detailParts.length > 0 ? dim6(detailParts.join(" ")) : "";
19794
+ return `${ts} ${prefix} ${label}${detail ? ` ${detail}` : ""}`.trimEnd();
19795
+ }
19796
+ 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`, green6 = (s) => `\x1B[32m${s}\x1B[0m`, blue = (s) => `\x1B[34m${s}\x1B[0m`, magenta = (s) => `\x1B[35m${s}\x1B[0m`, JOB_COLORS, EVENT_LABELS;
19797
+ var init_format_helpers = __esm(() => {
19798
+ JOB_COLORS = [cyan4, yellow5, magenta, green6, blue, red];
19799
+ EVENT_LABELS = {
19800
+ run_start: "START",
19801
+ meta: "META",
19802
+ thinking: "THINK",
19803
+ tool: "TOOL",
19804
+ text: "TEXT",
19805
+ run_complete: "DONE",
19806
+ done: "DONE",
19807
+ agent_end: "DONE",
19808
+ error: "ERR"
19809
+ };
19810
+ });
19811
+
18991
19812
  // src/cli/status.ts
18992
19813
  var exports_status = {};
18993
19814
  __export(exports_status, {
18994
19815
  run: () => run8
18995
19816
  });
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";
19817
+ import { spawnSync as spawnSync6 } from "node:child_process";
19818
+ import { existsSync as existsSync7 } from "node:fs";
19819
+ import { join as join11 } from "node:path";
18999
19820
  function ok2(msg) {
19000
- console.log(` ${green5("✓")} ${msg}`);
19821
+ console.log(` ${green6("✓")} ${msg}`);
19001
19822
  }
19002
19823
  function warn(msg) {
19003
19824
  console.log(` ${yellow5("○")} ${msg}`);
@@ -19014,7 +19835,7 @@ function section(label) {
19014
19835
  ${bold6(`── ${label} ${line}`)}`);
19015
19836
  }
19016
19837
  function cmd(bin, args) {
19017
- const r = spawnSync4(bin, args, {
19838
+ const r = spawnSync6(bin, args, {
19018
19839
  encoding: "utf8",
19019
19840
  stdio: "pipe",
19020
19841
  timeout: 5000
@@ -19022,9 +19843,9 @@ function cmd(bin, args) {
19022
19843
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
19023
19844
  }
19024
19845
  function isInstalled(bin) {
19025
- return spawnSync4("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
19846
+ return spawnSync6("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
19026
19847
  }
19027
- function formatElapsed(s) {
19848
+ function formatElapsed2(s) {
19028
19849
  if (s.elapsed_s === undefined)
19029
19850
  return "...";
19030
19851
  const m = Math.floor(s.elapsed_s / 60);
@@ -19036,7 +19857,7 @@ function statusColor(status) {
19036
19857
  case "running":
19037
19858
  return cyan4(status);
19038
19859
  case "done":
19039
- return green5(status);
19860
+ return green6(status);
19040
19861
  case "error":
19041
19862
  return red(status);
19042
19863
  case "starting":
@@ -19057,11 +19878,11 @@ async function run8() {
19057
19878
  `).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean)) : new Set;
19058
19879
  const bdInstalled = isInstalled("bd");
19059
19880
  const bdVersion = bdInstalled ? cmd("bd", ["--version"]) : null;
19060
- const beadsPresent = existsSync5(join8(process.cwd(), ".beads"));
19881
+ const beadsPresent = existsSync7(join11(process.cwd(), ".beads"));
19061
19882
  const specialistsBin = cmd("which", ["specialists"]);
19062
- const jobsDir = join8(process.cwd(), ".specialists", "jobs");
19883
+ const jobsDir = join11(process.cwd(), ".specialists", "jobs");
19063
19884
  let jobs = [];
19064
- if (existsSync5(jobsDir)) {
19885
+ if (existsSync7(jobsDir)) {
19065
19886
  const supervisor = new Supervisor({
19066
19887
  runner: null,
19067
19888
  runOptions: null,
@@ -19135,7 +19956,7 @@ ${bold6("specialists status")}
19135
19956
  }
19136
19957
  section("pi (coding agent runtime)");
19137
19958
  if (!piInstalled) {
19138
- fail(`pi not installed — run ${yellow5("specialists install")}`);
19959
+ fail(`pi not installed — install ${yellow5("pi")} first`);
19139
19960
  } else {
19140
19961
  const vStr = piVersion?.ok ? `v${piVersion.stdout}` : "unknown version";
19141
19962
  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 +19964,7 @@ ${bold6("specialists status")}
19143
19964
  }
19144
19965
  section("beads (issue tracker)");
19145
19966
  if (!bdInstalled) {
19146
- fail(`bd not installed — run ${yellow5("specialists install")}`);
19967
+ fail(`bd not installed — install ${yellow5("bd")} first`);
19147
19968
  } else {
19148
19969
  ok2(`bd installed${bdVersion?.ok ? ` ${dim6(bdVersion.stdout)}` : ""}`);
19149
19970
  if (beadsPresent) {
@@ -19163,17 +19984,17 @@ ${bold6("specialists status")}
19163
19984
  if (jobs.length > 0) {
19164
19985
  section("Active Jobs");
19165
19986
  for (const job of jobs) {
19166
- const elapsed = formatElapsed(job);
19987
+ const elapsed = formatElapsed2(job);
19167
19988
  const detail = job.status === "error" ? red(job.error?.slice(0, 40) ?? "error") : job.current_tool ? dim6(`tool: ${job.current_tool}`) : dim6(job.current_event ?? "");
19168
19989
  console.log(` ${dim6(job.id)} ${job.specialist.padEnd(20)} ${statusColor(job.status).padEnd(7)} ${elapsed.padStart(6)} ${detail}`);
19169
19990
  }
19170
19991
  }
19171
19992
  console.log();
19172
19993
  }
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`;
19174
19994
  var init_status = __esm(() => {
19175
19995
  init_loader();
19176
19996
  init_supervisor();
19997
+ init_format_helpers();
19177
19998
  });
19178
19999
 
19179
20000
  // src/cli/result.ts
@@ -19181,15 +20002,15 @@ var exports_result = {};
19181
20002
  __export(exports_result, {
19182
20003
  run: () => run9
19183
20004
  });
19184
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
19185
- import { join as join9 } from "node:path";
20005
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
20006
+ import { join as join12 } from "node:path";
19186
20007
  async function run9() {
19187
20008
  const jobId = process.argv[3];
19188
20009
  if (!jobId) {
19189
- console.error("Usage: specialists result <job-id>");
20010
+ console.error("Usage: specialists|sp result <job-id>");
19190
20011
  process.exit(1);
19191
20012
  }
19192
- const jobsDir = join9(process.cwd(), ".specialists", "jobs");
20013
+ const jobsDir = join12(process.cwd(), ".specialists", "jobs");
19193
20014
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19194
20015
  const status = supervisor.readStatus(jobId);
19195
20016
  if (!status) {
@@ -19206,118 +20027,453 @@ async function run9() {
19206
20027
  `);
19207
20028
  process.exit(1);
19208
20029
  }
19209
- const resultPath = join9(jobsDir, jobId, "result.txt");
19210
- if (!existsSync6(resultPath)) {
20030
+ const resultPath = join12(jobsDir, jobId, "result.txt");
20031
+ if (!existsSync8(resultPath)) {
19211
20032
  console.error(`Result file not found for job ${jobId}`);
19212
20033
  process.exit(1);
19213
20034
  }
19214
- process.stdout.write(readFileSync4(resultPath, "utf-8"));
20035
+ process.stdout.write(readFileSync5(resultPath, "utf-8"));
19215
20036
  }
19216
20037
  var dim7 = (s) => `\x1B[2m${s}\x1B[0m`, red2 = (s) => `\x1B[31m${s}\x1B[0m`;
19217
20038
  var init_result = __esm(() => {
19218
20039
  init_supervisor();
19219
20040
  });
19220
20041
 
20042
+ // src/specialist/timeline-query.ts
20043
+ import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
20044
+ import { join as join13 } from "node:path";
20045
+ function readJobEvents(jobDir) {
20046
+ const eventsPath = join13(jobDir, "events.jsonl");
20047
+ if (!existsSync9(eventsPath))
20048
+ return [];
20049
+ const content = readFileSync6(eventsPath, "utf-8");
20050
+ const lines = content.split(`
20051
+ `).filter(Boolean);
20052
+ const events = [];
20053
+ for (const line of lines) {
20054
+ const event = parseTimelineEvent(line);
20055
+ if (event)
20056
+ events.push(event);
20057
+ }
20058
+ events.sort(compareTimelineEvents);
20059
+ return events;
20060
+ }
20061
+ function readAllJobEvents(jobsDir) {
20062
+ if (!existsSync9(jobsDir))
20063
+ return [];
20064
+ const batches = [];
20065
+ const entries = readdirSync3(jobsDir);
20066
+ for (const entry of entries) {
20067
+ const jobDir = join13(jobsDir, entry);
20068
+ try {
20069
+ const stat2 = __require("node:fs").statSync(jobDir);
20070
+ if (!stat2.isDirectory())
20071
+ continue;
20072
+ } catch {
20073
+ continue;
20074
+ }
20075
+ const jobId = entry;
20076
+ const statusPath = join13(jobDir, "status.json");
20077
+ let specialist = "unknown";
20078
+ let beadId;
20079
+ if (existsSync9(statusPath)) {
20080
+ try {
20081
+ const status = JSON.parse(readFileSync6(statusPath, "utf-8"));
20082
+ specialist = status.specialist ?? "unknown";
20083
+ beadId = status.bead_id;
20084
+ } catch {}
20085
+ }
20086
+ const events = readJobEvents(jobDir);
20087
+ if (events.length > 0) {
20088
+ batches.push({ jobId, specialist, beadId, events });
20089
+ }
20090
+ }
20091
+ return batches;
20092
+ }
20093
+ function mergeTimelineEvents(batches) {
20094
+ const merged = [];
20095
+ for (const batch of batches) {
20096
+ for (const event of batch.events) {
20097
+ merged.push({
20098
+ jobId: batch.jobId,
20099
+ specialist: batch.specialist,
20100
+ beadId: batch.beadId,
20101
+ event
20102
+ });
20103
+ }
20104
+ }
20105
+ merged.sort((a, b) => compareTimelineEvents(a.event, b.event));
20106
+ return merged;
20107
+ }
20108
+ function filterTimelineEvents(merged, filter) {
20109
+ let result = merged;
20110
+ if (filter.since !== undefined) {
20111
+ result = result.filter(({ event }) => event.t >= filter.since);
20112
+ }
20113
+ if (filter.jobId !== undefined) {
20114
+ result = result.filter(({ jobId }) => jobId === filter.jobId);
20115
+ }
20116
+ if (filter.specialist !== undefined) {
20117
+ result = result.filter(({ specialist }) => specialist === filter.specialist);
20118
+ }
20119
+ if (filter.limit !== undefined && filter.limit > 0) {
20120
+ result = result.slice(0, filter.limit);
20121
+ }
20122
+ return result;
20123
+ }
20124
+ function queryTimeline(jobsDir, filter = {}) {
20125
+ let batches = readAllJobEvents(jobsDir);
20126
+ if (filter.jobId !== undefined) {
20127
+ batches = batches.filter((b) => b.jobId === filter.jobId);
20128
+ }
20129
+ if (filter.specialist !== undefined) {
20130
+ batches = batches.filter((b) => b.specialist === filter.specialist);
20131
+ }
20132
+ const merged = mergeTimelineEvents(batches);
20133
+ return filterTimelineEvents(merged, filter);
20134
+ }
20135
+ var init_timeline_query = __esm(() => {
20136
+ init_timeline_events();
20137
+ });
20138
+
19221
20139
  // src/cli/feed.ts
19222
20140
  var exports_feed = {};
19223
20141
  __export(exports_feed, {
19224
20142
  run: () => run10
19225
20143
  });
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;
20144
+ import { existsSync as existsSync10 } from "node:fs";
20145
+ import { join as join14 } from "node:path";
20146
+ function getHumanEventKey(event) {
20147
+ switch (event.type) {
20148
+ case "meta":
20149
+ return `meta:${event.backend}:${event.model}`;
20150
+ case "tool":
20151
+ return `tool:${event.tool}:${event.phase}:${event.is_error ? "error" : "ok"}`;
20152
+ case "text":
20153
+ return "text";
20154
+ case "thinking":
20155
+ return "thinking";
20156
+ case "run_start":
20157
+ return `run_start:${event.specialist}:${event.bead_id ?? ""}`;
20158
+ case "run_complete":
20159
+ return `run_complete:${event.status}:${event.error ?? ""}`;
20160
+ case "done":
20161
+ case "agent_end":
20162
+ return `complete:${event.type}`;
20163
+ default:
20164
+ return event.type;
19237
20165
  }
19238
20166
  }
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]));
20167
+ function shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey) {
20168
+ if (event.type === "meta") {
20169
+ const metaKey = `${event.backend}:${event.model}`;
20170
+ if (seenMetaKey.get(jobId) === metaKey)
20171
+ return true;
20172
+ seenMetaKey.set(jobId, metaKey);
20173
+ }
20174
+ const key = getHumanEventKey(event);
20175
+ if (lastPrintedEventKey.get(jobId) === key)
20176
+ return true;
20177
+ lastPrintedEventKey.set(jobId, key);
20178
+ return false;
20179
+ }
20180
+ function parseSince(value) {
20181
+ if (value.includes("T") || value.includes("-")) {
20182
+ return new Date(value).getTime();
20183
+ }
20184
+ const match = value.match(/^(\d+)([smhd])$/);
20185
+ if (match) {
20186
+ const num = parseInt(match[1], 10);
20187
+ const unit = match[2];
20188
+ const multipliers = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
20189
+ return Date.now() - num * multipliers[unit];
19244
20190
  }
19245
- return lines.length;
20191
+ return;
19246
20192
  }
19247
- async function run10() {
19248
- const argv = process.argv.slice(3);
20193
+ function parseArgs5(argv) {
19249
20194
  let jobId;
20195
+ let specialist;
20196
+ let since;
20197
+ let limit = 100;
19250
20198
  let follow = false;
20199
+ let forever = false;
20200
+ let json = false;
19251
20201
  for (let i = 0;i < argv.length; i++) {
19252
20202
  if (argv[i] === "--job" && argv[i + 1]) {
19253
20203
  jobId = argv[++i];
19254
20204
  continue;
19255
20205
  }
20206
+ if (argv[i] === "--specialist" && argv[i + 1]) {
20207
+ specialist = argv[++i];
20208
+ continue;
20209
+ }
20210
+ if (argv[i] === "--since" && argv[i + 1]) {
20211
+ since = parseSince(argv[++i]);
20212
+ continue;
20213
+ }
20214
+ if (argv[i] === "--limit" && argv[i + 1]) {
20215
+ limit = parseInt(argv[++i], 10);
20216
+ continue;
20217
+ }
19256
20218
  if (argv[i] === "--follow" || argv[i] === "-f") {
19257
20219
  follow = true;
19258
20220
  continue;
19259
20221
  }
20222
+ if (argv[i] === "--forever") {
20223
+ forever = true;
20224
+ continue;
20225
+ }
20226
+ if (argv[i] === "--json") {
20227
+ json = true;
20228
+ continue;
20229
+ }
19260
20230
  if (!jobId && !argv[i].startsWith("--"))
19261
20231
  jobId = argv[i];
19262
20232
  }
19263
- if (!jobId) {
19264
- console.error("Usage: specialists feed --job <job-id> [--follow]");
19265
- process.exit(1);
20233
+ return { jobId, specialist, since, limit, follow, forever, json };
20234
+ }
20235
+ function printSnapshot(merged, options) {
20236
+ if (merged.length === 0) {
20237
+ if (!options.json)
20238
+ console.log(dim6("No events found."));
20239
+ return;
19266
20240
  }
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);
20241
+ const colorMap = new JobColorMap;
20242
+ if (options.json) {
20243
+ for (const { jobId, specialist, beadId, event } of merged) {
20244
+ console.log(JSON.stringify({ jobId, specialist, beadId, ...event }));
20245
+ }
20246
+ return;
20247
+ }
20248
+ const lastPrintedEventKey = new Map;
20249
+ const seenMetaKey = new Map;
20250
+ for (const { jobId, specialist, beadId, event } of merged) {
20251
+ if (shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey))
20252
+ continue;
20253
+ const colorize = colorMap.get(jobId);
20254
+ console.log(formatEventLine(event, { jobId, specialist, beadId, colorize }));
20255
+ }
20256
+ }
20257
+ function isCompletionEvent(event) {
20258
+ return isRunCompleteEvent(event) || event.type === "done" || event.type === "agent_end";
20259
+ }
20260
+ async function followMerged(jobsDir, options) {
20261
+ const colorMap = new JobColorMap;
20262
+ const lastSeenT = new Map;
20263
+ const completedJobs = new Set;
20264
+ const filteredBatches = () => readAllJobEvents(jobsDir).filter((batch) => !options.jobId || batch.jobId === options.jobId).filter((batch) => !options.specialist || batch.specialist === options.specialist);
20265
+ const initial = queryTimeline(jobsDir, {
20266
+ jobId: options.jobId,
20267
+ specialist: options.specialist,
20268
+ since: options.since,
20269
+ limit: options.limit
20270
+ });
20271
+ printSnapshot(initial, { ...options, json: options.json });
20272
+ for (const batch of filteredBatches()) {
20273
+ if (batch.events.length > 0) {
20274
+ const maxT = Math.max(...batch.events.map((event) => event.t));
20275
+ lastSeenT.set(batch.jobId, maxT);
20276
+ }
20277
+ if (batch.events.some(isCompletionEvent)) {
20278
+ completedJobs.add(batch.jobId);
19274
20279
  }
19275
- console.log(dim8("No events yet."));
20280
+ }
20281
+ const initialBatchCount = filteredBatches().length;
20282
+ if (!options.forever && initialBatchCount > 0 && completedJobs.size === initialBatchCount) {
20283
+ if (!options.json) {
20284
+ process.stderr.write(dim6(`All jobs complete.
20285
+ `));
20286
+ }
20287
+ return;
20288
+ }
20289
+ if (!options.json) {
20290
+ process.stderr.write(dim6(`Following... (Ctrl+C to stop)
20291
+ `));
20292
+ }
20293
+ const lastPrintedEventKey = new Map;
20294
+ const seenMetaKey = new Map;
20295
+ await new Promise((resolve2) => {
20296
+ const interval = setInterval(() => {
20297
+ const batches = filteredBatches();
20298
+ const newEvents = [];
20299
+ for (const batch of batches) {
20300
+ const lastT = lastSeenT.get(batch.jobId) ?? 0;
20301
+ for (const event of batch.events) {
20302
+ if (event.t > lastT) {
20303
+ newEvents.push({
20304
+ jobId: batch.jobId,
20305
+ specialist: batch.specialist,
20306
+ beadId: batch.beadId,
20307
+ event
20308
+ });
20309
+ }
20310
+ }
20311
+ if (batch.events.length > 0) {
20312
+ const maxT = Math.max(...batch.events.map((e) => e.t));
20313
+ lastSeenT.set(batch.jobId, maxT);
20314
+ }
20315
+ if (batch.events.some(isCompletionEvent)) {
20316
+ completedJobs.add(batch.jobId);
20317
+ }
20318
+ }
20319
+ newEvents.sort((a, b) => a.event.t - b.event.t);
20320
+ for (const { jobId, specialist, beadId, event } of newEvents) {
20321
+ if (options.json) {
20322
+ console.log(JSON.stringify({ jobId, specialist, beadId, ...event }));
20323
+ } else {
20324
+ if (shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey))
20325
+ continue;
20326
+ const colorize = colorMap.get(jobId);
20327
+ console.log(formatEventLine(event, { jobId, specialist, beadId, colorize }));
20328
+ }
20329
+ }
20330
+ if (!options.forever && batches.length > 0 && completedJobs.size === batches.length) {
20331
+ clearInterval(interval);
20332
+ resolve2();
20333
+ }
20334
+ }, 500);
20335
+ });
20336
+ }
20337
+ async function run10() {
20338
+ const options = parseArgs5(process.argv.slice(3));
20339
+ const jobsDir = join14(process.cwd(), ".specialists", "jobs");
20340
+ if (!existsSync10(jobsDir)) {
20341
+ console.log(dim6("No jobs directory found."));
20342
+ return;
20343
+ }
20344
+ if (options.follow) {
20345
+ await followMerged(jobsDir, options);
19276
20346
  return;
19277
20347
  }
19278
- const content = readFileSync5(eventsPath, "utf-8");
19279
- let linesRead = printLines(content, 0);
19280
- if (!follow)
19281
- return;
19282
- process.stderr.write(dim8(`Following ${jobId}... (Ctrl+C to stop)
19283
- `));
19284
- 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 + `
20348
+ const merged = queryTimeline(jobsDir, {
20349
+ jobId: options.jobId,
20350
+ specialist: options.specialist,
20351
+ since: options.since,
20352
+ limit: options.limit
20353
+ });
20354
+ printSnapshot(merged, options);
20355
+ }
20356
+ var init_feed = __esm(() => {
20357
+ init_timeline_events();
20358
+ init_timeline_query();
20359
+ init_format_helpers();
20360
+ });
20361
+
20362
+ // src/cli/steer.ts
20363
+ var exports_steer = {};
20364
+ __export(exports_steer, {
20365
+ run: () => run11
20366
+ });
20367
+ import { join as join15 } from "node:path";
20368
+ import { writeFileSync as writeFileSync6 } from "node:fs";
20369
+ async function run11() {
20370
+ const jobId = process.argv[3];
20371
+ const message = process.argv[4];
20372
+ if (!jobId || !message) {
20373
+ console.error('Usage: specialists|sp steer <job-id> "<message>"');
20374
+ process.exit(1);
20375
+ }
20376
+ const jobsDir = join15(process.cwd(), ".specialists", "jobs");
20377
+ const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
20378
+ const status = supervisor.readStatus(jobId);
20379
+ if (!status) {
20380
+ console.error(`No job found: ${jobId}`);
20381
+ process.exit(1);
20382
+ }
20383
+ if (status.status === "done" || status.status === "error") {
20384
+ process.stderr.write(`Job ${jobId} is already ${status.status}.
20385
+ `);
20386
+ process.exit(1);
20387
+ }
20388
+ if (!status.fifo_path) {
20389
+ process.stderr.write(`${red3("Error:")} Job ${jobId} has no steer pipe.
20390
+ `);
20391
+ process.stderr.write(`Only jobs started with --background support mid-run steering.
20392
+ `);
20393
+ process.exit(1);
20394
+ }
20395
+ try {
20396
+ const payload = JSON.stringify({ type: "steer", message }) + `
20397
+ `;
20398
+ writeFileSync6(status.fifo_path, payload, { flag: "a" });
20399
+ process.stdout.write(`${green7("✓")} Steer message sent to job ${jobId}
20400
+ `);
20401
+ } catch (err) {
20402
+ process.stderr.write(`${red3("Error:")} Failed to write to steer pipe: ${err?.message}
20403
+ `);
20404
+ process.exit(1);
20405
+ }
20406
+ }
20407
+ var green7 = (s) => `\x1B[32m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
20408
+ var init_steer = __esm(() => {
20409
+ init_supervisor();
20410
+ });
20411
+
20412
+ // src/cli/follow-up.ts
20413
+ var exports_follow_up = {};
20414
+ __export(exports_follow_up, {
20415
+ run: () => run12
20416
+ });
20417
+ import { join as join16 } from "node:path";
20418
+ import { writeFileSync as writeFileSync7 } from "node:fs";
20419
+ async function run12() {
20420
+ const jobId = process.argv[3];
20421
+ const message = process.argv[4];
20422
+ if (!jobId || !message) {
20423
+ console.error('Usage: specialists|sp follow-up <job-id> "<message>"');
20424
+ process.exit(1);
20425
+ }
20426
+ const jobsDir = join16(process.cwd(), ".specialists", "jobs");
20427
+ const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
20428
+ const status = supervisor.readStatus(jobId);
20429
+ if (!status) {
20430
+ console.error(`No job found: ${jobId}`);
20431
+ process.exit(1);
20432
+ }
20433
+ if (status.status !== "waiting") {
20434
+ process.stderr.write(`${red4("Error:")} Job ${jobId} is not in waiting state (status: ${status.status}).
20435
+ `);
20436
+ process.stderr.write(`Only jobs started with --keep-alive and --background support follow-up prompts.
20437
+ `);
20438
+ process.exit(1);
20439
+ }
20440
+ if (!status.fifo_path) {
20441
+ process.stderr.write(`${red4("Error:")} Job ${jobId} has no steer pipe.
20442
+ `);
20443
+ process.exit(1);
20444
+ }
20445
+ try {
20446
+ const payload = JSON.stringify({ type: "prompt", message }) + `
20447
+ `;
20448
+ writeFileSync7(status.fifo_path, payload, { flag: "a" });
20449
+ process.stdout.write(`${green8("✓")} Follow-up sent to job ${jobId}
19296
20450
  `);
19297
- resolve();
19298
- }
19299
- } catch {}
19300
- });
19301
- });
20451
+ process.stdout.write(` Use 'specialists feed ${jobId} --follow' to watch the response.
20452
+ `);
20453
+ } catch (err) {
20454
+ process.stderr.write(`${red4("Error:")} Failed to write to steer pipe: ${err?.message}
20455
+ `);
20456
+ process.exit(1);
20457
+ }
19302
20458
  }
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
- var init_feed = __esm(() => {
20459
+ var green8 = (s) => `\x1B[32m${s}\x1B[0m`, red4 = (s) => `\x1B[31m${s}\x1B[0m`;
20460
+ var init_follow_up = __esm(() => {
19305
20461
  init_supervisor();
19306
20462
  });
19307
20463
 
19308
20464
  // src/cli/stop.ts
19309
20465
  var exports_stop = {};
19310
20466
  __export(exports_stop, {
19311
- run: () => run11
20467
+ run: () => run13
19312
20468
  });
19313
- import { join as join11 } from "node:path";
19314
- async function run11() {
20469
+ import { join as join17 } from "node:path";
20470
+ async function run13() {
19315
20471
  const jobId = process.argv[3];
19316
20472
  if (!jobId) {
19317
- console.error("Usage: specialists stop <job-id>");
20473
+ console.error("Usage: specialists|sp stop <job-id>");
19318
20474
  process.exit(1);
19319
20475
  }
19320
- const jobsDir = join11(process.cwd(), ".specialists", "jobs");
20476
+ const jobsDir = join17(process.cwd(), ".specialists", "jobs");
19321
20477
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19322
20478
  const status = supervisor.readStatus(jobId);
19323
20479
  if (!status) {
@@ -19325,31 +20481,31 @@ async function run11() {
19325
20481
  process.exit(1);
19326
20482
  }
19327
20483
  if (status.status === "done" || status.status === "error") {
19328
- process.stderr.write(`${dim9(`Job ${jobId} is already ${status.status}.`)}
20484
+ process.stderr.write(`${dim8(`Job ${jobId} is already ${status.status}.`)}
19329
20485
  `);
19330
20486
  return;
19331
20487
  }
19332
20488
  if (!status.pid) {
19333
- process.stderr.write(`${red4(`No PID recorded for job ${jobId}.`)}
20489
+ process.stderr.write(`${red5(`No PID recorded for job ${jobId}.`)}
19334
20490
  `);
19335
20491
  process.exit(1);
19336
20492
  }
19337
20493
  try {
19338
20494
  process.kill(status.pid, "SIGTERM");
19339
- process.stdout.write(`${green6("✓")} Sent SIGTERM to PID ${status.pid} (job ${jobId})
20495
+ process.stdout.write(`${green9("✓")} Sent SIGTERM to PID ${status.pid} (job ${jobId})
19340
20496
  `);
19341
20497
  } catch (err) {
19342
20498
  if (err.code === "ESRCH") {
19343
- process.stderr.write(`${red4(`Process ${status.pid} not found.`)} Job may have already completed.
20499
+ process.stderr.write(`${red5(`Process ${status.pid} not found.`)} Job may have already completed.
19344
20500
  `);
19345
20501
  } else {
19346
- process.stderr.write(`${red4("Error:")} ${err.message}
20502
+ process.stderr.write(`${red5("Error:")} ${err.message}
19347
20503
  `);
19348
20504
  process.exit(1);
19349
20505
  }
19350
20506
  }
19351
20507
  }
19352
- var green6 = (s) => `\x1B[32m${s}\x1B[0m`, red4 = (s) => `\x1B[31m${s}\x1B[0m`, dim9 = (s) => `\x1B[2m${s}\x1B[0m`;
20508
+ var green9 = (s) => `\x1B[32m${s}\x1B[0m`, red5 = (s) => `\x1B[31m${s}\x1B[0m`, dim8 = (s) => `\x1B[2m${s}\x1B[0m`;
19353
20509
  var init_stop = __esm(() => {
19354
20510
  init_supervisor();
19355
20511
  });
@@ -19357,32 +20513,33 @@ var init_stop = __esm(() => {
19357
20513
  // src/cli/quickstart.ts
19358
20514
  var exports_quickstart = {};
19359
20515
  __export(exports_quickstart, {
19360
- run: () => run12
20516
+ run: () => run14
19361
20517
  });
19362
20518
  function section2(title) {
19363
20519
  const bar = "─".repeat(60);
19364
20520
  return `
19365
- ${bold7(cyan6(title))}
19366
- ${dim10(bar)}`;
20521
+ ${bold7(cyan5(title))}
20522
+ ${dim9(bar)}`;
19367
20523
  }
19368
20524
  function cmd2(s) {
19369
- return yellow7(s);
20525
+ return yellow6(s);
19370
20526
  }
19371
20527
  function flag(s) {
19372
- return green7(s);
20528
+ return green10(s);
19373
20529
  }
19374
- async function run12() {
20530
+ async function run14() {
19375
20531
  const lines = [
19376
20532
  "",
19377
20533
  bold7("specialists · Quick Start Guide"),
19378
- dim10("One MCP server. Multiple AI backends. Intelligent orchestration."),
20534
+ dim9("One MCP server. Multiple AI backends. Intelligent orchestration."),
20535
+ dim9("Tip: sp is a shorter alias — sp run, sp list, sp feed etc. work identically."),
19379
20536
  ""
19380
20537
  ];
19381
20538
  lines.push(section2("1. Installation"));
19382
20539
  lines.push("");
19383
20540
  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")}`);
20541
+ lines.push(` ${cmd2("specialists install")} # project setup:`);
20542
+ lines.push(` ${dim9(" # checks pi · bd · xt, then wires MCP + hooks")}`);
19386
20543
  lines.push("");
19387
20544
  lines.push(` Verify everything is healthy:`);
19388
20545
  lines.push(` ${cmd2("specialists status")} # shows pi, beads, MCP, active jobs`);
@@ -19393,9 +20550,9 @@ async function run12() {
19393
20550
  lines.push(` ${cmd2("specialists init")} # creates specialists/, .specialists/, AGENTS.md`);
19394
20551
  lines.push("");
19395
20552
  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`);
20553
+ lines.push(` ${dim9("specialists/")} — put your .specialist.yaml files here`);
20554
+ lines.push(` ${dim9(".specialists/")} — runtime data (jobs/, ready/) — gitignored`);
20555
+ lines.push(` ${dim9("AGENTS.md")} — context block injected into Claude sessions`);
19399
20556
  lines.push("");
19400
20557
  lines.push(section2("3. Discover Specialists"));
19401
20558
  lines.push("");
@@ -19406,24 +20563,24 @@ async function run12() {
19406
20563
  lines.push(` ${cmd2("specialists list")} ${flag("--json")} # machine-readable JSON`);
19407
20564
  lines.push("");
19408
20565
  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)`);
20566
+ lines.push(` ${blue2("project")} ./specialists/*.specialist.yaml`);
20567
+ lines.push(` ${blue2("user")} ~/.specialists/*.specialist.yaml`);
20568
+ lines.push(` ${blue2("system")} bundled specialists (shipped with the package)`);
19412
20569
  lines.push("");
19413
20570
  lines.push(section2("4. Running a Specialist"));
19414
20571
  lines.push("");
19415
20572
  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"')}`);
20573
+ lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${dim9('"Review src/api.ts for security issues"')}`);
19417
20574
  lines.push("");
19418
20575
  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")}`);
20576
+ lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${dim9('"..."')} ${flag("--background")}`);
20577
+ lines.push(` ${dim9(" # → Job started: job_a1b2c3d4")}`);
19421
20578
  lines.push("");
19422
20579
  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('"..."')}`);
20580
+ lines.push(` ${cmd2("specialists run code-review")} ${flag("--model")} ${dim9("anthropic/claude-opus-4-6")} ${flag("--prompt")} ${dim9('"..."')}`);
19424
20581
  lines.push("");
19425
20582
  lines.push(` Run without beads issue tracking:`);
19426
- lines.push(` ${cmd2("specialists run code-review")} ${flag("--no-beads")} ${flag("--prompt")} ${dim10('"..."')}`);
20583
+ lines.push(` ${cmd2("specialists run code-review")} ${flag("--no-beads")} ${flag("--prompt")} ${dim9('"..."')}`);
19427
20584
  lines.push("");
19428
20585
  lines.push(` Pipe a prompt from stdin:`);
19429
20586
  lines.push(` ${cmd2("cat my-brief.md | specialists run code-review")}`);
@@ -19437,25 +20594,37 @@ async function run12() {
19437
20594
  lines.push(` ${bold7("Read results")} — print the final output:`);
19438
20595
  lines.push(` ${cmd2("specialists result job_a1b2c3d4")} # exits 1 if still running`);
19439
20596
  lines.push("");
20597
+ lines.push(` ${bold7("Steer a running job")} — redirect the agent mid-run without cancelling:`);
20598
+ lines.push(` ${cmd2("specialists steer job_a1b2c3d4")} ${flag('"focus only on supervisor.ts"')}`);
20599
+ lines.push(` ${dim9(" # delivered after current tool calls finish, before the next LLM call")}`);
20600
+ lines.push("");
20601
+ lines.push(` ${bold7("Keep-alive multi-turn")} — start with ${flag("--keep-alive")}, then follow up:`);
20602
+ lines.push(` ${cmd2("specialists run bug-hunt")} ${flag("--bead unitAI-abc --keep-alive --background")}`);
20603
+ lines.push(` ${dim9(" # → Job started: a1b2c3 (status: waiting after first turn)")}`);
20604
+ lines.push(` ${cmd2("specialists result a1b2c3")} # read first turn`);
20605
+ lines.push(` ${cmd2("specialists follow-up a1b2c3")} ${flag('"now write the fix"')} # next turn, same Pi context`);
20606
+ lines.push(` ${cmd2("specialists feed a1b2c3")} ${flag("--follow")} # watch response`);
20607
+ lines.push("");
19440
20608
  lines.push(` ${bold7("Cancel a job")}:`);
19441
20609
  lines.push(` ${cmd2("specialists stop job_a1b2c3d4")} # sends SIGTERM to the agent process`);
19442
20610
  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)`);
20611
+ lines.push(` ${bold7("Job files")} in ${dim9(".specialists/jobs/<job-id>/")}:`);
20612
+ lines.push(` ${dim9("status.json")} — id, specialist, status, pid, started_at, elapsed_s, current_tool`);
20613
+ lines.push(` ${dim9("events.jsonl")} — one JSON event per line (tool_use, text, agent_end, error …)`);
20614
+ lines.push(` ${dim9("result.txt")} — final output (written when status=done)`);
20615
+ lines.push(` ${dim9("steer.pipe")} — named FIFO for mid-run steering (removed on job completion)`);
19447
20616
  lines.push("");
19448
20617
  lines.push(section2("6. Editing Specialists"));
19449
20618
  lines.push("");
19450
20619
  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")}`);
20620
+ lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${dim9("anthropic/claude-sonnet-4-6")}`);
20621
+ lines.push(` ${cmd2("specialists edit code-review")} ${flag("--description")} ${dim9('"Updated description"')}`);
20622
+ lines.push(` ${cmd2("specialists edit code-review")} ${flag("--timeout")} ${dim9("120000")}`);
20623
+ lines.push(` ${cmd2("specialists edit code-review")} ${flag("--permission")} ${dim9("HIGH")}`);
20624
+ lines.push(` ${cmd2("specialists edit code-review")} ${flag("--tags")} ${dim9("analysis,security,review")}`);
19456
20625
  lines.push("");
19457
20626
  lines.push(` Preview without writing:`);
19458
- lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${dim10("...")} ${flag("--dry-run")}`);
20627
+ lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${dim9("...")} ${flag("--dry-run")}`);
19459
20628
  lines.push("");
19460
20629
  lines.push(section2("7. .specialist.yaml Schema"));
19461
20630
  lines.push("");
@@ -19502,21 +20671,21 @@ async function run12() {
19502
20671
  " priority: 2 # 0=critical … 4=backlog"
19503
20672
  ];
19504
20673
  for (const l of schemaLines) {
19505
- lines.push(` ${dim10(l)}`);
20674
+ lines.push(` ${dim9(l)}`);
19506
20675
  }
19507
20676
  lines.push("");
19508
20677
  lines.push(section2("8. Hook System"));
19509
20678
  lines.push("");
19510
- lines.push(` Specialists emits lifecycle events to ${dim10(".specialists/trace.jsonl")}:`);
20679
+ lines.push(` Specialists emits lifecycle events to ${dim9(".specialists/trace.jsonl")}:`);
19511
20680
  lines.push("");
19512
20681
  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`);
20682
+ lines.push(` ${yellow6("specialist:start")} before the agent session begins`);
20683
+ lines.push(` ${yellow6("specialist:token")} on each streamed token (delta)`);
20684
+ lines.push(` ${yellow6("specialist:done")} after successful completion`);
20685
+ lines.push(` ${yellow6("specialist:error")} on failure or timeout`);
19517
20686
  lines.push("");
19518
20687
  lines.push(` Each event line in trace.jsonl:`);
19519
- lines.push(` ${dim10('{"t":"<ISO>","hook":"specialist:done","specialist":"code-review","durationMs":4120}')}`);
20688
+ lines.push(` ${dim9('{"t":"<ISO>","hook":"specialist:done","specialist":"code-review","durationMs":4120}')}`);
19520
20689
  lines.push("");
19521
20690
  lines.push(` Tail the trace file to observe all activity:`);
19522
20691
  lines.push(` ${cmd2("tail -f .specialists/trace.jsonl | jq .")}`);
@@ -19531,7 +20700,9 @@ async function run12() {
19531
20700
  lines.push(` ${bold7("run_parallel")} — concurrent or pipeline execution`);
19532
20701
  lines.push(` ${bold7("start_specialist")} — async job start, returns job ID`);
19533
20702
  lines.push(` ${bold7("poll_specialist")} — poll job status/output by ID`);
19534
- lines.push(` ${bold7("stop_specialist")} cancel a running job by ID`);
20703
+ lines.push(` ${bold7("steer_specialist")} send a mid-run message to a running job`);
20704
+ lines.push(` ${bold7("follow_up_specialist")} — send a next-turn prompt to a keep-alive session`);
20705
+ lines.push(` ${bold7("stop_specialist")} — cancel a running job by ID`);
19535
20706
  lines.push(` ${bold7("specialist_status")} — circuit breaker health + staleness`);
19536
20707
  lines.push("");
19537
20708
  lines.push(section2("10. Common Workflows"));
@@ -19544,41 +20715,51 @@ async function run12() {
19544
20715
  lines.push(` ${cmd2("specialists feed <job-id> --follow")}`);
19545
20716
  lines.push(` ${cmd2("specialists result <job-id> > analysis.md")}`);
19546
20717
  lines.push("");
20718
+ lines.push(` ${bold7("Steer a job mid-run:")}`);
20719
+ lines.push(` ${cmd2('specialists run deep-analysis --prompt "..." --background')}`);
20720
+ lines.push(` ${cmd2('specialists steer <job-id> "focus only on the auth module"')}`);
20721
+ lines.push(` ${cmd2("specialists result <job-id>")}`);
20722
+ lines.push("");
20723
+ lines.push(` ${bold7("Multi-turn keep-alive (iterative work):")}`);
20724
+ lines.push(` ${cmd2("specialists run bug-hunt --bead unitAI-abc --keep-alive --background")}`);
20725
+ lines.push(` ${cmd2("specialists result <job-id>")}`);
20726
+ lines.push(` ${cmd2('specialists follow-up <job-id> "now write the fix for the root cause"')}`);
20727
+ lines.push(` ${cmd2("specialists feed <job-id> --follow")}`);
20728
+ lines.push("");
19547
20729
  lines.push(` ${bold7("Override model for a single run:")}`);
19548
20730
  lines.push(` ${cmd2('specialists run code-review --model anthropic/claude-opus-4-6 --prompt "..."')}`);
19549
20731
  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`);
20732
+ lines.push(dim9("─".repeat(62)));
20733
+ lines.push(` ${dim9("specialists help")} command list ${dim9("specialists <cmd> --help")} per-command flags`);
20734
+ lines.push(` ${dim9("specialists status")} health check ${dim9("specialists models")} available models`);
19553
20735
  lines.push("");
19554
20736
  console.log(lines.join(`
19555
20737
  `));
19556
20738
  }
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`;
20739
+ var bold7 = (s) => `\x1B[1m${s}\x1B[0m`, dim9 = (s) => `\x1B[2m${s}\x1B[0m`, yellow6 = (s) => `\x1B[33m${s}\x1B[0m`, cyan5 = (s) => `\x1B[36m${s}\x1B[0m`, blue2 = (s) => `\x1B[34m${s}\x1B[0m`, green10 = (s) => `\x1B[32m${s}\x1B[0m`;
19558
20740
 
19559
20741
  // src/cli/doctor.ts
19560
20742
  var exports_doctor = {};
19561
20743
  __export(exports_doctor, {
19562
- run: () => run13
20744
+ run: () => run15
19563
20745
  });
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";
20746
+ import { spawnSync as spawnSync7 } from "node:child_process";
20747
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
20748
+ import { join as join18 } from "node:path";
19568
20749
  function ok3(msg) {
19569
- console.log(` ${green8("✓")} ${msg}`);
20750
+ console.log(` ${green11("✓")} ${msg}`);
19570
20751
  }
19571
20752
  function warn2(msg) {
19572
- console.log(` ${yellow8("○")} ${msg}`);
20753
+ console.log(` ${yellow7("○")} ${msg}`);
19573
20754
  }
19574
20755
  function fail2(msg) {
19575
- console.log(` ${red5("✗")} ${msg}`);
20756
+ console.log(` ${red6("✗")} ${msg}`);
19576
20757
  }
19577
20758
  function fix(msg) {
19578
- console.log(` ${dim11("→ fix:")} ${yellow8(msg)}`);
20759
+ console.log(` ${dim10("→ fix:")} ${yellow7(msg)}`);
19579
20760
  }
19580
20761
  function hint(msg) {
19581
- console.log(` ${dim11(msg)}`);
20762
+ console.log(` ${dim10(msg)}`);
19582
20763
  }
19583
20764
  function section3(label) {
19584
20765
  const line = "─".repeat(Math.max(0, 38 - label.length));
@@ -19586,17 +20767,26 @@ function section3(label) {
19586
20767
  ${bold8(`── ${label} ${line}`)}`);
19587
20768
  }
19588
20769
  function sp(bin, args) {
19589
- const r = spawnSync5(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
20770
+ const r = spawnSync7(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
19590
20771
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
19591
20772
  }
19592
20773
  function isInstalled2(bin) {
19593
- return spawnSync5("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
20774
+ return spawnSync7("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
20775
+ }
20776
+ function loadJson2(path) {
20777
+ if (!existsSync11(path))
20778
+ return null;
20779
+ try {
20780
+ return JSON.parse(readFileSync7(path, "utf8"));
20781
+ } catch {
20782
+ return null;
20783
+ }
19594
20784
  }
19595
20785
  function checkPi() {
19596
20786
  section3("pi (coding agent runtime)");
19597
20787
  if (!isInstalled2("pi")) {
19598
20788
  fail2("pi not installed");
19599
- fix("specialists install");
20789
+ fix("install pi first");
19600
20790
  return false;
19601
20791
  }
19602
20792
  const version2 = sp("pi", ["--version"]);
@@ -19609,75 +20799,94 @@ function checkPi() {
19609
20799
  fix("pi config (add at least one API key)");
19610
20800
  return false;
19611
20801
  }
19612
- ok3(`pi ${vStr} — ${providers.size} provider${providers.size > 1 ? "s" : ""} active ${dim11(`(${[...providers].join(", ")})`)} `);
20802
+ ok3(`pi ${vStr} — ${providers.size} provider${providers.size > 1 ? "s" : ""} active ${dim10(`(${[...providers].join(", ")})`)}`);
20803
+ return true;
20804
+ }
20805
+ function checkBd() {
20806
+ section3("beads (issue tracker)");
20807
+ if (!isInstalled2("bd")) {
20808
+ fail2("bd not installed");
20809
+ fix("install beads (bd) first");
20810
+ return false;
20811
+ }
20812
+ ok3(`bd installed ${dim10(sp("bd", ["--version"]).stdout || "")}`);
20813
+ if (existsSync11(join18(CWD, ".beads")))
20814
+ ok3(".beads/ present in project");
20815
+ else
20816
+ warn2(".beads/ not found in project");
20817
+ return true;
20818
+ }
20819
+ function checkXt() {
20820
+ section3("xtrm-tools");
20821
+ if (!isInstalled2("xt")) {
20822
+ fail2("xt not installed");
20823
+ fix("install xtrm-tools first");
20824
+ return false;
20825
+ }
20826
+ ok3(`xt installed ${dim10(sp("xt", ["--version"]).stdout || "")}`);
19613
20827
  return true;
19614
20828
  }
19615
20829
  function checkHooks() {
19616
- section3("Claude Code hooks (7 expected)");
20830
+ section3("Claude Code hooks (2 expected)");
19617
20831
  let allPresent = true;
19618
20832
  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)");
20833
+ const dest = join18(HOOKS_DIR, name);
20834
+ if (!existsSync11(dest)) {
20835
+ fail2(`${name} ${red6("missing")}`);
20836
+ fix("specialists install");
19623
20837
  allPresent = false;
19624
20838
  } else {
19625
20839
  ok3(name);
19626
20840
  }
19627
20841
  }
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}`);
20842
+ const settings = loadJson2(SETTINGS_FILE);
20843
+ if (!settings) {
20844
+ warn2(`Could not read ${SETTINGS_FILE}`);
20845
+ fix("specialists install");
20846
+ return false;
20847
+ }
20848
+ const wiredCommands = new Set([
20849
+ ...settings.UserPromptSubmit ?? [],
20850
+ ...settings.SessionStart ?? []
20851
+ ].flatMap((entry) => (entry.hooks ?? []).map((h) => h.command ?? "")));
20852
+ for (const name of HOOK_NAMES) {
20853
+ const expectedRelative = `node .specialists/default/hooks/${name}`;
20854
+ if (!wiredCommands.has(expectedRelative)) {
20855
+ warn2(`${name} not wired in settings.json`);
20856
+ fix("specialists install");
19650
20857
  allPresent = false;
19651
20858
  }
19652
20859
  }
20860
+ if (allPresent)
20861
+ hint(`Hooks wired in ${SETTINGS_FILE}`);
19653
20862
  return allPresent;
19654
20863
  }
19655
20864
  function checkMCP() {
19656
20865
  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)`);
20866
+ const mcp = loadJson2(MCP_FILE2);
20867
+ const spec = mcp?.mcpServers?.specialists;
20868
+ if (!spec || spec.command !== "specialists") {
20869
+ fail2(`MCP server 'specialists' not registered in .mcp.json`);
20870
+ fix("specialists install");
19661
20871
  return false;
19662
20872
  }
19663
- ok3(`MCP server '${MCP_NAME}' registered`);
20873
+ ok3(`MCP server 'specialists' registered in ${MCP_FILE2}`);
19664
20874
  return true;
19665
20875
  }
19666
20876
  function checkRuntimeDirs() {
19667
20877
  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");
20878
+ const rootDir = join18(CWD, ".specialists");
20879
+ const jobsDir = join18(rootDir, "jobs");
20880
+ const readyDir = join18(rootDir, "ready");
19672
20881
  let allOk = true;
19673
- if (!existsSync8(rootDir)) {
20882
+ if (!existsSync11(rootDir)) {
19674
20883
  warn2(".specialists/ not found in current project");
19675
20884
  fix("specialists init");
19676
20885
  allOk = false;
19677
20886
  } else {
19678
20887
  ok3(".specialists/ present");
19679
20888
  for (const [subDir, label] of [[jobsDir, "jobs"], [readyDir, "ready"]]) {
19680
- if (!existsSync8(subDir)) {
20889
+ if (!existsSync11(subDir)) {
19681
20890
  warn2(`.specialists/${label}/ missing — auto-creating`);
19682
20891
  mkdirSync3(subDir, { recursive: true });
19683
20892
  ok3(`.specialists/${label}/ created`);
@@ -19690,14 +20899,14 @@ function checkRuntimeDirs() {
19690
20899
  }
19691
20900
  function checkZombieJobs() {
19692
20901
  section3("Background jobs");
19693
- const jobsDir = join12(process.cwd(), ".specialists", "jobs");
19694
- if (!existsSync8(jobsDir)) {
20902
+ const jobsDir = join18(CWD, ".specialists", "jobs");
20903
+ if (!existsSync11(jobsDir)) {
19695
20904
  hint("No .specialists/jobs/ — skipping");
19696
20905
  return true;
19697
20906
  }
19698
20907
  let entries;
19699
20908
  try {
19700
- entries = readdirSync2(jobsDir);
20909
+ entries = readdirSync4(jobsDir);
19701
20910
  } catch {
19702
20911
  entries = [];
19703
20912
  }
@@ -19709,11 +20918,11 @@ function checkZombieJobs() {
19709
20918
  let total = 0;
19710
20919
  let running = 0;
19711
20920
  for (const jobId of entries) {
19712
- const statusPath = join12(jobsDir, jobId, "status.json");
19713
- if (!existsSync8(statusPath))
20921
+ const statusPath = join18(jobsDir, jobId, "status.json");
20922
+ if (!existsSync11(statusPath))
19714
20923
  continue;
19715
20924
  try {
19716
- const status = JSON.parse(readFileSync6(statusPath, "utf8"));
20925
+ const status = JSON.parse(readFileSync7(statusPath, "utf8"));
19717
20926
  total++;
19718
20927
  if (status.status === "running" || status.status === "starting") {
19719
20928
  const pid = status.pid;
@@ -19723,11 +20932,11 @@ function checkZombieJobs() {
19723
20932
  process.kill(pid, 0);
19724
20933
  alive = true;
19725
20934
  } catch {}
19726
- if (alive) {
20935
+ if (alive)
19727
20936
  running++;
19728
- } else {
20937
+ else {
19729
20938
  zombies++;
19730
- warn2(`${jobId} ${yellow8("ZOMBIE")} ${dim11(`pid ${pid} not found, status=${status.status}`)}`);
20939
+ warn2(`${jobId} ${yellow7("ZOMBIE")} ${dim10(`pid ${pid} not found, status=${status.status}`)}`);
19731
20940
  fix(`Edit .specialists/jobs/${jobId}/status.json → set "status": "error"`);
19732
20941
  }
19733
20942
  }
@@ -19740,37 +20949,36 @@ function checkZombieJobs() {
19740
20949
  }
19741
20950
  return zombies === 0;
19742
20951
  }
19743
- async function run13() {
20952
+ async function run15() {
19744
20953
  console.log(`
19745
20954
  ${bold8("specialists doctor")}
19746
20955
  `);
19747
20956
  const piOk = checkPi();
20957
+ const bdOk = checkBd();
20958
+ const xtOk = checkXt();
19748
20959
  const hooksOk = checkHooks();
19749
20960
  const mcpOk = checkMCP();
19750
20961
  const dirsOk = checkRuntimeDirs();
19751
20962
  const jobsOk = checkZombieJobs();
19752
- const allOk = piOk && hooksOk && mcpOk && dirsOk && jobsOk;
20963
+ const allOk = piOk && bdOk && xtOk && hooksOk && mcpOk && dirsOk && jobsOk;
19753
20964
  console.log("");
19754
20965
  if (allOk) {
19755
- console.log(` ${green8("✓")} ${bold8("All checks passed")} — specialists is healthy`);
20966
+ console.log(` ${green11("✓")} ${bold8("All checks passed")} — specialists is healthy`);
19756
20967
  } else {
19757
- console.log(` ${yellow8("○")} ${bold8("Some checks failed")} — follow the fix hints above`);
19758
- console.log(` ${dim11("specialists install fixes most issues automatically.")}`);
20968
+ console.log(` ${yellow7("○")} ${bold8("Some checks failed")} — follow the fix hints above`);
20969
+ console.log(` ${dim10("specialists install fixes hook + MCP registration; pi, bd, and xt must be installed separately.")}`);
19759
20970
  }
19760
20971
  console.log("");
19761
20972
  }
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;
20973
+ var bold8 = (s) => `\x1B[1m${s}\x1B[0m`, dim10 = (s) => `\x1B[2m${s}\x1B[0m`, green11 = (s) => `\x1B[32m${s}\x1B[0m`, yellow7 = (s) => `\x1B[33m${s}\x1B[0m`, red6 = (s) => `\x1B[31m${s}\x1B[0m`, CWD, CLAUDE_DIR, SPECIALISTS_DIR, HOOKS_DIR, SETTINGS_FILE, MCP_FILE2, HOOK_NAMES;
19763
20974
  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");
20975
+ CWD = process.cwd();
20976
+ CLAUDE_DIR = join18(CWD, ".claude");
20977
+ SPECIALISTS_DIR = join18(CWD, ".specialists");
20978
+ HOOKS_DIR = join18(SPECIALISTS_DIR, "default", "hooks");
20979
+ SETTINGS_FILE = join18(CLAUDE_DIR, "settings.json");
20980
+ MCP_FILE2 = join18(CWD, ".mcp.json");
19768
20981
  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
20982
  "specialists-complete.mjs",
19775
20983
  "specialists-session-start.mjs"
19776
20984
  ];
@@ -19779,29 +20987,29 @@ var init_doctor = __esm(() => {
19779
20987
  // src/cli/setup.ts
19780
20988
  var exports_setup = {};
19781
20989
  __export(exports_setup, {
19782
- run: () => run14
20990
+ run: () => run16
19783
20991
  });
19784
- import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
20992
+ import { existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "node:fs";
19785
20993
  import { homedir as homedir3 } from "node:os";
19786
- import { join as join13, resolve } from "node:path";
20994
+ import { join as join19, resolve as resolve2 } from "node:path";
19787
20995
  function ok4(msg) {
19788
- console.log(` ${green9("✓")} ${msg}`);
20996
+ console.log(` ${green12("✓")} ${msg}`);
19789
20997
  }
19790
20998
  function skip2(msg) {
19791
- console.log(` ${yellow9("○")} ${msg}`);
20999
+ console.log(` ${yellow8("○")} ${msg}`);
19792
21000
  }
19793
21001
  function resolveTarget(target) {
19794
21002
  switch (target) {
19795
21003
  case "global":
19796
- return join13(homedir3(), ".claude", "CLAUDE.md");
21004
+ return join19(homedir3(), ".claude", "CLAUDE.md");
19797
21005
  case "agents":
19798
- return join13(process.cwd(), "AGENTS.md");
21006
+ return join19(process.cwd(), "AGENTS.md");
19799
21007
  case "project":
19800
21008
  default:
19801
- return join13(process.cwd(), "CLAUDE.md");
21009
+ return join19(process.cwd(), "CLAUDE.md");
19802
21010
  }
19803
21011
  }
19804
- function parseArgs5() {
21012
+ function parseArgs6() {
19805
21013
  const argv = process.argv.slice(3);
19806
21014
  let target = "project";
19807
21015
  let dryRun = false;
@@ -19826,30 +21034,30 @@ function parseArgs5() {
19826
21034
  }
19827
21035
  return { target, dryRun };
19828
21036
  }
19829
- async function run14() {
19830
- const { target, dryRun } = parseArgs5();
19831
- const filePath = resolve(resolveTarget(target));
21037
+ async function run16() {
21038
+ const { target, dryRun } = parseArgs6();
21039
+ const filePath = resolve2(resolveTarget(target));
19832
21040
  const label = target === "global" ? "~/.claude/CLAUDE.md" : filePath.replace(process.cwd() + "/", "");
19833
21041
  console.log(`
19834
21042
  ${bold9("specialists setup")}
19835
21043
  `);
19836
- console.log(` Target: ${yellow9(label)}${dryRun ? dim12(" (dry-run)") : ""}
21044
+ console.log(` Target: ${yellow8(label)}${dryRun ? dim11(" (dry-run)") : ""}
19837
21045
  `);
19838
- if (existsSync9(filePath)) {
19839
- const existing = readFileSync7(filePath, "utf8");
21046
+ if (existsSync12(filePath)) {
21047
+ const existing = readFileSync8(filePath, "utf8");
19840
21048
  if (existing.includes(MARKER)) {
19841
21049
  skip2(`${label} already contains Specialists Workflow section`);
19842
21050
  console.log(`
19843
- ${dim12("To force-update, remove the ## Specialists Workflow section and re-run.")}
21051
+ ${dim11("To force-update, remove the ## Specialists Workflow section and re-run.")}
19844
21052
  `);
19845
21053
  return;
19846
21054
  }
19847
21055
  if (dryRun) {
19848
- console.log(dim12("─".repeat(60)));
19849
- console.log(dim12("Would append to existing file:"));
21056
+ console.log(dim11("─".repeat(60)));
21057
+ console.log(dim11("Would append to existing file:"));
19850
21058
  console.log("");
19851
21059
  console.log(WORKFLOW_BLOCK);
19852
- console.log(dim12("─".repeat(60)));
21060
+ console.log(dim11("─".repeat(60)));
19853
21061
  return;
19854
21062
  }
19855
21063
  const separator = existing.trimEnd().endsWith(`
@@ -19857,28 +21065,28 @@ ${bold9("specialists setup")}
19857
21065
  ` : `
19858
21066
 
19859
21067
  `;
19860
- writeFileSync4(filePath, existing.trimEnd() + separator + WORKFLOW_BLOCK, "utf8");
21068
+ writeFileSync8(filePath, existing.trimEnd() + separator + WORKFLOW_BLOCK, "utf8");
19861
21069
  ok4(`Appended Specialists Workflow section to ${label}`);
19862
21070
  } else {
19863
21071
  if (dryRun) {
19864
- console.log(dim12("─".repeat(60)));
19865
- console.log(dim12(`Would create ${label}:`));
21072
+ console.log(dim11("─".repeat(60)));
21073
+ console.log(dim11(`Would create ${label}:`));
19866
21074
  console.log("");
19867
21075
  console.log(WORKFLOW_BLOCK);
19868
- console.log(dim12("─".repeat(60)));
21076
+ console.log(dim11("─".repeat(60)));
19869
21077
  return;
19870
21078
  }
19871
- writeFileSync4(filePath, WORKFLOW_BLOCK, "utf8");
21079
+ writeFileSync8(filePath, WORKFLOW_BLOCK, "utf8");
19872
21080
  ok4(`Created ${label} with Specialists Workflow section`);
19873
21081
  }
19874
21082
  console.log("");
19875
- console.log(` ${dim12("Next steps:")}`);
21083
+ console.log(` ${dim11("Next steps:")}`);
19876
21084
  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`);
21085
+ console.log(` • Run ${yellow8("specialists list")} to see available specialists`);
21086
+ console.log(` • Run ${yellow8("specialist_init")} in a new session to bootstrap context`);
19879
21087
  console.log("");
19880
21088
  }
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
21089
+ var bold9 = (s) => `\x1B[1m${s}\x1B[0m`, dim11 = (s) => `\x1B[2m${s}\x1B[0m`, green12 = (s) => `\x1B[32m${s}\x1B[0m`, yellow8 = (s) => `\x1B[33m${s}\x1B[0m`, MARKER = "## Specialists Workflow", WORKFLOW_BLOCK = `## Specialists Workflow
19882
21090
 
19883
21091
  > Injected by \`specialists setup\`. Keep this section — agents use it for context.
19884
21092
 
@@ -19946,61 +21154,104 @@ var init_setup = () => {};
19946
21154
  // src/cli/help.ts
19947
21155
  var exports_help = {};
19948
21156
  __export(exports_help, {
19949
- run: () => run15
21157
+ run: () => run17
19950
21158
  });
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
- ];
21159
+ function formatCommands(entries) {
21160
+ const width = Math.max(...entries.map(([cmd3]) => cmd3.length));
21161
+ return entries.map(([cmd3, desc]) => ` ${cmd3.padEnd(width)} ${desc}`);
19958
21162
  }
19959
- async function run15() {
21163
+ async function run17() {
19960
21164
  const lines = [
19961
21165
  "",
19962
- bold10("specialists <command> [options]"),
21166
+ "Specialists lets you run project-scoped specialist agents with a bead-first workflow.",
21167
+ "",
21168
+ bold10("Usage:"),
21169
+ " specialists|sp [command]",
21170
+ " specialists|sp [command] --help",
19963
21171
  "",
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),
21172
+ dim12(" sp is a shorter alias — sp run, sp list, sp feed etc. all work identically."),
19970
21173
  "",
19971
- dim13("Run 'specialists <command> --help' for command-specific options."),
19972
- dim13("Run 'specialists quickstart' for a full getting-started guide."),
21174
+ bold10("Common flows:"),
21175
+ "",
21176
+ " Tracked work (primary)",
21177
+ ' bd create "Task title" -t task -p 1 --json',
21178
+ " specialists run <name> --bead <id> [--context-depth N] [--background] [--follow]",
21179
+ " specialists feed -f",
21180
+ ' bd close <id> --reason "Done"',
21181
+ "",
21182
+ " Ad-hoc work",
21183
+ ' specialists run <name> --prompt "..."',
21184
+ "",
21185
+ " Rules",
21186
+ " --bead is for tracked work",
21187
+ " --prompt is for quick untracked work",
21188
+ " --context-depth defaults to 1 with --bead",
21189
+ " --no-beads does not disable bead reading",
21190
+ " --follow runs in background and streams output live",
21191
+ "",
21192
+ bold10("Core commands:"),
21193
+ ...formatCommands(CORE_COMMANDS),
21194
+ "",
21195
+ bold10("Extended commands:"),
21196
+ ...formatCommands(EXTENDED_COMMANDS),
21197
+ "",
21198
+ bold10("xtrm worktree commands:"),
21199
+ ...formatCommands(WORKTREE_COMMANDS),
21200
+ "",
21201
+ bold10("Examples:"),
21202
+ " specialists init",
21203
+ " specialists list",
21204
+ " specialists run bug-hunt --bead unitAI-123 --background",
21205
+ " specialists run sync-docs --follow # run + stream live output",
21206
+ ' specialists run codebase-explorer --prompt "Map the CLI architecture"',
21207
+ " specialists feed -f",
21208
+ ' specialists steer <job-id> "focus only on supervisor.ts"',
21209
+ ' specialists follow-up <job-id> "now write the fix"',
21210
+ " specialists result <job-id>",
21211
+ "",
21212
+ bold10("More help:"),
21213
+ " specialists quickstart Full guide and workflow reference",
21214
+ " specialists run --help Run command details and flags",
21215
+ " specialists steer --help Mid-run steering details",
21216
+ " specialists follow-up --help Multi-turn keep-alive details",
21217
+ " specialists init --help Bootstrap behavior and workflow injection",
21218
+ " specialists feed --help Background job monitoring details",
21219
+ "",
21220
+ dim12("Project model: specialists are project-only; user-scope discovery is deprecated."),
19973
21221
  ""
19974
21222
  ];
19975
21223
  console.log(lines.join(`
19976
21224
  `));
19977
21225
  }
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;
21226
+ var bold10 = (s) => `\x1B[1m${s}\x1B[0m`, dim12 = (s) => `\x1B[2m${s}\x1B[0m`, CORE_COMMANDS, EXTENDED_COMMANDS, WORKTREE_COMMANDS;
19979
21227
  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)"]
19991
- ];
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"]
21228
+ CORE_COMMANDS = [
21229
+ ["init", "Bootstrap a project: dirs, workflow injection, project MCP registration"],
21230
+ ["list", "List specialists in this project"],
21231
+ ["run", "Run a specialist with --bead for tracked work or --prompt for ad-hoc work"],
21232
+ ["feed", "Tail job events; use -f to follow all jobs"],
21233
+ ["result", "Print final output of a completed background job"],
21234
+ ["steer", "Send a mid-run message to a running background job"],
21235
+ ["follow-up", "Send a next-turn prompt to a keep-alive session (retains full context)"],
21236
+ ["stop", "Stop a running background job"],
21237
+ ["status", "Show health, MCP state, and active jobs"],
21238
+ ["doctor", "Diagnose installation/runtime problems"],
21239
+ ["quickstart", "Full getting-started guide"],
21240
+ ["help", "Show this help"]
20000
21241
  ];
20001
- OTHER = [
21242
+ EXTENDED_COMMANDS = [
21243
+ ["edit", "Edit a specialist field such as model or description"],
21244
+ ["models", "List models available on pi"],
20002
21245
  ["version", "Print installed version"],
20003
- ["help", "Show this help message"]
21246
+ ["setup", "[deprecated] Use specialists init instead"],
21247
+ ["install", "[deprecated] Use specialists init instead"]
21248
+ ];
21249
+ WORKTREE_COMMANDS = [
21250
+ ["xt pi [name]", "Start a Pi session in a sandboxed xt worktree"],
21251
+ ["xt claude [name]", "Start a Claude session in a sandboxed xt worktree"],
21252
+ ["xt attach [slug]", "Resume an existing xt worktree session"],
21253
+ ["xt worktree list", "List worktrees with runtime and activity"],
21254
+ ["xt end", "Close session, push, PR, cleanup"]
20004
21255
  ];
20005
21256
  });
20006
21257
 
@@ -26030,6 +27281,9 @@ class Protocol {
26030
27281
  }
26031
27282
  }
26032
27283
  async connect(transport) {
27284
+ if (this._transport) {
27285
+ throw new Error("Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.");
27286
+ }
26033
27287
  this._transport = transport;
26034
27288
  const _onclose = this.transport?.onclose;
26035
27289
  this._transport.onclose = () => {
@@ -26062,6 +27316,10 @@ class Protocol {
26062
27316
  this._progressHandlers.clear();
26063
27317
  this._taskProgressTokens.clear();
26064
27318
  this._pendingDebouncedNotifications.clear();
27319
+ for (const controller of this._requestHandlerAbortControllers.values()) {
27320
+ controller.abort();
27321
+ }
27322
+ this._requestHandlerAbortControllers.clear();
26065
27323
  const error2 = McpError.fromError(ErrorCode.ConnectionClosed, "Connection closed");
26066
27324
  this._transport = undefined;
26067
27325
  this.onclose?.();
@@ -26112,6 +27370,8 @@ class Protocol {
26112
27370
  sessionId: capturedTransport?.sessionId,
26113
27371
  _meta: request.params?._meta,
26114
27372
  sendNotification: async (notification) => {
27373
+ if (abortController.signal.aborted)
27374
+ return;
26115
27375
  const notificationOptions = { relatedRequestId: request.id };
26116
27376
  if (relatedTaskId) {
26117
27377
  notificationOptions.relatedTask = { taskId: relatedTaskId };
@@ -26119,6 +27379,9 @@ class Protocol {
26119
27379
  await this.notification(notification, notificationOptions);
26120
27380
  },
26121
27381
  sendRequest: async (r, resultSchema, options) => {
27382
+ if (abortController.signal.aborted) {
27383
+ throw new McpError(ErrorCode.ConnectionClosed, "Request was cancelled");
27384
+ }
26122
27385
  const requestOptions = { ...options, relatedRequestId: request.id };
26123
27386
  if (relatedTaskId && !requestOptions.relatedTask) {
26124
27387
  requestOptions.relatedTask = { taskId: relatedTaskId };
@@ -26732,6 +27995,62 @@ class ExperimentalServerTasks {
26732
27995
  requestStream(request, resultSchema, options) {
26733
27996
  return this._server.requestStream(request, resultSchema, options);
26734
27997
  }
27998
+ createMessageStream(params, options) {
27999
+ const clientCapabilities = this._server.getClientCapabilities();
28000
+ if ((params.tools || params.toolChoice) && !clientCapabilities?.sampling?.tools) {
28001
+ throw new Error("Client does not support sampling tools capability.");
28002
+ }
28003
+ if (params.messages.length > 0) {
28004
+ const lastMessage = params.messages[params.messages.length - 1];
28005
+ const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
28006
+ const hasToolResults = lastContent.some((c) => c.type === "tool_result");
28007
+ const previousMessage = params.messages.length > 1 ? params.messages[params.messages.length - 2] : undefined;
28008
+ const previousContent = previousMessage ? Array.isArray(previousMessage.content) ? previousMessage.content : [previousMessage.content] : [];
28009
+ const hasPreviousToolUse = previousContent.some((c) => c.type === "tool_use");
28010
+ if (hasToolResults) {
28011
+ if (lastContent.some((c) => c.type !== "tool_result")) {
28012
+ throw new Error("The last message must contain only tool_result content if any is present");
28013
+ }
28014
+ if (!hasPreviousToolUse) {
28015
+ throw new Error("tool_result blocks are not matching any tool_use from the previous message");
28016
+ }
28017
+ }
28018
+ if (hasPreviousToolUse) {
28019
+ const toolUseIds = new Set(previousContent.filter((c) => c.type === "tool_use").map((c) => c.id));
28020
+ const toolResultIds = new Set(lastContent.filter((c) => c.type === "tool_result").map((c) => c.toolUseId));
28021
+ if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every((id) => toolResultIds.has(id))) {
28022
+ throw new Error("ids of tool_result blocks and tool_use blocks from previous message do not match");
28023
+ }
28024
+ }
28025
+ }
28026
+ return this.requestStream({
28027
+ method: "sampling/createMessage",
28028
+ params
28029
+ }, CreateMessageResultSchema, options);
28030
+ }
28031
+ elicitInputStream(params, options) {
28032
+ const clientCapabilities = this._server.getClientCapabilities();
28033
+ const mode = params.mode ?? "form";
28034
+ switch (mode) {
28035
+ case "url": {
28036
+ if (!clientCapabilities?.elicitation?.url) {
28037
+ throw new Error("Client does not support url elicitation.");
28038
+ }
28039
+ break;
28040
+ }
28041
+ case "form": {
28042
+ if (!clientCapabilities?.elicitation?.form) {
28043
+ throw new Error("Client does not support form elicitation.");
28044
+ }
28045
+ break;
28046
+ }
28047
+ }
28048
+ const normalizedParams = mode === "form" && params.mode === undefined ? { ...params, mode: "form" } : params;
28049
+ return this.requestStream({
28050
+ method: "elicitation/create",
28051
+ params: normalizedParams
28052
+ }, ElicitResultSchema, options);
28053
+ }
26735
28054
  async getTask(taskId, options) {
26736
28055
  return this._server.getTask({ taskId }, options);
26737
28056
  }
@@ -27206,7 +28525,7 @@ class StdioServerTransport {
27206
28525
  }
27207
28526
 
27208
28527
  // src/server.ts
27209
- import { join as join3 } from "node:path";
28528
+ import { join as join7 } from "node:path";
27210
28529
 
27211
28530
  // src/constants.ts
27212
28531
  var LOG_PREFIX = "[specialists]";
@@ -27295,25 +28614,48 @@ function createListSpecialistsTool(loader) {
27295
28614
 
27296
28615
  // src/tools/specialist/use_specialist.tool.ts
27297
28616
  init_zod();
28617
+ init_beads();
27298
28618
  var useSpecialistSchema = exports_external.object({
27299
28619
  name: exports_external.string().describe("Specialist identifier (e.g. codebase-explorer)"),
27300
- prompt: exports_external.string().describe("The task or question for the specialist"),
28620
+ prompt: exports_external.string().optional().describe("The task or question for the specialist"),
28621
+ bead_id: exports_external.string().optional().describe("Use an existing bead as the specialist prompt"),
27301
28622
  variables: exports_external.record(exports_external.string()).optional().describe("Additional $variable substitutions"),
27302
28623
  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")
28624
+ autonomy_level: exports_external.string().optional().describe("Override permission level for this invocation"),
28625
+ context_depth: exports_external.number().optional().describe("Depth of blocker context injection (0 = none, 1 = immediate blockers, etc.)")
28626
+ }).refine((input) => Boolean(input.prompt?.trim() || input.bead_id), {
28627
+ message: "Either prompt or bead_id is required",
28628
+ path: ["prompt"]
27304
28629
  });
27305
28630
  function createUseSpecialistTool(runner) {
27306
28631
  return {
27307
28632
  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.",
28633
+ 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
28634
  inputSchema: useSpecialistSchema,
27310
28635
  async execute(input, onProgress) {
28636
+ let prompt = input.prompt?.trim() ?? "";
28637
+ let variables = input.variables;
28638
+ if (input.bead_id) {
28639
+ const beadsClient = new BeadsClient;
28640
+ const bead = beadsClient.readBead(input.bead_id);
28641
+ if (!bead) {
28642
+ throw new Error(`Unable to read bead '${input.bead_id}' via bd show --json`);
28643
+ }
28644
+ const beadContext = buildBeadContext(bead);
28645
+ prompt = beadContext;
28646
+ variables = {
28647
+ ...input.variables ?? {},
28648
+ bead_context: beadContext,
28649
+ bead_id: input.bead_id
28650
+ };
28651
+ }
27311
28652
  return runner.run({
27312
28653
  name: input.name,
27313
- prompt: input.prompt,
27314
- variables: input.variables,
28654
+ prompt,
28655
+ variables,
27315
28656
  backendOverride: input.backend_override,
27316
- autonomyLevel: input.autonomy_level
28657
+ autonomyLevel: input.autonomy_level,
28658
+ inputBeadId: input.bead_id
27317
28659
  }, onProgress);
27318
28660
  }
27319
28661
  };
@@ -27419,17 +28761,17 @@ function createSpecialistStatusTool(loader, circuitBreaker) {
27419
28761
  async execute(_) {
27420
28762
  const list = await loader.list();
27421
28763
  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");
28764
+ const { existsSync: existsSync4, readdirSync, readFileSync: readFileSync2 } = await import("node:fs");
28765
+ const { join: join3 } = await import("node:path");
28766
+ const jobsDir = join3(process.cwd(), ".specialists", "jobs");
27425
28767
  const jobs = [];
27426
- if (existsSync2(jobsDir)) {
28768
+ if (existsSync4(jobsDir)) {
27427
28769
  for (const entry of readdirSync(jobsDir)) {
27428
- const statusPath = join2(jobsDir, entry, "status.json");
27429
- if (!existsSync2(statusPath))
28770
+ const statusPath = join3(jobsDir, entry, "status.json");
28771
+ if (!existsSync4(statusPath))
27430
28772
  continue;
27431
28773
  try {
27432
- jobs.push(JSON.parse(readFileSync(statusPath, "utf-8")));
28774
+ jobs.push(JSON.parse(readFileSync2(statusPath, "utf-8")));
27433
28775
  } catch {}
27434
28776
  }
27435
28777
  jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
@@ -27508,6 +28850,74 @@ class JobRegistry {
27508
28850
  }
27509
28851
  job.killFn = killFn;
27510
28852
  }
28853
+ setSteerFn(id, steerFn) {
28854
+ const job = this.jobs.get(id);
28855
+ if (!job)
28856
+ return;
28857
+ job.steerFn = steerFn;
28858
+ }
28859
+ setResumeFn(id, resumeFn, closeFn) {
28860
+ const job = this.jobs.get(id);
28861
+ if (!job)
28862
+ return;
28863
+ job.resumeFn = resumeFn;
28864
+ job.closeFn = closeFn;
28865
+ job.status = "waiting";
28866
+ job.currentEvent = "waiting";
28867
+ }
28868
+ async followUp(id, message) {
28869
+ const job = this.jobs.get(id);
28870
+ if (!job)
28871
+ return { ok: false, error: `Job not found: ${id}` };
28872
+ if (job.status !== "waiting")
28873
+ return { ok: false, error: `Job is not waiting (status: ${job.status})` };
28874
+ if (!job.resumeFn)
28875
+ return { ok: false, error: "Job has no resume function" };
28876
+ job.status = "running";
28877
+ job.currentEvent = "starting";
28878
+ try {
28879
+ const output = await job.resumeFn(message);
28880
+ job.outputBuffer = output;
28881
+ job.status = "waiting";
28882
+ job.currentEvent = "waiting";
28883
+ return { ok: true, output };
28884
+ } catch (err) {
28885
+ job.status = "error";
28886
+ job.error = err?.message ?? String(err);
28887
+ return { ok: false, error: job.error };
28888
+ }
28889
+ }
28890
+ async closeSession(id) {
28891
+ const job = this.jobs.get(id);
28892
+ if (!job)
28893
+ return { ok: false, error: `Job not found: ${id}` };
28894
+ if (job.status !== "waiting")
28895
+ return { ok: false, error: `Job is not in waiting state` };
28896
+ try {
28897
+ await job.closeFn?.();
28898
+ job.status = "done";
28899
+ job.currentEvent = "done";
28900
+ job.endedAtMs = Date.now();
28901
+ return { ok: true };
28902
+ } catch (err) {
28903
+ return { ok: false, error: err?.message ?? String(err) };
28904
+ }
28905
+ }
28906
+ async steer(id, message) {
28907
+ const job = this.jobs.get(id);
28908
+ if (!job)
28909
+ return { ok: false, error: `Job not found: ${id}` };
28910
+ if (job.status !== "running")
28911
+ return { ok: false, error: `Job is not running (status: ${job.status})` };
28912
+ if (!job.steerFn)
28913
+ return { ok: false, error: "Job session not ready for steering yet" };
28914
+ try {
28915
+ await job.steerFn(message);
28916
+ return { ok: true };
28917
+ } catch (err) {
28918
+ return { ok: false, error: err?.message ?? String(err) };
28919
+ }
28920
+ }
27511
28921
  complete(id, result) {
27512
28922
  const job = this.jobs.get(id);
27513
28923
  if (!job || job.status !== "running")
@@ -27632,20 +29042,117 @@ function createStopSpecialistTool(registry2) {
27632
29042
  };
27633
29043
  }
27634
29044
 
29045
+ // src/tools/specialist/steer_specialist.tool.ts
29046
+ init_zod();
29047
+ init_supervisor();
29048
+ import { writeFileSync as writeFileSync2 } from "node:fs";
29049
+ import { join as join4 } from "node:path";
29050
+ var steerSpecialistSchema = exports_external.object({
29051
+ job_id: exports_external.string().describe("Job ID returned by start_specialist or specialists run --background"),
29052
+ message: exports_external.string().describe('Steering instruction to send to the running agent (e.g. "focus only on supervisor.ts")')
29053
+ });
29054
+ function createSteerSpecialistTool(registry2) {
29055
+ return {
29056
+ name: "steer_specialist",
29057
+ description: "Send a mid-run steering message to a running specialist job. The agent receives the message after its current tool calls finish, before the next LLM call. Works for both in-process jobs (start_specialist) and background CLI jobs (specialists run --background).",
29058
+ inputSchema: steerSpecialistSchema,
29059
+ async execute(input) {
29060
+ const snap = registry2.snapshot(input.job_id);
29061
+ if (snap) {
29062
+ const result = await registry2.steer(input.job_id, input.message);
29063
+ if (result.ok) {
29064
+ return { status: "steered", job_id: input.job_id, message: input.message };
29065
+ }
29066
+ return { status: "error", error: result.error, job_id: input.job_id };
29067
+ }
29068
+ const jobsDir = join4(process.cwd(), ".specialists", "jobs");
29069
+ const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
29070
+ const status = supervisor.readStatus(input.job_id);
29071
+ if (!status) {
29072
+ return { status: "error", error: `Job not found: ${input.job_id}`, job_id: input.job_id };
29073
+ }
29074
+ if (status.status === "done" || status.status === "error") {
29075
+ return { status: "error", error: `Job is already ${status.status}`, job_id: input.job_id };
29076
+ }
29077
+ if (!status.fifo_path) {
29078
+ return { status: "error", error: "Job has no steer pipe (may have been started without FIFO support)", job_id: input.job_id };
29079
+ }
29080
+ try {
29081
+ const payload = JSON.stringify({ type: "steer", message: input.message }) + `
29082
+ `;
29083
+ writeFileSync2(status.fifo_path, payload, { flag: "a" });
29084
+ return { status: "steered", job_id: input.job_id, message: input.message };
29085
+ } catch (err) {
29086
+ return { status: "error", error: `Failed to write to steer pipe: ${err?.message}`, job_id: input.job_id };
29087
+ }
29088
+ }
29089
+ };
29090
+ }
29091
+
29092
+ // src/tools/specialist/follow_up_specialist.tool.ts
29093
+ init_zod();
29094
+ init_supervisor();
29095
+ import { writeFileSync as writeFileSync3 } from "node:fs";
29096
+ import { join as join5 } from "node:path";
29097
+ var followUpSpecialistSchema = exports_external.object({
29098
+ job_id: exports_external.string().describe("Job ID of a waiting keep-alive specialist session"),
29099
+ message: exports_external.string().describe("Next prompt to send to the specialist (conversation history is retained)")
29100
+ });
29101
+ function createFollowUpSpecialistTool(registry2) {
29102
+ return {
29103
+ name: "follow_up_specialist",
29104
+ description: "Send a follow-up prompt to a waiting keep-alive specialist session. The Pi session retains full conversation history between turns. Only works for jobs started with keepAlive=true (CLI: --keep-alive --background).",
29105
+ inputSchema: followUpSpecialistSchema,
29106
+ async execute(input) {
29107
+ const snap = registry2.snapshot(input.job_id);
29108
+ if (snap) {
29109
+ if (snap.status !== "waiting") {
29110
+ return { status: "error", error: `Job is not waiting (status: ${snap.status})`, job_id: input.job_id };
29111
+ }
29112
+ const result = await registry2.followUp(input.job_id, input.message);
29113
+ if (result.ok) {
29114
+ return { status: "resumed", job_id: input.job_id, output: result.output };
29115
+ }
29116
+ return { status: "error", error: result.error, job_id: input.job_id };
29117
+ }
29118
+ const jobsDir = join5(process.cwd(), ".specialists", "jobs");
29119
+ const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
29120
+ const status = supervisor.readStatus(input.job_id);
29121
+ if (!status) {
29122
+ return { status: "error", error: `Job not found: ${input.job_id}`, job_id: input.job_id };
29123
+ }
29124
+ if (status.status !== "waiting") {
29125
+ return { status: "error", error: `Job is not waiting (status: ${status.status})`, job_id: input.job_id };
29126
+ }
29127
+ if (!status.fifo_path) {
29128
+ return { status: "error", error: "Job has no steer pipe", job_id: input.job_id };
29129
+ }
29130
+ try {
29131
+ const payload = JSON.stringify({ type: "prompt", message: input.message }) + `
29132
+ `;
29133
+ writeFileSync3(status.fifo_path, payload, { flag: "a" });
29134
+ return { status: "sent", job_id: input.job_id, message: input.message };
29135
+ } catch (err) {
29136
+ return { status: "error", error: `Failed to write to steer pipe: ${err?.message}`, job_id: input.job_id };
29137
+ }
29138
+ }
29139
+ };
29140
+ }
29141
+
27635
29142
  // src/server.ts
27636
29143
  init_zod();
27637
29144
 
27638
29145
  // src/tools/specialist/specialist_init.tool.ts
27639
29146
  init_zod();
27640
- import { spawnSync as spawnSync2 } from "node:child_process";
27641
- import { existsSync as existsSync2 } from "node:fs";
27642
- import { join as join2 } from "node:path";
29147
+ import { spawnSync as spawnSync4 } from "node:child_process";
29148
+ import { existsSync as existsSync5 } from "node:fs";
29149
+ import { join as join6 } from "node:path";
27643
29150
  var specialistInitSchema = objectType({});
27644
29151
  function createSpecialistInitTool(loader, deps) {
27645
29152
  const resolved = deps ?? {
27646
- bdAvailable: () => spawnSync2("bd", ["--version"], { stdio: "ignore" }).status === 0,
27647
- beadsExists: () => existsSync2(join2(process.cwd(), ".beads")),
27648
- bdInit: () => spawnSync2("bd", ["init"], { stdio: "ignore" })
29153
+ bdAvailable: () => spawnSync4("bd", ["--version"], { stdio: "ignore" }).status === 0,
29154
+ beadsExists: () => existsSync5(join6(process.cwd(), ".beads")),
29155
+ bdInit: () => spawnSync4("bd", ["init"], { stdio: "ignore" })
27649
29156
  };
27650
29157
  return {
27651
29158
  name: "specialist_init",
@@ -27679,7 +29186,7 @@ class SpecialistsServer {
27679
29186
  const circuitBreaker = new CircuitBreaker;
27680
29187
  const loader = new SpecialistLoader;
27681
29188
  const hooks = new HookEmitter({
27682
- tracePath: join3(process.cwd(), ".specialists", "trace.jsonl")
29189
+ tracePath: join7(process.cwd(), ".specialists", "trace.jsonl")
27683
29190
  });
27684
29191
  const beadsClient = new BeadsClient;
27685
29192
  const runner = new SpecialistRunner({ loader, hooks, circuitBreaker, beadsClient });
@@ -27692,6 +29199,8 @@ class SpecialistsServer {
27692
29199
  createStartSpecialistTool(runner, registry2),
27693
29200
  createPollSpecialistTool(registry2),
27694
29201
  createStopSpecialistTool(registry2),
29202
+ createSteerSpecialistTool(registry2),
29203
+ createFollowUpSpecialistTool(registry2),
27695
29204
  createSpecialistInitTool(loader)
27696
29205
  ];
27697
29206
  this.server = new Server({ name: MCP_CONFIG.SERVER_NAME, version: MCP_CONFIG.VERSION }, { capabilities: MCP_CONFIG.CAPABILITIES });
@@ -27707,6 +29216,8 @@ class SpecialistsServer {
27707
29216
  start_specialist: startSpecialistSchema,
27708
29217
  poll_specialist: pollSpecialistSchema,
27709
29218
  stop_specialist: stopSpecialistSchema,
29219
+ steer_specialist: steerSpecialistSchema,
29220
+ follow_up_specialist: followUpSpecialistSchema,
27710
29221
  specialist_init: specialistInitSchema
27711
29222
  };
27712
29223
  this.toolSchemas = schemaMap;
@@ -27779,15 +29290,15 @@ var next = process.argv[3];
27779
29290
  function wantsHelp() {
27780
29291
  return next === "--help" || next === "-h";
27781
29292
  }
27782
- async function run16() {
29293
+ async function run18() {
27783
29294
  if (sub === "install") {
27784
29295
  if (wantsHelp()) {
27785
29296
  console.log([
27786
29297
  "",
27787
29298
  "Usage: specialists install",
27788
29299
  "",
27789
- "Full-stack setup: installs pi, beads, dolt, registers the MCP server,",
27790
- "and installs session hooks and skills for Claude Code.",
29300
+ "Project setup: checks pi/bd/xt prerequisites, registers the MCP server,",
29301
+ "and installs specialists-specific project hooks.",
27791
29302
  "",
27792
29303
  "No flags — just run it.",
27793
29304
  ""
@@ -27808,18 +29319,24 @@ async function run16() {
27808
29319
  "",
27809
29320
  "Usage: specialists list [options]",
27810
29321
  "",
27811
- "List available specialists across all scopes.",
29322
+ "List specialists in the current project.",
29323
+ "",
29324
+ "What it shows:",
29325
+ " - specialist name",
29326
+ " - model",
29327
+ " - short description",
27812
29328
  "",
27813
29329
  "Options:",
27814
- " --scope <project|user> Filter by scope",
27815
- " --category <name> Filter by category tag",
27816
- " --json Output as JSON array",
29330
+ " --category <name> Filter by category tag",
29331
+ " --json Output as JSON array",
27817
29332
  "",
27818
29333
  "Examples:",
27819
29334
  " specialists list",
27820
- " specialists list --scope project",
27821
29335
  " specialists list --category analysis",
27822
29336
  " specialists list --json",
29337
+ "",
29338
+ "Project model:",
29339
+ " Specialists are project-only. User-scope discovery is deprecated.",
27823
29340
  ""
27824
29341
  ].join(`
27825
29342
  `));
@@ -27849,15 +29366,27 @@ async function run16() {
27849
29366
  if (wantsHelp()) {
27850
29367
  console.log([
27851
29368
  "",
27852
- "Usage: specialists init",
29369
+ "Usage: specialists init [--force-workflow]",
29370
+ "",
29371
+ "Bootstrap a project for specialists. This is the sole onboarding command.",
29372
+ "",
29373
+ "What it does:",
29374
+ " • creates specialists/ for project .specialist.yaml files",
29375
+ " • creates .specialists/ runtime dirs (jobs/, ready/)",
29376
+ " • adds .specialists/ to .gitignore",
29377
+ " • injects the managed workflow block into AGENTS.md and CLAUDE.md",
29378
+ " • registers the Specialists MCP server at project scope",
27853
29379
  "",
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",
29380
+ "Options:",
29381
+ " --force-workflow Overwrite existing managed workflow blocks",
29382
+ "",
29383
+ "Examples:",
29384
+ " specialists init",
29385
+ " specialists init --force-workflow",
27859
29386
  "",
27860
- "Safe to run on an existing project (skips already-present items).",
29387
+ "Notes:",
29388
+ " setup and install are deprecated; use specialists init.",
29389
+ " Safe to run again; existing project state is preserved where possible.",
27861
29390
  ""
27862
29391
  ].join(`
27863
29392
  `));
@@ -27904,24 +29433,31 @@ async function run16() {
27904
29433
  "",
27905
29434
  "Usage: specialists run <name> [options]",
27906
29435
  "",
27907
- "Run a specialist. Streams output to stdout by default.",
27908
- "Reads prompt from stdin if --prompt is not provided.",
29436
+ "Run a specialist in foreground or background.",
29437
+ "",
29438
+ "Primary modes:",
29439
+ " tracked: specialists run <name> --bead <id>",
29440
+ ' ad-hoc: specialists run <name> --prompt "..."',
27909
29441
  "",
27910
29442
  "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",
29443
+ " --bead <id> Use an existing bead as the prompt source",
29444
+ " --prompt <text> Ad-hoc prompt for untracked work",
29445
+ " --context-depth <n> Dependency context depth when using --bead (default: 1)",
29446
+ " --no-beads Do not create a new tracking bead (does not disable bead reading)",
29447
+ " --background Start async and return a job id",
29448
+ " --follow Run in background and stream output live",
29449
+ " --model <model> Override the configured model for this run",
27915
29450
  "",
27916
29451
  "Examples:",
29452
+ " specialists run bug-hunt --bead unitAI-55d",
29453
+ " specialists run bug-hunt --bead unitAI-55d --context-depth 2 --background",
29454
+ " specialists run sync-docs --follow",
27917
29455
  ' 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 "..."',
29456
+ " cat brief.md | specialists run report-generator",
27921
29457
  "",
27922
- "See also:",
27923
- " specialists feed --help (tail events for a background job)",
27924
- " specialists result --help (read background job output)",
29458
+ "Rules:",
29459
+ " Use --bead for tracked work.",
29460
+ " Use --prompt for quick ad-hoc work.",
27925
29461
  ""
27926
29462
  ].join(`
27927
29463
  `));
@@ -27936,11 +29472,17 @@ async function run16() {
27936
29472
  "",
27937
29473
  "Usage: specialists status [options]",
27938
29474
  "",
27939
- "Show system health: pi runtime, beads installation, MCP registration,",
27940
- "and all active background jobs.",
29475
+ "Show current runtime state.",
29476
+ "",
29477
+ "Sections include:",
29478
+ " - discovered specialists",
29479
+ " - pi provider/runtime health",
29480
+ " - beads availability",
29481
+ " - MCP registration hints",
29482
+ " - active background jobs",
27941
29483
  "",
27942
29484
  "Options:",
27943
- " --json Output as JSON",
29485
+ " --json Output machine-readable JSON",
27944
29486
  "",
27945
29487
  "Examples:",
27946
29488
  " specialists status",
@@ -27982,20 +29524,24 @@ async function run16() {
27982
29524
  console.log([
27983
29525
  "",
27984
29526
  "Usage: specialists feed <job-id> [options]",
27985
- " specialists feed --job <job-id> [options]",
29527
+ " specialists feed -f [--forever]",
27986
29528
  "",
27987
- "Print events emitted by a background job.",
29529
+ "Read background job events.",
29530
+ "",
29531
+ "Modes:",
29532
+ " specialists feed <job-id> Replay events for one job",
29533
+ " specialists feed <job-id> -f Follow one job until completion",
29534
+ " specialists feed -f Follow all jobs globally",
27988
29535
  "",
27989
29536
  "Options:",
27990
- " --follow, -f Stay open and stream new events as they arrive",
27991
- " (exits automatically when job completes)",
29537
+ " -f, --follow Follow live updates",
29538
+ " --forever Keep following in global mode even when all jobs complete",
27992
29539
  "",
27993
29540
  "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",
29541
+ " specialists feed 49adda",
29542
+ " specialists feed 49adda --follow",
29543
+ " specialists feed -f",
29544
+ " specialists feed -f --forever",
27999
29545
  ""
28000
29546
  ].join(`
28001
29547
  `));
@@ -28004,6 +29550,65 @@ async function run16() {
28004
29550
  const { run: handler } = await Promise.resolve().then(() => (init_feed(), exports_feed));
28005
29551
  return handler();
28006
29552
  }
29553
+ if (sub === "steer") {
29554
+ if (wantsHelp()) {
29555
+ console.log([
29556
+ "",
29557
+ 'Usage: specialists steer <job-id> "<message>"',
29558
+ "",
29559
+ "Send a mid-run steering message to a running background specialist job.",
29560
+ "The agent receives the message after its current tool calls finish,",
29561
+ "before the next LLM call.",
29562
+ "",
29563
+ 'Pi RPC steer command: {"type":"steer","message":"..."}',
29564
+ 'Response: {"type":"response","command":"steer","success":true}',
29565
+ "",
29566
+ "Examples:",
29567
+ ' specialists steer a1b2c3 "focus only on supervisor.ts"',
29568
+ ' specialists steer a1b2c3 "skip tests, just fix the bug"',
29569
+ "",
29570
+ "Notes:",
29571
+ " - Only works for jobs started with --background.",
29572
+ " - Delivery is best-effort: the agent processes it on its next turn.",
29573
+ ""
29574
+ ].join(`
29575
+ `));
29576
+ return;
29577
+ }
29578
+ const { run: handler } = await Promise.resolve().then(() => (init_steer(), exports_steer));
29579
+ return handler();
29580
+ }
29581
+ if (sub === "follow-up") {
29582
+ if (wantsHelp()) {
29583
+ console.log([
29584
+ "",
29585
+ 'Usage: specialists follow-up <job-id> "<message>"',
29586
+ "",
29587
+ "Send a follow-up prompt to a waiting keep-alive specialist session.",
29588
+ "The Pi session retains full conversation history between turns.",
29589
+ "",
29590
+ "Requires: job started with --keep-alive --background.",
29591
+ "",
29592
+ "Examples:",
29593
+ ' specialists follow-up a1b2c3 "Now write the fix for the bug you found"',
29594
+ ' specialists follow-up a1b2c3 "Focus only on the auth module"',
29595
+ "",
29596
+ "Workflow:",
29597
+ " specialists run bug-hunt --bead <id> --keep-alive --background",
29598
+ " # → Job started: a1b2c3 (status: waiting after first turn)",
29599
+ " specialists result a1b2c3 # read first turn output",
29600
+ ' specialists follow-up a1b2c3 "..." # send next prompt',
29601
+ " specialists feed a1b2c3 --follow # watch response",
29602
+ "",
29603
+ "See also: specialists steer (mid-run redirect)",
29604
+ ""
29605
+ ].join(`
29606
+ `));
29607
+ return;
29608
+ }
29609
+ const { run: handler } = await Promise.resolve().then(() => (init_follow_up(), exports_follow_up));
29610
+ return handler();
29611
+ }
28007
29612
  if (sub === "stop") {
28008
29613
  if (wantsHelp()) {
28009
29614
  console.log([
@@ -28033,15 +29638,20 @@ async function run16() {
28033
29638
  "",
28034
29639
  "Usage: specialists doctor",
28035
29640
  "",
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)",
29641
+ "Diagnose bootstrap and runtime problems.",
29642
+ "",
29643
+ "Checks:",
29644
+ " 1. pi installed and has active providers",
29645
+ " 2. beads installed and .beads/ present",
29646
+ " 3. xtrm-tools availability",
29647
+ " 4. Specialists MCP registration in .mcp.json",
29648
+ " 5. .specialists/ runtime directories",
29649
+ " 6. hook wiring expectations",
29650
+ " 7. zombie job detection",
28042
29651
  "",
28043
- "Prints fix hints for each failure.",
28044
- "Auto-creates missing runtime directories.",
29652
+ "Behavior:",
29653
+ " - prints fix hints for failing checks",
29654
+ " - auto-creates missing runtime directories when possible",
28045
29655
  "",
28046
29656
  "Examples:",
28047
29657
  " specialists doctor",
@@ -28094,7 +29704,7 @@ Run 'specialists help' to see available commands.`);
28094
29704
  const server = new SpecialistsServer;
28095
29705
  await server.start();
28096
29706
  }
28097
- run16().catch((error2) => {
29707
+ run18().catch((error2) => {
28098
29708
  logger.error(`Fatal error: ${error2}`);
28099
29709
  process.exit(1);
28100
29710
  });