@jaggerxtrm/specialists 3.4.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -17442,9 +17442,6 @@ function getFriendlyMessage(issue2) {
17442
17442
  if (issue2.code === "invalid_literal") {
17443
17443
  return `Invalid value at "${path}": expected "${issue2.expected}"`;
17444
17444
  }
17445
- if (issue2.code === "missing_key") {
17446
- return `Missing required field: "${formatPath(issue2.path)}"`;
17447
- }
17448
17445
  return issue2.message;
17449
17446
  }
17450
17447
  async function validateSpecialist(yamlContent) {
@@ -17505,7 +17502,7 @@ ${result.warnings.map((w) => ` ⚠ ${w}`).join(`
17505
17502
  const raw = $parse(yamlContent);
17506
17503
  return SpecialistSchema.parseAsync(raw);
17507
17504
  }
17508
- var KebabCase, Semver, MetadataSchema, ExecutionSchema, PromptSchema2, ScriptEntrySchema, SkillsSchema, CapabilitiesSchema, CommunicationSchema, ValidationSchema, SpecialistSchema;
17505
+ var KebabCase, Semver, MetadataSchema, ExecutionSchema, PromptSchema2, ScriptEntrySchema, SkillsSchema, CapabilitiesSchema, CommunicationSchema, ValidationSchema, StallDetectionSchema, SpecialistSchema;
17509
17506
  var init_schema = __esm(() => {
17510
17507
  init_zod();
17511
17508
  init_dist();
@@ -17527,6 +17524,8 @@ var init_schema = __esm(() => {
17527
17524
  fallback_model: stringType().optional(),
17528
17525
  timeout_ms: numberType().default(120000),
17529
17526
  stall_timeout_ms: numberType().optional(),
17527
+ max_retries: numberType().int().min(0).default(0),
17528
+ interactive: booleanType().default(false),
17530
17529
  response_format: enumType(["text", "json", "markdown"]).default("text"),
17531
17530
  permission_required: enumType(["READ_ONLY", "LOW", "MEDIUM", "HIGH"]).default("READ_ONLY"),
17532
17531
  thinking_level: enumType(["off", "minimal", "low", "medium", "high", "xhigh"]).optional(),
@@ -17564,9 +17563,14 @@ var init_schema = __esm(() => {
17564
17563
  }).optional();
17565
17564
  ValidationSchema = objectType({
17566
17565
  files_to_watch: arrayType(stringType()).optional(),
17567
- references: arrayType(unknownType()).optional(),
17568
17566
  stale_threshold_days: numberType().optional()
17569
17567
  }).optional();
17568
+ StallDetectionSchema = objectType({
17569
+ running_silence_warn_ms: numberType().optional(),
17570
+ running_silence_error_ms: numberType().optional(),
17571
+ waiting_stale_ms: numberType().optional(),
17572
+ tool_duration_warn_ms: numberType().optional()
17573
+ }).optional();
17570
17574
  SpecialistSchema = objectType({
17571
17575
  specialist: objectType({
17572
17576
  metadata: MetadataSchema,
@@ -17576,8 +17580,10 @@ var init_schema = __esm(() => {
17576
17580
  capabilities: CapabilitiesSchema,
17577
17581
  communication: CommunicationSchema,
17578
17582
  validation: ValidationSchema,
17583
+ stall_detection: StallDetectionSchema,
17579
17584
  output_file: stringType().optional(),
17580
17585
  beads_integration: enumType(["auto", "always", "never"]).default("auto"),
17586
+ beads_write_notes: booleanType().default(true),
17581
17587
  heartbeat: unknownType().optional()
17582
17588
  })
17583
17589
  });
@@ -17614,8 +17620,13 @@ class SpecialistLoader {
17614
17620
  }
17615
17621
  getScanDirs() {
17616
17622
  const dirs = [
17623
+ { path: join(this.projectDir, ".specialists", "user"), scope: "user" },
17617
17624
  { path: join(this.projectDir, ".specialists", "user", "specialists"), scope: "user" },
17618
- { path: join(this.projectDir, ".specialists", "default", "specialists"), scope: "default" }
17625
+ { path: join(this.projectDir, ".specialists", "default"), scope: "default" },
17626
+ { path: join(this.projectDir, ".specialists", "default", "specialists"), scope: "default" },
17627
+ { path: join(this.projectDir, "specialists"), scope: "default" },
17628
+ { path: join(this.projectDir, ".claude", "specialists"), scope: "default" },
17629
+ { path: join(this.projectDir, ".agent-forge", "specialists"), scope: "default" }
17619
17630
  ];
17620
17631
  return dirs.filter((d) => existsSync(d.path));
17621
17632
  }
@@ -17645,7 +17656,8 @@ class SpecialistLoader {
17645
17656
  filePath,
17646
17657
  updated,
17647
17658
  filestoWatch: spec.specialist.validation?.files_to_watch,
17648
- staleThresholdDays: spec.specialist.validation?.stale_threshold_days
17659
+ staleThresholdDays: spec.specialist.validation?.stale_threshold_days,
17660
+ stallDetection: spec.specialist.stall_detection ?? undefined
17649
17661
  });
17650
17662
  } catch (e) {
17651
17663
  const reason = e instanceof Error ? e.message : String(e);
@@ -17759,7 +17771,11 @@ class PiAgentSession {
17759
17771
  _agentEndReceived = false;
17760
17772
  _killed = false;
17761
17773
  _lineBuffer = "";
17762
- _pendingCommand;
17774
+ _pendingRequests = new Map;
17775
+ _nextRequestId = 1;
17776
+ _stderrBuffer = "";
17777
+ _stallTimer;
17778
+ _stallError;
17763
17779
  meta;
17764
17780
  constructor(options, meta) {
17765
17781
  this.options = options;
@@ -17809,7 +17825,7 @@ class PiAgentSession {
17809
17825
  args.push("--append-system-prompt", this.options.systemPrompt);
17810
17826
  }
17811
17827
  this.proc = spawn("pi", args, {
17812
- stdio: ["pipe", "pipe", "inherit"],
17828
+ stdio: ["pipe", "pipe", "pipe"],
17813
17829
  cwd: this.options.cwd
17814
17830
  });
17815
17831
  const donePromise = new Promise((resolve, reject) => {
@@ -17818,6 +17834,9 @@ class PiAgentSession {
17818
17834
  });
17819
17835
  donePromise.catch(() => {});
17820
17836
  this._donePromise = donePromise;
17837
+ this.proc.stderr?.on("data", (chunk) => {
17838
+ this._stderrBuffer += chunk.toString();
17839
+ });
17821
17840
  this.proc.stdout?.on("data", (chunk) => {
17822
17841
  this._lineBuffer += chunk.toString();
17823
17842
  const lines = this._lineBuffer.split(`
@@ -17835,6 +17854,7 @@ class PiAgentSession {
17835
17854
  }
17836
17855
  });
17837
17856
  this.proc.on("close", (code) => {
17857
+ this._clearStallTimer();
17838
17858
  if (this._agentEndReceived || this._killed) {
17839
17859
  this._doneResolve?.();
17840
17860
  } else if (code === 0 || code === null) {
@@ -17844,6 +17864,25 @@ class PiAgentSession {
17844
17864
  }
17845
17865
  });
17846
17866
  }
17867
+ _clearStallTimer() {
17868
+ if (this._stallTimer) {
17869
+ clearTimeout(this._stallTimer);
17870
+ this._stallTimer = undefined;
17871
+ }
17872
+ }
17873
+ _markActivity() {
17874
+ const timeoutMs = this.options.stallTimeoutMs;
17875
+ if (!timeoutMs || timeoutMs <= 0 || this._killed || this._agentEndReceived)
17876
+ return;
17877
+ this._clearStallTimer();
17878
+ this._stallTimer = setTimeout(() => {
17879
+ if (this._killed || this._agentEndReceived)
17880
+ return;
17881
+ const err = new StallTimeoutError(timeoutMs);
17882
+ this._stallError = err;
17883
+ this.kill(err);
17884
+ }, timeoutMs);
17885
+ }
17847
17886
  _handleEvent(line) {
17848
17887
  let event;
17849
17888
  try {
@@ -17851,11 +17890,18 @@ class PiAgentSession {
17851
17890
  } catch {
17852
17891
  return;
17853
17892
  }
17893
+ this._markActivity();
17854
17894
  const { type } = event;
17855
17895
  if (type === "response") {
17856
- const handler = this._pendingCommand;
17857
- this._pendingCommand = undefined;
17858
- handler?.(event);
17896
+ const id = event.id;
17897
+ if (id !== undefined) {
17898
+ const entry = this._pendingRequests.get(id);
17899
+ if (entry) {
17900
+ clearTimeout(entry.timer);
17901
+ this._pendingRequests.delete(id);
17902
+ entry.resolve(event);
17903
+ }
17904
+ }
17859
17905
  return;
17860
17906
  }
17861
17907
  if (type === "message_start") {
@@ -17895,11 +17941,13 @@ class PiAgentSession {
17895
17941
  this._lastOutput = last.content.filter((c) => c.type === "text").map((c) => c.text).join("");
17896
17942
  }
17897
17943
  this._agentEndReceived = true;
17944
+ this._clearStallTimer();
17898
17945
  this.options.onEvent?.("agent_end");
17899
17946
  this._doneResolve?.();
17900
17947
  return;
17901
17948
  }
17902
17949
  if (type === "tool_execution_start") {
17950
+ this.options.onToolStart?.(event.toolName ?? event.name ?? "tool", event.args, event.toolCallId);
17903
17951
  this.options.onEvent?.("tool_execution_start");
17904
17952
  return;
17905
17953
  }
@@ -17908,10 +17956,18 @@ class PiAgentSession {
17908
17956
  return;
17909
17957
  }
17910
17958
  if (type === "tool_execution_end") {
17911
- this.options.onToolEnd?.(event.toolName ?? event.name ?? "tool");
17959
+ this.options.onToolEnd?.(event.toolName ?? event.name ?? "tool", event.isError ?? false, event.toolCallId);
17912
17960
  this.options.onEvent?.("tool_execution_end");
17913
17961
  return;
17914
17962
  }
17963
+ if (type === "auto_compaction_start" || type === "auto_compaction_end") {
17964
+ this.options.onEvent?.("auto_compaction");
17965
+ return;
17966
+ }
17967
+ if (type === "auto_retry_start" || type === "auto_retry_end") {
17968
+ this.options.onEvent?.("auto_retry");
17969
+ return;
17970
+ }
17915
17971
  if (type === "message_update") {
17916
17972
  const ae = event.assistantMessageEvent;
17917
17973
  if (!ae)
@@ -17943,26 +17999,38 @@ class PiAgentSession {
17943
17999
  }
17944
18000
  }
17945
18001
  }
17946
- sendCommand(cmd) {
18002
+ sendCommand(cmd, timeoutMs = 1e4) {
17947
18003
  return new Promise((resolve, reject) => {
17948
18004
  if (!this.proc?.stdin) {
17949
18005
  reject(new Error("No stdin available"));
17950
18006
  return;
17951
18007
  }
17952
- this._pendingCommand = resolve;
17953
- this.proc.stdin.write(JSON.stringify(cmd) + `
18008
+ const id = this._nextRequestId++;
18009
+ const timer = setTimeout(() => {
18010
+ this._pendingRequests.delete(id);
18011
+ reject(new Error(`RPC timeout: no response for command id=${id} after ${timeoutMs}ms`));
18012
+ }, timeoutMs);
18013
+ this._pendingRequests.set(id, { resolve, reject, timer });
18014
+ this.proc.stdin.write(JSON.stringify({ ...cmd, id }) + `
17954
18015
  `, (err) => {
17955
18016
  if (err) {
17956
- this._pendingCommand = undefined;
18017
+ const entry = this._pendingRequests.get(id);
18018
+ if (entry) {
18019
+ clearTimeout(entry.timer);
18020
+ this._pendingRequests.delete(id);
18021
+ }
17957
18022
  reject(err);
17958
18023
  }
17959
18024
  });
17960
18025
  });
17961
18026
  }
17962
18027
  async prompt(task) {
17963
- const msg = JSON.stringify({ type: "prompt", message: task }) + `
17964
- `;
17965
- this.proc?.stdin?.write(msg);
18028
+ this._stallError = undefined;
18029
+ this._markActivity();
18030
+ const response = await this.sendCommand({ type: "prompt", message: task });
18031
+ if (response?.success === false) {
18032
+ throw new Error(`Prompt rejected by pi: ${response.error ?? "already streaming"}`);
18033
+ }
17966
18034
  }
17967
18035
  async waitForDone(timeout) {
17968
18036
  const donePromise = this._donePromise ?? Promise.resolve();
@@ -18001,6 +18069,7 @@ class PiAgentSession {
18001
18069
  async close() {
18002
18070
  if (this._killed)
18003
18071
  return;
18072
+ this._clearStallTimer();
18004
18073
  this.proc?.stdin?.end();
18005
18074
  if (this.proc) {
18006
18075
  await new Promise((resolve) => {
@@ -18014,23 +18083,41 @@ class PiAgentSession {
18014
18083
  });
18015
18084
  }
18016
18085
  }
18017
- kill() {
18086
+ kill(reason) {
18018
18087
  if (this._killed)
18019
18088
  return;
18020
18089
  this._killed = true;
18090
+ this._clearStallTimer();
18091
+ if (this.proc?.stdin?.writable) {
18092
+ try {
18093
+ this.proc.stdin.write(JSON.stringify({ type: "abort" }) + `
18094
+ `);
18095
+ } catch {}
18096
+ }
18097
+ const killError = reason ?? this._stallError ?? new SessionKilledError;
18098
+ for (const [, entry] of this._pendingRequests) {
18099
+ clearTimeout(entry.timer);
18100
+ entry.reject(killError);
18101
+ }
18102
+ this._pendingRequests.clear();
18021
18103
  this.proc?.kill();
18022
18104
  this.proc = undefined;
18023
- this._doneReject?.(new SessionKilledError);
18105
+ this._doneReject?.(killError);
18106
+ }
18107
+ getStderr() {
18108
+ return this._stderrBuffer;
18024
18109
  }
18025
18110
  async steer(message) {
18026
18111
  if (this._killed || !this.proc?.stdin) {
18027
18112
  throw new Error("Session is not active");
18028
18113
  }
18029
- const cmd = JSON.stringify({ type: "steer", message }) + `
18030
- `;
18031
- await new Promise((resolve, reject) => {
18032
- this.proc.stdin.write(cmd, (err) => err ? reject(err) : resolve());
18033
- });
18114
+ const response = await this.sendCommand({ type: "steer", message });
18115
+ if (response?.success === false) {
18116
+ throw new Error(`Steer rejected by pi: ${response.error ?? "steer failed"}`);
18117
+ }
18118
+ }
18119
+ followUp(_task) {
18120
+ throw new Error("followUp() is not yet implemented. Use resume() to send a next-turn prompt to a waiting session.");
18034
18121
  }
18035
18122
  async resume(task, timeout) {
18036
18123
  if (this._killed || !this.proc?.stdin) {
@@ -18047,7 +18134,7 @@ class PiAgentSession {
18047
18134
  await this.waitForDone(timeout);
18048
18135
  }
18049
18136
  }
18050
- var SessionKilledError;
18137
+ var SessionKilledError, StallTimeoutError;
18051
18138
  var init_session = __esm(() => {
18052
18139
  init_backendMap();
18053
18140
  SessionKilledError = class SessionKilledError extends Error {
@@ -18056,6 +18143,74 @@ var init_session = __esm(() => {
18056
18143
  this.name = "SessionKilledError";
18057
18144
  }
18058
18145
  };
18146
+ StallTimeoutError = class StallTimeoutError extends Error {
18147
+ constructor(timeoutMs) {
18148
+ super(`Session stalled: no activity for ${timeoutMs}ms`);
18149
+ this.name = "StallTimeoutError";
18150
+ }
18151
+ };
18152
+ });
18153
+
18154
+ // src/utils/circuitBreaker.ts
18155
+ function isTransientError(error2) {
18156
+ if (!error2)
18157
+ return false;
18158
+ const status = error2.status ?? error2.statusCode;
18159
+ if (typeof status === "number" && status >= 500 && status < 600) {
18160
+ return true;
18161
+ }
18162
+ const message = error2 instanceof Error ? error2.message : typeof error2 === "string" ? error2 : JSON.stringify(error2);
18163
+ return TRANSIENT_ERROR_PATTERNS.some((pattern) => pattern.test(message));
18164
+ }
18165
+
18166
+ class CircuitBreaker {
18167
+ states = new Map;
18168
+ threshold;
18169
+ cooldownMs;
18170
+ constructor(options = {}) {
18171
+ this.threshold = options.failureThreshold ?? 3;
18172
+ this.cooldownMs = options.cooldownMs ?? 60000;
18173
+ }
18174
+ getState(backend) {
18175
+ const entry = this.states.get(backend);
18176
+ if (!entry)
18177
+ return "CLOSED";
18178
+ if (entry.state === "OPEN" && Date.now() - entry.openedAt > this.cooldownMs) {
18179
+ entry.state = "HALF_OPEN";
18180
+ }
18181
+ return entry.state;
18182
+ }
18183
+ isAvailable(backend) {
18184
+ return this.getState(backend) !== "OPEN";
18185
+ }
18186
+ recordSuccess(backend) {
18187
+ this.states.set(backend, { state: "CLOSED", failures: 0 });
18188
+ }
18189
+ recordFailure(backend) {
18190
+ const entry = this.states.get(backend) ?? { state: "CLOSED", failures: 0 };
18191
+ entry.failures++;
18192
+ if (entry.failures >= this.threshold) {
18193
+ entry.state = "OPEN";
18194
+ entry.openedAt = Date.now();
18195
+ }
18196
+ this.states.set(backend, entry);
18197
+ }
18198
+ }
18199
+ var TRANSIENT_ERROR_PATTERNS;
18200
+ var init_circuitBreaker = __esm(() => {
18201
+ TRANSIENT_ERROR_PATTERNS = [
18202
+ /\b5\d{2}\b/,
18203
+ /timeout/i,
18204
+ /timed out/i,
18205
+ /econnreset/i,
18206
+ /econnrefused/i,
18207
+ /eai_again/i,
18208
+ /etimedout/i,
18209
+ /network error/i,
18210
+ /service unavailable/i,
18211
+ /bad gateway/i,
18212
+ /gateway timeout/i
18213
+ ];
18059
18214
  });
18060
18215
 
18061
18216
  // src/specialist/beads.ts
@@ -18065,6 +18220,9 @@ function buildBeadContext(bead, completedBlockers = []) {
18065
18220
  if (bead.description?.trim()) {
18066
18221
  lines.push(bead.description.trim());
18067
18222
  }
18223
+ if (bead.parent?.trim()) {
18224
+ lines.push("", "## Parent epic", bead.parent.trim());
18225
+ }
18068
18226
  if (bead.notes?.trim()) {
18069
18227
  lines.push("", "## Notes", bead.notes.trim());
18070
18228
  }
@@ -18203,12 +18361,17 @@ import { execSync, spawnSync as spawnSync2 } from "node:child_process";
18203
18361
  import { existsSync as existsSync3, readFileSync } from "node:fs";
18204
18362
  import { basename, resolve } from "node:path";
18205
18363
  import { homedir as homedir2 } from "node:os";
18206
- function runScript(run) {
18364
+ function runScript(command) {
18365
+ const run = (command ?? "").trim();
18366
+ if (!run) {
18367
+ return { name: "unknown", output: "Missing script command (expected `run` or legacy `path`).", exitCode: 1 };
18368
+ }
18369
+ const scriptName = basename(run.split(" ")[0]);
18207
18370
  try {
18208
18371
  const output = execSync(run, { encoding: "utf8", timeout: 30000 });
18209
- return { name: basename(run.split(" ")[0]), output, exitCode: 0 };
18372
+ return { name: scriptName, output, exitCode: 0 };
18210
18373
  } catch (e) {
18211
- return { name: basename(run.split(" ")[0]), output: e.stdout ?? e.message ?? "", exitCode: e.status ?? 1 };
18374
+ return { name: scriptName, output: e.stdout ?? e.message ?? "", exitCode: e.status ?? 1 };
18212
18375
  }
18213
18376
  }
18214
18377
  function formatScriptOutput(results) {
@@ -18253,7 +18416,14 @@ function validateShebang(filePath, errors5) {
18253
18416
  }
18254
18417
  } catch {}
18255
18418
  }
18256
- function validateBeforeRun(spec) {
18419
+ function isToolAvailable(tool, permissionLevel) {
18420
+ const normalized = permissionLevel.toUpperCase();
18421
+ const gatedLevels = PERMISSION_GATED_TOOLS[tool.toLowerCase()];
18422
+ if (!gatedLevels)
18423
+ return true;
18424
+ return gatedLevels.includes(normalized);
18425
+ }
18426
+ function validateBeforeRun(spec, permissionLevel) {
18257
18427
  const errors5 = [];
18258
18428
  const warnings = [];
18259
18429
  for (const p of spec.specialist.skills?.paths ?? []) {
@@ -18262,7 +18432,7 @@ function validateBeforeRun(spec) {
18262
18432
  warnings.push(` ⚠ skills.paths: file not found: ${p}`);
18263
18433
  }
18264
18434
  for (const script of spec.specialist.skills?.scripts ?? []) {
18265
- const { run } = script;
18435
+ const run = script.run ?? script.path;
18266
18436
  if (!run)
18267
18437
  continue;
18268
18438
  const isFilePath = run.startsWith("./") || run.startsWith("../") || run.startsWith("/") || run.startsWith("~/");
@@ -18285,6 +18455,11 @@ function validateBeforeRun(spec) {
18285
18455
  errors5.push(` ✗ capabilities.external_commands: not found on PATH: ${cmd}`);
18286
18456
  }
18287
18457
  }
18458
+ for (const tool of spec.specialist.capabilities?.required_tools ?? []) {
18459
+ if (!isToolAvailable(tool, permissionLevel)) {
18460
+ errors5.push(` ✗ capabilities.required_tools: tool "${tool}" requires higher permission than "${permissionLevel}"`);
18461
+ }
18462
+ }
18288
18463
  if (warnings.length > 0) {
18289
18464
  process.stderr.write(`[specialists] pre-run warnings:
18290
18465
  ${warnings.join(`
@@ -18297,6 +18472,18 @@ ${errors5.join(`
18297
18472
  `)}`);
18298
18473
  }
18299
18474
  }
18475
+ function sleep(ms) {
18476
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
18477
+ }
18478
+ function getRetryDelayMs(attemptNumber) {
18479
+ const baseDelay = RETRY_BASE_DELAY_MS * 2 ** Math.max(0, attemptNumber - 1);
18480
+ const jitterMultiplier = 1 + (Math.random() * 2 - 1) * RETRY_MAX_JITTER;
18481
+ return Math.max(0, Math.round(baseDelay * jitterMultiplier));
18482
+ }
18483
+ function isAuthError(error2) {
18484
+ const message = error2 instanceof Error ? error2.message : typeof error2 === "string" ? error2 : JSON.stringify(error2);
18485
+ return /\b(401|403|unauthorized|forbidden|authentication|auth|invalid api key|api key)\b/i.test(message);
18486
+ }
18300
18487
 
18301
18488
  class SpecialistRunner {
18302
18489
  deps;
@@ -18305,7 +18492,7 @@ class SpecialistRunner {
18305
18492
  this.deps = deps;
18306
18493
  this.sessionFactory = deps.sessionFactory ?? PiAgentSession.create.bind(PiAgentSession);
18307
18494
  }
18308
- async run(options, onProgress, onEvent, onMeta, onKillRegistered, onBeadCreated, onSteerRegistered, onResumeReady) {
18495
+ async run(options, onProgress, onEvent, onMeta, onKillRegistered, onBeadCreated, onSteerRegistered, onResumeReady, onToolStartCallback, onToolEndCallback) {
18309
18496
  const { loader, hooks, circuitBreaker, beadsClient } = this.deps;
18310
18497
  const invocationId = crypto.randomUUID();
18311
18498
  const start = Date.now();
@@ -18321,9 +18508,11 @@ class SpecialistRunner {
18321
18508
  circuit_breaker_state: circuitBreaker.getState(model),
18322
18509
  scope: "project"
18323
18510
  });
18324
- validateBeforeRun(spec);
18511
+ const permissionLevel = options.autonomyLevel ?? execution.permission_required;
18512
+ const effectiveKeepAlive = options.noKeepAlive ? false : options.keepAlive ?? execution.interactive ?? false;
18513
+ validateBeforeRun(spec, permissionLevel);
18325
18514
  const preScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "pre") ?? [];
18326
- const preResults = preScripts.map((s) => runScript(s.run)).filter((_, i) => preScripts[i].inject_output);
18515
+ const preResults = preScripts.map((s) => runScript(s.run ?? s.path)).filter((_, i) => preScripts[i].inject_output);
18327
18516
  const preScriptOutput = formatScriptOutput(preResults);
18328
18517
  const beadVariables = options.inputBeadId ? { bead_context: options.prompt, bead_id: options.inputBeadId } : {};
18329
18518
  const variables = {
@@ -18341,7 +18530,20 @@ class SpecialistRunner {
18341
18530
  estimated_tokens: Math.ceil(renderedTask.length / 4),
18342
18531
  system_prompt_present: !!prompt.system
18343
18532
  });
18344
- const agentsMd = prompt.system ?? "";
18533
+ let agentsMd = prompt.system ?? "";
18534
+ if (options.inputBeadId) {
18535
+ agentsMd += `
18536
+
18537
+ ---
18538
+ ## Specialist Run Context
18539
+ You are running as a specialist with bead ${options.inputBeadId} as your task.
18540
+ - Claim this bead directly: \`bd update ${options.inputBeadId} --claim\`
18541
+ - Do NOT create new beads or sub-issues — this bead IS your task.
18542
+ - Do NOT run \`bd create\` — the orchestrator manages issue tracking.
18543
+ - Close the bead when done: \`bd close ${options.inputBeadId} --reason="..."\`
18544
+ ---
18545
+ `;
18546
+ }
18345
18547
  const skillPaths = [];
18346
18548
  if (prompt.skill_inherit)
18347
18549
  skillPaths.push(prompt.skill_inherit);
@@ -18360,7 +18562,7 @@ ${skillPaths.map((p) => ` • ${p}`).join(`
18360
18562
  }
18361
18563
  if (preScripts.length > 0) {
18362
18564
  onProgress?.(` pre scripts/commands:
18363
- ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_output" : ""}`).join(`
18565
+ ${preScripts.map((s) => ` • ${s.run ?? s.path ?? "<missing>"}${s.inject_output ? " → $pre_script_output" : ""}`).join(`
18364
18566
  `)}
18365
18567
  `);
18366
18568
  }
@@ -18368,13 +18570,6 @@ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_o
18368
18570
 
18369
18571
  `);
18370
18572
  }
18371
- const permissionLevel = options.autonomyLevel ?? execution.permission_required;
18372
- await hooks.emit("pre_execute", invocationId, metadata.name, metadata.version, {
18373
- backend: model,
18374
- model,
18375
- timeout_ms: execution.timeout_ms,
18376
- permission_level: permissionLevel
18377
- });
18378
18573
  const beadsIntegration = spec.specialist.beads_integration ?? "auto";
18379
18574
  let beadId;
18380
18575
  let ownsBead = false;
@@ -18387,11 +18582,19 @@ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_o
18387
18582
  onBeadCreated?.(beadId);
18388
18583
  }
18389
18584
  }
18585
+ await hooks.emit("pre_execute", invocationId, metadata.name, metadata.version, {
18586
+ backend: model,
18587
+ model,
18588
+ timeout_ms: execution.timeout_ms,
18589
+ permission_level: permissionLevel
18590
+ });
18390
18591
  let output;
18391
18592
  let sessionBackend = model;
18392
18593
  let session;
18393
18594
  let keepAliveActive = false;
18394
18595
  let sessionClosed = false;
18596
+ const maxRetries = Math.max(0, Math.trunc(options.maxRetries ?? execution.max_retries ?? 0));
18597
+ const maxAttempts = maxRetries + 1;
18395
18598
  try {
18396
18599
  session = await this.sessionFactory({
18397
18600
  model,
@@ -18399,25 +18602,50 @@ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_o
18399
18602
  skillPaths: skillPaths.length > 0 ? skillPaths : undefined,
18400
18603
  thinkingLevel: execution.thinking_level,
18401
18604
  permissionLevel,
18605
+ stallTimeoutMs: execution.stall_timeout_ms,
18402
18606
  cwd: process.cwd(),
18403
18607
  onToken: (delta) => onProgress?.(delta),
18404
18608
  onThinking: (delta) => onProgress?.(`\uD83D\uDCAD ${delta}`),
18405
- onToolStart: (tool) => onProgress?.(`
18406
- ⚙ ${tool}…`),
18407
- onToolEnd: (_tool) => onProgress?.(`✓
18408
- `),
18609
+ onToolStart: (tool, args, toolCallId) => {
18610
+ onProgress?.(`
18611
+ ${tool}…`);
18612
+ onToolStartCallback?.(tool, args, toolCallId);
18613
+ },
18614
+ onToolEnd: (tool, isError, toolCallId) => {
18615
+ onProgress?.(`✓
18616
+ `);
18617
+ onToolEndCallback?.(tool, isError, toolCallId);
18618
+ },
18409
18619
  onEvent: (type) => onEvent?.(type),
18410
18620
  onMeta: (meta) => onMeta?.(meta)
18411
18621
  });
18412
18622
  await session.start();
18413
18623
  onKillRegistered?.(session.kill.bind(session));
18414
18624
  onSteerRegistered?.((msg) => session.steer(msg));
18415
- await session.prompt(renderedTask);
18416
- await session.waitForDone(execution.timeout_ms);
18417
- sessionBackend = session.meta.backend;
18418
- output = await session.getLastOutput();
18419
- sessionBackend = session.meta.backend;
18420
- if (options.keepAlive && onResumeReady) {
18625
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
18626
+ try {
18627
+ await session.prompt(renderedTask);
18628
+ await session.waitForDone(execution.timeout_ms);
18629
+ output = await session.getLastOutput();
18630
+ sessionBackend = session.meta.backend;
18631
+ break;
18632
+ } catch (err) {
18633
+ const shouldRetry = attempt < maxAttempts && !(err instanceof SessionKilledError) && !isAuthError(err) && isTransientError(err);
18634
+ if (!shouldRetry) {
18635
+ throw err;
18636
+ }
18637
+ const delayMs = getRetryDelayMs(attempt);
18638
+ onEvent?.("auto_retry");
18639
+ onProgress?.(`
18640
+ ↻ transient backend error on attempt ${attempt}/${maxAttempts}; retrying in ${delayMs}ms
18641
+ `);
18642
+ await sleep(delayMs);
18643
+ }
18644
+ }
18645
+ if (output === undefined) {
18646
+ throw new Error("Specialist run finished without output");
18647
+ }
18648
+ if (effectiveKeepAlive && onResumeReady) {
18421
18649
  keepAliveActive = true;
18422
18650
  const resumeFn = async (msg) => {
18423
18651
  await session.resume(msg, execution.timeout_ms);
@@ -18434,7 +18662,7 @@ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_o
18434
18662
  }
18435
18663
  const postScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "post") ?? [];
18436
18664
  for (const script of postScripts)
18437
- runScript(script.run);
18665
+ runScript(script.run ?? script.path);
18438
18666
  circuitBreaker.recordSuccess(model);
18439
18667
  } catch (err) {
18440
18668
  const isCancelled = err instanceof SessionKilledError;
@@ -18469,8 +18697,6 @@ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_o
18469
18697
  output_valid: true
18470
18698
  });
18471
18699
  if (beadId) {
18472
- if (ownsBead)
18473
- beadsClient?.closeBead(beadId, "COMPLETE", durationMs, model);
18474
18700
  beadsClient?.auditBead(beadId, metadata.name, model, 0);
18475
18701
  }
18476
18702
  return {
@@ -18480,7 +18706,8 @@ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_o
18480
18706
  durationMs,
18481
18707
  specialistVersion: metadata.version,
18482
18708
  promptHash,
18483
- beadId
18709
+ beadId,
18710
+ permissionRequired: execution.permission_required
18484
18711
  };
18485
18712
  }
18486
18713
  async startAsync(options, registry2) {
@@ -18499,9 +18726,16 @@ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_o
18499
18726
  return jobId;
18500
18727
  }
18501
18728
  }
18729
+ var PERMISSION_GATED_TOOLS, RETRY_BASE_DELAY_MS = 1000, RETRY_MAX_JITTER = 0.2;
18502
18730
  var init_runner = __esm(() => {
18503
18731
  init_session();
18732
+ init_circuitBreaker();
18504
18733
  init_beads();
18734
+ PERMISSION_GATED_TOOLS = {
18735
+ bash: ["LOW", "MEDIUM", "HIGH"],
18736
+ edit: ["MEDIUM", "HIGH"],
18737
+ write: ["HIGH"]
18738
+ };
18505
18739
  });
18506
18740
 
18507
18741
  // src/specialist/hooks.ts
@@ -18540,62 +18774,21 @@ class HookEmitter {
18540
18774
  }
18541
18775
  var init_hooks = () => {};
18542
18776
 
18543
- // src/utils/circuitBreaker.ts
18544
- class CircuitBreaker {
18545
- states = new Map;
18546
- threshold;
18547
- cooldownMs;
18548
- constructor(options = {}) {
18549
- this.threshold = options.failureThreshold ?? 3;
18550
- this.cooldownMs = options.cooldownMs ?? 60000;
18551
- }
18552
- getState(backend) {
18553
- const entry = this.states.get(backend);
18554
- if (!entry)
18555
- return "CLOSED";
18556
- if (entry.state === "OPEN" && Date.now() - entry.openedAt > this.cooldownMs) {
18557
- entry.state = "HALF_OPEN";
18558
- }
18559
- return entry.state;
18560
- }
18561
- isAvailable(backend) {
18562
- return this.getState(backend) !== "OPEN";
18563
- }
18564
- recordSuccess(backend) {
18565
- this.states.set(backend, { state: "CLOSED", failures: 0 });
18566
- }
18567
- recordFailure(backend) {
18568
- const entry = this.states.get(backend) ?? { state: "CLOSED", failures: 0 };
18569
- entry.failures++;
18570
- if (entry.failures >= this.threshold) {
18571
- entry.state = "OPEN";
18572
- entry.openedAt = Date.now();
18573
- }
18574
- this.states.set(backend, entry);
18575
- }
18576
- }
18577
-
18578
18777
  // src/specialist/timeline-events.ts
18579
18778
  function mapCallbackEventToTimelineEvent(callbackEvent, context) {
18580
18779
  const t = Date.now();
18581
18780
  switch (callbackEvent) {
18582
18781
  case "thinking":
18583
18782
  return { t, type: TIMELINE_EVENT_TYPES.THINKING };
18584
- case "toolcall":
18585
- return {
18586
- t,
18587
- type: TIMELINE_EVENT_TYPES.TOOL,
18588
- tool: context.tool ?? "unknown",
18589
- phase: "start",
18590
- tool_call_id: context.toolCallId
18591
- };
18592
18783
  case "tool_execution_start":
18593
18784
  return {
18594
18785
  t,
18595
18786
  type: TIMELINE_EVENT_TYPES.TOOL,
18596
18787
  tool: context.tool ?? "unknown",
18597
18788
  phase: "start",
18598
- tool_call_id: context.toolCallId
18789
+ tool_call_id: context.toolCallId,
18790
+ args: context.args,
18791
+ started_at: new Date(t).toISOString()
18599
18792
  };
18600
18793
  case "tool_execution_update":
18601
18794
  case "tool_execution":
@@ -18653,6 +18846,16 @@ function createMetaEvent(model, backend) {
18653
18846
  backend
18654
18847
  };
18655
18848
  }
18849
+ function createStaleWarningEvent(reason, options) {
18850
+ return {
18851
+ t: Date.now(),
18852
+ type: TIMELINE_EVENT_TYPES.STALE_WARNING,
18853
+ reason,
18854
+ silence_ms: options.silence_ms,
18855
+ threshold_ms: options.threshold_ms,
18856
+ ...options.tool !== undefined ? { tool: options.tool } : {}
18857
+ };
18858
+ }
18656
18859
  function createRunCompleteEvent(status, elapsed_s, options) {
18657
18860
  return {
18658
18861
  t: Date.now(),
@@ -18710,6 +18913,7 @@ var init_timeline_events = __esm(() => {
18710
18913
  MESSAGE: "message",
18711
18914
  TURN: "turn",
18712
18915
  RUN_COMPLETE: "run_complete",
18916
+ STALE_WARNING: "stale_warning",
18713
18917
  DONE: "done",
18714
18918
  AGENT_END: "agent_end"
18715
18919
  };
@@ -18717,6 +18921,7 @@ var init_timeline_events = __esm(() => {
18717
18921
 
18718
18922
  // src/specialist/supervisor.ts
18719
18923
  import {
18924
+ appendFileSync,
18720
18925
  closeSync,
18721
18926
  existsSync as existsSync4,
18722
18927
  fsyncSync,
@@ -18834,23 +19039,59 @@ class Supervisor {
18834
19039
  crashRecovery() {
18835
19040
  if (!existsSync4(this.opts.jobsDir))
18836
19041
  return;
19042
+ const thresholds = {
19043
+ ...STALL_DETECTION_DEFAULTS,
19044
+ ...this.opts.stallDetection
19045
+ };
19046
+ const now = Date.now();
18837
19047
  for (const entry of readdirSync(this.opts.jobsDir)) {
18838
19048
  const statusPath = join3(this.opts.jobsDir, entry, "status.json");
18839
19049
  if (!existsSync4(statusPath))
18840
19050
  continue;
18841
19051
  try {
18842
19052
  const s = JSON.parse(readFileSync2(statusPath, "utf-8"));
18843
- if (s.status !== "running" && s.status !== "starting")
18844
- continue;
18845
- if (!s.pid)
18846
- continue;
18847
- try {
18848
- process.kill(s.pid, 0);
18849
- } catch {
18850
- const tmp = statusPath + ".tmp";
18851
- const updated = { ...s, status: "error", error: "Process crashed or was killed" };
18852
- writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
18853
- renameSync(tmp, statusPath);
19053
+ if (s.status === "running" || s.status === "starting") {
19054
+ if (!s.pid)
19055
+ continue;
19056
+ let pidAlive = true;
19057
+ try {
19058
+ process.kill(s.pid, 0);
19059
+ } catch {
19060
+ pidAlive = false;
19061
+ }
19062
+ if (!pidAlive) {
19063
+ const tmp = statusPath + ".tmp";
19064
+ const updated = { ...s, status: "error", error: "Process crashed or was killed" };
19065
+ writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
19066
+ renameSync(tmp, statusPath);
19067
+ } else if (s.status === "running") {
19068
+ const lastEventAt = s.last_event_at_ms ?? s.started_at_ms;
19069
+ const silenceMs = now - lastEventAt;
19070
+ if (silenceMs > thresholds.running_silence_error_ms) {
19071
+ const tmp = statusPath + ".tmp";
19072
+ const updated = {
19073
+ ...s,
19074
+ status: "error",
19075
+ error: `No activity for ${Math.round(silenceMs / 1000)}s (threshold: ${thresholds.running_silence_error_ms / 1000}s)`
19076
+ };
19077
+ writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
19078
+ renameSync(tmp, statusPath);
19079
+ }
19080
+ }
19081
+ } else if (s.status === "waiting") {
19082
+ const lastEventAt = s.last_event_at_ms ?? s.started_at_ms;
19083
+ const silenceMs = now - lastEventAt;
19084
+ if (silenceMs > thresholds.waiting_stale_ms) {
19085
+ const eventsPath = join3(this.opts.jobsDir, entry, "events.jsonl");
19086
+ const event = createStaleWarningEvent("waiting_stale", {
19087
+ silence_ms: silenceMs,
19088
+ threshold_ms: thresholds.waiting_stale_ms
19089
+ });
19090
+ try {
19091
+ appendFileSync(eventsPath, JSON.stringify(event) + `
19092
+ `);
19093
+ } catch {}
19094
+ }
18854
19095
  }
18855
19096
  } catch {}
18856
19097
  }
@@ -18869,7 +19110,8 @@ class Supervisor {
18869
19110
  specialist: runOptions.name,
18870
19111
  status: "starting",
18871
19112
  started_at_ms: startedAtMs,
18872
- pid: process.pid
19113
+ pid: process.pid,
19114
+ ...runOptions.inputBeadId ? { bead_id: runOptions.inputBeadId } : {}
18873
19115
  };
18874
19116
  this.writeStatusFile(id, initialStatus);
18875
19117
  writeFileSync(join3(this.opts.jobsDir, "latest"), `${id}
@@ -18889,24 +19131,69 @@ class Supervisor {
18889
19131
  console.error(`[supervisor] Failed to write event: ${err?.message ?? err}`);
18890
19132
  }
18891
19133
  };
18892
- appendTimelineEvent(createRunStartEvent(runOptions.name));
19134
+ appendTimelineEvent(createRunStartEvent(runOptions.name, runOptions.inputBeadId));
18893
19135
  const fifoPath = join3(dir, "steer.pipe");
18894
- const needsFifo = runOptions.keepAlive;
18895
- if (needsFifo) {
18896
- try {
18897
- execFileSync("mkfifo", [fifoPath]);
18898
- setStatus({ fifo_path: fifoPath });
18899
- } catch {}
18900
- }
19136
+ try {
19137
+ execFileSync("mkfifo", [fifoPath]);
19138
+ setStatus({ fifo_path: fifoPath });
19139
+ } catch {}
18901
19140
  let textLogged = false;
18902
19141
  let currentTool = "";
18903
19142
  let currentToolCallId = "";
19143
+ let currentToolArgs;
19144
+ let currentToolIsError = false;
19145
+ const activeToolCalls = new Map;
18904
19146
  let killFn;
18905
19147
  let steerFn;
18906
19148
  let resumeFn;
18907
19149
  let closeFn;
18908
19150
  let fifoReadStream;
18909
19151
  let fifoReadline;
19152
+ const thresholds = {
19153
+ ...STALL_DETECTION_DEFAULTS,
19154
+ ...this.opts.stallDetection
19155
+ };
19156
+ let lastActivityMs = startedAtMs;
19157
+ let silenceWarnEmitted = false;
19158
+ let toolStartMs;
19159
+ let toolDurationWarnEmitted = false;
19160
+ let stuckIntervalId;
19161
+ stuckIntervalId = setInterval(() => {
19162
+ const now = Date.now();
19163
+ if (statusSnapshot.status === "running") {
19164
+ const silenceMs = now - lastActivityMs;
19165
+ if (!silenceWarnEmitted && silenceMs > thresholds.running_silence_warn_ms) {
19166
+ silenceWarnEmitted = true;
19167
+ appendTimelineEvent(createStaleWarningEvent("running_silence", {
19168
+ silence_ms: silenceMs,
19169
+ threshold_ms: thresholds.running_silence_warn_ms
19170
+ }));
19171
+ }
19172
+ if (silenceMs > thresholds.running_silence_error_ms) {
19173
+ appendTimelineEvent(createStaleWarningEvent("running_silence_error", {
19174
+ silence_ms: silenceMs,
19175
+ threshold_ms: thresholds.running_silence_error_ms
19176
+ }));
19177
+ setStatus({
19178
+ status: "error",
19179
+ error: `No activity for ${Math.round(silenceMs / 1000)}s (threshold: ${thresholds.running_silence_error_ms / 1000}s)`
19180
+ });
19181
+ killFn?.();
19182
+ clearInterval(stuckIntervalId);
19183
+ }
19184
+ }
19185
+ if (toolStartMs !== undefined && !toolDurationWarnEmitted) {
19186
+ const toolDurationMs = now - toolStartMs;
19187
+ if (toolDurationMs > thresholds.tool_duration_warn_ms) {
19188
+ toolDurationWarnEmitted = true;
19189
+ appendTimelineEvent(createStaleWarningEvent("tool_duration", {
19190
+ silence_ms: toolDurationMs,
19191
+ threshold_ms: thresholds.tool_duration_warn_ms,
19192
+ tool: currentTool
19193
+ }));
19194
+ }
19195
+ }
19196
+ }, 1e4);
18910
19197
  const sigtermHandler = () => killFn?.();
18911
19198
  process.once("SIGTERM", sigtermHandler);
18912
19199
  try {
@@ -18919,6 +19206,8 @@ class Supervisor {
18919
19206
  this.opts.onProgress?.(delta);
18920
19207
  }, (eventType) => {
18921
19208
  const now = Date.now();
19209
+ lastActivityMs = now;
19210
+ silenceWarnEmitted = false;
18922
19211
  setStatus({
18923
19212
  status: "running",
18924
19213
  current_event: eventType,
@@ -18927,7 +19216,9 @@ class Supervisor {
18927
19216
  });
18928
19217
  const timelineEvent = mapCallbackEventToTimelineEvent(eventType, {
18929
19218
  tool: currentTool,
18930
- toolCallId: currentToolCallId || undefined
19219
+ toolCallId: currentToolCallId || undefined,
19220
+ args: currentToolArgs,
19221
+ isError: currentToolIsError
18931
19222
  });
18932
19223
  if (timelineEvent) {
18933
19224
  appendTimelineEvent(timelineEvent);
@@ -18945,7 +19236,7 @@ class Supervisor {
18945
19236
  setStatus({ bead_id: beadId });
18946
19237
  }, (fn) => {
18947
19238
  steerFn = fn;
18948
- if (!needsFifo || !existsSync4(fifoPath))
19239
+ if (!existsSync4(fifoPath))
18949
19240
  return;
18950
19241
  fifoReadStream = createReadStream(fifoPath, { flags: "r+" });
18951
19242
  fifoReadline = createInterface({ input: fifoReadStream });
@@ -18954,7 +19245,24 @@ class Supervisor {
18954
19245
  const parsed = JSON.parse(line);
18955
19246
  if (parsed?.type === "steer" && typeof parsed.message === "string") {
18956
19247
  steerFn?.(parsed.message).catch(() => {});
19248
+ } else if (parsed?.type === "resume" && typeof parsed.task === "string") {
19249
+ if (resumeFn) {
19250
+ setStatus({ status: "running", current_event: "starting" });
19251
+ resumeFn(parsed.task).then((output) => {
19252
+ mkdirSync(this.jobDir(id), { recursive: true });
19253
+ writeFileSync(this.resultPath(id), output, "utf-8");
19254
+ setStatus({
19255
+ status: "waiting",
19256
+ current_event: "waiting",
19257
+ elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
19258
+ last_event_at_ms: Date.now()
19259
+ });
19260
+ }).catch((err) => {
19261
+ setStatus({ status: "error", error: err?.message ?? String(err) });
19262
+ });
19263
+ }
18957
19264
  } else if (parsed?.type === "prompt" && typeof parsed.message === "string") {
19265
+ console.error('[specialists] DEPRECATED: FIFO message {type:"prompt"} is deprecated. Use {type:"resume", task:"..."} instead.');
18958
19266
  if (resumeFn) {
18959
19267
  setStatus({ status: "running", current_event: "starting" });
18960
19268
  resumeFn(parsed.message).then((output) => {
@@ -18980,12 +19288,51 @@ class Supervisor {
18980
19288
  resumeFn = rFn;
18981
19289
  closeFn = cFn;
18982
19290
  setStatus({ status: "waiting", current_event: "waiting" });
19291
+ }, (tool, args, toolCallId) => {
19292
+ currentTool = tool;
19293
+ currentToolArgs = args;
19294
+ currentToolCallId = toolCallId ?? "";
19295
+ currentToolIsError = false;
19296
+ toolStartMs = Date.now();
19297
+ toolDurationWarnEmitted = false;
19298
+ setStatus({ current_tool: tool });
19299
+ if (toolCallId) {
19300
+ activeToolCalls.set(toolCallId, { tool, args });
19301
+ }
19302
+ }, (tool, isError, toolCallId) => {
19303
+ if (toolCallId && activeToolCalls.has(toolCallId)) {
19304
+ const entry = activeToolCalls.get(toolCallId);
19305
+ currentTool = entry.tool;
19306
+ currentToolArgs = entry.args;
19307
+ currentToolCallId = toolCallId;
19308
+ activeToolCalls.delete(toolCallId);
19309
+ } else {
19310
+ currentTool = tool;
19311
+ }
19312
+ currentToolIsError = isError;
19313
+ toolStartMs = undefined;
19314
+ toolDurationWarnEmitted = false;
18983
19315
  });
18984
19316
  const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
18985
19317
  mkdirSync(this.jobDir(id), { recursive: true });
18986
19318
  writeFileSync(this.resultPath(id), result.output, "utf-8");
18987
- if (result.beadId) {
19319
+ const inputBeadId = runOptions.inputBeadId;
19320
+ const ownsBead = Boolean(result.beadId && !inputBeadId);
19321
+ const shouldWriteExternalBeadNotes = runOptions.beadsWriteNotes ?? true;
19322
+ const shouldAppendReadOnlyResultToInputBead = Boolean(inputBeadId && result.permissionRequired === "READ_ONLY" && this.opts.beadsClient);
19323
+ if (ownsBead && result.beadId) {
18988
19324
  this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
19325
+ } else if (shouldWriteExternalBeadNotes) {
19326
+ if (shouldAppendReadOnlyResultToInputBead && inputBeadId) {
19327
+ this.opts.beadsClient?.updateBeadNotes(inputBeadId, formatBeadNotes(result));
19328
+ } else if (result.beadId) {
19329
+ this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
19330
+ }
19331
+ }
19332
+ if (result.beadId) {
19333
+ if (!inputBeadId) {
19334
+ this.opts.beadsClient?.closeBead(result.beadId, "COMPLETE", result.durationMs, result.model);
19335
+ }
18989
19336
  }
18990
19337
  setStatus({
18991
19338
  status: "done",
@@ -18998,7 +19345,8 @@ class Supervisor {
18998
19345
  appendTimelineEvent(createRunCompleteEvent("COMPLETE", elapsed, {
18999
19346
  model: result.model,
19000
19347
  backend: result.backend,
19001
- bead_id: result.beadId
19348
+ bead_id: result.beadId,
19349
+ output: result.output
19002
19350
  }));
19003
19351
  mkdirSync(this.readyDir(), { recursive: true });
19004
19352
  writeFileSync(join3(this.readyDir(), id), "", "utf-8");
@@ -19016,6 +19364,8 @@ class Supervisor {
19016
19364
  }));
19017
19365
  throw err;
19018
19366
  } finally {
19367
+ if (stuckIntervalId !== undefined)
19368
+ clearInterval(stuckIntervalId);
19019
19369
  process.removeListener("SIGTERM", sigtermHandler);
19020
19370
  try {
19021
19371
  fifoReadline?.close();
@@ -19034,52 +19384,184 @@ class Supervisor {
19034
19384
  }
19035
19385
  }
19036
19386
  }
19037
- var JOB_TTL_DAYS;
19387
+ var JOB_TTL_DAYS, STALL_DETECTION_DEFAULTS;
19038
19388
  var init_supervisor = __esm(() => {
19039
19389
  init_timeline_events();
19040
19390
  JOB_TTL_DAYS = Number(process.env.SPECIALISTS_JOB_TTL_DAYS ?? 7);
19391
+ STALL_DETECTION_DEFAULTS = {
19392
+ running_silence_warn_ms: 60000,
19393
+ running_silence_error_ms: 300000,
19394
+ waiting_stale_ms: 3600000,
19395
+ tool_duration_warn_ms: 120000
19396
+ };
19041
19397
  });
19042
19398
 
19043
- // src/cli/install.ts
19044
- var exports_install = {};
19045
- __export(exports_install, {
19046
- run: () => run
19047
- });
19048
- async function run() {
19049
- console.log("");
19050
- console.log(yellow("⚠ DEPRECATED: `specialists install` is deprecated"));
19051
- console.log("");
19052
- console.log(` Use ${bold("specialists init")} instead.`);
19053
- console.log("");
19054
- console.log(" The init command:");
19055
- console.log(" creates specialists/ and .specialists/ directories");
19056
- console.log(" registers the MCP server in .mcp.json");
19057
- console.log(" • injects workflow context into AGENTS.md/CLAUDE.md");
19058
- console.log("");
19059
- console.log(` ${dim("Run: specialists init --help for full details")}`);
19060
- console.log("");
19399
+ // src/specialist/timeline-query.ts
19400
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
19401
+ import { join as join8 } from "node:path";
19402
+ function readJobEvents(jobDir) {
19403
+ const eventsPath = join8(jobDir, "events.jsonl");
19404
+ if (!existsSync5(eventsPath))
19405
+ return [];
19406
+ const content = readFileSync3(eventsPath, "utf-8");
19407
+ const lines = content.split(`
19408
+ `).filter(Boolean);
19409
+ const events = [];
19410
+ for (const line of lines) {
19411
+ const event = parseTimelineEvent(line);
19412
+ if (event)
19413
+ events.push(event);
19414
+ }
19415
+ events.sort(compareTimelineEvents);
19416
+ return events;
19061
19417
  }
19062
- var bold = (s) => `\x1B[1m${s}\x1B[0m`, yellow = (s) => `\x1B[33m${s}\x1B[0m`, dim = (s) => `\x1B[2m${s}\x1B[0m`;
19063
-
19064
- // src/cli/version.ts
19065
- var exports_version = {};
19066
- __export(exports_version, {
19067
- run: () => run2
19068
- });
19069
- import { createRequire as createRequire2 } from "node:module";
19070
- import { fileURLToPath } from "node:url";
19071
- import { dirname as dirname2, join as join8 } from "node:path";
19072
- import { existsSync as existsSync6 } from "node:fs";
19073
- async function run2() {
19074
- const req = createRequire2(import.meta.url);
19075
- const here = dirname2(fileURLToPath(import.meta.url));
19076
- const bundlePkgPath = join8(here, "..", "package.json");
19077
- const sourcePkgPath = join8(here, "..", "..", "package.json");
19078
- let pkg;
19079
- if (existsSync6(bundlePkgPath)) {
19080
- pkg = req("../package.json");
19081
- } else if (existsSync6(sourcePkgPath)) {
19082
- pkg = req("../../package.json");
19418
+ function readJobEventsById(jobsDir, jobId) {
19419
+ return readJobEvents(join8(jobsDir, jobId));
19420
+ }
19421
+ function readAllJobEvents(jobsDir) {
19422
+ if (!existsSync5(jobsDir))
19423
+ return [];
19424
+ const batches = [];
19425
+ const entries = readdirSync2(jobsDir);
19426
+ for (const entry of entries) {
19427
+ const jobDir = join8(jobsDir, entry);
19428
+ try {
19429
+ const stat2 = __require("node:fs").statSync(jobDir);
19430
+ if (!stat2.isDirectory())
19431
+ continue;
19432
+ } catch {
19433
+ continue;
19434
+ }
19435
+ const jobId = entry;
19436
+ const statusPath = join8(jobDir, "status.json");
19437
+ let specialist = "unknown";
19438
+ let beadId;
19439
+ if (existsSync5(statusPath)) {
19440
+ try {
19441
+ const status = JSON.parse(readFileSync3(statusPath, "utf-8"));
19442
+ specialist = status.specialist ?? "unknown";
19443
+ beadId = status.bead_id;
19444
+ } catch {}
19445
+ }
19446
+ const events = readJobEvents(jobDir);
19447
+ if (events.length > 0) {
19448
+ batches.push({ jobId, specialist, beadId, events });
19449
+ }
19450
+ }
19451
+ return batches;
19452
+ }
19453
+ function mergeTimelineEvents(batches) {
19454
+ const merged = [];
19455
+ for (const batch of batches) {
19456
+ for (const event of batch.events) {
19457
+ merged.push({
19458
+ jobId: batch.jobId,
19459
+ specialist: batch.specialist,
19460
+ beadId: batch.beadId,
19461
+ event
19462
+ });
19463
+ }
19464
+ }
19465
+ merged.sort((a, b) => compareTimelineEvents(a.event, b.event));
19466
+ return merged;
19467
+ }
19468
+ function filterTimelineEvents(merged, filter) {
19469
+ let result = merged;
19470
+ if (filter.since !== undefined) {
19471
+ result = result.filter(({ event }) => event.t >= filter.since);
19472
+ }
19473
+ if (filter.jobId !== undefined) {
19474
+ result = result.filter(({ jobId }) => jobId === filter.jobId);
19475
+ }
19476
+ if (filter.specialist !== undefined) {
19477
+ result = result.filter(({ specialist }) => specialist === filter.specialist);
19478
+ }
19479
+ if (filter.limit !== undefined && filter.limit > 0) {
19480
+ result = result.slice(0, filter.limit);
19481
+ }
19482
+ return result;
19483
+ }
19484
+ function queryTimeline(jobsDir, filter = {}) {
19485
+ let batches = readAllJobEvents(jobsDir);
19486
+ if (filter.jobId !== undefined) {
19487
+ batches = batches.filter((b) => b.jobId === filter.jobId);
19488
+ }
19489
+ if (filter.specialist !== undefined) {
19490
+ batches = batches.filter((b) => b.specialist === filter.specialist);
19491
+ }
19492
+ const merged = mergeTimelineEvents(batches);
19493
+ return filterTimelineEvents(merged, filter);
19494
+ }
19495
+ function isJobComplete(events) {
19496
+ return events.some((e) => e.type === "run_complete");
19497
+ }
19498
+ var init_timeline_query = __esm(() => {
19499
+ init_timeline_events();
19500
+ });
19501
+
19502
+ // src/specialist/model-display.ts
19503
+ function extractModelId(model) {
19504
+ if (!model)
19505
+ return;
19506
+ const trimmed = model.trim();
19507
+ if (!trimmed)
19508
+ return;
19509
+ return trimmed.includes("/") ? trimmed.split("/").pop() : trimmed;
19510
+ }
19511
+ function toModelAlias(model) {
19512
+ const modelId = extractModelId(model);
19513
+ if (!modelId)
19514
+ return;
19515
+ if (modelId.startsWith("claude-")) {
19516
+ return modelId.slice("claude-".length);
19517
+ }
19518
+ return modelId;
19519
+ }
19520
+ function formatSpecialistModel(specialist, model) {
19521
+ const alias = toModelAlias(model);
19522
+ return alias ? `${specialist}/${alias}` : specialist;
19523
+ }
19524
+
19525
+ // src/cli/install.ts
19526
+ var exports_install = {};
19527
+ __export(exports_install, {
19528
+ run: () => run
19529
+ });
19530
+ async function run() {
19531
+ console.log("");
19532
+ console.log(yellow("⚠ DEPRECATED: `specialists install` is deprecated"));
19533
+ console.log("");
19534
+ console.log(` Use ${bold("specialists init")} instead.`);
19535
+ console.log("");
19536
+ console.log(" The init command:");
19537
+ console.log(" • creates specialists/ and .specialists/ directories");
19538
+ console.log(" • registers the MCP server in .mcp.json");
19539
+ console.log(" • injects workflow context into AGENTS.md/CLAUDE.md");
19540
+ console.log("");
19541
+ console.log(` ${dim("Run: specialists init --help for full details")}`);
19542
+ console.log("");
19543
+ }
19544
+ var bold = (s) => `\x1B[1m${s}\x1B[0m`, yellow = (s) => `\x1B[33m${s}\x1B[0m`, dim = (s) => `\x1B[2m${s}\x1B[0m`;
19545
+
19546
+ // src/cli/version.ts
19547
+ var exports_version = {};
19548
+ __export(exports_version, {
19549
+ run: () => run2
19550
+ });
19551
+ import { createRequire as createRequire2 } from "node:module";
19552
+ import { fileURLToPath } from "node:url";
19553
+ import { dirname as dirname2, join as join12 } from "node:path";
19554
+ import { existsSync as existsSync8 } from "node:fs";
19555
+ async function run2() {
19556
+ const req = createRequire2(import.meta.url);
19557
+ const here = dirname2(fileURLToPath(import.meta.url));
19558
+ const bundlePkgPath = join12(here, "..", "package.json");
19559
+ const sourcePkgPath = join12(here, "..", "..", "package.json");
19560
+ let pkg;
19561
+ if (existsSync8(bundlePkgPath)) {
19562
+ pkg = req("../package.json");
19563
+ } else if (existsSync8(sourcePkgPath)) {
19564
+ pkg = req("../../package.json");
19083
19565
  } else {
19084
19566
  console.error("Cannot find package.json");
19085
19567
  process.exit(1);
@@ -19281,8 +19763,8 @@ var exports_init = {};
19281
19763
  __export(exports_init, {
19282
19764
  run: () => run5
19283
19765
  });
19284
- import { copyFileSync, cpSync, existsSync as existsSync7, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
19285
- import { join as join9 } from "node:path";
19766
+ import { copyFileSync, cpSync, existsSync as existsSync9, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
19767
+ import { join as join13 } from "node:path";
19286
19768
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19287
19769
  function ok(msg) {
19288
19770
  console.log(` ${green3("✓")} ${msg}`);
@@ -19291,10 +19773,10 @@ function skip(msg) {
19291
19773
  console.log(` ${yellow4("○")} ${msg}`);
19292
19774
  }
19293
19775
  function loadJson(path, fallback) {
19294
- if (!existsSync7(path))
19776
+ if (!existsSync9(path))
19295
19777
  return structuredClone(fallback);
19296
19778
  try {
19297
- return JSON.parse(readFileSync3(path, "utf-8"));
19779
+ return JSON.parse(readFileSync5(path, "utf-8"));
19298
19780
  } catch {
19299
19781
  return structuredClone(fallback);
19300
19782
  }
@@ -19306,34 +19788,64 @@ function saveJson(path, value) {
19306
19788
  function resolvePackagePath(relativePath) {
19307
19789
  const configPath = `config/${relativePath}`;
19308
19790
  let resolved = fileURLToPath2(new URL(`../${configPath}`, import.meta.url));
19309
- if (existsSync7(resolved))
19791
+ if (existsSync9(resolved))
19310
19792
  return resolved;
19311
19793
  resolved = fileURLToPath2(new URL(`../../${configPath}`, import.meta.url));
19312
- if (existsSync7(resolved))
19794
+ if (existsSync9(resolved))
19313
19795
  return resolved;
19314
19796
  return null;
19315
19797
  }
19798
+ function migrateLegacySpecialists(cwd, scope) {
19799
+ const sourceDir = join13(cwd, ".specialists", scope, "specialists");
19800
+ if (!existsSync9(sourceDir))
19801
+ return;
19802
+ const targetDir = join13(cwd, ".specialists", scope);
19803
+ if (!existsSync9(targetDir)) {
19804
+ mkdirSync2(targetDir, { recursive: true });
19805
+ }
19806
+ const files = readdirSync3(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
19807
+ if (files.length === 0)
19808
+ return;
19809
+ let moved = 0;
19810
+ let skipped = 0;
19811
+ for (const file of files) {
19812
+ const src = join13(sourceDir, file);
19813
+ const dest = join13(targetDir, file);
19814
+ if (existsSync9(dest)) {
19815
+ skipped++;
19816
+ continue;
19817
+ }
19818
+ renameSync2(src, dest);
19819
+ moved++;
19820
+ }
19821
+ if (moved > 0) {
19822
+ ok(`migrated ${moved} specialist${moved === 1 ? "" : "s"} from .specialists/${scope}/specialists/ to .specialists/${scope}/`);
19823
+ }
19824
+ if (skipped > 0) {
19825
+ skip(`${skipped} legacy specialist${skipped === 1 ? "" : "s"} already exist in .specialists/${scope}/ (not moved)`);
19826
+ }
19827
+ }
19316
19828
  function copyCanonicalSpecialists(cwd) {
19317
19829
  const sourceDir = resolvePackagePath("specialists");
19318
19830
  if (!sourceDir) {
19319
19831
  skip("no canonical specialists found in package");
19320
19832
  return;
19321
19833
  }
19322
- const targetDir = join9(cwd, ".specialists", "default", "specialists");
19323
- const files = readdirSync2(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
19834
+ const targetDir = join13(cwd, ".specialists", "default");
19835
+ const files = readdirSync3(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
19324
19836
  if (files.length === 0) {
19325
19837
  skip("no specialist files found in package");
19326
19838
  return;
19327
19839
  }
19328
- if (!existsSync7(targetDir)) {
19840
+ if (!existsSync9(targetDir)) {
19329
19841
  mkdirSync2(targetDir, { recursive: true });
19330
19842
  }
19331
19843
  let copied = 0;
19332
19844
  let skipped = 0;
19333
19845
  for (const file of files) {
19334
- const src = join9(sourceDir, file);
19335
- const dest = join9(targetDir, file);
19336
- if (existsSync7(dest)) {
19846
+ const src = join13(sourceDir, file);
19847
+ const dest = join13(targetDir, file);
19848
+ if (existsSync9(dest)) {
19337
19849
  skipped++;
19338
19850
  } else {
19339
19851
  copyFileSync(src, dest);
@@ -19341,7 +19853,7 @@ function copyCanonicalSpecialists(cwd) {
19341
19853
  }
19342
19854
  }
19343
19855
  if (copied > 0) {
19344
- ok(`copied ${copied} canonical specialist${copied === 1 ? "" : "s"} to .specialists/default/specialists/`);
19856
+ ok(`copied ${copied} canonical specialist${copied === 1 ? "" : "s"} to .specialists/default/`);
19345
19857
  }
19346
19858
  if (skipped > 0) {
19347
19859
  skip(`${skipped} specialist${skipped === 1 ? "" : "s"} already exist (not overwritten)`);
@@ -19353,21 +19865,21 @@ function installProjectHooks(cwd) {
19353
19865
  skip("no canonical hooks found in package");
19354
19866
  return;
19355
19867
  }
19356
- const targetDir = join9(cwd, ".claude", "hooks");
19357
- const hooks = readdirSync2(sourceDir).filter((f) => f.endsWith(".mjs"));
19868
+ const targetDir = join13(cwd, ".claude", "hooks");
19869
+ const hooks = readdirSync3(sourceDir).filter((f) => f.endsWith(".mjs"));
19358
19870
  if (hooks.length === 0) {
19359
19871
  skip("no hook files found in package");
19360
19872
  return;
19361
19873
  }
19362
- if (!existsSync7(targetDir)) {
19874
+ if (!existsSync9(targetDir)) {
19363
19875
  mkdirSync2(targetDir, { recursive: true });
19364
19876
  }
19365
19877
  let copied = 0;
19366
19878
  let skipped = 0;
19367
19879
  for (const file of hooks) {
19368
- const src = join9(sourceDir, file);
19369
- const dest = join9(targetDir, file);
19370
- if (existsSync7(dest)) {
19880
+ const src = join13(sourceDir, file);
19881
+ const dest = join13(targetDir, file);
19882
+ if (existsSync9(dest)) {
19371
19883
  skipped++;
19372
19884
  } else {
19373
19885
  copyFileSync(src, dest);
@@ -19382,9 +19894,9 @@ function installProjectHooks(cwd) {
19382
19894
  }
19383
19895
  }
19384
19896
  function ensureProjectHookWiring(cwd) {
19385
- const settingsPath = join9(cwd, ".claude", "settings.json");
19386
- const settingsDir = join9(cwd, ".claude");
19387
- if (!existsSync7(settingsDir)) {
19897
+ const settingsPath = join13(cwd, ".claude", "settings.json");
19898
+ const settingsDir = join13(cwd, ".claude");
19899
+ if (!existsSync9(settingsDir)) {
19388
19900
  mkdirSync2(settingsDir, { recursive: true });
19389
19901
  }
19390
19902
  const settings = loadJson(settingsPath, {});
@@ -19413,25 +19925,25 @@ function installProjectSkills(cwd) {
19413
19925
  skip("no canonical skills found in package");
19414
19926
  return;
19415
19927
  }
19416
- const skills = readdirSync2(sourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
19928
+ const skills = readdirSync3(sourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
19417
19929
  if (skills.length === 0) {
19418
19930
  skip("no skill directories found in package");
19419
19931
  return;
19420
19932
  }
19421
19933
  const targetDirs = [
19422
- join9(cwd, ".claude", "skills"),
19423
- join9(cwd, ".pi", "skills")
19934
+ join13(cwd, ".claude", "skills"),
19935
+ join13(cwd, ".pi", "skills")
19424
19936
  ];
19425
19937
  let totalCopied = 0;
19426
19938
  let totalSkipped = 0;
19427
19939
  for (const targetDir of targetDirs) {
19428
- if (!existsSync7(targetDir)) {
19940
+ if (!existsSync9(targetDir)) {
19429
19941
  mkdirSync2(targetDir, { recursive: true });
19430
19942
  }
19431
19943
  for (const skill of skills) {
19432
- const src = join9(sourceDir, skill);
19433
- const dest = join9(targetDir, skill);
19434
- if (existsSync7(dest)) {
19944
+ const src = join13(sourceDir, skill);
19945
+ const dest = join13(targetDir, skill);
19946
+ if (existsSync9(dest)) {
19435
19947
  totalSkipped++;
19436
19948
  } else {
19437
19949
  cpSync(src, dest, { recursive: true });
@@ -19447,20 +19959,20 @@ function installProjectSkills(cwd) {
19447
19959
  }
19448
19960
  }
19449
19961
  function createUserDirs(cwd) {
19450
- const userDir = join9(cwd, ".specialists", "user", "specialists");
19451
- if (!existsSync7(userDir)) {
19962
+ const userDir = join13(cwd, ".specialists", "user");
19963
+ if (!existsSync9(userDir)) {
19452
19964
  mkdirSync2(userDir, { recursive: true });
19453
- ok("created .specialists/user/specialists/ for custom specialists");
19965
+ ok("created .specialists/user/ for custom specialists");
19454
19966
  }
19455
19967
  }
19456
19968
  function createRuntimeDirs(cwd) {
19457
19969
  const runtimeDirs = [
19458
- join9(cwd, ".specialists", "jobs"),
19459
- join9(cwd, ".specialists", "ready")
19970
+ join13(cwd, ".specialists", "jobs"),
19971
+ join13(cwd, ".specialists", "ready")
19460
19972
  ];
19461
19973
  let created = 0;
19462
19974
  for (const dir of runtimeDirs) {
19463
- if (!existsSync7(dir)) {
19975
+ if (!existsSync9(dir)) {
19464
19976
  mkdirSync2(dir, { recursive: true });
19465
19977
  created++;
19466
19978
  }
@@ -19470,7 +19982,7 @@ function createRuntimeDirs(cwd) {
19470
19982
  }
19471
19983
  }
19472
19984
  function ensureProjectMcp(cwd) {
19473
- const mcpPath = join9(cwd, MCP_FILE);
19985
+ const mcpPath = join13(cwd, MCP_FILE);
19474
19986
  const mcp = loadJson(mcpPath, { mcpServers: {} });
19475
19987
  mcp.mcpServers ??= {};
19476
19988
  const existing = mcp.mcpServers[MCP_SERVER_NAME];
@@ -19483,8 +19995,8 @@ function ensureProjectMcp(cwd) {
19483
19995
  ok("registered specialists in project .mcp.json");
19484
19996
  }
19485
19997
  function ensureGitignore(cwd) {
19486
- const gitignorePath = join9(cwd, ".gitignore");
19487
- const existing = existsSync7(gitignorePath) ? readFileSync3(gitignorePath, "utf-8") : "";
19998
+ const gitignorePath = join13(cwd, ".gitignore");
19999
+ const existing = existsSync9(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : "";
19488
20000
  let added = 0;
19489
20001
  const lines = existing.split(`
19490
20002
  `);
@@ -19504,9 +20016,9 @@ function ensureGitignore(cwd) {
19504
20016
  }
19505
20017
  }
19506
20018
  function ensureAgentsMd(cwd) {
19507
- const agentsPath = join9(cwd, "AGENTS.md");
19508
- if (existsSync7(agentsPath)) {
19509
- const existing = readFileSync3(agentsPath, "utf-8");
20019
+ const agentsPath = join13(cwd, "AGENTS.md");
20020
+ if (existsSync9(agentsPath)) {
20021
+ const existing = readFileSync5(agentsPath, "utf-8");
19510
20022
  if (existing.includes(AGENTS_MARKER)) {
19511
20023
  skip("AGENTS.md already has Specialists section");
19512
20024
  } else {
@@ -19525,6 +20037,8 @@ async function run5() {
19525
20037
  console.log(`
19526
20038
  ${bold4("specialists init")}
19527
20039
  `);
20040
+ migrateLegacySpecialists(cwd, "default");
20041
+ migrateLegacySpecialists(cwd, "user");
19528
20042
  copyCanonicalSpecialists(cwd);
19529
20043
  createUserDirs(cwd);
19530
20044
  createRuntimeDirs(cwd);
@@ -19546,15 +20060,13 @@ ${bold4("Done!")}
19546
20060
  console.log(` ${dim4(".specialists/ structure:")}`);
19547
20061
  console.log(` .specialists/`);
19548
20062
  console.log(` ├── default/ ${dim4("# canonical specialists (from init)")}`);
19549
- console.log(` │ └── specialists/`);
19550
20063
  console.log(` ├── user/ ${dim4("# your custom specialists")}`);
19551
- console.log(` │ └── specialists/`);
19552
20064
  console.log(` ├── jobs/ ${dim4("# runtime (gitignored)")}`);
19553
20065
  console.log(` └── ready/ ${dim4("# runtime (gitignored)")}`);
19554
20066
  console.log(`
19555
20067
  ${dim4("Next steps:")}`);
19556
20068
  console.log(` 1. Run ${yellow4("specialists list")} to see available specialists`);
19557
- console.log(` 2. Add custom specialists to ${yellow4(".specialists/user/specialists/")}`);
20069
+ console.log(` 2. Add custom specialists to ${yellow4(".specialists/user/")}`);
19558
20070
  console.log(` 3. Restart Claude Code or pi to pick up changes
19559
20071
  `);
19560
20072
  }
@@ -19563,12 +20075,29 @@ var init_init = __esm(() => {
19563
20075
  AGENTS_BLOCK = `
19564
20076
  ## Specialists
19565
20077
 
19566
- Call \`specialist_init\` at the start of every session to bootstrap context and
19567
- see available specialists. Use \`use_specialist\` or \`start_specialist\` to
19568
- delegate heavy tasks (code review, bug hunting, deep reasoning) to the right
19569
- specialist without user intervention.
19570
-
19571
- Add custom specialists to \`.specialists/user/specialists/\` to extend the defaults.
20078
+ Call \`specialists init\` once per project, then use CLI commands via Bash.
20079
+
20080
+ Core specialist commands (CLI-first in pi):
20081
+ - \`specialists list\`
20082
+ - \`specialists run <name> --bead <id>\`
20083
+ - \`specialists run <name> --prompt "..."\`
20084
+ - \`specialists feed -f\` / \`specialists feed <job-id>\`
20085
+ - \`specialists result <job-id>\`
20086
+ - \`specialists resume <job-id> "next task"\` (for keep-alive jobs in waiting)
20087
+ - \`specialists stop <job-id>\`
20088
+
20089
+ For background specialists in pi, prefer the process extension:
20090
+ - \`process start\`, \`process list\`, \`process output\`, \`process logs\`, \`process kill\`, \`process clear\`
20091
+ - TUI: \`/ps\`, \`/ps:pin\`, \`/ps:logs\`, \`/ps:kill\`, \`/ps:clear\`, \`/ps:dock\`, \`/ps:settings\`
20092
+
20093
+ Canonical tracked flow:
20094
+ 1. Create/claim bead issue
20095
+ 2. Run specialist with \`--bead <id>\` (for long work, launch via \`process start\`)
20096
+ 3. Observe progress (\`process output\` / \`process logs\` or \`specialists feed\`)
20097
+ 4. Read final output (\`specialists result <job-id>\`)
20098
+ 5. Close/update bead with outcome
20099
+
20100
+ Add custom specialists to \`.specialists/user/\` to extend defaults.
19572
20101
  `.trimStart();
19573
20102
  GITIGNORE_ENTRIES = [".specialists/jobs/", ".specialists/ready/"];
19574
20103
  MCP_SERVER_CONFIG = { command: "specialists", args: [] };
@@ -19582,8 +20111,8 @@ __export(exports_validate, {
19582
20111
  ArgParseError: () => ArgParseError2
19583
20112
  });
19584
20113
  import { readFile as readFile2 } from "node:fs/promises";
19585
- import { existsSync as existsSync8 } from "node:fs";
19586
- import { join as join10 } from "node:path";
20114
+ import { existsSync as existsSync10 } from "node:fs";
20115
+ import { join as join14 } from "node:path";
19587
20116
  function parseArgs3(argv) {
19588
20117
  const name = argv[0];
19589
20118
  if (!name || name.startsWith("--")) {
@@ -19594,13 +20123,15 @@ function parseArgs3(argv) {
19594
20123
  }
19595
20124
  function findSpecialistFile(name) {
19596
20125
  const scanDirs = [
19597
- join10(process.cwd(), ".specialists", "user", "specialists"),
19598
- join10(process.cwd(), ".specialists", "default", "specialists"),
19599
- join10(process.cwd(), "specialists")
20126
+ join14(process.cwd(), ".specialists", "user"),
20127
+ join14(process.cwd(), ".specialists", "user", "specialists"),
20128
+ join14(process.cwd(), ".specialists", "default"),
20129
+ join14(process.cwd(), ".specialists", "default", "specialists"),
20130
+ join14(process.cwd(), "specialists")
19600
20131
  ];
19601
20132
  for (const dir of scanDirs) {
19602
- const candidate = join10(dir, `${name}.specialist.yaml`);
19603
- if (existsSync8(candidate)) {
20133
+ const candidate = join14(dir, `${name}.specialist.yaml`);
20134
+ if (existsSync10(candidate)) {
19604
20135
  return candidate;
19605
20136
  }
19606
20137
  }
@@ -19692,7 +20223,9 @@ var exports_edit = {};
19692
20223
  __export(exports_edit, {
19693
20224
  run: () => run7
19694
20225
  });
19695
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
20226
+ import { spawnSync as spawnSync6 } from "node:child_process";
20227
+ import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
20228
+ import { join as join15 } from "node:path";
19696
20229
  function parseArgs4(argv) {
19697
20230
  const name = argv[0];
19698
20231
  if (!name || name.startsWith("--")) {
@@ -19755,8 +20288,30 @@ function setIn(doc2, path, value) {
19755
20288
  node.set(leaf, value);
19756
20289
  }
19757
20290
  }
20291
+ function openAllConfigSpecialistsInEditor() {
20292
+ const configDir = join15(process.cwd(), "config", "specialists");
20293
+ if (!existsSync11(configDir)) {
20294
+ console.error(`Error: missing directory: ${configDir}`);
20295
+ process.exit(1);
20296
+ }
20297
+ const files = readdirSync4(configDir).filter((file) => file.endsWith(".specialist.yaml")).sort().map((file) => join15(configDir, file));
20298
+ if (files.length === 0) {
20299
+ console.error("Error: no specialist YAML files found in config/specialists/");
20300
+ process.exit(1);
20301
+ }
20302
+ const editor = process.env.VISUAL ?? process.env.EDITOR ?? "vi";
20303
+ const result = spawnSync6(editor, files, { stdio: "inherit", shell: true });
20304
+ if (result.status !== 0) {
20305
+ process.exit(result.status ?? 1);
20306
+ }
20307
+ }
19758
20308
  async function run7() {
19759
- const args = parseArgs4(process.argv.slice(3));
20309
+ const rawArgs = process.argv.slice(3);
20310
+ if (rawArgs.length === 1 && rawArgs[0] === "--all") {
20311
+ openAllConfigSpecialistsInEditor();
20312
+ return;
20313
+ }
20314
+ const args = parseArgs4(rawArgs);
19760
20315
  const { name, field, value, dryRun, scope } = args;
19761
20316
  const loader = new SpecialistLoader;
19762
20317
  const all = await loader.list();
@@ -19767,7 +20322,7 @@ async function run7() {
19767
20322
  console.error(` Run ${yellow6("specialists list")} to see available specialists`);
19768
20323
  process.exit(1);
19769
20324
  }
19770
- const raw = readFileSync4(match.filePath, "utf-8");
20325
+ const raw = readFileSync6(match.filePath, "utf-8");
19771
20326
  const doc2 = $parseDocument(raw);
19772
20327
  const yamlPath = FIELD_MAP[field];
19773
20328
  let typedValue = value;
@@ -19817,168 +20372,178 @@ var init_edit = __esm(() => {
19817
20372
  VALID_PERMISSIONS = ["READ_ONLY", "LOW", "MEDIUM", "HIGH"];
19818
20373
  });
19819
20374
 
19820
- // src/cli/run.ts
19821
- var exports_run = {};
19822
- __export(exports_run, {
20375
+ // src/cli/config.ts
20376
+ var exports_config = {};
20377
+ __export(exports_config, {
19823
20378
  run: () => run8
19824
20379
  });
19825
- import { join as join11 } from "node:path";
19826
- async function parseArgs5(argv) {
19827
- const name = argv[0];
19828
- if (!name || name.startsWith("--")) {
19829
- console.error('Usage: specialists|sp run <name> [--prompt "..."] [--bead <id>] [--context-depth <n>] [--model <model>] [--no-beads] [--keep-alive]');
19830
- process.exit(1);
20380
+ import { existsSync as existsSync12 } from "node:fs";
20381
+ import { readdir as readdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
20382
+ import { basename as basename2, join as join16 } from "node:path";
20383
+ function usage() {
20384
+ return [
20385
+ "Usage:",
20386
+ " specialists config get <key> [--all] [--name <specialist>]",
20387
+ " specialists config set <key> <value> [--all] [--name <specialist>]",
20388
+ "",
20389
+ "Examples:",
20390
+ " specialists config get specialist.execution.stall_timeout_ms",
20391
+ " specialists config set specialist.execution.stall_timeout_ms 180000",
20392
+ " specialists config set specialist.execution.stall_timeout_ms 120000 --name executor"
20393
+ ].join(`
20394
+ `);
20395
+ }
20396
+ function parseArgs5(argv) {
20397
+ const command = argv[0];
20398
+ if (command !== "get" && command !== "set") {
20399
+ throw new ArgParseError3(usage());
19831
20400
  }
19832
- let prompt = "";
19833
- let beadId;
19834
- let model;
19835
- let noBeads = false;
19836
- let keepAlive = false;
19837
- let contextDepth = 1;
19838
- for (let i = 1;i < argv.length; i++) {
19839
- const token = argv[i];
19840
- if (token === "--prompt" && argv[i + 1]) {
19841
- prompt = argv[++i];
19842
- continue;
19843
- }
19844
- if (token === "--bead" && argv[i + 1]) {
19845
- beadId = argv[++i];
19846
- continue;
19847
- }
19848
- if (token === "--model" && argv[i + 1]) {
19849
- model = argv[++i];
19850
- continue;
19851
- }
19852
- if (token === "--context-depth" && argv[i + 1]) {
19853
- contextDepth = parseInt(argv[++i], 10) || 0;
19854
- continue;
20401
+ const key = argv[1];
20402
+ if (!key || key.startsWith("--")) {
20403
+ throw new ArgParseError3(`Missing key
20404
+
20405
+ ${usage()}`);
20406
+ }
20407
+ let value;
20408
+ let index = 2;
20409
+ if (command === "set") {
20410
+ value = argv[2];
20411
+ if (value === undefined || value.startsWith("--")) {
20412
+ throw new ArgParseError3(`Missing value for set
20413
+
20414
+ ${usage()}`);
19855
20415
  }
19856
- if (token === "--no-beads") {
19857
- noBeads = true;
20416
+ index = 3;
20417
+ }
20418
+ let name;
20419
+ let all = false;
20420
+ for (let i = index;i < argv.length; i++) {
20421
+ const token = argv[i];
20422
+ if (token === "--all") {
20423
+ all = true;
19858
20424
  continue;
19859
20425
  }
19860
- if (token === "--keep-alive") {
19861
- keepAlive = true;
20426
+ if (token === "--name") {
20427
+ const next = argv[++i];
20428
+ if (!next || next.startsWith("--")) {
20429
+ throw new ArgParseError3("--name requires a specialist name");
20430
+ }
20431
+ name = next;
19862
20432
  continue;
19863
20433
  }
20434
+ throw new ArgParseError3(`Unknown option: ${token}`);
19864
20435
  }
19865
- if (prompt && beadId) {
19866
- console.error("Error: use either --prompt or --bead, not both.");
19867
- process.exit(1);
20436
+ if (name && all) {
20437
+ throw new ArgParseError3("Use either --name or --all, not both");
19868
20438
  }
19869
- if (!prompt && !beadId && !process.stdin.isTTY) {
19870
- prompt = await new Promise((resolve2) => {
19871
- let buf = "";
19872
- process.stdin.setEncoding("utf-8");
19873
- process.stdin.on("data", (chunk) => {
19874
- buf += chunk;
19875
- });
19876
- process.stdin.on("end", () => resolve2(buf.trim()));
19877
- });
20439
+ if (!name) {
20440
+ all = true;
19878
20441
  }
19879
- if (!prompt && !beadId) {
19880
- console.error("Error: provide --prompt, pipe stdin, or use --bead <id>.");
19881
- process.exit(1);
20442
+ return { command, key, value, name, all };
20443
+ }
20444
+ function splitKeyPath(key) {
20445
+ const path = key.split(".").map((part) => part.trim()).filter(Boolean);
20446
+ if (path.length === 0) {
20447
+ throw new ArgParseError3(`Invalid key: ${key}`);
20448
+ }
20449
+ return path;
20450
+ }
20451
+ function getSpecialistDir(projectDir) {
20452
+ return join16(projectDir, "config", "specialists");
20453
+ }
20454
+ function getSpecialistNameFromPath(path) {
20455
+ return path.replace(/\.specialist\.yaml$/, "");
20456
+ }
20457
+ async function listSpecialistFiles(projectDir) {
20458
+ const specialistDir = getSpecialistDir(projectDir);
20459
+ if (!existsSync12(specialistDir)) {
20460
+ throw new Error(`Missing directory: ${specialistDir}`);
20461
+ }
20462
+ const entries = await readdir2(specialistDir);
20463
+ return entries.filter((entry) => entry.endsWith(".specialist.yaml")).sort((a, b) => a.localeCompare(b)).map((entry) => join16(specialistDir, entry));
20464
+ }
20465
+ async function findNamedSpecialistFile(projectDir, name) {
20466
+ const path = join16(getSpecialistDir(projectDir), `${name}.specialist.yaml`);
20467
+ if (!existsSync12(path)) {
20468
+ throw new Error(`Specialist not found in config/specialists/: ${name}`);
20469
+ }
20470
+ return path;
20471
+ }
20472
+ function parseValue(rawValue) {
20473
+ try {
20474
+ return $parse(rawValue);
20475
+ } catch {
20476
+ return rawValue;
20477
+ }
20478
+ }
20479
+ function formatValue(value) {
20480
+ if (value === undefined)
20481
+ return "<unset>";
20482
+ if (typeof value === "string")
20483
+ return value;
20484
+ return JSON.stringify(value);
20485
+ }
20486
+ async function getAcrossFiles(files, keyPath) {
20487
+ for (const file of files) {
20488
+ const content = await readFile3(file, "utf-8");
20489
+ const doc2 = $parseDocument(content);
20490
+ const value = doc2.getIn(keyPath);
20491
+ const name = getSpecialistNameFromPath(basename2(file));
20492
+ console.log(`${yellow7(name)}: ${formatValue(value)}`);
20493
+ }
20494
+ }
20495
+ async function setAcrossFiles(files, keyPath, rawValue) {
20496
+ const typedValue = parseValue(rawValue);
20497
+ for (const file of files) {
20498
+ const content = await readFile3(file, "utf-8");
20499
+ const doc2 = $parseDocument(content);
20500
+ doc2.setIn(keyPath, typedValue);
20501
+ await writeFile2(file, doc2.toString(), "utf-8");
19882
20502
  }
19883
- return { name, prompt, beadId, model, noBeads, keepAlive, contextDepth };
20503
+ console.log(`${green6("✓")} updated ${files.length} specialist${files.length === 1 ? "" : "s"}: ` + `${keyPath.join(".")} = ${formatValue(typedValue)}`);
19884
20504
  }
19885
20505
  async function run8() {
19886
- const args = await parseArgs5(process.argv.slice(3));
19887
- const loader = new SpecialistLoader;
19888
- const circuitBreaker = new CircuitBreaker;
19889
- const hooks = new HookEmitter({ tracePath: join11(process.cwd(), ".specialists", "trace.jsonl") });
19890
- const beadsClient = args.noBeads ? undefined : new BeadsClient;
19891
- const beadReader = beadsClient ?? new BeadsClient;
19892
- let prompt = args.prompt;
19893
- let variables;
19894
- if (args.beadId) {
19895
- const bead = beadReader.readBead(args.beadId);
19896
- if (!bead) {
19897
- throw new Error(`Unable to read bead '${args.beadId}' via bd show --json`);
19898
- }
19899
- const blockers = args.contextDepth > 0 ? beadReader.getCompletedBlockers(args.beadId, args.contextDepth) : [];
19900
- if (blockers.length > 0) {
19901
- process.stderr.write(dim7(`
19902
- [context: ${blockers.length} completed dep${blockers.length > 1 ? "s" : ""} injected]
19903
- `));
20506
+ let args;
20507
+ try {
20508
+ args = parseArgs5(process.argv.slice(3));
20509
+ } catch (error2) {
20510
+ if (error2 instanceof ArgParseError3) {
20511
+ console.error(error2.message);
20512
+ process.exit(1);
19904
20513
  }
19905
- const beadContext = buildBeadContext(bead, blockers);
19906
- prompt = beadContext;
19907
- variables = {
19908
- bead_context: beadContext,
19909
- bead_id: args.beadId
19910
- };
20514
+ throw error2;
19911
20515
  }
19912
- const runner = new SpecialistRunner({
19913
- loader,
19914
- hooks,
19915
- circuitBreaker,
19916
- beadsClient
19917
- });
19918
- const jobsDir = join11(process.cwd(), ".specialists", "jobs");
19919
- const supervisor = new Supervisor({
19920
- runner,
19921
- runOptions: {
19922
- name: args.name,
19923
- prompt,
19924
- variables,
19925
- backendOverride: args.model,
19926
- inputBeadId: args.beadId,
19927
- keepAlive: args.keepAlive
19928
- },
19929
- jobsDir,
19930
- beadsClient,
19931
- onProgress: (delta) => process.stdout.write(delta),
19932
- onMeta: (meta) => process.stderr.write(dim7(`
19933
- [${meta.backend} / ${meta.model}]
19934
-
19935
- `)),
19936
- onJobStarted: ({ id }) => process.stderr.write(dim7(`[job started: ${id}]
19937
- `))
19938
- });
20516
+ const keyPath = splitKeyPath(args.key);
20517
+ const projectDir = process.cwd();
20518
+ let files;
19939
20519
  try {
19940
- await loader.get(args.name);
19941
- } catch (err) {
19942
- process.stderr.write(`Error: ${err?.message ?? err}
19943
- `);
20520
+ files = args.name ? [await findNamedSpecialistFile(projectDir, args.name)] : await listSpecialistFiles(projectDir);
20521
+ } catch (error2) {
20522
+ const message = error2 instanceof Error ? error2.message : String(error2);
20523
+ console.error(message);
19944
20524
  process.exit(1);
20525
+ return;
19945
20526
  }
19946
- process.stderr.write(`
19947
- ${bold7(`Running ${cyan4(args.name)}`)}
19948
-
19949
- `);
19950
- let jobId;
19951
- try {
19952
- jobId = await supervisor.run();
19953
- } catch (err) {
19954
- process.stderr.write(`Error: ${err?.message ?? err}
19955
- `);
20527
+ if (files.length === 0) {
20528
+ console.error("No specialists found in config/specialists/");
19956
20529
  process.exit(1);
20530
+ return;
19957
20531
  }
19958
- const status = supervisor.readStatus(jobId);
19959
- const secs = ((status?.last_event_at_ms ?? Date.now()) - (status?.started_at_ms ?? Date.now())) / 1000;
19960
- const footer = [
19961
- `job ${jobId}`,
19962
- status?.bead_id ? `bead ${status.bead_id}` : "",
19963
- `${secs.toFixed(1)}s`,
19964
- status?.model ? dim7(`${status.backend}/${status.model}`) : ""
19965
- ].filter(Boolean).join(" ");
19966
- process.stderr.write(`
19967
- ${green6("✓")} ${footer}
19968
-
19969
- `);
19970
- process.stderr.write(dim7(`Poll: specialists poll ${jobId} --json
19971
-
19972
- `));
19973
- process.exit(0);
20532
+ if (args.command === "get") {
20533
+ await getAcrossFiles(files, keyPath);
20534
+ return;
20535
+ }
20536
+ await setAcrossFiles(files, keyPath, args.value);
19974
20537
  }
19975
- var bold7 = (s) => `\x1B[1m${s}\x1B[0m`, dim7 = (s) => `\x1B[2m${s}\x1B[0m`, green6 = (s) => `\x1B[32m${s}\x1B[0m`, cyan4 = (s) => `\x1B[36m${s}\x1B[0m`;
19976
- var init_run = __esm(() => {
19977
- init_loader();
19978
- init_runner();
19979
- init_hooks();
19980
- init_beads();
19981
- init_supervisor();
20538
+ var green6 = (s) => `\x1B[32m${s}\x1B[0m`, yellow7 = (s) => `\x1B[33m${s}\x1B[0m`, ArgParseError3;
20539
+ var init_config = __esm(() => {
20540
+ init_dist();
20541
+ ArgParseError3 = class ArgParseError3 extends Error {
20542
+ constructor(message) {
20543
+ super(message);
20544
+ this.name = "ArgParseError";
20545
+ }
20546
+ };
19982
20547
  });
19983
20548
 
19984
20549
  // src/cli/format-helpers.ts
@@ -20018,29 +20583,46 @@ class JobColorMap {
20018
20583
  return this.colors.size;
20019
20584
  }
20020
20585
  }
20586
+ function formatToolArgValue(value, maxLen = 240) {
20587
+ const raw = typeof value === "string" ? value : JSON.stringify(value);
20588
+ const flat = raw.replace(/\s+/g, " ").trim();
20589
+ return flat.length > maxLen ? `${flat.slice(0, maxLen - 3)}...` : flat;
20590
+ }
20591
+ function formatToolDetail(event) {
20592
+ const toolName = cyan4(event.tool);
20593
+ if (event.phase === "start") {
20594
+ if (typeof event.args?.command === "string") {
20595
+ return `${toolName}: ${yellow8(formatToolArgValue(event.args.command))}`;
20596
+ }
20597
+ if (event.args && Object.keys(event.args).length > 0) {
20598
+ const argStr = Object.entries(event.args).map(([k, v]) => `${k}=${formatToolArgValue(v)}`).join(" ");
20599
+ return `${toolName}: ${dim7(argStr)}`;
20600
+ }
20601
+ return `${toolName}: ${dim7("start")}`;
20602
+ }
20603
+ if (event.phase === "end" && event.is_error) {
20604
+ return `${toolName}: ${red2("error")}`;
20605
+ }
20606
+ return `${toolName}: ${dim7(event.phase)}`;
20607
+ }
20021
20608
  function formatEventLine(event, options) {
20022
- const ts = dim8(formatTime(event.t));
20023
- const label = options.colorize(bold8(getEventLabel(event.type).padEnd(5)));
20024
- const prefix = `${options.colorize(`[${options.jobId}]`)} ${options.specialist}${options.beadId ? ` ${dim8(`[${options.beadId}]`)}` : ""}`;
20609
+ const ts = dim7(formatTime(event.t));
20610
+ const job = options.colorize(`[${options.jobId}]`);
20611
+ const bead = dim7(`[${options.beadId ?? "-"}]`);
20612
+ const label = options.colorize(bold7(getEventLabel(event.type).padEnd(5)));
20025
20613
  const detailParts = [];
20614
+ let detail = "";
20026
20615
  if (event.type === "meta") {
20027
20616
  detailParts.push(`model=${event.model}`);
20028
20617
  detailParts.push(`backend=${event.backend}`);
20029
20618
  } else if (event.type === "tool") {
20030
- detailParts.push(`tool=${event.tool}`);
20031
- detailParts.push(`phase=${event.phase}`);
20032
- if (event.phase === "end") {
20033
- detailParts.push(`ok=${event.is_error ? "false" : "true"}`);
20034
- }
20619
+ detail = formatToolDetail(event);
20035
20620
  } else if (event.type === "run_complete") {
20036
20621
  detailParts.push(`status=${event.status}`);
20037
20622
  detailParts.push(`elapsed=${formatElapsed(event.elapsed_s)}`);
20038
20623
  if (event.error) {
20039
20624
  detailParts.push(`error=${event.error}`);
20040
20625
  }
20041
- } else if (event.type === "done" || event.type === "agent_end") {
20042
- detailParts.push("status=COMPLETE");
20043
- detailParts.push(`elapsed=${formatElapsed(event.elapsed_s ?? 0)}`);
20044
20626
  } else if (event.type === "run_start") {
20045
20627
  detailParts.push(`specialist=${event.specialist}`);
20046
20628
  if (event.bead_id) {
@@ -20056,54 +20638,396 @@ function formatEventLine(event, options) {
20056
20638
  } else if (event.type === "turn") {
20057
20639
  detailParts.push(`phase=${event.phase}`);
20058
20640
  }
20059
- const detail = detailParts.length > 0 ? dim8(detailParts.join(" ")) : "";
20060
- return `${ts} ${prefix} ${label}${detail ? ` ${detail}` : ""}`.trimEnd();
20641
+ if (!detail && detailParts.length > 0) {
20642
+ detail = dim7(detailParts.join(" "));
20643
+ }
20644
+ return `${ts} ${job} ${bead} ${label} ${options.specialist}${detail ? ` ${detail}` : ""}`.trimEnd();
20645
+ }
20646
+ function formatEventInline(event) {
20647
+ switch (event.type) {
20648
+ case "meta":
20649
+ return dim7(`[model] ${event.backend}/${event.model}`);
20650
+ case "thinking":
20651
+ return dim7("[thinking...]");
20652
+ case "text":
20653
+ return dim7("[response]");
20654
+ case "tool": {
20655
+ if (event.phase !== "start")
20656
+ return null;
20657
+ const firstArgVal = event.args ? Object.values(event.args)[0] : undefined;
20658
+ const argStr = firstArgVal !== undefined ? ": " + (typeof firstArgVal === "string" ? firstArgVal.split(`
20659
+ `)[0].slice(0, 80) : JSON.stringify(firstArgVal).slice(0, 80)) : "";
20660
+ return `${dim7("[tool]")} ${cyan4(event.tool)}${dim7(argStr)}`;
20661
+ }
20662
+ case "stale_warning":
20663
+ return yellow8(`[warning] ${event.reason}: ${Math.round(event.silence_ms / 1000)}s silent`);
20664
+ default:
20665
+ return null;
20666
+ }
20667
+ }
20668
+ function formatEventInlineDebounced(event, activePhase) {
20669
+ if (event.type === "thinking" || event.type === "text") {
20670
+ if (activePhase === event.type) {
20671
+ return { line: null, nextPhase: activePhase };
20672
+ }
20673
+ return { line: formatEventInline(event), nextPhase: event.type };
20674
+ }
20675
+ return {
20676
+ line: formatEventInline(event),
20677
+ nextPhase: null
20678
+ };
20679
+ }
20680
+ var dim7 = (s) => `\x1B[2m${s}\x1B[0m`, bold7 = (s) => `\x1B[1m${s}\x1B[0m`, cyan4 = (s) => `\x1B[36m${s}\x1B[0m`, yellow8 = (s) => `\x1B[33m${s}\x1B[0m`, red2 = (s) => `\x1B[31m${s}\x1B[0m`, green7 = (s) => `\x1B[32m${s}\x1B[0m`, blue = (s) => `\x1B[34m${s}\x1B[0m`, magenta = (s) => `\x1B[35m${s}\x1B[0m`, JOB_COLORS, EVENT_LABELS;
20681
+ var init_format_helpers = __esm(() => {
20682
+ JOB_COLORS = [cyan4, yellow8, magenta, green7, blue, red2];
20683
+ EVENT_LABELS = {
20684
+ run_start: "START",
20685
+ meta: "META",
20686
+ thinking: "THINK",
20687
+ tool: "TOOL",
20688
+ text: "TEXT",
20689
+ message: "MSG",
20690
+ turn: "TURN",
20691
+ run_complete: "DONE",
20692
+ error: "ERR"
20693
+ };
20694
+ });
20695
+
20696
+ // src/cli/run.ts
20697
+ var exports_run = {};
20698
+ __export(exports_run, {
20699
+ run: () => run9
20700
+ });
20701
+ import { join as join17 } from "node:path";
20702
+ import { readFileSync as readFileSync7 } from "node:fs";
20703
+ import { spawn as cpSpawn } from "node:child_process";
20704
+ async function parseArgs6(argv) {
20705
+ const name = argv[0];
20706
+ if (!name || name.startsWith("--")) {
20707
+ console.error('Usage: specialists|sp run <name> [--prompt "..."] [--bead <id>] [--context-depth <n>] [--model <model>] [--no-beads] [--no-bead-notes] [--keep-alive|--no-keep-alive] [--json|--raw]');
20708
+ process.exit(1);
20709
+ }
20710
+ let prompt = "";
20711
+ let beadId;
20712
+ let model;
20713
+ let noBeads = false;
20714
+ let noBeadNotes = false;
20715
+ let keepAlive;
20716
+ let noKeepAlive = false;
20717
+ let background = false;
20718
+ let outputMode = "human";
20719
+ let contextDepth = 1;
20720
+ for (let i = 1;i < argv.length; i++) {
20721
+ const token = argv[i];
20722
+ if (token === "--prompt" && argv[i + 1]) {
20723
+ prompt = argv[++i];
20724
+ continue;
20725
+ }
20726
+ if (token === "--bead" && argv[i + 1]) {
20727
+ beadId = argv[++i];
20728
+ continue;
20729
+ }
20730
+ if (token === "--model" && argv[i + 1]) {
20731
+ model = argv[++i];
20732
+ continue;
20733
+ }
20734
+ if (token === "--context-depth" && argv[i + 1]) {
20735
+ contextDepth = parseInt(argv[++i], 10) || 0;
20736
+ continue;
20737
+ }
20738
+ if (token === "--no-beads") {
20739
+ noBeads = true;
20740
+ continue;
20741
+ }
20742
+ if (token === "--no-bead-notes") {
20743
+ noBeadNotes = true;
20744
+ continue;
20745
+ }
20746
+ if (token === "--keep-alive") {
20747
+ keepAlive = true;
20748
+ noKeepAlive = false;
20749
+ continue;
20750
+ }
20751
+ if (token === "--no-keep-alive") {
20752
+ keepAlive = undefined;
20753
+ noKeepAlive = true;
20754
+ continue;
20755
+ }
20756
+ if (token === "--background") {
20757
+ background = true;
20758
+ continue;
20759
+ }
20760
+ if (token === "--json") {
20761
+ outputMode = "json";
20762
+ continue;
20763
+ }
20764
+ if (token === "--raw") {
20765
+ outputMode = "raw";
20766
+ continue;
20767
+ }
20768
+ }
20769
+ if (prompt && beadId) {
20770
+ console.error("Error: use either --prompt or --bead, not both.");
20771
+ process.exit(1);
20772
+ }
20773
+ if (!prompt && !beadId && !process.stdin.isTTY) {
20774
+ prompt = await new Promise((resolve2) => {
20775
+ let buf = "";
20776
+ process.stdin.setEncoding("utf-8");
20777
+ process.stdin.on("data", (chunk) => {
20778
+ buf += chunk;
20779
+ });
20780
+ process.stdin.on("end", () => resolve2(buf.trim()));
20781
+ });
20782
+ }
20783
+ if (!prompt && !beadId) {
20784
+ console.error("Error: provide --prompt, pipe stdin, or use --bead <id>.");
20785
+ process.exit(1);
20786
+ }
20787
+ return { name, prompt, beadId, model, noBeads, noBeadNotes, keepAlive, noKeepAlive, background, contextDepth, outputMode };
20788
+ }
20789
+ function startEventTailer(jobId, jobsDir, mode, specialist, beadId) {
20790
+ const eventsPath = join17(jobsDir, jobId, "events.jsonl");
20791
+ let linesRead = 0;
20792
+ let activeInlinePhase = null;
20793
+ const drain = () => {
20794
+ let content;
20795
+ try {
20796
+ content = readFileSync7(eventsPath, "utf-8");
20797
+ } catch {
20798
+ return;
20799
+ }
20800
+ if (!content)
20801
+ return;
20802
+ const lastNl = content.lastIndexOf(`
20803
+ `);
20804
+ if (lastNl < 0)
20805
+ return;
20806
+ const complete = content.slice(0, lastNl);
20807
+ const lines = complete.split(`
20808
+ `);
20809
+ for (let i = linesRead;i < lines.length; i++) {
20810
+ linesRead++;
20811
+ const line = lines[i].trim();
20812
+ if (!line)
20813
+ continue;
20814
+ let event;
20815
+ try {
20816
+ event = JSON.parse(line);
20817
+ } catch {
20818
+ continue;
20819
+ }
20820
+ if (mode === "json") {
20821
+ process.stdout.write(JSON.stringify({ jobId, specialist, beadId, ...event }) + `
20822
+ `);
20823
+ } else {
20824
+ if (event.type === "run_complete" && event.output) {
20825
+ activeInlinePhase = null;
20826
+ process.stdout.write(`
20827
+ ` + event.output + `
20828
+ `);
20829
+ } else {
20830
+ const { line: line2, nextPhase } = formatEventInlineDebounced(event, activeInlinePhase);
20831
+ activeInlinePhase = nextPhase;
20832
+ if (line2)
20833
+ process.stdout.write(line2 + `
20834
+ `);
20835
+ }
20836
+ }
20837
+ }
20838
+ };
20839
+ const intervalId = setInterval(drain, 100);
20840
+ return () => {
20841
+ clearInterval(intervalId);
20842
+ drain();
20843
+ };
20844
+ }
20845
+ function formatFooterModel(backend, model) {
20846
+ if (!model)
20847
+ return "";
20848
+ if (!backend)
20849
+ return model;
20850
+ return model.startsWith(`${backend}/`) ? model : `${backend}/${model}`;
20851
+ }
20852
+ async function run9() {
20853
+ const args = await parseArgs6(process.argv.slice(3));
20854
+ if (args.background) {
20855
+ const latestPath = join17(process.cwd(), ".specialists", "jobs", "latest");
20856
+ const oldLatest = (() => {
20857
+ try {
20858
+ return readFileSync7(latestPath, "utf-8").trim();
20859
+ } catch {
20860
+ return "";
20861
+ }
20862
+ })();
20863
+ const childArgs = process.argv.slice(2).filter((a) => a !== "--background");
20864
+ const child = cpSpawn(process.execPath, [process.argv[1], ...childArgs], {
20865
+ detached: true,
20866
+ stdio: "ignore",
20867
+ cwd: process.cwd(),
20868
+ env: process.env
20869
+ });
20870
+ child.unref();
20871
+ const deadline = Date.now() + 5000;
20872
+ let jobId2 = "";
20873
+ while (Date.now() < deadline) {
20874
+ await new Promise((r) => setTimeout(r, 100));
20875
+ try {
20876
+ const current = readFileSync7(latestPath, "utf-8").trim();
20877
+ if (current && current !== oldLatest) {
20878
+ jobId2 = current;
20879
+ break;
20880
+ }
20881
+ } catch {}
20882
+ }
20883
+ if (jobId2) {
20884
+ process.stdout.write(`${jobId2}
20885
+ `);
20886
+ } else {
20887
+ process.stderr.write(`Warning: job started but ID not yet available. Check specialists status.
20888
+ `);
20889
+ process.stdout.write(`${child.pid}
20890
+ `);
20891
+ }
20892
+ process.exit(0);
20893
+ }
20894
+ const loader = new SpecialistLoader;
20895
+ const circuitBreaker = new CircuitBreaker;
20896
+ const hooks = new HookEmitter({ tracePath: join17(process.cwd(), ".specialists", "trace.jsonl") });
20897
+ const beadsClient = args.noBeads ? undefined : new BeadsClient;
20898
+ const beadReader = beadsClient ?? new BeadsClient;
20899
+ let prompt = args.prompt;
20900
+ let variables;
20901
+ if (args.beadId) {
20902
+ const bead = beadReader.readBead(args.beadId);
20903
+ if (!bead) {
20904
+ throw new Error(`Unable to read bead '${args.beadId}' via bd show --json`);
20905
+ }
20906
+ const blockers = args.contextDepth > 0 ? beadReader.getCompletedBlockers(args.beadId, args.contextDepth) : [];
20907
+ if (blockers.length > 0) {
20908
+ process.stderr.write(dim8(`
20909
+ [context: ${blockers.length} completed dep${blockers.length > 1 ? "s" : ""} injected]
20910
+ `));
20911
+ }
20912
+ const beadContext = buildBeadContext(bead, blockers);
20913
+ prompt = beadContext;
20914
+ variables = {
20915
+ bead_context: beadContext,
20916
+ bead_id: args.beadId
20917
+ };
20918
+ }
20919
+ const specialist = await loader.get(args.name).catch((err) => {
20920
+ process.stderr.write(`Error: ${err?.message ?? err}
20921
+ `);
20922
+ process.exit(1);
20923
+ });
20924
+ const runner = new SpecialistRunner({
20925
+ loader,
20926
+ hooks,
20927
+ circuitBreaker,
20928
+ beadsClient
20929
+ });
20930
+ const beadsWriteNotes = args.noBeadNotes ? false : specialist.specialist.beads_write_notes ?? true;
20931
+ const jobsDir = join17(process.cwd(), ".specialists", "jobs");
20932
+ let stopTailer;
20933
+ const supervisor = new Supervisor({
20934
+ runner,
20935
+ runOptions: {
20936
+ name: args.name,
20937
+ prompt,
20938
+ variables,
20939
+ backendOverride: args.model,
20940
+ inputBeadId: args.beadId,
20941
+ keepAlive: args.keepAlive,
20942
+ noKeepAlive: args.noKeepAlive,
20943
+ beadsWriteNotes
20944
+ },
20945
+ jobsDir,
20946
+ beadsClient,
20947
+ stallDetection: specialist.specialist.stall_detection,
20948
+ onProgress: args.outputMode === "raw" ? (delta) => process.stdout.write(delta) : undefined,
20949
+ onMeta: args.outputMode !== "human" ? (meta) => process.stderr.write(dim8(`
20950
+ [${meta.backend} / ${meta.model}]
20951
+
20952
+ `)) : undefined,
20953
+ onJobStarted: ({ id }) => {
20954
+ process.stderr.write(dim8(`[job started: ${id}]
20955
+ `));
20956
+ if (args.outputMode !== "raw") {
20957
+ stopTailer = startEventTailer(id, jobsDir, args.outputMode, args.name, args.beadId);
20958
+ }
20959
+ }
20960
+ });
20961
+ process.stderr.write(`
20962
+ ${bold8(`Running ${cyan5(args.name)}`)}
20963
+
20964
+ `);
20965
+ let jobId;
20966
+ try {
20967
+ jobId = await supervisor.run();
20968
+ } catch (err) {
20969
+ stopTailer?.();
20970
+ process.stderr.write(`Error: ${err?.message ?? err}
20971
+ `);
20972
+ process.exit(1);
20973
+ }
20974
+ stopTailer?.();
20975
+ const status = supervisor.readStatus(jobId);
20976
+ const secs = ((status?.last_event_at_ms ?? Date.now()) - (status?.started_at_ms ?? Date.now())) / 1000;
20977
+ const modelLabel = formatFooterModel(status?.backend, status?.model);
20978
+ const footer = [
20979
+ `job ${jobId}`,
20980
+ status?.bead_id ? `bead ${status.bead_id}` : "",
20981
+ `${secs.toFixed(1)}s`,
20982
+ modelLabel ? dim8(modelLabel) : ""
20983
+ ].filter(Boolean).join(" ");
20984
+ process.stderr.write(`
20985
+ ${green8("✓")} ${footer}
20986
+
20987
+ `);
20988
+ process.stderr.write(dim8(`Poll: specialists poll ${jobId} --json
20989
+
20990
+ `));
20991
+ process.exit(0);
20061
20992
  }
20062
- var dim8 = (s) => `\x1B[2m${s}\x1B[0m`, bold8 = (s) => `\x1B[1m${s}\x1B[0m`, cyan5 = (s) => `\x1B[36m${s}\x1B[0m`, yellow7 = (s) => `\x1B[33m${s}\x1B[0m`, red2 = (s) => `\x1B[31m${s}\x1B[0m`, green7 = (s) => `\x1B[32m${s}\x1B[0m`, blue = (s) => `\x1B[34m${s}\x1B[0m`, magenta = (s) => `\x1B[35m${s}\x1B[0m`, JOB_COLORS, EVENT_LABELS;
20063
- var init_format_helpers = __esm(() => {
20064
- JOB_COLORS = [cyan5, yellow7, magenta, green7, blue, red2];
20065
- EVENT_LABELS = {
20066
- run_start: "START",
20067
- meta: "META",
20068
- thinking: "THINK",
20069
- tool: "TOOL",
20070
- text: "TEXT",
20071
- message: "MSG",
20072
- turn: "TURN",
20073
- run_complete: "DONE",
20074
- done: "DONE",
20075
- agent_end: "DONE",
20076
- error: "ERR"
20077
- };
20993
+ var bold8 = (s) => `\x1B[1m${s}\x1B[0m`, dim8 = (s) => `\x1B[2m${s}\x1B[0m`, green8 = (s) => `\x1B[32m${s}\x1B[0m`, cyan5 = (s) => `\x1B[36m${s}\x1B[0m`;
20994
+ var init_run = __esm(() => {
20995
+ init_loader();
20996
+ init_runner();
20997
+ init_circuitBreaker();
20998
+ init_hooks();
20999
+ init_beads();
21000
+ init_supervisor();
21001
+ init_format_helpers();
20078
21002
  });
20079
21003
 
20080
21004
  // src/cli/status.ts
20081
21005
  var exports_status = {};
20082
21006
  __export(exports_status, {
20083
- run: () => run9
21007
+ run: () => run10
20084
21008
  });
20085
- import { spawnSync as spawnSync6 } from "node:child_process";
20086
- import { existsSync as existsSync9 } from "node:fs";
20087
- import { join as join12 } from "node:path";
21009
+ import { spawnSync as spawnSync7 } from "node:child_process";
21010
+ import { existsSync as existsSync13, readFileSync as readFileSync8 } from "node:fs";
21011
+ import { join as join18 } from "node:path";
20088
21012
  function ok2(msg) {
20089
21013
  console.log(` ${green7("✓")} ${msg}`);
20090
21014
  }
20091
21015
  function warn(msg) {
20092
- console.log(` ${yellow7("○")} ${msg}`);
21016
+ console.log(` ${yellow8("○")} ${msg}`);
20093
21017
  }
20094
21018
  function fail(msg) {
20095
21019
  console.log(` ${red2("✗")} ${msg}`);
20096
21020
  }
20097
21021
  function info(msg) {
20098
- console.log(` ${dim8(msg)}`);
21022
+ console.log(` ${dim7(msg)}`);
20099
21023
  }
20100
21024
  function section(label) {
20101
21025
  const line = "─".repeat(Math.max(0, 38 - label.length));
20102
21026
  console.log(`
20103
- ${bold8(`── ${label} ${line}`)}`);
21027
+ ${bold7(`── ${label} ${line}`)}`);
20104
21028
  }
20105
21029
  function cmd(bin, args) {
20106
- const r = spawnSync6(bin, args, {
21030
+ const r = spawnSync7(bin, args, {
20107
21031
  encoding: "utf8",
20108
21032
  stdio: "pipe",
20109
21033
  timeout: 5000
@@ -20111,7 +21035,7 @@ function cmd(bin, args) {
20111
21035
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
20112
21036
  }
20113
21037
  function isInstalled(bin) {
20114
- return spawnSync6("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
21038
+ return spawnSync7("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
20115
21039
  }
20116
21040
  function formatElapsed2(s) {
20117
21041
  if (s.elapsed_s === undefined)
@@ -20123,20 +21047,83 @@ function formatElapsed2(s) {
20123
21047
  function statusColor(status) {
20124
21048
  switch (status) {
20125
21049
  case "running":
20126
- return cyan5(status);
21050
+ return cyan4(status);
20127
21051
  case "done":
20128
21052
  return green7(status);
20129
21053
  case "error":
20130
21054
  return red2(status);
20131
21055
  case "starting":
20132
- return yellow7(status);
21056
+ return yellow8(status);
20133
21057
  default:
20134
21058
  return status;
20135
21059
  }
20136
21060
  }
20137
- async function run9() {
21061
+ function parseStatusArgs(argv) {
21062
+ let jsonMode = false;
21063
+ let jobId;
21064
+ for (let i = 0;i < argv.length; i += 1) {
21065
+ const arg = argv[i];
21066
+ if (arg === "--json") {
21067
+ jsonMode = true;
21068
+ continue;
21069
+ }
21070
+ if (arg === "--job") {
21071
+ const candidate = argv[i + 1];
21072
+ if (!candidate || candidate.startsWith("--")) {
21073
+ throw new Error("--job requires a value");
21074
+ }
21075
+ jobId = candidate;
21076
+ i += 1;
21077
+ continue;
21078
+ }
21079
+ if (arg.startsWith("--job=")) {
21080
+ const candidate = arg.slice("--job=".length).trim();
21081
+ if (!candidate) {
21082
+ throw new Error("--job requires a value");
21083
+ }
21084
+ jobId = candidate;
21085
+ }
21086
+ }
21087
+ return { jsonMode, jobId };
21088
+ }
21089
+ function countJobEvents(jobsDir, jobId) {
21090
+ const eventsFile = join18(jobsDir, jobId, "events.jsonl");
21091
+ if (!existsSync13(eventsFile))
21092
+ return 0;
21093
+ const raw = readFileSync8(eventsFile, "utf-8").trim();
21094
+ if (!raw)
21095
+ return 0;
21096
+ return raw.split(`
21097
+ `).filter((line) => line.trim().length > 0).length;
21098
+ }
21099
+ function renderJobDetail(job, eventCount) {
21100
+ console.log(`
21101
+ ${bold7("specialists status")}
21102
+ `);
21103
+ section(`Job ${job.id}`);
21104
+ console.log(` specialist ${job.specialist}`);
21105
+ console.log(` status ${statusColor(job.status)}`);
21106
+ console.log(` model ${job.model ?? "n/a"}`);
21107
+ console.log(` backend ${job.backend ?? "n/a"}`);
21108
+ console.log(` elapsed ${formatElapsed2(job)}`);
21109
+ console.log(` bead_id ${job.bead_id ?? "n/a"}`);
21110
+ console.log(` events ${eventCount}`);
21111
+ if (job.session_file)
21112
+ console.log(` session_file ${job.session_file}`);
21113
+ if (job.error)
21114
+ console.log(` error ${red2(job.error)}`);
21115
+ console.log();
21116
+ }
21117
+ async function run10() {
20138
21118
  const argv = process.argv.slice(3);
20139
- const jsonMode = argv.includes("--json");
21119
+ let parsedArgs;
21120
+ try {
21121
+ parsedArgs = parseStatusArgs(argv);
21122
+ } catch (error2) {
21123
+ console.error(red2(error2.message));
21124
+ process.exit(1);
21125
+ }
21126
+ const { jsonMode, jobId } = parsedArgs;
20140
21127
  const loader = new SpecialistLoader;
20141
21128
  const allSpecialists = await loader.list();
20142
21129
  const piInstalled = isInstalled("pi");
@@ -20146,18 +21133,42 @@ async function run9() {
20146
21133
  `).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean)) : new Set;
20147
21134
  const bdInstalled = isInstalled("bd");
20148
21135
  const bdVersion = bdInstalled ? cmd("bd", ["--version"]) : null;
20149
- const beadsPresent = existsSync9(join12(process.cwd(), ".beads"));
21136
+ const beadsPresent = existsSync13(join18(process.cwd(), ".beads"));
20150
21137
  const specialistsBin = cmd("which", ["specialists"]);
20151
- const jobsDir = join12(process.cwd(), ".specialists", "jobs");
21138
+ const jobsDir = join18(process.cwd(), ".specialists", "jobs");
20152
21139
  let jobs = [];
20153
- if (existsSync9(jobsDir)) {
20154
- const supervisor = new Supervisor({
21140
+ let supervisor = null;
21141
+ if (existsSync13(jobsDir)) {
21142
+ supervisor = new Supervisor({
20155
21143
  runner: null,
20156
21144
  runOptions: null,
20157
21145
  jobsDir
20158
21146
  });
20159
21147
  jobs = supervisor.listJobs();
20160
21148
  }
21149
+ if (jobId) {
21150
+ const selectedJob = supervisor?.readStatus(jobId) ?? null;
21151
+ if (!selectedJob) {
21152
+ if (jsonMode) {
21153
+ console.log(JSON.stringify({ error: `Job not found: ${jobId}` }, null, 2));
21154
+ } else {
21155
+ fail(`job not found: ${jobId}`);
21156
+ }
21157
+ process.exit(1);
21158
+ }
21159
+ const eventCount = countJobEvents(jobsDir, jobId);
21160
+ if (jsonMode) {
21161
+ console.log(JSON.stringify({
21162
+ job: {
21163
+ ...selectedJob,
21164
+ event_count: eventCount
21165
+ }
21166
+ }, null, 2));
21167
+ return;
21168
+ }
21169
+ renderJobDetail(selectedJob, eventCount);
21170
+ return;
21171
+ }
20161
21172
  const stalenessMap = {};
20162
21173
  for (const s of allSpecialists) {
20163
21174
  stalenessMap[s.name] = await checkStaleness(s);
@@ -20201,51 +21212,51 @@ async function run9() {
20201
21212
  return;
20202
21213
  }
20203
21214
  console.log(`
20204
- ${bold8("specialists status")}
21215
+ ${bold7("specialists status")}
20205
21216
  `);
20206
21217
  section("Specialists");
20207
21218
  if (allSpecialists.length === 0) {
20208
- warn(`no specialists found — run ${yellow7("specialists init")} to scaffold`);
21219
+ warn(`no specialists found — run ${yellow8("specialists init")} to scaffold`);
20209
21220
  } else {
20210
21221
  const byScope = allSpecialists.reduce((acc, s) => {
20211
21222
  acc[s.scope] = (acc[s.scope] ?? 0) + 1;
20212
21223
  return acc;
20213
21224
  }, {});
20214
21225
  const scopeSummary = Object.entries(byScope).map(([scope, n]) => `${n} ${scope}`).join(", ");
20215
- ok2(`${allSpecialists.length} found ${dim8(`(${scopeSummary})`)}`);
21226
+ ok2(`${allSpecialists.length} found ${dim7(`(${scopeSummary})`)}`);
20216
21227
  for (const s of allSpecialists) {
20217
21228
  const staleness = stalenessMap[s.name];
20218
21229
  if (staleness === "AGED") {
20219
- warn(`${s.name} ${red2("AGED")} ${dim8(s.scope)}`);
21230
+ warn(`${s.name} ${red2("AGED")} ${dim7(s.scope)}`);
20220
21231
  } else if (staleness === "STALE") {
20221
- warn(`${s.name} ${yellow7("STALE")} ${dim8(s.scope)}`);
21232
+ warn(`${s.name} ${yellow8("STALE")} ${dim7(s.scope)}`);
20222
21233
  }
20223
21234
  }
20224
21235
  }
20225
21236
  section("pi (coding agent runtime)");
20226
21237
  if (!piInstalled) {
20227
- fail(`pi not installed — install ${yellow7("pi")} first`);
21238
+ fail(`pi not installed — install ${yellow8("pi")} first`);
20228
21239
  } else {
20229
21240
  const vStr = piVersion?.ok ? `v${piVersion.stdout}` : "unknown version";
20230
- const pStr = piProviders.size > 0 ? `${piProviders.size} provider${piProviders.size > 1 ? "s" : ""} active ${dim8(`(${[...piProviders].join(", ")})`)} ` : yellow7("no providers configured — run pi config");
21241
+ const pStr = piProviders.size > 0 ? `${piProviders.size} provider${piProviders.size > 1 ? "s" : ""} active ${dim7(`(${[...piProviders].join(", ")})`)} ` : yellow8("no providers configured — run pi config");
20231
21242
  ok2(`${vStr} — ${pStr}`);
20232
21243
  }
20233
21244
  section("beads (issue tracker)");
20234
21245
  if (!bdInstalled) {
20235
- fail(`bd not installed — install ${yellow7("bd")} first`);
21246
+ fail(`bd not installed — install ${yellow8("bd")} first`);
20236
21247
  } else {
20237
- ok2(`bd installed${bdVersion?.ok ? ` ${dim8(bdVersion.stdout)}` : ""}`);
21248
+ ok2(`bd installed${bdVersion?.ok ? ` ${dim7(bdVersion.stdout)}` : ""}`);
20238
21249
  if (beadsPresent) {
20239
21250
  ok2(".beads/ present in project");
20240
21251
  } else {
20241
- warn(`.beads/ not found — run ${yellow7("bd init")} to enable issue tracking`);
21252
+ warn(`.beads/ not found — run ${yellow8("bd init")} to enable issue tracking`);
20242
21253
  }
20243
21254
  }
20244
21255
  section("MCP");
20245
21256
  if (!specialistsBin.ok) {
20246
- fail(`specialists not installed globally — run ${yellow7("npm install -g @jaggerxtrm/specialists")}`);
21257
+ fail(`specialists not installed globally — run ${yellow8("npm install -g @jaggerxtrm/specialists")}`);
20247
21258
  } else {
20248
- ok2(`specialists binary installed ${dim8(specialistsBin.stdout)}`);
21259
+ ok2(`specialists binary installed ${dim7(specialistsBin.stdout)}`);
20249
21260
  info(`verify registration: claude mcp get specialists`);
20250
21261
  info(`re-register: specialists install`);
20251
21262
  }
@@ -20255,8 +21266,8 @@ ${bold8("specialists status")}
20255
21266
  } else {
20256
21267
  for (const job of jobs) {
20257
21268
  const elapsed = formatElapsed2(job);
20258
- const detail = job.status === "error" ? red2(job.error?.slice(0, 40) ?? "error") : job.current_tool ? dim8(`tool: ${job.current_tool}`) : dim8(job.current_event ?? "");
20259
- console.log(` ${dim8(job.id)} ${job.specialist.padEnd(20)} ${statusColor(job.status).padEnd(7)} ${elapsed.padStart(6)} ${detail}`);
21269
+ const detail = job.status === "error" ? red2(job.error?.slice(0, 40) ?? "error") : job.current_tool ? dim7(`tool: ${job.current_tool}`) : dim7(job.current_event ?? "");
21270
+ console.log(` ${dim7(job.id)} ${job.specialist.padEnd(20)} ${statusColor(job.status).padEnd(7)} ${elapsed.padStart(6)} ${detail}`);
20260
21271
  }
20261
21272
  }
20262
21273
  console.log();
@@ -20270,33 +21281,88 @@ var init_status = __esm(() => {
20270
21281
  // src/cli/result.ts
20271
21282
  var exports_result = {};
20272
21283
  __export(exports_result, {
20273
- run: () => run10
21284
+ run: () => run11
20274
21285
  });
20275
- import { existsSync as existsSync10, readFileSync as readFileSync5 } from "node:fs";
20276
- import { join as join13 } from "node:path";
20277
- async function run10() {
20278
- const jobId = process.argv[3];
20279
- if (!jobId) {
20280
- console.error("Usage: specialists|sp result <job-id>");
21286
+ import { existsSync as existsSync14, readFileSync as readFileSync9 } from "node:fs";
21287
+ import { join as join19 } from "node:path";
21288
+ function parseArgs7(argv) {
21289
+ const jobId = argv[0];
21290
+ if (!jobId || jobId.startsWith("--")) {
21291
+ console.error("Usage: specialists|sp result <job-id> [--wait] [--timeout <seconds>]");
20281
21292
  process.exit(1);
20282
21293
  }
20283
- const jobsDir = join13(process.cwd(), ".specialists", "jobs");
21294
+ let wait = false;
21295
+ let timeout;
21296
+ for (let i = 1;i < argv.length; i++) {
21297
+ const token = argv[i];
21298
+ if (token === "--wait") {
21299
+ wait = true;
21300
+ continue;
21301
+ }
21302
+ if (token === "--timeout" && argv[i + 1]) {
21303
+ const parsed = parseInt(argv[++i], 10);
21304
+ if (isNaN(parsed) || parsed <= 0) {
21305
+ console.error("Error: --timeout must be a positive integer (seconds)");
21306
+ process.exit(1);
21307
+ }
21308
+ timeout = parsed;
21309
+ continue;
21310
+ }
21311
+ }
21312
+ return { jobId, wait, timeout };
21313
+ }
21314
+ async function run11() {
21315
+ const args = parseArgs7(process.argv.slice(3));
21316
+ const { jobId } = args;
21317
+ const jobsDir = join19(process.cwd(), ".specialists", "jobs");
20284
21318
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
21319
+ const resultPath = join19(jobsDir, jobId, "result.txt");
21320
+ if (args.wait) {
21321
+ const startMs = Date.now();
21322
+ while (true) {
21323
+ const status2 = supervisor.readStatus(jobId);
21324
+ if (!status2) {
21325
+ console.error(`No job found: ${jobId}`);
21326
+ process.exit(1);
21327
+ }
21328
+ if (status2.status === "done") {
21329
+ if (!existsSync14(resultPath)) {
21330
+ console.error(`Result file not found for job ${jobId}`);
21331
+ process.exit(1);
21332
+ }
21333
+ process.stdout.write(readFileSync9(resultPath, "utf-8"));
21334
+ return;
21335
+ }
21336
+ if (status2.status === "error") {
21337
+ process.stderr.write(`${red3(`Job ${jobId} failed:`)} ${status2.error ?? "unknown error"}
21338
+ `);
21339
+ process.exit(1);
21340
+ }
21341
+ if (args.timeout !== undefined) {
21342
+ const elapsedSecs = (Date.now() - startMs) / 1000;
21343
+ if (elapsedSecs >= args.timeout) {
21344
+ process.stderr.write(`Timeout: job ${jobId} did not complete within ${args.timeout}s
21345
+ `);
21346
+ process.exit(1);
21347
+ }
21348
+ }
21349
+ await new Promise((r) => setTimeout(r, 1000));
21350
+ }
21351
+ }
20285
21352
  const status = supervisor.readStatus(jobId);
20286
21353
  if (!status) {
20287
21354
  console.error(`No job found: ${jobId}`);
20288
21355
  process.exit(1);
20289
21356
  }
20290
- const resultPath = join13(jobsDir, jobId, "result.txt");
20291
21357
  if (status.status === "running" || status.status === "starting") {
20292
- if (!existsSync10(resultPath)) {
21358
+ if (!existsSync14(resultPath)) {
20293
21359
  process.stderr.write(`${dim9(`Job ${jobId} is still ${status.status}. Use 'specialists feed --job ${jobId}' to follow.`)}
20294
21360
  `);
20295
21361
  process.exit(1);
20296
21362
  }
20297
21363
  process.stderr.write(`${dim9(`Job ${jobId} is currently ${status.status}. Showing last completed output while it continues.`)}
20298
21364
  `);
20299
- process.stdout.write(readFileSync5(resultPath, "utf-8"));
21365
+ process.stdout.write(readFileSync9(resultPath, "utf-8"));
20300
21366
  return;
20301
21367
  }
20302
21368
  if (status.status === "error") {
@@ -20304,130 +21370,30 @@ async function run10() {
20304
21370
  `);
20305
21371
  process.exit(1);
20306
21372
  }
20307
- if (!existsSync10(resultPath)) {
21373
+ if (!existsSync14(resultPath)) {
20308
21374
  console.error(`Result file not found for job ${jobId}`);
20309
21375
  process.exit(1);
20310
21376
  }
20311
- process.stdout.write(readFileSync5(resultPath, "utf-8"));
21377
+ process.stdout.write(readFileSync9(resultPath, "utf-8"));
20312
21378
  }
20313
21379
  var dim9 = (s) => `\x1B[2m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
20314
21380
  var init_result = __esm(() => {
20315
21381
  init_supervisor();
20316
21382
  });
20317
21383
 
20318
- // src/specialist/timeline-query.ts
20319
- import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
20320
- import { join as join14 } from "node:path";
20321
- function readJobEvents(jobDir) {
20322
- const eventsPath = join14(jobDir, "events.jsonl");
20323
- if (!existsSync11(eventsPath))
20324
- return [];
20325
- const content = readFileSync6(eventsPath, "utf-8");
20326
- const lines = content.split(`
20327
- `).filter(Boolean);
20328
- const events = [];
20329
- for (const line of lines) {
20330
- const event = parseTimelineEvent(line);
20331
- if (event)
20332
- events.push(event);
20333
- }
20334
- events.sort(compareTimelineEvents);
20335
- return events;
20336
- }
20337
- function readJobEventsById(jobsDir, jobId) {
20338
- return readJobEvents(join14(jobsDir, jobId));
20339
- }
20340
- function readAllJobEvents(jobsDir) {
20341
- if (!existsSync11(jobsDir))
20342
- return [];
20343
- const batches = [];
20344
- const entries = readdirSync3(jobsDir);
20345
- for (const entry of entries) {
20346
- const jobDir = join14(jobsDir, entry);
20347
- try {
20348
- const stat2 = __require("node:fs").statSync(jobDir);
20349
- if (!stat2.isDirectory())
20350
- continue;
20351
- } catch {
20352
- continue;
20353
- }
20354
- const jobId = entry;
20355
- const statusPath = join14(jobDir, "status.json");
20356
- let specialist = "unknown";
20357
- let beadId;
20358
- if (existsSync11(statusPath)) {
20359
- try {
20360
- const status = JSON.parse(readFileSync6(statusPath, "utf-8"));
20361
- specialist = status.specialist ?? "unknown";
20362
- beadId = status.bead_id;
20363
- } catch {}
20364
- }
20365
- const events = readJobEvents(jobDir);
20366
- if (events.length > 0) {
20367
- batches.push({ jobId, specialist, beadId, events });
20368
- }
20369
- }
20370
- return batches;
20371
- }
20372
- function mergeTimelineEvents(batches) {
20373
- const merged = [];
20374
- for (const batch of batches) {
20375
- for (const event of batch.events) {
20376
- merged.push({
20377
- jobId: batch.jobId,
20378
- specialist: batch.specialist,
20379
- beadId: batch.beadId,
20380
- event
20381
- });
20382
- }
20383
- }
20384
- merged.sort((a, b) => compareTimelineEvents(a.event, b.event));
20385
- return merged;
20386
- }
20387
- function filterTimelineEvents(merged, filter) {
20388
- let result = merged;
20389
- if (filter.since !== undefined) {
20390
- result = result.filter(({ event }) => event.t >= filter.since);
20391
- }
20392
- if (filter.jobId !== undefined) {
20393
- result = result.filter(({ jobId }) => jobId === filter.jobId);
20394
- }
20395
- if (filter.specialist !== undefined) {
20396
- result = result.filter(({ specialist }) => specialist === filter.specialist);
20397
- }
20398
- if (filter.limit !== undefined && filter.limit > 0) {
20399
- result = result.slice(0, filter.limit);
20400
- }
20401
- return result;
20402
- }
20403
- function queryTimeline(jobsDir, filter = {}) {
20404
- let batches = readAllJobEvents(jobsDir);
20405
- if (filter.jobId !== undefined) {
20406
- batches = batches.filter((b) => b.jobId === filter.jobId);
20407
- }
20408
- if (filter.specialist !== undefined) {
20409
- batches = batches.filter((b) => b.specialist === filter.specialist);
20410
- }
20411
- const merged = mergeTimelineEvents(batches);
20412
- return filterTimelineEvents(merged, filter);
20413
- }
20414
- var init_timeline_query = __esm(() => {
20415
- init_timeline_events();
20416
- });
20417
-
20418
21384
  // src/cli/feed.ts
20419
21385
  var exports_feed = {};
20420
21386
  __export(exports_feed, {
20421
- run: () => run11
21387
+ run: () => run12
20422
21388
  });
20423
- import { existsSync as existsSync12 } from "node:fs";
20424
- import { join as join15 } from "node:path";
21389
+ import { existsSync as existsSync15, readFileSync as readFileSync10 } from "node:fs";
21390
+ import { join as join20 } from "node:path";
20425
21391
  function getHumanEventKey(event) {
20426
21392
  switch (event.type) {
20427
21393
  case "meta":
20428
21394
  return `meta:${event.backend}:${event.model}`;
20429
21395
  case "tool":
20430
- return `tool:${event.tool}:${event.phase}:${event.is_error ? "error" : "ok"}`;
21396
+ return `tool:${event.tool}:${event.phase}:${event.tool_call_id ?? event.t}`;
20431
21397
  case "text":
20432
21398
  return "text";
20433
21399
  case "thinking":
@@ -20440,13 +21406,21 @@ function getHumanEventKey(event) {
20440
21406
  return `run_start:${event.specialist}:${event.bead_id ?? ""}`;
20441
21407
  case "run_complete":
20442
21408
  return `run_complete:${event.status}:${event.error ?? ""}`;
20443
- case "done":
20444
- case "agent_end":
20445
- return `complete:${event.type}`;
20446
21409
  default:
20447
21410
  return event.type;
20448
21411
  }
20449
21412
  }
21413
+ function shouldRenderHumanEvent(event) {
21414
+ if (event.type === "message" || event.type === "turn")
21415
+ return false;
21416
+ if (event.type === "tool") {
21417
+ if (event.phase === "update")
21418
+ return false;
21419
+ if (event.phase === "end" && !event.is_error)
21420
+ return false;
21421
+ }
21422
+ return true;
21423
+ }
20450
21424
  function shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey) {
20451
21425
  if (event.type === "meta") {
20452
21426
  const metaKey = `${event.backend}:${event.model}`;
@@ -20454,6 +21428,9 @@ function shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey) {
20454
21428
  return true;
20455
21429
  seenMetaKey.set(jobId, metaKey);
20456
21430
  }
21431
+ if (event.type === "tool") {
21432
+ return false;
21433
+ }
20457
21434
  const key = getHumanEventKey(event);
20458
21435
  if (lastPrintedEventKey.get(jobId) === key)
20459
21436
  return true;
@@ -20473,7 +21450,36 @@ function parseSince(value) {
20473
21450
  }
20474
21451
  return;
20475
21452
  }
20476
- function parseArgs6(argv) {
21453
+ function isTerminalJobStatus(jobsDir, jobId) {
21454
+ const statusPath = join20(jobsDir, jobId, "status.json");
21455
+ try {
21456
+ const status = JSON.parse(readFileSync10(statusPath, "utf-8"));
21457
+ return status.status === "done" || status.status === "error";
21458
+ } catch {
21459
+ return false;
21460
+ }
21461
+ }
21462
+ function makeJobMetaReader(jobsDir) {
21463
+ const cache = new Map;
21464
+ return (jobId) => {
21465
+ if (cache.has(jobId))
21466
+ return cache.get(jobId);
21467
+ const statusPath = join20(jobsDir, jobId, "status.json");
21468
+ let meta = { startedAtMs: Date.now() };
21469
+ try {
21470
+ const status = JSON.parse(readFileSync10(statusPath, "utf-8"));
21471
+ meta = {
21472
+ model: status.model,
21473
+ backend: status.backend,
21474
+ beadId: status.bead_id,
21475
+ startedAtMs: status.started_at_ms ?? Date.now()
21476
+ };
21477
+ } catch {}
21478
+ cache.set(jobId, meta);
21479
+ return meta;
21480
+ };
21481
+ }
21482
+ function parseArgs8(argv) {
20477
21483
  let jobId;
20478
21484
  let specialist;
20479
21485
  let since;
@@ -20515,33 +21521,52 @@ function parseArgs6(argv) {
20515
21521
  }
20516
21522
  return { jobId, specialist, since, limit, follow, forever, json };
20517
21523
  }
20518
- function printSnapshot(merged, options) {
21524
+ function printSnapshot(merged, options, jobsDir) {
20519
21525
  if (merged.length === 0) {
20520
21526
  if (!options.json)
20521
- console.log(dim8("No events found."));
21527
+ console.log(dim7("No events found."));
20522
21528
  return;
20523
21529
  }
20524
21530
  const colorMap = new JobColorMap;
20525
21531
  if (options.json) {
21532
+ const getJobMeta2 = jobsDir ? makeJobMetaReader(jobsDir) : () => ({ startedAtMs: Date.now() });
20526
21533
  for (const { jobId, specialist, beadId, event } of merged) {
20527
- console.log(JSON.stringify({ jobId, specialist, beadId, ...event }));
21534
+ const meta = getJobMeta2(jobId);
21535
+ const model = meta.model ?? (event.type === "meta" ? event.model : undefined);
21536
+ const backend = meta.backend ?? (event.type === "meta" ? event.backend : undefined);
21537
+ console.log(JSON.stringify({
21538
+ jobId,
21539
+ specialist,
21540
+ specialist_model: formatSpecialistModel(specialist, model),
21541
+ model,
21542
+ backend,
21543
+ beadId: meta.beadId ?? beadId,
21544
+ elapsed_ms: Date.now() - meta.startedAtMs,
21545
+ ...event
21546
+ }));
20528
21547
  }
20529
21548
  return;
20530
21549
  }
20531
21550
  const lastPrintedEventKey = new Map;
20532
21551
  const seenMetaKey = new Map;
21552
+ const getJobMeta = jobsDir ? makeJobMetaReader(jobsDir) : () => ({ startedAtMs: Date.now() });
20533
21553
  for (const { jobId, specialist, beadId, event } of merged) {
21554
+ if (!shouldRenderHumanEvent(event))
21555
+ continue;
20534
21556
  if (shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey))
20535
21557
  continue;
20536
21558
  const colorize = colorMap.get(jobId);
20537
- console.log(formatEventLine(event, { jobId, specialist, beadId, colorize }));
21559
+ const meta = getJobMeta(jobId);
21560
+ const specialistDisplay = formatSpecialistModel(specialist, meta.model ?? (event.type === "meta" ? event.model : undefined));
21561
+ console.log(formatEventLine(event, { jobId, specialist: specialistDisplay, beadId, colorize }));
20538
21562
  }
20539
21563
  }
20540
21564
  function isCompletionEvent(event) {
20541
- return isRunCompleteEvent(event) || event.type === "done" || event.type === "agent_end";
21565
+ return isRunCompleteEvent(event);
20542
21566
  }
20543
21567
  async function followMerged(jobsDir, options) {
20544
21568
  const colorMap = new JobColorMap;
21569
+ const getJobMeta = makeJobMetaReader(jobsDir);
20545
21570
  const lastSeenT = new Map;
20546
21571
  const completedJobs = new Set;
20547
21572
  const filteredBatches = () => readAllJobEvents(jobsDir).filter((batch) => !options.jobId || batch.jobId === options.jobId).filter((batch) => !options.specialist || batch.specialist === options.specialist);
@@ -20551,26 +21576,26 @@ async function followMerged(jobsDir, options) {
20551
21576
  since: options.since,
20552
21577
  limit: options.limit
20553
21578
  });
20554
- printSnapshot(initial, { ...options, json: options.json });
21579
+ printSnapshot(initial, { ...options, json: options.json }, jobsDir);
20555
21580
  for (const batch of filteredBatches()) {
20556
21581
  if (batch.events.length > 0) {
20557
21582
  const maxT = Math.max(...batch.events.map((event) => event.t));
20558
21583
  lastSeenT.set(batch.jobId, maxT);
20559
21584
  }
20560
- if (batch.events.some(isCompletionEvent)) {
21585
+ if (batch.events.some(isCompletionEvent) || isTerminalJobStatus(jobsDir, batch.jobId)) {
20561
21586
  completedJobs.add(batch.jobId);
20562
21587
  }
20563
21588
  }
20564
21589
  const initialBatchCount = filteredBatches().length;
20565
21590
  if (!options.forever && initialBatchCount > 0 && completedJobs.size === initialBatchCount) {
20566
21591
  if (!options.json) {
20567
- process.stderr.write(dim8(`All jobs complete.
21592
+ process.stderr.write(dim7(`All jobs complete.
20568
21593
  `));
20569
21594
  }
20570
21595
  return;
20571
21596
  }
20572
21597
  if (!options.json) {
20573
- process.stderr.write(dim8(`Following... (Ctrl+C to stop)
21598
+ process.stderr.write(dim7(`Following... (Ctrl+C to stop)
20574
21599
  `));
20575
21600
  }
20576
21601
  const lastPrintedEventKey = new Map;
@@ -20595,19 +21620,34 @@ async function followMerged(jobsDir, options) {
20595
21620
  const maxT = Math.max(...batch.events.map((e) => e.t));
20596
21621
  lastSeenT.set(batch.jobId, maxT);
20597
21622
  }
20598
- if (batch.events.some(isCompletionEvent)) {
21623
+ if (batch.events.some(isCompletionEvent) || isTerminalJobStatus(jobsDir, batch.jobId)) {
20599
21624
  completedJobs.add(batch.jobId);
20600
21625
  }
20601
21626
  }
20602
21627
  newEvents.sort((a, b) => a.event.t - b.event.t);
20603
21628
  for (const { jobId, specialist, beadId, event } of newEvents) {
21629
+ const meta = getJobMeta(jobId);
21630
+ const model = meta.model ?? (event.type === "meta" ? event.model : undefined);
21631
+ const backend = meta.backend ?? (event.type === "meta" ? event.backend : undefined);
20604
21632
  if (options.json) {
20605
- console.log(JSON.stringify({ jobId, specialist, beadId, ...event }));
21633
+ console.log(JSON.stringify({
21634
+ jobId,
21635
+ specialist,
21636
+ specialist_model: formatSpecialistModel(specialist, model),
21637
+ model,
21638
+ backend,
21639
+ beadId: meta.beadId ?? beadId,
21640
+ elapsed_ms: Date.now() - meta.startedAtMs,
21641
+ ...event
21642
+ }));
20606
21643
  } else {
21644
+ if (!shouldRenderHumanEvent(event))
21645
+ continue;
20607
21646
  if (shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey))
20608
21647
  continue;
20609
21648
  const colorize = colorMap.get(jobId);
20610
- console.log(formatEventLine(event, { jobId, specialist, beadId, colorize }));
21649
+ const specialistDisplay = formatSpecialistModel(specialist, model);
21650
+ console.log(formatEventLine(event, { jobId, specialist: specialistDisplay, beadId, colorize }));
20611
21651
  }
20612
21652
  }
20613
21653
  if (!options.forever && batches.length > 0 && completedJobs.size === batches.length) {
@@ -20617,37 +21657,11 @@ async function followMerged(jobsDir, options) {
20617
21657
  }, 500);
20618
21658
  });
20619
21659
  }
20620
- function showUsage() {
20621
- console.log(`Usage: specialists feed <job-id> [options]
20622
- specialists feed -f [--forever]
20623
-
20624
- Read background job events.
20625
-
20626
- Modes:
20627
- specialists feed <job-id> Replay events for one job
20628
- specialists feed <job-id> -f Follow one job until completion
20629
- specialists feed -f Follow all jobs globally
20630
-
20631
- Options:
20632
- -f, --follow Follow live updates
20633
- --forever Keep following in global mode even when all jobs complete
20634
-
20635
- Examples:
20636
- specialists feed 49adda
20637
- specialists feed 49adda --follow
20638
- specialists feed -f
20639
- specialists feed -f --forever
20640
- `);
20641
- }
20642
- async function run11() {
20643
- const options = parseArgs6(process.argv.slice(3));
20644
- if (!options.jobId && !options.follow) {
20645
- showUsage();
20646
- process.exit(1);
20647
- }
20648
- const jobsDir = join15(process.cwd(), ".specialists", "jobs");
20649
- if (!existsSync12(jobsDir)) {
20650
- console.log(dim8("No jobs directory found."));
21660
+ async function run12() {
21661
+ const options = parseArgs8(process.argv.slice(3));
21662
+ const jobsDir = join20(process.cwd(), ".specialists", "jobs");
21663
+ if (!existsSync15(jobsDir)) {
21664
+ console.log(dim7("No jobs directory found."));
20651
21665
  return;
20652
21666
  }
20653
21667
  if (options.follow) {
@@ -20660,7 +21674,7 @@ async function run11() {
20660
21674
  since: options.since,
20661
21675
  limit: options.limit
20662
21676
  });
20663
- printSnapshot(merged, options);
21677
+ printSnapshot(merged, options, jobsDir);
20664
21678
  }
20665
21679
  var init_feed = __esm(() => {
20666
21680
  init_timeline_events();
@@ -20671,14 +21685,14 @@ var init_feed = __esm(() => {
20671
21685
  // src/cli/poll.ts
20672
21686
  var exports_poll = {};
20673
21687
  __export(exports_poll, {
20674
- run: () => run12
21688
+ run: () => run13
20675
21689
  });
20676
- import { existsSync as existsSync13, readFileSync as readFileSync7 } from "node:fs";
20677
- import { join as join16 } from "node:path";
20678
- function parseArgs7(argv) {
21690
+ import { existsSync as existsSync16, readFileSync as readFileSync11 } from "node:fs";
21691
+ import { join as join21 } from "node:path";
21692
+ function parseArgs9(argv) {
20679
21693
  let jobId;
20680
21694
  let cursor = 0;
20681
- let json = false;
21695
+ let outputCursor = 0;
20682
21696
  for (let i = 0;i < argv.length; i++) {
20683
21697
  if (argv[i] === "--cursor" && argv[i + 1]) {
20684
21698
  cursor = parseInt(argv[++i], 10);
@@ -20686,50 +21700,44 @@ function parseArgs7(argv) {
20686
21700
  cursor = 0;
20687
21701
  continue;
20688
21702
  }
21703
+ if (argv[i] === "--output-cursor" && argv[i + 1]) {
21704
+ outputCursor = parseInt(argv[++i], 10);
21705
+ if (isNaN(outputCursor) || outputCursor < 0)
21706
+ outputCursor = 0;
21707
+ continue;
21708
+ }
20689
21709
  if (argv[i] === "--json") {
20690
- json = true;
20691
21710
  continue;
20692
21711
  }
20693
- if (!argv[i].startsWith("--")) {
20694
- jobId = argv[i];
21712
+ if (argv[i] === "--follow" || argv[i] === "-f") {
21713
+ process.stderr.write(`--follow removed from poll. Use 'specialists feed --follow' for live human-readable output.
21714
+ `);
21715
+ process.exit(1);
20695
21716
  }
20696
- }
20697
- if (!jobId) {
20698
- console.error("Usage: specialists poll <job-id> [--cursor N] [--json]");
20699
- process.exit(1);
20700
- }
20701
- return { jobId, cursor, json };
20702
- }
20703
- async function run12() {
20704
- const { jobId, cursor, json } = parseArgs7(process.argv.slice(3));
20705
- const jobsDir = join16(process.cwd(), ".specialists", "jobs");
20706
- const jobDir = join16(jobsDir, jobId);
20707
- if (!existsSync13(jobDir)) {
20708
- const result2 = {
20709
- job_id: jobId,
20710
- status: "error",
20711
- elapsed_ms: 0,
20712
- cursor: 0,
20713
- output: "",
20714
- output_delta: "",
20715
- events: [],
20716
- error: `Job not found: ${jobId}`
20717
- };
20718
- console.log(JSON.stringify(result2));
21717
+ if (!argv[i].startsWith("-")) {
21718
+ jobId = argv[i];
21719
+ }
21720
+ }
21721
+ if (!jobId) {
21722
+ console.error("Usage: specialists poll <job-id> [--cursor N] [--output-cursor N]");
20719
21723
  process.exit(1);
20720
21724
  }
20721
- const statusPath = join16(jobDir, "status.json");
21725
+ return { jobId, cursor, outputCursor };
21726
+ }
21727
+ function readJobState(jobsDir, jobId, cursor, outputCursor) {
21728
+ const jobDir = join21(jobsDir, jobId);
21729
+ const statusPath = join21(jobDir, "status.json");
20722
21730
  let status = null;
20723
- if (existsSync13(statusPath)) {
21731
+ if (existsSync16(statusPath)) {
20724
21732
  try {
20725
- status = JSON.parse(readFileSync7(statusPath, "utf-8"));
21733
+ status = JSON.parse(readFileSync11(statusPath, "utf-8"));
20726
21734
  } catch {}
20727
21735
  }
20728
- const resultPath = join16(jobDir, "result.txt");
20729
- let output = "";
20730
- if (existsSync13(resultPath)) {
21736
+ const resultPath = join21(jobDir, "result.txt");
21737
+ let fullOutput = "";
21738
+ if (existsSync16(resultPath)) {
20731
21739
  try {
20732
- output = readFileSync7(resultPath, "utf-8");
21740
+ fullOutput = readFileSync11(resultPath, "utf-8");
20733
21741
  } catch {}
20734
21742
  }
20735
21743
  const events = readJobEventsById(jobsDir, jobId);
@@ -20738,13 +21746,18 @@ async function run12() {
20738
21746
  const startedAt = status?.started_at_ms ?? Date.now();
20739
21747
  const lastEvent = status?.last_event_at_ms ?? Date.now();
20740
21748
  const elapsedMs = status?.status === "done" || status?.status === "error" ? lastEvent - startedAt : Date.now() - startedAt;
20741
- const result = {
21749
+ const isDone = status?.status === "done";
21750
+ const output = isDone ? fullOutput : "";
21751
+ const outputDelta = fullOutput.length > outputCursor ? fullOutput.slice(outputCursor) : "";
21752
+ const nextOutputCursor = fullOutput.length;
21753
+ return {
20742
21754
  job_id: jobId,
20743
21755
  status: status?.status ?? "starting",
20744
21756
  elapsed_ms: elapsedMs,
20745
21757
  cursor: nextCursor,
20746
- output: status?.status === "done" ? output : "",
20747
- output_delta: "",
21758
+ output_cursor: nextOutputCursor,
21759
+ output,
21760
+ output_delta: outputDelta,
20748
21761
  events: newEvents,
20749
21762
  current_event: status?.current_event,
20750
21763
  current_tool: status?.current_tool,
@@ -20753,27 +21766,32 @@ async function run12() {
20753
21766
  bead_id: status?.bead_id,
20754
21767
  error: status?.error
20755
21768
  };
20756
- if (json) {
20757
- console.log(JSON.stringify(result, null, 2));
20758
- } else {
20759
- console.log(`Job: ${jobId}`);
20760
- console.log(`Status: ${result.status}`);
20761
- console.log(`Elapsed: ${Math.round(result.elapsed_ms / 1000)}s`);
20762
- if (result.model)
20763
- console.log(`Model: ${result.backend}/${result.model}`);
20764
- if (result.bead_id)
20765
- console.log(`Bead: ${result.bead_id}`);
20766
- if (result.current_tool)
20767
- console.log(`Tool: ${result.current_tool}`);
20768
- if (result.error)
20769
- console.log(`Error: ${result.error}`);
20770
- console.log(`Events: ${newEvents.length} new (cursor: ${cursor} → ${nextCursor})`);
20771
- if (result.status === "done" && result.output) {
20772
- console.log(`
20773
- --- Output ---`);
20774
- console.log(result.output.slice(0, 500) + (result.output.length > 500 ? "..." : ""));
20775
- }
21769
+ }
21770
+ async function run13() {
21771
+ const { jobId, cursor, outputCursor } = parseArgs9(process.argv.slice(3));
21772
+ const jobsDir = join21(process.cwd(), ".specialists", "jobs");
21773
+ const jobDir = join21(jobsDir, jobId);
21774
+ if (!existsSync16(jobDir)) {
21775
+ const result2 = {
21776
+ job_id: jobId,
21777
+ status: "error",
21778
+ elapsed_ms: 0,
21779
+ cursor: 0,
21780
+ output_cursor: 0,
21781
+ output: "",
21782
+ output_delta: "",
21783
+ events: [],
21784
+ error: `Job not found: ${jobId}`
21785
+ };
21786
+ console.log(JSON.stringify(result2));
21787
+ process.exit(1);
20776
21788
  }
21789
+ const result = readJobState(jobsDir, jobId, cursor, outputCursor);
21790
+ if (result.status !== "done" && result.status !== "error" && !result.output_delta) {
21791
+ process.stderr.write(`Tip: use 'specialists feed --follow' for live human-readable output.
21792
+ `);
21793
+ }
21794
+ console.log(JSON.stringify(result));
20777
21795
  }
20778
21796
  var init_poll = __esm(() => {
20779
21797
  init_timeline_query();
@@ -20782,18 +21800,18 @@ var init_poll = __esm(() => {
20782
21800
  // src/cli/steer.ts
20783
21801
  var exports_steer = {};
20784
21802
  __export(exports_steer, {
20785
- run: () => run13
21803
+ run: () => run14
20786
21804
  });
20787
- import { join as join17 } from "node:path";
21805
+ import { join as join22 } from "node:path";
20788
21806
  import { writeFileSync as writeFileSync6 } from "node:fs";
20789
- async function run13() {
21807
+ async function run14() {
20790
21808
  const jobId = process.argv[3];
20791
21809
  const message = process.argv[4];
20792
21810
  if (!jobId || !message) {
20793
21811
  console.error('Usage: specialists|sp steer <job-id> "<message>"');
20794
21812
  process.exit(1);
20795
21813
  }
20796
- const jobsDir = join17(process.cwd(), ".specialists", "jobs");
21814
+ const jobsDir = join22(process.cwd(), ".specialists", "jobs");
20797
21815
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
20798
21816
  const status = supervisor.readStatus(jobId);
20799
21817
  if (!status) {
@@ -20808,7 +21826,7 @@ async function run13() {
20808
21826
  if (!status.fifo_path) {
20809
21827
  process.stderr.write(`${red4("Error:")} Job ${jobId} has no steer pipe.
20810
21828
  `);
20811
- process.stderr.write(`Only jobs started with --background support mid-run steering.
21829
+ process.stderr.write(`FIFO support may not be available on this system (mkfifo failed at job start).
20812
21830
  `);
20813
21831
  process.exit(1);
20814
21832
  }
@@ -20816,7 +21834,7 @@ async function run13() {
20816
21834
  const payload = JSON.stringify({ type: "steer", message }) + `
20817
21835
  `;
20818
21836
  writeFileSync6(status.fifo_path, payload, { flag: "a" });
20819
- process.stdout.write(`${green8("✓")} Steer message sent to job ${jobId}
21837
+ process.stdout.write(`${green9("✓")} Steer message sent to job ${jobId}
20820
21838
  `);
20821
21839
  } catch (err) {
20822
21840
  process.stderr.write(`${red4("Error:")} Failed to write to steer pipe: ${err?.message}
@@ -20824,26 +21842,26 @@ async function run13() {
20824
21842
  process.exit(1);
20825
21843
  }
20826
21844
  }
20827
- var green8 = (s) => `\x1B[32m${s}\x1B[0m`, red4 = (s) => `\x1B[31m${s}\x1B[0m`;
21845
+ var green9 = (s) => `\x1B[32m${s}\x1B[0m`, red4 = (s) => `\x1B[31m${s}\x1B[0m`;
20828
21846
  var init_steer = __esm(() => {
20829
21847
  init_supervisor();
20830
21848
  });
20831
21849
 
20832
- // src/cli/follow-up.ts
20833
- var exports_follow_up = {};
20834
- __export(exports_follow_up, {
20835
- run: () => run14
21850
+ // src/cli/resume.ts
21851
+ var exports_resume = {};
21852
+ __export(exports_resume, {
21853
+ run: () => run15
20836
21854
  });
20837
- import { join as join18 } from "node:path";
21855
+ import { join as join23 } from "node:path";
20838
21856
  import { writeFileSync as writeFileSync7 } from "node:fs";
20839
- async function run14() {
21857
+ async function run15() {
20840
21858
  const jobId = process.argv[3];
20841
- const message = process.argv[4];
20842
- if (!jobId || !message) {
20843
- console.error('Usage: specialists|sp follow-up <job-id> "<message>"');
21859
+ const task = process.argv[4];
21860
+ if (!jobId || !task) {
21861
+ console.error('Usage: specialists|sp resume <job-id> "<task>"');
20844
21862
  process.exit(1);
20845
21863
  }
20846
- const jobsDir = join18(process.cwd(), ".specialists", "jobs");
21864
+ const jobsDir = join23(process.cwd(), ".specialists", "jobs");
20847
21865
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
20848
21866
  const status = supervisor.readStatus(jobId);
20849
21867
  if (!status) {
@@ -20853,7 +21871,7 @@ async function run14() {
20853
21871
  if (status.status !== "waiting") {
20854
21872
  process.stderr.write(`${red5("Error:")} Job ${jobId} is not in waiting state (status: ${status.status}).
20855
21873
  `);
20856
- process.stderr.write(`Only jobs started with --keep-alive and --background support follow-up prompts.
21874
+ process.stderr.write(`resume is only valid for keep-alive jobs in waiting state. Use steer for running jobs.
20857
21875
  `);
20858
21876
  process.exit(1);
20859
21877
  }
@@ -20863,10 +21881,10 @@ async function run14() {
20863
21881
  process.exit(1);
20864
21882
  }
20865
21883
  try {
20866
- const payload = JSON.stringify({ type: "prompt", message }) + `
21884
+ const payload = JSON.stringify({ type: "resume", task }) + `
20867
21885
  `;
20868
21886
  writeFileSync7(status.fifo_path, payload, { flag: "a" });
20869
- process.stdout.write(`${green9("✓")} Follow-up sent to job ${jobId}
21887
+ process.stdout.write(`${green10("✓")} Resume sent to job ${jobId}
20870
21888
  `);
20871
21889
  process.stdout.write(` Use 'specialists feed ${jobId} --follow' to watch the response.
20872
21890
  `);
@@ -20876,24 +21894,226 @@ async function run14() {
20876
21894
  process.exit(1);
20877
21895
  }
20878
21896
  }
20879
- var green9 = (s) => `\x1B[32m${s}\x1B[0m`, red5 = (s) => `\x1B[31m${s}\x1B[0m`;
20880
- var init_follow_up = __esm(() => {
21897
+ var green10 = (s) => `\x1B[32m${s}\x1B[0m`, red5 = (s) => `\x1B[31m${s}\x1B[0m`;
21898
+ var init_resume = __esm(() => {
20881
21899
  init_supervisor();
20882
21900
  });
20883
21901
 
21902
+ // src/cli/follow-up.ts
21903
+ var exports_follow_up = {};
21904
+ __export(exports_follow_up, {
21905
+ run: () => run16
21906
+ });
21907
+ async function run16() {
21908
+ process.stderr.write("\x1B[33m⚠ DEPRECATED:\x1B[0m `specialists follow-up` is deprecated. Use `specialists resume` instead.\n\n");
21909
+ const { run: resumeRun } = await Promise.resolve().then(() => (init_resume(), exports_resume));
21910
+ return resumeRun();
21911
+ }
21912
+
21913
+ // src/cli/clean.ts
21914
+ var exports_clean = {};
21915
+ __export(exports_clean, {
21916
+ run: () => run17
21917
+ });
21918
+ import {
21919
+ existsSync as existsSync17,
21920
+ readdirSync as readdirSync5,
21921
+ readFileSync as readFileSync12,
21922
+ rmSync as rmSync2,
21923
+ statSync as statSync2
21924
+ } from "node:fs";
21925
+ import { join as join24 } from "node:path";
21926
+ function parseTtlDaysFromEnvironment() {
21927
+ const rawValue = process.env.SPECIALISTS_JOB_TTL_DAYS ?? process.env.JOB_TTL_DAYS;
21928
+ if (!rawValue)
21929
+ return DEFAULT_TTL_DAYS;
21930
+ const parsedValue = Number(rawValue);
21931
+ if (!Number.isFinite(parsedValue) || parsedValue < 0)
21932
+ return DEFAULT_TTL_DAYS;
21933
+ return parsedValue;
21934
+ }
21935
+ function parseOptions(argv) {
21936
+ let removeAllCompleted = false;
21937
+ let dryRun = false;
21938
+ let keepRecentCount = null;
21939
+ for (let index = 0;index < argv.length; index += 1) {
21940
+ const argument = argv[index];
21941
+ if (argument === "--all") {
21942
+ removeAllCompleted = true;
21943
+ continue;
21944
+ }
21945
+ if (argument === "--dry-run") {
21946
+ dryRun = true;
21947
+ continue;
21948
+ }
21949
+ if (argument === "--keep") {
21950
+ const value = argv[index + 1];
21951
+ if (!value) {
21952
+ throw new Error("Missing value for --keep");
21953
+ }
21954
+ const parsedValue = Number(value);
21955
+ const isInteger = Number.isInteger(parsedValue);
21956
+ if (!isInteger || parsedValue < 0) {
21957
+ throw new Error("--keep must be a non-negative integer");
21958
+ }
21959
+ keepRecentCount = parsedValue;
21960
+ index += 1;
21961
+ continue;
21962
+ }
21963
+ if (argument.startsWith("--keep=")) {
21964
+ const value = argument.slice("--keep=".length);
21965
+ const parsedValue = Number(value);
21966
+ const isInteger = Number.isInteger(parsedValue);
21967
+ if (!isInteger || parsedValue < 0) {
21968
+ throw new Error("--keep must be a non-negative integer");
21969
+ }
21970
+ keepRecentCount = parsedValue;
21971
+ continue;
21972
+ }
21973
+ throw new Error(`Unknown option: ${argument}`);
21974
+ }
21975
+ return { removeAllCompleted, dryRun, keepRecentCount };
21976
+ }
21977
+ function readDirectorySizeBytes(directoryPath) {
21978
+ let totalBytes = 0;
21979
+ const entries = readdirSync5(directoryPath, { withFileTypes: true });
21980
+ for (const entry of entries) {
21981
+ const entryPath = join24(directoryPath, entry.name);
21982
+ const stats = statSync2(entryPath);
21983
+ if (stats.isDirectory()) {
21984
+ totalBytes += readDirectorySizeBytes(entryPath);
21985
+ continue;
21986
+ }
21987
+ totalBytes += stats.size;
21988
+ }
21989
+ return totalBytes;
21990
+ }
21991
+ function readCompletedJobDirectory(baseDirectory, entry) {
21992
+ if (!entry.isDirectory())
21993
+ return null;
21994
+ const directoryPath = join24(baseDirectory, entry.name);
21995
+ const statusFilePath = join24(directoryPath, "status.json");
21996
+ if (!existsSync17(statusFilePath))
21997
+ return null;
21998
+ let statusData;
21999
+ try {
22000
+ statusData = JSON.parse(readFileSync12(statusFilePath, "utf-8"));
22001
+ } catch {
22002
+ return null;
22003
+ }
22004
+ if (!COMPLETED_STATUSES.has(statusData.status))
22005
+ return null;
22006
+ const directoryStats = statSync2(directoryPath);
22007
+ return {
22008
+ id: entry.name,
22009
+ directoryPath,
22010
+ modifiedAtMs: directoryStats.mtimeMs,
22011
+ startedAtMs: statusData.started_at_ms,
22012
+ sizeBytes: readDirectorySizeBytes(directoryPath)
22013
+ };
22014
+ }
22015
+ function collectCompletedJobDirectories(jobsDirectoryPath) {
22016
+ const entries = readdirSync5(jobsDirectoryPath, { withFileTypes: true });
22017
+ const completedJobs = [];
22018
+ for (const entry of entries) {
22019
+ const completedJob = readCompletedJobDirectory(jobsDirectoryPath, entry);
22020
+ if (completedJob) {
22021
+ completedJobs.push(completedJob);
22022
+ }
22023
+ }
22024
+ return completedJobs;
22025
+ }
22026
+ function selectJobsToRemove(completedJobs, options) {
22027
+ const jobsByNewest = [...completedJobs].sort((left, right) => {
22028
+ if (right.startedAtMs !== left.startedAtMs) {
22029
+ return right.startedAtMs - left.startedAtMs;
22030
+ }
22031
+ return right.modifiedAtMs - left.modifiedAtMs;
22032
+ });
22033
+ if (options.keepRecentCount !== null) {
22034
+ return jobsByNewest.slice(options.keepRecentCount);
22035
+ }
22036
+ if (options.removeAllCompleted) {
22037
+ return jobsByNewest;
22038
+ }
22039
+ const ttlDays = parseTtlDaysFromEnvironment();
22040
+ const cutoffMs = Date.now() - ttlDays * MS_PER_DAY;
22041
+ return jobsByNewest.filter((job) => job.modifiedAtMs < cutoffMs);
22042
+ }
22043
+ function formatBytes(bytes) {
22044
+ if (bytes < 1024)
22045
+ return `${bytes} B`;
22046
+ const units = ["KB", "MB", "GB", "TB"];
22047
+ let value = bytes / 1024;
22048
+ let unitIndex = 0;
22049
+ while (value >= 1024 && unitIndex < units.length - 1) {
22050
+ value /= 1024;
22051
+ unitIndex += 1;
22052
+ }
22053
+ return `${value.toFixed(1)} ${units[unitIndex]}`;
22054
+ }
22055
+ function renderSummary(removedCount, freedBytes, dryRun) {
22056
+ const action = dryRun ? "Would remove" : "Removed";
22057
+ const noun = removedCount === 1 ? "directory" : "directories";
22058
+ return `${action} ${removedCount} job ${noun} (${formatBytes(freedBytes)} freed)`;
22059
+ }
22060
+ function printDryRunPlan(jobs) {
22061
+ if (jobs.length === 0)
22062
+ return;
22063
+ console.log("Would remove:");
22064
+ for (const job of jobs) {
22065
+ console.log(` - ${job.id}`);
22066
+ }
22067
+ }
22068
+ function printUsageAndExit(message) {
22069
+ console.error(message);
22070
+ console.error("Usage: specialists|sp clean [--all] [--keep <n>] [--dry-run]");
22071
+ process.exit(1);
22072
+ }
22073
+ async function run17() {
22074
+ let options;
22075
+ try {
22076
+ options = parseOptions(process.argv.slice(3));
22077
+ } catch (error2) {
22078
+ const message = error2 instanceof Error ? error2.message : String(error2);
22079
+ printUsageAndExit(message);
22080
+ }
22081
+ const jobsDirectoryPath = join24(process.cwd(), ".specialists", "jobs");
22082
+ if (!existsSync17(jobsDirectoryPath)) {
22083
+ console.log("No jobs directory found.");
22084
+ return;
22085
+ }
22086
+ const completedJobs = collectCompletedJobDirectories(jobsDirectoryPath);
22087
+ const jobsToRemove = selectJobsToRemove(completedJobs, options);
22088
+ const freedBytes = jobsToRemove.reduce((total, job) => total + job.sizeBytes, 0);
22089
+ if (options.dryRun) {
22090
+ printDryRunPlan(jobsToRemove);
22091
+ console.log(renderSummary(jobsToRemove.length, freedBytes, true));
22092
+ return;
22093
+ }
22094
+ for (const job of jobsToRemove) {
22095
+ rmSync2(job.directoryPath, { recursive: true, force: true });
22096
+ }
22097
+ console.log(renderSummary(jobsToRemove.length, freedBytes, false));
22098
+ }
22099
+ var MS_PER_DAY = 86400000, DEFAULT_TTL_DAYS = 7, COMPLETED_STATUSES;
22100
+ var init_clean = __esm(() => {
22101
+ COMPLETED_STATUSES = new Set(["done", "error"]);
22102
+ });
22103
+
20884
22104
  // src/cli/stop.ts
20885
22105
  var exports_stop = {};
20886
22106
  __export(exports_stop, {
20887
- run: () => run15
22107
+ run: () => run18
20888
22108
  });
20889
- import { join as join19 } from "node:path";
20890
- async function run15() {
22109
+ import { join as join25 } from "node:path";
22110
+ async function run18() {
20891
22111
  const jobId = process.argv[3];
20892
22112
  if (!jobId) {
20893
22113
  console.error("Usage: specialists|sp stop <job-id>");
20894
22114
  process.exit(1);
20895
22115
  }
20896
- const jobsDir = join19(process.cwd(), ".specialists", "jobs");
22116
+ const jobsDir = join25(process.cwd(), ".specialists", "jobs");
20897
22117
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
20898
22118
  const status = supervisor.readStatus(jobId);
20899
22119
  if (!status) {
@@ -20912,7 +22132,7 @@ async function run15() {
20912
22132
  }
20913
22133
  try {
20914
22134
  process.kill(status.pid, "SIGTERM");
20915
- process.stdout.write(`${green10("✓")} Sent SIGTERM to PID ${status.pid} (job ${jobId})
22135
+ process.stdout.write(`${green11("✓")} Sent SIGTERM to PID ${status.pid} (job ${jobId})
20916
22136
  `);
20917
22137
  } catch (err) {
20918
22138
  if (err.code === "ESRCH") {
@@ -20925,7 +22145,7 @@ async function run15() {
20925
22145
  }
20926
22146
  }
20927
22147
  }
20928
- var green10 = (s) => `\x1B[32m${s}\x1B[0m`, red6 = (s) => `\x1B[31m${s}\x1B[0m`, dim10 = (s) => `\x1B[2m${s}\x1B[0m`;
22148
+ var green11 = (s) => `\x1B[32m${s}\x1B[0m`, red6 = (s) => `\x1B[31m${s}\x1B[0m`, dim10 = (s) => `\x1B[2m${s}\x1B[0m`;
20929
22149
  var init_stop = __esm(() => {
20930
22150
  init_supervisor();
20931
22151
  });
@@ -20933,7 +22153,7 @@ var init_stop = __esm(() => {
20933
22153
  // src/cli/quickstart.ts
20934
22154
  var exports_quickstart = {};
20935
22155
  __export(exports_quickstart, {
20936
- run: () => run16
22156
+ run: () => run19
20937
22157
  });
20938
22158
  function section2(title) {
20939
22159
  const bar = "─".repeat(60);
@@ -20942,12 +22162,12 @@ ${bold9(cyan6(title))}
20942
22162
  ${dim11(bar)}`;
20943
22163
  }
20944
22164
  function cmd2(s) {
20945
- return yellow8(s);
22165
+ return yellow9(s);
20946
22166
  }
20947
22167
  function flag(s) {
20948
- return green11(s);
22168
+ return green12(s);
20949
22169
  }
20950
- async function run16() {
22170
+ async function run19() {
20951
22171
  const lines = [
20952
22172
  "",
20953
22173
  bold9("specialists · Quick Start Guide"),
@@ -20967,11 +22187,12 @@ async function run16() {
20967
22187
  lines.push(section2("2. Initialize a Project"));
20968
22188
  lines.push("");
20969
22189
  lines.push(` Run once per project root:`);
20970
- lines.push(` ${cmd2("specialists init")} # creates specialists/, .specialists/, AGENTS.md`);
22190
+ lines.push(` ${cmd2("specialists init")} # creates .specialists/, wires MCP + AGENTS.md`);
20971
22191
  lines.push("");
20972
22192
  lines.push(` What this creates:`);
20973
- lines.push(` ${dim11("specialists/")} put your .specialist.yaml files here`);
20974
- lines.push(` ${dim11(".specialists/")} runtime data (jobs/, ready/) — gitignored`);
22193
+ lines.push(` ${dim11(".specialists/default/")} canonical specialists (from init)`);
22194
+ lines.push(` ${dim11(".specialists/user/")} custom .specialist.yaml files`);
22195
+ lines.push(` ${dim11(".specialists/jobs|ready")} — runtime data — gitignored`);
20975
22196
  lines.push(` ${dim11("AGENTS.md")} — context block injected into Claude sessions`);
20976
22197
  lines.push("");
20977
22198
  lines.push(section2("3. Discover Specialists"));
@@ -20983,8 +22204,8 @@ async function run16() {
20983
22204
  lines.push(` ${cmd2("specialists list")} ${flag("--json")} # machine-readable JSON`);
20984
22205
  lines.push("");
20985
22206
  lines.push(` Scopes (searched in order, user wins on name collision):`);
20986
- lines.push(` ${blue2("user")} .specialists/user/specialists/*.specialist.yaml`);
20987
- lines.push(` ${blue2("default")} .specialists/default/specialists/*.specialist.yaml`);
22207
+ lines.push(` ${blue2("user")} .specialists/user/*.specialist.yaml`);
22208
+ lines.push(` ${blue2("default")} .specialists/default/*.specialist.yaml`);
20988
22209
  lines.push("");
20989
22210
  lines.push(section2("4. Running a Specialist"));
20990
22211
  lines.push("");
@@ -21004,9 +22225,11 @@ async function run16() {
21004
22225
  lines.push(` Pipe a prompt from stdin:`);
21005
22226
  lines.push(` ${cmd2("cat my-brief.md | specialists run code-review")}`);
21006
22227
  lines.push("");
21007
- lines.push(section2("5. Background Job Lifecycle"));
22228
+ lines.push(section2("5. Async Job Lifecycle"));
21008
22229
  lines.push("");
21009
- lines.push(` Use Claude Code's native backgrounding or run in a separate terminal.`);
22230
+ lines.push(` ${bold9("MCP pattern")}: ${cmd2("start_specialist")} ${cmd2("feed_specialist")} (returns job_id directly)`);
22231
+ lines.push(` ${bold9("CLI pattern")}: ${cmd2('specialists run <name> --prompt "..."')} prints ${dim11("[job started: <id>]")} to stderr`);
22232
+ lines.push(` ${bold9("Shell pattern")}: ${cmd2('specialists run <name> --prompt "..." &')} for native backgrounding`);
21010
22233
  lines.push("");
21011
22234
  lines.push(` ${bold9("Watch progress")} — stream events as they arrive:`);
21012
22235
  lines.push(` ${cmd2("specialists feed job_a1b2c3d4")} # print events so far`);
@@ -21100,10 +22323,10 @@ async function run16() {
21100
22323
  lines.push(` Specialists emits lifecycle events to ${dim11(".specialists/trace.jsonl")}:`);
21101
22324
  lines.push("");
21102
22325
  lines.push(` ${bold9("Hook point")} ${bold9("When fired")}`);
21103
- lines.push(` ${yellow8("specialist:start")} before the agent session begins`);
21104
- lines.push(` ${yellow8("specialist:token")} on each streamed token (delta)`);
21105
- lines.push(` ${yellow8("specialist:done")} after successful completion`);
21106
- lines.push(` ${yellow8("specialist:error")} on failure or timeout`);
22326
+ lines.push(` ${yellow9("specialist:start")} before the agent session begins`);
22327
+ lines.push(` ${yellow9("specialist:token")} on each streamed token (delta)`);
22328
+ lines.push(` ${yellow9("specialist:done")} after successful completion`);
22329
+ lines.push(` ${yellow9("specialist:error")} on failure or timeout`);
21107
22330
  lines.push("");
21108
22331
  lines.push(` Each event line in trace.jsonl:`);
21109
22332
  lines.push(` ${dim11('{"t":"<ISO>","hook":"specialist:done","specialist":"code-review","durationMs":4120}')}`);
@@ -21120,9 +22343,9 @@ async function run16() {
21120
22343
  lines.push(` ${bold9("use_specialist")} — full lifecycle: load → agents.md → run → output`);
21121
22344
  lines.push(` ${bold9("run_parallel")} — concurrent or pipeline execution`);
21122
22345
  lines.push(` ${bold9("start_specialist")} — async job start, returns job ID`);
21123
- lines.push(` ${bold9("poll_specialist")} — poll job status/output by ID`);
22346
+ lines.push(` ${bold9("feed_specialist")} — stream events/output by job ID`);
21124
22347
  lines.push(` ${bold9("steer_specialist")} — send a mid-run message to a running job`);
21125
- lines.push(` ${bold9("follow_up_specialist")} send a next-turn prompt to a keep-alive session`);
22348
+ lines.push(` ${bold9("resume_specialist")} resume a waiting keep-alive session with a next-turn prompt`);
21126
22349
  lines.push(` ${bold9("stop_specialist")} — cancel a running job by ID`);
21127
22350
  lines.push(` ${bold9("specialist_status")} — circuit breaker health + staleness`);
21128
22351
  lines.push("");
@@ -21155,27 +22378,27 @@ async function run16() {
21155
22378
  console.log(lines.join(`
21156
22379
  `));
21157
22380
  }
21158
- var bold9 = (s) => `\x1B[1m${s}\x1B[0m`, dim11 = (s) => `\x1B[2m${s}\x1B[0m`, yellow8 = (s) => `\x1B[33m${s}\x1B[0m`, cyan6 = (s) => `\x1B[36m${s}\x1B[0m`, blue2 = (s) => `\x1B[34m${s}\x1B[0m`, green11 = (s) => `\x1B[32m${s}\x1B[0m`;
22381
+ var bold9 = (s) => `\x1B[1m${s}\x1B[0m`, dim11 = (s) => `\x1B[2m${s}\x1B[0m`, yellow9 = (s) => `\x1B[33m${s}\x1B[0m`, cyan6 = (s) => `\x1B[36m${s}\x1B[0m`, blue2 = (s) => `\x1B[34m${s}\x1B[0m`, green12 = (s) => `\x1B[32m${s}\x1B[0m`;
21159
22382
 
21160
22383
  // src/cli/doctor.ts
21161
22384
  var exports_doctor = {};
21162
22385
  __export(exports_doctor, {
21163
- run: () => run17
22386
+ run: () => run20
21164
22387
  });
21165
- import { spawnSync as spawnSync7 } from "node:child_process";
21166
- import { existsSync as existsSync14, mkdirSync as mkdirSync3, readFileSync as readFileSync8, readdirSync as readdirSync4 } from "node:fs";
21167
- import { join as join20 } from "node:path";
22388
+ import { spawnSync as spawnSync8 } from "node:child_process";
22389
+ import { existsSync as existsSync18, mkdirSync as mkdirSync3, readFileSync as readFileSync13, readdirSync as readdirSync6 } from "node:fs";
22390
+ import { join as join26 } from "node:path";
21168
22391
  function ok3(msg) {
21169
- console.log(` ${green12("✓")} ${msg}`);
22392
+ console.log(` ${green13("✓")} ${msg}`);
21170
22393
  }
21171
22394
  function warn2(msg) {
21172
- console.log(` ${yellow9("○")} ${msg}`);
22395
+ console.log(` ${yellow10("○")} ${msg}`);
21173
22396
  }
21174
22397
  function fail2(msg) {
21175
22398
  console.log(` ${red7("✗")} ${msg}`);
21176
22399
  }
21177
22400
  function fix(msg) {
21178
- console.log(` ${dim12("→ fix:")} ${yellow9(msg)}`);
22401
+ console.log(` ${dim12("→ fix:")} ${yellow10(msg)}`);
21179
22402
  }
21180
22403
  function hint(msg) {
21181
22404
  console.log(` ${dim12(msg)}`);
@@ -21186,17 +22409,17 @@ function section3(label) {
21186
22409
  ${bold10(`── ${label} ${line}`)}`);
21187
22410
  }
21188
22411
  function sp(bin, args) {
21189
- const r = spawnSync7(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
22412
+ const r = spawnSync8(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
21190
22413
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
21191
22414
  }
21192
22415
  function isInstalled2(bin) {
21193
- return spawnSync7("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
22416
+ return spawnSync8("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
21194
22417
  }
21195
22418
  function loadJson2(path) {
21196
- if (!existsSync14(path))
22419
+ if (!existsSync18(path))
21197
22420
  return null;
21198
22421
  try {
21199
- return JSON.parse(readFileSync8(path, "utf8"));
22422
+ return JSON.parse(readFileSync13(path, "utf8"));
21200
22423
  } catch {
21201
22424
  return null;
21202
22425
  }
@@ -21239,7 +22462,7 @@ function checkBd() {
21239
22462
  return false;
21240
22463
  }
21241
22464
  ok3(`bd installed ${dim12(sp("bd", ["--version"]).stdout || "")}`);
21242
- if (existsSync14(join20(CWD, ".beads")))
22465
+ if (existsSync18(join26(CWD, ".beads")))
21243
22466
  ok3(".beads/ present in project");
21244
22467
  else
21245
22468
  warn2(".beads/ not found in project");
@@ -21259,8 +22482,8 @@ function checkHooks() {
21259
22482
  section3("Claude Code hooks (2 expected)");
21260
22483
  let allPresent = true;
21261
22484
  for (const name of HOOK_NAMES) {
21262
- const dest = join20(HOOKS_DIR, name);
21263
- if (!existsSync14(dest)) {
22485
+ const dest = join26(HOOKS_DIR, name);
22486
+ if (!existsSync18(dest)) {
21264
22487
  fail2(`${name} ${red7("missing")}`);
21265
22488
  fix("specialists install");
21266
22489
  allPresent = false;
@@ -21304,18 +22527,18 @@ function checkMCP() {
21304
22527
  }
21305
22528
  function checkRuntimeDirs() {
21306
22529
  section3(".specialists/ runtime directories");
21307
- const rootDir = join20(CWD, ".specialists");
21308
- const jobsDir = join20(rootDir, "jobs");
21309
- const readyDir = join20(rootDir, "ready");
22530
+ const rootDir = join26(CWD, ".specialists");
22531
+ const jobsDir = join26(rootDir, "jobs");
22532
+ const readyDir = join26(rootDir, "ready");
21310
22533
  let allOk = true;
21311
- if (!existsSync14(rootDir)) {
22534
+ if (!existsSync18(rootDir)) {
21312
22535
  warn2(".specialists/ not found in current project");
21313
22536
  fix("specialists init");
21314
22537
  allOk = false;
21315
22538
  } else {
21316
22539
  ok3(".specialists/ present");
21317
22540
  for (const [subDir, label] of [[jobsDir, "jobs"], [readyDir, "ready"]]) {
21318
- if (!existsSync14(subDir)) {
22541
+ if (!existsSync18(subDir)) {
21319
22542
  warn2(`.specialists/${label}/ missing — auto-creating`);
21320
22543
  mkdirSync3(subDir, { recursive: true });
21321
22544
  ok3(`.specialists/${label}/ created`);
@@ -21328,14 +22551,14 @@ function checkRuntimeDirs() {
21328
22551
  }
21329
22552
  function checkZombieJobs() {
21330
22553
  section3("Background jobs");
21331
- const jobsDir = join20(CWD, ".specialists", "jobs");
21332
- if (!existsSync14(jobsDir)) {
22554
+ const jobsDir = join26(CWD, ".specialists", "jobs");
22555
+ if (!existsSync18(jobsDir)) {
21333
22556
  hint("No .specialists/jobs/ — skipping");
21334
22557
  return true;
21335
22558
  }
21336
22559
  let entries;
21337
22560
  try {
21338
- entries = readdirSync4(jobsDir);
22561
+ entries = readdirSync6(jobsDir);
21339
22562
  } catch {
21340
22563
  entries = [];
21341
22564
  }
@@ -21347,11 +22570,11 @@ function checkZombieJobs() {
21347
22570
  let total = 0;
21348
22571
  let running = 0;
21349
22572
  for (const jobId of entries) {
21350
- const statusPath = join20(jobsDir, jobId, "status.json");
21351
- if (!existsSync14(statusPath))
22573
+ const statusPath = join26(jobsDir, jobId, "status.json");
22574
+ if (!existsSync18(statusPath))
21352
22575
  continue;
21353
22576
  try {
21354
- const status = JSON.parse(readFileSync8(statusPath, "utf8"));
22577
+ const status = JSON.parse(readFileSync13(statusPath, "utf8"));
21355
22578
  total++;
21356
22579
  if (status.status === "running" || status.status === "starting") {
21357
22580
  const pid = status.pid;
@@ -21365,7 +22588,7 @@ function checkZombieJobs() {
21365
22588
  running++;
21366
22589
  else {
21367
22590
  zombies++;
21368
- warn2(`${jobId} ${yellow9("ZOMBIE")} ${dim12(`pid ${pid} not found, status=${status.status}`)}`);
22591
+ warn2(`${jobId} ${yellow10("ZOMBIE")} ${dim12(`pid ${pid} not found, status=${status.status}`)}`);
21369
22592
  fix(`Edit .specialists/jobs/${jobId}/status.json → set "status": "error"`);
21370
22593
  }
21371
22594
  }
@@ -21378,7 +22601,7 @@ function checkZombieJobs() {
21378
22601
  }
21379
22602
  return zombies === 0;
21380
22603
  }
21381
- async function run17() {
22604
+ async function run20() {
21382
22605
  console.log(`
21383
22606
  ${bold10("specialists doctor")}
21384
22607
  `);
@@ -21393,21 +22616,21 @@ ${bold10("specialists doctor")}
21393
22616
  const allOk = piOk && bdOk && xtOk && hooksOk && mcpOk && dirsOk && jobsOk;
21394
22617
  console.log("");
21395
22618
  if (allOk) {
21396
- console.log(` ${green12("✓")} ${bold10("All checks passed")} — specialists is healthy`);
22619
+ console.log(` ${green13("✓")} ${bold10("All checks passed")} — specialists is healthy`);
21397
22620
  } else {
21398
- console.log(` ${yellow9("○")} ${bold10("Some checks failed")} — follow the fix hints above`);
22621
+ console.log(` ${yellow10("○")} ${bold10("Some checks failed")} — follow the fix hints above`);
21399
22622
  console.log(` ${dim12("specialists install fixes hook + MCP registration; pi, bd, and xt must be installed separately.")}`);
21400
22623
  }
21401
22624
  console.log("");
21402
22625
  }
21403
- var bold10 = (s) => `\x1B[1m${s}\x1B[0m`, dim12 = (s) => `\x1B[2m${s}\x1B[0m`, green12 = (s) => `\x1B[32m${s}\x1B[0m`, yellow9 = (s) => `\x1B[33m${s}\x1B[0m`, red7 = (s) => `\x1B[31m${s}\x1B[0m`, CWD, CLAUDE_DIR, SPECIALISTS_DIR, HOOKS_DIR, SETTINGS_FILE, MCP_FILE2, HOOK_NAMES;
22626
+ var bold10 = (s) => `\x1B[1m${s}\x1B[0m`, dim12 = (s) => `\x1B[2m${s}\x1B[0m`, green13 = (s) => `\x1B[32m${s}\x1B[0m`, yellow10 = (s) => `\x1B[33m${s}\x1B[0m`, red7 = (s) => `\x1B[31m${s}\x1B[0m`, CWD, CLAUDE_DIR, SPECIALISTS_DIR, HOOKS_DIR, SETTINGS_FILE, MCP_FILE2, HOOK_NAMES;
21404
22627
  var init_doctor = __esm(() => {
21405
22628
  CWD = process.cwd();
21406
- CLAUDE_DIR = join20(CWD, ".claude");
21407
- SPECIALISTS_DIR = join20(CWD, ".specialists");
21408
- HOOKS_DIR = join20(SPECIALISTS_DIR, "default", "hooks");
21409
- SETTINGS_FILE = join20(CLAUDE_DIR, "settings.json");
21410
- MCP_FILE2 = join20(CWD, ".mcp.json");
22629
+ CLAUDE_DIR = join26(CWD, ".claude");
22630
+ SPECIALISTS_DIR = join26(CWD, ".specialists");
22631
+ HOOKS_DIR = join26(SPECIALISTS_DIR, "default", "hooks");
22632
+ SETTINGS_FILE = join26(CLAUDE_DIR, "settings.json");
22633
+ MCP_FILE2 = join26(CWD, ".mcp.json");
21411
22634
  HOOK_NAMES = [
21412
22635
  "specialists-complete.mjs",
21413
22636
  "specialists-session-start.mjs"
@@ -21417,11 +22640,11 @@ var init_doctor = __esm(() => {
21417
22640
  // src/cli/setup.ts
21418
22641
  var exports_setup = {};
21419
22642
  __export(exports_setup, {
21420
- run: () => run18
22643
+ run: () => run21
21421
22644
  });
21422
- async function run18() {
22645
+ async function run21() {
21423
22646
  console.log("");
21424
- console.log(yellow10("⚠ DEPRECATED: `specialists setup` is deprecated"));
22647
+ console.log(yellow11("⚠ DEPRECATED: `specialists setup` is deprecated"));
21425
22648
  console.log("");
21426
22649
  console.log(` Use ${bold11("specialists init")} instead.`);
21427
22650
  console.log("");
@@ -21437,18 +22660,18 @@ async function run18() {
21437
22660
  console.log(` ${dim13("Run: specialists init --help for full details")}`);
21438
22661
  console.log("");
21439
22662
  }
21440
- var bold11 = (s) => `\x1B[1m${s}\x1B[0m`, yellow10 = (s) => `\x1B[33m${s}\x1B[0m`, dim13 = (s) => `\x1B[2m${s}\x1B[0m`;
22663
+ var bold11 = (s) => `\x1B[1m${s}\x1B[0m`, yellow11 = (s) => `\x1B[33m${s}\x1B[0m`, dim13 = (s) => `\x1B[2m${s}\x1B[0m`;
21441
22664
 
21442
22665
  // src/cli/help.ts
21443
22666
  var exports_help = {};
21444
22667
  __export(exports_help, {
21445
- run: () => run19
22668
+ run: () => run22
21446
22669
  });
21447
22670
  function formatCommands(entries) {
21448
22671
  const width = Math.max(...entries.map(([cmd3]) => cmd3.length));
21449
22672
  return entries.map(([cmd3, desc]) => ` ${cmd3.padEnd(width)} ${desc}`);
21450
22673
  }
21451
- async function run19() {
22674
+ async function run22() {
21452
22675
  const lines = [
21453
22676
  "",
21454
22677
  "Specialists lets you run project-scoped specialist agents with a bead-first workflow.",
@@ -21476,10 +22699,16 @@ async function run19() {
21476
22699
  " --context-depth defaults to 1 with --bead",
21477
22700
  " --no-beads does not disable bead reading",
21478
22701
  "",
21479
- " Background execution",
21480
- " Use Claude Code's native backgrounding (run_in_background: true)",
21481
- " or run in a separate terminal and poll with:",
21482
- " specialists poll <job-id> --json",
22702
+ " Output modes",
22703
+ ' specialists run <name> --prompt "..." # human (default): formatted event summary',
22704
+ ' specialists run <name> --prompt "..." --json # NDJSON event stream to stdout',
22705
+ ' specialists run <name> --prompt "..." --raw # legacy: raw LLM text deltas',
22706
+ "",
22707
+ " Async patterns",
22708
+ " MCP: start_specialist + feed_specialist",
22709
+ ' CLI: specialists run <name> --prompt "..." # job ID prints on stderr',
22710
+ " specialists feed|poll|result <job-id> # observe/progress/final output",
22711
+ ' Shell: specialists run <name> --prompt "..." & # native shell backgrounding',
21483
22712
  "",
21484
22713
  bold12("Core commands:"),
21485
22714
  ...formatCommands(CORE_COMMANDS),
@@ -21493,20 +22722,24 @@ async function run19() {
21493
22722
  bold12("Examples:"),
21494
22723
  " specialists init",
21495
22724
  " specialists list",
22725
+ " specialists config get specialist.execution.stall_timeout_ms",
21496
22726
  " specialists run debugger --bead unitAI-123",
21497
22727
  ' specialists run codebase-explorer --prompt "Map the CLI architecture"',
21498
22728
  " specialists poll abc123 --json # check job status",
21499
22729
  " specialists feed -f # stream all job events",
21500
22730
  ' specialists steer <job-id> "focus only on supervisor.ts"',
21501
- ' specialists follow-up <job-id> "now write the fix"',
21502
- " specialists result <job-id>",
22731
+ ' specialists resume <job-id> "now write the fix"',
22732
+ ' specialists run debugger --prompt "why does auth fail"',
22733
+ " specialists report list",
22734
+ " specialists report show --specialists",
22735
+ " specialists result <job-id> --wait",
21503
22736
  "",
21504
22737
  bold12("More help:"),
21505
22738
  " specialists quickstart Full guide and workflow reference",
21506
22739
  " specialists run --help Run command details and flags",
21507
22740
  " specialists poll --help Job status polling details",
21508
22741
  " specialists steer --help Mid-run steering details",
21509
- " specialists follow-up --help Multi-turn keep-alive details",
22742
+ " specialists resume --help Multi-turn keep-alive details",
21510
22743
  " specialists init --help Bootstrap behavior and workflow injection",
21511
22744
  " specialists feed --help Job event streaming details",
21512
22745
  "",
@@ -21522,13 +22755,17 @@ var init_help = __esm(() => {
21522
22755
  ["init", "Bootstrap a project: dirs, workflow injection, project MCP registration"],
21523
22756
  ["list", "List specialists in this project"],
21524
22757
  ["validate", "Validate a specialist YAML against the schema"],
21525
- ["run", "Run a specialist with --bead for tracked work or --prompt for ad-hoc work"],
22758
+ ["config", "Batch get/set specialist YAML keys in config/specialists/"],
22759
+ ["run", "Run a specialist; --json for NDJSON event stream, --raw for legacy text"],
21526
22760
  ["feed", "Tail job events; use -f to follow all jobs"],
21527
22761
  ["poll", "Machine-readable job status polling (for scripts/Claude Code)"],
21528
- ["result", "Print final output of a completed job"],
22762
+ ["result", "Print final output of a completed job; --wait polls until done, --timeout <ms> sets a limit"],
22763
+ ["clean", "Purge completed job directories (TTL, --all, --keep, --dry-run)"],
21529
22764
  ["steer", "Send a mid-run message to a running job"],
21530
- ["follow-up", "Send a next-turn prompt to a keep-alive session (retains full context)"],
22765
+ ["resume", "Resume a waiting keep-alive session with a next-turn prompt (retains full context)"],
22766
+ ["follow-up", "[deprecated] Use resume instead"],
21531
22767
  ["stop", "Stop a running job"],
22768
+ ["report", "Generate/show/list/diff session reports in .xtrm/reports/"],
21532
22769
  ["status", "Show health, MCP state, and active jobs"],
21533
22770
  ["doctor", "Diagnose installation/runtime problems"],
21534
22771
  ["quickstart", "Full getting-started guide"],
@@ -21546,7 +22783,8 @@ var init_help = __esm(() => {
21546
22783
  ["xt claude [name]", "Start a Claude session in a sandboxed xt worktree"],
21547
22784
  ["xt attach [slug]", "Resume an existing xt worktree session"],
21548
22785
  ["xt worktree list", "List worktrees with runtime and activity"],
21549
- ["xt end", "Close session, push, PR, cleanup"]
22786
+ ["xt end", "Close session, push, PR, cleanup"],
22787
+ ["xt report show|list|diff", "Session report surfaces (same .xtrm/reports files)"]
21550
22788
  ];
21551
22789
  });
21552
22790
 
@@ -28752,7 +29990,7 @@ class StdioServerTransport {
28752
29990
  }
28753
29991
 
28754
29992
  // src/server.ts
28755
- import { join as join7 } from "node:path";
29993
+ import { join as join11 } from "node:path";
28756
29994
 
28757
29995
  // src/constants.ts
28758
29996
  var LOG_PREFIX = "[specialists]";
@@ -28819,6 +30057,7 @@ var logger = {
28819
30057
  init_loader();
28820
30058
  init_runner();
28821
30059
  init_hooks();
30060
+ init_circuitBreaker();
28822
30061
  init_beads();
28823
30062
 
28824
30063
  // src/tools/specialist/list_specialists.tool.ts
@@ -28842,14 +30081,14 @@ function createListSpecialistsTool(loader) {
28842
30081
  // src/tools/specialist/use_specialist.tool.ts
28843
30082
  init_zod();
28844
30083
  init_beads();
28845
- var useSpecialistSchema = exports_external.object({
28846
- name: exports_external.string().describe("Specialist identifier (e.g. codebase-explorer)"),
28847
- prompt: exports_external.string().optional().describe("The task or question for the specialist"),
28848
- bead_id: exports_external.string().optional().describe("Use an existing bead as the specialist prompt"),
28849
- variables: exports_external.record(exports_external.string()).optional().describe("Additional $variable substitutions"),
28850
- backend_override: exports_external.string().optional().describe("Force a specific backend (gemini, qwen, anthropic)"),
28851
- autonomy_level: exports_external.string().optional().describe("Override permission level for this invocation"),
28852
- context_depth: exports_external.number().optional().describe("Depth of blocker context injection (0 = none, 1 = immediate blockers, etc.)")
30084
+ var useSpecialistSchema = objectType({
30085
+ name: stringType().describe("Specialist identifier (e.g. codebase-explorer)"),
30086
+ prompt: stringType().optional().describe("The task or question for the specialist"),
30087
+ bead_id: stringType().optional().describe("Use an existing bead as the specialist prompt"),
30088
+ variables: recordType(stringType()).optional().describe("Additional $variable substitutions"),
30089
+ backend_override: stringType().optional().describe("Force a specific backend (gemini, qwen, anthropic)"),
30090
+ autonomy_level: stringType().optional().describe("Override permission level for this invocation"),
30091
+ context_depth: numberType().optional().describe("Depth of blocker context injection (0 = none, 1 = immediate blockers, etc.)")
28853
30092
  }).refine((input) => Boolean(input.prompt?.trim() || input.bead_id), {
28854
30093
  message: "Either prompt or bead_id is required",
28855
30094
  path: ["prompt"]
@@ -28944,7 +30183,7 @@ var runParallelSchema = objectType({
28944
30183
  function createRunParallelTool(runner) {
28945
30184
  return {
28946
30185
  name: "run_parallel",
28947
- description: "[DEPRECATED v3] Execute multiple specialists concurrently. Returns aggregated results. Prefer CLI background jobs for async work.",
30186
+ description: "[DEPRECATED v3] Execute multiple specialists concurrently. Returns aggregated results. Prefer start_specialist/feed_specialist for async orchestration.",
28948
30187
  inputSchema: runParallelSchema,
28949
30188
  async execute(input, onProgress) {
28950
30189
  if (input.merge_strategy === "pipeline") {
@@ -29205,66 +30444,99 @@ class JobRegistry {
29205
30444
 
29206
30445
  // src/tools/specialist/start_specialist.tool.ts
29207
30446
  init_zod();
29208
- var startSpecialistSchema = exports_external.object({
29209
- name: exports_external.string().describe("Specialist identifier (e.g. codebase-explorer)"),
29210
- prompt: exports_external.string().describe("The task or question for the specialist"),
29211
- variables: exports_external.record(exports_external.string()).optional().describe("Additional $variable substitutions"),
29212
- backend_override: exports_external.string().optional().describe("Force a specific backend (gemini, qwen, anthropic)")
29213
- });
29214
- function createStartSpecialistTool(runner, registry2) {
30447
+ init_supervisor();
30448
+ import { join as join4 } from "node:path";
30449
+ init_loader();
30450
+ var startSpecialistSchema = objectType({
30451
+ name: stringType().describe("Specialist identifier (e.g. codebase-explorer)"),
30452
+ prompt: stringType().describe("The task or question for the specialist"),
30453
+ variables: recordType(stringType()).optional().describe("Additional $variable substitutions"),
30454
+ backend_override: stringType().optional().describe("Force a specific backend (gemini, qwen, anthropic)"),
30455
+ bead_id: stringType().optional().describe("Existing bead ID to associate with this run (propagated into status.json and run_start event)"),
30456
+ keep_alive: booleanType().optional().describe("Keep the specialist session open for resume_specialist (overrides execution.interactive)."),
30457
+ no_keep_alive: booleanType().optional().describe("Force one-shot behavior even when execution.interactive is true.")
30458
+ });
30459
+ function createStartSpecialistTool(runner, beadsClient) {
29215
30460
  return {
29216
30461
  name: "start_specialist",
29217
- 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.",
30462
+ description: "Start a specialist asynchronously. Returns job_id immediately. " + "Use feed_specialist to stream events and track progress (pass job_id and --follow for live output). " + "Use specialist_status for circuit breaker health checks. " + "Use stop_specialist to cancel. Enables true parallel execution of multiple specialists.",
29218
30463
  inputSchema: startSpecialistSchema,
29219
30464
  async execute(input) {
29220
- const jobId = await runner.startAsync({
29221
- name: input.name,
29222
- prompt: input.prompt,
29223
- variables: input.variables,
29224
- backendOverride: input.backend_override
29225
- }, registry2);
30465
+ const jobsDir = join4(process.cwd(), ".specialists", "jobs");
30466
+ let keepAlive;
30467
+ try {
30468
+ const loader = new SpecialistLoader;
30469
+ const specialist = await loader.get(input.name);
30470
+ const interactiveDefault = specialist.specialist.execution.interactive ? true : undefined;
30471
+ keepAlive = input.no_keep_alive ? false : input.keep_alive ?? interactiveDefault;
30472
+ } catch {
30473
+ keepAlive = input.no_keep_alive ? false : input.keep_alive;
30474
+ }
30475
+ const jobStarted = new Promise((resolve2, reject) => {
30476
+ const supervisor = new Supervisor({
30477
+ runner,
30478
+ runOptions: {
30479
+ name: input.name,
30480
+ prompt: input.prompt,
30481
+ variables: input.variables,
30482
+ backendOverride: input.backend_override,
30483
+ inputBeadId: input.bead_id,
30484
+ keepAlive,
30485
+ noKeepAlive: input.no_keep_alive ?? false
30486
+ },
30487
+ jobsDir,
30488
+ beadsClient,
30489
+ onJobStarted: ({ id }) => resolve2(id)
30490
+ });
30491
+ supervisor.run().catch((error2) => {
30492
+ logger.error(`start_specialist job failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
30493
+ reject(error2);
30494
+ });
30495
+ });
30496
+ const jobId = await jobStarted;
29226
30497
  return { job_id: jobId };
29227
30498
  }
29228
30499
  };
29229
30500
  }
29230
30501
 
29231
- // src/tools/specialist/poll_specialist.tool.ts
29232
- init_zod();
29233
- var pollSpecialistSchema = exports_external.object({
29234
- job_id: exports_external.string().describe("Job ID returned by start_specialist"),
29235
- cursor: exports_external.number().int().min(0).optional().default(0).describe("Character offset from previous poll. Pass next_cursor from the last response to receive only new content. Omit (or pass 0) for the first poll.")
29236
- });
29237
- function createPollSpecialistTool(registry2) {
29238
- return {
29239
- name: "poll_specialist",
29240
- 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.',
29241
- inputSchema: pollSpecialistSchema,
29242
- async execute(input) {
29243
- const snapshot = registry2.snapshot(input.job_id, input.cursor ?? 0);
29244
- if (!snapshot) {
29245
- return { status: "error", error: `Job not found: ${input.job_id}`, job_id: input.job_id };
29246
- }
29247
- return snapshot;
29248
- }
29249
- };
29250
- }
29251
-
29252
30502
  // src/tools/specialist/stop_specialist.tool.ts
29253
30503
  init_zod();
29254
- var stopSpecialistSchema = exports_external.object({
29255
- job_id: exports_external.string().describe("Job ID returned by start_specialist")
30504
+ init_supervisor();
30505
+ import { join as join5 } from "node:path";
30506
+ var stopSpecialistSchema = objectType({
30507
+ job_id: stringType().describe("Job ID returned by start_specialist")
29256
30508
  });
29257
- function createStopSpecialistTool(registry2) {
30509
+ function createStopSpecialistTool() {
29258
30510
  return {
29259
30511
  name: "stop_specialist",
29260
- 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.",
30512
+ description: "Cancel a running specialist job by sending SIGTERM to its recorded process. Works for jobs started via start_specialist and CLI background runs.",
29261
30513
  inputSchema: stopSpecialistSchema,
29262
30514
  async execute(input) {
29263
- const result = registry2.cancel(input.job_id);
29264
- if (!result) {
30515
+ const jobsDir = join5(process.cwd(), ".specialists", "jobs");
30516
+ const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
30517
+ const status = supervisor.readStatus(input.job_id);
30518
+ if (!status) {
29265
30519
  return { status: "error", error: `Job not found: ${input.job_id}`, job_id: input.job_id };
29266
30520
  }
29267
- return { ...result, job_id: input.job_id };
30521
+ if (status.status === "done" || status.status === "error") {
30522
+ return {
30523
+ status: "error",
30524
+ error: `Job is already ${status.status}`,
30525
+ job_id: input.job_id
30526
+ };
30527
+ }
30528
+ if (!status.pid) {
30529
+ return { status: "error", error: `No PID recorded for job ${input.job_id}`, job_id: input.job_id };
30530
+ }
30531
+ try {
30532
+ process.kill(status.pid, "SIGTERM");
30533
+ return { status: "cancelled", job_id: input.job_id, pid: status.pid };
30534
+ } catch (err) {
30535
+ if (err?.code === "ESRCH") {
30536
+ return { status: "error", error: `Process ${status.pid} not found`, job_id: input.job_id };
30537
+ }
30538
+ return { status: "error", error: err?.message ?? String(err), job_id: input.job_id };
30539
+ }
29268
30540
  }
29269
30541
  };
29270
30542
  }
@@ -29273,15 +30545,15 @@ function createStopSpecialistTool(registry2) {
29273
30545
  init_zod();
29274
30546
  init_supervisor();
29275
30547
  import { writeFileSync as writeFileSync2 } from "node:fs";
29276
- import { join as join4 } from "node:path";
30548
+ import { join as join6 } from "node:path";
29277
30549
  var steerSpecialistSchema = exports_external.object({
29278
- job_id: exports_external.string().describe("Job ID returned by start_specialist or specialists run --background"),
30550
+ job_id: exports_external.string().describe("Job ID returned by start_specialist or printed by specialists run"),
29279
30551
  message: exports_external.string().describe('Steering instruction to send to the running agent (e.g. "focus only on supervisor.ts")')
29280
30552
  });
29281
30553
  function createSteerSpecialistTool(registry2) {
29282
30554
  return {
29283
30555
  name: "steer_specialist",
29284
- description: "Send a mid-run steering message to a running specialist job. The agent receives the message after its current tool calls finish, before the next LLM call. Works for both in-process jobs (start_specialist) and background CLI jobs (specialists run --background).",
30556
+ description: "Send a mid-run steering message to a running specialist job. The agent receives the message after its current tool calls finish, before the next LLM call. Works for both in-process jobs (start_specialist) and CLI-started jobs (specialists run).",
29285
30557
  inputSchema: steerSpecialistSchema,
29286
30558
  async execute(input) {
29287
30559
  const snap = registry2.snapshot(input.job_id);
@@ -29292,7 +30564,7 @@ function createSteerSpecialistTool(registry2) {
29292
30564
  }
29293
30565
  return { status: "error", error: result.error, job_id: input.job_id };
29294
30566
  }
29295
- const jobsDir = join4(process.cwd(), ".specialists", "jobs");
30567
+ const jobsDir = join6(process.cwd(), ".specialists", "jobs");
29296
30568
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
29297
30569
  const status = supervisor.readStatus(input.job_id);
29298
30570
  if (!status) {
@@ -29318,47 +30590,50 @@ function createSteerSpecialistTool(registry2) {
29318
30590
 
29319
30591
  // src/tools/specialist/follow_up_specialist.tool.ts
29320
30592
  init_zod();
30593
+
30594
+ // src/tools/specialist/resume_specialist.tool.ts
30595
+ init_zod();
29321
30596
  init_supervisor();
29322
30597
  import { writeFileSync as writeFileSync3 } from "node:fs";
29323
- import { join as join5 } from "node:path";
29324
- var followUpSpecialistSchema = exports_external.object({
30598
+ import { join as join7 } from "node:path";
30599
+ var resumeSpecialistSchema = exports_external.object({
29325
30600
  job_id: exports_external.string().describe("Job ID of a waiting keep-alive specialist session"),
29326
- message: exports_external.string().describe("Next prompt to send to the specialist (conversation history is retained)")
30601
+ task: exports_external.string().describe("Next task/prompt to send to the specialist (conversation history is retained)")
29327
30602
  });
29328
- function createFollowUpSpecialistTool(registry2) {
30603
+ function createResumeSpecialistTool(registry2) {
29329
30604
  return {
29330
- name: "follow_up_specialist",
29331
- description: "Send a follow-up prompt to a waiting keep-alive specialist session. The Pi session retains full conversation history between turns. Only works for jobs started with keepAlive=true (CLI: --keep-alive --background).",
29332
- inputSchema: followUpSpecialistSchema,
30605
+ name: "resume_specialist",
30606
+ description: "Resume a waiting keep-alive specialist session with a next-turn prompt. " + "The Pi session retains full conversation history between turns. " + "Only valid for jobs in waiting state (started with keepAlive=true, either explicit --keep-alive or execution.interactive default). " + "Use steer_specialist for mid-run steering of running jobs.",
30607
+ inputSchema: resumeSpecialistSchema,
29333
30608
  async execute(input) {
29334
30609
  const snap = registry2.snapshot(input.job_id);
29335
30610
  if (snap) {
29336
30611
  if (snap.status !== "waiting") {
29337
- return { status: "error", error: `Job is not waiting (status: ${snap.status})`, job_id: input.job_id };
30612
+ return { status: "error", error: `Job is not waiting (status: ${snap.status}). resume is only valid in waiting state.`, job_id: input.job_id };
29338
30613
  }
29339
- const result = await registry2.followUp(input.job_id, input.message);
30614
+ const result = await registry2.followUp(input.job_id, input.task);
29340
30615
  if (result.ok) {
29341
30616
  return { status: "resumed", job_id: input.job_id, output: result.output };
29342
30617
  }
29343
30618
  return { status: "error", error: result.error, job_id: input.job_id };
29344
30619
  }
29345
- const jobsDir = join5(process.cwd(), ".specialists", "jobs");
30620
+ const jobsDir = join7(process.cwd(), ".specialists", "jobs");
29346
30621
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
29347
30622
  const status = supervisor.readStatus(input.job_id);
29348
30623
  if (!status) {
29349
30624
  return { status: "error", error: `Job not found: ${input.job_id}`, job_id: input.job_id };
29350
30625
  }
29351
30626
  if (status.status !== "waiting") {
29352
- return { status: "error", error: `Job is not waiting (status: ${status.status})`, job_id: input.job_id };
30627
+ return { status: "error", error: `Job is not waiting (status: ${status.status}). resume is only valid in waiting state.`, job_id: input.job_id };
29353
30628
  }
29354
30629
  if (!status.fifo_path) {
29355
30630
  return { status: "error", error: "Job has no steer pipe", job_id: input.job_id };
29356
30631
  }
29357
30632
  try {
29358
- const payload = JSON.stringify({ type: "prompt", message: input.message }) + `
30633
+ const payload = JSON.stringify({ type: "resume", task: input.task }) + `
29359
30634
  `;
29360
30635
  writeFileSync3(status.fifo_path, payload, { flag: "a" });
29361
- return { status: "sent", job_id: input.job_id, message: input.message };
30636
+ return { status: "sent", job_id: input.job_id, task: input.task };
29362
30637
  } catch (err) {
29363
30638
  return { status: "error", error: `Failed to write to steer pipe: ${err?.message}`, job_id: input.job_id };
29364
30639
  }
@@ -29366,19 +30641,92 @@ function createFollowUpSpecialistTool(registry2) {
29366
30641
  };
29367
30642
  }
29368
30643
 
30644
+ // src/tools/specialist/follow_up_specialist.tool.ts
30645
+ var followUpSpecialistSchema = exports_external.object({
30646
+ job_id: exports_external.string().describe("Job ID of a waiting keep-alive specialist session"),
30647
+ message: exports_external.string().describe("Next prompt to send to the specialist (conversation history is retained)")
30648
+ });
30649
+ function createFollowUpSpecialistTool(registry2) {
30650
+ const resumeTool = createResumeSpecialistTool(registry2);
30651
+ return {
30652
+ name: "follow_up_specialist",
30653
+ description: "[DEPRECATED] Use resume_specialist instead. " + "Delegates to resume_specialist with a deprecation warning.",
30654
+ inputSchema: followUpSpecialistSchema,
30655
+ async execute(input) {
30656
+ console.error("[specialists] DEPRECATED: follow_up_specialist is deprecated. Use resume_specialist instead.");
30657
+ return resumeTool.execute({ job_id: input.job_id, task: input.message });
30658
+ }
30659
+ };
30660
+ }
30661
+
30662
+ // src/tools/specialist/feed_specialist.tool.ts
30663
+ init_zod();
30664
+ init_timeline_query();
30665
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
30666
+ import { join as join9 } from "node:path";
30667
+ var feedSpecialistSchema = objectType({
30668
+ job_id: stringType().describe("Job ID returned by start_specialist or printed by specialists run"),
30669
+ cursor: numberType().int().min(0).optional().default(0).describe("Event index offset from previous call. Pass next_cursor from the last response to receive only new events. Omit (or pass 0) for the first call."),
30670
+ limit: numberType().int().min(1).max(100).optional().default(50).describe("Maximum number of events to return per call.")
30671
+ });
30672
+ function createFeedSpecialistTool(jobsDir) {
30673
+ return {
30674
+ name: "feed_specialist",
30675
+ description: "Read cursor-paginated timeline events from a specialist job's events.jsonl. " + "Returns structured event objects (run_start, meta, tool, text, run_complete, etc.) " + "with job metadata (status, specialist, model, bead_id). " + "Poll incrementally: pass next_cursor from each response as cursor on the next call. " + "When is_complete=true and has_more=false, the job is fully observed. " + "Use for structured event inspection; use specialists result <job-id> for final text output.",
30676
+ inputSchema: feedSpecialistSchema,
30677
+ async execute(input) {
30678
+ const { job_id, cursor = 0, limit = 50 } = input;
30679
+ const statusPath = join9(jobsDir, job_id, "status.json");
30680
+ if (!existsSync6(statusPath)) {
30681
+ return { error: `Job not found: ${job_id}`, job_id };
30682
+ }
30683
+ let status = "unknown";
30684
+ let specialist = "unknown";
30685
+ let model;
30686
+ let bead_id;
30687
+ try {
30688
+ const s = JSON.parse(readFileSync4(statusPath, "utf-8"));
30689
+ status = s.status ?? "unknown";
30690
+ specialist = s.specialist ?? "unknown";
30691
+ model = s.model;
30692
+ bead_id = s.bead_id;
30693
+ } catch {}
30694
+ const allEvents = readJobEventsById(jobsDir, job_id);
30695
+ const total = allEvents.length;
30696
+ const sliced = allEvents.slice(cursor, cursor + limit);
30697
+ const next_cursor = cursor + sliced.length;
30698
+ const has_more = next_cursor < total;
30699
+ const is_complete = isJobComplete(allEvents);
30700
+ return {
30701
+ job_id,
30702
+ specialist,
30703
+ specialist_model: formatSpecialistModel(specialist, model),
30704
+ ...model !== undefined ? { model } : {},
30705
+ status,
30706
+ ...bead_id !== undefined ? { bead_id } : {},
30707
+ events: sliced,
30708
+ cursor,
30709
+ next_cursor,
30710
+ has_more,
30711
+ is_complete
30712
+ };
30713
+ }
30714
+ };
30715
+ }
30716
+
29369
30717
  // src/server.ts
29370
30718
  init_zod();
29371
30719
 
29372
30720
  // src/tools/specialist/specialist_init.tool.ts
29373
30721
  init_zod();
29374
30722
  import { spawnSync as spawnSync4 } from "node:child_process";
29375
- import { existsSync as existsSync5 } from "node:fs";
29376
- import { join as join6 } from "node:path";
30723
+ import { existsSync as existsSync7 } from "node:fs";
30724
+ import { join as join10 } from "node:path";
29377
30725
  var specialistInitSchema = objectType({});
29378
30726
  function createSpecialistInitTool(loader, deps) {
29379
30727
  const resolved = deps ?? {
29380
30728
  bdAvailable: () => spawnSync4("bd", ["--version"], { stdio: "ignore" }).status === 0,
29381
- beadsExists: () => existsSync5(join6(process.cwd(), ".beads")),
30729
+ beadsExists: () => existsSync7(join10(process.cwd(), ".beads")),
29382
30730
  bdInit: () => spawnSync4("bd", ["init"], { stdio: "ignore" })
29383
30731
  };
29384
30732
  return {
@@ -29413,22 +30761,24 @@ class SpecialistsServer {
29413
30761
  const circuitBreaker = new CircuitBreaker;
29414
30762
  const loader = new SpecialistLoader;
29415
30763
  const hooks = new HookEmitter({
29416
- tracePath: join7(process.cwd(), ".specialists", "trace.jsonl")
30764
+ tracePath: join11(process.cwd(), ".specialists", "trace.jsonl")
29417
30765
  });
29418
30766
  const beadsClient = new BeadsClient;
29419
30767
  const runner = new SpecialistRunner({ loader, hooks, circuitBreaker, beadsClient });
29420
30768
  const registry2 = new JobRegistry;
30769
+ const jobsDir = join11(process.cwd(), ".specialists", "jobs");
29421
30770
  this.tools = [
29422
30771
  createListSpecialistsTool(loader),
29423
30772
  createUseSpecialistTool(runner),
29424
30773
  createRunParallelTool(runner),
29425
30774
  createSpecialistStatusTool(loader, circuitBreaker),
29426
- createStartSpecialistTool(runner, registry2),
29427
- createPollSpecialistTool(registry2),
29428
- createStopSpecialistTool(registry2),
30775
+ createStartSpecialistTool(runner, beadsClient),
30776
+ createStopSpecialistTool(),
29429
30777
  createSteerSpecialistTool(registry2),
30778
+ createResumeSpecialistTool(registry2),
29430
30779
  createFollowUpSpecialistTool(registry2),
29431
- createSpecialistInitTool(loader)
30780
+ createSpecialistInitTool(loader),
30781
+ createFeedSpecialistTool(jobsDir)
29432
30782
  ];
29433
30783
  this.server = new Server({ name: MCP_CONFIG.SERVER_NAME, version: MCP_CONFIG.VERSION }, { capabilities: MCP_CONFIG.CAPABILITIES });
29434
30784
  this.setupHandlers();
@@ -29441,11 +30791,12 @@ class SpecialistsServer {
29441
30791
  run_parallel: runParallelSchema,
29442
30792
  specialist_status: exports_external.object({}),
29443
30793
  start_specialist: startSpecialistSchema,
29444
- poll_specialist: pollSpecialistSchema,
29445
30794
  stop_specialist: stopSpecialistSchema,
29446
30795
  steer_specialist: steerSpecialistSchema,
30796
+ resume_specialist: resumeSpecialistSchema,
29447
30797
  follow_up_specialist: followUpSpecialistSchema,
29448
- specialist_init: specialistInitSchema
30798
+ specialist_init: specialistInitSchema,
30799
+ feed_specialist: feedSpecialistSchema
29449
30800
  };
29450
30801
  this.toolSchemas = schemaMap;
29451
30802
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -29517,7 +30868,7 @@ var next = process.argv[3];
29517
30868
  function wantsHelp() {
29518
30869
  return next === "--help" || next === "-h";
29519
30870
  }
29520
- async function run20() {
30871
+ async function run23() {
29521
30872
  if (sub === "install") {
29522
30873
  if (wantsHelp()) {
29523
30874
  console.log([
@@ -29656,6 +31007,7 @@ async function run20() {
29656
31007
  console.log([
29657
31008
  "",
29658
31009
  "Usage: specialists edit <name> --<field> <value> [options]",
31010
+ " specialists edit --all",
29659
31011
  "",
29660
31012
  "Edit a field in a .specialist.yaml without opening the file.",
29661
31013
  "",
@@ -29670,11 +31022,13 @@ async function run20() {
29670
31022
  "Options:",
29671
31023
  " --dry-run Preview the change without writing",
29672
31024
  " --scope <default|user> Disambiguate if same name exists in multiple scopes",
31025
+ " --all Open all YAML files in config/specialists/ in $EDITOR",
29673
31026
  "",
29674
31027
  "Examples:",
29675
31028
  " specialists edit code-review --model anthropic/claude-opus-4-6",
29676
31029
  " specialists edit code-review --permission HIGH --dry-run",
29677
31030
  " specialists edit code-review --tags analysis,security",
31031
+ " specialists edit --all",
29678
31032
  ""
29679
31033
  ].join(`
29680
31034
  `));
@@ -29683,6 +31037,34 @@ async function run20() {
29683
31037
  const { run: handler } = await Promise.resolve().then(() => (init_edit(), exports_edit));
29684
31038
  return handler();
29685
31039
  }
31040
+ if (sub === "config") {
31041
+ if (wantsHelp()) {
31042
+ console.log([
31043
+ "",
31044
+ "Usage: specialists config <get|set> <key> [value] [options]",
31045
+ "",
31046
+ "Batch-read or batch-update specialist YAML config in config/specialists/.",
31047
+ "",
31048
+ "Commands:",
31049
+ " get <key> Show a key across all specialists",
31050
+ " set <key> <value> Set a key across all specialists",
31051
+ "",
31052
+ "Options:",
31053
+ " --all Apply to all specialists (default when --name omitted)",
31054
+ " --name <specialist> Target one specialist",
31055
+ "",
31056
+ "Examples:",
31057
+ " specialists config get specialist.execution.stall_timeout_ms",
31058
+ " specialists config set specialist.execution.stall_timeout_ms 180000",
31059
+ " specialists config set specialist.execution.stall_timeout_ms 120000 --name executor",
31060
+ ""
31061
+ ].join(`
31062
+ `));
31063
+ return;
31064
+ }
31065
+ const { run: handler } = await Promise.resolve().then(() => (init_config(), exports_config));
31066
+ return handler();
31067
+ }
29686
31068
  if (sub === "run") {
29687
31069
  if (wantsHelp()) {
29688
31070
  console.log([
@@ -29700,6 +31082,7 @@ async function run20() {
29700
31082
  " --prompt <text> Ad-hoc prompt for untracked work",
29701
31083
  " --context-depth <n> Dependency context depth when using --bead (default: 1)",
29702
31084
  " --no-beads Do not create a new tracking bead (does not disable bead reading)",
31085
+ " --no-bead-notes Do not append completion notes to an external --bead",
29703
31086
  " --model <model> Override the configured model for this run",
29704
31087
  " --keep-alive Keep session alive for follow-up prompts",
29705
31088
  "",
@@ -29713,10 +31096,10 @@ async function run20() {
29713
31096
  " Use --bead for tracked work.",
29714
31097
  " Use --prompt for quick ad-hoc work.",
29715
31098
  "",
29716
- "Background execution:",
29717
- " Use Claude Code's native backgrounding (run_in_background: true)",
29718
- " or run in a separate terminal and poll with:",
29719
- " specialists poll <job-id> --json",
31099
+ "Async execution patterns:",
31100
+ " MCP: start_specialist + feed_specialist",
31101
+ " CLI: run prints [job started: <id>] on stderr, then use feed/poll/result",
31102
+ ' Shell: specialists run <name> --prompt "..." &',
29720
31103
  ""
29721
31104
  ].join(`
29722
31105
  `));
@@ -29760,7 +31143,7 @@ async function run20() {
29760
31143
  "",
29761
31144
  "Usage: specialists result <job-id>",
29762
31145
  "",
29763
- "Print the final output of a completed background job.",
31146
+ "Print the final output of a completed job.",
29764
31147
  "Exits with code 1 if the job is still running or failed.",
29765
31148
  "",
29766
31149
  "Examples:",
@@ -29785,7 +31168,7 @@ async function run20() {
29785
31168
  "Usage: specialists feed <job-id> [options]",
29786
31169
  " specialists feed -f [--forever]",
29787
31170
  "",
29788
- "Read background job events.",
31171
+ "Read job events.",
29789
31172
  "",
29790
31173
  "Modes:",
29791
31174
  " specialists feed <job-id> Replay events for one job",
@@ -29880,35 +31263,82 @@ async function run20() {
29880
31263
  const { run: handler } = await Promise.resolve().then(() => (init_steer(), exports_steer));
29881
31264
  return handler();
29882
31265
  }
29883
- if (sub === "follow-up") {
31266
+ if (sub === "resume") {
29884
31267
  if (wantsHelp()) {
29885
31268
  console.log([
29886
31269
  "",
29887
- 'Usage: specialists follow-up <job-id> "<message>"',
31270
+ 'Usage: specialists resume <job-id> "<task>"',
29888
31271
  "",
29889
- "Send a follow-up prompt to a waiting keep-alive specialist session.",
31272
+ "Resume a waiting keep-alive specialist session with a next-turn prompt.",
29890
31273
  "The Pi session retains full conversation history between turns.",
29891
31274
  "",
29892
31275
  "Requires: job started with --keep-alive.",
29893
31276
  "",
29894
31277
  "Examples:",
29895
- ' specialists follow-up a1b2c3 "Now write the fix for the bug you found"',
29896
- ' specialists follow-up a1b2c3 "Focus only on the auth module"',
31278
+ ' specialists resume a1b2c3 "Now write the fix for the bug you found"',
31279
+ ' specialists resume a1b2c3 "Focus only on the auth module"',
29897
31280
  "",
29898
31281
  "Workflow:",
29899
31282
  " specialists run debugger --bead <id> --keep-alive",
29900
31283
  " # → Job started: a1b2c3 (status: waiting after first turn)",
29901
- " specialists result a1b2c3 # read first turn output",
29902
- ' specialists follow-up a1b2c3 "..." # send next prompt',
29903
- " specialists feed a1b2c3 --follow # watch response",
31284
+ " specialists result a1b2c3 # read first turn output",
31285
+ ' specialists resume a1b2c3 "..." # send next task',
31286
+ " specialists feed a1b2c3 --follow # watch response",
31287
+ "",
31288
+ "See also: specialists steer (mid-run redirect for running jobs)",
31289
+ ""
31290
+ ].join(`
31291
+ `));
31292
+ return;
31293
+ }
31294
+ const { run: handler } = await Promise.resolve().then(() => (init_resume(), exports_resume));
31295
+ return handler();
31296
+ }
31297
+ if (sub === "follow-up") {
31298
+ if (wantsHelp()) {
31299
+ console.log([
31300
+ "",
31301
+ "⚠ DEPRECATED: Use `specialists resume` instead.",
31302
+ "",
31303
+ 'Usage: specialists follow-up <job-id> "<task>"',
29904
31304
  "",
29905
- "See also: specialists steer (mid-run redirect)",
31305
+ "Delegates to `specialists resume`. This alias will be removed in a future release.",
31306
+ ""
31307
+ ].join(`
31308
+ `));
31309
+ return;
31310
+ }
31311
+ const { run: handler } = await Promise.resolve().then(() => exports_follow_up);
31312
+ return handler();
31313
+ }
31314
+ if (sub === "clean") {
31315
+ if (wantsHelp()) {
31316
+ console.log([
31317
+ "",
31318
+ "Usage: specialists clean [--all] [--keep <n>] [--dry-run]",
31319
+ "",
31320
+ "Purge completed job directories from .specialists/jobs/.",
31321
+ "",
31322
+ "Default behavior:",
31323
+ " - removes done/error jobs older than SPECIALISTS_JOB_TTL_DAYS",
31324
+ " - TTL defaults to 7 days if env is unset",
31325
+ "",
31326
+ "Options:",
31327
+ " --all Remove all done/error jobs regardless of age",
31328
+ " --keep <n> Keep only the N most recent done/error jobs",
31329
+ " --dry-run Show what would be removed without deleting",
31330
+ "",
31331
+ "Examples:",
31332
+ " specialists clean",
31333
+ " specialists clean --all",
31334
+ " specialists clean --keep 20",
31335
+ " specialists clean --dry-run",
29906
31336
  ""
29907
31337
  ].join(`
29908
31338
  `));
29909
31339
  return;
29910
31340
  }
29911
- const { run: handler } = await Promise.resolve().then(() => (init_follow_up(), exports_follow_up));
31341
+ const { run: handler } = await Promise.resolve().then(() => (init_clean(), exports_clean));
29912
31342
  return handler();
29913
31343
  }
29914
31344
  if (sub === "stop") {
@@ -29993,7 +31423,7 @@ Run 'specialists help' to see available commands.`);
29993
31423
  const server = new SpecialistsServer;
29994
31424
  await server.start();
29995
31425
  }
29996
- run20().catch((error2) => {
31426
+ run23().catch((error2) => {
29997
31427
  logger.error(`Fatal error: ${error2}`);
29998
31428
  process.exit(1);
29999
31429
  });