@jaggerxtrm/specialists 2.1.20 → 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);
@@ -18242,23 +18329,128 @@ var init_list = __esm(() => {
18242
18329
  };
18243
18330
  });
18244
18331
 
18332
+ // src/cli/models.ts
18333
+ var exports_models = {};
18334
+ __export(exports_models, {
18335
+ run: () => run4
18336
+ });
18337
+ import { spawnSync as spawnSync3 } from "node:child_process";
18338
+ function parsePiModels() {
18339
+ const r = spawnSync3("pi", ["--list-models"], {
18340
+ encoding: "utf8",
18341
+ stdio: "pipe",
18342
+ timeout: 8000
18343
+ });
18344
+ if (r.status !== 0 || r.error)
18345
+ return null;
18346
+ return r.stdout.split(`
18347
+ `).slice(1).map((line) => line.trim()).filter(Boolean).map((line) => {
18348
+ const cols = line.split(/\s+/);
18349
+ return {
18350
+ provider: cols[0] ?? "",
18351
+ model: cols[1] ?? "",
18352
+ context: cols[2] ?? "",
18353
+ maxOut: cols[3] ?? "",
18354
+ thinking: cols[4] === "yes",
18355
+ images: cols[5] === "yes"
18356
+ };
18357
+ }).filter((m) => m.provider && m.model);
18358
+ }
18359
+ function parseArgs2(argv) {
18360
+ const out = {};
18361
+ for (let i = 0;i < argv.length; i++) {
18362
+ if (argv[i] === "--provider" && argv[i + 1]) {
18363
+ out.provider = argv[++i];
18364
+ continue;
18365
+ }
18366
+ if (argv[i] === "--used") {
18367
+ out.used = true;
18368
+ continue;
18369
+ }
18370
+ }
18371
+ return out;
18372
+ }
18373
+ async function run4() {
18374
+ const args = parseArgs2(process.argv.slice(3));
18375
+ const loader = new SpecialistLoader;
18376
+ const specialists = await loader.list();
18377
+ const usedBy = new Map;
18378
+ for (const s of specialists) {
18379
+ const key = s.model;
18380
+ if (!usedBy.has(key))
18381
+ usedBy.set(key, []);
18382
+ usedBy.get(key).push(s.name);
18383
+ }
18384
+ const allModels = parsePiModels();
18385
+ if (!allModels) {
18386
+ console.error("pi not found or failed — run specialists install");
18387
+ process.exit(1);
18388
+ }
18389
+ let models = allModels;
18390
+ if (args.provider) {
18391
+ models = models.filter((m) => m.provider.toLowerCase().includes(args.provider.toLowerCase()));
18392
+ }
18393
+ if (args.used) {
18394
+ models = models.filter((m) => usedBy.has(`${m.provider}/${m.model}`));
18395
+ }
18396
+ if (models.length === 0) {
18397
+ console.log("No models match.");
18398
+ return;
18399
+ }
18400
+ const byProvider = new Map;
18401
+ for (const m of models) {
18402
+ if (!byProvider.has(m.provider))
18403
+ byProvider.set(m.provider, []);
18404
+ byProvider.get(m.provider).push(m);
18405
+ }
18406
+ const total = models.length;
18407
+ console.log(`
18408
+ ${bold2(`Models on pi`)} ${dim2(`(${total} total)`)}
18409
+ `);
18410
+ for (const [provider, pModels] of byProvider) {
18411
+ console.log(` ${cyan2(provider)} ${dim2(`${pModels.length} model${pModels.length !== 1 ? "s" : ""}`)}`);
18412
+ const modelWidth = Math.max(...pModels.map((m) => m.model.length));
18413
+ for (const m of pModels) {
18414
+ const key = `${m.provider}/${m.model}`;
18415
+ const inUse = usedBy.get(key);
18416
+ const flags = [
18417
+ m.thinking ? green("thinking") : dim2("·"),
18418
+ m.images ? dim2("images") : ""
18419
+ ].filter(Boolean).join(" ");
18420
+ const ctx = dim2(`ctx ${m.context}`);
18421
+ const usedLabel = inUse ? ` ${yellow2("←")} ${dim2(inUse.join(", "))}` : "";
18422
+ console.log(` ${m.model.padEnd(modelWidth)} ${ctx.padEnd(18)} ${flags}${usedLabel}`);
18423
+ }
18424
+ console.log();
18425
+ }
18426
+ if (!args.used) {
18427
+ console.log(dim2(` --provider <name> filter by provider`));
18428
+ console.log(dim2(` --used only show models used by your specialists`));
18429
+ console.log();
18430
+ }
18431
+ }
18432
+ 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`;
18433
+ var init_models = __esm(() => {
18434
+ init_loader();
18435
+ });
18436
+
18245
18437
  // src/cli/init.ts
18246
18438
  var exports_init = {};
18247
18439
  __export(exports_init, {
18248
- run: () => run4
18440
+ run: () => run5
18249
18441
  });
18250
18442
  import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18251
18443
  import { join as join5 } from "node:path";
18252
18444
  function ok(msg) {
18253
- console.log(` ${green("✓")} ${msg}`);
18445
+ console.log(` ${green2("✓")} ${msg}`);
18254
18446
  }
18255
18447
  function skip(msg) {
18256
- console.log(` ${yellow2("○")} ${msg}`);
18448
+ console.log(` ${yellow3("○")} ${msg}`);
18257
18449
  }
18258
- async function run4() {
18450
+ async function run5() {
18259
18451
  const cwd = process.cwd();
18260
18452
  console.log(`
18261
- ${bold2("specialists init")}
18453
+ ${bold3("specialists init")}
18262
18454
  `);
18263
18455
  const specialistsDir = join5(cwd, "specialists");
18264
18456
  if (existsSync3(specialistsDir)) {
@@ -18267,6 +18459,32 @@ ${bold2("specialists init")}
18267
18459
  mkdirSync(specialistsDir, { recursive: true });
18268
18460
  ok("created specialists/");
18269
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
+ }
18270
18488
  const agentsPath = join5(cwd, "AGENTS.md");
18271
18489
  if (existsSync3(agentsPath)) {
18272
18490
  const existing = readFileSync(agentsPath, "utf-8");
@@ -18283,15 +18501,15 @@ ${bold2("specialists init")}
18283
18501
  ok("created AGENTS.md with Specialists section");
18284
18502
  }
18285
18503
  console.log(`
18286
- ${bold2("Done!")}
18504
+ ${bold3("Done!")}
18287
18505
  `);
18288
- console.log(` ${dim2("Next steps:")}`);
18289
- console.log(` 1. Add your specialists to ${yellow2("specialists/")}`);
18290
- console.log(` 2. Run ${yellow2("specialists list")} to verify they are discovered`);
18506
+ console.log(` ${dim3("Next steps:")}`);
18507
+ console.log(` 1. Add your specialists to ${yellow3("specialists/")}`);
18508
+ console.log(` 2. Run ${yellow3("specialists list")} to verify they are discovered`);
18291
18509
  console.log(` 3. Restart Claude Code to pick up AGENTS.md changes
18292
18510
  `);
18293
18511
  }
18294
- var bold2 = (s) => `\x1B[1m${s}\x1B[0m`, green = (s) => `\x1B[32m${s}\x1B[0m`, yellow2 = (s) => `\x1B[33m${s}\x1B[0m`, dim2 = (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/";
18295
18513
  var init_init = __esm(() => {
18296
18514
  AGENTS_BLOCK = `
18297
18515
  ## Specialists
@@ -18306,10 +18524,10 @@ specialist without user intervention.
18306
18524
  // src/cli/edit.ts
18307
18525
  var exports_edit = {};
18308
18526
  __export(exports_edit, {
18309
- run: () => run5
18527
+ run: () => run6
18310
18528
  });
18311
18529
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
18312
- function parseArgs2(argv) {
18530
+ function parseArgs3(argv) {
18313
18531
  const name = argv[0];
18314
18532
  if (!name || name.startsWith("--")) {
18315
18533
  console.error("Usage: specialists edit <name> --<field> <value> [--dry-run]");
@@ -18371,8 +18589,8 @@ function setIn(doc2, path, value) {
18371
18589
  node.set(leaf, value);
18372
18590
  }
18373
18591
  }
18374
- async function run5() {
18375
- const args = parseArgs2(process.argv.slice(3));
18592
+ async function run6() {
18593
+ const args = parseArgs3(process.argv.slice(3));
18376
18594
  const { name, field, value, dryRun, scope } = args;
18377
18595
  const loader = new SpecialistLoader;
18378
18596
  const all = await loader.list();
@@ -18380,7 +18598,7 @@ async function run5() {
18380
18598
  if (!match) {
18381
18599
  const hint = scope ? ` (scope: ${scope})` : "";
18382
18600
  console.error(`Error: specialist "${name}" not found${hint}`);
18383
- console.error(` Run ${yellow3("specialists list")} to see available specialists`);
18601
+ console.error(` Run ${yellow4("specialists list")} to see available specialists`);
18384
18602
  process.exit(1);
18385
18603
  }
18386
18604
  const raw = readFileSync2(match.filePath, "utf-8");
@@ -18396,10 +18614,10 @@ async function run5() {
18396
18614
  const updated = doc2.toString();
18397
18615
  if (dryRun) {
18398
18616
  console.log(`
18399
- ${bold3(`[dry-run] ${match.filePath}`)}
18617
+ ${bold4(`[dry-run] ${match.filePath}`)}
18400
18618
  `);
18401
- console.log(dim3("--- current"));
18402
- console.log(dim3(`+++ updated`));
18619
+ console.log(dim4("--- current"));
18620
+ console.log(dim4(`+++ updated`));
18403
18621
  const oldLines = raw.split(`
18404
18622
  `);
18405
18623
  const newLines = updated.split(`
@@ -18407,8 +18625,8 @@ ${bold3(`[dry-run] ${match.filePath}`)}
18407
18625
  newLines.forEach((line, i) => {
18408
18626
  if (line !== oldLines[i]) {
18409
18627
  if (oldLines[i] !== undefined)
18410
- console.log(dim3(`- ${oldLines[i]}`));
18411
- console.log(green2(`+ ${line}`));
18628
+ console.log(dim4(`- ${oldLines[i]}`));
18629
+ console.log(green3(`+ ${line}`));
18412
18630
  }
18413
18631
  });
18414
18632
  console.log();
@@ -18416,9 +18634,9 @@ ${bold3(`[dry-run] ${match.filePath}`)}
18416
18634
  }
18417
18635
  writeFileSync2(match.filePath, updated, "utf-8");
18418
18636
  const displayValue = field === "tags" ? `[${typedValue.join(", ")}]` : String(typedValue);
18419
- console.log(`${green2("✓")} ${bold3(name)}: ${yellow3(field)} = ${displayValue}` + dim3(` (${match.filePath})`));
18637
+ console.log(`${green3("✓")} ${bold4(name)}: ${yellow4(field)} = ${displayValue}` + dim4(` (${match.filePath})`));
18420
18638
  }
18421
- 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`, FIELD_MAP, VALID_PERMISSIONS;
18639
+ 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;
18422
18640
  var init_edit = __esm(() => {
18423
18641
  init_dist();
18424
18642
  init_loader();
@@ -18433,21 +18651,220 @@ var init_edit = __esm(() => {
18433
18651
  VALID_PERMISSIONS = ["READ_ONLY", "LOW", "MEDIUM", "HIGH"];
18434
18652
  });
18435
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
+
18436
18852
  // src/cli/run.ts
18437
18853
  var exports_run = {};
18438
18854
  __export(exports_run, {
18439
- run: () => run6
18855
+ run: () => run7
18440
18856
  });
18441
- import { join as join6 } from "node:path";
18442
- async function parseArgs3(argv) {
18857
+ import { join as join7 } from "node:path";
18858
+ async function parseArgs4(argv) {
18443
18859
  const name = argv[0];
18444
18860
  if (!name || name.startsWith("--")) {
18445
- console.error('Usage: specialists run <name> [--prompt "..."] [--model <model>] [--no-beads]');
18861
+ console.error('Usage: specialists run <name> [--prompt "..."] [--model <model>] [--no-beads] [--background]');
18446
18862
  process.exit(1);
18447
18863
  }
18448
18864
  let prompt = "";
18449
18865
  let model;
18450
18866
  let noBeads = false;
18867
+ let background = false;
18451
18868
  for (let i = 1;i < argv.length; i++) {
18452
18869
  const token = argv[i];
18453
18870
  if (token === "--prompt" && argv[i + 1]) {
@@ -18462,10 +18879,14 @@ async function parseArgs3(argv) {
18462
18879
  noBeads = true;
18463
18880
  continue;
18464
18881
  }
18882
+ if (token === "--background") {
18883
+ background = true;
18884
+ continue;
18885
+ }
18465
18886
  }
18466
18887
  if (!prompt) {
18467
18888
  if (process.stdin.isTTY) {
18468
- process.stderr.write(dim4("Prompt (Ctrl+D when done): "));
18889
+ process.stderr.write(dim5("Prompt (Ctrl+D when done): "));
18469
18890
  }
18470
18891
  prompt = await new Promise((resolve) => {
18471
18892
  let buf = "";
@@ -18476,13 +18897,13 @@ async function parseArgs3(argv) {
18476
18897
  process.stdin.on("end", () => resolve(buf.trim()));
18477
18898
  });
18478
18899
  }
18479
- return { name, prompt, model, noBeads };
18900
+ return { name, prompt, model, noBeads, background };
18480
18901
  }
18481
- async function run6() {
18482
- const args = await parseArgs3(process.argv.slice(3));
18902
+ async function run7() {
18903
+ const args = await parseArgs4(process.argv.slice(3));
18483
18904
  const loader = new SpecialistLoader;
18484
18905
  const circuitBreaker = new CircuitBreaker;
18485
- const hooks = new HookEmitter({ tracePath: join6(process.cwd(), ".specialists", "trace.jsonl") });
18906
+ const hooks = new HookEmitter({ tracePath: join7(process.cwd(), ".specialists", "trace.jsonl") });
18486
18907
  const beadsClient = args.noBeads ? null : new BeadsClient;
18487
18908
  const runner = new SpecialistRunner({
18488
18909
  loader,
@@ -18490,8 +18911,26 @@ async function run6() {
18490
18911
  circuitBreaker,
18491
18912
  beadsClient: beadsClient ?? undefined
18492
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
+ }
18493
18932
  process.stderr.write(`
18494
- ${bold4(`Running ${cyan2(args.name)}`)}
18933
+ ${bold5(`Running ${cyan3(args.name)}`)}
18495
18934
 
18496
18935
  `);
18497
18936
  let beadId;
@@ -18499,7 +18938,7 @@ ${bold4(`Running ${cyan2(args.name)}`)}
18499
18938
  name: args.name,
18500
18939
  prompt: args.prompt,
18501
18940
  backendOverride: args.model
18502
- }, (delta) => process.stdout.write(delta), undefined, (meta) => process.stderr.write(dim4(`
18941
+ }, (delta) => process.stdout.write(delta), undefined, (meta) => process.stderr.write(dim5(`
18503
18942
  [${meta.backend} / ${meta.model}]
18504
18943
 
18505
18944
  `)), (killFn) => {
@@ -18513,7 +18952,7 @@ Interrupted.
18513
18952
  });
18514
18953
  }, (id) => {
18515
18954
  beadId = id;
18516
- process.stderr.write(dim4(`
18955
+ process.stderr.write(dim5(`
18517
18956
  [bead: ${id}]
18518
18957
  `));
18519
18958
  });
@@ -18525,48 +18964,49 @@ Interrupted.
18525
18964
  const footer = [
18526
18965
  beadId ? `bead ${beadId}` : "",
18527
18966
  `${secs}s`,
18528
- dim4(result.model)
18967
+ dim5(result.model)
18529
18968
  ].filter(Boolean).join(" ");
18530
18969
  process.stderr.write(`
18531
- ${green3("✓")} ${footer}
18970
+ ${green4("✓")} ${footer}
18532
18971
 
18533
18972
  `);
18534
18973
  }
18535
- var bold4 = (s) => `\x1B[1m${s}\x1B[0m`, dim4 = (s) => `\x1B[2m${s}\x1B[0m`, green3 = (s) => `\x1B[32m${s}\x1B[0m`, cyan2 = (s) => `\x1B[36m${s}\x1B[0m`;
18974
+ 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`;
18536
18975
  var init_run = __esm(() => {
18537
18976
  init_loader();
18538
18977
  init_runner();
18539
18978
  init_hooks();
18540
18979
  init_beads();
18980
+ init_supervisor();
18541
18981
  });
18542
18982
 
18543
18983
  // src/cli/status.ts
18544
18984
  var exports_status = {};
18545
18985
  __export(exports_status, {
18546
- run: () => run7
18986
+ run: () => run8
18547
18987
  });
18548
- import { spawnSync as spawnSync3 } from "node:child_process";
18549
- import { existsSync as existsSync4 } from "node:fs";
18550
- import { join as join7 } from "node:path";
18988
+ import { spawnSync as spawnSync4 } from "node:child_process";
18989
+ import { existsSync as existsSync5 } from "node:fs";
18990
+ import { join as join8 } from "node:path";
18551
18991
  function ok2(msg) {
18552
- console.log(` ${green4("✓")} ${msg}`);
18992
+ console.log(` ${green5("✓")} ${msg}`);
18553
18993
  }
18554
18994
  function warn(msg) {
18555
- console.log(` ${yellow4("○")} ${msg}`);
18995
+ console.log(` ${yellow5("○")} ${msg}`);
18556
18996
  }
18557
18997
  function fail(msg) {
18558
18998
  console.log(` ${red("✗")} ${msg}`);
18559
18999
  }
18560
19000
  function info(msg) {
18561
- console.log(` ${dim5(msg)}`);
19001
+ console.log(` ${dim6(msg)}`);
18562
19002
  }
18563
19003
  function section(label) {
18564
19004
  const line = "─".repeat(Math.max(0, 38 - label.length));
18565
19005
  console.log(`
18566
- ${bold5(`── ${label} ${line}`)}`);
19006
+ ${bold6(`── ${label} ${line}`)}`);
18567
19007
  }
18568
19008
  function cmd(bin, args) {
18569
- const r = spawnSync3(bin, args, {
19009
+ const r = spawnSync4(bin, args, {
18570
19010
  encoding: "utf8",
18571
19011
  stdio: "pipe",
18572
19012
  timeout: 5000
@@ -18574,102 +19014,323 @@ function cmd(bin, args) {
18574
19014
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
18575
19015
  }
18576
19016
  function isInstalled(bin) {
18577
- return spawnSync3("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
19017
+ return spawnSync4("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
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
+ }
18578
19039
  }
18579
- async function run7() {
19040
+ async function run8() {
18580
19041
  console.log(`
18581
- ${bold5("specialists status")}
19042
+ ${bold6("specialists status")}
18582
19043
  `);
18583
19044
  section("Specialists");
18584
19045
  const loader = new SpecialistLoader;
18585
19046
  const all = await loader.list();
18586
19047
  if (all.length === 0) {
18587
- warn(`no specialists found — run ${yellow4("specialists init")} to scaffold`);
19048
+ warn(`no specialists found — run ${yellow5("specialists init")} to scaffold`);
18588
19049
  } else {
18589
19050
  const byScope = all.reduce((acc, s) => {
18590
19051
  acc[s.scope] = (acc[s.scope] ?? 0) + 1;
18591
19052
  return acc;
18592
19053
  }, {});
18593
19054
  const scopeSummary = Object.entries(byScope).map(([scope, n]) => `${n} ${scope}`).join(", ");
18594
- ok2(`${all.length} found ${dim5(`(${scopeSummary})`)}`);
19055
+ ok2(`${all.length} found ${dim6(`(${scopeSummary})`)}`);
18595
19056
  for (const s of all) {
18596
19057
  const staleness = await checkStaleness(s);
18597
19058
  if (staleness === "AGED") {
18598
- warn(`${s.name} ${red("AGED")} ${dim5(s.scope)}`);
19059
+ warn(`${s.name} ${red("AGED")} ${dim6(s.scope)}`);
18599
19060
  } else if (staleness === "STALE") {
18600
- warn(`${s.name} ${yellow4("STALE")} ${dim5(s.scope)}`);
19061
+ warn(`${s.name} ${yellow5("STALE")} ${dim6(s.scope)}`);
18601
19062
  }
18602
19063
  }
18603
19064
  }
18604
19065
  section("pi (coding agent runtime)");
18605
19066
  if (!isInstalled("pi")) {
18606
- fail(`pi not installed — run ${yellow4("specialists install")}`);
19067
+ fail(`pi not installed — run ${yellow5("specialists install")}`);
18607
19068
  } else {
18608
19069
  const version2 = cmd("pi", ["--version"]);
18609
19070
  const models = cmd("pi", ["--list-models"]);
18610
19071
  const providers = new Set(models.stdout.split(`
18611
19072
  `).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean));
18612
19073
  const vStr = version2.ok ? `v${version2.stdout}` : "unknown version";
18613
- const pStr = providers.size > 0 ? `${providers.size} provider${providers.size > 1 ? "s" : ""} active ${dim5(`(${[...providers].join(", ")})`)} ` : yellow4("no providers configured — run pi config");
19074
+ const pStr = providers.size > 0 ? `${providers.size} provider${providers.size > 1 ? "s" : ""} active ${dim6(`(${[...providers].join(", ")})`)} ` : yellow5("no providers configured — run pi config");
18614
19075
  ok2(`${vStr} — ${pStr}`);
18615
19076
  }
18616
19077
  section("beads (issue tracker)");
18617
19078
  if (!isInstalled("bd")) {
18618
- fail(`bd not installed — run ${yellow4("specialists install")}`);
19079
+ fail(`bd not installed — run ${yellow5("specialists install")}`);
18619
19080
  } else {
18620
19081
  const bdVersion = cmd("bd", ["--version"]);
18621
- ok2(`bd installed${bdVersion.ok ? ` ${dim5(bdVersion.stdout)}` : ""}`);
18622
- if (existsSync4(join7(process.cwd(), ".beads"))) {
19082
+ ok2(`bd installed${bdVersion.ok ? ` ${dim6(bdVersion.stdout)}` : ""}`);
19083
+ if (existsSync5(join8(process.cwd(), ".beads"))) {
18623
19084
  ok2(".beads/ present in project");
18624
19085
  } else {
18625
- warn(`.beads/ not found — run ${yellow4("bd init")} to enable issue tracking`);
19086
+ warn(`.beads/ not found — run ${yellow5("bd init")} to enable issue tracking`);
18626
19087
  }
18627
19088
  }
18628
19089
  section("MCP");
18629
19090
  const specialistsBin = cmd("which", ["specialists"]);
18630
19091
  if (!specialistsBin.ok) {
18631
- fail(`specialists not installed globally — run ${yellow4("npm install -g @jaggerxtrm/specialists")}`);
19092
+ fail(`specialists not installed globally — run ${yellow5("npm install -g @jaggerxtrm/specialists")}`);
18632
19093
  } else {
18633
- ok2(`specialists binary installed ${dim5(specialistsBin.stdout)}`);
19094
+ ok2(`specialists binary installed ${dim6(specialistsBin.stdout)}`);
18634
19095
  info(`verify registration: claude mcp get specialists`);
18635
19096
  info(`re-register: specialists install`);
18636
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
+ }
18637
19115
  console.log();
18638
19116
  }
18639
- var bold5 = (s) => `\x1B[1m${s}\x1B[0m`, dim5 = (s) => `\x1B[2m${s}\x1B[0m`, green4 = (s) => `\x1B[32m${s}\x1B[0m`, yellow4 = (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`;
18640
19118
  var init_status = __esm(() => {
18641
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();
18642
19299
  });
18643
19300
 
18644
19301
  // src/cli/help.ts
18645
19302
  var exports_help = {};
18646
19303
  __export(exports_help, {
18647
- run: () => run8
19304
+ run: () => run12
18648
19305
  });
18649
- async function run8() {
19306
+ async function run12() {
18650
19307
  const lines = [
18651
19308
  "",
18652
- bold6("specialists <command>"),
19309
+ bold7("specialists <command>"),
18653
19310
  "",
18654
19311
  "Commands:",
18655
- ...COMMANDS.map(([cmd2, desc]) => ` ${cmd2.padEnd(COL_WIDTH)} ${dim6(desc)}`),
19312
+ ...COMMANDS.map(([cmd2, desc]) => ` ${cmd2.padEnd(COL_WIDTH)} ${dim10(desc)}`),
18656
19313
  "",
18657
- dim6("Run 'specialists <command> --help' for command-specific options."),
19314
+ dim10("Run 'specialists <command> --help' for command-specific options."),
18658
19315
  ""
18659
19316
  ];
18660
19317
  console.log(lines.join(`
18661
19318
  `));
18662
19319
  }
18663
- var bold6 = (s) => `\x1B[1m${s}\x1B[0m`, dim6 = (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;
18664
19321
  var init_help = __esm(() => {
18665
19322
  COMMANDS = [
18666
19323
  ["install", "Full-stack installer: pi, beads, dolt, MCP registration, hooks"],
18667
19324
  ["list", "List available specialists with model and description"],
19325
+ ["models", "List models available on pi, flagged with thinking/images support"],
18668
19326
  ["version", "Print installed version"],
18669
19327
  ["init", "Initialize specialists in the current project"],
18670
19328
  ["edit", "Edit a specialist field (e.g. --model, --description)"],
18671
- ["run", "Run a specialist with a prompt"],
18672
- ["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)"],
18673
19334
  ["help", "Show this help message"]
18674
19335
  ];
18675
19336
  COL_WIDTH = Math.max(...COMMANDS.map(([cmd2]) => cmd2.length));
@@ -26046,7 +26707,7 @@ var runParallelSchema = objectType({
26046
26707
  function createRunParallelTool(runner) {
26047
26708
  return {
26048
26709
  name: "run_parallel",
26049
- 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.",
26050
26711
  inputSchema: runParallelSchema,
26051
26712
  async execute(input, onProgress) {
26052
26713
  if (input.merge_strategy === "pipeline") {
@@ -26085,11 +26746,26 @@ var BACKENDS2 = ["gemini", "qwen", "anthropic", "openai"];
26085
26746
  function createSpecialistStatusTool(loader, circuitBreaker) {
26086
26747
  return {
26087
26748
  name: "specialist_status",
26088
- 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/.",
26089
26750
  inputSchema: exports_external.object({}),
26090
26751
  async execute(_) {
26091
26752
  const list = await loader.list();
26092
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
+ }
26093
26769
  return {
26094
26770
  loaded_count: list.length,
26095
26771
  backends_health: Object.fromEntries(BACKENDS2.map((b) => [b, circuitBreaker.getState(b)])),
@@ -26099,6 +26775,15 @@ function createSpecialistStatusTool(loader, circuitBreaker) {
26099
26775
  category: s.category,
26100
26776
  version: s.version,
26101
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
26102
26787
  }))
26103
26788
  };
26104
26789
  }
@@ -26224,7 +26909,7 @@ var startSpecialistSchema = exports_external.object({
26224
26909
  function createStartSpecialistTool(runner, registry2) {
26225
26910
  return {
26226
26911
  name: "start_specialist",
26227
- 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.",
26228
26913
  inputSchema: startSpecialistSchema,
26229
26914
  async execute(input) {
26230
26915
  const jobId = await runner.startAsync({
@@ -26247,7 +26932,7 @@ var pollSpecialistSchema = exports_external.object({
26247
26932
  function createPollSpecialistTool(registry2) {
26248
26933
  return {
26249
26934
  name: "poll_specialist",
26250
- 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.',
26251
26936
  inputSchema: pollSpecialistSchema,
26252
26937
  async execute(input) {
26253
26938
  const snapshot = registry2.snapshot(input.job_id, input.cursor ?? 0);
@@ -26267,7 +26952,7 @@ var stopSpecialistSchema = exports_external.object({
26267
26952
  function createStopSpecialistTool(registry2) {
26268
26953
  return {
26269
26954
  name: "stop_specialist",
26270
- 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.",
26271
26956
  inputSchema: stopSpecialistSchema,
26272
26957
  async execute(input) {
26273
26958
  const result = registry2.cancel(input.job_id);
@@ -26405,6 +27090,11 @@ class SpecialistsServer {
26405
27090
  const transport = new StdioServerTransport;
26406
27091
  await this.server.connect(transport);
26407
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
+ });
26408
27098
  } catch (error2) {
26409
27099
  logger.error("Failed to start server", error2);
26410
27100
  process.exit(1);
@@ -26417,7 +27107,7 @@ class SpecialistsServer {
26417
27107
 
26418
27108
  // src/index.ts
26419
27109
  var sub = process.argv[2];
26420
- async function run9() {
27110
+ async function run13() {
26421
27111
  if (sub === "install") {
26422
27112
  const { run: handler } = await Promise.resolve().then(() => (init_install(), exports_install));
26423
27113
  return handler();
@@ -26430,6 +27120,10 @@ async function run9() {
26430
27120
  const { run: handler } = await Promise.resolve().then(() => (init_list(), exports_list));
26431
27121
  return handler();
26432
27122
  }
27123
+ if (sub === "models") {
27124
+ const { run: handler } = await Promise.resolve().then(() => (init_models(), exports_models));
27125
+ return handler();
27126
+ }
26433
27127
  if (sub === "init") {
26434
27128
  const { run: handler } = await Promise.resolve().then(() => (init_init(), exports_init));
26435
27129
  return handler();
@@ -26446,6 +27140,18 @@ async function run9() {
26446
27140
  const { run: handler } = await Promise.resolve().then(() => (init_status(), exports_status));
26447
27141
  return handler();
26448
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
+ }
26449
27155
  if (sub === "help" || sub === "--help" || sub === "-h") {
26450
27156
  const { run: handler } = await Promise.resolve().then(() => (init_help(), exports_help));
26451
27157
  return handler();
@@ -26459,7 +27165,7 @@ Run 'specialists help' to see available commands.`);
26459
27165
  const server = new SpecialistsServer;
26460
27166
  await server.start();
26461
27167
  }
26462
- run9().catch((error2) => {
27168
+ run13().catch((error2) => {
26463
27169
  logger.error(`Fatal error: ${error2}`);
26464
27170
  process.exit(1);
26465
27171
  });