@jaggerxtrm/specialists 2.1.21 → 3.0.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
@@ -17435,6 +17435,7 @@ var init_schema = __esm(() => {
17435
17435
  model: stringType(),
17436
17436
  fallback_model: stringType().optional(),
17437
17437
  timeout_ms: numberType().default(120000),
17438
+ stall_timeout_ms: numberType().optional(),
17438
17439
  response_format: enumType(["text", "json", "markdown"]).default("text"),
17439
17440
  permission_required: enumType(["READ_ONLY", "LOW", "MEDIUM", "HIGH"]).default("READ_ONLY"),
17440
17441
  preferred_profile: stringType().optional(),
@@ -17455,7 +17456,8 @@ var init_schema = __esm(() => {
17455
17456
  inject_output: booleanType().default(false)
17456
17457
  })).optional(),
17457
17458
  references: arrayType(unknownType()).optional(),
17458
- tools: arrayType(stringType()).optional()
17459
+ tools: arrayType(stringType()).optional(),
17460
+ paths: arrayType(stringType()).optional()
17459
17461
  }).optional();
17460
17462
  CapabilitiesSchema = objectType({
17461
17463
  file_scope: arrayType(stringType()).optional(),
@@ -17570,6 +17572,19 @@ class SpecialistLoader {
17570
17572
  if (existsSync(filePath)) {
17571
17573
  const content = await readFile(filePath, "utf-8");
17572
17574
  const spec = await parseSpecialist(content);
17575
+ const rawPaths = spec.specialist.skills?.paths;
17576
+ if (rawPaths?.length) {
17577
+ const home = homedir();
17578
+ const fileDir = dir.path;
17579
+ const resolved = rawPaths.map((p) => {
17580
+ if (p.startsWith("~/"))
17581
+ return join(home, p.slice(2));
17582
+ if (p.startsWith("./"))
17583
+ return join(fileDir, p.slice(2));
17584
+ return p;
17585
+ });
17586
+ spec.specialist.skills.paths = resolved;
17587
+ }
17573
17588
  this.cache.set(name, spec);
17574
17589
  return spec;
17575
17590
  }
@@ -17631,6 +17646,10 @@ function mapPermissionToTools(level) {
17631
17646
  return "read,bash,grep,find,ls";
17632
17647
  case "BASH_ONLY":
17633
17648
  return "bash";
17649
+ case "LOW":
17650
+ case "MEDIUM":
17651
+ case "HIGH":
17652
+ return "read,bash,edit,write,grep,find,ls";
17634
17653
  default:
17635
17654
  return;
17636
17655
  }
@@ -17645,6 +17664,7 @@ class PiAgentSession {
17645
17664
  _agentEndReceived = false;
17646
17665
  _killed = false;
17647
17666
  _lineBuffer = "";
17667
+ _pendingCommand;
17648
17668
  meta;
17649
17669
  constructor(options, meta) {
17650
17670
  this.options = options;
@@ -17719,6 +17739,12 @@ class PiAgentSession {
17719
17739
  return;
17720
17740
  }
17721
17741
  const { type } = event;
17742
+ if (type === "response") {
17743
+ const handler = this._pendingCommand;
17744
+ this._pendingCommand = undefined;
17745
+ handler?.(event);
17746
+ return;
17747
+ }
17722
17748
  if (type === "message_start" && event.message?.role === "assistant") {
17723
17749
  const { provider, model } = event.message ?? {};
17724
17750
  if (provider || model) {
@@ -17783,17 +17809,66 @@ class PiAgentSession {
17783
17809
  }
17784
17810
  }
17785
17811
  }
17812
+ sendCommand(cmd) {
17813
+ return new Promise((resolve, reject) => {
17814
+ if (!this.proc?.stdin) {
17815
+ reject(new Error("No stdin available"));
17816
+ return;
17817
+ }
17818
+ this._pendingCommand = resolve;
17819
+ this.proc.stdin.write(JSON.stringify(cmd) + `
17820
+ `, (err) => {
17821
+ if (err) {
17822
+ this._pendingCommand = undefined;
17823
+ reject(err);
17824
+ }
17825
+ });
17826
+ });
17827
+ }
17786
17828
  async prompt(task) {
17787
17829
  const msg = JSON.stringify({ type: "prompt", message: task }) + `
17788
17830
  `;
17789
17831
  this.proc?.stdin?.write(msg);
17790
- this.proc?.stdin?.end();
17791
17832
  }
17792
- async waitForDone() {
17793
- return this._donePromise;
17833
+ async waitForDone(timeout) {
17834
+ const donePromise = this._donePromise;
17835
+ if (!timeout)
17836
+ return donePromise;
17837
+ return Promise.race([
17838
+ donePromise,
17839
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Specialist timed out after ${timeout}ms`)), timeout))
17840
+ ]);
17794
17841
  }
17795
17842
  async getLastOutput() {
17796
- return this._lastOutput;
17843
+ if (!this.proc?.stdin || !this.proc.stdin.writable) {
17844
+ return this._lastOutput;
17845
+ }
17846
+ try {
17847
+ const response = await Promise.race([
17848
+ this.sendCommand({ type: "get_last_assistant_text" }),
17849
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000))
17850
+ ]);
17851
+ return response?.data?.text ?? this._lastOutput;
17852
+ } catch {
17853
+ return this._lastOutput;
17854
+ }
17855
+ }
17856
+ async getState() {
17857
+ try {
17858
+ const response = await Promise.race([
17859
+ this.sendCommand({ type: "get_state" }),
17860
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000))
17861
+ ]);
17862
+ return response?.data;
17863
+ } catch {
17864
+ return null;
17865
+ }
17866
+ }
17867
+ async close() {
17868
+ if (this._killed)
17869
+ return;
17870
+ this.proc?.stdin?.end();
17871
+ await this._donePromise.catch(() => {});
17797
17872
  }
17798
17873
  kill() {
17799
17874
  if (this._killed)
@@ -17940,9 +18015,9 @@ class SpecialistRunner {
17940
18015
  estimated_tokens: Math.ceil(renderedTask.length / 4),
17941
18016
  system_prompt_present: !!prompt.system
17942
18017
  });
18018
+ const { readFile: readFile2 } = await import("node:fs/promises");
17943
18019
  let agentsMd = prompt.system ?? "";
17944
18020
  if (prompt.skill_inherit) {
17945
- const { readFile: readFile2 } = await import("node:fs/promises");
17946
18021
  const skillContent = await readFile2(prompt.skill_inherit, "utf-8").catch(() => "");
17947
18022
  if (skillContent)
17948
18023
  agentsMd += `
@@ -17950,6 +18025,17 @@ class SpecialistRunner {
17950
18025
  ---
17951
18026
  # Service Knowledge
17952
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
+
17953
18039
  ${skillContent}`;
17954
18040
  }
17955
18041
  if (spec.specialist.capabilities?.diagnostic_scripts?.length) {
@@ -17998,10 +18084,11 @@ You have access via Bash:
17998
18084
  await session.start();
17999
18085
  onKillRegistered?.(session.kill.bind(session));
18000
18086
  await session.prompt(renderedTask);
18001
- await session.waitForDone();
18087
+ await session.waitForDone(execution.timeout_ms);
18002
18088
  sessionBackend = session.meta.backend;
18003
18089
  output = await session.getLastOutput();
18004
18090
  sessionBackend = session.meta.backend;
18091
+ await session.close();
18005
18092
  const postScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "post") ?? [];
18006
18093
  for (const script of postScripts)
18007
18094
  runScript(script.path);
@@ -18372,6 +18459,32 @@ ${bold3("specialists init")}
18372
18459
  mkdirSync(specialistsDir, { recursive: true });
18373
18460
  ok("created specialists/");
18374
18461
  }
18462
+ const runtimeDir = join5(cwd, ".specialists");
18463
+ if (existsSync3(runtimeDir)) {
18464
+ skip(".specialists/ already exists");
18465
+ } else {
18466
+ mkdirSync(join5(runtimeDir, "jobs"), { recursive: true });
18467
+ mkdirSync(join5(runtimeDir, "ready"), { recursive: true });
18468
+ ok("created .specialists/ (jobs/, ready/)");
18469
+ }
18470
+ const gitignorePath = join5(cwd, ".gitignore");
18471
+ if (existsSync3(gitignorePath)) {
18472
+ const existing = readFileSync(gitignorePath, "utf-8");
18473
+ if (existing.includes(GITIGNORE_ENTRY)) {
18474
+ skip(".gitignore already has .specialists/ entry");
18475
+ } else {
18476
+ const separator = existing.endsWith(`
18477
+ `) ? "" : `
18478
+ `;
18479
+ writeFileSync(gitignorePath, existing + separator + GITIGNORE_ENTRY + `
18480
+ `, "utf-8");
18481
+ ok("added .specialists/ to .gitignore");
18482
+ }
18483
+ } else {
18484
+ writeFileSync(gitignorePath, GITIGNORE_ENTRY + `
18485
+ `, "utf-8");
18486
+ ok("created .gitignore with .specialists/ entry");
18487
+ }
18375
18488
  const agentsPath = join5(cwd, "AGENTS.md");
18376
18489
  if (existsSync3(agentsPath)) {
18377
18490
  const existing = readFileSync(agentsPath, "utf-8");
@@ -18396,7 +18509,7 @@ ${bold3("Done!")}
18396
18509
  console.log(` 3. Restart Claude Code to pick up AGENTS.md changes
18397
18510
  `);
18398
18511
  }
18399
- 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";
18512
+ var bold3 = (s) => `\x1B[1m${s}\x1B[0m`, green2 = (s) => `\x1B[32m${s}\x1B[0m`, yellow3 = (s) => `\x1B[33m${s}\x1B[0m`, dim3 = (s) => `\x1B[2m${s}\x1B[0m`, AGENTS_BLOCK, AGENTS_MARKER = "## Specialists", GITIGNORE_ENTRY = ".specialists/";
18400
18513
  var init_init = __esm(() => {
18401
18514
  AGENTS_BLOCK = `
18402
18515
  ## Specialists
@@ -18538,21 +18651,220 @@ var init_edit = __esm(() => {
18538
18651
  VALID_PERMISSIONS = ["READ_ONLY", "LOW", "MEDIUM", "HIGH"];
18539
18652
  });
18540
18653
 
18654
+ // src/specialist/supervisor.ts
18655
+ import {
18656
+ closeSync,
18657
+ existsSync as existsSync4,
18658
+ mkdirSync as mkdirSync2,
18659
+ openSync,
18660
+ readdirSync,
18661
+ readFileSync as readFileSync3,
18662
+ renameSync,
18663
+ rmSync,
18664
+ statSync,
18665
+ writeFileSync as writeFileSync3,
18666
+ writeSync
18667
+ } from "node:fs";
18668
+ import { join as join6 } from "node:path";
18669
+
18670
+ class Supervisor {
18671
+ opts;
18672
+ constructor(opts) {
18673
+ this.opts = opts;
18674
+ }
18675
+ jobDir(id) {
18676
+ return join6(this.opts.jobsDir, id);
18677
+ }
18678
+ statusPath(id) {
18679
+ return join6(this.jobDir(id), "status.json");
18680
+ }
18681
+ resultPath(id) {
18682
+ return join6(this.jobDir(id), "result.txt");
18683
+ }
18684
+ eventsPath(id) {
18685
+ return join6(this.jobDir(id), "events.jsonl");
18686
+ }
18687
+ readyDir() {
18688
+ return join6(this.opts.jobsDir, "..", "ready");
18689
+ }
18690
+ readStatus(id) {
18691
+ const path = this.statusPath(id);
18692
+ if (!existsSync4(path))
18693
+ return null;
18694
+ try {
18695
+ return JSON.parse(readFileSync3(path, "utf-8"));
18696
+ } catch {
18697
+ return null;
18698
+ }
18699
+ }
18700
+ listJobs() {
18701
+ if (!existsSync4(this.opts.jobsDir))
18702
+ return [];
18703
+ const jobs = [];
18704
+ for (const entry of readdirSync(this.opts.jobsDir)) {
18705
+ const path = join6(this.opts.jobsDir, entry, "status.json");
18706
+ if (!existsSync4(path))
18707
+ continue;
18708
+ try {
18709
+ jobs.push(JSON.parse(readFileSync3(path, "utf-8")));
18710
+ } catch {}
18711
+ }
18712
+ return jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
18713
+ }
18714
+ writeStatusFile(id, data) {
18715
+ const path = this.statusPath(id);
18716
+ const tmp = path + ".tmp";
18717
+ writeFileSync3(tmp, JSON.stringify(data, null, 2), "utf-8");
18718
+ renameSync(tmp, path);
18719
+ }
18720
+ updateStatus(id, updates) {
18721
+ const current = this.readStatus(id);
18722
+ if (!current)
18723
+ return;
18724
+ this.writeStatusFile(id, { ...current, ...updates });
18725
+ }
18726
+ gc() {
18727
+ if (!existsSync4(this.opts.jobsDir))
18728
+ return;
18729
+ const cutoff = Date.now() - JOB_TTL_DAYS * 86400000;
18730
+ for (const entry of readdirSync(this.opts.jobsDir)) {
18731
+ const dir = join6(this.opts.jobsDir, entry);
18732
+ try {
18733
+ const stat2 = statSync(dir);
18734
+ if (!stat2.isDirectory())
18735
+ continue;
18736
+ if (stat2.mtimeMs < cutoff)
18737
+ rmSync(dir, { recursive: true, force: true });
18738
+ } catch {}
18739
+ }
18740
+ }
18741
+ crashRecovery() {
18742
+ if (!existsSync4(this.opts.jobsDir))
18743
+ return;
18744
+ for (const entry of readdirSync(this.opts.jobsDir)) {
18745
+ const statusPath = join6(this.opts.jobsDir, entry, "status.json");
18746
+ if (!existsSync4(statusPath))
18747
+ continue;
18748
+ try {
18749
+ const s = JSON.parse(readFileSync3(statusPath, "utf-8"));
18750
+ if (s.status !== "running" && s.status !== "starting")
18751
+ continue;
18752
+ if (!s.pid)
18753
+ continue;
18754
+ try {
18755
+ process.kill(s.pid, 0);
18756
+ } catch {
18757
+ const tmp = statusPath + ".tmp";
18758
+ const updated = { ...s, status: "error", error: "Process crashed or was killed" };
18759
+ writeFileSync3(tmp, JSON.stringify(updated, null, 2), "utf-8");
18760
+ renameSync(tmp, statusPath);
18761
+ }
18762
+ } catch {}
18763
+ }
18764
+ }
18765
+ async run() {
18766
+ const { runner, runOptions, jobsDir } = this.opts;
18767
+ this.gc();
18768
+ this.crashRecovery();
18769
+ const id = crypto.randomUUID().slice(0, 6);
18770
+ const dir = this.jobDir(id);
18771
+ const startedAtMs = Date.now();
18772
+ mkdirSync2(dir, { recursive: true });
18773
+ mkdirSync2(this.readyDir(), { recursive: true });
18774
+ const initialStatus = {
18775
+ id,
18776
+ specialist: runOptions.name,
18777
+ status: "starting",
18778
+ started_at_ms: startedAtMs,
18779
+ pid: process.pid
18780
+ };
18781
+ this.writeStatusFile(id, initialStatus);
18782
+ const eventsFd = openSync(this.eventsPath(id), "a");
18783
+ const appendEvent = (obj) => {
18784
+ try {
18785
+ writeSync(eventsFd, JSON.stringify({ t: Date.now(), ...obj }) + `
18786
+ `);
18787
+ } catch {}
18788
+ };
18789
+ let textLogged = false;
18790
+ let currentTool = "";
18791
+ try {
18792
+ const result = await runner.run(runOptions, (delta) => {
18793
+ const toolMatch = delta.match(/⚙ (.+?)…/);
18794
+ if (toolMatch) {
18795
+ currentTool = toolMatch[1];
18796
+ this.updateStatus(id, { current_tool: currentTool });
18797
+ }
18798
+ }, (eventType) => {
18799
+ const now = Date.now();
18800
+ this.updateStatus(id, {
18801
+ status: "running",
18802
+ current_event: eventType,
18803
+ last_event_at_ms: now,
18804
+ elapsed_s: Math.round((now - startedAtMs) / 1000)
18805
+ });
18806
+ if (LOGGED_EVENTS.has(eventType)) {
18807
+ const tool = eventType === "toolcall" || eventType === "tool_execution_end" ? currentTool : undefined;
18808
+ appendEvent({ type: eventType, ...tool ? { tool } : {} });
18809
+ } else if (eventType === "text" && !textLogged) {
18810
+ textLogged = true;
18811
+ appendEvent({ type: "text" });
18812
+ }
18813
+ }, (meta) => {
18814
+ this.updateStatus(id, { model: meta.model, backend: meta.backend });
18815
+ appendEvent({ type: "meta", model: meta.model, backend: meta.backend });
18816
+ }, (_killFn) => {}, (beadId) => {
18817
+ this.updateStatus(id, { bead_id: beadId });
18818
+ });
18819
+ const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
18820
+ writeFileSync3(this.resultPath(id), result.output, "utf-8");
18821
+ this.updateStatus(id, {
18822
+ status: "done",
18823
+ elapsed_s: elapsed,
18824
+ last_event_at_ms: Date.now(),
18825
+ model: result.model,
18826
+ backend: result.backend,
18827
+ bead_id: result.beadId
18828
+ });
18829
+ appendEvent({ type: "agent_end", elapsed_s: elapsed });
18830
+ writeFileSync3(join6(this.readyDir(), id), "", "utf-8");
18831
+ return id;
18832
+ } catch (err) {
18833
+ const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
18834
+ this.updateStatus(id, {
18835
+ status: "error",
18836
+ elapsed_s: elapsed,
18837
+ error: err?.message ?? String(err)
18838
+ });
18839
+ appendEvent({ type: "error", message: err?.message ?? String(err) });
18840
+ throw err;
18841
+ } finally {
18842
+ closeSync(eventsFd);
18843
+ }
18844
+ }
18845
+ }
18846
+ var JOB_TTL_DAYS, LOGGED_EVENTS;
18847
+ var init_supervisor = __esm(() => {
18848
+ JOB_TTL_DAYS = Number(process.env.SPECIALISTS_JOB_TTL_DAYS ?? 7);
18849
+ LOGGED_EVENTS = new Set(["thinking", "toolcall", "tool_execution_end", "done"]);
18850
+ });
18851
+
18541
18852
  // src/cli/run.ts
18542
18853
  var exports_run = {};
18543
18854
  __export(exports_run, {
18544
18855
  run: () => run7
18545
18856
  });
18546
- import { join as join6 } from "node:path";
18857
+ import { join as join7 } from "node:path";
18547
18858
  async function parseArgs4(argv) {
18548
18859
  const name = argv[0];
18549
18860
  if (!name || name.startsWith("--")) {
18550
- console.error('Usage: specialists run <name> [--prompt "..."] [--model <model>] [--no-beads]');
18861
+ console.error('Usage: specialists run <name> [--prompt "..."] [--model <model>] [--no-beads] [--background]');
18551
18862
  process.exit(1);
18552
18863
  }
18553
18864
  let prompt = "";
18554
18865
  let model;
18555
18866
  let noBeads = false;
18867
+ let background = false;
18556
18868
  for (let i = 1;i < argv.length; i++) {
18557
18869
  const token = argv[i];
18558
18870
  if (token === "--prompt" && argv[i + 1]) {
@@ -18567,6 +18879,10 @@ async function parseArgs4(argv) {
18567
18879
  noBeads = true;
18568
18880
  continue;
18569
18881
  }
18882
+ if (token === "--background") {
18883
+ background = true;
18884
+ continue;
18885
+ }
18570
18886
  }
18571
18887
  if (!prompt) {
18572
18888
  if (process.stdin.isTTY) {
@@ -18581,13 +18897,13 @@ async function parseArgs4(argv) {
18581
18897
  process.stdin.on("end", () => resolve(buf.trim()));
18582
18898
  });
18583
18899
  }
18584
- return { name, prompt, model, noBeads };
18900
+ return { name, prompt, model, noBeads, background };
18585
18901
  }
18586
18902
  async function run7() {
18587
18903
  const args = await parseArgs4(process.argv.slice(3));
18588
18904
  const loader = new SpecialistLoader;
18589
18905
  const circuitBreaker = new CircuitBreaker;
18590
- const hooks = new HookEmitter({ tracePath: join6(process.cwd(), ".specialists", "trace.jsonl") });
18906
+ const hooks = new HookEmitter({ tracePath: join7(process.cwd(), ".specialists", "trace.jsonl") });
18591
18907
  const beadsClient = args.noBeads ? null : new BeadsClient;
18592
18908
  const runner = new SpecialistRunner({
18593
18909
  loader,
@@ -18595,6 +18911,24 @@ async function run7() {
18595
18911
  circuitBreaker,
18596
18912
  beadsClient: beadsClient ?? undefined
18597
18913
  });
18914
+ if (args.background) {
18915
+ const jobsDir = join7(process.cwd(), ".specialists", "jobs");
18916
+ const supervisor = new Supervisor({
18917
+ runner,
18918
+ runOptions: { name: args.name, prompt: args.prompt, backendOverride: args.model },
18919
+ jobsDir
18920
+ });
18921
+ try {
18922
+ const jobId = await supervisor.run();
18923
+ process.stdout.write(`Job started: ${jobId}
18924
+ `);
18925
+ } catch (err) {
18926
+ process.stderr.write(`Error: ${err?.message ?? err}
18927
+ `);
18928
+ process.exit(1);
18929
+ }
18930
+ return;
18931
+ }
18598
18932
  process.stderr.write(`
18599
18933
  ${bold5(`Running ${cyan3(args.name)}`)}
18600
18934
 
@@ -18643,6 +18977,7 @@ var init_run = __esm(() => {
18643
18977
  init_runner();
18644
18978
  init_hooks();
18645
18979
  init_beads();
18980
+ init_supervisor();
18646
18981
  });
18647
18982
 
18648
18983
  // src/cli/status.ts
@@ -18651,8 +18986,8 @@ __export(exports_status, {
18651
18986
  run: () => run8
18652
18987
  });
18653
18988
  import { spawnSync as spawnSync4 } from "node:child_process";
18654
- import { existsSync as existsSync4 } from "node:fs";
18655
- import { join as join7 } from "node:path";
18989
+ import { existsSync as existsSync5 } from "node:fs";
18990
+ import { join as join8 } from "node:path";
18656
18991
  function ok2(msg) {
18657
18992
  console.log(` ${green5("✓")} ${msg}`);
18658
18993
  }
@@ -18681,6 +19016,27 @@ function cmd(bin, args) {
18681
19016
  function isInstalled(bin) {
18682
19017
  return spawnSync4("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
18683
19018
  }
19019
+ function formatElapsed(s) {
19020
+ if (s.elapsed_s === undefined)
19021
+ return "...";
19022
+ const m = Math.floor(s.elapsed_s / 60);
19023
+ const sec = s.elapsed_s % 60;
19024
+ return m > 0 ? `${m}m${sec.toString().padStart(2, "0")}s` : `${sec}s`;
19025
+ }
19026
+ function statusColor(status) {
19027
+ switch (status) {
19028
+ case "running":
19029
+ return cyan4(status);
19030
+ case "done":
19031
+ return green5(status);
19032
+ case "error":
19033
+ return red(status);
19034
+ case "starting":
19035
+ return yellow5(status);
19036
+ default:
19037
+ return status;
19038
+ }
19039
+ }
18684
19040
  async function run8() {
18685
19041
  console.log(`
18686
19042
  ${bold6("specialists status")}
@@ -18724,7 +19080,7 @@ ${bold6("specialists status")}
18724
19080
  } else {
18725
19081
  const bdVersion = cmd("bd", ["--version"]);
18726
19082
  ok2(`bd installed${bdVersion.ok ? ` ${dim6(bdVersion.stdout)}` : ""}`);
18727
- if (existsSync4(join7(process.cwd(), ".beads"))) {
19083
+ if (existsSync5(join8(process.cwd(), ".beads"))) {
18728
19084
  ok2(".beads/ present in project");
18729
19085
  } else {
18730
19086
  warn(`.beads/ not found — run ${yellow5("bd init")} to enable issue tracking`);
@@ -18739,33 +19095,229 @@ ${bold6("specialists status")}
18739
19095
  info(`verify registration: claude mcp get specialists`);
18740
19096
  info(`re-register: specialists install`);
18741
19097
  }
19098
+ const jobsDir = join8(process.cwd(), ".specialists", "jobs");
19099
+ if (existsSync5(jobsDir)) {
19100
+ const supervisor = new Supervisor({
19101
+ runner: null,
19102
+ runOptions: null,
19103
+ jobsDir
19104
+ });
19105
+ const jobs = supervisor.listJobs();
19106
+ if (jobs.length > 0) {
19107
+ section("Active Jobs");
19108
+ for (const job of jobs) {
19109
+ const elapsed = formatElapsed(job);
19110
+ const detail = job.status === "error" ? red(job.error?.slice(0, 40) ?? "error") : job.current_tool ? dim6(`tool: ${job.current_tool}`) : dim6(job.current_event ?? "");
19111
+ console.log(` ${dim6(job.id)} ${job.specialist.padEnd(20)} ${statusColor(job.status).padEnd(7)} ${elapsed.padStart(6)} ${detail}`);
19112
+ }
19113
+ }
19114
+ }
18742
19115
  console.log();
18743
19116
  }
18744
- 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`;
19117
+ var bold6 = (s) => `\x1B[1m${s}\x1B[0m`, dim6 = (s) => `\x1B[2m${s}\x1B[0m`, green5 = (s) => `\x1B[32m${s}\x1B[0m`, yellow5 = (s) => `\x1B[33m${s}\x1B[0m`, red = (s) => `\x1B[31m${s}\x1B[0m`, cyan4 = (s) => `\x1B[36m${s}\x1B[0m`;
18745
19118
  var init_status = __esm(() => {
18746
19119
  init_loader();
19120
+ init_supervisor();
19121
+ });
19122
+
19123
+ // src/cli/result.ts
19124
+ var exports_result = {};
19125
+ __export(exports_result, {
19126
+ run: () => run9
19127
+ });
19128
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
19129
+ import { join as join9 } from "node:path";
19130
+ async function run9() {
19131
+ const jobId = process.argv[3];
19132
+ if (!jobId) {
19133
+ console.error("Usage: specialists result <job-id>");
19134
+ process.exit(1);
19135
+ }
19136
+ const jobsDir = join9(process.cwd(), ".specialists", "jobs");
19137
+ const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19138
+ const status = supervisor.readStatus(jobId);
19139
+ if (!status) {
19140
+ console.error(`No job found: ${jobId}`);
19141
+ process.exit(1);
19142
+ }
19143
+ if (status.status === "running" || status.status === "starting") {
19144
+ process.stderr.write(`${dim7(`Job ${jobId} is still ${status.status}. Use 'specialists feed --job ${jobId}' to follow.`)}
19145
+ `);
19146
+ process.exit(1);
19147
+ }
19148
+ if (status.status === "error") {
19149
+ process.stderr.write(`${red2(`Job ${jobId} failed:`)} ${status.error ?? "unknown error"}
19150
+ `);
19151
+ process.exit(1);
19152
+ }
19153
+ const resultPath = join9(jobsDir, jobId, "result.txt");
19154
+ if (!existsSync6(resultPath)) {
19155
+ console.error(`Result file not found for job ${jobId}`);
19156
+ process.exit(1);
19157
+ }
19158
+ process.stdout.write(readFileSync4(resultPath, "utf-8"));
19159
+ }
19160
+ var dim7 = (s) => `\x1B[2m${s}\x1B[0m`, red2 = (s) => `\x1B[31m${s}\x1B[0m`;
19161
+ var init_result = __esm(() => {
19162
+ init_supervisor();
19163
+ });
19164
+
19165
+ // src/cli/feed.ts
19166
+ var exports_feed = {};
19167
+ __export(exports_feed, {
19168
+ run: () => run10
19169
+ });
19170
+ import { existsSync as existsSync7, readFileSync as readFileSync5, watchFile } from "node:fs";
19171
+ import { join as join10 } from "node:path";
19172
+ function formatEvent(line) {
19173
+ try {
19174
+ const e = JSON.parse(line);
19175
+ const ts = new Date(e.t).toISOString().slice(11, 19);
19176
+ const type = e.type ?? "?";
19177
+ const extra = e.tool ? ` ${cyan5(e.tool)}` : e.model ? ` ${dim8(e.model)}` : e.message ? ` ${red3(e.message)}` : "";
19178
+ return `${dim8(ts)} ${type}${extra}`;
19179
+ } catch {
19180
+ return line;
19181
+ }
19182
+ }
19183
+ function printLines(content, from) {
19184
+ const lines = content.split(`
19185
+ `).filter(Boolean);
19186
+ for (let i = from;i < lines.length; i++) {
19187
+ console.log(formatEvent(lines[i]));
19188
+ }
19189
+ return lines.length;
19190
+ }
19191
+ async function run10() {
19192
+ const argv = process.argv.slice(3);
19193
+ let jobId;
19194
+ let follow = false;
19195
+ for (let i = 0;i < argv.length; i++) {
19196
+ if (argv[i] === "--job" && argv[i + 1]) {
19197
+ jobId = argv[++i];
19198
+ continue;
19199
+ }
19200
+ if (argv[i] === "--follow" || argv[i] === "-f") {
19201
+ follow = true;
19202
+ continue;
19203
+ }
19204
+ if (!jobId && !argv[i].startsWith("--"))
19205
+ jobId = argv[i];
19206
+ }
19207
+ if (!jobId) {
19208
+ console.error("Usage: specialists feed --job <job-id> [--follow]");
19209
+ process.exit(1);
19210
+ }
19211
+ const jobsDir = join10(process.cwd(), ".specialists", "jobs");
19212
+ const eventsPath = join10(jobsDir, jobId, "events.jsonl");
19213
+ if (!existsSync7(eventsPath)) {
19214
+ const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19215
+ if (!supervisor.readStatus(jobId)) {
19216
+ console.error(`No job found: ${jobId}`);
19217
+ process.exit(1);
19218
+ }
19219
+ console.log(dim8("No events yet."));
19220
+ return;
19221
+ }
19222
+ const content = readFileSync5(eventsPath, "utf-8");
19223
+ let linesRead = printLines(content, 0);
19224
+ if (!follow)
19225
+ return;
19226
+ process.stderr.write(dim8(`Following ${jobId}... (Ctrl+C to stop)
19227
+ `));
19228
+ await new Promise((resolve) => {
19229
+ watchFile(eventsPath, { interval: 500 }, () => {
19230
+ try {
19231
+ const updated = readFileSync5(eventsPath, "utf-8");
19232
+ linesRead = printLines(updated, linesRead);
19233
+ const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19234
+ const status = supervisor.readStatus(jobId);
19235
+ if (status && status.status !== "running" && status.status !== "starting") {
19236
+ const finalMsg = status.status === "done" ? `
19237
+ ${yellow6("Job complete.")} Run: specialists result ${jobId}` : `
19238
+ ${red3(`Job ${status.status}.`)} ${status.error ?? ""}`;
19239
+ process.stderr.write(finalMsg + `
19240
+ `);
19241
+ resolve();
19242
+ }
19243
+ } catch {}
19244
+ });
19245
+ });
19246
+ }
19247
+ var dim8 = (s) => `\x1B[2m${s}\x1B[0m`, cyan5 = (s) => `\x1B[36m${s}\x1B[0m`, yellow6 = (s) => `\x1B[33m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
19248
+ var init_feed = __esm(() => {
19249
+ init_supervisor();
19250
+ });
19251
+
19252
+ // src/cli/stop.ts
19253
+ var exports_stop = {};
19254
+ __export(exports_stop, {
19255
+ run: () => run11
19256
+ });
19257
+ import { join as join11 } from "node:path";
19258
+ async function run11() {
19259
+ const jobId = process.argv[3];
19260
+ if (!jobId) {
19261
+ console.error("Usage: specialists stop <job-id>");
19262
+ process.exit(1);
19263
+ }
19264
+ const jobsDir = join11(process.cwd(), ".specialists", "jobs");
19265
+ const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19266
+ const status = supervisor.readStatus(jobId);
19267
+ if (!status) {
19268
+ console.error(`No job found: ${jobId}`);
19269
+ process.exit(1);
19270
+ }
19271
+ if (status.status === "done" || status.status === "error") {
19272
+ process.stderr.write(`${dim9(`Job ${jobId} is already ${status.status}.`)}
19273
+ `);
19274
+ return;
19275
+ }
19276
+ if (!status.pid) {
19277
+ process.stderr.write(`${red4(`No PID recorded for job ${jobId}.`)}
19278
+ `);
19279
+ process.exit(1);
19280
+ }
19281
+ try {
19282
+ process.kill(status.pid, "SIGTERM");
19283
+ process.stdout.write(`${green6("✓")} Sent SIGTERM to PID ${status.pid} (job ${jobId})
19284
+ `);
19285
+ } catch (err) {
19286
+ if (err.code === "ESRCH") {
19287
+ process.stderr.write(`${red4(`Process ${status.pid} not found.`)} Job may have already completed.
19288
+ `);
19289
+ } else {
19290
+ process.stderr.write(`${red4("Error:")} ${err.message}
19291
+ `);
19292
+ process.exit(1);
19293
+ }
19294
+ }
19295
+ }
19296
+ var green6 = (s) => `\x1B[32m${s}\x1B[0m`, red4 = (s) => `\x1B[31m${s}\x1B[0m`, dim9 = (s) => `\x1B[2m${s}\x1B[0m`;
19297
+ var init_stop = __esm(() => {
19298
+ init_supervisor();
18747
19299
  });
18748
19300
 
18749
19301
  // src/cli/help.ts
18750
19302
  var exports_help = {};
18751
19303
  __export(exports_help, {
18752
- run: () => run9
19304
+ run: () => run12
18753
19305
  });
18754
- async function run9() {
19306
+ async function run12() {
18755
19307
  const lines = [
18756
19308
  "",
18757
19309
  bold7("specialists <command>"),
18758
19310
  "",
18759
19311
  "Commands:",
18760
- ...COMMANDS.map(([cmd2, desc]) => ` ${cmd2.padEnd(COL_WIDTH)} ${dim7(desc)}`),
19312
+ ...COMMANDS.map(([cmd2, desc]) => ` ${cmd2.padEnd(COL_WIDTH)} ${dim10(desc)}`),
18761
19313
  "",
18762
- dim7("Run 'specialists <command> --help' for command-specific options."),
19314
+ dim10("Run 'specialists <command> --help' for command-specific options."),
18763
19315
  ""
18764
19316
  ];
18765
19317
  console.log(lines.join(`
18766
19318
  `));
18767
19319
  }
18768
- var bold7 = (s) => `\x1B[1m${s}\x1B[0m`, dim7 = (s) => `\x1B[2m${s}\x1B[0m`, COMMANDS, COL_WIDTH;
19320
+ var bold7 = (s) => `\x1B[1m${s}\x1B[0m`, dim10 = (s) => `\x1B[2m${s}\x1B[0m`, COMMANDS, COL_WIDTH;
18769
19321
  var init_help = __esm(() => {
18770
19322
  COMMANDS = [
18771
19323
  ["install", "Full-stack installer: pi, beads, dolt, MCP registration, hooks"],
@@ -18774,8 +19326,11 @@ var init_help = __esm(() => {
18774
19326
  ["version", "Print installed version"],
18775
19327
  ["init", "Initialize specialists in the current project"],
18776
19328
  ["edit", "Edit a specialist field (e.g. --model, --description)"],
18777
- ["run", "Run a specialist with a prompt"],
18778
- ["status", "Show system health (pi, beads, MCP)"],
19329
+ ["run", "Run a specialist with a prompt (--background for async)"],
19330
+ ["result", "Print result of a background job"],
19331
+ ["feed", "Tail events for a background job (--follow to stream)"],
19332
+ ["stop", "Send SIGTERM to a running background job"],
19333
+ ["status", "Show system health (pi, beads, MCP, jobs)"],
18779
19334
  ["help", "Show this help message"]
18780
19335
  ];
18781
19336
  COL_WIDTH = Math.max(...COMMANDS.map(([cmd2]) => cmd2.length));
@@ -26152,7 +26707,7 @@ var runParallelSchema = objectType({
26152
26707
  function createRunParallelTool(runner) {
26153
26708
  return {
26154
26709
  name: "run_parallel",
26155
- description: "Execute multiple specialists concurrently. Returns aggregated results.",
26710
+ description: "[DEPRECATED v3] Execute multiple specialists concurrently. Returns aggregated results. Prefer CLI background jobs for async work.",
26156
26711
  inputSchema: runParallelSchema,
26157
26712
  async execute(input, onProgress) {
26158
26713
  if (input.merge_strategy === "pipeline") {
@@ -26191,11 +26746,26 @@ var BACKENDS2 = ["gemini", "qwen", "anthropic", "openai"];
26191
26746
  function createSpecialistStatusTool(loader, circuitBreaker) {
26192
26747
  return {
26193
26748
  name: "specialist_status",
26194
- description: "System health: backend circuit breaker states, loaded specialists, staleness.",
26749
+ description: "System health: backend circuit breaker states, loaded specialists, staleness. Also shows active background jobs from .specialists/jobs/.",
26195
26750
  inputSchema: exports_external.object({}),
26196
26751
  async execute(_) {
26197
26752
  const list = await loader.list();
26198
26753
  const stalenessResults = await Promise.all(list.map((s) => checkStaleness(s)));
26754
+ const { existsSync: existsSync2, readdirSync, readFileSync } = await import("node:fs");
26755
+ const { join: join2 } = await import("node:path");
26756
+ const jobsDir = join2(process.cwd(), ".specialists", "jobs");
26757
+ const jobs = [];
26758
+ if (existsSync2(jobsDir)) {
26759
+ for (const entry of readdirSync(jobsDir)) {
26760
+ const statusPath = join2(jobsDir, entry, "status.json");
26761
+ if (!existsSync2(statusPath))
26762
+ continue;
26763
+ try {
26764
+ jobs.push(JSON.parse(readFileSync(statusPath, "utf-8")));
26765
+ } catch {}
26766
+ }
26767
+ jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
26768
+ }
26199
26769
  return {
26200
26770
  loaded_count: list.length,
26201
26771
  backends_health: Object.fromEntries(BACKENDS2.map((b) => [b, circuitBreaker.getState(b)])),
@@ -26205,6 +26775,15 @@ function createSpecialistStatusTool(loader, circuitBreaker) {
26205
26775
  category: s.category,
26206
26776
  version: s.version,
26207
26777
  staleness: stalenessResults[i]
26778
+ })),
26779
+ background_jobs: jobs.map((j) => ({
26780
+ id: j.id,
26781
+ specialist: j.specialist,
26782
+ status: j.status,
26783
+ elapsed_s: j.elapsed_s,
26784
+ current_event: j.current_event,
26785
+ bead_id: j.bead_id,
26786
+ error: j.error
26208
26787
  }))
26209
26788
  };
26210
26789
  }
@@ -26330,7 +26909,7 @@ var startSpecialistSchema = exports_external.object({
26330
26909
  function createStartSpecialistTool(runner, registry2) {
26331
26910
  return {
26332
26911
  name: "start_specialist",
26333
- description: "Start a specialist asynchronously. Returns job_id immediately. " + "Use poll_specialist to track progress, receive output delta, and retrieve beadId " + "(the beads issue auto-created for this run, if beads_integration policy applies). " + "Use stop_specialist to cancel. Enables true parallel execution of multiple specialists.",
26912
+ description: "[DEPRECATED v3] Start a specialist asynchronously. Returns job_id immediately. Prefer CLI: `specialists run <name> --background`. " + "Use poll_specialist to track progress, receive output delta, and retrieve beadId " + "(the beads issue auto-created for this run, if beads_integration policy applies). " + "Use stop_specialist to cancel. Enables true parallel execution of multiple specialists.",
26334
26913
  inputSchema: startSpecialistSchema,
26335
26914
  async execute(input) {
26336
26915
  const jobId = await runner.startAsync({
@@ -26353,7 +26932,7 @@ var pollSpecialistSchema = exports_external.object({
26353
26932
  function createPollSpecialistTool(registry2) {
26354
26933
  return {
26355
26934
  name: "poll_specialist",
26356
- description: "Poll a running specialist job. Returns status (running|done|error|cancelled), " + "delta (new tokens since cursor), next_cursor, and full output when done. " + "Pass next_cursor back as cursor on each subsequent poll to receive only new content. " + "Response also includes beadId (string | undefined) once the specialist has started — " + "this is the beads issue tracking this run. If present after status=done, consider: " + '`bd update <beadId> --notes "<key finding>"` to attach results, or ' + '`bd remember "<insight>"` to persist discoveries across sessions.',
26935
+ description: "[DEPRECATED v3] Poll a running specialist job. Returns status (running|done|error|cancelled), " + "delta (new tokens since cursor), next_cursor, and full output when done. " + "Pass next_cursor back as cursor on each subsequent poll to receive only new content. " + "Response also includes beadId (string | undefined) once the specialist has started — " + "this is the beads issue tracking this run. If present after status=done, consider: " + '`bd update <beadId> --notes "<key finding>"` to attach results, or ' + '`bd remember "<insight>"` to persist discoveries across sessions.',
26357
26936
  inputSchema: pollSpecialistSchema,
26358
26937
  async execute(input) {
26359
26938
  const snapshot = registry2.snapshot(input.job_id, input.cursor ?? 0);
@@ -26373,7 +26952,7 @@ var stopSpecialistSchema = exports_external.object({
26373
26952
  function createStopSpecialistTool(registry2) {
26374
26953
  return {
26375
26954
  name: "stop_specialist",
26376
- description: "Cancel a running specialist job. Kills the pi process immediately and sets status to cancelled. Subsequent poll_specialist calls return status: cancelled with output buffered up to that point.",
26955
+ description: "[DEPRECATED v3] Cancel a running specialist job. Prefer CLI: `specialists stop <id>`. Kills the pi process immediately and sets status to cancelled. Subsequent poll_specialist calls return status: cancelled with output buffered up to that point.",
26377
26956
  inputSchema: stopSpecialistSchema,
26378
26957
  async execute(input) {
26379
26958
  const result = registry2.cancel(input.job_id);
@@ -26511,6 +27090,11 @@ class SpecialistsServer {
26511
27090
  const transport = new StdioServerTransport;
26512
27091
  await this.server.connect(transport);
26513
27092
  logger.info(`Specialists MCP Server v2 started — ${this.tools.length} tools registered`);
27093
+ process.on("SIGTERM", async () => {
27094
+ logger.info("SIGTERM received — shutting down");
27095
+ await this.stop();
27096
+ process.exit(0);
27097
+ });
26514
27098
  } catch (error2) {
26515
27099
  logger.error("Failed to start server", error2);
26516
27100
  process.exit(1);
@@ -26523,7 +27107,7 @@ class SpecialistsServer {
26523
27107
 
26524
27108
  // src/index.ts
26525
27109
  var sub = process.argv[2];
26526
- async function run10() {
27110
+ async function run13() {
26527
27111
  if (sub === "install") {
26528
27112
  const { run: handler } = await Promise.resolve().then(() => (init_install(), exports_install));
26529
27113
  return handler();
@@ -26556,6 +27140,18 @@ async function run10() {
26556
27140
  const { run: handler } = await Promise.resolve().then(() => (init_status(), exports_status));
26557
27141
  return handler();
26558
27142
  }
27143
+ if (sub === "result") {
27144
+ const { run: handler } = await Promise.resolve().then(() => (init_result(), exports_result));
27145
+ return handler();
27146
+ }
27147
+ if (sub === "feed") {
27148
+ const { run: handler } = await Promise.resolve().then(() => (init_feed(), exports_feed));
27149
+ return handler();
27150
+ }
27151
+ if (sub === "stop") {
27152
+ const { run: handler } = await Promise.resolve().then(() => (init_stop(), exports_stop));
27153
+ return handler();
27154
+ }
26559
27155
  if (sub === "help" || sub === "--help" || sub === "-h") {
26560
27156
  const { run: handler } = await Promise.resolve().then(() => (init_help(), exports_help));
26561
27157
  return handler();
@@ -26569,7 +27165,7 @@ Run 'specialists help' to see available commands.`);
26569
27165
  const server = new SpecialistsServer;
26570
27166
  await server.start();
26571
27167
  }
26572
- run10().catch((error2) => {
27168
+ run13().catch((error2) => {
26573
27169
  logger.error(`Fatal error: ${error2}`);
26574
27170
  process.exit(1);
26575
27171
  });